Skip to content

Commit fceb25d

Browse files
authored
Merge pull request #2579 from lorenzozanee/fix/tsx-arrow-jsx-calls
fix(ts-lsp): resolve JSX calls from top-level arrow components
2 parents 424ab76 + f0d34de commit fceb25d

2 files changed

Lines changed: 151 additions & 1 deletion

File tree

‎internal/cbm/lsp/ts_lsp.c‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3845,6 +3845,30 @@ static void process_function_body(TSLSPContext *ctx, TSNode func_node, const cha
38453845
ctx->current_scope = saved_scope;
38463846
}
38473847

3848+
static void process_top_level_variable_arrows(TSLSPContext *ctx, TSNode declaration) {
3849+
if (!ctx || ts_node_is_null(declaration) || !ctx->module_qn)
3850+
return;
3851+
3852+
uint32_t count = ts_node_named_child_count(declaration);
3853+
for (uint32_t i = 0; i < count; i++) {
3854+
TSNode declarator = ts_node_named_child(declaration, i);
3855+
if (!node_kind_is(declarator, "variable_declarator"))
3856+
continue;
3857+
3858+
TSNode name = ts_node_child_by_field_name(declarator, "name", TS_LSP_FIELD_LEN("name"));
3859+
TSNode value = ts_node_child_by_field_name(declarator, "value", TS_LSP_FIELD_LEN("value"));
3860+
if (!node_kind_is(name, "identifier") || !node_kind_is(value, "arrow_function"))
3861+
continue;
3862+
3863+
char *function_name = node_text(ctx, name);
3864+
if (!function_name)
3865+
continue;
3866+
const char *function_qn =
3867+
cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, function_name);
3868+
process_function_body(ctx, value, function_qn, NULL);
3869+
}
3870+
}
3871+
38483872
void ts_lsp_process_file(TSLSPContext *ctx, TSNode root) {
38493873
if (!ctx || ts_node_is_null(root))
38503874
return;
@@ -3870,7 +3894,9 @@ void ts_lsp_process_file(TSLSPContext *ctx, TSNode root) {
38703894
TSNode child = kids[i];
38713895
const char *kind = ts_node_type(child);
38723896

3873-
if (strcmp(kind, "function_declaration") == 0) {
3897+
if (strcmp(kind, "lexical_declaration") == 0 || strcmp(kind, "variable_declaration") == 0) {
3898+
process_top_level_variable_arrows(ctx, child);
3899+
} else if (strcmp(kind, "function_declaration") == 0) {
38743900
TSNode name = ts_node_child_by_field_name(child, "name", TS_LSP_FIELD_LEN("name"));
38753901
if (ts_node_is_null(name) || !ctx->module_qn)
38763902
continue;
@@ -3909,6 +3935,9 @@ void ts_lsp_process_file(TSLSPContext *ctx, TSNode root) {
39093935
continue;
39103936
const char *fqn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, fn);
39113937
process_function_body(ctx, inner, fqn, NULL);
3938+
} else if (strcmp(ik, "lexical_declaration") == 0 ||
3939+
strcmp(ik, "variable_declaration") == 0) {
3940+
process_top_level_variable_arrows(ctx, inner);
39123941
} else if (strcmp(ik, "class_declaration") == 0 ||
39133942
strcmp(ik, "internal_module") == 0) {
39143943
process_node(ctx, inner);

‎tests/test_ts_lsp.c‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,6 +1156,125 @@ TEST(tslsp_jsx_component_resolutions_join_exact_tag_sites) {
11561156
PASS();
11571157
}
11581158

1159+
TEST(tslsp_jsx_top_level_async_arrow_callers_keep_exact_identity) {
1160+
const char *source = "function Card(): JSX.Element { return <span/>; }\n"
1161+
"const Direct = async () => <Card/>;\n"
1162+
"export const Exported = async () => <Card/>;\n"
1163+
"function Declared(): JSX.Element { return <Card/>; }\n";
1164+
const char *const declarations[] = {"const Direct", "export const Exported",
1165+
"function Declared"};
1166+
const char *const callers[] = {"test.main.Direct", "test.main.Exported", "test.main.Declared"};
1167+
CBMFileResult *r = extract_tsx(source);
1168+
const bool result_created = r != NULL;
1169+
bool exact_carrier[3] = {false};
1170+
bool exact_resolution[3] = {false};
1171+
bool resolved_invocation[3] = {false};
1172+
bool resolved_card_callee[3] = {false};
1173+
bool resolved_jsx_strategy[3] = {false};
1174+
int carrier_count[3] = {0};
1175+
int failed_cases = result_created ? 0 : 1;
1176+
1177+
if (!result_created)
1178+
printf(" JSX caller diagnostic: extraction failed\n");
1179+
if (r) {
1180+
for (int i = 0; i < 3; i++) {
1181+
const char *declaration = strstr(source, declarations[i]);
1182+
const char *site = declaration ? strstr(declaration, "<Card/>") : NULL;
1183+
if (!site) {
1184+
printf(" JSX caller diagnostic: source site missing for %s\n", callers[i]);
1185+
failed_cases++;
1186+
continue;
1187+
}
1188+
const uint32_t start = (uint32_t)(site - source);
1189+
const uint32_t end = start + (uint32_t)strlen("<Card/>");
1190+
const CBMCall *carrier = NULL;
1191+
for (int j = 0; j < r->calls.count; j++) {
1192+
const CBMCall *call = &r->calls.items[j];
1193+
if (call->callee_name && strcmp(call->callee_name, "Card") == 0 &&
1194+
call->site_start_byte == start && call->site_end_byte == end) {
1195+
carrier = call;
1196+
carrier_count[i]++;
1197+
}
1198+
}
1199+
1200+
const CBMResolvedCall *resolved = carrier ? cbm_pipeline_find_lsp_resolution(
1201+
&r->resolved_calls, carrier, false)
1202+
: NULL;
1203+
exact_carrier[i] = carrier && carrier->enclosing_func_qn &&
1204+
strcmp(carrier->enclosing_func_qn, callers[i]) == 0 &&
1205+
carrier->site_start_byte == start && carrier->site_end_byte == end &&
1206+
carrier->requires_lsp_resolution;
1207+
exact_resolution[i] = resolved && resolved->caller_qn &&
1208+
strcmp(resolved->caller_qn, callers[i]) == 0 &&
1209+
resolved->site_start_byte == start && resolved->site_end_byte == end;
1210+
resolved_invocation[i] = resolved && resolved->kind == CBM_RESOLVED_INVOCATION;
1211+
if (resolved && resolved->callee_qn) {
1212+
const size_t callee_len = strlen(resolved->callee_qn);
1213+
resolved_card_callee[i] = callee_len >= strlen(".Card") &&
1214+
strcmp(resolved->callee_qn + callee_len - strlen(".Card"),
1215+
".Card") == 0;
1216+
}
1217+
resolved_jsx_strategy[i] =
1218+
resolved && resolved->strategy && strcmp(resolved->strategy, "lsp_ts_jsx") == 0;
1219+
if (carrier_count[i] != 1 || !exact_carrier[i] || !exact_resolution[i] ||
1220+
!resolved_invocation[i] || !resolved_card_callee[i] || !resolved_jsx_strategy[i]) {
1221+
printf(" JSX caller diagnostic: expected=%s span=%u:%u carriers=%d caller=%s "
1222+
"requires_lsp=%d resolution=%s resolution_span=%u:%u kind=%d callee=%s "
1223+
"strategy=%s\n",
1224+
callers[i], start, end, carrier_count[i],
1225+
carrier && carrier->enclosing_func_qn ? carrier->enclosing_func_qn : "(null)",
1226+
carrier ? carrier->requires_lsp_resolution : 0,
1227+
resolved && resolved->caller_qn ? resolved->caller_qn : "(null)",
1228+
resolved ? resolved->site_start_byte : 0, resolved ? resolved->site_end_byte : 0,
1229+
resolved ? resolved->kind : -1,
1230+
resolved && resolved->callee_qn ? resolved->callee_qn : "(null)",
1231+
resolved && resolved->strategy ? resolved->strategy : "(null)");
1232+
failed_cases++;
1233+
}
1234+
}
1235+
}
1236+
1237+
cbm_free_result(r);
1238+
ASSERT_EQ(failed_cases, 0);
1239+
PASS();
1240+
}
1241+
1242+
/* An exported arrow const is a module definition, not a lexical local. Bound
1243+
* into the module scope, it made the JSX resolver fail closed on `<Badge />`
1244+
* (the edge was lost: JSX carriers have no other resolver) and the exact
1245+
* same-module call path skip `formatLabel()`. Both callers must resolve both
1246+
* targets exactly, whether the caller is a function or an arrow const. */
1247+
static bool ts_resolved_with_strategy(const CBMFileResult *r, const char *caller,
1248+
const char *callee, const char *strategy) {
1249+
int idx = require_resolved(r, caller, callee);
1250+
const char *got = idx >= 0 ? r->resolved_calls.items[idx].strategy : NULL;
1251+
if (idx >= 0 && (!got || strcmp(got, strategy) != 0)) {
1252+
printf(" %s -> %s resolved by %s, expected %s\n", caller, callee, got ? got : "(null)",
1253+
strategy);
1254+
}
1255+
return got && strcmp(got, strategy) == 0;
1256+
}
1257+
1258+
TEST(tslsp_jsx_exported_arrow_component_resolves_in_same_module) {
1259+
CBMFileResult *r =
1260+
extract_tsx("export const Badge = () => <span/>;\n"
1261+
"export const formatLabel = (s: string) => s.trim();\n"
1262+
"export function FnUser() { formatLabel('a'); return <Badge/>; }\n"
1263+
"export const ArrowUser = () => { formatLabel('b'); return <Badge/>; };\n");
1264+
ASSERT_NOT_NULL(r);
1265+
bool fn_jsx = ts_resolved_with_strategy(r, "main.FnUser", "main.Badge", "lsp_ts_jsx");
1266+
bool arrow_jsx = ts_resolved_with_strategy(r, "main.ArrowUser", "main.Badge", "lsp_ts_jsx");
1267+
bool fn_call = ts_resolved_with_strategy(r, "main.FnUser", "main.formatLabel", "lsp_ts_local");
1268+
bool arrow_call =
1269+
ts_resolved_with_strategy(r, "main.ArrowUser", "main.formatLabel", "lsp_ts_local");
1270+
cbm_free_result(r);
1271+
ASSERT_TRUE(fn_jsx);
1272+
ASSERT_TRUE(arrow_jsx);
1273+
ASSERT_TRUE(fn_call);
1274+
ASSERT_TRUE(arrow_call);
1275+
PASS();
1276+
}
1277+
11591278
/* A callable value named like a module component owns the JSX tag. The LSP
11601279
* pass must not confidently redirect it to the module function, and the
11611280
* carrier must require semantic resolution so graph construction cannot do
@@ -4863,6 +4982,8 @@ SUITE(ts_lsp) {
48634982
RUN_TEST(tslsp_jsx_component_with_children);
48644983
RUN_TEST(tslsp_jsx_import_requires_registered_component);
48654984
RUN_TEST(tslsp_jsx_component_resolutions_join_exact_tag_sites);
4985+
RUN_TEST(tslsp_jsx_top_level_async_arrow_callers_keep_exact_identity);
4986+
RUN_TEST(tslsp_jsx_exported_arrow_component_resolves_in_same_module);
48664987
RUN_TEST(tslsp_jsx_local_tag_shadow_blocks_module_component);
48674988
RUN_TEST(tslsp_jsx_intrinsic_skipped);
48684989
RUN_TEST(tslsp_jsx_nested_component);

0 commit comments

Comments
 (0)