/** * Tree-sitter Parser Wrapper * * Handles parsing source code and extracting structural information. */ import { Node as SyntaxNode, Tree } from 'web-tree-sitter'; import * as path from 'path'; import { Language, Node, Edge, NodeKind, ExtractionResult, ExtractionError, UnresolvedReference, } from '../types'; import { getParser, detectLanguage, isLanguageSupported } from './grammars'; import { generateNodeId, getNodeText, getChildByField, getPrecedingDocstring } from './tree-sitter-helpers'; import type { LanguageExtractor } from './tree-sitter-types'; import { EXTRACTORS } from './languages'; import { LiquidExtractor } from './liquid-extractor'; import { SvelteExtractor } from './svelte-extractor'; import { DfmExtractor } from './dfm-extractor'; // Re-export for backward compatibility export { generateNodeId } from './tree-sitter-helpers'; /** * Extract the name from a node based on language */ function extractName(node: SyntaxNode, source: string, extractor: LanguageExtractor): string { // Try field name first const nameNode = getChildByField(node, extractor.nameField); if (nameNode) { // Handle complex declarators (C/C++) if (nameNode.type === 'function_declarator' || nameNode.type === 'declarator') { const innerName = getChildByField(nameNode, 'declarator') || nameNode.namedChild(0); return innerName ? getNodeText(innerName, source) : getNodeText(nameNode, source); } return getNodeText(nameNode, source); } // For Dart method_signature, look inside inner signature types if (node.type === 'method_signature') { for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (child && ( child.type === 'function_signature' || child.type === 'getter_signature' || child.type === 'setter_signature' || child.type === 'constructor_signature' || child.type === 'factory_constructor_signature' )) { // Find identifier inside the inner signature for (let j = 0; j < child.namedChildCount; j++) { const inner = child.namedChild(j); if (inner?.type === 'identifier') { return getNodeText(inner, source); } } } } } // Fall back to first identifier child for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if ( child && (child.type === 'identifier' || child.type === 'type_identifier' || child.type === 'simple_identifier' || child.type === 'constant') ) { return getNodeText(child, source); } } return ''; } /** * TreeSitterExtractor - Main extraction class */ export class TreeSitterExtractor { private filePath: string; private language: Language; private source: string; private tree: Tree | null = null; private nodes: Node[] = []; private edges: Edge[] = []; private unresolvedReferences: UnresolvedReference[] = []; private errors: ExtractionError[] = []; private extractor: LanguageExtractor | null = null; private nodeStack: string[] = []; // Stack of parent node IDs private methodIndex: Map | null = null; // lookup key → node ID for Pascal defProc lookup constructor(filePath: string, source: string, language?: Language) { this.filePath = filePath; this.source = source; this.language = language || detectLanguage(filePath); this.extractor = EXTRACTORS[this.language] || null; } /** * Parse and extract from the source code */ extract(): ExtractionResult { const startTime = Date.now(); if (!isLanguageSupported(this.language)) { return { nodes: [], edges: [], unresolvedReferences: [], errors: [ { message: `Unsupported language: ${this.language}`, filePath: this.filePath, severity: 'error', code: 'unsupported_language', }, ], durationMs: Date.now() - startTime, }; } const parser = getParser(this.language); if (!parser) { return { nodes: [], edges: [], unresolvedReferences: [], errors: [ { message: `Failed to get parser for language: ${this.language}`, filePath: this.filePath, severity: 'error', code: 'parser_error', }, ], durationMs: Date.now() - startTime, }; } try { this.tree = parser.parse(this.source) ?? null; if (!this.tree) { throw new Error('Parser returned null tree'); } // Create file node representing the source file const fileNode: Node = { id: `file:${this.filePath}`, kind: 'file', name: path.basename(this.filePath), qualifiedName: this.filePath, filePath: this.filePath, language: this.language, startLine: 1, endLine: this.source.split('\n').length, startColumn: 0, endColumn: 0, isExported: false, updatedAt: Date.now(), }; this.nodes.push(fileNode); // Push file node onto stack so top-level declarations get contains edges this.nodeStack.push(fileNode.id); this.visitNode(this.tree.rootNode); this.nodeStack.pop(); } catch (error) { const msg = error instanceof Error ? error.message : String(error); // WASM memory errors leave the module in a corrupted state — all subsequent // parses would also fail. Re-throw so the worker can detect and crash, // forcing a clean restart with a fresh heap. if (msg.includes('memory access out of bounds') || msg.includes('out of memory')) { throw error; } this.errors.push({ message: `Parse error: ${msg}`, filePath: this.filePath, severity: 'error', code: 'parse_error', }); } finally { // Free tree-sitter WASM memory immediately — trees hold native heap memory // invisible to V8's GC that accumulates across thousands of files. if (this.tree) { this.tree.delete(); this.tree = null; } // Release source string to reduce GC pressure this.source = ''; } return { nodes: this.nodes, edges: this.edges, unresolvedReferences: this.unresolvedReferences, errors: this.errors, durationMs: Date.now() - startTime, }; } /** * Visit a node and extract information */ private visitNode(node: SyntaxNode): void { if (!this.extractor) return; const nodeType = node.type; let skipChildren = false; // Pascal-specific AST handling if (this.language === 'pascal') { skipChildren = this.visitPascalNode(node); if (skipChildren) return; } // Check for function declarations // For Python/Ruby, function_definition inside a class should be treated as method if (this.extractor.functionTypes.includes(nodeType)) { if (this.isInsideClassLikeNode() && this.extractor.methodTypes.includes(nodeType)) { // Inside a class - treat as method this.extractMethod(node); skipChildren = true; // extractMethod visits children via visitFunctionBody } else { this.extractFunction(node); skipChildren = true; // extractFunction visits children via visitFunctionBody } } // Check for class declarations else if (this.extractor.classTypes.includes(nodeType)) { // Some languages reuse class_declaration for structs/enums (e.g. Swift) const classification = this.extractor.classifyClassNode?.(node) ?? 'class'; if (classification === 'struct') { this.extractStruct(node); } else if (classification === 'enum') { this.extractEnum(node); } else { this.extractClass(node); } skipChildren = true; // extractClass visits body children } // Extra class node types (e.g. Dart mixin_declaration, extension_declaration) else if (this.extractor.extraClassNodeTypes?.includes(nodeType)) { this.extractClass(node); skipChildren = true; } // Check for method declarations (only if not already handled by functionTypes) else if (this.extractor.methodTypes.includes(nodeType)) { this.extractMethod(node); skipChildren = true; // extractMethod visits children via visitFunctionBody } // Check for interface/protocol/trait declarations else if (this.extractor.interfaceTypes.includes(nodeType)) { this.extractInterface(node); skipChildren = true; // extractInterface visits body children } // Check for struct declarations else if (this.extractor.structTypes.includes(nodeType)) { this.extractStruct(node); skipChildren = true; // extractStruct visits body children } // Check for enum declarations else if (this.extractor.enumTypes.includes(nodeType)) { this.extractEnum(node); skipChildren = true; // extractEnum visits body children } // Check for type alias declarations (e.g. `type X = ...` in TypeScript) // For Go, type_spec wraps struct/interface definitions — resolveTypeAliasKind // detects these and extractTypeAlias creates the correct node kind. else if (this.extractor.typeAliasTypes.includes(nodeType)) { skipChildren = this.extractTypeAlias(node); } // Check for class fields (e.g. Java field_declaration, C# field_declaration) else if (this.extractor.fieldTypes?.includes(nodeType) && this.isInsideClassLikeNode()) { this.extractField(node); skipChildren = true; } // Check for variable declarations (const, let, var, etc.) // Only extract top-level variables (not inside functions/methods) else if (this.extractor.variableTypes.includes(nodeType) && !this.isInsideClassLikeNode()) { this.extractVariable(node); skipChildren = true; // extractVariable handles children } // Check for export statements containing non-function variable declarations // e.g. `export const X = create(...)`, `export const X = { ... }` else if (nodeType === 'export_statement') { this.extractExportedVariables(node); // Don't skip children — still need to visit inner nodes (functions, calls, etc.) } // Check for imports else if (this.extractor.importTypes.includes(nodeType)) { this.extractImport(node); } // Check for function calls else if (this.extractor.callTypes.includes(nodeType)) { this.extractCall(node); } // Visit children (unless the extract method already visited them) if (!skipChildren) { for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (child) { this.visitNode(child); } } } } /** * Create a Node object */ private createNode( kind: NodeKind, name: string, node: SyntaxNode, extra?: Partial ): Node | null { // Skip nodes with empty/missing names — they are not meaningful symbols // and would cause FK violations when edges reference them (see issue #42) if (!name) { return null; } const id = generateNodeId(this.filePath, kind, name, node.startPosition.row + 1); const newNode: Node = { id, kind, name, qualifiedName: this.buildQualifiedName(name), filePath: this.filePath, language: this.language, startLine: node.startPosition.row + 1, endLine: node.endPosition.row + 1, startColumn: node.startPosition.column, endColumn: node.endPosition.column, updatedAt: Date.now(), ...extra, }; this.nodes.push(newNode); // Add containment edge from parent if (this.nodeStack.length > 0) { const parentId = this.nodeStack[this.nodeStack.length - 1]; if (parentId) { this.edges.push({ source: parentId, target: id, kind: 'contains', }); } } return newNode; } /** * Build qualified name from node stack */ private buildQualifiedName(name: string): string { // Build a qualified name from the semantic hierarchy only (no file path). // The file path is stored separately in filePath and pollutes FTS if included here. const parts: string[] = []; for (const nodeId of this.nodeStack) { const node = this.nodes.find((n) => n.id === nodeId); if (node && node.kind !== 'file') { parts.push(node.name); } } parts.push(name); return parts.join('::'); } /** * Check if the current node stack indicates we are inside a class-like node * (class, struct, interface, trait). File nodes do not count as class-like. */ private isInsideClassLikeNode(): boolean { if (this.nodeStack.length === 0) return false; const parentId = this.nodeStack[this.nodeStack.length - 1]; if (!parentId) return false; const parentNode = this.nodes.find((n) => n.id === parentId); if (!parentNode) return false; return ( parentNode.kind === 'class' || parentNode.kind === 'struct' || parentNode.kind === 'interface' || parentNode.kind === 'trait' || parentNode.kind === 'enum' ); } /** * Extract a function */ private extractFunction(node: SyntaxNode): void { if (!this.extractor) return; let name = extractName(node, this.source, this.extractor); // For arrow functions and function expressions assigned to variables, // resolve the name from the parent variable_declarator. // e.g. `export const useAuth = () => { ... }` — the arrow_function node // has no `name` field; the name lives on the variable_declarator. if ( name === '' && (node.type === 'arrow_function' || node.type === 'function_expression') ) { const parent = node.parent; if (parent?.type === 'variable_declarator') { const varName = getChildByField(parent, 'name'); if (varName) { name = getNodeText(varName, this.source); } } } if (name === '') return; // Skip anonymous functions const docstring = getPrecedingDocstring(node, this.source); const signature = this.extractor.getSignature?.(node, this.source); const visibility = this.extractor.getVisibility?.(node); const isExported = this.extractor.isExported?.(node, this.source); const isAsync = this.extractor.isAsync?.(node); const isStatic = this.extractor.isStatic?.(node); const funcNode = this.createNode('function', name, node, { docstring, signature, visibility, isExported, isAsync, isStatic, }); if (!funcNode) return; // Extract type annotations (parameter types and return type) this.extractTypeAnnotations(node, funcNode.id); // Push to stack and visit body this.nodeStack.push(funcNode.id); const body = this.extractor.resolveBody?.(node, this.extractor.bodyField) ?? getChildByField(node, this.extractor.bodyField); if (body) { this.visitFunctionBody(body, funcNode.id); } this.nodeStack.pop(); } /** * Extract a class */ private extractClass(node: SyntaxNode): void { if (!this.extractor) return; const name = extractName(node, this.source, this.extractor); const docstring = getPrecedingDocstring(node, this.source); const visibility = this.extractor.getVisibility?.(node); const isExported = this.extractor.isExported?.(node, this.source); const classNode = this.createNode('class', name, node, { docstring, visibility, isExported, }); if (!classNode) return; // Extract extends/implements this.extractInheritance(node, classNode.id); // Push to stack and visit body this.nodeStack.push(classNode.id); let body = this.extractor.resolveBody?.(node, this.extractor.bodyField) ?? getChildByField(node, this.extractor.bodyField); if (!body) body = node; // Visit all children for methods and properties for (let i = 0; i < body.namedChildCount; i++) { const child = body.namedChild(i); if (child) { this.visitNode(child); } } this.nodeStack.pop(); } /** * Extract a method */ private extractMethod(node: SyntaxNode): void { if (!this.extractor) return; // For most languages, only extract as method if inside a class-like node // Languages with methodsAreTopLevel (e.g. Go) always treat them as methods if (!this.isInsideClassLikeNode() && !this.extractor.methodsAreTopLevel) { // Not inside a class-like node and not Go, treat as function this.extractFunction(node); return; } const name = extractName(node, this.source, this.extractor); const docstring = getPrecedingDocstring(node, this.source); const signature = this.extractor.getSignature?.(node, this.source); const visibility = this.extractor.getVisibility?.(node); const isAsync = this.extractor.isAsync?.(node); const isStatic = this.extractor.isStatic?.(node); // For languages with receiver types (Go), include receiver in qualified name // so FTS can match "scrapeLoop.run" → qualified_name "...::scrapeLoop::run" const receiverType = this.extractor.getReceiverType?.(node, this.source); const extraProps: Partial = { docstring, signature, visibility, isAsync, isStatic, }; if (receiverType) { extraProps.qualifiedName = `${receiverType}::${name}`; } const methodNode = this.createNode('method', name, node, extraProps); if (!methodNode) return; // Extract type annotations (parameter types and return type) this.extractTypeAnnotations(node, methodNode.id); // Push to stack and visit body this.nodeStack.push(methodNode.id); const body = this.extractor.resolveBody?.(node, this.extractor.bodyField) ?? getChildByField(node, this.extractor.bodyField); if (body) { this.visitFunctionBody(body, methodNode.id); } this.nodeStack.pop(); } /** * Extract an interface/protocol/trait */ private extractInterface(node: SyntaxNode): void { if (!this.extractor) return; const name = extractName(node, this.source, this.extractor); const docstring = getPrecedingDocstring(node, this.source); const isExported = this.extractor.isExported?.(node, this.source); const kind: NodeKind = this.extractor.interfaceKind ?? 'interface'; const interfaceNode = this.createNode(kind, name, node, { docstring, isExported, }); if (!interfaceNode) return; // Extract extends (interface inheritance) this.extractInheritance(node, interfaceNode.id); } /** * Extract a struct */ private extractStruct(node: SyntaxNode): void { if (!this.extractor) return; const name = extractName(node, this.source, this.extractor); const docstring = getPrecedingDocstring(node, this.source); const visibility = this.extractor.getVisibility?.(node); const isExported = this.extractor.isExported?.(node, this.source); const structNode = this.createNode('struct', name, node, { docstring, visibility, isExported, }); if (!structNode) return; // Push to stack for field extraction this.nodeStack.push(structNode.id); const body = getChildByField(node, this.extractor.bodyField) || node; for (let i = 0; i < body.namedChildCount; i++) { const child = body.namedChild(i); if (child) { this.visitNode(child); } } this.nodeStack.pop(); } /** * Extract an enum */ private extractEnum(node: SyntaxNode): void { if (!this.extractor) return; const name = extractName(node, this.source, this.extractor); const docstring = getPrecedingDocstring(node, this.source); const visibility = this.extractor.getVisibility?.(node); const isExported = this.extractor.isExported?.(node, this.source); const enumNode = this.createNode('enum', name, node, { docstring, visibility, isExported, }); if (!enumNode) return; // Extract inheritance (e.g. Swift: enum AFError: Error) this.extractInheritance(node, enumNode.id); // Push to stack and visit body children (enum members, nested types, methods) this.nodeStack.push(enumNode.id); const body = this.extractor.resolveBody?.(node, this.extractor.bodyField) ?? getChildByField(node, this.extractor.bodyField) ?? node; const memberTypes = this.extractor.enumMemberTypes; for (let i = 0; i < body.namedChildCount; i++) { const child = body.namedChild(i); if (!child) continue; if (memberTypes?.includes(child.type)) { this.extractEnumMembers(child); } else { this.visitNode(child); } } this.nodeStack.pop(); } /** * Extract enum member names from an enum member node. * Handles multi-case declarations (Swift: `case put, delete`) and single-case patterns. */ private extractEnumMembers(node: SyntaxNode): void { // Try field-based name first (e.g. Rust enum_variant has a 'name' field) const nameNode = getChildByField(node, 'name'); if (nameNode) { this.createNode('enum_member', getNodeText(nameNode, this.source), node); return; } // Check for identifier-like children (Swift: simple_identifier, TS: property_identifier) let found = false; for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (child && (child.type === 'simple_identifier' || child.type === 'identifier' || child.type === 'property_identifier')) { this.createNode('enum_member', getNodeText(child, this.source), child); found = true; } } // If the node itself IS the identifier (e.g. TS property_identifier directly in enum body) if (!found && node.namedChildCount === 0) { this.createNode('enum_member', getNodeText(node, this.source), node); } } /** * Extract a class field declaration (e.g. Java field_declaration, C# field_declaration). * Extracts each declarator as a 'field' kind node inside the owning class. */ private extractField(node: SyntaxNode): void { if (!this.extractor) return; const docstring = getPrecedingDocstring(node, this.source); const visibility = this.extractor.getVisibility?.(node); const isStatic = this.extractor.isStatic?.(node) ?? false; // Java field_declaration: "private final String name = value;" // Children include modifiers, type, variable_declarator(s) const declarators = node.namedChildren.filter( c => c.type === 'variable_declarator' ); if (declarators.length > 0) { // Get field type from the type child const typeNode = node.namedChildren.find( c => c.type !== 'modifiers' && c.type !== 'variable_declarator' && c.type !== 'marker_annotation' && c.type !== 'annotation' ); const typeText = typeNode ? getNodeText(typeNode, this.source) : undefined; for (const decl of declarators) { const nameNode = getChildByField(decl, 'name'); if (!nameNode) continue; const name = getNodeText(nameNode, this.source); const signature = typeText ? `${typeText} ${name}` : name; this.createNode('field', name, decl, { docstring, signature, visibility, isStatic, }); } } else { // Fallback: try to find an identifier child directly const nameNode = getChildByField(node, 'name') || node.namedChildren.find(c => c.type === 'identifier'); if (nameNode) { const name = getNodeText(nameNode, this.source); this.createNode('field', name, node, { docstring, visibility, isStatic, }); } } } /** * Extract a variable declaration (const, let, var, etc.) * * Extracts top-level and module-level variable declarations. * Captures the variable name and first 100 chars of initializer in signature for searchability. */ private extractVariable(node: SyntaxNode): void { if (!this.extractor) return; // Different languages have different variable declaration structures // TypeScript/JavaScript: lexical_declaration contains variable_declarator children // Python: assignment has left (identifier) and right (value) // Go: var_declaration, short_var_declaration, const_declaration const isConst = this.extractor.isConst?.(node) ?? false; const kind: NodeKind = isConst ? 'constant' : 'variable'; const docstring = getPrecedingDocstring(node, this.source); const isExported = this.extractor.isExported?.(node, this.source) ?? false; // Extract variable declarators based on language if (this.language === 'typescript' || this.language === 'javascript' || this.language === 'tsx' || this.language === 'jsx') { // Handle lexical_declaration and variable_declaration // These contain one or more variable_declarator children for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (child?.type === 'variable_declarator') { const nameNode = getChildByField(child, 'name'); const valueNode = getChildByField(child, 'value'); if (nameNode) { const name = getNodeText(nameNode, this.source); // Arrow functions / function expressions: extract as function instead of variable if (valueNode && (valueNode.type === 'arrow_function' || valueNode.type === 'function_expression')) { this.extractFunction(valueNode); continue; } // Capture first 100 chars of initializer for context (stored in signature for searchability) const initValue = valueNode ? getNodeText(valueNode, this.source).slice(0, 100) : undefined; const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined; const varNode = this.createNode(kind, name, child, { docstring, signature: initSignature, isExported, }); // Extract type annotation references (e.g., const x: ITextModel = ...) if (varNode) { this.extractVariableTypeAnnotation(child, varNode.id); } } } } } else if (this.language === 'python' || this.language === 'ruby') { // Python/Ruby assignment: left = right const left = getChildByField(node, 'left') || node.namedChild(0); const right = getChildByField(node, 'right') || node.namedChild(1); if (left && left.type === 'identifier') { const name = getNodeText(left, this.source); // Skip if name starts with lowercase and looks like a function call result // Python constants are usually UPPER_CASE const initValue = right ? getNodeText(right, this.source).slice(0, 100) : undefined; const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined; this.createNode(kind, name, node, { docstring, signature: initSignature, }); } } else if (this.language === 'go') { // Go: var_declaration, short_var_declaration, const_declaration // These can have multiple identifiers on the left const specs = node.namedChildren.filter(c => c.type === 'var_spec' || c.type === 'const_spec' ); for (const spec of specs) { const nameNode = spec.namedChild(0); if (nameNode && nameNode.type === 'identifier') { const name = getNodeText(nameNode, this.source); const valueNode = spec.namedChildCount > 1 ? spec.namedChild(spec.namedChildCount - 1) : null; const initValue = valueNode ? getNodeText(valueNode, this.source).slice(0, 100) : undefined; const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined; this.createNode(node.type === 'const_declaration' ? 'constant' : 'variable', name, spec, { docstring, signature: initSignature, }); } } // Handle short_var_declaration (:=) if (node.type === 'short_var_declaration') { const left = getChildByField(node, 'left'); const right = getChildByField(node, 'right'); if (left) { // Can be expression_list with multiple identifiers const identifiers = left.type === 'expression_list' ? left.namedChildren.filter(c => c.type === 'identifier') : [left]; for (const id of identifiers) { const name = getNodeText(id, this.source); const initValue = right ? getNodeText(right, this.source).slice(0, 100) : undefined; const initSignature = initValue ? `= ${initValue}${initValue.length >= 100 ? '...' : ''}` : undefined; this.createNode('variable', name, node, { docstring, signature: initSignature, }); } } } } else { // Generic fallback for other languages // Try to find identifier children for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (child?.type === 'identifier' || child?.type === 'variable_declarator') { const name = child.type === 'identifier' ? getNodeText(child, this.source) : extractName(child, this.source, this.extractor); if (name && name !== '') { this.createNode(kind, name, child, { docstring, isExported, }); } } } } } /** * Extract a type alias (e.g. `export type X = ...` in TypeScript). * For languages like Go, resolveTypeAliasKind detects when the type_spec * wraps a struct or interface definition and creates the correct node kind. * Returns true if children should be skipped (struct/interface handled body visiting). */ private extractTypeAlias(node: SyntaxNode): boolean { if (!this.extractor) return false; const name = extractName(node, this.source, this.extractor); if (name === '') return false; const docstring = getPrecedingDocstring(node, this.source); const isExported = this.extractor.isExported?.(node, this.source); // Check if this type alias is actually a struct or interface definition // (e.g. Go: `type Foo struct { ... }` is a type_spec wrapping struct_type) const resolvedKind = this.extractor.resolveTypeAliasKind?.(node, this.source); if (resolvedKind === 'struct') { const structNode = this.createNode('struct', name, node, { docstring, isExported }); if (!structNode) return true; // Visit body children for field extraction this.nodeStack.push(structNode.id); const typeChild = getChildByField(node, 'type'); if (typeChild) { // Extract struct embedding (e.g. Go: `type DB struct { *Head; Queryable }`) this.extractInheritance(typeChild, structNode.id); const body = getChildByField(typeChild, this.extractor.bodyField) || typeChild; for (let i = 0; i < body.namedChildCount; i++) { const child = body.namedChild(i); if (child) this.visitNode(child); } } this.nodeStack.pop(); return true; } if (resolvedKind === 'interface') { const kind: NodeKind = this.extractor.interfaceKind ?? 'interface'; const interfaceNode = this.createNode(kind, name, node, { docstring, isExported }); if (!interfaceNode) return true; // Extract interface inheritance from the inner type node const typeChild = getChildByField(node, 'type'); if (typeChild) this.extractInheritance(typeChild, interfaceNode.id); return true; } const typeAliasNode = this.createNode('type_alias', name, node, { docstring, isExported, }); // Extract type references from the alias value (e.g., `type X = ITextModel | null`) if (typeAliasNode && this.TYPE_ANNOTATION_LANGUAGES.has(this.language)) { // The value is everything after the `=`, which is typically the last named child // In tree-sitter TS: type_alias_declaration has name + value children const value = getChildByField(node, 'value'); if (value) { this.extractTypeRefsFromSubtree(value, typeAliasNode.id); } } return false; } /** * Extract an exported variable declaration that isn't a function. * Handles patterns like: * export const X = create(...) * export const X = { ... } * export const X = [...] * export const X = "value" * * This is called for `export_statement` nodes that contain a * `lexical_declaration` with `variable_declarator` children whose * values are NOT already handled by functionTypes (arrow_function, * function_expression). */ private extractExportedVariables(exportNode: SyntaxNode): void { if (!this.extractor) return; // Find the lexical_declaration or variable_declaration child for (let i = 0; i < exportNode.namedChildCount; i++) { const decl = exportNode.namedChild(i); if (!decl || (decl.type !== 'lexical_declaration' && decl.type !== 'variable_declaration')) { continue; } // Iterate over each variable_declarator in the declaration for (let j = 0; j < decl.namedChildCount; j++) { const declarator = decl.namedChild(j); if (!declarator || declarator.type !== 'variable_declarator') continue; const nameNode = getChildByField(declarator, 'name'); if (!nameNode) continue; const name = getNodeText(nameNode, this.source); // Skip if the value is a function type — those are already handled // by extractFunction via the functionTypes dispatch const value = getChildByField(declarator, 'value'); if (value) { const valueType = value.type; if ( this.extractor.functionTypes.includes(valueType) ) { continue; // Already handled by extractFunction } } const docstring = getPrecedingDocstring(exportNode, this.source); this.createNode('variable', name, declarator, { docstring, isExported: true, }); } } } /** * Extract an import * * Creates an import node with the full import statement stored in signature for searchability. * Also creates unresolved references for resolution purposes. */ private extractImport(node: SyntaxNode): void { if (!this.extractor) return; const importText = getNodeText(node, this.source).trim(); // Try language-specific hook first if (this.extractor.extractImport) { const info = this.extractor.extractImport(node, this.source); if (info) { this.createNode('import', info.moduleName, node, { signature: info.signature, }); // Create unresolved reference unless the hook handled it if (!info.handledRefs && info.moduleName && this.nodeStack.length > 0) { const parentId = this.nodeStack[this.nodeStack.length - 1]; if (parentId) { this.unresolvedReferences.push({ fromNodeId: parentId, referenceName: info.moduleName, referenceKind: 'imports', line: node.startPosition.row + 1, column: node.startPosition.column, }); } } return; } // Hook returned null — fall through to multi-import inline handlers only // (hook returning null means "I didn't handle this" for multi-import cases, // NOT "use generic fallback" — the hook already declined) } // Multi-import cases that create multiple nodes (can't be expressed with single-return hook) // Python import_statement: import os, sys (creates one import per module) if (this.language === 'python' && node.type === 'import_statement') { for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (child?.type === 'dotted_name') { this.createNode('import', getNodeText(child, this.source), node, { signature: importText, }); } else if (child?.type === 'aliased_import') { const dottedName = child.namedChildren.find(c => c.type === 'dotted_name'); if (dottedName) { this.createNode('import', getNodeText(dottedName, this.source), node, { signature: importText, }); } } } return; } // Go imports: single or grouped (creates one import per spec) if (this.language === 'go') { const parentId = this.nodeStack.length > 0 ? this.nodeStack[this.nodeStack.length - 1] : null; const extractFromSpec = (spec: SyntaxNode): void => { const stringLiteral = spec.namedChildren.find(c => c.type === 'interpreted_string_literal'); if (stringLiteral) { const importPath = getNodeText(stringLiteral, this.source).replace(/['"]/g, ''); if (importPath) { this.createNode('import', importPath, spec, { signature: getNodeText(spec, this.source).trim(), }); // Create unresolved reference so the resolver can create imports edges if (parentId) { this.unresolvedReferences.push({ fromNodeId: parentId, referenceName: importPath, referenceKind: 'imports', line: spec.startPosition.row + 1, column: spec.startPosition.column, }); } } } }; const importSpecList = node.namedChildren.find(c => c.type === 'import_spec_list'); if (importSpecList) { for (const spec of importSpecList.namedChildren.filter(c => c.type === 'import_spec')) { extractFromSpec(spec); } } else { const importSpec = node.namedChildren.find(c => c.type === 'import_spec'); if (importSpec) { extractFromSpec(importSpec); } } return; } // PHP grouped imports: use X\{A, B} (creates one import per item) if (this.language === 'php') { const namespacePrefix = node.namedChildren.find(c => c.type === 'namespace_name'); const useGroup = node.namedChildren.find(c => c.type === 'namespace_use_group'); if (namespacePrefix && useGroup) { const prefix = getNodeText(namespacePrefix, this.source); const useClauses = useGroup.namedChildren.filter((c: SyntaxNode) => c.type === 'namespace_use_group_clause' || c.type === 'namespace_use_clause' ); for (const clause of useClauses) { const nsName = clause.namedChildren.find((c: SyntaxNode) => c.type === 'namespace_name'); const name = nsName ? nsName.namedChildren.find((c: SyntaxNode) => c.type === 'name') : clause.namedChildren.find((c: SyntaxNode) => c.type === 'name'); if (name) { const fullPath = `${prefix}\\${getNodeText(name, this.source)}`; this.createNode('import', fullPath, node, { signature: importText, }); } } return; } } // If a hook exists but returned null, it intentionally declined this node — don't create fallback if (this.extractor.extractImport) return; // Generic fallback for languages without hooks this.createNode('import', importText, node, { signature: importText, }); } /** * Extract a function call */ private extractCall(node: SyntaxNode): void { if (this.nodeStack.length === 0) return; const callerId = this.nodeStack[this.nodeStack.length - 1]; if (!callerId) return; // Get the function/method being called let calleeName = ''; // Java/Kotlin method_invocation has 'object' + 'name' fields instead of 'function' const nameField = getChildByField(node, 'name'); const objectField = getChildByField(node, 'object'); if (nameField && objectField && node.type === 'method_invocation') { // Java-style method call: receiver.method() const methodName = getNodeText(nameField, this.source); const receiverName = getNodeText(objectField, this.source); if (methodName) { // Emit receiver.method form for qualified resolution calleeName = `${receiverName}.${methodName}`; } } else { const func = getChildByField(node, 'function') || node.namedChild(0); if (func) { if (func.type === 'member_expression' || func.type === 'attribute' || func.type === 'selector_expression') { // Method call: obj.method() or obj.field.method() // Go uses selector_expression with 'field', JS/TS uses member_expression with 'property' const property = getChildByField(func, 'property') || getChildByField(func, 'field') || func.namedChild(1); if (property) { const methodName = getNodeText(property, this.source); // Include receiver name for qualified resolution (e.g., console.print → "console.print") // This helps the resolver distinguish method calls from bare function calls // (e.g., Python's console.print() vs builtin print()) // Skip self/this/cls as they don't aid resolution const receiver = getChildByField(func, 'object') || getChildByField(func, 'operand') || func.namedChild(0); const SKIP_RECEIVERS = new Set(['self', 'this', 'cls', 'super']); if (receiver && receiver.type === 'identifier') { const receiverName = getNodeText(receiver, this.source); if (!SKIP_RECEIVERS.has(receiverName)) { calleeName = `${receiverName}.${methodName}`; } else { calleeName = methodName; } } else { calleeName = methodName; } } } else if (func.type === 'scoped_identifier' || func.type === 'scoped_call_expression') { // Scoped call: Module::function() calleeName = getNodeText(func, this.source); } else { calleeName = getNodeText(func, this.source); } } } if (calleeName) { this.unresolvedReferences.push({ fromNodeId: callerId, referenceName: calleeName, referenceKind: 'calls', line: node.startPosition.row + 1, column: node.startPosition.column, }); } } /** * Visit function body and extract calls */ private visitFunctionBody(body: SyntaxNode, _functionId: string): void { if (!this.extractor) return; // Recursively find all call expressions const visitForCalls = (node: SyntaxNode): void => { if (this.extractor!.callTypes.includes(node.type)) { this.extractCall(node); } for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (child) { visitForCalls(child); } } }; visitForCalls(body); } /** * Extract inheritance relationships */ private extractInheritance(node: SyntaxNode, classId: string): void { // Look for extends/implements clauses for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (!child) continue; if ( child.type === 'extends_clause' || child.type === 'class_heritage' || child.type === 'superclass' || child.type === 'extends_interfaces' // Java interface extends ) { // Extract parent class/interface names // Java uses type_list wrapper: superclass -> type_identifier, extends_interfaces -> type_list -> type_identifier const typeList = child.namedChildren.find((c: SyntaxNode) => c.type === 'type_list'); const targets = typeList ? typeList.namedChildren : [child.namedChild(0)]; for (const target of targets) { if (target) { const name = getNodeText(target, this.source); this.unresolvedReferences.push({ fromNodeId: classId, referenceName: name, referenceKind: 'extends', line: target.startPosition.row + 1, column: target.startPosition.column, }); } } } if ( child.type === 'implements_clause' || child.type === 'class_interface_clause' || child.type === 'super_interfaces' || // Java class implements child.type === 'interfaces' // Dart ) { // Extract implemented interfaces // Java uses type_list wrapper: super_interfaces -> type_list -> type_identifier const typeList = child.namedChildren.find((c: SyntaxNode) => c.type === 'type_list'); const targets = typeList ? typeList.namedChildren : child.namedChildren; for (const iface of targets) { if (iface) { const name = getNodeText(iface, this.source); this.unresolvedReferences.push({ fromNodeId: classId, referenceName: name, referenceKind: 'implements', line: iface.startPosition.row + 1, column: iface.startPosition.column, }); } } } // Python superclass list: `class Flask(Scaffold, Mixin):` // argument_list contains identifier children for each parent class if (child.type === 'argument_list' && node.type === 'class_definition') { for (const arg of child.namedChildren) { if (arg.type === 'identifier' || arg.type === 'attribute') { const name = getNodeText(arg, this.source); this.unresolvedReferences.push({ fromNodeId: classId, referenceName: name, referenceKind: 'extends', line: arg.startPosition.row + 1, column: arg.startPosition.column, }); } } } // Go interface embedding: `type Querier interface { LabelQuerier; ... }` // constraint_elem wraps the embedded interface type identifier if (child.type === 'constraint_elem') { const typeId = child.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier'); if (typeId) { const name = getNodeText(typeId, this.source); this.unresolvedReferences.push({ fromNodeId: classId, referenceName: name, referenceKind: 'extends', line: typeId.startPosition.row + 1, column: typeId.startPosition.column, }); } } // Go struct embedding: field_declaration without field_identifier // e.g. `type DB struct { *Head; Queryable }` — no field name means embedded type if (child.type === 'field_declaration') { const hasFieldIdentifier = child.namedChildren.some((c: SyntaxNode) => c.type === 'field_identifier'); if (!hasFieldIdentifier) { const typeId = child.namedChildren.find((c: SyntaxNode) => c.type === 'type_identifier'); if (typeId) { const name = getNodeText(typeId, this.source); this.unresolvedReferences.push({ fromNodeId: classId, referenceName: name, referenceKind: 'extends', line: typeId.startPosition.row + 1, column: typeId.startPosition.column, }); } } } // Recurse into container nodes (e.g. field_declaration_list in Go structs) if (child.type === 'field_declaration_list') { this.extractInheritance(child, classId); } } } /** * Languages that support type annotations (TypeScript, etc.) */ private readonly TYPE_ANNOTATION_LANGUAGES = new Set([ 'typescript', 'tsx', 'dart', 'kotlin', 'swift', 'rust', 'go', 'java', 'csharp', ]); /** * Built-in/primitive type names that shouldn't create references */ private readonly BUILTIN_TYPES = new Set([ 'string', 'number', 'boolean', 'void', 'null', 'undefined', 'never', 'any', 'unknown', 'object', 'symbol', 'bigint', 'true', 'false', // Rust 'str', 'bool', 'i8', 'i16', 'i32', 'i64', 'i128', 'isize', 'u8', 'u16', 'u32', 'u64', 'u128', 'usize', 'f32', 'f64', 'char', // Java/C# 'int', 'long', 'short', 'byte', 'float', 'double', 'char', // Go 'int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64', 'float32', 'float64', 'complex64', 'complex128', 'rune', 'error', ]); /** * Extract type references from type annotations on a function/method/field node. * Creates 'references' edges for parameter types, return types, and field types. */ private extractTypeAnnotations(node: SyntaxNode, nodeId: string): void { if (!this.extractor) return; if (!this.TYPE_ANNOTATION_LANGUAGES.has(this.language)) return; // Extract parameter type annotations const params = getChildByField(node, this.extractor.paramsField || 'parameters'); if (params) { this.extractTypeRefsFromSubtree(params, nodeId); } // Extract return type annotation const returnType = getChildByField(node, this.extractor.returnField || 'return_type'); if (returnType) { this.extractTypeRefsFromSubtree(returnType, nodeId); } // Extract direct type annotation (for class fields like `model: ITextModel`) const typeAnnotation = node.namedChildren.find( (c: SyntaxNode) => c.type === 'type_annotation' ); if (typeAnnotation) { this.extractTypeRefsFromSubtree(typeAnnotation, nodeId); } } /** * Extract type references from a variable's type annotation. */ private extractVariableTypeAnnotation(node: SyntaxNode, nodeId: string): void { if (!this.TYPE_ANNOTATION_LANGUAGES.has(this.language)) return; // Find type_annotation child (covers TS `: Type`, Rust `: Type`, etc.) const typeAnnotation = node.namedChildren.find( (c: SyntaxNode) => c.type === 'type_annotation' ); if (typeAnnotation) { this.extractTypeRefsFromSubtree(typeAnnotation, nodeId); } } /** * Recursively walk a subtree and extract all type_identifier references. * Handles unions, intersections, generics, arrays, etc. */ private extractTypeRefsFromSubtree(node: SyntaxNode, fromNodeId: string): void { if (node.type === 'type_identifier') { const typeName = getNodeText(node, this.source); if (typeName && !this.BUILTIN_TYPES.has(typeName)) { this.unresolvedReferences.push({ fromNodeId, referenceName: typeName, referenceKind: 'references', line: node.startPosition.row + 1, column: node.startPosition.column, }); } return; // type_identifier is a leaf } // Recurse into children (handles union_type, intersection_type, generic_type, etc.) for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (child) { this.extractTypeRefsFromSubtree(child, fromNodeId); } } } /** * Handle Pascal-specific AST structures. * Returns true if the node was fully handled and children should be skipped. */ private visitPascalNode(node: SyntaxNode): boolean { const nodeType = node.type; // Unit/Program/Library → module node if (nodeType === 'unit' || nodeType === 'program' || nodeType === 'library') { const moduleNameNode = node.namedChildren.find( (c: SyntaxNode) => c.type === 'moduleName' ); const name = moduleNameNode ? getNodeText(moduleNameNode, this.source) : ''; // Fallback to filename without extension if module name is empty const moduleName = name || path.basename(this.filePath).replace(/\.[^.]+$/, ''); this.createNode('module', moduleName, node); // Continue visiting children (interface/implementation sections) for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (child) this.visitNode(child); } return true; } // declType wraps declClass/declIntf/declEnum/type-alias // The name lives on declType, the inner node determines the kind if (nodeType === 'declType') { this.extractPascalDeclType(node); return true; } // declUses → import nodes for each unit name if (nodeType === 'declUses') { this.extractPascalUses(node); return true; } // declConsts → container; visit children for individual declConst if (nodeType === 'declConsts') { for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (child?.type === 'declConst') { this.extractPascalConst(child); } } return true; } // declConst at top level (outside declConsts) if (nodeType === 'declConst') { this.extractPascalConst(node); return true; } // declTypes → container for type declarations if (nodeType === 'declTypes') { for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (child) this.visitNode(child); } return true; } // declVars → container for variable declarations if (nodeType === 'declVars') { for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (child?.type === 'declVar') { const nameNode = getChildByField(child, 'name'); if (nameNode) { const name = getNodeText(nameNode, this.source); this.createNode('variable', name, child); } } } return true; } // defProc in implementation section → extract calls but don't create duplicate nodes if (nodeType === 'defProc') { this.extractPascalDefProc(node); return true; } // declProp → property node if (nodeType === 'declProp') { const nameNode = getChildByField(node, 'name'); if (nameNode) { const name = getNodeText(nameNode, this.source); const visibility = this.extractor!.getVisibility?.(node); this.createNode('property', name, node, { visibility }); } return true; } // declField → field node if (nodeType === 'declField') { const nameNode = getChildByField(node, 'name'); if (nameNode) { const name = getNodeText(nameNode, this.source); const visibility = this.extractor!.getVisibility?.(node); this.createNode('field', name, node, { visibility }); } return true; } // declSection → visit children (propagates visibility via getVisibility) if (nodeType === 'declSection') { for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (child) this.visitNode(child); } return true; } // exprCall → extract function call reference if (nodeType === 'exprCall') { this.extractPascalCall(node); return true; } // interface/implementation sections → visit children if (nodeType === 'interface' || nodeType === 'implementation') { for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (child) this.visitNode(child); } return true; } // block (begin..end) → visit for calls if (nodeType === 'block') { this.visitPascalBlock(node); return true; } return false; } /** * Extract a Pascal declType node (class, interface, enum, or type alias) */ private extractPascalDeclType(node: SyntaxNode): void { const nameNode = getChildByField(node, 'name'); if (!nameNode) return; const name = getNodeText(nameNode, this.source); // Find the inner type declaration const declClass = node.namedChildren.find( (c: SyntaxNode) => c.type === 'declClass' ); const declIntf = node.namedChildren.find( (c: SyntaxNode) => c.type === 'declIntf' ); const typeChild = node.namedChildren.find( (c: SyntaxNode) => c.type === 'type' ); if (declClass) { const classNode = this.createNode('class', name, node); if (classNode) { // Extract inheritance from typeref children of declClass this.extractPascalInheritance(declClass, classNode.id); // Visit class body this.nodeStack.push(classNode.id); for (let i = 0; i < declClass.namedChildCount; i++) { const child = declClass.namedChild(i); if (child) this.visitNode(child); } this.nodeStack.pop(); } } else if (declIntf) { const ifaceNode = this.createNode('interface', name, node); if (ifaceNode) { // Visit interface members this.nodeStack.push(ifaceNode.id); for (let i = 0; i < declIntf.namedChildCount; i++) { const child = declIntf.namedChild(i); if (child) this.visitNode(child); } this.nodeStack.pop(); } } else if (typeChild) { // Check if it contains a declEnum const declEnum = typeChild.namedChildren.find( (c: SyntaxNode) => c.type === 'declEnum' ); if (declEnum) { const enumNode = this.createNode('enum', name, node); if (enumNode) { // Extract enum members this.nodeStack.push(enumNode.id); for (let i = 0; i < declEnum.namedChildCount; i++) { const child = declEnum.namedChild(i); if (child?.type === 'declEnumValue') { const memberName = getChildByField(child, 'name'); if (memberName) { this.createNode('enum_member', getNodeText(memberName, this.source), child); } } } this.nodeStack.pop(); } } else { // Simple type alias: type TFoo = string / type TFoo = Integer this.createNode('type_alias', name, node); } } else { // Fallback: could be a forward declaration or simple alias this.createNode('type_alias', name, node); } } /** * Extract Pascal uses clause into individual import nodes */ private extractPascalUses(node: SyntaxNode): void { const importText = getNodeText(node, this.source).trim(); for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (child?.type === 'moduleName') { const unitName = getNodeText(child, this.source); this.createNode('import', unitName, child, { signature: importText, }); // Create unresolved reference for resolution if (this.nodeStack.length > 0) { const parentId = this.nodeStack[this.nodeStack.length - 1]; if (parentId) { this.unresolvedReferences.push({ fromNodeId: parentId, referenceName: unitName, referenceKind: 'imports', line: child.startPosition.row + 1, column: child.startPosition.column, }); } } } } } /** * Extract a Pascal constant declaration */ private extractPascalConst(node: SyntaxNode): void { const nameNode = getChildByField(node, 'name'); if (!nameNode) return; const name = getNodeText(nameNode, this.source); const defaultValue = node.namedChildren.find( (c: SyntaxNode) => c.type === 'defaultValue' ); const sig = defaultValue ? getNodeText(defaultValue, this.source) : undefined; this.createNode('constant', name, node, { signature: sig }); } /** * Extract Pascal inheritance (extends/implements) from declClass typeref children */ private extractPascalInheritance(declClass: SyntaxNode, classId: string): void { const typerefs = declClass.namedChildren.filter( (c: SyntaxNode) => c.type === 'typeref' ); for (let i = 0; i < typerefs.length; i++) { const ref = typerefs[i]!; const name = getNodeText(ref, this.source); this.unresolvedReferences.push({ fromNodeId: classId, referenceName: name, referenceKind: i === 0 ? 'extends' : 'implements', line: ref.startPosition.row + 1, column: ref.startPosition.column, }); } } /** * Extract calls and resolve method context from a Pascal defProc (implementation body). * Does not create a new node — the declaration was already captured from the interface section. */ private extractPascalDefProc(node: SyntaxNode): void { // Find the matching declaration node by name to use as call parent const declProc = node.namedChildren.find( (c: SyntaxNode) => c.type === 'declProc' ); if (!declProc) return; const nameNode = getChildByField(declProc, 'name'); if (!nameNode) return; const fullName = getNodeText(nameNode, this.source).trim(); // fullName is like "TAuthService.Create" const shortName = fullName.includes('.') ? fullName.split('.').pop()! : fullName; const fullNameKey = fullName.toLowerCase(); const shortNameKey = shortName.toLowerCase(); // Build method index on first use (O(n) once, then O(1) per lookup) if (!this.methodIndex) { this.methodIndex = new Map(); for (const n of this.nodes) { if (n.kind === 'method' || n.kind === 'function') { const nameKey = n.name.toLowerCase(); // Keep first seen short-name mapping to avoid silently overwriting earlier entries. if (!this.methodIndex.has(nameKey)) { this.methodIndex.set(nameKey, n.id); } // For Pascal methods, also index qualified forms (e.g. TAuthService.Create). if (n.kind === 'method') { const qualifiedParts = n.qualifiedName.split('::'); if (qualifiedParts.length >= 2) { // Create suffix keys so both "Module.Class.Method" and "Class.Method" can resolve. for (let i = 0; i < qualifiedParts.length - 1; i++) { const scopedName = qualifiedParts.slice(i).join('.').toLowerCase(); this.methodIndex.set(scopedName, n.id); } } } } } } const parentId = this.methodIndex.get(fullNameKey) || this.methodIndex.get(shortNameKey) || this.nodeStack[this.nodeStack.length - 1]; if (!parentId) return; // Visit the block for calls const block = node.namedChildren.find( (c: SyntaxNode) => c.type === 'block' ); if (block) { this.nodeStack.push(parentId); this.visitPascalBlock(block); this.nodeStack.pop(); } } /** * Extract function calls from a Pascal expression */ private extractPascalCall(node: SyntaxNode): void { if (this.nodeStack.length === 0) return; const callerId = this.nodeStack[this.nodeStack.length - 1]; if (!callerId) return; // Get the callee name — first child is typically the identifier or exprDot const firstChild = node.namedChild(0); if (!firstChild) return; let calleeName = ''; if (firstChild.type === 'exprDot') { // Qualified call: Obj.Method(...) const identifiers = firstChild.namedChildren.filter( (c: SyntaxNode) => c.type === 'identifier' ); if (identifiers.length > 0) { calleeName = identifiers.map((id: SyntaxNode) => getNodeText(id, this.source)).join('.'); } } else if (firstChild.type === 'identifier') { calleeName = getNodeText(firstChild, this.source); } if (calleeName) { this.unresolvedReferences.push({ fromNodeId: callerId, referenceName: calleeName, referenceKind: 'calls', line: node.startPosition.row + 1, column: node.startPosition.column, }); } // Also visit arguments for nested calls const args = node.namedChildren.find( (c: SyntaxNode) => c.type === 'exprArgs' ); if (args) { this.visitPascalBlock(args); } } /** * Recursively visit a Pascal block/statement tree for call expressions */ private visitPascalBlock(node: SyntaxNode): void { for (let i = 0; i < node.namedChildCount; i++) { const child = node.namedChild(i); if (!child) continue; if (child.type === 'exprCall') { this.extractPascalCall(child); } else if (child.type === 'exprDot') { // Check if exprDot contains an exprCall for (let j = 0; j < child.namedChildCount; j++) { const grandchild = child.namedChild(j); if (grandchild?.type === 'exprCall') { this.extractPascalCall(grandchild); } } } else { this.visitPascalBlock(child); } } } } /** * Extract nodes and edges from source code */ export function extractFromSource( filePath: string, source: string, language?: Language ): ExtractionResult { const detectedLanguage = language || detectLanguage(filePath); const fileExtension = path.extname(filePath).toLowerCase(); // Use custom extractor for Svelte if (detectedLanguage === 'svelte') { const extractor = new SvelteExtractor(filePath, source); return extractor.extract(); } // Use custom extractor for Liquid if (detectedLanguage === 'liquid') { const extractor = new LiquidExtractor(filePath, source); return extractor.extract(); } // Use custom extractor for DFM/FMX form files if ( detectedLanguage === 'pascal' && (fileExtension === '.dfm' || fileExtension === '.fmx') ) { const extractor = new DfmExtractor(filePath, source); return extractor.extract(); } const extractor = new TreeSitterExtractor(filePath, source, detectedLanguage); return extractor.extract(); }