mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-16 20:53:57 +08:00
837a1866f1
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>
201 lines
6.6 KiB
TypeScript
201 lines
6.6 KiB
TypeScript
/**
|
|
* 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']);
|
|
});
|
|
});
|