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 @@ -3551,6 +3551,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
32 changes: 29 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,39 @@ 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 line into the QN. 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) mirrors this
* exact formula so init-body calls keep their source 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:L%d", def.qualified_name, base, (int)def.start_line);
}

// C++/CUDA: out-of-line method definition (`Foo::bar` in a .cc/.cpp). The
Expand Down
33 changes: 33 additions & 0 deletions internal/cbm/extract_unified.c
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,39 @@ 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 line into the
* QN so every init in a package survives the upsert (#1910, the #495
* cfg-twin pattern). 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:L%d", base_qn, base,
(int)ts_node_start_point(node).row + 1);
}

/* Nix: a binding's own attrpath contributes scope (`a.b.fn = …`), and the def
* extractor bakes it into the def QN. Compose it identically here — otherwise
* an in-body call sources to a QN one or more segments short of the def, and
Expand Down
6 changes: 6 additions & 0 deletions internal/cbm/helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 @@ -2478,12 +2478,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