refactor(completion): rewrite lambda scope-context onto the CST ancestor-walk#202
Merged
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
Refactors completion’s lambda scope-context construction from a bounded backward text scan to a CST ancestor-walk, making scope resolution more structurally accurate and removing the 50-line scan cap.
Changes:
- Add a CST catalogue walk for enclosing lambda scopes (
CstQuery::lambda_scope/cst_lambda_scopes) and exposeLambdaScopeInfofor consumption by completion. - Update completion scope-context building to use
live_doc_or_parse+ cursor-node lookup + CST walk, removing the old scan-based helpers. - Add a regression test proving scope discovery beyond the former backward-scan window.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/indexer/infer/mod.rs | Exposes CstQuery::lambda_scope as the new completion-facing CST lambda-scope query. |
| src/indexer/infer/cst_lambda.rs | Implements the enclosing-lambda CST ancestor-walk and shared lambda-param typing helper. |
| src/indexer.rs | Re-exports cursor_node_at and LambdaScopeInfo for feature-layer use. |
| src/features/completion.rs | Updates CompletionContext::analyse call signature after removing line-based scope scanning needs. |
| src/features/completion_context.rs | Replaces the old backward scan (build_lambda_scope et al.) with the CST-based scope walk via live_doc_or_parse. |
| src/features/completion_context_tests.rs | Adds a test covering lambdas beyond the previous 50-line scan window and updates analyse callers. |
This was referenced Jul 3, 2026
Merged
…tor-walk Replace completion_context's ~50-line backward brace-scan (collect_lambda_scopes / build_lambda_scope) with a CST ancestor-walk: CstQuery::lambda_scope walks every enclosing lambda_literal from the cursor node and builds the full LambdaScope stack (it type, named params, label), outermost first. The tree comes from Indexer::live_doc_or_parse, so open files use the live tree and closed files a transient parse. The walk lives in the infer catalogue (cst_lambda::cst_lambda_scopes, LambdaScopeInfo) and reuses the existing per-lambda typing engine (locate_and_extract / cst_lambda_param_type_via_call) via the new cst_lambda_param_type_at shared by cst_named_lambda_param_type. Closes Gap 4 (completion_context.rs direct lambda_receiver_type_from_context text call); the function itself stays for its remaining it_this/cst_lambda callers. The ancestor walk also lifts the 50-line SCOPE_SCAN_BACK_LINES ceiling — decoy test covers a lambda opening 61 lines above the cursor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJsyQ1UgF8uJYR4HgpxEBB
60cbeb1 to
bf3673a
Compare
Hessesian
added a commit
that referenced
this pull request
Jul 4, 2026
…tion path Move-don't-rewrite relocation of the three survivors to their sole callers: `uppercase_dotted_type_prefix` → chain.rs; `fun_trailing_lambda_this_type` and `resolve_call_params` → cst_lambda.rs. receiver.rs and its `mod` declaration are gone; every deleted function verified at zero references. Also folds in the PR #202 review finding: `cursor_node_at` looked the cursor line up via `str::lines()`, which yields no final empty line for `\n`-terminated content — a cursor on the trailing empty line (end-of-file, e.g. right after typing `items.forEach {` + Enter) returned `None`, failing `it` resolution and the brace-repair gate before they could run, and emptying the completion lambda-scope stack. The trailing-gap cursor now resolves on the last character of the last content line (node ends are exclusive), whose node carries the enclosing context. Pinned by `it_resolves_on_trailing_empty_line_at_eof` (RED without the fix, GREEN with it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJsyQ1UgF8uJYR4HgpxEBB
Hessesian
added a commit
that referenced
this pull request
Jul 4, 2026
…tion path (#205) * refactor(infer): resolve walk scopes and inner-call callees on CST nodes; retire the text heuristics Problem A: the completion ancestor-walk (`lambda_scope_info`) resolved each enclosing lambda's `it` type by slicing before-brace text and calling `lambda_receiver_type_from_context`. It now resolves the `lambda_literal` node directly through `cst_lambda_param_type_at`, the same path the innermost lambda and named params already use. Problem B: `f(args) { it }` nests as `outer_call(inner_call, call_suffix{lambda})`, so the callee lived on the inner call_expression that the CST lookups did not unwrap. `call_fn_and_qualifier` and `resolve_callee_chain` now unwrap that inner call, so `loadProduct(a, b) { it }` and `items.mapNotNull(::t) { it }` resolve through the node path. Decoys `trailing_lambda_it_infer_at_cursor` and `it_element_type_with_call_args` are RED without the text call, GREEN via nodes. With both call sites CST-native the text-heuristic family is production-dead, and the pre-commit clippy gate (-D dead-code) forces its removal here rather than in the follow-up: `lambda_receiver_type_from_context` + its ~15 internal helpers, `lambda_receiver_type_named_arg_ml`, `fun_trailing_lambda_it_type`, `inline_lambda_param_type`, `receiver_aware_params_from_text`, and the orphaned `extract_named_arg_name`, `lambda_type_first_input`, `first_concrete_type_arg_str`. `it_type_at_lambda` collapses onto `cst_lambda_param_type_at`; the never-constructed `LambdaParamKind::This` walk branch and the enum go with it (`cst_it_or_this_type` → `cst_it_element_type`); the duplicate inner-call unwrap in `lambda_this_ctx` is removed. receiver.rs keeps only the three signature utilities the CST resolvers still call — the follow-up commit relocates them and deletes the file. Tests: the 25 it_this_tests sites that unit-tested the text heuristic now drive the production CST resolver via `cst_param_type` (same TestDeps, real lambda nodes); indexer_tests conversions use full-document + cursor positions. Deleted as implementation-detail or exact CST twins: trailing_lambda_it_from_fun_def, trailing_lambda_it_with_method_call_arg, this_in_run_resolves_to_receiver_type, extract_named_arg_name_test, extract_named_arg_* (args_tests, 5), lambda_type_first_input_parses_correctly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJsyQ1UgF8uJYR4HgpxEBB * refactor(infer): delete receiver.rs — the CST path is the only resolution path Move-don't-rewrite relocation of the three survivors to their sole callers: `uppercase_dotted_type_prefix` → chain.rs; `fun_trailing_lambda_this_type` and `resolve_call_params` → cst_lambda.rs. receiver.rs and its `mod` declaration are gone; every deleted function verified at zero references. Also folds in the PR #202 review finding: `cursor_node_at` looked the cursor line up via `str::lines()`, which yields no final empty line for `\n`-terminated content — a cursor on the trailing empty line (end-of-file, e.g. right after typing `items.forEach {` + Enter) returned `None`, failing `it` resolution and the brace-repair gate before they could run, and emptying the completion lambda-scope stack. The trailing-gap cursor now resolves on the last character of the last content line (node ends are exclusive), whose node carries the enclosing context. Pinned by `it_resolves_on_trailing_empty_line_at_eof` (RED without the fix, GREEN with it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJsyQ1UgF8uJYR4HgpxEBB * fix(infer): EOF remap targets the last character's start; repair gate goes tree-wide The remap used `len() - 1` as a byte column, splitting a multi-byte final codepoint. Fixing that surfaced a second hole the new pinning test caught: a trailing comment is a tree-sitter extra attached outside the ERROR node, so the chain-only gate saw no error and skipped repair for an unclosed lambda ending in `// note`. The gate now treats any parse error in the tree as license to repair when no enclosing lambda_literal exists — repair self-verifies, so over-permission still converges to authoritative None. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJsyQ1UgF8uJYR4HgpxEBB --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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
Task 7 of the CST lambda-triad collapse — the hardest step: the completion scope-context builder (
build_lambda_scope) was the last architectural text scan, assemblingLambdaScopefor ALL enclosing lambdas via a 50-line backward line-walk. It is now a CST ancestor-walk.cst_lambda_scopeswalk in the catalogue, exposed asCstQuery::lambda_scope(generic overD: InferDeps— TestDeps keeps driving the engine); tree viaIndexer::live_doc_or_parse(universal CST path).completion_context.rsconsumes the walk; the directlambda_receiver_type_from_contextcall and import are gone from this module (the function itself stays until Tasks 6/9 remove its remaining callers).collect_lambda_scopes/build_lambda_scopetext bodies,parse_named_params,should_collect_named_param,lambda_label(relocated into the catalogue),line_before_cursor,SCOPE_SCAN_BACK_LINES.cst_lambda_param_type_at, shared between the named-param resolver and the walk.Behavior
Verified subsumption, not just claimed: the scope-name consumers (
resolve_receiver,named_param_type) iterate innermost-first, so every previously-answered input keeps its answer. The walk additionally covers what the 50-line cap and single-line lambda-header parsing missed (decoy test: lambda opening 61 lines up — old pathNone, new path resolves). Known pre-existing quirk now more visible:resolve_labeled_receiveriterates outermost-first; duplicatethis@labelcollisions spanning >50 lines can differ (follow-up noted in the ledger).Verification
cargo test --bin kmp-lsp— 1441 passed, 0 failed (baseline 1438 + 2 named-arg decoys from the s5 base + 1 scope-walk decoy).cargo clippy --bin kmp-lsp— clean. Decoy test RED→GREEN evidence in the task report.Stacked on #200 (
refactor/cst-resolution-s5). Remaining plan steps: Task 6 (ittext-body deletion), Task 8 (receiver.rs deletion), Task 9 (dead helpers).🤖 Generated with Claude Code
https://claude.ai/code/session_01EJsyQ1UgF8uJYR4HgpxEBB