Skip to content

Commit 5a4af9c

Browse files
authored
Merge pull request #1913 from ilyabrykau-orca/fix/go-method-receiver-qn
fix(extract): receiver-qualify Go method QNs
2 parents 171f99b + cd16ef8 commit 5a4af9c

20 files changed

Lines changed: 771 additions & 123 deletions

‎internal/cbm/doclink_go.c‎

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@
4343
* drops these lines and so carries no line number
4444
* at all.
4545
* G path go.mod: the module path
46-
* w says who keeps the node. A Go method node has a FLAT qualified name
47-
* (<dir>.<Method>), so a function, the methods of every receiver, a type and
48-
* a value of one name all claim the node <dir>.<name>, and within a file the
49-
* declaration on the LAST line keeps it (cbm_gbuf_upsert_node). w is 1 for
46+
* w says who keeps the node <dir>.<name>. A function, a type and a value of
47+
* one name all claim it, and within a file the declaration on the LAST line
48+
* keeps it (cbm_gbuf_upsert_node). A method claims nothing: its node is
49+
* receiver-qualified (<dir>.<Recv>.<Method>, #1913), and its w is `-`. w is 1 for
5050
* that declaration, 0 for the others of that name, x when two of them start
5151
* on that last line, and - for a value the extractor gives no node at all
5252
* (it gives one to the first name of a constant spec and of a parenthesis-
@@ -1511,9 +1511,11 @@ static void go_scan_method(go_scan_t *s, TSNode m) {
15111511
recv = "?";
15121512
recv_len = SKIP_ONE;
15131513
}
1514-
go_push_claim(s,
1515-
cbm_arena_sprintf(s->tmp, "M\t%.*s\t%.*s", (int)recv_len, recv, (int)len, name),
1516-
name, len, m);
1514+
/* #1913: a Go method node is receiver-qualified (<dir>.<Recv>.<Method>),
1515+
* so a method claims no flat name: w is `-`, and the method is found
1516+
* through its receiver (the member table), never through <dir>.<name>. */
1517+
(void)go_push_rec(
1518+
s, cbm_arena_sprintf(s->tmp, "M\t%.*s\t%.*s\t-", (int)recv_len, recv, (int)len, name));
15171519
}
15181520

15191521
/* The implied name of an embedded field: the type's own name, without

‎internal/cbm/extract_calls.c‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4493,6 +4493,24 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
44934493
}
44944494
}
44954495
}
4496+
// Go receiver-aware guard (same direction as the TS/JS flag above).
4497+
// Flag a selector call x.foo(). The Go AST cannot separate a method
4498+
// call on a value from a package-qualified call — but every selector
4499+
// call the Go LSP or the import/qualified registry strategies CAN
4500+
// place never reaches the weak short-name guards, so the flag only
4501+
// bites on unresolvable receivers (`f.Close()` on an os.File,
4502+
// `sha256.New()` behind an unindexed import), where a project-wide
4503+
// short-name match fabricates an edge to an unrelated project
4504+
// symbol sharing the name. Bare calls (helper()) keep
4505+
// is_method=false and resolve same-module/import paths as before.
4506+
if (ctx->language == CBM_LANG_GO &&
4507+
strcmp(ts_node_type(node), "call_expression") == 0) {
4508+
TSNode gofn = ts_node_child_by_field_name(node, TS_FIELD("function"));
4509+
if (!ts_node_is_null(gofn) &&
4510+
strcmp(ts_node_type(gofn), "selector_expression") == 0) {
4511+
call.is_method = true;
4512+
}
4513+
}
44964514

44974515
TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
44984516
/* tree-sitter-elixir attaches NO field name to a call's arguments

‎internal/cbm/extract_defs.c‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5772,7 +5772,7 @@ static void set_def_complexity(CBMDefinition *def, TSNode body, const CBMLangSpe
57725772
* Walks to the parameter_declaration's `type` field, unwrapping pointer_type
57735773
* and generic_type, and returns the type_identifier text (e.g. "OrderService").
57745774
* Returns NULL if no type_identifier is found. */
5775-
static char *go_receiver_type_name(CBMArena *a, TSNode recv, const char *source) {
5775+
char *cbm_go_receiver_type_name(CBMArena *a, TSNode recv, const char *source) {
57765776
uint32_t nc = ts_node_child_count(recv);
57775777
for (uint32_t i = 0; i < nc; i++) {
57785778
TSNode child = ts_node_child(recv, i);
@@ -7006,12 +7006,27 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
70067006
* (and downstream Go IMPLEMENTS/OVERRIDE) link the method to its owning
70077007
* struct/type node. The parent QN must match the type's node QN, which
70087008
* is computed the same way (cbm_fqn_compute on the type name). */
7009-
char *recv_type = go_receiver_type_name(a, recv, ctx->source);
7009+
char *recv_type = cbm_go_receiver_type_name(a, recv, ctx->source);
70107010
if (recv_type && recv_type[0]) {
70117011
/* Must match the Go type node QN (directory-based module) so the
70127012
* DEFINES_METHOD edge links the method to its owning type. */
70137013
def.parent_class = cbm_fqn_compute_source_lang(a, ctx->project, ctx->rel_path,
70147014
recv_type, ctx->language);
7015+
/* Receiver-qualify the method QN (proj.pkg.Recv.method) — same
7016+
* shape as the C++ out-of-line path below and Go interface
7017+
* members. With the flat proj.pkg.method QN every same-name
7018+
* method in a package collided in the graph upsert: one body
7019+
* survived and the twins' call edges accreted onto it. The
7020+
* call-scope side (compute_func_qn in extract_unified.c) mirrors
7021+
* this formula, and go_lsp consumers read the def QN and
7022+
* parent_class (receiver_type) verbatim, so resolution joins
7023+
* stay exact. Go only: templ's Go-derived grammar reaches this
7024+
* branch too, but its call-scope QN (compute_func_qn) stays flat,
7025+
* and a def QN the calls do not share would orphan every call
7026+
* inside a templ method. */
7027+
if (ctx->language == CBM_LANG_GO && def.parent_class) {
7028+
def.qualified_name = cbm_arena_sprintf(a, "%s.%s", def.parent_class, name);
7029+
}
70157030
}
70167031
}
70177032

‎internal/cbm/extract_unified.c‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,29 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan
11101110
}
11111111
}
11121112

1113+
/* Go method `func (s *Storage) Close() {...}`: the def extractor records
1114+
* this as Method "proj.pkg.Storage.Close" (receiver-qualified, mirroring
1115+
* the C++ out-of-line rule above). The call-scope QN must match — a bare
1116+
* "proj.pkg.Close" names a node that no longer exists, so every call
1117+
* inside the method body would fall back to File-node attribution
1118+
* (calls_find_source). Same ONE-formula contract as the def side:
1119+
* cbm_go_receiver_type_name + cbm_fqn_compute_source_lang. */
1120+
if (ctx->language == CBM_LANG_GO && strcmp(ts_node_type(node), "method_declaration") == 0) {
1121+
TSNode recv = ts_node_child_by_field_name(node, TS_FIELD("receiver"));
1122+
if (!ts_node_is_null(recv)) {
1123+
char *recv_type = cbm_go_receiver_type_name(ctx->arena, recv, ctx->source);
1124+
if (recv_type && recv_type[0]) {
1125+
const char *type_qn = cbm_fqn_compute_source_lang(
1126+
ctx->arena, ctx->project, ctx->rel_path, recv_type, ctx->language);
1127+
/* NULL only on allocation failure: fall through to the flat
1128+
* QN, as the def side does (extract_defs.c). */
1129+
if (type_qn) {
1130+
return cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, name);
1131+
}
1132+
}
1133+
}
1134+
}
1135+
11131136
/* Nix: a binding's own attrpath contributes scope (`a.b.fn = …`), and the def
11141137
* extractor bakes it into the def QN. Compose it identically here — otherwise
11151138
* an in-body call sources to a QN one or more segments short of the def, and

‎internal/cbm/helpers.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang);
173173
// def extractor — drift dropped the class qualifier from in-body calls (#554/#621).
174174
char *cbm_cpp_out_of_line_parent_class(CBMArena *a, TSNode node, const char *source);
175175

176+
/* Go: resolve a method_declaration's receiver parameter_list down to the bare
177+
* receiver type_identifier (unwrapping pointer_type / generic_type). Shared by
178+
* def extraction (extract_defs.c) and call-scope attribution
179+
* (extract_unified.c) so the receiver-qualified method QN has ONE formula. */
180+
char *cbm_go_receiver_type_name(CBMArena *a, TSNode recv, const char *source);
181+
176182
// Find a child node by kind string.
177183
TSNode cbm_find_child_by_kind(TSNode parent, const char *kind);
178184

‎internal/cbm/lsp/go_lsp.c‎

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1987,15 +1987,24 @@ static void process_function(GoLSPContext *ctx, TSNode func_node) {
19871987
if (!func_name || !func_name[0])
19881988
return;
19891989

1990-
// Enclosing-function QN must be the BARE package.Func form (no receiver
1991-
// type segment). The textual call events (extract_unified.c) source calls
1992-
// as package_qn.func_name — methods included — and the defs pass creates
1993-
// the graph Method node under the same QN, so any other form breaks the
1994-
// caller-QN join in cbm_pipeline_find_lsp_resolution and the LSP-resolved
1995-
// call silently falls back to the registry short-name resolver. The
1996-
// receiver type still reaches the registry via the def's parent_class /
1997-
// method->receiver_type; it just does not appear in the caller QN.
1990+
// Enclosing-function QN must be EXACTLY the QN the textual call events
1991+
// (extract_unified.c compute_func_qn) and the defs pass produce, or the
1992+
// caller-QN join in cbm_pipeline_find_lsp_resolution breaks and the
1993+
// LSP-resolved call silently falls back to the registry short-name
1994+
// resolver. Since #1909 receiver-qualified Go method QNs, that shared
1995+
// form is package_qn.ReceiverType.method for methods (the ONE-formula
1996+
// contract: cbm_go_receiver_type_name) and package_qn.func for functions.
19981997
ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->package_qn, func_name);
1998+
if (strcmp(ts_node_type(func_node), "method_declaration") == 0) {
1999+
TSNode recv = ts_node_child_by_field_name(func_node, "receiver", 8);
2000+
if (!ts_node_is_null(recv)) {
2001+
char *recv_type = cbm_go_receiver_type_name(ctx->arena, recv, ctx->source);
2002+
if (recv_type && recv_type[0]) {
2003+
ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s.%s", ctx->package_qn,
2004+
recv_type, func_name);
2005+
}
2006+
}
2007+
}
19992008

20002009
// Push function scope
20012010
CBMScope *saved_scope = ctx->current_scope;

‎src/pipeline/doc_links_go.c‎

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@
3232
* fields of its own struct declaration and the methods of its own interface
3333
* declaration: nothing promoted through embedding.
3434
*
35-
* Binding. The graph keeps one node per qualified name, Go method nodes have
36-
* a FLAT name (<dir>.<Method>), and <dir> is the directory, not the package
37-
* clause: functions, methods of every receiver, types and values of one name,
38-
* in every file of the directory, share one node, and the node is ONE of those
35+
* Binding. The graph keeps one node per qualified name, and <dir> is the
36+
* directory, not the package clause. Go method nodes are receiver-qualified
37+
* (<dir>.<Recv>.<Method>, #1913) and are found through their receiver only.
38+
* Functions, types and values of one name, in every file of the directory,
39+
* share one node <dir>.<name>, and the node is ONE of those
3940
* declarations (cbm_gbuf_upsert_node keeps the smallest file path, then the
4041
* last line). So a reference binds only when the node IS the entity it names:
4142
* the node's file is a file of the named package, the declaration that keeps
@@ -59,7 +60,7 @@
5960
* not), missing (a repository package that does not declare the name; an
6061
* import path inside a module without a package there), graph_gap (declared,
6162
* but the node is another declaration's or absent: block variables, later
62-
* names of a declaration, embedded fields, methods that lost the flat name;
63+
* names of a declaration, embedded fields;
6364
* also an undeclared name in a package with a parse error), test_only_target
6465
* (product code naming a declaration that only test files have), ambiguous
6566
* (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) {
10631064
FIELD_FIELDS = 4,
10641065
IM_FIELDS = 3
10651066
};
1066-
enum { F_OWNER = 2, T_ALIAS = 3, T_OWNER = 4, V_OWNER = 2, M_OWNER = 3, FIELD_NODE = 3 };
1067+
enum { F_OWNER = 2, T_ALIAS = 3, T_OWNER = 4, V_OWNER = 2, FIELD_NODE = 3 };
10671068
char kind = f[0][0];
10681069
if (nf < IM_FIELDS || !go_name_ok(f[SKIP_ONE])) {
10691070
return;
@@ -1078,10 +1079,12 @@ static void go_record_decl(go_index_t *ix, go_file_t *file, char **f, int nf) {
10781079
go_add_sym(ix, file, GO_KIND_VALUE, f[SKIP_ONE], NULL);
10791080
go_add_claim(ix, file, GO_KIND_VALUE, f[SKIP_ONE], NULL, f[V_OWNER]);
10801081
} else if (kind == 'M' && nf == M_FIELDS && go_name_ok(f[PAIR_LEN])) {
1082+
/* #1913: a method is found through its receiver only; it claims no
1083+
* flat <dir>.<name> (its record says w `-`), so a type, function or
1084+
* value of the same name keeps that node. */
10811085
if (strcmp(f[SKIP_ONE], "?") != 0) {
10821086
go_add_member(ix, file, GO_MEM_METHOD, f[SKIP_ONE], f[PAIR_LEN], true);
10831087
}
1084-
go_add_claim(ix, file, GO_KIND_METHOD, f[PAIR_LEN], f[SKIP_ONE], f[M_OWNER]);
10851088
} else if (kind == 'f' && nf == FIELD_FIELDS && go_name_ok(f[PAIR_LEN])) {
10861089
go_add_member(ix, file, GO_MEM_FIELD, f[SKIP_ONE], f[PAIR_LEN], f[FIELD_NODE][0] == '1');
10871090
} 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 *
13421345
return (fm && (fm->node_kinds & kind)) ? n : NULL;
13431346
}
13441347

1345-
/* The node of the method `recv.name`: its receiver-qualified node, else the
1346-
* flat node <dir>.<name> when the method is the one that kept it (the last
1347-
* declaration of that name in the node's file). */
1348+
/* The node of the method `recv.name`: its receiver-qualified node. The flat
1349+
* fallback below (the method that kept <dir>.<name>) is unreachable since
1350+
* #1913: no method record claims a flat name any more. */
13481351
static const cbm_gbuf_node_t *go_method_node(const go_ctx_t *c, const go_pkg_t *pkg, go_str_t recv,
13491352
go_str_t name) {
13501353
const cbm_gbuf_node_t *qualified = go_member_node(c, pkg, GO_MEM_METHOD, recv, name);

‎src/pipeline/doc_links_pdf.c‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -328,9 +328,10 @@ static void qn_candidate(const pdfr_index_t *x, const cbm_gbuf_node_t *n, const
328328
const char *rel = rel_qn(x, n->qualified_name);
329329
char alt[PDFR_KEY_CAP];
330330
bool owned = owner_name(x, n, alt, sizeof(alt));
331-
/* A member whose QN leaves its owner out (a Go method's has no receiver)
332-
* is named through the owner: `time.Now` is package time's function,
333-
* never the method Now of a type that package time declares. */
331+
/* A member whose QN leaves its owner out (a templ method's keeps the flat
332+
* package QN; a Go method's did before index format 4) is named through
333+
* the owner: `time.Now` is package time's function, never the method Now
334+
* of a type that package time declares. */
334335
bool ownerless_qn = owned && !comp_suffix(rel, alt);
335336
bool via_rel = !ownerless_qn && comp_suffix(rel, s);
336337
bool via_alt = !via_rel && owned && strcmp(alt, rel) != 0 && comp_suffix(alt, s);

‎src/pipeline/pass_calls.c‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1116,7 +1116,13 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
11161116
* language gated on only one resolver produces an edge on the sequential
11171117
* path and not the parallel one (or vice versa), breaking MT determinism.
11181118
* ArkTS belongs to the JS/TS family here (#1842); dropping it would
1119-
* reintroduce the #592/#606 false-edge class for .ets files. */
1119+
* reintroduce the #592/#606 false-edge class for .ets files.
1120+
*
1121+
* Go (#1906) rides the same deferred-drop plumbing through its OWN
1122+
* predicate: its drop-list differs (field_type_hint is receiver-aware for
1123+
* Go, and unique_name drops only when import-unreachability-penalized), so
1124+
* it composes via cbm_go_suppress_weak_method_match instead of widening
1125+
* the shared gate. Same lockstep rule: mirror pass_parallel.c. */
11201126
bool suppress_weak_member = lang == CBM_LANG_PYTHON || lang == CBM_LANG_JAVASCRIPT ||
11211127
lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
11221128
lang == CBM_LANG_ARKTS ||
@@ -1144,6 +1150,8 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call, const CBM
11441150
res.strategy)) ||
11451151
cbm_suppress_weak_local_binding_call(suppress_weak_local_binding,
11461152
call->callee_is_locally_bound, res.strategy) ||
1153+
cbm_go_suppress_weak_method_match(lang == CBM_LANG_GO, call->is_method, res.strategy,
1154+
res.confidence) ||
11471155
/* Import-binding suppression (#2127): an external `from m import f`
11481156
* binds `f` for the whole module, so a weak short-name match onto a
11491157
* project `X.f` outside m is fabricated. Python-only (the import

‎src/pipeline/pass_parallel.c‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3348,7 +3348,15 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
33483348
}
33493349

33503350
_rc_t0 = extract_now_ns();
3351-
try_field_type_hint(rc, &res, call->callee_name, source_node->id);
3351+
/* Not for Go (F-G9): the hint matches the capitalized receiver
3352+
* VARIABLE name as a substring of a candidate QN (`t.Run` -> any
3353+
* `...T...Run`), with no type evidence. The sequential resolver has no
3354+
* such step, so for Go it produced parallel-only edges (par/seq CALLS
3355+
* diverged), and the Go selector guard could not see them as the weak
3356+
* pick they are. SCIP precision of Go field_type_hint: 0.016. */
3357+
if (lang != CBM_LANG_GO) {
3358+
try_field_type_hint(rc, &res, call->callee_name, source_node->id);
3359+
}
33523360
/* Cross-language veto — the same predicate as pass_calls.c, applied
33533361
* AFTER the field-type hint so the hint may still re-pick a compatible
33543362
* 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
33853393
* #606 direction.
33863394
*
33873395
* This language set MUST match the one in pass_calls.c exactly — see the
3388-
* note there. ArkTS belongs to the JS/TS family (#1842). */
3396+
* note there. ArkTS belongs to the JS/TS family (#1842). Go (#1906)
3397+
* composes via its own predicate (different drop-list — see
3398+
* cbm_go_suppress_weak_method_match), mirrored in pass_calls.c. */
33893399
bool suppress_weak_member = lang == CBM_LANG_PYTHON || lang == CBM_LANG_JAVASCRIPT ||
33903400
lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
33913401
lang == CBM_LANG_ARKTS ||
@@ -3403,6 +3413,8 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
34033413
call->callee_name, res.strategy)) ||
34043414
cbm_suppress_weak_local_binding_call(suppress_weak_local_binding,
34053415
call->callee_is_locally_bound, res.strategy) ||
3416+
cbm_go_suppress_weak_method_match(lang == CBM_LANG_GO, call->is_method, res.strategy,
3417+
res.confidence) ||
34063418
/* Import-binding suppression (#2127) — see pass_calls.c; this gate
34073419
* MUST stay identical to the one there. */
34083420
(lang == CBM_LANG_PYTHON &&

0 commit comments

Comments
 (0)