Skip to content
Merged
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
19 changes: 14 additions & 5 deletions src/pipeline/pass_parallel.c
Original file line number Diff line number Diff line change
Expand Up @@ -2274,10 +2274,7 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
if (lsp) {
/* Canonicalise to the gbuf node's QN so res.qualified_name matches
* the gbuf even when the cross-file fallback had to prefix the
* project name. If neither lookup hits, leave res.qualified_name
* empty — the LSP was confident but its target isn't in the gbuf
* (external/unindexed), so drop the edge rather than fall back to
* the registry resolver, matching prior single-lookup semantics. */
* project name. */
lsp_target = cbm_pipeline_lsp_target_node(rc->main_gbuf, rc->project_name,
lsp->callee_qn, allow_tail);
if (lsp_target) {
Expand All @@ -2287,7 +2284,19 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
res.candidate_count = 1;
ws->lsp_overrides++;
}
} else {
}
/* #1085: fall back to the registry resolver whenever the LSP did not
* yield a gbuf-resolvable target — whether no LSP resolution existed,
* OR the LSP was confident but its callee_qn isn't a node in the gbuf
* (the JSX-via-tsconfig-alias case: the TS LSP resolves the element
* ref to an alias-path QN that never matches a def node, so lsp_target
* is NULL). The old `else` ran the registry ONLY when lsp was null, so
* an LSP-with-unresolvable-target dropped the edge outright — silently
* losing every alias-imported JSX component edge on the parallel path
* (~21% of a Next.js call graph) while the sequential pass, which falls
* THROUGH to the registry here, kept them. This restores seq/parallel
* parity via the import_map / unique_name resolution. */
if (!res.qualified_name || !res.qualified_name[0]) {
res = cbm_registry_resolve(rc->registry, call->callee_name, module_qn, imp_keys,
imp_vals, imp_count);
}
Expand Down
58 changes: 58 additions & 0 deletions tests/test_edge_types_probe.c
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,63 @@ static cbm_store_t *et_index_parallel(EtProj *lp, const EtFile *meaningful, int
return et_index_files(lp, files, n);
}

/* #1085: count CALLS edges whose target node has `name`, indexing via the
* PARALLEL path (et_index_parallel pads to >50 files). The parallel resolver
* used to drop the edge whenever the LSP resolved a callee but its target QN
* wasn't a gbuf node (JSX component imported through a tsconfig `paths` alias:
* the TS LSP resolves the element ref to an alias-path QN that never matches a
* def node) — sequential kept the edge via the registry import_map fallback,
* so the two pipelines disagreed and ~21% of a Next.js call graph vanished on
* the default (parallel) path. Needs >50 files to reproduce. */
static int et_calls_to_name_parallel(const EtFile *meaningful, int n_mean, const char *name) {
EtProj lp;
cbm_store_t *store = et_index_parallel(&lp, meaningful, n_mean);
int hits = 0;
if (store) {
cbm_edge_t *edges = NULL;
int n = 0;
if (cbm_store_find_edges_by_type(store, lp.project, "CALLS", &edges, &n) == CBM_STORE_OK) {
for (int i = 0; i < n; i++) {
cbm_node_t tgt;
if (cbm_store_find_node_by_id(store, edges[i].target_id, &tgt) != CBM_STORE_OK)
continue;
if (tgt.name && strcmp(tgt.name, name) == 0)
hits++;
cbm_node_free_fields(&tgt);
}
cbm_store_free_edges(edges, n);
}
}
et_cleanup(&lp, store);
return hits;
}

TEST(calls_jsx_component_via_tsconfig_alias_parallel_issue1085) {
static const EtFile f[] = {
{"tsconfig.json",
"{ \"compilerOptions\": { \"baseUrl\": \".\", "
"\"paths\": { \"@/*\": [\"./src/*\"] } } }\n"},
{"src/components/ui/kpi-card.tsx",
"export function KpiCard({ label }: { label: string }) {\n"
" return <div>{label}</div>;\n}\n"},
{"src/app/dashboard-a.tsx",
"import { KpiCard } from \"@/components/ui/kpi-card\";\n"
"export function DashboardA() {\n return <KpiCard label=\"a\" />;\n}\n"},
{"src/app/dashboard-b.tsx",
"import { KpiCard } from \"@/components/ui/kpi-card\";\n"
"export function DashboardB() {\n return <KpiCard label=\"b\" />;\n}\n"}};
/* RED before the fix: 0 (parallel drops alias-JSX). GREEN: both renders
* resolve, exactly as the sequential path already does. */
int hits = et_calls_to_name_parallel(f, 4, "KpiCard");
if (hits < 2) {
fprintf(stderr, " [1085] FAIL CALLS->KpiCard on parallel path = %d (expected >= 2); "
"alias-imported JSX component edges dropped\n",
hits);
}
ASSERT_TRUE(hits >= 2);
PASS();
}

/* ══════════════════════════════════════════════════════════════════
* HANDLES — route→handler across web frameworks
*
Expand Down Expand Up @@ -1514,6 +1571,7 @@ TEST(override_go_interface) {

SUITE(edge_types_probe) {
/* HANDLES — route→handler across web frameworks */
RUN_TEST(calls_jsx_component_via_tsconfig_alias_parallel_issue1085);
RUN_TEST(handles_flask_python);
RUN_TEST(handles_fastapi_python);
RUN_TEST(handles_drf_action_python);
Expand Down
Loading