mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Merge pull request #2579 from lorenzozanee/fix/tsx-arrow-jsx-calls
fix(ts-lsp): resolve JSX calls from top-level arrow components
This commit is contained in:
2 files changed
+151
-1
No files matched your search
@@ -3845,6 +3845,30 @@ static void process_function_body(TSLSPContext *ctx, TSNode func_node, const cha
|
|||||||
ctx->current_scope = saved_scope;
|
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) {
|
void ts_lsp_process_file(TSLSPContext *ctx, TSNode root) {
|
||||||
if (!ctx || ts_node_is_null(root))
|
if (!ctx || ts_node_is_null(root))
|
||||||
return;
|
return;
|
||||||
@@ -3870,7 +3894,9 @@ void ts_lsp_process_file(TSLSPContext *ctx, TSNode root) {
|
|||||||
TSNode child = kids[i];
|
TSNode child = kids[i];
|
||||||
const char *kind = ts_node_type(child);
|
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"));
|
TSNode name = ts_node_child_by_field_name(child, "name", TS_LSP_FIELD_LEN("name"));
|
||||||
if (ts_node_is_null(name) || !ctx->module_qn)
|
if (ts_node_is_null(name) || !ctx->module_qn)
|
||||||
continue;
|
continue;
|
||||||
@@ -3909,6 +3935,9 @@ void ts_lsp_process_file(TSLSPContext *ctx, TSNode root) {
|
|||||||
continue;
|
continue;
|
||||||
const char *fqn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, fn);
|
const char *fqn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, fn);
|
||||||
process_function_body(ctx, inner, fqn, NULL);
|
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 ||
|
} else if (strcmp(ik, "class_declaration") == 0 ||
|
||||||
strcmp(ik, "internal_module") == 0) {
|
strcmp(ik, "internal_module") == 0) {
|
||||||
process_node(ctx, inner);
|
process_node(ctx, inner);
|
||||||
|
|||||||
@@ -1156,6 +1156,125 @@ TEST(tslsp_jsx_component_resolutions_join_exact_tag_sites) {
|
|||||||
PASS();
|
PASS();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(tslsp_jsx_top_level_async_arrow_callers_keep_exact_identity) {
|
||||||
|
const char *source = "function Card(): JSX.Element { return <span/>; }\n"
|
||||||
|
"const Direct = async () => <Card/>;\n"
|
||||||
|
"export const Exported = async () => <Card/>;\n"
|
||||||
|
"function Declared(): JSX.Element { return <Card/>; }\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, "<Card/>") : 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("<Card/>");
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 `<Badge />`
|
||||||
|
* (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 = () => <span/>;\n"
|
||||||
|
"export const formatLabel = (s: string) => s.trim();\n"
|
||||||
|
"export function FnUser() { formatLabel('a'); return <Badge/>; }\n"
|
||||||
|
"export const ArrowUser = () => { formatLabel('b'); return <Badge/>; };\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
|
/* 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
|
* pass must not confidently redirect it to the module function, and the
|
||||||
* carrier must require semantic resolution so graph construction cannot do
|
* carrier must require semantic resolution so graph construction cannot do
|
||||||
@@ -4863,6 +4982,8 @@ SUITE(ts_lsp) {
|
|||||||
RUN_TEST(tslsp_jsx_component_with_children);
|
RUN_TEST(tslsp_jsx_component_with_children);
|
||||||
RUN_TEST(tslsp_jsx_import_requires_registered_component);
|
RUN_TEST(tslsp_jsx_import_requires_registered_component);
|
||||||
RUN_TEST(tslsp_jsx_component_resolutions_join_exact_tag_sites);
|
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_local_tag_shadow_blocks_module_component);
|
||||||
RUN_TEST(tslsp_jsx_intrinsic_skipped);
|
RUN_TEST(tslsp_jsx_intrinsic_skipped);
|
||||||
RUN_TEST(tslsp_jsx_nested_component);
|
RUN_TEST(tslsp_jsx_nested_component);
|
||||||
|
|||||||
Reference in new issue
Block a user