Merge pull request #1106 from DeusData/fix/1085-jsx-alias-calls

fix(pipeline): restore parallel CALLS resolution for LSP-unresolvable targets
This commit is contained in:
Martin Vogel
2026-07-16 01:14:09 +02:00
committed by GitHub
2 changed files with 72 additions and 5 deletions
+14 -5
View File
@@ -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) {
@@ -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);
}
+58
View File
@@ -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
*
@@ -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);