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
+101
View File
@@ -1554,6 +1554,21 @@ export function matchMethodCall(
}
}
// Go 2-hop field chain `base.field.Method` (#1276): the base's type comes
// from the enclosing scope (typed parameter / method receiver / local var),
// the field's declared type from that struct's own declaration lines, and
// the method is VALIDATED on the field's type by resolveMethodOnType. This
// branch is EXCLUSIVE for chained Go receivers: when the hop can't be
// inferred or the field's type is external (`conn *sql.DB` — no project
// node), the ref stays unresolved rather than falling through to the
// bare-name strategies below, which is exactly how `target.conn.Exec(...)`
// fabricated a dependency on an unrelated local interface's same-named
// method. Chained Go receivers were never emitted before #1276, so there
// is no prior recall to preserve on the fallback path.
if (ref.language === 'go' && dotMatch && objectOrClass!.includes('.')) {
return matchGoFieldChainCall(objectOrClass!, methodName!, ref, context);
}
// Java/Kotlin: receiver may be a field whose name doesn't match the type by
// Java naming convention (`userbo` → class `UserBO`, abbreviated). Look up
// the field in the enclosing class to get its declared type, then resolve
@@ -1717,6 +1732,92 @@ export function matchMethodCall(
return null;
}
/** Go builtin/primitive field types that can never carry a project method. */
const GO_BUILTIN_FIELD_TYPES = new Set([
'string', 'bool', 'byte', 'rune', 'error', 'any',
'int', 'int8', 'int16', 'int32', 'int64',
'uint', 'uint8', 'uint16', 'uint32', 'uint64', 'uintptr',
'float32', 'float64', 'complex64', 'complex128',
'chan', 'map', 'func', 'struct', 'interface',
]);
/**
* Resolve a Go 2-hop field-chain call `base.field.Method(...)` (#1276):
* `target.conn.Exec("insert")` where `func (target *Target) Write()` and
* `type Target struct { conn *sql.DB }`. Two inference hops, both read from
* source the same way #1108 does:
* 1. `base`'s type from the enclosing scope (method receiver, typed
* parameter, or local declaration) via inferLocalReceiverType;
* 2. `field`'s declared type from the struct's own declaration lines.
* The method is then resolved AND VALIDATED on the field's type. A field
* whose type has no project node (`sql.DB`, any external dependency) yields
* null — the caller treats this branch as exclusive for chained Go
* receivers, so the ref stays unresolved instead of name-guessing.
*/
function matchGoFieldChainCall(
receiverChain: string,
methodName: string,
ref: UnresolvedRef,
context: ResolutionContext
): ResolvedRef | null {
const segs = receiverChain.split('.');
if (segs.length !== 2 || !segs[0] || !segs[1]) return null;
const [base, field] = segs;
const baseType = inferLocalReceiverType(base!, ref, context);
if (!baseType) return null;
const fieldEsc = field!.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const fieldTypeRe = new RegExp(`\\b${fieldEsc}\\s+\\*?\\[?\\]?([A-Za-z_][\\w.]*)`);
const structs = preferCallSiteFile(context.getNodesByName(baseType), ref.filePath).filter(
(n) => (n.kind === 'struct' || n.kind === 'class') && n.language === 'go'
);
for (const s of structs) {
const source = context.readFile(s.filePath);
if (!source) continue;
// Only the struct's own declaration lines — a same-named identifier
// elsewhere in the file can't donate a type. Matched LINE BY LINE with
// comments stripped: chi's `Mux` has a doc comment reading "the tree
// router" right above `tree *node`, and a whole-block match captured
// `router` from the prose instead of `node` from the field.
const declLines = source.split('\n').slice(Math.max(0, s.startLine - 1), s.endLine);
for (const rawLine of declLines) {
const line = rawLine.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '');
const m = line.match(fieldTypeRe);
if (!m || !m[1]) continue;
const rawType = m[1];
// A package-qualified field type (`http.Handler`, `sql.DB`) is only
// followed when the package is IN-MODULE: stripping the qualifier and
// matching the bare name would conflate a stdlib/third-party type with
// any same-named project type — on chi, `handler http.Handler` bound
// to an example app's unrelated local `Handler`. That is the exact
// fabrication this matcher exists to prevent (#1276).
if (rawType.includes('.')) {
const pkg = rawType.split('.')[0]!;
const mod = context.getGoModule?.();
const imp = context
.getImportMappings(s.filePath, 'go')
.find((i) => i.localName === pkg);
const inModule =
!!mod &&
!!imp &&
(imp.source === mod.modulePath || imp.source.startsWith(mod.modulePath + '/'));
if (!inModule) continue;
}
// Unexported (lowercase) types are idiomatic Go and stay eligible —
// chi's `mx.tree.FindRoute()` chains through `tree *node`. A
// mis-capture is harmless: resolveMethodOnType only returns a
// validated `<type>::<method>` match.
const fieldType = rawType.split('.').pop();
if (!fieldType || !/^[A-Za-z_]/.test(fieldType) || GO_BUILTIN_FIELD_TYPES.has(fieldType)) continue;
const resolved = resolveMethodOnType(fieldType, methodName, ref, context, 0.85, 'instance-method');
if (resolved) return resolved;
}
}
return null;
}
/**
* Split a camelCase or PascalCase string into words.
*/