Skip to content

fix(dart): a call through an import prefix reaches only what that import exports - #2389

Merged
colbymchenry merged 1 commit into
mainfrom
claude/modest-elgamal-61b4df
Oct 6, 2026
Merged

colbymchenry merged 1 commit into
mainfrom
claude/modest-elgamal-61b4df

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 6, 2026 •

Copy link
Copy Markdown
Owner

Summary

A Dart call written through an import prefix — http.get(uri) after import 'package:http/http.dart' as http; — was resolved like a method call on a receiver named http, so it linked to any project member that fit the name. On riverpod, 25 calls in the website docs (http.get ×21, http.post ×4) went to docs examples' Http::get / Http::post through matchMethodCall's capitalized-receiver guess. Meanwhile calls through a prefix into the project's own libraries (utils.Listener(), create.main()) linked nothing, because only methods were considered.

Builds on the Dart library model from #2386 (dart-libraries.ts).

Fix

  • p.y(…): matchDartPrefixedCall runs ahead of the name-matching strategies for a Dart X.y call ref. When X is an import prefix of the caller's library (as X or deferred as X; a part uses its library's imports), y can only be a top-level declaration exported by the imports with that prefix (dartPrefixSees: export chains, show / hide). That declaration is linked at 0.9 (resolvedBy: 'import'), or nothing is linked. There is no member guess.
    • A parameter or local (dart-local-scope's scanner) or a member of the enclosing type that shares the prefix's name still hides the prefix.
    • The call site must read X.y. Both extractors record const p.Box.named() as p.named because they take the first type_identifier, so that call now links nothing instead of a top-level named.
  • Chains headed by a prefix (p.Box.create(), p.Report.empty()), which arrive as bare chain links: the head now resolves among the declarations the prefix brings in. A static member is looked up on that type's own declaration (DartValue.owner), not on any type of the same name. Before, the chain typer dropped the prefix and took any project type of that name, so http.Client.fromEnv() reached a project Client::fromEnv.
  • p.Box<int>.named(…) is a constructor_invocation and arrives as Box.named with its column on the prefix. matchDartTypeMemberCall now restricts the owner to the type the prefix brings in, or links nothing.
  • dart-libraries.ts: new dartImportPrefixes(file), the prefixes declared by the file's library imports, following the part chain (also where no pubspec says what the package is) and memoized. It replaces name-matcher's regex reader (DartCode.prefixes), which missed a part's library prefixes and imports with conditional URIs. It also replaces fix(dart): an annotation links the constant or constructor it names, even behind a comment #2390's isDartImportPrefix, which did the same job: matchDartAnnotation now reads prefixes through it, so the library model has one prefix reader.
  • dart-local-scope.ts: new isDartLocallyBound(file, name, offset).

Validation

Native kernel loaded, main d2fb323e vs this branch, rrousselGit/riverpod @4ba1be2 and felangel/bloc @b9be1e2, edges paired by call site:

repo edges before after removed added re-targeted
riverpod 84,028 84,097 25 94 0
bloc 17,713 17,716 0 3 0
  • Removed (25): all are http.get / http.post in riverpod's website docs, previously linked to Http::get / Http::post (instance-method 0.8). package:http is not in the repository.
  • Added (97): every target was checked against the prefix with dartPrefixSees.
    • riverpod, 86 instantiates:
      • internals.ElementId / ContainerId / OriginId / ProviderId in riverpod_devtool, reached through hooks_riverpod's export of riverpod's devtool.dart. This includes a .g.dart part using its library's prefix.
      • utils.Listener / ErrorListener / DeferredNotifier in two transformer tests that import both ../matrix.dart as utils and ../utils.dart as utils.
    • riverpod, 8 calls: create.main() and similar in the benchmarks, and utils.throwsProviderException.
    • bloc, 3 calls: app.main() and replay_*_test.main().
  • Unchanged: riverpod's one existing prefixed chain edge (internals.Notification.fromJson). Prefixed calls into external packages stay unlinked: 105 in riverpod, 120 in bloc.
  • No annotation (decorates) edge changes: using dartImportPrefixes in place of isDartImportPrefix leaves fix(dart): an annotation links the constant or constructor it names, even behind a comment #2390's results as they were.
  • The changed call sites are identical on d2fb323e, on 631df013, on 99246848 (where riverpod goes from 85,482 to 85,551 edges and bloc from 18,354 to 18,357) and on 249d9a86 plus fix(dart): a bare call or type name reaches only what its library can see #2386.
  • Cost: in riverpod's resolve profile the new stage takes about 0.06 s in total over about 31k Dart calls, 1–2 µs for each call that isn't through a prefix. Interleaved whole-resolve timings are within noise.

Tests

Not in this PR

  • An extraction quirk in both the TS extractor and the kernel: const p.Foo.named() is recorded as p.named, and const p.Foo() / new p.Foo() as p. Fixing it needs a kernel change. This PR only makes sure those refs link nothing.
  • In a file with no pubspec.yaml above it, the library model can't say what a prefix imports. There, as for unprefixed calls, a prefixed call links a top-level declaration of that name when the project has exactly one. riverpod and bloc have no such files with prefixed imports.

🤖 Generated with Claude Code

…ort exports

A Dart call written through an import prefix - `http.get(uri)` after
`import 'package:http/http.dart' as http;` - was resolved like a method call
on a receiver named `http`, so it linked to any project member that fit the
name: riverpod's docs' `http.get(...)` / `http.post(...)` calls went to a docs
example's `Http::get` / `Http::post` by the receiver's capitalized name, and
`fmt.describe(...)` to a class's `describe` method.

Now `p.name(...)` calls a top-level declaration a library imported with that
prefix exports (export chains, show / hide and parts followed through the
library model), or nothing when the prefixed library is a package outside the
repository. A constructor call (`p.Widget()`) instantiates the prefixed
library's class. The same holds where the prefix heads a chain -
`p.Box.create()`, `p.Report.empty()` - and for `p.Box<int>.named(...)`, which
arrives as `Box.named`: the type is the one the prefix brings in. A
parameter, local or member named like the prefix still hides it, and
`const p.Box.named()`, which the extractors record as `p.named`, links
nothing.

The library model's one prefix reader is `dartImportPrefixes`: Dart
annotations (#2390's `matchDartAnnotation`) read prefixes through it too, in
place of the `isDartImportPrefix` it had alongside.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@colbymchenry
colbymchenry force-pushed the claude/modest-elgamal-61b4df branch from c0da73f to 0d6e90c Compare October 6, 2026 17:35
@colbymchenry
colbymchenry merged commit 837a186 into main Oct 6, 2026
@colbymchenry
colbymchenry deleted the claude/modest-elgamal-61b4df branch October 10, 2026 06:15
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