Skip to content

Apply class purity tags to annotated methods - #6656

Open
truongbo17 wants to merge 4 commits into
phpstan:2.3.xfrom
truongbo17:fix/pure-annotated-methods
Open

truongbo17 wants to merge 4 commits into
phpstan:2.3.xfrom
truongbo17:fix/pure-annotated-methods

Conversation

@truongbo17

Copy link
Copy Markdown

Summary

@method declarations currently have unknown purity even when their class is marked @phpstan-all-methods-pure or @phpstan-all-methods-impure. This makes a call to a pure annotated method report a possibly impure call from pure code.

Propagate the class-level purity tag when creating an annotation method reflection and use it for both isPure() and hasSideEffects(). Void-returning methods keep their existing side-effect behavior, matching native methods.

Closes phpstan/phpstan#15322

Tests

  • Added a failing regression test for the reflection and a rule-level reproduction of the reported behavior.
  • php vendor/bin/phpunit tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php
  • php vendor/bin/phpunit tests/PHPStan/Rules/Pure/PureMethodRuleTest.php
  • php -d memory_limit=450M bin/phpstan analyse src/Reflection/Annotations/AnnotationMethodReflection.php src/Reflection/Annotations/AnnotationsMethodsClassReflectionExtension.php tests/PHPStan/Reflection/Annotations/AnnotationsMethodsClassReflectionExtensionTest.php tests/PHPStan/Rules/Pure/PureMethodRuleTest.php --no-progress --debug
  • php vendor/bin/parallel-lint on the six changed PHP files

@truongbo17
truongbo17 force-pushed the fix/pure-annotated-methods branch from 4dfeee8 to 6b7a389 Compare October 2, 2026 09:36
VincentLanglet
VincentLanglet previously approved these changes Oct 2, 2026
@VincentLanglet
VincentLanglet requested a review from staabm October 2, 2026 09:40
Comment on lines +152 to +153
* @phpstan-all-methods-pure
* @method int magic()

@staabm staabm Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

my gut feeling is, that magic behaviour and purity kind of contradict each other.

phpstan cannot give guarantes for a method it will not see/know and I think people will shoot into their own foot by doing impure stuff in such magic context without seeing it

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Now you point it out, it's indeed annoying that

  • PHPStan won't report impurity in magic
  • People cannot annotate a magic method as impure even when all are annotated as pure (which is doable with a well defined method).

@VincentLanglet VincentLanglet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With your change, do we still report the following example
https://phpstan.org/r/fb9d4b25-5cbf-4335-b566-11fdc26a1f0d

?

@truongbo17

Copy link
Copy Markdown
Author

Thanks for the counterexample. The previous revision suppressed that diagnostic. I updated the annotated method reflection so a native __call() or __callStatic() known to be impure takes precedence over the class-wide pure tag. Running your linked example on the updated branch now reports Impure call to method Foo::magicFoo() in pure method Bar::magicBar(). at line 36.

The regression test covers both instance and static dispatchers, while the original issue case (no impure dispatcher) remains pure. The focused purity and annotation tests pass locally.

Comment on lines +78 to +86
$isPure = null;
if ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsPure()) {
$isPure = true;
} elseif ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsImpure()) {
$isPure = false;
}
if ($isPure === true && $nativeCallMethod !== null && $nativeCallMethod->isPure()->no()) {
$isPure = false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I'm not sure about the order of those condition.

As soon as nativeCallMethod is not null shouldn't we use the purity from it ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Agreed. I changed the precedence in 70fe6a2: when a native __call() or __callStatic() exists, the annotated method now takes its yes/no/maybe purity from that dispatcher. The class-wide tag is used only when there is no native dispatcher. I added a regression case where an explicitly pure __call() overrides @phpstan-all-methods-impure, alongside the impure dispatcher cases. The focused tests and PHPStan analysis pass locally.

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.

@phpstan-all-methods-pure is ignored for methods declared by @method

3 participants