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

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>
This commit is contained in:
Colby Mchenry
2026-10-06 18:17:12 +00:00
committed by GitHub
parent d2fb323e51
commit 837a1866f1
6 changed files with 401 additions and 43 deletions
+1
View File
@@ -59,6 +59,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- In Dart, an `import` or `export` now links to the file it names: `package:my_app/src/cart.dart` to that package's `lib/src/cart.dart`, and a relative path to the file it points to from the importing file. Before, only the file name at the end was matched, so `import 'package:flutter/widgets.dart'` linked to whichever project file was called `widgets.dart`, and most imports of your own files didn't link to the file at all, which threw off `codegraph affected` and the files listed as using a file. An import of a Dart SDK library or of a package from outside your project now links nothing, and when packages in one repository share a name, the one the importing app depends on by `path:` is used. Re-index Dart projects after upgrading.
- In Dart, an annotation now links to the constant or constructor it names, read the way it is written: `@riverpod` reaches Riverpod's `const riverpod = Riverpod();`, `@Riverpod(keepAlive: true)` the `Riverpod` class, an annotation written through a class, like `@SomeClass.value` or `@SomeClass.named(…)`, that class's own constant, enum value or constructor, and `@meta.immutable` only what the `meta` import brings in. Before, an annotation was matched like a function decorator, so a method, getter or function that only shared its name won: every `@riverpod` provider in Riverpod's own repository was linked to an unrelated getter called `riverpod`, and `callers` and impact for the real annotation missed them. Re-index Dart projects after upgrading.
- In Dart, an annotation is no longer lost when a comment sits between it and the declaration, like `@override` followed by `// ignore: must_call_super`, a `///` comment written below the annotations, or `@riverpod` followed by an `// expect_lint: …` line. Before, the comment hid the annotation, so a provider written that way was missing from the annotation's `callers` and impact. Re-index Dart projects after upgrading.
- In Dart, a call written through an import prefix, like `http.get(…)` after `import 'package:http/http.dart' as http;`, or `utils.Listener()` and `kit.Box.create()` through a prefix of your own, now links to what the library imported with that prefix exports, following its `export` chains, `show` and `hide`, and to nothing when that library is a package outside your project. Before, the prefix was taken for a variable, so `http.get(…)` could link to an unrelated `Http` class's `get` method, and calls made through a prefix into your own libraries were missing from `callers` and impact. Re-index Dart projects after upgrading.
## [1.6.2] - 2026-10-03
+200
View File
@@ -0,0 +1,200 @@
/**
* A Dart call written through an import prefix — `http.get(uri)` after
* `import 'package:http/http.dart' as http;` — calls a top-level declaration
* the libraries imported with that prefix export, or nothing in the project.
* It is no method call on a receiver named `http`: riverpod's docs' 25
* `http.get(…)` / `http.post(…)` calls went to a docs example's `Http` class.
*
* Runs against the native kernel (when built) and the wasm extractor.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
const FILES: Record<string, string> = {
'pubspec.yaml': 'name: app\n',
// Namesakes of everything the prefixes below name, in a library beside the
// callers — nearer than the libraries the prefixes import.
'lib/legacy.dart': `class Http {
Future<String> get(Uri url) async => '';
}
class Client {
Client();
static Client fromEnv() => Client();
}
class Formatter {
String describe(Object value) => '';
}
class Loader {
int load() => 0;
}
class Report {
Report.empty();
static Report blank() => Report.empty();
}
class Box<T> {
const Box.named();
static Box<int> create() => const Box.named();
}
String describe(Object value) => 'legacy';
int shown() => 0;
`,
'lib/src/format.dart': `String describe(Object value) => '$value';
class Report {
Report();
Report.empty();
static Report blank() => Report();
}
`,
'lib/src/tools.dart': `int secret() => 1;
int open() => 2;
`,
'lib/lazy.dart': `int load() => 1;
`,
'packages/kit/pubspec.yaml': 'name: kit\n',
'packages/kit/lib/kit.dart': `export 'src/impl.dart' show shown, Widget, Box, named;
`,
'packages/kit/lib/src/impl.dart': `int shown() => 1;
int notShown() => 2;
int named() => 3;
class Widget {
Widget();
}
class Box<T> {
const Box.named();
static Box<int> create() => const Box.named();
}
`,
// package:http is not in the repository.
'lib/net.dart': `import 'package:http/http.dart' as http;
Future<void> fetch() async {
await http.get(Uri.parse('https://example.com'));
http.Client();
http.Client.fromEnv();
}
`,
'lib/app.dart': `import 'package:kit/kit.dart' as kit;
import 'src/format.dart' as fmt;
import 'src/tools.dart' as tools hide secret;
import 'lazy.dart' deferred as lazy;
part 'app_part.dart';
String run(Object value) {
fmt.Report();
fmt.Report.empty();
fmt.Report.blank();
kit.shown();
kit.notShown();
kit.Widget();
tools.open();
tools.secret();
final box = kit.Box<int>.named();
kit.Box.create();
const kit.Box.named();
return fmt.describe(value);
}
Future<int> boot() async {
await lazy.loadLibrary();
return lazy.load();
}
`,
'lib/app_part.dart': `part of 'app.dart';
String again(Object value) => fmt.describe(value);
`,
// A parameter named like the prefix is the parameter, there.
'lib/shadow.dart': `import 'package:http/http.dart' as http;
import 'legacy.dart';
Future<String> viaParameter(Http http) => http.get(Uri());
`,
};
/** `<kind> <line> <ref name> -> <target file>:<target qualified name>` for every call out of `file`. */
function callsFrom(cg: CodeGraph, file: string): string[] {
const ids = cg.getNodesInFile(file).map((n) => n.id);
return cg.getOutgoingEdgesFrom(ids)
.filter((e) => e.kind === 'calls' || e.kind === 'instantiates')
.map((e) => {
const t = cg.getNode(e.target)!;
return `${e.kind} ${e.line} ${String(e.metadata?.refName ?? '')} -> ${t.filePath}:${t.qualifiedName}`;
});
}
describe.each(['default', 'wasm'])('Dart calls through an import prefix (%s)', (backend) => {
let root = '';
let cg: CodeGraph | undefined;
let kernel: string | undefined;
const calls = (file: string): string[] => callsFrom(cg!, file);
beforeAll(async () => {
kernel = process.env.CODEGRAPH_KERNEL;
if (backend === 'wasm') process.env.CODEGRAPH_KERNEL = '0';
else delete process.env.CODEGRAPH_KERNEL;
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-dart-prefix-'));
for (const [rel, content] of Object.entries(FILES)) {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
}
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
if (kernel === undefined) delete process.env.CODEGRAPH_KERNEL;
else process.env.CODEGRAPH_KERNEL = kernel;
});
it('links nothing for a call into a package outside the repository', () => {
expect(calls('lib/net.dart')).toEqual([]);
});
it("links a call to the prefixed library's function, not a namesake or a method", () => {
expect(calls('lib/app.dart')).toContain('calls 20 fmt.describe -> lib/src/format.dart:describe');
expect(calls('lib/app.dart')).toContain('calls 25 lazy.load -> lib/lazy.dart:load');
expect(calls('lib/app_part.dart')).toEqual(['calls 3 fmt.describe -> lib/src/format.dart:describe']);
});
it('follows the export chain, show and hide of the prefixed import', () => {
const edges = calls('lib/app.dart');
expect(edges).toContain('calls 12 kit.shown -> packages/kit/lib/src/impl.dart:shown');
expect(edges).toContain('instantiates 14 kit.Widget -> packages/kit/lib/src/impl.dart:Widget');
expect(edges).toContain('calls 15 tools.open -> lib/src/tools.dart:open');
// Not shown by kit.dart, hidden from `tools`, the deferred library's own `loadLibrary`.
expect(edges.filter((e) => / (?:kit\.notShown|tools\.secret|lazy\.loadLibrary) /.test(e))).toEqual([]);
});
it("constructs and calls through the prefixed library's type", () => {
const edges = calls('lib/app.dart');
expect(edges).toContain('instantiates 9 fmt.Report -> lib/src/format.dart:Report');
expect(edges).toContain('calls 10 empty -> lib/src/format.dart:Report::empty');
expect(edges).toContain('calls 11 blank -> lib/src/format.dart:Report::blank');
expect(edges).toContain('calls 17 Box.named -> packages/kit/lib/src/impl.dart:Box::named');
expect(edges).toContain('calls 18 create -> packages/kit/lib/src/impl.dart:Box::create');
});
it('never lands a prefixed call on another library', () => {
expect(calls('lib/app.dart').filter((e) => e.includes('lib/legacy.dart'))).toEqual([]);
// `const kit.Box.named()` is no call of kit's top-level `named`.
expect(calls('lib/app.dart').filter((e) => e.startsWith('calls 19 '))).toEqual([]);
});
it('leaves a name a parameter shadows to the parameter', () => {
expect(calls('lib/shadow.dart')).toEqual(['calls 4 http.get -> lib/legacy.dart:Http::get']);
});
});
+1 -1
View File
@@ -948,7 +948,7 @@ enum emits nothing). Mechanics for dart:
the own library shadowing imports). `@X(…)`: a visible class `X`, not
an extension.
- `@p.x` / `@p.X(…)`, when `p` is an import prefix (dart-libraries.ts
`isDartImportPrefix`, the imports of the file's whole library): the same
`dartImportPrefixes`, the imports of the file's whole library): the same
through that prefix. Otherwise `@T.x` → T's own `constant` /
`enum_member` `x`, and `@T.named(…)` → T's named constructor (#2380's
`isDartConstructor`); `@p.T.x` / `@p.T.named(…)` likewise through `p`.
+24 -11
View File
@@ -368,6 +368,7 @@ interface Memo {
members: Map<string, ReadonlySet<string>>;
exports: Map<string, ReadonlyMap<string, readonly NameFilter[]>>;
scopes: Map<string, FileScope | null>;
prefixes: Map<string, ReadonlySet<string>>;
}
const memos = new WeakMap<ResolutionContext, Memo>();
@@ -386,6 +387,7 @@ function memoFor(context: ResolutionContext): Memo {
members: new Map(),
exports: new Map(),
scopes: new Map(),
prefixes: new Map(),
};
memos.set(context, memo);
}
@@ -763,20 +765,31 @@ export function dartPrefixSees(fromFile: string, prefix: string, declFile: strin
return filters === null || (!name.startsWith('_') && filters.some((f) => lets(f, name)));
}
/**
* The import prefixes written in `fromFile` can stand for — the `p` of
* `import '…' as p` and `deferred as p` — read from the imports of its
* library, which a part (`part of …`) takes from the file it is part of.
*/
export function dartImportPrefixes(fromFile: string, context: ResolutionContext): ReadonlySet<string> {
const memo = memoFor(context);
const hit = memo.prefixes.get(fromFile);
if (hit) return hit;
const prefixes = new Set<string>();
// Records each part's parent, a library in no package included.
libraryOf(fromFile, context, memo);
let file: string | undefined = fromFile;
for (let hops = 0; file !== undefined && hops < 32; file = memo.parentOf.get(file), hops++) {
for (const directive of directivesOf(file, context, memo)?.imports ?? []) {
if (directive.prefix !== null) prefixes.add(directive.prefix);
}
}
memo.prefixes.set(fromFile, prefixes);
return prefixes;
}
/** Whether `declFile` is a file of the library `fromFile` belongs to (false when that library is unknown). */
export function inSameDartLibrary(fromFile: string, declFile: string, context: ResolutionContext): boolean {
if (fromFile === declFile) return true;
const scope = scopeOf(fromFile, context, memoFor(context));
return scope !== null && scope.own.has(declFile);
}
/**
* Whether `name` is an import prefix where `fromFile` is written: an import
* of its library is `as name` (`p` in `p.Foo`, not the `Foo` of `Foo.bar`).
* Where the library is unknown, the file's own imports say.
*/
export function isDartImportPrefix(fromFile: string, name: string, context: ResolutionContext): boolean {
const memo = memoFor(context);
const importers = scopeOf(fromFile, context, memo)?.importers ?? [fromFile];
return importers.some((file) => (directivesOf(file, context, memo)?.imports ?? []).some((d) => d.prefix === name));
}
+10
View File
@@ -77,6 +77,16 @@ export function gateDartLocal(resolved: ResolvedRef | null, ref: UnresolvedRef,
return !nearest || nearest.fn ? resolved : null;
}
/**
* Whether a parameter, local or local function `name` is bound at offset `at`
* of the file's source — where it hides a library name, an import prefix
* among them: `http.get(…)` with a parameter `http` calls the parameter's.
*/
export function isDartLocallyBound(filePath: string, name: string, at: number, context: ResolutionContext): boolean {
const bound = fileScopes(filePath, context)?.names.get(name);
return bound !== undefined && bound.some((scope) => at >= scope.start && at <= scope.end);
}
/**
* Where on its line a reference's name starts. A Dart ref's column sits just
* past the name (a function value's at it); a column counted in bytes misses
+165 -31
View File
@@ -11,7 +11,8 @@ import { UnresolvedRef, ResolvedRef, ResolutionContext, ImportMapping, isSuperty
import { blankStringContents, stripCommentsForRegex } from './strip-comments';
import { JS_BUILT_INS, JS_BUILTIN_METHODS, TS_PRIMITIVE_TYPES } from './js-builtins';
import { SWIFT_TYPE_PATH_CALL, resolveSwiftTypePathCall } from './swift-type-visibility';
import { dartLibrarySees, dartPrefixSees, inSameDartLibrary, isDartImportPrefix } from './dart-libraries';
import { dartImportPrefixes, dartLibrarySees, dartPrefixSees, inSameDartLibrary } from './dart-libraries';
import { isDartLocallyBound } from './dart-local-scope';
import { breakVbTie, isVbMemberInScope, isVbNestedTypeInScope, isVbTypeQualifiedBy, matchVbTypedCall, preferVbProject, sameVbProject } from './vbnet-receivers';
import { isTestPath } from '../search/query-utils';
import { isMinifiedContent } from '../extraction/generated-detection';
@@ -3035,6 +3036,85 @@ function preferOwnDartLibrary(candidates: Node[], ref: UnresolvedRef, context: R
return own.length === 0 ? candidates : candidates.filter((n) => !isDartLibraryDecl(n) || own.includes(n));
}
/**
* A Dart call through an import prefix — `http.get(…)` after `import
* 'package:http/http.dart' as http;`, `fmt.describe(…)`, `kit.Widget()` —
* calls a top-level declaration a library imported with that prefix exports,
* `export` chains and `show` / `hide` followed, or nothing of the project:
* never a member found by its name. riverpod's docs' 25 `http.get(…)` calls
* went to a docs example's `Http::get`, by their receiver's capitalized name.
* Undefined when the receiver is no import prefix there, or a parameter, local
* or member of that name hides it.
*/
function matchDartPrefixedCall(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null | undefined {
const parts = DART_MEMBER_READ.exec(ref.referenceName);
if (!parts || !dartImportPrefixes(ref.filePath, context).has(parts[1]!)) return undefined;
const prefix = parts[1]!;
const name = parts[2]!;
const at = dartPrefixedCallSite(ref, prefix, name, context);
// `const p.Box.named()` reaches here as `p.named`: no call of a top-level `named`.
if (at < 0) return null;
if (isDartNameHidden(prefix, ref, at, context)) return undefined;
const found = dartPrefixedDecls(prefix, name, ref, context);
return found.length === 1 ? { original: ref, targetNodeId: found[0]!.id, confidence: 0.9, resolvedBy: 'import' } : null;
}
/** The top-level Dart declarations named `name` that the imports written with `prefix` bring into the reference's library. */
function dartPrefixedDecls(prefix: string, name: string, ref: UnresolvedRef, context: ResolutionContext): Node[] {
return context.getNodesByName(name).filter((n) => isDartLibraryDecl(n) && dartPrefixSees(ref.filePath, prefix, n.filePath, name, context));
}
/**
* Where `prefix` starts in the code of a Dart call written `prefix.name(…)`,
* or -1 when the call is written otherwise: `const p.Box.named()` arrives as
* `p.named`. (The reference's column sits just past `name`.)
*/
function dartPrefixedCallSite(ref: UnresolvedRef, prefix: string, name: string, context: ResolutionContext): number {
const code = dartCodeOf(ref.filePath, context);
const start = code ? dartNameStart({ ...ref, referenceName: name }, code) : -1;
if (!code || start < 0) return -1;
const text = code.text;
let k = start - 1;
while (k >= 0 && /\s/.test(text[k]!)) k--;
if (text[k] !== '.' || text[k - 1] === '.' || text[k - 1] === '?') return -1;
k--;
while (k >= 0 && /\s/.test(text[k]!)) k--;
const end = k + 1;
while (k >= 0 && /[\w$]/.test(text[k]!)) k--;
if (text.slice(k + 1, end) !== prefix) return -1;
while (k >= 0 && /\s/.test(text[k]!)) k--;
return text[k] === '.' ? -1 : end - prefix.length;
}
/**
* Whether a parameter or local around offset `at`, or a member of the type the
* reference is written in, is named `name`: it hides a library-level name
* there — an import prefix among them — as Dart's scopes nest.
*/
function isDartNameHidden(name: string, ref: UnresolvedRef, at: number, context: ResolutionContext): boolean {
if (isDartLocallyBound(ref.filePath, name, at, context)) return true;
const own = dartEnclosingType(ref, context);
return own !== null && (classMemberType(own, name, context) !== null ||
context.getNodesByName(name).some((n) => n.filePath === own.filePath && n.qualifiedName === `${own.qualifiedName}::${name}`));
}
/**
* The import prefix a Dart call through a type is written with — the `p` of
* `p.Box<int>.named(…)`, which arrives as `Box.named` with its column on the
* prefix — or null.
*/
function dartTypeCallPrefix(typeName: string, member: string, ref: UnresolvedRef, context: ResolutionContext): string | null {
const prefixes = dartImportPrefixes(ref.filePath, context);
if (prefixes.size === 0) return null;
const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1] ?? context.readFile(ref.filePath)?.split(/\r?\n/)[ref.line - 1];
if (line === undefined) return null;
const written = new RegExp(String.raw`(?<![\w$.])([A-Za-z_$][\w$]*)\s*\.\s*${typeName.replace(/\$/g, '\\$')}\s*` +
String.raw`(?:<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?\s*\.\s*${member.replace(/\$/g, '\\$')}(?![\w$])`, 'g');
// The one at the reference's column, else the first: a column counted in bytes misses after non-ASCII text.
const found = [...line.matchAll(written)].filter((m) => prefixes.has(m[1]!));
return (found.find((m) => m.index === ref.column) ?? found[0])?.[1] ?? null;
}
/**
* Whether a bare Dart reference really is receiver-less at its call site. The
* extractor keeps one receiver level, so the later links of a chain —
@@ -3096,7 +3176,7 @@ export function matchDartChainLink(ref: UnresolvedRef, context: ResolutionContex
// Static members are the type's own: `p.Provider.autoDispose(…)` calls the
// constant, as `Provider.autoDispose(…)` does.
if (receiver.static) {
const member = dartStaticMember(receiver.type, ref.referenceName, ref, context);
const member = dartStaticMember(receiver.type, ref.referenceName, ref, context, dartOwnerOf(receiver));
return member && member.kind !== 'enum_member'
? { original: ref, targetNodeId: member.id, confidence: 0.85, resolvedBy: 'qualified-name' } : null;
}
@@ -3127,25 +3207,29 @@ interface DartChainLink { name: string; typeArgs: string[]; call: boolean; index
/**
* What a Dart chain starts from: a name (`x`, `Foo`, `foo(…)`, `Foo<T>(…)`,
* with the keyword written before it — `const`, `new`, `as`, `await`), a
* string literal, or a parenthesized expression.
* with the keyword written before it — `const`, `new`, `as`, `await` — and
* where it starts), a string literal, or a parenthesized expression.
*/
type DartChainHead =
| { kind: 'name'; name: string; typeArgs: string[]; call: boolean; keyword: string }
| { kind: 'name'; name: string; typeArgs: string[]; call: boolean; keyword: string; at: number }
| { kind: 'string' }
| { kind: 'paren'; open: number; close: number };
interface DartChain { head: DartChainHead; links: DartChainLink[] }
/** What a Dart chain evaluates to: an instance of `type`, or (`static`) the type itself, before a static member. */
interface DartValue { type: string; static: boolean; viaSuper?: boolean }
/**
* What a Dart chain evaluates to: an instance of `type`, or (`static`) the
* type itself, before a static member — of the type `owner` declares, when
* the chain says which file that is (`p.Box` names the one `p` brings in).
*/
interface DartValue { type: string; static: boolean; viaSuper?: boolean; owner?: string }
/**
* A Dart file's code with comments and string contents blanked, where each
* line starts, the import prefixes it declares, and the files of its library
* it names: its `part`s and the file it is `part of`.
* line starts, and the files of its library it names: its `part`s and the
* file it is `part of`.
*/
interface DartCode { text: string; lineStarts: number[]; prefixes: Set<string>; parts: string[]; partOf: string | null }
interface DartCode { text: string; lineStarts: number[]; parts: string[]; partOf: string | null }
const DART_CODE = new WeakMap<ResolutionContext, Map<string, DartCode | null>>();
@@ -3160,11 +3244,10 @@ function dartCodeOf(filePath: string, context: ResolutionContext): DartCode | nu
const text = blankDartCode(source);
const lineStarts = [0];
for (let i = 0; i < text.length; i++) if (text[i] === '\n') lineStarts.push(i + 1);
const prefixes = new Set([...text.matchAll(/^\s*import\s+['"][^'"]*['"]\s+(?:deferred\s+)?as\s+([A-Za-z_$][\w$]*)/gm)].map((m) => m[1]!));
const sibling = (uri: string): string => path.posix.normalize(path.posix.join(path.posix.dirname(filePath), uri));
const parts = [...source.matchAll(/^\s*part\s+['"]([^'"]+)['"]\s*;/gm)].map((m) => sibling(m[1]!));
const partOf = /^\s*part\s+of\s+['"]([^'"]+)['"]\s*;/m.exec(source)?.[1];
code = { text, lineStarts, prefixes, parts, partOf: partOf ? sibling(partOf) : null };
code = { text, lineStarts, parts, partOf: partOf ? sibling(partOf) : null };
}
// Refs arrive file by file, so a few files' worth is enough.
if (memo.size >= 64) memo.delete(memo.keys().next().value!);
@@ -3350,7 +3433,7 @@ function readDartChain(text: string, end: number): DartChain | null {
continue;
}
const keyword = /([A-Za-z_]\w*)$/.exec(text.slice(Math.max(0, j - 10), j + 1))?.[1] ?? '';
return { head: { kind: 'name', name, typeArgs, call, keyword }, links: links.reverse() };
return { head: { kind: 'name', name, typeArgs, call, keyword, at: i + 1 }, links: links.reverse() };
}
return null;
}
@@ -3364,17 +3447,18 @@ function typeDartChain(chain: DartChain, ref: UnresolvedRef, context: Resolution
if (head.kind === 'string') value = { type: 'String', static: false };
else if (head.kind === 'paren') value = typeDartParen(head, ref, context, depth);
else {
let name = head;
// `p.Provider.autoDispose`: an import prefix names a library, so the chain
// starts at the link after it.
// starts at the link after it, which names what that library exports.
const first = links[0];
if (!name.call && name.typeArgs.length === 0 && first && !first.index && dartCodeOf(ref.filePath, context)?.prefixes.has(name.name)) {
name = { kind: 'name', name: first.name, typeArgs: first.typeArgs, call: first.call, keyword: '' };
if (!head.call && head.typeArgs.length === 0 && first && !first.index && dartImportPrefixes(ref.filePath, context).has(head.name) &&
!isDartNameHidden(head.name, ref, head.at, context)) {
value = typeDartPrefixedHead(head.name, first, ref, context, depth);
links = links.slice(1);
} else {
// `const Foo.named(…)`: the named constructor is the head's own link.
if ((head.keyword === 'const' || head.keyword === 'new') && !head.call && links[0]?.call) links = links.slice(1);
value = typeDartHead(head, ref, context, depth);
}
// `const Foo.named(…)`: the named constructor is the head's own link.
if ((name.keyword === 'const' || name.keyword === 'new') && !name.call && links[0]?.call) links = links.slice(1);
value = typeDartHead(name, ref, context, depth);
}
for (const link of links) {
// Every Dart object's `toString()` returns a String, whatever it is.
@@ -3397,10 +3481,15 @@ function typeDartChain(chain: DartChain, ref: UnresolvedRef, context: Resolution
/** Whether the type a Dart chain has reached declares the member a link names. */
function dartLinkDeclared(value: DartValue, link: DartChainLink, ref: UnresolvedRef, context: ResolutionContext): boolean {
return value.static
? dartStaticMember(value.type, link.name, ref, context) !== null
? dartStaticMember(value.type, link.name, ref, context, dartOwnerOf(value)) !== null
: dartMemberOf(value.type, link.name, ref, context, (n) => !isDartSetter(n, context), value.viaSuper ? 1 : 0) !== null;
}
/** Which of the types named like a chain's value it is: the one its `owner` file declares, when the chain said. */
function dartOwnerOf(value: DartValue): ((type: Node) => boolean) | undefined {
return value.owner === undefined ? undefined : (type) => type.filePath === value.owner;
}
/** `(x as Foo)` is a Foo; any other parenthesized expression is what its last operand is: `(a ?? Todo(…))`. */
function typeDartParen(head: { open: number; close: number }, ref: UnresolvedRef, context: ResolutionContext, depth: number): DartValue | null {
const code = dartCodeOf(ref.filePath, context);
@@ -3459,11 +3548,36 @@ function typeDartHead(
return type ? { type, static: false } : null;
}
/**
* What the first link after an import prefix evaluates to — `p.Provider`
* before a static member, `p.Report(…)`, `p.make(…)`, `p.config` — read from
* the top-level declarations the prefixed import brings in, and nothing when
* it brings in none of that name: a package outside the repository, whatever
* the project declares under the name elsewhere.
*/
function typeDartPrefixedHead(prefix: string, link: DartChainLink, ref: UnresolvedRef, context: ResolutionContext, depth: number): DartValue | null {
const found = dartPrefixedDecls(prefix, link.name, ref, context);
const types = found.filter((n) => DART_TYPE_KINDS.has(n.kind));
if (types.length > 0) {
return { type: link.name, static: !link.call, owner: types.length === 1 ? types[0]!.filePath : undefined };
}
if (found.length !== 1) return null;
const decl = found[0]!;
if (decl.kind === 'function') {
const type = link.call ? dartReturnType(decl, link.typeArgs, context) : null;
return type ? { type, static: false } : null;
}
if (decl.kind !== 'constant' && decl.kind !== 'variable') return null;
const held = dartConstantType(decl, ref, context, depth);
if (!held) return null;
return link.call ? dartCallResult(held, link.typeArgs, ref, context) : { type: held, static: false };
}
/** What one link of a Dart chain makes of the value before it. */
function typeDartLink(value: DartValue, link: DartChainLink, ref: UnresolvedRef, context: ResolutionContext, depth: number): DartValue | null {
if (link.index) return null;
if (value.static) {
const member = dartStaticMember(value.type, link.name, ref, context);
const member = dartStaticMember(value.type, link.name, ref, context, dartOwnerOf(value));
if (!member) {
// `BlocProvider.value(…)`, `AsyncValue.data(…)`: a constructor the index holds no node for.
return link.call && context.getNodesByName(value.type).some((n) => n.language === 'dart' && DART_TYPE_KINDS.has(n.kind))
@@ -3548,10 +3662,17 @@ function dartEnclosingType(ref: UnresolvedRef, context: ResolutionContext): Node
* The static member `name` the Dart type `typeName` itself declares — a
* static method or getter, a named constructor or factory, a static
* constant, an enum value — preferring the call site's file, then the
* nearest. Static members are not inherited.
* nearest. Static members are not inherited. `isOwner` narrows the types of
* that name to the one meant (the one an import prefix brings in).
*/
function dartStaticMember(typeName: string, name: string, ref: UnresolvedRef, context: ResolutionContext): Node | null {
const owners = context.getNodesByName(typeName).filter((n) => n.language === 'dart' && DART_TYPE_KINDS.has(n.kind));
function dartStaticMember(
typeName: string,
name: string,
ref: UnresolvedRef,
context: ResolutionContext,
isOwner: (type: Node) => boolean = () => true,
): Node | null {
const owners = context.getNodesByName(typeName).filter((n) => n.language === 'dart' && DART_TYPE_KINDS.has(n.kind) && isOwner(n));
if (owners.length === 0) return null;
const found = context.getNodesByName(name).filter((m) => m.language === 'dart' &&
(m.kind === 'method' || m.kind === 'function' || m.kind === 'constant' || m.kind === 'enum_member') &&
@@ -4077,14 +4198,14 @@ export function matchDartAnnotation(ref: UnresolvedRef, context: ResolutionConte
const { names, call } = written;
let pool: Node[];
let resolvedBy: ResolvedRef['resolvedBy'] = 'exact-match';
if (names.length === 1 || (names.length === 2 && isDartImportPrefix(ref.filePath, names[0]!, context))) {
if (names.length === 1 || (names.length === 2 && dartImportPrefixes(ref.filePath, context).has(names[0]!))) {
const prefix = names.length === 2 ? names[0]! : null;
const own = prefix === null && !call ? dartEnclosingStatic(name, ref, context) : null;
if (own) return { original: ref, targetNodeId: own.id, confidence: 0.9, resolvedBy };
pool = context.getNodesByName(name).filter((n) => isDartLibraryDecl(n) &&
(call ? isDartAnnotationClass(n, context) : n.kind === 'constant') && isDartTopLevelVisible(n, ref, context, prefix));
if (prefix === null) pool = preferOwnDartLibrary(pool, ref, context);
} else if (names.length === 2 || (names.length === 3 && isDartImportPrefix(ref.filePath, names[0]!, context))) {
} else if (names.length === 2 || (names.length === 3 && dartImportPrefixes(ref.filePath, context).has(names[0]!))) {
const prefix = names.length === 3 ? names[0]! : null;
let owners = context.getNodesByName(names[names.length - 2]!).filter((n) =>
isDartLibraryDecl(n) && DART_TYPE_KINDS.has(n.kind) && isDartTopLevelVisible(n, ref, context, prefix));
@@ -4194,8 +4315,10 @@ function dartExtensionMemberOf(typeName: string, name: string, ref: UnresolvedRe
* it, and the SDK's `Uri.parse(…)`, past bloc_lint's `extension on Uri`, to a
* project `parse`. A static constant's callee is the value it holds (`static
* const autoDispose = AutoDisposeFutureProviderBuilder();`). The member is
* found as a chain's static link finds it (dartStaticMember). Undefined when
* the receiver names no Dart type of the project.
* found as a chain's static link finds it (dartStaticMember). Written through
* an import prefix (`p.Box<int>.named(…)`, which arrives as `Box.named`), the
* type is the one that import brings in, if any. Undefined when the receiver
* names no Dart type of the project.
*/
function matchDartTypeMemberCall(
typeName: string,
@@ -4203,8 +4326,11 @@ function matchDartTypeMemberCall(
ref: UnresolvedRef,
context: ResolutionContext,
): ResolvedRef | null | undefined {
if (!context.getNodesByName(typeName).some((n) => n.language === 'dart' && DART_TYPE_KINDS.has(n.kind))) return undefined;
const own = dartStaticMember(typeName, member, ref, context);
const types = context.getNodesByName(typeName).filter((n) => n.language === 'dart' && DART_TYPE_KINDS.has(n.kind));
const prefix = types.length > 0 || /^[_$]*[A-Z]/.test(typeName) ? dartTypeCallPrefix(typeName, member, ref, context) : null;
const isOwner = (n: Node): boolean => prefix === null || dartPrefixSees(ref.filePath, prefix, n.filePath, typeName, context);
if (!types.some(isOwner)) return prefix === null ? undefined : null;
const own = dartStaticMember(typeName, member, ref, context, isOwner);
return own && own.kind !== 'enum_member'
? { original: ref, targetNodeId: own.id, confidence: 0.85, resolvedBy: 'qualified-name' } : null;
}
@@ -12222,6 +12348,14 @@ function matchReferenceInner(
return nmTimed('swiftTypePath', ref, () => resolveSwiftTypePathCall(ref, context));
}
// A Dart call through an import prefix (`http.get(…)`) is to what the
// prefixed import exports, or to nothing: the strategies below would take
// the prefix for a receiver and guess a method by its name.
if (ref.language === 'dart' && ref.referenceKind === 'calls') {
const prefixed = nmTimedT('dartPrefixed', ref, () => matchDartPrefixedCall(ref, context));
if (prefixed !== undefined) return prefixed;
}
// Try strategies in order of confidence
let result: ResolvedRef | null;