From ef551460ce5fe432eca00458d338c24c910284d4 Mon Sep 17 00:00:00 2001 From: lorenzozanee Date: Sun, 11 Oct 2026 00:01:23 +0800 Subject: [PATCH 1/2] fix(ts-lsp): resolve JSX calls from top-level arrow components Signed-off-by: lorenzozanee --- internal/cbm/lsp/ts_lsp.c | 40 ++++++++++++++++++- tests/test_ts_lsp.c | 84 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 1 deletion(-) diff --git a/internal/cbm/lsp/ts_lsp.c b/internal/cbm/lsp/ts_lsp.c index c25a58bbe4..e18ee0c5ab 100644 --- a/internal/cbm/lsp/ts_lsp.c +++ b/internal/cbm/lsp/ts_lsp.c @@ -3760,6 +3760,31 @@ static void process_function_body(TSLSPContext *ctx, TSNode func_node, const cha ctx->current_scope = saved_scope; } +static void process_top_level_variable_arrows(TSLSPContext *ctx, TSNode declaration) { + if (!ctx || ts_node_is_null(declaration) || !ctx->module_qn) + return; + + uint32_t count = ts_node_named_child_count(declaration); + for (uint32_t i = 0; i < count; i++) { + TSNode declarator = ts_node_named_child(declaration, i); + if (!node_kind_is(declarator, "variable_declarator")) + continue; + + TSNode name = ts_node_child_by_field_name(declarator, "name", TS_LSP_FIELD_LEN("name")); + TSNode value = + ts_node_child_by_field_name(declarator, "value", TS_LSP_FIELD_LEN("value")); + if (!node_kind_is(name, "identifier") || !node_kind_is(value, "arrow_function")) + continue; + + char *function_name = node_text(ctx, name); + if (!function_name) + continue; + const char *function_qn = + cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, function_name); + process_function_body(ctx, value, function_qn, NULL); + } +} + void ts_lsp_process_file(TSLSPContext *ctx, TSNode root) { if (!ctx || ts_node_is_null(root)) return; @@ -3777,6 +3802,14 @@ void ts_lsp_process_file(TSLSPContext *ctx, TSNode root) { const char *kind = ts_node_type(child); if (strcmp(kind, "lexical_declaration") == 0 || strcmp(kind, "variable_declaration") == 0) { ts_process_statement(ctx, child); + } else if (strcmp(kind, "export_statement") == 0) { + uint32_t count = ts_node_named_child_count(child); + for (uint32_t j = 0; j < count; j++) { + TSNode inner = ts_node_named_child(child, j); + if (node_kind_is(inner, "lexical_declaration") || + node_kind_is(inner, "variable_declaration")) + ts_process_statement(ctx, inner); + } } } @@ -3785,7 +3818,9 @@ void ts_lsp_process_file(TSLSPContext *ctx, TSNode root) { TSNode child = kids[i]; const char *kind = ts_node_type(child); - if (strcmp(kind, "function_declaration") == 0) { + if (strcmp(kind, "lexical_declaration") == 0 || strcmp(kind, "variable_declaration") == 0) { + process_top_level_variable_arrows(ctx, child); + } else if (strcmp(kind, "function_declaration") == 0) { TSNode name = ts_node_child_by_field_name(child, "name", TS_LSP_FIELD_LEN("name")); if (ts_node_is_null(name) || !ctx->module_qn) continue; @@ -3824,6 +3859,9 @@ void ts_lsp_process_file(TSLSPContext *ctx, TSNode root) { continue; const char *fqn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, fn); process_function_body(ctx, inner, fqn, NULL); + } else if (strcmp(ik, "lexical_declaration") == 0 || + strcmp(ik, "variable_declaration") == 0) { + process_top_level_variable_arrows(ctx, inner); } else if (strcmp(ik, "class_declaration") == 0 || strcmp(ik, "internal_module") == 0) { process_node(ctx, inner); diff --git a/tests/test_ts_lsp.c b/tests/test_ts_lsp.c index 12b4ef2bd0..ea35264ecf 100644 --- a/tests/test_ts_lsp.c +++ b/tests/test_ts_lsp.c @@ -1157,6 +1157,89 @@ TEST(tslsp_jsx_component_resolutions_join_exact_tag_sites) { PASS(); } +TEST(tslsp_jsx_top_level_async_arrow_callers_keep_exact_identity) { + const char *source = "function Card(): JSX.Element { return ; }\n" + "const Direct = async () => ;\n" + "export const Exported = async () => ;\n" + "function Declared(): JSX.Element { return ; }\n"; + const char *const declarations[] = {"const Direct", "export const Exported", + "function Declared"}; + const char *const callers[] = {"test.main.Direct", "test.main.Exported", "test.main.Declared"}; + CBMFileResult *r = extract_tsx(source); + const bool result_created = r != NULL; + bool exact_carrier[3] = {false}; + bool exact_resolution[3] = {false}; + bool resolved_invocation[3] = {false}; + bool resolved_card_callee[3] = {false}; + bool resolved_jsx_strategy[3] = {false}; + int carrier_count[3] = {0}; + int failed_cases = result_created ? 0 : 1; + + if (!result_created) + printf(" JSX caller diagnostic: extraction failed\n"); + if (r) { + for (int i = 0; i < 3; i++) { + const char *declaration = strstr(source, declarations[i]); + const char *site = declaration ? strstr(declaration, "") : NULL; + if (!site) { + printf(" JSX caller diagnostic: source site missing for %s\n", callers[i]); + failed_cases++; + continue; + } + const uint32_t start = (uint32_t)(site - source); + const uint32_t end = start + (uint32_t)strlen(""); + const CBMCall *carrier = NULL; + for (int j = 0; j < r->calls.count; j++) { + const CBMCall *call = &r->calls.items[j]; + if (call->callee_name && strcmp(call->callee_name, "Card") == 0 && + call->site_start_byte == start && call->site_end_byte == end) { + carrier = call; + carrier_count[i]++; + } + } + + const CBMResolvedCall *resolved = carrier ? cbm_pipeline_find_lsp_resolution( + &r->resolved_calls, carrier, false) + : NULL; + exact_carrier[i] = carrier && carrier->enclosing_func_qn && + strcmp(carrier->enclosing_func_qn, callers[i]) == 0 && + carrier->site_start_byte == start && carrier->site_end_byte == end && + carrier->requires_lsp_resolution; + exact_resolution[i] = resolved && resolved->caller_qn && + strcmp(resolved->caller_qn, callers[i]) == 0 && + resolved->site_start_byte == start && resolved->site_end_byte == end; + resolved_invocation[i] = resolved && resolved->kind == CBM_RESOLVED_INVOCATION; + if (resolved && resolved->callee_qn) { + const size_t callee_len = strlen(resolved->callee_qn); + resolved_card_callee[i] = callee_len >= strlen(".Card") && + strcmp(resolved->callee_qn + callee_len - strlen(".Card"), + ".Card") == 0; + } + resolved_jsx_strategy[i] = + resolved && resolved->strategy && strcmp(resolved->strategy, "lsp_ts_jsx") == 0; + if (carrier_count[i] != 1 || !exact_carrier[i] || !exact_resolution[i] || + !resolved_invocation[i] || !resolved_card_callee[i] || !resolved_jsx_strategy[i]) { + printf(" JSX caller diagnostic: expected=%s span=%u:%u carriers=%d caller=%s " + "requires_lsp=%d resolution=%s resolution_span=%u:%u kind=%d callee=%s " + "strategy=%s\n", + callers[i], start, end, carrier_count[i], + carrier && carrier->enclosing_func_qn ? carrier->enclosing_func_qn : "(null)", + carrier ? carrier->requires_lsp_resolution : 0, + resolved && resolved->caller_qn ? resolved->caller_qn : "(null)", + resolved ? resolved->site_start_byte : 0, resolved ? resolved->site_end_byte : 0, + resolved ? resolved->kind : -1, + resolved && resolved->callee_qn ? resolved->callee_qn : "(null)", + resolved && resolved->strategy ? resolved->strategy : "(null)"); + failed_cases++; + } + } + } + + cbm_free_result(r); + ASSERT_EQ(failed_cases, 0); + PASS(); +} + /* A callable value named like a module component owns the JSX tag. The LSP * pass must not confidently redirect it to the module function, and the * carrier must require semantic resolution so graph construction cannot do @@ -4867,6 +4950,7 @@ SUITE(ts_lsp) { RUN_TEST(tslsp_jsx_component_with_children); RUN_TEST(tslsp_jsx_import_requires_registered_component); RUN_TEST(tslsp_jsx_component_resolutions_join_exact_tag_sites); + RUN_TEST(tslsp_jsx_top_level_async_arrow_callers_keep_exact_identity); RUN_TEST(tslsp_jsx_local_tag_shadow_blocks_module_component); RUN_TEST(tslsp_jsx_intrinsic_skipped); RUN_TEST(tslsp_jsx_nested_component); From 3867625c442e6a580025c5337214fccbb3b587c4 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Tue, 20 Oct 2026 04:57:14 +0200 Subject: [PATCH 2/2] fix(ts-lsp): keep exported arrow consts out of the module lexical scope Walking top-level arrow components also bound every exported `const` declaration into the module root scope in pass 1. A root-scope binding makes the JSX resolver fail closed and the exact same-module call path skip the name, so a same-file `` rendered from a function component lost its CALLS edge (JSX carriers have no other resolver), and calls such as `formatLabel()` fell back from lsp_ts_local to same_module. Walking the arrow bodies does not need that binding, so drop it, and pin both forms with a test that renders and calls an exported arrow const from a function component and from an arrow component. On excalidraw/excalidraw @ e1b7aacb the change keeps every edge it adds over main (+270 CALLS, 0 removed; 6 more same-file JSX edges than with the binding) and the 17 lsp_ts_local -> same_module downgrades are gone. Also joins one declaration line that clang-format reflows. Refs #2559 Signed-off-by: Martin Vogel --- internal/cbm/lsp/ts_lsp.c | 11 +---------- tests/test_ts_lsp.c | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/internal/cbm/lsp/ts_lsp.c b/internal/cbm/lsp/ts_lsp.c index e8b8b83ee2..b06779eb26 100644 --- a/internal/cbm/lsp/ts_lsp.c +++ b/internal/cbm/lsp/ts_lsp.c @@ -3768,8 +3768,7 @@ static void process_top_level_variable_arrows(TSLSPContext *ctx, TSNode declarat continue; TSNode name = ts_node_child_by_field_name(declarator, "name", TS_LSP_FIELD_LEN("name")); - TSNode value = - ts_node_child_by_field_name(declarator, "value", TS_LSP_FIELD_LEN("value")); + TSNode value = ts_node_child_by_field_name(declarator, "value", TS_LSP_FIELD_LEN("value")); if (!node_kind_is(name, "identifier") || !node_kind_is(value, "arrow_function")) continue; @@ -3799,14 +3798,6 @@ void ts_lsp_process_file(TSLSPContext *ctx, TSNode root) { const char *kind = ts_node_type(child); if (strcmp(kind, "lexical_declaration") == 0 || strcmp(kind, "variable_declaration") == 0) { ts_process_statement(ctx, child); - } else if (strcmp(kind, "export_statement") == 0) { - uint32_t count = ts_node_named_child_count(child); - for (uint32_t j = 0; j < count; j++) { - TSNode inner = ts_node_named_child(child, j); - if (node_kind_is(inner, "lexical_declaration") || - node_kind_is(inner, "variable_declaration")) - ts_process_statement(ctx, inner); - } } } diff --git a/tests/test_ts_lsp.c b/tests/test_ts_lsp.c index 55a067b4cd..d4ed45c0c6 100644 --- a/tests/test_ts_lsp.c +++ b/tests/test_ts_lsp.c @@ -1239,6 +1239,42 @@ TEST(tslsp_jsx_top_level_async_arrow_callers_keep_exact_identity) { PASS(); } +/* An exported arrow const is a module definition, not a lexical local. Bound + * into the module scope, it made the JSX resolver fail closed on `` + * (the edge was lost: JSX carriers have no other resolver) and the exact + * same-module call path skip `formatLabel()`. Both callers must resolve both + * targets exactly, whether the caller is a function or an arrow const. */ +static bool ts_resolved_with_strategy(const CBMFileResult *r, const char *caller, + const char *callee, const char *strategy) { + int idx = require_resolved(r, caller, callee); + const char *got = idx >= 0 ? r->resolved_calls.items[idx].strategy : NULL; + if (idx >= 0 && (!got || strcmp(got, strategy) != 0)) { + printf(" %s -> %s resolved by %s, expected %s\n", caller, callee, got ? got : "(null)", + strategy); + } + return got && strcmp(got, strategy) == 0; +} + +TEST(tslsp_jsx_exported_arrow_component_resolves_in_same_module) { + CBMFileResult *r = + extract_tsx("export const Badge = () => ;\n" + "export const formatLabel = (s: string) => s.trim();\n" + "export function FnUser() { formatLabel('a'); return ; }\n" + "export const ArrowUser = () => { formatLabel('b'); return ; };\n"); + ASSERT_NOT_NULL(r); + bool fn_jsx = ts_resolved_with_strategy(r, "main.FnUser", "main.Badge", "lsp_ts_jsx"); + bool arrow_jsx = ts_resolved_with_strategy(r, "main.ArrowUser", "main.Badge", "lsp_ts_jsx"); + bool fn_call = ts_resolved_with_strategy(r, "main.FnUser", "main.formatLabel", "lsp_ts_local"); + bool arrow_call = + ts_resolved_with_strategy(r, "main.ArrowUser", "main.formatLabel", "lsp_ts_local"); + cbm_free_result(r); + ASSERT_TRUE(fn_jsx); + ASSERT_TRUE(arrow_jsx); + ASSERT_TRUE(fn_call); + ASSERT_TRUE(arrow_call); + PASS(); +} + /* A callable value named like a module component owns the JSX tag. The LSP * pass must not confidently redirect it to the module function, and the * carrier must require semantic resolution so graph construction cannot do @@ -4947,6 +4983,7 @@ SUITE(ts_lsp) { RUN_TEST(tslsp_jsx_import_requires_registered_component); RUN_TEST(tslsp_jsx_component_resolutions_join_exact_tag_sites); RUN_TEST(tslsp_jsx_top_level_async_arrow_callers_keep_exact_identity); + RUN_TEST(tslsp_jsx_exported_arrow_component_resolves_in_same_module); RUN_TEST(tslsp_jsx_local_tag_shadow_blocks_module_component); RUN_TEST(tslsp_jsx_intrinsic_skipped); RUN_TEST(tslsp_jsx_nested_component);