mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
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:
@@ -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
|
||||
|
||||
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
@@ -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`.
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user