mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-21 05:59:35 +08:00
Merge pull request #1913 from ilyabrykau-orca/fix/go-method-receiver-qn
fix(extract): receiver-qualify Go method QNs
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
@@ -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\"");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user