Skip to content

Commit ea95907

Browse files
authored
Check T of mixed&Foo and T of mixed|Foo
1 parent 12edd87 commit ea95907

12 files changed

+305
-42
lines changed

phpstan-baseline.neon

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -666,6 +666,11 @@ parameters:
666666
count: 2
667667
path: src/Rules/RuleErrorBuilder.php
668668

669+
-
670+
message: "#^Doing instanceof PHPStan\\\\Type\\\\IntersectionType is error\\-prone and deprecated\\.$#"
671+
count: 1
672+
path: src/Rules/RuleLevelHelper.php
673+
669674
-
670675
message: "#^Doing instanceof PHPStan\\\\Type\\\\NullType is error\\-prone and deprecated\\. Use Type\\:\\:isNull\\(\\) instead\\.$#"
671676
count: 2

src/Rules/Methods/MethodCallCheck.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHPStan\Rules\RuleErrorBuilder;
1313
use PHPStan\Rules\RuleLevelHelper;
1414
use PHPStan\Type\ErrorType;
15+
use PHPStan\Type\StaticType;
1516
use PHPStan\Type\Type;
1617
use PHPStan\Type\VerbosityLevel;
1718
use function count;
@@ -50,13 +51,18 @@ public function check(
5051
if ($type instanceof ErrorType) {
5152
return [$typeResult->getUnknownClassErrors(), null];
5253
}
54+
55+
$typeForDescribe = $type;
56+
if ($type instanceof StaticType) {
57+
$typeForDescribe = $type->getStaticObjectType();
58+
}
5359
if (!$type->canCallMethods()->yes() || $type->isClassStringType()->yes()) {
5460
return [
5561
[
5662
RuleErrorBuilder::message(sprintf(
5763
'Cannot call method %s() on %s.',
5864
$methodName,
59-
$type->describe(VerbosityLevel::typeOnly()),
65+
$typeForDescribe->describe(VerbosityLevel::typeOnly()),
6066
))->build(),
6167
],
6268
null,
@@ -105,7 +111,7 @@ public function check(
105111
[
106112
RuleErrorBuilder::message(sprintf(
107113
'Call to an undefined method %s::%s().',
108-
$type->describe(VerbosityLevel::typeOnly()),
114+
$typeForDescribe->describe(VerbosityLevel::typeOnly()),
109115
$methodName,
110116
))->build(),
111117
],

src/Rules/Methods/StaticMethodCallCheck.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
use PHPStan\TrinaryLogic;
2323
use PHPStan\Type\ErrorType;
2424
use PHPStan\Type\Generic\GenericClassStringType;
25+
use PHPStan\Type\StaticType;
2526
use PHPStan\Type\StringType;
26-
use PHPStan\Type\ThisType;
2727
use PHPStan\Type\Type;
2828
use PHPStan\Type\TypeCombinator;
2929
use PHPStan\Type\VerbosityLevel;
@@ -169,7 +169,7 @@ public function check(
169169
}
170170

171171
$typeForDescribe = $classType;
172-
if ($classType instanceof ThisType) {
172+
if ($classType instanceof StaticType) {
173173
$typeForDescribe = $classType->getStaticObjectType();
174174
}
175175
$classType = TypeCombinator::remove($classType, new StringType());

src/Rules/Properties/AccessPropertiesRule.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use PHPStan\Rules\RuleLevelHelper;
1616
use PHPStan\Type\Constant\ConstantStringType;
1717
use PHPStan\Type\ErrorType;
18+
use PHPStan\Type\StaticType;
1819
use PHPStan\Type\Type;
1920
use PHPStan\Type\VerbosityLevel;
2021
use function array_map;
@@ -78,12 +79,17 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string
7879
return [];
7980
}
8081

82+
$typeForDescribe = $type;
83+
if ($type instanceof StaticType) {
84+
$typeForDescribe = $type->getStaticObjectType();
85+
}
86+
8187
if ($type->canAccessProperties()->no() || $type->canAccessProperties()->maybe() && !$scope->isUndefinedExpressionAllowed($node)) {
8288
return [
8389
RuleErrorBuilder::message(sprintf(
8490
'Cannot access property $%s on %s.',
8591
$name,
86-
$type->describe(VerbosityLevel::typeOnly()),
92+
$typeForDescribe->describe(VerbosityLevel::typeOnly()),
8793
))->build(),
8894
];
8995
}
@@ -138,7 +144,7 @@ private function processSingleProperty(Scope $scope, PropertyFetch $node, string
138144

139145
$ruleErrorBuilder = RuleErrorBuilder::message(sprintf(
140146
'Access to an undefined property %s::$%s.',
141-
$type->describe(VerbosityLevel::typeOnly()),
147+
$typeForDescribe->describe(VerbosityLevel::typeOnly()),
142148
$name,
143149
));
144150
if ($typeResult->getTip() !== null) {

src/Rules/RuleLevelHelper.php

Lines changed: 99 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Type\ClosureType;
1111
use PHPStan\Type\ErrorType;
1212
use PHPStan\Type\Generic\TemplateMixedType;
13+
use PHPStan\Type\IntersectionType;
1314
use PHPStan\Type\MixedType;
1415
use PHPStan\Type\NeverType;
1516
use PHPStan\Type\NullType;
@@ -303,6 +304,20 @@ public function findTypeToCheck(
303304
return new FoundTypeResult(new ErrorType(), [], [], null);
304305
}
305306
$type = $scope->getType($var);
307+
308+
return $this->findTypeToCheckImplementation($scope, $var, $type, $unknownClassErrorPattern, $unionTypeCriteriaCallback, true);
309+
}
310+
311+
/** @param callable(Type $type): bool $unionTypeCriteriaCallback */
312+
private function findTypeToCheckImplementation(
313+
Scope $scope,
314+
Expr $var,
315+
Type $type,
316+
string $unknownClassErrorPattern,
317+
callable $unionTypeCriteriaCallback,
318+
bool $isTopLevel = false,
319+
): FoundTypeResult
320+
{
306321
if (!$this->checkNullables && !$type->isNull()->yes()) {
307322
$type = TypeCombinator::removeNull($type);
308323
}
@@ -345,27 +360,33 @@ public function findTypeToCheck(
345360
if ($type instanceof MixedType || $type instanceof NeverType) {
346361
return new FoundTypeResult(new ErrorType(), [], [], null);
347362
}
348-
if ($type instanceof StaticType) {
349-
$type = $type->getStaticObjectType();
363+
if (!$this->newRuleLevelHelper) {
364+
if ($isTopLevel && $type instanceof StaticType) {
365+
$type = $type->getStaticObjectType();
366+
}
350367
}
351368

352369
$errors = [];
353-
$directClassNames = $type->getObjectClassNames();
354370
$hasClassExistsClass = false;
355-
foreach ($directClassNames as $referencedClass) {
356-
if ($this->reflectionProvider->hasClass($referencedClass)) {
357-
$classReflection = $this->reflectionProvider->getClass($referencedClass);
358-
if (!$classReflection->isTrait()) {
371+
$directClassNames = [];
372+
373+
if ($isTopLevel) {
374+
$directClassNames = $type->getObjectClassNames();
375+
foreach ($directClassNames as $referencedClass) {
376+
if ($this->reflectionProvider->hasClass($referencedClass)) {
377+
$classReflection = $this->reflectionProvider->getClass($referencedClass);
378+
if (!$classReflection->isTrait()) {
379+
continue;
380+
}
381+
}
382+
383+
if ($scope->isInClassExists($referencedClass)) {
384+
$hasClassExistsClass = true;
359385
continue;
360386
}
361-
}
362387

363-
if ($scope->isInClassExists($referencedClass)) {
364-
$hasClassExistsClass = true;
365-
continue;
388+
$errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass))->line($var->getLine())->discoveringSymbolsTip()->build();
366389
}
367-
368-
$errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass))->line($var->getLine())->discoveringSymbolsTip()->build();
369390
}
370391

371392
if (count($errors) > 0 || $hasClassExistsClass) {
@@ -376,28 +397,76 @@ public function findTypeToCheck(
376397
return new FoundTypeResult(new ErrorType(), [], [], null);
377398
}
378399

379-
if (
380-
(
381-
!$this->checkUnionTypes
382-
&& $type instanceof UnionType
383-
&& !$type instanceof BenevolentUnionType
384-
) || (
385-
!$this->checkBenevolentUnionTypes
386-
&& $type instanceof BenevolentUnionType
387-
)
388-
) {
389-
$newTypes = [];
400+
if ($this->newRuleLevelHelper) {
401+
if ($type instanceof UnionType) {
402+
$shouldFilterUnion = (
403+
!$this->checkUnionTypes
404+
&& !$type instanceof BenevolentUnionType
405+
) || (
406+
!$this->checkBenevolentUnionTypes
407+
&& $type instanceof BenevolentUnionType
408+
);
390409

391-
foreach ($type->getTypes() as $innerType) {
392-
if (!$unionTypeCriteriaCallback($innerType)) {
393-
continue;
410+
$newTypes = [];
411+
412+
foreach ($type->getTypes() as $innerType) {
413+
if ($shouldFilterUnion && !$unionTypeCriteriaCallback($innerType)) {
414+
continue;
415+
}
416+
417+
$newTypes[] = $this->findTypeToCheckImplementation(
418+
$scope,
419+
$var,
420+
$innerType,
421+
$unknownClassErrorPattern,
422+
$unionTypeCriteriaCallback,
423+
)->getType();
394424
}
395425

396-
$newTypes[] = $innerType;
426+
if (count($newTypes) > 0) {
427+
return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null);
428+
}
397429
}
398430

399-
if (count($newTypes) > 0) {
400-
return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null);
431+
if ($type instanceof IntersectionType) {
432+
$newTypes = [];
433+
434+
foreach ($type->getTypes() as $innerType) {
435+
$newTypes[] = $this->findTypeToCheckImplementation(
436+
$scope,
437+
$var,
438+
$innerType,
439+
$unknownClassErrorPattern,
440+
$unionTypeCriteriaCallback,
441+
)->getType();
442+
}
443+
444+
return new FoundTypeResult(TypeCombinator::intersect(...$newTypes), $directClassNames, [], null);
445+
}
446+
} else {
447+
if (
448+
(
449+
!$this->checkUnionTypes
450+
&& $type instanceof UnionType
451+
&& !$type instanceof BenevolentUnionType
452+
) || (
453+
!$this->checkBenevolentUnionTypes
454+
&& $type instanceof BenevolentUnionType
455+
)
456+
) {
457+
$newTypes = [];
458+
459+
foreach ($type->getTypes() as $innerType) {
460+
if (!$unionTypeCriteriaCallback($innerType)) {
461+
continue;
462+
}
463+
464+
$newTypes[] = $innerType;
465+
}
466+
467+
if (count($newTypes) > 0) {
468+
return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null);
469+
}
401470
}
402471
}
403472

tests/PHPStan/Levels/data/acceptTypes-5.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,11 @@
164164
"line": 585,
165165
"ignorable": true
166166
},
167+
{
168+
"message": "Parameter #1 $static of method Levels\\AcceptTypes\\RequireObjectWithoutClassType::requireStatic() expects static(Levels\\AcceptTypes\\RequireObjectWithoutClassType), object given.",
169+
"line": 648,
170+
"ignorable": true
171+
},
167172
{
168173
"message": "Parameter #1 $min (0) of function random_int expects lower number than parameter #2 $max (-1).",
169174
"line": 671,

tests/PHPStan/Levels/data/acceptTypes-7.json

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,6 @@
129129
"line": 647,
130130
"ignorable": true
131131
},
132-
{
133-
"message": "Parameter #1 $static of method Levels\\AcceptTypes\\RequireObjectWithoutClassType::requireStatic() expects Levels\\AcceptTypes\\RequireObjectWithoutClassType, object given.",
134-
"line": 648,
135-
"ignorable": true
136-
},
137132
{
138133
"message": "Parameter #1 $min (int<-1, 1>) of function random_int expects lower number than parameter #2 $max (int<0, 1>).",
139134
"line": 690,

tests/PHPStan/Rules/Methods/CallMethodsRuleTest.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,10 @@ public function testCallMethods(): void
499499
1490,
500500
'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type',
501501
],
502+
[
503+
'Parameter #1 $other of method Test\CollectionWithStaticParam::add() expects static(Test\AppleCollection), Test\AppleCollection given.',
504+
1512,
505+
],
502506
[
503507
'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array<string>): array<string>, Closure(array): (array{\'foo\'}|null) given.',
504508
1533,
@@ -819,6 +823,10 @@ public function testCallMethodsOnThisOnly(): void
819823
1490,
820824
'See: https://phpstan.org/blog/solving-phpstan-error-unable-to-resolve-template-type',
821825
],
826+
[
827+
'Parameter #1 $other of method Test\CollectionWithStaticParam::add() expects static(Test\AppleCollection), Test\AppleCollection given.',
828+
1512,
829+
],
822830
[
823831
'Parameter #1 $a of method Test\\CallableWithMixedArray::doBar() expects callable(array<string>): array<string>, Closure(array): (array{\'foo\'}|null) given.',
824832
1533,
@@ -3191,4 +3199,45 @@ public function testBug6371(): void
31913199
]);
31923200
}
31933201

3202+
public function testBugTemplateMixedUnionIntersect(): void
3203+
{
3204+
if (PHP_VERSION_ID < 80000) {
3205+
$this->markTestSkipped('Test requires PHP 8.0');
3206+
}
3207+
3208+
$this->checkThisOnly = false;
3209+
$this->checkNullables = true;
3210+
$this->checkUnionTypes = true;
3211+
$this->checkExplicitMixed = true;
3212+
3213+
$this->analyse([__DIR__ . '/data/bug-template-mixed-union-intersect.php'], [
3214+
[
3215+
'Call to an undefined method BugTemplateMixedUnionIntersect\FooInterface&T of mixed::bar().',
3216+
17,
3217+
],
3218+
[
3219+
'Call to an undefined method BugTemplateMixedUnionIntersect\FooInterface::bar().',
3220+
20,
3221+
],
3222+
[
3223+
'Cannot call method foo() on BugTemplateMixedUnionIntersect\FooInterface|T of mixed.',
3224+
23,
3225+
],
3226+
[
3227+
'Cannot call method foo() on mixed.',
3228+
25,
3229+
],
3230+
]);
3231+
}
3232+
3233+
public function testBug9009(): void
3234+
{
3235+
$this->checkThisOnly = false;
3236+
$this->checkNullables = true;
3237+
$this->checkUnionTypes = true;
3238+
$this->checkExplicitMixed = true;
3239+
3240+
$this->analyse([__DIR__ . '/data/bug-9009.php'], []);
3241+
}
3242+
31943243
}

0 commit comments

Comments
 (0)