diff --git a/src/pipeline/pass_pkgmap.c b/src/pipeline/pass_pkgmap.c index 97dfb2ad..2bc75590 100644 --- a/src/pipeline/pass_pkgmap.c +++ b/src/pipeline/pass_pkgmap.c @@ -1328,8 +1328,9 @@ static const cbm_gbuf_node_t *resolve_sibling_file(const cbm_pipeline_ctx_t *ctx } const cbm_gbuf_node_t *n = cbm_gbuf_find_by_qn(ctx->gbuf, qn); free(qn); - if (n && (!source_file_qn || !n->qualified_name || - strcmp(n->qualified_name, source_file_qn) != 0)) { + if (n && import_targetable_label(n->label) && + (!source_file_qn || !n->qualified_name || + strcmp(n->qualified_name, source_file_qn) != 0)) { found = n; break; } @@ -1347,7 +1348,13 @@ const cbm_gbuf_node_t *cbm_pipeline_resolve_import_node(const cbm_pipeline_ctx_t return NULL; } - /* Strategy 1: module-path resolution → existing node (Python/TS/Go). */ + /* Strategy 1: module-path resolution → existing node (Python/TS/Go). + * No label filter here: directory-module languages (Go/Java packages) + * legitimately resolve straight to a Folder node -- that's the intended, + * correct import target, not a collision. The Folder-collision problem + * (#767) only shows up downstream, in Strategy 4's retry-with-truncated- + * path loop, which re-enters resolve_module with a DIFFERENT, shortened + * string that the original import never named. */ char *target_qn = cbm_pipeline_resolve_module(ctx, source_rel, imp->module_path); const cbm_gbuf_node_t *target = target_qn ? cbm_gbuf_find_by_qn(ctx->gbuf, target_qn) : NULL; free(target_qn); @@ -1571,8 +1578,9 @@ const cbm_gbuf_node_t *cbm_pipeline_resolve_import_node(const cbm_pipeline_ctx_t char *rqn = cbm_pipeline_resolve_module(ctx, source_rel, work); const cbm_gbuf_node_t *n = rqn ? cbm_gbuf_find_by_qn(ctx->gbuf, rqn) : NULL; free(rqn); - if (n && (!source_file_qn || !n->qualified_name || - strcmp(n->qualified_name, source_file_qn) != 0)) { + if (n && import_targetable_label(n->label) && + (!source_file_qn || !n->qualified_name || + strcmp(n->qualified_name, source_file_qn) != 0)) { return n; } char *sl = strrchr(work, '/'); diff --git a/tests/test_lang_contract.c b/tests/test_lang_contract.c index 2386e5e1..f9f91066 100644 --- a/tests/test_lang_contract.c +++ b/tests/test_lang_contract.c @@ -1158,6 +1158,50 @@ TEST(contract_edge_workspaces_imports_issue408) { PASS(); } +/* #767: a wildcard tsconfig alias for the "@lib" prefix (mapped to ./src/lib) + * shares that prefix with an unrelated scoped npm package ("@lib/external-pkg", + * meant to resolve normally from node_modules). The engine has no such file + * and must NOT invent an edge to the "src/lib" Folder node via a later + * fallback strategy that re-tries the truncated "@lib" prefix against the + * tsconfig's other, bare alias entry. Zero IMPORTS edges in the whole project + * is the correct outcome: the same as any other unresolved external import. */ +TEST(contract_edge_imports_alias_no_phantom_folder_edge_issue767) { + LangProj lp; + static const LangFile f[] = { + {"tsconfig.json", "{\n \"compilerOptions\": {\n \"paths\": {\n" + " \"@lib\": [\"./src/lib\"],\n" + " \"@lib/*\": [\"./src/lib/*\"]\n }\n }\n}\n"}, + {"src/lib/thing.ts", "export const Thing = {};\n"}, + {"src/consumer.ts", + "import { ClientC } from '@lib/external-pkg';\n\n" + "export function useClient() {\n return new ClientC();\n}\n"}}; + cbm_store_t *store = lang_index_files(&lp, f, 3); + int got = store ? cbm_store_count_edges_by_type(store, lp.project, "IMPORTS") : -1; + if (got != 0) { + fprintf(stderr, " [EDGE] FAIL IMPORTS count=%d expected=0 (phantom Folder edge)\n", got); + } + ASSERT_EQ(got, 0); + lang_cleanup(&lp, store); + PASS(); +} + +/* #767 regression guard: a wildcard tsconfig alias resolving to a REAL, + * indexed file must still produce its IMPORTS edge — the import-targetable + * label filter must reject Folder/Project/etc. matches without rejecting + * legitimate File/Module matches. */ +TEST(contract_edge_imports_alias_resolves_real_file_issue767) { + static const LangFile f[] = { + {"tsconfig.json", "{\n \"compilerOptions\": {\n \"paths\": {\n" + " \"@lib\": [\"./src/lib\"],\n" + " \"@lib/*\": [\"./src/lib/*\"]\n }\n }\n}\n"}, + {"src/lib/thing.ts", "export const Thing = {};\n"}, + {"src/consumer.ts", + "import { Thing } from '@lib/thing';\n\n" + "export function useThing() {\n return Thing;\n}\n"}}; + ASSERT_TRUE(edge_present(f, 3, "IMPORTS", 1)); + PASS(); +} + /* DEPENDS_ON — Helm Chart.yaml `dependencies:` -> per-dependency Chart node. * Basename must be exactly "Chart.yaml"; pass_k8s runs in both pipeline paths. */ TEST(contract_edge_depends_on) { @@ -1365,6 +1409,8 @@ SUITE(lang_contract) { * FILE_CHANGES_WITH (git co-change). Completes 25-edge-type coverage. */ RUN_TEST(contract_edge_tests); RUN_TEST(contract_edge_workspaces_imports_issue408); + RUN_TEST(contract_edge_imports_alias_no_phantom_folder_edge_issue767); + RUN_TEST(contract_edge_imports_alias_resolves_real_file_issue767); RUN_TEST(contract_edge_depends_on); RUN_TEST(contract_edge_parallel_service_edges); RUN_TEST(contract_edge_file_changes_with);