From 2641ec858dff11307d42e60e8e3dccc627d02f0e Mon Sep 17 00:00:00 2001
From: mikemikimike <13286568797@163.com>
Date: Wed, 2 Sep 2026 07:03:29 +0800
Subject: [PATCH 1/2] fix(ts): resolve inferred imported constructor receivers
Signed-off-by: mikemikimike <13286568797@163.com>
---
internal/cbm/lsp/ts_lsp.c | 17 +++++++++++++----
tests/repro/repro_lsp_ts.c | 26 ++++++++++++++++++++++++++
2 files changed, 39 insertions(+), 4 deletions(-)
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..c41bd8291 100644
--- a/tests/repro/repro_lsp_ts.c
+++ b/tests/repro/repro_lsp_ts.c
@@ -297,6 +297,23 @@ 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"},
+};
+
/* 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 +387,14 @@ 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_jsx) {
return assert_lsp_strategy("app.tsx", kTsxJsx, "lsp_ts_jsx");
}
@@ -403,6 +428,7 @@ 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_jsx);
RUN_TEST(repro_lsp_ts_jsx_import);
RUN_TEST(repro_lsp_ts_default);
From a739a2d81b43eb6938d014e005b306ab27e02396 Mon Sep 17 00:00:00 2001
From: mikemikimike <13286568797@163.com>
Date: Wed, 2 Sep 2026 08:58:42 +0800
Subject: [PATCH 2/2] test(ts): cover inferred stdlib constructor receivers
Signed-off-by: mikemikimike <13286568797@163.com>
---
tests/repro/repro_lsp_ts.c | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/tests/repro/repro_lsp_ts.c b/tests/repro/repro_lsp_ts.c
index c41bd8291..8c441af22 100644
--- a/tests/repro/repro_lsp_ts.c
+++ b/tests/repro/repro_lsp_ts.c
@@ -314,6 +314,14 @@ static const RFile kTsInferredImportedMethod[] = {
"}\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
@@ -395,6 +403,11 @@ TEST(repro_lsp_ts_inferred_import_receiver) {
"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");
}
@@ -429,6 +442,7 @@ SUITE(repro_lsp_ts) {
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);