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
24 changes: 23 additions & 1 deletion sast-engine/graph/callgraph/builder/go_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ func BuildGoCallGraph(codeGraph *graph.CodeGraph, registry *core.GoModuleRegistr
importMap = core.NewGoImportMap(callSite.CallerFile)
}

targetFQN, resolved, isStdlib := resolveGoCallTarget(callSite, importMap, registry, functionContext, typeEngine, callGraph)
targetFQN, resolved, isStdlib := resolveGoCallTarget(callSite, importMap, registry, functionContext, typeEngine, callGraph, codeGraph)

if resolved {
resolvedCount++
Expand Down Expand Up @@ -438,6 +438,7 @@ func resolveGoCallTarget(
functionContext map[string][]*graph.Node,
typeEngine *resolution.GoTypeInferenceEngine,
callGraph *core.CallGraph,
codeGraph *graph.CodeGraph,
) (string, bool, bool) {
// Pattern 1a: Qualified call (pkg.Func or obj.Method)
if callSite.ObjectName != "" {
Expand Down Expand Up @@ -491,6 +492,27 @@ func resolveGoCallTarget(
}
}

// Source 3: Package-level variable types from CodeGraph nodes.
// Covers `var globalDB *sql.DB` at package scope — not tracked by
// GoTypeInferenceEngine (which only processes := / = assignments in
// function bodies). Only fires when Source 1 and Source 2 both fail.
if typeFQN == "" && codeGraph != nil {
for _, node := range codeGraph.Nodes {
if node.Type != "module_variable" || node.DataType == "" {
continue
}
if node.Name != callSite.ObjectName {
continue
}
if !isSameGoPackage(callSite.CallerFile, node.File) {
continue
}
typeStr := strings.TrimPrefix(node.DataType, "*")
typeFQN = resolveGoTypeFQN(typeStr, importMap)
break
}
}

if typeFQN != "" {
methodFQN := typeFQN + "." + callSite.FunctionName

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestApproachC_ThirdPartyPartialResolution(t *testing.T) {
}

targetFQN, resolved, _ := resolveGoCallTarget(
callSite, importMap, goRegistry, nil, typeEngine, callGraph,
callSite, importMap, goRegistry, nil, typeEngine, callGraph, nil,
)

assert.Equal(t, "github.com/redis/go-redis/v9.Client.Get", targetFQN)
Expand Down Expand Up @@ -82,7 +82,7 @@ func TestApproachC_UserCodeMethodResolution(t *testing.T) {
}

targetFQN, resolved, isStdlib := resolveGoCallTarget(
callSite, importMap, goRegistry, nil, typeEngine, callGraph,
callSite, importMap, goRegistry, nil, typeEngine, callGraph, nil,
)

assert.Equal(t, "testapp.Service.Handle", targetFQN)
Expand Down Expand Up @@ -118,7 +118,7 @@ func TestApproachC_PointerTypeStripping(t *testing.T) {
}

targetFQN, resolved, _ := resolveGoCallTarget(
callSite, importMap, goRegistry, nil, typeEngine, callGraph,
callSite, importMap, goRegistry, nil, typeEngine, callGraph, nil,
)

// Pointer * should be stripped: *database/sql.DB → database/sql.DB
Expand Down Expand Up @@ -184,7 +184,7 @@ func TestApproachC_NoTypeEngine(t *testing.T) {

// No typeEngine → Pattern 1b skipped → unresolved
targetFQN, resolved, _ := resolveGoCallTarget(
callSite, importMap, goRegistry, nil, nil, callGraph,
callSite, importMap, goRegistry, nil, nil, callGraph, nil,
)

assert.Equal(t, "", targetFQN)
Expand Down
153 changes: 153 additions & 0 deletions sast-engine/graph/callgraph/builder/go_builder_pkgvar_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package builder

import (
"testing"

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

// makePackageVarCodeGraph builds a CodeGraph containing a module_variable node
// that represents `var <varName> <dataType>` in the given file.
func makePackageVarCodeGraph(varName, dataType, file string) *graph.CodeGraph {
cg := graph.NewCodeGraph()
cg.Nodes[varName] = &graph.Node{
ID: varName,
Type: "module_variable",
Name: varName,
DataType: dataType,
File: file,
Language: "go",
}
return cg
}

// TestSource3_PackageLevelVariable verifies that Source 3 resolves the type of a
// package-level variable and returns the correct method FQN.
func TestSource3_PackageLevelVariable(t *testing.T) {
cg := makePackageVarCodeGraph("globalDB", "sql.DB", "/project/main.go")

callSite := &CallSiteInternal{
CallerFQN: "main.handler",
CallerFile: "/project/main.go",
ObjectName: "globalDB",
FunctionName: "Query",
}

importMap := &core.GoImportMap{
Imports: map[string]string{"sql": "database/sql"},
}

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

targetFQN, resolved, _ := resolveGoCallTarget(
callSite, importMap, reg, nil, nil, callGraph, cg,
)

assert.True(t, resolved)
assert.Equal(t, "database/sql.DB.Query", targetFQN)
}

// TestSource3_PointerType verifies that Source 3 strips the leading `*` from the
// DataType field (e.g. `var db *sql.DB` stores DataType as "*sql.DB").
func TestSource3_PointerType(t *testing.T) {
cg := makePackageVarCodeGraph("db", "*sql.DB", "/project/store.go")

callSite := &CallSiteInternal{
CallerFQN: "main.runQuery",
CallerFile: "/project/store.go",
ObjectName: "db",
FunctionName: "Exec",
}

importMap := &core.GoImportMap{
Imports: map[string]string{"sql": "database/sql"},
}

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

targetFQN, resolved, _ := resolveGoCallTarget(
callSite, importMap, reg, nil, nil, callGraph, cg,
)

assert.True(t, resolved)
assert.Equal(t, "database/sql.DB.Exec", targetFQN)
}

// TestSource3_SamePackageFilter verifies that Source 3 only resolves variables
// defined in the same package as the caller (same directory).
func TestSource3_SamePackageFilter(t *testing.T) {
cg := graph.NewCodeGraph()
// Variable in a DIFFERENT package (/project/other/db.go)
cg.Nodes["otherDB"] = &graph.Node{
ID: "otherDB",
Type: "module_variable",
Name: "globalDB",
DataType: "sql.DB",
File: "/project/other/db.go",
Language: "go",
}

callSite := &CallSiteInternal{
CallerFQN: "main.handler",
CallerFile: "/project/main.go", // different directory
ObjectName: "globalDB",
FunctionName: "Query",
}

importMap := &core.GoImportMap{Imports: map[string]string{"sql": "database/sql"}}
reg := core.NewGoModuleRegistry()
callGraph := &core.CallGraph{Functions: make(map[string]*graph.Node)}

_, resolved, _ := resolveGoCallTarget(
callSite, importMap, reg, nil, nil, callGraph, cg,
)

// Must NOT resolve: variable is in a different package directory.
assert.False(t, resolved)
}

// TestSource3_NoTypeAnnotation verifies that Source 3 gracefully skips a
// module_variable node whose DataType is empty (e.g. `var db = sql.Open(...)`).
func TestSource3_NoTypeAnnotation(t *testing.T) {
cg := makePackageVarCodeGraph("db", "", "/project/main.go") // empty DataType

callSite := &CallSiteInternal{
CallerFQN: "main.handler",
CallerFile: "/project/main.go",
ObjectName: "db",
FunctionName: "Query",
}

importMap := &core.GoImportMap{Imports: map[string]string{}}
reg := core.NewGoModuleRegistry()
callGraph := &core.CallGraph{Functions: make(map[string]*graph.Node)}

_, resolved, _ := resolveGoCallTarget(
callSite, importMap, reg, nil, nil, callGraph, cg,
)

// Must NOT resolve: no type info available.
assert.False(t, resolved)
}

// TestSource3_NilCodeGraph verifies that Source 3 does not panic with a nil CodeGraph.
func TestSource3_NilCodeGraph(t *testing.T) {
callSite := &CallSiteInternal{
CallerFQN: "main.handler",
CallerFile: "/project/main.go",
ObjectName: "globalDB",
FunctionName: "Query",
}

importMap := &core.GoImportMap{Imports: map[string]string{}}
reg := core.NewGoModuleRegistry()
callGraph := &core.CallGraph{Functions: make(map[string]*graph.Node)}

assert.NotPanics(t, func() {
resolveGoCallTarget(callSite, importMap, reg, nil, nil, callGraph, nil)
})
}
12 changes: 6 additions & 6 deletions sast-engine/graph/callgraph/builder/go_builder_stdlib_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func TestResolveGoCallTarget_StdlibImport(t *testing.T) {

cs := &CallSiteInternal{FunctionName: "Println", ObjectName: "fmt"}

targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil)
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil, nil)

require.True(t, resolved)
assert.Equal(t, "fmt.Println", targetFQN)
Expand All @@ -89,7 +89,7 @@ func TestResolveGoCallTarget_NilStdlibLoader(t *testing.T) {

cs := &CallSiteInternal{FunctionName: "Println", ObjectName: "fmt"}

targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil)
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil, nil)

require.True(t, resolved)
assert.Equal(t, "fmt.Println", targetFQN)
Expand All @@ -105,7 +105,7 @@ func TestResolveGoCallTarget_ThirdPartyImport(t *testing.T) {

cs := &CallSiteInternal{FunctionName: "Default", ObjectName: "gin"}

targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil)
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil, nil)

require.True(t, resolved)
assert.Equal(t, "github.com/gin-gonic/gin.Default", targetFQN)
Expand All @@ -119,7 +119,7 @@ func TestResolveGoCallTarget_StdlibMultiSegmentPath(t *testing.T) {

cs := &CallSiteInternal{FunctionName: "ListenAndServe", ObjectName: "http"}

targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil)
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil, nil)

require.True(t, resolved)
assert.Equal(t, "net/http.ListenAndServe", targetFQN)
Expand All @@ -137,7 +137,7 @@ func TestResolveGoCallTarget_Builtin(t *testing.T) {

cs := &CallSiteInternal{FunctionName: "append", ObjectName: ""}

targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil)
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil, nil)

require.True(t, resolved)
assert.Equal(t, "builtin.append", targetFQN)
Expand All @@ -151,7 +151,7 @@ func TestResolveGoCallTarget_Unresolved(t *testing.T) {

cs := &CallSiteInternal{FunctionName: "Foo", ObjectName: "unknown"}

targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil)
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil, nil)

assert.False(t, resolved)
assert.Empty(t, targetFQN)
Expand Down
3 changes: 2 additions & 1 deletion sast-engine/graph/callgraph/builder/go_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ func TestResolveGoCallTarget(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Pass nil for typeEngine and callGraph (backward compatibility)
targetFQN, resolved, _ := resolveGoCallTarget(tt.callSite, tt.importMap, tt.registry, tt.funcContext, nil, nil)
targetFQN, resolved, _ := resolveGoCallTarget(tt.callSite, tt.importMap, tt.registry, tt.funcContext, nil, nil, nil)

assert.Equal(t, tt.shouldResolve, resolved, "Resolution status mismatch")

Expand Down Expand Up @@ -833,6 +833,7 @@ func TestResolveGoCallTarget_VariableMethod(t *testing.T) {
functionContext,
typeEngine,
callGraph,
nil,
)

// Assert
Expand Down
2 changes: 1 addition & 1 deletion sast-engine/graph/callgraph/builder/go_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func InitGoThirdPartyLoader(reg *core.GoModuleRegistry, projectPath string, refr
return
}

loader := registry.NewGoThirdPartyLocalLoader(projectPath, refreshCache, logger)
loader := registry.NewGoThirdPartyLocalLoader(projectPath, refreshCache, logger, reg)
if loader.PackageCount() == 0 {
if logger != nil {
logger.Debug("No Go third-party dependencies found in go.mod")
Expand Down
Loading
Loading