Skip to content

Commit 18bdd34

Browse files
committed
Bleeding edge - check that function-scoped template type is in a parameter
1 parent f11c0f0 commit 18bdd34

11 files changed

+96
-13
lines changed

conf/bleedingEdge.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ parameters:
1111
detectDuplicateStubFiles: true
1212
checkLogicalAndConstantCondition: true
1313
checkLogicalOrConstantCondition: true
14+
checkMissingTemplateTypeInParameter: true

conf/config.neon

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ parameters:
2424
detectDuplicateStubFiles: false
2525
checkLogicalAndConstantCondition: false
2626
checkLogicalOrConstantCondition: false
27+
checkMissingTemplateTypeInParameter: false
2728
fileExtensions:
2829
- php
2930
checkAlwaysTrueCheckTypeFunctionCall: false
@@ -174,7 +175,8 @@ parametersSchema:
174175
dateTimeInstantiation: bool(),
175176
detectDuplicateStubFiles: bool(),
176177
checkLogicalAndConstantCondition: bool(),
177-
checkLogicalOrConstantCondition: bool()
178+
checkLogicalOrConstantCondition: bool(),
179+
checkMissingTemplateTypeInParameter: bool()
178180
])
179181
fileExtensions: listOf(string())
180182
checkAlwaysTrueCheckTypeFunctionCall: bool()
@@ -699,6 +701,7 @@ services:
699701
arguments:
700702
checkClassCaseSensitivity: %checkClassCaseSensitivity%
701703
checkThisOnly: %checkThisOnly%
704+
checkMissingTemplateTypeInParameter: %featureToggles.checkMissingTemplateTypeInParameter%
702705

703706
-
704707
class: PHPStan\Rules\FunctionReturnTypeCheck

src/Rules/FunctionDefinitionCheck.php

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
use PHPStan\Reflection\ParametersAcceptorWithPhpDocs;
2020
use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
2121
use PHPStan\Reflection\ReflectionProvider;
22+
use PHPStan\Type\Generic\TemplateType;
2223
use PHPStan\Type\NonexistentParentClassType;
24+
use PHPStan\Type\Type;
25+
use PHPStan\Type\TypeTraverser;
2326
use PHPStan\Type\VerbosityLevel;
2427
use PHPStan\Type\VoidType;
2528

@@ -36,34 +39,40 @@ class FunctionDefinitionCheck
3639

3740
private bool $checkThisOnly;
3841

42+
private bool $checkMissingTemplateTypeInParameter;
43+
3944
public function __construct(
4045
ReflectionProvider $reflectionProvider,
4146
ClassCaseSensitivityCheck $classCaseSensitivityCheck,
4247
PhpVersion $phpVersion,
4348
bool $checkClassCaseSensitivity,
44-
bool $checkThisOnly
49+
bool $checkThisOnly,
50+
bool $checkMissingTemplateTypeInParameter
4551
)
4652
{
4753
$this->reflectionProvider = $reflectionProvider;
4854
$this->classCaseSensitivityCheck = $classCaseSensitivityCheck;
4955
$this->phpVersion = $phpVersion;
5056
$this->checkClassCaseSensitivity = $checkClassCaseSensitivity;
5157
$this->checkThisOnly = $checkThisOnly;
58+
$this->checkMissingTemplateTypeInParameter = $checkMissingTemplateTypeInParameter;
5259
}
5360

5461
/**
5562
* @param \PhpParser\Node\Stmt\Function_ $function
5663
* @param string $parameterMessage
5764
* @param string $returnMessage
5865
* @param string $unionTypesMessage
66+
* @param string $templateTypeMissingInParameterMessage
5967
* @return RuleError[]
6068
*/
6169
public function checkFunction(
6270
Function_ $function,
6371
FunctionReflection $functionReflection,
6472
string $parameterMessage,
6573
string $returnMessage,
66-
string $unionTypesMessage
74+
string $unionTypesMessage,
75+
string $templateTypeMissingInParameterMessage
6776
): array
6877
{
6978
$parametersAcceptor = ParametersAcceptorSelector::selectSingle($functionReflection->getVariants());
@@ -73,7 +82,8 @@ public function checkFunction(
7382
$function,
7483
$parameterMessage,
7584
$returnMessage,
76-
$unionTypesMessage
85+
$unionTypesMessage,
86+
$templateTypeMissingInParameterMessage
7787
);
7888
}
7989

@@ -170,14 +180,16 @@ public function checkAnonymousFunction(
170180
* @param string $parameterMessage
171181
* @param string $returnMessage
172182
* @param string $unionTypesMessage
183+
* @param string $templateTypeMissingInParameterMessage
173184
* @return RuleError[]
174185
*/
175186
public function checkClassMethod(
176187
PhpMethodFromParserNodeReflection $methodReflection,
177188
ClassMethod $methodNode,
178189
string $parameterMessage,
179190
string $returnMessage,
180-
string $unionTypesMessage
191+
string $unionTypesMessage,
192+
string $templateTypeMissingInParameterMessage
181193
): array
182194
{
183195
/** @var \PHPStan\Reflection\ParametersAcceptorWithPhpDocs $parametersAcceptor */
@@ -188,7 +200,8 @@ public function checkClassMethod(
188200
$methodNode,
189201
$parameterMessage,
190202
$returnMessage,
191-
$unionTypesMessage
203+
$unionTypesMessage,
204+
$templateTypeMissingInParameterMessage
192205
);
193206
}
194207

@@ -198,14 +211,16 @@ public function checkClassMethod(
198211
* @param string $parameterMessage
199212
* @param string $returnMessage
200213
* @param string $unionTypesMessage
214+
* @param string $templateTypeMissingInParameterMessage
201215
* @return RuleError[]
202216
*/
203217
private function checkParametersAcceptor(
204218
ParametersAcceptor $parametersAcceptor,
205219
FunctionLike $functionNode,
206220
string $parameterMessage,
207221
string $returnMessage,
208-
string $unionTypesMessage
222+
string $unionTypesMessage,
223+
string $templateTypeMissingInParameterMessage
209224
): array
210225
{
211226
$errors = [];
@@ -301,6 +316,25 @@ private function checkParametersAcceptor(
301316
$errors[] = RuleErrorBuilder::message(sprintf($returnMessage, $parametersAcceptor->getReturnType()->describe(VerbosityLevel::typeOnly())))->line($returnTypeNode->getLine())->build();
302317
}
303318

319+
if ($this->checkMissingTemplateTypeInParameter) {
320+
$templateTypeMap = $parametersAcceptor->getTemplateTypeMap();
321+
$templateTypes = $templateTypeMap->getTypes();
322+
foreach ($parametersAcceptor->getParameters() as $parameter) {
323+
TypeTraverser::map($parameter->getType(), static function (Type $type, callable $traverse) use (&$templateTypes): Type {
324+
if ($type instanceof TemplateType) {
325+
unset($templateTypes[$type->getName()]);
326+
return $type;
327+
}
328+
329+
return $traverse($type);
330+
});
331+
}
332+
333+
foreach (array_keys($templateTypes) as $templateTypeName) {
334+
$errors[] = RuleErrorBuilder::message(sprintf($templateTypeMissingInParameterMessage, $templateTypeName))->build();
335+
}
336+
}
337+
304338
return $errors;
305339
}
306340

src/Rules/Functions/ExistingClassesInTypehintsRule.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ public function processNode(Node $node, Scope $scope): array
4545
'Return typehint of function %s() has invalid type %%s.',
4646
$functionName
4747
),
48-
sprintf('Function %s() uses native union types but they\'re supported only on PHP 8.0 and later.', $functionName)
48+
sprintf('Function %s() uses native union types but they\'re supported only on PHP 8.0 and later.', $functionName),
49+
sprintf('Template type %%s of function %s() is not referenced in a parameter.', $functionName)
4950
);
5051
}
5152

src/Rules/Methods/ExistingClassesInTypehintsRule.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ public function processNode(Node $node, Scope $scope): array
4949
$scope->getClassReflection()->getDisplayName(),
5050
$methodReflection->getName()
5151
),
52-
sprintf('Method %s::%s() uses native union types but they\'re supported only on PHP 8.0 and later.', $scope->getClassReflection()->getDisplayName(), $methodReflection->getName())
52+
sprintf('Method %s::%s() uses native union types but they\'re supported only on PHP 8.0 and later.', $scope->getClassReflection()->getDisplayName(), $methodReflection->getName()),
53+
sprintf('Template type %%s of method %s::%s() is not referenced in a parameter.', $scope->getClassReflection()->getDisplayName(), $methodReflection->getName())
5354
);
5455
}
5556

tests/PHPStan/Rules/Functions/ExistingClassesInArrowFunctionTypehintsRuleTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class ExistingClassesInArrowFunctionTypehintsRuleTest extends \PHPStan\Testing\R
1818
protected function getRule(): \PHPStan\Rules\Rule
1919
{
2020
$broker = $this->createReflectionProvider();
21-
return new ExistingClassesInArrowFunctionTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersionId), true, false));
21+
return new ExistingClassesInArrowFunctionTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersionId), true, false, true));
2222
}
2323

2424
public function testRule(): void

tests/PHPStan/Rules/Functions/ExistingClassesInClosureTypehintsRuleTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class ExistingClassesInClosureTypehintsRuleTest extends \PHPStan\Testing\RuleTes
1818
protected function getRule(): \PHPStan\Rules\Rule
1919
{
2020
$broker = $this->createReflectionProvider();
21-
return new ExistingClassesInClosureTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersionId), true, false));
21+
return new ExistingClassesInClosureTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersionId), true, false, true));
2222
}
2323

2424
public function testExistingClassInTypehint(): void

tests/PHPStan/Rules/Functions/ExistingClassesInTypehintsRuleTest.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class ExistingClassesInTypehintsRuleTest extends \PHPStan\Testing\RuleTestCase
1919
protected function getRule(): \PHPStan\Rules\Rule
2020
{
2121
$broker = $this->createReflectionProvider();
22-
return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersionId), true, false));
22+
return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersionId), true, false, true));
2323
}
2424

2525
public function testExistingClassInTypehint(): void
@@ -86,6 +86,10 @@ public function testExistingClassInTypehint(): void
8686
'Parameter $string of function TestFunctionTypehints\genericTemplateClassString() has invalid typehint type TestFunctionTypehints\SomeNonexistentClass.',
8787
87,
8888
],
89+
[
90+
'Template type T of function TestFunctionTypehints\templateTypeMissingInParameter() is not referenced in a parameter.',
91+
96,
92+
],
8993
]);
9094
}
9195

tests/PHPStan/Rules/Functions/data/typehints.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,12 @@ function genericTemplateClassString(string $string)
8888
{
8989

9090
}
91+
92+
/**
93+
* @template T
94+
* @param class-string $a
95+
*/
96+
function templateTypeMissingInParameter(string $a)
97+
{
98+
99+
}

tests/PHPStan/Rules/Methods/ExistingClassesInTypehintsRuleTest.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ class ExistingClassesInTypehintsRuleTest extends \PHPStan\Testing\RuleTestCase
1919
protected function getRule(): \PHPStan\Rules\Rule
2020
{
2121
$broker = $this->createReflectionProvider();
22-
return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersionId), true, false));
22+
return new ExistingClassesInTypehintsRule(new FunctionDefinitionCheck($broker, new ClassCaseSensitivityCheck($broker), new PhpVersion($this->phpVersionId), true, false, true));
2323
}
2424

2525
public function testExistingClassInTypehint(): void
@@ -124,6 +124,10 @@ public function testExistingClassInTypehint(): void
124124
'Parameter $cb of method TestMethodTypehints\CallableTypehints::doFoo() has invalid typehint type TestMethodTypehints\Ble.',
125125
113,
126126
],
127+
[
128+
'Template type U of method TestMethodTypehints\TemplateTypeMissingInParameter::doFoo() is not referenced in a parameter.',
129+
130,
130+
],
127131
]);
128132
}
129133

tests/PHPStan/Rules/Methods/data/typehints.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,29 @@ public function doFoo(callable $cb): void
116116
}
117117

118118
}
119+
120+
/**
121+
* @template T
122+
*/
123+
class TemplateTypeMissingInParameter
124+
{
125+
126+
/**
127+
* @template U of object
128+
* @param class-string $class
129+
*/
130+
public function doFoo(string $class): void
131+
{
132+
133+
}
134+
135+
/**
136+
* @template U of object
137+
* @param class-string<U> $class
138+
*/
139+
public function doBar(string $class): void
140+
{
141+
142+
}
143+
144+
}

0 commit comments

Comments
 (0)