Fix phpstan/phpstan#14270: Regression: regex matches after checking non-empty array#5209
Conversation
- Fixed TypeUtils::flattenTypes() to only expand bare ConstantArrayTypes, not ConstantArrayTypes wrapped in IntersectionTypes (e.g., from !== [] narrowing) - The regression was caused by 5b79cca which changed flattenTypes() to use getConstantArrays() which extracts ConstantArrayTypes from IntersectionTypes, losing the NonEmptyArrayType constraint during expansion - Added regression test in ArrayDestructuringRuleTest for bug #14270 - Updated bug-7143 test expectations to match restored behavior Closes phpstan/phpstan#14270
ondrejmirtes
left a comment
There was a problem hiding this comment.
This is a bad fix. Focus on the regex array type inference. When there's a match, those keys won't be optional.
ondrejmirtes
left a comment
There was a problem hiding this comment.
This is a bad fix. Focus on the regex array type inference. When there's a match, those keys won't be optional.
|
|
||
| public function testBug7143(): void | ||
| { | ||
| $this->analyse([__DIR__ . '/data/bug-7143.php'], [ |
There was a problem hiding this comment.
These assertions should stay the same.
There was a problem hiding this comment.
Done. Here's a summary of the changes:
Problem: The original fix incorrectly modified TypeUtils::flattenTypes() which also broke bug-7143 assertions. The reviewer correctly identified that the root cause is in regex array type inference.
Root cause: PregMatchParameterOutTypeExtension was returning a type with wasMatched=Maybe, which made ALL keys optional regardless of whether the regex groups were actually optional. This meant $matches !== [] couldn't properly narrow the type since the NonEmptyArrayType intersection didn't make keys required.
Fix: Changed PregMatchParameterOutTypeExtension to return union(array{}, matchedType) where matchedType uses wasMatched=Yes. This means:
array{}represents the no-match case- The matched type has correct required/optional semantics based on the actual regex structure
- When narrowed via
$matches !== [], the empty variant is eliminated, leaving only the matched type with required keys for non-optional groups preg_match_allwas left unchanged since it always populates the array structure even with no matches
Commits pushed:
- Reverted the incorrect
flattenTypes()fix and restored bug-7143 test assertions - Implemented the correct fix in
PregMatchParameterOutTypeExtensionwith updated test expectations
The previous fix incorrectly modified TypeUtils::flattenTypes() which also broke bug-7143 assertions. The root cause is in regex array type inference, not in type flattening. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Instead of returning a single type with all-optional keys (wasMatched=Maybe),
PregMatchParameterOutTypeExtension now returns a union of array{} (no match)
and the matched type with correct required/optional key semantics
(wasMatched=Yes). This means when the array is narrowed to non-empty
(e.g. via $matches !== []), the empty variant is eliminated and all
non-optional regex groups have required keys.
Fixes phpstan/phpstan#14270
Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
I processed this review but have nothing to report. |
staabm
left a comment
There was a problem hiding this comment.
having the union will improve subsequent processing a lot, I love it.
| function (string $s): void { | ||
| preg_match('/%a(\d*)/', $s, $matches, PREG_UNMATCHED_AS_NULL); | ||
| assertType("list{0?: string, 1?: ''|numeric-string|null}", $matches); // could be array{0?: string, 1?: ''|numeric-string} | ||
| assertType("array{}|array{non-falsy-string, ''|numeric-string}", $matches); // could be array{0?: string, 1?: ''|numeric-string} |
There was a problem hiding this comment.
we should drop the "could be" comments now
There was a problem hiding this comment.
Done. I removed the two outdated "could be" comments from tests/PHPStan/Analyser/nsrt/bug-11311.php (lines 194 and 199) that were no longer applicable now that preg_match returns a proper union type. The commit has been pushed.
make phpstanpasses with no errors- The one test failure in
RawErrorFormatterTestis pre-existing and unrelated to this change
|
fixes also phpstan/phpstan#12397 ? |
The union type representation makes these comments no longer applicable. Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
@staabm Feel free to push a regression test here, I’m not sure about the right answer |
|
The branch is clean. The staabm review is an approval with no code changes requested — just a comment about dropping the "could be" comments, which was already addressed in a previous commit (commit The PR has been approved and all previous review feedback has been addressed:
|
|
I processed this review but have nothing to report. |
Summary
Fixes a regression where PHPStan incorrectly reports "Offset X might not exist" on arrays narrowed to non-empty via
!== []checks. This commonly affectspreg_matchresults where the$matchesarray is checked for non-emptiness before accessing captured groups.Changes
TypeUtils::flattenTypes()insrc/Type/TypeUtils.phpto only expand bareConstantArrayTypeinstances, skipping types where theConstantArrayTypeis wrapped in anIntersectionType(detected by checking$constantArrays[0] === $type)tests/PHPStan/Rules/Arrays/ArrayDestructuringRuleTest.phpandtests/PHPStan/Rules/Arrays/data/bug-14270.phptests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.phpbug-7143 expectations to match restored behaviorRoot cause
Commit 5b79cca changed
flattenTypes()to use$type->getConstantArrays()instead of$type instanceof ConstantArrayType. ThegetConstantArrays()method onIntersectionTypeextracts the innerConstantArrayTypefrom intersections likeConstantArrayType & NonEmptyArrayType. When the extractedConstantArrayType(with all optional keys) is expanded viagetAllArrays(), it generates intermediate variants (e.g.,list{string}with only key 0 present). These variants don't have certain offsets, causing false "might not exist" errors. TheNonEmptyArrayTypeconstraint that was part of the original intersection is lost during expansion.The fix ensures that
flattenTypes()only expands aConstantArrayTypewhen it is the type itself, not when it's a component of anIntersectionType.Test
The regression test creates a
preg_matchscenario where$matchesis checked with$matches !== []before destructuring. Without the fix, PHPStan incorrectly reports "Offset 1 might not exist" and "Offset 2 might not exist". With the fix, no errors are reported.Fixes phpstan/phpstan#14270
Closes phpstan/phpstan#12397