From ef6b43186b2f548c1c7d5475a7384807fba686cf Mon Sep 17 00:00:00 2001 From: Antonio Espinosa Date: Fri, 2 Oct 2026 13:25:30 +0200 Subject: [PATCH 1/6] fix(514): resolve TS imports through index.ts barrels Signed-off-by: Antonio Espinosa --- src/pipeline/pass_lsp_cross.c | 101 +++++++++++++++++++++++++++++ tests/test_lsp_resolution_probe.c | 103 ++++++++++++++++++++++++++++++ 2 files changed, 204 insertions(+) diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index a0a769fd12..d70da29501 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -1419,6 +1419,91 @@ static char *pxc_import_value_qn(CBMLanguage lang, const CBMFileResult *result, return qualified; } +/* JS/TS barrels. An import that lands on a Folder (`./svc` -> svc/index.ts) + * or on a module that only re-exports the name never reaches the declaring + * module, so `module.Name` is not in the registry and DI member calls on the + * imported type stay unresolved. Follow the barrel's own IMPORTS edges (its + * `export ... from` statements) to the one module that defines `name`. Those + * edges do not record which names each statement re-exports, so the + * definition node is the proof; two different definitions fail closed. */ +enum { PXC_BARREL_MAX_DEPTH = 8 }; + +static bool pxc_is_js_family(CBMLanguage lang) { + return lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX; +} + +/* QN of `name` when the file at rel_path defines it, else NULL. */ +static char *pxc_defined_symbol_qn(const cbm_gbuf_t *gbuf, const char *project_name, + const char *rel_path, const char *name) { + char *qn = cbm_pipeline_fqn_compute(project_name, rel_path, name); + const cbm_gbuf_node_t *node = qn ? cbm_gbuf_find_by_qn(gbuf, qn) : NULL; + if (node && node->label && strcmp(node->label, "Module") != 0 && + strcmp(node->label, "Folder") != 0 && strcmp(node->label, "File") != 0) { + return qn; + } + free(qn); + return NULL; +} + +/* `visited` holds barrel file QNs already searched for `name`: barrels form + * DAGs (and cycles) where one lib index is reachable through many paths, and + * re-walking it on each path is exponential in the depth cap. */ +static char *pxc_barrel_symbol_qn(const cbm_gbuf_t *gbuf, const char *project_name, + const cbm_gbuf_node_t *target, const char *name, int depth, + CBMHashTable *visited) { + if (!target || !target->label || !target->file_path || depth > PXC_BARREL_MAX_DEPTH) { + return NULL; + } + static const char *const index_names[] = {"index.ts", "index.tsx", "index.mts", + "index.js", "index.jsx", "index.mjs"}; + char rel[CBM_SZ_1K]; + const cbm_gbuf_node_t *file_node = NULL; + if (strcmp(target->label, "Module") == 0) { + snprintf(rel, sizeof(rel), "%s", target->file_path); + char *file_qn = cbm_pipeline_fqn_compute(project_name, rel, "__file__"); + file_node = file_qn ? cbm_gbuf_find_by_qn(gbuf, file_qn) : NULL; + free(file_qn); + } else if (strcmp(target->label, "Folder") == 0) { + for (size_t i = 0; i < sizeof(index_names) / sizeof(index_names[0]) && !file_node; i++) { + snprintf(rel, sizeof(rel), "%s/%s", target->file_path, index_names[i]); + char *file_qn = cbm_pipeline_fqn_compute(project_name, rel, "__file__"); + file_node = file_qn ? cbm_gbuf_find_by_qn(gbuf, file_qn) : NULL; + free(file_qn); + } + } + if (!file_node || cbm_ht_has(visited, file_node->qualified_name)) { + return NULL; + } + cbm_ht_set(visited, file_node->qualified_name, (void *)file_node); + char *defined = pxc_defined_symbol_qn(gbuf, project_name, rel, name); + if (defined) { + return defined; + } + + const cbm_gbuf_edge_t **edges = NULL; + int edge_count = 0; + if (cbm_gbuf_find_edges_by_source_type(gbuf, file_node->id, "IMPORTS", &edges, &edge_count) != + 0) { + return NULL; + } + char *found = NULL; + for (int i = 0; i < edge_count; i++) { + const cbm_gbuf_node_t *next = cbm_gbuf_find_by_id(gbuf, edges[i]->target_id); + char *qn = pxc_barrel_symbol_qn(gbuf, project_name, next, name, depth + 1, visited); + if (!qn) { + continue; + } + if (found && strcmp(found, qn) != 0) { + free(found); + free(qn); + return NULL; + } + free(found); + found = qn; + } + return found; +} + static bool pxc_import_map_has_local(const char *const *keys, int count, const char *local_name) { if (!keys || !local_name) return false; @@ -1568,6 +1653,22 @@ int cbm_pxc_build_import_map_ex(const cbm_gbuf_t *gbuf, const char *project_name free(local); continue; } + if (pxc_is_js_family(lang) && target->label && target->file_path) { + char *direct = strcmp(target->label, "Module") == 0 + ? pxc_defined_symbol_qn(gbuf, project_name, target->file_path, local) + : NULL; + char *barrel = NULL; + CBMHashTable *visited = direct ? NULL : cbm_ht_create(16); + if (visited) { + barrel = pxc_barrel_symbol_qn(gbuf, project_name, target, local, 0, visited); + cbm_ht_free(visited); + } + free(direct); + if (barrel) { + free(value); + value = barrel; + } + } keys[count] = local; vals[count] = value; count++; diff --git a/tests/test_lsp_resolution_probe.c b/tests/test_lsp_resolution_probe.c index 91844f67e4..2a506899fb 100644 --- a/tests/test_lsp_resolution_probe.c +++ b/tests/test_lsp_resolution_probe.c @@ -1272,6 +1272,108 @@ TEST(lrp_ts_s8_field_type_hint) { PASS(); } +/* Barrels: a type imported through an index.ts that re-exports it must resolve + * to the defining module. Covers a dotted folder imported by alias (B1), a lib + * barrel with a named re-export (B2), a relative import of a barrel folder + * (B3) and a nested `export *` barrel (B4). The import used to stop at the + * Folder/barrel module, so every DI call below had no CALLS edge. */ +TEST(lrp_ts_barrel_reexport_member_calls) { + static const LRP_File f[] = { + {"apps/api/tsconfig.json", "{\"compilerOptions\":{\"baseUrl\":\"./\",\"paths\":{" + "\"@acme/shipping\":[\"libs/shipping/src\"]," + "\"@acme/shipping/*\":[\"libs/shipping/src/*\"]}}}\n"}, + {"apps/api/libs/shipping/src/application/service/label-print.service/" + "label-print.service.ts", + "import { LabelPrintParams } from \"./label-print.interface\";\n" + "export class LabelPrintService {\n" + " printStream(params: LabelPrintParams): string {\n" + " return this.printWithStats(params).body;\n" + " }\n" + " printWithStats(params: LabelPrintParams): { body: string; pages: number } {\n" + " return { body: params.shipmentId, pages: 1 };\n" + " }\n" + "}\n"}, + {"apps/api/libs/shipping/src/application/service/label-print.service/" + "label-print.interface.ts", + "export interface LabelPrintParams {\n" + " shipmentId: string;\n" + "}\n"}, + {"apps/api/libs/shipping/src/application/service/label-print.service/index.ts", + "export * from \"./label-print.service\";\n" + "export * from \"./label-print.interface\";\n"}, + {"apps/api/libs/shipping/src/domain/tracking/tracking-code.service.ts", + "export class TrackingCodeService {\n" + " generate(shipmentId: string): string {\n" + " return shipmentId;\n" + " }\n" + "}\n"}, + {"apps/api/libs/shipping/src/domain/tracking/index.ts", + "export * from \"./tracking-code.service\";\n"}, + {"apps/api/libs/shipping/src/domain/carrier.service.ts", + "export class CarrierService {\n" + " quote(weightKg: number): number {\n" + " return weightKg * 2;\n" + " }\n" + "}\n"}, + {"apps/api/libs/shipping/src/index.ts", + "export * from \"./domain/tracking\";\n" + "export {\n" + " CarrierService,\n" + "} from \"./domain/carrier.service\";\n" + "export type { LabelPrintParams } from \"./application/service/label-print.service\";\n"}, + {"apps/api/libs/invoice/src/application/command/handler/ship-invoice.handler.ts", + "import { LabelPrintService } from " + "\"@acme/shipping/application/service/label-print.service\";\n" + "import { CarrierService, TrackingCodeService } from \"@acme/shipping\";\n" + "export class ShipInvoiceHandler {\n" + " constructor(\n" + " private readonly labelPrintService: LabelPrintService,\n" + " private readonly carrierService: CarrierService,\n" + " private readonly trackingCodeService: TrackingCodeService,\n" + " ) {}\n" + " execute(shipmentId: string): string {\n" + " const cost = this.carrierService.quote(3);\n" + " const code = this.trackingCodeService.generate(shipmentId);\n" + " const label = this.labelPrintService.printStream({ shipmentId });\n" + " return `${label}:${code}:${cost}`;\n" + " }\n" + "}\n"}, + {"apps/api/libs/invoice/src/application/service/label-preview.service.ts", + "import { LabelPrintService } from " + "\"../../../../shipping/src/application/service/label-print.service\";\n" + "export class LabelPreviewService {\n" + " constructor(private readonly labelPrintService: LabelPrintService) {}\n" + " preview(shipmentId: string): number {\n" + " return this.labelPrintService.printWithStats({ shipmentId }).pages;\n" + " }\n" + "}\n"}, + }; + LRP_Proj lp; + cbm_store_t *store = lrp_index(&lp, f, (int)(sizeof(f) / sizeof(f[0]))); + ASSERT_NOT_NULL(store); + + int b1 = lrp_exact_edge_by_qn_suffix(store, lp.project, "CALLS", "ShipInvoiceHandler.execute", + "LabelPrintService.printStream"); + int b2 = lrp_exact_edge_by_qn_suffix(store, lp.project, "CALLS", "ShipInvoiceHandler.execute", + "CarrierService.quote"); + int b3 = lrp_exact_edge_by_qn_suffix(store, lp.project, "CALLS", "LabelPreviewService.preview", + "LabelPrintService.printWithStats"); + int b4 = lrp_exact_edge_by_qn_suffix(store, lp.project, "CALLS", "ShipInvoiceHandler.execute", + "TrackingCodeService.generate"); + int self = + lrp_exact_edge_by_qn_suffix(store, lp.project, "CALLS", "LabelPrintService.printStream", + "LabelPrintService.printWithStats"); + printf(" [ts/barrel] B1=%d B2=%d B3=%d B4=%d self=%d\n", b1, b2, b3, b4, self); + lrp_cleanup(&lp, store); + + ASSERT_EQ(b1, 1); + ASSERT_EQ(b2, 1); + ASSERT_EQ(b3, 1); + ASSERT_EQ(b4, 1); + ASSERT_EQ(self, 1); + PASS(); +} + /* ══════════════════════════════════════════════════════════════════════ * ─── GROUP 7: JAVA (lsp_cross NOT WIRED) ───────────────────────────── * ══════════════════════════════════════════════════════════════════════ @@ -1919,6 +2021,7 @@ SUITE(lsp_resolution_probe) { RUN_TEST(lrp_ts_s6_inherited_method); RUN_TEST(lrp_ts_s7_generic); RUN_TEST(lrp_ts_s8_field_type_hint); + RUN_TEST(lrp_ts_barrel_reexport_member_calls); /* ── Java (lsp_cross NOT WIRED) — S1 GREEN, S2–S8 RED reproductions ── */ RUN_TEST(lrp_java_s1_crossfile_call); From da30a216658d81358602da69a7907a139d63d52f Mon Sep 17 00:00:00 2001 From: Antonio Espinosa Date: Fri, 2 Oct 2026 11:10:21 +0200 Subject: [PATCH 2/6] fix(514): preserve dotted filename Signed-off-by: Antonio Espinosa --- src/pipeline/pass_pkgmap.c | 5 +- src/pipeline/path_alias.c | 21 ++++--- tests/repro/repro_issue514.c | 110 +++++++++++++++++++++++++++++++++++ tests/test_edge_imports.c | 61 +++++++++++++++++++ tests/test_path_alias.c | 17 ++++++ 5 files changed, 204 insertions(+), 10 deletions(-) diff --git a/src/pipeline/pass_pkgmap.c b/src/pipeline/pass_pkgmap.c index 7af626b2da..fe2ce0a55d 100644 --- a/src/pipeline/pass_pkgmap.c +++ b/src/pipeline/pass_pkgmap.c @@ -1442,7 +1442,10 @@ char *cbm_pipeline_resolve_module(const cbm_pipeline_ctx_t *ctx, const char *sou if (amap) { char *aliased = cbm_path_alias_resolve(amap, module_path); if (aliased) { - char *qn = cbm_pipeline_fqn_module(ctx->project_name, aliased); + /* Like step 1: the alias resolver already removed an explicit + * file extension, so keep dotted basenames such as + * `cat.service` intact. */ + char *qn = cbm_pipeline_fqn_folder(ctx->project_name, aliased); free(aliased); return qn; } diff --git a/src/pipeline/path_alias.c b/src/pipeline/path_alias.c index 7ef54c3691..dc144a1683 100644 --- a/src/pipeline/path_alias.c +++ b/src/pipeline/path_alias.c @@ -47,20 +47,23 @@ enum { /* ── Helpers ───────────────────────────────────────────────────── */ -/* Strip .ts/.tsx/.js/.jsx in place. Returns its argument. */ +/* Strip an explicit JS/TS module extension in place, keeping dotted + * basenames such as "cat.service". Same set as the relative resolver in + * fqn.c. Returns its argument. */ static char *strip_resolved_ext(char *path) { if (!path) { return path; } + static const char *const extensions[] = { + ".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx", ".mts", ".cts", ".json", + }; size_t len = strlen(path); - if (len > 3 && path[len - 3] == '.' && (path[len - 2] == 't' || path[len - 2] == 'j') && - path[len - 1] == 's') { - path[len - 3] = '\0'; - return path; - } - if (len > 4 && path[len - 4] == '.' && (path[len - 3] == 't' || path[len - 3] == 'j') && - path[len - 2] == 's' && path[len - 1] == 'x') { - path[len - 4] = '\0'; + for (size_t i = 0; i < sizeof(extensions) / sizeof(extensions[0]); i++) { + size_t ext_len = strlen(extensions[i]); + if (len > ext_len && strcmp(path + len - ext_len, extensions[i]) == 0) { + path[len - ext_len] = '\0'; + break; + } } return path; } diff --git a/tests/repro/repro_issue514.c b/tests/repro/repro_issue514.c index 96f255045a..abf2be6259 100644 --- a/tests/repro/repro_issue514.c +++ b/tests/repro/repro_issue514.c @@ -197,7 +197,117 @@ TEST(repro_issue514_data_flow_surfaces_arg_expr) { PASS(); } +/* + * Sub-claim (B), minimal form from the issue thread: a NestJS controller + * calling a service injected through a constructor parameter property + * (`constructor(private readonly catService: CatService)`) must produce an + * inbound caller for CatService.findAll. + */ +static const RFile k_nest_files[] = { + {"src/cats/cat.service.ts", "export class CatService {\n" + " findAll(): string[] {\n" + " return [\"a\"];\n" + " }\n" + "}\n"}, + {"src/cats/cat.controller.ts", "import { CatService } from \"./cat.service\";\n" + "\n" + "export class CatController {\n" + " constructor(private readonly catService: CatService) {}\n" + "\n" + " list() {\n" + " return this.catService.findAll();\n" + " }\n" + "}\n"}, +}; + +/* Run trace_path inbound depth 1 on fn_name against the fixture project. + * rh_index_files restores CBM_CACHE_DIR after indexing; point the query at + * the fixture's cache so trace_path finds the project. */ +static char *rh514_trace_inbound(RProj *lp, const char *fn_name) { + char args[512]; + snprintf(args, sizeof(args), + "{\"function_name\":\"%s\"," + "\"project\":\"%s\"," + "\"direction\":\"inbound\"," + "\"depth\":1}", + fn_name, lp->project); + + const char *prior_cache_dir = getenv("CBM_CACHE_DIR"); + char *saved_cache_dir = prior_cache_dir ? strdup(prior_cache_dir) : NULL; + cbm_setenv("CBM_CACHE_DIR", lp->cachedir, 1); + char *resp = cbm_mcp_handle_tool(lp->srv, "trace_path", args); + if (saved_cache_dir) { + cbm_setenv("CBM_CACHE_DIR", saved_cache_dir, 1); + free(saved_cache_dir); + } else { + cbm_unsetenv("CBM_CACHE_DIR"); + } + return resp; +} + +TEST(repro_issue514_ctor_param_property_callers) { + RProj lp; + cbm_store_t *store = + rh_index_files(&lp, k_nest_files, (int)(sizeof(k_nest_files) / sizeof(k_nest_files[0]))); + ASSERT_NOT_NULL(store); + + char *resp = rh514_trace_inbound(&lp, "findAll"); + ASSERT_NOT_NULL(resp); + fprintf(stderr, " [514] trace_path inbound response: %.400s\n", resp); + + ASSERT_NULL(strstr(resp, "function not found")); + ASSERT_NOT_NULL(strstr(resp, "CatController.list")); + + free(resp); + rh_cleanup(&lp, store); + PASS(); +} + +/* Same DI shape, but the injected class is imported through a tsconfig + * `paths` alias, the norm in NestJS monorepos. The alias branch of import + * resolution stripped the dotted basename (`cat.service` -> `cat`), so the + * IMPORTS edge missed the module and the DI call lost its target. */ +static const RFile k_nest_alias_files[] = { + {"apps/api/tsconfig.json", + "{\"compilerOptions\":{\"baseUrl\":\"./\",\"paths\":{\"@app/*\":[\"src/*\"]}}}\n"}, + {"apps/api/src/cats/cat.service.ts", "export class CatService {\n" + " findAll(): string[] {\n" + " return [\"a\"];\n" + " }\n" + "}\n"}, + {"apps/api/src/cats/cat.controller.ts", + "import { CatService } from \"@app/cats/cat.service\";\n" + "\n" + "export class CatController {\n" + " constructor(private readonly catService: CatService) {}\n" + "\n" + " list() {\n" + " return this.catService.findAll();\n" + " }\n" + "}\n"}, +}; + +TEST(repro_issue514_ctor_param_property_alias_import_callers) { + RProj lp; + cbm_store_t *store = rh_index_files( + &lp, k_nest_alias_files, (int)(sizeof(k_nest_alias_files) / sizeof(k_nest_alias_files[0]))); + ASSERT_NOT_NULL(store); + + char *resp = rh514_trace_inbound(&lp, "findAll"); + ASSERT_NOT_NULL(resp); + fprintf(stderr, " [514] trace_path inbound (alias) response: %.400s\n", resp); + + ASSERT_NULL(strstr(resp, "function not found")); + ASSERT_NOT_NULL(strstr(resp, "CatController.list")); + + free(resp); + rh_cleanup(&lp, store); + PASS(); +} + /* ── Suite ─────────────────────────────────────────────────────────────────── */ SUITE(repro_issue514) { RUN_TEST(repro_issue514_data_flow_surfaces_arg_expr); + RUN_TEST(repro_issue514_ctor_param_property_callers); + RUN_TEST(repro_issue514_ctor_param_property_alias_import_callers); } diff --git a/tests/test_edge_imports.c b/tests/test_edge_imports.c index 21f433c319..efa1c1dd10 100644 --- a/tests/test_edge_imports.c +++ b/tests/test_edge_imports.c @@ -597,6 +597,66 @@ TEST(ei_typescript_dotted_relative_import_targets_source_module_issue1682) { PASS(); } +/* #514 follow-up: the tsconfig `paths` branch must preserve dotted basenames + * like the relative branch (#1682). It stripped `.service` from + * `@app/cats/cat.service`, missed the module, and bound the import by last + * segment to an unrelated node. */ +TEST(ei_typescript_dotted_alias_import_targets_source_module) { + static const char *service_path = "apps/api/src/cats/cat.service.ts"; + static const char *controller_path = "apps/api/src/cats/cat.controller.ts"; + static const EILangFile f[] = { + {"apps/api/tsconfig.json", + "{\"compilerOptions\":{\"baseUrl\":\"./\",\"paths\":{\"@app/*\":[\"src/*\"]}}}\n"}, + {"apps/api/src/cats/cat.service.ts", "export class CatService {\n" + " findAll(): string[] {\n" + " return [\"a\"];\n" + " }\n" + "}\n"}, + {"apps/api/src/cats/cat.controller.ts", + "import { CatService } from \"@app/cats/cat.service\";\n" + "export class CatController {\n" + " constructor(private readonly catService: CatService) {}\n" + "}\n"}, + {"apps/api/src/other/service.ts", "export function service(): number {\n" + " return 1;\n" + "}\n"}, + }; + + EILangProj lp; + cbm_store_t *store = ei_index_files(&lp, f, (int)(sizeof(f) / sizeof(f[0]))); + ASSERT_NOT_NULL(store); + + int64_t controller_id = ei_node_id_for_file_label(store, lp.project, controller_path, "File"); + ASSERT_GT(controller_id, 0); + + cbm_edge_t *edges = NULL; + int edge_count = 0; + ASSERT_EQ( + cbm_store_find_edges_by_source_type(store, controller_id, "IMPORTS", &edges, &edge_count), + CBM_STORE_OK); + + bool saw_import = false; + bool target_ok = false; + for (int i = 0; i < edge_count; i++) { + const char *props = edges[i].properties_json ? edges[i].properties_json : ""; + if (!strstr(props, "\"local_name\":\"CatService\"")) { + continue; + } + cbm_node_t *target = (cbm_node_t *)calloc(1, sizeof(cbm_node_t)); + ASSERT_NOT_NULL(target); + ASSERT_EQ(cbm_store_find_node_by_id(store, edges[i].target_id, target), CBM_STORE_OK); + saw_import = true; + target_ok = target->file_path && strcmp(target->file_path, service_path) == 0; + cbm_store_free_nodes(target, 1); + } + cbm_store_free_edges(edges, edge_count); + ei_cleanup(&lp, store); + + ASSERT_TRUE(saw_import); + ASSERT_TRUE(target_ok); + PASS(); +} + /* TypeScript: default import `import helper from './util'`. */ TEST(ei_typescript_default_import) { static const EILangFile f[] = { @@ -2496,6 +2556,7 @@ SUITE(edge_imports) { /* ── GREEN GUARDS — TypeScript (must stay passing) ── */ RUN_TEST(ei_typescript_named_relative_import); RUN_TEST(ei_typescript_dotted_relative_import_targets_source_module_issue1682); + RUN_TEST(ei_typescript_dotted_alias_import_targets_source_module); RUN_TEST(ei_typescript_default_import); RUN_TEST(ei_typescript_namespace_import); RUN_TEST(ei_typescript_aliased_import); diff --git a/tests/test_path_alias.c b/tests/test_path_alias.c index 9158c04535..0361227b49 100644 --- a/tests/test_path_alias.c +++ b/tests/test_path_alias.c @@ -155,6 +155,22 @@ TEST(path_alias_strips_ext) { PASS(); } +/* Same extension set as the relative resolver (#1682); dotted basenames + * without a module extension stay intact. */ +TEST(path_alias_strips_module_ext_keeps_dotted_basename) { + cbm_path_alias_map_t *m = make_map(NULL, 1, "@/*", "src/*"); + char *r = cbm_path_alias_resolve(m, "@/lib/util.mjs"); + ASSERT_NOT_NULL(r); + ASSERT_STR_EQ(r, "src/lib/util"); + free(r); + r = cbm_path_alias_resolve(m, "@/cats/cat.service"); + ASSERT_NOT_NULL(r); + ASSERT_STR_EQ(r, "src/cats/cat.service"); + free(r); + free_map(m); + PASS(); +} + /* ── baseUrl fallback for non-relative, non-package imports ────── */ TEST(path_alias_baseurl_fallback) { @@ -557,6 +573,7 @@ void suite_path_alias(void) { RUN_TEST(path_alias_specificity_longest_first); RUN_TEST(path_alias_exact_match); RUN_TEST(path_alias_strips_ext); + RUN_TEST(path_alias_strips_module_ext_keeps_dotted_basename); RUN_TEST(path_alias_baseurl_fallback); RUN_TEST(path_alias_null_safety); RUN_TEST(path_alias_find_for_file_nearest_ancestor); From dc6effcf899095ef6c960c2e1d8869fb2c33965b Mon Sep 17 00:00:00 2001 From: chidev Date: Mon, 2 Nov 2026 16:30:37 -0600 Subject: [PATCH 3/6] fix(514): follow TS re-export chains to the defining file Named re-exports (export { a as b } from './m') and star re-exports (export * from './m') are now recorded as re-export imports instead of one module-level import. JS and TS call resolution uses the cross-file import map, so a call through a barrel file reaches the function's defining file, renames included. Builds on the index.ts barrel commit from #2402. Covers the rename and export * cases in #2718 and #1557. Signed-off-by: chidev --- internal/cbm/cbm.h | 7 ++++ internal/cbm/extract_imports.c | 61 +++++++++++++++++++++++++------ src/pipeline/pass_calls.c | 40 ++++++++++++++++++++ src/pipeline/pass_definitions.c | 14 ++++++- src/pipeline/pass_lsp_cross.c | 59 +++++++++++++++++++++++++++--- src/pipeline/pass_parallel.c | 14 ++++++- tests/test_extraction_imports.c | 25 +++++++++++++ tests/test_lsp_resolution_probe.c | 17 +++++++-- 8 files changed, 215 insertions(+), 22 deletions(-) diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index b0216eddb7..c80ba2fa65 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -407,6 +407,12 @@ typedef enum { CBM_IMPORT_KIND_CONST, // PHP `use const A\B` } CBMImportKind; +typedef enum { + CBM_REEXPORT_NONE = 0, + CBM_REEXPORT_NAMED, + CBM_REEXPORT_STAR, +} CBMReexportKind; + typedef struct { const char *local_name; // local alias or name const char *module_path; // resolved module path / QN @@ -416,6 +422,7 @@ typedef struct { bool type_checking_only; // Python: inside a module-level `if TYPE_CHECKING:` block -- // binds the name for annotations, never at runtime (#1277) CBMImportKind kind; // CBM_IMPORT_KIND_DEFAULT unless the syntax names a member kind + CBMReexportKind reexport; // ES `export {a as b} from` or `export * from`; NONE otherwise } CBMImport; /* True when an import binds its local name at runtime. A Python import under diff --git a/internal/cbm/extract_imports.c b/internal/cbm/extract_imports.c index c19debd806..fd7d5ed247 100644 --- a/internal/cbm/extract_imports.c +++ b/internal/cbm/extract_imports.c @@ -1814,6 +1814,54 @@ static bool process_commonjs_require(CBMExtractCtx *ctx, TSNode call) { return true; } +static void process_es_export_statement(CBMExtractCtx *ctx, TSNode node) { + CBMArena *a = ctx->arena; + TSNode src = ts_node_child_by_field_name(node, TS_FIELD("source")); + if (ts_node_is_null(src)) { + return; + } + char *path = strip_quotes(a, cbm_node_text(a, src, ctx->source)); + if (!path || !path[0]) { + return; + } + uint32_t nc = ts_node_child_count(node); + for (uint32_t i = 0; i < nc; i++) { + TSNode child = ts_node_child(node, i); + const char *kind = ts_node_type(child); + if (strcmp(kind, "export_clause") == 0) { + uint32_t sc = ts_node_child_count(child); + for (uint32_t j = 0; j < sc; j++) { + TSNode spec = ts_node_child(child, j); + if (strcmp(ts_node_type(spec), "export_specifier") != 0) { + continue; + } + TSNode original = ts_node_child_by_field_name(spec, TS_FIELD("name")); + TSNode alias = ts_node_child_by_field_name(spec, TS_FIELD("alias")); + if (ts_node_is_null(original)) { + continue; + } + const char *member = cbm_node_text(a, original, ctx->source); + const char *exported = ts_node_is_null(alias) + ? member + : cbm_node_text(a, alias, ctx->source); + CBMImport imp = {.local_name = exported, + .module_path = path, + .member = member, + .reexport = CBM_REEXPORT_NAMED}; + cbm_imports_push(&ctx->result->imports, a, imp); + } + return; + } + if (strcmp(kind, "*") == 0) { + CBMImport imp = {.local_name = "*", + .module_path = path, + .reexport = CBM_REEXPORT_STAR}; + cbm_imports_push(&ctx->result->imports, a, imp); + return; + } + } +} + static void walk_es_imports(CBMExtractCtx *ctx, TSNode root) { TSNodeStack stack; ts_nstack_init(&stack, ctx, CBM_SZ_512); @@ -1829,18 +1877,7 @@ static void walk_es_imports(CBMExtractCtx *ctx, TSNode root) { push_children = false; } } else if (strcmp(kind, "export_statement") == 0) { - /* Re-export: `export { x } from './mod'` / `export * from './mod'`. - * It carries a `source` string just like an import and creates the - * same module dependency. */ - TSNode src = ts_node_child_by_field_name(node, TS_FIELD("source")); - if (!ts_node_is_null(src)) { - char *path = strip_quotes(ctx->arena, cbm_node_text(ctx->arena, src, ctx->source)); - if (path && path[0]) { - CBMImport imp = {.local_name = path_last(ctx->arena, path), - .module_path = path}; - cbm_imports_push(&ctx->result->imports, ctx->arena, imp); - } - } + process_es_export_statement(ctx, node); } else if (strcmp(kind, "call_expression") == 0) { /* CommonJS require() — only consume the node if we recognized * it as a require call; otherwise keep traversing the children. */ diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index a673e75392..d06392027e 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -25,6 +25,7 @@ enum { #include #include "pipeline/pipeline_internal.h" #include "pipeline/lsp_resolve.h" +#include "pipeline/pass_lsp_cross.h" #include "pipeline/laravel_routing.h" #include "graph_buffer/graph_buffer.h" #include "foundation/log.h" @@ -138,6 +139,45 @@ static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path, *out_vals = NULL; *out_count = 0; + CBMLanguage lang = cbm_language_for_filename(rel_path); + if (lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX) { + const char **keys = NULL; + const char **owned_vals = NULL; + int count = 0; + cbm_pxc_build_import_map(ctx->gbuf, ctx->project_name, rel_path, lang, result, &keys, + &owned_vals, &count); + if (count > 0 && keys && owned_vals) { + const char **borrowed_vals = calloc((size_t)count, sizeof(*borrowed_vals)); + if (!borrowed_vals) { + cbm_pxc_free_import_map(keys, owned_vals, count); + return 0; + } + int kept = 0; + for (int i = 0; i < count; i++) { + const cbm_gbuf_node_t *node = + owned_vals[i] ? cbm_gbuf_find_by_qn(ctx->gbuf, owned_vals[i]) : NULL; + if (node && node->qualified_name) { + keys[kept] = keys[i]; + borrowed_vals[kept++] = node->qualified_name; + } else { + free((void *)keys[i]); + } + free((void *)owned_vals[i]); + } + free((void *)owned_vals); + if (kept > 0 && borrowed_vals) { + *out_keys = keys; + *out_vals = borrowed_vals; + *out_count = kept; + return 0; + } + free((void *)keys); + free((void *)borrowed_vals); + } else { + cbm_pxc_free_import_map(keys, owned_vals, count); + } + } + /* Fast path: build from cached extraction via the same resolver used for * IMPORTS edges. Do NOT use cbm_pipeline_fqn_module(module_path) — Python * from-imports store "pkg.symbol" (and aliases) in module_path, which is diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index 7c7aa438f4..21dfbb0c6d 100644 --- a/src/pipeline/pass_definitions.c +++ b/src/pipeline/pass_definitions.c @@ -683,7 +683,19 @@ static int create_import_edges_for_file(cbm_pipeline_ctx_t *ctx, const CBMFileRe char esc_ln[CBM_SZ_128]; cbm_json_escape(esc_ln, sizeof(esc_ln), imp->local_name ? imp->local_name : ""); char imp_props[CBM_SZ_256]; - snprintf(imp_props, sizeof(imp_props), "{\"local_name\":\"%s\"}", esc_ln); + if (imp->reexport == CBM_REEXPORT_NAMED && imp->member) { + char esc_member[CBM_SZ_128]; + cbm_json_escape(esc_member, sizeof(esc_member), imp->member); + snprintf(imp_props, sizeof(imp_props), + "{\"local_name\":\"%s\",\"reexport\":\"named\"," + "\"imported\":\"%s\"}", + esc_ln, esc_member); + } else if (imp->reexport == CBM_REEXPORT_STAR) { + snprintf(imp_props, sizeof(imp_props), + "{\"local_name\":\"%s\",\"reexport\":\"star\"}", esc_ln); + } else { + snprintf(imp_props, sizeof(imp_props), "{\"local_name\":\"%s\"}", esc_ln); + } cbm_gbuf_insert_edge(ctx->gbuf, source_node->id, target->id, "IMPORTS", imp_props); count++; } diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index d70da29501..af5da15bc3 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -1422,10 +1422,9 @@ static char *pxc_import_value_qn(CBMLanguage lang, const CBMFileResult *result, /* JS/TS barrels. An import that lands on a Folder (`./svc` -> svc/index.ts) * or on a module that only re-exports the name never reaches the declaring * module, so `module.Name` is not in the registry and DI member calls on the - * imported type stay unresolved. Follow the barrel's own IMPORTS edges (its - * `export ... from` statements) to the one module that defines `name`. Those - * edges do not record which names each statement re-exports, so the - * definition node is the proof; two different definitions fail closed. */ + * imported type stay unresolved. Follow only the barrel's re-export IMPORTS + * edges, preserving `export { original as public }` names across each hop. + * The definition node is the proof; two different definitions fail closed. */ enum { PXC_BARREL_MAX_DEPTH = 8 }; static bool pxc_is_js_family(CBMLanguage lang) { @@ -1445,6 +1444,30 @@ static char *pxc_defined_symbol_qn(const cbm_gbuf_t *gbuf, const char *project_n return NULL; } +static bool pxc_edge_string_prop(const char *json, const char *key, char *out, size_t out_sz) { + if (!json || !key || !out || out_sz == 0) { + return false; + } + char needle[CBM_SZ_64]; + int wrote = snprintf(needle, sizeof(needle), "\"%s\":\"", key); + if (wrote <= 0 || (size_t)wrote >= sizeof(needle)) { + return false; + } + const char *start = strstr(json, needle); + if (!start) { + return false; + } + start += (size_t)wrote; + const char *end = strchr(start, '"'); + size_t len = end ? (size_t)(end - start) : 0; + if (!end || len == 0 || len >= out_sz || memchr(start, '\\', len)) { + return false; + } + memcpy(out, start, len); + out[len] = '\0'; + return true; +} + /* `visited` holds barrel file QNs already searched for `name`: barrels form * DAGs (and cycles) where one lib index is reachable through many paths, and * re-walking it on each path is exponential in the depth cap. */ @@ -1454,6 +1477,13 @@ static char *pxc_barrel_symbol_qn(const cbm_gbuf_t *gbuf, const char *project_na if (!target || !target->label || !target->file_path || depth > PXC_BARREL_MAX_DEPTH) { return NULL; } + if (strcmp(target->label, "Module") != 0 && strcmp(target->label, "Folder") != 0 && + strcmp(target->label, "File") != 0) { + /* A named re-export edge reaches a definition directly when ordinary + * import resolution proved its imported member. The edge metadata has + * already matched the public/imported names at the caller above. */ + return target->qualified_name ? strdup(target->qualified_name) : NULL; + } static const char *const index_names[] = {"index.ts", "index.tsx", "index.mts", "index.js", "index.jsx", "index.mjs"}; char rel[CBM_SZ_1K]; @@ -1488,8 +1518,27 @@ static char *pxc_barrel_symbol_qn(const cbm_gbuf_t *gbuf, const char *project_na } char *found = NULL; for (int i = 0; i < edge_count; i++) { + char kind[CBM_SZ_16]; + char public_name[CBM_SZ_256]; + char imported_name[CBM_SZ_256]; + if (!pxc_edge_string_prop(edges[i]->properties_json, "reexport", kind, sizeof(kind))) { + continue; + } + const char *next_name = name; + if (strcmp(kind, "named") == 0) { + if (!pxc_edge_string_prop(edges[i]->properties_json, "local_name", public_name, + sizeof(public_name)) || + strcmp(public_name, name) != 0 || + !pxc_edge_string_prop(edges[i]->properties_json, "imported", imported_name, + sizeof(imported_name))) { + continue; + } + next_name = imported_name; + } else if (strcmp(kind, "star") != 0) { + continue; + } const cbm_gbuf_node_t *next = cbm_gbuf_find_by_id(gbuf, edges[i]->target_id); - char *qn = pxc_barrel_symbol_qn(gbuf, project_name, next, name, depth + 1, visited); + char *qn = pxc_barrel_symbol_qn(gbuf, project_name, next, next_name, depth + 1, visited); if (!qn) { continue; } diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index e02d68c675..0a399171c6 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -1900,7 +1900,19 @@ static int create_imports_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *re char esc_ln[CBM_SZ_128]; cbm_json_escape(esc_ln, sizeof(esc_ln), imp->local_name ? imp->local_name : ""); char imp_props[CBM_SZ_256]; - snprintf(imp_props, sizeof(imp_props), "{\"local_name\":\"%s\"}", esc_ln); + if (imp->reexport == CBM_REEXPORT_NAMED && imp->member) { + char esc_member[CBM_SZ_128]; + cbm_json_escape(esc_member, sizeof(esc_member), imp->member); + snprintf(imp_props, sizeof(imp_props), + "{\"local_name\":\"%s\",\"reexport\":\"named\"," + "\"imported\":\"%s\"}", + esc_ln, esc_member); + } else if (imp->reexport == CBM_REEXPORT_STAR) { + snprintf(imp_props, sizeof(imp_props), + "{\"local_name\":\"%s\",\"reexport\":\"star\"}", esc_ln); + } else { + snprintf(imp_props, sizeof(imp_props), "{\"local_name\":\"%s\"}", esc_ln); + } cbm_gbuf_insert_edge(ctx->gbuf, source_node->id, target->id, "IMPORTS", imp_props); count++; } diff --git a/tests/test_extraction_imports.c b/tests/test_extraction_imports.c index 1b1e93d6c8..002561ba73 100644 --- a/tests/test_extraction_imports.c +++ b/tests/test_extraction_imports.c @@ -1310,6 +1310,30 @@ TEST(es_named_import_member_survives_compaction) { PASS(); } +TEST(es_reexport_names_and_star_survive_compaction) { + CBMFileResult *r = do_extract("export { internal as publicName, plain } from './child.js';\n" + "export * from './nested.js';\n", + CBM_LANG_TYPESCRIPT, "barrel.ts"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + cbm_result_compact(r); + const CBMImport *renamed = imp_by_local(r, "publicName"); + const CBMImport *plain = imp_by_local(r, "plain"); + const CBMImport *star = imp_by_local(r, "*"); + ASSERT_NOT_NULL(renamed); + ASSERT_NOT_NULL(plain); + ASSERT_NOT_NULL(star); + ASSERT_STR_EQ(renamed->module_path, "./child.js"); + ASSERT_STR_EQ(renamed->member, "internal"); + ASSERT_EQ(renamed->reexport, CBM_REEXPORT_NAMED); + ASSERT_STR_EQ(plain->member, "plain"); + ASSERT_EQ(plain->reexport, CBM_REEXPORT_NAMED); + ASSERT_STR_EQ(star->module_path, "./nested.js"); + ASSERT_EQ(star->reexport, CBM_REEXPORT_STAR); + cbm_free_result(r); + PASS(); +} + /* ═══════════════════════════════════════════════════════════════════════════ * SUITE declaration — do NOT register in test_main.c; the main workstream will * ═══════════════════════════════════════════════════════════════════════════ */ @@ -1353,4 +1377,5 @@ SUITE(extraction_imports) { /* ES named-import member (call resolution spells calls through it) */ RUN_TEST(es_named_import_member_survives_compaction); + RUN_TEST(es_reexport_names_and_star_survive_compaction); } diff --git a/tests/test_lsp_resolution_probe.c b/tests/test_lsp_resolution_probe.c index 2a506899fb..ad69d7df08 100644 --- a/tests/test_lsp_resolution_probe.c +++ b/tests/test_lsp_resolution_probe.c @@ -1315,16 +1315,21 @@ TEST(lrp_ts_barrel_reexport_member_calls) { " return weightKg * 2;\n" " }\n" "}\n"}, + {"apps/api/libs/shipping/src/domain/internal.service.ts", + "export function internalCall(): number { return 7; }\n"}, + {"apps/api/libs/shipping/src/domain/public.ts", + "export { internalCall as publicCall } from './internal.service.js';\n"}, {"apps/api/libs/shipping/src/index.ts", "export * from \"./domain/tracking\";\n" "export {\n" " CarrierService,\n" "} from \"./domain/carrier.service\";\n" + "export { publicCall as apiCall } from './domain/public.js';\n" "export type { LabelPrintParams } from \"./application/service/label-print.service\";\n"}, {"apps/api/libs/invoice/src/application/command/handler/ship-invoice.handler.ts", "import { LabelPrintService } from " "\"@acme/shipping/application/service/label-print.service\";\n" - "import { CarrierService, TrackingCodeService } from \"@acme/shipping\";\n" + "import { apiCall, CarrierService, TrackingCodeService } from \"@acme/shipping\";\n" "export class ShipInvoiceHandler {\n" " constructor(\n" " private readonly labelPrintService: LabelPrintService,\n" @@ -1334,8 +1339,9 @@ TEST(lrp_ts_barrel_reexport_member_calls) { " execute(shipmentId: string): string {\n" " const cost = this.carrierService.quote(3);\n" " const code = this.trackingCodeService.generate(shipmentId);\n" + " const renamed = apiCall();\n" " const label = this.labelPrintService.printStream({ shipmentId });\n" - " return `${label}:${code}:${cost}`;\n" + " return `${label}:${code}:${cost}:${renamed}`;\n" " }\n" "}\n"}, {"apps/api/libs/invoice/src/application/service/label-preview.service.ts", @@ -1360,16 +1366,21 @@ TEST(lrp_ts_barrel_reexport_member_calls) { "LabelPrintService.printWithStats"); int b4 = lrp_exact_edge_by_qn_suffix(store, lp.project, "CALLS", "ShipInvoiceHandler.execute", "TrackingCodeService.generate"); + int renamed = lrp_exact_edge_by_qn_suffix(store, lp.project, "CALLS", + "ShipInvoiceHandler.execute", + "internal.service.internalCall"); int self = lrp_exact_edge_by_qn_suffix(store, lp.project, "CALLS", "LabelPrintService.printStream", "LabelPrintService.printWithStats"); - printf(" [ts/barrel] B1=%d B2=%d B3=%d B4=%d self=%d\n", b1, b2, b3, b4, self); + printf(" [ts/barrel] B1=%d B2=%d B3=%d B4=%d renamed=%d self=%d\n", b1, b2, b3, b4, + renamed, self); lrp_cleanup(&lp, store); ASSERT_EQ(b1, 1); ASSERT_EQ(b2, 1); ASSERT_EQ(b3, 1); ASSERT_EQ(b4, 1); + ASSERT_EQ(renamed, 1); ASSERT_EQ(self, 1); PASS(); } From cc9504d4a141a88b837d73f6576e803b3f9c2c03 Mon Sep 17 00:00:00 2001 From: chidev Date: Mon, 2 Nov 2026 19:03:01 -0600 Subject: [PATCH 4/6] fix(514): search a re-exported module once per name, not once per walk The barrel walker kept a visited set of file QNs. A barrel with both `export * from './m'` and `export { a as b } from './m'` searched m for `b` through the star edge first, marked m visited, and then skipped m when the renamed edge asked for `a`. The call to `b` got no edge. The visited set is now keyed on (file QN, name). Each key is an owned string stored as its own value and freed with the table. Tested: new lrp_ts_barrel_star_and_renamed_same_module fails before (renamed=0) and passes after. Suites extraction_imports, edge_imports, path_alias, mcp, lsp_resolution_probe: 626 passed, 4 skipped. The issue repro binds both calls at 0.95. Signed-off-by: chidev --- src/pipeline/pass_lsp_cross.c | 34 +++++++++++++++++++++++++++---- tests/test_lsp_resolution_probe.c | 31 ++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index af5da15bc3..b4c9548a9b 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -1468,7 +1468,19 @@ static bool pxc_edge_string_prop(const char *json, const char *key, char *out, s return true; } -/* `visited` holds barrel file QNs already searched for `name`: barrels form +static void pxc_free_visit_key(const char *key, void *value, void *userdata) { + (void)key; + (void)userdata; + free(value); +} + +/* Free a `visited` table built by pxc_barrel_symbol_qn and its owned keys. */ +static void pxc_barrel_visited_free(CBMHashTable *visited) { + cbm_ht_foreach(visited, pxc_free_visit_key, NULL); + cbm_ht_free(visited); +} + +/* `visited` holds (barrel file QN, name) pairs already searched: barrels form * DAGs (and cycles) where one lib index is reachable through many paths, and * re-walking it on each path is exponential in the depth cap. */ static char *pxc_barrel_symbol_qn(const cbm_gbuf_t *gbuf, const char *project_name, @@ -1501,10 +1513,24 @@ static char *pxc_barrel_symbol_qn(const cbm_gbuf_t *gbuf, const char *project_na free(file_qn); } } - if (!file_node || cbm_ht_has(visited, file_node->qualified_name)) { + if (!file_node || !file_node->qualified_name) { + return NULL; + } + /* Key on file and name: a named re-export renames the symbol, so one + * file can be searched for two names in one walk (`export *` and + * `export { a as b }` from the same module). The key owns its bytes and + * is the stored value; pxc_barrel_visited_free releases it. */ + size_t vk_len = strlen(file_node->qualified_name) + 1 + strlen(name) + 1; + char *visit_key = (char *)malloc(vk_len); + if (!visit_key) { + return NULL; + } + snprintf(visit_key, vk_len, "%s\x1f%s", file_node->qualified_name, name); + if (cbm_ht_has(visited, visit_key)) { + free(visit_key); return NULL; } - cbm_ht_set(visited, file_node->qualified_name, (void *)file_node); + cbm_ht_set(visited, visit_key, visit_key); char *defined = pxc_defined_symbol_qn(gbuf, project_name, rel, name); if (defined) { return defined; @@ -1710,7 +1736,7 @@ int cbm_pxc_build_import_map_ex(const cbm_gbuf_t *gbuf, const char *project_name CBMHashTable *visited = direct ? NULL : cbm_ht_create(16); if (visited) { barrel = pxc_barrel_symbol_qn(gbuf, project_name, target, local, 0, visited); - cbm_ht_free(visited); + pxc_barrel_visited_free(visited); } free(direct); if (barrel) { diff --git a/tests/test_lsp_resolution_probe.c b/tests/test_lsp_resolution_probe.c index ad69d7df08..eaefe5bece 100644 --- a/tests/test_lsp_resolution_probe.c +++ b/tests/test_lsp_resolution_probe.c @@ -1385,6 +1385,36 @@ TEST(lrp_ts_barrel_reexport_member_calls) { PASS(); } +/* One barrel re-exports the same module twice: `export *` and a renamed + * `export { innerFn as renamedFn }`. The walk searches impl for renamedFn + * through the star edge first; it must still search impl for innerFn + * through the renamed edge, so the call binds to impl's innerFn. */ +TEST(lrp_ts_barrel_star_and_renamed_same_module) { + static const LRP_File f[] = { + {"src/lib/impl.ts", "export function innerFn(x: number): number { return x - 1; }\n" + "export function starFn(x: number): number { return x * 2; }\n"}, + {"src/lib/barrel.ts", "export * from './impl';\n" + "export { innerFn as renamedFn } from './impl';\n"}, + {"src/app.ts", "import { renamedFn, starFn } from './lib/barrel';\n" + "export function run(x: number): number {\n" + " return renamedFn(x) + starFn(x);\n" + "}\n"}, + }; + LRP_Proj lp; + cbm_store_t *store = lrp_index(&lp, f, (int)(sizeof(f) / sizeof(f[0]))); + ASSERT_NOT_NULL(store); + + int renamed = lrp_exact_edge_by_qn_suffix(store, lp.project, "CALLS", "app.run", + "impl.innerFn"); + int star = lrp_exact_edge_by_qn_suffix(store, lp.project, "CALLS", "app.run", "impl.starFn"); + printf(" [ts/barrel-same-module] renamed=%d star=%d\n", renamed, star); + lrp_cleanup(&lp, store); + + ASSERT_EQ(renamed, 1); + ASSERT_EQ(star, 1); + PASS(); +} + /* ══════════════════════════════════════════════════════════════════════ * ─── GROUP 7: JAVA (lsp_cross NOT WIRED) ───────────────────────────── * ══════════════════════════════════════════════════════════════════════ @@ -2033,6 +2063,7 @@ SUITE(lsp_resolution_probe) { RUN_TEST(lrp_ts_s7_generic); RUN_TEST(lrp_ts_s8_field_type_hint); RUN_TEST(lrp_ts_barrel_reexport_member_calls); + RUN_TEST(lrp_ts_barrel_star_and_renamed_same_module); /* ── Java (lsp_cross NOT WIRED) — S1 GREEN, S2–S8 RED reproductions ── */ RUN_TEST(lrp_java_s1_crossfile_call); From 936b4c4a9a28ec0bb45145f853b6dbf67b522d66 Mon Sep 17 00:00:00 2001 From: chidev Date: Mon, 2 Nov 2026 20:51:54 -0600 Subject: [PATCH 5/6] style: clang-format and cppcheck fixes for the re-export commits Formats the changed lines with clang-format 20.1.8, drops a condition that is always true (pass_calls.c), and narrows the scope of public_name (pass_lsp_cross.c), as the lint job reported. Signed-off-by: chidev --- internal/cbm/cbm.h | 16 ++++++++-------- internal/cbm/extract_imports.c | 9 +++------ src/pipeline/pass_calls.c | 2 +- src/pipeline/pass_lsp_cross.c | 2 +- tests/test_lsp_resolution_probe.c | 13 ++++++------- 5 files changed, 19 insertions(+), 23 deletions(-) diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index c80ba2fa65..d455e16d47 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -414,14 +414,14 @@ typedef enum { } CBMReexportKind; typedef struct { - const char *local_name; // local alias or name - const char *module_path; // resolved module path / QN - const char *member; // ES named import: the exported name the local binds (`get` in - // `import { get as navigate } from "axios"`); NULL otherwise - bool is_default; // ES default import (`import X from "Y"`), JS/TS only (#1916) - bool type_checking_only; // Python: inside a module-level `if TYPE_CHECKING:` block -- - // binds the name for annotations, never at runtime (#1277) - CBMImportKind kind; // CBM_IMPORT_KIND_DEFAULT unless the syntax names a member kind + const char *local_name; // local alias or name + const char *module_path; // resolved module path / QN + const char *member; // ES named import: the exported name the local binds (`get` in + // `import { get as navigate } from "axios"`); NULL otherwise + bool is_default; // ES default import (`import X from "Y"`), JS/TS only (#1916) + bool type_checking_only; // Python: inside a module-level `if TYPE_CHECKING:` block -- + // binds the name for annotations, never at runtime (#1277) + CBMImportKind kind; // CBM_IMPORT_KIND_DEFAULT unless the syntax names a member kind CBMReexportKind reexport; // ES `export {a as b} from` or `export * from`; NONE otherwise } CBMImport; diff --git a/internal/cbm/extract_imports.c b/internal/cbm/extract_imports.c index fd7d5ed247..bd69ba06f1 100644 --- a/internal/cbm/extract_imports.c +++ b/internal/cbm/extract_imports.c @@ -1841,9 +1841,8 @@ static void process_es_export_statement(CBMExtractCtx *ctx, TSNode node) { continue; } const char *member = cbm_node_text(a, original, ctx->source); - const char *exported = ts_node_is_null(alias) - ? member - : cbm_node_text(a, alias, ctx->source); + const char *exported = + ts_node_is_null(alias) ? member : cbm_node_text(a, alias, ctx->source); CBMImport imp = {.local_name = exported, .module_path = path, .member = member, @@ -1853,9 +1852,7 @@ static void process_es_export_statement(CBMExtractCtx *ctx, TSNode node) { return; } if (strcmp(kind, "*") == 0) { - CBMImport imp = {.local_name = "*", - .module_path = path, - .reexport = CBM_REEXPORT_STAR}; + CBMImport imp = {.local_name = "*", .module_path = path, .reexport = CBM_REEXPORT_STAR}; cbm_imports_push(&ctx->result->imports, a, imp); return; } diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index d06392027e..de14812e19 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -165,7 +165,7 @@ static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path, free((void *)owned_vals[i]); } free((void *)owned_vals); - if (kept > 0 && borrowed_vals) { + if (kept > 0) { *out_keys = keys; *out_vals = borrowed_vals; *out_count = kept; diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index b4c9548a9b..8a879f706e 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -1545,13 +1545,13 @@ static char *pxc_barrel_symbol_qn(const cbm_gbuf_t *gbuf, const char *project_na char *found = NULL; for (int i = 0; i < edge_count; i++) { char kind[CBM_SZ_16]; - char public_name[CBM_SZ_256]; char imported_name[CBM_SZ_256]; if (!pxc_edge_string_prop(edges[i]->properties_json, "reexport", kind, sizeof(kind))) { continue; } const char *next_name = name; if (strcmp(kind, "named") == 0) { + char public_name[CBM_SZ_256]; if (!pxc_edge_string_prop(edges[i]->properties_json, "local_name", public_name, sizeof(public_name)) || strcmp(public_name, name) != 0 || diff --git a/tests/test_lsp_resolution_probe.c b/tests/test_lsp_resolution_probe.c index eaefe5bece..41579ec9bc 100644 --- a/tests/test_lsp_resolution_probe.c +++ b/tests/test_lsp_resolution_probe.c @@ -1366,14 +1366,13 @@ TEST(lrp_ts_barrel_reexport_member_calls) { "LabelPrintService.printWithStats"); int b4 = lrp_exact_edge_by_qn_suffix(store, lp.project, "CALLS", "ShipInvoiceHandler.execute", "TrackingCodeService.generate"); - int renamed = lrp_exact_edge_by_qn_suffix(store, lp.project, "CALLS", - "ShipInvoiceHandler.execute", - "internal.service.internalCall"); + int renamed = lrp_exact_edge_by_qn_suffix( + store, lp.project, "CALLS", "ShipInvoiceHandler.execute", "internal.service.internalCall"); int self = lrp_exact_edge_by_qn_suffix(store, lp.project, "CALLS", "LabelPrintService.printStream", "LabelPrintService.printWithStats"); - printf(" [ts/barrel] B1=%d B2=%d B3=%d B4=%d renamed=%d self=%d\n", b1, b2, b3, b4, - renamed, self); + printf(" [ts/barrel] B1=%d B2=%d B3=%d B4=%d renamed=%d self=%d\n", b1, b2, b3, b4, renamed, + self); lrp_cleanup(&lp, store); ASSERT_EQ(b1, 1); @@ -1404,8 +1403,8 @@ TEST(lrp_ts_barrel_star_and_renamed_same_module) { cbm_store_t *store = lrp_index(&lp, f, (int)(sizeof(f) / sizeof(f[0]))); ASSERT_NOT_NULL(store); - int renamed = lrp_exact_edge_by_qn_suffix(store, lp.project, "CALLS", "app.run", - "impl.innerFn"); + int renamed = + lrp_exact_edge_by_qn_suffix(store, lp.project, "CALLS", "app.run", "impl.innerFn"); int star = lrp_exact_edge_by_qn_suffix(store, lp.project, "CALLS", "app.run", "impl.starFn"); printf(" [ts/barrel-same-module] renamed=%d star=%d\n", renamed, star); lrp_cleanup(&lp, store); From 2f999c30a94398d488de7aed952512f6f173a1d9 Mon Sep 17 00:00:00 2001 From: chidev Date: Tue, 3 Nov 2026 00:12:52 -0600 Subject: [PATCH 6/6] refactor: allocate through mem_core in the re-export code The memory-core lint gate counted new raw allocator sites in the re-export commits (pass_calls.c 22 -> 28, pass_lsp_cross.c 49 -> 61). - The barrel walker now returns the graph node it reaches, borrowed for the life of the graph buffer, instead of a strdup'd QN. The import map stores that node's QN through the existing pxc_import_value_qn copy. - File and symbol lookups use cbm_pipeline_file_node and a stack-built symbol QN (cbm_pipeline_fqn_symbol_prefix_len), so they allocate nothing. - The visited-set keys are allocated and freed with cbm_alloc/cbm_free (CBM_MEM_CLASS_OTHER). - pass_calls keeps the JS/TS cross-file map as built and frees it with cbm_pxc_free_import_map, instead of copying it into a borrowed array. Both files are back at their baseline counts. Signed-off-by: chidev --- src/pipeline/pass_calls.c | 68 ++++++++++----------- src/pipeline/pass_lsp_cross.c | 108 +++++++++++++++++++--------------- 2 files changed, 90 insertions(+), 86 deletions(-) diff --git a/src/pipeline/pass_calls.c b/src/pipeline/pass_calls.c index de14812e19..212dc47d86 100644 --- a/src/pipeline/pass_calls.c +++ b/src/pipeline/pass_calls.c @@ -132,50 +132,35 @@ static const cbm_gbuf_node_t *pc_find_by_computed_qn(const cbm_gbuf_t *gbuf, con return n; } +/* The import map of rel_path. *out_vals_owned says who owns the value strings: + * the JS/TS map is cbm_pxc_build_import_map's (values owned), every other + * path borrows its values from the graph buffer. free_import_map takes the + * same flag. */ static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path, const CBMFileResult *result, const char ***out_keys, - const char ***out_vals, int *out_count) { + const char ***out_vals, int *out_count, bool *out_vals_owned) { *out_keys = NULL; *out_vals = NULL; *out_count = 0; + *out_vals_owned = false; + /* JS/TS: the cross-file map follows re-export chains to the defining + * file, and every value it holds is the QN of a graph node. */ CBMLanguage lang = cbm_language_for_filename(rel_path); if (lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX) { const char **keys = NULL; - const char **owned_vals = NULL; + const char **vals = NULL; int count = 0; - cbm_pxc_build_import_map(ctx->gbuf, ctx->project_name, rel_path, lang, result, &keys, - &owned_vals, &count); - if (count > 0 && keys && owned_vals) { - const char **borrowed_vals = calloc((size_t)count, sizeof(*borrowed_vals)); - if (!borrowed_vals) { - cbm_pxc_free_import_map(keys, owned_vals, count); - return 0; - } - int kept = 0; - for (int i = 0; i < count; i++) { - const cbm_gbuf_node_t *node = - owned_vals[i] ? cbm_gbuf_find_by_qn(ctx->gbuf, owned_vals[i]) : NULL; - if (node && node->qualified_name) { - keys[kept] = keys[i]; - borrowed_vals[kept++] = node->qualified_name; - } else { - free((void *)keys[i]); - } - free((void *)owned_vals[i]); - } - free((void *)owned_vals); - if (kept > 0) { - *out_keys = keys; - *out_vals = borrowed_vals; - *out_count = kept; - return 0; - } - free((void *)keys); - free((void *)borrowed_vals); - } else { - cbm_pxc_free_import_map(keys, owned_vals, count); + cbm_pxc_build_import_map(ctx->gbuf, ctx->project_name, rel_path, lang, result, &keys, &vals, + &count); + if (count > 0) { + *out_keys = keys; + *out_vals = vals; + *out_count = count; + *out_vals_owned = true; + return 0; } + cbm_pxc_free_import_map(keys, vals, count); } /* Fast path: build from cached extraction via the same resolver used for @@ -265,7 +250,11 @@ static int build_import_map(cbm_pipeline_ctx_t *ctx, const char *rel_path, return 0; } -static void free_import_map(const char **keys, const char **vals, int count) { +static void free_import_map(const char **keys, const char **vals, int count, bool vals_owned) { + if (vals_owned) { + cbm_pxc_free_import_map(keys, vals, count); + return; + } if (keys) { for (int i = 0; i < count; i++) { free((void *)keys[i]); @@ -1576,7 +1565,8 @@ int cbm_pipeline_pass_calls(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file const char **imp_keys = NULL; const char **imp_vals = NULL; int imp_count = 0; - build_import_map(ctx, rel, result, &imp_keys, &imp_vals, &imp_count); + bool imp_vals_owned = false; + build_import_map(ctx, rel, result, &imp_keys, &imp_vals, &imp_count, &imp_vals_owned); cbm_php_vendor_names_t php_vendor; cbm_pipeline_php_vendor_names_build(ctx->gbuf, ctx->project_name, rel, result, &php_vendor); @@ -1614,7 +1604,7 @@ int cbm_pipeline_pass_calls(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file cbm_free(CBM_MEM_CLASS_RESOLVE, evidence); free(module_qn); cbm_pipeline_php_vendor_names_free(&php_vendor); - free_import_map(imp_keys, imp_vals, imp_count); + free_import_map(imp_keys, imp_vals, imp_count, imp_vals_owned); if (result_owned) { cbm_free_result(result); } @@ -1750,7 +1740,9 @@ void cbm_pipeline_pass_fastapi_depends(cbm_pipeline_ctx_t *ctx, const cbm_file_i const char **imp_keys = NULL; const char **imp_vals = NULL; int imp_count = 0; - build_import_map(ctx, files[i].rel_path, result, &imp_keys, &imp_vals, &imp_count); + bool imp_vals_owned = false; + build_import_map(ctx, files[i].rel_path, result, &imp_keys, &imp_vals, &imp_count, + &imp_vals_owned); for (int d = 0; d < result->defs.count; d++) { CBMDefinition *def = &result->defs.items[d]; @@ -1769,7 +1761,7 @@ void cbm_pipeline_pass_fastapi_depends(cbm_pipeline_ctx_t *ctx, const cbm_file_i } free(module_qn); - free_import_map(imp_keys, imp_vals, imp_count); + free_import_map(imp_keys, imp_vals, imp_count, imp_vals_owned); free(source); } diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index 8a879f706e..3c32068d3a 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -1431,16 +1431,33 @@ static bool pxc_is_js_family(CBMLanguage lang) { return lang == CBM_LANG_JAVASCRIPT || lang == CBM_LANG_TYPESCRIPT || lang == CBM_LANG_TSX; } -/* QN of `name` when the file at rel_path defines it, else NULL. */ -static char *pxc_defined_symbol_qn(const cbm_gbuf_t *gbuf, const char *project_name, - const char *rel_path, const char *name) { - char *qn = cbm_pipeline_fqn_compute(project_name, rel_path, name); - const cbm_gbuf_node_t *node = qn ? cbm_gbuf_find_by_qn(gbuf, qn) : NULL; - if (node && node->label && strcmp(node->label, "Module") != 0 && +/* The definition of `name` in the file at rel_path, else NULL. The symbol QN + * cbm_pipeline_fqn_compute would give is the file's symbol prefix (a byte + * prefix of its File node QN: extension and a trailing index segment are + * only ever cut from the end) plus `.name`, so it is built on the stack and + * the lookup allocates nothing. */ +static const cbm_gbuf_node_t *pxc_defined_symbol(const cbm_gbuf_t *gbuf, const char *project_name, + const char *rel_path, const char *name) { + const cbm_gbuf_node_t *file_node = cbm_pipeline_file_node(gbuf, project_name, rel_path); + if (!file_node || !file_node->qualified_name) { + return NULL; + } + size_t prefix_len = cbm_pipeline_fqn_symbol_prefix_len(project_name, rel_path); + if (prefix_len == 0 || prefix_len >= strlen(file_node->qualified_name) || + file_node->qualified_name[prefix_len] != '.') { + return NULL; + } + char qn[CBM_SZ_1K]; + int wrote = + snprintf(qn, sizeof(qn), "%.*s.%s", (int)prefix_len, file_node->qualified_name, name); + if (wrote <= 0 || (size_t)wrote >= sizeof(qn)) { + return NULL; + } + const cbm_gbuf_node_t *node = cbm_gbuf_find_by_qn(gbuf, qn); + if (node && node->qualified_name && node->label && strcmp(node->label, "Module") != 0 && strcmp(node->label, "Folder") != 0 && strcmp(node->label, "File") != 0) { - return qn; + return node; } - free(qn); return NULL; } @@ -1471,10 +1488,10 @@ static bool pxc_edge_string_prop(const char *json, const char *key, char *out, s static void pxc_free_visit_key(const char *key, void *value, void *userdata) { (void)key; (void)userdata; - free(value); + cbm_free(CBM_MEM_CLASS_OTHER, value); } -/* Free a `visited` table built by pxc_barrel_symbol_qn and its owned keys. */ +/* Free a `visited` table built by pxc_barrel_symbol and its owned keys. */ static void pxc_barrel_visited_free(CBMHashTable *visited) { cbm_ht_foreach(visited, pxc_free_visit_key, NULL); cbm_ht_free(visited); @@ -1482,10 +1499,11 @@ static void pxc_barrel_visited_free(CBMHashTable *visited) { /* `visited` holds (barrel file QN, name) pairs already searched: barrels form * DAGs (and cycles) where one lib index is reachable through many paths, and - * re-walking it on each path is exponential in the depth cap. */ -static char *pxc_barrel_symbol_qn(const cbm_gbuf_t *gbuf, const char *project_name, - const cbm_gbuf_node_t *target, const char *name, int depth, - CBMHashTable *visited) { + * re-walking it on each path is exponential in the depth cap. The result is a + * graph node: the caller borrows it for as long as the graph buffer lives. */ +static const cbm_gbuf_node_t *pxc_barrel_symbol(const cbm_gbuf_t *gbuf, const char *project_name, + const cbm_gbuf_node_t *target, const char *name, + int depth, CBMHashTable *visited) { if (!target || !target->label || !target->file_path || depth > PXC_BARREL_MAX_DEPTH) { return NULL; } @@ -1494,7 +1512,7 @@ static char *pxc_barrel_symbol_qn(const cbm_gbuf_t *gbuf, const char *project_na /* A named re-export edge reaches a definition directly when ordinary * import resolution proved its imported member. The edge metadata has * already matched the public/imported names at the caller above. */ - return target->qualified_name ? strdup(target->qualified_name) : NULL; + return target->qualified_name ? target : NULL; } static const char *const index_names[] = {"index.ts", "index.tsx", "index.mts", "index.js", "index.jsx", "index.mjs"}; @@ -1502,15 +1520,11 @@ static char *pxc_barrel_symbol_qn(const cbm_gbuf_t *gbuf, const char *project_na const cbm_gbuf_node_t *file_node = NULL; if (strcmp(target->label, "Module") == 0) { snprintf(rel, sizeof(rel), "%s", target->file_path); - char *file_qn = cbm_pipeline_fqn_compute(project_name, rel, "__file__"); - file_node = file_qn ? cbm_gbuf_find_by_qn(gbuf, file_qn) : NULL; - free(file_qn); + file_node = cbm_pipeline_file_node(gbuf, project_name, rel); } else if (strcmp(target->label, "Folder") == 0) { for (size_t i = 0; i < sizeof(index_names) / sizeof(index_names[0]) && !file_node; i++) { snprintf(rel, sizeof(rel), "%s/%s", target->file_path, index_names[i]); - char *file_qn = cbm_pipeline_fqn_compute(project_name, rel, "__file__"); - file_node = file_qn ? cbm_gbuf_find_by_qn(gbuf, file_qn) : NULL; - free(file_qn); + file_node = cbm_pipeline_file_node(gbuf, project_name, rel); } } if (!file_node || !file_node->qualified_name) { @@ -1521,17 +1535,17 @@ static char *pxc_barrel_symbol_qn(const cbm_gbuf_t *gbuf, const char *project_na * `export { a as b }` from the same module). The key owns its bytes and * is the stored value; pxc_barrel_visited_free releases it. */ size_t vk_len = strlen(file_node->qualified_name) + 1 + strlen(name) + 1; - char *visit_key = (char *)malloc(vk_len); + char *visit_key = (char *)cbm_alloc(CBM_MEM_CLASS_OTHER, vk_len); if (!visit_key) { return NULL; } snprintf(visit_key, vk_len, "%s\x1f%s", file_node->qualified_name, name); if (cbm_ht_has(visited, visit_key)) { - free(visit_key); + cbm_free(CBM_MEM_CLASS_OTHER, visit_key); return NULL; } cbm_ht_set(visited, visit_key, visit_key); - char *defined = pxc_defined_symbol_qn(gbuf, project_name, rel, name); + const cbm_gbuf_node_t *defined = pxc_defined_symbol(gbuf, project_name, rel, name); if (defined) { return defined; } @@ -1542,7 +1556,7 @@ static char *pxc_barrel_symbol_qn(const cbm_gbuf_t *gbuf, const char *project_na 0) { return NULL; } - char *found = NULL; + const cbm_gbuf_node_t *found = NULL; for (int i = 0; i < edge_count; i++) { char kind[CBM_SZ_16]; char imported_name[CBM_SZ_256]; @@ -1564,17 +1578,15 @@ static char *pxc_barrel_symbol_qn(const cbm_gbuf_t *gbuf, const char *project_na continue; } const cbm_gbuf_node_t *next = cbm_gbuf_find_by_id(gbuf, edges[i]->target_id); - char *qn = pxc_barrel_symbol_qn(gbuf, project_name, next, next_name, depth + 1, visited); - if (!qn) { + const cbm_gbuf_node_t *def = + pxc_barrel_symbol(gbuf, project_name, next, next_name, depth + 1, visited); + if (!def) { continue; } - if (found && strcmp(found, qn) != 0) { - free(found); - free(qn); + if (found && strcmp(found->qualified_name, def->qualified_name) != 0) { return NULL; } - free(found); - found = qn; + found = def; } return found; } @@ -1723,27 +1735,27 @@ int cbm_pxc_build_import_map_ex(const cbm_gbuf_t *gbuf, const char *project_name /* An annotation-only import (#1277) binds no CALLS name: it gets no value. */ bool binds = include_type_only || cbm_imports_local_binds_runtime_name(result ? &result->imports : NULL, local); - char *value = binds ? pxc_import_value_qn(lang, result, local, target) : NULL; - if (!value) { - free(local); - continue; - } - if (pxc_is_js_family(lang) && target->label && target->file_path) { - char *direct = strcmp(target->label, "Module") == 0 - ? pxc_defined_symbol_qn(gbuf, project_name, target->file_path, local) - : NULL; - char *barrel = NULL; - CBMHashTable *visited = direct ? NULL : cbm_ht_create(16); + /* JS/TS: a Module that defines the name keeps its own QN; a barrel + * that only re-exports it stands for the definition it reaches. */ + const cbm_gbuf_node_t *value_node = target; + if (binds && pxc_is_js_family(lang) && target->label && target->file_path && + (strcmp(target->label, "Module") != 0 || + !pxc_defined_symbol(gbuf, project_name, target->file_path, local))) { + CBMHashTable *visited = cbm_ht_create(16); + const cbm_gbuf_node_t *def = + visited ? pxc_barrel_symbol(gbuf, project_name, target, local, 0, visited) : NULL; if (visited) { - barrel = pxc_barrel_symbol_qn(gbuf, project_name, target, local, 0, visited); pxc_barrel_visited_free(visited); } - free(direct); - if (barrel) { - free(value); - value = barrel; + if (def) { + value_node = def; } } + char *value = binds ? pxc_import_value_qn(lang, result, local, value_node) : NULL; + if (!value) { + free(local); + continue; + } keys[count] = local; vals[count] = value; count++;