diff --git a/src/pipeline/pass_pkgmap.c b/src/pipeline/pass_pkgmap.c index ac17bdac8..68124dbe1 100644 --- a/src/pipeline/pass_pkgmap.c +++ b/src/pipeline/pass_pkgmap.c @@ -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); diff --git a/tests/test_resolver_stabilization.c b/tests/test_resolver_stabilization.c index 056e8951b..3e3f205a8 100644 --- a/tests/test_resolver_stabilization.c +++ b/tests/test_resolver_stabilization.c @@ -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);