fix(go): a module rooted at the repository root imports as the Project node

A Go import targets the node of its package's directory, and the strict
import path accepted only Folder nodes. The repository root has none: the
Project node is that directory, and its QN is the module of every root
file. caddy keeps go.mod and `package caddy` at the root, imported as
"github.com/caddyserver/caddy/v2", so that import never resolved and every
caddy.X call, and everything typed through one, stayed unknown under the
generic shared_binding_type_fixed_point reason. No cap was involved.

go_package_node accepts the Project node for the root directory in the
module-path strategy, the duplicate-module fallback and the ownership
check, which reads the package directory ("" for the root) from it. The
ownership rule itself is unchanged: a module without a replace directive
or go.work entry still takes the path from the module cache.

caddy against SCIP: recall 83.45% -> 95.89%, real lost 794 -> 200, never
found 229 -> 54, new false 0 -> 0. traefik, minio, cli, helm, prometheus
and etcd unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-11-03 06:08:26 +01:00
1 parent a1c437432f
commit 0ecf116e61
2 files changed
+123 -9

No files matched your search

+29 -9
View File
@@ -5445,6 +5445,27 @@ static const cbm_gbuf_node_t *typescript_paths_target(const cbm_pipeline_ctx_t *
static const cbm_gbuf_node_t *go_import_by_source_module(const cbm_pipeline_ctx_t *ctx,
const char *source_rel, const char *path);
/* A Go package is a directory, and an import targets that directory's node. The repository's
* own root is no Folder: the Project node is that directory, its QN the module of every root
* file (graph_buffer.c). caddy's go.mod and `package caddy` both sit there. *directory is the
* package's directory relative to the repository, NULL when the node records none. */
static bool go_package_node(const cbm_pipeline_ctx_t *ctx, const cbm_gbuf_node_t *node,
const char **directory) {
if (!node || !node->label)
return false;
if (!strcmp(node->label, "Folder")) {
if (directory)
*directory = node->file_path;
return true;
}
if (strcmp(node->label, "Project") || !ctx->project_name || !node->qualified_name ||
strcmp(node->qualified_name, ctx->project_name))
return false;
if (directory)
*directory = "";
return true;
}
static const cbm_gbuf_node_t *resolve_import_strategies(
const cbm_pipeline_ctx_t *ctx, const char *source_rel, const CBMImport *imp,
CBMHashTable *namespace_map, CBMLanguage src_lang, bool php_only, bool *ambiguous) {
@@ -5498,7 +5519,7 @@ static const cbm_gbuf_node_t *resolve_import_strategies(
free(qn);
if (target && (!php_only || node_in_php_file(target)) &&
(import_language_matches(src_lang, target) ||
(src_lang == CBM_LANG_GO && target->label && strcmp(target->label, "Folder") == 0))) {
(src_lang == CBM_LANG_GO && go_package_node(ctx, target, NULL)))) {
if (target->label &&
(strcmp(target->label, "Module") == 0 || strcmp(target->label, "File") == 0)) {
char sym[CBM_SZ_256];
@@ -5517,7 +5538,7 @@ static const cbm_gbuf_node_t *resolve_import_strategies(
}
return target;
}
if (src_lang == CBM_LANG_GO && target->label && strcmp(target->label, "Folder") == 0)
if (src_lang == CBM_LANG_GO && go_package_node(ctx, target, NULL))
return target;
/* Python's dotted import path may name a declaration exactly; a
* plain module import may never bind an arbitrary function/class. */
@@ -6506,7 +6527,7 @@ static const cbm_gbuf_node_t *go_import_by_source_module(const cbm_pipeline_ctx_
char *qn = cbm_pipeline_fqn_folder(ctx->project_name, package);
const cbm_gbuf_node_t *node = qn ? cbm_gbuf_find_by_qn(ctx->gbuf, qn) : NULL;
free(qn);
return node && node->label && !strcmp(node->label, "Folder") ? node : NULL;
return go_package_node(ctx, node, NULL) ? node : NULL;
}
static bool go_import_inside_module(const char *import, const char *module, const char *owner,
@@ -6524,18 +6545,17 @@ static bool go_import_inside_module(const char *import, const char *module, cons
bool cbm_pipeline_go_import_owned(const cbm_pipeline_ctx_t *ctx, const char *source_rel,
const CBMImport *import, const cbm_gbuf_node_t *target) {
if (!ctx || !source_rel || !import || !import->module_path || !target || !target->file_path ||
!target->label || strcmp(target->label, "Folder"))
const char *package_dir = NULL;
if (!ctx || !source_rel || !import || !import->module_path ||
!go_package_node(ctx, target, &package_dir) || !package_dir)
return false;
char source_owner[PKGMAP_PATH_BUF], source_module[PKGMAP_PATH_BUF],
target_owner[PKGMAP_PATH_BUF], target_module[PKGMAP_PATH_BUF];
if (!go_module_owner(ctx, source_rel, false, source_owner, source_module,
sizeof(source_owner)) ||
!go_module_owner(ctx, target->file_path, true, target_owner, target_module,
sizeof(target_owner)))
!go_module_owner(ctx, package_dir, true, target_owner, target_module, sizeof(target_owner)))
return false;
if (!go_import_inside_module(import->module_path, target_module, target_owner,
target->file_path))
if (!go_import_inside_module(import->module_path, target_module, target_owner, package_dir))
return false;
if (!strcmp(source_owner, target_owner))
return !strcmp(source_module, target_module);
+94
View File
@@ -1912,6 +1912,84 @@ static int go_duplicate_module_case(bool parallel) {
th_rmtree(root);
PASS();
}
/* caddy's go.mod sits at the repository root and its root directory is a package, imported as
* "github.com/caddyserver/caddy/v2" and bound as caddy. That directory has no Folder node: the
* Project node is it, since its QN is every root file's module. A decoy namesake lives in a
* subpackage, and a module without a replace directive takes the path from the module cache.
* With `duplicate`, a fixture's go.mod also declares the module path, as cli/cli's CodeQL tests
* do: the importer's own module still means the root, never the fixture's namesake package. */
static int go_module_root_package_case(bool parallel, bool duplicate) {
const char *root = th_mktempdir("cbm_go_root_package");
ASSERT_NOT_NULL(root);
char repo[1024], database[1024];
snprintf(repo, sizeof(repo), "%s/repo", root);
snprintf(database, sizeof(database), "%s/index.db", root);
ASSERT(cbm_mkdir_p(TH_PATH(repo, "caddyconfig/caddyfile"), 0755));
ASSERT(cbm_mkdir_p(TH_PATH(repo, "foreign"), 0755));
ASSERT_EQ(th_write_file(TH_PATH(repo, "go.mod"), "module example/caddy/v2\ngo 1.22\n"), 0);
ASSERT_EQ(th_write_file(TH_PATH(repo, "foreign/go.mod"), "module example/foreign\ngo 1.22\n"),
0);
ASSERT_EQ(th_write_file(TH_PATH(repo, "caddy.go"),
"// Copyright notice.\n\npackage caddy\n\n"
"type Replacer struct{}\n\n"
"func NewReplacer() *Replacer { return &Replacer{} }\n\n"
"func (r *Replacer) ReplaceKnown(input, empty string) string {\n"
"\treturn input\n}\n\n"
"func GetModule(name string) (string, error) { return name, nil }\n"),
0);
ASSERT_EQ(th_write_file(TH_PATH(repo, "caddyconfig/adapters.go"),
"package caddyconfig\n\n"
"func GetModule(name string) (string, error) { return \"\", nil }\n"),
0);
ASSERT_EQ(th_write_file(TH_PATH(repo, "caddyconfig/caddyfile/adapter.go"),
"package caddyfile\n\nimport (\n\t\"example/caddy/v2\"\n)\n\n"
"func UnmarshalModule(moduleID string) (string, error) {\n"
"\tmod, err := caddy.GetModule(moduleID)\n"
"\trepl := caddy.NewReplacer()\n"
"\treturn repl.ReplaceKnown(mod, \"\"), err\n}\n"),
0);
ASSERT_EQ(th_write_file(TH_PATH(repo, "foreign/use.go"),
"package foreign\n\nimport \"example/caddy/v2\"\n\n"
"func Cached() { caddy.GetModule(\"x\") }\n"),
0);
if (duplicate) {
ASSERT(cbm_mkdir_p(TH_PATH(repo, "fixture"), 0755));
ASSERT_EQ(
th_write_file(TH_PATH(repo, "fixture/go.mod"), "module example/caddy/v2\ngo 1.22\n"),
0);
ASSERT_EQ(
th_write_file(TH_PATH(repo, "fixture/caddy.go"),
"package caddy\n\n"
"func GetModule(name string) (string, error) { return \"\", nil }\n"),
0);
}
for (int i = 0; parallel && i < 52; i++) {
char name[64], source[96];
snprintf(name, sizeof(name), "padding_%02d.py", i);
snprintf(source, sizeof(source), "def padding_%02d():\n return %d\n", i, i);
ASSERT_EQ(th_write_file(TH_PATH(repo, name), source), 0);
}
ASSERT_EQ(index_fixture(repo, database, parallel), 0);
cbm_store_t *store = cbm_store_open_path_existing(database);
ASSERT_NOT_NULL(store);
sqlite3 *db = cbm_store_get_db(store);
const char *user = "test.caddyconfig.caddyfile.UnmarshalModule";
ASSERT_EQ(
count_edge_between(db, "IMPORTS", "test.caddyconfig.caddyfile.adapter.go.__file__", "test"),
1);
ASSERT_EQ(count_edge_between(db, "CALLS", user, "test.GetModule"), 1);
ASSERT_EQ(count_edge_between(db, "CALLS", user, "test.NewReplacer"), 1);
ASSERT_EQ(count_edge_between(db, "CALLS", user, "test.Replacer.ReplaceKnown"), 1);
ASSERT_EQ(count_edge_between(db, "CALLS", user, "test.caddyconfig.GetModule"), 0);
ASSERT_EQ(count_edge_between(db, "CALLS", user, "test.fixture.GetModule"), 0);
ASSERT_EQ(count_edge_between(db, "IMPORTS", "test.foreign.use.go.__file__", "test"), 0);
ASSERT_EQ(count_edge_between(db, "CALLS", "test.foreign.Cached", "test.GetModule"), 0);
ASSERT_EQ(count_edge_between(db, "CALLS", "test.foreign.Cached", "test.caddyconfig.GetModule"),
0);
cbm_store_close(store);
th_rmtree(root);
PASS();
}
/* Go 1.26's new(expr) is an ERROR to the grammar, which used to cost the declaration around it.
* A decoy method and a function that merely end in new stay what they are. */
static int go_new_expression_case(bool parallel) {
@@ -2219,6 +2297,18 @@ TEST(resolver_stabilization_go_duplicate_module_path_sequential) {
TEST(resolver_stabilization_go_duplicate_module_path_parallel) {
return go_duplicate_module_case(true);
}
TEST(resolver_stabilization_go_module_root_package_sequential) {
return go_module_root_package_case(false, false);
}
TEST(resolver_stabilization_go_module_root_package_parallel) {
return go_module_root_package_case(true, false);
}
TEST(resolver_stabilization_go_module_root_beside_duplicate_module_sequential) {
return go_module_root_package_case(false, true);
}
TEST(resolver_stabilization_go_module_root_beside_duplicate_module_parallel) {
return go_module_root_package_case(true, true);
}
TEST(resolver_stabilization_go_pipeline_references_sequential) {
return go_references_case(false);
}
@@ -2337,6 +2427,10 @@ SUITE(resolver_stabilization) {
RUN_TEST(resolver_stabilization_go_anonymous_structs_and_conversions_parallel);
RUN_TEST(resolver_stabilization_go_duplicate_module_path_sequential);
RUN_TEST(resolver_stabilization_go_duplicate_module_path_parallel);
RUN_TEST(resolver_stabilization_go_module_root_package_sequential);
RUN_TEST(resolver_stabilization_go_module_root_package_parallel);
RUN_TEST(resolver_stabilization_go_module_root_beside_duplicate_module_sequential);
RUN_TEST(resolver_stabilization_go_module_root_beside_duplicate_module_parallel);
RUN_TEST(resolver_stabilization_go_pipeline_references_sequential);
RUN_TEST(resolver_stabilization_go_pipeline_references_parallel);
RUN_TEST(resolver_stabilization_go_pipeline_twins_and_package_level_sequential);