Skip to content

Commit 79e5e97

Browse files
committed
Bleeding edge - report nested too-wide type
1 parent d5766e4 commit 79e5e97

12 files changed

+244
-3
lines changed

src/Rules/TooWideTypehints/TooWideParameterOutTypeCheck.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPStan\Node\ReturnStatement;
1010
use PHPStan\Reflection\ExtendedParameterReflection;
1111
use PHPStan\Rules\IdentifierRuleError;
12+
use function lcfirst;
1213
use function sprintf;
1314

1415
#[AutowiredService]
@@ -108,6 +109,11 @@ private function processSingleParameter(
108109
$parameter->getName(),
109110
$isParamOutType ? '@param-out type' : 'by-ref type',
110111
),
112+
sprintf(
113+
'%s %%s of %s can be narrowed to %%s.',
114+
$isParamOutType ? 'PHPDoc tag @param-out type' : 'By-ref type',
115+
lcfirst($functionDescription),
116+
),
111117
$scope,
112118
$startLine,
113119
$isParamOutType ? 'paramOut' : 'parameterByRef',

src/Rules/TooWideTypehints/TooWideTypeCheck.php

Lines changed: 77 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use PHPStan\Type\Constant\ConstantBooleanType;
1717
use PHPStan\Type\MixedType;
1818
use PHPStan\Type\NullType;
19+
use PHPStan\Type\SimultaneousTypeTraverser;
1920
use PHPStan\Type\Type;
2021
use PHPStan\Type\TypeCombinator;
2122
use PHPStan\Type\TypehintHelper;
@@ -24,6 +25,7 @@
2425
use PHPStan\Type\VerbosityLevel;
2526
use PHPStan\Type\VoidType;
2627
use function count;
28+
use function lcfirst;
2729
use function sprintf;
2830

2931
#[AutowiredService]
@@ -103,7 +105,31 @@ public function checkProperty(
103105
);
104106
}
105107

106-
return [];
108+
if (!$this->reportTooWideBool) {
109+
return [];
110+
}
111+
112+
$narrowedPhpDocType = SimultaneousTypeTraverser::map($phpDocPropertyType, $assignedType, function (Type $declaredType, Type $actualType, callable $traverse) use ($scope) {
113+
$narrowed = $this->narrowType($declaredType, $actualType, $scope, false);
114+
if (!$narrowed->equals($declaredType)) {
115+
return $narrowed;
116+
}
117+
return $traverse($declaredType, $actualType);
118+
});
119+
if ($narrowedPhpDocType->equals($phpDocPropertyType)) {
120+
return [];
121+
}
122+
123+
return [
124+
RuleErrorBuilder::message(sprintf(
125+
'Type %s of %s can be narrowed to %s.',
126+
$phpDocPropertyType->describe(VerbosityLevel::getRecommendedLevelByType($phpDocPropertyType)),
127+
lcfirst($propertyDescription),
128+
$narrowedPhpDocType->describe(VerbosityLevel::getRecommendedLevelByType($narrowedPhpDocType)),
129+
))->identifier('property.nestedUnusedType')
130+
->line($node->getStartLine())
131+
->build(),
132+
];
107133
}
108134

109135
$narrowedNativeType = $this->narrowType($nativePropertyType, $assignedType, $scope, true);
@@ -200,7 +226,31 @@ public function checkFunctionReturnType(
200226
);
201227
}
202228

203-
return [];
229+
if (!$this->reportTooWideBool) {
230+
return [];
231+
}
232+
233+
$narrowedPhpDocType = SimultaneousTypeTraverser::map($phpDocFunctionReturnType, $returnType, function (Type $declaredType, Type $actualReturnType, callable $traverse) use ($scope) {
234+
$narrowed = $this->narrowType($declaredType, $actualReturnType, $scope, false);
235+
if (!$narrowed->equals($declaredType)) {
236+
return $narrowed;
237+
}
238+
return $traverse($declaredType, $actualReturnType);
239+
});
240+
if ($narrowedPhpDocType->equals($phpDocFunctionReturnType)) {
241+
return [];
242+
}
243+
244+
return [
245+
RuleErrorBuilder::message(sprintf(
246+
'Return type %s of %s can be narrowed to %s.',
247+
$phpDocFunctionReturnType->describe(VerbosityLevel::getRecommendedLevelByType($phpDocFunctionReturnType)),
248+
lcfirst($functionDescription),
249+
$narrowedPhpDocType->describe(VerbosityLevel::getRecommendedLevelByType($narrowedPhpDocType)),
250+
))->identifier('return.nestedUnusedType')
251+
->line($node->getStartLine())
252+
->build(),
253+
];
204254
}
205255

206256
$narrowedNativeType = $this->narrowType($nativeFunctionReturnType, $returnType, $scope, true);
@@ -228,6 +278,7 @@ public function checkParameterOutType(
228278
Type $actualVariableType,
229279
string $unionMessagePattern,
230280
string $boolMessagePattern,
281+
string $nestedTooWideTypePattern,
231282
Scope $scope,
232283
int $startLine,
233284
string $identifierPart,
@@ -237,7 +288,30 @@ public function checkParameterOutType(
237288
$parameterOutType = TypeUtils::resolveLateResolvableTypes($parameterOutType);
238289
$narrowedType = $this->narrowType($parameterOutType, $actualVariableType, $scope, false);
239290
if ($narrowedType->equals($parameterOutType)) {
240-
return [];
291+
if (!$this->reportTooWideBool) {
292+
return [];
293+
}
294+
295+
$narrowedType = SimultaneousTypeTraverser::map($parameterOutType, $actualVariableType, function (Type $declaredType, Type $actualType, callable $traverse) use ($scope) {
296+
$narrowed = $this->narrowType($declaredType, $actualType, $scope, false);
297+
if (!$narrowed->equals($declaredType)) {
298+
return $narrowed;
299+
}
300+
return $traverse($declaredType, $actualType);
301+
});
302+
if ($narrowedType->equals($parameterOutType)) {
303+
return [];
304+
}
305+
306+
return [
307+
RuleErrorBuilder::message(sprintf(
308+
$nestedTooWideTypePattern,
309+
$parameterOutType->describe(VerbosityLevel::getRecommendedLevelByType($parameterOutType)),
310+
$narrowedType->describe(VerbosityLevel::getRecommendedLevelByType($narrowedType)),
311+
))->identifier(sprintf('%s.nestedUnusedType', $identifierPart))
312+
->line($startLine)
313+
->build(),
314+
];
241315
}
242316

243317
return $this->createErrors(

tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionParameterOutTypeRuleTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,14 @@ public function testRule(): void
3939
]);
4040
}
4141

42+
public function testNestedTooWideType(): void
43+
{
44+
$this->analyse([__DIR__ . '/data/nested-too-wide-function-parameter-out-type.php'], [
45+
[
46+
'PHPDoc tag @param-out type array<array{int, bool}> of function NestedTooWideFunctionParameterOutType\doFoo() can be narrowed to array<array{int, false}>.',
47+
9,
48+
],
49+
]);
50+
}
51+
4252
}

tests/PHPStan/Rules/TooWideTypehints/TooWideFunctionReturnTypehintRuleTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,15 @@ public function testBug13384cOff(): void
115115
$this->analyse([__DIR__ . '/data/bug-13384c.php'], []);
116116
}
117117

118+
public function testNestedTooWideType(): void
119+
{
120+
$this->reportTooWideBool = true;
121+
$this->analyse([__DIR__ . '/data/nested-too-wide-function-return-type.php'], [
122+
[
123+
'Return type array<array{int, bool}> of function NestedTooWideFunctionReturnType\dataProvider() can be narrowed to array<array{int, false}>.',
124+
8,
125+
],
126+
]);
127+
}
128+
118129
}

tests/PHPStan/Rules/TooWideTypehints/TooWideMethodParameterOutTypeRuleTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,14 @@ public function testBug12080(): void
133133
$this->analyse([__DIR__ . '/data/bug-12080.php'], []);
134134
}
135135

136+
public function testNestedTooWideType(): void
137+
{
138+
$this->analyse([__DIR__ . '/data/nested-too-wide-method-parameter-out-type.php'], [
139+
[
140+
'PHPDoc tag @param-out type array<array{int, bool}> of method NestedTooWideMethodParameterOutType\Foo::doFoo() can be narrowed to array<array{int, false}>.',
141+
12,
142+
],
143+
]);
144+
}
145+
136146
}

tests/PHPStan/Rules/TooWideTypehints/TooWideMethodReturnTypehintRuleTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,4 +276,15 @@ public function testBug13384cOff(): void
276276
$this->analyse([__DIR__ . '/data/bug-13384c.php'], []);
277277
}
278278

279+
public function testNestedTooWideType(): void
280+
{
281+
$this->reportTooWideBool = true;
282+
$this->analyse([__DIR__ . '/data/nested-too-wide-method-return-type.php'], [
283+
[
284+
'Return type array<array{int, bool}> of method NestedTooWideMethodReturnType\Foo::dataProvider() can be narrowed to array<array{int, false}>.',
285+
11,
286+
],
287+
]);
288+
}
289+
279290
}

tests/PHPStan/Rules/TooWideTypehints/TooWidePropertyTypeRuleTest.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,15 @@ public function testBugPR4318(): void
111111
$this->analyse([__DIR__ . '/data/bug-pr-4318.php'], []);
112112
}
113113

114+
public function testNestedTooWideType(): void
115+
{
116+
$this->reportTooWideBool = true;
117+
$this->analyse([__DIR__ . '/data/nested-too-wide-property-type.php'], [
118+
[
119+
'Type array<array{int, bool}> of property NestedTooWidePropertyType\Foo::$a can be narrowed to array<array{int, false}>.',
120+
9,
121+
],
122+
]);
123+
}
124+
114125
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace NestedTooWideFunctionParameterOutType;
4+
5+
/**
6+
* @param array<mixed> $a
7+
* @param-out array<array{int, bool}> $a
8+
*/
9+
function doFoo(array &$a): void
10+
{
11+
$a = [
12+
[
13+
1,
14+
false,
15+
],
16+
[
17+
2,
18+
false,
19+
],
20+
];
21+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace NestedTooWideFunctionReturnType;
4+
5+
/**
6+
* @return array<array{int, bool}>
7+
*/
8+
function dataProvider(): array
9+
{
10+
return [
11+
[
12+
1,
13+
false,
14+
],
15+
[
16+
2,
17+
false,
18+
],
19+
];
20+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
namespace NestedTooWideMethodParameterOutType;
4+
5+
class Foo
6+
{
7+
8+
/**
9+
* @param array<mixed> $a
10+
* @param-out array<array{int, bool}> $a
11+
*/
12+
public function doFoo(array &$a): void
13+
{
14+
$a = [
15+
[
16+
1,
17+
false,
18+
],
19+
[
20+
2,
21+
false,
22+
],
23+
];
24+
}
25+
26+
}

0 commit comments

Comments
 (0)