Skip to content

Commit 56b2002

Browse files
committed
Bleeding edge - run missing type check on @param-out
1 parent f46c11c commit 56b2002

9 files changed

+128
-36
lines changed

Diff for: conf/config.level6.neon

+15-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,21 @@ parameters:
99

1010
rules:
1111
- PHPStan\Rules\Constants\MissingClassConstantTypehintRule
12-
- PHPStan\Rules\Functions\MissingFunctionParameterTypehintRule
1312
- PHPStan\Rules\Functions\MissingFunctionReturnTypehintRule
14-
- PHPStan\Rules\Methods\MissingMethodParameterTypehintRule
1513
- PHPStan\Rules\Methods\MissingMethodReturnTypehintRule
1614
- PHPStan\Rules\Properties\MissingPropertyTypehintRule
15+
16+
services:
17+
-
18+
class: PHPStan\Rules\Functions\MissingFunctionParameterTypehintRule
19+
arguments:
20+
paramOut: %featureToggles.paramOutType%
21+
tags:
22+
- phpstan.rules.rule
23+
24+
-
25+
class: PHPStan\Rules\Methods\MissingMethodParameterTypehintRule
26+
arguments:
27+
paramOut: %featureToggles.paramOutType%
28+
tags:
29+
- phpstan.rules.rule

Diff for: src/PhpDoc/StubValidator.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,9 @@ private function getRuleRegistry(Container $container): RuleRegistry
194194
new InvalidThrowsPhpDocValueRule($fileTypeMapper),
195195

196196
// level 6
197-
new MissingFunctionParameterTypehintRule($missingTypehintCheck),
197+
new MissingFunctionParameterTypehintRule($missingTypehintCheck, $container->getParameter('featureToggles')['paramOutType']),
198198
new MissingFunctionReturnTypehintRule($missingTypehintCheck),
199-
new MissingMethodParameterTypehintRule($missingTypehintCheck),
199+
new MissingMethodParameterTypehintRule($missingTypehintCheck, $container->getParameter('featureToggles')['paramOutType']),
200200
new MissingMethodReturnTypehintRule($missingTypehintCheck),
201201
new MissingPropertyTypehintRule($missingTypehintCheck),
202202
];

Diff for: src/Rules/Functions/MissingFunctionParameterTypehintRule.php

+23-13
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Node\InFunctionNode;
88
use PHPStan\Reflection\FunctionReflection;
9-
use PHPStan\Reflection\ParameterReflection;
109
use PHPStan\Reflection\ParametersAcceptorSelector;
1110
use PHPStan\Rules\IdentifierRuleError;
1211
use PHPStan\Rules\MissingTypehintCheck;
1312
use PHPStan\Rules\Rule;
1413
use PHPStan\Rules\RuleErrorBuilder;
1514
use PHPStan\Type\MixedType;
15+
use PHPStan\Type\Type;
1616
use PHPStan\Type\VerbosityLevel;
1717
use function implode;
1818
use function sprintf;
@@ -25,6 +25,7 @@ final class MissingFunctionParameterTypehintRule implements Rule
2525

2626
public function __construct(
2727
private MissingTypehintCheck $missingTypehintCheck,
28+
private bool $paramOut,
2829
)
2930
{
3031
}
@@ -40,7 +41,18 @@ public function processNode(Node $node, Scope $scope): array
4041
$messages = [];
4142

4243
foreach (ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getParameters() as $parameterReflection) {
43-
foreach ($this->checkFunctionParameter($functionReflection, $parameterReflection) as $parameterMessage) {
44+
foreach ($this->checkFunctionParameter($functionReflection, sprintf('parameter $%s', $parameterReflection->getName()), $parameterReflection->getType()) as $parameterMessage) {
45+
$messages[] = $parameterMessage;
46+
}
47+
48+
if (!$this->paramOut) {
49+
continue;
50+
}
51+
if ($parameterReflection->getOutType() === null) {
52+
continue;
53+
}
54+
55+
foreach ($this->checkFunctionParameter($functionReflection, sprintf('@param-out PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getOutType()) as $parameterMessage) {
4456
$messages[] = $parameterMessage;
4557
}
4658
}
@@ -51,16 +63,14 @@ public function processNode(Node $node, Scope $scope): array
5163
/**
5264
* @return list<IdentifierRuleError>
5365
*/
54-
private function checkFunctionParameter(FunctionReflection $functionReflection, ParameterReflection $parameterReflection): array
66+
private function checkFunctionParameter(FunctionReflection $functionReflection, string $parameterMessage, Type $parameterType): array
5567
{
56-
$parameterType = $parameterReflection->getType();
57-
5868
if ($parameterType instanceof MixedType && !$parameterType->isExplicitMixed()) {
5969
return [
6070
RuleErrorBuilder::message(sprintf(
61-
'Function %s() has parameter $%s with no type specified.',
71+
'Function %s() has %s with no type specified.',
6272
$functionReflection->getName(),
63-
$parameterReflection->getName(),
73+
$parameterMessage,
6474
))->identifier('missingType.parameter')->build(),
6575
];
6676
}
@@ -69,9 +79,9 @@ private function checkFunctionParameter(FunctionReflection $functionReflection,
6979
foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($parameterType) as $iterableType) {
7080
$iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly());
7181
$messages[] = RuleErrorBuilder::message(sprintf(
72-
'Function %s() has parameter $%s with no value type specified in iterable type %s.',
82+
'Function %s() has %s with no value type specified in iterable type %s.',
7383
$functionReflection->getName(),
74-
$parameterReflection->getName(),
84+
$parameterMessage,
7585
$iterableTypeDescription,
7686
))
7787
->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP)
@@ -81,9 +91,9 @@ private function checkFunctionParameter(FunctionReflection $functionReflection,
8191

8292
foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($parameterType) as [$name, $genericTypeNames]) {
8393
$messages[] = RuleErrorBuilder::message(sprintf(
84-
'Function %s() has parameter $%s with generic %s but does not specify its types: %s',
94+
'Function %s() has %s with generic %s but does not specify its types: %s',
8595
$functionReflection->getName(),
86-
$parameterReflection->getName(),
96+
$parameterMessage,
8797
$name,
8898
implode(', ', $genericTypeNames),
8999
))
@@ -94,9 +104,9 @@ private function checkFunctionParameter(FunctionReflection $functionReflection,
94104

95105
foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($parameterType) as $callableType) {
96106
$messages[] = RuleErrorBuilder::message(sprintf(
97-
'Function %s() has parameter $%s with no signature specified for %s.',
107+
'Function %s() has %s with no signature specified for %s.',
98108
$functionReflection->getName(),
99-
$parameterReflection->getName(),
109+
$parameterMessage,
100110
$callableType->describe(VerbosityLevel::typeOnly()),
101111
))->identifier('missingType.callable')->build();
102112
}

Diff for: src/Rules/Methods/MissingMethodParameterTypehintRule.php

+26-14
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
use PHPStan\Analyser\Scope;
77
use PHPStan\Node\InClassMethodNode;
88
use PHPStan\Reflection\MethodReflection;
9-
use PHPStan\Reflection\ParameterReflection;
109
use PHPStan\Reflection\ParametersAcceptorSelector;
1110
use PHPStan\Rules\IdentifierRuleError;
1211
use PHPStan\Rules\MissingTypehintCheck;
1312
use PHPStan\Rules\Rule;
1413
use PHPStan\Rules\RuleErrorBuilder;
1514
use PHPStan\Type\MixedType;
15+
use PHPStan\Type\Type;
1616
use PHPStan\Type\VerbosityLevel;
1717
use function implode;
1818
use function sprintf;
@@ -23,7 +23,10 @@
2323
final class MissingMethodParameterTypehintRule implements Rule
2424
{
2525

26-
public function __construct(private MissingTypehintCheck $missingTypehintCheck)
26+
public function __construct(
27+
private MissingTypehintCheck $missingTypehintCheck,
28+
private bool $paramOut,
29+
)
2730
{
2831
}
2932

@@ -38,7 +41,18 @@ public function processNode(Node $node, Scope $scope): array
3841
$messages = [];
3942

4043
foreach (ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getParameters() as $parameterReflection) {
41-
foreach ($this->checkMethodParameter($methodReflection, $parameterReflection) as $parameterMessage) {
44+
foreach ($this->checkMethodParameter($methodReflection, sprintf('parameter $%s', $parameterReflection->getName()), $parameterReflection->getType()) as $parameterMessage) {
45+
$messages[] = $parameterMessage;
46+
}
47+
48+
if (!$this->paramOut) {
49+
continue;
50+
}
51+
if ($parameterReflection->getOutType() === null) {
52+
continue;
53+
}
54+
55+
foreach ($this->checkMethodParameter($methodReflection, sprintf('@param-out PHPDoc tag for parameter $%s', $parameterReflection->getName()), $parameterReflection->getOutType()) as $parameterMessage) {
4256
$messages[] = $parameterMessage;
4357
}
4458
}
@@ -49,17 +63,15 @@ public function processNode(Node $node, Scope $scope): array
4963
/**
5064
* @return list<IdentifierRuleError>
5165
*/
52-
private function checkMethodParameter(MethodReflection $methodReflection, ParameterReflection $parameterReflection): array
66+
private function checkMethodParameter(MethodReflection $methodReflection, string $parameterMessage, Type $parameterType): array
5367
{
54-
$parameterType = $parameterReflection->getType();
55-
5668
if ($parameterType instanceof MixedType && !$parameterType->isExplicitMixed()) {
5769
return [
5870
RuleErrorBuilder::message(sprintf(
59-
'Method %s::%s() has parameter $%s with no type specified.',
71+
'Method %s::%s() has %s with no type specified.',
6072
$methodReflection->getDeclaringClass()->getDisplayName(),
6173
$methodReflection->getName(),
62-
$parameterReflection->getName(),
74+
$parameterMessage,
6375
))->identifier('missingType.parameter')->build(),
6476
];
6577
}
@@ -68,10 +80,10 @@ private function checkMethodParameter(MethodReflection $methodReflection, Parame
6880
foreach ($this->missingTypehintCheck->getIterableTypesWithMissingValueTypehint($parameterType) as $iterableType) {
6981
$iterableTypeDescription = $iterableType->describe(VerbosityLevel::typeOnly());
7082
$messages[] = RuleErrorBuilder::message(sprintf(
71-
'Method %s::%s() has parameter $%s with no value type specified in iterable type %s.',
83+
'Method %s::%s() has %s with no value type specified in iterable type %s.',
7284
$methodReflection->getDeclaringClass()->getDisplayName(),
7385
$methodReflection->getName(),
74-
$parameterReflection->getName(),
86+
$parameterMessage,
7587
$iterableTypeDescription,
7688
))
7789
->tip(MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP)
@@ -81,10 +93,10 @@ private function checkMethodParameter(MethodReflection $methodReflection, Parame
8193

8294
foreach ($this->missingTypehintCheck->getNonGenericObjectTypesWithGenericClass($parameterType) as [$name, $genericTypeNames]) {
8395
$messages[] = RuleErrorBuilder::message(sprintf(
84-
'Method %s::%s() has parameter $%s with generic %s but does not specify its types: %s',
96+
'Method %s::%s() has %s with generic %s but does not specify its types: %s',
8597
$methodReflection->getDeclaringClass()->getDisplayName(),
8698
$methodReflection->getName(),
87-
$parameterReflection->getName(),
99+
$parameterMessage,
88100
$name,
89101
implode(', ', $genericTypeNames),
90102
))
@@ -95,10 +107,10 @@ private function checkMethodParameter(MethodReflection $methodReflection, Parame
95107

96108
foreach ($this->missingTypehintCheck->getCallablesWithMissingSignature($parameterType) as $callableType) {
97109
$messages[] = RuleErrorBuilder::message(sprintf(
98-
'Method %s::%s() has parameter $%s with no signature specified for %s.',
110+
'Method %s::%s() has %s with no signature specified for %s.',
99111
$methodReflection->getDeclaringClass()->getDisplayName(),
100112
$methodReflection->getName(),
101-
$parameterReflection->getName(),
113+
$parameterMessage,
102114
$callableType->describe(VerbosityLevel::typeOnly()),
103115
))->identifier('missingType.callable')->build();
104116
}

Diff for: stubs/core.stub

+3-3
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,13 @@ function str_shuffle(string $string): string {}
7575

7676
/**
7777
* @param array<mixed> $result
78-
* @param-out array<int|string, array|string> $result
78+
* @param-out array<int|string, array<mixed>|string> $result
7979
*/
8080
function parse_str(string $string, array &$result): void {}
8181

8282
/**
8383
* @param array<mixed> $result
84-
* @param-out array<string, array|string> $result
84+
* @param-out array<string, array<mixed>|string> $result
8585
*/
8686
function mb_parse_str(string $string, array &$result): bool {}
8787

@@ -193,7 +193,7 @@ function sscanf(string $string, string $format, &$war, &...$vars) {}
193193
* ? list<array<?string>>
194194
* : (TFlags is 770
195195
* ? list<array<array{?string, int}>>
196-
* : array
196+
* : array<mixed>
197197
* )
198198
* )
199199
* )

Diff for: tests/PHPStan/Rules/Functions/MissingFunctionParameterTypehintRuleTest.php

+11-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class MissingFunctionParameterTypehintRuleTest extends RuleTestCase
1414

1515
protected function getRule(): Rule
1616
{
17-
return new MissingFunctionParameterTypehintRule(new MissingTypehintCheck(true, true, true, true, []));
17+
return new MissingFunctionParameterTypehintRule(new MissingTypehintCheck(true, true, true, true, []), true);
1818
}
1919

2020
public function testRule(): void
@@ -82,6 +82,16 @@ public function testRule(): void
8282
'Function MissingFunctionParameterTypehint\missingCallableSignature() has parameter $cb with no signature specified for callable.',
8383
161,
8484
],
85+
[
86+
'Function MissingParamOutType\oneArray() has @param-out PHPDoc tag for parameter $a with no value type specified in iterable type array.',
87+
173,
88+
MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP,
89+
],
90+
[
91+
'Function MissingParamOutType\generics() has @param-out PHPDoc tag for parameter $a with generic class ReflectionClass but does not specify its types: T',
92+
181,
93+
'You can turn this off by setting <fg=cyan>checkGenericClassInNonGenericObjectType: false</> in your <fg=cyan>%configurationFile%</>.',
94+
],
8595
]);
8696
}
8797

Diff for: tests/PHPStan/Rules/Functions/data/missing-function-parameter-typehint.php

+18
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,21 @@ function missingCallableSignature(callable $cb)
164164
}
165165

166166
}
167+
168+
namespace MissingParamOutType {
169+
/**
170+
* @param array<int> $a
171+
* @param-out array $a
172+
*/
173+
function oneArray(&$a): void {
174+
175+
}
176+
177+
/**
178+
* @param mixed $a
179+
* @param-out \ReflectionClass $a
180+
*/
181+
function generics(&$a): void {
182+
183+
}
184+
}

Diff for: tests/PHPStan/Rules/Methods/MissingMethodParameterTypehintRuleTest.php

+11-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class MissingMethodParameterTypehintRuleTest extends RuleTestCase
1414

1515
protected function getRule(): Rule
1616
{
17-
return new MissingMethodParameterTypehintRule(new MissingTypehintCheck(true, true, true, true, []));
17+
return new MissingMethodParameterTypehintRule(new MissingTypehintCheck(true, true, true, true, []), true);
1818
}
1919

2020
public function testRule(): void
@@ -69,6 +69,16 @@ public function testRule(): void
6969
'Method MissingMethodParameterTypehint\CallableSignature::doFoo() has parameter $cb with no signature specified for callable.',
7070
180,
7171
],
72+
[
73+
'Method MissingMethodParameterTypehint\MissingParamOutType::oneArray() has @param-out PHPDoc tag for parameter $a with no value type specified in iterable type array.',
74+
207,
75+
MissingTypehintCheck::MISSING_ITERABLE_VALUE_TYPE_TIP,
76+
],
77+
[
78+
'Method MissingMethodParameterTypehint\MissingParamOutType::generics() has @param-out PHPDoc tag for parameter $a with generic class ReflectionClass but does not specify its types: T',
79+
215,
80+
'You can turn this off by setting <fg=cyan>checkGenericClassInNonGenericObjectType: false</> in your <fg=cyan>%configurationFile%</>.',
81+
],
7282
];
7383

7484
$this->analyse([__DIR__ . '/data/missing-method-parameter-typehint.php'], $errors);

Diff for: tests/PHPStan/Rules/Methods/data/missing-method-parameter-typehint.php

+19
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,22 @@ public function unserialize($data): void
197197
}
198198

199199
}
200+
201+
class MissingParamOutType {
202+
203+
/**
204+
* @param array<int> $a
205+
* @param-out array $a
206+
*/
207+
function oneArray(&$a): void {
208+
209+
}
210+
211+
/**
212+
* @param mixed $a
213+
* @param-out \ReflectionClass $a
214+
*/
215+
function generics(&$a): void {
216+
217+
}
218+
}

0 commit comments

Comments
 (0)