mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-17 05:07:37 +08:00
fix(dart): a library links the files its part directives name (#2391)
A Dart `part 'x.g.dart';` now mints an `import` node (name = the URI,
signature = the directive) and an `imports` ref from the library's file
node, in both extractors. The ref resolves through `dartDirectiveFile` by
the rules an import or export follows, so the library's file links the
part's file. A library and its parts are one library, so a change to a
part, generated or hand-written, now reaches the library and what imports
it in `codegraph affected` and the files listed as using the part. Before,
neither `part` nor `part of` left a trace, and the two files were joined
only by whatever symbol edges happened to cross between them: riverpod's
framework.dart, which holds nothing but `part` lines, by none.
`part of` still records nothing: the library's edge already joins the two
files, and a part -> library edge would list every pair on both import
rails of both files and make each a file 2-cycle.
The kernel's directive arm now goes on into the directive's children, as
the TS ladder does. Its `import_or_export` arm used to return first, so an
annotated directive's arguments (`@Tag(f) import 'a.dart';`) were fn-ref
candidates on the wasm path only.
Validation on rrousselGit/riverpod @4ba1be2 and felangel/bloc @b9be1e2
with the kernel loaded, base 837a1866:
- bloc: +53 import nodes, +53 contains, +53 imports (library -> part).
- riverpod: +294 import nodes, +294 contains, +258 imports, +36 refs left
unresolved (generated parts that are not committed).
- Nothing removed; kernel and wasm dumps byte-identical; parity sweeps
0 diffs. Every added edge sits on a `part` line whose URI names the
target, and the target's `part of` names the library back (311/311).
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -57,6 +57,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In Dart, a comment inside a chain of calls no longer loses part of it: a call with a comment before its arguments, like `box.grow /* by */ (3)`, now links to the method it calls, and a getter read or a static access written after a comment, like `Config //` followed by `.instance` on the next line (the empty `//` keeps `dart format` from joining the lines), now counts as a use. Re-index Dart projects after upgrading.
|
||||
- In Dart, a call, a constructor call or a type written without a receiver now links only to a declaration the file can see: one in its own library (the file and its `part` files) or one exported by a library it imports, with `export` chains, `show`, `hide` and `as` prefixes followed the way Dart follows them. So a generated `.g.dart` file reaches its own library's function or type instead of a same-named one in a neighbouring file, a call to a package function such as `fakeAsync`, `runApp` or `unawaited` no longer links to an unrelated project function of that name, and a bare call is never taken for another type's enum value or static constant. Re-index Dart projects after upgrading.
|
||||
- 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, a `part` directive, like `part 'user.g.dart';` or `part 'src/widgets.dart';`, now links a library to the part's file the way an import does. A library and its parts are one library, so a change to a part, whether a generated `.g.dart` or `.freezed.dart` file or one you wrote, now reaches the library and the code that imports it in `codegraph affected` and in the files listed as using the part. Before, the two files were connected only when a call or a type happened to cross between them, which a library made only of `part` lines never has. A part your project doesn't have, like a generated file that isn't committed, links nothing. 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.
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* A Dart library names each of its parts with `part '<uri>';`, and that links
|
||||
* the library's file to the part's: an `imports` edge, by the URI rules an
|
||||
* import or export follows. A part and its library are one library, so a
|
||||
* change to the part is a change to what every importer of the library sees.
|
||||
* Before, neither `part` nor `part of` left a trace, and the two files were
|
||||
* joined only by whatever symbol edges happened to cross between them —
|
||||
* riverpod's framework.dart, which holds nothing but its parts, by none, so a
|
||||
* change to one of them reached no test that imports riverpod. `part of`
|
||||
* still adds nothing: the library's edge already joins the two files.
|
||||
*/
|
||||
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';
|
||||
import { buildFile } from '../src/ui-server/api/file';
|
||||
|
||||
function writeTree(root: string, files: Record<string, string>): void {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/** `<line> -> <target>` for every `imports` edge out of `file`: a file by its path, anything else by kind, file and name. */
|
||||
function importsFrom(cg: CodeGraph, file: string): string[] {
|
||||
const ids = cg.getNodesInFile(file).map((n) => n.id);
|
||||
return cg.getOutgoingEdgesFrom(ids, ['imports'])
|
||||
.map((e) => {
|
||||
const t = cg.getNode(e.target)!;
|
||||
return `${e.line} -> ${t.kind === 'file' ? t.filePath : `${t.kind} ${t.filePath}#${t.name}`}`;
|
||||
})
|
||||
.sort();
|
||||
}
|
||||
|
||||
/** The URIs of `file`'s imports left unresolved. */
|
||||
function unresolvedImportsOf(cg: CodeGraph, file: string): string[] {
|
||||
const fileNode = cg.getNodesInFile(file).find((n) => n.kind === 'file')!;
|
||||
return cg.getUnresolvedReferencesFrom(fileNode.id)
|
||||
.filter((r) => r.referenceKind === 'imports')
|
||||
.map((r) => r.referenceName)
|
||||
.sort();
|
||||
}
|
||||
|
||||
/** `<name> | <signature>` of each `import` node in `file`. */
|
||||
function importNodesIn(cg: CodeGraph, file: string): string[] {
|
||||
return cg.getNodesInFile(file)
|
||||
.filter((n) => n.kind === 'import')
|
||||
.map((n) => `${n.name} | ${n.signature}`)
|
||||
.sort();
|
||||
}
|
||||
|
||||
/** Every file a change to `file` reaches through its dependents, as `codegraph affected` walks them. */
|
||||
function reachedFrom(cg: CodeGraph, file: string): string[] {
|
||||
const seen = new Set([file]);
|
||||
const queue = [file];
|
||||
while (queue.length > 0) {
|
||||
for (const dependent of cg.getFileDependents(queue.shift()!)) {
|
||||
if (seen.has(dependent)) continue;
|
||||
seen.add(dependent);
|
||||
queue.push(dependent);
|
||||
}
|
||||
}
|
||||
seen.delete(file);
|
||||
return [...seen].sort();
|
||||
}
|
||||
|
||||
describe('Dart part directives', () => {
|
||||
let root = '';
|
||||
let cg: CodeGraph;
|
||||
|
||||
beforeAll(async () => {
|
||||
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-dart-parts-'));
|
||||
writeTree(root, {
|
||||
// riverpod's framework.dart: a library made of nothing but its parts,
|
||||
// one in a subdirectory, one generated; a barrel exports it and a test
|
||||
// imports the barrel.
|
||||
'pubspec.yaml': 'name: app\n',
|
||||
'lib/src/framework.dart': `import 'dart:async';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
part 'core/element.dart';
|
||||
part "core/container.dart";
|
||||
part 'framework.g.dart';
|
||||
`,
|
||||
'lib/src/core/element.dart': `part of '../framework.dart';
|
||||
|
||||
class Element {}
|
||||
`,
|
||||
'lib/src/core/container.dart': `part of '../framework.dart';
|
||||
|
||||
class Container {}
|
||||
`,
|
||||
'lib/src/framework.g.dart': `// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'framework.dart';
|
||||
|
||||
String _$frameworkHash() => 'abc';
|
||||
`,
|
||||
'lib/app.dart': `export 'src/framework.dart';
|
||||
`,
|
||||
'test/framework_test.dart': `import 'package:app/app.dart';
|
||||
|
||||
void main() {}
|
||||
`,
|
||||
// A part named by a package: URI, and a part of a library named by its library name.
|
||||
'packages/kit/pubspec.yaml': 'name: kit\n',
|
||||
'packages/kit/lib/kit.dart': `part 'package:kit/src/widgets.dart';
|
||||
`,
|
||||
'packages/kit/lib/src/widgets.dart': `part of 'package:kit/kit.dart';
|
||||
|
||||
class Widget {}
|
||||
`,
|
||||
'lib/legacy/models.dart': `library app.models;
|
||||
|
||||
part 'user.dart';
|
||||
`,
|
||||
'lib/legacy/user.dart': `part of app.models;
|
||||
|
||||
class User {}
|
||||
`,
|
||||
// A generated part nobody committed, and another app's file of that name.
|
||||
'examples/a/pubspec.yaml': 'name: a\n',
|
||||
'examples/a/lib/main.dart': `part 'main.g.dart';
|
||||
|
||||
void main() {}
|
||||
`,
|
||||
'examples/b/pubspec.yaml': 'name: b\n',
|
||||
'examples/b/lib/main.dart': `part 'main.g.dart';
|
||||
|
||||
void main() {}
|
||||
`,
|
||||
'examples/b/lib/main.g.dart': `part of 'main.dart';
|
||||
`,
|
||||
});
|
||||
cg = await CodeGraph.init(root, { index: true });
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cg?.close();
|
||||
if (root) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('links a library to each part it names, as an import node and an imports edge', () => {
|
||||
expect(importsFrom(cg, 'lib/src/framework.dart')).toEqual([
|
||||
'5 -> lib/src/core/element.dart',
|
||||
'6 -> lib/src/core/container.dart',
|
||||
'7 -> lib/src/framework.g.dart',
|
||||
]);
|
||||
expect(unresolvedImportsOf(cg, 'lib/src/framework.dart')).toEqual(['dart:async', 'package:meta/meta.dart']);
|
||||
expect(importNodesIn(cg, 'lib/src/framework.dart')).toEqual([
|
||||
'core/container.dart | part "core/container.dart";',
|
||||
'core/element.dart | part \'core/element.dart\';',
|
||||
'dart:async | import \'dart:async\';',
|
||||
'framework.g.dart | part \'framework.g.dart\';',
|
||||
'package:meta/meta.dart | import \'package:meta/meta.dart\';',
|
||||
]);
|
||||
});
|
||||
|
||||
it('resolves a part named by a package: URI or under a library name', () => {
|
||||
expect(importsFrom(cg, 'packages/kit/lib/kit.dart')).toEqual(['1 -> packages/kit/lib/src/widgets.dart']);
|
||||
expect(importsFrom(cg, 'lib/legacy/models.dart')).toEqual(['3 -> lib/legacy/user.dart']);
|
||||
});
|
||||
|
||||
it('records nothing for `part of`', () => {
|
||||
for (const part of ['lib/src/core/element.dart', 'lib/src/framework.g.dart', 'packages/kit/lib/src/widgets.dart', 'lib/legacy/user.dart']) {
|
||||
expect(importsFrom(cg, part), part).toEqual([]);
|
||||
expect(unresolvedImportsOf(cg, part), part).toEqual([]);
|
||||
expect(importNodesIn(cg, part), part).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it("leaves a part the project doesn't have unresolved, never another file of its name", () => {
|
||||
expect(importsFrom(cg, 'examples/a/lib/main.dart')).toEqual([]);
|
||||
expect(unresolvedImportsOf(cg, 'examples/a/lib/main.dart')).toEqual(['main.g.dart']);
|
||||
expect(importsFrom(cg, 'examples/b/lib/main.dart')).toEqual(['1 -> examples/b/lib/main.g.dart']);
|
||||
});
|
||||
|
||||
it('makes a change to a part reach what depends on its library', () => {
|
||||
expect(cg.getFileDependents('lib/src/core/element.dart')).toEqual(['lib/src/framework.dart']);
|
||||
expect(cg.getFileDependencies('lib/src/framework.dart').sort()).toEqual([
|
||||
'lib/src/core/container.dart',
|
||||
'lib/src/core/element.dart',
|
||||
'lib/src/framework.g.dart',
|
||||
]);
|
||||
expect(reachedFrom(cg, 'lib/src/core/element.dart')).toEqual([
|
||||
'lib/app.dart',
|
||||
'lib/src/framework.dart',
|
||||
'test/framework_test.dart',
|
||||
]);
|
||||
});
|
||||
|
||||
it("shows the parts on the library's import rail and the library on each part's", () => {
|
||||
const library = buildFile(cg, root, 'lib/src/framework.dart') as {
|
||||
imports: { items: Array<{ file: string }> };
|
||||
dependencies: string[];
|
||||
};
|
||||
expect(library.imports.items.map((row) => row.file).sort()).toEqual([
|
||||
'lib/src/core/container.dart',
|
||||
'lib/src/core/element.dart',
|
||||
'lib/src/framework.g.dart',
|
||||
]);
|
||||
const part = buildFile(cg, root, 'lib/src/framework.g.dart') as {
|
||||
imports: { items: unknown[] };
|
||||
importedBy: { items: Array<{ file: string }> };
|
||||
unresolvedImports: unknown[];
|
||||
dependents: string[];
|
||||
};
|
||||
expect(part.importedBy.items.map((row) => row.file)).toEqual(['lib/src/framework.dart']);
|
||||
expect(part.imports.items).toEqual([]);
|
||||
expect(part.unresolvedImports).toEqual([]);
|
||||
expect(part.dependents).toEqual(['lib/src/framework.dart']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
/// A library's directives: imports and exports in every form, and its parts.
|
||||
library torture.directives;
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:torture/other.dart' as other show OtherClass, otherFn;
|
||||
import 'package:torture/hidden.dart' hide Hidden;
|
||||
import 'src/conditional_stub.dart'
|
||||
if (dart.library.io) 'src/conditional_io.dart'
|
||||
if (dart.library.js_interop) 'src/conditional_web.dart';
|
||||
import 'src/deferred.dart' deferred as lazy;
|
||||
|
||||
@Tag(viaImport)
|
||||
import 'src/annotated.dart';
|
||||
|
||||
export 'src/exported.dart' show Exported;
|
||||
@Tag(viaExport)
|
||||
export 'src/annotated_export.dart';
|
||||
|
||||
// Parts: in a subdirectory, above this one, by package: URI, in each quote
|
||||
// style, with comments inside and around, and annotated.
|
||||
part 'directives.g.dart';
|
||||
part "src/directives_part.dart";
|
||||
part '../shared_part.dart';
|
||||
part 'package:torture/src/package_part.dart';
|
||||
part '''triple_part.dart''';
|
||||
part /* generated */ 'commented_part.dart'; // trailing
|
||||
part
|
||||
'wrapped_part.dart';
|
||||
@Tag(viaPart)
|
||||
part 'annotated_part.dart';
|
||||
@Tag([viaPartList, viaPart])
|
||||
part 'annotated_list_part.dart';
|
||||
|
||||
class Tag {
|
||||
const Tag(Object value);
|
||||
}
|
||||
|
||||
void viaImport() {}
|
||||
void viaExport() {}
|
||||
void viaPart() {}
|
||||
void viaPartList() {}
|
||||
|
||||
String describe() => 'directives';
|
||||
@@ -0,0 +1,10 @@
|
||||
/// A part of a library it names by library name, not by URI: the `part of`
|
||||
/// directive records nothing.
|
||||
@Tag(helper)
|
||||
part of torture.directives;
|
||||
|
||||
class PartOfName {
|
||||
void run() => helper();
|
||||
}
|
||||
|
||||
void helper() {}
|
||||
@@ -35,8 +35,10 @@
|
||||
* by every sibling step; TortureAnnotatedDocs.dart: the dartdoc above a
|
||||
* member's annotations, read past them, and what still ends the walk;
|
||||
* TortureAnnotatedComments.dart: the comments between annotations and their
|
||||
* declaration, stepped over by the decorator scan) and their CRLF variants
|
||||
* (derived in-memory — #1329), plus defer and generated-file pins.
|
||||
* declaration, stepped over by the decorator scan; TortureDirectives.dart /
|
||||
* TorturePartOfName.dart: `part` as an import of the part's file, `part of`
|
||||
* as nothing, and the arguments of annotated directives) and their CRLF
|
||||
* variants (derived in-memory — #1329), plus defer and generated-file pins.
|
||||
*
|
||||
* The full-repo sweeps live in scripts/kernel-parity.mjs (shelf/bloc/flutter
|
||||
* with --max-deferral 0.3); this suite keeps the invariant alive in
|
||||
@@ -159,6 +161,12 @@ describe.skipIf(!kernelBuilt)('kernel Dart extraction parity', () => {
|
||||
// body — are stepped over by the decorator scan; a field, a variable, an
|
||||
// import or the previous member's body still ends it.
|
||||
['TortureAnnotatedComments.dart', 30],
|
||||
// A library's directives: imports and exports in every form, and `part`
|
||||
// in every URI and quote form — each an import node and an `imports` ref
|
||||
// — with the arguments of an annotated directive walked for fn-refs.
|
||||
['TortureDirectives.dart', 20],
|
||||
// `part of` a library named by its library name records nothing.
|
||||
['TorturePartOfName.dart', 3],
|
||||
] as const;
|
||||
|
||||
for (const [file, minNodes] of FIXTURES) {
|
||||
|
||||
@@ -826,9 +826,13 @@ impl<'t> Walker<'t> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
"import_or_export" => {
|
||||
// importTypes (dart.ts): `import` / `export`, and `part
|
||||
// 'x.g.dart';`, which links the library to the part's file. The
|
||||
// TS ladder goes on into the directive's children, so this does
|
||||
// too: an annotation's arguments are fn-ref capture points
|
||||
// (`@Tag(bar) import 'a.dart';` captures `bar`).
|
||||
"import_or_export" | "part_directive" => {
|
||||
self.extract_import(node);
|
||||
return;
|
||||
}
|
||||
"new_expression" => {
|
||||
// INSTANTIATION_KINDS row — from the FILE/CLASS on the
|
||||
@@ -1036,7 +1040,7 @@ impl<'t> Walker<'t> {
|
||||
false
|
||||
}
|
||||
|
||||
// --- extractImport (:3170; hook dart.ts:261-304) ----------------------
|
||||
// --- extractImport (:3170; hook dart.ts extractImport) -----------------
|
||||
|
||||
fn extract_import(&mut self, node: Node<'t>) {
|
||||
let find_child = |parent: Node<'t>, kind: &str| -> Option<Node<'t>> {
|
||||
@@ -1050,6 +1054,12 @@ impl<'t> Walker<'t> {
|
||||
find_child(uri, "string_literal")
|
||||
};
|
||||
let mut module: Option<String> = None;
|
||||
// A part: `part 'x.g.dart';` holds its `uri` directly.
|
||||
if node.kind() == "part_directive" {
|
||||
if let Some(sl) = find_child(node, "uri").and_then(|uri| find_child(uri, "string_literal")) {
|
||||
module = Some(self.text(sl).replace(['\'', '"'], ""));
|
||||
}
|
||||
}
|
||||
if let Some(li) = find_child(node, "library_import") {
|
||||
if let Some(spec) = find_child(li, "import_specification") {
|
||||
if let Some(sl) = uri_of(spec) {
|
||||
|
||||
@@ -194,8 +194,8 @@ Types: functionTypes=[`function_signature`] (:119); classTypes=
|
||||
`redirecting_factory_constructor_signature`] (:126 — the last two since
|
||||
2026-10-06); interfaceTypes=[] ; structTypes=[];
|
||||
enumTypes=[`enum_declaration`] (:129); enumMemberTypes=[`enum_constant`]
|
||||
(:130); typeAliasTypes=[`type_alias`] (:131); importTypes=[`import_or_export`]
|
||||
(:132); **callTypes=[] (:133 — extractCall NEVER runs for dart; all call refs
|
||||
(:130); typeAliasTypes=[`type_alias`] (:131); importTypes=[`import_or_export`,
|
||||
`part_directive`] (:132 — the second since 2026-10-06); **callTypes=[] (:133 — extractCall NEVER runs for dart; all call refs
|
||||
ride `extractBareCall` in the body walker)**; variableTypes=[] (:134 —
|
||||
extractVariable never runs); extraClassNodeTypes=[`mixin_declaration`,
|
||||
`extension_declaration`] (:135). nameField=`name` (:172), bodyField=`body`
|
||||
@@ -420,20 +420,26 @@ Hooks PRESENT (port each exactly — anchors into languages/dart.ts):
|
||||
- **extractImport (:261-304)** — importText = trimmed full node slice;
|
||||
moduleName = the URI string content: `library_import >
|
||||
import_specification > configurable_uri > uri > string_literal` (else the
|
||||
same chain under `library_export`), quotes stripped via
|
||||
`.replace(/['"]/g,'')`. Returns {moduleName, signature} (no handledRefs)
|
||||
same chain under `library_export`; a `part_directive` holds its `uri`
|
||||
directly), quotes stripped via `.replace(/['"]/g,'')` (dartUriText).
|
||||
Returns {moduleName, signature} (no handledRefs)
|
||||
→ import NODE (name = the URI, e.g. `package:torture/other.dart`,
|
||||
`dart:async`, `src/reexported.dart`) + the generic `imports` ref
|
||||
`dart:async`, `src/reexported.dart`, `x.g.dart`) + the generic `imports` ref
|
||||
(tree-sitter.ts:3183-3194) {from: file node (no namespace exists), name =
|
||||
URI, line/col of the import_or_export node}. `as alias`, `show`/`hide`
|
||||
URI, line/col of the directive node — at its first annotation when it has
|
||||
one}. `as alias`, `show`/`hide`
|
||||
combinators: IGNORED (not read). **`import 'x.dart' deferred as y;` is
|
||||
INVISIBLE** — the deferred form's import_specification holds a bare `uri`
|
||||
(NO configurable_uri wrapper) → hook returns null → falls through the
|
||||
multi-import inline handlers (none match import_or_export) → no node, no
|
||||
ref (pinned, `extract-mini.txt`). `part`/`part of`/`library` directives
|
||||
are different node types (part_directive, part_of_directive, library_name)
|
||||
in NO type list → invisible. None of the TS/py/rust/php/ruby binding
|
||||
emitters (:3197-3234) fire for dart.
|
||||
ref (pinned, `extract-mini.txt`). **`part 'x.g.dart';` is an import since
|
||||
2026-10-06** (signature `part 'x.g.dart';`): the resolver reads its URI by
|
||||
the import/export rules (`dartDirectiveFile`), so the library's file links
|
||||
the part's file and a change to the part reaches the library's dependents.
|
||||
`part of` and `library` (part_of_directive, library_name) are in NO type
|
||||
list → invisible: the library's `part` edge already joins the two files.
|
||||
None of the TS/py/rust/php/ruby binding emitters (:3197-3234) fire for
|
||||
dart.
|
||||
- **extractBareCall (:305-379)** — §Calls, the full matrix.
|
||||
|
||||
Hooks ABSENT (the walker must NOT do these): `preParse`, `recoverMangledName`,
|
||||
@@ -467,12 +473,12 @@ Registration: `EXTRACTORS.dart` (languages/index.ts:56), `FN_REF_SPECS.dart`
|
||||
| `method_signature`, `constructor_signature` | methodTypes:1027 | **NOT class-gated at the ladder** — extractMethod:1737 runs anywhere; its own gate :1747 (not class-like, no methodsAreTopLevel, no receiver) falls back: parent `object`/`object_expression`? (never in dart) else **extractFunction** — this is how extension-TYPE members and any stray non-class method_signature become plain `function` nodes (pinned: `extension type MetersT` → function `km`, bare QN) |
|
||||
| `enum_declaration` | enumTypes:1064 → extractEnum:1914 | §Class family |
|
||||
| `type_alias` | typeAliasTypes:1071 → extractTypeAlias:2890 | plain type_alias node; `getChildByField(node,'value')` → **null** (no fields) → **NO refs from the aliased type** (`typedef MapAlias = Map<String, WidgetT>` emits nothing); returns false → children re-visited (function_type/formal_parameter_list children match nothing) |
|
||||
| `import_or_export` | importTypes:1209 → extractImport:3170 | §Extractor config |
|
||||
| `import_or_export`, `part_directive` | importTypes:1209 → extractImport:3170 | §Extractor config. No skipChildren: the directive's children are still recursed, so an annotated directive's arguments are fn-ref capture points (`@Tag(f) import 'a.dart';` captures `f`). The kernel arm recurses too — since 2026-10-06; before, its `import_or_export` arm returned and missed those (TortureDirectives.dart pins both) |
|
||||
| `new_expression` | INSTANTIATION_KINDS:1255 (`new_expression` ∈ :354-361) | extractInstantiation:4610 → ctor field lookups null → namedChild(0) = type_identifier → `instantiates` ref from stack top; `<`-strip + last-`.`-segment apply (`new p.Foo<T>()` → `Foo`). findAnonymousClassBody → always null for dart. Children still recursed |
|
||||
| `function_body` (sibling of a consumed signature) | **no branch** | recursed → THE DOUBLE-WALK (§below) |
|
||||
| `declaration` (fields, bodiless ctors) | no branch | recursed → constructor_signature, constant_constructor_signature and redirecting_factory_constructor_signature hit methodTypes; each field `initialized_identifier` hits the hook (body-walked for the class); initializers, annotations-in-place: nothing |
|
||||
| `getter_signature` / `setter_signature` BARE (top level) | no branch | **top-level getters/setters are INVISIBLE** (no node; their sibling function_body is visitNode-recursed where calls don't extract) — in classes they're method_signature-wrapped → methods |
|
||||
| `const_object_expression`, `selector`, `cascade_section`, `assignment_expression`, `local_variable_declaration`, patterns, `extension_type_declaration`, `part_directive`, `library_name`, lambdas | no branch | recursed; calls only extract in the BODY walker (`extractBareCall` is not consulted by visitNode!) — §Calls for the consequences |
|
||||
| `const_object_expression`, `selector`, `cascade_section`, `assignment_expression`, `local_variable_declaration`, patterns, `extension_type_declaration`, `part_of_directive`, `library_name`, lambdas | no branch | recursed; calls only extract in the BODY walker (`extractBareCall` is not consulted by visitNode!) — §Calls for the consequences |
|
||||
| `property_signature`/`method_signature` TS branch (:1282) | **shadowed** | method_signature is consumed at :1027 first; property_signature isn't a dart kind — branch unreachable |
|
||||
|
||||
### THE DOUBLE-WALK (sibling bodies) — reproduce it exactly
|
||||
@@ -671,9 +677,10 @@ chain → FIRST value child only); `static final sharedInst = WidgetT(0)` →
|
||||
constant under the class, sig `= WidgetT`; multi-declarations → one node
|
||||
each with own columns. **NO nodes ever**: instance fields (typed/untyped/
|
||||
late/var), `static var`, top-level var/typed vars, top-level getters/
|
||||
setters, extension_type containers, `part`/`part of`/`library`/deferred
|
||||
setters, extension_type containers, `part of`/`library`/deferred
|
||||
imports. (Named `const` constructors and redirecting factories mint
|
||||
methods since 2026-10-06; the unnamed ones still mint nothing.) **Initializers
|
||||
methods since 2026-10-06, and a `part` directive an import; the unnamed
|
||||
constructors still mint nothing.) **Initializers
|
||||
are code:** every initializer is walked with the body walker — a
|
||||
constant's for the constant (riverpod's `final repoProvider =
|
||||
Provider((ref) => Repository(ref.watch(dioProvider)));` → calls
|
||||
@@ -1270,7 +1277,9 @@ NO unwrap, NO ungatedModes, NO addressOfOnly. Pins:
|
||||
`svy-dart/torture.dart` (231 lines, parses clean; its
|
||||
`extract-torture.txt` is the expected-output pin). Inventory by branch:
|
||||
imports (dart:, package: with `as`+`show`, export with `hide`,
|
||||
**deferred → invisible**, part → invisible); doc shapes (`///` run,
|
||||
**deferred → invisible**, part → invisible — an import since
|
||||
2026-10-06, pinned with every directive form in TortureDirectives.dart,
|
||||
and `part of` by library name in TorturePartOfName.dart); doc shapes (`///` run,
|
||||
`/** */` kept, `//` kept, a `///` above an annotation); annotations (bare,
|
||||
with-args, stacked → reverse order, on class); top-level constants
|
||||
(CAPS/lowercase/multi/typed/derived, sig truncation ≥100 chars);
|
||||
|
||||
@@ -434,6 +434,12 @@ function dartCallee(prev: SyntaxNode): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** The URI a directive's `uri` node holds, quotes stripped: `x.g.dart` for `'x.g.dart'`. */
|
||||
function dartUriText(uri: SyntaxNode | undefined, source: string): string {
|
||||
const literal = uri?.namedChildren.find((c: SyntaxNode) => c.type === 'string_literal');
|
||||
return literal ? getNodeText(literal, source).replace(/['"]/g, '') : '';
|
||||
}
|
||||
|
||||
export const dartExtractor: LanguageExtractor = {
|
||||
functionTypes: ['function_signature'],
|
||||
classTypes: ['class_definition'],
|
||||
@@ -452,7 +458,12 @@ export const dartExtractor: LanguageExtractor = {
|
||||
enumTypes: ['enum_declaration'],
|
||||
enumMemberTypes: ['enum_constant'],
|
||||
typeAliasTypes: ['type_alias'],
|
||||
importTypes: ['import_or_export'],
|
||||
// `part 'x.g.dart';` is a library naming one of its own files — a generated
|
||||
// part, often. Its URI resolves as an import's does, so the library links
|
||||
// the part's file the way it links a file it imports, and a change to the
|
||||
// part reaches whatever depends on the library. `part of` adds nothing to
|
||||
// that edge, and stays out.
|
||||
importTypes: ['import_or_export', 'part_directive'],
|
||||
callTypes: [], // Dart calls use identifier+selector, handled via extractBareCall
|
||||
variableTypes: [],
|
||||
// `extension_type_declaration` is Dart 3's extension type. It sits beside the
|
||||
@@ -662,6 +673,11 @@ export const dartExtractor: LanguageExtractor = {
|
||||
const importText = source.substring(node.startIndex, node.endIndex).trim();
|
||||
let moduleName = '';
|
||||
|
||||
// A part: part 'x.g.dart';
|
||||
if (node.type === 'part_directive') {
|
||||
moduleName = dartUriText(node.namedChildren.find((c: SyntaxNode) => c.type === 'uri'), source);
|
||||
}
|
||||
|
||||
// Dart imports: import 'dart:async'; import 'package:foo/bar.dart' as bar;
|
||||
const libraryImport = node.namedChildren.find((c: SyntaxNode) => c.type === 'library_import');
|
||||
if (libraryImport) {
|
||||
@@ -669,13 +685,7 @@ export const dartExtractor: LanguageExtractor = {
|
||||
if (importSpec) {
|
||||
const configurableUri = importSpec.namedChildren.find((c: SyntaxNode) => c.type === 'configurable_uri');
|
||||
if (configurableUri) {
|
||||
const uri = configurableUri.namedChildren.find((c: SyntaxNode) => c.type === 'uri');
|
||||
if (uri) {
|
||||
const stringLiteral = uri.namedChildren.find((c: SyntaxNode) => c.type === 'string_literal');
|
||||
if (stringLiteral) {
|
||||
moduleName = getNodeText(stringLiteral, source).replace(/['"]/g, '');
|
||||
}
|
||||
}
|
||||
moduleName = dartUriText(configurableUri.namedChildren.find((c: SyntaxNode) => c.type === 'uri'), source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -686,13 +696,7 @@ export const dartExtractor: LanguageExtractor = {
|
||||
if (libraryExport) {
|
||||
const configurableUri = libraryExport.namedChildren.find((c: SyntaxNode) => c.type === 'configurable_uri');
|
||||
if (configurableUri) {
|
||||
const uri = configurableUri.namedChildren.find((c: SyntaxNode) => c.type === 'uri');
|
||||
if (uri) {
|
||||
const stringLiteral = uri.namedChildren.find((c: SyntaxNode) => c.type === 'string_literal');
|
||||
if (stringLiteral) {
|
||||
moduleName = getNodeText(stringLiteral, source).replace(/['"]/g, '');
|
||||
}
|
||||
}
|
||||
moduleName = dartUriText(configurableUri.namedChildren.find((c: SyntaxNode) => c.type === 'uri'), source);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
* .dart file, never above the project root. When packages share a name
|
||||
* (example apps), the importing file's own package wins, then the one
|
||||
* enclosing it, then the one its package depends on by `path:`, else all of
|
||||
* them count. An `import` / `export` edge links the one file its URI names
|
||||
* (`dartDirectiveFile`).
|
||||
* them count. An `import`, `export` or `part` edge links the one file its URI
|
||||
* names (`dartDirectiveFile`).
|
||||
*
|
||||
* Directives are read from the head of each file at resolution time;
|
||||
* extraction, and the kernel's, are unchanged. Where the library cannot be
|
||||
* Directives are read from the head of each file at resolution time, not from
|
||||
* what extraction recorded of them. Where the library cannot be
|
||||
* established — a `part of` naming no library this can find, a file it cannot
|
||||
* read, a file in no package — every file counts as visible, so the rule only
|
||||
* narrows where the library is known.
|
||||
@@ -542,9 +542,10 @@ function resolveDartUri(from: string, uri: string, context: ResolutionContext, m
|
||||
}
|
||||
|
||||
/**
|
||||
* The project file a Dart `import` or `export` in `from` names by `uri`, or
|
||||
* null: a `dart:` library, a package from outside the project, a path out of
|
||||
* it, or a package name the project repeats with nothing to say which.
|
||||
* The project file a Dart `import`, `export` or `part` in `from` names by
|
||||
* `uri`, or null: a `dart:` library, a package from outside the project, a
|
||||
* path out of it, or a package name the project repeats with nothing to say
|
||||
* which.
|
||||
*/
|
||||
export function dartDirectiveFile(from: string, uri: string, context: ResolutionContext): string | null {
|
||||
const files = resolveDartUri(from, uri, context, memoFor(context));
|
||||
|
||||
@@ -894,11 +894,12 @@ export function isCobolCopybookRef(ref: UnresolvedRef): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a Dart `import` / `export`? Its name is the URI, which names one
|
||||
* library file (see ./dart-libraries) or none — `dart:async`, a package from
|
||||
* outside the project — and never a symbol: the name-matcher took the URI's
|
||||
* last segment for a file name (`package:flutter/foundation.dart` went to
|
||||
* riverpod's own foundation.dart) or bound it to the file's own `import` node.
|
||||
* Is this a Dart `import`, `export` or `part`? Its name is the URI, which
|
||||
* names one file (see ./dart-libraries) or none — `dart:async`, a package from
|
||||
* outside the project, a generated part nobody committed — and never a
|
||||
* symbol: the name-matcher took the URI's last segment for a file name
|
||||
* (`package:flutter/foundation.dart` went to riverpod's own foundation.dart)
|
||||
* or bound it to the file's own `import` node.
|
||||
*/
|
||||
export function isDartImportRef(ref: UnresolvedRef): boolean {
|
||||
return ref.language === 'dart' && ref.referenceKind === 'imports';
|
||||
@@ -1651,8 +1652,9 @@ export function resolveViaImport(
|
||||
const fileNode = file && file !== ref.filePath ? context.getNodesInFile(file).find((n) => n.kind === 'file') : undefined;
|
||||
if (fileNode) return { original: ref, targetNodeId: fileNode.id, confidence: 0.9, resolvedBy: 'import' };
|
||||
}
|
||||
// A Dart `import` / `export` URI names a library file: `package:app/x.dart`
|
||||
// is app's lib/x.dart, any other path is from the importing file.
|
||||
// A Dart `import` / `export` URI names a library file, and a `part` URI one
|
||||
// of the library's own files: `package:app/x.dart` is app's lib/x.dart, any
|
||||
// other path is from the file the directive is in.
|
||||
if (isDartImportRef(ref)) {
|
||||
const file = dartDirectiveFile(ref.filePath, ref.referenceName, context);
|
||||
const fileNode = file && file !== ref.filePath ? context.getNodesInFile(file).find((n) => n.kind === 'file') : undefined;
|
||||
|
||||
Reference in New Issue
Block a user