Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions internal/cbm/extract_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -3619,6 +3619,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"));
// ObjectScript stores args under oref_method/method_args, not the
Expand Down
35 changes: 32 additions & 3 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -3508,7 +3508,7 @@ static void set_def_complexity(CBMDefinition *def, TSNode body, const CBMLangSpe
* Walks to the parameter_declaration's `type` field, unwrapping pointer_type
* and generic_type, and returns the type_identifier text (e.g. "OrderService").
* Returns NULL if no type_identifier is found. */
static char *go_receiver_type_name(CBMArena *a, TSNode recv, const char *source) {
char *cbm_go_receiver_type_name(CBMArena *a, TSNode recv, const char *source) {
uint32_t nc = ts_node_child_count(recv);
for (uint32_t i = 0; i < nc; i++) {
TSNode child = ts_node_child(recv, i);
Expand Down Expand Up @@ -3720,13 +3720,42 @@ 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. */
def.qualified_name = cbm_arena_sprintf(a, "%s.%s", def.parent_class, name);
}
}

/* Go allows any number of init() functions per package — even several in
* one file — and they all run at start-up. On the flat QN they all
* collided and the graph upsert kept ONE node per package, silently
* dropping the rest (#1910). Disambiguate with the #495 cfg-twin pattern:
* fold the file basename and the init's per-file ordinal into the QN.
* The ordinal (not the line) keeps the QN stable when code is edited
* above the function — the QN is node identity, and a line suffix would
* churn nodes on every unrelated insertion. Nothing ever joins on the
* plain QN — calling init explicitly is illegal in Go — and the
* call-scope side (compute_func_qn in extract_unified.c) computes the
* same cbm_go_init_ordinal so init-body calls keep their attribution. */
if (ctx->language == CBM_LANG_GO && strcmp(def.label, "Function") == 0 &&
strcmp(name, "init") == 0) {
const char *base = strrchr(ctx->rel_path, '/');
base = base ? base + 1 : ctx->rel_path;
def.qualified_name = cbm_arena_sprintf(a, "%s#%s:%d", def.qualified_name, base,
cbm_go_init_ordinal(node, ctx->source));
}

// C++/CUDA: out-of-line method definition (`Foo::bar` in a .cc/.cpp). The
Expand Down
35 changes: 35 additions & 0 deletions internal/cbm/extract_unified.c
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,41 @@ 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);
return cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, name);
}
}
}

/* Go init(): the def extractor folds the file basename and the init's
* per-file ordinal into the QN so every init in a package survives the
* upsert (#1910, the #495 cfg-twin pattern; ordinal, not line, so the QN
* is stable under edits above the function). Mirror the exact formula
* here, or init-body calls fall back to File-node attribution
* (calls_find_source). */
if (ctx->language == CBM_LANG_GO && strcmp(name, "init") == 0 &&
strcmp(ts_node_type(node), "function_declaration") == 0) {
const char *base_qn = cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path,
name, ctx->language);
const char *base = strrchr(ctx->rel_path, '/');
base = base ? base + 1 : ctx->rel_path;
return cbm_arena_sprintf(ctx->arena, "%s#%s:%d", base_qn, base,
cbm_go_init_ordinal(node, ctx->source));
}

/* Nix: a binding's own attrpath contributes scope (`a.b.fn = …`), and the def
* extractor bakes it into the def QN. Compose it identically here — otherwise
* an in-body call sources to a QN one or more segments short of the def, and
Expand Down
36 changes: 36 additions & 0 deletions internal/cbm/helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -1734,6 +1734,42 @@ int cbm_classify_string(const char *str, int len) {
* canonical parameter shape of server-side route paths
* (`/things/${id}/x` -> "/things/{}/x"). Returns NULL when the node yields
* no text or exceeds the route-sized buffer. */
/* 1-based source-order ordinal of a top-level Go `init` function_declaration
* among the file's `init`s. Both the definition extractor and compute_func_qn
* fold it into the QN (`pkg.init#file.go:N`, #1910), so it must be computed
* from the tree on both sides with this one formula. Unlike a line number,
* the ordinal is stable under edits above the function — QNs are node
* identity, and a line-based suffix would churn nodes on every unrelated
* insertion (the #495 cfg suffix and the GoogleTest derivation are both
* edit-stable for the same reason). */
int cbm_go_init_ordinal(TSNode fn_node, const char *source) {
TSNode root = fn_node;
for (TSNode p = ts_node_parent(root); !ts_node_is_null(p); p = ts_node_parent(p)) {
root = p;
}
int ordinal = 0;
uint32_t n = ts_node_named_child_count(root);
for (uint32_t i = 0; i < n; i++) {
TSNode child = ts_node_named_child(root, i);
if (strcmp(ts_node_type(child), "function_declaration") != 0) {
continue;
}
TSNode name = ts_node_child_by_field_name(child, TS_FIELD("name"));
if (ts_node_is_null(name)) {
continue;
}
uint32_t start = ts_node_start_byte(name);
if (ts_node_end_byte(name) - start != 4 || memcmp(source + start, "init", 4) != 0) {
continue;
}
ordinal++;
if (ts_node_eq(child, fn_node)) {
return ordinal;
}
}
return ordinal;
}

const char *cbm_template_string_text(CBMArena *a, TSNode node, const char *source) {
enum { TPL_BUF = 512 };
char buf[TPL_BUF];
Expand Down
10 changes: 10 additions & 0 deletions internal/cbm/helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ TSNode cbm_resolve_func_name(TSNode node, CBMLanguage lang);
// def extractor — drift dropped the class qualifier from in-body calls (#554/#621).
char *cbm_cpp_out_of_line_parent_class(CBMArena *a, TSNode node, const char *source);

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

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

Expand Down Expand Up @@ -191,6 +197,10 @@ bool cbm_is_loop_node_type(const char *kind);
// Is this a module-level node? (not nested inside function/class body)
bool cbm_is_module_level(TSNode node, CBMLanguage lang);

/* 1-based source-order ordinal of a Go `init` among its file's top-level
* `init` function_declarations (QN disambiguation, #1910). */
int cbm_go_init_ordinal(TSNode fn_node, const char *source);

// Same check, but the node's PARENT is supplied directly — avoids the
// O(n) ts_node_parent rescan. Use at call sites iterating a known
// parent's children (the common case). `parent` is the parent of the
Expand Down
25 changes: 17 additions & 8 deletions internal/cbm/lsp/go_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -1859,15 +1859,24 @@ static void process_function(GoLSPContext* ctx, TSNode func_node) {
char* func_name = lsp_node_text(ctx, name_node);
if (!func_name || !func_name[0]) return;

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

// Push function scope
CBMScope* saved_scope = ctx->current_scope;
Expand Down
12 changes: 10 additions & 2 deletions src/pipeline/pass_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -623,12 +623,20 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
* 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;
bool drop_plain_call =
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy);
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy) ||
cbm_go_suppress_weak_method_match(lang == CBM_LANG_GO, call->is_method, res.strategy,
res.confidence);

/* Service-pattern HTTP/ASYNC calls to an EXTERNAL client library (e.g.
* `requests.get("/api/orders/{id}")`) resolve to a QN containing the library
Expand Down
8 changes: 6 additions & 2 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -2488,12 +2488,16 @@ 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;
bool drop_plain_call =
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy);
cbm_suppress_weak_member_match(suppress_weak_member, call->is_method, res.strategy) ||
cbm_go_suppress_weak_method_match(lang == CBM_LANG_GO, call->is_method, res.strategy,
res.confidence);

/* Service-pattern HTTP/ASYNC client call (`requests.get(url)`): the
* service signal lives in the callee_name. The registry can mis-resolve
Expand Down
9 changes: 9 additions & 0 deletions src/pipeline/pipeline.h
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,15 @@ bool cbm_perl_suppress_generic_match(bool is_perl, bool is_method, const char *c
* Pure; unit-tested in test_registry.c. */
bool cbm_suppress_weak_member_match(bool enabled, bool is_method, 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);

/* #725: drop a suffix_match CALLS edge when the caller language and the
* target file's language disagree. unique_name (candidates == 1) is #1572
* and is left alone; same_module / import_map / lsp_* are kept. JS/TS/TSX
Expand Down
27 changes: 27 additions & 0 deletions src/pipeline/registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,33 @@ bool cbm_suppress_weak_member_match(bool enabled, bool is_method, const char *st
strcmp(strategy, "field_type_hint") == 0 || strcmp(strategy, "fuzzy") == 0;
}

bool cbm_go_suppress_weak_method_match(bool is_go, bool is_method, const char *strategy,
double confidence) {
if (!is_go || !is_method || !strategy || !strategy[0]) {
return false;
}
/* 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). Unlike the TS/JS list, field_type_hint is KEPT: a Go struct
* field carries a declared type, so the parallel resolver's field-type
* hint is receiver-aware for Go (lrp_go_s8_field_type_hint), not a
* heuristic. */
if (strcmp(strategy, "suffix_match") == 0 || strcmp(strategy, "fuzzy") == 0) {
return true;
}
/* 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 strcmp(strategy, "unique_name") == 0 && confidence < CONF_UNIQUE_NAME;
}

static bool js_ts_family(CBMLanguage lang) {
return lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX ||
lang == CBM_LANG_ARKTS;
Expand Down
Loading
Loading