feat(core): DartExtractor — import directives (package/relative/show/as) + export directives

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
thejesh
2026-06-13 05:30:40 -07:00
Unverified
parent fd1d1c6450
commit 798c1747b9
2 changed files with 163 additions and 1 deletions
@@ -263,4 +263,68 @@ describe("DartExtractor", () => {
parser.delete();
});
});
describe("extractStructure - imports", () => {
it("extracts a package import with no specifiers", () => {
const { tree, parser, root } = parse(`import 'package:flutter/material.dart';\n`);
const result = extractor.extractStructure(root);
expect(result.imports).toHaveLength(1);
expect(result.imports[0].source).toBe("package:flutter/material.dart");
expect(result.imports[0].specifiers).toEqual([]);
tree.delete();
parser.delete();
});
it("extracts a relative import", () => {
const { tree, parser, root } = parse(`import './foo.dart';\n`);
const result = extractor.extractStructure(root);
expect(result.imports[0].source).toBe("./foo.dart");
tree.delete();
parser.delete();
});
it("extracts a `show` clause as specifiers", () => {
const { tree, parser, root } = parse(`import 'foo.dart' show Bar, Baz;\n`);
const result = extractor.extractStructure(root);
expect(result.imports[0].source).toBe("foo.dart");
expect(result.imports[0].specifiers).toEqual(["Bar", "Baz"]);
tree.delete();
parser.delete();
});
it("extracts an `as` prefix as the sole specifier", () => {
const { tree, parser, root } = parse(`import 'bar.dart' as b;\n`);
const result = extractor.extractStructure(root);
expect(result.imports[0].source).toBe("bar.dart");
expect(result.imports[0].specifiers).toEqual(["b"]);
tree.delete();
parser.delete();
});
it("does NOT include `hide` names as specifiers", () => {
const { tree, parser, root } = parse(`import 'foo.dart' hide Qux;\n`);
const result = extractor.extractStructure(root);
expect(result.imports[0].source).toBe("foo.dart");
expect(result.imports[0].specifiers).toEqual([]);
tree.delete();
parser.delete();
});
});
describe("extractStructure - exports", () => {
it("extracts a top-level export directive", () => {
const { tree, parser, root } = parse(`export 'shared.dart';\n`);
const result = extractor.extractStructure(root);
const sharedExport = result.exports.find((e) => e.name === "shared.dart");
expect(sharedExport).toBeDefined();
tree.delete();
parser.delete();
});
});
});
@@ -1,6 +1,6 @@
import type { StructuralAnalysis, CallGraphEntry } from "../../types.js";
import type { LanguageExtractor, TreeSitterNode } from "./types.js";
import { findChild, findChildren } from "./base-extractor.js";
import { findChild, findChildren, getStringValue } from "./base-extractor.js";
/**
* Whether a Dart name is exported.
@@ -113,6 +113,17 @@ function pushMethod(
}
}
/**
* Unwrap the string-literal text from `uri > string_literal` via
* `base-extractor.getStringValue` so the quote-stripping logic lives in
* exactly one place across all extractors.
*/
function uriText(uriNode: TreeSitterNode): string | null {
const lit = findChild(uriNode, "string_literal");
if (!lit) return null;
return getStringValue(lit);
}
/**
* Build a constructor's method-graph name from a constructor_signature /
* factory_constructor_signature node:
@@ -244,6 +255,9 @@ export class DartExtractor implements LanguageExtractor {
case "enum_declaration":
this.extractEnumDeclaration(node, classes, exports);
break;
case "import_or_export":
this.extractImportOrExport(node, imports, exports);
break;
}
}
@@ -381,6 +395,90 @@ export class DartExtractor implements LanguageExtractor {
}
}
private extractImportOrExport(
declNode: TreeSitterNode,
imports: StructuralAnalysis["imports"],
exports: StructuralAnalysis["exports"],
): void {
const libImport = findChild(declNode, "library_import");
if (libImport) {
this.extractLibraryImport(libImport, imports);
return;
}
const libExport = findChild(declNode, "library_export");
if (libExport) {
this.extractLibraryExport(libExport, declNode, exports);
}
}
private extractLibraryImport(
libImport: TreeSitterNode,
imports: StructuralAnalysis["imports"],
): void {
const spec = findChild(libImport, "import_specification");
if (!spec) return;
const configurable = findChild(spec, "configurable_uri");
const uri = configurable ? findChild(configurable, "uri") : null;
if (!uri) return;
const source = uriText(uri);
if (!source) return;
const specifiers: string[] = [];
// Combinators come in two flavours:
// show Bar, Baz → leading keyword "show", names are specifiers
// hide Qux → leading keyword "hide", names are excluded — skip
const combinators = findChildren(spec, "combinator");
for (const c of combinators) {
// Inspect the first child to determine show vs hide. The keyword is an
// unnamed token; use `child()` not `namedChild()`.
const first = c.child(0);
if (first && first.type === "hide") continue;
for (const id of findChildren(c, "identifier")) {
specifiers.push(id.text);
}
}
// `as Foo` → direct `identifier` child of import_specification.
// Only treat as alias when there were no `show`/`hide` specifiers.
const asId = findChild(spec, "identifier");
if (asId && specifiers.length === 0) {
specifiers.push(asId.text);
}
imports.push({
source,
specifiers,
lineNumber: libImport.startPosition.row + 1,
});
}
/**
* Extract an `export` directive's URI into `exports[]`.
*
* Takes both `libExport` (the `library_export` node containing the URI)
* and `outerNode` (the wrapping `import_or_export` node). The line number
* uses `outerNode.startPosition` because `library_export` may start one
* child deeper than the `export` keyword, while `import_or_export` is
* guaranteed to start at the keyword.
*/
private extractLibraryExport(
libExport: TreeSitterNode,
outerNode: TreeSitterNode,
exports: StructuralAnalysis["exports"],
): void {
const configurable = findChild(libExport, "configurable_uri");
const uri = configurable ? findChild(configurable, "uri") : null;
if (!uri) return;
const source = uriText(uri);
if (!source) return;
exports.push({
name: source,
lineNumber: outerNode.startPosition.row + 1,
});
}
extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] {
// Implementation lands in a later task.
void rootNode;