diff --git a/CLAUDE.md b/CLAUDE.md index e33c66a1c2..58cbb779ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -256,6 +256,12 @@ When a method with side effects is called, `invalidateExpression()` invalidates `NodeScopeResolver::processArgs()` has special handling for `Closure::bind()` and `Closure::bindTo()` calls. When the first argument is a closure/arrow function literal, a `$closureBindScope` is created with `$this` rebound to the second argument's type, and this scope is used to process the closure body. However, this `$closureBindScope` must ONLY be applied when the first argument is actually an `Expr\Closure` or `Expr\ArrowFunction`. If the first argument is a general expression that returns a closure (e.g. `$this->hydrate()`), the expression itself must be evaluated in the original scope — otherwise `$this` in the expression gets incorrectly resolved as the bound object type instead of the current class. The condition at the `$scopeToPass` assignment must check the argument node type. +### Variable static calls ($var::method()) and method reflection resolution + +When `NodeScopeResolver` processes `StaticCall` with `$expr->class instanceof Expr` (variable class like `$var::method()`), the code at ~line 3303 only resolves `$methodReflection` and `$parametersAcceptor` for `$expr->class instanceof Name` calls. For Expr-based class references, the method reflection was previously left as `null`, causing by-reference parameter types and other parameter-dependent analysis to be skipped. The fix resolves the class type via `getObjectTypeOrClassStringObjectType()` and looks up the method when the class can be determined from the expression type (ConstantStringType, GenericClassStringType, or ObjectType). + +When adding method reflection for Expr-based static calls, the `$this` invalidation and constructor property initialization blocks (~lines 3427-3460) must be guarded with `$expr->class instanceof Name`. These blocks handle `self::__construct()`, `parent::__construct()`, and similar Name-based calls that affect `$this`. Without the guard, `$other::__construct()` (calling on a different object) would incorrectly trigger `$this` property initialization and invalidation. + ### Array type tracking: SetExistingOffsetValueTypeExpr vs SetOffsetValueTypeExpr When assigning to an array offset, NodeScopeResolver must distinguish: diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index 8a807ea745..0136e65f32 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -3378,6 +3378,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) { @@ -3407,6 +3426,7 @@ function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $sto if ( $methodReflection !== null + && $expr->class instanceof Name && ( ( !$methodReflection->isStatic() @@ -3422,6 +3442,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); +} diff --git a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php index 3232e63282..511bf6136d 100644 --- a/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php +++ b/tests/PHPStan/Rules/Classes/InstantiationRuleTest.php @@ -484,7 +484,7 @@ public function testBug10248(): void $this->analyse([__DIR__ . '/data/bug-10248.php'], []); } - #[RequiresPhp('>= 8.0')] + #[RequiresPhp('>= 8.2')] public function testBug11815(): void { $this->analyse([__DIR__ . '/data/bug-11815.php'], []);