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), * 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": case "record_declaration": case "struct_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": case "record_declaration": case "struct_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, }); } } } } }