Skip to content

fix: seed typeMap for Dart local var initialized by a constructor call - #2567

Merged
carlos-alm merged 3 commits into
mainfrom
fix/issue-2474
Aug 18, 2026
Merged

fix: seed typeMap for Dart local var initialized by a constructor call#2567
carlos-alm merged 3 commits into
mainfrom
fix/issue-2474

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

var svc = UserService(repo); never seeded a typeMap entry for svc, unlike every other language extractor's identical "assign a constructor call to a local variable" convention (e.g. JS/TS's handleVarDeclaratorTypeMap). A later call through that local (svc.createUser()) could therefore never resolve via the typeMap, and the call edge was silently dropped.

  • Adds handleDartLocalVarTypeMap (TS, src/extractors/dart.ts) and its mirror handle_dart_local_var_type_map (Rust, crates/codegraph-core/src/extractors/dart.rs), wired to initialized_variable_definition. Seeds a function-scoped typeMap entry (confidence 1.0) when the initializer is a bare constructor call.
  • Both engines' tree-sitter-dart grammars needed separate handling:
    • Native (crates.io tree-sitter-dart 0.2): a clean value: call_expression.
    • WASM (npm tree-sitter-dart 1.x): confirmed by direct parse dump that value: is a field marker on two different children (the bare callee identifier and the trailing call selector) — childForFieldName('value') only returns the first match, so an earlier version of this fix wrongly took the "native" branch and bailed out before reaching the correct sibling-based lookup. Fixed by gating on the value field's type (=== 'call_expression'), not merely its presence.
  • Extracted a shared findEnclosingDartSignatureFromBody / find_enclosing_dart_signature_from_body helper (refactored out of findEnclosingDartParamListForCall / find_enclosing_dart_param_list_for_call) since a Dart function's signature and body are sibling nodes, not nested — a body descendant (like a local variable declaration) can't reach its own signature via a simple ancestor walk.

Deliberately does not attempt to resolve a local variable shadowing a same-named class field (tracked separately as #2478 — already noted in this file's existing doc comments as out of scope for this fix).

Test plan

  • New Rust unit tests in dart.rs (local_var_constructor_call_typing module) — seeding, scoping across functions, non-constructor-call no-op, class-method scoping, issue repro
  • New TS unit tests in tests/parsers/dart.test.ts (#2474 describe block) — same coverage, WASM engine
  • New dual-engine integration test tests/integration/issue-2474-dart-local-var-constructor-call-typing.test.ts — asserts the actual call edge resolves via buildGraph, both wasm and native
  • Revert-verified: disabling the dispatch arm on each side reproduces the pre-fix failures for the exact new tests added
  • npx tsc --noEmit -p ., npm run lint, full npm test (5464 passed)
  • cargo fmt -- --check, cargo clippy --lib -- -D warnings, cargo test --lib (1107 passed)

Closes #2474

var svc = UserService(repo); never seeded a typeMap entry for svc,
unlike every other language extractor's constructor-call-initializer
convention, so a later call through it (svc.createUser()) could never
resolve and the call edge was silently dropped.

Adds handleDartLocalVarTypeMap / handle_dart_local_var_type_map,
seeding a function-scoped entry for both tree-sitter-dart grammar
shapes (native's clean value: call_expression, and WASM's sibling
identifier + selector layout). Requires extracting a shared
findEnclosingDartSignatureFromBody / find_enclosing_dart_signature_from_body
helper, since a function's signature and body are sibling nodes rather
than nested, so a body descendant can't reach its own signature via a
simple ancestor walk.

No doc updates needed — internal bug fix to an existing extractor, no
language/feature/architecture surface change. docs check acknowledged.

Closes #2474

Impact: 5 functions changed, 8 affected
@greptile-apps

greptile-apps Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds function-scoped Dart type-map inference for local variables initialized by bare constructor calls, allowing subsequent calls through those locals to resolve.

  • Implements mirrored extraction logic for the TypeScript/WASM and Rust/native engines.
  • Handles the engines’ distinct tree-sitter Dart initializer shapes.
  • Refactors sibling-body signature discovery for local-variable scoping.
  • Adds parser, native unit, and dual-engine integration coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/extractors/dart.ts Adds WASM-compatible local constructor inference and shared sibling-signature traversal; the previously reported lowercase-call issue is fixed.
crates/codegraph-core/src/extractors/dart.rs Mirrors the TypeScript extraction and scoping behavior for the native engine with corresponding unit coverage.
tests/integration/issue-2474-dart-local-var-constructor-call-typing.test.ts Verifies call-edge resolution and engine parity while explicitly documenting the deferred uppercase-function ambiguity.
tests/parsers/dart.test.ts Covers scoped and bare entries, function isolation, unsupported initializers, lowercase function calls, and class methods.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Source["Dart local declaration"] --> Parse{"Parser engine"}
  Parse -->|WASM selector layout| TS["TypeScript extractor"]
  Parse -->|Native call_expression layout| Rust["Rust extractor"]
  TS --> Gate{"Capitalized bare callee?"}
  Rust --> Gate
  Gate -->|Yes| TypeMap["Seed function-scoped typeMap entry"]
  Gate -->|No| Skip["Leave typeMap unchanged"]
  TypeMap --> Resolve["Resolve later receiver calls"]
Loading

Reviews (3): Last reviewed commit: "fix: lower Dart local-var constructor ty..." | Re-trigger Greptile

Comment thread src/extractors/dart.ts
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

5 functions changed10 callers affected across 3 files

  • walkDartNode in src/extractors/dart.ts:27 (3 transitive callers)
  • handleDartLocalVarTypeMap in src/extractors/dart.ts:416 (4 transitive callers)
  • findEnclosingDartParamListForCall in src/extractors/dart.ts:596 (3 transitive callers)
  • findEnclosingDartSignatureFromBody in src/extractors/dart.ts:621 (6 transitive callers)
  • findEnclosingDartFunctionQualifierForBody in src/extractors/dart.ts:663 (3 transitive callers)

Greptile finding on PR #2567: since Dart lets a constructor call omit
new, an ordinary lowercase factory function call (var svc =
makeService();) was indistinguishable from a real constructor call
(var svc = UserService();) at the call_expression level, so it was
wrongly seeded as if svc's type were the literal function name
makeService, corrupting later receiver-typed resolution for calls
through that local.

Gates the seeding on the callee being capitalized, matching Dart's own
type-naming convention and this codebase's existing precedent for the
identical ambiguity in javascript.ts/javascript.rs. Uses a plain ASCII
check on both sides (TS /^[A-Z]/, Rust is_ascii_uppercase()) rather
than a full-Unicode comparison, avoiding the astral-plane/titlecase
engine-divergence risk #2396 already found in the fuller heuristic.

docs check acknowledged.

Impact: 1 functions changed, 2 affected
@carlos-alm

Copy link
Copy Markdown
Contributor Author

@greptileai

Comment thread src/extractors/dart.ts
…idual gap

Follow-up to Greptile's second finding on PR #2567 (a capitalized
ordinary function is still indistinguishable from a constructor call).
Empirically confirmed via a new dual-engine integration test that a
wrong guess currently drops (not misroutes) the receiver call's edge,
because resolveByReceiver / resolve_call_targets_core both skip the
untyped direct-qualified fallback whenever any typeMap entry exists
for the receiver, right or wrong. This is a pre-existing,
language-agnostic property of the shared resolver, not something this
fix introduces, and fully closing it needs either a shared-resolver
change or a same-file cross-check that requires refactoring dart.ts's
single-pass walker into a two-pass design first -- both out of scope
here. Filed as follow-up issue #2568.

Lowers the heuristic's confidence from 1.0 to 0.7, matching the same
tier JS/TS's own capitalization-based Foo.create() factory heuristic
already uses for the identical class of uncertainty. Replaces the
integration test's incorrect "safety net" assumption (which turned out
not to hold once verified empirically) with an honest regression test
locking in the one guarantee that does hold: the wrong guess never
fabricates an edge to a nonexistent node.

docs check acknowledged.

Impact: 1 functions changed, 2 affected
@carlos-alm

Copy link
Copy Markdown
Contributor Author

Addressed both findings in the latest commit (fd438c6):

Finding 1 (bare lowercase call → constructor type): fixed by gating the seeding on the callee being capitalized, matching Dart's own type-naming convention.

Finding 2 (uppercase ordinary function → constructor type): this one is real and I verified it empirically rather than arguing it away — added a dual-engine integration test with a capitalized top-level function (OrderService MakeOrderService()), and confirmed the resulting call edge (order.placeOrder()) is genuinely dropped on both engines. Root cause: resolveByReceiver (TS) and resolve_call_targets_core (Rust) both skip the untyped direct-qualified-method fallback whenever any typeMap entry exists for the receiver, right or wrong — this is a pre-existing, language-agnostic property of the shared call resolver, not something this PR introduces.

Fully closing this needs either (a) a shared-resolver change so a failed type-aware lookup falls through to the untyped fallback, which has blast radius across every language using this cascade and needs its own precision/recall validation, or (b) a same-file "is this name already a known ordinary function?" cross-check in the Dart extractor, which requires first refactoring dart.ts's single-pass walkDartNode into the two-pass design dart.rs/javascript.ts already use (a single-pass check would be declaration-order-dependent and diverge from the native engine). Both are out of scope for this fix — filed as #2568.

For this PR, I lowered the heuristic's confidence from 1.0 to 0.7, matching the identical capitalization-based uncertainty tier this codebase already accepts for JS/TS's Foo.create() factory heuristic, and added a test locking in the one guarantee that does hold: the wrong guess never fabricates an edge to a nonexistent node (it drops the edge, same as the pre-#2474 baseline for this specific ambiguous shape — not a regression, just not a full fix of an unrelated, harder problem).

@carlos-alm

Copy link
Copy Markdown
Contributor Author

@greptileai

@carlos-alm
carlos-alm merged commit 4431aa4 into main Aug 18, 2026
30 checks passed
@carlos-alm
carlos-alm deleted the fix/issue-2474 branch August 18, 2026 12:43
@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 constructor-call typing not seeded (svc.method() unresolved)

1 participant