feat(core): DartExtractor — extension declarations (named + anonymous)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
thejesh
2026-06-13 05:21:25 -07:00
Unverified
parent 05ce514db5
commit 306ee2c070
2 changed files with 80 additions and 6 deletions
@@ -219,4 +219,35 @@ describe("DartExtractor", () => {
parser.delete();
});
});
describe("extractStructure - extensions", () => {
it("extracts a named extension on String", () => {
const { tree, parser, root } = parse(`extension StringX on String {
String shout() => toUpperCase() + '!';
}
`);
const result = extractor.extractStructure(root);
expect(result.classes).toHaveLength(1);
expect(result.classes[0].name).toBe("StringX");
expect(result.classes[0].methods).toContain("shout");
tree.delete();
parser.delete();
});
it("names an anonymous extension after its target type", () => {
const { tree, parser, root } = parse(`extension on int {
int squared() => this * this;
}
`);
const result = extractor.extractStructure(root);
expect(result.classes).toHaveLength(1);
// Anonymous extension on int → "on int" so it isn't dropped.
expect(result.classes[0].name).toBe("on int");
expect(result.classes[0].methods).toContain("squared");
tree.delete();
parser.delete();
});
});
});
@@ -238,6 +238,9 @@ export class DartExtractor implements LanguageExtractor {
case "mixin_declaration":
this.extractClassLikeDeclaration(node, "class_body", classes, functions, exports);
break;
case "extension_declaration":
this.extractExtensionDeclaration(node, classes, functions, exports);
break;
}
}
@@ -270,9 +273,9 @@ export class DartExtractor implements LanguageExtractor {
* `extension_declaration`. The only difference between these shapes is the
* body's node type name, which is passed in via `bodyNodeType`.
*
* Anonymous variants (e.g. `extension on Foo` with no name) are handled by
* the caller — this method requires `declNode` to have a leading
* `identifier` child for the name.
* When `nameOverride` is provided, it is used as the entry's name instead of
* looking up a leading `identifier` child — used by anonymous extensions,
* which have no name in the source.
*/
private extractClassLikeDeclaration(
declNode: TreeSitterNode,
@@ -280,10 +283,16 @@ export class DartExtractor implements LanguageExtractor {
classes: StructuralAnalysis["classes"],
functions: StructuralAnalysis["functions"],
exports: StructuralAnalysis["exports"],
nameOverride?: string,
): void {
const nameNode = findChild(declNode, "identifier");
if (!nameNode) return;
const name = nameNode.text;
let name: string;
if (nameOverride !== undefined) {
name = nameOverride;
} else {
const nameNode = findChild(declNode, "identifier");
if (!nameNode) return;
name = nameNode.text;
}
const methods: string[] = [];
const properties: string[] = [];
@@ -305,6 +314,40 @@ export class DartExtractor implements LanguageExtractor {
}
}
private extractExtensionDeclaration(
declNode: TreeSitterNode,
classes: StructuralAnalysis["classes"],
functions: StructuralAnalysis["functions"],
exports: StructuralAnalysis["exports"],
): void {
// Named extension — extractClassLikeDeclaration finds the leading identifier itself.
const idNode = findChild(declNode, "identifier");
if (idNode) {
this.extractClassLikeDeclaration(
declNode,
"extension_body",
classes,
functions,
exports,
);
return;
}
// Anonymous extension — no `identifier` child. The on-type is the first
// `type_identifier`. Name the entry "on <TargetType>" so the graph
// builder doesn't drop it for having an empty name.
const onType = findChild(declNode, "type_identifier");
if (!onType) return;
this.extractClassLikeDeclaration(
declNode,
"extension_body",
classes,
functions,
exports,
`on ${onType.text}`,
);
}
extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] {
// Implementation lands in a later task.
void rootNode;