From 833685134a52b716a8dc0dd6323579894f53274e Mon Sep 17 00:00:00 2001 From: Ilya Brykau Date: Sat, 29 Aug 2026 19:02:12 +0200 Subject: [PATCH] fix(extract): drop cgo's `import "C"` pseudo-package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `import "C"` is not an import: "C" is reserved by go/build and no package with that path can exist — it only tells the toolchain to compile the preceding comment as C. cbm emitted it as an ordinary import, so the import resolver fell through to its symbol-name fallback (cbm_pipeline_resolve_import_node Strategy 3, pass_pkgmap.c), matched the literal name "C" against every project node called C, and picked the lexicographically smallest one. Measured on a real Go+C repo: all 27 files carrying `import "C"` pointed their IMPORTS edge at the same unrelated `C` member of a test helper — 27 false edges, and 27 files whose import map named a package that does not exist. Skip the spec in parse_go_import_spec. Node counts are unchanged and the file's real imports are untouched; only the false edge disappears (a 3-file fixture goes 22 edges -> 21, 14 nodes -> 14; the real repo goes 27 -> 0 such edges). Reproduce-first: go_cgo_pseudo_import_dropped is RED without the extractor change (the "C" import is still in imports[]) and GREEN with it, while still asserting the file's real `fmt` import survives. Signed-off-by: Ilya Brykau --- internal/cbm/extract_imports.c | 13 +++++++++++++ tests/test_extraction.c | 20 ++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/internal/cbm/extract_imports.c b/internal/cbm/extract_imports.c index 2d8ed1e4e..cf70d1289 100644 --- a/internal/cbm/extract_imports.c +++ b/internal/cbm/extract_imports.c @@ -133,6 +133,19 @@ static void parse_go_import_spec(CBMExtractCtx *ctx, TSNode spec) { return; } + /* cgo: `import "C"` names the cgo PSEUDO-package, not a real one — "C" is + * reserved by go/build and no package with that import path can exist. + * Emitting it as an ordinary import makes the import resolver fall through + * to its symbol-name fallback (cbm_pipeline_resolve_import_node Strategy 3, + * pass_pkgmap.c), which matches the literal name "C" against every project + * node called C and picks the lexicographically smallest: on a real Go repo + * all 27 cgo files pointed their IMPORTS edge at the same unrelated `C` + * member of a test helper. Drop it — the C side is not a graph package, and + * the file's remaining imports are unaffected. */ + if (strcmp(path, "C") == 0) { + return; + } + TSNode name_node = ts_node_child_by_field_name(spec, TS_FIELD("name")); const char *local_name = !ts_node_is_null(name_node) ? cbm_node_text(a, name_node, ctx->source) : path_last(a, path); diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 73219f4da..e3f743657 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -2865,6 +2865,25 @@ TEST(go_imports) { PASS(); } +/* cgo's `import "C"` is a pseudo-package, not a real import: keeping it lets the + * import resolver name-match "C" onto an arbitrary project symbol called C. The + * real imports of the same file must survive. */ +TEST(go_cgo_pseudo_import_dropped) { + CBMFileResult *r = extract("package m\n\n/*\nstatic int helper(void) { return 1; }\n*/\n" + "import \"C\"\n\nimport \"fmt\"\n\n" + "func Run() { fmt.Println(C.helper()) }\n", + CBM_LANG_GO, "t", "cgo.go"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + ASSERT(has_import(r, "fmt")); + for (int i = 0; i < r->imports.count; i++) { + ASSERT_NOT_NULL(r->imports.items[i].module_path); + ASSERT_TRUE(strcmp(r->imports.items[i].module_path, "C") != 0); + } + 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" @@ -6543,6 +6562,7 @@ SUITE(extraction) { RUN_TEST(python_imports); RUN_TEST(js_imports); RUN_TEST(go_imports); + RUN_TEST(go_cgo_pseudo_import_dropped); RUN_TEST(java_imports); RUN_TEST(rust_imports); RUN_TEST(c_imports);