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
+9 -7
View File
@@ -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-
@@ -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
+18
View File
@@ -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
+17 -2
View File
@@ -5772,7 +5772,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);
@@ -7006,12 +7006,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);
}
}
}
+23
View File
@@ -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
+6
View File
@@ -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);
+17 -8
View File
@@ -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;
+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)
+9 -6
View File
@@ -1087,8 +1087,9 @@ TEST(pdf_links_pipeline) {
ASSERT_NOT_NULL(strstr(props, "\"line\":1"));
/* qualified names (pdf_qn) passed a second held-out audit (97 of 99
* correct, Wilson 95 % low 0.929) once a member whose QN leaves its owner
* out binds only through the owner. A Go method through its receiver: */
dm_edge(db, "docs.design.page_1", "pkg.server.Serve", props, sizeof(props), &n);
* out binds only through the owner. A Go method through its receiver
* (receiver-qualified QN since index format 4): */
dm_edge(db, "docs.design.page_1", "pkg.server.Handler.Serve", props, sizeof(props), &n);
ASSERT_EQ(n, 1);
ASSERT_NOT_NULL(strstr(props, "\"syntax\":\"pdf_qn\""));
ASSERT_NOT_NULL(strstr(props, "\"tier\":\"unique\""));
@@ -1142,10 +1143,12 @@ TEST(pdf_links_pipeline) {
PASS();
}
/* A held-out finding of the PDF audit: a Go method's QN has no receiver, so
/* A held-out finding of the PDF audit: a Go method's QN had no receiver, so
* `time.Now` (the standard library's function, in a spec) matched the method
* Now of a type that the repository's own package time declares. A method is
* named through its owner: `time.Provider.Now` still binds it. */
* named through its owner: `time.Provider.Now` still binds it. Since index
* format 4 the QN carries the receiver (pkg.time.Provider.Now), and both
* directions are checked against that node. */
TEST(pdf_qn_method_through_owner) {
cbm_doclink_test_set_ships(CBM_DOCLINK_PDF_QN, true);
char tmp[256];
@@ -1169,9 +1172,9 @@ TEST(pdf_qn_method_through_owner) {
ASSERT_EQ(dm_index(repo, db, NULL), 0);
char props[512];
int n;
dm_edge(db, "docs.spec.page_1", "pkg.time.Now", props, sizeof(props), &n);
dm_edge(db, "docs.spec.page_1", "pkg.time.Provider.Now", props, sizeof(props), &n);
ASSERT_EQ(n, 0);
dm_edge(db, "docs.spec.page_2", "pkg.time.Now", props, sizeof(props), &n);
dm_edge(db, "docs.spec.page_2", "pkg.time.Provider.Now", props, sizeof(props), &n);
ASSERT_EQ(n, 1);
ASSERT_NOT_NULL(strstr(props, "\"syntax\":\"pdf_qn\""));
th_cleanup(tmp);
+122 -68
View File
@@ -228,10 +228,10 @@ TEST(doc_mentions_go_sources) {
const char *raw;
const char *source;
} want[] = {
{"RefF", "p.pkg.F"}, {"RefS", "p.pkg.S"}, {"RefField", "p.pkg.S.Field"},
{"RefM", "p.pkg.M"}, /* Go method nodes have a flat name */
{"RefI", "p.pkg.I"}, {"RefDo", "p.pkg.I.Do"}, {"RefC", "p.pkg.C"},
{"RefK", "p.pkg.K"}, {"RefV", "p.pkg.V"}, {"RefA", "p.pkg.A"},
{"RefF", "p.pkg.F"}, {"RefS", "p.pkg.S"}, {"RefField", "p.pkg.S.Field"},
{"RefM", "p.pkg.S.M"}, /* #1913: Go method nodes are receiver-qualified */
{"RefI", "p.pkg.I"}, {"RefDo", "p.pkg.I.Do"}, {"RefC", "p.pkg.C"},
{"RefK", "p.pkg.K"}, {"RefV", "p.pkg.V"}, {"RefA", "p.pkg.A"},
};
for (size_t i = 0; i < GO_COUNT(want); i++) {
const CBMDocLink *t = dm_find_token(r, want[i].raw);
@@ -772,14 +772,15 @@ TEST(doc_mentions_go_scope_blob) {
"T\tAlias\ta\tal.Widget\t1",
"T\tSame\ta\tKind\t1",
"T\tList\ts\t-\t1",
/* functions and methods; two methods named Run compete for one node
* and the later one keeps it */
/* functions and methods; a method node is receiver-qualified
* (#1913), so a method claims no flat name and its w is `-`: the two
* methods named Run no longer compete */
"F\tNew\t1",
"F\tMap\t1",
"M\tWidget\tRun\t0",
"M\tKind\tRun\t1",
"M\tList\tPush\t1",
"M\tlower\tExported\t1",
"M\tWidget\tRun\t-",
"M\tKind\tRun\t-",
"M\tList\tPush\t-",
"M\tlower\tExported\t-",
/* own type parameters, by the declaration's first and last line */
"g\t36\t36\tT,U",
"g\t42\t42\tA,B",
@@ -814,15 +815,20 @@ TEST(doc_mentions_go_scope_blob) {
char *portable = cbm_doclink_portable_scope(s);
ASSERT_NOT_NULL(portable);
ASSERT_NULL(strstr(portable, "\ng\t"));
ASSERT_TRUE(go_has_rec(portable, "M\tKind\tRun\t1"));
ASSERT_TRUE(go_has_rec(portable, "M\tKind\tRun\t-"));
ASSERT(strncmp(portable, "go1\n", 4) == 0);
cbm_free(CBM_MEM_CLASS_OTHER, portable);
cbm_free_result(r);
/* moving declarations changes nothing unless a node changes its owner */
/* moving declarations changes nothing unless a node changes its owner.
* Methods own no flat name (#1913), so swapping two methods of one name
* changes nothing either; two package-level declarations of one name
* still hand <dir>.<name> over when swapped */
const char *a = "package p\n\nfunc (a A) Run() {}\nfunc (b B) Run() {}\nfunc F() {}\n";
const char *moved = "package p\n\nfunc F() {}\n\n\nfunc (a A) Run() {}\nfunc (b B) Run() {}\n";
const char *swapped = "package p\n\nfunc (b B) Run() {}\nfunc (a A) Run() {}\nfunc F() {}\n";
const char *pair = "package p\n\nfunc Open() {}\ntype Open struct{}\n";
const char *pair_swapped = "package p\n\ntype Open struct{}\nfunc Open() {}\n";
CBMFileResult *ra = go_extract(a, "p/x.go");
CBMFileResult *rm = go_extract(moved, "p/x.go");
CBMFileResult *rs = go_extract(swapped, "p/x.go");
@@ -830,16 +836,25 @@ TEST(doc_mentions_go_scope_blob) {
ASSERT_NOT_NULL(rm);
ASSERT_NOT_NULL(rs);
ASSERT_STR_EQ(ra->doc_scope, rm->doc_scope);
ASSERT(strcmp(ra->doc_scope, rs->doc_scope) != 0);
ASSERT_TRUE(go_has_rec(rs->doc_scope, "M\tA\tRun\t1"));
ASSERT_TRUE(go_has_rec(rs->doc_scope, "M\tB\tRun\t0"));
ASSERT_STR_EQ(ra->doc_scope, rs->doc_scope);
ASSERT_TRUE(go_has_rec(rs->doc_scope, "M\tA\tRun\t-"));
ASSERT_TRUE(go_has_rec(rs->doc_scope, "M\tB\tRun\t-"));
cbm_free_result(ra);
cbm_free_result(rm);
cbm_free_result(rs);
CBMFileResult *rp = go_extract(pair, "p/x.go");
CBMFileResult *rq = go_extract(pair_swapped, "p/x.go");
ASSERT_NOT_NULL(rp);
ASSERT_NOT_NULL(rq);
ASSERT(strcmp(rp->doc_scope, rq->doc_scope) != 0);
ASSERT_TRUE(go_has_rec(rp->doc_scope, "T\tOpen\ts\t-\t1"));
ASSERT_TRUE(go_has_rec(rq->doc_scope, "F\tOpen\t1"));
cbm_free_result(rp);
cbm_free_result(rq);
/* the node <dir>.<name> is one for a function, a type, a value and the
* methods of that name: the last declaration keeps it, whatever its kind,
* and two on one line contest it */
/* the node <dir>.<name> is one for a function, a type and a value of
* that name: the last declaration keeps it, whatever its kind, and two on
* one line contest it. A method of that name claims nothing (#1913) */
CBMFileResult *k1 = go_extract("package p\n\nfunc Open() {}\nfunc (t T) Open() {}\n", "p/k.go");
CBMFileResult *k2 = go_extract("package p\n\nfunc (t T) Open() {}\nfunc Open() {}\n", "p/k.go");
CBMFileResult *k3 = go_extract("package p\n\ntype Open int\nvar Open = 1\n"
@@ -848,14 +863,21 @@ TEST(doc_mentions_go_scope_blob) {
ASSERT_NOT_NULL(k1);
ASSERT_NOT_NULL(k2);
ASSERT_NOT_NULL(k3);
ASSERT_TRUE(go_has_rec(k1->doc_scope, "F\tOpen\t0"));
ASSERT_TRUE(go_has_rec(k1->doc_scope, "M\tT\tOpen\t1"));
ASSERT_TRUE(go_has_rec(k1->doc_scope, "F\tOpen\t1"));
ASSERT_TRUE(go_has_rec(k1->doc_scope, "M\tT\tOpen\t-"));
ASSERT_TRUE(go_has_rec(k2->doc_scope, "F\tOpen\t1"));
ASSERT_TRUE(go_has_rec(k2->doc_scope, "M\tT\tOpen\t0"));
ASSERT_TRUE(go_has_rec(k2->doc_scope, "M\tT\tOpen\t-"));
ASSERT_TRUE(go_has_rec(k3->doc_scope, "T\tOpen\to\t-\t0"));
ASSERT_TRUE(go_has_rec(k3->doc_scope, "V\tOpen\t0"));
ASSERT_TRUE(go_has_rec(k3->doc_scope, "V\tOpen\tx"));
ASSERT_TRUE(go_has_rec(k3->doc_scope, "M\tT\tOpen\tx"));
ASSERT_TRUE(go_has_rec(k3->doc_scope, "V\tOpen\t1"));
ASSERT_TRUE(go_has_rec(k3->doc_scope, "M\tT\tOpen\t-"));
/* two package-level declarations on one line contest the name */
CBMFileResult *k4 =
go_extract("package p\n\ntype Open int\nvar Open = 1; const Open = 2\n", "p/k.go");
ASSERT_NOT_NULL(k4);
ASSERT_TRUE(go_has_rec(k4->doc_scope, "T\tOpen\to\t-\t0"));
ASSERT_TRUE(go_has_rec(k4->doc_scope, "V\tOpen\tx"));
cbm_free_result(k4);
cbm_free_result(k1);
cbm_free_result(k2);
cbm_free_result(k3);
@@ -1030,7 +1052,7 @@ static const char GO_ALPHA_C[] = "package alpha\n"
"// Target is a target; see [A.Close] and [B.Close].\n"
"func Target() {}\n"
"\n"
"// Open is a type that loses its node name to the method below.\n"
"// Open is a type; a method of B below has the same name.\n"
"type Open struct{}\n"
"\n"
"// Open opens: a method named like the type.\n"
@@ -1295,7 +1317,7 @@ TEST(doc_mentions_go_forms) {
static const go_want_edge_t edges[] = {
/* [Name], [Recv.Method], an interface's method, a constant, a variable */
{"alpha.Widget", "alpha.New", "unique"},
{"alpha.Widget", "alpha.Run", "unique"},
{"alpha.Widget", "alpha.Widget.Run", "unique"},
{"alpha.Widget", "alpha.Runner.Run", "unique"},
{"alpha.Widget", "alpha.Max", "unique"},
{"alpha.Widget", "alpha.Default", "unique"},
@@ -1311,7 +1333,7 @@ TEST(doc_mentions_go_forms) {
{"alpha.New", "sub.deep.Deep", "exact"},
{"alpha.New", "nested.pkg.Far", "exact"},
/* [pkg.Recv.Name]: a method, a field, an interface method */
{"alpha.New", "beta.Do", "exact"},
{"alpha.New", "beta.Thing.Do", "exact"},
{"alpha.New", "beta.Thing.Field", "exact"},
{"alpha.New", "beta.Doer.Act", "exact"},
/* an identifier with letters beyond ASCII */
@@ -1395,7 +1417,7 @@ TEST(doc_mentions_go_plain_text) {
ASSERT_EQ(go_bad_rows(db, rows, GO_COUNT(rows)), 0);
/* and none of them is an edge: Run's doc holds rows, plain text and one
* link, to the field Widget.Size */
ASSERT_EQ(dm_mentions_from(db, "alpha.Run"), 1);
ASSERT_EQ(dm_mentions_from(db, "alpha.Widget.Run"), 1);
ASSERT_EQ(dm_mentions_from(db, "alpha.Bare"), 0);
static const go_want_edge_t edges[] = {
{"alpha.Sibling", "alpha.Widget", "unique"},
@@ -1424,14 +1446,14 @@ TEST(doc_mentions_go_struct_fields) {
static const go_want_edge_t edges[] = {
/* the same package: a field named from a method's doc and from a
* sibling field's doc */
{"alpha.Run", "alpha.Widget.Size", "exact"},
{"alpha.Widget.Run", "alpha.Widget.Size", "exact"},
{"alpha.Widget.Name", "alpha.Widget.Size", "exact"},
/* another package, by its import */
{"alpha.New", "beta.Thing.Field", "exact"},
{"gamma.ViaAlias", "alpha.Widget.Name", "exact"},
/* Depth is a field of beta.Embedded, which Widget embeds: it is no
* field of Widget */
{"alpha.Run", "beta.Embedded.Depth", ""},
{"alpha.Widget.Run", "beta.Embedded.Depth", ""},
};
ASSERT_EQ(go_bad_edges(db, edges, GO_COUNT(edges)), 0);
/* the promoted field exists as a node: the test above is not vacuous */
@@ -1545,7 +1567,7 @@ TEST(doc_mentions_go_lookup_order) {
* file that imports "fmt", the repository's own in a file that imports
* example.com/demo/fmt */
ASSERT_STR_EQ(go_reason(db, "alpha/a.go", "fmt.Println"), "external");
ASSERT_STR_EQ(go_tier(db, "alpha.Run", "fmt.Println"), "");
ASSERT_STR_EQ(go_tier(db, "alpha.Widget.Run", "fmt.Println"), "");
ASSERT_STR_EQ(go_tier(db, "gamma.Local", "fmt.Println"), "exact");
/* 5. the longest module path decides: a directory inside the root module
* that belongs to a nested module of another path is not reachable through
@@ -1555,17 +1577,17 @@ TEST(doc_mentions_go_lookup_order) {
ASSERT_STR_EQ(go_tier(db, "alpha.New", "sub.deep.Deep"), "exact");
/* 6. the bare name stops at the package: beta.Make is imported and still
* no [Make] */
ASSERT_STR_EQ(go_tier(db, "alpha.Run", "beta.Make"), "");
ASSERT_STR_EQ(go_tier(db, "alpha.Widget.Run", "beta.Make"), "");
/* 7. members stop at the type: nothing promoted through embedding */
ASSERT_STR_EQ(go_reason(db, "gamma/k.go", "alpha.Widget.Promoted"), "missing");
ASSERT_STR_EQ(go_tier(db, "gamma.ViaAlias", "beta.Promoted"), "");
ASSERT_STR_EQ(go_tier(db, "gamma.ViaAlias", "beta.Embedded.Promoted"), "");
/* 8. an alias is followed for a member written with its package, to the
* type that declares the member */
static const go_want_edge_t alias[] = {
{"gamma.ViaAlias", "beta.Do", "exact"},
{"gamma.ViaAlias", "beta.Thing.Do", "exact"},
{"gamma.ViaAlias", "beta.Thing.Field", "exact"},
{"gamma.ViaAlias", "alpha.Alias", "exact"},
{"gamma.ViaAlias", "alpha.End", "exact"}, /* seven aliases away */
{"gamma.ViaAlias", "alpha.Deep10.End", "exact"}, /* seven aliases away */
{"gamma.ViaAlias", "alpha.Widget.Name", "exact"},
};
ASSERT_EQ(go_bad_edges(db, alias, GO_COUNT(alias)), 0);
@@ -1638,28 +1660,42 @@ TEST(doc_mentions_go_node_owner) {
char tmp[256];
char db[512];
ASSERT_EQ(go_index_lookup(tmp, sizeof(tmp), db, sizeof(db)), 0);
/* Go method nodes have a flat name: A.Close and B.Close share
* alpha.Close, and the later declaration keeps it */
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE qualified_name LIKE '%.alpha.Close'"),
/* #1913: Go method nodes are receiver-qualified. A.Close and B.Close are
* two nodes (no shared alpha.Close), each a target of its own */
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE label = 'Method' AND "
"qualified_name LIKE '%.alpha.A.Close'"),
1);
ASSERT_STR_EQ(go_tier(db, "alpha.Target", "alpha.Close"), "unique");
ASSERT_STR_EQ(go_reason(db, "alpha/c.go", "A.Close"), "graph_gap");
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE label = 'Method' AND "
"qualified_name LIKE '%.alpha.B.Close'"),
1);
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE qualified_name LIKE '%.alpha.Close'"),
0);
ASSERT_STR_EQ(go_tier(db, "alpha.Target", "alpha.A.Close"), "unique");
ASSERT_STR_EQ(go_tier(db, "alpha.Target", "alpha.B.Close"), "unique");
ASSERT_STR_EQ(go_reason(db, "alpha/c.go", "A.Close"), "");
ASSERT_STR_EQ(go_reason(db, "alpha/c.go", "B.Close"), "");
/* the doc of the method that lost the name is not attributed to the one
* that kept it: B.Close's [Target] is the edge, A.Close's a row */
/* each method's doc is attributed to its own node: both [Target]s are
* edges, from the line of that method's doc, and neither is a row */
char props[512];
int n = 0;
dm_edge(db, "alpha.Close", "alpha.Target", props, sizeof(props), &n);
dm_edge(db, "alpha.A.Close", "alpha.Target", props, sizeof(props), &n);
ASSERT_EQ(n, 1);
ASSERT_NOT_NULL(strstr(props, "\"line\":9,\"count\":1"));
dm_edge(db, "alpha.B.Close", "alpha.Target", props, sizeof(props), &n);
ASSERT_EQ(n, 1);
ASSERT_NOT_NULL(strstr(props, "\"line\":12,\"count\":1"));
ASSERT_EQ(dm_count(db, "SELECT line FROM doc_link_unresolved WHERE rel_path = 'alpha/c.go' "
"AND raw = 'Target'"),
9);
/* a type that lost its name to a method of that name is no target, the
* method is */
ASSERT_STR_EQ(go_reason(db, "alpha/c.go", "Open"), "graph_gap");
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM doc_link_unresolved WHERE rel_path = "
"'alpha/c.go' AND raw = 'Target'"),
0);
/* a type and a method of the same name no longer share a node: [Open]
* is the type, [B.Open] the method */
ASSERT_STR_EQ(go_reason(db, "alpha/c.go", "Open"), "");
ASSERT_EQ(dm_count(db, "SELECT COUNT(*) FROM nodes WHERE label <> 'Method' AND "
"qualified_name LIKE '%.alpha.Open'"),
1);
ASSERT_STR_EQ(go_tier(db, "alpha.UsesOpen", "alpha.Open"), "unique");
ASSERT_EQ(dm_mentions_from(db, "alpha.UsesOpen"), 1);
ASSERT_STR_EQ(go_tier(db, "alpha.UsesOpen", "alpha.B.Open"), "unique");
ASSERT_EQ(dm_mentions_from(db, "alpha.UsesOpen"), 2);
/* build variants are one entity: whichever file holds the node */
static const go_want_edge_t variants[] = {
@@ -1836,15 +1872,16 @@ TEST(doc_mentions_go_incremental_equals_full) {
ASSERT_EQ(dm_index(repo, inc_db, NULL), 0);
/* the preconditions the steps below move away from */
static const go_want_edge_t before[] = {
{"a.Use", "b.Target", "exact"}, {"a.Use", "b.Gone", "exact"},
{"a.Use", "b.M", "exact"}, /* T.M, in the first file of the directory, keeps b.M */
{"a.Use", "b.Name", "exact"}, /* W.Name, the later declaration, keeps b.Name */
{"a.Use", "a.Local", "unique"}, {"a.doc.go.__file__", "a.Local", "unique"},
{"a.Use", "b.Target", "exact"}, {"a.Use", "b.Gone", "exact"},
{"a.Use", "b.T.M", "exact"}, /* #1913: every method has its own node */
{"a.Use", "b.W.Name", "exact"}, {"a.Use", "b.V.Name", "exact"},
{"b.U.M", "b.Target", "unique"}, /* U.M is a source of its own */
{"a.Use", "a.Local", "unique"}, {"a.doc.go.__file__", "a.Local", "unique"},
{"a.Other", "b.Target", ""}, /* other.go does not import b */
};
ASSERT_EQ(go_bad_edges(inc_db, before, GO_COUNT(before)), 0);
ASSERT_STR_EQ(go_reason(inc_db, "a/a.go", "b.V.Name"), "graph_gap");
ASSERT_STR_EQ(go_reason(inc_db, "b/c.go", "Target"), "graph_gap"); /* U.M has no node */
ASSERT_STR_EQ(go_reason(inc_db, "a/a.go", "b.V.Name"), "");
ASSERT_STR_EQ(go_reason(inc_db, "b/c.go", "Target"), "");
/* a doc-comment edit re-extracts the file alone */
go_w(repo, "a/a.go", GO_INC_A_DOC_EDIT);
@@ -1880,23 +1917,27 @@ TEST(doc_mentions_go_incremental_equals_full) {
0);
ASSERT_STR_EQ(go_reason(inc_db, "a/a.go", "b.Gone"), "missing");
/* the method that kept a shared node name goes: the other file's method of
* that name takes the node over, and its doc becomes a source */
/* a method goes: links to it break, and another receiver's method of
* the same name, in another file, is untouched */
go_w(repo, "b/b.go", GO_INC_B_NO_M);
ASSERT_EQ(
dm_step(repo, inc_db, full_db, "name keeper removed", CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR),
0);
ASSERT_STR_EQ(go_reason(inc_db, "a/a.go", "b.T.M"), "missing");
ASSERT_STR_EQ(go_tier(inc_db, "b.M", "b.Target"), "unique");
ASSERT_STR_EQ(go_tier(inc_db, "b.U.M", "b.Target"), "unique");
ASSERT_STR_EQ(go_reason(inc_db, "b/c.go", "Target"), "");
/* declaration order decides which declaration keeps a shared name:
* swapping two methods moves the node, for files with no edge into it too */
/* declaration order no longer decides anything: swapping two methods of
* the same name keeps both nodes and both links */
go_w(repo, "b/d.go", GO_INC_D_SWAPPED);
ASSERT_EQ(dm_step(repo, inc_db, full_db, "methods swapped", CBM_INCREMENTAL_ROUTE_FORCED_FULL),
0);
ASSERT_STR_EQ(go_reason(inc_db, "a/a.go", "b.W.Name"), "graph_gap");
/* and the scope blob does not change (no method w moves), so the step
* is an ordinary closure repair, no longer a forced full rebuild */
ASSERT_EQ(
dm_step(repo, inc_db, full_db, "methods swapped", CBM_INCREMENTAL_ROUTE_CLOSURE_REPAIR), 0);
ASSERT_STR_EQ(go_reason(inc_db, "a/a.go", "b.W.Name"), "");
ASSERT_STR_EQ(go_reason(inc_db, "a/a.go", "b.V.Name"), "");
ASSERT_STR_EQ(go_tier(inc_db, "a.Use", "b.W.Name"), "exact");
ASSERT_STR_EQ(go_tier(inc_db, "a.Use", "b.V.Name"), "exact");
/* renaming a target */
go_w(repo, "b/b.go", GO_INC_B_RENAME);
@@ -2006,10 +2047,20 @@ TEST(doc_mentions_go_scope_delta_rules) {
GO_DELTA_HEAD GO_DELTA_F GO_DELTA_G GO_DELTA_T GO_DELTA_M GO_DELTA_I GO_DELTA_C
"var (\n\tV = 2\n)\n",
CBM_DOCLINK_DELTA_GLOBAL, NULL},
/* the declaration that keeps a shared node name changes */
{"same-named declarations swapped", twins,
"package p\n\nfunc (T) Open() {}\nfunc Open() {}\n", CBM_DOCLINK_DELTA_GLOBAL, NULL},
{"one of two same-named declarations removed", twins, "package p\n\nfunc Open() {}\n",
/* a method and a function of one name no longer share a node
* (#1913): swapping them changes no keeper */
{"method and function swapped", twins, "package p\n\nfunc (T) Open() {}\nfunc Open() {}\n",
CBM_DOCLINK_DELTA_LOCAL, ""},
/* the declaration that keeps a shared node name changes: two
* package-level declarations of one name still share <dir>.<name> */
{"same-named declarations swapped", "package p\n\nfunc Open() {}\ntype Open struct{}\n",
"package p\n\ntype Open struct{}\nfunc Open() {}\n", CBM_DOCLINK_DELTA_GLOBAL, NULL},
/* the method of a method/function pair removed: a member left, the
* function keeps its node either way */
{"method of a same-named pair removed", twins, "package p\n\nfunc Open() {}\n",
CBM_DOCLINK_DELTA_LOCAL, "Open"},
{"one of two same-named declarations removed",
"package p\n\nfunc Open() {}\ntype Open struct{}\n", "package p\n\nfunc Open() {}\n",
CBM_DOCLINK_DELTA_GLOBAL, NULL},
/* other files follow `type X = q.Y` through this file's imports */
{"import changed under a qualified alias", alias,
@@ -2091,7 +2142,10 @@ TEST(doc_mentions_go_workers_agree) {
ASSERT_STR_EQ(go_tier(par_db, "p00.T0", "p01.T0"), "exact");
ASSERT_STR_EQ(go_reason(par_db, "p00/f0.go", "p01.Gone"), "missing");
ASSERT_STR_EQ(go_reason(par_db, "p00/f0.go", "fmt.Println"), "external");
ASSERT_STR_EQ(go_reason(par_db, "p00/f0.go", "p01.T3.Close"), "graph_gap");
/* #1913: [p01.T3.Close] lands on its own method node now; the graph_gap
* reason stays covered by Shared, one name declared by every file */
ASSERT_STR_EQ(go_reason(par_db, "p00/f0.go", "p01.T3.Close"), "");
ASSERT_STR_EQ(go_tier(par_db, "p00.T0", "p01.T3.Close"), "exact");
ASSERT_STR_EQ(go_reason(par_db, "p00/f3.go", "Shared"), "graph_gap");
ASSERT_TRUE(dm_count(par_db, "SELECT COUNT(*) FROM edges WHERE type = 'MENTIONS'") > 100);
dm_unlink_db(par_db);
+128 -8
View File
@@ -6683,12 +6683,12 @@ TEST(extract_go_no_filename_in_module_qn) {
ASSERT_NOT_NULL(conn);
ASSERT_STR_EQ(conn->qualified_name, "proj.myapp.db.Conn");
/* Go method nodes keep a FLAT QN (module + name) with a separate
* parent_class link to the receiver type — the QN must carry the
/* Go method nodes carry a receiver-qualified QN (module + receiver type +
* name) plus the parent_class link — the QN must carry the
* directory-based module and NOT the `.conn.` filename segment. */
const CBMDefinition *query = find_def_by_name(r, "Query");
ASSERT_NOT_NULL(query);
ASSERT_STR_EQ(query->qualified_name, "proj.myapp.db.Query");
ASSERT_STR_EQ(query->qualified_name, "proj.myapp.db.Conn.Query");
ASSERT_EQ(strstr(query->qualified_name, ".conn."), NULL);
/* The method's parent_class must match the type node QN (for DEFINES_METHOD). */
ASSERT_NOT_NULL(query->parent_class);
@@ -7353,12 +7353,17 @@ TEST(extract_rust_generic_impl_call_scope_matches_def) {
PASS();
}
/* Languages OUTSIDE the is_method flag set (only Perl and TS/JS/TSX set it) must
* be unaffected: a Go method call never sets is_method. */
/* Languages OUTSIDE the is_method flag set (Perl, Python, TS/JS/TSX and Go set
* it) must be unaffected: a Rust method call never sets is_method. */
TEST(extract_flag_exempt_method_call_not_flagged_is_method) {
CBMFileResult *r = extract("package m\n"
"func run(o Obj) { o.Commit(); helper() }\n",
CBM_LANG_GO, "t", "x.go");
/* Rust is flag-exempt: only Perl, Python, TS/JS and Go set is_method.
* Guards the blast radius of the receiver-aware flags for every other
* language. */
CBMFileResult *r = extract("fn run(o: Obj) {\n"
" o.commit();\n"
" helper();\n"
"}\n",
CBM_LANG_RUST, "t", "x.rs");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
for (int i = 0; i < r->calls.count; i++) {
@@ -7516,6 +7521,33 @@ TEST(extract_python_bare_call_flags_locally_bound_callee) {
PASS();
}
TEST(extract_go_selector_call_flags_is_method) {
/* Go selector calls are flagged so the weak-match guard can fire when the
* Go LSP cannot type the receiver; bare calls stay unflagged. */
CBMFileResult *r = extract("package m\n"
"func run(o Obj) { o.Commit(); helper() }\n",
CBM_LANG_GO, "t", "x.go");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
bool saw_selector = false;
bool saw_bare = false;
for (int i = 0; i < r->calls.count; i++) {
const CBMCall *c = &r->calls.items[i];
if (c->callee_name && strstr(c->callee_name, "Commit") != NULL) {
ASSERT_TRUE(c->is_method);
saw_selector = true;
}
if (c->callee_name && strcmp(c->callee_name, "helper") == 0) {
ASSERT_FALSE(c->is_method);
saw_bare = true;
}
}
ASSERT_TRUE(saw_selector);
ASSERT_TRUE(saw_bare);
cbm_free_result(r);
PASS();
}
/* The bare-call flag is DEPTH-INDEPENDENT, and the binding is UNWOUND when its
* scope closes.
*
@@ -7621,6 +7653,91 @@ TEST(extract_python_bare_call_flag_is_depth_independent) {
PASS();
}
TEST(extract_go_method_receiver_qualified_qn) {
/* A Go method QN carries the receiver type (proj.pkg.Recv.method), same
* shape as C++ out-of-line methods and Go interface members — so two
* same-name methods on different receivers no longer collide in the
* graph upsert. Free functions keep the flat package QN. */
CBMFileResult *r = extract("package m\n"
"type Storage struct{}\n"
"type Cache struct{}\n"
"func (s *Storage) Close() {}\n"
"func (c Cache) Close() {}\n"
"func Shutdown() {}\n",
CBM_LANG_GO, "t", "x.go");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
bool saw_storage = false;
bool saw_cache = false;
bool saw_free = false;
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (!d->name || !d->qualified_name) {
continue;
}
if (strcmp(d->name, "Close") == 0 && strcmp(d->qualified_name, "t.Storage.Close") == 0) {
ASSERT_STR_EQ(d->label, "Method");
ASSERT_STR_EQ(d->parent_class, "t.Storage");
saw_storage = true;
}
if (strcmp(d->name, "Close") == 0 && strcmp(d->qualified_name, "t.Cache.Close") == 0) {
ASSERT_STR_EQ(d->parent_class, "t.Cache");
saw_cache = true;
}
if (strcmp(d->name, "Shutdown") == 0) {
ASSERT_STR_EQ(d->qualified_name, "t.Shutdown");
saw_free = true;
}
}
ASSERT_TRUE(saw_storage);
ASSERT_TRUE(saw_cache);
ASSERT_TRUE(saw_free);
cbm_free_result(r);
PASS();
}
/* Templ shares Go's method grammar (method_declaration with a receiver), but
* only Go method QNs are receiver-qualified (#1913); templ's call-scope QN
* stays flat. The method's def QN and the enclosing-function QN of a call
* inside it must agree, or the call has no source node and degrades to the
* File node. */
TEST(extract_templ_method_qn_matches_call_scope) {
CBMFileResult *r = extract("package views\n"
"\n"
"type Page struct{}\n"
"\n"
"func (p Page) Title() string {\n"
"\treturn helper()\n"
"}\n"
"\n"
"func helper() string { return \"x\" }\n"
"\n"
"templ hello(name string) {\n"
"\t<div>{ name }</div>\n"
"}\n",
CBM_LANG_TEMPL, "t", "x.templ");
ASSERT_NOT_NULL(r);
const char *title_qn = NULL;
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (d->name && d->qualified_name && strcmp(d->name, "Title") == 0) {
title_qn = d->qualified_name;
}
}
const char *scope = NULL;
for (int i = 0; i < r->calls.count; i++) {
const CBMCall *c = &r->calls.items[i];
if (c->callee_name && strcmp(c->callee_name, "helper") == 0) {
scope = c->enclosing_func_qn;
}
}
ASSERT_NOT_NULL(title_qn);
ASSERT_NOT_NULL(scope);
ASSERT_STR_EQ(scope, title_qn);
cbm_free_result(r);
PASS();
}
/* TS/JS/TSX receiver-aware flag (#592/#606; same intent as the Perl flag above).
* A member call x.foo() with a non-this/super receiver is flagged is_method so
* the resolver can suppress a weak short-name match (`re.test()` must not bind a
@@ -12821,6 +12938,9 @@ SUITE(extraction) {
RUN_TEST(extract_python_member_call_flags_is_method);
RUN_TEST(extract_python_bare_call_flags_locally_bound_callee);
RUN_TEST(extract_python_bare_call_flag_is_depth_independent);
RUN_TEST(extract_go_selector_call_flags_is_method);
RUN_TEST(extract_go_method_receiver_qualified_qn);
RUN_TEST(extract_templ_method_qn_matches_call_scope);
RUN_TEST(extract_ts_member_call_flags_is_method);
RUN_TEST(extract_ts_this_super_receiver_not_flagged);
RUN_TEST(extract_js_member_call_flags_is_method);
+3 -2
View File
@@ -4080,8 +4080,9 @@ TEST(parallel_go_cross_package_field_chain_resolves) {
cbm_gbuf_t *gbuf = run_go_field_chain_sequential("go_field_fold", tmpdir, files, 2);
ASSERT_NOT_NULL(gbuf);
const cbm_gbuf_edge_t *edge =
find_call_edge_to_target_fragment(gbuf, "handler.PlaceOrder", ".service.PlaceOrder");
/* Since #1909, Go method QNs are receiver-qualified on both endpoints. */
const cbm_gbuf_edge_t *edge = find_call_edge_to_target_fragment(
gbuf, "OrderHandler.PlaceOrder", ".service.OrderService.PlaceOrder");
const bool found = edge != NULL;
const bool dispatch = edge && edge->properties_json &&
strstr(edge->properties_json, "\"strategy\":\"lsp_type_dispatch\"");
+280
View File
@@ -8761,6 +8761,283 @@ TEST(pipeline_html_embedded_member_call_stays_unbound) {
PASS();
}
/* CALLS edges whose source and target QNs end with the given suffixes and
* whose serialized properties contain `props_fragment` (NULL: any edge). QN
* suffixes pin the exact node, not just a same-named one. */
static int go_calls_between(cbm_store_t *s, const char *project, const char *caller_suffix,
const char *callee_suffix, const char *props_fragment) {
cbm_edge_t *edges = NULL;
int edge_count = 0;
cbm_store_find_edges_by_type(s, project, "CALLS", &edges, &edge_count);
size_t cs = strlen(caller_suffix);
size_t ts = strlen(callee_suffix);
int hits = 0;
for (int i = 0; i < edge_count; i++) {
cbm_node_t src = {0};
cbm_node_t tgt = {0};
if (cbm_store_find_node_by_id(s, edges[i].source_id, &src) == CBM_STORE_OK &&
cbm_store_find_node_by_id(s, edges[i].target_id, &tgt) == CBM_STORE_OK &&
src.qualified_name && tgt.qualified_name) {
size_t sl = strlen(src.qualified_name);
size_t tl = strlen(tgt.qualified_name);
if (sl >= cs && tl >= ts && strcmp(src.qualified_name + sl - cs, caller_suffix) == 0 &&
strcmp(tgt.qualified_name + tl - ts, callee_suffix) == 0 &&
(!props_fragment ||
(edges[i].properties_json && strstr(edges[i].properties_json, props_fragment)))) {
hits++;
}
}
cbm_node_free_fields(&src);
cbm_node_free_fields(&tgt);
}
if (edges) {
cbm_store_free_edges(edges, edge_count);
}
return hits;
}
TEST(pipeline_go_method_caller_keeps_lsp_join) {
/* #1909: receiver-qualifying method QNs moves the def/textual-call QN to
* package.Type.method — go_lsp's enclosing-function QN must move with it
* (the ONE-formula contract), or every LSP resolution sourced from inside
* a method body loses its caller join and the edge dies. The callee name
* collides across receivers so no short-name fallback can mask the loss. */
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_mcall_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "go.mod", "module example.com/fxmcall\n\ngo 1.22\n");
write_temp_file(tmp, "svc/repo.go",
"package svc\n"
"\n"
"type Repo struct {\n"
"\tn int\n"
"}\n"
"\n"
"func (r *Repo) Install() int {\n"
"\treturn r.helper()\n"
"}\n");
write_temp_file(tmp, "svc/helper.go",
"package svc\n"
"\n"
"func (r *Repo) helper() int {\n"
"\treturn r.n\n"
"}\n");
write_temp_file(tmp, "other/other.go",
"package other\n"
"\n"
"type Decoy struct{}\n"
"\n"
"func (d *Decoy) helper() int { return 2 }\n");
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/go_mcall.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
ASSERT_TRUE(cross_file_call_exists(s, project, "Install", "helper"));
/* Pinned to the receiver-qualified target in svc/helper.go and to an LSP
* strategy: a name-only binding, or one onto other.Decoy.helper, would
* still satisfy the by-name check above. */
ASSERT_GTE(go_calls_between(s, project, ".svc.Repo.Install", ".svc.Repo.helper",
"\"strategy\":\"lsp_"),
1);
ASSERT_EQ(go_calls_between(s, project, ".svc.Repo.Install", ".other.Decoy.helper", NULL), 0);
cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
PASS();
}
/* Fixture for the #1906 Go selector-guard probes (sequential and parallel
* twins). pad_files > 0 adds filler Go files to push the index over
* MIN_FILES_FOR_PARALLEL, because the guard is wired into both resolvers
* (pass_calls.c and pass_parallel.c) and each needs its own probe. */
static void write_go_receiver_fixture(const char *tmp, int pad_files) {
/* go.mod makes project imports resolvable — real Go repos always have one,
* and import reachability (the unique_name penalty) depends on it. */
write_temp_file(tmp, "go.mod", "module example.com/myapp\n\ngo 1.22\n");
/* The lone project symbol named "Close" — a real method. */
write_temp_file(tmp, "storage/storage.go",
"package storage\n"
"\n"
"type Storage struct{ open bool }\n"
"\n"
"func NewStorage() *Storage { return &Storage{open: true} }\n"
"\n"
"func (s *Storage) Close() {\n"
"\ts.open = false\n"
"}\n"
"\n"
"func Boot() {\n"
"\ts := NewStorage()\n"
"\ts.Close()\n"
"}\n");
/* Cross-package control target: imported by hash.go, so the caller file
* has a non-empty import map (like any real Go file) and unreachable
* unique_name candidates get the import penalty. */
write_temp_file(tmp, "util/util.go",
"package util\n"
"\n"
"func Tag() string { return \"t\" }\n");
/* Stdlib receiver: `f.Close()` closes an *os.File, NOT the project method.
* The Go LSP cannot bind it to a project symbol → the registry would guess
* Close by short name (weak). This is the false edge to suppress —
* the exact shape that attached every file/rows/gzip Close in a real Go
* repo to one unrelated project method. */
write_temp_file(tmp, "hash/hash.go",
"package hash\n"
"\n"
"import (\n"
"\t\"os\"\n"
"\n"
"\t\"example.com/myapp/util\"\n"
")\n"
"\n"
"func FileLen(path string) int64 {\n"
"\tf, err := os.Open(path)\n"
"\tif err != nil {\n"
"\t\treturn 0\n"
"\t}\n"
"\tdefer f.Close()\n"
"\tst, err := f.Stat()\n"
"\tif err != nil {\n"
"\t\treturn 0\n"
"\t}\n"
"\treturn st.Size()\n"
"}\n"
"\n"
"func localHelper() int { return 1 }\n"
"\n"
"func CallsLocal() int { return localHelper() }\n"
"\n"
"func UsesUtil() string { return util.Tag() }\n");
/* F-G9: two project methods named Run, one under a directory whose QN
* carries an uppercase T. `t.Run` on a *testing.T must bind neither. The
* parallel resolver field-type hint matched the capitalized receiver
* variable ("T") as a QN substring and produced a par-only edge. */
write_temp_file(tmp, "TBench/bench.go",
"package tbench\n"
"\n"
"type Bench struct{}\n"
"\n"
"func (b *Bench) Run(name string) bool { return name != \"\" }\n");
write_temp_file(tmp, "worker/worker.go",
"package worker\n"
"\n"
"type Worker struct{}\n"
"\n"
"func (w *Worker) Run(name string) bool { return false }\n");
write_temp_file(tmp, "hash/hash_test.go",
"package hash\n"
"\n"
"import \"testing\"\n"
"\n"
"func TestSubRun(t *testing.T) {\n"
"\tt.Run(\"a\", func(t *testing.T) {})\n"
"}\n");
for (int i = 0; i < pad_files; i++) {
char name[64];
char body[128];
snprintf(name, sizeof(name), "filler/filler%d.go", i);
snprintf(body, sizeof(body), "package filler\n\nfunc Filler%d() int { return %d }\n", i, i);
write_temp_file(tmp, name, body);
}
}
TEST(pipeline_go_receiver_suppresses_weak_method_edge) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_recv_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_go_receiver_fixture(tmp, 0);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/go_recv.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
/* (1) The false edge is suppressed (reproduce-first: RED before the fix). */
ASSERT_FALSE(cross_file_call_exists(s, project, "FileLen", "Close"));
/* (2) The same-package typed-receiver call survives (LSP / same_module —
* both outside the weak drop-list). */
ASSERT_TRUE(cross_file_call_exists(s, project, "Boot", "Close"));
/* (3) The bare local call survives (is_method stays false for bare calls). */
ASSERT_TRUE(cross_file_call_exists(s, project, "CallsLocal", "localHelper"));
/* (4) The import-qualified cross-package call survives (import-aware
* strategies are outside the drop-list). */
ASSERT_TRUE(cross_file_call_exists(s, project, "UsesUtil", "Tag"));
/* (5) F-G9: the stdlib *testing.T Run binds no project Run. */
ASSERT_FALSE(cross_file_call_exists(s, project, "TestSubRun", "Run"));
cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
PASS();
}
/* Parallel twin of the test above. The guard is wired into pass_parallel.c as
* well as pass_calls.c, but the four-file fixture only ever runs the
* sequential resolver, so the parallel hunk was bound by no test. Real
* repositories take the parallel path: >= MIN_FILES_FOR_PARALLEL (50) files
* plus CBM_WORKERS=4 force it, the shape of
* pipeline_python_receiver_parallel_suppresses_weak_method_edges. Same
* both-directions pin as the sequential test. */
TEST(pipeline_go_receiver_parallel_suppresses_weak_method_edge) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_go_recv_par_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_go_receiver_fixture(tmp, 52);
char *old_workers = getenv("CBM_WORKERS");
char *saved = old_workers ? strdup(old_workers) : NULL;
cbm_setenv("CBM_WORKERS", "4", 1);
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/go_recv_par.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
/* NEGATIVE: the stdlib *os.File Close must not bind the project method. */
ASSERT_FALSE(cross_file_call_exists(s, project, "FileLen", "Close"));
/* POSITIVE: typed same-package, bare local and import-qualified calls
* survive the parallel resolver too. */
ASSERT_TRUE(cross_file_call_exists(s, project, "Boot", "Close"));
ASSERT_TRUE(cross_file_call_exists(s, project, "CallsLocal", "localHelper"));
ASSERT_TRUE(cross_file_call_exists(s, project, "UsesUtil", "Tag"));
/* F-G9: the parallel-only field-type hint must not bind `t.Run` either. */
ASSERT_FALSE(cross_file_call_exists(s, project, "TestSubRun", "Run"));
cbm_store_close(s);
cbm_pipeline_free(p);
if (saved) {
cbm_setenv("CBM_WORKERS", saved, 1);
free(saved);
} else {
cbm_unsetenv("CBM_WORKERS");
}
th_rmtree(tmp);
PASS();
}
/* Fixture for the #1928 cross-language reference-guard probes (sequential and
* parallel twins). pad_files > 0 adds filler files to push the index over the
* parallel-pipeline threshold, since USAGE/WRITES/READS have one resolver per
@@ -26204,6 +26481,9 @@ SUITE(pipeline) {
RUN_TEST(pipeline_python_receiver_suppresses_weak_method_edge);
RUN_TEST(pipeline_python_receiver_keeps_specific_unique_name_member_call);
RUN_TEST(pipeline_html_embedded_member_call_stays_unbound);
RUN_TEST(pipeline_go_method_caller_keeps_lsp_join);
RUN_TEST(pipeline_go_receiver_suppresses_weak_method_edge);
RUN_TEST(pipeline_go_receiver_parallel_suppresses_weak_method_edge);
RUN_TEST(pipeline_go_multi_return_receiver_is_typed);
RUN_TEST(pipeline_go_multi_return_receiver_is_typed_parallel);
RUN_TEST(pipeline_go_rw_usage_never_cross_into_c);
+53
View File
@@ -1673,10 +1673,61 @@ TEST(weak_call_guards_share_one_drop_list) {
strategies[i], member, binding);
}
ASSERT_EQ(member, binding);
/* The Go selector guard builds on the same list. At a penalized
* confidence (0.75 * 0.5) it agrees with it exactly; its one narrowing
* (an unpenalized unique_name) does not apply here. Any drift from
* the shared list fails here. */
bool go = cbm_go_suppress_weak_method_match(true, true, strategies[i], 0.375);
bool go_expected = member;
if (go != go_expected) {
printf(" Go drop-list divergence on strategy \"%s\": go=%d expected=%d\n",
strategies[i], go, go_expected);
}
ASSERT_EQ(go, go_expected);
}
PASS();
}
TEST(go_suppress_drops_weak_selector_matches) {
/* Go selector call with an untyped receiver, landed via a receiver-blind
* short-name strategy → drop (same failure class as #592/#606).
* suffix_match/fuzzy drop at any confidence; unique_name drops only when
* import-unreachability-penalized (CONF_UNIQUE_NAME 0.75 * 0.5 = 0.375 —
* the `io.Copy` -> project `Copy` stdlib-hijack shape). */
ASSERT_TRUE(cbm_go_suppress_weak_method_match(true, true, "suffix_match", 0.9));
ASSERT_TRUE(cbm_go_suppress_weak_method_match(true, true, "suffix_match", 0.11));
ASSERT_TRUE(cbm_go_suppress_weak_method_match(true, true, "fuzzy", 0.9));
ASSERT_TRUE(cbm_go_suppress_weak_method_match(true, true, "unique_name", 0.375));
/* field_type_hint is a receiver-NAME substring match, not type evidence
* (F-G9): weak for Go like the shared list. */
ASSERT_TRUE(cbm_go_suppress_weak_method_match(true, true, "field_type_hint", 0.85));
PASS();
}
TEST(go_suppress_keeps_typed_and_import_aware_matches) {
/* Unpenalized unique_name = lone candidate inside the caller's import
* closure (or an import-free file, e.g. same-package) — a genuinely-typed
* lone-candidate call never enters the field-type-hint upgrade, so it must
* survive (lrp_go_s8_field_type_hint). */
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "unique_name", 0.75));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "same_module", 0.9));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "import_map", 0.95));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "import_map_suffix", 0.9));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "qualified_suffix", 0.9));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "callee_suffix", 0.5));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "service_pattern", 0.5));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "lsp_strategy_cross_file", 0.92));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "lsp_direct", 0.95));
/* A bare call (is_method=false) is a free-function call → never suppressed. */
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, false, "suffix_match", 0.11));
/* Non-Go languages are never affected by this gate. */
ASSERT_FALSE(cbm_go_suppress_weak_method_match(false, true, "suffix_match", 0.11));
/* No match (NULL/empty strategy) → nothing to suppress. */
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, NULL, 0.5));
ASSERT_FALSE(cbm_go_suppress_weak_method_match(true, true, "", 0.5));
PASS();
}
/* ── Suite ─────────────────────────────────────────────────────── */
/* Method call THROUGH an imported symbol that is itself an indexed node
@@ -2235,6 +2286,8 @@ SUITE(registry) {
RUN_TEST(python_import_binding_contradicts_only_foreign_chains);
RUN_TEST(python_external_base_contradicts_django_shapes);
RUN_TEST(weak_call_guards_share_one_drop_list);
RUN_TEST(go_suppress_drops_weak_selector_matches);
RUN_TEST(go_suppress_keeps_typed_and_import_aware_matches);
RUN_TEST(registry_python_function_local_is_exact_only_issue1548);
RUN_TEST(registry_resolve_lexical_walks_enclosing_defs_issue1548);
RUN_TEST(registry_python_local_bound_at_own_file_issue1548);