Isolate alternation branches in RegexGroupParser::walkGroupAst() to prevent non-empty/non-falsy state bleeding#5602
Merged
staabm merged 4 commits intophpstan:2.1.xfrom May 6, 2026
Conversation
… prevent non-empty/non-falsy state bleeding - Walk each alternation branch with fresh nonEmpty/nonFalsy/numeric state instead of carrying state from previous branches - Combine branch results with AND (all branches must agree for the property to hold) - Merge alternation result with parent context using OR for nonEmpty/nonFalsy (parent's positive determination is preserved) and AND for numeric (parent's negative determination is preserved) - Pass inAlternation=false to each branch since the AND combination now handles cross-branch semantics that the flag was meant to enforce - Update test assertions where the fix correctly improves type precision from non-empty-string to non-falsy-string (all alternatives in those patterns are multi-character non-falsy strings)
Contributor
|
the actual change is in commit 1. |
staabm
approved these changes
May 5, 2026
…ty/nonFalsy/numeric When #alternation has no children, return $walkResult unchanged instead of incorrectly propagating Yes for all three flags from the unexecuted loop. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VincentLanglet
approved these changes
May 5, 2026
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.
Summary
When a regex alternation contained a zero-width assertion (like
$,^,\b) alongside a non-empty pattern (likefoo),preg_matchincorrectly inferred the match asnon-empty-stringinstead ofstring. The root cause was inRegexGroupParser::walkGroupAst()where the alternation handler accumulatednonEmpty/nonFalsy/numericstate across branches instead of evaluating each branch independently.Changes
src/Type/Regex/RegexGroupParser.php: Rewrote the#alternationhandler inwalkGroupAst():nonEmpty/nonFalsy/numericstate (allmaybe) instead of inheriting state from previous branchesTrinaryLogic::and()— the property holds only if ALL branches agreeor()fornonEmpty/nonFalsy(preserves parent concatenation's positive determination) andand()fornumeric(preserves parent's negative determination)inAlternationparameter fromtruetofalsefor each branch, since the AND combination now handles cross-branch semantics that the flag was originally designed to enforcetests/PHPStan/Analyser/nsrt/bug-14575.php: New regression test covering:$,^,\banchors in alternations (the reported bug)^abc(def|$))non-empty-string/non-falsy-stringa|b)tests/PHPStan/Analyser/nsrt/bug-12210.php,tests/PHPStan/Analyser/nsrt/preg_match_shapes.php: Updated existing assertions fromnon-empty-stringtonon-falsy-stringwhere the fix correctly improves type precision (patterns where all alternatives are multi-character non-falsy strings)Root cause
The
walkGroupAst()alternation handler reused the$walkResultobject across alternation branches. When the first branch (e.g.,foo) setnonEmpty = yes, this state carried into subsequent branches (e.g.,$). Since the$anchor returns empty string''fromgetLiteralValue()but doesn't actively resetnonEmpty, theyesvalue persisted, making the overall alternation appear non-empty.The
inAlternationflag was a partial workaround — it suppressednonFalsydetermination within alternation branches to prevent similar bleeding, but it didn't addressnonEmptyand also prevented correctnonFalsyinference when all branches genuinely were non-falsy.Test
tests/PHPStan/Analyser/nsrt/bug-14575.phpcovers the reported bug (foo|$→string) and analogous cases with^,\b, capturing groups, parent concatenations, and multi-way alternations.createGroupTypemethod's alternation handling (already correct — processes alternatives independently and unions types), theisMaybeEmptyNodefunction (correctly identifies anchors as maybe-empty), and the subject base type computation (correctly useswalkGroupAstwhose alternation handler is now fixed).Fixes phpstan/phpstan#14575