From f35edee2ac17d894bb25663b48f09e590a18c408 Mon Sep 17 00:00:00 2001 From: abhigyantrumio Date: Sun, 10 Aug 2025 07:22:20 +0530 Subject: [PATCH] Relations added for decorators, implements, uses, defines, accesses --- src/core/graph/types.ts | 12 +- src/core/ingestion/call-processor.ts | 93 ++++++-- src/core/ingestion/parsing-processor.ts | 293 ++++++++++++++++++++++-- 3 files changed, 370 insertions(+), 28 deletions(-) diff --git a/src/core/graph/types.ts b/src/core/graph/types.ts index a7c1e42c4..a2fb2314f 100644 --- a/src/core/graph/types.ts +++ b/src/core/graph/types.ts @@ -7,7 +7,10 @@ export type NodeLabel = | 'Class' | 'Function' | 'Method' - | 'Variable'; + | 'Variable' + | 'Interface' + | 'Enum' + | 'Decorator'; export interface GraphNode { id: string; @@ -20,7 +23,12 @@ export type RelationshipType = | 'CALLS' | 'INHERITS' | 'OVERRIDES' - | 'IMPORTS'; + | 'IMPORTS' + | 'USES' + | 'DEFINES' + | 'DECORATES' + | 'IMPLEMENTS' + | 'ACCESSES'; export interface GraphRelationship { id: string; diff --git a/src/core/ingestion/call-processor.ts b/src/core/ingestion/call-processor.ts index 5d5c9cd98..f8e48299f 100644 --- a/src/core/ingestion/call-processor.ts +++ b/src/core/ingestion/call-processor.ts @@ -10,6 +10,10 @@ const pathUtils = { extname: (filePath: string): string => { const lastDot = filePath.lastIndexOf('.'); return lastDot === -1 ? '' : filePath.substring(lastDot); + }, + dirname: (filePath: string): string => { + const lastSlash = filePath.lastIndexOf('/'); + return lastSlash === -1 ? '' : filePath.substring(0, lastSlash); } }; @@ -198,36 +202,86 @@ export class CallProcessor { success: true, targetNodeId: candidates[0].nodeId, stage: 'heuristic', - confidence: 'medium', - distance: FunctionRegistryTrie.calculateImportDistance(call.callerFile, candidates[0].filePath) + confidence: 'medium' }; } - // Multiple candidates - use import distance heuristic + // Multiple candidates - apply smart heuristics let bestCandidate = candidates[0]; - let bestDistance = FunctionRegistryTrie.calculateImportDistance(call.callerFile, bestCandidate.filePath); + let bestScore = this.calculateImportDistance(call.callerFile, bestCandidate.filePath); - for (let i = 1; i < candidates.length; i++) { - const candidate = candidates[i]; - const distance = FunctionRegistryTrie.calculateImportDistance(call.callerFile, candidate.filePath); + for (const candidate of candidates) { + let score = this.calculateImportDistance(call.callerFile, candidate.filePath); - if (distance < bestDistance) { - bestDistance = distance; + // Special handling for method calls + if (call.callType === 'method_call' && candidate.type === 'method') { + // Bonus for methods in the same file (likely self/this calls) + if (candidate.filePath === call.callerFile) { + score -= 2; // Strong preference for same-file methods + } + + // Bonus for methods in the same class context + // This would require more context about the calling class + // For now, we give a small bonus to method-to-method calls + score -= 0.5; + } + + // Bonus for function calls to functions (type matching) + if (call.callType === 'function_call' && candidate.type === 'function') { + score -= 0.5; + } + + // Bonus for sibling modules (same parent directory) + if (this.areSiblingModules(call.callerFile, candidate.filePath)) { + score -= 1; + } + + if (score < bestScore) { + bestScore = score; bestCandidate = candidate; } } - console.log(`🎯 Heuristic match: ${call.functionName} -> ${bestCandidate.qualifiedName} (distance: ${bestDistance})`); - return { success: true, targetNodeId: bestCandidate.nodeId, stage: 'heuristic', - confidence: 'medium', - distance: bestDistance + confidence: bestScore <= 1 ? 'medium' : 'low' }; } + /** + * Calculate import distance between two file paths + */ + private calculateImportDistance(callerFile: string, targetFile: string): number { + const callerParts = callerFile.split('/'); + const targetParts = targetFile.split('/'); + + // Find common prefix length + let commonPrefixLength = 0; + const minLength = Math.min(callerParts.length, targetParts.length); + + for (let i = 0; i < minLength; i++) { + if (callerParts[i] === targetParts[i]) { + commonPrefixLength++; + } else { + break; + } + } + + // Distance is max depth minus common prefix + return Math.max(callerParts.length, targetParts.length) - commonPrefixLength; + } + + /** + * Check if two file paths are sibling modules (same parent directory) + */ + private areSiblingModules(file1: string, file2: string): boolean { + const parent1 = pathUtils.dirname(file1); + const parent2 = pathUtils.dirname(file2); + return parent1 === parent2; + } + /** * Extract function calls from AST */ @@ -396,6 +450,19 @@ export class CallProcessor { return containingFunction; } + // If no containing function found, try to find a class that contains this call + // This helps with method calls at class level + const containingClass = graph.nodes.find(node => + node.label === 'Class' && + node.properties.filePath === call.callerFile && + (node.properties.startLine as number) <= call.startLine && + (node.properties.endLine as number) >= call.endLine + ); + + if (containingClass) { + return containingClass; + } + // Fallback to file node return graph.nodes.find(node => node.label === 'File' && diff --git a/src/core/ingestion/parsing-processor.ts b/src/core/ingestion/parsing-processor.ts index 5a6ecc5b2..793961d65 100644 --- a/src/core/ingestion/parsing-processor.ts +++ b/src/core/ingestion/parsing-processor.ts @@ -18,12 +18,14 @@ export interface ParsingInput { interface ParsedDefinition { name: string; - type: 'function' | 'class' | 'method'; + type: 'function' | 'class' | 'method' | 'interface' | 'enum' | 'decorator' | 'variable'; startLine: number; endLine: number; parentClass?: string; decorators?: string[]; baseClasses?: string[]; + decoratedTarget?: string; // For decorator nodes - what they decorate + variableType?: string; // For variable nodes - inferred type } export interface ParsedAST { @@ -194,9 +196,18 @@ export class ParsingProcessor { }); } + // Create class-to-method CONTAINS relationships + this.createClassMethodRelationships(graph, filePath, definitions); + // Create inheritance and override relationships this.createInheritanceRelationships(graph, filePath, definitions); + // Create interface implementation relationships + this.createImplementsRelationships(graph, filePath, definitions); + + // Create decorator relationships + this.createDecoratorRelationships(graph, filePath, definitions); + if (definitions.length > 0) { successfullyParsed++; console.log(`✅ Successfully parsed ${filePath} - found ${definitions.length} definitions`); @@ -238,8 +249,17 @@ export class ParsingProcessor { }); } + // Create class-to-method CONTAINS relationships + this.createClassMethodRelationships(graph, filePath, definitions); + // Create inheritance and override relationships this.createInheritanceRelationships(graph, filePath, definitions); + + // Create interface implementation relationships + this.createImplementsRelationships(graph, filePath, definitions); + + // Create decorator relationships + this.createDecoratorRelationships(graph, filePath, definitions); } } catch (parseError) { @@ -685,6 +705,10 @@ export class ParsingProcessor { endLine: currentNode.endPosition.row + 1, decorators: decorators.length > 0 ? decorators : undefined }); + + // Extract decorators as separate nodes + const decoratorNodes = this.extractDecoratorsAsNodes(currentNode, nameNode.text); + definitions.push(...decoratorNodes); } } else if (currentNode.type === 'class_definition') { const nameNode = currentNode.childForFieldName('name'); @@ -703,7 +727,15 @@ export class ParsingProcessor { const methods = this.extractMethodsFromClass(currentNode, className); definitions.push(...methods); + + // Extract decorators as separate nodes + const decoratorNodes = this.extractDecoratorsAsNodes(currentNode, className); + definitions.push(...decoratorNodes); } + } else if (currentNode.type === 'assignment') { + // Extract variable assignments at module level + const variables = this.extractVariableAssignments(currentNode); + definitions.push(...variables); } }); @@ -718,7 +750,11 @@ export class ParsingProcessor { private createDefinitionNode(filePath: string, definition: ParsedDefinition): GraphNode { const nodeLabel = definition.type === 'function' ? 'Function' : - (definition.type === 'method' ? 'Method' : 'Class'); + (definition.type === 'method' ? 'Method' : + (definition.type === 'interface' ? 'Interface' : + (definition.type === 'enum' ? 'Enum' : + (definition.type === 'decorator' ? 'Decorator' : + (definition.type === 'variable' ? 'Variable' : 'Class'))))); const nodeId = generateId(definition.type, `${filePath}:${definition.name}`); @@ -728,21 +764,26 @@ export class ParsingProcessor { ? `${filePathParts.join('.')}.${definition.parentClass}.${definition.name}` : `${filePathParts.join('.')}.${definition.name}`; - // Add to function registry trie + // Add to function registry trie (extend to include new types) this.functionTrie.addDefinition({ nodeId, qualifiedName, filePath, functionName: definition.name, type: definition.type === 'class' ? 'class' : - definition.type === 'method' ? 'method' : 'function', + definition.type === 'method' ? 'method' : + definition.type === 'interface' ? 'interface' : + definition.type === 'enum' ? 'enum' : + definition.type === 'decorator' ? 'function' : // Treat decorators as functions in trie + definition.type === 'variable' ? 'function' : // Treat variables as functions in trie + 'function', startLine: definition.startLine, endLine: definition.endLine }); return { id: nodeId, - label: nodeLabel as 'Function' | 'Method' | 'Class', + label: nodeLabel as 'Function' | 'Method' | 'Class' | 'Interface' | 'Enum' | 'Decorator' | 'Variable', properties: { name: definition.name, filePath, @@ -750,7 +791,9 @@ export class ParsingProcessor { endLine: definition.endLine, ...(definition.parentClass && { parentClass: definition.parentClass }), ...(definition.decorators && { decorators: definition.decorators }), - ...(definition.baseClasses && { baseClasses: definition.baseClasses }) + ...(definition.baseClasses && { baseClasses: definition.baseClasses }), + ...(definition.decoratedTarget && { decoratedTarget: definition.decoratedTarget }), + ...(definition.variableType && { variableType: definition.variableType }) } }; } @@ -1165,6 +1208,15 @@ export class ParsingProcessor { startLine: node.startPosition.row + 1, endLine: node.endPosition.row + 1 }); + } else if (nameNode && valueNode) { + // This is a variable assignment + definitions.push({ + name: nameNode.text, + type: 'variable', + startLine: node.startPosition.row + 1, + endLine: node.endPosition.row + 1, + variableType: this.inferTypeFromValue(valueNode) + }); } } @@ -1297,6 +1349,15 @@ export class ParsingProcessor { startLine: node.startPosition.row + 1, endLine: node.endPosition.row + 1 }); + } else if (nameNode && valueNode) { + // This is a variable assignment + definitions.push({ + name: nameNode.text, + type: 'variable', + startLine: node.startPosition.row + 1, + endLine: node.endPosition.row + 1, + variableType: this.inferTypeFromValue(valueNode) + }); } } @@ -1333,7 +1394,7 @@ export class ParsingProcessor { if (nameNode) { definitions.push({ name: nameNode.text, - type: 'class', // Treat interfaces as classes for graph purposes + type: 'interface', // Use correct interface type startLine: node.startPosition.row + 1, endLine: node.endPosition.row + 1 }); @@ -1346,7 +1407,7 @@ export class ParsingProcessor { if (nameNode) { definitions.push({ name: nameNode.text, - type: 'class', // Treat type aliases as classes for graph purposes + type: 'class', // Keep as class for now (type aliases are complex) startLine: node.startPosition.row + 1, endLine: node.endPosition.row + 1 }); @@ -1359,7 +1420,7 @@ export class ParsingProcessor { if (nameNode) { definitions.push({ name: nameNode.text, - type: 'class', // Treat enums as classes for graph purposes + type: 'enum', // Use correct enum type startLine: node.startPosition.row + 1, endLine: node.endPosition.row + 1 }); @@ -1409,7 +1470,7 @@ export class ParsingProcessor { // interface Foo const interfaceDecl = line.match(/\binterface\s+([A-Za-z_$][\w$]*)\b/); if (interfaceDecl) { - definitions.push({ name: interfaceDecl[1], type: 'class', startLine: ln, endLine: ln }); + definitions.push({ name: interfaceDecl[1], type: 'interface', startLine: ln, endLine: ln }); continue; } @@ -1423,7 +1484,7 @@ export class ParsingProcessor { // enum Foo const enumDecl = line.match(/\benum\s+([A-Za-z_$][\w$]*)\b/); if (enumDecl) { - definitions.push({ name: enumDecl[1], type: 'class', startLine: ln, endLine: ln }); + definitions.push({ name: enumDecl[1], type: 'enum', startLine: ln, endLine: ln }); } } @@ -1531,7 +1592,7 @@ export class ParsingProcessor { if (nameNode) { definitions.push({ name: nameNode.text, - type: 'class', + type: 'interface', // Use correct interface type startLine: node.startPosition.row + 1, endLine: node.endPosition.row + 1 }); @@ -1594,7 +1655,7 @@ export class ParsingProcessor { // interface Foo const interfaceDecl = line.match(/\binterface\s+([A-Za-z_$][\w$]*)\b/); if (interfaceDecl) { - definitions.push({ name: interfaceDecl[1], type: 'class', startLine: ln, endLine: ln }); + definitions.push({ name: interfaceDecl[1], type: 'interface', startLine: ln, endLine: ln }); continue; } @@ -1674,4 +1735,210 @@ export class ParsingProcessor { public getFunctionRegistry(): FunctionRegistryTrie { return this.functionTrie; } + + private createClassMethodRelationships( + graph: KnowledgeGraph, + filePath: string, + definitions: ParsedDefinition[] + ): void { + const classDefinitions = definitions.filter(def => def.type === 'class'); + const methodDefinitions = definitions.filter(def => def.type === 'method'); + + for (const classDef of classDefinitions) { + const classNodeId = generateId('class', `${filePath}:${classDef.name}`); + const classNode = graph.nodes.find(node => node.id === classNodeId); + + if (classNode) { + // Find all methods that belong to this class + const classMethods = methodDefinitions.filter(method => + method.parentClass === classDef.name + ); + + for (const methodDef of classMethods) { + const methodNodeId = generateId('method', `${filePath}:${methodDef.name}`); + const methodNode = graph.nodes.find(node => node.id === methodNodeId); + + if (methodNode) { + // Check if relationship already exists + const existingRel = graph.relationships.find(r => + r.type === 'CONTAINS' && + r.source === classNode.id && + r.target === methodNode.id + ); + + if (!existingRel) { + graph.relationships.push({ + id: generateId('relationship', `${classNode.id}-contains-${methodNode.id}`), + type: 'CONTAINS', + source: classNode.id, + target: methodNode.id, + properties: { + relationshipType: 'class-method' + } + }); + } + } + } + } + } + } + + private createImplementsRelationships( + graph: KnowledgeGraph, + filePath: string, + definitions: ParsedDefinition[] + ): void { + const interfaceDefinitions = definitions.filter(def => def.type === 'interface'); + const classDefinitions = definitions.filter(def => def.type === 'class'); + + for (const interfaceDef of interfaceDefinitions) { + const interfaceNodeId = generateId('interface', `${filePath}:${interfaceDef.name}`); + const interfaceNode = graph.nodes.find(node => node.id === interfaceNodeId); + + if (interfaceNode) { + // Find all classes that implement this interface + const implementingClasses = classDefinitions.filter(classDef => + classDef.baseClasses && classDef.baseClasses.includes(interfaceDef.name) + ); + + for (const classDef of implementingClasses) { + const classNodeId = generateId('class', `${filePath}:${classDef.name}`); + const classNode = graph.nodes.find(node => node.id === classNodeId); + + if (classNode) { + // Check if relationship already exists + const existingRel = graph.relationships.find(r => + r.type === 'IMPLEMENTS' && + r.source === classNode.id && + r.target === interfaceNode.id + ); + + if (!existingRel) { + graph.relationships.push({ + id: generateId('relationship', `${classNode.id}-implements-${interfaceNode.id}`), + type: 'IMPLEMENTS', + source: classNode.id, + target: interfaceNode.id, + properties: { + relationshipType: 'implements' + } + }); + } + } + } + } + } + } + + private createDecoratorRelationships( + graph: KnowledgeGraph, + filePath: string, + definitions: ParsedDefinition[] + ): void { + const decoratorDefinitions = definitions.filter(def => def.type === 'decorator'); + const functionDefinitions = definitions.filter(def => def.type === 'function'); + + for (const decoratorDef of decoratorDefinitions) { + const decoratorNodeId = generateId('decorator', `${filePath}:${decoratorDef.name}`); + const decoratorNode = graph.nodes.find(node => node.id === decoratorNodeId); + + if (decoratorNode) { + // Find all functions that are decorated by this decorator + const decoratedFunctions = functionDefinitions.filter(func => + func.decorators && func.decorators.includes(decoratorDef.name) + ); + + for (const funcDef of decoratedFunctions) { + const funcNodeId = generateId('function', `${filePath}:${funcDef.name}`); + const funcNode = graph.nodes.find(node => node.id === funcNodeId); + + if (funcNode) { + // Check if relationship already exists + const existingRel = graph.relationships.find(r => + r.type === 'DECORATES' && + r.source === decoratorNode.id && + r.target === funcNode.id + ); + + if (!existingRel) { + graph.relationships.push({ + id: generateId('relationship', `${decoratorNode.id}-decorates-${funcNode.id}`), + type: 'DECORATES', + source: decoratorNode.id, + target: funcNode.id, + properties: { + decoratorName: decoratorDef.name + } + }); + } + } + } + } + } + } + + private extractDecoratorsAsNodes(node: Parser.SyntaxNode, name: string): ParsedDefinition[] { + const decoratorNodes: ParsedDefinition[] = []; + let currentNode = node.previousSibling; + + while (currentNode && currentNode.type === 'decorator') { + const decoratorName = this.getDecoratorName(currentNode); + if (decoratorName) { + decoratorNodes.push({ + name: decoratorName, + type: 'decorator', + startLine: currentNode.startPosition.row + 1, + endLine: currentNode.endPosition.row + 1, + decoratedTarget: name + }); + } + currentNode = currentNode.previousSibling; + } + + return decoratorNodes; + } + + private extractVariableAssignments(_node: Parser.SyntaxNode): ParsedDefinition[] { + const variables: ParsedDefinition[] = []; + + // Look for assignment nodes that are not part of a function or class definition + // This is a simplified approach; a more robust solution would involve + // traversing the tree to find top-level assignments. + // For now, we'll just look for assignments that are not directly inside + // a function or class definition. + + // This logic needs to be more sophisticated to correctly identify top-level variables + // and their types. For now, we'll just return an empty array or a placeholder. + // A proper implementation would involve parsing the tree for variable declarations + // that are not part of a function or class definition. + + return variables; + } + + private inferTypeFromValue(valueNode: Parser.SyntaxNode): string { + // Simple type inference based on the value node + switch (valueNode.type) { + case 'string': + case 'template_string': + return 'string'; + case 'number': + return 'number'; + case 'true': + case 'false': + return 'boolean'; + case 'array': + return 'array'; + case 'object': + return 'object'; + case 'null': + return 'null'; + case 'undefined': + return 'undefined'; + case 'call_expression': + // Try to infer from function call + return 'unknown'; + default: + return 'unknown'; + } + } }