Repository navigation
Check only the right side of ??= as the value written to its target - #6860
Open
ChrisAbner wants to merge 1 commit into
Open
ChrisAbner wants to merge 1 commit into
ChrisAbner wants to merge 1 commit into
Conversation
`$x ??= $v` evaluates to `$x ?? $v`, but only ever writes `$v`. The nodes rules use to check the write (PropertyAssignNode, VariableAssignNode and the innermost value of the Set*OffsetValueTypeExpr chain) now carry `$v` when the target is the left side of the `??=`, while the scope update keeps `$x ?? $v`. The write is also conditional, so a by-ref parameter keeps its original-value marker with Maybe certainty after `$p ??= ...` when the left side may be non-null, and ParameterOutExecutionEndTypeRule still checks it at exit. Ported to the turbo-ext mirrors of AssignHandler and AssignOpHandler.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes phpstan/phpstan#15228
AssignOpHandlerresolves$x ??= $vas$x ?? $v. That is right for the value of the expression and for the type$xhas afterwards, butAssignHandlerhanded the same expression to the nodes that rules use to check what gets written:PropertyAssignNode,VariableAssignNodeand the innermost value of theSet*OffsetValueTypeExprchain. So every rule sawnonNull($x)|$vinstead of$v, the only thing??=ever writes. That caused the false negative and false positive in the issue (TypesAssignedToPropertiesRule), and the same bug in static properties, offset writes ($this->m['k'] ??= ...),ParameterOutAssignedTypeRuleandTooWidePropertyTypeRule.AssignHandler::getWrittenExpr()now passes$vto those nodes when the target is the left side of the??=itself, so$y = ($x ??= $v)is unchanged, and the scope update still uses$x ?? $v.That alone would hide one bug that the whole-expression type had been catching by accident. With
@param-out stringonint|string|null &$p,$p ??= 'x'leaves anintin$p.ParameterOutExecutionEndTypeRuleis the rule that should report it, but it skips a parameter onceParameterVariableOriginalValueExpris gone, and the write removed it as if it always happened.AssignOpHandlernow keeps it withMaybecertainty when the left side may be non-null, which is what merging the two branches of the equivalentifgives.I first fixed only
TypesAssignedToPropertiesRule, but the same mix-up showed up in three more consumers, so the fix belongs where the written expression is chosen. I also tried modelling??=fully as theifit stands for, merging the written and not-written scopes. That brokebug-15498,bug-optionalArrayKeyInMultiDimArrayLoopandMissingReadOnlyPropertyAssignRuleTest: in those cases the current??=state is more precise than theifform. Losing other facts on??=only costs precision; the original-value marker is the one whose absence turns a check off. Both handlers have turbo mirrors, so the change is ported toAssignHandler.cppandAssignOpHandler.cpp.generated/AssignHandler.his regenerated with PHP 8.5: on PHP 8.4 the generator also rewrites 56 unrelated headers.Verification.
bin/phpstan analysefrom this branch, before (2.3.x at49861bec) and after. The "after" column is the same at levels 6 and 7, and the same with the turbo extension loaded:$this->a ??= 'boom';(?int)does not accept int|stringdoes not accept string$this->b ??= [1, 2];after$this->b = strings();(list<int>|null)does not accept list<1|2|string>self::$s ??= 'boom';(static ?int)does not accept int|stringdoes not accept string$this->m['k'] ??= 'boom';(array<string, int>)does not accept array<string, int|string>does not accept array<string, int|string>private int|string|null $xonly written by$this->x ??= 1;never assigned string@param-out int,?int &$p,$p ??= 'boom';expects int, int|string givenexpects int, string givenon the write,int|stringat exit@param-out list<int>|null,$p = strings(); $p ??= [1, 2];list<string> givenon the??=line??=line@param-out string,int|string|null &$p,$p ??= 'x';expects string, int|string givenEach "after" cell matches what the
if (null === …)form reports. The new tests (TypesAssignedToPropertiesRuleTest×2,ParameterOutAssignedTypeRuleTest,ParameterOutExecutionEndTypeRuleTest,TooWidePropertyTypeRuleTest) fail on 2.3.x and pass with the change. The exit-check test also fails if only theAssignOpHandlerpart is reverted. With and without the extension loaded, these pass:tests/PHPStan/Rules(3618 tests),NodeScopeResolverTest(1888), the rest oftests/PHPStan/Analyser(1168),tests/PHPStan/Type(6531),tests/PHPStan/Reflectionand the remaining directories. For the extension: strict build without warnings,smoke.phpALL OK,side-by-side.phpandsignature-parity.phpOK,make sanitizeOK (smoke, arena-smoke, signature-parity, parser-corpus under UBSan), and clang-tidy 21 clean on the two changed files. Analysis output is byte-identical with and without the extension over the 55 test data files and the 105 files ofsrc/that use??=. phpcs and PHPStan with the repository config are clean on the changed PHP files.Not covered: I did not run the
levelsandexecgroups or the fullmake lint-turbolocally, and I did not benchmark the change.$p['k'] ??= 'x'on a by-ref parameter is reported once, on the write (whose value is the whole array), where theifform also reports it again at function exit.