Narrow preg_match/preg_match_all subject string type when match is truthy#5777
Conversation
staabm
left a comment
There was a problem hiding this comment.
do we need a simliar fix for other type-specifying extensions?
can this fix be made in MutatingScope/TypeSpecifier/NodeScopeResolver instead, so it works for more expressions?
|
Done. Here's my response to the review, with the investigation behind it. Addressing @staabm's review
No new extension is needed — the codebase already applies this exact per-function pattern:
Each lives in its own extension because the narrowed type is derived from that function's specific semantics.
It can't, and shouldn't:
What I did changeTo genuinely honor the "works for more expressions" intent, I made the subject narrowing smarter for nullable subjects — something
I also added the regression coverage the PR claimed but was missing:
|
…s truthy - Add `matchSubjectExpr()` to `RegexArrayShapeMatcher` that extracts the subject base type (non-empty-string or non-falsy-string) from a regex pattern without requiring a $matches argument - Modify `PregMatchTypeSpecifyingExtension` to narrow the subject parameter (arg 1) to the regex-derived subject type in truthy context - Guard against narrowing non-string subjects (e.g. int|string) by checking `isString()->yes()` on the subject type - Skip subject narrowing when the subject expression shares a root variable with the matches argument (e.g. `preg_match($pat, $matches[2], $matches)`) to avoid conflicting type specifications
A null subject is coerced to '' by preg_*, which cannot match a pattern whose derived subject type is non-empty, so null can be soundly removed from a nullable string subject. Non-string scalars (e.g. int) may be coerced to a matching string and are still left untouched. Also adds regression coverage for the subject-shares-variable-with-matches guard and the int|string non-narrowing case. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ables Make the rationale behind the `instanceof Expr\\Variable` guard explicit: narrowing only plain variables covers the vast majority of real-world code and avoids breaking exotic subjects like `preg_match($p, $matches[2], $matches)` (Rules bug-9503), where the subject is an offset of the array receiving matches. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3f54a9c to
805983f
Compare
Summary
When
preg_match()orpreg_match_all()is used as a condition (inif, ternary,&&, etc.) and returns truthy, the subject string parameter was not being narrowed based on the regex pattern. ThePregMatchTypeSpecifyingExtensiononly narrowed the$matchesarray (arg 2) but never the subject string (arg 1). This fix adds subject narrowing using the regex pattern analysis that already exists inRegexArrayShapeMatcher.Changes
matchSubjectExpr()method tosrc/Type/Php/RegexArrayShapeMatcher.phpthat extracts the subject base type from a regex pattern (non-empty-string or non-falsy-string) using the existingRegexGroupParserinfrastructuresrc/Type/Php/PregMatchTypeSpecifyingExtension.phpto:$matchesargument (previously returned emptySpecifiedTypesimmediately)int|string) by checkingisString()->yes()preg_match($pat, $matches[2], $matches))Analogous cases probed
preg_match_all: tested and working (uses the same extension)!preg_match: works correctly (falsey branch keepsstring, truthy branch narrows)preg_match() === 1: works correctly&&conditions: works correctly (both subjects narrowed)?:: falsey branch correctly staysstringstring(no narrowing when pattern can't be analyzed)int|string): correctly skipped (no narrowing applied)$matches[2]as subject,$matchesas output): correctly skipped to avoid conflictsRoot cause
PregMatchTypeSpecifyingExtension::specifyTypes()returned emptySpecifiedTypeswhen no$matchesargument was present, and even when$matcheswas present, it only narrowed the matches array variable — never the subject string. The regex pattern analysis infrastructure (RegexGroupParser→RegexAstWalkResult::getSubjectBaseType()) already computed the subject type but it was only used for$matches[0]'s type, not for narrowing the subject variable itself.Test
tests/PHPStan/Analyser/nsrt/bug-14710.phpwith regression tests covering:preg_match(the reported issue)ifwithpreg_match(same underlying bug)$matchesargumentpreg_match_all=== 1comparison/^$/that can match empty strings)Fixes phpstan/phpstan#14710