Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
50ee111
fix(pipeline): suppress weak short-name matches for Go selector calls
ilyabrykau-orca Aug 28, 2026
da76194
fix(extract): receiver-qualify Go method QNs
ilyabrykau-orca Aug 29, 2026
b74cecc
Merge branch 'main' into fix/go-weak-selector-calls
DeusData Sep 28, 2026
71ebbb9
Merge branch 'fix/go-weak-selector-calls' (with main) into fix/go-met…
DeusData Sep 28, 2026
285d21c
Merge branch 'main' into fix/go-weak-selector-calls
DeusData Oct 17, 2026
7528ec0
test(pipeline): bind the Go selector guard on the parallel resolver
DeusData Oct 17, 2026
9aeea8e
Merge branch 'fix/go-weak-selector-calls' (with main) into fix/go-met…
DeusData Oct 17, 2026
87cafb9
fix(extract): bump the index format for receiver-qualified Go method QNs
DeusData Oct 17, 2026
db8d547
fix(extract): keep both Go method QN sides flat when the type QN is NULL
DeusData Oct 17, 2026
e554a2a
fix(go): no field-type hint for Go; it is a name guess, par-only
DeusData Oct 18, 2026
ec1e8b8
Merge branch 'gq-l5/stack-a-1907' into gq-l5/stack-a-1913
DeusData Oct 18, 2026
4feb475
Merge remote-tracking branch 'origin/main' into gq-l5/stack-a-1907
DeusData Oct 18, 2026
ee2a4d6
Merge branch 'gq-l5/stack-a-1907' into gq-l5/stack-a-1913
DeusData Oct 18, 2026
033be76
fix(doc-links): a Go method claims no flat doc-link node name
DeusData Oct 18, 2026
4c62b61
test(doc_mentions_go): receiver-qualified Go method nodes (#1913)
DeusData Oct 18, 2026
8055f4e
Merge remote-tracking branch 'origin/main' into gq-l5/stack-a-1907
DeusData Oct 18, 2026
099d063
Merge branch 'gq-l5/stack-a-1907' into gq-l5/stack-a-1913
DeusData Oct 18, 2026
aa28277
Merge remote-tracking branch 'origin/main' into gq-l5/stack-a-1907
DeusData Oct 18, 2026
4e2dcd5
Merge branch 'gq-l5/stack-a-1907' into gq-l5/stack-a-1913
DeusData Oct 18, 2026
aa4775f
test(doc_links_pdf): receiver-qualified Go method nodes (#1913)
DeusData Oct 18, 2026
730a874
Merge remote-tracking branch 'origin/main' into gq-l5/stack-a-1907
DeusData Oct 18, 2026
7d92e74
Merge branch 'gq-l5/stack-a-1907' into gq-l5/stack-a-1913
DeusData Oct 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions internal/cbm/doclink_go.c
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@
* drops these lines and so carries no line number
* at all.
* G path go.mod: the module path
* w says who keeps the node. A Go method node has a FLAT qualified name
* (<dir>.<Method>), so a function, the methods of every receiver, a type and
* a value of one name all claim the node <dir>.<name>, and within a file the
* declaration on the LAST line keeps it (cbm_gbuf_upsert_node). w is 1 for
* w says who keeps the node <dir>.<name>. A function, a type and a value of
* one name all claim it, and within a file the declaration on the LAST line
* keeps it (cbm_gbuf_upsert_node). A method claims nothing: its node is
* receiver-qualified (<dir>.<Recv>.<Method>, #1913), and its w is `-`. w is 1 for
* that declaration, 0 for the others of that name, x when two of them start
* on that last line, and - for a value the extractor gives no node at all
* (it gives one to the first name of a constant spec and of a parenthesis-
Expand Down Expand Up @@ -1511,9 +1511,11 @@ static void go_scan_method(go_scan_t *s, TSNode m) {
recv = "?";
recv_len = SKIP_ONE;
}
go_push_claim(s,
cbm_arena_sprintf(s->tmp, "M\t%.*s\t%.*s", (int)recv_len, recv, (int)len, name),
name, len, m);
/* #1913: a Go method node is receiver-qualified (<dir>.<Recv>.<Method>),
* so a method claims no flat name: w is `-`, and the method is found
* through its receiver (the member table), never through <dir>.<name>. */
(void)go_push_rec(
s, cbm_arena_sprintf(s->tmp, "M\t%.*s\t%.*s\t-", (int)recv_len, recv, (int)len, name));
}

/* The implied name of an embedded field: the type's own name, without
Expand Down
18 changes: 18 additions & 0 deletions internal/cbm/extract_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -4493,6 +4493,24 @@ CBMInvocationDescriptor handle_calls(CBMExtractCtx *ctx, TSNode node, const CBML
}
}
}
// Go receiver-aware guard (same direction as the TS/JS flag above).
// Flag a selector call x.foo(). The Go AST cannot separate a method
// call on a value from a package-qualified call — but every selector
// call the Go LSP or the import/qualified registry strategies CAN
// place never reaches the weak short-name guards, so the flag only
// bites on unresolvable receivers (`f.Close()` on an os.File,
// `sha256.New()` behind an unindexed import), where a project-wide
// short-name match fabricates an edge to an unrelated project
// symbol sharing the name. Bare calls (helper()) keep
// is_method=false and resolve same-module/import paths as before.
if (ctx->language == CBM_LANG_GO &&
strcmp(ts_node_type(node), "call_expression") == 0) {
TSNode gofn = ts_node_child_by_field_name(node, TS_FIELD("function"));
if (!ts_node_is_null(gofn) &&
strcmp(ts_node_type(gofn), "selector_expression") == 0) {
call.is_method = true;
}
}

TSNode args = ts_node_child_by_field_name(node, TS_FIELD("arguments"));
/* tree-sitter-elixir attaches NO field name to a call's arguments
Expand Down
19 changes: 17 additions & 2 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -5453,7 +5453,7 @@ static void set_def_complexity(CBMDefinition *def, TSNode body, const CBMLangSpe
* Walks to the parameter_declaration's `type` field, unwrapping pointer_type
* and generic_type, and returns the type_identifier text (e.g. "OrderService").
* Returns NULL if no type_identifier is found. */
static char *go_receiver_type_name(CBMArena *a, TSNode recv, const char *source) {
char *cbm_go_receiver_type_name(CBMArena *a, TSNode recv, const char *source) {
uint32_t nc = ts_node_child_count(recv);
for (uint32_t i = 0; i < nc; i++) {
TSNode child = ts_node_child(recv, i);
Expand Down Expand Up @@ -6687,12 +6687,27 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
* (and downstream Go IMPLEMENTS/OVERRIDE) link the method to its owning
* struct/type node. The parent QN must match the type's node QN, which
* is computed the same way (cbm_fqn_compute on the type name). */
char *recv_type = go_receiver_type_name(a, recv, ctx->source);
char *recv_type = cbm_go_receiver_type_name(a, recv, ctx->source);
if (recv_type && recv_type[0]) {
/* Must match the Go type node QN (directory-based module) so the
* DEFINES_METHOD edge links the method to its owning type. */
def.parent_class = cbm_fqn_compute_source_lang(a, ctx->project, ctx->rel_path,
recv_type, ctx->language);
/* Receiver-qualify the method QN (proj.pkg.Recv.method) — same
* shape as the C++ out-of-line path below and Go interface
* members. With the flat proj.pkg.method QN every same-name
* method in a package collided in the graph upsert: one body
* survived and the twins' call edges accreted onto it. The
* call-scope side (compute_func_qn in extract_unified.c) mirrors
* this formula, and go_lsp consumers read the def QN and
* parent_class (receiver_type) verbatim, so resolution joins
* stay exact. Go only: templ's Go-derived grammar reaches this
* branch too, but its call-scope QN (compute_func_qn) stays flat,
* and a def QN the calls do not share would orphan every call
* inside a templ method. */
if (ctx->language == CBM_LANG_GO && def.parent_class) {
def.qualified_name = cbm_arena_sprintf(a, "%s.%s", def.parent_class, name);
}
}
}

Expand Down
23 changes: 23 additions & 0 deletions internal/cbm/extract_unified.c
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,29 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan
}
}

/* Go method `func (s *Storage) Close() {...}`: the def extractor records
* this as Method "proj.pkg.Storage.Close" (receiver-qualified, mirroring
* the C++ out-of-line rule above). The call-scope QN must match — a bare
* "proj.pkg.Close" names a node that no longer exists, so every call
* inside the method body would fall back to File-node attribution
* (calls_find_source). Same ONE-formula contract as the def side:
* cbm_go_receiver_type_name + cbm_fqn_compute_source_lang. */
if (ctx->language == CBM_LANG_GO && strcmp(ts_node_type(node), "method_declaration") == 0) {
TSNode recv = ts_node_child_by_field_name(node, TS_FIELD("receiver"));
if (!ts_node_is_null(recv)) {
char *recv_type = cbm_go_receiver_type_name(ctx->arena, recv, ctx->source);
if (recv_type && recv_type[0]) {
const char *type_qn = cbm_fqn_compute_source_lang(
ctx->arena, ctx->project, ctx->rel_path, recv_type, ctx->language);
/* NULL only on allocation failure: fall through to the flat
* QN, as the def side does (extract_defs.c). */
if (type_qn) {
return cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, name);
}
}
}
}

/* Nix: a binding's own attrpath contributes scope (`a.b.fn = …`), and the def
* extractor bakes it into the def QN. Compose it identically here — otherwise
* an in-body call sources to a QN one or more segments short of the def, and
Expand Down
6 changes: 6 additions & 0 deletions internal/cbm/helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,12 @@ TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang);
// def extractor — drift dropped the class qualifier from in-body calls (#554/#621).
char *cbm_cpp_out_of_line_parent_class(CBMArena *a, TSNode node, const char *source);

/* Go: resolve a method_declaration's receiver parameter_list down to the bare
* receiver type_identifier (unwrapping pointer_type / generic_type). Shared by
* def extraction (extract_defs.c) and call-scope attribution
* (extract_unified.c) so the receiver-qualified method QN has ONE formula. */
char *cbm_go_receiver_type_name(CBMArena *a, TSNode recv, const char *source);

// Find a child node by kind string.
TSNode cbm_find_child_by_kind(TSNode parent, const char *kind);

Expand Down
25 changes: 17 additions & 8 deletions internal/cbm/lsp/go_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -1987,15 +1987,24 @@ static void process_function(GoLSPContext *ctx, TSNode func_node) {
if (!func_name || !func_name[0])
return;

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

// Push function scope
CBMScope *saved_scope = ctx->current_scope;
Expand Down
23 changes: 13 additions & 10 deletions src/pipeline/doc_links_go.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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])) {
Expand Down Expand Up @@ -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);
Expand Down
7 changes: 4 additions & 3 deletions src/pipeline/doc_links_pdf.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 9 additions & 1 deletion src/pipeline/pass_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -1103,7 +1103,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 ||
Expand Down Expand Up @@ -1131,6 +1137,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
Expand Down
16 changes: 14 additions & 2 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -3364,7 +3364,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
Expand Down Expand Up @@ -3401,7 +3409,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 ||
Expand All @@ -3419,6 +3429,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 &&
Expand Down
Loading
Loading