Skip to content

Fix phpstan/phpstan#13770: Nonexistent offset detection on lists (if ($index < count($array)))#5090

Open
phpstan-bot wants to merge 6 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-hen0nm5
Open

Fix phpstan/phpstan#13770: Nonexistent offset detection on lists (if ($index < count($array)))#5090
phpstan-bot wants to merge 6 commits intophpstan:2.1.xfrom
phpstan-bot:create-pull-request/patch-hen0nm5

Conversation

@phpstan-bot
Copy link
Collaborator

Summary

When accessing a list with a non-negative integer index after checking $index < count($list), PHPStan incorrectly reported "Offset int<1, max> might not exist on non-empty-list." This fix teaches the TypeSpecifier that the offset is guaranteed to be valid in that context.

Changes

  • Added a new type narrowing block in src/Analyser/TypeSpecifier.php (around line 337) that creates a dim fetch type specification when:
    • The condition is $index < count($list) (strict less-than, not <=)
    • The index type is non-negative (int<0, max> or positive-int)
    • The index is not a constant integer (those are already handled by specifyTypesForCountFuncCall)
    • The array argument is a list (sequential 0-based integer keys)
  • Added regression test in tests/PHPStan/Rules/Arrays/data/bug-13770.php and tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php

Root cause

The TypeSpecifier already narrowed the array to non-empty-list and the index to an integer range when processing $index < count($list), but it did not create a type specification for the actual array dim fetch expression $list[$index]. The NonexistentOffsetInArrayDimFetchRule checks $scope->hasExpressionType($node) before reporting errors — by adding the dim fetch specification, the rule correctly recognizes the offset as valid.

Constant integer left operands (like 0 < count($list)) are excluded because specifyTypesForCountFuncCall already handles those cases with precise offset tracking, and adding a redundant dim fetch specification caused unwanted hasOffsetValue propagation through array operations like array_map.

Test

The regression test covers four scenarios:

  1. positive-int < count($list) — no error expected (the fix)
  2. int<0, max> < count($list) — no error expected (the fix)
  3. positive-int <= count($list) — error expected (off-by-one, not safe)
  4. int < count($list) — error expected (negative index possible)

Fixes phpstan/phpstan#13770

@staabm staabm self-assigned this Feb 27, 2026
}

// infer $list[$index] after $index < count($list)
if (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it seems correct, I feel like it should be in the specifyTypesForCountFuncCall method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

specifyTypesForCountFuncCall has some complex expressions already and the if-case here seems independent from what happens in specifyTypesForCountFuncCall.

do you have already something specific in mind? I feel like keeping this if here separate is easier to follow

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I had nothing in mind, but was surprised of the

&& !$leftType instanceof ConstantIntegerType

and understood it was maybe handled in specifyTypesForCountFuncCall, so it would have been clearer to group them.

@staabm staabm changed the title Fix #13770: Nonexistent offset detection on lists (if ($index < count($array))) Fix phpstan/phpstan#13770: Nonexistent offset detection on lists (if ($index < count($array))) Feb 28, 2026
@staabm staabm force-pushed the create-pull-request/patch-hen0nm5 branch from 904a6ae to 33fba32 Compare February 28, 2026 11:22
staabm and others added 6 commits February 28, 2026 12:35
- Added type specification in TypeSpecifier for list dim fetch when a non-negative
  integer variable is compared with count() using strict less-than
- When $index < count($list) holds and $index is non-negative, $list[$index] is
  guaranteed to be a valid offset for a list (sequential 0-based keys)
- Excluded ConstantIntegerType left operands since those are already handled by
  specifyTypesForCountFuncCall
- New regression test in tests/PHPStan/Rules/Arrays/data/bug-13770.php
@staabm staabm force-pushed the create-pull-request/patch-hen0nm5 branch from 33fba32 to aef2632 Compare February 28, 2026 11:35
Copy link
Contributor

@VincentLanglet VincentLanglet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency we should support

 if ($index <= count($array) - 1) {
			return $array[$index]; // should not report
		}

But the count($array) - 1 expression might be more complex so I dunno how hard it is.

}

// infer $list[$index] after $index < count($list)
if (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No I had nothing in mind, but was surprised of the

&& !$leftType instanceof ConstantIntegerType

and understood it was maybe handled in specifyTypesForCountFuncCall, so it would have been clearer to group them.

@staabm
Copy link
Contributor

staabm commented Feb 28, 2026

Good idea. I had implemented count()-1 in the past. Will re-use

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.

3 participants