feat: add CSharpExtractor for tree-sitter C# structural analysis

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Lum1104
2026-04-15 19:19:24 +08:00
Unverified
parent 48e1daa838
commit 3e4f7b6c02
2 changed files with 1189 additions and 0 deletions
@@ -0,0 +1,665 @@
import { describe, it, expect, beforeAll } from "vitest";
import { createRequire } from "node:module";
import { CSharpExtractor } from "../csharp-extractor.js";
const require = createRequire(import.meta.url);
// Load tree-sitter + C# grammar once
let Parser: any;
let Language: any;
let csharpLang: any;
beforeAll(async () => {
const mod = await import("web-tree-sitter");
Parser = mod.Parser;
Language = mod.Language;
await Parser.init();
const wasmPath = require.resolve(
"tree-sitter-c-sharp/tree-sitter-c_sharp.wasm",
);
csharpLang = await Language.load(wasmPath);
});
function parse(code: string) {
const parser = new Parser();
parser.setLanguage(csharpLang);
const tree = parser.parse(code);
const root = tree.rootNode;
return { tree, parser, root };
}
describe("CSharpExtractor", () => {
const extractor = new CSharpExtractor();
it("has correct languageIds", () => {
expect(extractor.languageIds).toEqual(["csharp"]);
});
// ---- Methods/Constructors (mapped to functions) ----
describe("extractStructure - functions (methods & constructors)", () => {
it("extracts methods with params and return types", () => {
const { tree, parser, root } = parse(`namespace App {
public class Foo {
public string GetName(int id) {
return "";
}
private void Process(string data, int count) {
}
}
}
`);
const result = extractor.extractStructure(root);
expect(result.functions).toHaveLength(2);
expect(result.functions[0].name).toBe("GetName");
expect(result.functions[0].params).toEqual(["id"]);
expect(result.functions[0].returnType).toBe("string");
expect(result.functions[1].name).toBe("Process");
expect(result.functions[1].params).toEqual(["data", "count"]);
expect(result.functions[1].returnType).toBe("void");
tree.delete();
parser.delete();
});
it("extracts constructors", () => {
const { tree, parser, root } = parse(`namespace App {
public class Foo {
public Foo(string name, int value) {
this.name = name;
}
}
}
`);
const result = extractor.extractStructure(root);
expect(result.functions).toHaveLength(1);
expect(result.functions[0].name).toBe("Foo");
expect(result.functions[0].params).toEqual(["name", "value"]);
expect(result.functions[0].returnType).toBeUndefined();
tree.delete();
parser.delete();
});
it("extracts methods with no params", () => {
const { tree, parser, root } = parse(`namespace App {
public class Foo {
public void Run() {}
}
}
`);
const result = extractor.extractStructure(root);
expect(result.functions).toHaveLength(1);
expect(result.functions[0].name).toBe("Run");
expect(result.functions[0].params).toEqual([]);
expect(result.functions[0].returnType).toBe("void");
tree.delete();
parser.delete();
});
it("extracts methods with generic return types", () => {
const { tree, parser, root } = parse(`namespace App {
public class Foo {
public List<string> GetItems() {
return null;
}
}
}
`);
const result = extractor.extractStructure(root);
expect(result.functions).toHaveLength(1);
expect(result.functions[0].name).toBe("GetItems");
expect(result.functions[0].returnType).toBe("List<string>");
tree.delete();
parser.delete();
});
it("reports correct line ranges for multi-line methods", () => {
const { tree, parser, root } = parse(`namespace App {
public class Foo {
public int Calculate(
int a,
int b
) {
int result = a + b;
return result;
}
}
}
`);
const result = extractor.extractStructure(root);
expect(result.functions).toHaveLength(1);
expect(result.functions[0].lineRange[0]).toBe(3);
expect(result.functions[0].lineRange[1]).toBe(9);
tree.delete();
parser.delete();
});
});
// ---- Classes ----
describe("extractStructure - classes", () => {
it("extracts class with methods, properties, and fields", () => {
const { tree, parser, root } = parse(`namespace App {
public class Server {
private string _host;
private int _port;
public string Address { get; set; }
public void Start() {}
public void Stop() {}
}
}
`);
const result = extractor.extractStructure(root);
expect(result.classes).toHaveLength(1);
expect(result.classes[0].name).toBe("Server");
expect(result.classes[0].properties).toEqual(["_host", "_port", "Address"]);
expect(result.classes[0].methods).toEqual(["Start", "Stop"]);
expect(result.classes[0].lineRange[0]).toBe(2);
tree.delete();
parser.delete();
});
it("extracts empty class", () => {
const { tree, parser, root } = parse(`namespace App {
public class Empty {
}
}
`);
const result = extractor.extractStructure(root);
expect(result.classes).toHaveLength(1);
expect(result.classes[0].name).toBe("Empty");
expect(result.classes[0].properties).toEqual([]);
expect(result.classes[0].methods).toEqual([]);
tree.delete();
parser.delete();
});
it("includes constructors in methods list", () => {
const { tree, parser, root } = parse(`namespace App {
public class Foo {
public Foo() {}
public void Run() {}
}
}
`);
const result = extractor.extractStructure(root);
expect(result.classes[0].methods).toEqual(["Foo", "Run"]);
tree.delete();
parser.delete();
});
});
// ---- Interfaces ----
describe("extractStructure - interfaces", () => {
it("extracts interface with method signatures", () => {
const { tree, parser, root } = parse(`namespace App {
interface IRepository {
List<User> FindAll();
User FindById(int id);
}
}
`);
const result = extractor.extractStructure(root);
expect(result.classes).toHaveLength(1);
expect(result.classes[0].name).toBe("IRepository");
expect(result.classes[0].methods).toEqual(["FindAll", "FindById"]);
expect(result.classes[0].properties).toEqual([]);
tree.delete();
parser.delete();
});
it("extracts empty interface", () => {
const { tree, parser, root } = parse(`namespace App {
interface IMarker {
}
}
`);
const result = extractor.extractStructure(root);
expect(result.classes).toHaveLength(1);
expect(result.classes[0].name).toBe("IMarker");
expect(result.classes[0].methods).toEqual([]);
tree.delete();
parser.delete();
});
});
// ---- Imports (using directives) ----
describe("extractStructure - imports", () => {
it("extracts simple using directives", () => {
const { tree, parser, root } = parse(`using System;
namespace App {
public class Foo {}
}
`);
const result = extractor.extractStructure(root);
expect(result.imports).toHaveLength(1);
expect(result.imports[0].source).toBe("System");
expect(result.imports[0].specifiers).toEqual(["System"]);
expect(result.imports[0].lineNumber).toBe(1);
tree.delete();
parser.delete();
});
it("extracts qualified using directives", () => {
const { tree, parser, root } = parse(`using System;
using System.Collections.Generic;
namespace App {
public class Foo {}
}
`);
const result = extractor.extractStructure(root);
expect(result.imports).toHaveLength(2);
expect(result.imports[0].source).toBe("System");
expect(result.imports[0].specifiers).toEqual(["System"]);
expect(result.imports[0].lineNumber).toBe(1);
expect(result.imports[1].source).toBe("System.Collections.Generic");
expect(result.imports[1].specifiers).toEqual(["Generic"]);
expect(result.imports[1].lineNumber).toBe(2);
tree.delete();
parser.delete();
});
it("reports correct import line numbers with gaps", () => {
const { tree, parser, root } = parse(`using System;
using System.Linq;
namespace App {
public class Foo {}
}
`);
const result = extractor.extractStructure(root);
expect(result.imports[0].lineNumber).toBe(1);
expect(result.imports[1].lineNumber).toBe(3);
tree.delete();
parser.delete();
});
});
// ---- Exports ----
describe("extractStructure - exports", () => {
it("exports public class, methods, constructor, and properties", () => {
const { tree, parser, root } = parse(`namespace App {
public class UserService {
private string _name;
public int MaxRetries { get; set; }
public UserService(string name) {
_name = name;
}
public void Start() {}
private void Helper() {}
}
}
`);
const result = extractor.extractStructure(root);
const exportNames = result.exports.map((e) => e.name);
expect(exportNames).toContain("UserService"); // class
// Constructor is also named UserService
const userServiceExports = result.exports.filter(
(e) => e.name === "UserService",
);
expect(userServiceExports.length).toBe(2); // class + constructor
expect(exportNames).toContain("MaxRetries"); // public property
expect(exportNames).toContain("Start");
expect(exportNames).not.toContain("Helper");
expect(exportNames).not.toContain("_name"); // private field
tree.delete();
parser.delete();
});
it("does not export non-public classes", () => {
const { tree, parser, root } = parse(`namespace App {
class Internal {
void Run() {}
}
}
`);
const result = extractor.extractStructure(root);
expect(result.exports).toHaveLength(0);
tree.delete();
parser.delete();
});
it("exports public fields", () => {
const { tree, parser, root } = parse(`namespace App {
public class Config {
public string ApiKey;
private int _retries;
}
}
`);
const result = extractor.extractStructure(root);
const exportNames = result.exports.map((e) => e.name);
expect(exportNames).toContain("Config");
expect(exportNames).toContain("ApiKey");
expect(exportNames).not.toContain("_retries");
tree.delete();
parser.delete();
});
it("exports public interface", () => {
const { tree, parser, root } = parse(`namespace App {
public interface IRepository {
void Save();
}
}
`);
const result = extractor.extractStructure(root);
const exportNames = result.exports.map((e) => e.name);
expect(exportNames).toContain("IRepository");
tree.delete();
parser.delete();
});
});
// ---- Call Graph ----
describe("extractCallGraph", () => {
it("extracts simple method calls", () => {
const { tree, parser, root } = parse(`namespace App {
public class Foo {
public void Process(int data) {
Transform(data);
Format(data);
}
}
}
`);
const result = extractor.extractCallGraph(root);
expect(result).toHaveLength(2);
expect(result[0].caller).toBe("Process");
expect(result[0].callee).toBe("Transform");
expect(result[1].caller).toBe("Process");
expect(result[1].callee).toBe("Format");
tree.delete();
parser.delete();
});
it("extracts qualified method calls (e.g. Console.WriteLine)", () => {
const { tree, parser, root } = parse(`namespace App {
public class Foo {
private void Log(string message) {
Console.WriteLine(message);
}
}
}
`);
const result = extractor.extractCallGraph(root);
expect(result).toHaveLength(1);
expect(result[0].caller).toBe("Log");
expect(result[0].callee).toBe("Console.WriteLine");
tree.delete();
parser.delete();
});
it("extracts object creation expressions", () => {
const { tree, parser, root } = parse(`namespace App {
public class Foo {
public void Create() {
var b = new Bar();
}
}
}
`);
const result = extractor.extractCallGraph(root);
expect(result).toHaveLength(1);
expect(result[0].caller).toBe("Create");
expect(result[0].callee).toBe("new Bar");
tree.delete();
parser.delete();
});
it("tracks correct caller for constructors", () => {
const { tree, parser, root } = parse(`namespace App {
public class Foo {
public Foo() {
Init();
}
}
}
`);
const result = extractor.extractCallGraph(root);
expect(result).toHaveLength(1);
expect(result[0].caller).toBe("Foo");
expect(result[0].callee).toBe("Init");
tree.delete();
parser.delete();
});
it("reports correct line numbers for calls", () => {
const { tree, parser, root } = parse(`namespace App {
public class Foo {
public void Run() {
Foo();
Bar();
}
}
}
`);
const result = extractor.extractCallGraph(root);
expect(result).toHaveLength(2);
expect(result[0].lineNumber).toBe(4);
expect(result[1].lineNumber).toBe(5);
tree.delete();
parser.delete();
});
it("ignores calls outside methods (no caller)", () => {
const { tree, parser, root } = parse(`namespace App {
public class Foo {
private string _value = String.Empty;
}
}
`);
const result = extractor.extractCallGraph(root);
// No enclosing method, so these are skipped
expect(result).toHaveLength(0);
tree.delete();
parser.delete();
});
});
// ---- Namespace handling ----
describe("namespace handling", () => {
it("extracts declarations from block-scoped namespace", () => {
const { tree, parser, root } = parse(`namespace App.Services {
public class Svc {
public void Run() {}
}
}
`);
const result = extractor.extractStructure(root);
expect(result.classes).toHaveLength(1);
expect(result.classes[0].name).toBe("Svc");
expect(result.functions).toHaveLength(1);
expect(result.functions[0].name).toBe("Run");
tree.delete();
parser.delete();
});
it("extracts declarations alongside file-scoped namespace", () => {
const { tree, parser, root } = parse(`namespace App.Services;
public class Svc {
public void Run() {}
}
`);
const result = extractor.extractStructure(root);
expect(result.classes).toHaveLength(1);
expect(result.classes[0].name).toBe("Svc");
expect(result.functions).toHaveLength(1);
expect(result.functions[0].name).toBe("Run");
tree.delete();
parser.delete();
});
});
// ---- Comprehensive ----
describe("comprehensive C# file", () => {
it("handles a realistic C# module", () => {
const { tree, parser, root } = parse(`using System;
using System.Collections.Generic;
namespace App.Services
{
public class UserService
{
private string _name;
public int MaxRetries { get; set; }
public UserService(string name)
{
_name = name;
}
public List<User> GetUsers(int limit)
{
return FetchFromDb(limit);
}
private void Log(string message)
{
Console.WriteLine(message);
}
}
public interface IRepository
{
List<User> FindAll();
User FindById(int id);
}
}
`);
const result = extractor.extractStructure(root);
// Functions: UserService (constructor), GetUsers, Log
expect(result.functions).toHaveLength(3);
expect(result.functions.map((f) => f.name).sort()).toEqual(
["GetUsers", "Log", "UserService"].sort(),
);
// Constructor has params but no return type
const ctor = result.functions.find((f) => f.name === "UserService");
expect(ctor?.params).toEqual(["name"]);
expect(ctor?.returnType).toBeUndefined();
// GetUsers has params and generic return type
const getUsers = result.functions.find((f) => f.name === "GetUsers");
expect(getUsers?.params).toEqual(["limit"]);
expect(getUsers?.returnType).toBe("List<User>");
// Log has params and void return type
const log = result.functions.find((f) => f.name === "Log");
expect(log?.params).toEqual(["message"]);
expect(log?.returnType).toBe("void");
// Classes: UserService, IRepository
expect(result.classes).toHaveLength(2);
const userService = result.classes.find(
(c) => c.name === "UserService",
);
expect(userService).toBeDefined();
expect(userService!.methods.sort()).toEqual(
["GetUsers", "Log", "UserService"].sort(),
);
expect(userService!.properties.sort()).toEqual(
["MaxRetries", "_name"].sort(),
);
const repository = result.classes.find(
(c) => c.name === "IRepository",
);
expect(repository).toBeDefined();
expect(repository!.methods).toEqual(["FindAll", "FindById"]);
expect(repository!.properties).toEqual([]);
// Imports: 2 (System, System.Collections.Generic)
expect(result.imports).toHaveLength(2);
expect(result.imports[0].source).toBe("System");
expect(result.imports[0].specifiers).toEqual(["System"]);
expect(result.imports[1].source).toBe("System.Collections.Generic");
expect(result.imports[1].specifiers).toEqual(["Generic"]);
// Exports: UserService (class + constructor), MaxRetries, GetUsers, IRepository
const exportNames = result.exports.map((e) => e.name);
expect(exportNames).toContain("UserService");
expect(exportNames).toContain("GetUsers");
expect(exportNames).toContain("MaxRetries");
expect(exportNames).toContain("IRepository");
expect(exportNames).not.toContain("Log"); // private
expect(exportNames).not.toContain("_name"); // private field
// Call graph
const calls = extractor.extractCallGraph(root);
const getUsersCalls = calls.filter((e) => e.caller === "GetUsers");
expect(getUsersCalls.some((e) => e.callee === "FetchFromDb")).toBe(
true,
);
const logCalls = calls.filter((e) => e.caller === "Log");
expect(
logCalls.some((e) => e.callee === "Console.WriteLine"),
).toBe(true);
tree.delete();
parser.delete();
});
});
});
@@ -0,0 +1,524 @@
import type { StructuralAnalysis, CallGraphEntry } from "../../types.js";
import type { LanguageExtractor, TreeSitterNode } from "./types.js";
import { findChild, findChildren } from "./base-extractor.js";
/**
* Extract parameter names from a C# `parameter_list` node.
*
* Each `parameter` child has a `name` field (identifier) and a `type` field.
*/
function extractParams(paramsNode: TreeSitterNode | null): string[] {
if (!paramsNode) return [];
const params: string[] = [];
const paramNodes = findChildren(paramsNode, "parameter");
for (const param of paramNodes) {
const nameNode = param.childForFieldName("name");
if (nameNode) {
params.push(nameNode.text);
}
}
return params;
}
/**
* Extract the return type text from a method_declaration node.
*
* In tree-sitter-c-sharp, the return type is the `returns` named field.
* It can be a predefined_type (void, int, string), generic_name (List<T>),
* identifier, nullable_type, etc.
*/
function extractReturnType(node: TreeSitterNode): string | undefined {
const typeNode = node.childForFieldName("returns");
if (!typeNode) return undefined;
return typeNode.text;
}
/**
* Check if a C# declaration node has a specific modifier.
*
* Unlike Java (which has a single `modifiers` container), C# tree-sitter
* emits multiple separate `modifier` nodes as direct children of the
* declaration. Each modifier node contains a single keyword child
* (e.g., `public`, `private`, `static`).
*/
function hasModifier(node: TreeSitterNode, modifier: string): boolean {
const modifierNodes = findChildren(node, "modifier");
for (const mod of modifierNodes) {
for (let i = 0; i < mod.childCount; i++) {
const child = mod.child(i);
if (child && child.text === modifier) return true;
}
}
return false;
}
/**
* Extract the namespace source text from a using_directive.
*
* Handles both simple identifiers (`using System;`) and qualified names
* (`using System.Collections.Generic;`). For aliased usings like
* `using Alias = Some.Namespace;`, extracts the target namespace.
*/
function extractUsingSource(node: TreeSitterNode): string | null {
// Check for alias form: `using Alias = Some.Namespace;`
const hasEquals = findChild(node, "=") !== null;
if (hasEquals) {
// The target namespace is the qualified_name after the `=`
const qualifiedName = findChild(node, "qualified_name");
return qualifiedName ? qualifiedName.text : null;
}
// Simple or qualified using
const qualifiedName = findChild(node, "qualified_name");
if (qualifiedName) return qualifiedName.text;
const identifier = findChild(node, "identifier");
return identifier ? identifier.text : null;
}
/**
* Get the last component of a dotted namespace path.
* e.g. "System.Collections.Generic" -> "Generic"
*/
function lastComponent(path: string): string {
const parts = path.split(".");
return parts[parts.length - 1];
}
/**
* Extract the callee name from an invocation_expression node.
*
* Handles:
* - Plain method call: `FetchFromDb(limit)` -> "FetchFromDb"
* (function field is an identifier)
* - Qualified call: `Console.WriteLine(msg)` -> "Console.WriteLine"
* (function field is a member_access_expression)
*/
function extractInvocationName(node: TreeSitterNode): string | null {
const funcNode = node.childForFieldName("function");
if (!funcNode) return null;
return funcNode.text;
}
/**
* C# extractor for tree-sitter structural analysis and call graph extraction.
*
* Handles classes, interfaces, methods, constructors, properties, fields,
* using directives, visibility-based exports, and call graphs for C# source code.
*
* C#-specific mapping decisions:
* - Classes and interfaces are mapped to the `classes` array.
* - Constructors are mapped to the `functions` array (named after the class).
* - Methods (including interface method signatures) are listed in the
* containing class/interface's `methods` array and also in the `functions` array.
* - Properties (e.g., `public string Name { get; set; }`) are extracted into
* the containing class's `properties` array alongside fields.
* - Exports are determined by the `public` modifier on classes, interfaces,
* methods, constructors, properties, and fields.
* - Namespaces: both block-scoped (`namespace Foo { ... }`) and file-scoped
* (`namespace Foo;`) are traversed to find declarations.
* - Using directives are mapped to imports, with the last dotted component
* as the specifier.
*/
export class CSharpExtractor implements LanguageExtractor {
readonly languageIds = ["csharp"];
extractStructure(rootNode: TreeSitterNode): StructuralAnalysis {
const functions: StructuralAnalysis["functions"] = [];
const classes: StructuralAnalysis["classes"] = [];
const imports: StructuralAnalysis["imports"] = [];
const exports: StructuralAnalysis["exports"] = [];
this.walkTopLevel(rootNode, functions, classes, imports, exports);
return { functions, classes, imports, exports };
}
extractCallGraph(rootNode: TreeSitterNode): CallGraphEntry[] {
const entries: CallGraphEntry[] = [];
const functionStack: string[] = [];
const walkForCalls = (node: TreeSitterNode) => {
let pushedName = false;
// Track entering method/constructor declarations
if (
node.type === "method_declaration" ||
node.type === "constructor_declaration"
) {
const nameNode = node.childForFieldName("name");
if (nameNode) {
functionStack.push(nameNode.text);
pushedName = true;
}
}
// Extract method invocations: e.g. FetchFromDb(limit), Console.WriteLine(msg)
if (node.type === "invocation_expression") {
if (functionStack.length > 0) {
const callee = extractInvocationName(node);
if (callee) {
entries.push({
caller: functionStack[functionStack.length - 1],
callee,
lineNumber: node.startPosition.row + 1,
});
}
}
}
// Extract object creation: e.g. new Foo()
if (node.type === "object_creation_expression") {
if (functionStack.length > 0) {
// The type is the child after `new` — can be identifier or generic_name
const typeNode = findChild(node, "identifier") ?? findChild(node, "generic_name");
if (typeNode) {
entries.push({
caller: functionStack[functionStack.length - 1],
callee: `new ${typeNode.text}`,
lineNumber: node.startPosition.row + 1,
});
}
}
}
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (child) walkForCalls(child);
}
if (pushedName) {
functionStack.pop();
}
};
walkForCalls(rootNode);
return entries;
}
// ---- Private helpers ----
/**
* Walk the top-level nodes of a compilation_unit, recursing into
* namespace bodies to find declarations.
*/
private walkTopLevel(
node: TreeSitterNode,
functions: StructuralAnalysis["functions"],
classes: StructuralAnalysis["classes"],
imports: StructuralAnalysis["imports"],
exports: StructuralAnalysis["exports"],
): void {
for (let i = 0; i < node.childCount; i++) {
const child = node.child(i);
if (!child) continue;
switch (child.type) {
case "using_directive":
this.extractUsing(child, imports);
break;
case "namespace_declaration":
// Recurse into namespace body (declaration_list)
this.walkNamespaceBody(child, functions, classes, imports, exports);
break;
case "file_scoped_namespace_declaration":
// File-scoped namespace: declarations are siblings at the root,
// not children of this node. Nothing to recurse into.
break;
case "class_declaration":
this.extractClass(child, functions, classes, exports);
break;
case "interface_declaration":
this.extractInterface(child, functions, classes, exports);
break;
}
}
}
/**
* Walk into a namespace_declaration's body (declaration_list) to find
* classes, interfaces, and nested namespaces.
*/
private walkNamespaceBody(
nsNode: TreeSitterNode,
functions: StructuralAnalysis["functions"],
classes: StructuralAnalysis["classes"],
imports: StructuralAnalysis["imports"],
exports: StructuralAnalysis["exports"],
): void {
const body = nsNode.childForFieldName("body");
if (!body) return;
for (let i = 0; i < body.childCount; i++) {
const child = body.child(i);
if (!child) continue;
switch (child.type) {
case "class_declaration":
this.extractClass(child, functions, classes, exports);
break;
case "interface_declaration":
this.extractInterface(child, functions, classes, exports);
break;
case "namespace_declaration":
// Nested namespaces
this.walkNamespaceBody(child, functions, classes, imports, exports);
break;
}
}
}
private extractUsing(
node: TreeSitterNode,
imports: StructuralAnalysis["imports"],
): void {
const source = extractUsingSource(node);
if (!source) return;
imports.push({
source,
specifiers: [lastComponent(source)],
lineNumber: node.startPosition.row + 1,
});
}
private extractClass(
node: TreeSitterNode,
functions: StructuralAnalysis["functions"],
classes: StructuralAnalysis["classes"],
exports: StructuralAnalysis["exports"],
): void {
const nameNode = node.childForFieldName("name");
if (!nameNode) return;
const methods: string[] = [];
const properties: string[] = [];
const body = node.childForFieldName("body");
if (body) {
this.extractClassBodyMembers(body, methods, properties, functions, exports);
}
classes.push({
name: nameNode.text,
lineRange: [
node.startPosition.row + 1,
node.endPosition.row + 1,
],
methods,
properties,
});
if (hasModifier(node, "public")) {
exports.push({
name: nameNode.text,
lineNumber: node.startPosition.row + 1,
});
}
}
private extractInterface(
node: TreeSitterNode,
functions: StructuralAnalysis["functions"],
classes: StructuralAnalysis["classes"],
exports: StructuralAnalysis["exports"],
): void {
const nameNode = node.childForFieldName("name");
if (!nameNode) return;
const methods: string[] = [];
const properties: string[] = [];
const body = node.childForFieldName("body");
if (body) {
// Interface body contains method_declaration nodes (signatures without bodies)
const methodNodes = findChildren(body, "method_declaration");
for (const methodNode of methodNodes) {
const methNameNode = methodNode.childForFieldName("name");
if (methNameNode) {
methods.push(methNameNode.text);
}
}
// Interface can contain property_declaration nodes
const propNodes = findChildren(body, "property_declaration");
for (const propNode of propNodes) {
const propNameNode = propNode.childForFieldName("name");
if (propNameNode) {
properties.push(propNameNode.text);
}
}
}
classes.push({
name: nameNode.text,
lineRange: [
node.startPosition.row + 1,
node.endPosition.row + 1,
],
methods,
properties,
});
if (hasModifier(node, "public")) {
exports.push({
name: nameNode.text,
lineNumber: node.startPosition.row + 1,
});
}
}
/**
* Extract methods, constructors, properties, and fields from a
* class declaration_list body.
*/
private extractClassBodyMembers(
body: TreeSitterNode,
methods: string[],
properties: string[],
functions: StructuralAnalysis["functions"],
exports: StructuralAnalysis["exports"],
): void {
for (let i = 0; i < body.childCount; i++) {
const child = body.child(i);
if (!child) continue;
switch (child.type) {
case "method_declaration":
this.extractMethod(child, methods, functions, exports);
break;
case "constructor_declaration":
this.extractConstructor(child, methods, functions, exports);
break;
case "property_declaration":
this.extractProperty(child, properties, exports);
break;
case "field_declaration":
this.extractField(child, properties, exports);
break;
}
}
}
private extractMethod(
node: TreeSitterNode,
methods: string[],
functions: StructuralAnalysis["functions"],
exports: StructuralAnalysis["exports"],
): void {
const nameNode = node.childForFieldName("name");
if (!nameNode) return;
const paramsNode = node.childForFieldName("parameters");
const params = extractParams(paramsNode ?? null);
const returnType = extractReturnType(node);
methods.push(nameNode.text);
functions.push({
name: nameNode.text,
lineRange: [
node.startPosition.row + 1,
node.endPosition.row + 1,
],
params,
returnType,
});
if (hasModifier(node, "public")) {
exports.push({
name: nameNode.text,
lineNumber: node.startPosition.row + 1,
});
}
}
private extractConstructor(
node: TreeSitterNode,
methods: string[],
functions: StructuralAnalysis["functions"],
exports: StructuralAnalysis["exports"],
): void {
const nameNode = node.childForFieldName("name");
if (!nameNode) return;
const paramsNode = node.childForFieldName("parameters");
const params = extractParams(paramsNode ?? null);
methods.push(nameNode.text);
functions.push({
name: nameNode.text,
lineRange: [
node.startPosition.row + 1,
node.endPosition.row + 1,
],
params,
// Constructors have no return type
});
if (hasModifier(node, "public")) {
exports.push({
name: nameNode.text,
lineNumber: node.startPosition.row + 1,
});
}
}
private extractProperty(
node: TreeSitterNode,
properties: string[],
exports: StructuralAnalysis["exports"],
): void {
const nameNode = node.childForFieldName("name");
if (!nameNode) return;
properties.push(nameNode.text);
if (hasModifier(node, "public")) {
exports.push({
name: nameNode.text,
lineNumber: node.startPosition.row + 1,
});
}
}
private extractField(
node: TreeSitterNode,
properties: string[],
exports: StructuralAnalysis["exports"],
): void {
// field_declaration -> variable_declaration -> variable_declarator(s)
const varDecl = findChild(node, "variable_declaration");
if (!varDecl) return;
const declarators = findChildren(varDecl, "variable_declarator");
for (const decl of declarators) {
// variable_declarator's first child is the identifier
const nameNode = findChild(decl, "identifier");
if (nameNode) {
properties.push(nameNode.text);
if (hasModifier(node, "public")) {
exports.push({
name: nameNode.text,
lineNumber: node.startPosition.row + 1,
});
}
}
}
}
}