@@ -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
15151572SUITE (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