Skip to content
Draft
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
66 changes: 66 additions & 0 deletions internal/cbm/cbm.c
Original file line number Diff line number Diff line change
Expand Up @@ -1156,6 +1156,54 @@ static void cbm_subtract_macro_invocation_regions(cbm_error_regions_t *regs,
}

/* Serialize collected regions as "start-end,start-end,..." into the arena. */
/* #1929: the cgo preamble — the comment run directly above `import "C"` — is
* C source the Go grammar consumes as a comment, so every definition in it is
* absent from the graph while parse coverage reports the file as fully
* indexed. Collect that run as an unparsed region so the gap is visible. */
static void cbm_flag_go_cgo_preamble(cbm_error_regions_t *regs, TSNode root, const char *source) {
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), "import_declaration") != 0) {
continue;
}
/* The spec path must be exactly "C" — the three bytes '"C"' appear in
* the clause text only for the pseudo-package (a longer path puts
* other characters inside the quotes). */
uint32_t sb = ts_node_start_byte(child);
uint32_t eb = ts_node_end_byte(child);
bool is_cgo = false;
for (uint32_t b = sb; b + 2 < eb; b++) {
if (source[b] == '"' && source[b + 1] == 'C' && source[b + 2] == '"') {
is_cgo = true;
break;
}
}
if (!is_cgo) {
continue;
}
TSNode prev = ts_node_prev_named_sibling(child);
if (ts_node_is_null(prev) || strcmp(ts_node_type(prev), "comment") != 0) {
continue; /* no preamble — nothing hidden */
}
uint32_t end_row = ts_node_end_point(prev).row;
uint32_t start_row = ts_node_start_point(prev).row;
while (true) {
TSNode before = ts_node_prev_named_sibling(prev);
if (ts_node_is_null(before) || strcmp(ts_node_type(before), "comment") != 0) {
break;
}
prev = before;
start_row = ts_node_start_point(prev).row;
}
if (regs->count < CBM_MAX_ERROR_REGIONS) {
regs->starts[regs->count] = start_row + 1;
regs->ends[regs->count] = end_row + 1;
regs->count++;
}
}
}

static const char *cbm_error_ranges_str(CBMArena *a, const cbm_error_regions_t *regs) {
if (regs->count <= 0) {
return NULL;
Expand Down Expand Up @@ -1651,6 +1699,24 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
}
}

/* #1929: a cgo file parses CLEAN — the C preamble is a comment to the Go
* grammar — so the block above never flags it, and coverage silently
* claims full indexing while every preamble definition is missing. Flag
* the preamble range explicitly. Detection aid only, like #963 above. */
if (language == CBM_LANG_GO) {
cbm_error_regions_t cgo_regs = {{0}, {0}, 0};
cbm_flag_go_cgo_preamble(&cgo_regs, root, source);
if (cgo_regs.count > 0) {
result->parse_incomplete = true;
result->error_region_count += cgo_regs.count;
const char *cgo_ranges = cbm_error_ranges_str(a, &cgo_regs);
result->error_ranges =
result->error_ranges
? cbm_arena_sprintf(a, "%s,%s", result->error_ranges, cgo_ranges)
: cgo_ranges;
}
}

result->imports_count = result->imports.count;

// Accumulate profiling counters
Expand Down
5 changes: 5 additions & 0 deletions internal/cbm/cbm.h
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ typedef struct {
bool is_abstract;
bool is_test;
bool is_entry_point;
/* #1929: a cgo `//export Name` directive names this Go function's C
* symbol — a declared ABI contract (cgo requires Name to equal the func
* name). C-family callers bind it by contract (export_linkage) instead of
* short-name luck. Go only; false elsewhere. */
bool is_cgo_export;
const char *structural_profile; // AST structural profile (arena-allocated) or NULL
const char *body_tokens; // space-separated raw identifier tokens from body (arena) or NULL
/* Rust only: raw trait path from the exact `impl Trait for Type` block
Expand Down
37 changes: 37 additions & 0 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -2055,6 +2055,35 @@ static bool rust_def_is_test(const char *const *decorators) {
return false;
}

/* #1929: does a cgo `//export <name>` directive sit in the comment run
* directly above this Go func def, naming exactly this function? cgo requires
* the exported name to equal the func name, so anything else is not a
* contract for THIS def. */
static bool go_def_has_cgo_export(CBMExtractCtx *ctx, TSNode func_node, const char *name) {
if (!name || !name[0]) {
return false;
}
static const char kPrefix[] = "//export ";
size_t name_len = strlen(name);
TSNode prev = ts_node_prev_named_sibling(func_node);
while (!ts_node_is_null(prev) && strcmp(ts_node_type(prev), "comment") == 0) {
char *text = cbm_node_text(ctx->arena, prev, ctx->source);
if (text && strncmp(text, kPrefix, sizeof(kPrefix) - 1) == 0) {
const char *exported = text + sizeof(kPrefix) - 1;
size_t elen = strlen(exported);
while (elen > 0 && (exported[elen - 1] == '\n' || exported[elen - 1] == '\r' ||
exported[elen - 1] == ' ' || exported[elen - 1] == '\t')) {
elen--;
}
if (elen == name_len && strncmp(exported, name, name_len) == 0) {
return true;
}
}
prev = ts_node_prev_named_sibling(prev);
}
return false;
}

static const char *rust_cfg_qualified_name(CBMArena *a, const char *base_qn,
const char *const *decorators) {
if (!decorators) {
Expand Down Expand Up @@ -3778,6 +3807,14 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
def.is_test = rust_def_is_test(def.decorators);
}

// Go: a cgo `//export Name` directive above the func declares its C ABI
// symbol (#1929). cgo requires Name to equal the func name; record only an
// exact match so the resolver upgrade cannot be spoofed by an unrelated
// directive in the same comment run.
if (ctx->language == CBM_LANG_GO) {
def.is_cgo_export = go_def_has_cgo_export(ctx, node, name);
}

// C++/CUDA: GoogleTest macros are test functions (#1266).
if (is_gtest) {
def.is_test = true;
Expand Down
15 changes: 15 additions & 0 deletions src/pipeline/pass_calls.c
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,11 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,

/* LSP-resolved calls take precedence over registry-textual matching.
* Unique-tail fallbacks are JVM-only (see cbm_pipeline_lsp_allow_tail_match). */
/* #1929: `C.<ident>` is the cgo pseudo-namespace — no resolution path may
* bind it to a project symbol. */
if (cbm_go_suppress_cgo_callee(lang == CBM_LANG_GO, call->callee_name)) {
return 0;
}
bool allow_tail = cbm_pipeline_lsp_allow_tail_match(lang);
const CBMResolvedCall *lsp = cbm_pipeline_find_lsp_resolution_in_graph(
lsp_calls, call, allow_tail, ctx->gbuf, ctx->project_name);
Expand Down Expand Up @@ -654,6 +659,16 @@ static int resolve_single_call(cbm_pipeline_ctx_t *ctx, CBMCall *call,
if (!target_node || source_node->id == target_node->id) {
return 0;
}
/* #1929: a Go //export function is a declared C ABI contract. A C-family
* caller naming it exactly binds by that contract, not by short-name luck
* — upgrade the strategy so the edge is honest and no weak-match guard
* can drop it. Mirrors pass_parallel.c. */
if ((lang == CBM_LANG_C || lang == CBM_LANG_CPP) && call->callee_name && target_node->name &&
strcmp(call->callee_name, target_node->name) == 0 && target_node->properties_json &&
strstr(target_node->properties_json, "\"cgo_export\":true")) {
res.strategy = "export_linkage";
res.confidence = CBM_EXPORT_LINKAGE_CONF;
}
/* #725: suffix_match is language-agnostic and will attach a Python
* Store.commit() call to a JS function named commit (or a Bash main
* to a Python main). Drop that weak cross-language edge. */
Expand Down
6 changes: 6 additions & 0 deletions src/pipeline/pass_definitions.c
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,12 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def)
return;
}
size_t pos = (size_t)n;
if (def->is_cgo_export && pos + 1 < bufsize) {
int m = snprintf(buf + pos, bufsize - pos, ",\"cgo_export\":true");
if (m > 0 && (size_t)m < bufsize - pos) {
pos += (size_t)m;
}
}
append_json_string(buf, bufsize, &pos, "docstring", def->docstring);
append_json_string(buf, bufsize, &pos, "signature", def->signature);
append_json_string(buf, bufsize, &pos, "return_type", def->return_type);
Expand Down
22 changes: 22 additions & 0 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,12 @@ static void build_def_props(char *buf, size_t bufsize, const CBMDefinition *def)
return;
}
size_t pos = (size_t)n;
if (def->is_cgo_export && pos + 1 < bufsize) {
int m = snprintf(buf + pos, bufsize - pos, ",\"cgo_export\":true");
if (m > 0 && (size_t)m < bufsize - pos) {
pos += (size_t)m;
}
}
append_json_string(buf, bufsize, &pos, "docstring", def->docstring);
append_json_string(buf, bufsize, &pos, "signature", def->signature);
append_json_string(buf, bufsize, &pos, "return_type", def->return_type);
Expand Down Expand Up @@ -2447,6 +2453,12 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
continue;
}

/* #1929: `C.<ident>` is the cgo pseudo-namespace — no resolution path
* may bind it to a project symbol. Mirrors pass_calls.c. */
if (cbm_go_suppress_cgo_callee(lang == CBM_LANG_GO, call->callee_name)) {
continue;
}

_rc_t0 = extract_now_ns();
try_field_type_hint(rc, &res, call->callee_name, source_node->id);
atomic_fetch_add_explicit(&rc->time_ns_rc_hint, extract_now_ns() - _rc_t0,
Expand Down Expand Up @@ -2548,6 +2560,16 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
}
atomic_fetch_add_explicit(&rc->time_ns_rc_target, extract_now_ns() - _rc_t0,
memory_order_relaxed);
/* #1929: a Go //export function is a declared C ABI contract — a
* C-family caller naming it exactly binds by that contract. Mirrors
* pass_calls.c. */
if (target_node && (lang == CBM_LANG_C || lang == CBM_LANG_CPP) && call->callee_name &&
target_node->name && strcmp(call->callee_name, target_node->name) == 0 &&
target_node->properties_json &&
strstr(target_node->properties_json, "\"cgo_export\":true")) {
res.strategy = "export_linkage";
res.confidence = CBM_EXPORT_LINKAGE_CONF;
}
if (target_node && source_node->id != target_node->id &&
cbm_suppress_cross_language_suffix_match(lang, target_node->file_path, res.strategy)) {
/* #725: same guard as pass_calls.c — do not emit a suffix_match
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);

/* #1929: confidence for a C-family call bound to a Go //export function by
* its declared ABI symbol — a contract, not a name guess. */
#define CBM_EXPORT_LINKAGE_CONF 0.95

/* #1929: `C.<ident>` in a Go file names the cgo pseudo-namespace — never a
* project symbol ("C" is reserved by go/build). Veto the callee before any
* resolution path can bind it. Pure; unit-tested in test_registry.c. */
bool cbm_go_suppress_cgo_callee(bool is_go, const char *callee_name);

/* #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
10 changes: 10 additions & 0 deletions src/pipeline/registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,16 @@ 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_cgo_callee(bool is_go, const char *callee_name) {
/* #1929/#1926: `C.<ident>` in a Go file names the cgo pseudo-namespace.
* `"C"` is reserved by go/build — no project symbol can ever be behind it,
* so ANY binding the general resolver produces for such a callee is
* fabricated. Veto before resolution instead of after: the pseudo-package
* is a standing hijack surface of exactly the #1906 shape. */
return is_go && callee_name && callee_name[0] == 'C' && callee_name[1] == '.' &&
callee_name[2] != '\0';
}

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
62 changes: 62 additions & 0 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -3015,6 +3015,67 @@ TEST(go_imports) {
PASS();
}

TEST(extract_go_cgo_export_flag) {
/* #1929: `//export Name` above a Go func declares its C ABI symbol; cgo
* requires Name == the func name, so only an exact match sets the flag. */
CBMFileResult *r = extract("package fx\n\n"
"/*\nstatic int helper(int a) { return a + 1; }\n*/\n"
"import \"C\"\n\n"
"//export GoCallback\n"
"func GoCallback(v int) int { return v }\n\n"
"//export SomethingElse\n"
"func Mismatched(v int) int { return v }\n\n"
"func Plain(v int) int { return v }\n",
CBM_LANG_GO, "t", "cgo.go");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
bool saw_cb = false;
bool saw_mismatch = false;
bool saw_plain = false;
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (!d->name) {
continue;
}
if (strcmp(d->name, "GoCallback") == 0) {
ASSERT_TRUE(d->is_cgo_export);
saw_cb = true;
}
if (strcmp(d->name, "Mismatched") == 0) {
ASSERT_FALSE(d->is_cgo_export);
saw_mismatch = true;
}
if (strcmp(d->name, "Plain") == 0) {
ASSERT_FALSE(d->is_cgo_export);
saw_plain = true;
}
}
ASSERT_TRUE(saw_cb);
ASSERT_TRUE(saw_mismatch);
ASSERT_TRUE(saw_plain);
/* #1929: the preamble range is flagged as unparsed — the C definitions in
* it are invisible to the Go grammar and coverage must say so. */
ASSERT_TRUE(r->parse_incomplete);
ASSERT_NOT_NULL(r->error_ranges);
ASSERT_NOT_NULL(strstr(r->error_ranges, "3-5"));
cbm_free_result(r);

/* A Go file without cgo keeps a clean coverage signal. */
r = extract("package fx\n\nfunc Clean(v int) int { return v }\n", CBM_LANG_GO, "t", "clean.go");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT_FALSE(r->parse_incomplete);
cbm_free_result(r);

/* `import "C"` with no preamble comment hides nothing — not flagged. */
r = extract("package fx\n\nimport \"C\"\n\nfunc Bare(v int) int { return v }\n", CBM_LANG_GO,
"t", "bare.go");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->parse_incomplete);
cbm_free_result(r);
PASS();
}

TEST(java_imports) {
CBMFileResult *r = extract(
"import java.util.List;\nimport java.util.ArrayList;\nimport static java.lang.Math.PI;\n"
Expand Down Expand Up @@ -6699,6 +6760,7 @@ SUITE(extraction) {
RUN_TEST(python_imports);
RUN_TEST(js_imports);
RUN_TEST(go_imports);
RUN_TEST(extract_go_cgo_export_flag);
RUN_TEST(java_imports);
RUN_TEST(rust_imports);
RUN_TEST(c_imports);
Expand Down
Loading
Loading