Advance edge case handling for python codebases ( eg: decorators, similar function name, etc

This commit is contained in:
abhigyantrumio 2025-08-05 09:12:42 +05:30
parent 34e7ba20f9
commit 57ac6face5
4 changed files with 15432 additions and 16 deletions

14244
repomix-output.txt Normal file

File diff suppressed because it is too large Load diff

View file

@ -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');

View file

@ -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);

View file

@ -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