Merge pull request #2746 from gaurav0107/fix/2297-typescript-calls-through-a-renamed-named

fix(ts-lsp): resolve calls through renamed named imports
This commit is contained in:
Martin Vogel authored and GitHub committed 2026-10-29 01:33:12 +01:00
commit f1efbf60d8
4 files changed
+492 -17

No files matched your search

+134 -17
View File
@@ -269,8 +269,12 @@ static TSNode find_first_kind(TSNode node, const char *kind) {
return null_node;
}
static void ts_emit_resolved_call_at(TSLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence, TSNode site) {
/* `reason` carries the name written at the site when it differs from the
* callee's leaf (`bar()` through `import { foo as bar }`): the pipeline joins an
* alias strategy by that name instead of by the callee QN's last segment. */
static void ts_emit_resolved_call_reason_at(TSLSPContext *ctx, const char *callee_qn,
const char *strategy, float confidence,
const char *reason, TSNode site) {
if (!ctx || !ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn)
return;
CBMResolvedCall rc = {0};
@@ -278,7 +282,7 @@ static void ts_emit_resolved_call_at(TSLSPContext *ctx, const char *callee_qn, c
rc.callee_qn = callee_qn;
rc.strategy = strategy ? strategy : "lsp_ts";
rc.confidence = confidence;
rc.reason = NULL;
rc.reason = reason;
rc.kind = CBM_RESOLVED_INVOCATION;
if (!ts_node_is_null(site)) {
rc.site_start_byte = ts_node_start_byte(site);
@@ -287,8 +291,14 @@ static void ts_emit_resolved_call_at(TSLSPContext *ctx, const char *callee_qn, c
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
}
static void ts_emit_resolved_reference(TSLSPContext *ctx, const char *callee_qn,
const char *strategy, float confidence, TSNode site) {
static void ts_emit_resolved_call_at(TSLSPContext *ctx, const char *callee_qn, const char *strategy,
float confidence, TSNode site) {
ts_emit_resolved_call_reason_at(ctx, callee_qn, strategy, confidence, NULL, site);
}
static void ts_emit_resolved_reference_reason(TSLSPContext *ctx, const char *callee_qn,
const char *strategy, float confidence,
const char *reason, TSNode site) {
if (!ctx || !ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn ||
ts_node_is_null(site)) {
return;
@@ -298,13 +308,18 @@ static void ts_emit_resolved_reference(TSLSPContext *ctx, const char *callee_qn,
rc.callee_qn = callee_qn;
rc.strategy = strategy ? strategy : "lsp_ts_value_reference";
rc.confidence = confidence;
rc.reason = NULL;
rc.reason = reason;
rc.kind = CBM_RESOLVED_CALL_REFERENCE;
rc.site_start_byte = ts_node_start_byte(site);
rc.site_end_byte = ts_node_end_byte(site);
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
}
static void ts_emit_resolved_reference(TSLSPContext *ctx, const char *callee_qn,
const char *strategy, float confidence, TSNode site) {
ts_emit_resolved_reference_reason(ctx, callee_qn, strategy, confidence, NULL, site);
}
static void ts_emit_unresolved_call_at(TSLSPContext *ctx, const char *expr_text, const char *reason,
TSNode site) {
if (!ctx || !ctx->resolved_calls || !ctx->enclosing_func_qn)
@@ -1425,6 +1440,19 @@ static bool ts_scope_has_binding(const CBMScope *scope, const char *name) {
return ts_scope_binding_owner(scope, name) != NULL;
}
/* The member an import binding names in its module: the exported name of a
* renamed named import (`foo` for `import { foo as bar }`, #2297), else the
* binding itself. Callers have already matched `local` to an import binding. */
static const char *ts_import_export_name(const TSLSPContext *ctx, const char *local) {
if (!ctx || !local)
return local;
for (int i = 0; i < ctx->import_alias_count; i++) {
if (strcmp(ctx->import_alias_locals[i], local) == 0)
return ctx->import_alias_exports[i];
}
return local;
}
/* Full QN of an imported symbol. An import value is normally the module QN
* (symbol = module + "." + name), but may already be the symbol QN. When the
* module QN ends in ".name" the spelling is ambiguous: `Foo.ts` exporting
@@ -1463,8 +1491,9 @@ static const CBMRegisteredFunc *ts_exact_same_module_function(TSLSPContext *ctx,
}
/* Return a function only when the local import binding maps to one unambiguous
* module and that module contains the exact local-name QN. Aliases whose export
* name is unavailable in CBMImport deliberately fail closed and remain USAGE. */
* module and that module contains the exact QN of the member the binding names
* (the exported name for `import { foo as bar }`). A renamed import of a name
* the module does not export fails closed and remains USAGE. */
static const CBMRegisteredFunc *ts_exact_imported_function(TSLSPContext *ctx, const char *name) {
if (!ctx || !ctx->registry || !name || ts_scope_has_binding(ctx->current_scope, name)) {
return NULL;
@@ -1484,7 +1513,8 @@ static const CBMRegisteredFunc *ts_exact_imported_function(TSLSPContext *ctx, co
if (!matched_module) {
return NULL;
}
const char *full_qn = ts_import_symbol_qn(ctx, matched_module, name);
const char *full_qn =
ts_import_symbol_qn(ctx, matched_module, ts_import_export_name(ctx, name));
return full_qn ? cbm_registry_lookup_func(ctx->registry, full_qn) : NULL;
}
@@ -1532,7 +1562,7 @@ static const CBMType *type_of_identifier(TSLSPContext *ctx, const char *name) {
// Construct the full symbol QN using the same convention as exact
// imported function-value resolution.
const char *full_qn = ts_import_symbol_qn(ctx, mqn, name);
const char *full_qn = ts_import_symbol_qn(ctx, mqn, ts_import_export_name(ctx, name));
// Try the symbol lookup precedence: registered func, then registered type.
const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, full_qn);
@@ -2087,7 +2117,8 @@ static const CBMType *ts_new_bare_class_type(TSLSPContext *ctx, const char *cnam
bool import_matched = false;
const char *import_module = ts_import_module_for_local(ctx, cname, &import_matched);
if (import_module)
return cbm_type_named(ctx->arena, ts_import_symbol_qn(ctx, import_module, cname));
return cbm_type_named(
ctx->arena, ts_import_symbol_qn(ctx, import_module, ts_import_export_name(ctx, cname)));
return cbm_type_named(ctx->arena, local_qn);
}
@@ -2208,7 +2239,8 @@ static const CBMType *ts_signature_for_call(TSLSPContext *ctx, TSNode function,
if (!registered) {
const char *module_qn = ts_import_module_for_local(ctx, name, NULL);
if (module_qn)
registered = ts_lookup_symbol_for_call(ctx, module_qn, name, args);
registered = ts_lookup_symbol_for_call(ctx, module_qn,
ts_import_export_name(ctx, name), args);
}
if (registered)
return registered->signature;
@@ -2566,7 +2598,8 @@ static const CBMType *resolve_type_with_imports(TSLSPContext *ctx, const CBMType
continue;
if (strcmp(lname, bare) != 0)
continue;
return cbm_type_named(ctx->arena, ts_import_symbol_qn(ctx, mqn, bare));
return cbm_type_named(ctx->arena,
ts_import_symbol_qn(ctx, mqn, ts_import_export_name(ctx, bare)));
}
return t;
}
@@ -2933,12 +2966,19 @@ static void resolve_value_references_at(TSLSPContext *ctx, TSNode call_node) {
}
const CBMRegisteredFunc *target = ts_exact_same_module_function(ctx, name);
const char *strategy = "lsp_ts_same_module_value_reference";
const char *reason = NULL;
if (!target) {
target = ts_exact_imported_function(ctx, name);
strategy = "lsp_ts_import_value_reference";
/* `import { foo as bar }`: the site names `bar`, the target is `foo`. */
if (target && ts_import_export_name(ctx, name) != name) {
strategy = "lsp_callable_value_reference";
reason = name;
}
}
if (target) {
ts_emit_resolved_reference(ctx, target->qualified_name, strategy, 0.95f, arg);
ts_emit_resolved_reference_reason(ctx, target->qualified_name, strategy, 0.95f, reason,
arg);
}
}
}
@@ -3025,14 +3065,20 @@ static void resolve_call_at(TSLSPContext *ctx, TSNode call_node) {
const char *lname = ctx->import_local_names ? ctx->import_local_names[i] : NULL;
const char *mqn = ctx->import_module_qns ? ctx->import_module_qns[i] : NULL;
if (lname && mqn && strcmp(lname, name) == 0) {
const CBMRegisteredFunc *f = ts_lookup_symbol_for_call(ctx, mqn, name, args);
const char *member = ts_import_export_name(ctx, name);
const CBMRegisteredFunc *f = ts_lookup_symbol_for_call(ctx, mqn, member, args);
if (f && member != name) {
ts_emit_resolved_call_reason_at(ctx, f->qualified_name, "lsp_import_alias",
0.95f, name, call_node);
return;
}
if (f) {
ts_emit_resolved_call_at(ctx, f->qualified_name, "lsp_ts_import", 0.95f,
call_node);
return;
}
// Fall through to qualified_name fallback.
const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", mqn, name);
const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", mqn, member);
ts_emit_unresolved_call_at(ctx, qn, "import_symbol_not_in_registry", call_node);
return;
}
@@ -3374,7 +3420,13 @@ static void resolve_jsx_element(TSLSPContext *ctx, TSNode element_node) {
* per-file row deliberately keeps the file eligible for the
* cross-file pass, where resolved import QNs and project defs are
* available. */
const CBMRegisteredFunc *f = cbm_registry_lookup_symbol(ctx->registry, mqn, tag_name);
const char *member = ts_import_export_name(ctx, tag_name);
const CBMRegisteredFunc *f = cbm_registry_lookup_symbol(ctx->registry, mqn, member);
if (f && member != tag_name) {
ts_emit_resolved_call_reason_at(ctx, f->qualified_name, "lsp_import_alias", 0.95f,
tag_name, tag);
return;
}
if (f) {
ts_emit_resolved_call_at(ctx, f->qualified_name, "lsp_ts_jsx_import", 0.95f, tag);
return;
@@ -4005,6 +4057,68 @@ void ts_lsp_add_import(TSLSPContext *ctx, const char *local_name, const char *mo
ctx->import_count = new_count;
}
static void ts_lsp_add_import_alias(TSLSPContext *ctx, const char *local, const char *exported) {
if ((size_t)ctx->import_alias_count == ctx->import_alias_capacity) {
/* The arena retains old arrays: grow geometrically so collecting A
* aliases retains O(A) pointers instead of O(A squared). */
size_t capacity = ctx->import_alias_capacity ? ctx->import_alias_capacity * 2 : 8;
const char **locals =
(const char **)cbm_arena_alloc(ctx->arena, capacity * sizeof(const char *));
const char **exports =
(const char **)cbm_arena_alloc(ctx->arena, capacity * sizeof(const char *));
if (!locals || !exports)
return;
for (int i = 0; i < ctx->import_alias_count; i++) {
locals[i] = ctx->import_alias_locals[i];
exports[i] = ctx->import_alias_exports[i];
}
ctx->import_alias_locals = locals;
ctx->import_alias_exports = exports;
ctx->import_alias_capacity = capacity;
}
ctx->import_alias_locals[ctx->import_alias_count] = local;
ctx->import_alias_exports[ctx->import_alias_count] = exported;
ctx->import_alias_count++;
}
/* Record each `import { foo as bar }` binding of the file (#2297). The import
* map holds only local bindings, so a renamed binding spelled `<module>.bar`,
* named nothing, and every call through it lost its CALLS edge. Reading the
* file's own import statements serves the per-file and both cross-file entry
* points alike. `default as x` keeps today's local-name lookup (a default
* import binds no exported member name); string export names and identity
* aliases need no entry. Must run before the type passes, which resolve
* imported type names. */
static void ts_lsp_collect_import_aliases(TSLSPContext *ctx, TSNode root) {
if (!ctx || !ctx->arena || ts_node_is_null(root) || ctx->import_count == 0)
return;
uint32_t kn = 0;
TSNode *kids = collect_children(ctx->arena, root, &kn);
for (uint32_t i = 0; i < kn; i++) {
if (!node_kind_is(kids[i], "import_statement"))
continue;
TSNode named = find_first_kind(find_first_kind(kids[i], "import_clause"), "named_imports");
if (ts_node_is_null(named))
continue;
uint32_t sn = 0;
TSNode *specs = collect_children(ctx->arena, named, &sn);
for (uint32_t j = 0; j < sn; j++) {
if (!node_kind_is(specs[j], "import_specifier"))
continue;
TSNode name = ts_node_child_by_field_name(specs[j], "name", TS_LSP_FIELD_LEN("name"));
TSNode alias =
ts_node_child_by_field_name(specs[j], "alias", TS_LSP_FIELD_LEN("alias"));
if (!node_kind_is(name, "identifier") || !node_kind_is(alias, "identifier"))
continue;
char *exported = node_text(ctx, name);
char *local = node_text(ctx, alias);
if (exported && local && strcmp(exported, "default") != 0 &&
strcmp(exported, local) != 0)
ts_lsp_add_import_alias(ctx, local, exported);
}
}
}
// ── Stdlib seeds (Phase 5 will replace with generator) ────────────────────────
static const CBMType *tt_builtin(CBMArena *a, const char *n) {
@@ -5948,6 +6062,7 @@ void cbm_run_ts_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
ts_lsp_add_import(&ctx, imp->local_name, imp->module_path);
}
}
ts_lsp_collect_import_aliases(&ctx, root);
ast_sweep_shapes(&ctx, root, &reg);
refine_ast_signatures(&ctx, root, &reg);
@@ -6201,6 +6316,7 @@ void cbm_run_ts_lsp_cross_with_registry(CBMArena *arena, const char *source, int
ts_lsp_add_import(&ctx, import_names[i], import_qns[i]);
}
}
ts_lsp_collect_import_aliases(&ctx, root);
ast_sweep_shapes(&ctx, root, &overlay);
refine_ast_signatures(&ctx, root, &overlay);
@@ -6416,6 +6532,7 @@ void cbm_run_ts_lsp_cross(CBMArena *arena, const char *source, int source_len,
ts_lsp_add_import(&ctx, import_names[i], import_qns[i]);
}
}
ts_lsp_collect_import_aliases(&ctx, root);
ast_sweep_shapes(&ctx, root, &reg);
refine_ast_signatures(&ctx, root, &reg);
+8
View File
@@ -33,6 +33,14 @@ typedef struct {
const char **import_module_qns;
int import_count;
// Renamed named imports (`import { foo as bar }`): local binding -> the name
// the module exports. Parallel arrays of length import_alias_count; a
// binding absent here is exported under its local name.
const char **import_alias_locals;
const char **import_alias_exports;
int import_alias_count;
size_t import_alias_capacity;
// File / surrounding context.
const char *module_qn; // QN of this file's module
const char *enclosing_func_qn; // current function being walked, NULL at module scope
+99
View File
@@ -9743,6 +9743,104 @@ TEST(pipeline_ts_crossfile_new_instance_method_call_issue1354) {
PASS();
}
/* Issue #2297: a call through `import { foo as fooShared }` produced no CALLS
* edge, so trace_path(foo, inbound) missed the caller. End to end through the
* real pipeline: the renamed call, an app-local wrapper that shares the
* exported name, and a method on a renamed class import all reach the
* exported member, while the plain named import control keeps lsp_ts_import. */
TEST(pipeline_ts_renamed_import_call_issue2297) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_ts_2297_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "src/lib.ts",
"export function fooUniqueQrs(x: number): number {\n"
" return x + 1;\n"
"}\n"
"export function buildHealthUniqueQrs(input: { a: number }): number {\n"
" return input.a;\n"
"}\n"
"export class ConnUniqueQrs {\n"
" pingUniqueQrs(): number {\n"
" return 1;\n"
" }\n"
"}\n");
write_temp_file(tmp, "src/plain.ts",
"import { fooUniqueQrs } from './lib';\n"
"export function callsPlain(): number {\n"
" return fooUniqueQrs(1);\n"
"}\n");
write_temp_file(tmp, "src/renamed.ts",
"import { fooUniqueQrs as fooShared } from './lib';\n"
"export function callsRenamed(): number {\n"
" return fooShared(1);\n"
"}\n");
write_temp_file(tmp, "src/wrapper.ts",
"import { buildHealthUniqueQrs as buildShared } from './lib';\n"
"export function buildHealthUniqueQrs(input: { a: number }): number {\n"
" return buildShared({ ...input });\n"
"}\n");
write_temp_file(tmp, "src/klass.ts",
"import { ConnUniqueQrs as Connection } from './lib';\n"
"export function viaRenamedClass(): number {\n"
" return new Connection().pingUniqueQrs();\n"
"}\n");
char db_path[512];
snprintf(db_path, sizeof(db_path), "%s/ts_2297.db", tmp);
cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL);
ASSERT_NOT_NULL(p);
ASSERT_EQ(cbm_pipeline_run(p), 0);
const char *project = cbm_pipeline_project_name(p);
cbm_store_t *s = cbm_store_open_path(db_path);
ASSERT_NOT_NULL(s);
bool plain =
cross_file_call_has_strategy(s, project, "callsPlain", "fooUniqueQrs", "lsp_ts_import");
bool renamed = cross_file_call_has_strategy(s, project, "callsRenamed", "fooUniqueQrs",
"lsp_import_alias");
char wrapper_qn[512];
char exported_qn[512];
snprintf(wrapper_qn, sizeof(wrapper_qn), "%s.src.wrapper.buildHealthUniqueQrs", project);
snprintf(exported_qn, sizeof(exported_qn), "%s.src.lib.buildHealthUniqueQrs", project);
cbm_node_t wrapper_node = {0};
cbm_node_t exported_node = {0};
bool wrapper = false;
bool recursive = false;
if (cbm_store_find_node_by_qn(s, project, wrapper_qn, &wrapper_node) == CBM_STORE_OK &&
cbm_store_find_node_by_qn(s, project, exported_qn, &exported_node) == CBM_STORE_OK) {
cbm_edge_t *edges = NULL;
int edge_count = 0;
cbm_store_find_edges_by_source_type(s, wrapper_node.id, "CALLS", &edges, &edge_count);
for (int i = 0; i < edge_count; i++) {
if (edges[i].target_id == exported_node.id && edges[i].properties_json &&
strstr(edges[i].properties_json, "lsp_import_alias"))
wrapper = true;
if (edges[i].target_id == wrapper_node.id)
recursive = true;
}
cbm_store_free_edges(edges, edge_count);
}
cbm_node_free_fields(&wrapper_node);
cbm_node_free_fields(&exported_node);
bool renamed_class = cross_file_call_has_strategy(s, project, "viaRenamedClass",
"pingUniqueQrs", "lsp_ts_method");
cbm_store_close(s);
cbm_pipeline_free(p);
th_rmtree(tmp);
ASSERT_TRUE(plain);
ASSERT_TRUE(renamed);
ASSERT_TRUE(wrapper);
ASSERT_FALSE(recursive);
ASSERT_TRUE(renamed_class);
PASS();
}
/* A query failure is distinct from a valid zero-edge result. */
static int ts514_pipeline_count(cbm_store_t *store, const char *sql, const char **values,
int value_count) {
@@ -28750,6 +28848,7 @@ SUITE(pipeline) {
RUN_TEST(pipeline_tsjs_receiver_suppresses_weak_method_edge);
RUN_TEST(pipeline_axios_wrapper_baseurl_composes_http_calls_issue1916);
RUN_TEST(pipeline_ts_crossfile_new_instance_method_call_issue1354);
RUN_TEST(pipeline_ts_renamed_import_call_issue2297);
RUN_TEST(pipeline_ts_param_property_injection_issue514);
RUN_TEST(pipeline_python_receiver_suppresses_weak_method_edge);
RUN_TEST(pipeline_php_receiver_suppresses_weak_method_edge);
+251
View File
@@ -2787,6 +2787,125 @@ TEST(tslsp_crossfile_chain_through_return) {
PASS();
}
/* The call target and its return signature must use the same exported name,
* even when the module also exports a function named like the local alias. */
static int ts_renamed_import_return_chain(bool shared_registry) {
const char *source = "import { make as alias } from './lib';\n"
"function go() { alias().read(); }\n";
CBMLSPDef defs[] = {
{.qualified_name = "test.main.go",
.short_name = "go",
.label = "Function",
.lang = CBM_LANG_TYPESCRIPT,
.def_module_qn = "test.main"},
{.qualified_name = "test.lib.Good",
.short_name = "Good",
.label = "Class",
.lang = CBM_LANG_TYPESCRIPT,
.def_module_qn = "test.lib",
.method_names_str = "read"},
{.qualified_name = "test.lib.Good.read",
.short_name = "read",
.label = "Method",
.lang = CBM_LANG_TYPESCRIPT,
.def_module_qn = "test.lib",
.receiver_type = "test.lib.Good"},
{.qualified_name = "test.lib.Bad",
.short_name = "Bad",
.label = "Class",
.lang = CBM_LANG_TYPESCRIPT,
.def_module_qn = "test.lib",
.method_names_str = "read"},
{.qualified_name = "test.lib.Bad.read",
.short_name = "read",
.label = "Method",
.lang = CBM_LANG_TYPESCRIPT,
.def_module_qn = "test.lib",
.receiver_type = "test.lib.Bad"},
{.qualified_name = "test.lib.make",
.short_name = "make",
.label = "Function",
.lang = CBM_LANG_TYPESCRIPT,
.def_module_qn = "test.lib",
.return_types = "test.lib.Good"},
{.qualified_name = "test.lib.alias",
.short_name = "alias",
.label = "Function",
.lang = CBM_LANG_TYPESCRIPT,
.def_module_qn = "test.lib",
.return_types = "test.lib.Bad"},
};
enum { NDEFS = (int)(sizeof(defs) / sizeof(defs[0])) };
const char *imp_names[] = {"alias"};
const char *imp_qns[] = {"test.lib"};
CBMArena arena;
cbm_arena_init(&arena);
CBMResolvedCallArray out = {0};
if (shared_registry) {
CBMTypeRegistry *reg = cbm_ts_build_cross_registry(&arena, defs, NDEFS);
ASSERT_NOT_NULL(reg);
cbm_run_ts_lsp_cross_with_registry(&arena, source, (int)strlen(source), "test.main", false,
false, false, reg, defs, NDEFS, imp_names, imp_qns, 1,
NULL, &out);
} else {
cbm_run_ts_lsp_cross(&arena, source, (int)strlen(source), "test.main", false, false, false,
defs, NDEFS, imp_names, imp_qns, 1, NULL, &out);
}
bool direct = has_exact_resolved(&out, "test.main.go", "test.lib.make", "lsp_import_alias");
bool good = has_exact_resolved(&out, "test.main.go", "test.lib.Good.read", "lsp_ts_method");
bool bad = has_exact_resolved(&out, "test.main.go", "test.lib.Bad.read", "lsp_ts_method");
cbm_arena_destroy(&arena);
ASSERT_TRUE(direct);
ASSERT_FALSE(bad);
ASSERT_TRUE(good);
PASS();
}
TEST(tslsp_crossfile_renamed_import_return_chain) {
return ts_renamed_import_return_chain(false);
}
TEST(tslsp_crossfile_renamed_import_return_chain_shared) {
return ts_renamed_import_return_chain(true);
}
/* Keep the supplied import map at one entry: this measures the AST alias
* collector independently of the older import-map insertion path. */
static size_t ts_import_alias_arena_bytes(int count) {
size_t capacity = (size_t)count * 48 + 64;
char *source = malloc(capacity);
if (!source)
return 0;
size_t len = (size_t)snprintf(source, capacity, "import { ");
for (int i = 0; i < count; i++) {
len += (size_t)snprintf(source + len, capacity - len, "%sf%d as a%d", i ? ", " : "", i, i);
}
len += (size_t)snprintf(source + len, capacity - len, " } from './lib';\n");
const char *imp_names[] = {"a0"};
const char *imp_qns[] = {"test.lib"};
CBMArena arena;
cbm_arena_init(&arena);
CBMResolvedCallArray out = {0};
cbm_run_ts_lsp_cross(&arena, source, (int)len, "test.main", false, false, false, NULL, 0,
imp_names, imp_qns, 1, NULL, &out);
size_t bytes = cbm_arena_total(&arena);
cbm_arena_destroy(&arena);
free(source);
return bytes;
}
TEST(tslsp_import_alias_arena_growth) {
size_t small = ts_import_alias_arena_bytes(1024);
size_t large = ts_import_alias_arena_bytes(2048);
ASSERT_GT(small, 0);
ASSERT_GT(large, small);
printf(" aliases arena bytes: 1024=%zu 2048=%zu ", small, large);
/* Linear source/AST work plus geometric storage stays below 3x; retaining
* two complete arrays at every insertion grows approximately 4x. */
ASSERT(large < small * 3);
PASS();
}
TEST(tslsp_crossfile_no_def_match) {
/* Function that imports a symbol not in the def list — should resolve safely
* (emit unresolved, no crash). */
@@ -2815,6 +2934,132 @@ TEST(tslsp_crossfile_no_def_match) {
PASS();
}
/* Issue #2297: `import { foo as fooShared }` bound only the local alias, and
* every import lookup spelled `<module>.<alias>`, which names nothing, so a call
* through a renamed import produced no CALLS edge. The binding resolves to the
* name the module exports, exactly like a plain named import. Driven through
* the production Tier-2 path (shared sealed registry + per-file overlay). */
TEST(tslsp_crossfile_renamed_import_issue2297) {
CBMLSPDef defs[] = {
{.qualified_name = "test.lib.foo",
.short_name = "foo",
.label = "Function",
.lang = CBM_LANG_TYPESCRIPT,
.def_module_qn = "test.lib",
.return_types = "number"},
{.qualified_name = "test.conn.Conn",
.short_name = "Conn",
.label = "Class",
.lang = CBM_LANG_TYPESCRIPT,
.def_module_qn = "test.conn",
.method_names_str = "ping"},
{.qualified_name = "test.conn.Conn.ping",
.short_name = "ping",
.label = "Method",
.lang = CBM_LANG_TYPESCRIPT,
.def_module_qn = "test.conn",
.receiver_type = "test.conn.Conn"},
{.qualified_name = "test.main.callsRenamed",
.short_name = "callsRenamed",
.label = "Function",
.lang = CBM_LANG_TYPESCRIPT,
.def_module_qn = "test.main"},
{.qualified_name = "test.main.viaRenamedClass",
.short_name = "viaRenamedClass",
.label = "Function",
.lang = CBM_LANG_TYPESCRIPT,
.def_module_qn = "test.main"},
};
enum { NDEFS = (int)(sizeof(defs) / sizeof(defs[0])) };
const char *imp_names[] = {"fooShared", "Connection"};
const char *imp_qns[] = {"test.lib", "test.conn"};
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry *reg = cbm_ts_build_cross_registry(&arena, defs, NDEFS);
ASSERT_NOT_NULL(reg);
const char *src = "import { foo as fooShared } from './lib';\n"
"import { Conn as Connection } from './conn';\n"
"export function callsRenamed() { return fooShared(1); }\n"
"export function viaRenamedClass() { const c = new Connection(); c.ping(); }\n";
CBMResolvedCallArray out = {0};
cbm_run_ts_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.main", false, false,
false, reg, defs, NDEFS, imp_names, imp_qns, 2, NULL, &out);
/* The call site names the alias, so the join reads it from `reason`. */
ASSERT_TRUE(
has_exact_resolved(&out, "test.main.callsRenamed", "test.lib.foo", "lsp_import_alias"));
ASSERT_TRUE(has_exact_resolved(&out, "test.main.viaRenamedClass", "test.conn.Conn.ping",
"lsp_ts_method"));
cbm_arena_destroy(&arena);
PASS();
}
/* The export name is looked up exactly. A renamed import of a name the module
* does not export must not fall back to the alias: before #2297 the alias `foo`
* spelled `test.lib.foo` and bound this call to an unrelated export. */
TEST(tslsp_crossfile_renamed_import_missing_export_issue2297) {
const char *source = "import { missing as foo } from './lib';\n"
"function go() { foo(1); }\n";
CBMLSPDef defs[] = {
{.qualified_name = "test.main.go",
.short_name = "go",
.label = "Function",
.def_module_qn = "test.main"},
{.qualified_name = "test.lib.foo",
.short_name = "foo",
.label = "Function",
.def_module_qn = "test.lib"},
};
const char *imp_names[] = {"foo"};
const char *imp_qns[] = {"test.lib"};
CBMArena arena;
cbm_arena_init(&arena);
CBMResolvedCallArray out = {0};
cbm_run_ts_lsp_cross(&arena, source, (int)strlen(source), "test.main", false, false, false,
defs, 2, imp_names, imp_qns, 1, NULL, &out);
ASSERT_EQ(find_resolved_arr(&out, "go", "test.lib.foo"), -1);
cbm_arena_destroy(&arena);
PASS();
}
/* `import { default as greet }` is a default import: it keeps resolving by its
* local name, as `import greet from` does. */
TEST(tslsp_crossfile_default_as_import_issue2297) {
const char *source = "import { default as greet } from './greetings';\n"
"function go() { greet('hi'); }\n";
CBMLSPDef defs[] = {
{.qualified_name = "test.main.go",
.short_name = "go",
.label = "Function",
.def_module_qn = "test.main"},
{.qualified_name = "test.greetings.greet",
.short_name = "greet",
.label = "Function",
.def_module_qn = "test.greetings",
.return_types = "string"},
};
const char *imp_names[] = {"greet"};
const char *imp_qns[] = {"test.greetings"};
CBMArena arena;
cbm_arena_init(&arena);
CBMResolvedCallArray out = {0};
cbm_run_ts_lsp_cross(&arena, source, (int)strlen(source), "test.main", false, false, false,
defs, 2, imp_names, imp_qns, 1, NULL, &out);
ASSERT_GTE(find_resolved_arr(&out, "go", "test.greetings.greet"), 0);
cbm_arena_destroy(&arena);
PASS();
}
/* ── More stdlib (Date, RegExp, Error, Math) ──────────────────────────────── */
TEST(tslsp_stdlib_date_toISOString) {
@@ -5243,7 +5488,13 @@ SUITE(ts_lsp) {
RUN_TEST(tslsp_scale_many_defs_no_crash_issue344);
RUN_TEST(tslsp_crossfile_function_call);
RUN_TEST(tslsp_crossfile_chain_through_return);
RUN_TEST(tslsp_crossfile_renamed_import_return_chain);
RUN_TEST(tslsp_crossfile_renamed_import_return_chain_shared);
RUN_TEST(tslsp_import_alias_arena_growth);
RUN_TEST(tslsp_crossfile_no_def_match);
RUN_TEST(tslsp_crossfile_renamed_import_issue2297);
RUN_TEST(tslsp_crossfile_renamed_import_missing_export_issue2297);
RUN_TEST(tslsp_crossfile_default_as_import_issue2297);
/* More stdlib */
RUN_TEST(tslsp_stdlib_date_toISOString);