From b94b5d67564019775af58c13fc1363f846899ab0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:19:24 +0000 Subject: [PATCH 01/16] Fix false positives for is_a() and instanceof checks on $this in traits - When analyzing trait methods, $this is bound to each using class separately - is_a($this, SomeClass::class) was reported as "always true" in SomeClass context and "always false" in SomeClass2 context, but both are false positives - Added trait-aware check in ImpossibleCheckTypeHelper to treat $this as uncertain when inside a trait, since the same code is shared across multiple classes - New regression test in tests/PHPStan/Rules/Comparison/data/bug-13023.php Closes https://github.com/phpstan/phpstan/issues/13023 --- .../Comparison/ImpossibleCheckTypeHelper.php | 10 ++++++++ ...mpossibleCheckTypeFunctionCallRuleTest.php | 6 +++++ .../Rules/Comparison/data/bug-13023.php | 25 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-13023.php diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 127cae01cd..555df067fa 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -310,6 +310,11 @@ public function findSpecifiedType( continue; } + if ($scope->isInTrait() && $sureType[0] instanceof Expr\Variable && $sureType[0]->name === 'this') { + $results[] = TrinaryLogic::createMaybe(); + continue; + } + if ($this->treatPhpDocTypesAsCertain) { $argumentType = $scope->getType($sureType[0]); } else { @@ -336,6 +341,11 @@ public function findSpecifiedType( continue; } + if ($scope->isInTrait() && $sureNotType[0] instanceof Expr\Variable && $sureNotType[0]->name === 'this') { + $results[] = TrinaryLogic::createMaybe(); + continue; + } + if ($this->treatPhpDocTypesAsCertain) { $argumentType = $scope->getType($sureNotType[0]); } else { diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 69e992139d..8f0f4843c2 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1207,4 +1207,10 @@ public function testBug13799(): void ]); } + public function testBug13023(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13023.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13023.php b/tests/PHPStan/Rules/Comparison/data/bug-13023.php new file mode 100644 index 0000000000..cba8bf0226 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13023.php @@ -0,0 +1,25 @@ + Date: Sat, 14 Mar 2026 09:37:28 +0000 Subject: [PATCH 02/16] Generalize trait $this check to cover property fetches and method calls The previous fix only handled `$this` directly in trait context, but similar false positives occur with expressions like `$this->foo` when different classes using the trait define properties with different types. Introduced `isExpressionDependentOnThis()` helper that recursively checks if an expression is rooted in `$this`, covering Variable, PropertyFetch, and MethodCall nodes. Co-Authored-By: Claude Opus 4.6 --- .../Comparison/ImpossibleCheckTypeHelper.php | 21 +++++++++++++-- .../Rules/Comparison/data/bug-13023.php | 27 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 555df067fa..cc0fe697c8 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -310,7 +310,7 @@ public function findSpecifiedType( continue; } - if ($scope->isInTrait() && $sureType[0] instanceof Expr\Variable && $sureType[0]->name === 'this') { + if ($scope->isInTrait() && self::isExpressionDependentOnThis($sureType[0])) { $results[] = TrinaryLogic::createMaybe(); continue; } @@ -341,7 +341,7 @@ public function findSpecifiedType( continue; } - if ($scope->isInTrait() && $sureNotType[0] instanceof Expr\Variable && $sureNotType[0]->name === 'this') { + if ($scope->isInTrait() && self::isExpressionDependentOnThis($sureNotType[0])) { $results[] = TrinaryLogic::createMaybe(); continue; } @@ -391,6 +391,23 @@ private static function isSpecified(Scope $scope, Expr $node, Expr $expr): bool ) && $scope->hasExpressionType($expr)->yes(); } + private static function isExpressionDependentOnThis(Expr $expr): bool + { + if ($expr instanceof Expr\Variable && $expr->name === 'this') { + return true; + } + + if ($expr instanceof Expr\PropertyFetch) { + return self::isExpressionDependentOnThis($expr->var); + } + + if ($expr instanceof Expr\MethodCall) { + return self::isExpressionDependentOnThis($expr->var); + } + + return false; + } + /** * @param Node\Arg[] $args */ diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13023.php b/tests/PHPStan/Rules/Comparison/data/bug-13023.php index cba8bf0226..2b68670e1e 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-13023.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-13023.php @@ -23,3 +23,30 @@ public function getRandom(): int return $value; } } + +class SomeClass3 +{ + use MyTrait2; + + public string $foo = 'foo'; +} + +class SomeClass4 +{ + use MyTrait2; + + public int $foo = 1; +} + +trait MyTrait2 +{ + public function getRandom(): int + { + $value = random_int(1, 100); + if (\is_int($this->foo)) { + return $value * $value; + } + + return $value; + } +} From af1247cc40f8967b1b2658022afd55ce78983ade Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 14 Mar 2026 09:53:34 +0000 Subject: [PATCH 03/16] Handle static access, nullsafe access, and $this:: in trait $this check Extend isExpressionDependentOnThis() to also recognize: - NullsafePropertyFetch ($this?->foo) - NullsafeMethodCall ($this?->method()) - StaticPropertyFetch (self::$foo, static::$foo, $this::$foo) - StaticCall (self::method(), static::method(), $this::method()) Co-Authored-By: Claude Opus 4.6 --- .../Comparison/ImpossibleCheckTypeHelper.php | 13 +++- .../Rules/Comparison/data/bug-13023.php | 60 +++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index cc0fe697c8..18141403bd 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -397,14 +397,23 @@ private static function isExpressionDependentOnThis(Expr $expr): bool return true; } - if ($expr instanceof Expr\PropertyFetch) { + if ($expr instanceof Expr\PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) { return self::isExpressionDependentOnThis($expr->var); } - if ($expr instanceof Expr\MethodCall) { + if ($expr instanceof Expr\MethodCall || $expr instanceof Expr\NullsafeMethodCall) { return self::isExpressionDependentOnThis($expr->var); } + if ($expr instanceof Expr\StaticPropertyFetch || $expr instanceof Expr\StaticCall) { + if ($expr->class instanceof Expr) { + return self::isExpressionDependentOnThis($expr->class); + } + + $className = $expr->class->toString(); + return in_array($className, ['self', 'static', 'parent'], true); + } + return false; } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13023.php b/tests/PHPStan/Rules/Comparison/data/bug-13023.php index 2b68670e1e..e967ef93d2 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-13023.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-13023.php @@ -50,3 +50,63 @@ public function getRandom(): int return $value; } } + +class SomeClass5 +{ + use MyTrait3; + + public static string $bar = 'bar'; +} + +class SomeClass6 +{ + use MyTrait3; + + public static int $bar = 1; +} + +trait MyTrait3 +{ + public function getRandom(): int + { + $value = random_int(1, 100); + if (\is_int(self::$bar)) { + return $value * $value; + } + if (\is_int(static::$bar)) { + return $value * $value; + } + if (\is_int($this::$bar)) { + return $value * $value; + } + + return $value; + } +} + +class SomeClass7 +{ + use MyTrait4; + + public ?string $baz = 'baz'; +} + +class SomeClass8 +{ + use MyTrait4; + + public ?int $baz = 1; +} + +trait MyTrait4 +{ + public function getRandom(): int + { + $value = random_int(1, 100); + if (\is_int($this?->baz)) { + return $value * $value; + } + + return $value; + } +} From 6b9535daac903caed7a6309b89caafa4dc268014 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 14 Mar 2026 10:07:53 +0000 Subject: [PATCH 04/16] Fix BooleanNot false positive for $this-dependent checks in traits When ImpossibleCheckTypeHelper returns null (uncertain) for trait+$this function calls, ConstantConditionRuleHelper fell through to scope type resolution which still gave a definitive answer. This caused BooleanNotConstantConditionRule to report "always false" for negated type checks like `! is_string($this->message)` in traits. The fix skips function calls with $this-dependent arguments in trait context in ConstantConditionRuleHelper::shouldSkip(), consistent with how ImpossibleCheckTypeHelper already handles these cases. Co-Authored-By: Claude Opus 4.6 --- .../ConstantConditionRuleHelper.php | 8 +++ .../Comparison/ImpossibleCheckTypeHelper.php | 2 +- .../BooleanNotConstantConditionRuleTest.php | 6 ++ .../Rules/Comparison/data/bug-13023.php | 56 +++++++++++++++++++ 4 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/Rules/Comparison/ConstantConditionRuleHelper.php b/src/Rules/Comparison/ConstantConditionRuleHelper.php index 30a08f665d..d680baaffa 100644 --- a/src/Rules/Comparison/ConstantConditionRuleHelper.php +++ b/src/Rules/Comparison/ConstantConditionRuleHelper.php @@ -61,6 +61,14 @@ private function shouldSkip(Scope $scope, Expr $expr): bool if ($isAlways !== null) { return true; } + + if ($scope->isInTrait()) { + foreach ($expr->getArgs() as $arg) { + if (ImpossibleCheckTypeHelper::isExpressionDependentOnThis($arg->value)) { + return true; + } + } + } } return false; diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 18141403bd..30901ac5f6 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -391,7 +391,7 @@ private static function isSpecified(Scope $scope, Expr $node, Expr $expr): bool ) && $scope->hasExpressionType($expr)->yes(); } - private static function isExpressionDependentOnThis(Expr $expr): bool + public static function isExpressionDependentOnThis(Expr $expr): bool { if ($expr instanceof Expr\Variable && $expr->name === 'this') { return true; diff --git a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php index 3cf3f2a8bb..f265e02db8 100644 --- a/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/BooleanNotConstantConditionRuleTest.php @@ -236,4 +236,10 @@ public function testBug6702(): void $this->analyse([__DIR__ . '/data/bug-6702.php'], []); } + public function testBug13023(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13023.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13023.php b/tests/PHPStan/Rules/Comparison/data/bug-13023.php index e967ef93d2..5b1bbd1a6a 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-13023.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-13023.php @@ -110,3 +110,59 @@ public function getRandom(): int return $value; } } + +class HelloWorld +{ + use SomeTrait; + + public string $message = 'Hello'; + + public function foo(): void + { + $this->bar(); + } +} + +class EmptyClass { + use SomeTrait; +} + +trait SomeTrait { + public function bar(): void + { + if (property_exists($this, 'message')) { + if (! is_string($this->message)) { + return; + } + + echo $this->message . "\n"; + } + } +} + +class SomeClass9 +{ + use MyTrait5; + + public string $prop = 'foo'; +} + +class SomeClass10 +{ + use MyTrait5; + + public int $prop = 1; +} + +trait MyTrait5 +{ + public function getRandom(): int + { + $value = random_int(1, 100); + if (!\is_int($this->prop)) { + return $value; + } + + return $value * $value; + } +} From 5ac5e5f65d061b10510c1c26079c06a6b09b5fec Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 14 Mar 2026 11:10:42 +0000 Subject: [PATCH 05/16] Move isExpressionDependentOnThis to ExpressionDependsOnThisHelper Extract the static method into a standalone helper class to avoid ConstantConditionRuleHelper depending on ImpossibleCheckTypeHelper. Co-Authored-By: Claude Opus 4.6 --- .../ConstantConditionRuleHelper.php | 2 +- .../ExpressionDependsOnThisHelper.php | 36 +++++++++++++++++++ .../Comparison/ImpossibleCheckTypeHelper.php | 34 ++++-------------- 3 files changed, 43 insertions(+), 29 deletions(-) create mode 100644 src/Rules/Comparison/ExpressionDependsOnThisHelper.php diff --git a/src/Rules/Comparison/ConstantConditionRuleHelper.php b/src/Rules/Comparison/ConstantConditionRuleHelper.php index d680baaffa..183862b59b 100644 --- a/src/Rules/Comparison/ConstantConditionRuleHelper.php +++ b/src/Rules/Comparison/ConstantConditionRuleHelper.php @@ -64,7 +64,7 @@ private function shouldSkip(Scope $scope, Expr $expr): bool if ($scope->isInTrait()) { foreach ($expr->getArgs() as $arg) { - if (ImpossibleCheckTypeHelper::isExpressionDependentOnThis($arg->value)) { + if (ExpressionDependsOnThisHelper::isExpressionDependentOnThis($arg->value)) { return true; } } diff --git a/src/Rules/Comparison/ExpressionDependsOnThisHelper.php b/src/Rules/Comparison/ExpressionDependsOnThisHelper.php new file mode 100644 index 0000000000..ef555cf5ec --- /dev/null +++ b/src/Rules/Comparison/ExpressionDependsOnThisHelper.php @@ -0,0 +1,36 @@ +name === 'this') { + return true; + } + + if ($expr instanceof Expr\PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) { + return self::isExpressionDependentOnThis($expr->var); + } + + if ($expr instanceof Expr\MethodCall || $expr instanceof Expr\NullsafeMethodCall) { + return self::isExpressionDependentOnThis($expr->var); + } + + if ($expr instanceof Expr\StaticPropertyFetch || $expr instanceof Expr\StaticCall) { + if ($expr->class instanceof Expr) { + return self::isExpressionDependentOnThis($expr->class); + } + + $className = $expr->class->toString(); + return in_array($className, ['self', 'static', 'parent'], true); + } + + return false; + } + +} diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 30901ac5f6..c22c3efca4 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -198,6 +198,10 @@ public function findSpecifiedType( } } elseif ($functionName === 'method_exists' && $argsCount >= 2) { $objectArg = $args[0]->value; + + if ($scope->isInTrait() && ExpressionDependsOnThisHelper::isExpressionDependentOnThis($objectArg)) { + return null; + } $objectType = $this->treatPhpDocTypesAsCertain ? $scope->getType($objectArg) : $scope->getNativeType($objectArg); if ($objectType instanceof ConstantStringType @@ -310,7 +314,7 @@ public function findSpecifiedType( continue; } - if ($scope->isInTrait() && self::isExpressionDependentOnThis($sureType[0])) { + if ($scope->isInTrait() && ExpressionDependsOnThisHelper::isExpressionDependentOnThis($sureType[0])) { $results[] = TrinaryLogic::createMaybe(); continue; } @@ -341,7 +345,7 @@ public function findSpecifiedType( continue; } - if ($scope->isInTrait() && self::isExpressionDependentOnThis($sureNotType[0])) { + if ($scope->isInTrait() && ExpressionDependsOnThisHelper::isExpressionDependentOnThis($sureNotType[0])) { $results[] = TrinaryLogic::createMaybe(); continue; } @@ -391,32 +395,6 @@ private static function isSpecified(Scope $scope, Expr $node, Expr $expr): bool ) && $scope->hasExpressionType($expr)->yes(); } - public static function isExpressionDependentOnThis(Expr $expr): bool - { - if ($expr instanceof Expr\Variable && $expr->name === 'this') { - return true; - } - - if ($expr instanceof Expr\PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) { - return self::isExpressionDependentOnThis($expr->var); - } - - if ($expr instanceof Expr\MethodCall || $expr instanceof Expr\NullsafeMethodCall) { - return self::isExpressionDependentOnThis($expr->var); - } - - if ($expr instanceof Expr\StaticPropertyFetch || $expr instanceof Expr\StaticCall) { - if ($expr->class instanceof Expr) { - return self::isExpressionDependentOnThis($expr->class); - } - - $className = $expr->class->toString(); - return in_array($className, ['self', 'static', 'parent'], true); - } - - return false; - } - /** * @param Node\Arg[] $args */ From ba6e8f3fdc4d5efe984c6d8e28559ea3a062320f Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 14 Mar 2026 11:10:47 +0000 Subject: [PATCH 06/16] Fix method_exists($this) false positives in traits and PHP 7.4 lint Add early return for method_exists() with $this-dependent expressions in trait context, and add // lint >= 8.0 comment to bug-13023 test file for nullsafe operator compatibility. Co-Authored-By: Claude Opus 4.6 --- ...mpossibleCheckTypeFunctionCallRuleTest.php | 45 ++++++++++++++----- .../Rules/Comparison/data/bug-13023.php | 2 +- 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 8f0f4843c2..c7148ae01a 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -183,18 +183,6 @@ public function testImpossibleCheckTypeFunctionCall(): void 'Call to function method_exists() with CheckTypeFunctionCall\MethodExists and \'testWithNewObjectIn…\' will always evaluate to true.', 635, ], - [ - 'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'method\' will always evaluate to true.', - 650, - ], - [ - 'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'someAnother\' will always evaluate to true.', - 653, - ], - [ - 'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'unknown\' will always evaluate to false.', - 656, - ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'method\' will always evaluate to true.', 659, @@ -1213,4 +1201,37 @@ public function testBug13023(): void $this->analyse([__DIR__ . '/data/bug-13023.php'], []); } + public function testBug7599(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-7599.php'], [ + [ + 'Call to function method_exists() with Bug7599\SecondEnum::Baz and \'barMethod\' will always evaluate to true.', + 13, + ], + [ + 'Call to function method_exists() with Bug7599\TestEnum::Bar|Bug7599\TestEnum::Foo and \'barMethod\' will always evaluate to false.', + 13, + ], + ]); + } + + public function testBug9095(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-9095.php'], []); + } + + public function testBug13474(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13474.php'], []); + } + + public function testBug13687(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-13687.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13023.php b/tests/PHPStan/Rules/Comparison/data/bug-13023.php index 5b1bbd1a6a..2183704383 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-13023.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-13023.php @@ -1,4 +1,4 @@ -= 8.0 namespace Bug13023; From b18a0b053a3237c4cff7002d5f4eb8b8b487119b Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 14 Mar 2026 11:10:51 +0000 Subject: [PATCH 07/16] Add regression tests for trait-related false positives Add test data files for phpstan/phpstan#7599, #9095, #13474, and #13687 to document and prevent regressions in trait type checking behavior. Co-Authored-By: Claude Opus 4.6 --- .../Rules/Comparison/data/bug-13474.php | 86 +++++++++++++++++++ .../Rules/Comparison/data/bug-13687.php | 29 +++++++ .../Rules/Comparison/data/bug-7599.php | 39 +++++++++ .../Rules/Comparison/data/bug-9095.php | 23 +++++ 4 files changed, 177 insertions(+) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-13474.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-13687.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-7599.php create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-9095.php diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13474.php b/tests/PHPStan/Rules/Comparison/data/bug-13474.php new file mode 100644 index 0000000000..db93fd400f --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13474.php @@ -0,0 +1,86 @@ += 8.0 + +namespace Bug13474; + +/** + * @template TValue of mixed + */ +interface ModelInterface { + /** + * @return TValue + */ + public function getValue(): mixed; +} + +/** + * @implements ModelInterface + */ +class ModelA implements ModelInterface +{ + public function getValue(): int + { + return 0; + } +} + +/** + * @implements ModelInterface + */ +class ModelB implements ModelInterface +{ + public function getValue(): string + { + return 'foo'; + } +} + +/** + * @template T of ModelInterface + */ +trait ModelTrait +{ + /** + * @return T + */ + abstract function model(): ModelInterface; + + /** + * @return template-type + */ + public function getValue(): mixed + { + return $this->model()->getValue(); + } + + public function test(): void + { + if (is_string($this->getValue())) { + echo 'string'; + return; + } + + echo 'other'; + } +} + +class TestA +{ + /** @use ModelTrait */ + use ModelTrait; + + function model(): ModelA + { + return new ModelA(); + } +} + +class TestB +{ + /** @use ModelTrait */ + use ModelTrait; + + function model(): ModelB + { + return new ModelB(); + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-13687.php b/tests/PHPStan/Rules/Comparison/data/bug-13687.php new file mode 100644 index 0000000000..08927d78c6 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-13687.php @@ -0,0 +1,29 @@ +bar(); + } + + if (property_exists($this, 'baz')) { + $a = $this->baz; + } + } +} + +class A { + use MyTrait; + + public string $baz = 'baz'; +} + +class B { + use MyTrait; + + public function bar(): void { + echo 'bar'; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-7599.php b/tests/PHPStan/Rules/Comparison/data/bug-7599.php new file mode 100644 index 0000000000..c4fa4b3fd7 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-7599.php @@ -0,0 +1,39 @@ += 8.1 + +namespace Bug7599; + +trait TraitForEnum +{ + /** + * @return array + */ + public static function fooMethod(): array + { + return array_map( + fn(self $enum): string => method_exists($enum, 'barMethod') + ? $enum->barMethod() + : $enum->name, + static::cases() + ); + } +} + +enum TestEnum: string +{ + use TraitForEnum; + + case Foo = 'foo'; + case Bar = 'bar'; +} + +enum SecondEnum: string +{ + use TraitForEnum; + + case Baz = 'baz'; + + public function barMethod(): string + { + return 'blah'; + } +} diff --git a/tests/PHPStan/Rules/Comparison/data/bug-9095.php b/tests/PHPStan/Rules/Comparison/data/bug-9095.php new file mode 100644 index 0000000000..4b9b84f858 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-9095.php @@ -0,0 +1,23 @@ +createdAt) && + $this->createdAt instanceof \DateTimeInterface + ) { + return $this->createdAt; + } + return null; + } +} + +final class Event +{ + use EventTrait; +} From 4f8d0b3255643aeb441ceda3470f6d64bbf7df7b Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 14 Mar 2026 11:29:30 +0000 Subject: [PATCH 08/16] Still report method_exists($this) as always true when method is defined in the trait When a trait defines a method itself, method_exists($this, 'method') will always be true regardless of which class uses the trait. Only suppress the report when the method is NOT defined in the trait, since then the result depends on the using class. Co-Authored-By: Claude Opus 4.6 --- src/Rules/Comparison/ImpossibleCheckTypeHelper.php | 11 ++++++++++- .../ImpossibleCheckTypeFunctionCallRuleTest.php | 4 ++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index c22c3efca4..558cead204 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -200,7 +200,16 @@ public function findSpecifiedType( $objectArg = $args[0]->value; if ($scope->isInTrait() && ExpressionDependsOnThisHelper::isExpressionDependentOnThis($objectArg)) { - return null; + $traitReflection = $scope->getTraitReflection(); + $methodArgValue = $args[1]->value; + $methodArgType = $this->treatPhpDocTypesAsCertain ? $scope->getType($methodArgValue) : $scope->getNativeType($methodArgValue); + $constantMethodNames = $methodArgType->getConstantStrings(); + if ( + count($constantMethodNames) !== 1 + || !$traitReflection->hasNativeMethod($constantMethodNames[0]->getValue()) + ) { + return null; + } } $objectType = $this->treatPhpDocTypesAsCertain ? $scope->getType($objectArg) : $scope->getNativeType($objectArg); diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index c7148ae01a..a1534fed98 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -183,6 +183,10 @@ public function testImpossibleCheckTypeFunctionCall(): void 'Call to function method_exists() with CheckTypeFunctionCall\MethodExists and \'testWithNewObjectIn…\' will always evaluate to true.', 635, ], + [ + 'Call to function method_exists() with $this(CheckTypeFunctionCall\MethodExistsWithTrait) and \'method\' will always evaluate to true.', + 650, + ], [ 'Call to function method_exists() with \'CheckTypeFunctionCall\\\\MethodExistsWithTrait\' and \'method\' will always evaluate to true.', 659, From 6d73f01d227f33fb908483314e20247443d8082a Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 14 Mar 2026 11:54:24 +0000 Subject: [PATCH 09/16] Extend trait context detection to cover self-typed variables and fix multiple method names - Add isExpressionDependentOnTraitContext() that checks both AST-level ($this, self::, static::) and type-level (object type matches using class) dependency on trait context - Fix method_exists handling to support multiple constant method names (e.g. 'foo'|'bar') instead of requiring exactly one - Add null check for getTraitReflection() - Update ConstantConditionRuleHelper with same type-level check - Fix testBug7599: method_exists($enum, 'barMethod') where $enum is typed as self in a trait should not report errors Co-Authored-By: Claude Opus 4.6 --- .../ConstantConditionRuleHelper.php | 10 +++++ .../Comparison/ImpossibleCheckTypeHelper.php | 44 ++++++++++++++++--- ...mpossibleCheckTypeFunctionCallRuleTest.php | 11 +---- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/Rules/Comparison/ConstantConditionRuleHelper.php b/src/Rules/Comparison/ConstantConditionRuleHelper.php index 183862b59b..3f15c28e76 100644 --- a/src/Rules/Comparison/ConstantConditionRuleHelper.php +++ b/src/Rules/Comparison/ConstantConditionRuleHelper.php @@ -67,6 +67,16 @@ private function shouldSkip(Scope $scope, Expr $expr): bool if (ExpressionDependsOnThisHelper::isExpressionDependentOnThis($arg->value)) { return true; } + + $classReflection = $scope->getClassReflection(); + if ($classReflection !== null) { + $argType = $this->treatPhpDocTypesAsCertain ? $scope->getType($arg->value) : $scope->getNativeType($arg->value); + foreach ($argType->getObjectClassNames() as $className) { + if ($className === $classReflection->getName()) { + return true; + } + } + } } } } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 558cead204..af01efbb1f 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -199,17 +199,22 @@ public function findSpecifiedType( } elseif ($functionName === 'method_exists' && $argsCount >= 2) { $objectArg = $args[0]->value; - if ($scope->isInTrait() && ExpressionDependsOnThisHelper::isExpressionDependentOnThis($objectArg)) { + if ($this->isExpressionDependentOnTraitContext($scope, $objectArg)) { $traitReflection = $scope->getTraitReflection(); + if ($traitReflection === null) { + return null; + } $methodArgValue = $args[1]->value; $methodArgType = $this->treatPhpDocTypesAsCertain ? $scope->getType($methodArgValue) : $scope->getNativeType($methodArgValue); $constantMethodNames = $methodArgType->getConstantStrings(); - if ( - count($constantMethodNames) !== 1 - || !$traitReflection->hasNativeMethod($constantMethodNames[0]->getValue()) - ) { + if (count($constantMethodNames) === 0) { return null; } + foreach ($constantMethodNames as $constantMethodName) { + if (!$traitReflection->hasNativeMethod($constantMethodName->getValue())) { + return null; + } + } } $objectType = $this->treatPhpDocTypesAsCertain ? $scope->getType($objectArg) : $scope->getNativeType($objectArg); @@ -323,7 +328,7 @@ public function findSpecifiedType( continue; } - if ($scope->isInTrait() && ExpressionDependsOnThisHelper::isExpressionDependentOnThis($sureType[0])) { + if ($this->isExpressionDependentOnTraitContext($scope, $sureType[0])) { $results[] = TrinaryLogic::createMaybe(); continue; } @@ -354,7 +359,7 @@ public function findSpecifiedType( continue; } - if ($scope->isInTrait() && ExpressionDependsOnThisHelper::isExpressionDependentOnThis($sureNotType[0])) { + if ($this->isExpressionDependentOnTraitContext($scope, $sureNotType[0])) { $results[] = TrinaryLogic::createMaybe(); continue; } @@ -379,6 +384,31 @@ public function findSpecifiedType( return $result->maybe() ? null : $result->yes(); } + private function isExpressionDependentOnTraitContext(Scope $scope, Expr $expr): bool + { + if (!$scope->isInTrait()) { + return false; + } + + if (ExpressionDependsOnThisHelper::isExpressionDependentOnThis($expr)) { + return true; + } + + $classReflection = $scope->getClassReflection(); + if ($classReflection === null) { + return false; + } + + $type = $this->treatPhpDocTypesAsCertain ? $scope->getType($expr) : $scope->getNativeType($expr); + foreach ($type->getObjectClassNames() as $className) { + if ($className === $classReflection->getName()) { + return true; + } + } + + return false; + } + private static function isSpecified(Scope $scope, Expr $node, Expr $expr): bool { if ($expr === $node) { diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index a1534fed98..2d6b3a0199 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1208,16 +1208,7 @@ public function testBug13023(): void public function testBug7599(): void { $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/bug-7599.php'], [ - [ - 'Call to function method_exists() with Bug7599\SecondEnum::Baz and \'barMethod\' will always evaluate to true.', - 13, - ], - [ - 'Call to function method_exists() with Bug7599\TestEnum::Bar|Bug7599\TestEnum::Foo and \'barMethod\' will always evaluate to false.', - 13, - ], - ]); + $this->analyse([__DIR__ . '/data/bug-7599.php'], []); } public function testBug9095(): void From 44458d8fe89d5fb01812283b8b4e7393d3d77c30 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 14 Mar 2026 12:12:34 +0000 Subject: [PATCH 10/16] Fix lint errors and skip testBug7599 on PHP < 8.1 - Use early exit in ConstantConditionRuleHelper to reduce nesting (SlevomatCodingStandard.ControlStructures.EarlyExit) - Use \in_array() instead of fallback global name in ExpressionDependsOnThisHelper (SlevomatCodingStandard.Namespaces.ReferenceUsedNamesOnly) - Skip testBug7599 on PHP < 8.1 using #[RequiresPhp] attribute (enums require PHP 8.1) Co-Authored-By: Claude Opus 4.6 --- .../ConstantConditionRuleHelper.php | 30 +++++++++++-------- .../ExpressionDependsOnThisHelper.php | 2 +- ...mpossibleCheckTypeFunctionCallRuleTest.php | 1 + 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/Rules/Comparison/ConstantConditionRuleHelper.php b/src/Rules/Comparison/ConstantConditionRuleHelper.php index 3f15c28e76..95f6934e5b 100644 --- a/src/Rules/Comparison/ConstantConditionRuleHelper.php +++ b/src/Rules/Comparison/ConstantConditionRuleHelper.php @@ -62,20 +62,24 @@ private function shouldSkip(Scope $scope, Expr $expr): bool return true; } - if ($scope->isInTrait()) { - foreach ($expr->getArgs() as $arg) { - if (ExpressionDependsOnThisHelper::isExpressionDependentOnThis($arg->value)) { - return true; - } + if (!$scope->isInTrait()) { + return false; + } + + foreach ($expr->getArgs() as $arg) { + if (ExpressionDependsOnThisHelper::isExpressionDependentOnThis($arg->value)) { + return true; + } + + $classReflection = $scope->getClassReflection(); + if ($classReflection === null) { + continue; + } - $classReflection = $scope->getClassReflection(); - if ($classReflection !== null) { - $argType = $this->treatPhpDocTypesAsCertain ? $scope->getType($arg->value) : $scope->getNativeType($arg->value); - foreach ($argType->getObjectClassNames() as $className) { - if ($className === $classReflection->getName()) { - return true; - } - } + $argType = $this->treatPhpDocTypesAsCertain ? $scope->getType($arg->value) : $scope->getNativeType($arg->value); + foreach ($argType->getObjectClassNames() as $className) { + if ($className === $classReflection->getName()) { + return true; } } } diff --git a/src/Rules/Comparison/ExpressionDependsOnThisHelper.php b/src/Rules/Comparison/ExpressionDependsOnThisHelper.php index ef555cf5ec..638d817265 100644 --- a/src/Rules/Comparison/ExpressionDependsOnThisHelper.php +++ b/src/Rules/Comparison/ExpressionDependsOnThisHelper.php @@ -27,7 +27,7 @@ public static function isExpressionDependentOnThis(Expr $expr): bool } $className = $expr->class->toString(); - return in_array($className, ['self', 'static', 'parent'], true); + return \in_array($className, ['self', 'static', 'parent'], true); } return false; diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 2d6b3a0199..3204b2ee05 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1205,6 +1205,7 @@ public function testBug13023(): void $this->analyse([__DIR__ . '/data/bug-13023.php'], []); } + #[RequiresPhp('>= 8.1')] public function testBug7599(): void { $this->treatPhpDocTypesAsCertain = true; From ee69212cc470d475bdbb4ad28e97e6a141c9cc94 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sat, 14 Mar 2026 12:31:36 +0000 Subject: [PATCH 11/16] Add regression test for phpstan/phpstan#12798 Test is_subclass_of in enum traits with BackedEnum and custom interface checks. Verifies no false positive function.alreadyNarrowedType or function.impossibleType errors are reported. Co-Authored-By: Claude Opus 4.6 --- ...mpossibleCheckTypeFunctionCallRuleTest.php | 7 +++ .../Rules/Comparison/data/bug-12798.php | 49 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 tests/PHPStan/Rules/Comparison/data/bug-12798.php diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index 3204b2ee05..34fdb5c4b1 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -1230,4 +1230,11 @@ public function testBug13687(): void $this->analyse([__DIR__ . '/data/bug-13687.php'], []); } + #[RequiresPhp('>= 8.1')] + public function testBug12798(): void + { + $this->treatPhpDocTypesAsCertain = true; + $this->analyse([__DIR__ . '/data/bug-12798.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12798.php b/tests/PHPStan/Rules/Comparison/data/bug-12798.php new file mode 100644 index 0000000000..f93d0077a5 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-12798.php @@ -0,0 +1,49 @@ += 8.1 + +namespace Bug12798; + +interface Colorable +{ + public function color(): string; +} + +trait HasColors +{ + /** @return array */ + public static function colors(): array { + return array_reduce(self::cases(), function (array $colors, self $case) { + $key = is_subclass_of($case, \BackedEnum::class) ? $case->value : $case->name; + $color = is_subclass_of($case, Colorable::class) ? $case->color() : 'gray'; + + $colors[$key] = $color; + return $colors; + }, []); + } +} + +enum AlertLevelBacked: int implements Colorable +{ + use HasColors; + + case Low = 1; + case Medium = 2; + case Critical = 3; + + public function color(): string + { + return match ($this) { + self::Low => 'green', + self::Medium => 'yellow', + self::Critical => 'red', + }; + } +} + +enum AlertLevel +{ + use HasColors; + + case Low; + case Medium; + case Critical; +} From 8f559bc1815186dbdd1e0089f1b4e492e06b8481 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 14 Mar 2026 13:55:30 +0100 Subject: [PATCH 12/16] Add test --- src/Rules/Comparison/ExpressionDependsOnThisHelper.php | 3 ++- .../Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Rules/Comparison/ExpressionDependsOnThisHelper.php b/src/Rules/Comparison/ExpressionDependsOnThisHelper.php index 638d817265..223f2c9aa3 100644 --- a/src/Rules/Comparison/ExpressionDependsOnThisHelper.php +++ b/src/Rules/Comparison/ExpressionDependsOnThisHelper.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Comparison; use PhpParser\Node\Expr; +use function in_array; final class ExpressionDependsOnThisHelper { @@ -27,7 +28,7 @@ public static function isExpressionDependentOnThis(Expr $expr): bool } $className = $expr->class->toString(); - return \in_array($className, ['self', 'static', 'parent'], true); + return in_array($className, ['self', 'static', 'parent'], true); } return false; diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php index e61164f515..832f01f53e 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php @@ -144,6 +144,11 @@ public function testBug12273(): void ]); } + public function testBug12798(): void + { + $this->analyse([__DIR__ . '/../Comparison/data/bug-12798.php'], []); + } + public function testBug12981(): void { $this->analyse([__DIR__ . '/data/bug-12981.php'], [ From cb69e3eac23dd107d9114f49be92abd082135f25 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 14 Mar 2026 22:17:55 +0100 Subject: [PATCH 13/16] Rework --- .../Comparison/ConstantConditionRuleHelper.php | 2 +- .../ImpossibleCheckTypeFunctionCallRule.php | 4 ++-- .../Comparison/ImpossibleCheckTypeHelper.php | 16 +++++++++++++--- .../ImpossibleCheckTypeMethodCallRule.php | 4 ++-- .../ImpossibleCheckTypeStaticMethodCallRule.php | 4 ++-- ...ifyingFunctionsDynamicReturnTypeExtension.php | 1 + .../PHPStan/Rules/Comparison/data/bug-12798.php | 1 + 7 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/Rules/Comparison/ConstantConditionRuleHelper.php b/src/Rules/Comparison/ConstantConditionRuleHelper.php index 95f6934e5b..06ca065e89 100644 --- a/src/Rules/Comparison/ConstantConditionRuleHelper.php +++ b/src/Rules/Comparison/ConstantConditionRuleHelper.php @@ -57,7 +57,7 @@ private function shouldSkip(Scope $scope, Expr $expr): bool || $expr instanceof Expr\StaticCall ) && !$expr->isFirstClassCallable() ) { - $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $expr); + $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $expr, true); if ($isAlways !== null) { return true; } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php index 61e3b57c30..eaebc42672 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeFunctionCallRule.php @@ -43,7 +43,7 @@ public function processNode(Node $node, Scope $scope): array } $functionName = (string) $node->name; - $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node); + $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node, true); if ($isAlways === null) { return []; } @@ -53,7 +53,7 @@ public function processNode(Node $node, Scope $scope): array return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); } - $isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $node); + $isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $node, true); if ($isAlways !== null) { return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index af01efbb1f..42e75360eb 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -61,6 +61,7 @@ public function __construct( public function findSpecifiedType( Scope $scope, Expr $node, + bool $ignoreTraitContext, ): ?bool { if ($node instanceof FuncCall) { @@ -199,7 +200,10 @@ public function findSpecifiedType( } elseif ($functionName === 'method_exists' && $argsCount >= 2) { $objectArg = $args[0]->value; - if ($this->isExpressionDependentOnTraitContext($scope, $objectArg)) { + if ( + $ignoreTraitContext + && $this->isExpressionDependentOnTraitContext($scope, $objectArg) + ) { $traitReflection = $scope->getTraitReflection(); if ($traitReflection === null) { return null; @@ -328,7 +332,10 @@ public function findSpecifiedType( continue; } - if ($this->isExpressionDependentOnTraitContext($scope, $sureType[0])) { + if ( + $ignoreTraitContext + && $this->isExpressionDependentOnTraitContext($scope, $sureType[0]) + ) { $results[] = TrinaryLogic::createMaybe(); continue; } @@ -359,7 +366,10 @@ public function findSpecifiedType( continue; } - if ($this->isExpressionDependentOnTraitContext($scope, $sureNotType[0])) { + if ( + $ignoreTraitContext + && $this->isExpressionDependentOnTraitContext($scope, $sureNotType[0]) + ) { $results[] = TrinaryLogic::createMaybe(); continue; } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php index bc8284d111..92265b5051 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeMethodCallRule.php @@ -45,7 +45,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node); + $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node, true); if ($isAlways === null) { return []; } @@ -55,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); } - $isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $node); + $isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $node, true); if ($isAlways !== null) { return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); } diff --git a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php index 3c24b38176..647c89ab23 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeStaticMethodCallRule.php @@ -45,7 +45,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node); + $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $node, true); if ($isAlways === null) { return []; } @@ -55,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); } - $isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $node); + $isAlways = $this->impossibleCheckTypeHelper->doNotTreatPhpDocTypesAsCertain()->findSpecifiedType($scope, $node, true); if ($isAlways !== null) { return $this->possiblyImpureTipHelper->addTip($scope, $node, $ruleErrorBuilder); } diff --git a/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php b/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php index 8ed3c6b605..81f1af6b7d 100644 --- a/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php +++ b/src/Type/Php/TypeSpecifyingFunctionsDynamicReturnTypeExtension.php @@ -66,6 +66,7 @@ public function getTypeFromFunctionCall( $isAlways = $this->getHelper()->findSpecifiedType( $scope, $functionCall, + false, ); if ($isAlways === null) { return null; diff --git a/tests/PHPStan/Rules/Comparison/data/bug-12798.php b/tests/PHPStan/Rules/Comparison/data/bug-12798.php index f93d0077a5..7bc404d240 100644 --- a/tests/PHPStan/Rules/Comparison/data/bug-12798.php +++ b/tests/PHPStan/Rules/Comparison/data/bug-12798.php @@ -16,6 +16,7 @@ public static function colors(): array { $color = is_subclass_of($case, Colorable::class) ? $case->color() : 'gray'; $colors[$key] = $color; + $colors[$color] = $color; return $colors; }, []); } From cb0a2e5efc5417e1f5009b7e17a5e99f0e2284bd Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 14 Mar 2026 22:25:47 +0100 Subject: [PATCH 14/16] Simplify --- .../ConstantConditionRuleHelper.php | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/Rules/Comparison/ConstantConditionRuleHelper.php b/src/Rules/Comparison/ConstantConditionRuleHelper.php index 06ca065e89..48fe9b0d84 100644 --- a/src/Rules/Comparison/ConstantConditionRuleHelper.php +++ b/src/Rules/Comparison/ConstantConditionRuleHelper.php @@ -57,32 +57,10 @@ private function shouldSkip(Scope $scope, Expr $expr): bool || $expr instanceof Expr\StaticCall ) && !$expr->isFirstClassCallable() ) { - $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $expr, true); + $isAlways = $this->impossibleCheckTypeHelper->findSpecifiedType($scope, $expr, false); if ($isAlways !== null) { return true; } - - if (!$scope->isInTrait()) { - return false; - } - - foreach ($expr->getArgs() as $arg) { - if (ExpressionDependsOnThisHelper::isExpressionDependentOnThis($arg->value)) { - return true; - } - - $classReflection = $scope->getClassReflection(); - if ($classReflection === null) { - continue; - } - - $argType = $this->treatPhpDocTypesAsCertain ? $scope->getType($arg->value) : $scope->getNativeType($arg->value); - foreach ($argType->getObjectClassNames() as $className) { - if ($className === $classReflection->getName()) { - return true; - } - } - } } return false; From 9ba6146c5624e2545e6af4b601e0b1b038e449d5 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 14 Mar 2026 22:27:20 +0100 Subject: [PATCH 15/16] Remove dedicated class --- .../ExpressionDependsOnThisHelper.php | 37 ------------------- .../Comparison/ImpossibleCheckTypeHelper.php | 28 +++++++++++++- 2 files changed, 27 insertions(+), 38 deletions(-) delete mode 100644 src/Rules/Comparison/ExpressionDependsOnThisHelper.php diff --git a/src/Rules/Comparison/ExpressionDependsOnThisHelper.php b/src/Rules/Comparison/ExpressionDependsOnThisHelper.php deleted file mode 100644 index 223f2c9aa3..0000000000 --- a/src/Rules/Comparison/ExpressionDependsOnThisHelper.php +++ /dev/null @@ -1,37 +0,0 @@ -name === 'this') { - return true; - } - - if ($expr instanceof Expr\PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) { - return self::isExpressionDependentOnThis($expr->var); - } - - if ($expr instanceof Expr\MethodCall || $expr instanceof Expr\NullsafeMethodCall) { - return self::isExpressionDependentOnThis($expr->var); - } - - if ($expr instanceof Expr\StaticPropertyFetch || $expr instanceof Expr\StaticCall) { - if ($expr->class instanceof Expr) { - return self::isExpressionDependentOnThis($expr->class); - } - - $className = $expr->class->toString(); - return in_array($className, ['self', 'static', 'parent'], true); - } - - return false; - } - -} diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 42e75360eb..39088e3c32 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -400,7 +400,7 @@ private function isExpressionDependentOnTraitContext(Scope $scope, Expr $expr): return false; } - if (ExpressionDependsOnThisHelper::isExpressionDependentOnThis($expr)) { + if (self::isExpressionDependentOnThis($expr)) { return true; } @@ -419,6 +419,32 @@ private function isExpressionDependentOnTraitContext(Scope $scope, Expr $expr): return false; } + public static function isExpressionDependentOnThis(Expr $expr): bool + { + if ($expr instanceof Expr\Variable && $expr->name === 'this') { + return true; + } + + if ($expr instanceof Expr\PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) { + return self::isExpressionDependentOnThis($expr->var); + } + + if ($expr instanceof Expr\MethodCall || $expr instanceof Expr\NullsafeMethodCall) { + return self::isExpressionDependentOnThis($expr->var); + } + + if ($expr instanceof Expr\StaticPropertyFetch || $expr instanceof Expr\StaticCall) { + if ($expr->class instanceof Expr) { + return self::isExpressionDependentOnThis($expr->class); + } + + $className = $expr->class->toString(); + return in_array($className, ['self', 'static', 'parent'], true); + } + + return false; + } + private static function isSpecified(Scope $scope, Expr $node, Expr $expr): bool { if ($expr === $node) { From 1c6d39384a282cdc4c257b4c8a414ba45c20101a Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 14 Mar 2026 22:31:41 +0100 Subject: [PATCH 16/16] Fix test --- tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php index 832f01f53e..c7b2144c9f 100644 --- a/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/InvalidKeyInArrayDimFetchRuleTest.php @@ -144,6 +144,7 @@ public function testBug12273(): void ]); } + #[RequiresPhp('>= 8.1')] public function testBug12798(): void { $this->analyse([__DIR__ . '/../Comparison/data/bug-12798.php'], []);