Skip to content

Skip "might not exist" report for string types with integer offsets in union decomposition check#5705

Closed
phpstan-bot wants to merge 1 commit into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-ezhgdno
Closed

Skip "might not exist" report for string types with integer offsets in union decomposition check#5705
phpstan-bot wants to merge 1 commit into
phpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-ezhgdno

Conversation

@phpstan-bot
Copy link
Copy Markdown
Collaborator

Summary

When accessing a string offset with an integer index on a union of constant strings (e.g. ''|':'), the reportMaybes decomposition in NonexistentOffsetInArrayDimFetchCheck incorrectly reports "Offset 0 might not exist" because the empty string member returns no for offset 0. The union-level hasOffsetValueType correctly returns maybe, but the per-member decomposition is too aggressive for string types where offset validity depends on length.

Changes

  • Modified src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php: In the inner decomposition loop, skip the "might not exist" report when $innerType->isString()->yes() and $innerDimType->isInteger()->yes(). This prevents false positives from short string members in unions while preserving all other offset checks.
  • Added regression test tests/PHPStan/Rules/Arrays/data/bug-13688.php with the exact reproducer from the issue (empty string in union with strlen guard) plus an additional case for mixed-length constant string unions.
  • Added test method testBug13688 in NonexistentOffsetInArrayDimFetchRuleTest.php.

Root cause

The reportMaybes block in NonexistentOffsetInArrayDimFetchCheck decomposes both the container type and the dim type into individual members and checks every combination. When the container is a union of constant strings like ''|':' and the dim is an integer like 0:

  1. The union-level hasOffsetValueType(0) correctly returns maybe
  2. The decomposition iterates over each string member
  3. ConstantStringType('')->hasOffsetValueType(ConstantIntegerType(0)) returns no (empty string has no valid offsets)
  4. This triggers the report

For arrays, this decomposition is valuable because array keys are structural properties. For strings, offset validity is a function of string length, which users typically guard with length checks through variables that PHPStan cannot track (e.g. $len = strlen($s); if ($len > 0) { ... $s[$len-1] ... }).

The fix skips the per-member report specifically for string+integer combinations. This does not affect:

  • Definite errors: "Offset X does not exist" (caught by the earlier ->no() check on the full type)
  • Non-integer offsets on strings: objects, floats, string keys still report correctly
  • Array offset checks: completely unaffected since isString()->yes() is false for arrays

Probed analogous cases that are already correct:

  • ArrayDestructuringRule shares NonexistentOffsetInArrayDimFetchCheck and benefits from the same fix
  • Non-integer dim types on strings (object, float, string) — the isInteger()->yes() guard preserves these reports
  • Array unions with missing keys — isString()->yes() guard preserves these reports

Test

  • testBug13688: Regression test with the exact reproducer from the issue — strlen() guard in && with string offset access on ''|':' union. Also tests that accessing offset 0 on a mixed-length constant string union ('a'|'abc') does not produce a false positive.

Fixes phpstan/phpstan#13688

…n union decomposition check

- In NonexistentOffsetInArrayDimFetchCheck, the reportMaybes decomposition
  loop flattens union types and checks each member individually. When a
  constant string member (e.g. '') returns "no" for an integer offset (e.g. 0),
  it incorrectly triggers "might not exist" even though the union-level check
  correctly returns "maybe".
- Skip the report when innerType is a string and innerDimType is an integer,
  since string offset validity is length-dependent and users typically guard
  with length checks through variables that PHPStan cannot track.
- The fix does not affect: definite "does not exist" errors (caught earlier),
  non-integer offsets on strings (objects, floats, strings still report), or
  array offset checks (completely unaffected).
- Probed analogous cases: non-integer dim types on strings, array offset
  decomposition, ArrayDestructuringRule (shares the same check) — all
  already correct.
@staabm staabm closed this May 19, 2026
@staabm staabm deleted the create-pull-request/patch-ezhgdno branch May 19, 2026 06:06
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.

2 participants