Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions sourcecode-parser/graph/callgraph/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -346,12 +346,12 @@ func BenchmarkResolveCallTarget(b *testing.B) {

for i := 0; i < b.N; i++ {
// Test simple attribute access (most common case)
_, _ = resolveCallTarget("utils.process_data", importMap, registry, currentModule, codeGraph)
_, _ = resolveCallTarget("utils.process_data", importMap, registry, currentModule, codeGraph, nil, "")

// Test aliased import
_, _ = resolveCallTarget("helper.format", importMap, registry, currentModule, codeGraph)
_, _ = resolveCallTarget("helper.format", importMap, registry, currentModule, codeGraph, nil, "")

// Test fully qualified name
_, _ = resolveCallTarget("myapp.utils.validate", importMap, registry, currentModule, codeGraph)
_, _ = resolveCallTarget("myapp.utils.validate", importMap, registry, currentModule, codeGraph, nil, "")
}
}
124 changes: 122 additions & 2 deletions sourcecode-parser/graph/callgraph/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,10 @@ func BuildCallGraph(codeGraph *graph.CodeGraph, registry *ModuleRegistry, projec
// This avoids re-parsing imports from the same file multiple times
importCache := NewImportMapCache()

// Initialize type inference engine
typeEngine := NewTypeInferenceEngine(registry)
typeEngine.Builtins = NewBuiltinRegistry()

// First, index all function definitions from the code graph
// This builds the Functions map for quick lookup
indexFunctions(codeGraph, callGraph, registry)
Expand All @@ -163,6 +167,9 @@ func BuildCallGraph(codeGraph *graph.CodeGraph, registry *ModuleRegistry, projec
continue
}

// Extract variable assignments for type inference
_ = ExtractVariableAssignments(filePath, sourceCode, typeEngine, registry, typeEngine.Builtins)

// Extract all call sites from this file
callSites, err := ExtractCallSites(filePath, sourceCode, importMap)
if err != nil {
Expand All @@ -183,7 +190,7 @@ func BuildCallGraph(codeGraph *graph.CodeGraph, registry *ModuleRegistry, projec
}

// Resolve the call target to a fully qualified name
targetFQN, resolved := resolveCallTarget(callSite.Target, importMap, registry, modulePath, codeGraph)
targetFQN, resolved := resolveCallTarget(callSite.Target, importMap, registry, modulePath, codeGraph, typeEngine, callerFQN)

// Update call site with resolution information
callSite.TargetFQN = targetFQN
Expand Down Expand Up @@ -423,7 +430,11 @@ func categorizeResolutionFailure(target, targetFQN string) string {
return "unknown"
}

func resolveCallTarget(target string, importMap *ImportMap, registry *ModuleRegistry, currentModule string, codeGraph *graph.CodeGraph) (string, bool) {
func resolveCallTarget(target string, importMap *ImportMap, registry *ModuleRegistry, currentModule string, codeGraph *graph.CodeGraph, typeEngine *TypeInferenceEngine, callerFQN string) (string, bool) {
// Backward compatibility: if typeEngine or callerFQN not provided, skip type inference
if typeEngine == nil || callerFQN == "" {
return resolveCallTargetLegacy(target, importMap, registry, currentModule, codeGraph)
}
// Handle self.method() calls - resolve to current module
if strings.HasPrefix(target, "self.") {
methodName := strings.TrimPrefix(target, "self.")
Expand Down Expand Up @@ -472,6 +483,30 @@ func resolveCallTarget(target string, importMap *ImportMap, registry *ModuleRegi
base := parts[0]
rest := parts[1]

// Try type inference for variable.method() calls
if typeEngine != nil && callerFQN != "" {
scope := typeEngine.GetScope(callerFQN)
if scope != nil {
// Check if base is a known variable
if binding, exists := scope.Variables[base]; exists && binding.Type != nil {
varTypeFQN := binding.Type.TypeFQN
// Check if it's a builtin type
if typeEngine.Builtins != nil {
method := typeEngine.Builtins.GetMethod(varTypeFQN, rest)
if method != nil {
// Resolved to builtin method
return varTypeFQN + "." + rest, true
}
}
// Try to resolve as user-defined type method
fullFQN := varTypeFQN + "." + rest
if validateFQN(fullFQN, registry) {
return fullFQN, true
}
}
}
}

// Try to resolve base through imports
if baseFQN, ok := importMap.Resolve(base); ok {
fullFQN := baseFQN + "." + rest
Expand Down Expand Up @@ -538,6 +573,91 @@ func validateFQN(fqn string, registry *ModuleRegistry) bool {
return false
}

// resolveCallTargetLegacy is the old resolution logic without type inference.
// Used for backward compatibility with existing tests.
func resolveCallTargetLegacy(target string, importMap *ImportMap, registry *ModuleRegistry, currentModule string, codeGraph *graph.CodeGraph) (string, bool) {
// Handle self.method() calls - resolve to current module
if strings.HasPrefix(target, "self.") {
methodName := strings.TrimPrefix(target, "self.")
// Resolve to module.method
moduleFQN := currentModule + "." + methodName
// Validate exists
if validateFQN(moduleFQN, registry) {
return moduleFQN, true
}
// Return unresolved but with module prefix
return moduleFQN, false
}

// Handle simple names (no dots)
if !strings.Contains(target, ".") {
// Check if it's a Python built-in
if pythonBuiltins[target] {
// Return as builtins.function for pattern matching
return "builtins." + target, true
}

// Try to resolve through imports
if fqn, ok := importMap.Resolve(target); ok {
// Found in imports - return the FQN
// Check if it's a known framework
if isKnown, _ := IsKnownFramework(fqn); isKnown {
return fqn, true
}
// Validate if it exists in registry
resolved := validateFQN(fqn, registry)
return fqn, resolved
}

// Not in imports - might be in same module
sameLevelFQN := currentModule + "." + target
if validateFQN(sameLevelFQN, registry) {
return sameLevelFQN, true
}

// Can't resolve - return as-is
return target, false
}

// Handle qualified names (with dots)
parts := strings.SplitN(target, ".", 2)
base := parts[0]
rest := parts[1]

// Try to resolve base through imports
if baseFQN, ok := importMap.Resolve(base); ok {
fullFQN := baseFQN + "." + rest
// Check if it's a known framework
if isKnown, _ := IsKnownFramework(fullFQN); isKnown {
return fullFQN, true
}
// Check if it's an ORM pattern (before validateFQN, since ORM methods don't exist in source)
if ormFQN, resolved := ResolveORMCall(target, currentModule, registry, codeGraph); resolved {
return ormFQN, true
}
if validateFQN(fullFQN, registry) {
return fullFQN, true
}
return fullFQN, false
}

// Base not in imports - might be module-level access
// Try current module
fullFQN := currentModule + "." + target
if validateFQN(fullFQN, registry) {
return fullFQN, true
}

// Before giving up, check if it's an ORM pattern (Django, SQLAlchemy, etc.)
// ORM methods are dynamically generated at runtime and won't be in source
if ormFQN, resolved := ResolveORMCall(target, currentModule, registry, codeGraph); resolved {
return ormFQN, true
}

// Can't resolve - return as-is
return target, false
}

// readFileBytes reads a file and returns its contents as a byte slice.
// Helper function for reading source code.
func readFileBytes(filePath string) ([]byte, error) {
Expand Down
20 changes: 10 additions & 10 deletions sourcecode-parser/graph/callgraph/builder_framework_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,27 +68,27 @@ def test_stdlib():
codeGraph := &graph.CodeGraph{Nodes: make(map[string]*graph.Node)}

// Test Django models resolution
targetFQN, resolved := resolveCallTarget("models.User", importMap, registry, modulePath, codeGraph)
targetFQN, resolved := resolveCallTarget("models.User", importMap, registry, modulePath, codeGraph, nil, "")
assert.True(t, resolved, "Django models.User should be resolved")
assert.Equal(t, "django.db.models.User", targetFQN)

// Test REST framework resolution
targetFQN, resolved = resolveCallTarget("serializers.ModelSerializer", importMap, registry, modulePath, codeGraph)
targetFQN, resolved = resolveCallTarget("serializers.ModelSerializer", importMap, registry, modulePath, codeGraph, nil, "")
assert.True(t, resolved, "REST framework serializers should be resolved")
assert.Equal(t, "rest_framework.serializers.ModelSerializer", targetFQN)

// Test pytest resolution
targetFQN, resolved = resolveCallTarget("pytest.fixture", importMap, registry, modulePath, codeGraph)
targetFQN, resolved = resolveCallTarget("pytest.fixture", importMap, registry, modulePath, codeGraph, nil, "")
assert.True(t, resolved, "pytest.fixture should be resolved")
assert.Equal(t, "pytest.fixture", targetFQN)

// Test json (stdlib) resolution
targetFQN, resolved = resolveCallTarget("json.loads", importMap, registry, modulePath, codeGraph)
targetFQN, resolved = resolveCallTarget("json.loads", importMap, registry, modulePath, codeGraph, nil, "")
assert.True(t, resolved, "json.loads should be resolved")
assert.Equal(t, "json.loads", targetFQN)

// Test logging (stdlib) resolution
targetFQN, resolved = resolveCallTarget("logging.getLogger", importMap, registry, modulePath, codeGraph)
targetFQN, resolved = resolveCallTarget("logging.getLogger", importMap, registry, modulePath, codeGraph, nil, "")
assert.True(t, resolved, "logging.getLogger should be resolved")
assert.Equal(t, "logging.getLogger", targetFQN)
}
Expand Down Expand Up @@ -143,11 +143,11 @@ def process():
codeGraph := &graph.CodeGraph{Nodes: make(map[string]*graph.Node)}

// Test local function resolution (should resolve to local module)
targetFQN, resolved := resolveCallTarget("sanitize", importMap, registry, modulePath, codeGraph)
targetFQN, resolved := resolveCallTarget("sanitize", importMap, registry, modulePath, codeGraph, nil, "")
assert.True(t, resolved, "Local function sanitize should be resolved")
assert.Contains(t, targetFQN, "utils.sanitize")

targetFQN, resolved = resolveCallTarget("validate", importMap, registry, modulePath, codeGraph)
targetFQN, resolved = resolveCallTarget("validate", importMap, registry, modulePath, codeGraph, nil, "")
assert.True(t, resolved, "Local function validate should be resolved")
assert.Contains(t, targetFQN, "utils.validate")
}
Expand Down Expand Up @@ -197,7 +197,7 @@ def process():
codeGraph := &graph.CodeGraph{Nodes: make(map[string]*graph.Node)}

// Test that local json takes precedence over stdlib
targetFQN, resolved := resolveCallTarget("loads", importMap, registry, modulePath, codeGraph)
targetFQN, resolved := resolveCallTarget("loads", importMap, registry, modulePath, codeGraph, nil, "")
assert.True(t, resolved, "Local json.loads should be resolved")
// When there's a local module that shadows stdlib, it resolves to local
// The FQN will be json.loads but from the local module, not stdlib
Expand Down Expand Up @@ -257,12 +257,12 @@ def process():
codeGraph := &graph.CodeGraph{Nodes: make(map[string]*graph.Node)}

// Test local function resolution
targetFQN, resolved := resolveCallTarget("helper", importMap, registry, modulePath, codeGraph)
targetFQN, resolved := resolveCallTarget("helper", importMap, registry, modulePath, codeGraph, nil, "")
assert.True(t, resolved, "Local helper should be resolved")
assert.Contains(t, targetFQN, "utils.helper")

// Test framework resolution
targetFQN, resolved = resolveCallTarget("json.loads", importMap, registry, modulePath, codeGraph)
targetFQN, resolved = resolveCallTarget("json.loads", importMap, registry, modulePath, codeGraph, nil, "")
assert.True(t, resolved, "json.loads should be resolved as framework")
assert.Equal(t, "json.loads", targetFQN)
}
10 changes: 5 additions & 5 deletions sourcecode-parser/graph/callgraph/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func TestResolveCallTarget_SimpleImportedFunction(t *testing.T) {
importMap.AddImport("sanitize", "myapp.utils.sanitize")

codeGraph := &graph.CodeGraph{Nodes: make(map[string]*graph.Node)}
fqn, resolved := resolveCallTarget("sanitize", importMap, registry, "myapp.views", codeGraph)
fqn, resolved := resolveCallTarget("sanitize", importMap, registry, "myapp.views", codeGraph, nil, "")

assert.True(t, resolved)
assert.Equal(t, "myapp.utils.sanitize", fqn)
Expand All @@ -42,7 +42,7 @@ func TestResolveCallTarget_QualifiedImport(t *testing.T) {
importMap.AddImport("utils", "myapp.utils")

codeGraph := &graph.CodeGraph{Nodes: make(map[string]*graph.Node)}
fqn, resolved := resolveCallTarget("utils.sanitize", importMap, registry, "myapp.views", codeGraph)
fqn, resolved := resolveCallTarget("utils.sanitize", importMap, registry, "myapp.views", codeGraph, nil, "")

assert.True(t, resolved)
assert.Equal(t, "myapp.utils.sanitize", fqn)
Expand All @@ -58,7 +58,7 @@ func TestResolveCallTarget_SameModuleFunction(t *testing.T) {
importMap := NewImportMap("/project/myapp/views.py")

codeGraph := &graph.CodeGraph{Nodes: make(map[string]*graph.Node)}
fqn, resolved := resolveCallTarget("helper", importMap, registry, "myapp.views", codeGraph)
fqn, resolved := resolveCallTarget("helper", importMap, registry, "myapp.views", codeGraph, nil, "")

assert.True(t, resolved)
assert.Equal(t, "myapp.views.helper", fqn)
Expand All @@ -74,7 +74,7 @@ func TestResolveCallTarget_UnresolvedMethodCall(t *testing.T) {
importMap := NewImportMap("/project/myapp/views.py")

codeGraph := &graph.CodeGraph{Nodes: make(map[string]*graph.Node)}
fqn, resolved := resolveCallTarget("obj.method", importMap, registry, "myapp.views", codeGraph)
fqn, resolved := resolveCallTarget("obj.method", importMap, registry, "myapp.views", codeGraph, nil, "")

assert.False(t, resolved)
assert.Equal(t, "obj.method", fqn)
Expand All @@ -90,7 +90,7 @@ func TestResolveCallTarget_NonExistentFunction(t *testing.T) {
importMap.AddImport("missing", "nonexistent.module.function")

codeGraph := &graph.CodeGraph{Nodes: make(map[string]*graph.Node)}
fqn, resolved := resolveCallTarget("missing", importMap, registry, "myapp.views", codeGraph)
fqn, resolved := resolveCallTarget("missing", importMap, registry, "myapp.views", codeGraph, nil, "")

assert.False(t, resolved)
assert.Equal(t, "nonexistent.module.function", fqn)
Expand Down
Loading
Loading