feat: variable-flow type tracking for chain receivers (closes #23) - #26
Merged
Merged
Conversation
Phase 2 of #11. Chains rooted at `$var` now resolve to the right Eloquent model even when the variable is established several statements earlier or rebuilt across conditional branches: $query = User::query(); if ($active) { $query = $query->where('active', true); } $query->orderBy('|'); // ← completes User's columns New `query_chain/flow.rs` does intra-procedural assignment tracking with `use ($var)` capture traversal and arrow-function auto-capture. Cycles terminate naturally via a monotonically-decreasing byte boundary — recursing into an assignment's RHS uses that assignment's start as the new lookup boundary, so `$q = $q->where(...)` walks back to the original seed instead of looping. `var_type::resolve` is now a thin wrapper that delegates to `flow::resolve`; the per-scope helpers (`typed_param_in`, `docblock_in`) are exposed so flow can apply them at each scope hop. Resolution order, per scope: latest classifiable assignment > typed param > docblock. When flow can't classify an assignment ("tracking cleared"), we fall through to declared types — those are user-asserted intent and aren't invalidated by an unrecognised reassignment. Cursor's `closure_scope > php_type` ordering is preserved, so `whereHas('posts', fn ($q) => $q->where('|'))` still resolves via the relation-hop binding rather than trying to complete against a generic Builder type. Tests: 18 unit (flow/tests.rs) + 4 integration (extractor/tests.rs). Full suite: 1108 passing, 0 failures, clippy clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #23 — Phase 2 of #11.
Adds intra-procedural flow-sensitive type tracking for chain-receiver variables. Chains rooted at
$varnow resolve to the right Eloquent model when the variable is established several statements earlier or rebuilt across conditional branches:Also handles
use ($var)closure capture and arrow-function auto-capture, so this works:Architecture
query_chain/flow.rs(~290 lines + module doc) — assignment tracking, RHS classifier, closure-capture traversal.var_type::resolveis now a thin wrapper that delegates toflow::resolve; per-scope helperstyped_param_in/docblock_inare exposed so flow can apply them at each scope hop.(scope, var)pair was incorrectly blocked during legitimate backward recursion.Resolution order
Per scope: latest classifiable assignment > typed param > docblock.
Across scopes (
use ($var)capture or arrow auto-capture): if nothing resolves locally, step out one scope and try again with the closure's start as the new boundary.When flow can't classify an assignment ("tracking cleared" per AC #3), we fall through to declared types within the same scope. Those are user-asserted intent and aren't invalidated by an unrecognised reassignment.
Notable design choice — closure_scope ordering preserved
The issue AC states
typed param > docblock > flow-tracked > closure scope > unknown. Implementing this literally would have regressedwhereHas('posts', function (Builder \$q) { \$q->where('|'); })— today the closure_scope binding correctly resolves to Post; flipping the order would let the typedBuilderparam win and try to complete against a generic Builder class.After discussion, the resolution model is dataflow-closeness, walking backward from the use site:
\$varentered the scope:closure_scopebinding > typed annotation > docblockuse (\$var)capture → recurse into outer scope, apply same rulesThis preserves all existing
closure_scopesemantics. The cursor.rs dispatch is unchanged; only the contents ofphp_typeare richer now (flow tracking + typed param + docblock).Acceptance criteria
\$query = User::query();followed by\$query->where('|')on a later line completes User's columns\$query = \$query->where(...)keeps the type — subsequent\$query->orderBy('|')still completes columnsuse (\$query)captures the variable's tracked typetap(fn (\$q) => \$q->where('|'))resolves\$qto the chain's effective model (unchanged — handled by Phase 8 closure_scope SameModel binding; not regressed)Tests
flow/tests.rscovering: seed from static query / where / new, self-reassignment chains, branch reassignment (the motivating example), clearing on unknown RHS, fallback to typed param + docblock,use ()capture + auto-capture, scope isolation (nested closure assignments don't pollute outer), mutual-reassignment cycle guard.extractor/tests.rsverifyingInstanceVar.php_typegets populated end-to-end on parsed files (not just at the var_type unit level).Full suite: 1108 passing, 0 failures, clippy clean.
Out of scope (per issue)
Test plan
\$query = User::query(); if (...) { \$query = \$query->where(...); } \$query->where('|')) — confirm column completion fireswhereHas('posts', fn (\$q) => \$q->where('|'))→ Post's columnsuse (\$query)capture in an anonymous function carries the type forwardfn (\$i) => \$query->where('|')carries the type forward\$query = somethingElse();) doesn't fire stale completions