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);