Skip to content

Infer non-empty-list for preg_match_all() matches when the match succeeded#5812

Merged
staabm merged 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-c0r5995
Jun 6, 2026
Merged

Infer non-empty-list for preg_match_all() matches when the match succeeded#5812
staabm merged 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-c0r5995

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

After a successful preg_match_all() (e.g. inside if (preg_match_all(...))), the resulting $matches arrays were inferred with plain list<...> value types. Because a list may be empty, accessing $matches[0][0] produced a false positive Offset 0 might not exist on list<string>.. A successful preg_match_all() returns the number of full matches, which is >= 1, so every produced list is guaranteed non-empty.

This change infers non-empty-list<...> for those lists when the match is known to have succeeded.

Changes

  • src/Type/Php/RegexArrayShapeMatcher.php:
    • createSubjectValueType() now takes $wasMatched and intersects the PREG_PATTERN_ORDER subject list with NonEmptyArrayType when $wasMatched->yes().
    • createGroupValueType() intersects the PREG_PATTERN_ORDER per-group list with NonEmptyArrayType when $wasMatched->yes().
    • buildArrayType() intersects the PREG_SET_ORDER outer list with NonEmptyArrayType when $wasMatched->yes().
    • Updated the two non-matchesAll call sites of createSubjectValueType() to pass $wasMatched.
  • tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php: updated existing matched-branch expectations to non-empty-list and added a bug14781() regression covering both PREG_PATTERN_ORDER and PREG_SET_ORDER, including safe $matches[0][0] access.

Root cause

RegexArrayShapeMatcher built the preg_match_all() result lists as ArrayType & AccessoryArrayListType without recording non-emptiness, even when the call was specified as a successful match ($wasMatched->yes()). The fix adds NonEmptyArrayType to the intersection at each of the three places that build a list for preg_match_all():

  • PREG_PATTERN_ORDER subject list
  • PREG_PATTERN_ORDER capturing-group lists
  • PREG_SET_ORDER outer list

preg_match() (single match) and the non-matchesAll tagged-union branches were probed and are unaffected — they do not produce lists, so there is no non-emptiness to preserve. Non-constant patterns fall back to the generic param-out types and are likewise unaffected.

Test

  • bug14781() in tests/PHPStan/Analyser/nsrt/preg_match_all_shapes.php asserts non-empty-list for both PREG_PATTERN_ORDER and PREG_SET_ORDER inside if (preg_match_all(...)), and that $matches[0][0] / $setMatches[0] no longer trigger possibly-missing-offset errors.
  • Existing matched-branch assertions in the same file were updated from list to non-empty-list. Assertions outside a success check (plain calls, === 0 guards) correctly remain list.

Fixes phpstan/phpstan#14781

…succeeded

- In `RegexArrayShapeMatcher`, intersect the per-pattern `list` types with `NonEmptyArrayType` when `$wasMatched->yes()`, since a successful `preg_match_all()` guarantees at least one match.
- Covers all three list-producing sites: the `PREG_PATTERN_ORDER` subject list (`createSubjectValueType`), the `PREG_PATTERN_ORDER` per-group lists (`createGroupValueType`), and the `PREG_SET_ORDER` outer list (`buildArrayType`).
- Thread `$wasMatched` into `createSubjectValueType`.
@staabm staabm requested a review from VincentLanglet June 6, 2026 12:40
@staabm staabm merged commit 792edea into phpstan:2.2.x Jun 6, 2026
938 of 966 checks passed
@staabm staabm deleted the create-pull-request/patch-c0r5995 branch June 6, 2026 13:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

preg_match_all() should infer non-empty-list

3 participants