/* * test_ts_lsp.c — Tests for TypeScript / JavaScript / JSX / TSX hybrid LSP type resolver. * * Categories (per docs/TS_LSP_INTEGRATION_PLAN.md §9.1): * 1. Param type inference (`tslsp_param_*`) * 2. Return type propagation (`tslsp_return_*`) * 3. Method chaining (`tslsp_chain_*`) * 4. Multi-return / destructuring (`tslsp_destructure_*`) * 7. Object literal property typing (`tslsp_object_*`) * 8. Type alias (`tslsp_alias_*`) * 9. Class / inheritance (`tslsp_class_*`) * 10. Interface dispatch (`tslsp_iface_*`) * 11. Generics (`tslsp_generic_*`) * 12. Stdlib (`tslsp_stdlib_*`) * 13. Optional chaining (`tslsp_optional_*`) * 14. Async / await (`tslsp_await_*`) * 15. Type guards / narrowing (`tslsp_narrow_*`) * 16. Union & intersection (`tslsp_union_*` / `tslsp_inter_*`) * 17. Literal types (`tslsp_literal_*`) * 20. Module / re-export resolution (`tslsp_module_*`) * 25. Diagnostics (`tslsp_diag_*`) * 26. Crash safety (`tslsp_nocrash_*`) */ #include "test_framework.h" #include "cbm.h" #include "../src/foundation/compat.h" #include "lsp/ts_lsp.h" /* ── Helpers ───────────────────────────────────────────────────────────────── */ static CBMFileResult *extract_with(const char *source, CBMLanguage lang, const char *fname) { return cbm_extract_file(source, (int)strlen(source), lang, "test", fname, 0, NULL, NULL); } static CBMFileResult *extract_ts(const char *source) { return extract_with(source, CBM_LANG_TYPESCRIPT, "main.ts"); } static CBMFileResult *extract_tsx(const char *source) { return extract_with(source, CBM_LANG_TSX, "main.tsx"); } static CBMFileResult *extract_js(const char *source) { return extract_with(source, CBM_LANG_JAVASCRIPT, "main.js"); } static CBMFileResult *extract_dts(const char *source) { return extract_with(source, CBM_LANG_TYPESCRIPT, "main.d.ts"); } static int find_resolved(const CBMFileResult *r, const char *callerSub, const char *calleeSub) { for (int i = 0; i < r->resolved_calls.count; i++) { const CBMResolvedCall *rc = &r->resolved_calls.items[i]; if (rc->confidence > 0 && rc->caller_qn && strstr(rc->caller_qn, callerSub) && rc->callee_qn && strstr(rc->callee_qn, calleeSub)) return i; } return -1; } static int require_resolved(const CBMFileResult *r, const char *callerSub, const char *calleeSub) { int idx = find_resolved(r, callerSub, calleeSub); if (idx < 0) { printf(" MISSING resolved call: caller~%s -> callee~%s (have %d)\n", callerSub, calleeSub, r->resolved_calls.count); for (int i = 0; i < r->resolved_calls.count; i++) { const CBMResolvedCall *rc = &r->resolved_calls.items[i]; printf(" %s -> %s [%s %.2f]\n", rc->caller_qn ? rc->caller_qn : "(null)", rc->callee_qn ? rc->callee_qn : "(null)", rc->strategy ? rc->strategy : "(null)", rc->confidence); } } return idx; } /* ── Category 0: smoke (Phase 1 carryover) ─────────────────────────────────── */ TEST(tslsp_smoke_empty_file) { CBMFileResult *r = extract_ts(""); ASSERT_NOT_NULL(r); ASSERT(r->resolved_calls.count >= 0); cbm_free_result(r); PASS(); } TEST(tslsp_smoke_minimal_function) { CBMFileResult *r = extract_ts("function greet(name: string): string {\n" " return 'hello ' + name;\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT(r->resolved_calls.count >= 0); cbm_free_result(r); PASS(); } TEST(tslsp_smoke_jsx_minimal) { CBMFileResult *r = extract_tsx("function App(): JSX.Element {\n" " return
hello
;\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT(r->resolved_calls.count >= 0); cbm_free_result(r); PASS(); } TEST(tslsp_smoke_js_minimal) { CBMFileResult *r = extract_js("function add(a, b) {\n" " return a + b;\n" "}\n" "add(1, 2);\n"); ASSERT_NOT_NULL(r); ASSERT(r->resolved_calls.count >= 0); cbm_free_result(r); PASS(); } TEST(tslsp_smoke_dts_minimal) { CBMFileResult *r = extract_dts("export interface User {\n" " name: string;\n" " age: number;\n" "}\n" "export declare function loadUser(id: string): User;\n"); ASSERT_NOT_NULL(r); ASSERT(r->resolved_calls.count >= 0); cbm_free_result(r); PASS(); } /* ── Category 1: param type inference ──────────────────────────────────────── */ TEST(tslsp_param_simple) { CBMFileResult *r = extract_ts("class Database { query(sql: string): string { return ''; } }\n" "function doWork(db: Database) { db.query('SELECT 1'); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "doWork", "query"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_param_multi) { CBMFileResult *r = extract_ts("class Logger { info(msg: string): void {} }\n" "class Config { get(key: string): string { return ''; } }\n" "function setup(log: Logger, cfg: Config) {\n" " log.info('starting');\n" " cfg.get('port');\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "setup", "info"), 0); ASSERT_GTE(require_resolved(r, "setup", "get"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_param_arrow) { CBMFileResult *r = extract_ts("class Conn { close(): void {} }\n" "const useConn = (c: Conn) => { c.close(); };\n"); ASSERT_NOT_NULL(r); /* arrow fn may or may not be a graphed Function — accept either */ cbm_free_result(r); PASS(); } TEST(tslsp_param_optional) { CBMFileResult *r = extract_ts("class Foo { ping(): void {} }\n" "function maybe(x?: Foo) { if (x) { x.ping(); } }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "maybe", "ping"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_param_with_default) { CBMFileResult *r = extract_ts("class Cache { put(k: string): void {} }\n" "function f(c: Cache = new Cache()) { c.put('x'); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".f", "put"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_param_destructured) { CBMFileResult *r = extract_ts("interface Box { tag: string; }\n" "interface Pair { a: Box; b: Box; }\n" "class Box2 { read(): string { return ''; } }\n" "function take({ a }: { a: Box2 }) { a.read(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "take", "read"), 0); cbm_free_result(r); PASS(); } /* ── Category 2: return type propagation ───────────────────────────────────── */ TEST(tslsp_return_simple) { CBMFileResult *r = extract_ts("class File { read(): number { return 0; } }\n" "function open(path: string): File { return new File(); }\n" "function doRead() { const f = open('/tmp'); f.read(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "doRead", "read"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_return_chain) { CBMFileResult *r = extract_ts("class Result { describe(): string { return ''; } }\n" "class Builder { build(): Result { return new Result(); } }\n" "function newBuilder(): Builder { return new Builder(); }\n" "function chain() {\n" " const b = newBuilder();\n" " const res = b.build();\n" " res.describe();\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "chain", "build"), 0); ASSERT_GTE(require_resolved(r, "chain", "describe"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_return_inferred_local) { CBMFileResult *r = extract_ts("class Conn { ping(): void {} }\n" "function makeConn() { return new Conn(); }\n" "function go() { const c = makeConn(); c.ping(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "ping"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_return_async_promise) { CBMFileResult *r = extract_ts("class User { hello(): void {} }\n" "async function load(): Promise { return new User(); }\n" "async function go() { const u = await load(); u.hello(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "hello"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_return_method_no_rttype) { CBMFileResult *r = extract_ts("class Service { handle(): string { return ''; } }\n" "function locate(): Service { return new Service(); }\n" "function go() { locate().handle(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "handle"), 0); cbm_free_result(r); PASS(); } /* ── Category 3: method chaining ───────────────────────────────────────────── */ TEST(tslsp_chain_fluent) { CBMFileResult *r = extract_ts("class Query {\n" " where(c: string): Query { return this; }\n" " limit(n: number): Query { return this; }\n" " execute(): number { return 0; }\n" "}\n" "function newQuery(): Query { return new Query(); }\n" "function go() { newQuery().where('a').limit(5).execute(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "where"), 0); ASSERT_GTE(require_resolved(r, ".go", "limit"), 0); ASSERT_GTE(require_resolved(r, ".go", "execute"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_chain_with_intermediate_var) { CBMFileResult *r = extract_ts("class Pipe { in(): Pipe { return this; } out(): string { return ''; } }\n" "function go() { const p = new Pipe(); p.in().out(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "in"), 0); ASSERT_GTE(require_resolved(r, ".go", "out"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_chain_promise_then) { CBMFileResult *r = extract_ts("function fetchData(): Promise { return Promise.resolve(0); }\n" "function go() { fetchData().then(x => x); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "then"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_chain_string_methods) { CBMFileResult *r = extract_ts("function go(s: string) { s.trim().toUpperCase().includes('X'); }\n"); ASSERT_NOT_NULL(r); /* Even partial chain resolution is acceptable for v1 */ cbm_free_result(r); PASS(); } /* ── Category 4: destructuring ─────────────────────────────────────────────── */ TEST(tslsp_destructure_object) { CBMFileResult *r = extract_ts("class Inner { speak(): void {} }\n" "function provider(): { val: Inner } { return { val: new Inner() }; }\n" "function go() { const { val } = provider(); val.speak(); }\n"); ASSERT_NOT_NULL(r); /* Destructuring across object literal return is hard; accept smoke pass */ cbm_free_result(r); PASS(); } TEST(tslsp_destructure_array) { CBMFileResult *r = extract_ts("class A { run(): void {} }\n" "function pair(): A[] { return [new A()]; }\n" "function go() { const [first] = pair(); first.run(); }\n"); ASSERT_NOT_NULL(r); /* Tuple/array destructuring may resolve via Array first arg */ cbm_free_result(r); PASS(); } TEST(tslsp_destructure_param) { CBMFileResult *r = extract_ts("interface Pkg { tool: { name(): string } }\n" "function go({ tool }: Pkg) { tool.name(); }\n"); ASSERT_NOT_NULL(r); /* Inline anonymous object types — accept smoke pass */ cbm_free_result(r); PASS(); } TEST(tslsp_destructure_renamed) { CBMFileResult *r = extract_ts("class Engine { fire(): void {} }\n" "function provider(): { motor: Engine } { return { motor: new Engine() }; }\n" "function go() { const { motor: m } = provider(); m.fire(); }\n"); ASSERT_NOT_NULL(r); /* Renamed destructuring — accept smoke pass */ cbm_free_result(r); PASS(); } /* ── Category 7: object literal property typing ───────────────────────────── */ TEST(tslsp_object_property_method) { CBMFileResult *r = extract_ts("class Greeter { say(msg: string): void {} }\n" "function go() {\n" " const env = { greeter: new Greeter() };\n" " env.greeter.say('hi');\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "say"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_object_nested_property) { CBMFileResult *r = extract_ts("class Tool { use(): number { return 0; } }\n" "function go() {\n" " const env = { box: { tool: new Tool() } };\n" " env.box.tool.use();\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "use"), 0); cbm_free_result(r); PASS(); } /* ── Category 8: type alias ───────────────────────────────────────────────── */ TEST(tslsp_alias_simple) { CBMFileResult *r = extract_ts("class Inner { ping(): void {} }\n" "type Wrapper = Inner;\n" "function go(w: Wrapper) { w.ping(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "ping"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_alias_chain) { CBMFileResult *r = extract_ts("class Inner { ping(): void {} }\n" "type A = Inner;\n" "type B = A;\n" "function go(w: B) { w.ping(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "ping"), 0); cbm_free_result(r); PASS(); } /* ── Category 9: class / inheritance ──────────────────────────────────────── */ TEST(tslsp_class_method_dispatch) { CBMFileResult *r = extract_ts("class Foo { bar(): void {} }\n" "function go(f: Foo) { f.bar(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "bar"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_class_this_dispatch) { CBMFileResult *r = extract_ts("class Counter {\n" " inc(): void {}\n" " bump(): void { this.inc(); }\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "Counter.bump", "inc"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_class_inheritance_method) { CBMFileResult *r = extract_ts("class Animal { breathe(): void {} }\n" "class Dog extends Animal {}\n" "function go(d: Dog) { d.breathe(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "breathe"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_class_super_call) { CBMFileResult *r = extract_ts("class Base { run(): void {} }\n" "class Child extends Base { go(): void { super.run(); } }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "Child.go", "run"), 0); cbm_free_result(r); PASS(); } /* ── Category 10: interface dispatch ──────────────────────────────────────── */ TEST(tslsp_iface_method) { CBMFileResult *r = extract_ts("interface Closer { close(): void; }\n" "function go(c: Closer) { c.close(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "close"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_iface_field_chain) { CBMFileResult *r = extract_ts("interface Tool { run(): void; }\n" "interface Box { tool: Tool; }\n" "function go(b: Box) { b.tool.run(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "run"), 0); cbm_free_result(r); PASS(); } /* ── Category 12: stdlib (subset) ─────────────────────────────────────────── */ TEST(tslsp_stdlib_array_push) { CBMFileResult *r = extract_ts("function go() { const a: number[] = []; a.push(1); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "push"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_stdlib_array_map) { CBMFileResult *r = extract_ts("function go(xs: number[]) { xs.map(x => x + 1); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "map"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_stdlib_console_log) { CBMFileResult *r = extract_ts("function go() { console.log('hi'); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "log"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_stdlib_promise_then) { CBMFileResult *r = extract_ts("function load(): Promise { return Promise.resolve('hi'); }\n" "function go() { load().then(x => x); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "then"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_stdlib_map_get) { CBMFileResult *r = extract_ts("function go(m: Map) { m.get('k'); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "get"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_stdlib_set_has) { CBMFileResult *r = extract_ts("function go(s: Set) { s.has('k'); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "has"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_stdlib_json_stringify) { CBMFileResult *r = extract_ts("function go(x: object) { JSON.stringify(x); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "stringify"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_stdlib_object_keys) { CBMFileResult *r = extract_ts("function go(x: object) { Object.keys(x); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "keys"), 0); cbm_free_result(r); PASS(); } /* ── Category 13: optional chaining ───────────────────────────────────────── */ TEST(tslsp_optional_chain_method) { CBMFileResult *r = extract_ts("class Foo { run(): void {} }\n" "function go(f?: Foo) { f?.run(); }\n"); ASSERT_NOT_NULL(r); /* Optional chain support — graceful fallback is fine for v1 */ cbm_free_result(r); PASS(); } /* ── Category 14: async / await ───────────────────────────────────────────── */ TEST(tslsp_await_promise_unwrap) { CBMFileResult *r = extract_ts("class Conn { ping(): void {} }\n" "async function getConn(): Promise { return new Conn(); }\n" "async function go() { const c = await getConn(); c.ping(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "ping"), 0); cbm_free_result(r); PASS(); } /* ── Category 16: union types ─────────────────────────────────────────────── */ TEST(tslsp_union_member_present_on_both) { CBMFileResult *r = extract_ts("class A { run(): void {} }\n" "class B { run(): void {} }\n" "function go(x: A | B) { x.run(); }\n"); ASSERT_NOT_NULL(r); /* Union dispatch: lookup_member tries each branch — accept either as resolved */ cbm_free_result(r); PASS(); } /* ── Category 17: literal types (smoke) ───────────────────────────────────── */ TEST(tslsp_literal_const_assertion) { CBMFileResult *r = extract_ts("function go() { const k = 'foo' as const; }\n"); ASSERT_NOT_NULL(r); /* No call to resolve — verify it doesn't crash on `as const` */ cbm_free_result(r); PASS(); } /* ── Category 20: imports / module resolution ─────────────────────────────── */ TEST(tslsp_module_named_import_call) { CBMFileResult *r = extract_ts("import { console as logger } from 'console';\n" "function go() { logger.log('hi'); }\n"); ASSERT_NOT_NULL(r); /* Single-file import resolution — module 'console' doesn't auto-resolve. * Accept smoke pass; cross-file path tests this in Phase 3. */ cbm_free_result(r); PASS(); } /* ── Category 25: diagnostics (unresolved-call markers) ───────────────────── */ TEST(tslsp_diag_unknown_call_emits_unresolved) { CBMFileResult *r = extract_ts("function go() { mystery(); }\n"); ASSERT_NOT_NULL(r); /* At least one entry — either a diagnostic with confidence 0 or none. Skeleton-friendly. */ ASSERT(r->resolved_calls.count >= 0); cbm_free_result(r); PASS(); } /* ── Category 26: crash safety ─────────────────────────────────────────────── */ TEST(tslsp_nocrash_unclosed_string) { CBMFileResult *r = extract_ts("function go() { const s = 'unclosed"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_self_referential_alias) { CBMFileResult *r = extract_ts("type T = T;\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_deep_generic) { CBMFileResult *r = extract_ts("type X = Array>>>>>>>;\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_with_block_legacy) { CBMFileResult *r = extract_js("function go(o) { with (o) { foo(); } }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_eval_value) { CBMFileResult *r = extract_js("function go() { eval('1+1'); }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_using_decl) { CBMFileResult *r = extract_ts("function go() { using x = { [Symbol.dispose]: () => {} }; }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_megasource) { /* 50 KB of `;` should parse and walk without blowing the stack or arena. */ char *buf = (char *)malloc(50001); if (!buf) PASS(); memset(buf, ';', 50000); buf[50000] = '\0'; CBMFileResult *r = extract_ts(buf); free(buf); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } /* ── Category 11: generics (deeper) ─────────────────────────────────────────── */ TEST(tslsp_generic_identity_inference) { CBMFileResult *r = extract_ts("function identity(x: T): T { return x; }\n" "class Box { use(): void {} }\n" "function go() { const b = identity(new Box()); b.use(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "use"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_generic_array_map_chain) { CBMFileResult *r = extract_ts("function go(xs: number[]) { xs.map(x => x).filter(x => x > 0).length; }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "map"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_generic_promise_resolve) { CBMFileResult *r = extract_ts("function go() { Promise.resolve(42).then(x => x); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "resolve"), 0); ASSERT_GTE(require_resolved(r, ".go", "then"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_generic_array_from) { CBMFileResult *r = extract_ts("function go(s: string) { Array.from(s); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "from"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_generic_array_isArray) { CBMFileResult *r = extract_ts("function go(x: unknown) { Array.isArray(x); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "isArray"), 0); cbm_free_result(r); PASS(); } /* ── Category 14: async deeper ─────────────────────────────────────────────── */ TEST(tslsp_await_promise_all) { CBMFileResult *r = extract_ts("async function go() { await Promise.all([1, 2, 3]); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "all"), 0); cbm_free_result(r); PASS(); } /* ── Category 15: type narrowing (basic) ────────────────────────────────────── */ TEST(tslsp_narrow_typeof_check) { CBMFileResult *r = extract_ts("function go(x: string | number) {\n" " if (typeof x === 'string') { x.toLowerCase(); }\n" "}\n"); ASSERT_NOT_NULL(r); /* String narrowing — accept smoke pass for v1; tsserver narrows fully */ cbm_free_result(r); PASS(); } TEST(tslsp_narrow_instanceof) { CBMFileResult *r = extract_ts("class A { ping(): void {} }\n" "class B { pong(): void {} }\n" "function go(x: A | B) {\n" " if (x instanceof A) { x.ping(); }\n" "}\n"); ASSERT_NOT_NULL(r); /* instanceof narrowing — accept smoke pass for v1 */ cbm_free_result(r); PASS(); } /* ── Category 17: literal types (deeper) ────────────────────────────────────── */ TEST(tslsp_literal_string_union) { CBMFileResult *r = extract_ts("type Mode = 'on' | 'off';\n" "function go(m: Mode) { console.log(m); }\n"); ASSERT_NOT_NULL(r); /* Literal-type union as parameter; call to console.log should resolve */ ASSERT_GTE(require_resolved(r, ".go", "log"), 0); cbm_free_result(r); PASS(); } /* ── Category 16: union deeper ─────────────────────────────────────────────── */ TEST(tslsp_union_common_method) { CBMFileResult *r = extract_ts("interface A { run(): void; }\n" "interface B { run(): void; }\n" "function go(x: A | B) { x.run(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "run"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_union_many_members_no_overflow) { // Regression: a union with >=16 members used to overflow the // 16-slot members[] array in parse_ts_type_text() — the trailing // post-loop append wrote past the end (UBSan: index 16 out of // bounds). The crash was non-deterministic under pthread parallel // extraction, surfaced by indexing the zod TS codebase. Just // extracting must not crash; resolution behavior is unconstrained. CBMFileResult *r = extract_ts("function go(x: 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' " "| 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' " "| 's' | 't') { return x; }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } /* ── Category 9: more class patterns ───────────────────────────────────────── */ TEST(tslsp_class_static_method) { CBMFileResult *r = extract_ts("class Util { static helper(): void {} }\n" "function go() { Util.helper(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "helper"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_class_method_then_field) { CBMFileResult *r = extract_ts("class Inner { fire(): void {} }\n" "class Outer { tool: Inner = new Inner(); }\n" "function go(o: Outer) { o.tool.fire(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "fire"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_class_chained_field_method) { CBMFileResult *r = extract_ts("class Leaf { ping(): void {} }\n" "class Branch { leaf: Leaf = new Leaf(); }\n" "class Trunk { branch: Branch = new Branch(); }\n" "function go(t: Trunk) { t.branch.leaf.ping(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "ping"), 0); cbm_free_result(r); PASS(); } /* ── Category 12: more stdlib ──────────────────────────────────────────────── */ TEST(tslsp_stdlib_array_filter) { CBMFileResult *r = extract_ts("function go(xs: number[]) { xs.filter(x => x > 0); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "filter"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_stdlib_array_find) { CBMFileResult *r = extract_ts("function go(xs: number[]) { xs.find(x => x > 0); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "find"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_stdlib_array_reduce) { CBMFileResult *r = extract_ts("function go(xs: number[]) { xs.reduce((a, b) => a + b, 0); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "reduce"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_stdlib_string_split) { CBMFileResult *r = extract_ts("function go(s: string) { s.split(',').map(p => p.trim()); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "split"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_stdlib_set_add) { CBMFileResult *r = extract_ts("function go(s: Set) { s.add('x').size; }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "add"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_stdlib_map_set) { CBMFileResult *r = extract_ts("function go(m: Map) { m.set('k', 1).get('k'); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "set"), 0); ASSERT_GTE(require_resolved(r, ".go", "get"), 0); cbm_free_result(r); PASS(); } /* ── Category 26: more crash safety ────────────────────────────────────────── */ TEST(tslsp_nocrash_circular_extends) { CBMFileResult *r = extract_ts("class A extends B {}\n" "class B extends A {}\n" "function go(a: A) { a.x; }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_recursive_type) { CBMFileResult *r = extract_ts("interface List { next?: List; value: number; }\n" "function go(l: List) { l.next?.value; }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_unicode_identifier) { CBMFileResult *r = extract_ts("class \xc3\x84pf { ping(): void {} }\n" "function go(a: \xc3\x84pf) { a.ping(); }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_template_with_call) { CBMFileResult *r = extract_ts("function tag(strs: TemplateStringsArray): string { return ''; }\n" "function go() { const x = tag`hello ${1 + 2}`; }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_decorator) { CBMFileResult *r = extract_ts("function dec(target: any) { return target; }\n" "@dec\n" "class Foo { run(): void {} }\n" "function go(f: Foo) { f.run(); }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_private_field) { CBMFileResult *r = extract_ts("class Foo { #x = 1; bar(): number { return this.#x; } }\n" "function go(f: Foo) { f.bar(); }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_getter_setter) { CBMFileResult *r = extract_ts("class Foo { _x = 0; get x(): number { return this._x; } set " "x(v: number) { this._x = v; } }\n" "function go(f: Foo) { f.x; f.x = 5; }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_abstract_class) { CBMFileResult *r = extract_ts("abstract class Base { abstract step(): void; }\n" "class Impl extends Base { step(): void {} }\n" "function go(i: Impl) { i.step(); }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } /* ── Category 22: JSX intrinsic + component ─────────────────────────────────── */ TEST(tslsp_jsx_component_self_closing) { CBMFileResult *r = extract_tsx("function Greeting(props: { name: string }): JSX.Element { return " "{props.name}; }\n" "function App(): JSX.Element { return ; }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "App", "Greeting"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_jsx_component_with_children) { CBMFileResult *r = extract_tsx( "function Card(props: { title: string }): JSX.Element { return
{props.title}
; " "}\n" "function App(): JSX.Element { return x; }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "App", "Card"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_jsx_intrinsic_skipped) { CBMFileResult *r = extract_tsx("function go(): JSX.Element { return
plain
; }\n"); ASSERT_NOT_NULL(r); /* Intrinsic
should be skipped (no false positive). */ cbm_free_result(r); PASS(); } TEST(tslsp_jsx_nested_component) { CBMFileResult *r = extract_tsx("function Inner(): JSX.Element { return ; }\n" "function Outer(): JSX.Element { return
; }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "Outer", "Inner"), 0); cbm_free_result(r); PASS(); } /* ── Category 23: TSX combined ─────────────────────────────────────────────── */ TEST(tslsp_tsx_typed_props_method_call) { CBMFileResult *r = extract_tsx("class Service { fetch(): string { return ''; } }\n" "function App(props: { svc: Service }): JSX.Element {\n" " props.svc.fetch();\n" " return
;\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "App", "fetch"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_tsx_class_component_method) { CBMFileResult *r = extract_tsx("class Logger { log(): void {} }\n" "class App {\n" " logger: Logger = new Logger();\n" " render(): JSX.Element { this.logger.log(); return
; }\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "App.render", "log"), 0); cbm_free_result(r); PASS(); } /* ── Tuple types (TS) ─────────────────────────────────────────────────────── */ TEST(tslsp_tuple_destructure_method) { CBMFileResult *r = extract_ts("class Box { run(): void {} }\n" "function pair(): [Box, number] { return [new Box(), 42]; }\n" "function go() { const [b, n] = pair(); b.run(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "run"), 0); cbm_free_result(r); PASS(); } /* ── Number wrapper class ──────────────────────────────────────────────────── */ TEST(tslsp_stdlib_number_toString) { CBMFileResult *r = extract_ts("function go(n: number) { n.toString(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "toString"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_stdlib_number_toFixed) { CBMFileResult *r = extract_ts("function go(n: number) { n.toFixed(2); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "toFixed"), 0); cbm_free_result(r); PASS(); } /* ── More crash-safety ─────────────────────────────────────────────────────── */ TEST(tslsp_nocrash_huge_union) { CBMFileResult *r = extract_ts("type T = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j';\n" "function go(x: T) { console.log(x); }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_intersection_chain) { CBMFileResult *r = extract_ts( "interface A { x: number; } interface B { y: string; } interface C { z: boolean; }\n" "function go(o: A & B & C) { console.log(o.x, o.y, o.z); }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_satisfies_keyword) { CBMFileResult *r = extract_ts( "type X = { name: string };\n" "function go() { const obj = { name: 'a' } satisfies X; console.log(obj.name); }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } /* ── Category 21: JSDoc inference (js_mode) ────────────────────────────────── */ TEST(tslsp_jsdoc_param_method_call) { CBMFileResult *r = extract_js("class Database { query(sql) { return ''; } }\n" "/** @param {Database} db */\n" "function doWork(db) { db.query('SELECT 1'); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "doWork", "query"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_jsdoc_returns_propagation) { CBMFileResult *r = extract_js("class Conn { ping() {} }\n" "/** @returns {Conn} */\n" "function makeConn() { return new Conn(); }\n" "function go() { const c = makeConn(); c.ping(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "ping"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_jsdoc_multi_param) { CBMFileResult *r = extract_js("class Logger { info(s) {} }\n" "class Cache { put(k) {} }\n" "/**\n" " * @param {Logger} log\n" " * @param {Cache} cache\n" " */\n" "function setup(log, cache) {\n" " log.info('hi');\n" " cache.put('k');\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "setup", "info"), 0); ASSERT_GTE(require_resolved(r, "setup", "put"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_jsdoc_array_generic) { CBMFileResult *r = extract_js("/** @param {Array} xs */\n" "function go(xs) { xs.map(x => x); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "map"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_jsdoc_promise_returns) { CBMFileResult *r = extract_js("/** @returns {Promise} */\n" "function load() { return Promise.resolve('hi'); }\n" "function go() { load().then(x => x); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "then"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_jsdoc_ignored_in_ts_mode) { /* In TS mode, JSDoc is informative only — the inline annotations win. */ CBMFileResult *r = extract_ts("class Foo { run(): void {} }\n" "/** @param {Foo} x */\n" "function go(x: Foo) { x.run(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "run"), 0); cbm_free_result(r); PASS(); } /* ── Category 31: real-library patterns (smoke) ────────────────────────────── */ TEST(tslsp_real_express_handler) { CBMFileResult *r = extract_ts( "interface Request { params: { [key: string]: string }; }\n" "interface Response { send(body: string): Response; status(code: number): Response; }\n" "function handle(req: Request, res: Response) { res.status(200).send('ok'); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "handle", "status"), 0); ASSERT_GTE(require_resolved(r, "handle", "send"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_real_fluent_builder) { CBMFileResult *r = extract_ts("class QueryBuilder {\n" " select(cols: string[]): QueryBuilder { return this; }\n" " from(t: string): QueryBuilder { return this; }\n" " where(c: string): QueryBuilder { return this; }\n" " execute(): string[] { return []; }\n" "}\n" "function go() {\n" " new QueryBuilder().select(['id']).from('users').where('1=1').execute();\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "select"), 0); ASSERT_GTE(require_resolved(r, ".go", "from"), 0); ASSERT_GTE(require_resolved(r, ".go", "where"), 0); ASSERT_GTE(require_resolved(r, ".go", "execute"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_real_observer_pattern) { CBMFileResult *r = extract_ts("interface Observer { update(value: number): void; }\n" "class Subject {\n" " notify(o: Observer): void { o.update(42); }\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, "Subject.notify", "update"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_real_event_emitter) { CBMFileResult *r = extract_ts( "class EventEmitter {\n" " on(event: string, cb: (data: number) => void): EventEmitter { return this; }\n" " emit(event: string, data: number): EventEmitter { return this; }\n" "}\n" "function go(em: EventEmitter) { em.on('x', d => d).emit('x', 1); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "on"), 0); ASSERT_GTE(require_resolved(r, ".go", "emit"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_real_singleton) { CBMFileResult *r = extract_ts("class Singleton {\n" " private static instance: Singleton;\n" " static getInstance(): Singleton { return new Singleton(); }\n" " run(): void {}\n" "}\n" "function go() { Singleton.getInstance().run(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "getInstance"), 0); ASSERT_GTE(require_resolved(r, ".go", "run"), 0); cbm_free_result(r); PASS(); } /* ── Edge cases ────────────────────────────────────────────────────────────── */ TEST(tslsp_readonly_field_method) { CBMFileResult *r = extract_ts("class Tool { fire(): void {} }\n" "class Box { readonly tool: Tool = new Tool(); }\n" "function go(b: Box) { b.tool.fire(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "fire"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_constructor_method_call) { CBMFileResult *r = extract_ts("class Foo { run(): void {} }\n" "function go() { new Foo().run(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "run"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_method_chained_through_field) { CBMFileResult *r = extract_ts("class Repo { all(): string[] { return []; } }\n" "class App { repo: Repo = new Repo(); }\n" "function go(app: App) { app.repo.all().map(s => s.toUpperCase()); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "all"), 0); ASSERT_GTE(require_resolved(r, ".go", "map"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_callback_returning_method_call) { CBMFileResult *r = extract_ts("class Box { run(): string { return ''; } }\n" "function go(b: Box) { [1, 2].forEach(_n => b.run()); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "run"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_this_in_arrow_method) { CBMFileResult *r = extract_ts("class Foo {\n" " a: number = 1;\n" " handler = () => { this.bar(); };\n" " bar(): void {}\n" "}\n"); ASSERT_NOT_NULL(r); /* Arrow methods bind `this` lexically — accept smoke pass for v1 */ cbm_free_result(r); PASS(); } TEST(tslsp_optional_field_method) { CBMFileResult *r = extract_ts("class Tool { fire(): void {} }\n" "class Box { tool?: Tool; }\n" "function go(b: Box) { if (b.tool) { b.tool.fire(); } }\n"); ASSERT_NOT_NULL(r); /* Optional field narrowing — accept partial success for v1 */ cbm_free_result(r); PASS(); } /* ── More crash-safety ─────────────────────────────────────────────────────── */ TEST(tslsp_nocrash_enum_basic) { CBMFileResult *r = extract_ts("enum Color { Red, Green, Blue }\n" "function go(c: Color) { console.log(c); }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_namespace) { CBMFileResult *r = extract_ts("namespace MyNS { export function inner(): void {} }\n" "function go() { MyNS.inner(); }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_async_generator) { CBMFileResult *r = extract_ts("async function* go() { yield 1; yield 2; }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_template_literal_expr) { CBMFileResult *r = extract_ts("function go(s: string) { console.log(`hello ${s.length}`); }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } TEST(tslsp_nocrash_complex_generic_constraint) { CBMFileResult *r = extract_ts("function id(x: T): T { return x; }\n" "function go(arr: number[]) { id(arr); }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } /* ── Discriminated unions / dispatch ───────────────────────────────────────── */ TEST(tslsp_discriminated_union_method) { CBMFileResult *r = extract_ts("interface Circle { kind: 'circle'; radius: number; render(): void; }\n" "interface Square { kind: 'square'; side: number; render(): void; }\n" "function go(s: Circle | Square) { s.render(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "render"), 0); cbm_free_result(r); PASS(); } /* ── Abstract class + concrete subclass ────────────────────────────────────── */ TEST(tslsp_abstract_concrete_dispatch) { CBMFileResult *r = extract_ts( "abstract class Shape { abstract area(): number; describe(): string { return ''; } }\n" "class Square extends Shape { area(): number { return 1; } }\n" "function go(s: Square) { s.area(); s.describe(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "area"), 0); ASSERT_GTE(require_resolved(r, ".go", "describe"), 0); cbm_free_result(r); PASS(); } /* ── Generics + inheritance ────────────────────────────────────────────────── */ TEST(tslsp_generic_class_method) { CBMFileResult *r = extract_ts( "class Container { items: T[] = []; add(x: T): void { this.items.push(x); } }\n" "function go(c: Container) { c.add(1); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "add"), 0); cbm_free_result(r); PASS(); } /* ── Static fields ─────────────────────────────────────────────────────────── */ TEST(tslsp_static_field_method) { CBMFileResult *r = extract_ts("class Tool { fire(): void {} }\n" "class Registry { static defaultTool: Tool = new Tool(); }\n" "function go() { Registry.defaultTool.fire(); }\n"); ASSERT_NOT_NULL(r); /* Static fields not yet first-class — accept smoke pass */ cbm_free_result(r); PASS(); } /* ── More JSDoc patterns ───────────────────────────────────────────────────── */ TEST(tslsp_jsdoc_param_after_text) { CBMFileResult *r = extract_js("class Box { run() {} }\n" "/**\n" " * Helpful description.\n" " * @param {Box} box - the box to use\n" " */\n" "function go(box) { box.run(); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "run"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_jsdoc_typedef_used_via_param) { /* JSDoc @typedef is declaration-merging-like; v1 may not honour it, * but parsing shouldn't crash. */ CBMFileResult *r = extract_js("/** @typedef {{ name: string }} User */\n" "/** @param {User} u */\n" "function go(u) { console.log(u.name); }\n"); ASSERT_NOT_NULL(r); cbm_free_result(r); PASS(); } /* ── Real-library: Promise.all chain ───────────────────────────────────────── */ TEST(tslsp_real_promise_all_then) { CBMFileResult *r = extract_ts("function go() { Promise.all([1, 2]).then(arr => arr.length); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "all"), 0); ASSERT_GTE(require_resolved(r, ".go", "then"), 0); cbm_free_result(r); PASS(); } /* ── Edge: optional method on union ────────────────────────────────────────── */ TEST(tslsp_method_on_typeof_class) { CBMFileResult *r = extract_ts("class Foo { run(): void {} }\n" "function go(F: typeof Foo) { const f = new F(); f.run(); }\n"); ASSERT_NOT_NULL(r); /* `typeof Foo` is a class constructor type — accept smoke pass for v1 */ cbm_free_result(r); PASS(); } /* ── Generic Promise chain ─────────────────────────────────────────────────── */ TEST(tslsp_promise_chain_via_local) { CBMFileResult *r = extract_ts("function load(): Promise { return Promise.resolve('hi'); }\n" "function go() {\n" " const p = load();\n" " p.then(s => s).catch(_e => '');\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "then"), 0); ASSERT_GTE(require_resolved(r, ".go", "catch"), 0); cbm_free_result(r); PASS(); } /* ── Object literal nested resolution ──────────────────────────────────────── */ TEST(tslsp_obj_literal_method_property) { CBMFileResult *r = extract_ts("class Logger { info(): void {} }\n" "function go() {\n" " const env = { logger: new Logger() };\n" " env.logger.info();\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "info"), 0); cbm_free_result(r); PASS(); } /* ── Contextual callback typing ─────────────────────────────────────────────── */ TEST(tslsp_ctx_array_map_callback) { CBMFileResult *r = extract_ts("class Box { run(): void {} }\n" "function go(boxes: Box[]) { boxes.map(b => b.run()); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "run"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_ctx_array_filter_callback) { CBMFileResult *r = extract_ts("class User { isActive(): boolean { return true; } }\n" "function go(users: User[]) { users.filter(u => u.isActive()); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "isActive"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_ctx_array_find_callback) { CBMFileResult *r = extract_ts("class Item { matches(q: string): boolean { return false; } }\n" "function go(items: Item[]) { items.find(i => i.matches('x')); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "matches"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_ctx_array_forEach_callback) { CBMFileResult *r = extract_ts("class Job { execute(): void {} }\n" "function go(jobs: Job[]) { jobs.forEach(j => j.execute()); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "execute"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_ctx_array_block_body_callback) { CBMFileResult *r = extract_ts("class Foo { ping(): void {} bar(): string { return ''; } }\n" "function go(xs: Foo[]) {\n" " xs.forEach(x => { x.ping(); x.bar(); });\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "ping"), 0); ASSERT_GTE(require_resolved(r, ".go", "bar"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_ctx_chain_filter_then_map) { CBMFileResult *r = extract_ts("class Row { active(): boolean { return true; } id(): number { return 0; } }\n" "function go(rows: Row[]) {\n" " rows.filter(r => r.active()).map(r => r.id());\n" "}\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "active"), 0); ASSERT_GTE(require_resolved(r, ".go", "id"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_ctx_user_defined_callback) { CBMFileResult *r = extract_ts("class Foo { run(): void {} }\n" "function withFoo(f: Foo, cb: (x: Foo) => void): void { cb(f); }\n" "function go(f: Foo) { withFoo(f, x => x.run()); }\n"); ASSERT_NOT_NULL(r); ASSERT_GTE(require_resolved(r, ".go", "run"), 0); cbm_free_result(r); PASS(); } /* ── Narrowing (real, not smoke) ───────────────────────────────────────────── */ TEST(tslsp_narrow_typeof_string_method) { CBMFileResult *r = extract_ts("function go(x: string | number) {\n" " if (typeof x === 'string') { x.toLowerCase(); }\n" "}\n"); ASSERT_NOT_NULL(r); /* Real narrowing — String.toLowerCase resolves through wrapper. */ ASSERT_GTE(require_resolved(r, ".go", "toLowerCase"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_narrow_instanceof_dispatch) { CBMFileResult *r = extract_ts("class A { ping(): void {} }\n" "class B { pong(): void {} }\n" "function go(x: A | B) {\n" " if (x instanceof A) { x.ping(); }\n" " else { x.pong(); }\n" "}\n"); ASSERT_NOT_NULL(r); /* v2: real flow-sensitive narrowing — `x instanceof A` narrows x to A in the * consequence, so x.ping() resolves via NAMED("test.main.A") directly. The * else branch falls back to union dispatch which finds pong on B. */ ASSERT_GTE(require_resolved(r, ".go", "ping"), 0); ASSERT_GTE(require_resolved(r, ".go", "pong"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_narrow_discriminated_kind) { CBMFileResult *r = extract_ts("interface Circle { kind: 'circle'; r(): number; }\n" "interface Square { kind: 'square'; s(): number; }\n" "function go(shape: Circle | Square) {\n" " if (shape.kind === 'circle') { shape.r(); }\n" " else { shape.s(); }\n" "}\n"); ASSERT_NOT_NULL(r); /* Both methods resolve — basic union dispatch covers both branches. */ ASSERT_GTE(require_resolved(r, ".go", "r"), 0); ASSERT_GTE(require_resolved(r, ".go", "s"), 0); cbm_free_result(r); PASS(); } /* ── React-like patterns ───────────────────────────────────────────────────── */ TEST(tslsp_react_useState_pattern) { CBMFileResult *r = extract_tsx( "function useState(initial: T): [T, (v: T) => void] { return [initial, _v => {}]; }\n" "function App(): JSX.Element {\n" " const [count, setCount] = useState(0);\n" " return
{count}
;\n" "}\n"); ASSERT_NOT_NULL(r); /* useState resolves; setCount is callable */ ASSERT_GTE(require_resolved(r, "App", "useState"), 0); cbm_free_result(r); PASS(); } TEST(tslsp_react_callback_in_jsx) { CBMFileResult *r = extract_tsx( "interface Service { fetch(): string; }\n" "function Button(props: { onClick: () => void }): JSX.Element { return