Merge pull request #1913 from ilyabrykau-orca/fix/go-method-receiver-qn

fix(extract): receiver-qualify Go method QNs
This commit is contained in:
Martin Vogel
2026-10-20 04:16:17 +02:00
committed by GitHub
20 changed files with 770 additions and 122 deletions
+13 -10
View File
@@ -32,10 +32,11 @@
* fields of its own struct declaration and the methods of its own interface
* declaration: nothing promoted through embedding.
*
* Binding. The graph keeps one node per qualified name, Go method nodes have
* a FLAT name (<dir>.<Method>), and <dir> is the directory, not the package
* clause: functions, methods of every receiver, types and values of one name,
* in every file of the directory, share one node, and the node is ONE of those
* Binding. The graph keeps one node per qualified name, and <dir> is the
* directory, not the package clause. Go method nodes are receiver-qualified
* (<dir>.<Recv>.<Method>, #1913) and are found through their receiver only.
* Functions, types and values of one name, in every file of the directory,
* share one node <dir>.<name>, and the node is ONE of those
* declarations (cbm_gbuf_upsert_node keeps the smallest file path, then the
* last line). So a reference binds only when the node IS the entity it names:
* the node's file is a file of the named package, the declaration that keeps
@@ -59,7 +60,7 @@
* not), missing (a repository package that does not declare the name; an
* import path inside a module without a package there), graph_gap (declared,
* but the node is another declaration's or absent: block variables, later
* names of a declaration, embedded fields, methods that lost the flat name;
* names of a declaration, embedded fields;
* also an undeclared name in a package with a parse error), test_only_target
* (product code naming a declaration that only test files have), ambiguous
* (several modules or packages fit; a name declared as two kinds; a name two
@@ -1063,7 +1064,7 @@ static void go_record_decl(go_index_t *ix, go_file_t *file, char **f, int nf) {
FIELD_FIELDS = 4,
IM_FIELDS = 3
};
enum { F_OWNER = 2, T_ALIAS = 3, T_OWNER = 4, V_OWNER = 2, M_OWNER = 3, FIELD_NODE = 3 };
enum { F_OWNER = 2, T_ALIAS = 3, T_OWNER = 4, V_OWNER = 2, FIELD_NODE = 3 };
char kind = f[0][0];
if (nf < IM_FIELDS || !go_name_ok(f[SKIP_ONE])) {
return;
@@ -1078,10 +1079,12 @@ static void go_record_decl(go_index_t *ix, go_file_t *file, char **f, int nf) {
go_add_sym(ix, file, GO_KIND_VALUE, f[SKIP_ONE], NULL);
go_add_claim(ix, file, GO_KIND_VALUE, f[SKIP_ONE], NULL, f[V_OWNER]);
} else if (kind == 'M' && nf == M_FIELDS && go_name_ok(f[PAIR_LEN])) {
/* #1913: a method is found through its receiver only; it claims no
* flat <dir>.<name> (its record says w `-`), so a type, function or
* value of the same name keeps that node. */
if (strcmp(f[SKIP_ONE], "?") != 0) {
go_add_member(ix, file, GO_MEM_METHOD, f[SKIP_ONE], f[PAIR_LEN], true);
}
go_add_claim(ix, file, GO_KIND_METHOD, f[PAIR_LEN], f[SKIP_ONE], f[M_OWNER]);
} else if (kind == 'f' && nf == FIELD_FIELDS && go_name_ok(f[PAIR_LEN])) {
go_add_member(ix, file, GO_MEM_FIELD, f[SKIP_ONE], f[PAIR_LEN], f[FIELD_NODE][0] == '1');
} else if (kind == 'm' && nf == IM_FIELDS && go_name_ok(f[PAIR_LEN])) {
@@ -1342,9 +1345,9 @@ static const cbm_gbuf_node_t *go_member_node(const go_ctx_t *c, const go_pkg_t *
return (fm && (fm->node_kinds & kind)) ? n : NULL;
}
/* The node of the method `recv.name`: its receiver-qualified node, else the
* flat node <dir>.<name> when the method is the one that kept it (the last
* declaration of that name in the node's file). */
/* The node of the method `recv.name`: its receiver-qualified node. The flat
* fallback below (the method that kept <dir>.<name>) is unreachable since
* #1913: no method record claims a flat name any more. */
static const cbm_gbuf_node_t *go_method_node(const go_ctx_t *c, const go_pkg_t *pkg, go_str_t recv,
go_str_t name) {
const cbm_gbuf_node_t *qualified = go_member_node(c, pkg, GO_MEM_METHOD, recv, name);
+4 -3
View File
@@ -328,9 +328,10 @@ static void qn_candidate(const pdfr_index_t *x, const cbm_gbuf_node_t *n, const
const char *rel = rel_qn(x, n->qualified_name);
char alt[PDFR_KEY_CAP];
bool owned = owner_name(x, n, alt, sizeof(alt));
/* A member whose QN leaves its owner out (a Go method's has no receiver)
* is named through the owner: `time.Now` is package time's function,
* never the method Now of a type that package time declares. */
/* A member whose QN leaves its owner out (a templ method's keeps the flat
* package QN; a Go method's did before index format 4) is named through
* the owner: `time.Now` is package time's function, never the method Now
* of a type that package time declares. */
bool ownerless_qn = owned && !comp_suffix(rel, alt);
bool via_rel = !ownerless_qn && comp_suffix(rel, s);
bool via_alt = !via_rel && owned && strcmp(alt, rel) != 0 && comp_suffix(alt, s);
+9 -1
View File
@@ -1116,7 +1116,13 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
* language gated on only one resolver produces an edge on the sequential
* path and not the parallel one (or vice versa), breaking MT determinism.
* ArkTS belongs to the JS/TS family here (#1842); dropping it would
* reintroduce the #592/#606 false-edge class for .ets files. */
* reintroduce the #592/#606 false-edge class for .ets files.
*
* Go (#1906) rides the same deferred-drop plumbing through its OWN
* predicate: its drop-list differs (field_type_hint is receiver-aware for
* Go, and unique_name drops only when import-unreachability-penalized), so
* it composes via cbm_go_suppress_weak_method_match instead of widening
* the shared gate. Same lockstep rule: mirror pass_parallel.c. */
bool suppress_weak_member = lang == CBM_LANG_PYTHON || lang == CBM_LANG_JAVASCRIPT ||
lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
lang == CBM_LANG_ARKTS ||
@@ -1144,6 +1150,8 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
res.strategy)) ||
cbm_suppress_weak_local_binding_call(suppress_weak_local_binding,
call->callee_is_locally_bound, res.strategy) ||
cbm_go_suppress_weak_method_match(lang == CBM_LANG_GO, call->is_method, res.strategy,
res.confidence) ||
/* Import-binding suppression (#2127): an external `from m import f`
* binds `f` for the whole module, so a weak short-name match onto a
* project `X.f` outside m is fabricated. Python-only (the import
+14 -2
View File
@@ -3348,7 +3348,15 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
}
_rc_t0 = extract_now_ns();
try_field_type_hint(rc, &res, call->callee_name, source_node->id);
/* Not for Go (F-G9): the hint matches the capitalized receiver
* VARIABLE name as a substring of a candidate QN (`t.Run` -> any
* `...T...Run`), with no type evidence. The sequential resolver has no
* such step, so for Go it produced parallel-only edges (par/seq CALLS
* diverged), and the Go selector guard could not see them as the weak
* pick they are. SCIP precision of Go field_type_hint: 0.016. */
if (lang != CBM_LANG_GO) {
try_field_type_hint(rc, &res, call->callee_name, source_node->id);
}
/* Cross-language veto — the same predicate as pass_calls.c, applied
* AFTER the field-type hint so the hint may still re-pick a compatible
* candidate first, and BEFORE the empty-resolution fallbacks below so a
@@ -3385,7 +3393,9 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
* #606 direction.
*
* This language set MUST match the one in pass_calls.c exactly — see the
* note there. ArkTS belongs to the JS/TS family (#1842). */
* note there. ArkTS belongs to the JS/TS family (#1842). Go (#1906)
* composes via its own predicate (different drop-list — see
* cbm_go_suppress_weak_method_match), mirrored in pass_calls.c. */
bool suppress_weak_member = lang == CBM_LANG_PYTHON || lang == CBM_LANG_JAVASCRIPT ||
lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
lang == CBM_LANG_ARKTS ||
@@ -3403,6 +3413,8 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
call->callee_name, res.strategy)) ||
cbm_suppress_weak_local_binding_call(suppress_weak_local_binding,
call->callee_is_locally_bound, res.strategy) ||
cbm_go_suppress_weak_method_match(lang == CBM_LANG_GO, call->is_method, res.strategy,
res.confidence) ||
/* Import-binding suppression (#2127) — see pass_calls.c; this gate
* MUST stay identical to the one there. */
(lang == CBM_LANG_PYTHON &&
+3 -3
View File
@@ -266,9 +266,9 @@ static int check_go_class_implements(cbm_pipeline_ctx_t *ctx, const cbm_gbuf_nod
}
/* Resolve the struct's methods two ways and use whichever finds them:
* (a) the struct's DEFINES_METHOD edges, matched by method NAME — the real
* pipeline path, where Go receiver methods carry a flat QN (e.g.
* "pkg.Area" rather than "pkg.Circle.Area"), so QN-string
* reconstruction would miss; and
* pipeline path. Go method QNs are receiver-qualified since #1913
* ("pkg.Circle.Area"), so (b) matches them as well, but the edges
* stay the primary join; and
* (b) the reconstructed QN "<ClassQN>.<methodName>" — used by tests and any
* extractor that does emit class-qualified method QNs without
* DEFINES_METHOD edges from the class. */
+9
View File
@@ -412,6 +412,15 @@ bool cbm_weak_member_unique_name_exempt(bool is_python, bool receiver_is_self_at
bool cbm_suppress_weak_local_binding_call(bool enabled, bool callee_is_locally_bound,
const char *strategy);
/* Go analog of the TS/JS guard, same failure class: a selector call whose
* receiver the Go LSP could not type must not be bound by a receiver-blind
* short-name strategy. Drops suffix_match / fuzzy always, and unique_name only
* when its confidence is import-unreachability-penalized (the stdlib/vendor
* hijack shape). field_type_hint is deliberately NOT dropped for Go — struct
* fields carry declared types, so the hint is receiver-aware there. */
bool cbm_go_suppress_weak_method_match(bool is_go, bool is_method, const char *strategy,
double confidence);
/* Import-binding counterpart (#2127). True when the callee's root identifier
* is bound by an import of this file whose module chain contradicts the
* resolved target (`from unittest.mock import patch; patch()` must not bind a
+29
View File
@@ -750,6 +750,35 @@ bool cbm_suppress_weak_local_binding_call(bool enabled, bool callee_is_locally_b
return weak_short_name_strategy(strategy);
}
bool cbm_go_suppress_weak_method_match(bool is_go, bool is_method, const char *strategy,
double confidence) {
/* Go analog of the TS/JS guard above, same failure class: a selector call
* whose receiver the Go LSP could not type reaches the registry and a bare
* short-name strategy binds it to an arbitrary same-named project symbol
* (`f.Close()` on an os.File -> a project `Close`, suffix_match over 15
* candidates). Built on the shared drop-list so "weak" means one thing
* for every guard; Go narrows it in one place only (unique_name below).
* field_type_hint stays weak: it is a receiver-NAME substring match, not
* a declared-type lookup (the parallel resolver no longer produces it for
* Go; a typed field call such as lrp_go_s8's `s.repo.Find()` resolves
* through the Go LSP). */
if (!is_go || !is_method || !weak_short_name_strategy(strategy)) {
return false;
}
if (strcmp(strategy, "unique_name") != 0) {
return true; /* suffix_match, field_type_hint, fuzzy */
}
/* unique_name is dropped only when PENALIZED: resolve_name_lookup scales
* CONF_UNIQUE_NAME by DEFAULT_CONFIDENCE exactly when the lone candidate
* is not reachable through the caller's imports — the stdlib/vendor
* hijack shape (`io.Copy` -> a project `Copy`). An unpenalized
* unique_name target sits inside the caller's import closure (or the
* file has no imports, e.g. a same-package call) and must be kept —
* dropping it kills genuinely-typed lone-candidate calls that never
* enter the field-type-hint upgrade (candidate_count == 1). */
return confidence < CONF_UNIQUE_NAME;
}
/* Import-binding counterpart of the parameter guard above (#2127). An import
* binds the identifier for the whole module; when that import is external it
* never materializes, and the chain falls through to project-wide short-name
+4 -2
View File
@@ -32,8 +32,10 @@ typedef struct cbm_store_read_scope cbm_store_read_scope_t;
* Dockerfile.<variant> keeps its suffix in its QNs.
* 3 Image identity invents no registry: a reference without a host is
* __image__<repository as written>, registry "unknown" -- no longer
* __image__docker.io/[library/]<repository>. */
#define CBM_INDEX_FORMAT_VERSION 3
* __image__docker.io/[library/]<repository>.
* 4 #1913: Go method QNs are receiver-qualified (pkg.Recv.method, generic
* receivers unwrapped); free functions stay pkg.func. */
#define CBM_INDEX_FORMAT_VERSION 4
#define CBM_STORE_CANCELLED (-3)
#define CBM_STORE_SCAN_LIMIT (-4)
#define CBM_STORE_CALLBACK_ERR (-5)