You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
13
13
-**`instanceof` narrowing with unresolvable target class.** When the `instanceof` target class cannot be loaded (e.g. it lives inside a phar archive), the variable's type is now treated as unknown instead of keeping the un-narrowed base type. This eliminates false-positive "unknown member" diagnostics for members that only exist on the narrowed subclass. Applies to all narrowing contexts: if-bodies, ternary expressions, `assert()` statements, and inline `&&` chains.
14
14
-**`DB::select()` return type.**`DB::select()`, `DB::selectFromWriteConnection()`, and `DB::selectResultSets()` now return `array<int, stdClass>` instead of bare `array`, and `DB::selectOne()` returns `?stdClass` instead of `mixed`. Property access on query results (e.g. `$result[0]->column_name`) no longer produces false-positive diagnostics. The same fix applies to the underlying `Illuminate\Database\Connection` class.
15
15
-**Redis `Connection` method resolution.**`Illuminate\Redis\Connections\Connection` now has `@mixin \Redis` applied automatically, so Redis commands like `del()`, `get()`, `set()`, etc. resolve through the phpredis stubs instead of producing false-positive diagnostics.
16
+
-**Array shape tracking from keyed assignments inside conditional branches.** When an array is built incrementally with variable keys inside a loop that contains if/else branching (e.g. `$arr[$id] = ['bundle' => $obj, 'count' => 1]` in an else branch), the shape type is now preserved through foreach iteration. Previously the variable resolution depth limit was too low, causing intermediate variables in the shape value to resolve as `mixed` instead of their actual class type.
| B13 |[Array shape tracking from keyed literal assignments in loops](todo/bugs.md#b13-array-shape-tracking-from-keyed-literal-assignments-in-loops)| Low | High |
27
26
| H10 |[`return.unusedType` — remove unused type from return union](todo/phpstan-actions.md#h10-returnunusedtype--remove-unused-type-from-return-union)| Medium | Medium |
28
27
| H6 |`return.type` — update return type to match actual returns | Medium | Medium |
29
28
||**Release 0.7.0**|||
@@ -90,6 +89,7 @@ unlikely to move the needle for most users.
90
89
| C6 |`#[ExpectedValues]` parameter value suggestions | Low | Medium |
91
90
| C10 |[Deprecation markers on class-name completions from all sources](todo/completion.md#c10-deprecation-markers-on-class-name-completions-from-all-sources)| Low | Low |
92
91
||**[Type Inference](todo/type-inference.md)**|||
92
+
| T25 |[Forward-walking scope model](todo/type-inference.md#t25-forward-walking-scope-model-for-variable-type-resolution) (eliminate backward-scanning depth limit) | High | Very High |
93
93
| T19 |[Structured type representation](todo/type-inference.md#t19-structured-type-representation) (replace string-based types with `PhpType` enum) | High | Very High |
Copy file name to clipboardExpand all lines: docs/todo/type-inference.md
+77Lines changed: 77 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -527,3 +527,80 @@ with `stdClass` property access suppression, but the general gap
527
527
remains for any `$arr[$i] instanceof Foo` pattern.
528
528
529
529
---
530
+
531
+
## T25. Forward-walking scope model for variable type resolution
532
+
**Impact: High · Effort: Very High**
533
+
534
+
PHPantom resolves variable types lazily: when the user triggers
535
+
completion on `$x->`, it walks backward from the cursor to find
536
+
where `$x` was assigned, then recursively resolves any variables
537
+
referenced in the RHS. Each level of indirection adds a call to
538
+
`resolve_variable_types`, which re-parses the file and re-walks the
539
+
AST. A global depth counter (`MAX_VAR_RESOLUTION_DEPTH`, currently 4)
540
+
caps the recursion to prevent stack overflows.
541
+
542
+
PHPStan, Psalm, and Mago all use the opposite strategy: an eager,
543
+
single-pass, forward-walking scope model. They walk statements
544
+
top-to-bottom, carrying a mutable type map (`expressionTypes` in
545
+
PHPStan, `locals` in Mago). When they encounter `$a = $b->prop`,
546
+
they look up `$b` in the already-populated map, resolve the property,
547
+
and store `$a`'s type. Variable lookup is a flat O(1) map fetch with
548
+
zero recursion regardless of assignment chain depth.
549
+
550
+
The backward-scanning approach causes several problems:
551
+
552
+
-**Depth limit fragility.** Every real-world pattern that adds one
553
+
more level of indirection (e.g. array shape literals referencing
554
+
variables assigned from foreach bindings inside conditional
555
+
branches) requires bumping the limit. The limit was 3, then 4;
556
+
it will need to grow again.
557
+
-**Redundant work.** Each recursive call re-parses the source and
558
+
re-walks the AST from the top. A forward pass would parse once and
559
+
walk once.
560
+
-**No scope threading.** Narrowing, guard clauses, and branch-aware
561
+
resolution are bolted on as post-hoc corrections rather than
562
+
flowing naturally through the scope.
563
+
564
+
**Depth limits eliminated by this item:**
565
+
566
+
| Constant | Value | Location | Why it exists |
567
+
|---|---|---|---|
568
+
|`MAX_VAR_RESOLUTION_DEPTH`| 4 |`completion/variable/resolution.rs`| Each variable in an assignment chain triggers a full re-parse and re-walk. Was 3, bumped to 4 for array shapes in conditional loop branches. |
569
+
|`MAX_CLOSURE_INFER_DEPTH`| 4 |`completion/variable/closure_resolution.rs`|`infer_callable_params_from_receiver` resolves the receiver type to infer closure parameter types, which can trigger another variable resolution cycle. |
570
+
571
+
Both share the same root cause: resolving a variable's type triggers
572
+
a full re-parse and re-walk from scratch, which recurses when the RHS
573
+
references another variable. In a forward-walking model, both would
574
+
be flat map lookups with zero recursion.
575
+
576
+
The remaining depth limits (`MAX_INHERITANCE_DEPTH`,
0 commit comments