Skip to content

Resolve method reflection for dynamic static calls ($var::method()) to enable purity and side-effect checking#5572

Merged
VincentLanglet merged 5 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-nw3wc3z
May 3, 2026
Merged

Resolve method reflection for dynamic static calls ($var::method()) to enable purity and side-effect checking#5572
VincentLanglet merged 5 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-nw3wc3z

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

@phpstan-bot phpstan-bot commented Apr 29, 2026

Summary

When calling a static method on a class-string or object variable (e.g. $enum::from('foo') where $enum is enum-string<BackedEnum>), PHPStan previously failed to resolve the method reflection, treating the call as "call to unknown method" with a possibly-impure point. This caused false positives inside @phpstan-pure functions/methods.

Changes

  • src/Analyser/ExprHandler/StaticCallHandler.php:

    • Added an elseif ($expr->class instanceof Expr) branch in processExpr() that resolves the method reflection using getObjectTypeOrClassStringObjectType() — matching the existing approach in resolveType().
    • Added $expr->class instanceof Name guards to the $this-invalidation block (constructor side effects) and the promoted-property initialization block, preventing incorrect scope mutations when calling constructors on other objects via expressions (e.g. $other::__construct()).
  • tests/PHPStan/Rules/Pure/PureMethodRuleTest.php: Added testBug14557 covering enum-string<T>::from(), enum-string<T>::tryFrom(), class-string<T>::from(), class-string<T>::tryFrom(), pure static methods via class-string, and impure static methods via class-string.

  • tests/PHPStan/Rules/Pure/PureFunctionRuleTest.php: Added testBug14557 covering the same patterns in pure functions.

  • tests/PHPStan/Rules/Methods/CallToStaticMethodStatementWithoutSideEffectsRuleTest.php: Updated testDynamicStaticCall expectations — pure dynamic static calls ($foo::pureMethod()) are now correctly detected as having no side effects, which is a natural improvement from resolving the method reflection.

Analogous cases probed

  • class-string<T> (not just enum-string<T>): Both are handled by getObjectTypeOrClassStringObjectType() — tested with class-string<SomeClass> calling pure and impure static methods.
  • tryFrom() alongside from(): Both tested.
  • Pure functions (not just methods): Covered via PureFunctionRuleTest.
  • Impure methods via class-string: Tested to confirm they are still correctly flagged.
  • Constructor calls on other objects ($other::__construct()): Verified that the $this-invalidation and property-initialization guards prevent incorrect behavior (the MissingReadOnlyPropertyAssignRuleTest::testRedeclaredReadonlyProperties test).
  • Dead code detection (CallToStaticMethodStatementWithoutSideEffectsRule): Now correctly detects pure dynamic static calls as having no effect.

Root cause

StaticCallHandler::processExpr() only resolved method reflection when the class part was a Name node (direct class reference like Foo::method()). When the class part was an Expr node (variable like $enum::method()), $methodReflection stayed null, leading to a fallback "call to unknown method" impure point. The resolveType() method already handled this case correctly via getObjectTypeOrClassStringObjectType(), but the purity/side-effect logic in processExpr() did not.

Test

  • tests/PHPStan/Rules/Pure/data/bug-14557.php — method-level test with enum-string, class-string, pure and impure static methods
  • tests/PHPStan/Rules/Pure/data/bug-14557-function.php — function-level test with enum-string and class-string
  • Updated expectations in CallToStaticMethodStatementWithoutSideEffectsRuleTest::testDynamicStaticCall for the improved detection

Fixes phpstan/phpstan#14557
Fixes phpstan/phpstan#5020

@VincentLanglet
Copy link
Copy Markdown
Contributor

@staabm You're the one who wrote that no error should be reported on line 32

'Call to static method DynamicStaticCall\Foo::doFoo() on a separate line has no effect.'

with the comment // no error, subclass could override static method with impure impl

But still we consider $foo->pureMethod() to be pure while something like
https://phpstan.org/r/e325d706-4fc2-46bd-89f0-f2c3890c9c44
could be written.

Do you agree that

  • We should forbid overriding a pure method in an impure way
  • We should report @phpstan-impure method which override pure methods.

@staabm
Copy link
Copy Markdown
Contributor

staabm commented Apr 30, 2026

Do you agree that

* We should forbid overriding a pure method in an impure way

* We should report `@phpstan-impure`  method which override pure methods.

I am not sure we can/should enforce purity across inheritance.
a less aggressive approach would be taking purity only from final classes.

feels like a topic which needs to be discussed with @ondrejmirtes before we implement somthing

@VincentLanglet
Copy link
Copy Markdown
Contributor

I am not sure we can/should enforce purity across inheritance.

It's already allowed to add @pure over an interface method

a less aggressive approach would be taking purity only from final classes.

That will change a lot of existing behavior.
Maybe it will require a new config parameter

feels like a topic which needs to be discussed with @ondrejmirtes before we implement somthing

Indeed

@VincentLanglet
Copy link
Copy Markdown
Contributor

feels like a topic which needs to be discussed with @ondrejmirtes before we implement somthing

Seems decided by this answer phpstan/phpstan#14563 (comment)

So IMHO this PR is ready.

Copy link
Copy Markdown
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add non regression test for phpstan/phpstan#5020

It should be a RuleTest which check that there is no If condition is always false. error anymore with the snippet https://phpstan.org/r/ada88746-fd60-4187-abfc-046ed28bf750

@VincentLanglet VincentLanglet self-assigned this May 2, 2026
@phpstan-bot
Copy link
Copy Markdown
Collaborator Author

Done. Added a non-regression test for issue #5020 that verifies PHPStan no longer reports "If condition is always false" when a dynamic static call ($transformer::Transform(...)) modifies a by-reference parameter. The test was verified to fail without the fix (reporting the false positive on line 30) and pass with it. All tests pass and make phpstan reports no errors.

@VincentLanglet VincentLanglet requested a review from staabm May 2, 2026 18:54
* @param class-string<MyEnum> $enum
* @phpstan-pure
*/
function tryFromClassString(string $enum): ?MyEnum
Copy link
Copy Markdown
Contributor

@staabm staabm May 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add another test for

/**
 * @phpstan-pure
 */
function fromEnum(MyEnum $enum): MyEnum
{
	return $enum::from('foo');
}

which unexpectedly errors before this PR
https://phpstan.org/r/1dccccac-c269-4df9-9cb1-d7715a501d5b

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added the test case staabm requested — a pure function/method calling $enum::from('foo') where $enum is typed as MyEnum (an object instance, not a class-string or enum-string). Added to both the function-level and method-level test files, updated the error line expectation, and all tests + make phpstan pass.

phpstan-bot and others added 5 commits May 2, 2026 21:18
… to enable purity and side-effect checking

- In `StaticCallHandler::processExpr()`, when the class part of a static call
  is an expression (e.g. `$enum::from()`), resolve the method reflection via
  `getObjectTypeOrClassStringObjectType()` — the same approach already used in
  `resolveType()` for return type computation.
- Previously, `$methodReflection` was always null for `$expr->class instanceof Expr`,
  causing a false "call to unknown method" impure point for every dynamic static call.
- Guard `$this`-invalidation and promoted-property initialization logic with
  `$expr->class instanceof Name` to prevent incorrect scope effects when the call
  target is an expression (e.g. `$other::__construct()`).
- Update `CallToStaticMethodStatementWithoutSideEffectsRuleTest::testDynamicStaticCall`
  expectations: pure dynamic static calls are now correctly detected as having no effect.
…ameter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@staabm staabm force-pushed the create-pull-request/patch-nw3wc3z branch from f914a4c to 998d101 Compare May 2, 2026 19:18
@VincentLanglet VincentLanglet merged commit 166f460 into phpstan:2.1.x May 3, 2026
655 of 658 checks passed
@VincentLanglet VincentLanglet deleted the create-pull-request/patch-nw3wc3z branch May 3, 2026 10:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants