Skip to content

fix: detect Dart local variable shadowing a class field, in both engines - #2571

Merged
carlos-alm merged 1 commit into
mainfrom
fix/issue-2478
Aug 18, 2026
Merged

fix: detect Dart local variable shadowing a class field, in both engines#2571
carlos-alm merged 1 commit into
mainfrom
fix/issue-2478

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

The #2319 second follow-up (PR #2477) only detected a shadowing parameter, explicitly scoping out a shadowing local variable declaration (#2478) as needing more design work — a parameter is trivially in scope for the whole function body, but a local variable's scope is block-bounded and position-dependent.

Rather than leaving this deferred, I worked through a design that avoids the hard part (full control-flow-order reasoning, or relying on any assumption about whether Dart's compiler itself rejects a forward reference) by checking ordering and block-scope directly from the tree:

findEnclosingDartShadowingLocalName / find_enclosing_dart_shadowing_local_name walks up from the call site one enclosing block at a time, stopping at the function_body boundary. At each level, only that block's siblings before the entry statement's own index are checked for a matching local_variable_declaration — a local declared later in the same block is not yet in scope there, and one declared in a different branch of an if/for lives in a sibling block this walk never visits at all, so it can't falsely match either.

Confirmed via parse dumps (both engines) that this produces the identical block / local_variable_declaration / initialized_variable_definition AST shape for this scenario, so the fix mirrors 1:1 between findDartSelectorReceiver (TS) and handle_dart_call_expression + the legacy find_dart_selector_receiver (Rust, kept mirrored per this file's own convention even though it's currently dead code for the pinned grammar).

Two pre-existing tests (sets_receiver_on_a_local_variable_method_call / sets receiver on a local-variable method call) asserted the old, less-precise this.-prefixed behavior for a bare local — updated both to the new, correct bare-receiver behavior, since this is a genuine improvement these tests' own comments already flagged as imprecise ("the extractor cannot tell the two apart... prefixing is harmless here").

Test plan

  • New Rust unit tests in dart.rs (local_variable_shadows_field module) — basic shadow, sibling-method no-shadow, sibling-block no-shadow, nested-block shadow, before-declaration no-shadow, end-to-end type resolution
  • New TS unit tests in tests/parsers/dart.test.ts (#2478 describe block) — same coverage, WASM engine
  • New dual-engine integration test tests/integration/issue-2478-dart-local-var-shadows-field.test.ts — mirrors issue-2319-dart-parameter-shadows-field.test.ts's exact pattern (same-named method on both types, confirms the call edge targets the local's type, never the field's)
  • Updated 2 pre-existing tests whose old assertions described the now-fixed imprecise behavior
  • Revert-verified: disabling each side's fix reproduces the pre-fix failures for every new test, including the integration test on both engines
  • npx tsc --noEmit -p ., npm run lint, full npm test (5485 passed)
  • cargo fmt -- --check, cargo clippy --lib -- -D warnings, cargo test --lib (1122 passed)

Closes #2478

Dart's #2319 second follow-up (PR #2477) only detected a shadowing
PARAMETER, leaving a shadowing LOCAL VARIABLE declaration wrongly
resolved (or dropped) against the field's own type -- a parameter is
trivially in scope for the whole function body, but a local
variable's scope is block-bounded and position-dependent, which is
why this was scoped out at the time as needing more design work.

Adds findEnclosingDartShadowingLocalName / find_enclosing_dart_
shadowing_local_name: walks up the call site's enclosing blocks one
level at a time (stopping at the function_body boundary), and at each
level only checks that block's siblings BEFORE the entry statement's
own index for a matching local_variable_declaration. This verifies
declaration order and block scope directly from the tree -- a local
declared later in the same block, or in a sibling if/for block, can
never falsely match -- rather than assuming anything about how Dart's
own compiler treats forward references.

Confirmed via parse dumps that both engines' grammars produce the
same block/local_variable_declaration/initialized_variable_definition
shape for this scenario, so the fix mirrors identically on both sides.

docs check acknowledged.

Closes #2478

Impact: 2 functions changed, 4 affected
@greptile-apps

greptile-apps Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR updates both Dart extraction engines to distinguish a block-scoped local variable from a same-named class field when extracting method-call receivers.

  • Walks preceding declarations across enclosing blocks while respecting declaration order and block boundaries.
  • Mirrors the receiver-classification logic in the TypeScript/WASM and Rust/native extractors.
  • Adds parser, native unit, and dual-engine integration coverage for shadowing, nested scopes, sibling scopes, declaration order, and end-to-end call resolution.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete actionable defects established in the changed paths.

The mirrored extractors apply the same block- and order-aware receiver classification, and the new tests cover the primary scope transitions and end-to-end resolution behavior.

Important Files Changed

Filename Overview
crates/codegraph-core/src/extractors/dart.rs Adds the native Dart block- and order-aware local-shadowing lookup, integrates it into both receiver paths, and adds focused unit coverage.
src/extractors/dart.ts Adds the mirrored WASM extractor lookup and uses it to emit bare receivers for recognized shadowing locals.
tests/integration/issue-2478-dart-local-var-shadows-field.test.ts Verifies both engines resolve a shadowed receiver to the local variable’s type and never to the field’s type.
tests/parsers/dart.test.ts Adds WASM parser coverage for declaration order, nested and sibling blocks, method boundaries, and receiver/type-map output.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  Call["Dart receiver call: name.method()"] --> Param{"Same-named parameter?"}
  Param -->|Yes| Bare["Emit bare receiver: name"]
  Param -->|No| Local{"Earlier matching local in an enclosing block?"}
  Local -->|Yes| Bare
  Local -->|No| Field["Emit field receiver: this.name"]
  Bare --> LocalType["Resolve function-scoped local type"]
  Field --> FieldType["Resolve class-scoped field type"]
Loading

Reviews (1): Last reviewed commit: "fix: detect Dart local variable shadowin..." | Re-trigger Greptile

@github-actions

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

2 functions changed4 callers affected across 1 files

  • findEnclosingDartShadowingLocalName in src/extractors/dart.ts:664 (3 transitive callers)
  • findDartSelectorReceiver in src/extractors/dart.ts:1117 (3 transitive callers)

@carlos-alm
carlos-alm merged commit 08863c7 into main Aug 18, 2026
34 checks passed
@carlos-alm
carlos-alm deleted the fix/issue-2478 branch August 18, 2026 14:52
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 18, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dart: local variable (not just parameter) shadowing a class field still mis-resolves receiver type

1 participant