Implement @pure-unless-callable-is-impure#3482
Conversation
There was a problem hiding this comment.
Will be replaced by phpstan/phpdoc-parser#253
There was a problem hiding this comment.
WIP. = false will be removed.
|
Just updated phpdoc-parser here so you can use that. |
eddd22a to
f207c72
Compare
f207c72 to
8c756cc
Compare
|
@zonuexe any plans to move this forward? |
|
@staabm Hi, I know this branch is important but it was blocked due to my motivation. I'm going to rebase it on 2.0.x and do the necessary work. If I miss it tonight, I won't be able to start the rest of the work until next week, so it would be quicker for you to take over then. |
|
I don't want to put pressure on you. It was not clear to me whether you stopped working on it or not. I am totally fine if this takes a bit more time - thank you |
8c756cc to
389c7f0
Compare
|
No problem, this is a very interesting subject for me too, so I'm happy to work on it because I realized that there are other people who are interested in it. More importantly, I'm always helped by your hard work. |
2146ae0 to
1cc1fad
Compare
|
Friendly ping @zonuexe, still interested in this PR ? Do you have time to finish it ? |
|
@VincentLanglet |
adb02ca to
abc8cd8
Compare
abc8cd8 to
c4999be
Compare
|
Friendly ping @zonuexe I don't want to put pressure on you. It's just not clear to me whether you stopped working on it or not. It will unblock the phpstan/phpdoc-parser#259 pr :) |
|
Maybe I'm wrong, but I start to think this annotation is useless PHPStan already handle cb with immediately/later invoked callable, even for a pure function/method. So we just need to declare array_map and so as pure. |
|
i'm happy to help contribute towards a solution if it's decided which direction is best. |
|
Hi @devbanana feel free to work on this. Laybe @zonuexe could explain what he had in mind ; or @ondrejmirtes will have some guidelines |
| */ | ||
| public function checkAllowedConstants(array $constants): AllowedConstantsResult; | ||
|
|
||
| public function isPureUnlessCallableIsImpureParameter(): bool; |
There was a problem hiding this comment.
should return trinary.
quoting ondrey:
I feel like on ExtendedParametersAcceptor, the method isPureUnlessCallableIsImpureParameter should return TrinaryLogic.
Because you need to correctly set it on unions and intersection methods
| * @param callable(int): int $f | ||
| * @param array<int> $arr | ||
| * @return array<int> | ||
| * @pure-unless-callable-is-impure $f |
There was a problem hiding this comment.
we need a test which shows that a function which is declared @pure-unless-callable-is-impure $f errors about impure side-effects when the impure statement/expression is outside the callable $f
| * @param callable(int): int $f | ||
| * @param array<int> $arr | ||
| * @return array<int> | ||
| * @phpstan-pure-unless-callable-is-impure $f |
There was a problem hiding this comment.
If @pure-unless-callable-is-impure is paired with pure-callable or pure-Closure , that should be a new kind of error (that the PHPdoc tag could be replaced with @phpstan-pure essentially
Per review: the parameter-level flag must be a TrinaryLogic so it composes correctly on union and intersection method variants (combineAcceptors uses ->or(), mirroring isImmediatelyInvokedCallable). Origins convert the bool from PHPDoc/metadata with TrinaryLogic::createFromBoolean(); the consumption in SimpleImpurePoint skips only parameters whose flag is ->no().
A function/method declared @pure-unless-callable-is-impure $f is pure except for the flagged callable, so its body is now checked by FunctionPurityCheck: impure statements outside the callable (e.g. echo) are reported, while the flagged callable's own invocation is exempt. Thread the parameter flags through enterFunction() so the check sees them on plain functions too.
Per review: when @pure-unless-callable-is-impure $f is paired with a pure callable type (pure-callable / pure-Closure) for $f, the tag is redundant because the callback is always pure, so the function/method can be marked @phpstan-pure instead. FunctionPurityCheck now reports this.
ondrejmirtes
left a comment
There was a problem hiding this comment.
One more thing - in MethodSignatureRule, we need to enforce covariance for these methods. Check around $this->reportMethodPurityOverride.
If a method has @phpstan-pure or @phpstan-pure-unless-callable-is-impure, the child method cannot be @phpstan-impure.
Also are there same tests around @phpstan-pure-unless-callable-is-impure being inherited implicitly? (The child method does not need to mention the parameter but if parent method has it, it should also apply to child. Even when the parameter is renamed.)
| public function isPure(): TrinaryLogic; | ||
|
|
||
| /** | ||
| * @return array<string, bool> |
There was a problem hiding this comment.
Should still be TrinaryLogic here. When you have UnionMethodReflection or IntersectionMethodReflection (and the methods in question have different PHPDoc tags involved, you need TrinaryLogic to properly model the behaviour). Also add some tests.
Per review (ondrejmirtes): the method/function-level map must return array<string, TrinaryLogic> so UnionTypeMethodReflection and IntersectionTypeMethodReflection can model Maybe when constituent methods disagree on the tag (new MergedPureUnlessCallableIsImpureParameters helper). combineAcceptors() now merges the parameter-level flag with equals-or-Maybe instead of ->or(), so a union receiver yields a Maybe flag. FunctionPurityCheck drives its flagged-parameter set off the map getter. Adds union and inherited (incl. renamed-parameter) regression tests.
Per review (ondrejmirtes): MethodSignatureRule now enforces purity covariance for the tag - a child method marked @phpstan-impure cannot override a parent method carrying @pure-unless-callable-is-impure (identifier method.impureOverridePureUnlessCallable), mirroring the existing pure-override check. Gated by reportMethodPurityOverride.
|
@ondrejmirtes @staabm Both review points are addressed. 1. TrinaryLogic on the map getter. 2. Purity covariance. Note: the Mutation Testing job is still red on a few mutants around the verdict/override branches. The tests seem to kill them locally, so I will leave it to you two to decide how to proceed. |
|
This pull request has been marked as ready for review. |
|
Might be valuable to fix the escaped mutant. It means that either the code logic needs to change, or a test has to be added to fail when the code is changed this way. |
|
@ondrejmirtes @staabm |
|
@zonuexe I don't think you need to set up Infection locally to verify this mutant. The question is: if you modify the file: Do you get a failing test locally or not? If no tests fail the mutant is valid and then it's easy to fix the code/add the test. |
Kills the TrinaryLogicMutator mutant on the parameter-flag check (->no() -> !->yes()) deterministically, without going through the analyser: a Maybe flag must be processed like Yes, so with the callback omitted the verdict is Yes, while the mutant skips the parameter and yields null.
Extend plain TestCase and pass a mocked Scope instead of booting the DI container and creating a real analyser Scope. The verdict method never consults the scope when no arguments are passed, so this keeps the test fully isolated (avoids interfering with other tests' shared state) while still killing the parameter-flag mutant.
Extract the verdict->yes()/->no() branching from createFromVariant() and NewHandler into SimpleImpurePoint::applyPureUnlessCallableIsImpureVerdict(), covered by a direct unit test. This removes the duplicated logic and lets the unit test deterministically kill the mutants on those branches instead of relying on coverage attribution from the rule tests.
Reverts 33f825a, f0cf3d4 and 4859c2f. Local Infection with the exact custom TrinaryLogicMutator kills every mutant on the changed lines (MSI 100%), so the remaining CI Mutation Testing failure is a coverage-attribution issue in the parallel run rather than a missing test - the added unit test and the verdict-to-certainty helper did not change that, so they are dropped to keep the PR focused on the reviewed changes.
|
I ran the exact custom Using the same mutator with In CI the escaped mutant is a moving target: across the last three runs it was I briefly tried a direct unit test plus factoring the verdict branching into one helper, but since that does not change the CI attribution I reverted it to keep the PR focused on the reviewed changes. |
|
Thank you @zonuexe |
|
I am always helped by the great work of the PHPStan team! |
|
Yeah, thank you! And it's documented already. I love these times! |
|
As a followup it would be great to look into phpstan 1st party extension packages and also often used packages in the php ecosystem, which could utilize the new phpdoc |
Implements the
@pure-unless-callable-is-impurePHPDoc tag (parser support: phpstan/phpdoc-parser#253, merged): a parameter-level annotation declaring that the function/method is pure unless the callable passed to that parameter is impure. Rebuilt on top of2.2.x.How it works
@phpstan-prefixed alias) is resolved into per-parameter flags, threaded through the reflection layer (ExtendedParameterReflection::isPureUnlessCallableIsImpureParameter()), and populated for builtins viafunctionMetadata(array_filter,array_map,array_reduce, thearray_*u*comparator family,call_user_func(_array),forward_static_call(_array),preg_replace_callback(_array), …) and forarray_reducevia its stub.SimpleImpurePoint::createFromVariant()evaluates the purity of arguments passed to flagged parameters:null) → no impure point — the call is treated as pure;callabletype) → unchanged possibly-impure behavior.NoopExpressionNode) is driven by "the statement has zero impure points", standalonearray_map($pureCb, $arr);statements are now reported byCallToFunctionStatementWithoutSideEffectsRulewith no rule changes.PureFunctionRule/PureMethodRuleregressions,CallToFunctionStatementWithoutSideEffectsRulecases incl. impure/opaque/by-ref non-regressions)refs #3106, #5912
Closes phpstan/phpstan#11101
Closes phpstan/phpstan#11100