Skip to content

fix(dart): a bare call to a parameter or local links nothing - #2379

Merged
colbymchenry merged 4 commits into
mainfrom
fix/dart-local-call-scope
Oct 6, 2026
Merged

colbymchenry merged 4 commits into
mainfrom
fix/dart-local-call-scope

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

What

A receiver-less Dart call, or a function passed by name, whose name a parameter, a local variable, a for/catch variable or a pattern variable around it binds now links nothing. Before, it linked to whatever same-named symbol the name matcher found: a top-level function or constant in a file the caller doesn't import, or a member the class inherits. A local function declared around the call still links, and this.create() / x.create() are untouched.

New src/resolution/dart-local-scope.ts, applied at ReferenceResolver.resolveOne next to gateTypeParameter, so every strategy's result obeys it. Per file it reads the source, with comments, string text and type arguments blanked, and records:

  • parameter lists a body follows ({ … }, => …, a constructor's initializer list) of functions, methods, closures and catch clauses, bound over the body;
  • the variables a for head declares, over the loop;
  • locals (final x = …, Foo? x;, void Function() x = …, final (a, b) = …, case Foo(:final x)), from the declaration to the block's end;
  • local functions, which win when they are the nearest binding.

Fields and top-level variables are not locals.

Why

On rrousselGit/riverpod @4ba1be2, 48 create(ref, arg) calls linked at exact-match 0.9 to final create = Mutation<…>() in website/docs/concepts2/mutations/generic.dart, which none of them import. They came from $FunctionalFamilyOverride::overrideWith's function-typed parameter, _Cache::call, ExpandoUtils::upsert, the generated overrideWiths in sync.g.dart, and the test matrix's provider factories.

Two candidate fixes, evaluated

Scope check (this PR) Import visibility
the 48 create edges removed removed
a parameter hiding an inherited member (bloc's handler emit → Bloc::emit; riverpod's listen, error, onError, fromJson) removed missed
same-file namesakes; functions passed by name removed missed
calls into a library the caller can't see (after this PR: riverpod 649, bloc 7) untouched dropped

I prototyped import visibility offline: a pubspec package map, part/part of, the export closure with show/hide, and prefixed imports skipped.

  • About 600 of riverpod's remaining 649 are generated .g.dart parts whose call to a function in their own library resolved to a namesake in another library, for example async.g.dart's family(…) → annotated.dart.
  • The rest are package: functions (fakeAsync, runApp, unawaited) linked to project stubs.

A veto would drop the part-file edges instead of fixing them. Fixing them needs a pre-ranking filter and a Dart library model that codegraph doesn't have, so that is left for a follow-up.

A/B (native kernel loaded, nodes unchanged)

Measured against main at 5a11cd7, after #2376 and #2377. The removed set is identical, edge for edge, to the one measured on 6421644 before those landed.

repo edges before → after removed added
riverpod @4ba1be2 85,173 → 84,281 892 0
felangel/bloc @b9be1e2 18,307 → 18,146 161 0

I traced every removed edge back to the binding that covers it. All are correct.

  • riverpod
    • 777 were calls into another file's top-level function or constant: locals such as final listener = Listener<int>() mocks (332) and final provider = … / final family = ….family(…) (348), and parameters such as the creates.
    • 106 hit a same-named member: 89 functions passed by name and 17 calls (listen, error, a parameter named set → an enum member, …).
    • 7 were alias-forwarded constructions of locals, and 2 were same-file.
  • bloc
    • 132 were event-handler emit(…) calls that went to Bloc::emit (130) and ReplayBlocMixin::emit (2). Each is the Emitter parameter of on<E>((event, emit) => …) or void _onX(E e, Emitter<S> emit).
    • The rest are parameters and locals: hydrate's onError → BlocBase::onError, testBloc's build → a brick template, and functions passed by name.
  • Recall: none of the remaining cross-library bare calls has a binding of its name in the caller's file.

Cost: the scanner reads all of riverpod's Dart (6.4 MB) in about 0.8 s of CPU. Interleaved index runs stayed within noise: CPU 54.2 / 54.5 / 50.3 s on main against 58.5 / 53.9 / 54.3 s here.

Tests

  • New __tests__/dart-local-call-scope.test.ts runs on kernel and wasm. It fails on main in both and passes here. It pins the riverpod and bloc shapes, plus controls that must keep linking: an inherited emit called bare, this.create(), sibling closure and loop scopes, a nearer local function, final Box? box, and a parameter named set.
  • Full suite with CODEGRAPH_KERNEL_EXPECT=1: 6,152 passed, 0 failed, 80 skipped. tsc is clean.

🤖 Generated with Claude Code

colbymchenry and others added 4 commits October 6, 2026 09:36
A receiver-less Dart call, or a function passed by name, whose name a
parameter, local, loop or catch variable, or pattern variable around it
binds now links nothing, instead of a same-named top-level symbol in
another file or a member the class inherits; a local function declared
there still links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant