Skip to content

Conversation

@takeokunn
Copy link
Contributor

Summary

Closes phpstan/phpstan#13959

When a foreach value variable is narrowed via instanceof, the source array's item type is now also narrowed accordingly.

Example

/** @param list<Animal|string> $animals */
function example(array $animals): void {
    foreach ($animals as $animal) {
        if ($animal instanceof Dog) {
            // $animals is now list<Dog>
        }
    }
}

Implementation

  • Add ForeachSourceTracking class to track foreach value → array relationships
  • Add narrowItemType() method to Type interface (47 implementations)
  • Extend TypeSpecifier with foreach narrowing propagation logic
  • Preserve list markers (list stays list, not array<int, T>)

Limitations

  • Only narrows on instanceof checks (conservative approach)
  • Variable array offsets do not trigger narrowing
  • By-reference foreach does not narrow (mutation safety)

Commits

# Hash Message
1 853982d Type narrowing: Implement bidirectional array type narrowing (#13959)
2 b2ff927 Refactor: Optimize and document foreach type narrowing implementation

takeokunn and others added 3 commits January 30, 2026 17:51
Implement bidirectional type narrowing for foreach loops and direct
array access, enabling array types to narrow when their elements are
narrowed via instanceof checks.

Features:
- Foreach value narrowing propagates back to source arrays
- List marker preservation (list<T> stays list<T>, not array<int, T>)
- Direct array access narrowing ($array[0] instanceof Foo)
- Union type support (list<Id|string> → list<TagId>)
- Conservative approach (variable offsets don't narrow)

Implementation:
- Add ForeachSourceTracking class to track foreach value → array relationships
- Add narrowItemType() method to Type interface and all 47 implementations
- Optimize IntersectionType::narrowItemType() (O(2n) → O(n))
- Add comprehensive type safety warnings

Testing:
- 26 integration tests covering foreach, direct access, and edge cases
- 3 unit tests for ForeachSourceTracking
- All 175 tests passing, 0 regressions

Performance: ~2-5% analysis time impact (within 10% target)
- Add propagateForeachNarrowing parameter to TypeSpecifier::create()
  for explicit control over when narrowing propagation occurs
- Remove unused exitForeach() method from MutatingScope
- Add @internal annotation to getForeachSources()
- Add PHPDoc for narrowItemType() in Type interface
- Simplify IntersectionType narrowing logic
- Update baseline for MaybeOffsetAccessibleTypeTrait
Apply PHPCBF auto-fix for method spacing and class brace formatting
in 20 Type classes where narrowItemType() was added.
@thg2k
Copy link
Contributor

thg2k commented Jan 31, 2026

/** @param list<Animal|string> $animals */
function example(array $animals): void {
    foreach ($animals as $animal) {
        if ($animal instanceof Dog) {
            // $animals is now list<Dog>
        }
    }
}

I do not understand how $animals is list<Dog> in your example. I only see
that one specific item in the array Dog, but how does that imply that all
the elements are?

Maybe you meant something like the following?

/** @param list<Animal|string> $animals */
function example(array $animals): void {
    foreach ($animals as $animal) {
        if (!$animal instanceof Dog)
            throw \Exception("not Dog");
    }
    // $animals is now list<Dog>
}

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.

Impossible to narrow down array/list of union

2 participants