From ef551460ce5fe432eca00458d338c24c910284d4 Mon Sep 17 00:00:00 2001 From: lorenzozanee Date: Sun, 11 Oct 2026 00:01:23 +0800 Subject: [PATCH] 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 c25a58bbe..e18ee0c5a 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 12b4ef2bd..ea35264ec 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);