Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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)

AltStyle によって変換されたページ (->オリジナル) /