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
12 changes: 9 additions & 3 deletions sast-engine/graph/callgraph/builder/go_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func BuildGoCallGraph(codeGraph *graph.CodeGraph, registry *core.GoModuleRegistr

// Pass 1: Index all function definitions
fmt.Fprintf(os.Stderr, " Pass 1: Indexing functions...\n")
functionContext := indexGoFunctions(codeGraph, callGraph, registry)
functionContext := indexGoFunctions(codeGraph, callGraph, registry, typeEngine)
fmt.Fprintf(os.Stderr, " Indexed %d functions\n", len(callGraph.Functions))

// Pass 2a: Extract return types from all indexed Go functions
Expand Down Expand Up @@ -112,7 +112,7 @@ func BuildGoCallGraph(codeGraph *graph.CodeGraph, registry *core.GoModuleRegistr

// Extract variable assignments for this file
// ExtractGoVariableAssignments is thread-safe (uses mutex internally)
_ = extraction.ExtractGoVariableAssignments(filePath, sourceCode, typeEngine, registry, importMaps[filePath])
_ = extraction.ExtractGoVariableAssignments(filePath, sourceCode, typeEngine, registry, importMaps[filePath], callGraph)

// Progress tracking
count := varProcessed.Add(1)
Expand Down Expand Up @@ -284,7 +284,7 @@ func BuildGoCallGraph(codeGraph *graph.CodeGraph, registry *core.GoModuleRegistr
//
// Returns:
// - functionContext: map from simple name to list of nodes for resolution
func indexGoFunctions(codeGraph *graph.CodeGraph, callGraph *core.CallGraph, registry *core.GoModuleRegistry) map[string][]*graph.Node {
func indexGoFunctions(codeGraph *graph.CodeGraph, callGraph *core.CallGraph, registry *core.GoModuleRegistry, typeEngine *resolution.GoTypeInferenceEngine) map[string][]*graph.Node {
functionContext := make(map[string][]*graph.Node)

totalNodes := len(codeGraph.Nodes)
Expand Down Expand Up @@ -312,6 +312,12 @@ func indexGoFunctions(codeGraph *graph.CodeGraph, callGraph *core.CallGraph, reg
// Add to CallGraph.Functions
callGraph.Functions[fqn] = node

// Eagerly create scope so Pattern 1b Source 2 always finds one.
// Guard with GetScope == nil so Pass 2b bindings are not overwritten.
if typeEngine != nil && typeEngine.GetScope(fqn) == nil {
typeEngine.AddScope(resolution.NewGoFunctionScope(fqn))
}

// Add to function context for name-based lookup
functionContext[node.Name] = append(functionContext[node.Name], node)
indexed++
Expand Down
141 changes: 141 additions & 0 deletions sast-engine/graph/callgraph/builder/go_builder_scope_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package builder

import (
"testing"

"github.com/shivasurya/code-pathfinder/sast-engine/graph"
"github.com/shivasurya/code-pathfinder/sast-engine/graph/callgraph/core"
"github.com/shivasurya/code-pathfinder/sast-engine/graph/callgraph/resolution"
"github.com/stretchr/testify/assert"
)

// TestIndexGoFunctions_EagerScopeCreation verifies that indexGoFunctions creates
// an empty GoFunctionScope for every indexed Go function even when no variable
// bindings are available. This ensures Pattern 1b Source 2 always finds a scope.
func TestIndexGoFunctions_EagerScopeCreation(t *testing.T) {
codeGraph := &graph.CodeGraph{
Nodes: map[string]*graph.Node{
"fn1": {
ID: "fn1",
Type: "function_declaration",
Name: "HandleRequest",
File: "/project/main.go",
},
"fn2": {
ID: "fn2",
Type: "method",
Name: "Close",
File: "/project/main.go",
},
"other": {
ID: "other",
Type: "identifier", // non-function node — must be skipped
Name: "x",
File: "/project/main.go",
},
},
}

callGraph := &core.CallGraph{
Functions: make(map[string]*graph.Node),
}

registry := core.NewGoModuleRegistry()
registry.ModulePath = "github.com/example/app"
registry.DirToImport = map[string]string{
"/project": "github.com/example/app",
}

typeEngine := resolution.NewGoTypeInferenceEngine(registry)

indexGoFunctions(codeGraph, callGraph, registry, typeEngine)

// Every indexed function should have an eager scope.
for fqn := range callGraph.Functions {
scope := typeEngine.GetScope(fqn)
assert.NotNil(t, scope, "scope should be eagerly created for %s", fqn)
}

// Non-function node must not appear in Functions map.
for fqn := range callGraph.Functions {
assert.NotContains(t, fqn, "identifier")
}
}

// TestIndexGoFunctions_EagerScope_NilTypeEngine verifies that passing nil for
// typeEngine does not panic and still indexes functions normally.
func TestIndexGoFunctions_EagerScope_NilTypeEngine(t *testing.T) {
codeGraph := &graph.CodeGraph{
Nodes: map[string]*graph.Node{
"fn1": {
ID: "fn1",
Type: "function_declaration",
Name: "Run",
File: "/project/main.go",
},
},
}

callGraph := &core.CallGraph{
Functions: make(map[string]*graph.Node),
}

registry := core.NewGoModuleRegistry()
registry.ModulePath = "github.com/example/app"
registry.DirToImport = map[string]string{
"/project": "github.com/example/app",
}

// Must not panic with nil typeEngine.
assert.NotPanics(t, func() {
indexGoFunctions(codeGraph, callGraph, registry, nil)
})

assert.NotEmpty(t, callGraph.Functions)
}

// TestIndexGoFunctions_EagerScope_NotOverwritten verifies that if a scope already
// exists (e.g., created by a previous Pass 2b run), it is not replaced.
func TestIndexGoFunctions_EagerScope_NotOverwritten(t *testing.T) {
fqn := "github.com/example/app.ExistingFunc"

codeGraph := &graph.CodeGraph{
Nodes: map[string]*graph.Node{
"fn1": {
ID: "fn1",
Type: "function_declaration",
Name: "ExistingFunc",
File: "/project/main.go",
},
},
}

callGraph := &core.CallGraph{
Functions: make(map[string]*graph.Node),
}

registry := core.NewGoModuleRegistry()
registry.ModulePath = "github.com/example/app"
registry.DirToImport = map[string]string{
"/project": "github.com/example/app",
}

typeEngine := resolution.NewGoTypeInferenceEngine(registry)

// Pre-create scope with a binding to simulate Pass 2b having run first.
preScope := resolution.NewGoFunctionScope(fqn)
preScope.AddVariable(&resolution.GoVariableBinding{
VarName: "existing",
Type: &core.TypeInfo{TypeFQN: "builtin.string"},
})
typeEngine.AddScope(preScope)

indexGoFunctions(codeGraph, callGraph, registry, typeEngine)

// The pre-created scope must still have the binding.
scope := typeEngine.GetScope(fqn)
assert.NotNil(t, scope)
bindings, ok := scope.Variables["existing"]
assert.True(t, ok, "pre-existing binding should not be overwritten")
assert.Len(t, bindings, 1)
}
2 changes: 1 addition & 1 deletion sast-engine/graph/callgraph/builder/go_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ func TestIndexGoFunctions(t *testing.T) {
},
}

functionContext := indexGoFunctions(codeGraph, callGraph, registry)
functionContext := indexGoFunctions(codeGraph, callGraph, registry, nil)

// Check expected FQNs
for _, expectedFQN := range tt.expectedFQNs {
Expand Down
Loading
Loading