Skip to content

Commit 3867625

Browse files
committed
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 `<Badge />` 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 <martin.vogel.tech@gmail.com>
1 parent 1b2238e commit 3867625

2 files changed

Lines changed: 38 additions & 10 deletions

File tree

‎internal/cbm/lsp/ts_lsp.c‎

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3768,8 +3768,7 @@ static void process_top_level_variable_arrows(TSLSPContext *ctx, TSNode declarat
37683768
continue;
37693769

37703770
TSNode name = ts_node_child_by_field_name(declarator, "name", TS_LSP_FIELD_LEN("name"));
3771-
TSNode value =
3772-
ts_node_child_by_field_name(declarator, "value", TS_LSP_FIELD_LEN("value"));
3771+
TSNode value = ts_node_child_by_field_name(declarator, "value", TS_LSP_FIELD_LEN("value"));
37733772
if (!node_kind_is(name, "identifier") || !node_kind_is(value, "arrow_function"))
37743773
continue;
37753774

@@ -3799,14 +3798,6 @@ void ts_lsp_process_file(TSLSPContext *ctx, TSNode root) {
37993798
const char *kind = ts_node_type(child);
38003799
if (strcmp(kind, "lexical_declaration") == 0 || strcmp(kind, "variable_declaration") == 0) {
38013800
ts_process_statement(ctx, child);
3802-
} else if (strcmp(kind, "export_statement") == 0) {
3803-
uint32_t count = ts_node_named_child_count(child);
3804-
for (uint32_t j = 0; j < count; j++) {
3805-
TSNode inner = ts_node_named_child(child, j);
3806-
if (node_kind_is(inner, "lexical_declaration") ||
3807-
node_kind_is(inner, "variable_declaration"))
3808-
ts_process_statement(ctx, inner);
3809-
}
38103801
}
38113802
}
38123803

‎tests/test_ts_lsp.c‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1239,6 +1239,42 @@ TEST(tslsp_jsx_top_level_async_arrow_callers_keep_exact_identity) {
12391239
PASS();
12401240
}
12411241

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+
12421278
/* A callable value named like a module component owns the JSX tag. The LSP
12431279
* pass must not confidently redirect it to the module function, and the
12441280
* carrier must require semantic resolution so graph construction cannot do
@@ -4947,6 +4983,7 @@ SUITE(ts_lsp) {
49474983
RUN_TEST(tslsp_jsx_import_requires_registered_component);
49484984
RUN_TEST(tslsp_jsx_component_resolutions_join_exact_tag_sites);
49494985
RUN_TEST(tslsp_jsx_top_level_async_arrow_callers_keep_exact_identity);
4986+
RUN_TEST(tslsp_jsx_exported_arrow_component_resolves_in_same_module);
49504987
RUN_TEST(tslsp_jsx_local_tag_shadow_blocks_module_component);
49514988
RUN_TEST(tslsp_jsx_intrinsic_skipped);
49524989
RUN_TEST(tslsp_jsx_nested_component);

0 commit comments

Comments
 (0)