Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b94b5d6
Fix false positives for is_a() and instanceof checks on $this in traits
github-actions[bot] Mar 10, 2026
4fc6007
Generalize trait $this check to cover property fetches and method calls
phpstan-bot Mar 14, 2026
af1247c
Handle static access, nullsafe access, and $this:: in trait $this check
phpstan-bot Mar 14, 2026
6b9535d
Fix BooleanNot false positive for $this-dependent checks in traits
phpstan-bot Mar 14, 2026
5ac5e5f
Move isExpressionDependentOnThis to ExpressionDependsOnThisHelper
phpstan-bot Mar 14, 2026
ba6e8f3
Fix method_exists($this) false positives in traits and PHP 7.4 lint
phpstan-bot Mar 14, 2026
b18a0b0
Add regression tests for trait-related false positives
phpstan-bot Mar 14, 2026
4f8d0b3
Still report method_exists($this) as always true when method is defin…
phpstan-bot Mar 14, 2026
6d73f01
Extend trait context detection to cover self-typed variables and fix …
phpstan-bot Mar 14, 2026
44458d8
Fix lint errors and skip testBug7599 on PHP < 8.1
phpstan-bot Mar 14, 2026
ee69212
Add regression test for phpstan/phpstan#12798
phpstan-bot Mar 14, 2026
8f559bc
Add test
VincentLanglet Mar 14, 2026
cb69e3e
Rework
VincentLanglet Mar 14, 2026
cb0a2e5
Simplify
VincentLanglet Mar 14, 2026
9ba6146
Remove dedicated class
VincentLanglet Mar 14, 2026
1c6d393
Fix test
VincentLanglet Mar 14, 2026
53eb552
Reduce visibility
VincentLanglet Mar 15, 2026
c86721e
Add test
VincentLanglet Mar 15, 2026
9fa10d3
Add tests
VincentLanglet Mar 15, 2026
7e49d26
Add trait context awareness to comparison rules
phpstan-bot Mar 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/Rules/Comparison/ConstantConditionRuleHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 (ExpressionDependsOnThisHelper::isExpressionDependentOnThis($arg->value)) {
return true;
}
}
}
}

return false;
Expand Down
36 changes: 36 additions & 0 deletions src/Rules/Comparison/ExpressionDependsOnThisHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php declare(strict_types = 1);

namespace PHPStan\Rules\Comparison;

use PhpParser\Node\Expr;

final class ExpressionDependsOnThisHelper
{

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;
}

}
14 changes: 14 additions & 0 deletions src/Rules/Comparison/ImpossibleCheckTypeHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -310,6 +314,11 @@ public function findSpecifiedType(
continue;
}

if ($scope->isInTrait() && ExpressionDependsOnThisHelper::isExpressionDependentOnThis($sureType[0])) {
$results[] = TrinaryLogic::createMaybe();
continue;
}

if ($this->treatPhpDocTypesAsCertain) {
$argumentType = $scope->getType($sureType[0]);
} else {
Expand All @@ -336,6 +345,11 @@ public function findSpecifiedType(
continue;
}

if ($scope->isInTrait() && ExpressionDependsOnThisHelper::isExpressionDependentOnThis($sureNotType[0])) {
$results[] = TrinaryLogic::createMaybe();
continue;
}

if ($this->treatPhpDocTypesAsCertain) {
$argumentType = $scope->getType($sureNotType[0]);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'], []);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1207,4 +1195,43 @@ public function testBug13799(): void
]);
}

public function testBug13023(): void
{
$this->treatPhpDocTypesAsCertain = true;
$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'], []);
}

}
168 changes: 168 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/bug-13023.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
<?php declare(strict_types = 1); // lint >= 8.0

namespace Bug13023;

class SomeClass
{
use MyTrait;
}

class SomeClass2
{
use MyTrait;
}

trait MyTrait
{
public function getRandom(): int
{
$value = random_int(1, 100);
if (is_a($this, SomeClass::class)) {
return $value * $value;
}
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;
}
}

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;
}
}

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;
}
}
Loading
Loading