mirror of
https://github.com/Egonex-AI/Understand-Anything.git
synced 2026-06-22 10:58:03 +08:00
feat(core): DartExtractor — class extraction with fields + methods
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+74
@@ -74,4 +74,78 @@ describe("DartExtractor", () => {
|
||||
parser.delete();
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractStructure - classes", () => {
|
||||
it("extracts a class with fields and methods", () => {
|
||||
const { tree, parser, root } = parse(`class Counter {
|
||||
int count = 0;
|
||||
String? label;
|
||||
void increment() { count++; }
|
||||
int get value => count;
|
||||
}
|
||||
`);
|
||||
const result = extractor.extractStructure(root);
|
||||
|
||||
expect(result.classes).toHaveLength(1);
|
||||
expect(result.classes[0].name).toBe("Counter");
|
||||
expect(result.classes[0].methods).toContain("increment");
|
||||
// method declarations land in functions[] too (matching Kotlin convention)
|
||||
expect(result.functions.map((f) => f.name)).toContain("increment");
|
||||
// Field extraction: `int count = 0;` and `String? label;` both parse as
|
||||
// declaration > initialized_identifier_list > initialized_identifier > identifier
|
||||
expect(result.classes[0].properties).toEqual(
|
||||
expect.arrayContaining(["count", "label"]),
|
||||
);
|
||||
// Getters appear as `method_signature > getter_signature`, a separate node
|
||||
// type from `function_signature` — not yet surfaced (documented limitation).
|
||||
expect(result.classes[0].methods).not.toContain("value");
|
||||
|
||||
tree.delete();
|
||||
parser.delete();
|
||||
});
|
||||
|
||||
it("extracts an empty class", () => {
|
||||
const { tree, parser, root } = parse(`class Empty {}\n`);
|
||||
const result = extractor.extractStructure(root);
|
||||
|
||||
expect(result.classes).toHaveLength(1);
|
||||
expect(result.classes[0].name).toBe("Empty");
|
||||
expect(result.classes[0].methods).toEqual([]);
|
||||
|
||||
tree.delete();
|
||||
parser.delete();
|
||||
});
|
||||
|
||||
it("extracts an abstract class with method requirements", () => {
|
||||
const { tree, parser, root } = parse(`abstract class Shape {
|
||||
double area();
|
||||
}
|
||||
`);
|
||||
const result = extractor.extractStructure(root);
|
||||
|
||||
expect(result.classes).toHaveLength(1);
|
||||
expect(result.classes[0].name).toBe("Shape");
|
||||
expect(result.classes[0].methods).toContain("area");
|
||||
|
||||
tree.delete();
|
||||
parser.delete();
|
||||
});
|
||||
|
||||
it("extracts a class with extends + with + implements clauses", () => {
|
||||
const { tree, parser, root } = parse(`class Square extends Shape with Comparable<Square> implements Cloneable {
|
||||
double side;
|
||||
Square(this.side);
|
||||
double area() => side * side;
|
||||
}
|
||||
`);
|
||||
const result = extractor.extractStructure(root);
|
||||
|
||||
expect(result.classes).toHaveLength(1);
|
||||
expect(result.classes[0].name).toBe("Square");
|
||||
expect(result.classes[0].methods).toContain("area");
|
||||
|
||||
tree.delete();
|
||||
parser.delete();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,10 +16,12 @@ function isExported(name: string): boolean {
|
||||
/**
|
||||
* Extract the identifier name from a `function_signature` node.
|
||||
*
|
||||
* NOTE: for `method_signature` (class-body method declarations), callers
|
||||
* must first unwrap to the inner `function_signature` child before invoking
|
||||
* this helper — the Dart grammar layers `method_signature > function_signature`
|
||||
* and `findChild(..., "identifier")` would otherwise miss the function name.
|
||||
* NOTE: this helper expects a `function_signature` node. The Dart grammar
|
||||
* wraps the function_signature inside two different parent shapes:
|
||||
* - `method_signature > function_signature` for CONCRETE class methods.
|
||||
* - `declaration > function_signature` for ABSTRACT class methods (no body).
|
||||
* Callers (`collectClassBody`) unwrap to the inner `function_signature`
|
||||
* before invoking this helper.
|
||||
*/
|
||||
function extractFunctionName(sig: TreeSitterNode): string | null {
|
||||
const id = findChild(sig, "identifier");
|
||||
@@ -86,6 +88,83 @@ function extractReturnType(sig: TreeSitterNode): string | undefined {
|
||||
return parts.length > 0 ? parts.join("") : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Push a method/function entry. Used by `collectClassBody` for both
|
||||
* `method_signature` and `declaration > function_signature` shapes so a
|
||||
* future change to the entry's fields lands in one place.
|
||||
*/
|
||||
function pushMethod(
|
||||
declNode: TreeSitterNode,
|
||||
sig: TreeSitterNode,
|
||||
name: string,
|
||||
methods: string[],
|
||||
functions: StructuralAnalysis["functions"],
|
||||
exports: StructuralAnalysis["exports"],
|
||||
): void {
|
||||
methods.push(name);
|
||||
functions.push({
|
||||
name,
|
||||
lineRange: [declNode.startPosition.row + 1, declNode.endPosition.row + 1],
|
||||
params: extractParams(sig),
|
||||
returnType: extractReturnType(sig),
|
||||
});
|
||||
if (isExported(name)) {
|
||||
exports.push({ name, lineNumber: declNode.startPosition.row + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk a `class_body` (or `extension_body` / `enum_body`) and collect
|
||||
* `method_signature` declarations into the class's `methods` array AND the
|
||||
* top-level `functions` array, mirroring KotlinExtractor.collectClassBody.
|
||||
*
|
||||
* Field extraction: `int count = 0;` and `String? label;` inside a class body
|
||||
* both parse as `declaration > initialized_identifier_list > initialized_identifier
|
||||
* > identifier`. The nullable `?` is an unnamed sibling of `type_identifier`,
|
||||
* so it does not affect this path.
|
||||
*/
|
||||
function collectClassBody(
|
||||
body: TreeSitterNode,
|
||||
methods: string[],
|
||||
properties: string[],
|
||||
functions: StructuralAnalysis["functions"],
|
||||
exports: StructuralAnalysis["exports"],
|
||||
): void {
|
||||
for (let i = 0; i < body.childCount; i++) {
|
||||
const member = body.child(i);
|
||||
if (!member) continue;
|
||||
|
||||
if (member.type === "method_signature") {
|
||||
// Concrete method: `method_signature > function_signature`.
|
||||
// NOTE: `getter_signature` also nests under `method_signature` but is a
|
||||
// separate node type — getters are not yet surfaced (documented limitation).
|
||||
const inner = findChild(member, "function_signature");
|
||||
if (!inner) continue;
|
||||
const name = extractFunctionName(inner);
|
||||
if (!name) continue;
|
||||
pushMethod(member, inner, name, methods, functions, exports);
|
||||
} else if (member.type === "declaration") {
|
||||
// Abstract method declarations (e.g. `double area();`) appear as
|
||||
// `declaration > function_signature` — not wrapped in `method_signature`.
|
||||
const fnSig = findChild(member, "function_signature");
|
||||
if (fnSig) {
|
||||
const name = extractFunctionName(fnSig);
|
||||
if (name) {
|
||||
pushMethod(member, fnSig, name, methods, functions, exports);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Field declaration — surface initialized_identifier names as properties.
|
||||
const list = findChild(member, "initialized_identifier_list");
|
||||
if (!list) continue;
|
||||
for (const init of findChildren(list, "initialized_identifier")) {
|
||||
const id = findChild(init, "identifier");
|
||||
if (id) properties.push(id.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dart extractor for tree-sitter structural analysis + call graph.
|
||||
*
|
||||
@@ -112,6 +191,9 @@ export class DartExtractor implements LanguageExtractor {
|
||||
case "function_signature":
|
||||
this.extractTopLevelFunction(node, functions, exports);
|
||||
break;
|
||||
case "class_definition":
|
||||
this.extractClassDefinition(node, classes, functions, exports);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +220,36 @@ export class DartExtractor implements LanguageExtractor {
|
||||
}
|
||||
}
|
||||
|
||||
private extractClassDefinition(
|
||||
declNode: TreeSitterNode,
|
||||
classes: StructuralAnalysis["classes"],
|
||||
functions: StructuralAnalysis["functions"],
|
||||
exports: StructuralAnalysis["exports"],
|
||||
): void {
|
||||
const nameNode = findChild(declNode, "identifier");
|
||||
if (!nameNode) return;
|
||||
const name = nameNode.text;
|
||||
|
||||
const methods: string[] = [];
|
||||
const properties: string[] = [];
|
||||
|
||||
const body = findChild(declNode, "class_body");
|
||||
if (body) {
|
||||
collectClassBody(body, methods, properties, functions, exports);
|
||||
}
|
||||
|
||||
classes.push({
|
||||
name,
|
||||
lineRange: [declNode.startPosition.row + 1, declNode.endPosition.row + 1],
|
||||
methods,
|
||||
properties,
|
||||
});
|
||||
|
||||
if (isExported(name)) {
|
||||
exports.push({ name, lineNumber: declNode.startPosition.row + 1 });
|
||||
}
|
||||
}
|
||||
|
||||
extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] {
|
||||
// Implementation lands in a later task.
|
||||
void rootNode;
|
||||
|
||||
Reference in New Issue
Block a user