Implement @pure-unless-parameter-passed#6018
Conversation
| continue; | ||
| } | ||
|
|
||
| if (!$hasNamedParameter && $i === $parameterIndex) { |
There was a problem hiding this comment.
Missing named arg test. Did we also miss it in the @pure-unless-callable-is-impure PR?
|
|
||
| namespace PureUnlessParameterPassedBuiltin; | ||
|
|
||
| /** |
There was a problem hiding this comment.
Feels like we are missing most tests, which the @pure-unless-callable-is-impure PR contained?
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.
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. |
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