Skip to content

Commit 8c39465

Browse files
committed
fix(pipeline): restore parallel CALLS resolution for LSP-unresolvable targets
The parallel resolver dropped a CALLS edge whenever the LSP produced a resolution whose callee_qn was not a node in the graph buffer: the registry fallback lived in an else that ran only when NO LSP resolution existed, so an LSP-with-unresolvable-target left res empty and the edge was discarded outright. The sequential pass falls THROUGH to the registry resolver in the same situation. This diverged the two pipelines on the exact intersection the reporter identified (#1085): a 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, so lsp_target is NULL and the parallel path dropped it — while sequential resolved it via the import_map / unique_name registry path. On a Next.js repo this silently removed ~21% of the call graph (every alias-imported JSX composition edge), and the parallel path is the default above 50 files, so the default index was the broken one. The fix runs the registry fallback whenever the LSP did not yield a gbuf-resolvable target, matching the sequential fall-through and restoring seq/parallel parity. Reproduce-first: calls_jsx_component_via_tsconfig_alias_parallel_issue1085 indexes the alias+JSX shape through the parallel path (et_index_parallel pads past the 50-file threshold) and asserts the CALLS edges land on the component. RED before (0 edges), GREEN after. Closes #1085 Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent 807a4a3 commit 8c39465

2 files changed

Lines changed: 72 additions & 5 deletions

File tree

src/pipeline/pass_parallel.c

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2274,10 +2274,7 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
22742274
if (lsp) {
22752275
/* Canonicalise to the gbuf node's QN so res.qualified_name matches
22762276
* the gbuf even when the cross-file fallback had to prefix the
2277-
* project name. If neither lookup hits, leave res.qualified_name
2278-
* empty — the LSP was confident but its target isn't in the gbuf
2279-
* (external/unindexed), so drop the edge rather than fall back to
2280-
* the registry resolver, matching prior single-lookup semantics. */
2277+
* project name. */
22812278
lsp_target = cbm_pipeline_lsp_target_node(rc->main_gbuf, rc->project_name,
22822279
lsp->callee_qn, allow_tail);
22832280
if (lsp_target) {
@@ -2287,7 +2284,19 @@ static void resolve_file_calls(resolve_ctx_t *rc, resolve_worker_state_t *ws, CB
22872284
res.candidate_count = 1;
22882285
ws->lsp_overrides++;
22892286
}
2290-
} else {
2287+
}
2288+
/* #1085: fall back to the registry resolver whenever the LSP did not
2289+
* yield a gbuf-resolvable target — whether no LSP resolution existed,
2290+
* OR the LSP was confident but its callee_qn isn't a node in the gbuf
2291+
* (the JSX-via-tsconfig-alias case: the TS LSP resolves the element
2292+
* ref to an alias-path QN that never matches a def node, so lsp_target
2293+
* is NULL). The old `else` ran the registry ONLY when lsp was null, so
2294+
* an LSP-with-unresolvable-target dropped the edge outright — silently
2295+
* losing every alias-imported JSX component edge on the parallel path
2296+
* (~21% of a Next.js call graph) while the sequential pass, which falls
2297+
* THROUGH to the registry here, kept them. This restores seq/parallel
2298+
* parity via the import_map / unique_name resolution. */
2299+
if (!res.qualified_name || !res.qualified_name[0]) {
22912300
res = cbm_registry_resolve(rc->registry, call->callee_name, module_qn, imp_keys,
22922301
imp_vals, imp_count);
22932302
}

tests/test_edge_types_probe.c

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,63 @@ static cbm_store_t *et_index_parallel(EtProj *lp, const EtFile *meaningful, int
222222
return et_index_files(lp, files, n);
223223
}
224224

225+
/* #1085: count CALLS edges whose target node has `name`, indexing via the
226+
* PARALLEL path (et_index_parallel pads to >50 files). The parallel resolver
227+
* used to drop the edge whenever the LSP resolved a callee but its target QN
228+
* wasn't a gbuf node (JSX component imported through a tsconfig `paths` alias:
229+
* the TS LSP resolves the element ref to an alias-path QN that never matches a
230+
* def node) — sequential kept the edge via the registry import_map fallback,
231+
* so the two pipelines disagreed and ~21% of a Next.js call graph vanished on
232+
* the default (parallel) path. Needs >50 files to reproduce. */
233+
static int et_calls_to_name_parallel(const EtFile *meaningful, int n_mean, const char *name) {
234+
EtProj lp;
235+
cbm_store_t *store = et_index_parallel(&lp, meaningful, n_mean);
236+
int hits = 0;
237+
if (store) {
238+
cbm_edge_t *edges = NULL;
239+
int n = 0;
240+
if (cbm_store_find_edges_by_type(store, lp.project, "CALLS", &edges, &n) == CBM_STORE_OK) {
241+
for (int i = 0; i < n; i++) {
242+
cbm_node_t tgt;
243+
if (cbm_store_find_node_by_id(store, edges[i].target_id, &tgt) != CBM_STORE_OK)
244+
continue;
245+
if (tgt.name && strcmp(tgt.name, name) == 0)
246+
hits++;
247+
cbm_node_free_fields(&tgt);
248+
}
249+
cbm_store_free_edges(edges, n);
250+
}
251+
}
252+
et_cleanup(&lp, store);
253+
return hits;
254+
}
255+
256+
TEST(calls_jsx_component_via_tsconfig_alias_parallel_issue1085) {
257+
static const EtFile f[] = {
258+
{"tsconfig.json",
259+
"{ \"compilerOptions\": { \"baseUrl\": \".\", "
260+
"\"paths\": { \"@/*\": [\"./src/*\"] } } }\n"},
261+
{"src/components/ui/kpi-card.tsx",
262+
"export function KpiCard({ label }: { label: string }) {\n"
263+
" return <div>{label}</div>;\n}\n"},
264+
{"src/app/dashboard-a.tsx",
265+
"import { KpiCard } from \"@/components/ui/kpi-card\";\n"
266+
"export function DashboardA() {\n return <KpiCard label=\"a\" />;\n}\n"},
267+
{"src/app/dashboard-b.tsx",
268+
"import { KpiCard } from \"@/components/ui/kpi-card\";\n"
269+
"export function DashboardB() {\n return <KpiCard label=\"b\" />;\n}\n"}};
270+
/* RED before the fix: 0 (parallel drops alias-JSX). GREEN: both renders
271+
* resolve, exactly as the sequential path already does. */
272+
int hits = et_calls_to_name_parallel(f, 4, "KpiCard");
273+
if (hits < 2) {
274+
fprintf(stderr, " [1085] FAIL CALLS->KpiCard on parallel path = %d (expected >= 2); "
275+
"alias-imported JSX component edges dropped\n",
276+
hits);
277+
}
278+
ASSERT_TRUE(hits >= 2);
279+
PASS();
280+
}
281+
225282
/* ══════════════════════════════════════════════════════════════════
226283
* HANDLES — route→handler across web frameworks
227284
*
@@ -1514,6 +1571,7 @@ TEST(override_go_interface) {
15141571

15151572
SUITE(edge_types_probe) {
15161573
/* HANDLES — route→handler across web frameworks */
1574+
RUN_TEST(calls_jsx_component_via_tsconfig_alias_parallel_issue1085);
15171575
RUN_TEST(handles_flask_python);
15181576
RUN_TEST(handles_fastapi_python);
15191577
RUN_TEST(handles_drf_action_python);

0 commit comments

Comments
 (0)