Implement @pure-unless-parameter-passed#6018
Conversation
a4f91d3 to
52e31bd
Compare
|
Rebased onto latest 2.2.x. While reworking the tests I found and fixed a real asymmetry in Current CI status — the remaining red checks are all unrelated to this PR's code:
|
|
This pull request has been marked as ready for review. |
|
would it make sense to emit a new error when |
52e31bd to
4902e4b
Compare
|
This pull request has been marked as ready for review. |
|
Update after the latest review round:
Mutation status: 8.4 is green (0 escaped). 8.3 still reports the two |
|
This pull request has been marked as ready for review. |
915ecf0 to
0b1a724
Compare
Resolve the tag into per-parameter flags and merge them across parent PHPDocs. Until phpstan/phpdoc-parser#259 is merged the parser does not understand the tag natively, so parse it from the generic tag value and whitelist the @phpstan- alias in InvalidPHPStanDocTagRule.
Add isPureUnlessParameterPassedParameter(): TrinaryLogic on parameter reflections, populated from PHPDoc and function metadata, mirroring the already-merged @pure-unless-callable-is-impure threading (same TrinaryLogic typing from the start, so combineAcceptors() merges it with equals-or-Maybe instead of ->or(), giving correct behavior on union method variants).
Thread the raw parameter flags through enterClassMethod()/enterFunction() and getPhpDocs(), and consult them in SimpleImpurePoint::resolvePureUnlessParameterPassedVerdict(): a call to a function flagged with @pure-unless-parameter-passed stays pure as long as none of those by-ref parameters received an argument.
str_replace, str_ireplace, preg_replace, preg_match, preg_match_all and similar_text are pure unless their optional by-ref out parameter (count / matches / percent) receives an argument. Add the pureUnlessParameterPassedParameters metadata shape to the generator, the metadata schema test, and the metadata files.
…tins str_replace/str_ireplace's count and preg_match/preg_match_all's matches resolve to different parameter names (replace_count/subpatterns) when the target PHP version is below 8.0: NativeFunctionReflectionProvider falls back from php-8-stubs to the legacy functionMap.php names in that case. List both names in the metadata so the by-ref-omitted suppression works regardless of the analysed PHP version. Found via the old-PHPUnit (7.4) CI job, which runs the test suite against phpVersion 7.4 by default.
…d named arguments NewHandler now applies the parameter-passed verdict to 'new' the same way the sibling callable verdict is applied (constructors return void, so createFromVariant cannot be reused). The verdict also treats argument unpacking conservatively (an unpacked argument might cover the flagged by-ref parameter, so the call stays possibly impure) and respects the flag's own TrinaryLogic certainty from union-variant composition (an uncertain flag downgrades a passed argument to Maybe instead of No).
phpstan/phpdoc-parser#259 is merged and released, so replace the generic-tag stopgap with PhpDocNode::getPureUnlessParameterIsPassedTagValues() and bump the dependency.
… is passed createFromVariant suppressed the impure point when the flagged parameter was omitted, but never promoted the verdict to certain when it was passed, unlike its @pure-unless-callable-is-impure sibling right above it and unlike NewHandler's own constructor handling. Passing the by-ref out-parameter is a definite side effect, not a possible one. Also covers intersection types, method inheritance (incl. a renamed parameter), and first-class callables.
InvalidPHPStanDocTagRule matches @phpstan-* tags by name against a hardcoded list, so native phpdoc-parser recognition does not exempt them. @phpstan-pure-unless-callable-is-impure was never added to that list and is currently reported as an unknown tag; add it alongside @phpstan-pure-unless-parameter-passed and drop the stale TODO.
A non-optional parameter is always passed, so the tag can never keep the function or method pure. Report it as a misuse, mirroring the existing @pure-unless-callable-is-impure redundancy check. The declaration-side flag was dropped for functions: enterFunction() was never given the @pure-unless-parameter-passed parameters (the method path already passed them), so FunctionPurityCheck could not see it. Thread it through as well.
FunctionPurityCheck read the per-parameter TrinaryLogic flag via ->yes() to find the flagged parameters, which is an equivalent mutation at a declaration site (the flag is only ever Yes or No there, never Maybe). Mirror the @pure-unless-callable-is-impure sibling instead: expose the flagged parameters as a function/method-level array<string, TrinaryLogic> getter and look them up with array_key_exists.
Cover a function whose flagged by-ref parameter is followed by a trailing variadic: omitting the parameter stays pure, passing it is impure, and the extra variadic arguments do not affect the flagged parameter.
preg_filter() has the same signature as the already-covered preg_replace() (a pure regex substitution with an optional by-ref $count out-parameter), so it stays pure unless $count is passed.
A function can carry both @pure-unless-callable-is-impure and @pure-unless-parameter-passed at once - preg_replace_callback() is pure unless its callback is impure or its $count is passed. The two verdicts were checked in sequence, so a pure callback short-circuited to "pure" and never looked at $count. Combine them with TrinaryLogic::and() in both SimpleImpurePoint::createFromVariant() and NewHandler, and flag preg_replace_callback()'s $count.
Co-authored-by: Markus Staab <maggus.staab@googlemail.com>
c8d7c5c to
cc28625
Compare
Implements the
@pure-unless-parameter-passedPHPDoc tag: a parameter-level annotation declaring that the function/method is pure unless an argument is passed for that (by-ref out) parameter. Built on top of2.2.x, following the same architecture as@pure-unless-callable-is-impure(#3482) — the parameter-level flag isTrinaryLogicfrom the start so it composes correctly on union/intersection method variants.Background
This closes @staabm's phpstan/phpstan#11884, where he pointed out that
str_replace()(andstr_ireplace/preg_replace) could be treated as pure when the by-ref$countargument is omitted, and asked for "a solution independent of concrete function signatures" rather than one-off metadata flags. ondrejmirtes suggested the@phpstan-pure-unless-parameter-passedannotation name in response. @staabm then implemented the parser-side syntax support at phpstan/phpdoc-parser#259, which is still open — ondrejmirtes asked there for the phpstan-src implementation to be ready first before merging the parser side. This PR is that implementation.Parser support
Since phpstan/phpdoc-parser#259 is not merged yet, this PR parses the tag from the generic tag value as a stopgap (
PhpDocNodeResolver::resolveParamPureUnlessParameterPassed(), marked with aTODOto switch toPhpDocNode::getPureUnlessParameterIsPassedTagValues()once the parser PR merges).How it works
@phpstan-prefixed alias) is resolved into per-parameter flags, threaded through the reflection layer (ExtendedParameterReflection::isPureUnlessParameterPassedParameter(): TrinaryLogic), and populated for builtins viafunctionMetadata(str_replace,str_ireplace,preg_replace,preg_match,preg_match_all,similar_text).SimpleImpurePoint::resolvePureUnlessParameterPassedVerdict()checks whether an argument was actually passed for each flagged parameter:null) → the call stays pure;combineAcceptors()merges the flag across union method variants with equals-or-Maybe, so a parameter tagged in some members but not others correctly becomesMayberather than being silently dropped.str_replace,str_ireplace,preg_replace,preg_match,preg_match_all,similar_text)refs #3482, phpstan/phpdoc-parser#259
Closes phpstan/phpstan#11884