fix(go): field-chain calls resolve via validated type inference, never bare-name guessing (#1316)

target.conn.Exec("insert") with `conn *sql.DB` emitted a BARE `Exec`
ref (the receiver chain was dropped for non-identifier receivers), and
exact-match then bound it to the only local `Exec` — an unrelated
interface's method — fabricating an internal dependency (#1276).

Extraction now keeps Go 2-hop selector chains (`base.field.Method`),
and a dedicated matcher resolves them EXCLUSIVELY via two inference
hops: base's type from the enclosing scope (#1108 machinery), field's
declared type from the struct's own declaration lines (comment-
stripped, per-line — chi's "the tree router" doc comment otherwise
donates a phantom type). resolveMethodOnType validates the target.
Package-qualified field types are followed only when the package is
in-module — `handler http.Handler` must not bind a same-named local
decoy. Failure at any hop leaves the ref unresolved: chained Go
receivers never fall through to the bare-name strategies (they were
never emitted before, so no prior recall depends on that path).

chi before/after: node count stable (1,181); 8 correct field-chain
edges gained (mx.tree.FindRoute/InsertRoute/routes, validated,
including the unexported `node` type); the removed edges are the
prior bare-name guesses on external receivers.

Fixes #1276

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-16 15:49:24 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent 2ec877b08c
commit 41c2029798
4 changed files with 262 additions and 0 deletions
+145
View File
@@ -2541,6 +2541,151 @@ func main() {
});
});
describe('Go field-chain receiver calls (#1276)', () => {
// `target.conn.Exec(...)` where `conn *sql.DB` used to emit a BARE `Exec`
// ref, which exact-matched the only local `Exec` — an unrelated
// interface's method — fabricating an internal dependency. Chained Go
// receivers now resolve exclusively via validated field-hop inference:
// external field types produce NO edge; in-project ones produce the
// correct edge (new recall).
it('external receiver types produce no edge; in-project field chains resolve correctly', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1276-'));
try {
fs.writeFileSync(path.join(tmpDir, 'go.mod'), 'module example.com/app\n\ngo 1.22\n');
fs.mkdirSync(path.join(tmpDir, 'flow'));
fs.writeFileSync(
path.join(tmpDir, 'flow', 'flow.go'),
`package flow
import "database/sql"
type InternalStore interface {
Exec(string, ...any) (sql.Result, error)
QueryRow(string, ...any) *sql.Row
}
type Target struct{ conn *sql.DB }
func (target *Target) Write() error {
_, err := target.conn.Exec("insert")
return err
}
func (target *Target) Read() *sql.Row {
return target.conn.QueryRow("select")
}
type Store struct{}
func (s *Store) Put(key string) {}
type Repo struct{ db *Store }
func (r *Repo) Save() {
r.db.Put("k")
}
`
);
const cg = CodeGraph.initSync(tmpDir);
await cg.indexAll();
// The unrelated local interface's methods have NO callers — the
// external sql.DB calls must not bind to them.
const execDecl = (await cg.searchNodes('Exec', { limit: 10 })).find(
(r) => r.node.kind === 'method'
);
if (execDecl) {
const execCallers = await cg.getCallers(execDecl.node.id);
expect(execCallers.map((c) => c.node.name)).not.toContain('Write');
}
const qrDecl = (await cg.searchNodes('QueryRow', { limit: 10 })).find(
(r) => r.node.kind === 'method'
);
if (qrDecl) {
const qrCallers = await cg.getCallers(qrDecl.node.id);
expect(qrCallers.map((c) => c.node.name)).not.toContain('Read');
}
// The in-project field chain resolves (validated), gaining an edge the
// bare-name era never produced.
const put = (await cg.searchNodes('Put', { limit: 10 })).find(
(r) => r.node.kind === 'method' && r.node.qualifiedName?.includes('Store')
);
expect(put).toBeDefined();
const putCallers = await cg.getCallers(put!.node.id);
expect(putCallers.map((c) => c.node.name)).toContain('Save');
cg.close();
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}, 30000);
it('unexported field types resolve; stdlib-qualified types never bind a same-named local decoy', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1276b-'));
try {
fs.writeFileSync(path.join(tmpDir, 'go.mod'), 'module example.com/b\n\ngo 1.22\n');
fs.writeFileSync(
path.join(tmpDir, 'm.go'),
`package m
import "net/http"
type node struct{}
func (n *node) InsertRoute(path string) {}
// A local type sharing the stdlib interface's name — the decoy the
// package-qualifier gate exists for.
type Handler func()
func (h Handler) ServeHTTP() {}
type Mux struct {
// the tree router lives below this comment (the comment must not
// donate a field type)
handler http.Handler
tree *node
}
func (mx *Mux) handle(path string) {
mx.tree.InsertRoute(path)
}
func (mx *Mux) dispatch() {
mx.handler.ServeHTTP(nil, nil)
}
`
);
const cg = CodeGraph.initSync(tmpDir);
await cg.indexAll();
// Unexported in-package field type: chain resolves (chi's mx.tree shape),
// and the doc comment above the field donates nothing.
const insert = (await cg.searchNodes('InsertRoute', { limit: 5 })).find(
(r) => r.node.kind === 'method'
);
expect(insert).toBeDefined();
const insertCallers = await cg.getCallers(insert!.node.id);
expect(insertCallers.map((c) => c.node.name)).toContain('handle');
// `handler http.Handler` is stdlib — the call must NOT bind to the
// local decoy `Handler.ServeHTTP`.
const serve = (await cg.searchNodes('ServeHTTP', { limit: 5 })).find(
(r) => r.node.kind === 'method'
);
if (serve) {
const serveCallers = await cg.getCallers(serve.node.id);
expect(serveCallers.map((c) => c.node.name)).not.toContain('dispatch');
}
cg.close();
} finally {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
}, 30000);
});
describe('Imported singleton instance-method calls (#1292)', () => {
// `reproStore.notifyJoinGuildStatus()` after `import { reproStore }` used
// to emit its calls edge to the CONSTANT (resolvedBy:'import'), while the