Skip to content

Commit 81346a6

Browse files
authored
feat(go): package-level var Source 3 + StdlibLoader embed resolution (#643)
## Summary - **Fix F8 (Source 3)**: Add package-level variable type resolution to Pattern 1b in `resolveGoCallTarget`. When Source 1 (function params) and Source 2 (scope bindings) both fail, Source 3 scans `codeGraph.Nodes` for `module_variable` nodes in the same package as the caller. Reads `node.DataType` (e.g. `*sql.DB`), strips pointer prefix, resolves via importMap. Fixes the one PoC failure: `globalDB.Query()` → `database/sql.DB.Query`. Extends `resolveGoCallTarget` signature with `codeGraph *graph.CodeGraph`. - **Fix C (cross-package embeds)**: Add `resolveEmbeddings` method to `GoThirdPartyLocalLoader`. Tries `StdlibLoader.GetType` first (covers all stdlib interfaces: `context.Context`, `sort.Interface`, etc.), falls back to the hardcoded well-known table when StdlibLoader is nil. Add `registry` field to loader; wire via `InitGoThirdPartyLoader`. Called in `getOrLoadPackage` after tree-sitter extraction. ## Test plan - [ ] `TestSource3_PackageLevelVariable` — `var globalDB *sql.DB` resolves `globalDB.Query` → `database/sql.DB.Query` - [ ] `TestSource3_PointerType` — leading `*` stripped from DataType - [ ] `TestSource3_SamePackageFilter` — variable in different package is NOT resolved - [ ] `TestSource3_NoTypeAnnotation` — empty DataType skipped gracefully - [ ] `TestSource3_NilCodeGraph` — no panic with nil codeGraph - [ ] `TestResolveEmbeddings_ViaStdlibLoader` — `context.Context` methods flattened via StdlibLoader - [ ] `TestResolveEmbeddings_FallbackToWellKnown` — `io.Closer.Close` resolves without StdlibLoader - [ ] `TestResolveEmbeddings_NilRegistryStdlibLoader` — no panic, fallback fires - [ ] `TestResolveEmbeddings_DoesNotOverwriteExistingMethods` — existing methods preserved - [ ] `TestResolveEmbeddings_SamePackageEmbedSkipped` — same-package embeds ignored - [ ] Full suite: `go test ./...` — 29/29 packages pass - [ ] Lint: `golangci-lint run` — 0 issues 🤖 Generated with [Claude Code](https://claude.ai/claude-code)
1 parent 4eb8da7 commit 81346a6

File tree

9 files changed

+463
-41
lines changed

9 files changed

+463
-41
lines changed

sast-engine/graph/callgraph/builder/go_builder.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ func BuildGoCallGraph(codeGraph *graph.CodeGraph, registry *core.GoModuleRegistr
155155
importMap = core.NewGoImportMap(callSite.CallerFile)
156156
}
157157

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

160160
if resolved {
161161
resolvedCount++
@@ -438,6 +438,7 @@ func resolveGoCallTarget(
438438
functionContext map[string][]*graph.Node,
439439
typeEngine *resolution.GoTypeInferenceEngine,
440440
callGraph *core.CallGraph,
441+
codeGraph *graph.CodeGraph,
441442
) (string, bool, bool) {
442443
// Pattern 1a: Qualified call (pkg.Func or obj.Method)
443444
if callSite.ObjectName != "" {
@@ -491,6 +492,27 @@ func resolveGoCallTarget(
491492
}
492493
}
493494

495+
// Source 3: Package-level variable types from CodeGraph nodes.
496+
// Covers `var globalDB *sql.DB` at package scope — not tracked by
497+
// GoTypeInferenceEngine (which only processes := / = assignments in
498+
// function bodies). Only fires when Source 1 and Source 2 both fail.
499+
if typeFQN == "" && codeGraph != nil {
500+
for _, node := range codeGraph.Nodes {
501+
if node.Type != "module_variable" || node.DataType == "" {
502+
continue
503+
}
504+
if node.Name != callSite.ObjectName {
505+
continue
506+
}
507+
if !isSameGoPackage(callSite.CallerFile, node.File) {
508+
continue
509+
}
510+
typeStr := strings.TrimPrefix(node.DataType, "*")
511+
typeFQN = resolveGoTypeFQN(typeStr, importMap)
512+
break
513+
}
514+
}
515+
494516
if typeFQN != "" {
495517
methodFQN := typeFQN + "." + callSite.FunctionName
496518

sast-engine/graph/callgraph/builder/go_builder_approach_c_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ func TestApproachC_ThirdPartyPartialResolution(t *testing.T) {
4141
}
4242

4343
targetFQN, resolved, _ := resolveGoCallTarget(
44-
callSite, importMap, goRegistry, nil, typeEngine, callGraph,
44+
callSite, importMap, goRegistry, nil, typeEngine, callGraph, nil,
4545
)
4646

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

8484
targetFQN, resolved, isStdlib := resolveGoCallTarget(
85-
callSite, importMap, goRegistry, nil, typeEngine, callGraph,
85+
callSite, importMap, goRegistry, nil, typeEngine, callGraph, nil,
8686
)
8787

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

120120
targetFQN, resolved, _ := resolveGoCallTarget(
121-
callSite, importMap, goRegistry, nil, typeEngine, callGraph,
121+
callSite, importMap, goRegistry, nil, typeEngine, callGraph, nil,
122122
)
123123

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

185185
// No typeEngine → Pattern 1b skipped → unresolved
186186
targetFQN, resolved, _ := resolveGoCallTarget(
187-
callSite, importMap, goRegistry, nil, nil, callGraph,
187+
callSite, importMap, goRegistry, nil, nil, callGraph, nil,
188188
)
189189

190190
assert.Equal(t, "", targetFQN)
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package builder
2+
3+
import (
4+
"testing"
5+
6+
"github.com/shivasurya/code-pathfinder/sast-engine/graph"
7+
"github.com/shivasurya/code-pathfinder/sast-engine/graph/callgraph/core"
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
// makePackageVarCodeGraph builds a CodeGraph containing a module_variable node
12+
// that represents `var <varName> <dataType>` in the given file.
13+
func makePackageVarCodeGraph(varName, dataType, file string) *graph.CodeGraph {
14+
cg := graph.NewCodeGraph()
15+
cg.Nodes[varName] = &graph.Node{
16+
ID: varName,
17+
Type: "module_variable",
18+
Name: varName,
19+
DataType: dataType,
20+
File: file,
21+
Language: "go",
22+
}
23+
return cg
24+
}
25+
26+
// TestSource3_PackageLevelVariable verifies that Source 3 resolves the type of a
27+
// package-level variable and returns the correct method FQN.
28+
func TestSource3_PackageLevelVariable(t *testing.T) {
29+
cg := makePackageVarCodeGraph("globalDB", "sql.DB", "/project/main.go")
30+
31+
callSite := &CallSiteInternal{
32+
CallerFQN: "main.handler",
33+
CallerFile: "/project/main.go",
34+
ObjectName: "globalDB",
35+
FunctionName: "Query",
36+
}
37+
38+
importMap := &core.GoImportMap{
39+
Imports: map[string]string{"sql": "database/sql"},
40+
}
41+
42+
reg := core.NewGoModuleRegistry()
43+
callGraph := &core.CallGraph{Functions: make(map[string]*graph.Node)}
44+
45+
targetFQN, resolved, _ := resolveGoCallTarget(
46+
callSite, importMap, reg, nil, nil, callGraph, cg,
47+
)
48+
49+
assert.True(t, resolved)
50+
assert.Equal(t, "database/sql.DB.Query", targetFQN)
51+
}
52+
53+
// TestSource3_PointerType verifies that Source 3 strips the leading `*` from the
54+
// DataType field (e.g. `var db *sql.DB` stores DataType as "*sql.DB").
55+
func TestSource3_PointerType(t *testing.T) {
56+
cg := makePackageVarCodeGraph("db", "*sql.DB", "/project/store.go")
57+
58+
callSite := &CallSiteInternal{
59+
CallerFQN: "main.runQuery",
60+
CallerFile: "/project/store.go",
61+
ObjectName: "db",
62+
FunctionName: "Exec",
63+
}
64+
65+
importMap := &core.GoImportMap{
66+
Imports: map[string]string{"sql": "database/sql"},
67+
}
68+
69+
reg := core.NewGoModuleRegistry()
70+
callGraph := &core.CallGraph{Functions: make(map[string]*graph.Node)}
71+
72+
targetFQN, resolved, _ := resolveGoCallTarget(
73+
callSite, importMap, reg, nil, nil, callGraph, cg,
74+
)
75+
76+
assert.True(t, resolved)
77+
assert.Equal(t, "database/sql.DB.Exec", targetFQN)
78+
}
79+
80+
// TestSource3_SamePackageFilter verifies that Source 3 only resolves variables
81+
// defined in the same package as the caller (same directory).
82+
func TestSource3_SamePackageFilter(t *testing.T) {
83+
cg := graph.NewCodeGraph()
84+
// Variable in a DIFFERENT package (/project/other/db.go)
85+
cg.Nodes["otherDB"] = &graph.Node{
86+
ID: "otherDB",
87+
Type: "module_variable",
88+
Name: "globalDB",
89+
DataType: "sql.DB",
90+
File: "/project/other/db.go",
91+
Language: "go",
92+
}
93+
94+
callSite := &CallSiteInternal{
95+
CallerFQN: "main.handler",
96+
CallerFile: "/project/main.go", // different directory
97+
ObjectName: "globalDB",
98+
FunctionName: "Query",
99+
}
100+
101+
importMap := &core.GoImportMap{Imports: map[string]string{"sql": "database/sql"}}
102+
reg := core.NewGoModuleRegistry()
103+
callGraph := &core.CallGraph{Functions: make(map[string]*graph.Node)}
104+
105+
_, resolved, _ := resolveGoCallTarget(
106+
callSite, importMap, reg, nil, nil, callGraph, cg,
107+
)
108+
109+
// Must NOT resolve: variable is in a different package directory.
110+
assert.False(t, resolved)
111+
}
112+
113+
// TestSource3_NoTypeAnnotation verifies that Source 3 gracefully skips a
114+
// module_variable node whose DataType is empty (e.g. `var db = sql.Open(...)`).
115+
func TestSource3_NoTypeAnnotation(t *testing.T) {
116+
cg := makePackageVarCodeGraph("db", "", "/project/main.go") // empty DataType
117+
118+
callSite := &CallSiteInternal{
119+
CallerFQN: "main.handler",
120+
CallerFile: "/project/main.go",
121+
ObjectName: "db",
122+
FunctionName: "Query",
123+
}
124+
125+
importMap := &core.GoImportMap{Imports: map[string]string{}}
126+
reg := core.NewGoModuleRegistry()
127+
callGraph := &core.CallGraph{Functions: make(map[string]*graph.Node)}
128+
129+
_, resolved, _ := resolveGoCallTarget(
130+
callSite, importMap, reg, nil, nil, callGraph, cg,
131+
)
132+
133+
// Must NOT resolve: no type info available.
134+
assert.False(t, resolved)
135+
}
136+
137+
// TestSource3_NilCodeGraph verifies that Source 3 does not panic with a nil CodeGraph.
138+
func TestSource3_NilCodeGraph(t *testing.T) {
139+
callSite := &CallSiteInternal{
140+
CallerFQN: "main.handler",
141+
CallerFile: "/project/main.go",
142+
ObjectName: "globalDB",
143+
FunctionName: "Query",
144+
}
145+
146+
importMap := &core.GoImportMap{Imports: map[string]string{}}
147+
reg := core.NewGoModuleRegistry()
148+
callGraph := &core.CallGraph{Functions: make(map[string]*graph.Node)}
149+
150+
assert.NotPanics(t, func() {
151+
resolveGoCallTarget(callSite, importMap, reg, nil, nil, callGraph, nil)
152+
})
153+
}

sast-engine/graph/callgraph/builder/go_builder_stdlib_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func TestResolveGoCallTarget_StdlibImport(t *testing.T) {
7575

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

78-
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil)
78+
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil, nil)
7979

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

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

92-
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil)
92+
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil, nil)
9393

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

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

108-
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil)
108+
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil, nil)
109109

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

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

122-
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil)
122+
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil, nil)
123123

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

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

140-
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil)
140+
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil, nil)
141141

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

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

154-
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil)
154+
targetFQN, resolved, isStdlib := resolveGoCallTarget(cs, importMap, reg, nil, nil, nil, nil)
155155

156156
assert.False(t, resolved)
157157
assert.Empty(t, targetFQN)

sast-engine/graph/callgraph/builder/go_builder_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ func TestResolveGoCallTarget(t *testing.T) {
391391
for _, tt := range tests {
392392
t.Run(tt.name, func(t *testing.T) {
393393
// Pass nil for typeEngine and callGraph (backward compatibility)
394-
targetFQN, resolved, _ := resolveGoCallTarget(tt.callSite, tt.importMap, tt.registry, tt.funcContext, nil, nil)
394+
targetFQN, resolved, _ := resolveGoCallTarget(tt.callSite, tt.importMap, tt.registry, tt.funcContext, nil, nil, nil)
395395

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

@@ -833,6 +833,7 @@ func TestResolveGoCallTarget_VariableMethod(t *testing.T) {
833833
functionContext,
834834
typeEngine,
835835
callGraph,
836+
nil,
836837
)
837838

838839
// Assert

sast-engine/graph/callgraph/builder/go_version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ func InitGoThirdPartyLoader(reg *core.GoModuleRegistry, projectPath string, refr
130130
return
131131
}
132132

133-
loader := registry.NewGoThirdPartyLocalLoader(projectPath, refreshCache, logger)
133+
loader := registry.NewGoThirdPartyLocalLoader(projectPath, refreshCache, logger, reg)
134134
if loader.PackageCount() == 0 {
135135
if logger != nil {
136136
logger.Debug("No Go third-party dependencies found in go.mod")

0 commit comments

Comments
 (0)