diff --git a/internal/cbm/lsp/ts_lsp.c b/internal/cbm/lsp/ts_lsp.c index fc8b8911f..ec0b80e0e 100644 --- a/internal/cbm/lsp/ts_lsp.c +++ b/internal/cbm/lsp/ts_lsp.c @@ -2176,10 +2176,19 @@ const CBMType *ts_eval_expr_type(TSLSPContext *ctx, TSNode node) { if (!ts_node_is_null(ctor)) { char *cname = node_text(ctx, ctor); if (cname) { - // Bare class name → qualify against module. - if (strchr(cname, '.') == NULL && ctx->module_qn) { - const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cname); - result = cbm_type_named(ctx->arena, qn); + // Resolve lexical/import bindings before falling back to the + // current module. This preserves the imported class QN for + // inferred locals such as `const x = new ImportedClass()`. + if (strchr(cname, '.') == NULL) { + const CBMType *bound = type_of_identifier(ctx, cname); + if (bound && bound->kind == CBM_TYPE_NAMED && + bound->data.named.qualified_name) { + result = bound; + } else if (ctx->module_qn) { + const char *qn = + cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cname); + result = cbm_type_named(ctx->arena, qn); + } } else { result = cbm_type_named(ctx->arena, cname); } diff --git a/tests/repro/repro_lsp_ts.c b/tests/repro/repro_lsp_ts.c index bedcb33d8..8c441af22 100644 --- a/tests/repro/repro_lsp_ts.c +++ b/tests/repro/repro_lsp_ts.c @@ -297,6 +297,31 @@ static const RFile kTsImport[] = { "function caller(v: number): number { return helper(v); }\n"}, }; +/* An imported class constructed through an inferred local variable must retain + * the imported class QN for subsequent member dispatch. The explicit type + * annotation path already covers this shape; this fixture isolates inference. + */ +static const RFile kTsInferredImportedMethod[] = { + {"store.ts", + "export class WidgetStore {\n" + " findById(id: string): string { return id; }\n" + "}\n"}, + {"service.ts", + "import { WidgetStore } from \"./store\";\n" + "function load(id: string): string {\n" + " const store = new WidgetStore();\n" + " return store.findById(id);\n" + "}\n"}, +}; + +/* A standard-library class constructed through an inferred local variable must + * retain the bare stdlib QN so subsequent member dispatch finds Map.get. */ +static const char kTsInferredStdlibMethod[] = + "function lookup(id: string): string {\n" + " const m = new Map();\n" + " return m.get(id);\n" + "}\n"; + /* lsp_ts_jsx — JSX element whose tag is a module-local component * function (ts_lsp.c:2643-2647). TSX only (jsx_mode); the tag's first letter is * uppercase so it is NOT treated as an intrinsic HTML element; it resolves via @@ -370,6 +395,19 @@ TEST(repro_lsp_ts_import) { "lsp_ts_import"); } +TEST(repro_lsp_ts_inferred_import_receiver) { + return assert_lsp_strategy_files( + kTsInferredImportedMethod, + (int)(sizeof(kTsInferredImportedMethod) / + sizeof(kTsInferredImportedMethod[0])), + "lsp_ts_method"); +} + +TEST(repro_lsp_ts_inferred_stdlib_receiver) { + return assert_lsp_strategy("main.ts", kTsInferredStdlibMethod, + "lsp_ts_method"); +} + TEST(repro_lsp_ts_jsx) { return assert_lsp_strategy("app.tsx", kTsxJsx, "lsp_ts_jsx"); } @@ -403,6 +441,8 @@ SUITE(repro_lsp_ts) { RUN_TEST(repro_lsp_ts_method); RUN_TEST(repro_lsp_ts_namespace); RUN_TEST(repro_lsp_ts_import); + RUN_TEST(repro_lsp_ts_inferred_import_receiver); + RUN_TEST(repro_lsp_ts_inferred_stdlib_receiver); RUN_TEST(repro_lsp_ts_jsx); RUN_TEST(repro_lsp_ts_jsx_import); RUN_TEST(repro_lsp_ts_default);