Skip to content

Commit 3688ea5

Browse files
committed
Rewrite variable resolver as a single forward pass
1 parent b9679a9 commit 3688ea5

126 files changed

Lines changed: 15169 additions & 7455 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 37 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ mago-composer = "1.14"
2929
bumpalo = "3"
3030
ignore = "0.4"
3131
memchr = "2"
32+
ustr = "1"
3233
parking_lot = "0.12"
3334
tempfile = "3"
3435
toml = "1"

docs/CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1919
- **Import all missing classes.** A bulk code action that imports every unresolved class name in the file at once. Only names with a single unambiguous candidate are imported; ambiguous names are left for manual resolution via the single-class import action. Short-name conflicts are detected and skipped. Appears in the quick-fix menu when the cursor is on an unresolved class name and the file has two or more unresolved names.
2020
- **Context-aware import candidate filtering.** Import class actions now filter candidates by the syntactic context of the reference. After `implements` only interfaces are offered, after trait `use` only traits, after `extends` only classes or interfaces (as appropriate). This eliminates wrong-kind suggestions and reduces ambiguity for both single and bulk imports.
2121

22+
### Changed
23+
24+
- **Rewritten variable resolver.** Variable type resolution now uses a single top-to-bottom pass through each function body on both the diagnostic and completion paths, with zero recursion and no depth limit.
25+
- **Broader type narrowing.** `instanceof`, type-guard functions (`is_array`, `is_string`, etc.), `in_array()` strict mode, `assert()`, `@phpstan-assert-if-true`/`-if-false`, and compound `&&`/`||` conditions now narrow variable types in if/else branches, guard clauses, while-loop bodies, ternary expressions, and `match(true)` arms. Negated guards and compound conditions like `if (!$x instanceof A && !$x instanceof B) { return; }` are handled correctly.
26+
- **Closure and arrow function parameter inference.** Untyped closure and arrow function parameters are inferred from the enclosing call's callable signature, including through method chains that return `static`. Generic type substitution flows through to inferred parameters (e.g. a `callable(T)` parameter on a `Repository<Product>` resolves `T` to `Product`).
27+
- **Editing responsiveness.** Classes evicted from the resolved-class cache after a file edit are now eagerly re-populated in dependency order, eliminating lazy resolution delays for diagnostics and completions.
28+
- **Virtual member resolution.** Virtual member providers (PHPDoc mixins, Laravel model, Eloquent Builder) now resolve completely on every class, eliminating cases where mixins or Eloquent accessors were missing after edits.
29+
- **Faster analysis.** String interning, O(1) method lookup, and reduced redundant work cut analysis time significantly on large projects.
30+
2231
### Fixed
2332

33+
- **Infinite loop on array key reassignment patterns.** Files containing `$arr['key'] = f($arr['key'])` or similar read-then-write patterns on the same array key no longer hang the analyzer indefinitely.
34+
- **`analyze` stack overflow on large codebases.** Worker threads in the `analyze` command no longer overflow the stack on projects with deeply nested PHP files.
35+
- **Analysis crash on large files.** Files with hundreds of class definitions (e.g. single-file playgrounds) no longer crash with a stack overflow during analysis.
36+
- **`class-string<T>` parameter completion.** Parameters typed as `class-string<T>` with a bounded template (`@template T of Foo`) now resolve to the bound class during completion and diagnostics. Static member access like `$class::KEY` and `$class::from()` finds members on the bound class.
37+
- **Inherited parameter types propagate to child methods.** When a child method overrides a parent method without its own `@param` docblock, the parent's richer type annotation (e.g. `list<Pen>` vs bare `array`) is now used for completion inside the child method body.
38+
- **Docblock `@param` annotations no longer leak across sibling methods.** The docblock scanner now stops at sibling function/method boundaries, preventing `@param` annotations from one method's docblock from being applied to a different method's parameters.
39+
- **Foreach iterable type alias expansion.** Foreach loops over variables whose type is a `@phpstan-type` alias (e.g. `UserList` defined as `array<int, User>`) now resolve the element type correctly.
40+
- **Loop-carried assignments visible during completion.** Variables assigned later in a loop body are now discovered during completion inside the same loop.
41+
- **Variable completion inside assignment RHS sees the pre-assignment type.** When the cursor is inside the right-hand side of a reassignment (e.g. `$request = new Bar(name: $request->)`), the variable retains its previous type for completion.
42+
- **Scope leakage after closures in chained method calls.** Variables from the enclosing method are no longer invisible after a closure argument in a chained call (e.g. `whereHas(function ($q) { ... })->where('id', $product->id)` now resolves `$product` correctly).
43+
- **Unresolvable instanceof target no longer causes false diagnostics.** When an instanceof check targets a class that cannot be resolved, the variable's type is cleared rather than keeping the pre-narrowing type, preventing false-positive "method not found" diagnostics.
44+
- **`@phpstan-assert-if-false self` no longer narrows to the enclosing class.** Guard clauses using cross-file methods with `@phpstan-assert-if-false self` now narrow against the declaring class, not the class that contains the call site.
2445
- **Short-name collisions eliminated project-wide.** Two unrelated classes sharing a short name (e.g. `App\Carbon` vs `Vendor\Carbon`) were treated as identical across diagnostics, go-to-implementation, trait conflict resolution, throws analysis, and catch-clause filtering. Argument type mismatch diagnostics now use the class hierarchy walker for all Traversable, Stringable, and Countable checks instead of matching short names or suffixes.
2546
- **Lowercase built-in class names recognized as subtypes of `object`.** PHP classes with lowercase names (e.g. `finfo`, `stdClass` in stubs) are now correctly treated as class-like names in subtype checks, where previously the uppercase-first heuristic rejected them.
2647
- **Union-typed method calls no longer lose resolution on second occurrence.** When a variable has a union type (e.g. `ClassA|ClassB`) and a method exists only on a later branch, repeated calls in the same file now resolve correctly instead of returning a stale negative cache entry from the first branch.
@@ -36,6 +57,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3657
- **False "undefined variable" diagnostics for by-reference parameters in inherited static methods and constructors.** By-reference parameters in parent class static methods and constructors are now correctly resolved when called on a child class, preventing false diagnostics for variables defined via the call. Class lookup for by-reference resolution now tries the fully-qualified name first, preventing incorrect matches when multiple classes share a short name across namespaces.
3758
- **Nested `match(true)` expressions.** Multiple `match(true)` expressions sharing narrowed variable names no longer produce incorrect diagnostics.
3859
- **False-positive type errors on generic class methods resolved through return types.** When a method returns a generic class (e.g. `@return HasMany<Translation, Tag>`), calling a method on the result whose parameter references a class-level template parameter (e.g. `@param TRelatedModel $model`) no longer produces a spurious "expects TRelatedModel, got Translation" diagnostic. The return type's generic arguments are now preserved and used for template substitution. Covers instance methods, static methods, standalone functions, inherited methods, trait methods, and nullable generic return types.
60+
- **`analyze` and `fix` commands run at consistent speed regardless of invocation style.** Running `cd project && phpantom_lsp analyze` was roughly 8x slower than `phpantom_lsp analyze --project-root project`. Classes parsed during initialization are now found immediately without re-parsing.
3961
- **Variables prefixed with `$this` receiving stale type information.** Variables like `$thisItem` or `$thisValue` no longer receive stale type information from `$this`-related narrowing.
4062
- **False-positive type errors on generic class methods and templated static methods.** Class-level `@template` parameters (e.g. `Collection<User>`) are now substituted into method parameter types before checking argument compatibility. Method-level `@template` parameters (e.g. PHPUnit's `assertSame`) are resolved from call-site argument types, covering literals, variables, enum cases, property accesses, and method call return types. Argument type checking correctly rejects `float` passed where an int-range type like `positive-int` is expected.
4163
- **False "class not found" for global-namespace classes loaded via Composer's `files` autoloading.** Classes like `Mockery` that live in the global namespace and are autoloaded through Composer's `files` directive (rather than PSR-4 or classmap) are now resolved correctly. The class index populated during initialization was never consulted by the class resolver, causing false diagnostics when such classes were imported with `use Mockery;` in namespaced files.

docs/todo.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,18 @@ within the same impact tier.
2323

2424
## Sprint 5 — Polish for office adoption
2525

26-
| # | Item | Impact | Effort |
27-
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | --------- |
28-
| 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 |
29-
| P9 | [`resolved_class_cache` generic-arg specialisation](todo/performance.md#p9-resolved_class_cache-generic-arg-specialisation) | Medium | Medium |
30-
| P18 | [Subtype result caching](todo/performance.md#p18-subtype-result-caching) (per-request HashMap for hierarchy walks) | Medium | Low |
31-
| D4 | [Unused variable diagnostic](todo/diagnostics.md#d4-unused-variable-diagnostic) | Medium | Medium |
32-
| D12 | [Mago diagnostic proxy](todo/diagnostics.md#d12-mago-diagnostic-proxy) | Medium | Medium |
33-
| F4 | [Return type and closure parameter type inlay hints](todo/lsp-features.md#f4-return-type-and-closure-parameter-type-inlay-hints) | Medium | Medium |
34-
| F9 | [Namespace renaming](todo/lsp-features.md#f9-namespace-renaming) | Medium | Medium |
35-
| A40 | [Convert to instance variable](todo/actions.md#a40-convert-to-instance-variable) | Medium | Medium |
36-
| D10 | [PHPMD diagnostic proxy](todo/diagnostics.md#d10-phpmd-diagnostic-proxy) | Low | Medium |
37-
| | **Release 0.8.0** | | |
26+
| # | Item | Impact | Effort |
27+
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------- |
28+
| ER5 | [Mago-style separated metadata](todo/eager-resolution.md#er5--mago-style-separated-metadata) (pre-populated immutable codebase, O(1) method resolution) | High | High |
29+
| P9 | [`resolved_class_cache` generic-arg specialisation](todo/performance.md#p9-resolved_class_cache-generic-arg-specialisation) | Medium | Medium |
30+
| P18 | [Subtype result caching](todo/performance.md#p18-subtype-result-caching) (per-request HashMap for hierarchy walks) | Medium | Low |
31+
| D4 | [Unused variable diagnostic](todo/diagnostics.md#d4-unused-variable-diagnostic) | Medium | Medium |
32+
| D12 | [Mago diagnostic proxy](todo/diagnostics.md#d12-mago-diagnostic-proxy) | Medium | Medium |
33+
| F4 | [Return type and closure parameter type inlay hints](todo/lsp-features.md#f4-return-type-and-closure-parameter-type-inlay-hints) | Medium | Medium |
34+
| F9 | [Namespace renaming](todo/lsp-features.md#f9-namespace-renaming) | Medium | Medium |
35+
| A40 | [Convert to instance variable](todo/actions.md#a40-convert-to-instance-variable) | Medium | Medium |
36+
| D10 | [PHPMD diagnostic proxy](todo/diagnostics.md#d10-phpmd-diagnostic-proxy) | Low | Medium |
37+
| | **Release 0.8.0** | | |
3838

3939
> **Note:** F1 (Workspace symbol search), F2 (Document symbols), A8
4040
> (Implement interface methods), A9 (Auto import), D1 (Unknown class
@@ -162,13 +162,13 @@ unlikely to move the needle for most users.
162162
| | **[External Stubs](todo/external-stubs.md)** | | |
163163
| E6 | Stub install prompt for non-Composer projects | Low | Low |
164164
| E7 | [Stub-based framework patches](todo/external-stubs.md#e7-stub-based-framework-patches) | Medium | Medium |
165-
| | **[Performance](todo/performance.md)** | | |
166-
| P1.5 | [Layered class resolution (zero-copy inheritance)](todo/performance.md#p15-layered-class-resolution-zero-copy-inheritance) | High | Very High |
165+
| | **[Performance](todo/performance.md) / [Eager Resolution](todo/eager-resolution.md)** | | |
167166
| P13 | [Tiered storage: drop per-file maps for non-open files](todo/performance.md#p13-tiered-storage-drop-per-file-maps-for-non-open-files) | Medium-High | Medium-High |
168167
| P14 | [Eager docblock parsing into structured fields](todo/performance.md#p14-eager-docblock-parsing-into-structured-fields) | Medium | Medium |
169168
| P10 | [Redundant `parse_and_cache_file` from multiple threads](todo/performance.md#p10-redundant-parse_and_cache_file-from-multiple-threads) | Medium | Low |
170169
| P11 | [Uncached base-resolution in `build_scope_methods_for_builder`](todo/performance.md#p11-uncached-base-resolution-in-build_scope_methods_for_builder) | Low-Medium | Low |
171170
| P3 | Parallel pre-filter in `find_implementors` | Low-Medium | Medium |
171+
| P19 | [Analysis time dominated by per-expression class resolution](todo/performance.md#p19-analysis-time-dominated-by-per-expression-class-resolution) (root cause; fix is ER5) | High | High |
172172
| P1b | Propagate `Arc<ClassInfo>` through variable-resolution pipeline | Low | Medium |
173173
| P4 | `memmem` for block comment terminator search | Low | Low |
174174
| P5 | `memmap2` for file reads during scanning | Low | Low |

0 commit comments

Comments
 (0)