Skip to content
Open
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
18 changes: 13 additions & 5 deletions src/pipeline/pass_pkgmap.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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);
Expand Down Expand Up @@ -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, '/');
Expand Down
46 changes: 46 additions & 0 deletions tests/test_lang_contract.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
Loading