mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-07 02:58:02 +00:00
Advance edge case handling for python codebases ( eg: decorators, similar function name, etc
This commit is contained in:
parent
34e7ba20f9
commit
57ac6face5
4 changed files with 15432 additions and 16 deletions
14244
repomix-output.txt
Normal file
14244
repomix-output.txt
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -13,11 +13,14 @@ interface FunctionCall {
|
|||
callerFilePath: string;
|
||||
callerFunction: string;
|
||||
calledName: string;
|
||||
callType: 'function' | 'method' | 'attribute';
|
||||
callType: 'function' | 'method' | 'attribute' | 'super';
|
||||
line: number;
|
||||
column: number;
|
||||
isChained?: boolean;
|
||||
objectName?: string;
|
||||
superMethodName?: string;
|
||||
assignedToVariable?: string;
|
||||
chainedFromVariable?: string;
|
||||
}
|
||||
|
||||
interface ImportInfo {
|
||||
|
|
@ -28,6 +31,24 @@ interface ImportInfo {
|
|||
importType: 'function' | 'class' | 'module' | 'attribute';
|
||||
}
|
||||
|
||||
interface VariableTypeInfo {
|
||||
variableName: string;
|
||||
inferredType: string;
|
||||
filePath: string;
|
||||
functionContext: string;
|
||||
line: number;
|
||||
confidence: 'high' | 'medium' | 'low';
|
||||
source: 'constructor' | 'method_return' | 'factory' | 'assignment' | 'parameter';
|
||||
}
|
||||
|
||||
interface MethodReturnTypeInfo {
|
||||
methodId: string;
|
||||
methodName: string;
|
||||
className: string;
|
||||
returnType?: string;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
const BUILTIN_FUNCTIONS = new Set([
|
||||
'print', 'len', 'str', 'int', 'float', 'bool', 'list', 'dict', 'tuple', 'set',
|
||||
'range', 'enumerate', 'zip', 'map', 'filter', 'sum', 'max', 'min', 'abs',
|
||||
|
|
@ -39,6 +60,9 @@ const BUILTIN_FUNCTIONS = new Set([
|
|||
export class CallProcessor {
|
||||
private importCache: Map<string, ImportInfo[]> = new Map();
|
||||
private functionNodes: Map<string, GraphNode> = new Map();
|
||||
private variableTypes: Map<string, VariableTypeInfo> = new Map();
|
||||
private methodReturnTypes: Map<string, MethodReturnTypeInfo> = new Map();
|
||||
private classConstructors: Map<string, GraphNode> = new Map();
|
||||
|
||||
public async process(input: CallResolutionInput): Promise<void> {
|
||||
const { graph, astCache, fileContents } = input;
|
||||
|
|
@ -185,14 +209,111 @@ export class CallProcessor {
|
|||
if (!this.functionNodes.has(generalMethodKey)) {
|
||||
this.functionNodes.set(generalMethodKey, node);
|
||||
}
|
||||
|
||||
// Track method return type information
|
||||
this.buildMethodReturnTypeInfo(node, parentClass, functionName, filePath);
|
||||
} else {
|
||||
const functionKey = `${filePath}:function:${functionName}`;
|
||||
this.functionNodes.set(functionKey, node);
|
||||
}
|
||||
} else if (node.label === 'Class') {
|
||||
// Track class constructors for instantiation inference
|
||||
const className = node.properties.name as string;
|
||||
const filePath = node.properties.filePath as string;
|
||||
const classKey = `${filePath}:${className}`;
|
||||
this.classConstructors.set(classKey, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private buildMethodReturnTypeInfo(methodNode: GraphNode, className: string, methodName: string, filePath: string): void {
|
||||
const methodId = methodNode.id;
|
||||
|
||||
// Infer return type based on method name and class context
|
||||
let returnType: string | undefined;
|
||||
|
||||
// Constructor methods return the class instance
|
||||
if (methodName === '__init__' || methodName === '__new__') {
|
||||
returnType = className;
|
||||
}
|
||||
// Factory methods often return class instances
|
||||
else if (methodName.startsWith('create_') || methodName.startsWith('build_') ||
|
||||
methodName.startsWith('make_') || methodName.includes('factory')) {
|
||||
returnType = this.inferFactoryReturnType(methodName, className);
|
||||
}
|
||||
// Getter methods often return specific types
|
||||
else if (methodName.startsWith('get_')) {
|
||||
returnType = this.inferGetterReturnType(methodName, className);
|
||||
}
|
||||
// Property methods (decorated with @property) return the property type
|
||||
else if (methodNode.properties.decorators) {
|
||||
const decorators = methodNode.properties.decorators as string[];
|
||||
if (decorators.includes('property')) {
|
||||
returnType = this.inferPropertyReturnType(methodName, className);
|
||||
}
|
||||
}
|
||||
|
||||
const returnTypeInfo: MethodReturnTypeInfo = {
|
||||
methodId,
|
||||
methodName,
|
||||
className,
|
||||
returnType,
|
||||
filePath
|
||||
};
|
||||
|
||||
this.methodReturnTypes.set(methodId, returnTypeInfo);
|
||||
}
|
||||
|
||||
private inferFactoryReturnType(methodName: string, className: string): string | undefined {
|
||||
// Factory methods like create_user, build_report, make_connection
|
||||
if (methodName.startsWith('create_')) {
|
||||
const typeName = methodName.substring(7); // Remove 'create_'
|
||||
return this.capitalizeFirstLetter(typeName);
|
||||
}
|
||||
if (methodName.startsWith('build_')) {
|
||||
const typeName = methodName.substring(6); // Remove 'build_'
|
||||
return this.capitalizeFirstLetter(typeName);
|
||||
}
|
||||
if (methodName.startsWith('make_')) {
|
||||
const typeName = methodName.substring(5); // Remove 'make_'
|
||||
return this.capitalizeFirstLetter(typeName);
|
||||
}
|
||||
|
||||
// If it's a factory class, it might return instances of the main entity
|
||||
if (className.endsWith('Factory')) {
|
||||
const entityName = className.substring(0, className.length - 7); // Remove 'Factory'
|
||||
return entityName;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private inferGetterReturnType(methodName: string, className: string): string | undefined {
|
||||
// Common getter patterns
|
||||
if (methodName === 'get_name' || methodName === 'get_title') return 'str';
|
||||
if (methodName === 'get_id' || methodName === 'get_count') return 'int';
|
||||
if (methodName === 'get_price' || methodName === 'get_amount') return 'float';
|
||||
if (methodName === 'get_active' || methodName === 'get_enabled') return 'bool';
|
||||
if (methodName.includes('_list') || methodName.includes('_all')) return 'list';
|
||||
if (methodName.includes('_dict') || methodName.includes('_data')) return 'dict';
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private inferPropertyReturnType(methodName: string, className: string): string | undefined {
|
||||
// Property return type inference based on naming patterns
|
||||
if (methodName === 'name' || methodName === 'title' || methodName === 'description') return 'str';
|
||||
if (methodName === 'id' || methodName === 'count' || methodName === 'size') return 'int';
|
||||
if (methodName === 'price' || methodName === 'amount' || methodName === 'rate') return 'float';
|
||||
if (methodName === 'active' || methodName === 'enabled' || methodName === 'valid') return 'bool';
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private capitalizeFirstLetter(str: string): string {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
private async extractImports(filePath: string, ast: any, content: string): Promise<void> {
|
||||
const imports: ImportInfo[] = [];
|
||||
|
||||
|
|
@ -278,10 +399,21 @@ export class CallProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
// Track variable assignments for type inference
|
||||
if (node.type === 'assignment') {
|
||||
this.processVariableAssignment(node, filePath, currentFunction);
|
||||
}
|
||||
|
||||
// Find function calls
|
||||
if (node.type === 'call') {
|
||||
const callInfo = this.extractCallInfo(node, filePath, currentFunction, content);
|
||||
if (callInfo) {
|
||||
// Check if this call is part of an assignment
|
||||
const assignmentInfo = this.extractAssignmentInfo(node);
|
||||
if (assignmentInfo) {
|
||||
callInfo.assignedToVariable = assignmentInfo.variableName;
|
||||
}
|
||||
|
||||
functionCalls.push(callInfo);
|
||||
}
|
||||
}
|
||||
|
|
@ -290,6 +422,143 @@ export class CallProcessor {
|
|||
// Resolve calls and create relationships
|
||||
for (const call of functionCalls) {
|
||||
await this.resolveAndCreateCallRelationship(graph, call);
|
||||
|
||||
// Track variable type if this call is assigned to a variable
|
||||
if (call.assignedToVariable) {
|
||||
this.inferAndTrackVariableType(graph, call);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private processVariableAssignment(assignmentNode: any, filePath: string, currentFunction: string | null): void {
|
||||
const leftNode = assignmentNode.childForFieldName('left');
|
||||
const rightNode = assignmentNode.childForFieldName('right');
|
||||
|
||||
if (!leftNode || !rightNode) return;
|
||||
|
||||
// Extract variable name from left side
|
||||
let variableName: string | null = null;
|
||||
if (leftNode.type === 'identifier') {
|
||||
variableName = leftNode.text;
|
||||
}
|
||||
|
||||
if (!variableName) return;
|
||||
|
||||
// Analyze right side for type inference
|
||||
if (rightNode.type === 'call') {
|
||||
// This will be handled in the call processing
|
||||
return;
|
||||
} else if (rightNode.type === 'identifier') {
|
||||
// Variable assignment from another variable
|
||||
const sourceVariable = rightNode.text;
|
||||
this.copyVariableType(filePath, currentFunction || '<module>', sourceVariable, variableName);
|
||||
}
|
||||
}
|
||||
|
||||
private extractAssignmentInfo(callNode: any): { variableName: string } | null {
|
||||
// Walk up the AST to find if this call is part of an assignment
|
||||
let parent = callNode.parent;
|
||||
|
||||
while (parent) {
|
||||
if (parent.type === 'assignment') {
|
||||
const leftNode = parent.childForFieldName('left');
|
||||
if (leftNode && leftNode.type === 'identifier') {
|
||||
return { variableName: leftNode.text };
|
||||
}
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private inferAndTrackVariableType(graph: KnowledgeGraph, call: FunctionCall): void {
|
||||
if (!call.assignedToVariable) return;
|
||||
|
||||
let inferredType: string | undefined;
|
||||
let confidence: 'high' | 'medium' | 'low' = 'low';
|
||||
let source: VariableTypeInfo['source'] = 'assignment';
|
||||
|
||||
// Try to infer type based on the call
|
||||
if (call.callType === 'function' || call.callType === 'method') {
|
||||
// Check if it's a class constructor call
|
||||
const constructorType = this.inferConstructorType(graph, call);
|
||||
if (constructorType) {
|
||||
inferredType = constructorType;
|
||||
confidence = 'high';
|
||||
source = 'constructor';
|
||||
} else {
|
||||
// Check if it's a method call with known return type
|
||||
const methodReturnType = this.inferMethodReturnType(graph, call);
|
||||
if (methodReturnType) {
|
||||
inferredType = methodReturnType.returnType;
|
||||
confidence = methodReturnType.confidence;
|
||||
source = 'method_return';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (inferredType) {
|
||||
const variableKey = `${call.callerFilePath}:${call.callerFunction}:${call.assignedToVariable}`;
|
||||
const typeInfo: VariableTypeInfo = {
|
||||
variableName: call.assignedToVariable,
|
||||
inferredType,
|
||||
filePath: call.callerFilePath,
|
||||
functionContext: call.callerFunction,
|
||||
line: call.line,
|
||||
confidence,
|
||||
source
|
||||
};
|
||||
|
||||
this.variableTypes.set(variableKey, typeInfo);
|
||||
this.logTypeInference(call, inferredType, confidence);
|
||||
}
|
||||
}
|
||||
|
||||
private inferConstructorType(graph: KnowledgeGraph, call: FunctionCall): string | undefined {
|
||||
// Check if the called function is a class constructor
|
||||
const classKey = `${call.callerFilePath}:${call.calledName}`;
|
||||
if (this.classConstructors.has(classKey)) {
|
||||
return call.calledName;
|
||||
}
|
||||
|
||||
// Check for imported class constructors
|
||||
const imports = this.importCache.get(call.callerFilePath) || [];
|
||||
for (const importInfo of imports) {
|
||||
if (importInfo.importedName === call.calledName && importInfo.importType === 'class') {
|
||||
return call.calledName;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private inferMethodReturnType(graph: KnowledgeGraph, call: FunctionCall): { returnType?: string, confidence: 'high' | 'medium' | 'low' } | null {
|
||||
// Find the method node that was called
|
||||
const targetNode = this.resolveMethodCall(graph, call) || this.resolveLocalFunction(call);
|
||||
|
||||
if (targetNode && this.methodReturnTypes.has(targetNode.id)) {
|
||||
const returnTypeInfo = this.methodReturnTypes.get(targetNode.id)!;
|
||||
return {
|
||||
returnType: returnTypeInfo.returnType,
|
||||
confidence: returnTypeInfo.returnType ? 'medium' : 'low'
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private copyVariableType(filePath: string, functionContext: string, sourceVar: string, targetVar: string): void {
|
||||
const sourceKey = `${filePath}:${functionContext}:${sourceVar}`;
|
||||
const sourceType = this.variableTypes.get(sourceKey);
|
||||
|
||||
if (sourceType) {
|
||||
const targetKey = `${filePath}:${functionContext}:${targetVar}`;
|
||||
const copiedType: VariableTypeInfo = {
|
||||
...sourceType,
|
||||
variableName: targetVar
|
||||
};
|
||||
this.variableTypes.set(targetKey, copiedType);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -317,11 +586,41 @@ export class CallProcessor {
|
|||
column
|
||||
};
|
||||
} else if (functionNode.type === 'attribute') {
|
||||
// Method call: obj.method()
|
||||
// Method call: obj.method() or super().method() or chained call
|
||||
const objectNode = functionNode.childForFieldName('object');
|
||||
const attributeNode = functionNode.childForFieldName('attribute');
|
||||
|
||||
if (objectNode && attributeNode) {
|
||||
// Check if this is a super() call
|
||||
if (objectNode.type === 'call') {
|
||||
const superFunctionNode = objectNode.childForFieldName('function');
|
||||
if (superFunctionNode && superFunctionNode.type === 'identifier' && superFunctionNode.text === 'super') {
|
||||
// This is a super().method() call
|
||||
return {
|
||||
callerFilePath: filePath,
|
||||
callerFunction: currentFunction || '<module>',
|
||||
calledName: 'super',
|
||||
callType: 'super',
|
||||
line,
|
||||
column,
|
||||
superMethodName: attributeNode.text
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the object is a variable with known type (for chained calls)
|
||||
let objectName = objectNode.text;
|
||||
let chainedFromVariable: string | undefined;
|
||||
|
||||
if (objectNode.type === 'identifier') {
|
||||
// This might be a method call on a typed variable
|
||||
const variableType = this.getVariableType(filePath, currentFunction || '<module>', objectName);
|
||||
if (variableType) {
|
||||
chainedFromVariable = objectName;
|
||||
}
|
||||
}
|
||||
|
||||
// Regular method call
|
||||
return {
|
||||
callerFilePath: filePath,
|
||||
callerFunction: currentFunction || '<module>',
|
||||
|
|
@ -329,7 +628,8 @@ export class CallProcessor {
|
|||
callType: 'method',
|
||||
line,
|
||||
column,
|
||||
objectName: objectNode.text
|
||||
objectName: objectName,
|
||||
chainedFromVariable
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -348,8 +648,10 @@ export class CallProcessor {
|
|||
const targetNode =
|
||||
this.resolveBuiltinFunction(call) ||
|
||||
this.resolveImportedFunction(call) || // Check imports first
|
||||
this.resolveSuperCall(graph, call) || // Check super() calls
|
||||
this.resolveMethodCall(graph, call) ||
|
||||
this.resolveLocalFunction(call); // Check local functions last
|
||||
this.resolveLocalFunction(call) || // Check local functions
|
||||
this.resolveWithAdvancedFallback(graph, call); // Advanced fallback for ambiguous calls
|
||||
|
||||
if (targetNode) {
|
||||
// Prevent self-referential calls unless it's actually recursive
|
||||
|
|
@ -420,6 +722,363 @@ export class CallProcessor {
|
|||
return null;
|
||||
}
|
||||
|
||||
private resolveWithAdvancedFallback(graph: KnowledgeGraph, call: FunctionCall): GraphNode | null {
|
||||
// Find all potential candidates across the entire graph
|
||||
const candidates = this.findAllCandidates(graph, call);
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (candidates.length === 1) {
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
// Multiple candidates found - use heuristics to pick the best one
|
||||
const rankedCandidates = this.rankCandidatesByProximity(call.callerFilePath, candidates);
|
||||
|
||||
if (rankedCandidates.length > 0) {
|
||||
const bestCandidate = rankedCandidates[0];
|
||||
|
||||
// Enable detailed logging for debugging (can be controlled via environment variable)
|
||||
const enableDetailedLogging = process.env.GITNEXUS_DEBUG_FALLBACK === 'true';
|
||||
if (enableDetailedLogging) {
|
||||
this.logCandidateAnalysis(call, rankedCandidates);
|
||||
} else {
|
||||
console.log(`Advanced fallback: Selected ${bestCandidate.node.properties.filePath}:${bestCandidate.node.properties.name} for call to ${call.calledName} from ${call.callerFilePath} (score: ${bestCandidate.score.toFixed(2)})`);
|
||||
}
|
||||
|
||||
return bestCandidate.node;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private applySpecialCaseHeuristics(call: FunctionCall, candidates: GraphNode[]): GraphNode[] {
|
||||
// Apply special case filtering and prioritization
|
||||
const filtered = candidates.filter(candidate => {
|
||||
const candidatePath = candidate.properties.filePath as string;
|
||||
const candidateName = candidate.properties.name as string;
|
||||
|
||||
// Skip obvious non-matches
|
||||
if (this.isObviousNonMatch(call, candidate)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
// Apply framework-specific heuristics
|
||||
return this.applyFrameworkHeuristics(call, filtered);
|
||||
}
|
||||
|
||||
private isObviousNonMatch(call: FunctionCall, candidate: GraphNode): boolean {
|
||||
const candidatePath = candidate.properties.filePath as string;
|
||||
const candidateName = candidate.properties.name as string;
|
||||
const callerPath = call.callerFilePath;
|
||||
|
||||
// Skip if candidate is in a completely different domain
|
||||
const callerDomain = this.extractDomain(callerPath);
|
||||
const candidateDomain = this.extractDomain(candidatePath);
|
||||
|
||||
if (callerDomain && candidateDomain && callerDomain !== candidateDomain) {
|
||||
const commonDomains = ['utils', 'helpers', 'common', 'shared', 'lib', 'core'];
|
||||
if (!commonDomains.includes(candidateDomain.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip private/internal functions when caller is not in same module
|
||||
if (candidateName.startsWith('_') && !this.areInSameModule(callerPath, candidatePath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private applyFrameworkHeuristics(call: FunctionCall, candidates: GraphNode[]): GraphNode[] {
|
||||
// Django-specific heuristics
|
||||
if (this.isDjangoProject(call.callerFilePath)) {
|
||||
return this.applyDjangoHeuristics(call, candidates);
|
||||
}
|
||||
|
||||
// Flask-specific heuristics
|
||||
if (this.isFlaskProject(call.callerFilePath)) {
|
||||
return this.applyFlaskHeuristics(call, candidates);
|
||||
}
|
||||
|
||||
// FastAPI-specific heuristics
|
||||
if (this.isFastAPIProject(call.callerFilePath)) {
|
||||
return this.applyFastAPIHeuristics(call, candidates);
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private extractDomain(filePath: string): string | null {
|
||||
const parts = filePath.split('/').filter(p => p.length > 0);
|
||||
if (parts.length >= 2) {
|
||||
return parts[parts.length - 2]; // Directory containing the file
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private areInSameModule(path1: string, path2: string): boolean {
|
||||
const parts1 = path1.split('/').slice(0, -1); // Remove filename
|
||||
const parts2 = path2.split('/').slice(0, -1); // Remove filename
|
||||
|
||||
// Consider same module if they share at least 2 path segments
|
||||
let commonSegments = 0;
|
||||
const minLength = Math.min(parts1.length, parts2.length);
|
||||
|
||||
for (let i = 0; i < minLength; i++) {
|
||||
if (parts1[i] === parts2[i]) {
|
||||
commonSegments++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return commonSegments >= 2;
|
||||
}
|
||||
|
||||
private isDjangoProject(filePath: string): boolean {
|
||||
return filePath.includes('django') ||
|
||||
filePath.includes('models.py') ||
|
||||
filePath.includes('views.py') ||
|
||||
filePath.includes('urls.py');
|
||||
}
|
||||
|
||||
private isFlaskProject(filePath: string): boolean {
|
||||
return filePath.includes('flask') ||
|
||||
filePath.includes('app.py') ||
|
||||
filePath.includes('routes.py');
|
||||
}
|
||||
|
||||
private isFastAPIProject(filePath: string): boolean {
|
||||
return filePath.includes('fastapi') ||
|
||||
filePath.includes('main.py') ||
|
||||
filePath.includes('routers/');
|
||||
}
|
||||
|
||||
private applyDjangoHeuristics(call: FunctionCall, candidates: GraphNode[]): GraphNode[] {
|
||||
// Prefer models.py for model-related functions, views.py for view functions, etc.
|
||||
return candidates.sort((a, b) => {
|
||||
const pathA = a.properties.filePath as string;
|
||||
const pathB = b.properties.filePath as string;
|
||||
|
||||
if (call.callerFilePath.includes('views.py') && pathA.includes('models.py')) {
|
||||
return -1; // Prefer models.py when called from views.py
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
private applyFlaskHeuristics(call: FunctionCall, candidates: GraphNode[]): GraphNode[] {
|
||||
// Flask-specific prioritization logic
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private applyFastAPIHeuristics(call: FunctionCall, candidates: GraphNode[]): GraphNode[] {
|
||||
// FastAPI-specific prioritization logic
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private findAllCandidates(graph: KnowledgeGraph, call: FunctionCall): GraphNode[] {
|
||||
const candidates: GraphNode[] = [];
|
||||
|
||||
// Look for functions and methods with matching names across all files
|
||||
for (const node of graph.nodes) {
|
||||
if ((node.label === 'Function' || node.label === 'Method') &&
|
||||
node.properties.name === call.calledName &&
|
||||
node.properties.filePath !== call.callerFilePath) { // Exclude same file (already checked)
|
||||
candidates.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply special case heuristics to filter and prioritize candidates
|
||||
return this.applySpecialCaseHeuristics(call, candidates);
|
||||
}
|
||||
|
||||
private rankCandidatesByProximity(callerFilePath: string, candidates: GraphNode[]): Array<{node: GraphNode, score: number}> {
|
||||
const scored = candidates.map(candidate => ({
|
||||
node: candidate,
|
||||
score: this.calculateProximityScore(callerFilePath, candidate.properties.filePath as string)
|
||||
}));
|
||||
|
||||
// Sort by score (higher is better)
|
||||
return scored.sort((a, b) => b.score - a.score);
|
||||
}
|
||||
|
||||
private calculateProximityScore(callerPath: string, candidatePath: string): number {
|
||||
// Normalize paths (convert backslashes to forward slashes)
|
||||
const normalizedCaller = this.normalizePath(callerPath);
|
||||
const normalizedCandidate = this.normalizePath(candidatePath);
|
||||
|
||||
const callerParts = normalizedCaller.split('/').filter(part => part.length > 0);
|
||||
const candidateParts = normalizedCandidate.split('/').filter(part => part.length > 0);
|
||||
|
||||
let score = 0;
|
||||
|
||||
// Base score: prefer shorter paths (closer to root)
|
||||
score += Math.max(0, 10 - candidateParts.length);
|
||||
|
||||
// Proximity score: count common path segments from the beginning
|
||||
let commonPrefixLength = 0;
|
||||
const minLength = Math.min(callerParts.length, candidateParts.length);
|
||||
|
||||
for (let i = 0; i < minLength - 1; i++) { // Exclude filename
|
||||
if (callerParts[i] === candidateParts[i]) {
|
||||
commonPrefixLength++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Higher score for more common path segments
|
||||
score += commonPrefixLength * 20;
|
||||
|
||||
// Same directory bonus
|
||||
if (commonPrefixLength === Math.min(callerParts.length - 1, candidateParts.length - 1)) {
|
||||
score += 30;
|
||||
}
|
||||
|
||||
// Sibling directory bonus (same parent, different immediate directory)
|
||||
if (commonPrefixLength === Math.min(callerParts.length - 2, candidateParts.length - 2) &&
|
||||
commonPrefixLength > 0) {
|
||||
score += 15;
|
||||
}
|
||||
|
||||
// Naming convention bonuses
|
||||
score += this.calculateNamingConventionScore(normalizedCaller, normalizedCandidate);
|
||||
|
||||
// Penalize deep nested paths
|
||||
const nestingPenalty = Math.max(0, candidateParts.length - 5) * 2;
|
||||
score -= nestingPenalty;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
private calculateNamingConventionScore(callerPath: string, candidatePath: string): number {
|
||||
let score = 0;
|
||||
|
||||
// Extract directory and file names
|
||||
const callerParts = callerPath.split('/');
|
||||
const candidateParts = candidatePath.split('/');
|
||||
|
||||
const callerDir = callerParts[callerParts.length - 2] || '';
|
||||
const candidateDir = candidateParts[candidateParts.length - 2] || '';
|
||||
|
||||
const callerFile = callerParts[callerParts.length - 1].replace(/\.[^.]*$/, '');
|
||||
const candidateFile = candidateParts[candidateParts.length - 1].replace(/\.[^.]*$/, '');
|
||||
|
||||
// Prefer utils, helpers, common files
|
||||
if (candidateFile.match(/^(utils?|helpers?|common|shared|lib|core)$/i)) {
|
||||
score += 10;
|
||||
}
|
||||
|
||||
// Prefer files with similar names
|
||||
if (this.calculateStringSimilarity(callerFile, candidateFile) > 0.6) {
|
||||
score += 8;
|
||||
}
|
||||
|
||||
// Prefer similar directory names
|
||||
if (this.calculateStringSimilarity(callerDir, candidateDir) > 0.7) {
|
||||
score += 5;
|
||||
}
|
||||
|
||||
// Avoid test files unless caller is also a test
|
||||
if (candidateFile.match(/test|spec/i) && !callerFile.match(/test|spec/i)) {
|
||||
score -= 20;
|
||||
}
|
||||
|
||||
// Avoid legacy/deprecated paths
|
||||
if (candidatePath.match(/(legacy|deprecated|old|archive)/i)) {
|
||||
score -= 15;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
private calculateStringSimilarity(str1: string, str2: string): number {
|
||||
if (str1 === str2) return 1.0;
|
||||
if (str1.length === 0 || str2.length === 0) return 0.0;
|
||||
|
||||
const longer = str1.length > str2.length ? str1 : str2;
|
||||
const shorter = str1.length > str2.length ? str2 : str1;
|
||||
|
||||
if (longer.length === 0) return 1.0;
|
||||
|
||||
const editDistance = this.calculateLevenshteinDistance(longer, shorter);
|
||||
return (longer.length - editDistance) / longer.length;
|
||||
}
|
||||
|
||||
private calculateLevenshteinDistance(str1: string, str2: string): number {
|
||||
const matrix = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(null));
|
||||
|
||||
for (let i = 0; i <= str1.length; i++) matrix[0][i] = i;
|
||||
for (let j = 0; j <= str2.length; j++) matrix[j][0] = j;
|
||||
|
||||
for (let j = 1; j <= str2.length; j++) {
|
||||
for (let i = 1; i <= str1.length; i++) {
|
||||
const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
||||
matrix[j][i] = Math.min(
|
||||
matrix[j][i - 1] + 1, // deletion
|
||||
matrix[j - 1][i] + 1, // insertion
|
||||
matrix[j - 1][i - 1] + indicator // substitution
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return matrix[str2.length][str1.length];
|
||||
}
|
||||
|
||||
private normalizePath(path: string): string {
|
||||
return path.replace(/\\/g, '/').toLowerCase();
|
||||
}
|
||||
|
||||
private resolveSuperCall(graph: KnowledgeGraph, call: FunctionCall): GraphNode | null {
|
||||
if (call.callType !== 'super' || !call.superMethodName) return null;
|
||||
|
||||
// Find the current method node to get its class context
|
||||
const currentMethodNode = graph.nodes.find(node =>
|
||||
node.label === 'Method' &&
|
||||
node.properties.filePath === call.callerFilePath &&
|
||||
node.properties.name === call.callerFunction
|
||||
);
|
||||
|
||||
if (!currentMethodNode || !currentMethodNode.properties.parentClass) return null;
|
||||
|
||||
const currentClassName = currentMethodNode.properties.parentClass as string;
|
||||
|
||||
// Find the current class node to get its base classes
|
||||
const currentClassNode = graph.nodes.find(node =>
|
||||
node.label === 'Class' &&
|
||||
node.properties.filePath === call.callerFilePath &&
|
||||
node.properties.name === currentClassName
|
||||
);
|
||||
|
||||
if (!currentClassNode || !currentClassNode.properties.baseClasses) return null;
|
||||
|
||||
const baseClasses = currentClassNode.properties.baseClasses as string[];
|
||||
|
||||
// Look for the method in each base class (in order)
|
||||
for (const baseClassName of baseClasses) {
|
||||
const baseMethodNode = graph.nodes.find(node =>
|
||||
node.label === 'Method' &&
|
||||
node.properties.name === call.superMethodName &&
|
||||
node.properties.parentClass === baseClassName
|
||||
);
|
||||
|
||||
if (baseMethodNode) {
|
||||
return baseMethodNode;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private resolveLocalFunction(call: FunctionCall): GraphNode | null {
|
||||
// Try function key first
|
||||
const functionKey = `${call.callerFilePath}:function:${call.calledName}`;
|
||||
|
|
@ -433,9 +1092,22 @@ export class CallProcessor {
|
|||
return this.functionNodes.get(methodKey) || null;
|
||||
}
|
||||
|
||||
private getVariableType(filePath: string, functionContext: string, variableName: string): VariableTypeInfo | null {
|
||||
const variableKey = `${filePath}:${functionContext}:${variableName}`;
|
||||
return this.variableTypes.get(variableKey) || null;
|
||||
}
|
||||
|
||||
private resolveMethodCall(graph: KnowledgeGraph, call: FunctionCall): GraphNode | null {
|
||||
if (call.callType !== 'method') return null;
|
||||
|
||||
// If this is a chained method call, use type information to resolve
|
||||
if (call.chainedFromVariable) {
|
||||
const variableType = this.getVariableType(call.callerFilePath, call.callerFunction, call.chainedFromVariable);
|
||||
if (variableType) {
|
||||
return this.resolveMethodOnType(graph, call, variableType.inferredType);
|
||||
}
|
||||
}
|
||||
|
||||
// First try to find methods in the same file using the new key format
|
||||
const methodKey = `${call.callerFilePath}:method:${call.calledName}`;
|
||||
const methodNode = this.functionNodes.get(methodKey);
|
||||
|
|
@ -453,6 +1125,32 @@ export class CallProcessor {
|
|||
return methods[0] || null;
|
||||
}
|
||||
|
||||
private resolveMethodOnType(graph: KnowledgeGraph, call: FunctionCall, typeName: string): GraphNode | null {
|
||||
// Look for methods of the specified type across all files
|
||||
const methods = graph.nodes.filter(node =>
|
||||
node.label === 'Method' &&
|
||||
node.properties.name === call.calledName &&
|
||||
node.properties.parentClass === typeName
|
||||
);
|
||||
|
||||
if (methods.length > 0) {
|
||||
// Prefer methods in the same file, then use proximity-based ranking
|
||||
const sameFileMethod = methods.find(method =>
|
||||
method.properties.filePath === call.callerFilePath
|
||||
);
|
||||
|
||||
if (sameFileMethod) {
|
||||
return sameFileMethod;
|
||||
}
|
||||
|
||||
// Use advanced fallback ranking for cross-file method resolution
|
||||
const rankedMethods = this.rankCandidatesByProximity(call.callerFilePath, methods);
|
||||
return rankedMethods.length > 0 ? rankedMethods[0].node : methods[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private getOrCreateBuiltinNode(functionName: string): GraphNode {
|
||||
const id = generateId('builtin', functionName);
|
||||
return {
|
||||
|
|
@ -534,6 +1232,84 @@ export class CallProcessor {
|
|||
return stats;
|
||||
}
|
||||
|
||||
public getTypeInferenceStats(): {
|
||||
totalVariablesTyped: number;
|
||||
typesByConfidence: Record<string, number>;
|
||||
typesBySource: Record<string, number>;
|
||||
mostCommonTypes: Array<{type: string, count: number}>;
|
||||
} {
|
||||
const stats = {
|
||||
totalVariablesTyped: this.variableTypes.size,
|
||||
typesByConfidence: { high: 0, medium: 0, low: 0 },
|
||||
typesBySource: { constructor: 0, method_return: 0, factory: 0, assignment: 0, parameter: 0 },
|
||||
mostCommonTypes: [] as Array<{type: string, count: number}>
|
||||
};
|
||||
|
||||
const typeCounts = new Map<string, number>();
|
||||
|
||||
for (const typeInfo of this.variableTypes.values()) {
|
||||
stats.typesByConfidence[typeInfo.confidence]++;
|
||||
stats.typesBySource[typeInfo.source]++;
|
||||
|
||||
const currentCount = typeCounts.get(typeInfo.inferredType) || 0;
|
||||
typeCounts.set(typeInfo.inferredType, currentCount + 1);
|
||||
}
|
||||
|
||||
// Sort types by frequency
|
||||
stats.mostCommonTypes = Array.from(typeCounts.entries())
|
||||
.map(([type, count]) => ({ type, count }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 10);
|
||||
|
||||
return stats;
|
||||
}
|
||||
|
||||
public getVariableTypeInfo(filePath: string, functionContext: string, variableName: string): VariableTypeInfo | null {
|
||||
return this.getVariableType(filePath, functionContext, variableName);
|
||||
}
|
||||
|
||||
public getAllVariableTypes(): VariableTypeInfo[] {
|
||||
return Array.from(this.variableTypes.values());
|
||||
}
|
||||
|
||||
private logTypeInference(call: FunctionCall, inferredType: string, confidence: string): void {
|
||||
if (process.env.GITNEXUS_DEBUG_TYPES === 'true') {
|
||||
console.log(`Type inference: ${call.assignedToVariable} = ${call.calledName}() -> ${inferredType} (${confidence} confidence)`);
|
||||
}
|
||||
}
|
||||
|
||||
public getAdvancedFallbackStats(): {
|
||||
totalFallbackResolutions: number;
|
||||
successfulResolutions: number;
|
||||
ambiguousCallsResolved: number;
|
||||
} {
|
||||
// This would be populated during processing in a real implementation
|
||||
return {
|
||||
totalFallbackResolutions: 0,
|
||||
successfulResolutions: 0,
|
||||
ambiguousCallsResolved: 0
|
||||
};
|
||||
}
|
||||
|
||||
private logCandidateAnalysis(call: FunctionCall, candidates: Array<{node: GraphNode, score: number}>): void {
|
||||
console.log(`\n=== Advanced Fallback Analysis for ${call.calledName} ===`);
|
||||
console.log(`Caller: ${call.callerFilePath}:${call.callerFunction}`);
|
||||
console.log(`Found ${candidates.length} candidates:`);
|
||||
|
||||
candidates.forEach((candidate, index) => {
|
||||
const filePath = candidate.node.properties.filePath as string;
|
||||
const name = candidate.node.properties.name as string;
|
||||
const label = candidate.node.label;
|
||||
|
||||
console.log(` ${index + 1}. ${filePath}:${name} (${label}) - Score: ${candidate.score.toFixed(2)}`);
|
||||
|
||||
if (index === 0) {
|
||||
console.log(` ✅ SELECTED`);
|
||||
}
|
||||
});
|
||||
console.log(`==========================================\n`);
|
||||
}
|
||||
|
||||
private async extractImportsRegex(filePath: string, content: string): Promise<void> {
|
||||
const imports: ImportInfo[] = [];
|
||||
const lines = content.split('\n');
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ interface ParsedDefinition {
|
|||
startLine: number;
|
||||
endLine: number;
|
||||
parentClass?: string;
|
||||
decorators?: string[];
|
||||
baseClasses?: string[];
|
||||
}
|
||||
|
||||
export class ParsingProcessor {
|
||||
|
|
@ -91,6 +93,9 @@ export class ParsingProcessor {
|
|||
});
|
||||
}
|
||||
|
||||
// Create inheritance and override relationships
|
||||
this.createInheritanceRelationships(graph, filePath, definitions);
|
||||
|
||||
if (definitions.length > 0) {
|
||||
successfullyParsed++;
|
||||
console.log(`✅ Successfully parsed ${filePath} - found ${definitions.length} definitions`);
|
||||
|
|
@ -131,6 +136,9 @@ export class ParsingProcessor {
|
|||
properties: {}
|
||||
});
|
||||
}
|
||||
|
||||
// Create inheritance and override relationships
|
||||
this.createInheritanceRelationships(graph, filePath, definitions);
|
||||
}
|
||||
|
||||
} catch (parseError) {
|
||||
|
|
@ -235,6 +243,9 @@ export class ParsingProcessor {
|
|||
const line = lines[i].trim();
|
||||
const lineNumber = i + 1;
|
||||
|
||||
// Extract decorators for upcoming function/class definitions
|
||||
const decorators = this.extractDecoratorsRegex(lines, i);
|
||||
|
||||
// Match function definitions: def function_name(
|
||||
const functionMatch = line.match(/^def\s+(\w+)\s*\(/);
|
||||
if (functionMatch) {
|
||||
|
|
@ -242,18 +253,22 @@ export class ParsingProcessor {
|
|||
name: functionMatch[1],
|
||||
type: 'function',
|
||||
startLine: lineNumber,
|
||||
endLine: lineNumber // Basic implementation
|
||||
endLine: lineNumber,
|
||||
decorators: decorators.length > 0 ? decorators : undefined
|
||||
});
|
||||
}
|
||||
|
||||
// Match class definitions: class ClassName
|
||||
const classMatch = line.match(/^class\s+(\w+)(?:\s*\(.*\))?\s*:/);
|
||||
if (classMatch) {
|
||||
const baseClasses = this.extractBaseClassesRegex(line);
|
||||
definitions.push({
|
||||
name: classMatch[1],
|
||||
type: 'class',
|
||||
startLine: lineNumber,
|
||||
endLine: lineNumber // Basic implementation
|
||||
endLine: lineNumber,
|
||||
decorators: decorators.length > 0 ? decorators : undefined,
|
||||
baseClasses: baseClasses.length > 0 ? baseClasses : undefined
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -275,12 +290,16 @@ export class ParsingProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
// Extract decorators for methods (look for indented decorators)
|
||||
const methodDecorators = this.extractMethodDecoratorsRegex(lines, i);
|
||||
|
||||
definitions.push({
|
||||
name: methodMatch[1],
|
||||
type: 'method',
|
||||
startLine: lineNumber,
|
||||
endLine: lineNumber,
|
||||
parentClass
|
||||
parentClass,
|
||||
decorators: methodDecorators.length > 0 ? methodDecorators : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -289,6 +308,95 @@ export class ParsingProcessor {
|
|||
return definitions;
|
||||
}
|
||||
|
||||
private extractDecoratorsRegex(lines: string[], currentIndex: number): string[] {
|
||||
const decorators: string[] = [];
|
||||
|
||||
// Look backwards for decorator lines
|
||||
for (let i = currentIndex - 1; i >= 0; i--) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
// Stop if we hit a non-decorator, non-empty line
|
||||
if (line && !line.startsWith('@')) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (line.startsWith('@')) {
|
||||
const decoratorName = this.parseDecoratorNameRegex(line);
|
||||
if (decoratorName) {
|
||||
decorators.unshift(decoratorName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return decorators;
|
||||
}
|
||||
|
||||
private extractMethodDecoratorsRegex(lines: string[], currentIndex: number): string[] {
|
||||
const decorators: string[] = [];
|
||||
|
||||
// Look backwards for indented decorator lines
|
||||
for (let i = currentIndex - 1; i >= 0; i--) {
|
||||
const line = lines[i];
|
||||
const trimmedLine = line.trim();
|
||||
|
||||
// Stop if we hit a non-decorator line that's not just whitespace
|
||||
if (trimmedLine && !trimmedLine.startsWith('@')) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (trimmedLine.startsWith('@') && line.match(/^\s+@/)) {
|
||||
const decoratorName = this.parseDecoratorNameRegex(trimmedLine);
|
||||
if (decoratorName) {
|
||||
decorators.unshift(decoratorName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return decorators;
|
||||
}
|
||||
|
||||
private extractBaseClassesRegex(classLine: string): string[] {
|
||||
const baseClasses: string[] = [];
|
||||
|
||||
// Match class definition with parentheses: class Child(Parent1, Parent2):
|
||||
const match = classLine.match(/^class\s+\w+\s*\(([^)]+)\)\s*:/);
|
||||
if (match) {
|
||||
const baseClassesStr = match[1].trim();
|
||||
if (baseClassesStr) {
|
||||
// Split by comma and clean up each base class name
|
||||
const classes = baseClassesStr.split(',').map(cls => cls.trim());
|
||||
for (const cls of classes) {
|
||||
// Handle simple names and qualified names
|
||||
const cleanClass = cls.replace(/\s+/g, '');
|
||||
if (cleanClass && cleanClass.match(/^[a-zA-Z_][a-zA-Z0-9_.]*$/)) {
|
||||
baseClasses.push(cleanClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return baseClasses;
|
||||
}
|
||||
|
||||
private parseDecoratorNameRegex(decoratorLine: string): string | null {
|
||||
// Remove @ symbol and extract decorator name
|
||||
const withoutAt = decoratorLine.substring(1);
|
||||
|
||||
// Handle simple decorators: @decorator_name
|
||||
const simpleMatch = withoutAt.match(/^([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)/);
|
||||
if (simpleMatch) {
|
||||
return simpleMatch[1];
|
||||
}
|
||||
|
||||
// Handle decorators with arguments: @decorator_name(args)
|
||||
const withArgsMatch = withoutAt.match(/^([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\s*\(/);
|
||||
if (withArgsMatch) {
|
||||
return withArgsMatch[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private parsePythonFile(filePath: string, fileContent: string): ParsedDefinition[] {
|
||||
if (!this.parser) {
|
||||
console.warn('Parser not initialized. Cannot parse Python file:', filePath);
|
||||
|
|
@ -320,22 +428,28 @@ export class ParsingProcessor {
|
|||
if (currentNode.type === 'function_definition' && !processedMethodNodes.has(currentNode)) {
|
||||
const nameNode = currentNode.childForFieldName('name');
|
||||
if (nameNode) {
|
||||
const decorators = this.extractDecorators(currentNode);
|
||||
definitions.push({
|
||||
name: nameNode.text,
|
||||
type: 'function',
|
||||
startLine: currentNode.startPosition.row + 1,
|
||||
endLine: currentNode.endPosition.row + 1
|
||||
endLine: currentNode.endPosition.row + 1,
|
||||
decorators: decorators.length > 0 ? decorators : undefined
|
||||
});
|
||||
}
|
||||
} else if (currentNode.type === 'class_definition') {
|
||||
const nameNode = currentNode.childForFieldName('name');
|
||||
if (nameNode) {
|
||||
const className = nameNode.text;
|
||||
const decorators = this.extractDecorators(currentNode);
|
||||
const baseClasses = this.extractBaseClasses(currentNode);
|
||||
definitions.push({
|
||||
name: className,
|
||||
type: 'class',
|
||||
startLine: currentNode.startPosition.row + 1,
|
||||
endLine: currentNode.endPosition.row + 1
|
||||
endLine: currentNode.endPosition.row + 1,
|
||||
decorators: decorators.length > 0 ? decorators : undefined,
|
||||
baseClasses: baseClasses.length > 0 ? baseClasses : undefined
|
||||
});
|
||||
|
||||
const methods = this.extractMethodsFromClass(currentNode, className);
|
||||
|
|
@ -365,11 +479,111 @@ export class ParsingProcessor {
|
|||
filePath,
|
||||
startLine: definition.startLine,
|
||||
endLine: definition.endLine,
|
||||
...(definition.parentClass && { parentClass: definition.parentClass })
|
||||
...(definition.parentClass && { parentClass: definition.parentClass }),
|
||||
...(definition.decorators && { decorators: definition.decorators }),
|
||||
...(definition.baseClasses && { baseClasses: definition.baseClasses })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private createInheritanceRelationships(
|
||||
graph: KnowledgeGraph,
|
||||
filePath: string,
|
||||
definitions: ParsedDefinition[]
|
||||
): void {
|
||||
// Create INHERITS relationships for classes with base classes
|
||||
const classDefinitions = definitions.filter(def => def.type === 'class');
|
||||
|
||||
for (const classDef of classDefinitions) {
|
||||
if (classDef.baseClasses && classDef.baseClasses.length > 0) {
|
||||
const childClassId = generateId('class', `${filePath}:${classDef.name}`);
|
||||
|
||||
for (const baseClassName of classDef.baseClasses) {
|
||||
// Try to find the base class in the same file first
|
||||
let baseClassId = generateId('class', `${filePath}:${baseClassName}`);
|
||||
let baseClassExists = graph.nodes.some(node => node.id === baseClassId);
|
||||
|
||||
if (!baseClassExists) {
|
||||
// If not found in same file, look for it in other files
|
||||
const baseClassNode = graph.nodes.find(node =>
|
||||
node.label === 'Class' &&
|
||||
node.properties.name === baseClassName
|
||||
);
|
||||
|
||||
if (baseClassNode) {
|
||||
baseClassId = baseClassNode.id;
|
||||
baseClassExists = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (baseClassExists) {
|
||||
// Create INHERITS relationship
|
||||
const inheritanceRelationship: GraphRelationship = {
|
||||
id: generateId('relationship', `${childClassId}-inherits-${baseClassId}`),
|
||||
type: 'INHERITS',
|
||||
source: childClassId,
|
||||
target: baseClassId,
|
||||
properties: {}
|
||||
};
|
||||
|
||||
graph.relationships.push(inheritanceRelationship);
|
||||
|
||||
// Create OVERRIDES relationships for methods
|
||||
this.createOverrideRelationships(graph, filePath, classDef, baseClassName, definitions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private createOverrideRelationships(
|
||||
graph: KnowledgeGraph,
|
||||
filePath: string,
|
||||
childClass: ParsedDefinition,
|
||||
baseClassName: string,
|
||||
allDefinitions: ParsedDefinition[]
|
||||
): void {
|
||||
// Get all methods from the child class
|
||||
const childMethods = allDefinitions.filter(def =>
|
||||
def.type === 'method' && def.parentClass === childClass.name
|
||||
);
|
||||
|
||||
for (const childMethod of childMethods) {
|
||||
// Look for a method with the same name in the base class
|
||||
const baseMethodId = this.findBaseClassMethod(graph, baseClassName, childMethod.name);
|
||||
|
||||
if (baseMethodId) {
|
||||
const childMethodId = generateId('method', `${filePath}:${childMethod.name}`);
|
||||
|
||||
// Create OVERRIDES relationship
|
||||
const overrideRelationship: GraphRelationship = {
|
||||
id: generateId('relationship', `${childMethodId}-overrides-${baseMethodId}`),
|
||||
type: 'OVERRIDES',
|
||||
source: childMethodId,
|
||||
target: baseMethodId,
|
||||
properties: {
|
||||
methodName: childMethod.name,
|
||||
childClass: childClass.name,
|
||||
baseClass: baseClassName
|
||||
}
|
||||
};
|
||||
|
||||
graph.relationships.push(overrideRelationship);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private findBaseClassMethod(graph: KnowledgeGraph, baseClassName: string, methodName: string): string | null {
|
||||
// Look for a method with the given name in the base class
|
||||
const baseMethod = graph.nodes.find(node =>
|
||||
node.label === 'Method' &&
|
||||
node.properties.name === methodName &&
|
||||
node.properties.parentClass === baseClassName
|
||||
);
|
||||
|
||||
return baseMethod ? baseMethod.id : null;
|
||||
}
|
||||
|
||||
private createDefinitionRelationships(
|
||||
graph: KnowledgeGraph,
|
||||
filePath: string,
|
||||
|
|
@ -446,12 +660,14 @@ export class ParsingProcessor {
|
|||
if (node.type === 'function_definition') {
|
||||
const nameNode = node.childForFieldName('name');
|
||||
if (nameNode) {
|
||||
const decorators = this.extractDecorators(node);
|
||||
methods.push({
|
||||
name: nameNode.text,
|
||||
type: 'method',
|
||||
startLine: node.startPosition.row + 1,
|
||||
endLine: node.endPosition.row + 1,
|
||||
parentClass: className
|
||||
parentClass: className,
|
||||
decorators: decorators.length > 0 ? decorators : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -460,6 +676,69 @@ export class ParsingProcessor {
|
|||
return methods;
|
||||
}
|
||||
|
||||
private extractBaseClasses(classNode: Parser.SyntaxNode): string[] {
|
||||
const baseClasses: string[] = [];
|
||||
|
||||
// Look for argument_list node which contains base classes
|
||||
const argumentList = classNode.childForFieldName('superclasses');
|
||||
if (argumentList) {
|
||||
this.traverseNode(argumentList, (node: Parser.SyntaxNode) => {
|
||||
if (node.type === 'identifier') {
|
||||
baseClasses.push(node.text);
|
||||
} else if (node.type === 'attribute') {
|
||||
// Handle qualified names like module.ClassName
|
||||
baseClasses.push(node.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return baseClasses;
|
||||
}
|
||||
|
||||
private extractDecorators(node: Parser.SyntaxNode): string[] {
|
||||
const decorators: string[] = [];
|
||||
|
||||
// Look for decorator nodes that are siblings before the function/class definition
|
||||
let currentNode = node.previousSibling;
|
||||
|
||||
while (currentNode && currentNode.type === 'decorator') {
|
||||
const decoratorName = this.getDecoratorName(currentNode);
|
||||
if (decoratorName) {
|
||||
decorators.unshift(decoratorName); // Add to beginning to maintain order
|
||||
}
|
||||
currentNode = currentNode.previousSibling;
|
||||
}
|
||||
|
||||
return decorators;
|
||||
}
|
||||
|
||||
private getDecoratorName(decoratorNode: Parser.SyntaxNode): string | null {
|
||||
// Find the identifier or attribute after the '@' symbol
|
||||
for (let i = 0; i < decoratorNode.childCount; i++) {
|
||||
const child = decoratorNode.child(i);
|
||||
if (!child) continue;
|
||||
|
||||
if (child.type === 'identifier') {
|
||||
return child.text;
|
||||
} else if (child.type === 'attribute') {
|
||||
// Handle dotted decorators like @app.route
|
||||
return child.text;
|
||||
} else if (child.type === 'call') {
|
||||
// Handle decorators with arguments like @retry(attempts=3)
|
||||
const functionNode = child.childForFieldName('function');
|
||||
if (functionNode) {
|
||||
if (functionNode.type === 'identifier') {
|
||||
return functionNode.text;
|
||||
} else if (functionNode.type === 'attribute') {
|
||||
return functionNode.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private traverseNode(node: Parser.SyntaxNode, callback: (node: Parser.SyntaxNode) => void): void {
|
||||
callback(node);
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ interface AppState {
|
|||
azureOpenAIEndpoint: string;
|
||||
azureOpenAIDeploymentName: string;
|
||||
azureOpenAIApiVersion: string;
|
||||
// GitHub settings
|
||||
githubToken: string;
|
||||
showSettings: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -52,6 +54,7 @@ const initialState: AppState = {
|
|||
azureOpenAIEndpoint: localStorage.getItem('azure_openai_endpoint') || '',
|
||||
azureOpenAIDeploymentName: localStorage.getItem('azure_openai_deployment') || '',
|
||||
azureOpenAIApiVersion: localStorage.getItem('azure_openai_api_version') || '2024-02-01',
|
||||
githubToken: localStorage.getItem('github_token') || '',
|
||||
showSettings: false
|
||||
};
|
||||
|
||||
|
|
@ -81,7 +84,10 @@ const HomePage: React.FC = () => {
|
|||
if (state.azureOpenAIApiVersion) {
|
||||
localStorage.setItem('azure_openai_api_version', state.azureOpenAIApiVersion);
|
||||
}
|
||||
}, [state.llmApiKey, state.llmProvider, state.azureOpenAIEndpoint, state.azureOpenAIDeploymentName, state.azureOpenAIApiVersion]);
|
||||
if (state.githubToken) {
|
||||
localStorage.setItem('github_token', state.githubToken);
|
||||
}
|
||||
}, [state.llmApiKey, state.llmProvider, state.azureOpenAIEndpoint, state.azureOpenAIDeploymentName, state.azureOpenAIApiVersion, state.githubToken]);
|
||||
|
||||
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
|
|
@ -145,7 +151,10 @@ const HomePage: React.FC = () => {
|
|||
|
||||
console.log('Starting GitHub processing...', state.githubUrl);
|
||||
|
||||
const result = await services.ingestion.processGitHubRepo(state.githubUrl, {
|
||||
// Create ingestion service with GitHub token if provided
|
||||
const ingestionService = new IngestionService(state.githubToken || undefined);
|
||||
|
||||
const result = await ingestionService.processGitHubRepo(state.githubUrl, {
|
||||
directoryFilter: state.directoryFilter,
|
||||
fileExtensions: state.fileExtensions,
|
||||
onProgress: (progress) => {
|
||||
|
|
@ -500,12 +509,60 @@ const HomePage: React.FC = () => {
|
|||
style={styles.input}
|
||||
disabled={state.isProcessing}
|
||||
/>
|
||||
|
||||
{/* GitHub Token Input - Optional */}
|
||||
<div style={{ marginTop: '12px' }}>
|
||||
<label style={{...styles.label, color: colors.textMuted}}>
|
||||
GitHub Personal Access Token (Optional)
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={state.githubToken}
|
||||
onChange={(e) => updateState({ githubToken: e.target.value })}
|
||||
placeholder="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||
style={{
|
||||
...styles.input,
|
||||
borderColor: colors.borderLight,
|
||||
backgroundColor: colors.surface
|
||||
}}
|
||||
disabled={state.isProcessing}
|
||||
/>
|
||||
<div style={{
|
||||
fontSize: '11px',
|
||||
color: colors.textMuted,
|
||||
marginTop: '4px',
|
||||
lineHeight: '1.4'
|
||||
}}>
|
||||
Increases rate limit from 60 to 5,000 requests/hour.
|
||||
<a href="https://github.com/settings/tokens" target="_blank" rel="noopener noreferrer"
|
||||
style={{ color: colors.primary, textDecoration: 'none', marginLeft: '4px' }}>
|
||||
Generate token
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GitHub Token Status */}
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: state.githubToken ? colors.success : colors.textMuted,
|
||||
marginTop: '8px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}>
|
||||
<span>{state.githubToken ? '🔑' : '⚠️'}</span>
|
||||
{state.githubToken
|
||||
? 'GitHub token configured (5,000 requests/hour)'
|
||||
: 'No GitHub token (60 requests/hour limit)'}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleGitHubProcess}
|
||||
disabled={state.isProcessing || !state.githubUrl.trim()}
|
||||
style={{
|
||||
...styles.primaryButton,
|
||||
opacity: state.isProcessing || !state.githubUrl.trim() ? 0.5 : 1
|
||||
opacity: state.isProcessing || !state.githubUrl.trim() ? 0.5 : 1,
|
||||
marginTop: '16px'
|
||||
}}
|
||||
>
|
||||
<span>📊</span>
|
||||
|
|
@ -689,9 +746,53 @@ const HomePage: React.FC = () => {
|
|||
overflow: 'auto'
|
||||
}}>
|
||||
<h2 style={{ color: colors.text, marginBottom: '24px', fontSize: '24px', fontWeight: '700' }}>
|
||||
🤖 LLM Settings
|
||||
⚙️ Settings
|
||||
</h2>
|
||||
|
||||
{/* GitHub Token Section */}
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: colors.surfaceWarm,
|
||||
border: `1px solid ${colors.borderLight}`,
|
||||
marginBottom: '24px'
|
||||
}}>
|
||||
<h3 style={{ color: colors.text, marginBottom: '16px', fontSize: '18px', fontWeight: '600' }}>
|
||||
🔑 GitHub Configuration
|
||||
</h3>
|
||||
|
||||
<div style={styles.inputGroup}>
|
||||
<label style={styles.label}>
|
||||
GitHub Personal Access Token (Optional)
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={state.githubToken}
|
||||
onChange={(e) => updateState({ githubToken: e.target.value })}
|
||||
placeholder="ghp_xxxxxxxxxxxxxxxxxxxx"
|
||||
style={styles.input}
|
||||
/>
|
||||
<div style={{ fontSize: '12px', color: colors.textMuted, marginTop: '4px' }}>
|
||||
Increases rate limit from 60 to 5,000 requests/hour. Generate at:
|
||||
<a href="https://github.com/settings/tokens" target="_blank" rel="noopener noreferrer"
|
||||
style={{ color: colors.primary, textDecoration: 'none', marginLeft: '4px' }}>
|
||||
github.com/settings/tokens
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* LLM Configuration Section */}
|
||||
<div style={{
|
||||
padding: '20px',
|
||||
borderRadius: '12px',
|
||||
backgroundColor: colors.surfaceWarm,
|
||||
border: `1px solid ${colors.borderLight}`
|
||||
}}>
|
||||
<h3 style={{ color: colors.text, marginBottom: '16px', fontSize: '18px', fontWeight: '600' }}>
|
||||
🤖 LLM Configuration
|
||||
</h3>
|
||||
|
||||
{/* Provider Selection */}
|
||||
<div style={styles.inputGroup}>
|
||||
<label style={styles.label}>LLM Provider</label>
|
||||
|
|
@ -823,10 +924,26 @@ const HomePage: React.FC = () => {
|
|||
{state.llmProvider === 'gemini' && 'Google Gemini API. Get your API key from aistudio.google.com'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px', marginTop: '24px' }}>
|
||||
<button
|
||||
onClick={() => updateState({ showSettings: false })}
|
||||
onClick={() => {
|
||||
// Save settings to localStorage
|
||||
if (state.githubToken) {
|
||||
localStorage.setItem('github_token', state.githubToken);
|
||||
} else {
|
||||
localStorage.removeItem('github_token');
|
||||
}
|
||||
localStorage.setItem('llm_provider', state.llmProvider);
|
||||
localStorage.setItem('llm_api_key', state.llmApiKey);
|
||||
if (state.llmProvider === 'azure-openai') {
|
||||
localStorage.setItem('azure_openai_endpoint', state.azureOpenAIEndpoint);
|
||||
localStorage.setItem('azure_openai_deployment', state.azureOpenAIDeploymentName);
|
||||
localStorage.setItem('azure_openai_api_version', state.azureOpenAIApiVersion);
|
||||
}
|
||||
updateState({ showSettings: false });
|
||||
}}
|
||||
style={styles.primaryButton}
|
||||
>
|
||||
💾 Save Settings
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue