From 21148eb99bf1b0a3bfd4c7d7b998f88a562f1554 Mon Sep 17 00:00:00 2001 From: ondrejmirtes <104888+ondrejmirtes@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:32:13 +0000 Subject: [PATCH 01/12] Resolve method reflection for $var::method() static calls - When $expr->class is an Expr (variable), resolve method reflection and parameters acceptor using getObjectTypeOrClassStringObjectType() so by-reference parameters and other argument analysis work correctly - Guard $this invalidation and constructor property initialization blocks with $expr->class instanceof Name to prevent $other::__construct() from incorrectly affecting $this - New regression test in tests/PHPStan/Analyser/nsrt/bug-5020.php --- src/Analyser/NodeScopeResolver.php | 21 ++++++++++ tests/PHPStan/Analyser/nsrt/bug-5020.php | 52 ++++++++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-5020.php diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 412fb12576..b83aff649c 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3426,6 +3426,25 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto foreach ($additionalThrowPoints as $throwPoint) { $throwPoints[] = $throwPoint; } + + if ($expr->name instanceof Identifier && $methodReflection === null) { + $staticMethodCalledOnType = TypeCombinator::removeNull($scope->getType($expr->class))->getObjectTypeOrClassStringObjectType(); + $methodName = $expr->name->name; + if ($staticMethodCalledOnType->hasMethod($methodName)->yes()) { + $methodReflection = $staticMethodCalledOnType->getMethod($methodName, $scope); + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( + $scope, + $expr->getArgs(), + $methodReflection->getVariants(), + $methodReflection->getNamedArgumentsVariants(), + ); + + $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); + if ($methodThrowPoint !== null) { + $throwPoints[] = $methodThrowPoint; + } + } + } } if ($methodReflection !== null) { @@ -3455,6 +3474,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto if ( $methodReflection !== null + && $expr->class instanceof Name && ( ( !$methodReflection->isStatic() @@ -3484,6 +3504,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto if ( $methodReflection !== null + && $expr->class instanceof Name && !$methodReflection->isStatic() && $methodReflection->getName() === '__construct' && $scopeFunction instanceof MethodReflection diff --git a/tests/PHPStan/Analyser/nsrt/bug-5020.php b/tests/PHPStan/Analyser/nsrt/bug-5020.php new file mode 100644 index 0000000000..cefd581368 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-5020.php @@ -0,0 +1,52 @@ + $transformer */ + $transformer = 'Bug5020\Transformer'; + $input = ' asdasda asdasd '; + $error = false; + $output = $transformer::Transform($input, $error); + assertType('string', $output); + assertType('bool', $error); +} From 6a0954c93cc9fdb817c46c3e6a09f2cd9d5557ab Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 19 Feb 2026 19:39:52 +0100 Subject: [PATCH 02/12] de-duplicate --- src/Analyser/NodeScopeResolver.php | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index b83aff649c..c8170d4bfa 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3341,7 +3341,9 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto } $parametersAcceptor = null; + $classType = null; $methodReflection = null; + $methodName = null; if ($expr->name instanceof Expr) { $result = $this->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); @@ -3351,6 +3353,12 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto } elseif ($expr->class instanceof Name) { $classType = $scope->resolveTypeByName($expr->class); $methodName = $expr->name->name; + } elseif ($expr->class instanceof Expr && $expr->name instanceof Identifier) { + $classType = TypeCombinator::removeNull($scope->getType($expr->class))->getObjectTypeOrClassStringObjectType(); + $methodName = $expr->name->name; + } + + if ($classType !== null && $methodName !== null) { if ($classType->hasMethod($methodName)->yes()) { $methodReflection = $classType->getMethod($methodName, $scope); $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( @@ -3426,25 +3434,6 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto foreach ($additionalThrowPoints as $throwPoint) { $throwPoints[] = $throwPoint; } - - if ($expr->name instanceof Identifier && $methodReflection === null) { - $staticMethodCalledOnType = TypeCombinator::removeNull($scope->getType($expr->class))->getObjectTypeOrClassStringObjectType(); - $methodName = $expr->name->name; - if ($staticMethodCalledOnType->hasMethod($methodName)->yes()) { - $methodReflection = $staticMethodCalledOnType->getMethod($methodName, $scope); - $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( - $scope, - $expr->getArgs(), - $methodReflection->getVariants(), - $methodReflection->getNamedArgumentsVariants(), - ); - - $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); - if ($methodThrowPoint !== null) { - $throwPoints[] = $methodThrowPoint; - } - } - } } if ($methodReflection !== null) { From f2e12c1753f55497a5522628015c1e8941f2f76f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 19 Feb 2026 19:44:15 +0100 Subject: [PATCH 03/12] Update NodeScopeResolver.php --- src/Analyser/NodeScopeResolver.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index c8170d4bfa..aefec32501 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3353,7 +3353,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto } elseif ($expr->class instanceof Name) { $classType = $scope->resolveTypeByName($expr->class); $methodName = $expr->name->name; - } elseif ($expr->class instanceof Expr && $expr->name instanceof Identifier) { + } elseif ($expr->class instanceof Expr) { $classType = TypeCombinator::removeNull($scope->getType($expr->class))->getObjectTypeOrClassStringObjectType(); $methodName = $expr->name->name; } From 1282b4edd17769949e76407586f73e32ec67d04d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 19 Feb 2026 19:46:57 +0100 Subject: [PATCH 04/12] Update NodeScopeResolver.php --- src/Analyser/NodeScopeResolver.php | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index aefec32501..7cf790a77a 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3420,22 +3420,6 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto } } - if ($expr->class instanceof Expr) { - $objectClasses = $scope->getType($expr->class)->getObjectClassNames(); - if (count($objectClasses) !== 1) { - $objectClasses = $scope->getType(new New_($expr->class))->getObjectClassNames(); - } - if (count($objectClasses) === 1) { - $objectExprResult = $this->processExprNode($stmt, new StaticCall(new Name($objectClasses[0]), $expr->name, []), $scope, $storage, new NoopNodeCallback(), $context->enterDeep()); - $additionalThrowPoints = $objectExprResult->getThrowPoints(); - } else { - $additionalThrowPoints = [InternalThrowPoint::createImplicit($scope, $expr)]; - } - foreach ($additionalThrowPoints as $throwPoint) { - $throwPoints[] = $throwPoint; - } - } - if ($methodReflection !== null) { $impurePoint = SimpleImpurePoint::createFromVariant($methodReflection, $parametersAcceptor, $scope, $expr->getArgs()); if ($impurePoint !== null) { From e3ba0bdc02d3812368e42b826425c5324406d034 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 19 Feb 2026 20:04:57 +0100 Subject: [PATCH 05/12] Update CallToStaticMethodStatementWithoutSideEffectsRuleTest.php --- .../CallToStaticMethodStatementWithoutSideEffectsRuleTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php index f8088b412f..8980046974 100644 --- a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php @@ -42,6 +42,10 @@ public function testRulePhp7(): void 'Call to static method DateTimeImmutable::createFromFormat() on a separate line has no effect.', 12, ], + [ + 'Call to static method DateTimeImmutable::createFromFormat() on a separate line has no effect.', + 13, + ], [ 'Call to method DateTime::format() on a separate line has no effect.', 23, From 5013543b133311a335b976272ce85b306c37654f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 19 Feb 2026 21:39:00 +0100 Subject: [PATCH 06/12] Revert "Update CallToStaticMethodStatementWithoutSideEffectsRuleTest.php" This reverts commit cf3c807bec3aa4c6f9216668f88df970997f26e0. --- .../CallToStaticMethodStatementWithoutSideEffectsRuleTest.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php index 8980046974..f8088b412f 100644 --- a/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php +++ b/tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php @@ -42,10 +42,6 @@ public function testRulePhp7(): void 'Call to static method DateTimeImmutable::createFromFormat() on a separate line has no effect.', 12, ], - [ - 'Call to static method DateTimeImmutable::createFromFormat() on a separate line has no effect.', - 13, - ], [ 'Call to method DateTime::format() on a separate line has no effect.', 23, From 85ab575d642a74ad4f3a7a64296a75807c69472c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 19 Feb 2026 21:39:04 +0100 Subject: [PATCH 07/12] Revert "Update NodeScopeResolver.php" This reverts commit 48601a3d7c1f8855d2321829413e66bf477b6b50. --- src/Analyser/NodeScopeResolver.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 7cf790a77a..aefec32501 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3420,6 +3420,22 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto } } + if ($expr->class instanceof Expr) { + $objectClasses = $scope->getType($expr->class)->getObjectClassNames(); + if (count($objectClasses) !== 1) { + $objectClasses = $scope->getType(new New_($expr->class))->getObjectClassNames(); + } + if (count($objectClasses) === 1) { + $objectExprResult = $this->processExprNode($stmt, new StaticCall(new Name($objectClasses[0]), $expr->name, []), $scope, $storage, new NoopNodeCallback(), $context->enterDeep()); + $additionalThrowPoints = $objectExprResult->getThrowPoints(); + } else { + $additionalThrowPoints = [InternalThrowPoint::createImplicit($scope, $expr)]; + } + foreach ($additionalThrowPoints as $throwPoint) { + $throwPoints[] = $throwPoint; + } + } + if ($methodReflection !== null) { $impurePoint = SimpleImpurePoint::createFromVariant($methodReflection, $parametersAcceptor, $scope, $expr->getArgs()); if ($impurePoint !== null) { From 79bb9169b0a41d14e5c8f6c7b746d08a56c00c4c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 19 Feb 2026 22:04:02 +0100 Subject: [PATCH 08/12] Update NodeScopeResolver.php --- src/Analyser/NodeScopeResolver.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index aefec32501..7849fa65b3 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3344,6 +3344,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $classType = null; $methodReflection = null; $methodName = null; + $dynamicCall = false; if ($expr->name instanceof Expr) { $result = $this->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); @@ -3356,6 +3357,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto } elseif ($expr->class instanceof Expr) { $classType = TypeCombinator::removeNull($scope->getType($expr->class))->getObjectTypeOrClassStringObjectType(); $methodName = $expr->name->name; + $dynamicCall = true; } if ($classType !== null && $methodName !== null) { @@ -3368,9 +3370,11 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $methodReflection->getNamedArgumentsVariants(), ); - $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); - if ($methodThrowPoint !== null) { - $throwPoints[] = $methodThrowPoint; + if (!$dynamicCall) { + $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); + if ($methodThrowPoint !== null) { + $throwPoints[] = $methodThrowPoint; + } } $declaringClass = $methodReflection->getDeclaringClass(); From 34a1fcf465e7e39ae190446f19723a3864261945 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 19 Feb 2026 22:16:09 +0100 Subject: [PATCH 09/12] Update NodeScopeResolver.php --- src/Analyser/NodeScopeResolver.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 7849fa65b3..b4f20ade84 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3344,7 +3344,6 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $classType = null; $methodReflection = null; $methodName = null; - $dynamicCall = false; if ($expr->name instanceof Expr) { $result = $this->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep()); $hasYield = $hasYield || $result->hasYield(); @@ -3357,7 +3356,6 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto } elseif ($expr->class instanceof Expr) { $classType = TypeCombinator::removeNull($scope->getType($expr->class))->getObjectTypeOrClassStringObjectType(); $methodName = $expr->name->name; - $dynamicCall = true; } if ($classType !== null && $methodName !== null) { @@ -3370,7 +3368,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $methodReflection->getNamedArgumentsVariants(), ); - if (!$dynamicCall) { + if (!$expr->class instanceof Expr) { $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); if ($methodThrowPoint !== null) { $throwPoints[] = $methodThrowPoint; From e7a7968e58ed7c6885ab1fb5372684618cdb4b74 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 19 Feb 2026 22:26:35 +0100 Subject: [PATCH 10/12] Update static-method-call-statement-no-side-effects.php --- .../data/static-method-call-statement-no-side-effects.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/PHPStan/Rules/Methods/data/static-method-call-statement-no-side-effects.php b/tests/PHPStan/Rules/Methods/data/static-method-call-statement-no-side-effects.php index 9c4e984e42..8a21b6f47a 100644 --- a/tests/PHPStan/Rules/Methods/data/static-method-call-statement-no-side-effects.php +++ b/tests/PHPStan/Rules/Methods/data/static-method-call-statement-no-side-effects.php @@ -10,7 +10,7 @@ class Foo public function doFoo(\DateTimeImmutable $dt) { DateTimeImmutable::createFromFormat('Y-m-d', '2019-07-24'); - $dt::createFromFormat('Y-m-d', '2019-07-24'); + $dt::createFromFormat('Y-m-d', '2019-07-24'); // method might be impure in DateTimeImmutable subclass } } From f2a906419adcd6fcce0ec8e268a0c715bb76b3e2 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 19 Feb 2026 23:00:35 +0100 Subject: [PATCH 11/12] lsb --- src/Analyser/MutatingScope.php | 12 ++++++++++++ src/Analyser/NodeScopeResolver.php | 10 ++++------ 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 53bbcfc76c..8d954b50a2 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2044,6 +2044,18 @@ public function resolveTypeByName(Name $name): TypeWithClassName return new ObjectType($originalClass); } + public function resolveStaticCallWithLateStaticBinding(Expr\StaticCall $expr): ?TypeWithClassName + { + $objectClasses = TypeCombinator::removeNull($this->getType($expr->class))->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); + if (count($objectClasses) !== 1) { + return null; + } + if (!$expr->name instanceof Identifier) { + return null; + } + return $this->resolveTypeByNameWithLateStaticBinding(new Name($objectClasses[0]), $expr->name); + } + private function resolveTypeByNameWithLateStaticBinding(Name $class, Node\Identifier $name): TypeWithClassName { $classType = $this->resolveTypeByName($class); diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index b4f20ade84..585eef6027 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3354,7 +3354,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $classType = $scope->resolveTypeByName($expr->class); $methodName = $expr->name->name; } elseif ($expr->class instanceof Expr) { - $classType = TypeCombinator::removeNull($scope->getType($expr->class))->getObjectTypeOrClassStringObjectType(); + $classType = $scope->resolveStaticCallWithLateStaticBinding($expr); $methodName = $expr->name->name; } @@ -3368,11 +3368,9 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $methodReflection->getNamedArgumentsVariants(), ); - if (!$expr->class instanceof Expr) { - $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); - if ($methodThrowPoint !== null) { - $throwPoints[] = $methodThrowPoint; - } + $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope); + if ($methodThrowPoint !== null) { + $throwPoints[] = $methodThrowPoint; } $declaringClass = $methodReflection->getDeclaringClass(); From 48006e7bb3dd089946378c58f15240f835aca01d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 19 Feb 2026 23:08:24 +0100 Subject: [PATCH 12/12] simplify --- src/Analyser/MutatingScope.php | 14 +------------- src/Analyser/NodeScopeResolver.php | 7 +++++-- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 8d954b50a2..206869a6a3 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2044,19 +2044,7 @@ public function resolveTypeByName(Name $name): TypeWithClassName return new ObjectType($originalClass); } - public function resolveStaticCallWithLateStaticBinding(Expr\StaticCall $expr): ?TypeWithClassName - { - $objectClasses = TypeCombinator::removeNull($this->getType($expr->class))->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); - if (count($objectClasses) !== 1) { - return null; - } - if (!$expr->name instanceof Identifier) { - return null; - } - return $this->resolveTypeByNameWithLateStaticBinding(new Name($objectClasses[0]), $expr->name); - } - - private function resolveTypeByNameWithLateStaticBinding(Name $class, Node\Identifier $name): TypeWithClassName + public function resolveTypeByNameWithLateStaticBinding(Name $class, Node\Identifier $name): TypeWithClassName { $classType = $this->resolveTypeByName($class); diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 585eef6027..2cc8922ca6 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3354,8 +3354,11 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto $classType = $scope->resolveTypeByName($expr->class); $methodName = $expr->name->name; } elseif ($expr->class instanceof Expr) { - $classType = $scope->resolveStaticCallWithLateStaticBinding($expr); - $methodName = $expr->name->name; + $objectClasses = TypeCombinator::removeNull($scope->getType($expr->class))->getObjectTypeOrClassStringObjectType()->getObjectClassNames(); + if (count($objectClasses) === 1) { + $classType = $scope->resolveTypeByNameWithLateStaticBinding(new Name($objectClasses[0]), $expr->name); + $methodName = $expr->name->name; + } } if ($classType !== null && $methodName !== null) {