import * as tstl from "../../src"; import * as util from "../util"; import { formatCode } from "../util"; // TODO: Make some utils for testing other targets const expectUnpack: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).toMatch(/[^.]unpack\(/); const expectTableUnpack: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).toContain("table.unpack"); const expectLualibUnpack: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).toContain("__TS__Unpack"); const arrayLiteralCases = [ "1, 2, ...[3, 4, 5]", "...[1, 2], 3, 4, 5", "1, ...[[2]], 3", "...[1, 2, 3], 4, ...[5, 6]", "1, 2, ...[3, 4], ...[5, 6]", ]; describe.each(["function call", "array literal"] as const)("in %s", kind => { const factory = (code: string) => (kind === "function call" ? `((...args: any[]) => args)(${code})` : `[${code}]`); test.each(arrayLiteralCases)("of array literal (%p)", expression => { util.testExpression(factory(expression)).expectToMatchJsResult(); }); test.each(arrayLiteralCases)("of multi return call (%p)", expression => { util.testFunction` function tuple(...args: any[]) { return $multi(...args); } return ${factory(`...tuple(${expression})`)}; ` .withLanguageExtensions() .expectToMatchJsResult(); }); test("of multiple string literals", () => { util.testExpression(factory('..."spread", ..."string"')).expectToMatchJsResult(); }); test.each(["", "string", "string with spaces", "string 1 2 3"])("of string literal (%p)", str => { util.testExpression(factory(`...${formatCode(str)}`)).expectToMatchJsResult(); }); test("of iterable", () => { util.testFunction` const it = { i: -1, [Symbol.iterator]() { return this; }, next() { ++this.i; return { value: 2 ** this.i, done: this.i == 9, } } }; return ${factory("...it")}; `.expectToMatchJsResult(); }); }); describe("in function call", () => { util.testEachVersion( undefined, () => util.testFunction` function foo(a: number, b: number, ...rest: number[]) { return { a, b, rest } } const array = [0, 1, 2, 3] as const; return foo(...array); `, { [tstl.LuaTarget.Universal]: builder => builder.tap(expectLualibUnpack).expectToMatchJsResult(), [tstl.LuaTarget.LuaJIT]: builder => builder.tap(expectUnpack), [tstl.LuaTarget.Lua50]: builder => builder.tap(expectUnpack).expectToMatchJsResult(), [tstl.LuaTarget.Lua51]: builder => builder.tap(expectUnpack).expectToMatchJsResult(), [tstl.LuaTarget.Lua52]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), [tstl.LuaTarget.Lua53]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), [tstl.LuaTarget.Lua54]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), [tstl.LuaTarget.Lua55]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), [tstl.LuaTarget.Luau]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), } ); }); describe("in array literal", () => { util.testEachVersion(undefined, () => util.testExpression`[...[0, 1, 2]]`, { [tstl.LuaTarget.Universal]: builder => builder.tap(expectLualibUnpack).expectToMatchJsResult(), [tstl.LuaTarget.LuaJIT]: builder => builder.tap(expectUnpack), [tstl.LuaTarget.Lua50]: builder => builder.tap(expectUnpack).expectToMatchJsResult(), [tstl.LuaTarget.Lua51]: builder => builder.tap(expectUnpack).expectToMatchJsResult(), [tstl.LuaTarget.Lua52]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), [tstl.LuaTarget.Lua53]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), [tstl.LuaTarget.Lua54]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), [tstl.LuaTarget.Lua55]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), [tstl.LuaTarget.Luau]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), }); test("of array literal /w OmittedExpression", () => { util.testFunction` const array = [1, 2, ...[3], 5, , 6]; return { a: array[0], b: array[1], c: array[2], d: array[3] }; `.expectToMatchJsResult(); }); }); describe("in object literal", () => { test.each([ "{ x: false, ...{ x: true, y: true } }", "{ ...{ x: true, y: true } }", "{ ...{ x: true }, ...{ y: true, z: true } }", "{ ...{ x: false }, x: true }", "{ ...{ x: false }, x: false, ...{ x: true } }", ])("of object literal (%p)", expression => { util.testExpression(expression) .ignoreDiagnostics([2783]) // duplicate property in spread — intentional, testing spread override behavior .expectToMatchJsResult(); }); test.each([ "{ ...((false && { a: 1 }) as any) }", "{ ...((true && { a: 1 }) as any) }", "{ a: 1, ...((false && { b: 2 }) as any) }", "{ ...(null as any), ...(undefined as any) }", ])("of short-circuited operand (%p)", expression => { util.testExpression(expression).expectToMatchJsResult(); }); test("of object reference", () => { util.testFunction` const object = { x: 0, y: 1 }; const result = { ...object, z: 2 }; return { object, result }; `.expectToMatchJsResult(); }); test.each([ ["literal", "const object = { ...[0, 1, 2] };"], ["reference", "const array = [0, 1, 2]; const object = { ...array };"], ])("of array %p", (_name, expressionToCreateObject) => { util.testFunction` ${expressionToCreateObject} return { "0": object[0], "1": object[1], "2": object[2] }; `.expectToMatchJsResult(); }); }); describe("vararg spread optimization", () => { util.testEachVersion( "basic use", () => util.testFunction` function pick(...args: string[]) { return args[1]; } function test(...args: string[]) { return pick(...args); } return test("a", "b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "body-less arrow function", () => util.testFunction` function pick(...args: string[]) { return args[1]; } const test = (...args: string[]) => pick(...args); return test("a", "b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "if statement", () => util.testFunction` function pick(...args: string[]) { return args[1]; } function test(...args: string[]) { if (true) { return pick(...args); } } return test("a", "b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "loop statement", () => util.testFunction` function pick(...args: string[]) { return args[1]; } function test(...args: string[]) { do { return pick(...args); } while (false); } return test("a", "b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "block statement", () => util.testFunction` function pick(...args: string[]) { return args[1]; } function test(...args: string[]) { let result: string; { result = pick(...args); } return result; } return test("a", "b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "finally clause", () => util.testFunction` function pick(...args: string[]) { return args[1]; } function test(...args: string[]) { try { throw "foobar"; } catch { } finally { return pick(...args); } } return test("a" ,"b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); test("$multi", () => { util.testFunction` function multi(...args: string[]) { return $multi(...args); } function test(...args: string[]) { return multi(...args)[1]; } return test("a" ,"b", "c"); ` .withLanguageExtensions() .expectLuaToMatchSnapshot() .expectToEqual("b"); }); util.testEachVersion( "curry", () => util.testFunction` function test(fn: (...args: A) => void, ...args: A) { return fn(...args); } return test((arg: string) => arg, "foobar");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "curry with indirect type", () => util.testFunction` function test(obj: {fn: (...args: A) => void}, ...args: A) { const fn = obj.fn; return fn(...args); } return test({fn: (arg: string) => arg}, "foobar");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "function type declared inside scope", () => util.testFunction` function test(...args: A) { const fn: (...args: A) => A[0] = (...args) => args[0]; return fn(...args); } test("foobar");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "With cast", () => util.testFunction` function pick(...args: any[]) { return args[1]; } function testany>(...args: Parameters) { return pick(...(args as any[])); } return test<(...args: string[])=>void>("a", "b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); }); describe("vararg spread de-optimization", () => { util.testEachVersion( "array modification", () => util.testFunction` function pick(...args: string[]) { return args[1]; } function test(...args: string[]) { args[1] = "foobar"; return pick(...args); } return test("a", "b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "array modification in hoisted function", () => util.testFunction` function pick(...args: string[]) { return args[1]; } function test(...args: string[]) { hoisted(); const result = pick(...args); function hoisted() { args[1] = "foobar"; } return result; } return test("a", "b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "array modification in secondary hoisted function", () => util.testFunction` function pick(...args: string[]) { return args[1]; } function test(...args: string[]) { function triggersHoisted() { hoisted(); } triggersHoisted(); const result = pick(...args); function hoisted() { args[1] = "foobar"; } return result; } return test("a", "b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); }); describe("vararg spread in IIFE", () => { util.testEachVersion( "comma operator", () => util.testFunction` function dummy() { return "foobar"; } function pick(...args: string[]) { return args[1]; } function test(...args: string[]) { return (dummy(), pick(...args)); } return test("a", "b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "assignment expression", () => util.testFunction` function pick(...args: string[]) { return args[1]; } function test(...args: string[]) { let x: string; return (x = pick(...args)); } return test("a", "b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "destructured assignment expression", () => util.testFunction` function pick(...args: string[]) { return [args[1]]; } function test(...args: string[]) { let x: string; return ([x] = pick(...args)); } return test("a", "b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "property-access assignment expression", () => util.testFunction` function pick(...args: string[]) { return args[1]; } function test(...args: string[]) { let x: {val?: string} = {}; return (x.val = pick(...args)); } return test("a", "b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "binary compound assignment", () => util.testFunction` function pick(...args: number[]) { return args[1]; } function test(...args: number[]) { let x = 1; return x += pick(...args); } return test(1, 2, 3);`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "postfix unary compound assignment", () => util.testFunction` function pick(...args: number[]) { return args[1]; } function test(...args: number[]) { let x = [7, 8, 9]; return x[pick(...args)]++; } return test(1, 2, 3);`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "prefix unary compound assignment", () => util.testFunction` function pick(...args: number[]) { return args[1]; } function test(...args: number[]) { let x = [7, 8, 9]; return ++x[pick(...args)]; } return test(1, 2, 3);`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "try clause", () => util.testFunction` function pick(...args: string[]) { return args[1]; } function test(...args: string[]) { try { return pick(...args) } catch {} } return test("a" ,"b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "catch clause", () => util.testFunction` function pick(...args: string[]) { return args[1]; } function test(...args: string[]) { try { throw "foobar"; } catch { return pick(...args) } } return test("a" ,"b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "class expression", () => util.testFunction` function pick(...args: string[]) { return args[1]; } function test(...args: string[]) { const fooClass = class Foo { foo = pick(...args); }; return new fooClass().foo; } return test("a" ,"b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "self-referencing function expression", () => util.testFunction` function pick(...args: string[]) { return args[1]; } const test = function testName(...args: string[]) { return \`\${typeof testName}:\${pick(...args)}\`; } return test("a" ,"b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "method indirect access (access args)", () => util.testFunction` const obj = { $method: () => obj.arg, arg: "foobar" }; function getObj(...args: string[]) { obj.arg = args[1]; return obj; } function test(...args: string[]) { return getObj(...args).$method(); } return test("a" ,"b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "method indirect access (method args)", () => util.testFunction` const obj = { $pick: (...args: string[]) => args[1] }; function getObj() { return obj; } function test(...args: string[]) { return getObj().$pick(...args); } return test("a" ,"b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); util.testEachVersion( "tagged template method indirect access", () => util.testFunction` const obj = { $tag: (t: TemplateStringsArray, ...args: string[]) => args[1] }; function getObj() { return obj; } function pick(...args: string[]): string { return args[1]; } function test(...args: string[]) { return getObj().$tag\`FOO\${pick(...args)}BAR\`; } return test("a" ,"b", "c");`, util.expectEachVersionExceptJit(builder => builder.expectToMatchJsResult()) ); }); // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1244 test.each(["pairs", "ipairs"])("can spread %s (#1244)", func => { util.testFunction` const arr = ["a", "b", "c"]; return [...${func}(arr)]; ` .withLanguageExtensions() .setTsHeader( ` declare function ipairs(this: void, t: T): LuaIterable]>>; declare function pairs(this: void, t: T): LuaIterable]>>; ` ) .expectToEqual([ [1, "a"], [2, "b"], [3, "c"], ]); }); // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1244 test.each(["LuaTable", "LuaMap"])("can spread %s with pairs (#1244)", type => { const result: Array<[string, string]> = util.testFunction` const tbl = new ${type}(); tbl.set("foo", "bar"); tbl.set("fizz", "buzz"); return [...pairs(tbl)]; ` .withLanguageExtensions() .setTsHeader( "declare function pairs(this: void, t: T): LuaIterable]>>;" ) .getLuaExecutionResult(); // We don't know the order so match like this expect(result).toHaveLength(2); expect(result).toContainEqual(["foo", "bar"]); expect(result).toContainEqual(["fizz", "buzz"]); }); // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1384 test.each(["LuaTable", "LuaMap"])("can spread %s (#1384)", type => { const result: Array<[string, string]> = util.testFunction` const tbl = new ${type}(); tbl.set("foo", "bar"); tbl.set("fizz", "buzz"); return [...tbl]; ` .withLanguageExtensions() .getLuaExecutionResult(); // We don't know the order so match like this expect(result).toHaveLength(2); expect(result).toContainEqual(["foo", "bar"]); expect(result).toContainEqual(["fizz", "buzz"]); }); // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1384 test("can spread LuaSet (#1384)", () => { const result: Array<[string, string]> = util.testFunction` const tbl = new LuaSet(); tbl.add("foo"); tbl.add("bar"); return [...tbl]; ` .withLanguageExtensions() .getLuaExecutionResult(); // We don't know the order so match like this expect(result).toHaveLength(2); expect(result).toContain("foo"); expect(result).toContain("bar"); }); // https://github.com/TypeScriptToLua/TypeScriptToLua/issues/1426 test.each([true, false])("spread a decorator or array union type (#1426)", choice => { util.testFunction` function* things() { yield "a"; yield "b" } const c: boolean = ${util.formatCode(choice)}; return [...(c ? things() : ["c", "d"])]; `.expectToMatchJsResult(); });