Skip to content

Commit e1c8380

Browse files
authored
Fixed '$this instanceof X will always be false' in traits
1 parent 39e645c commit e1c8380

File tree

9 files changed

+377
-9
lines changed

9 files changed

+377
-9
lines changed

src/Analyser/MutatingScope.php

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2424,18 +2424,18 @@ public function isInFunctionExists(string $functionName): bool
24242424
public function enterClass(ClassReflection $classReflection): self
24252425
{
24262426
$thisHolder = ExpressionTypeHolder::createYes(new Variable('this'), new ThisType($classReflection));
2427+
$constantTypes = $this->getConstantTypes();
2428+
$constantTypes['$this'] = $thisHolder;
2429+
$nativeConstantTypes = $this->getNativeConstantTypes();
2430+
$nativeConstantTypes['$this'] = $thisHolder;
24272431

24282432
return $this->scopeFactory->create(
24292433
$this->context->enterClass($classReflection),
24302434
$this->isDeclareStrictTypes(),
24312435
null,
24322436
$this->getNamespace(),
2433-
array_merge($this->getConstantTypes(), [
2434-
'$this' => $thisHolder,
2435-
]),
2436-
array_merge($this->getNativeConstantTypes(), [
2437-
'$this' => $thisHolder,
2438-
]),
2437+
$constantTypes,
2438+
$nativeConstantTypes,
24392439
[],
24402440
null,
24412441
null,
@@ -2456,13 +2456,26 @@ public function enterTrait(ClassReflection $traitReflection): self
24562456
if (count($traitNameParts) > 1) {
24572457
$namespace = implode('\\', array_slice($traitNameParts, 0, -1));
24582458
}
2459+
2460+
$traitContext = $this->context->enterTrait($traitReflection);
2461+
$classReflection = $traitContext->getClassReflection();
2462+
if ($classReflection === null) {
2463+
throw new ShouldNotHappenException();
2464+
}
2465+
2466+
$thisHolder = ExpressionTypeHolder::createYes(new Variable('this'), new ThisType($classReflection, null, $traitReflection));
2467+
$expressionTypes = $this->expressionTypes;
2468+
$expressionTypes['$this'] = $thisHolder;
2469+
$nativeExpressionTypes = $this->nativeExpressionTypes;
2470+
$nativeExpressionTypes['$this'] = $thisHolder;
2471+
24592472
return $this->scopeFactory->create(
2460-
$this->context->enterTrait($traitReflection),
2473+
$traitContext,
24612474
$this->isDeclareStrictTypes(),
24622475
$this->getFunction(),
24632476
$namespace,
2464-
$this->expressionTypes,
2465-
$this->nativeExpressionTypes,
2477+
$expressionTypes,
2478+
$nativeExpressionTypes,
24662479
[],
24672480
$this->inClosureBindScopeClass,
24682481
$this->anonymousFunctionReflection,

src/Type/ObjectType.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,31 @@ public function isSuperTypeOf(Type $type): TrinaryLogic
286286
return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe();
287287
}
288288

289+
if ($type instanceof ThisType && $type->isInTrait()) {
290+
if ($type->getSubtractedType() !== null) {
291+
$isSuperType = $type->getSubtractedType()->isSuperTypeOf($this);
292+
if ($isSuperType->yes()) {
293+
return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo();
294+
}
295+
}
296+
297+
if ($this->getClassReflection() === null) {
298+
return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe();
299+
}
300+
301+
$thisClassReflection = $this->getClassReflection();
302+
if ($thisClassReflection->isTrait()) {
303+
return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo();
304+
}
305+
306+
$traitReflection = $type->getTraitReflection();
307+
if ($thisClassReflection->isFinal() && !$thisClassReflection->hasTraitUse($traitReflection->getName())) {
308+
return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createNo();
309+
}
310+
311+
return self::$superTypes[$thisDescription][$description] = TrinaryLogic::createMaybe();
312+
}
313+
289314
$transformResult = static fn (TrinaryLogic $result) => $result;
290315
if ($this->subtractedType !== null) {
291316
$isSuperType = $this->subtractedType->isSuperTypeOf($type);

src/Type/ThisType.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class ThisType extends StaticType
1717
public function __construct(
1818
ClassReflection $classReflection,
1919
?Type $subtractedType = null,
20+
private ?ClassReflection $traitReflection = null,
2021
)
2122
{
2223
parent::__construct($classReflection, $subtractedType);
@@ -57,6 +58,19 @@ public function changeSubtractedType(?Type $subtractedType): Type
5758
return $type;
5859
}
5960

61+
/**
62+
* @phpstan-assert-if-true !null $this->getTraitReflection()
63+
*/
64+
public function isInTrait(): bool
65+
{
66+
return $this->traitReflection !== null;
67+
}
68+
69+
public function getTraitReflection(): ?ClassReflection
70+
{
71+
return $this->traitReflection;
72+
}
73+
6074
public function traverse(callable $cb): Type
6175
{
6276
$subtractedType = $this->getSubtractedType() !== null ? $cb($this->getSubtractedType()) : null;

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1179,6 +1179,7 @@ public function dataFileAsserts(): iterable
11791179
yield from $this->gatherAssertTypes(__DIR__ . '/data/callsite-cast-narrowing.php');
11801180
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8775.php');
11811181
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8752.php');
1182+
yield from $this->gatherAssertTypes(__DIR__ . '/data/trait-instance-of.php');
11821183
}
11831184

11841185
/**
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace TraitInstanceOf;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
trait Trait1 {
8+
public function test(): string {
9+
assertType('$this(TraitInstanceOf\ATrait1Class)', $this);
10+
if ($this instanceof WithoutFoo) {
11+
assertType('$this(TraitInstanceOf\ATrait1Class)&TraitInstanceOf\WithoutFoo', $this);
12+
return 'hello world';
13+
}
14+
15+
if ($this instanceof FinalOther) {
16+
assertType('*NEVER*', $this);
17+
return 'hello world';
18+
}
19+
20+
assertType('$this(TraitInstanceOf\ATrait1Class)', $this);
21+
if ($this instanceof Trait2) {
22+
assertType('*NEVER*', $this);
23+
return 'hello world';
24+
}
25+
26+
if ($this instanceof FinalTrait2Class) {
27+
assertType('*NEVER*', $this);
28+
return 'hello world';
29+
}
30+
31+
assertType('$this(TraitInstanceOf\ATrait1Class)', $this);
32+
throw new \Error();
33+
}
34+
}
35+
36+
trait Trait2 {
37+
public function test(): string {
38+
assertType('$this(TraitInstanceOf\FinalTrait2Class)', $this);
39+
40+
if ($this instanceof FinalTrait2Class) {
41+
assertType('$this(TraitInstanceOf\FinalTrait2Class)&TraitInstanceOf\FinalTrait2Class', $this);
42+
return 'hello world';
43+
}
44+
45+
if ($this instanceof ATrait1Class) {
46+
assertType('*NEVER*', $this);
47+
return 'hello world';
48+
}
49+
50+
if ($this instanceof FinalOther) {
51+
assertType('*NEVER*', $this);
52+
return 'hello world';
53+
}
54+
55+
return 'hello world';
56+
}
57+
}
58+
59+
final class FinalOther {
60+
}
61+
62+
final class FinalTrait2Class {
63+
use Trait2;
64+
}
65+
66+
class WithoutFoo {}
67+
68+
class ATrait1Class {
69+
use Trait1;
70+
}

tests/PHPStan/Rules/Classes/ExistingClassInInstanceOfRuleTest.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,14 @@ public function testBug7720(): void
7070
]);
7171
}
7272

73+
public function testTraitInstanceOf(): void
74+
{
75+
$this->analyse([__DIR__ . '/../../Analyser/data/trait-instance-of.php'], [
76+
[
77+
'Instanceof between $this(TraitInstanceOf\ATrait1Class) and trait TraitInstanceOf\Trait2 will always evaluate to false.',
78+
21,
79+
],
80+
]);
81+
}
82+
7383
}

tests/PHPStan/Rules/Classes/ImpossibleInstanceOfRuleTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,4 +368,11 @@ public function testBug5333(): void
368368
]);
369369
}
370370

371+
public function testBug3632(): void
372+
{
373+
$this->checkAlwaysTrueInstanceOf = true;
374+
$this->treatPhpDocTypesAsCertain = false;
375+
$this->analyse([__DIR__ . '/data/bug-3632.php'], []);
376+
}
377+
371378
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug3632;
4+
5+
trait Foo {
6+
public function test(): string {
7+
if ($this instanceof HelloWorld) {
8+
return 'hello world';
9+
}
10+
if ($this instanceof OtherClass) {
11+
return 'other class';
12+
}
13+
14+
return 'no';
15+
}
16+
}
17+
18+
class HelloWorld
19+
{
20+
use Foo;
21+
22+
function bar(): string {
23+
return $this->test();
24+
}
25+
}
26+
27+
class OtherClass {
28+
use Foo;
29+
30+
function bar(): string {
31+
return $this->test();
32+
}
33+
}

0 commit comments

Comments
 (0)