diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index 819c5f2df..a8a5cdd35 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -4552,7 +4552,7 @@ export class LuaTransformer { return tstl.createNumericLiteral(Math[name], identifier); default: - throw TSTLErrors.UnsupportedProperty("math", name, identifier); + throw TSTLErrors.UnsupportedProperty("Math", name, identifier); } } @@ -4622,7 +4622,7 @@ export class LuaTransformer { } default: - throw TSTLErrors.UnsupportedProperty("math", expressionName, expression); + throw TSTLErrors.UnsupportedProperty("Math", expressionName, expression); } } @@ -4872,11 +4872,7 @@ export class LuaTransformer { tstl.createStringLiteral("char") ); default: - throw TSTLErrors.UnsupportedForTarget( - `string property ${identifierString}`, - this.luaTarget, - identifier - ); + throw TSTLErrors.UnsupportedProperty("String", identifierString, identifier); } } @@ -4898,7 +4894,7 @@ export class LuaTransformer { case "values": return this.transformLuaLibFunction(LuaLibFeature.ObjectValues, expression, ...parameters); default: - throw TSTLErrors.UnsupportedForTarget(`object property ${methodName}`, this.luaTarget, expression); + throw TSTLErrors.UnsupportedProperty("Object", methodName, expression); } } @@ -4969,7 +4965,7 @@ export class LuaTransformer { ); return tstl.createCallExpression(tstl.createIdentifier("print"), [debugTracebackCall]); default: - throw TSTLErrors.UnsupportedForTarget(`console property ${methodName}`, this.luaTarget, expression); + throw TSTLErrors.UnsupportedProperty("console", methodName, expression); } } @@ -4992,7 +4988,7 @@ export class LuaTransformer { const functionIdentifier = tstl.createIdentifier(`__TS__SymbolRegistry${upperMethodName}`); return tstl.createCallExpression(functionIdentifier, parameters, expression); default: - throw TSTLErrors.UnsupportedForTarget(`symbol property ${methodName}`, this.luaTarget, expression); + throw TSTLErrors.UnsupportedProperty("Symbol", methodName, expression); } } @@ -5008,7 +5004,7 @@ export class LuaTransformer { case "isFinite": return this.transformLuaLibFunction(LuaLibFeature.NumberIsFinite, expression, ...parameters); default: - throw TSTLErrors.UnsupportedForTarget(`number property ${methodName}`, this.luaTarget, expression); + throw TSTLErrors.UnsupportedProperty("Number", methodName, expression); } } @@ -5186,11 +5182,14 @@ export class LuaTransformer { } public transformAssertionExpression(expression: ts.AssertionExpression): ExpressionVisitResult { - this.validateFunctionAssignment( - expression, - this.checker.getTypeAtLocation(expression.expression), - this.checker.getTypeAtLocation(expression.type) - ); + if (!ts.isConstTypeReference(expression.type)) { + this.validateFunctionAssignment( + expression, + this.checker.getTypeAtLocation(expression.expression), + this.checker.getTypeAtLocation(expression.type) + ); + } + return this.transformExpression(expression.expression); } diff --git a/test/json.lua b/test/json.lua index 6dafe9fe4..e41c3798a 100644 --- a/test/json.lua +++ b/test/json.lua @@ -112,11 +112,15 @@ end local function encode_number(val) - -- Check for NaN, -inf and inf - if val ~= val or val <= -math.huge or val >= math.huge then - error("unexpected number value '" .. tostring(val) .. "'") + if val ~= val then + return "NaN" + elseif val == math.huge then + return "Infinity" + elseif val == -math.huge then + return "-Infinity" + else + return string.format("%.17g", val) end - return string.format("%.14g", val) end @@ -139,6 +143,7 @@ encode = function(val, stack) end +-- TODO: Since it supports NaN and Infinity it is considered a superset of JSON, so it probably should be renamed function JSONStringify(val) return ( encode(val) ) end diff --git a/test/legacy-utils.ts b/test/legacy-utils.ts new file mode 100644 index 000000000..4b9815d17 --- /dev/null +++ b/test/legacy-utils.ts @@ -0,0 +1,162 @@ +import { lauxlib, lua, lualib, to_jsstring, to_luastring } from "fengari"; +import * as fs from "fs"; +import * as path from "path"; +import * as ts from "typescript"; +import * as tstl from "../src"; +import * as tsHelper from "../src/TSHelper"; + +export function transpileString( + str: string | { [filename: string]: string }, + options: tstl.CompilerOptions = {}, + ignoreDiagnostics = true +): string { + const { diagnostics, file } = transpileStringResult(str, options); + expect(file.lua).toBeDefined(); + + const errors = diagnostics.filter(d => !ignoreDiagnostics || d.source === "typescript-to-lua"); + expect(errors).not.toHaveDiagnostics(); + + return file.lua!.trim(); +} + +export function transpileStringsAsProject( + input: Record, + options: tstl.CompilerOptions = {} +): tstl.TranspileResult { + const optionsWithDefaults = { + luaTarget: tstl.LuaTarget.Lua53, + noHeader: true, + skipLibCheck: true, + target: ts.ScriptTarget.ESNext, + lib: ["lib.esnext.d.ts"], + experimentalDecorators: true, + ...options, + }; + + return tstl.transpileVirtualProject(input, optionsWithDefaults); +} + +export function transpileStringResult( + input: string | Record, + options: tstl.CompilerOptions = {} +): Required { + const { diagnostics, transpiledFiles } = transpileStringsAsProject( + typeof input === "string" ? { "main.ts": input } : input, + options + ); + + const file = transpiledFiles.find(({ fileName }) => /\bmain\.[a-z]+$/.test(fileName)); + if (file === undefined) { + throw new Error('Program should have a file named "main"'); + } + + return { diagnostics, file }; +} + +const lualibContent = fs.readFileSync(path.resolve(__dirname, "../dist/lualib/lualib_bundle.lua"), "utf8"); +const minimalTestLib = fs.readFileSync(path.join(__dirname, "json.lua"), "utf8") + "\n"; +export function executeLua(luaStr: string, withLib = true): any { + luaStr = luaStr.replace(/require\("lualib_bundle"\)/g, lualibContent); + if (withLib) { + luaStr = minimalTestLib + luaStr; + } + + const L = lauxlib.luaL_newstate(); + lualib.luaL_openlibs(L); + const status = lauxlib.luaL_dostring(L, to_luastring(luaStr)); + + if (status === lua.LUA_OK) { + // Read the return value from stack depending on its type. + if (lua.lua_isboolean(L, -1)) { + return lua.lua_toboolean(L, -1); + } else if (lua.lua_isnil(L, -1)) { + return undefined; + } else if (lua.lua_isnumber(L, -1)) { + return lua.lua_tonumber(L, -1); + } else if (lua.lua_isstring(L, -1)) { + return lua.lua_tojsstring(L, -1); + } else { + throw new Error("Unsupported lua return type: " + to_jsstring(lua.lua_typename(L, lua.lua_type(L, -1)))); + } + } else { + // If the lua VM did not terminate with status code LUA_OK an error occurred. + // Throw a JS error with the message, retrieved by reading a string from the stack. + + // Filter control characters out of string which are in there because ???? + throw new Error("LUA ERROR: " + to_jsstring(lua.lua_tostring(L, -1).filter(c => c >= 20))); + } +} + +export function transpileAndExecute( + tsStr: string, + compilerOptions?: tstl.CompilerOptions, + luaHeader?: string, + tsHeader?: string +): any { + const wrappedTsString = `${tsHeader ? tsHeader : ""} + declare function JSONStringify(this: void, p: any): string; + function __runTest(this: void): any {${tsStr}}`; + + const lua = `${luaHeader ? luaHeader : ""} + ${transpileString(wrappedTsString, compilerOptions, false)} + return __runTest();`; + + return executeLua(lua); +} + +export function transpileAndExecuteProjectReturningMainExport( + typeScriptFiles: Record, + exportName: string, + options: tstl.CompilerOptions = {} +): [any, string] { + const mainFile = Object.keys(typeScriptFiles).find(typeScriptFileName => typeScriptFileName === "main.ts"); + if (!mainFile) { + throw new Error("An entry point file needs to be specified. This should be called main.ts"); + } + + const joinedTranspiledFiles = Object.keys(typeScriptFiles) + .filter(typeScriptFileName => typeScriptFileName !== "main.ts") + .map(typeScriptFileName => { + const modulePath = tsHelper.getExportPath(typeScriptFileName, options); + const tsCode = typeScriptFiles[typeScriptFileName]; + const luaCode = transpileString(tsCode, options); + return `package.preload["${modulePath}"] = function() + ${luaCode} + end`; + }) + .join("\n"); + + const luaCode = `return (function() + ${joinedTranspiledFiles} + ${transpileString(typeScriptFiles[mainFile])} + end)().${exportName}`; + + try { + return [executeLua(luaCode), luaCode]; + } catch (err) { + throw new Error(` + Encountered an error when executing the following Lua code: + + ${luaCode} + + ${err} + `); + } +} + +export function transpileExecuteAndReturnExport( + tsStr: string, + returnExport: string, + compilerOptions?: tstl.CompilerOptions, + luaHeader?: string +): any { + const wrappedTsString = `declare function JSONStringify(this: void, p: any): string; + ${tsStr}`; + + const lua = `return (function() + ${luaHeader ? luaHeader : ""} + ${transpileString(wrappedTsString, compilerOptions, false)} + end)().${returnExport}`; + + return executeLua(lua); +} diff --git a/test/translation/__snapshots__/transformation.spec.ts.snap b/test/translation/__snapshots__/transformation.spec.ts.snap index 381a7b987..044e1bf82 100644 --- a/test/translation/__snapshots__/transformation.spec.ts.snap +++ b/test/translation/__snapshots__/transformation.spec.ts.snap @@ -1,7 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Transformation (callNamespace) 1`] = `"Namespace:myFunction()"`; - exports[`Transformation (characterEscapeSequence) 1`] = ` "local quoteInDoubleQuotes = \\"\\\\' \\\\' \\\\'\\" local quoteInTemplateString = \\"\\\\' \\\\' \\\\'\\" @@ -57,148 +55,6 @@ function ClassB.prototype.____constructor(self) end" `; -exports[`Transformation (continue) 1`] = ` -"do - local i = 0 - while i < 10 do - do - if i < 5 then - goto __continue1 - end - end - ::__continue1:: - i = i + 1 - end -end" -`; - -exports[`Transformation (continueConcurrent) 1`] = ` -"do - local i = 0 - while i < 10 do - do - if i < 5 then - goto __continue1 - end - if i == 7 then - goto __continue1 - end - end - ::__continue1:: - i = i + 1 - end -end" -`; - -exports[`Transformation (continueNested) 1`] = ` -"do - local i = 0 - while i < 5 do - do - if i % 2 == 0 then - goto __continue1 - end - do - local j = 0 - while j < 2 do - do - if j == 1 then - goto __continue3 - end - end - ::__continue3:: - j = j + 1 - end - end - end - ::__continue1:: - i = i + 1 - end -end" -`; - -exports[`Transformation (continueNestedConcurrent) 1`] = ` -"do - local i = 0 - while i < 5 do - do - if i % 2 == 0 then - goto __continue1 - end - do - local j = 0 - while j < 2 do - do - if j == 1 then - goto __continue3 - end - end - ::__continue3:: - j = j + 1 - end - end - if i == 4 then - goto __continue1 - end - end - ::__continue1:: - i = i + 1 - end -end" -`; - -exports[`Transformation (do) 1`] = ` -"local e = 10 -repeat - do - e = e - 1 - end -until not (e > 0)" -`; - -exports[`Transformation (enum) 1`] = ` -"TestEnum = {} -TestEnum.val1 = 0 -TestEnum[0] = \\"val1\\" -TestEnum.val2 = 2 -TestEnum[2] = \\"val2\\" -TestEnum.val3 = 3 -TestEnum[3] = \\"val3\\"" -`; - -exports[`Transformation (enumHeterogeneous) 1`] = ` -"TestEnum = {} -TestEnum.val1 = 0 -TestEnum[0] = \\"val1\\" -TestEnum.val2 = 3 -TestEnum[3] = \\"val2\\" -TestEnum.val3 = \\"baz\\" -TestEnum.baz = \\"val3\\"" -`; - -exports[`Transformation (enumMembersOnly) 1`] = ` -"val1 = 0 -val2 = 2 -val3 = 3 -val4 = \\"bye\\" -local a = val1" -`; - -exports[`Transformation (enumString) 1`] = ` -"TestEnum = {} -TestEnum.val1 = \\"foo\\" -TestEnum.foo = \\"val1\\" -TestEnum.val2 = \\"bar\\" -TestEnum.bar = \\"val2\\" -TestEnum.val3 = \\"baz\\" -TestEnum.baz = \\"val3\\"" -`; - -exports[`Transformation (exportEquals) 1`] = ` -"local ____exports = true -return ____exports" -`; - exports[`Transformation (exportStatement) 1`] = ` "local ____exports = {} local xyz = 4 @@ -225,77 +81,6 @@ end return ____exports" `; -exports[`Transformation (for) 1`] = ` -"do - local i = 1 - while i <= 100 do - i = i + 1 - end -end" -`; - -exports[`Transformation (forIn) 1`] = ` -"for i in pairs({a = 1, b = 2, c = 3, d = 4}) do -end" -`; - -exports[`Transformation (forOf) 1`] = ` -"for ____, i in ipairs({1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) do -end" -`; - -exports[`Transformation (functionRestArguments) 1`] = ` -"function varargsFunction(self, a, ...) - local b = ({...}) - local c = b -end" -`; - -exports[`Transformation (getSetAccessors) 1`] = ` -"require(\\"lualib_bundle\\"); -MyClass = {} -MyClass.name = \\"MyClass\\" -MyClass.__index = MyClass -MyClass.prototype = {} -MyClass.prototype.____getters = {} -MyClass.prototype.__index = __TS__Index(MyClass.prototype) -MyClass.prototype.____setters = {} -MyClass.prototype.__newindex = __TS__NewIndex(MyClass.prototype) -MyClass.prototype.constructor = MyClass -function MyClass.new(...) - local self = setmetatable({}, MyClass.prototype) - self:____constructor(...) - return self -end -function MyClass.prototype.____constructor(self) -end -function MyClass.prototype.____getters.field(self) - return self._field + 4 -end -function MyClass.prototype.____setters.field(self, v) - self._field = v * 2 -end -local instance = MyClass.new() -instance.field = 4 -local b = instance.field -local c = (4 + instance.field) * 3" -`; - -exports[`Transformation (interfaceIndex) 1`] = ` -"local a = {} -a.abc = \\"def\\"" -`; - -exports[`Transformation (luaTable) 1`] = ` -"tbl = {} -tbl.value = 5 -local value = tbl.value -local tblLength = #tbl -itbl.value = 5 -local ivalue = itbl.value -local ilength = #tbl" -`; - exports[`Transformation (methodRestArguments) 1`] = ` "MyClass = {} MyClass.name = \\"MyClass\\" @@ -433,20 +218,6 @@ ____exports.TestSpace = {} return ____exports" `; -exports[`Transformation (modulesNamespaceExportEnum) 1`] = ` -"local ____exports = {} -____exports.test = {} -local test = ____exports.test -do - test.TestEnum = {} - test.TestEnum.foo = \\"foo\\" - test.TestEnum.foo = \\"foo\\" - test.TestEnum.bar = \\"bar\\" - test.TestEnum.bar = \\"bar\\" -end -return ____exports" -`; - exports[`Transformation (modulesNamespaceNestedWithMemberExport) 1`] = ` "local ____exports = {} ____exports.TestSpace = {} @@ -493,64 +264,6 @@ return ____exports" exports[`Transformation (modulesVariableNoExport) 1`] = `"local foo = \\"bar\\""`; -exports[`Transformation (namespace) 1`] = ` -"myNamespace = myNamespace or {} -do - local function nsMember(self) - end -end" -`; - -exports[`Transformation (namespaceMerge) 1`] = ` -"MergedClass = {} -MergedClass.name = \\"MergedClass\\" -MergedClass.__index = MergedClass -MergedClass.prototype = {} -MergedClass.prototype.__index = MergedClass.prototype -MergedClass.prototype.constructor = MergedClass -function MergedClass.new(...) - local self = setmetatable({}, MergedClass.prototype) - self:____constructor(...) - return self -end -function MergedClass.prototype.____constructor(self) - self.propertyFunc = function() - end -end -function MergedClass.staticMethodA(self) -end -function MergedClass.staticMethodB(self) - self:staticMethodA() -end -function MergedClass.prototype.methodA(self) -end -function MergedClass.prototype.methodB(self) - self:methodA() - self:propertyFunc() -end -MergedClass = MergedClass or {} -do - function MergedClass.namespaceFunc(self) - end -end -local mergedClass = MergedClass.new() -mergedClass:methodB() -mergedClass:propertyFunc() -MergedClass:staticMethodB() -MergedClass:namespaceFunc()" -`; - -exports[`Transformation (namespaceNested) 1`] = ` -"myNamespace = myNamespace or {} -do - local myNestedNamespace = {} - do - local function nsMember(self) - end - end -end" -`; - exports[`Transformation (namespacePhantom) 1`] = ` "function nsMember(self) end" @@ -562,124 +275,7 @@ exports[`Transformation (returnDefault) 1`] = ` end" `; -exports[`Transformation (shorthandPropertyAssignment) 1`] = ` -"local f -f = function(____, x) return ({x = x}) end" -`; - -exports[`Transformation (spreadAssignment) 1`] = ` -"require(\\"lualib_bundle\\"); -local xy = __TS__ObjectAssign({x = 0, y = 1}) -local xyz = __TS__ObjectAssign({x = 0, y = 1}, {z = 2}) -local xyz2 = __TS__ObjectAssign({z = 2}, {x = 0, y = 1})" -`; - -exports[`Transformation (tryCatch) 1`] = ` -"do - local ____try, er = pcall( - function() - local a = 42 - end - ) - if not ____try then - local b = \\"fail\\" - end -end" -`; - -exports[`Transformation (tryCatchFinally) 1`] = ` -"do - local ____try, er = pcall( - function() - local a = 42 - end - ) - if not ____try then - local b = \\"fail\\" - end - do - local c = \\"finally\\" - end -end" -`; - -exports[`Transformation (tryFinally) 1`] = ` -"do - pcall( - function() - local a = 42 - end - ) - do - local b = \\"finally\\" - end -end" -`; - -exports[`Transformation (tupleReturn) 1`] = ` -"function tupleReturn(self) - return 0, \\"foobar\\" -end -tupleReturn(_G) -noTupleReturn(_G) -local a, b = tupleReturn(_G) -local c, d = table.unpack( - noTupleReturn(_G) -) -a, b = tupleReturn(_G) -c, d = table.unpack( - noTupleReturn(_G) -) -local e = ({ - tupleReturn(_G) -}) -local f = noTupleReturn(_G) -e = ({ - tupleReturn(_G) -}) -f = noTupleReturn(_G) -foo( - _G, - ({ - tupleReturn(_G) - }) -) -foo( - _G, - noTupleReturn(_G) -) -function tupleReturnFromVar(self) - local r = {1, \\"baz\\"} - return table.unpack(r) -end -function tupleReturnForward(self) - return tupleReturn(_G) -end -function tupleNoForward(self) - return ({ - tupleReturn(_G) - }) -end -function tupleReturnUnpack(self) - return table.unpack( - tupleNoForward(_G) - ) -end" -`; - -exports[`Transformation (typeAssert) 1`] = ` -"local test1 = 10 -local test2 = 10" -`; - exports[`Transformation (unusedDefaultWithNamespaceImport) 1`] = ` "local x = require(\\"module\\") local ____ = x" `; - -exports[`Transformation (while) 1`] = ` -"local d = 10 -while d > 0 do - d = d - 1 -end" -`; diff --git a/test/translation/transformation.spec.ts b/test/translation/transformation.spec.ts index d7467a78f..7a5088fcb 100644 --- a/test/translation/transformation.spec.ts +++ b/test/translation/transformation.spec.ts @@ -11,6 +11,8 @@ const fixtures = fs .map(f => [path.parse(f).name, fs.readFileSync(path.join(fixturesPath, f), "utf8")]); test.each(fixtures)("Transformation (%s)", (_name, content) => { - const result = util.transpileString(content, { luaLibImport: tstl.LuaLibImportKind.Require }); - expect(result).toMatchSnapshot(); + util.testModule(content) + .setOptions({ luaLibImport: tstl.LuaLibImportKind.Require }) + .disableSemanticCheck() + .expectLuaToMatchSnapshot(); }); diff --git a/test/translation/transformation/callNamespace.ts b/test/translation/transformation/callNamespace.ts deleted file mode 100644 index 43ed32d6d..000000000 --- a/test/translation/transformation/callNamespace.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare namespace Namespace { - function myFunction(); -} -Namespace.myFunction(); diff --git a/test/translation/transformation/continue.ts b/test/translation/transformation/continue.ts deleted file mode 100644 index 64830718d..000000000 --- a/test/translation/transformation/continue.ts +++ /dev/null @@ -1,5 +0,0 @@ -for (let i = 0; i < 10; i++) { - if (i < 5) { - continue; - } -} diff --git a/test/translation/transformation/continueConcurrent.ts b/test/translation/transformation/continueConcurrent.ts deleted file mode 100644 index ef1b20c68..000000000 --- a/test/translation/transformation/continueConcurrent.ts +++ /dev/null @@ -1,9 +0,0 @@ -for (let i = 0; i < 10; i++) { - if (i < 5) { - continue; - } - - if (i === 7) { - continue; - } -} diff --git a/test/translation/transformation/continueNested.ts b/test/translation/transformation/continueNested.ts deleted file mode 100644 index 563b7d0f1..000000000 --- a/test/translation/transformation/continueNested.ts +++ /dev/null @@ -1,11 +0,0 @@ -for (let i = 0; i < 5; i++) { - if (i % 2 === 0) { - continue; - } - - for (let j = 0; j < 2; j++) { - if (j === 1) { - continue; - } - } -} diff --git a/test/translation/transformation/continueNestedConcurrent.ts b/test/translation/transformation/continueNestedConcurrent.ts deleted file mode 100644 index fe0bd0966..000000000 --- a/test/translation/transformation/continueNestedConcurrent.ts +++ /dev/null @@ -1,15 +0,0 @@ -for (let i = 0; i < 5; i++) { - if (i % 2 === 0) { - continue; - } - - for (let j = 0; j < 2; j++) { - if (j === 1) { - continue; - } - } - - if (i === 4) { - continue; - } -} diff --git a/test/translation/transformation/do.ts b/test/translation/transformation/do.ts deleted file mode 100644 index c17e3d1aa..000000000 --- a/test/translation/transformation/do.ts +++ /dev/null @@ -1,4 +0,0 @@ -let e = 10; -do { - e--; -} while (e > 0); diff --git a/test/translation/transformation/enum.ts b/test/translation/transformation/enum.ts deleted file mode 100644 index 3d48fbcfb..000000000 --- a/test/translation/transformation/enum.ts +++ /dev/null @@ -1,5 +0,0 @@ -enum TestEnum { - val1 = 0, - val2 = 2, - val3, -} diff --git a/test/translation/transformation/enumHeterogeneous.ts b/test/translation/transformation/enumHeterogeneous.ts deleted file mode 100644 index d4e69f10b..000000000 --- a/test/translation/transformation/enumHeterogeneous.ts +++ /dev/null @@ -1,5 +0,0 @@ -enum TestEnum { - val1, - val2 = 3, - val3 = "baz", -} diff --git a/test/translation/transformation/enumMembersOnly.ts b/test/translation/transformation/enumMembersOnly.ts deleted file mode 100644 index 9d384dc6c..000000000 --- a/test/translation/transformation/enumMembersOnly.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** @compileMembersOnly */ -enum TestEnum { - val1 = 0, - val2 = 2, - val3, - val4 = "bye", -} - -const a = TestEnum.val1; diff --git a/test/translation/transformation/enumString.ts b/test/translation/transformation/enumString.ts deleted file mode 100644 index 3d7d29747..000000000 --- a/test/translation/transformation/enumString.ts +++ /dev/null @@ -1,5 +0,0 @@ -enum TestEnum { - val1 = "foo", - val2 = "bar", - val3 = "baz", -} diff --git a/test/translation/transformation/exportEquals.ts b/test/translation/transformation/exportEquals.ts deleted file mode 100644 index ba27c6482..000000000 --- a/test/translation/transformation/exportEquals.ts +++ /dev/null @@ -1 +0,0 @@ -export = true; diff --git a/test/translation/transformation/for.ts b/test/translation/transformation/for.ts deleted file mode 100644 index a9dd48620..000000000 --- a/test/translation/transformation/for.ts +++ /dev/null @@ -1 +0,0 @@ -for (let i = 1; i <= 100; i++) {} diff --git a/test/translation/transformation/forIn.ts b/test/translation/transformation/forIn.ts deleted file mode 100644 index 8d1bff356..000000000 --- a/test/translation/transformation/forIn.ts +++ /dev/null @@ -1,7 +0,0 @@ -for (let i in { - a: 1, - b: 2, - c: 3, - d: 4, -}) { -} diff --git a/test/translation/transformation/forOf.ts b/test/translation/transformation/forOf.ts deleted file mode 100644 index c017d5c0c..000000000 --- a/test/translation/transformation/forOf.ts +++ /dev/null @@ -1,2 +0,0 @@ -for (let i of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) { -} diff --git a/test/translation/transformation/functionRestArguments.ts b/test/translation/transformation/functionRestArguments.ts deleted file mode 100644 index 63649f09b..000000000 --- a/test/translation/transformation/functionRestArguments.ts +++ /dev/null @@ -1,3 +0,0 @@ -function varargsFunction(a: string, ...b: string[]): void { - const c = b; -} diff --git a/test/translation/transformation/getSetAccessors.ts b/test/translation/transformation/getSetAccessors.ts deleted file mode 100644 index d61f592a2..000000000 --- a/test/translation/transformation/getSetAccessors.ts +++ /dev/null @@ -1,14 +0,0 @@ -class MyClass { - private _field: number; - public get field(): number { - return this._field + 4; - } - public set field(v: number) { - this._field = v * 2; - } -} - -let instance = new MyClass(); -instance.field = 4; -const b = instance.field; -const c = (4 + instance.field) * 3; diff --git a/test/translation/transformation/interfaceIndex.ts b/test/translation/transformation/interfaceIndex.ts deleted file mode 100644 index f209e153e..000000000 --- a/test/translation/transformation/interfaceIndex.ts +++ /dev/null @@ -1,6 +0,0 @@ -declare interface Dictionary { - [index: string]: T; -} - -let a: Dictionary = {}; -a["abc"] = "def"; diff --git a/test/translation/transformation/luaTable.ts b/test/translation/transformation/luaTable.ts deleted file mode 100644 index 06bd03cd2..000000000 --- a/test/translation/transformation/luaTable.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** @luaTable */ -declare class Table { - public readonly length: number; - public set(key: K, value: V): void; - public get(key: K): V; -} -declare let tbl: Table; -tbl = new Table(); -tbl.set("value", 5); -const value = tbl.get("value"); -const tblLength = tbl.length; - -/** @luaTable */ -declare interface InterfaceTable { - readonly length: number; - set(key: K, value: V): void; - get(key: K): V; -} - -declare const itbl: InterfaceTable; -itbl.set("value", 5); -const ivalue = itbl.get("value"); -const ilength = tbl.length; diff --git a/test/translation/transformation/modulesNamespaceExportEnum.ts b/test/translation/transformation/modulesNamespaceExportEnum.ts deleted file mode 100644 index 0c85b6872..000000000 --- a/test/translation/transformation/modulesNamespaceExportEnum.ts +++ /dev/null @@ -1,6 +0,0 @@ -export namespace test { - export enum TestEnum { - foo = "foo", - bar = "bar", - } -} diff --git a/test/translation/transformation/namespace.ts b/test/translation/transformation/namespace.ts deleted file mode 100644 index 24f4a78e0..000000000 --- a/test/translation/transformation/namespace.ts +++ /dev/null @@ -1,3 +0,0 @@ -namespace myNamespace { - function nsMember() {} -} diff --git a/test/translation/transformation/namespaceMerge.ts b/test/translation/transformation/namespaceMerge.ts deleted file mode 100644 index b6905def9..000000000 --- a/test/translation/transformation/namespaceMerge.ts +++ /dev/null @@ -1,24 +0,0 @@ -class MergedClass { - public static staticMethodA(): void {} - public static staticMethodB(): void { - this.staticMethodA(); - } - - public propertyFunc: () => void = () => {}; - - public methodA(): void {} - public methodB(): void { - this.methodA(); - this.propertyFunc(); - } -} - -namespace MergedClass { - export function namespaceFunc(): void {} -} - -const mergedClass = new MergedClass(); -mergedClass.methodB(); -mergedClass.propertyFunc(); -MergedClass.staticMethodB(); -MergedClass.namespaceFunc(); diff --git a/test/translation/transformation/namespaceNested.ts b/test/translation/transformation/namespaceNested.ts deleted file mode 100644 index 85869547f..000000000 --- a/test/translation/transformation/namespaceNested.ts +++ /dev/null @@ -1,5 +0,0 @@ -namespace myNamespace { - namespace myNestedNamespace { - function nsMember() {} - } -} diff --git a/test/translation/transformation/shorthandPropertyAssignment.ts b/test/translation/transformation/shorthandPropertyAssignment.ts deleted file mode 100644 index 4b2aa7e82..000000000 --- a/test/translation/transformation/shorthandPropertyAssignment.ts +++ /dev/null @@ -1 +0,0 @@ -const f = x => ({ x }); diff --git a/test/translation/transformation/spreadAssignment.ts b/test/translation/transformation/spreadAssignment.ts deleted file mode 100644 index 0ad87be82..000000000 --- a/test/translation/transformation/spreadAssignment.ts +++ /dev/null @@ -1,3 +0,0 @@ -const xy = { ...{ x: 0, y: 1 } }; -const xyz = { ...{ x: 0, y: 1 }, z: 2 }; -const xyz2 = { z: 2, ...{ x: 0, y: 1 } }; diff --git a/test/translation/transformation/tryCatch.ts b/test/translation/transformation/tryCatch.ts deleted file mode 100644 index d5e2f1310..000000000 --- a/test/translation/transformation/tryCatch.ts +++ /dev/null @@ -1,5 +0,0 @@ -try { - let a = 42; -} catch (er) { - let b = "fail"; -} diff --git a/test/translation/transformation/tryCatchFinally.ts b/test/translation/transformation/tryCatchFinally.ts deleted file mode 100644 index dfbd57c7f..000000000 --- a/test/translation/transformation/tryCatchFinally.ts +++ /dev/null @@ -1,7 +0,0 @@ -try { - let a = 42; -} catch (er) { - let b = "fail"; -} finally { - let c = "finally"; -} diff --git a/test/translation/transformation/tryFinally.ts b/test/translation/transformation/tryFinally.ts deleted file mode 100644 index d33f4b95a..000000000 --- a/test/translation/transformation/tryFinally.ts +++ /dev/null @@ -1,5 +0,0 @@ -try { - let a = 42; -} finally { - let b = "finally"; -} diff --git a/test/translation/transformation/tupleReturn.ts b/test/translation/transformation/tupleReturn.ts deleted file mode 100644 index 75b7fbf01..000000000 --- a/test/translation/transformation/tupleReturn.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** @tupleReturn */ -function tupleReturn(): [number, string] { - return [0, "foobar"]; -} -declare function noTupleReturn(): [number, string]; -declare function foo(a: [number, string]): void; -tupleReturn(); -noTupleReturn(); -let [a, b] = tupleReturn(); -let [c, d] = noTupleReturn(); -[a, b] = tupleReturn(); -[c, d] = noTupleReturn(); -let e = tupleReturn(); -let f = noTupleReturn(); -e = tupleReturn(); -f = noTupleReturn(); -foo(tupleReturn()); -foo(noTupleReturn()); -/** @tupleReturn */ -function tupleReturnFromVar(): [number, string] { - const r: [number, string] = [1, "baz"]; - return r; -} -/** @tupleReturn */ -function tupleReturnForward(): [number, string] { - return tupleReturn(); -} -function tupleNoForward(): [number, string] { - return tupleReturn(); -} -/** @tupleReturn */ -function tupleReturnUnpack(): [number, string] { - return tupleNoForward(); -} diff --git a/test/translation/transformation/typeAssert.ts b/test/translation/transformation/typeAssert.ts deleted file mode 100644 index 4b654d851..000000000 --- a/test/translation/transformation/typeAssert.ts +++ /dev/null @@ -1,2 +0,0 @@ -const test1 = 10; -const test2 = 10 as number; diff --git a/test/translation/transformation/while.ts b/test/translation/transformation/while.ts deleted file mode 100644 index 4014a4add..000000000 --- a/test/translation/transformation/while.ts +++ /dev/null @@ -1,4 +0,0 @@ -let d = 10; -while (d > 0) { - d--; -} diff --git a/test/tsconfig.json b/test/tsconfig.json index b22f71bc9..700c9d291 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -8,6 +8,7 @@ "target": "es2017", "lib": ["es2017"], "types": ["node", "jest"], + "experimentalDecorators": true, "noEmit": true, "module": "commonjs" diff --git a/test/unit/__snapshots__/expressions.spec.ts.snap b/test/unit/__snapshots__/expressions.spec.ts.snap new file mode 100644 index 000000000..db8863e35 --- /dev/null +++ b/test/unit/__snapshots__/expressions.spec.ts.snap @@ -0,0 +1,404 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Binary expressions ordering parentheses ("-1+1") 1`] = ` +"local ____exports = {} +____exports.__result = -1 + 1 +return ____exports" +`; + +exports[`Binary expressions ordering parentheses ("1*(3+4)") 1`] = ` +"local ____exports = {} +____exports.__result = 1 * (3 + 4) +return ____exports" +`; + +exports[`Binary expressions ordering parentheses ("1*(3+4*2)") 1`] = ` +"local ____exports = {} +____exports.__result = 1 * (3 + 4 * 2) +return ____exports" +`; + +exports[`Binary expressions ordering parentheses ("1*30+4") 1`] = ` +"local ____exports = {} +____exports.__result = 1 * 30 + 4 +return ____exports" +`; + +exports[`Binary expressions ordering parentheses ("1+1") 1`] = ` +"local ____exports = {} +____exports.__result = 1 + 1 +return ____exports" +`; + +exports[`Binary expressions ordering parentheses ("10-(4+5)") 1`] = ` +"local ____exports = {} +____exports.__result = 10 - (4 + 5) +return ____exports" +`; + +exports[`Bitop [5.2] ("~a") 1`] = ` +"local ____exports = {} +____exports.__result = bit32.bnot(a) +return ____exports" +`; + +exports[`Bitop [5.2] ("a&=b") 1`] = ` +"local ____exports = {} +____exports.__result = (function() + a = bit32.band(a, b) + return a +end)() +return ____exports" +`; + +exports[`Bitop [5.2] ("a&b") 1`] = ` +"local ____exports = {} +____exports.__result = bit32.band(a, b) +return ____exports" +`; + +exports[`Bitop [5.2] ("a<<=b") 1`] = ` +"local ____exports = {} +____exports.__result = (function() + a = bit32.lshift(a, b) + return a +end)() +return ____exports" +`; + +exports[`Bitop [5.2] ("a<>=b") 1`] = ` +"local ____exports = {} +____exports.__result = (function() + a = bit32.arshift(a, b) + return a +end)() +return ____exports" +`; + +exports[`Bitop [5.2] ("a>>>=b") 1`] = ` +"local ____exports = {} +____exports.__result = (function() + a = bit32.rshift(a, b) + return a +end)() +return ____exports" +`; + +exports[`Bitop [5.2] ("a>>>b") 1`] = ` +"local ____exports = {} +____exports.__result = bit32.rshift(a, b) +return ____exports" +`; + +exports[`Bitop [5.2] ("a>>b") 1`] = ` +"local ____exports = {} +____exports.__result = bit32.arshift(a, b) +return ____exports" +`; + +exports[`Bitop [5.2] ("a^=b") 1`] = ` +"local ____exports = {} +____exports.__result = (function() + a = bit32.bxor(a, b) + return a +end)() +return ____exports" +`; + +exports[`Bitop [5.2] ("a^b") 1`] = ` +"local ____exports = {} +____exports.__result = bit32.bxor(a, b) +return ____exports" +`; + +exports[`Bitop [5.2] ("a|=b") 1`] = ` +"local ____exports = {} +____exports.__result = (function() + a = bit32.bor(a, b) + return a +end)() +return ____exports" +`; + +exports[`Bitop [5.2] ("a|b") 1`] = ` +"local ____exports = {} +____exports.__result = bit32.bor(a, b) +return ____exports" +`; + +exports[`Bitop [5.3] ("~a") 1`] = ` +"local ____exports = {} +____exports.__result = ~a +return ____exports" +`; + +exports[`Bitop [5.3] ("a&=b") 1`] = ` +"local ____exports = {} +____exports.__result = (function() + a = a & b + return a +end)() +return ____exports" +`; + +exports[`Bitop [5.3] ("a&b") 1`] = ` +"local ____exports = {} +____exports.__result = a & b +return ____exports" +`; + +exports[`Bitop [5.3] ("a<<=b") 1`] = ` +"local ____exports = {} +____exports.__result = (function() + a = a << b + return a +end)() +return ____exports" +`; + +exports[`Bitop [5.3] ("a<>>=b") 1`] = ` +"local ____exports = {} +____exports.__result = (function() + a = a >> b + return a +end)() +return ____exports" +`; + +exports[`Bitop [5.3] ("a>>>b") 1`] = ` +"local ____exports = {} +____exports.__result = a >> b +return ____exports" +`; + +exports[`Bitop [5.3] ("a^=b") 1`] = ` +"local ____exports = {} +____exports.__result = (function() + a = a ~ b + return a +end)() +return ____exports" +`; + +exports[`Bitop [5.3] ("a^b") 1`] = ` +"local ____exports = {} +____exports.__result = a ~ b +return ____exports" +`; + +exports[`Bitop [5.3] ("a|=b") 1`] = ` +"local ____exports = {} +____exports.__result = (function() + a = a | b + return a +end)() +return ____exports" +`; + +exports[`Bitop [5.3] ("a|b") 1`] = ` +"local ____exports = {} +____exports.__result = a | b +return ____exports" +`; + +exports[`Bitop [JIT] ("~a") 1`] = ` +"local ____exports = {} +____exports.__result = bit.bnot(a) +return ____exports" +`; + +exports[`Bitop [JIT] ("a&=b") 1`] = ` +"local ____exports = {} +____exports.__result = (function() + a = bit.band(a, b) + return a +end)() +return ____exports" +`; + +exports[`Bitop [JIT] ("a&b") 1`] = ` +"local ____exports = {} +____exports.__result = bit.band(a, b) +return ____exports" +`; + +exports[`Bitop [JIT] ("a<<=b") 1`] = ` +"local ____exports = {} +____exports.__result = (function() + a = bit.lshift(a, b) + return a +end)() +return ____exports" +`; + +exports[`Bitop [JIT] ("a<>=b") 1`] = ` +"local ____exports = {} +____exports.__result = (function() + a = bit.arshift(a, b) + return a +end)() +return ____exports" +`; + +exports[`Bitop [JIT] ("a>>>=b") 1`] = ` +"local ____exports = {} +____exports.__result = (function() + a = bit.rshift(a, b) + return a +end)() +return ____exports" +`; + +exports[`Bitop [JIT] ("a>>>b") 1`] = ` +"local ____exports = {} +____exports.__result = bit.rshift(a, b) +return ____exports" +`; + +exports[`Bitop [JIT] ("a>>b") 1`] = ` +"local ____exports = {} +____exports.__result = bit.arshift(a, b) +return ____exports" +`; + +exports[`Bitop [JIT] ("a^=b") 1`] = ` +"local ____exports = {} +____exports.__result = (function() + a = bit.bxor(a, b) + return a +end)() +return ____exports" +`; + +exports[`Bitop [JIT] ("a^b") 1`] = ` +"local ____exports = {} +____exports.__result = bit.bxor(a, b) +return ____exports" +`; + +exports[`Bitop [JIT] ("a|=b") 1`] = ` +"local ____exports = {} +____exports.__result = (function() + a = bit.bor(a, b) + return a +end)() +return ____exports" +`; + +exports[`Bitop [JIT] ("a|b") 1`] = ` +"local ____exports = {} +____exports.__result = bit.bor(a, b) +return ____exports" +`; + +exports[`Unary expressions basic ("!a") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local ____ = not a +end +return ____exports" +`; + +exports[`Unary expressions basic ("++i") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + i = i + 1 +end +return ____exports" +`; + +exports[`Unary expressions basic ("+a") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local ____ = a +end +return ____exports" +`; + +exports[`Unary expressions basic ("--i") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + i = i - 1 +end +return ____exports" +`; + +exports[`Unary expressions basic ("-a") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local ____ = -a +end +return ____exports" +`; + +exports[`Unary expressions basic ("delete tbl.test") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + tbl.test = nil +end +return ____exports" +`; + +exports[`Unary expressions basic ("delete tbl['test']") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + tbl.test = nil +end +return ____exports" +`; + +exports[`Unary expressions basic ("i++") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + i = i + 1 +end +return ____exports" +`; + +exports[`Unary expressions basic ("i--") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + i = i - 1 +end +return ____exports" +`; + +exports[`Unary expressions basic ("let a = delete tbl.test") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local a = (function() + tbl.test = nil + return true + end)() +end +return ____exports" +`; + +exports[`Unary expressions basic ("let a = delete tbl['test']") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local a = (function() + tbl.test = nil + return true + end)() +end +return ____exports" +`; diff --git a/test/unit/array.spec.ts b/test/unit/array.spec.ts deleted file mode 100644 index 6b1b653a9..000000000 --- a/test/unit/array.spec.ts +++ /dev/null @@ -1,263 +0,0 @@ -import * as util from "../util"; - -test("Array access", () => { - const result = util.transpileAndExecute( - `const arr: Array = [3,5,1]; - return arr[1];` - ); - expect(result).toBe(5); -}); - -test("ReadonlyArray access", () => { - const result = util.transpileAndExecute( - `const arr: ReadonlyArray = [3,5,1]; - return arr[1];` - ); - expect(result).toBe(5); -}); - -test("Array literal access", () => { - const result = util.transpileAndExecute( - `const arr: number[] = [3,5,1]; - return arr[1];` - ); - expect(result).toBe(5); -}); - -test("Readonly array literal access", () => { - const result = util.transpileAndExecute( - `const arr: readonly number[] = [3,5,1]; - return arr[1];` - ); - expect(result).toBe(5); -}); - -test("Array union access", () => { - const result = util.transpileAndExecute( - `function makeArray(): number[] | string[] { return [3,5,1]; } - const arr = makeArray(); - return arr[1];` - ); - expect(result).toBe(5); -}); - -test("Array union access with empty tuple", () => { - const result = util.transpileAndExecute( - `function makeArray(): number[] | [] { return [3,5,1]; } - const arr = makeArray(); - return arr[1];` - ); - expect(result).toBe(5); -}); - -test("Array union length", () => { - const result = util.transpileAndExecute( - `function makeArray(): number[] | string[] { return [3,5,1]; } - const arr = makeArray(); - return arr.length;` - ); - expect(result).toBe(3); -}); - -test("Array intersection access", () => { - const result = util.transpileAndExecute( - `type I = number[] & {foo: string}; - function makeArray(): I { - let t = [3,5,1]; - (t as I).foo = "bar"; - return (t as I); - } - const arr = makeArray(); - return arr[1];` - ); - expect(result).toBe(5); -}); - -test("Array intersection length", () => { - const result = util.transpileAndExecute( - `type I = number[] & {foo: string}; - function makeArray(): I { - let t = [3,5,1]; - (t as I).foo = "bar"; - return (t as I); - } - const arr = makeArray(); - return arr.length;` - ); - expect(result).toBe(3); -}); - -test.each([ - { member: "firstElement()", expected: 3 }, - { member: "name", expected: "array" }, - { member: "length", expected: 1 }, -])("Derived array access (%p)", ({ member, expected }) => { - const luaHeader = `local arr = {name="array", firstElement=function(self) return self[1]; end};`; - const typeScriptHeader = ` - interface CustomArray extends Array{ - name:string, - firstElement():number; - }; - declare const arr: CustomArray; - `; - - const result = util.transpileAndExecute( - ` - arr[0] = 3; - return arr.${member};`, - undefined, - luaHeader, - typeScriptHeader - ); - - expect(result).toBe(expected); -}); - -test("Array delete", () => { - const result = util.transpileAndExecute( - `const myarray = [1,2,3,4]; - delete myarray[2]; - return \`\${myarray[0]},\${myarray[1]},\${myarray[2]},\${myarray[3]}\`;` - ); - - expect(result).toBe("1,2,nil,4"); -}); - -test("Array delete return true", () => { - const result = util.transpileAndExecute( - `const myarray = [1,2,3,4]; - const exists = delete myarray[2]; - return \`\${exists}:\${myarray[0]},\${myarray[1]},\${myarray[2]},\${myarray[3]}\`;` - ); - - expect(result).toBe("true:1,2,nil,4"); -}); - -test("Array delete return false", () => { - const result = util.transpileAndExecute( - `const myarray = [1,2,3,4]; - const exists = delete myarray[4]; - return \`\${exists}:\${myarray[0]},\${myarray[1]},\${myarray[2]},\${myarray[3]}\`;` - ); - - expect(result).toBe("true:1,2,3,4"); -}); - -test("Array property access", () => { - const code = ` - type A = number[] & {foo?: string}; - const a: A = [1,2,3]; - a.foo = "bar"; - return \`\${a.foo}\${a[0]}\${a[1]}\${a[2]}\`; - `; - expect(util.transpileAndExecute(code)).toBe("bar123"); -}); - -test.each([{ length: 0, result: 0 }, { length: 1, result: 1 }, { length: 7, result: 3 }])( - "Array length set", - ({ length, result }) => { - const code = ` - const arr = [1, 2, 3]; - arr.length = ${length}; - return arr.length; - `; - expect(util.transpileAndExecute(code)).toBe(result); - } -); - -test.each([{ length: 0, result: "0/0" }, { length: 1, result: "1/1" }, { length: 7, result: "7/3" }])( - "Array length set as expression", - ({ length, result }) => { - const code = ` - const arr = [1, 2, 3]; - const l = arr.length = ${length}; - return \`\${l}/\${arr.length}\`; - `; - expect(util.transpileAndExecute(code)).toBe(result); - } -); - -test.each([ - { length: -1, result: -1 }, - { length: -7, result: -7 }, - { length: 0.1, result: 0.1 }, - { length: "0 / 0", result: "NaN" }, - { length: "1 / 0", result: "Infinity" }, - { length: "-1 / 0", result: "-Infinity" }, -])("Invalid array length set", ({ length, result }) => { - const code = ` - const arr = [1, 2, 3]; - arr.length = ${length}; - `; - expect(() => util.transpileAndExecute(code)).toThrowError(`invalid array length: ${result}`); -}); - -test.each([0, 1, 2])("Array with OmittedExpression", index => { - const result = util.transpileAndExecute( - `const myarray = [1, , 2]; - return myarray[${index}];` - ); - - expect(result).toBe([1, , 2][index]); -}); - -test("OmittedExpression in Array Binding Assignment Statement", () => { - const result = util.transpileAndExecute( - `let a, c; - [a, , c] = [1, 2, 3]; - return a + c;` - ); - - expect(result).toBe(4); -}); - -test("array access call", () => { - const code = ` - const arr = [() => "foo", () => "bar"]; - return arr[1]();`; - expect(util.transpileAndExecute(code)).toBe("bar"); -}); - -test.each([`["foo", "bar"].length`, `["foo", "bar"][0]`, `[() => "foo", () => "bar"][0]()`])( - "array literal property access (%p)", - expression => { - const code = `return ${expression}`; - const expectResult = eval(expression); - expect(util.transpileAndExecute(code)).toBe(expectResult); - } -); - -const genericChecks = [ - "function generic(array: T)", - "function generic(array: T)", - "function generic(array: T[])", - "type ArrayType = number[]; function generic(array: T)", - "function generic(array: T & {})", - "function generic(array: T)", -]; - -test.each(genericChecks)("array constrained generic foreach (%p)", signature => { - const code = ` - ${signature}: number { - let sum = 0; - array.forEach(item => { - if (typeof item === "number") { - sum += item; - } - }); - return sum; - } - return generic([1, 2, 3]); - `; - expect(util.transpileAndExecute(code)).toBe(6); -}); - -test.each(genericChecks)("array constrained generic length (%p)", signature => { - const code = ` - ${signature}: number { - return array.length; - } - return generic([1, 2, 3]); - `; - expect(util.transpileAndExecute(code)).toBe(3); -}); diff --git a/test/unit/assignmentDestructuring.spec.ts b/test/unit/assignmentDestructuring.spec.ts deleted file mode 100644 index 908a4eac9..000000000 --- a/test/unit/assignmentDestructuring.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import * as tstl from "../../src"; -import * as util from "../util"; - -const assignmentDestruturingTs = ` - declare function myFunc(this: void): [number, string]; - let [a, b] = myFunc();`; - -test("Assignment destructuring [5.1]", () => { - const lua = util.transpileString(assignmentDestruturingTs, { - luaTarget: tstl.LuaTarget.Lua51, - luaLibImport: tstl.LuaLibImportKind.None, - }); - expect(lua).toBe(`local a, b = unpack(\n myFunc()\n)`); -}); - -test("Assignment destructuring [5.2]", () => { - const lua = util.transpileString(assignmentDestruturingTs, { - luaTarget: tstl.LuaTarget.Lua52, - luaLibImport: tstl.LuaLibImportKind.None, - }); - expect(lua).toBe(`local a, b = table.unpack(\n myFunc()\n)`); -}); - -test("Assignment destructuring [JIT]", () => { - const lua = util.transpileString(assignmentDestruturingTs, { - luaTarget: tstl.LuaTarget.LuaJIT, - luaLibImport: tstl.LuaLibImportKind.None, - }); - expect(lua).toBe(`local a, b = unpack(\n myFunc()\n)`); -}); - -test.each([ - "function foo(): [] { return []; }; let [] = foo();", - "let [] = ['a', 'b', 'c'];", - "let [] = [];", - "let [] = [] = [];", - "function foo(): [] { return []; }; [] = foo();", - "[] = ['a', 'b', 'c'];", - "[] = [];", - "[] = [] = [];", -])("Empty destructuring (%p)", code => { - expect(() => util.transpileAndExecute(code)).not.toThrow(); -}); - -test("Union destructuring", () => { - const code = ` - function foo(): [string] | [] { return ["bar"]; } - let x: string; - [x] = foo(); - return x; - `; - expect(util.transpileAndExecute(code)).toBe("bar"); -}); diff --git a/test/unit/assignments.spec.ts b/test/unit/assignments.spec.ts new file mode 100644 index 000000000..9bb53c78c --- /dev/null +++ b/test/unit/assignments.spec.ts @@ -0,0 +1,292 @@ +import * as util from "../util"; + +test("Const assignment (%p)", () => { + const lua = util.transpileString(`const foo = true;`); + expect(lua).toBe(`local foo = true`); +}); + +test("Let assignment (%p)", () => { + const lua = util.transpileString(`let foo = true;`); + expect(lua).toBe(`local foo = true`); +}); + +test("Var assignment (%p)", () => { + const lua = util.transpileString(`var foo = true;`); + expect(lua).toBe(`foo = true`); +}); + +test.each(["var myvar;", "let myvar;", "const myvar = null;", "const myvar = undefined;"])( + "Null assignments (%p)", + declaration => { + const result = util.transpileAndExecute(declaration + " return myvar;"); + expect(result).toBe(undefined); + } +); + +test.each(["x = y", "x += y"])("Assignment expressions (%p)", expression => { + util.testFunction` + let x = "x"; + let y = "y"; + return ${expression}; + `.expectToMatchJsResult(); +}); + +test.each(["x = o.p", "x = a[0]", "x = y = o.p", "x = o.p"])("Assignment expressions using temp (%p)", expression => { + util.testFunction` + let x = "x"; + let y = "y"; + let o = {p: "o"}; + let a = ["a"]; + return ${expression}; + `.expectToMatchJsResult(); +}); + +test.each(["o.p = x", "a[0] = x", "o.p = a[0]", "o.p = a[0] = x"])( + "Property assignment expressions (%p)", + expression => { + util.testFunction` + let x = "x"; + let o = {p: "o"}; + let a = ["a"]; + return ${expression}; + `.expectToMatchJsResult(); + } +); + +test.each([ + "x = t()", + "x = tr()", + "[x[1], x[0]] = t()", + "[x[1], x[0]] = tr()", + "x = [y[1], y[0]]", + "[x[0], x[1]] = [y[1], y[0]]", +])("Tuple assignment expressions (%p)", expression => { + util.testFunction` + let x: [string, string] = ["x0", "x1"]; + let y: [string, string] = ["y0", "y1"]; + function t(): [string, string] { return ["t0", "t1"] }; + /** @tupleReturn */ + function tr(): [string, string] { return ["tr0", "tr1"] }; + const r = ${expression}; + return \`\${r[0]},\${r[1]}\` + `.expectToMatchJsResult(); +}); + +test.each([ + "++x", + "x++", + "--x", + "x--", + "x += y", + "x -= y", + "x *= y", + "y /= x", + "y %= x", + "y **= x", + "x |= y", + "x &= y", + "x ^= y", + "x <<= y", + "x >>>= y", +])("Operator assignment statements (%p)", statement => { + util.testFunction` + let x = 3; + let y = 6; + ${statement}; + return { x, y }; + `.expectToMatchJsResult(); +}); + +test.each([ + "++o.p", + "o.p++", + "--o.p", + "o.p--", + "o.p += a[0]", + "o.p -= a[0]", + "o.p *= a[0]", + "a[0] /= o.p", + "a[0] %= o.p", + "a[0] **= o.p", + "o.p |= a[0]", + "o.p &= a[0]", + "o.p ^= a[0]", + "o.p <<= a[0]", + "o.p >>>= a[0]", +])("Operator assignment to simple property statements (%p)", statement => { + util.testFunction` + let o = { p: 3 }; + let a = [6]; + ${statement}; + return { o, a }; + `.expectToMatchJsResult(); +}); + +test.each([ + "++o.p.d", + "o.p.d++", + "--o.p.d", + "o.p.d--", + "o.p.d += a[0][0]", + "o.p.d -= a[0][0]", + "o.p.d *= a[0][0]", + "a[0][0] /= o.p.d", + "a[0][0] %= o.p.d", + "a[0][0] **= o.p.d", + "o.p.d |= a[0][0]", + "o.p.d &= a[0][0]", + "o.p.d ^= a[0][0]", + "o.p.d <<= a[0][0]", + "o.p.d >>>= a[0][0]", +])("Operator assignment to deep property statements (%p)", statement => { + util.testFunction` + let o = { p: { d: 3 } }; + let a = [[6,11], [7,13]]; + ${statement}; + return { o, a }; + `.expectToMatchJsResult(); +}); + +test.each([ + "++of().p", + "of().p++", + "--of().p", + "of().p--", + "of().p += af()[i()]", + "of().p -= af()[i()]", + "of().p *= af()[i()]", + "af()[i()] /= of().p", + "af()[i()] %= of().p", + "af()[i()] **= of().p", + "of().p |= af()[i()]", + "of().p &= af()[i()]", + "of().p ^= af()[i()]", + "of().p <<= af()[i()]", + "of().p >>>= af()[i()]", +])("Operator assignment to complex property statements (%p)", statement => { + util.testFunction` + let o = { p: 3 }; + let a = [6]; + function of() { return o; } + function af() { return a; } + function i() { return 0; } + ${statement}; + return { o, a }; + `.expectToMatchJsResult(); +}); + +test.each([ + "++of().p.d", + "of().p.d++", + "--of().p.d", + "of().p.d--", + "of().p.d += af()[i()][i()]", + "of().p.d -= af()[i()][i()]", + "of().p.d *= af()[i()][i()]", + "af()[i()][i()] /= of().p.d", + "af()[i()][i()] %= of().p.d", + "af()[i()][i()] **= of().p.d", + "of().p.d |= af()[i()][i()]", + "of().p.d &= af()[i()][i()]", + "of().p.d ^= af()[i()][i()]", + "of().p.d <<= af()[i()][i()]", + "of().p.d >>>= af()[i()][i()]", +])("Operator assignment to complex deep property statements (%p)", statement => { + util.testFunction` + let o = { p: { d: 3 } }; + let a = [[7, 6], [11, 13]]; + function of() { return o; } + function af() { return a; } + let _i = 0; + function i() { return _i++; } + ${statement}; + return { o, a, _i }; + `.expectToMatchJsResult(); +}); + +test.each([ + "++x", + "x++", + "--x", + "x--", + "x += y", + "x -= y", + "x *= y", + "y /= x", + "y %= x", + "y **= x", + "x |= y", + "x &= y", + "x ^= y", + "x <<= y", + "x >>>= y", + "x + (y += 7)", + "x + (y += 7)", + "x++ + (y += 7)", +])("Operator assignment expressions (%p)", expression => { + util.testFunction` + let x = 3; + let y = 6; + const r = ${expression}; + return { r, x, y }; + `.expectToMatchJsResult(); +}); + +test.each([ + "++o.p", + "o.p++", + "--o.p", + "o.p--", + "o.p += a[0]", + "o.p -= a[0]", + "o.p *= a[0]", + "a[0] /= o.p", + "a[0] %= o.p", + "a[0] **= o.p", + "o.p |= a[0]", + "o.p &= a[0]", + "o.p ^= a[0]", + "o.p <<= a[0]", + "o.p >>>= a[0]", + "o.p + (a[0] += 7)", + "o.p += (a[0] += 7)", + "o.p++ + (a[0] += 7)", +])("Operator assignment to simple property expressions (%p)", expression => { + util.testFunction` + let o = { p: 3 }; + let a = [6]; + const r = ${expression}; + return { r, o, a }; + `.expectToMatchJsResult(); +}); + +test.each([ + "++of().p", + "of().p++", + "--of().p", + "of().p--", + "of().p += af()[i()]", + "of().p -= af()[i()]", + "of().p *= af()[i()]", + "af()[i()] /= of().p", + "af()[i()] %= of().p", + "af()[i()] **= of().p", + "of().p |= af()[i()]", + "of().p &= af()[i()]", + "of().p ^= af()[i()]", + "of().p <<= af()[i()]", + "of().p >>>= af()[i()]", + "of().p + (af()[i()] += 7)", + "of().p += (af()[i()] += 7)", + "of().p++ + (af()[i()] += 7)", +])("Operator assignment to complex property expressions (%p)", expression => { + util.testFunction` + let o = { p: 3 }; + let a = [6]; + function of() { return o; } + function af() { return a; } + function i() { return 0; } + const r = ${expression}; + return { r, o, a }; + `.expectToMatchJsResult(); +}); diff --git a/test/unit/assignments/assignments.spec.ts b/test/unit/assignments/assignments.spec.ts deleted file mode 100644 index c5da9beff..000000000 --- a/test/unit/assignments/assignments.spec.ts +++ /dev/null @@ -1,186 +0,0 @@ -import * as TSTLErrors from "../../../src/TSTLErrors"; -import * as util from "../../util"; - -test.each([ - { inp: `"abc"`, out: `"abc"` }, - { inp: "3", out: "3" }, - { inp: "[1,2,3]", out: "{1, 2, 3}" }, - { inp: "true", out: "true" }, - { inp: "false", out: "false" }, - { inp: `{a:3,b:"4"}`, out: `{a = 3, b = "4"}` }, -])("Const assignment (%p)", ({ inp, out }) => { - const lua = util.transpileString(`const myvar = ${inp}`); - expect(lua).toBe(`local myvar = ${out}`); -}); - -test.each([ - { inp: `"abc"`, out: `"abc"` }, - { inp: "3", out: "3" }, - { inp: "[1,2,3]", out: "{1, 2, 3}" }, - { inp: "true", out: "true" }, - { inp: "false", out: "false" }, - { inp: `{a:3,b:"4"}`, out: `{a = 3, b = "4"}` }, -])("Let assignment (%p)", ({ inp, out }) => { - const lua = util.transpileString(`let myvar = ${inp}`); - expect(lua).toBe(`local myvar = ${out}`); -}); - -test.each([ - { inp: `"abc"`, out: `"abc"` }, - { inp: "3", out: "3" }, - { inp: "[1,2,3]", out: "{1, 2, 3}" }, - { inp: "true", out: "true" }, - { inp: "false", out: "false" }, - { inp: `{a:3,b:"4"}`, out: `{a = 3, b = "4"}` }, -])("Var assignment (%p)", ({ inp, out }) => { - const lua = util.transpileString(`var myvar = ${inp}`); - expect(lua).toBe(`myvar = ${out}`); -}); - -test.each(["var myvar;", "let myvar;", "const myvar = null;", "const myvar = undefined;"])( - "Null assignments (%p)", - declaration => { - const result = util.transpileAndExecute(declaration + " return myvar;"); - expect(result).toBe(undefined); - } -); - -test.each([ - { input: ["a", "b"], values: ["e", "f"] }, - { input: ["a", "b"], values: ["e", "f", "g"] }, - { input: ["a", "b", "c"], values: ["e", "f", "g"] }, -])("Binding pattern assignment (%p)", ({ input, values }) => { - const pattern = input.join(","); - const initializer = values.map(v => `"${v}"`).join(","); - - const tsCode = `const [${pattern}] = [${initializer}]; return [${pattern}].join("-");`; - const result = util.transpileAndExecute(tsCode); - - expect(result).toBe(values.slice(0, input.length).join("-")); -}); - -test("Ellipsis binding pattern", () => { - expect(() => util.transpileString("let [a,b,...c] = [1,2,3];")).toThrowExactError( - TSTLErrors.ForbiddenEllipsisDestruction(util.nodeStub) - ); -}); - -test("Tuple Assignment", () => { - const code = ` - function abc(): [number, number] { return [1, 2]; }; - let t: [number, number] = abc(); - return t[0] + t[1]; - `; - const result = util.transpileAndExecute(code); - expect(result).toBe(3); -}); - -test("TupleReturn assignment", () => { - const code = ` - /** @tupleReturn */ - declare function abc(this: void): number[] - let [a,b] = abc(); - `; - - const lua = util.transpileString(code); - expect(lua).toBe("local a, b = abc()"); -}); - -test("TupleReturn Single assignment", () => { - const code = ` - /** @tupleReturn */ - declare function abc(this: void): [number, string]; - let a = abc(); - a = abc(); - `; - - const lua = util.transpileString(code); - expect(lua).toBe("local a = ({\n abc()\n})\na = ({\n abc()\n})"); -}); - -test("TupleReturn interface assignment", () => { - const code = ` - interface def { - /** @tupleReturn */ - abc(); - } declare const jkl : def; - let [a,b] = jkl.abc(); - `; - - const lua = util.transpileString(code); - expect(lua).toBe("local a, b = jkl:abc()"); -}); - -test("TupleReturn namespace assignment", () => { - const code = ` - declare namespace def { - /** @tupleReturn */ - function abc(this: void) {} - } - let [a,b] = def.abc(); - `; - - const lua = util.transpileString(code); - expect(lua).toBe("local a, b = def.abc()"); -}); - -test("TupleReturn method assignment", () => { - const code = ` - declare class def { - /** @tupleReturn */ - abc() { return [1,2,3]; } - } const jkl = new def(); - let [a,b] = jkl.abc(); - `; - - const lua = util.transpileString(code); - expect(lua).toBe("local jkl = def.new()\nlocal a, b = jkl:abc()"); -}); - -test("TupleReturn functional", () => { - const code = ` - /** @tupleReturn */ - function abc(): [number, string] { return [3, "a"]; } - const [a, b] = abc(); - return b + a; - `; - - const result = util.transpileAndExecute(code); - - expect(result).toBe("a3"); -}); - -test("TupleReturn single", () => { - const code = ` - /** @tupleReturn */ - function abc(): [number, string] { return [3, "a"]; } - const res = abc(); - return res.length - `; - - const result = util.transpileAndExecute(code); - - expect(result).toBe(2); -}); - -test("TupleReturn in expression", () => { - const code = ` - /** @tupleReturn */ - function abc(): [number, string] { return [3, "a"]; } - return abc()[1] + abc()[0]; - `; - - const result = util.transpileAndExecute(code); - - expect(result).toBe("a3"); -}); - -test("String table access", () => { - const code = ` - const dict : {[key:string]:any} = {}; - dict["a b"] = 3; - return dict["a b"]; - `; - const result = util.transpileAndExecute(code); - expect(result).toBe(3); -}); diff --git a/test/unit/bindingpatterns.spec.ts b/test/unit/bindingpatterns.spec.ts deleted file mode 100644 index 39684ae50..000000000 --- a/test/unit/bindingpatterns.spec.ts +++ /dev/null @@ -1,191 +0,0 @@ -import * as util from "../util"; - -const testCases = [ - { bindingString: "{x}", objectString: "{x: true}", returnVariable: "x" }, - { bindingString: "[x, y]", objectString: "[false, true]", returnVariable: "y" }, - { bindingString: "{x: [y, z]}", objectString: "{x: [false, true]}", returnVariable: "z" }, - { bindingString: "{x: [, z]}", objectString: "{x: [false, true]}", returnVariable: "z" }, - { bindingString: "{x: [{y}]}", objectString: "{x: [{y: true}]}", returnVariable: "y" }, - { bindingString: "[[y, z]]", objectString: "[[false, true]]", returnVariable: "z" }, - { bindingString: "{x, y}", objectString: "{x: false, y: true}", returnVariable: "y" }, - { bindingString: "{x: foo, y}", objectString: "{x: true, y: false}", returnVariable: "foo" }, - { bindingString: "{x: foo, y: bar}", objectString: "{x: false, y: true}", returnVariable: "bar" }, - { bindingString: "{x: {x, y}, z}", objectString: "{x: {x: true, y: false}, z: false}", returnVariable: "x" }, - { bindingString: "{x: {x, y}, z}", objectString: "{x: {x: false, y: true}, z: false}", returnVariable: "y" }, - { bindingString: "{x: {x, y}, z}", objectString: "{x: {x: false, y: false}, z: true}", returnVariable: "z" }, -]; - -const testCasesDefault = [ - { bindingString: "{x = true}", objectString: "{}", returnVariable: "x" }, - { bindingString: "{x, y = true}", objectString: "{x: false}", returnVariable: "y" }, - { bindingString: "[x = true, y = false]", objectString: "[undefined, undefined]", returnVariable: "x" }, - { bindingString: "[x = false, y = false]", objectString: "[false, true]", returnVariable: "y" }, -]; - -test.each([ - { bindingString: "{x, y}, z", objectString: "{x: false, y: false}, true", returnVariable: "z" }, - { bindingString: "{x, y}, {z}", objectString: "{x: false, y: false}, {z: true}", returnVariable: "z" }, - ...testCases, - ...testCasesDefault, -])("Object bindings in functions (%p)", ({ bindingString, objectString, returnVariable }) => { - const result = util.transpileAndExecute(` - function test(${bindingString}) { - return ${returnVariable}; - } - return test(${objectString}); - `); - expect(result).toBe(true); -}); - -test.each([...testCases, ...testCasesDefault])( - "testBindingPatternDeclarations (%p)", - ({ bindingString, objectString, returnVariable }) => { - const result = util.transpileAndExecute(` - let ${bindingString} = ${objectString}; - return ${returnVariable}; - `); - expect(result).toBe(true); - } -); - -test.each([...testCases, ...testCasesDefault])( - "testBindingPatternExportDeclarations (%p)", - ({ bindingString, objectString, returnVariable }) => { - const result = util.transpileExecuteAndReturnExport( - `export const ${bindingString} = ${objectString};`, - returnVariable - ); - expect(result).toBe(true); - } -); - -test.each(testCases)( - "Object bindings with call expressions (%p)", - ({ bindingString, objectString, returnVariable }) => { - const result = util.transpileAndExecute(` - function call() { - return ${objectString}; - } - let ${bindingString} = call(); - return ${returnVariable}; - `); - expect(result).toBe(true); - } -); - -test.each([ - { bindingString: "{x, y = true}", objectString: "{x: false, y: false}", returnVariable: "y" }, - { bindingString: "{x, y: [z = true]}", objectString: "{x: false, y: [false]}", returnVariable: "z" }, - { bindingString: "[x = true]", objectString: "[false]", returnVariable: "x" }, -])("Binding patterns handle false correctly (%p)", ({ bindingString, objectString, returnVariable }) => { - const result = util.transpileExecuteAndReturnExport( - `export const ${bindingString} = ${objectString};`, - returnVariable - ); - expect(result).toBe(false); -}); - -const assignmentBindingPatterns = [ - { bindingString: "{x: obj.prop}", objectString: "{x: true}", returnVariable: "obj.prop" }, - { - bindingString: "{x: obj.prop = true}", - objectString: "{x: undefined}", - returnVariable: "obj.prop", - }, - { bindingString: "[{x: obj.prop}]", objectString: "[{x: true}]", returnVariable: "obj.prop" }, - { - bindingString: "{obj: {prop: obj.prop}}", - objectString: "{obj: {prop: true}}", - returnVariable: "obj.prop", - }, - { bindingString: "{x = true}", objectString: "{}", returnVariable: "x" }, - { - bindingString: "{x: {[2 + 1]: y}}", - objectString: "{x: {[2 + 1]: true}}", - returnVariable: "y", - }, -]; - -test.each([...assignmentBindingPatterns, ...testCases])( - "Binding pattern expressions (%p)", - ({ bindingString, objectString, returnVariable }) => { - const result = util.transpileAndExecute(` - let x, y, z, foo, bar, obj: { prop: boolean }; - obj = { prop: false }; - (${bindingString} = ${objectString}) - return ${returnVariable}; - `); - expect(result).toBe(true); - } -); - -test.each([...assignmentBindingPatterns, ...testCases])( - "Binding patterns expressions pass conditional checks (%p)", - ({ bindingString, objectString, returnVariable }) => { - const result = util.transpileAndExecute(` - let x, y, z, foo, bar, obj: { prop: boolean }; - obj = { prop: false }; - if (${bindingString} = ${objectString}) { - return ${returnVariable}; - } - `); - expect(result).toBe(true); - } -); - -test.each([ - { bindingString: "{ x: x.prop = true } = {}", returnValue: "x.prop", expectedResult: true }, - { - bindingString: "{ x: x.prop = true } = {}", - returnValue: "typeof y === 'object'", - expectedResult: true, - }, - { - bindingString: "{ x: x.prop = false } = { x: true }", - returnValue: "x.prop", - expectedResult: true, - }, -])("Binding pattern assignment pass-through (%p)", ({ bindingString, returnValue, expectedResult }) => { - const result = util.transpileAndExecute(` - let x: any = {}, y: any = {}; - y = ${bindingString}; - return ${returnValue}; - `); - expect(result).toBe(expectedResult); -}); - -test("Array binding pattern to assign array length (%p)", () => { - const result = util.transpileAndExecute(` - let x = [0, 1, 2]; - [x.length] = [0]; - return x.length; - `); - expect(result).toBe(0); -}); - -test("Nested array binding pattern to assign array length (%p)", () => { - const result = util.transpileAndExecute(` - let x = [0, 1, 2]; - [[x.length]] = [[0]]; - return x.length; - `); - expect(result).toBe(0); -}); - -test("Object binding pattern to assign array length (%p)", () => { - const result = util.transpileAndExecute(` - let x = [0, 1, 2]; - ({ x: x.length } = { x: 0 }); - return x.length; - `); - expect(result).toBe(0); -}); - -test("Nested object binding pattern to assign array length (%p)", () => { - const result = util.transpileAndExecute(` - let x = [0, 1, 2]; - ({ x: { x: x.length } } = { x: { x: 0 } }); - return x.length; - `); - expect(result).toBe(0); -}); diff --git a/test/unit/builtins/__snapshots__/console.spec.ts.snap b/test/unit/builtins/__snapshots__/console.spec.ts.snap new file mode 100644 index 000000000..64baea0f4 --- /dev/null +++ b/test/unit/builtins/__snapshots__/console.spec.ts.snap @@ -0,0 +1,145 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`console.assert ("console.assert(false)") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + assert(false) +end +return ____exports" +`; + +exports[`console.assert ("console.assert(false, \\"message %%s\\", \\"info\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + assert( + false, + string.format(\\"message %%s\\", \\"info\\") + ) +end +return ____exports" +`; + +exports[`console.assert ("console.assert(false, \\"message %s\\", \\"info\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + assert( + false, + string.format(\\"message %s\\", \\"info\\") + ) +end +return ____exports" +`; + +exports[`console.assert ("console.assert(false, \\"message\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + assert(false, \\"message\\") +end +return ____exports" +`; + +exports[`console.assert ("console.assert(false, \\"message\\", \\"more\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + assert(false, \\"message\\", \\"more\\") +end +return ____exports" +`; + +exports[`console.log ("console.log()") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print() +end +return ____exports" +`; + +exports[`console.log ("console.log(\\"Hello %%s\\", \\"there\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print( + string.format(\\"Hello %%s\\", \\"there\\") + ) +end +return ____exports" +`; + +exports[`console.log ("console.log(\\"Hello %s\\", \\"there\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print( + string.format(\\"Hello %s\\", \\"there\\") + ) +end +return ____exports" +`; + +exports[`console.log ("console.log(\\"Hello\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print(\\"Hello\\") +end +return ____exports" +`; + +exports[`console.log ("console.log(\\"Hello\\", \\"There\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print(\\"Hello\\", \\"There\\") +end +return ____exports" +`; + +exports[`console.trace ("console.trace()") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print( + debug.traceback() + ) +end +return ____exports" +`; + +exports[`console.trace ("console.trace(\\"Hello %%s\\", \\"there\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print( + debug.traceback( + string.format(\\"Hello %%s\\", \\"there\\") + ) + ) +end +return ____exports" +`; + +exports[`console.trace ("console.trace(\\"Hello %s\\", \\"there\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print( + debug.traceback( + string.format(\\"Hello %s\\", \\"there\\") + ) + ) +end +return ____exports" +`; + +exports[`console.trace ("console.trace(\\"Hello\\", \\"there\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print( + debug.traceback(\\"Hello\\", \\"there\\") + ) +end +return ____exports" +`; + +exports[`console.trace ("console.trace(\\"message\\")") 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + print( + debug.traceback(\\"message\\") + ) +end +return ____exports" +`; diff --git a/test/unit/builtins/__snapshots__/math.spec.ts.snap b/test/unit/builtins/__snapshots__/math.spec.ts.snap new file mode 100644 index 000000000..65d6680d8 --- /dev/null +++ b/test/unit/builtins/__snapshots__/math.spec.ts.snap @@ -0,0 +1,89 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Math.PI 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local ____ = math.pi +end +return ____exports" +`; + +exports[`Math.atan2(2, 3) 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + math.atan(2 / 3) +end +return ____exports" +`; + +exports[`Math.cos() 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + math.cos() +end +return ____exports" +`; + +exports[`Math.log1p(3) 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + math.log(1 + 3) +end +return ____exports" +`; + +exports[`Math.log2(3) 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local ____ = (math.log(3) / 0.6931471805599453) +end +return ____exports" +`; + +exports[`Math.log10(3) 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local ____ = (math.log(3) / 2.302585092994046) +end +return ____exports" +`; + +exports[`Math.min() 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + math.min() +end +return ____exports" +`; + +exports[`Math.round(3.3) 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + math.floor(3.3 + 0.5) +end +return ____exports" +`; + +exports[`Math.sin() 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + math.sin() +end +return ____exports" +`; + +exports[`const x = Math.log2(3) 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local x = (math.log(3) / 0.6931471805599453) +end +return ____exports" +`; + +exports[`const x = Math.log10(3) 1`] = ` +"local ____exports = {} +function ____exports.__main(self) + local x = (math.log(3) / 2.302585092994046) +end +return ____exports" +`; diff --git a/test/unit/builtins/array.spec.ts b/test/unit/builtins/array.spec.ts new file mode 100644 index 000000000..edac44090 --- /dev/null +++ b/test/unit/builtins/array.spec.ts @@ -0,0 +1,522 @@ +import * as util from "../../util"; + +test("omitted expression", () => { + util.testFunction` + const array = [1, , 2]; + return { a: array[0], b: array[1], c: array[2] }; + `.expectToMatchJsResult(); +}); + +describe("access", () => { + test("Array", () => { + util.testFunction` + const array: Array = [3, 5, 1]; + return array[1]; + `.expectToMatchJsResult(); + }); + + test("ReadonlyArray", () => { + util.testFunction` + const array: ReadonlyArray = [3, 5, 1]; + return array[1]; + `.expectToMatchJsResult(); + }); + + test("array literal", () => { + util.testExpression`[3, 5, 1][1]`.expectToMatchJsResult(); + }); + + test("const array literal", () => { + util.testExpression`([3, 5, 1] as const)[1]`.expectToMatchJsResult(); + }); + + test("tuple", () => { + util.testFunction` + const tuple: [number, number, number] = [3, 5, 1]; + return tuple[1]; + `.expectToMatchJsResult(); + }); + + test("readonly tuple", () => { + util.testFunction` + const tuple: readonly [number, number, number] = [3, 5, 1]; + return tuple[1]; + `.expectToMatchJsResult(); + }); + + test("union", () => { + util.testFunction` + const array: number[] | string[] = [3, 5, 1]; + return array[1]; + `.expectToMatchJsResult(); + }); + + test("union with empty tuple", () => { + util.testFunction` + const array: number[] | [] = [3, 5, 1]; + return array[1]; + `.expectToMatchJsResult(); + }); + + test("union with tuple", () => { + util.testFunction` + const tuple: number[] | [number, number, number] = [3, 5, 1]; + return tuple[1]; + `.expectToMatchJsResult(); + }); + + test("access in call", () => { + util.testExpression`[() => "foo", () => "bar"][0]()`.expectToMatchJsResult(); + }); + + test("intersection", () => { + util.testFunction` + const array = Object.assign([3, 5, 1], { foo: "bar" }); + return { foo: array.foo, a: array[0], b: array[1], c: array[2] }; + `.expectToMatchJsResult(); + }); + + test("with enum value index", () => { + util.testFunction` + enum TestEnum { + A, + B, + C, + } + + const array = ["a", "b", "c"]; + let index = TestEnum.A; + return array[index]; + `.expectToMatchJsResult(); + }); + + test.each([ + { member: "firstElement()", expected: 3 }, + { member: "name", expected: "array" }, + { member: "length", expected: 1 }, + ])("derived array (.%p)", ({ member, expected }) => { + const luaHeader = ` + local array = { + name = "array", + firstElement = function(self) return self[1] end + } + `; + + util.testModule` + interface CustomArray extends Array { + name: string; + firstElement(): number; + }; + + declare const array: CustomArray; + + array[0] = 3; + export const result = array.${member}; + ` + .setReturnExport("result") + .setLuaHeader(luaHeader) + .expectToEqual(expected); + }); +}); + +describe("array.length", () => { + describe("get", () => { + test("union", () => { + util.testFunction` + const array: number[] | string[] = [3, 5, 1]; + return array.length; + `.expectToMatchJsResult(); + }); + + test("intersection", () => { + util.testFunction` + const array = Object.assign([3, 5, 1], { foo: "bar" }); + return array.length; + `.expectToMatchJsResult(); + }); + + test("tuple", () => { + util.testFunction` + const tuple: [number, number, number] = [3, 5, 1]; + return tuple.length; + `.expectToMatchJsResult(); + }); + }); + + describe("set", () => { + test.each([{ length: 0, newLength: 0 }, { length: 1, newLength: 1 }, { length: 7, newLength: 3 }])( + "removes extra elements", + ({ length, newLength }) => { + util.testFunction` + const array = [1, 2, 3]; + array.length = ${length}; + return array.length; + `.expectToEqual(newLength); + } + ); + + test.each([0, 1, 7])("returns right-hand side value", length => { + util.testExpression`[1, 2, 3].length = ${length}`.expectToEqual(length); + }); + + test.each([-1, -7, 0.1, NaN, Infinity, -Infinity])("throws on invalid values (%p)", length => { + util.testFunction` + [1, 2, 3].length = ${length}; + `.expectToEqual(new util.ExecutionError(`invalid array length: ${length}`)); + }); + + test("in array destructuring", () => { + util.testFunction` + const array = [0, 1, 2]; + [array.length] = [0]; + return array.length; + `.expectToEqual(0); + }); + + test("in nested array destructuring", () => { + util.testFunction` + const array = [0, 1, 2]; + [[array.length]] = [[0]]; + return array.length; + `.expectToEqual(0); + }); + + test("in object destructuring", () => { + util.testFunction` + const array = [0, 1, 2]; + ({ length: array.length } = { length: 0 }); + return array.length; + `.expectToEqual(0); + }); + + test("in nested object destructuring", () => { + util.testFunction` + const array = [0, 1, 2]; + ({ obj: { length: array.length } } = { obj: { length: 0 } }); + return array.length; + `.expectToEqual(0); + }); + }); +}); + +describe("delete", () => { + test("deletes element", () => { + util.testFunction` + const array = [1, 2, 3, 4]; + delete array[2]; + return { a: array[0], b: array[1], c: array[2], d: array[3] }; + `.expectToMatchJsResult(); + }); + + test("returns true when element exists", () => { + util.testFunction` + const array = [1, 2, 3, 4]; + const exists = delete array[2]; + return { exists, a: array[0], b: array[1], c: array[2], d: array[3] }; + `.expectToMatchJsResult(); + }); + + test("returns false when element not exists", () => { + util.testFunction` + const array = [1, 2, 3, 4]; + const exists = delete array[4]; + return { exists, a: array[0], b: array[1], c: array[2], d: array[3] }; + `.expectToMatchJsResult(); + }); +}); + +test("tuple.forEach", () => { + util.testFunction` + const tuple: [number, number, number] = [3, 5, 1]; + let count = 0; + tuple.forEach(value => { + count += value; + }); + return count; + `.expectToMatchJsResult(); +}); + +test("array.forEach (%p)", () => { + util.testFunction` + const array = [0, 1, 2, 3]; + array.forEach((elem, index) => { + array[index] = array[index] + 1; + }); + return array; + `.expectToMatchJsResult(); +}); + +test.each([ + { array: [], searchElement: 3 }, + { array: [0, 2, 4, 8], searchElement: 10 }, + { array: [0, 2, 4, 8], searchElement: 8 }, +])("array.findIndex (%p)", ({ array, searchElement }) => { + util.testFunction` + const array = ${util.valueToString(array)}; + return array.findIndex((elem, index, arr) => elem === ${searchElement} && arr[index] === elem); + `.expectToMatchJsResult(); +}); + +test.each([ + { array: [], func: "x => x" }, + { array: [0, 1, 2, 3], func: "x => x" }, + { array: [0, 1, 2, 3], func: "x => x*2" }, + { array: [1, 2, 3, 4], func: "x => -x" }, + { array: [0, 1, 2, 3], func: "x => x+2" }, + { array: [0, 1, 2, 3], func: "x => x%2 == 0 ? x + 1 : x - 1" }, +])("array.map (%p)", ({ array, func }) => { + util.testExpression`${util.valueToString(array)}.map(${func})`.expectToMatchJsResult(); +}); + +test.each([ + { array: [], func: "x => x > 1" }, + { array: [0, 1, 2, 3], func: "x => x > 1" }, + { array: [0, 1, 2, 3], func: "x => x < 3" }, + { array: [0, 1, 2, 3], func: "x => x < 0" }, + { array: [0, -1, -2, -3], func: "x => x < 0" }, + { array: [0, 1, 2, 3], func: "() => true" }, + { array: [0, 1, 2, 3], func: "() => false" }, +])("array.filter (%p)", ({ array, func }) => { + util.testExpression`${util.valueToString(array)}.filter(${func})`.expectToMatchJsResult(); +}); + +test.each([ + { array: [], func: "x => x > 1" }, + { array: [0, 1, 2, 3], func: "x => x > 1" }, + { array: [false, true, false], func: "x => x" }, + { array: [true, true, true], func: "x => x" }, +])("array.every (%p)", ({ array, func }) => { + util.testExpression`${util.valueToString(array)}.every(${func})`.expectToMatchJsResult(); +}); + +test.each([ + { array: [], func: "x => x > 1" }, + { array: [0, 1, 2, 3], func: "x => x > 1" }, + { array: [false, true, false], func: "x => x" }, + { array: [true, true, true], func: "x => x" }, +])("array.some (%p)", ({ array, func }) => { + util.testExpression`${util.valueToString(array)}.some(${func})`.expectToMatchJsResult(); +}); + +test.each([ + { array: [2, 3, 4, 5], args: [] }, + { array: [], args: [1, 2] }, + { array: [0, 1, 2, 3], args: [1, 2] }, + { array: [0, 1, 2, 3], args: [1, 1] }, + { array: [0, 1, 2, 3], args: [1, -1] }, + { array: [0, 1, 2, 3], args: [-3, -1] }, + { array: [0, 1, 2, 3, 4, 5], args: [1, 3] }, + { array: [0, 1, 2, 3, 4, 5], args: [3] }, +])("array.slice (%p)", ({ array, args }) => { + util.testExpression`${util.valueToString(array)}.slice(${util.valuesToString(args)})`.expectToMatchJsResult(); +}); + +test.each([ + // Insert + { array: [], start: 0, deleteCount: 0, newElements: [9, 10, 11] }, + { array: [0, 1, 2, 3], start: 1, deleteCount: 0, newElements: [9, 10, 11] }, + { array: [0, 1, 2, 3], start: 2, deleteCount: 2, newElements: [9, 10, 11] }, + { array: [0, 1, 2, 3], start: 4, deleteCount: 1, newElements: [8, 9] }, + { array: [0, 1, 2, 3], start: 4, deleteCount: 0, newElements: [8, 9] }, + { array: [0, 1, 2, 3], start: -2, deleteCount: 0, newElements: [8, 9] }, + { array: [0, 1, 2, 3], start: -3, deleteCount: 0, newElements: [8, 9] }, + { array: [0, 1, 2, 3, 4, 5], start: 5, deleteCount: 9, newElements: [10, 11] }, + { array: [0, 1, 2, 3, 4, 5], start: 3, deleteCount: 2, newElements: [3, 4, 5] }, + + // Remove + { array: [], start: 1, deleteCount: 1 }, + { array: [0, 1, 2, 3], start: 1, deleteCount: 1 }, + { array: [0, 1, 2, 3], start: 10, deleteCount: 1 }, + { array: [0, 1, 2, 3], start: 1, deleteCount: undefined }, + { array: [0, 1, 2, 3], start: 4 }, + { array: [0, 1, 2, 3, 4, 5], start: 3 }, + { array: [0, 1, 2, 3, 4, 5], start: -3 }, + { array: [0, 1, 2, 3, 4, 5], start: -2 }, + { array: [0, 1, 2, 3, 4, 5], start: 2, deleteCount: 2 }, + { array: [0, 1, 2, 3, 4, 5, 6, 7, 8], start: 5, deleteCount: 9, newElements: [10, 11] }, +])("array.splice (%p)", ({ array, start, deleteCount, newElements = [] }) => { + util.testFunction` + const array = ${util.valueToString(array)}; + array.splice(${util.valuesToString([start, deleteCount, ...newElements])}); + return array; + `.expectToMatchJsResult(); +}); + +test.each([ + { array: [], args: [[]] }, + { array: [1, 2, 3], args: [[]] }, + { array: [1, 2, 3], args: [[4]] }, + { array: [1, 2, 3], args: [[4, 5]] }, + { array: [1, 2, 3], args: [[4, 5]] }, + { array: [1, 2, 3], args: [4, [5]] }, + { array: [1, 2, 3], args: [4, [5, 6]] }, + { array: [1, 2, 3], args: [4, [5, 6], 7] }, + { array: [1, 2, 3], args: ["test", [5, 6], 7, ["test1", "test2"]] }, + { array: [1, 2, "test"], args: ["test", ["test1", "test2"]] }, +])("array.concat (%p)", ({ array, args }) => { + util.testFunction` + const array: any[] = ${util.valueToString(array)}; + return array.concat(${util.valuesToString(args)}); + `.expectToMatchJsResult(); +}); + +test.each([ + { array: [] }, + { array: ["test1"] }, + { array: ["test1", "test2"] }, + { array: ["test1", "test2"], separator: ";" }, + { array: ["test1", "test2"], separator: "" }, +])("array.join (%p)", ({ array, separator }) => { + util.testExpression`${util.valueToString(array)}.join(${util.valueToString(separator)})`.expectToMatchJsResult(); +}); + +test.each([ + { array: [], args: ["test1"] }, + { array: ["test1"], args: ["test1"] }, + { array: ["test1", "test2"], args: ["test2"] }, + { array: ["test1", "test2", "test3"], args: ["test3", 1] }, + { array: ["test1", "test2", "test3"], args: ["test1", 2] }, + { array: ["test1", "test2", "test3"], args: ["test1", -2] }, + { array: ["test1", "test2", "test3"], args: ["test1", 12] }, +])("array.indexOf (%p)", ({ array, args }) => { + util.testExpression`${util.valueToString(array)}.indexOf(${util.valuesToString(args)})`.expectToMatchJsResult(); +}); + +test.each([{ args: [1] }, { args: [1, 2, 3] }])("array.push (%p)", ({ args }) => { + util.testFunction` + const array = [0]; + const value = array.push(${util.valuesToString(args)}); + return { array, value }; + `.expectToMatchJsResult(); +}); + +// tslint:disable-next-line: no-null-keyword +test.each([{ array: [1, 2, 3], expected: [3, 2] }, { array: [1, 2, 3, null], expected: [3, 2] }])( + "array.pop (%p)", + ({ array, expected }) => { + util.testFunction` + const array = ${util.valueToString(array)}; + const value = array.pop(); + return [value, array.length]; + `.expectToEqual(expected); + } +); + +test.each([{ array: [1, 2, 3] }, { array: [1, 2, 3, 4] }, { array: [1] }, { array: [] }])( + "array.reverse (%p)", + ({ array }) => { + util.testFunction` + const array = ${util.valueToString(array)}; + array.reverse(); + return array; + `.expectToMatchJsResult(); + } +); + +test.each([{ array: [1, 2, 3] }, { array: [1] }, { array: [] }])("array.shift (%p)", ({ array }) => { + util.testFunction` + const array = ${util.valueToString(array)}; + const value = array.shift(); + return { array, value }; + `.expectToMatchJsResult(); +}); + +test.each([ + { array: [3, 4, 5], args: [1, 2] }, + { array: [], args: [] }, + { array: [1], args: [] }, + { array: [], args: [1] }, +])("array.unshift (%p)", ({ array, args }) => { + util.testFunction` + const array = ${util.valueToString(array)}; + const value = array.unshift(${util.valuesToString(args)}); + return { array, value }; + `.expectToMatchJsResult(); +}); + +test.each([{ array: [4, 5, 3, 2, 1] }, { array: [1] }, { array: [] }])("array.sort (%p)", ({ array }) => { + util.testFunctionTemplate` + const array = ${array}; + array.sort(); + return array; + `.expectToMatchJsResult(); +}); + +test.each([ + { array: [1, 2, 3, 4, 5], compare: (a: number, b: number) => a - b }, + { array: ["4", "5", "3", "2", "1"], compare: (a: string, b: string) => Number(a) - Number(b) }, + { array: ["4", "5", "3", "2", "1"], compare: (a: string, b: string) => Number(b) - Number(a) }, +])("array.sort with compare function (%p)", ({ array, compare }) => { + util.testFunctionTemplate` + const array = ${array}; + array.sort(${compare}); + return array; + `.expectToMatchJsResult(); +}); + +test.each([ + { array: [1, [2, 3], 4], expected: [1, 2, 3, 4] }, + { array: [1, [2, 3], 4], depth: 0, expected: [1, [2, 3], 4] }, + { array: [1, [[2], [3]], 4], expected: [1, [2], [3], 4] }, + { array: [1, [[[2], [3]]], 4], depth: Infinity, expected: [1, 2, 3, 4] }, +])("array.flat (%p)", ({ array, depth, expected }) => { + // TODO: Node 12 + util.testExpressionTemplate`${array}.flat(${depth})`.expectToEqual(expected); +}); + +test.each([ + { array: [1, [2, 3], [4]], map: (value: T) => value, expected: [1, 2, 3, 4] }, + { array: [1, 2, 3], map: (v: number) => v * 2, expected: [2, 4, 6] }, + { array: [1, 2, 3], map: (v: number) => [v, v * 2], expected: [1, 2, 2, 4, 3, 6] }, + { array: [1, 2, 3], map: (v: number) => [v, [v]], expected: [1, [1], 2, [2], 3, [3]] }, + { array: [1, 2, 3], map: (v: number, i: number) => [v * 2 * i], expected: [0, 4, 12] }, +])("array.flatMap (%p)", ({ array, map, expected }) => { + // TODO: Node 12 + util.testExpressionTemplate`${array}.flatMap(${map})`.expectToEqual(expected); +}); + +test.each<[[(total: number, currentItem: number, index: number, array: number[]) => number, number?]]>([ + [[(total, currentItem) => total + currentItem]], + [[(total, currentItem) => total * currentItem]], + [[(total, currentItem) => total + currentItem, 10]], + [[(total, currentItem) => total * currentItem, 10]], + [[(total, _, index, array) => total + array[index]]], + [[(a, b) => a + b]], +])("array.reduce (%p)", args => { + util.testExpression`[1, 3, 5, 7].reduce(${util.valuesToString(args)})`.expectToMatchJsResult(); +}); + +const genericChecks = [ + "function generic(array: T)", + "function generic(array: T)", + "function generic(array: T[])", + "type ArrayType = number[]; function generic(array: T)", + "function generic(array: T & {})", + "function generic(array: T)", +]; + +test.each(genericChecks)("array constrained generic foreach (%p)", signature => { + const code = ` + ${signature}: number { + let sum = 0; + array.forEach(item => { + if (typeof item === "number") { + sum += item; + } + }); + return sum; + } + return generic([1, 2, 3]); + `; + expect(util.transpileAndExecute(code)).toBe(6); +}); + +test.each(genericChecks)("array constrained generic length (%p)", signature => { + const code = ` + ${signature}: number { + return array.length; + } + return generic([1, 2, 3]); + `; + expect(util.transpileAndExecute(code)).toBe(3); +}); diff --git a/test/unit/builtins/console.spec.ts b/test/unit/builtins/console.spec.ts new file mode 100644 index 000000000..7e7f68e9e --- /dev/null +++ b/test/unit/builtins/console.spec.ts @@ -0,0 +1,58 @@ +import * as util from "../../util"; + +const compilerOptions = { lib: ["lib.esnext.d.ts", "lib.dom.d.ts"] }; + +test.each([ + "console.log()", + 'console.log("Hello")', + 'console.log("Hello %s", "there")', + 'console.log("Hello %%s", "there")', + 'console.log("Hello", "There")', +])("console.log (%p)", code => { + util.testFunction(code) + .setOptions(compilerOptions) + .expectLuaToMatchSnapshot(); +}); + +test.each([ + "console.trace()", + 'console.trace("message")', + 'console.trace("Hello %s", "there")', + 'console.trace("Hello %%s", "there")', + 'console.trace("Hello", "there")', +])("console.trace (%p)", code => { + util.testFunction(code) + .setOptions(compilerOptions) + .expectLuaToMatchSnapshot(); +}); + +test.each([ + "console.assert(false)", + 'console.assert(false, "message")', + 'console.assert(false, "message %s", "info")', + 'console.assert(false, "message %%s", "info")', + 'console.assert(false, "message", "more")', +])("console.assert (%p)", code => { + util.testFunction(code) + .setOptions(compilerOptions) + .expectLuaToMatchSnapshot(); +}); + +test("console.differentiation", () => { + util.testModule` + export class Console { + public test() { + return 42; + } + } + + function test() { + const console = new Console(); + return console.test(); + } + + export const result = test(); + ` + .setReturnExport("result") + .expectToMatchJsResult(); +}); diff --git a/test/unit/builtins/globalThis.spec.ts b/test/unit/builtins/globalThis.spec.ts new file mode 100644 index 000000000..85463f7ff --- /dev/null +++ b/test/unit/builtins/globalThis.spec.ts @@ -0,0 +1,32 @@ +import * as util from "../../util"; + +test("equals _G", () => { + util.testExpression`globalThis === _G`.setTsHeader("declare const _G: typeof globalThis;").expectToEqual(true); +}); + +test("registers global symbol", () => { + util.testFunction` + globalThis.foo = "bar"; + return foo; + ` + .setTsHeader("declare global { var foo: string }") + .expectToEqual("bar"); +}); + +test("uses global symbol", () => { + util.testFunction` + foo = "bar"; + return globalThis.foo; + ` + .setTsHeader("declare global { var foo: string }") + .expectToEqual("bar"); +}); + +test("function call", () => { + util.testFunction` + foo = () => "bar"; + return globalThis.foo(); + ` + .setTsHeader("declare global { var foo: () => string }") + .expectToEqual("bar"); +}); diff --git a/test/unit/builtins/loading.spec.ts b/test/unit/builtins/loading.spec.ts new file mode 100644 index 000000000..d08f846e9 --- /dev/null +++ b/test/unit/builtins/loading.spec.ts @@ -0,0 +1,53 @@ +import * as tstl from "../../../src"; +import * as TSTLErrors from "../../../src/TSTLErrors"; +import * as util from "../../util"; + +describe("luaLibImport", () => { + test("inline", () => { + util.testExpression`[0].push(1)` + .setOptions({ luaLibImport: tstl.LuaLibImportKind.Inline }) + .tap(builder => expect(builder.getMainLuaCodeChunk()).not.toContain(`require("lualib_bundle")`)) + .expectToMatchJsResult(); + }); + + test("require", () => { + util.testExpression`[0].push(1)` + .setOptions({ luaLibImport: tstl.LuaLibImportKind.Require }) + .tap(builder => expect(builder.getMainLuaCodeChunk()).toContain(`require("lualib_bundle")`)) + .expectToMatchJsResult(); + }); + + test("always", () => { + util.testModule`` + .setOptions({ luaLibImport: tstl.LuaLibImportKind.Always }) + .tap(builder => expect(builder.getMainLuaCodeChunk()).toContain(`require("lualib_bundle")`)) + .expectToEqual(undefined); + }); +}); + +test.each([tstl.LuaLibImportKind.Inline, tstl.LuaLibImportKind.None, tstl.LuaLibImportKind.Require])( + "should not include lualib without code (%p)", + luaLibImport => { + util.testModule``.setOptions({ luaLibImport }).tap(builder => expect(builder.getMainLuaCodeChunk()).toBe("")); + } +); + +test("lualib should not include tstl header", () => { + util.testExpression`[0].push(1)`.tap(builder => + expect(builder.getMainLuaCodeChunk()).not.toContain("Generated with") + ); +}); + +describe("Unknown builtin property", () => { + test("access", () => { + util.testExpression`Math.unknownProperty` + .disableSemanticCheck() + .expectToHaveDiagnosticOfError(TSTLErrors.UnsupportedProperty("Math", "unknownProperty", util.nodeStub)); + }); + + test("function call", () => { + util.testExpression`[].unknownFunction()` + .disableSemanticCheck() + .expectToHaveDiagnosticOfError(TSTLErrors.UnsupportedProperty("array", "unknownFunction", util.nodeStub)); + }); +}); diff --git a/test/unit/lualib/map.spec.ts b/test/unit/builtins/map.spec.ts similarity index 100% rename from test/unit/lualib/map.spec.ts rename to test/unit/builtins/map.spec.ts diff --git a/test/unit/builtins/math.spec.ts b/test/unit/builtins/math.spec.ts new file mode 100644 index 000000000..6dcd0749a --- /dev/null +++ b/test/unit/builtins/math.spec.ts @@ -0,0 +1,26 @@ +import * as util from "../../util"; + +test.each([ + "Math.cos()", + "Math.sin()", + "Math.min()", + "Math.atan2(2, 3)", + "Math.log2(3)", + "Math.log10(3)", + "const x = Math.log2(3)", + "const x = Math.log10(3)", + "Math.log1p(3)", + "Math.round(3.3)", + "Math.PI", +])("%s", code => { + // TODO: Remove? + util.testFunction(code) + .disableSemanticCheck() + .expectLuaToMatchSnapshot(); +}); + +test.each(["E", "LN10", "LN2", "LOG10E", "LOG2E", "SQRT1_2", "SQRT2"])("Math.%s", constant => { + util.testExpression`Math.${constant}`.tap(builder => { + expect(builder.getLuaExecutionResult()).toBeCloseTo(builder.getJsExecutionResult()); + }); +}); diff --git a/test/unit/builtins/numbers.spec.ts b/test/unit/builtins/numbers.spec.ts new file mode 100644 index 000000000..4369f22c6 --- /dev/null +++ b/test/unit/builtins/numbers.spec.ts @@ -0,0 +1,57 @@ +import * as util from "../../util"; + +test.each([ + "NaN === NaN", + "NaN !== NaN", + "NaN + NaN", + "NaN - NaN", + "NaN * NaN", + "NaN / NaN", + "NaN + 1", + "1 + NaN", + "1 / NaN", + "NaN * 0", + + "Infinity", + "Infinity - Infinity", + "Infinity / -1", + "Infinity * -1", + "Infinity + 1", + "Infinity - 1", +])("%s", code => { + util.testExpression(code).expectToMatchJsResult(); +}); + +test.skip.each(["NaN", "Infinity"])("%s reassignment", name => { + util.testFunction` + const ${name} = 1; + return ${name}; + `.expectToMatchJsResult(); +}); + +const numberCases = [-1, 0, 1, 1.5, Infinity, -Infinity]; +const stringCases = ["-1", "0", "1", "1.5", "Infinity", "-Infinity"]; +const restCases = [true, false, "", " ", "\t", "\n", "foo", {}]; +const cases = [...numberCases, ...stringCases, ...restCases]; + +describe("Number", () => { + test.each(cases)("constructor(%p)", value => { + util.testExpressionTemplate`Number(${value})`.expectToMatchJsResult(); + }); + + test.each(cases)("isNaN(%p)", value => { + util.testExpressionTemplate`Number.isNaN(${value} as any)`.expectToMatchJsResult(); + }); + + test.each(cases)("isFinite(%p)", value => { + util.testExpressionTemplate`Number.isFinite(${value} as any)`.expectToMatchJsResult(); + }); +}); + +test.each(cases)("isNaN(%p)", value => { + util.testExpressionTemplate`isNaN(${value} as any)`.expectToMatchJsResult(); +}); + +test.each(cases)("isFinite(%p)", value => { + util.testExpressionTemplate`isFinite(${value} as any)`.expectToMatchJsResult(); +}); diff --git a/test/unit/builtins/object.spec.ts b/test/unit/builtins/object.spec.ts new file mode 100644 index 000000000..ca1516254 --- /dev/null +++ b/test/unit/builtins/object.spec.ts @@ -0,0 +1,35 @@ +import * as util from "../../util"; + +test.each([ + { initial: { a: 3 }, args: [{}] }, + { initial: {}, args: [{ a: 3 }] }, + { initial: { a: 3 }, args: [{ a: 5 }] }, + { initial: { a: 3 }, args: [{ b: 5 }, { c: 7 }] }, +])("Object.assign (%p)", ({ initial, args }) => { + util.testExpression`Object.assign(${util.valueToString(initial)}, ${util.valuesToString( + args + )})`.expectToMatchJsResult(); +}); + +test.each([{}, { abc: 3 }, { abc: 3, def: "xyz" }])("Object.entries (%p)", obj => { + util.testExpressionTemplate`Object.entries(${obj})`.expectToMatchJsResult(); +}); + +test.each([{}, { abc: 3 }, { abc: 3, def: "xyz" }])("Object.keys (%p)", obj => { + util.testExpressionTemplate`Object.keys(${obj})`.expectToMatchJsResult(); +}); + +test.each([{}, { abc: "def" }, { abc: 3, def: "xyz" }])("Object.values (%p)", obj => { + util.testExpressionTemplate`Object.values(${obj})`.expectToMatchJsResult(); +}); + +// TODO: Jest 25: as const +test.each<[string, object]>([ + ["[]", []], + ['[["a", 1], ["b", 2]]', { a: 1, b: 2 }], + ['[["a", 1], ["a", 2]]', { a: 2 }], + ['new Map([["foo", "bar"]])', { foo: "bar" }], +])("Object.fromEntries(%s)", (entries, expected) => { + // TODO: Node 12 + util.testExpression`Object.fromEntries(${entries})`.expectToEqual(expected); +}); diff --git a/test/unit/builtins/set.spec.ts b/test/unit/builtins/set.spec.ts new file mode 100644 index 000000000..bac8773f8 --- /dev/null +++ b/test/unit/builtins/set.spec.ts @@ -0,0 +1,122 @@ +import * as util from "../../util"; + +test("set constructor", () => { + util.testFunction` + let myset = new Set(); + return myset.size; + `.expectToMatchJsResult(); +}); + +test("set iterable constructor", () => { + util.testFunction` + let myset = new Set(["a", "b"]); + return myset.has("a") || myset.has("b"); + `.expectToMatchJsResult(); +}); + +test("set iterable constructor set", () => { + util.testFunction` + let myset = new Set(new Set(["a", "b"])); + return myset.has("a") || myset.has("b"); + `.expectToMatchJsResult(); +}); + +test("set add", () => { + util.testFunction` + let myset = new Set(); + myset.add("a"); + return myset.has("a"); + `.expectToMatchJsResult(); +}); + +test("set clear", () => { + util.testFunction` + let myset = new Set(["a", "b"]); + myset.clear(); + return { size: myset.size, has: !myset.has("a") && !myset.has("b") }; + `.expectToMatchJsResult(); +}); + +test("set delete", () => { + util.testFunction` + let myset = new Set(["a", "b"]); + myset.delete("a"); + return myset.has("b") && !myset.has("a"); + `.expectToMatchJsResult(); +}); + +test("set entries", () => { + util.testFunction` + let myset = new Set([5, 6, 7]); + let count = 0; + for (var [key, value] of myset.entries()) { count += key + value; } + return count; + `.expectToMatchJsResult(); +}); + +test("set foreach", () => { + util.testFunction` + let myset = new Set([2, 3, 4]); + let count = 0; + myset.forEach(i => { count += i; }); + return count; + `.expectToMatchJsResult(); +}); + +test("set foreach keys", () => { + util.testFunction` + let myset = new Set([2, 3, 4]); + let count = 0; + myset.forEach((value, key) => { count += key; }); + return count; + `.expectToMatchJsResult(); +}); + +test("set has", () => { + util.testFunction` + let myset = new Set(["a", "c"]); + return myset.has("a"); + `.expectToMatchJsResult(); +}); + +test("set has false", () => { + util.testFunction` + let myset = new Set(); + return myset.has("a"); + `.expectToMatchJsResult(); +}); + +test("set has null", () => { + util.testFunction` + let myset = new Set(["a", "c"]); + return myset.has(null); + `.expectToMatchJsResult(); +}); + +test("set keys", () => { + util.testFunction` + let myset = new Set([5, 6, 7]); + let count = 0; + for (var key of myset.keys()) { count += key; } + return count; + `.expectToMatchJsResult(); +}); + +test("set values", () => { + util.testFunction` + let myset = new Set([5, 6, 7]); + let count = 0; + for (var value of myset.values()) { count += value; } + return count; + `.expectToMatchJsResult(); +}); + +test.each([ + `let m = new Set()`, + `let m = new Set(); m.add(1)`, + `let m = new Set([1, 2])`, + `let m = new Set([1, 2]); m.clear()`, + `let m = new Set([1, 2]); m.delete(2)`, +])("set size (%p)", code => { + util.testFunction`${code}; return m.size`.expectToMatchJsResult(); +}); diff --git a/test/unit/builtins/string.spec.ts b/test/unit/builtins/string.spec.ts new file mode 100644 index 000000000..af5b46762 --- /dev/null +++ b/test/unit/builtins/string.spec.ts @@ -0,0 +1,264 @@ +import * as TSTLErrors from "../../../src/TSTLErrors"; +import * as util from "../../util"; + +test("Unsupported string function", () => { + util.testExpression`"test".testThisIsNoMember()` + .disableSemanticCheck() + .expectToHaveDiagnosticOfError(TSTLErrors.UnsupportedProperty("string", "testThisIsNoMember", util.nodeStub)); +}); + +test("Supported lua string function", () => { + const tsHeader = ` + declare global { + interface String { + upper(): string; + } + } + `; + + util.testExpression`"test".upper()`.setTsHeader(tsHeader).expectToEqual("TEST"); +}); + +test.each([[], [65], [65, 66], [65, 66, 67]])("String.fromCharCode (%p)", (...args) => { + util.testExpression`String.fromCharCode(${util.valuesToString(args)})`.expectToMatchJsResult(); +}); + +test.each([ + { a: 12, b: 23, c: 43 }, + { a: "test", b: "hello", c: "bye" }, + { a: "test", b: 42, c: "bye" }, + { a: "test", b: 42, c: 12 }, + { a: "test", b: 42, c: true }, + { a: false, b: 42, c: 12 }, +])("String Concat Operator (%p)", ({ a, b, c }) => { + util.testFunctionTemplate` + let a = ${a}; + let b = ${b}; + let c = ${c}; + return a + " " + b + " test " + c; + `.expectToMatchJsResult(); +}); + +test.each([ + { input: "abcd", index: 3 }, + { input: "abcde", index: 3 }, + { input: "abcde", index: 0 }, + { input: "a", index: 0 }, +])("string index (%p)", ({ input, index }) => { + util.testExpressionTemplate`${input}[${index}]`.expectToMatchJsResult(); +}); + +test.each([ + { inp: "hello test", searchValue: "", replaceValue: "" }, + { inp: "hello test", searchValue: " ", replaceValue: "" }, + { inp: "hello test", searchValue: "hello", replaceValue: "" }, + { inp: "hello test", searchValue: "test", replaceValue: "" }, + { inp: "hello test", searchValue: "test", replaceValue: "world" }, + { inp: "hello test", searchValue: "test", replaceValue: "%world" }, + { inp: "hello %test", searchValue: "test", replaceValue: "world" }, + { inp: "hello %test", searchValue: "%test", replaceValue: "world" }, + { inp: "hello test", searchValue: "test", replaceValue: (): string => "a" }, + { inp: "hello test", searchValue: "test", replaceValue: (): string => "%a" }, + { inp: "aaa", searchValue: "a", replaceValue: "b" }, +])("string.replace (%p)", ({ inp, searchValue, replaceValue }) => { + util.testExpression`"${inp}".replace(${util.valuesToString([searchValue, replaceValue])})`.expectToMatchJsResult(); +}); + +test.each([["", ""], ["hello", "test"], ["hello", "test", "bye"], ["hello", 42], [42, "hello"]])( + "string.concat[+] (%p)", + (...elements) => { + util.testExpression(elements.map(e => util.valueToString(e)).join(" + ")).expectToMatchJsResult(); + } +); + +test.each([ + { str: "", args: ["", ""] }, + { str: "hello", args: ["test"] }, + { str: "hello", args: [] }, + { str: "hello", args: ["test", "bye"] }, +])("string.concatFct (%p)", ({ str, args }) => { + util.testExpression`"${str}".concat(${util.valuesToString(args)})`.expectToMatchJsResult(); +}); + +test.each([ + { inp: "hello test", searchValue: "" }, + { inp: "hello test", searchValue: "t" }, + { inp: "hello test", searchValue: "h" }, + { inp: "hello test", searchValue: "invalid" }, + { inp: "hello.test", searchValue: "." }, +])("string.indexOf (%p)", ({ inp, searchValue }) => { + util.testExpressionTemplate`${inp}.indexOf(${searchValue})`.expectToMatchJsResult(); +}); + +test.each([ + { inp: "hello test", searchValue: "t", offset: 5 }, + { inp: "hello test", searchValue: "t", offset: 6 }, + { inp: "hello test", searchValue: "t", offset: 7 }, + { inp: "hello test", searchValue: "h", offset: 4 }, +])("string.indexOf with offset (%p)", ({ inp, searchValue, offset }) => { + util.testExpressionTemplate`${inp}.indexOf(${searchValue}, ${offset})`.expectToMatchJsResult(); +}); + +test.each([{ inp: "hello test", searchValue: "t", x: 4, y: 3 }, { inp: "hello test", searchValue: "h", x: 3, y: 4 }])( + "string.indexOf with offset expression (%p)", + ({ inp, searchValue, x, y }) => { + util.testExpressionTemplate`${inp}.indexOf(${searchValue}, 2 > 1 && ${x} || ${y})`.expectToMatchJsResult(); + } +); + +test.each([ + { inp: "hello test", args: [] }, + { inp: "hello test", args: [0] }, + { inp: "hello test", args: [1] }, + { inp: "hello test", args: [1, 2] }, + { inp: "hello test", args: [1, 5] }, +])("string.slice (%p)", ({ inp, args }) => { + util.testExpression`"${inp}".slice(${util.valuesToString(args)})`.expectToMatchJsResult(); +}); + +test.each([ + { inp: "hello test", args: [0] }, + { inp: "hello test", args: [1] }, + { inp: "hello test", args: [1, 2] }, + { inp: "hello test", args: [1, 5] }, +])("string.substring (%p)", ({ inp, args }) => { + util.testExpression`"${inp}".substring(${util.valuesToString(args)})`.expectToMatchJsResult(); +}); + +test.each([{ inp: "hello test", start: 1, ignored: 0 }, { inp: "hello test", start: 3, ignored: 0, end: 5 }])( + "string.substring with expression (%p)", + ({ inp, start, ignored, end }) => { + const paramStr = `2 > 1 && ${start} || ${ignored}` + (end ? `, ${end}` : ""); + util.testExpression`"${inp}".substring(${paramStr})`.expectToMatchJsResult(); + } +); + +test.each([ + { inp: "hello test", args: [0] }, + { inp: "hello test", args: [1] }, + { inp: "hello test", args: [1, 2] }, + { inp: "hello test", args: [1, 5] }, +])("string.substr (%p)", ({ inp, args }) => { + util.testExpression`"${inp}".substr(${util.valuesToString(args)})`.expectToMatchJsResult(); +}); + +test.each([{ inp: "hello test", start: 1, ignored: 0 }, { inp: "hello test", start: 3, ignored: 0, end: 2 }])( + "string.substr with expression (%p)", + ({ inp, start, ignored, end }) => { + const paramStr = `2 > 1 && ${start} || ${ignored}` + (end ? `, ${end}` : ""); + const result = util.transpileAndExecute(`return "${inp}".substr(${paramStr})`); + + expect(result).toBe(inp.substr(start, end)); + } +); + +test.each(["", "h", "hello"])("string.length (%p)", input => { + util.testExpressionTemplate`${input}.length`.expectToMatchJsResult(); +}); + +test.each(["hello TEST"])("string.toLowerCase (%p)", inp => { + util.testExpressionTemplate`${inp}.toLowerCase()`.expectToMatchJsResult(); +}); + +test.each(["hello test"])("string.toUpperCase (%p)", inp => { + util.testExpressionTemplate`${inp}.toUpperCase()`.expectToMatchJsResult(); +}); + +test.each([ + { inp: "hello test", separator: "" }, + { inp: "hello test", separator: " " }, + { inp: "hello test", separator: "h" }, + { inp: "hello test", separator: "t" }, + { inp: "hello test", separator: "l" }, + { inp: "hello test", separator: "invalid" }, + { inp: "hello test", separator: "hello test" }, +])("string.split (%p)", ({ inp, separator }) => { + util.testExpressionTemplate`${inp}.split(${separator})`.expectToMatchJsResult(); +}); + +test.each([ + { inp: "hello test", index: 1 }, + { inp: "hello test", index: 2 }, + { inp: "hello test", index: 3 }, + { inp: "hello test", index: 99 }, +])("string.charAt (%p)", ({ inp, index }) => { + util.testExpressionTemplate`${inp}.charAt(${index})`.expectToMatchJsResult(); +}); + +test.each([{ inp: "hello test", index: 1 }, { inp: "hello test", index: 2 }, { inp: "hello test", index: 3 }])( + "string.charCodeAt (%p)", + ({ inp, index }) => { + util.testExpressionTemplate`${inp}.charCodeAt(${index})`.expectToMatchJsResult(); + } +); + +test.each([ + { inp: "hello test", index: 1, ignored: 0 }, + { inp: "hello test", index: 1, ignored: 2 }, + { inp: "hello test", index: 3, ignored: 2 }, + { inp: "hello test", index: 3, ignored: 99 }, +])("string.charAt with expression (%p)", ({ inp, index, ignored }) => { + util.testExpressionTemplate`${inp}.charAt(2 > 1 && ${index} || ${ignored})`.expectToMatchJsResult(); +}); + +test.each<{ inp: string; args: Parameters }>([ + { inp: "hello test", args: [""] }, + { inp: "hello test", args: ["hello"] }, + { inp: "hello test", args: ["test"] }, + { inp: "hello test", args: ["test", 6] }, +])("string.startsWith (%p)", ({ inp, args }) => { + util.testExpression`"${inp}".startsWith(${util.valuesToString(args)})`.expectToMatchJsResult(); +}); + +test.each<{ inp: string; args: Parameters }>([ + { inp: "hello test", args: [""] }, + { inp: "hello test", args: ["test"] }, + { inp: "hello test", args: ["hello"] }, + { inp: "hello test", args: ["hello", 5] }, +])("string.endsWith (%p)", ({ inp, args }) => { + const argsString = util.valuesToString(args); + util.testExpression`"${inp}".endsWith(${argsString})`.expectToMatchJsResult(); +}); + +test.each([ + { inp: "hello test", count: 0 }, + { inp: "hello test", count: 1 }, + { inp: "hello test", count: 2 }, + { inp: "hello test", count: 1.1 }, + { inp: "hello test", count: 1.5 }, + { inp: "hello test", count: 1.9 }, +])("string.repeat (%p)", ({ inp, count }) => { + util.testExpression`"${inp}".repeat(${count})`.expectToMatchJsResult(); +}); + +const padCases = [ + { inp: "foo", args: [0] }, + { inp: "foo", args: [3] }, + { inp: "foo", args: [5] }, + { inp: "foo", args: [4, " "] }, + { inp: "foo", args: [10, " "] }, + { inp: "foo", args: [5, "1234"] }, + { inp: "foo", args: [5.9, "1234"] }, + { inp: "foo", args: [NaN] }, +]; + +test.each(padCases)("string.padStart (%p)", ({ inp, args }) => { + util.testExpression`"${inp}".padStart(${util.valuesToString(args)})`.expectToMatchJsResult(); +}); + +test.each(padCases)("string.padEnd (%p)", ({ inp, args }) => { + util.testExpression`"${inp}".padEnd(${util.valuesToString(args)})`.expectToMatchJsResult(); +}); + +test.each([ + "function generic(string: T)", + "type StringType = string; function generic(string: T)", +])("string constrained generic foreach (%p)", signature => { + const code = ` + ${signature}: number { + return string.length; + } + return generic("string"); + `; + expect(util.transpileAndExecute(code)).toBe(6); +}); diff --git a/test/unit/builtins/symbol.spec.ts b/test/unit/builtins/symbol.spec.ts new file mode 100644 index 000000000..75da67a59 --- /dev/null +++ b/test/unit/builtins/symbol.spec.ts @@ -0,0 +1,38 @@ +import * as util from "../../util"; + +test.each([undefined, 1, "name"])("symbol.toString() (%p)", description => { + util.testExpression`Symbol(${util.valueToString(description)}).toString()`.expectToMatchJsResult(); +}); + +test.each([undefined, 1, "name"])("symbol.description (%p)", description => { + // TODO: Supported since node 11 + util.testExpression`Symbol(${util.valueToString(description)}).description`.expectToEqual(description); +}); + +test("symbol uniqueness", () => { + util.testExpression`Symbol("a") === Symbol("a")`.expectToMatchJsResult(); +}); + +test("Symbol.for", () => { + // TODO: Supported since node 11 + util.testExpression(`Symbol.for("name").description`).expectToEqual("name"); +}); + +test("Symbol.for non-uniqueness", () => { + util.testExpression`Symbol.for("a") === Symbol.for("a")`.expectToMatchJsResult(); +}); + +test("Symbol.keyFor", () => { + util.testFunction` + const sym = Symbol.for("a"); + Symbol.for("b"); + return Symbol.keyFor(sym); + `.expectToMatchJsResult(); +}); + +test("Symbol.keyFor empty", () => { + util.testFunction` + Symbol.for("a"); + return Symbol.keyFor(Symbol()); + `.expectToMatchJsResult(); +}); diff --git a/test/unit/lualib/weakMap.spec.ts b/test/unit/builtins/weakMap.spec.ts similarity index 100% rename from test/unit/lualib/weakMap.spec.ts rename to test/unit/builtins/weakMap.spec.ts diff --git a/test/unit/lualib/weakSet.spec.ts b/test/unit/builtins/weakSet.spec.ts similarity index 100% rename from test/unit/lualib/weakSet.spec.ts rename to test/unit/builtins/weakSet.spec.ts diff --git a/test/unit/classDecorator.spec.ts b/test/unit/classDecorator.spec.ts deleted file mode 100644 index 8d7afd8fa..000000000 --- a/test/unit/classDecorator.spec.ts +++ /dev/null @@ -1,199 +0,0 @@ -import * as TSTLErrors from "../../src/TSTLErrors"; -import * as util from "../util"; - -test("Class decorator with no parameters", () => { - const source = ` - function SetBool(constructor: T) { - return class extends constructor { - decoratorBool = true; - } - } - - @SetBool - class TestClass { - public decoratorBool = false; - } - - const classInstance = new TestClass(); - return classInstance.decoratorBool; - `; - - const result = util.transpileAndExecute(source); - expect(result).toBe(true); -}); - -test("Class decorator with parameters", () => { - const source = ` - function SetNum(numArg: number) { - return {}>(constructor: T) => { - return class extends constructor { - decoratorNum = numArg; - }; - }; - } - - @SetNum(420) - class TestClass { - public decoratorNum; - } - - const classInstance = new TestClass(); - return classInstance.decoratorNum; - `; - - const result = util.transpileAndExecute(source); - expect(result).toBe(420); -}); - -test("Class decorator with variable parameters", () => { - const source = ` - function SetNumbers(...numArgs: number[]) { - return {}>(constructor: T) => { - return class extends constructor { - decoratorNums = new Set(numArgs); - }; - }; - } - - @SetNumbers(120, 30, 54) - class TestClass { - public decoratorNums; - } - - const classInstance = new TestClass(); - let sum = 0; - for (const value of classInstance.decoratorNums) { - sum += value; - } - return sum; - `; - - const result = util.transpileAndExecute(source); - expect(result).toBe(204); -}); - -test("Multiple class decorators", () => { - const source = ` - function SetTen(constructor: T) { - return class extends constructor { - decoratorTen = 10; - } - } - - function SetNum(numArg: number) { - return {}>(constructor: T) => { - return class extends constructor { - decoratorNum = numArg; - }; - }; - } - - @SetTen - @SetNum(410) - class TestClass { - public decoratorTen; - public decoratorNum; - } - - const classInstance = new TestClass(); - return classInstance.decoratorNum + classInstance.decoratorTen; - `; - - const result = util.transpileAndExecute(source); - expect(result).toBe(420); -}); - -test("Class decorator with inheritance", () => { - const source = ` - function SetTen(constructor: T) { - return class extends constructor { - decoratorTen = 10; - } - } - - function SetNum(numArg: number) { - return {}>(constructor: T) => { - return class extends constructor { - decoratorNum = numArg; - }; - }; - } - - class TestClass { - public decoratorTen = 0; - public decoratorNum = 0; - } - - @SetTen - @SetNum(410) - class SubTestClass extends TestClass {} - - const classInstance = new SubTestClass(); - return classInstance.decoratorNum + classInstance.decoratorTen; - `; - - const result = util.transpileAndExecute(source); - expect(result).toBe(420); -}); - -test("Class decorators are applied in order and executed in reverse order", () => { - const source = ` - const order = []; - - function SetString(stringArg: string) { - order.push("eval " + stringArg); - return {}>(constructor: T) => { - order.push("execute " + stringArg); - return class extends constructor { - decoratorString = stringArg; - }; - }; - } - - @SetString("fox") - @SetString("jumped") - @SetString("over dog") - class TestClass { - public static decoratorString = ""; - } - - const inst = new TestClass(); - return order.join(" "); - `; - - const result = util.transpileAndExecute(source); - expect(result).toBe("eval fox eval jumped eval over dog execute over dog execute jumped execute fox"); -}); - -test("Throws error if decorator function has void context", () => { - const source = ` - function SetBool(this: void, constructor: T) { - return class extends constructor { - decoratorBool = true; - } - } - - @SetBool - class TestClass { - public decoratorBool = false; - } - - const classInstance = new TestClass(); - return classInstance.decoratorBool; - `; - - expect(() => util.transpileAndExecute(source)).toThrowExactError(TSTLErrors.InvalidDecoratorContext(util.nodeStub)); -}); - -test("Exported class decorator", () => { - const code = ` - function decorator(c: T): T { - c.bar = "foobar"; - return c; - } - - @decorator - export class Foo {}`; - - expect(util.transpileExecuteAndReturnExport(code, "Foo.bar")).toBe("foobar"); -}); diff --git a/test/unit/accessors.spec.ts b/test/unit/classes/accessors.spec.ts similarity index 76% rename from test/unit/accessors.spec.ts rename to test/unit/classes/accessors.spec.ts index fd63233df..5f9232246 100644 --- a/test/unit/accessors.spec.ts +++ b/test/unit/classes/accessors.spec.ts @@ -1,19 +1,18 @@ -import * as util from "../util"; +import * as util from "../../util"; test("get accessor", () => { - const code = ` + util.testFunction` class Foo { _foo = "foo"; get foo() { return this._foo; } } const f = new Foo(); return f.foo; - `; - expect(util.transpileAndExecute(code)).toBe("foo"); + `.expectToMatchJsResult(); }); test("get accessor in base class", () => { - const code = ` + util.testFunction` class Foo { _foo = "foo"; get foo() { return this._foo; } @@ -21,12 +20,11 @@ test("get accessor in base class", () => { class Bar extends Foo {} const b = new Bar(); return b.foo; - `; - expect(util.transpileAndExecute(code)).toBe("foo"); + `.expectToMatchJsResult(); }); -test("get accessor override", () => { - const code = ` +test.skip("get accessor override", () => { + util.testFunction` class Foo { _foo = "foo"; foo = "foo"; @@ -36,12 +34,11 @@ test("get accessor override", () => { } const b = new Bar(); return b.foo; - `; - expect(util.transpileAndExecute(code)).toBe("foobar"); + `.expectToMatchJsResult(); }); -test("get accessor overridden", () => { - const code = ` +test.skip("get accessor overridden", () => { + util.testFunction` class Foo { _foo = "foo"; get foo() { return this._foo; } @@ -51,12 +48,11 @@ test("get accessor overridden", () => { } const b = new Bar(); return b.foo; - `; - expect(util.transpileAndExecute(code)).toBe("bar"); + `.expectToMatchJsResult(); }); test("get accessor override accessor", () => { - const code = ` + util.testFunction` class Foo { _foo = "foo"; get foo() { return this._foo; } @@ -67,12 +63,11 @@ test("get accessor override accessor", () => { } const b = new Bar(); return b.foo; - `; - expect(util.transpileAndExecute(code)).toBe("bar"); + `.expectToMatchJsResult(); }); test("get accessor from interface", () => { - const code = ` + util.testFunction` class Foo { _foo = "foo"; get foo() { return this._foo; } @@ -82,12 +77,11 @@ test("get accessor from interface", () => { } const b: Bar = new Foo(); return b.foo; - `; - expect(util.transpileAndExecute(code)).toBe("foo"); + `.expectToMatchJsResult(); }); test("set accessor", () => { - const code = ` + util.testFunction` class Foo { _foo = "foo"; set foo(val: string) { this._foo = val; } @@ -95,12 +89,11 @@ test("set accessor", () => { const f = new Foo(); f.foo = "bar" return f._foo; - `; - expect(util.transpileAndExecute(code)).toBe("bar"); + `.expectToMatchJsResult(); }); test("set accessor in base class", () => { - const code = ` + util.testFunction` class Foo { _foo = "foo"; set foo(val: string) { this._foo = val; } @@ -109,12 +102,11 @@ test("set accessor in base class", () => { const b = new Bar(); b.foo = "bar" return b._foo; - `; - expect(util.transpileAndExecute(code)).toBe("bar"); + `.expectToMatchJsResult(); }); test("set accessor override", () => { - const code = ` + util.testFunction` class Foo { _foo = "foo"; foo = "foo"; @@ -125,12 +117,11 @@ test("set accessor override", () => { const b = new Bar(); b.foo = "bar" return b._foo; - `; - expect(util.transpileAndExecute(code)).toBe("bar"); + `.expectToMatchJsResult(); }); test("set accessor overridden", () => { - const code = ` + util.testFunction` class Foo { _foo = "baz"; set foo(val: string) { this._foo = val; } @@ -142,12 +133,11 @@ test("set accessor overridden", () => { const fooOriginal = b._foo; b.foo = "bar" return fooOriginal + b._foo; - `; - expect(util.transpileAndExecute(code)).toBe("foobar"); + `.expectToMatchJsResult(); }); test("set accessor override accessor", () => { - const code = ` + util.testFunction` class Foo { _foo = "foo"; set foo(val: string) { this._foo = "foo"; } @@ -158,12 +148,11 @@ test("set accessor override accessor", () => { const b = new Bar(); b.foo = "bar" return b._foo; - `; - expect(util.transpileAndExecute(code)).toBe("bar"); + `.expectToMatchJsResult(); }); test("set accessor from interface", () => { - const code = ` + util.testFunction` class Foo { _foo = "foo"; set foo(val: string) { this._foo = val; } @@ -175,12 +164,11 @@ test("set accessor from interface", () => { const b: Bar = new Foo(); b.foo = "bar" return b._foo; - `; - expect(util.transpileAndExecute(code)).toBe("bar"); + `.expectToMatchJsResult(); }); test("get/set accessors", () => { - const code = ` + util.testFunction` class Foo { _foo = "foo"; get foo() { return this._foo; } @@ -190,12 +178,11 @@ test("get/set accessors", () => { const fooOriginal = f.foo; f.foo = "bar"; return fooOriginal + f.foo; - `; - expect(util.transpileAndExecute(code)).toBe("foobar"); + `.expectToMatchJsResult(); }); test("get/set accessors in base class", () => { - const code = ` + util.testFunction` class Foo { _foo = "foo"; get foo() { return this._foo; } @@ -206,35 +193,32 @@ test("get/set accessors in base class", () => { const fooOriginal = b.foo; b.foo = "bar" return fooOriginal + b.foo; - `; - expect(util.transpileAndExecute(code)).toBe("foobar"); + `.expectToMatchJsResult(); }); test("static get accessor", () => { - const code = ` + util.testFunction` class Foo { static _foo = "foo"; static get foo() { return this._foo; } } return Foo.foo; - `; - expect(util.transpileAndExecute(code)).toBe("foo"); + `.expectToMatchJsResult(); }); test("static get accessor in base class", () => { - const code = ` + util.testFunction` class Foo { static _foo = "foo"; static get foo() { return this._foo; } } class Bar extends Foo {} return Bar.foo; - `; - expect(util.transpileAndExecute(code)).toBe("foo"); + `.expectToMatchJsResult(); }); test("static get accessor override", () => { - const code = ` + util.testFunction` class Foo { static _foo = "foo"; static foo = "foo"; @@ -243,12 +227,11 @@ test("static get accessor override", () => { static get foo() { return this._foo + "bar"; } } return Bar.foo; - `; - expect(util.transpileAndExecute(code)).toBe("foobar"); + `.expectToMatchJsResult(); }); -test("static get accessor overridden", () => { - const code = ` +test.skip("static get accessor overridden", () => { + util.testFunction` class Foo { static _foo = "foo"; static get foo() { return this._foo; } @@ -257,12 +240,11 @@ test("static get accessor overridden", () => { static foo = "bar"; } return Bar.foo; - `; - expect(util.transpileAndExecute(code)).toBe("bar"); + `.expectToMatchJsResult(); }); test("static get accessor override accessor", () => { - const code = ` + util.testFunction` class Foo { static _foo = "foo"; static get foo() { return this._foo; } @@ -272,12 +254,11 @@ test("static get accessor override accessor", () => { static get foo() { return this._bar; } } return Bar.foo; - `; - expect(util.transpileAndExecute(code)).toBe("bar"); + `.expectToMatchJsResult(); }); test("static get accessor from interface", () => { - const code = ` + util.testFunction` class Foo { static _foo = "foo"; static get foo() { return this._foo; } @@ -287,24 +268,22 @@ test("static get accessor from interface", () => { } const b: Bar = Foo; return b.foo; - `; - expect(util.transpileAndExecute(code)).toBe("foo"); + `.expectToMatchJsResult(); }); test("static set accessor", () => { - const code = ` + util.testFunction` class Foo { static _foo = "foo"; static set foo(val: string) { this._foo = val; } } Foo.foo = "bar" return Foo._foo; - `; - expect(util.transpileAndExecute(code)).toBe("bar"); + `.expectToMatchJsResult(); }); test("static set accessor in base class", () => { - const code = ` + util.testFunction` class Foo { static _foo = "foo"; static set foo(val: string) { this._foo = val; } @@ -312,12 +291,11 @@ test("static set accessor in base class", () => { class Bar extends Foo {} Bar.foo = "bar" return Bar._foo; - `; - expect(util.transpileAndExecute(code)).toBe("bar"); + `.expectToMatchJsResult(); }); test("static set accessor override", () => { - const code = ` + util.testFunction` class Foo { static _foo = "foo"; static foo = "foo"; @@ -327,12 +305,11 @@ test("static set accessor override", () => { } Bar.foo = "bar" return Bar._foo; - `; - expect(util.transpileAndExecute(code)).toBe("bar"); + `.expectToMatchJsResult(); }); test("static set accessor overridden", () => { - const code = ` + util.testFunction` class Foo { static _foo = "baz"; static set foo(val: string) { this._foo = val; } @@ -343,12 +320,11 @@ test("static set accessor overridden", () => { const fooOriginal = Bar._foo; Bar.foo = "bar" return fooOriginal + Bar._foo; - `; - expect(util.transpileAndExecute(code)).toBe("foobar"); + `.expectToMatchJsResult(); }); test("static set accessor override accessor", () => { - const code = ` + util.testFunction` class Foo { static _foo = "foo"; static set foo(val: string) { this._foo = "foo"; } @@ -358,12 +334,11 @@ test("static set accessor override accessor", () => { } Bar.foo = "bar" return Bar._foo; - `; - expect(util.transpileAndExecute(code)).toBe("bar"); + `.expectToMatchJsResult(); }); test("static set accessor from interface", () => { - const code = ` + util.testFunction` class Foo { static _foo = "foo"; static set foo(val: string) { this._foo = val; } @@ -375,12 +350,11 @@ test("static set accessor from interface", () => { const b: Bar = Foo; b.foo = "bar" return b._foo; - `; - expect(util.transpileAndExecute(code)).toBe("bar"); + `.expectToMatchJsResult(); }); test("static get/set accessors", () => { - const code = ` + util.testFunction` class Foo { static _foo = "foo"; static get foo() { return this._foo; } @@ -389,12 +363,11 @@ test("static get/set accessors", () => { const fooOriginal = Foo.foo; Foo.foo = "bar"; return fooOriginal + Foo.foo; - `; - expect(util.transpileAndExecute(code)).toBe("foobar"); + `.expectToMatchJsResult(); }); test("static get/set accessors in base class", () => { - const code = ` + util.testFunction` class Foo { static _foo = "foo"; static get foo() { return this._foo; } @@ -404,6 +377,5 @@ test("static get/set accessors in base class", () => { const fooOriginal = Bar.foo; Bar.foo = "bar" return fooOriginal + Bar.foo; - `; - expect(util.transpileAndExecute(code)).toBe("foobar"); + `.expectToMatchJsResult(); }); diff --git a/test/unit/class.spec.ts b/test/unit/classes/classes.spec.ts similarity index 96% rename from test/unit/class.spec.ts rename to test/unit/classes/classes.spec.ts index 37f1db79a..3f156c93a 100644 --- a/test/unit/class.spec.ts +++ b/test/unit/classes/classes.spec.ts @@ -1,6 +1,6 @@ import * as ts from "typescript"; -import * as TSTLErrors from "../../src/TSTLErrors"; -import * as util from "../util"; +import * as TSTLErrors from "../../../src/TSTLErrors"; +import * as util from "../../util"; test("ClassFieldInitializer", () => { const result = util.transpileAndExecute( @@ -733,28 +733,6 @@ test.each([{ input: "(new Foo())", expectResult: "foo" }, { input: "Foo", expect } ); -test.each(["extension", "metaExtension"])("Class extends extension (%p)", extensionType => { - const code = ` - declare class A {} - /** @${extensionType} **/ - class B extends A {} - class C extends B {} - `; - expect(() => util.transpileString(code)).toThrowExactError(TSTLErrors.InvalidExtendsExtension(util.nodeStub)); -}); - -test.each(["extension", "metaExtension"])("Class construct extension (%p)", extensionType => { - const code = ` - declare class A {} - /** @${extensionType} **/ - class B extends A {} - const b = new B(); - `; - expect(() => util.transpileString(code)).toThrowExactError( - TSTLErrors.InvalidNewExpressionOnExtension(util.nodeStub) - ); -}); - test("Class static instance of self", () => { const code = ` class Foo { diff --git a/test/unit/classes/decorators.spec.ts b/test/unit/classes/decorators.spec.ts new file mode 100644 index 000000000..e5c44e51d --- /dev/null +++ b/test/unit/classes/decorators.spec.ts @@ -0,0 +1,127 @@ +import * as TSTLErrors from "../../../src/TSTLErrors"; +import * as util from "../../util"; + +test("Class decorator with no parameters", () => { + util.testFunction` + function setBool {}>(constructor: T) { + return class extends constructor { + decoratorBool = true; + } + } + + @setBool + class TestClass { + public decoratorBool = false; + } + + return new TestClass(); + `.expectToMatchJsResult(); +}); + +test("Class decorator with parameters", () => { + util.testFunction` + function setNum(numArg: number) { + return {}>(constructor: T) => { + return class extends constructor { + decoratorNum = numArg; + }; + }; + } + + @setNum(420) + class TestClass { + public decoratorNum; + } + + return new TestClass(); + `.expectToMatchJsResult(); +}); + +test("Multiple class decorators", () => { + util.testFunction` + function setTen {}>(constructor: T) { + return class extends constructor { + decoratorTen = 10; + } + } + + function setNum {}>(constructor: T) { + return class extends constructor { + decoratorNum = 410; + } + } + + @setTen + @setNum + class TestClass { + public decoratorTen; + public decoratorNum; + } + + return new TestClass(); + `.expectToMatchJsResult(); +}); + +test("Class decorator with inheritance", () => { + util.testFunction` + function setTen {}>(constructor: T) { + return class extends constructor { + decoratorTen = 10; + } + } + + class TestClass { + public decoratorTen = 0; + } + + @setTen + class SubTestClass extends TestClass { + public decoratorTen = 5; + } + + return new SubTestClass(); + `.expectToMatchJsResult(); +}); + +test("Class decorators are applied in order and executed in reverse order", () => { + util.testFunction` + const order = []; + + function pushOrder(index: number) { + order.push("eval " + index); + return (constructor: new (...args: any[]) => {}) => { + order.push("execute " + index); + }; + } + + @pushOrder(1) + @pushOrder(2) + @pushOrder(3) + class TestClass {} + + return order; + `.expectToMatchJsResult(); +}); + +test("Throws error if decorator function has void context", () => { + util.testFunction` + function SetBool(this: void, constructor: new (...args: any[]) => {}) {} + + @SetBool + class TestClass {} + `.expectToHaveDiagnosticOfError(TSTLErrors.InvalidDecoratorContext(util.nodeStub)); +}); + +test("Exported class decorator", () => { + util.testModule` + function decorator(c: T): T { + c.bar = "foobar"; + return c; + } + + @decorator + export class Foo {} + ` + .setReturnExport("Foo.bar") + .expectToMatchJsResult(); +}); diff --git a/test/unit/classes/instanceof.spec.ts b/test/unit/classes/instanceof.spec.ts new file mode 100644 index 000000000..fdbb6e36e --- /dev/null +++ b/test/unit/classes/instanceof.spec.ts @@ -0,0 +1,76 @@ +import * as util from "../../util"; + +test("instanceof", () => { + util.testFunction` + class myClass {} + const instance = new myClass(); + return instance instanceof myClass; + `.expectToMatchJsResult(); +}); + +test("instanceof inheritance", () => { + util.testFunction` + class myClass {} + class childClass extends myClass {} + const instance = new childClass(); + return instance instanceof myClass; + `.expectToMatchJsResult(); +}); + +test("instanceof inheritance false", () => { + util.testFunction` + class myClass {} + class childClass extends myClass {} + const instance = new myClass(); + return instance instanceof childClass; + `.expectToMatchJsResult(); +}); + +test("{} instanceof Object", () => { + util.testExpression`{} instanceof Object`.expectToMatchJsResult(); +}); + +test("function instanceof Object", () => { + util.testExpression`(() => {}) instanceof Object`.expectToMatchJsResult(); +}); + +test("null instanceof Object", () => { + util.testExpression`(null as any) instanceof Object`.expectToMatchJsResult(); +}); + +test("instanceof undefined", () => { + util.testExpression`{} instanceof (undefined as any)`.expectToMatchJsResult(true); +}); + +test("null instanceof Class", () => { + util.testFunction` + class myClass {} + return (null as any) instanceof myClass; + `.expectToMatchJsResult(); +}); + +test("instanceof export", () => { + util.testModule` + export class myClass {} + const instance = new myClass(); + export const result = instance instanceof myClass; + ` + .setReturnExport("result") + .expectToMatchJsResult(); +}); + +test("instanceof Symbol.hasInstance", () => { + util.testFunction` + class myClass { + static [Symbol.hasInstance]() { + return false; + } + } + + const instance = new myClass(); + const isInstanceOld = instance instanceof myClass; + myClass[Symbol.hasInstance] = () => true; + const isInstanceNew = instance instanceof myClass; + return { isInstanceOld, isInstanceNew }; + `.expectToMatchJsResult(); +}); diff --git a/test/unit/conditionals.spec.ts b/test/unit/conditionals.spec.ts index 04a285457..d5bc14792 100644 --- a/test/unit/conditionals.spec.ts +++ b/test/unit/conditionals.spec.ts @@ -2,36 +2,30 @@ import * as tstl from "../../src"; import * as TSTLErrors from "../../src/TSTLErrors"; import * as util from "../util"; -test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }])("if (%p)", ({ inp, expected }) => { - const result = util.transpileAndExecute( - `let input: number = ${inp}; +test.each([0, 1])("if (%p)", inp => { + util.testFunction` + let input: number = ${inp}; if (input === 0) { return 0; } - return 1;` - ); - - expect(result).toBe(expected); + return 1; + `.expectToMatchJsResult(); }); -test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }])("ifelse (%p)", ({ inp, expected }) => { - const result = util.transpileAndExecute( - `let input: number = ${inp}; - if (input === 0) { - return 0; - } else { - return 1; - }` - ); - - expect(result).toBe(expected); +test.each([0, 1])("ifelse (%p)", inp => { + util.testFunction` + let input: number = ${inp}; + if (input === 0) { + return 0; + } else { + return 1; + } + `.expectToMatchJsResult(); }); -test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }, { inp: 2, expected: 2 }, { inp: 3, expected: 3 }])( - "ifelseif (%p)", - ({ inp, expected }) => { - const result = util.transpileAndExecute( - `let input: number = ${inp}; +test.each([0, 1, 2, 3])("ifelseif (%p)", inp => { + util.testFunction` + let input: number = ${inp}; if (input === 0) { return 0; } else if (input === 1){ @@ -39,18 +33,13 @@ test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }, { inp: 2, expected: } else if (input === 2){ return 2; } - return 3;` - ); - - expect(result).toBe(expected); - } -); + return 3; + `.expectToMatchJsResult(); +}); -test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }, { inp: 2, expected: 2 }, { inp: 3, expected: 3 }])( - "ifelseifelse (%p)", - ({ inp, expected }) => { - const result = util.transpileAndExecute( - `let input: number = ${inp}; +test.each([0, 1, 2, 3])("ifelseifelse (%p)", inp => { + util.testFunction` + let input: number = ${inp}; if (input === 0) { return 0; } else if (input === 1){ @@ -59,18 +48,13 @@ test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }, { inp: 2, expected: return 2; } else { return 3; - }` - ); - - expect(result).toBe(expected); - } -); + } + `.expectToMatchJsResult(); +}); -test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }, { inp: 2, expected: 2 }, { inp: 3, expected: -1 }])( - "switch (%p)", - ({ inp, expected }) => { - const result = util.transpileAndExecute( - `let result: number = -1; +test.each([0, 1, 2, 3])("switch (%p)", inp => { + util.testFunction` + let result: number = -1; switch (${inp}) { case 0: @@ -83,18 +67,13 @@ test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }, { inp: 2, expected: result = 2; break; } - return result;` - ); - - expect(result).toBe(expected); - } -); + return result; + `.expectToMatchJsResult(); +}); -test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }, { inp: 2, expected: 2 }, { inp: 3, expected: -2 }])( - "switchdefault (%p)", - ({ inp, expected }) => { - const result = util.transpileAndExecute( - `let result: number = -1; +test.each([0, 1, 2, 3])("switchdefault (%p)", inp => { + util.testFunction` + let result: number = -1; switch (${inp}) { case 0: @@ -110,24 +89,13 @@ test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }, { inp: 2, expected: result = -2; break; } - return result;` - ); - - expect(result).toBe(expected); - } -); + return result; + `.expectToMatchJsResult(); +}); -test.each([ - { inp: 0, expected: 1 }, - { inp: 0, expected: 1 }, - { inp: 2, expected: 4 }, - { inp: 3, expected: 4 }, - { inp: 4, expected: 4 }, - { inp: 5, expected: 15 }, - { inp: 7, expected: -2 }, -])("switchfallthrough (%p)", ({ inp, expected }) => { - const result = util.transpileAndExecute( - `let result: number = -1; +test.each([0, 0, 2, 3, 4, 5, 7])("switchfallthrough (%p)", inp => { + util.testFunction` + let result: number = -1; switch (${inp}) { case 0: @@ -152,24 +120,21 @@ test.each([ result = -2; break; } - return result;` - ); - expect(result).toBe(expected); + return result; + `.expectToMatchJsResult(); }); -test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }, { inp: 2, expected: 2 }, { inp: 3, expected: -2 }])( - "nestedSwitch (%p)", - ({ inp, expected }) => { - const result = util.transpileAndExecute( - `let result: number = -1; +test.each([0, 1, 2, 3])("nestedSwitch (%p)", inp => { + util.testFunction` + let result: number = -1; - switch (${inp}) { + switch (${inp} as number) { case 0: result = 0; break; case 1: - switch(${inp}) { + switch(${inp} as number) { case 0: result = 0; break; @@ -188,47 +153,37 @@ test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }, { inp: 2, expected: result = -2; break; } - return result;` - ); - - expect(result).toBe(expected); - } -); + return result; + `.expectToMatchJsResult(); +}); -test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 2 }, { inp: 2, expected: 2 }])( - "switchLocalScope (%p)", - ({ inp, expected }) => { - const result = util.transpileAndExecute( - `let result: number = -1; +test.each([0, 1, 2])("switchLocalScope (%p)", inp => { + util.testFunction` + let result: number = -1; - switch (${inp}) { - case 0: { - let x = 0; - result = 0; - break; - } - case 1: { - let x = 1; - result = x; - } - case 2: { - let x = 2; - result = x; - break; - } + switch (${inp}) { + case 0: { + let x = 0; + result = 0; + break; } - return result;` - ); - - expect(result).toBe(expected); - } -); + case 1: { + let x = 1; + result = x; + } + case 2: { + let x = 2; + result = x; + break; + } + } + return result; + `.expectToMatchJsResult(); +}); -test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }, { inp: 2, expected: 2 }, { inp: 3, expected: -1 }])( - "switchReturn (%p)", - ({ inp, expected }) => { - const result = util.transpileAndExecute( - `const result: number = -1; +test.each([0, 1, 2, 3])("switchReturn (%p)", inp => { + util.testFunction` + const result: number = -1; switch (${inp}) { case 0: @@ -240,18 +195,13 @@ test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }, { inp: 2, expected: return 2; break; } - return result;` - ); - - expect(result).toBe(expected); - } -); + return result; + `.expectToMatchJsResult(); +}); -test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }, { inp: 2, expected: 2 }, { inp: 3, expected: -1 }])( - "switchWithBrackets (%p)", - ({ inp, expected }) => { - const result = util.transpileAndExecute( - `let result: number = -1; +test.each([0, 1, 2, 3])("switchWithBrackets (%p)", inp => { + util.testFunction` + let result: number = -1; switch (${inp}) { case 0: { @@ -267,18 +217,13 @@ test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }, { inp: 2, expected: break; } } - return result;` - ); - - expect(result).toBe(expected); - } -); + return result; + `.expectToMatchJsResult(); +}); -test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }, { inp: 2, expected: 2 }, { inp: 3, expected: -1 }])( - "switchWithBracketsBreakInConditional (%p)", - ({ inp, expected }) => { - const result = util.transpileAndExecute( - `let result: number = -1; +test.each([0, 1, 2, 3])("switchWithBracketsBreakInConditional (%p)", inp => { + util.testFunction` + let result: number = -1; switch (${inp}) { case 0: { @@ -295,20 +240,15 @@ test.each([{ inp: 0, expected: 0 }, { inp: 1, expected: 1 }, { inp: 2, expected: break; } } - return result;` - ); - - expect(result).toBe(expected); - } -); + return result; + `.expectToMatchJsResult(); +}); -test.each([{ inp: 0, expected: 4 }, { inp: 1, expected: 0 }, { inp: 2, expected: 2 }, { inp: 3, expected: -1 }])( - "switchWithBracketsBreakInInternalLoop (%p)", - ({ inp, expected }) => { - const result = util.transpileAndExecute( - `let result: number = -1; +test.each([0, 1, 2, 3])("switchWithBracketsBreakInInternalLoop (%p)", inp => { + util.testFunction` + let result: number = -1; - switch (${inp}) { + switch (${inp} as number) { case 0: { result = 0; @@ -329,29 +269,64 @@ test.each([{ inp: 0, expected: 4 }, { inp: 1, expected: 0 }, { inp: 2, expected: break; } } - return result;` - ); - - expect(result).toBe(expected); - } -); - -test("If dead code after return", () => { - const result = util.transpileAndExecute(`if (true) { return 3; const b = 8; }`); + return result; + `.expectToMatchJsResult(); +}); - expect(result).toBe(3); +test("switch not allowed in 5.1", () => { + util.testFunction` + switch ("abc") {} + ` + .setOptions({ luaTarget: tstl.LuaTarget.Lua51 }) + .expectToHaveDiagnosticOfError( + TSTLErrors.UnsupportedForTarget("Switch statements", tstl.LuaTarget.Lua51, util.nodeStub) + ); }); -test("switch dead code after return", () => { - const result = util.transpileAndExecute( - `switch ("abc") { case "def": return 4; let abc = 4; case "abc": return 5; let def = 6; }` - ); +test.each([ + { input: "true ? 'a' : 'b'" }, + { input: "false ? 'a' : 'b'" }, + { input: "true ? false : true" }, + { input: "false ? false : true" }, + { input: "true ? literalValue : true" }, + { input: "true ? variableValue : true" }, + { input: "true ? maybeUndefinedValue : true" }, + { input: "true ? maybeBooleanValue : true" }, + { input: "true ? maybeUndefinedValue : true", options: { strictNullChecks: true } }, + { input: "true ? maybeBooleanValue : true", options: { strictNullChecks: true } }, + { input: "true ? undefined : true", options: { strictNullChecks: true } }, + { input: "true ? null : true", options: { strictNullChecks: true } }, + { input: "true ? false : true", options: { luaTarget: tstl.LuaTarget.Lua51 } }, + { input: "false ? false : true", options: { luaTarget: tstl.LuaTarget.Lua51 } }, + { input: "true ? undefined : true", options: { luaTarget: tstl.LuaTarget.Lua51 } }, + { input: "true ? false : true", options: { luaTarget: tstl.LuaTarget.LuaJIT } }, + { input: "false ? false : true", options: { luaTarget: tstl.LuaTarget.LuaJIT } }, + { input: "true ? undefined : true", options: { luaTarget: tstl.LuaTarget.LuaJIT } }, +])("Ternary operator (%p)", ({ input, options }) => { + util.testFunction` + const literalValue = "literal"; + let variableValue: string; + let maybeBooleanValue: string | boolean = false; + let maybeUndefinedValue: string | undefined; + return ${input}; + ` + .setOptions(options) + .expectToMatchJsResult(); +}); - expect(result).toBe(5); +test.each([ + { condition: true, lhs: 4, rhs: 5 }, + { condition: false, lhs: 4, rhs: 5 }, + { condition: 3, lhs: 4, rhs: 5 }, +])("Ternary Conditional (%p)", ({ condition, lhs, rhs }) => { + util.testExpressionTemplate`${condition} ? ${lhs} : ${rhs}`.expectToMatchJsResult(); }); -test("switch not allowed in 5.1", () => { - expect(() => util.transpileString(`switch ("abc") {}`, { luaTarget: tstl.LuaTarget.Lua51 })).toThrowExactError( - TSTLErrors.UnsupportedForTarget("Switch statements", tstl.LuaTarget.Lua51, util.nodeStub) - ); +test.each(["true", "false", "a < 4", "a == 8"])("Ternary Conditional Delayed (%p)", condition => { + util.testFunction` + let a = 3; + let delay = () => ${condition} ? a + 3 : a + 5; + a = 8; + return delay(); + `.expectToMatchJsResult(); }); diff --git a/test/unit/console.spec.ts b/test/unit/console.spec.ts deleted file mode 100644 index e82c23b04..000000000 --- a/test/unit/console.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import * as util from "../util"; - -const compilerOptions = { lib: ["lib.es2015.d.ts", "lib.dom.d.ts"] }; - -test.each([ - { inp: "console.log()", expected: "print()" }, - { inp: 'console.log("Hello")', expected: 'print("Hello")' }, - { inp: 'console.log("Hello %s", "there")', expected: 'print(\n string.format("Hello %s", "there")\n)' }, - { inp: 'console.log("Hello %%s", "there")', expected: 'print(\n string.format("Hello %%s", "there")\n)' }, - { inp: 'console.log("Hello", "There")', expected: 'print("Hello", "There")' }, -])("console.log (%p)", ({ inp, expected }) => { - expect(util.transpileString(inp, compilerOptions)).toBe(expected); -}); - -test.each([ - { - inp: "console.trace()", - expected: "print(\n debug.traceback()\n)", - }, - { - inp: 'console.trace("message")', - expected: 'print(\n debug.traceback("message")\n)', - }, - { - inp: 'console.trace("Hello %s", "there")', - expected: 'print(\n debug.traceback(\n string.format("Hello %s", "there")\n )\n)', - }, - { - inp: 'console.trace("Hello %%s", "there")', - expected: 'print(\n debug.traceback(\n string.format("Hello %%s", "there")\n )\n)', - }, - { - inp: 'console.trace("Hello", "there")', - expected: 'print(\n debug.traceback("Hello", "there")\n)', - }, -])("console.trace (%p)", ({ inp, expected }) => { - expect(util.transpileString(inp, compilerOptions)).toBe(expected); -}); - -test.each([ - { - inp: "console.assert(false)", - expected: "assert(false)", - }, - { - inp: 'console.assert(false, "message")', - expected: 'assert(false, "message")', - }, - { - inp: 'console.assert(false, "message %s", "info")', - expected: 'assert(\n false,\n string.format("message %s", "info")\n)', - }, - { - inp: 'console.assert(false, "message %%s", "info")', - expected: 'assert(\n false,\n string.format("message %%s", "info")\n)', - }, - { - inp: 'console.assert(false, "message", "more")', - expected: 'assert(false, "message", "more")', - }, -])("console.assert (%p)", ({ inp, expected }) => { - expect(util.transpileString(inp, compilerOptions)).toBe(expected); -}); - -test("console.differentiation", () => { - const result = util.transpileExecuteAndReturnExport( - ` - export class Console { - test() { return 42; } - } - - function test() { - const console = new Console(); - return console.test(); - } - - export const result = test(); - `, - "result", - compilerOptions - ); - expect(result).toBe(42); -}); diff --git a/test/unit/curry.spec.ts b/test/unit/curry.spec.ts deleted file mode 100644 index b39d2f3c6..000000000 --- a/test/unit/curry.spec.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as util from "../util"; - -test.each([{ x: 2, y: 3 }, { x: 5, y: 4 }])("curryingAdd (%p)", ({ x, y }) => { - const result = util.transpileAndExecute( - `let add = (x: number) => (y: number) => x + y; - return add(${x})(${y})` - ); - - expect(result).toBe(x + y); -}); diff --git a/test/unit/declarations.spec.ts b/test/unit/declarations.spec.ts deleted file mode 100644 index 415153e2c..000000000 --- a/test/unit/declarations.spec.ts +++ /dev/null @@ -1,135 +0,0 @@ -import * as util from "../util"; - -test("Declaration function call", () => { - const libLua = `function declaredFunction(x) return 3*x end`; - - const tsHeader = `declare function declaredFunction(this: void, x: number): number;`; - - const source = `return declaredFunction(2) + 4;`; - - const result = util.transpileAndExecute(source, undefined, libLua, tsHeader); - expect(result).toBe(10); -}); - -test("Declaration function call tupleReturn", () => { - const libLua = `function declaredFunction(x) return x, 2*x + 1 end`; - - const tsHeader = ` - /** @tupleReturn */ - declare function declaredFunction(this: void, x: number): [number, number]; - `; - - const source = ` - const tuple = declaredFunction(3); - const [destructedLeft, destructedRight] = declaredFunction(2); - return \`\${tuple[0] + destructedLeft},\${tuple[1] + destructedRight}\`; - `; - - const result = util.transpileAndExecute(source, undefined, libLua, tsHeader); - expect(result).toBe("5,12"); -}); - -test("Declaration namespace function call", () => { - const libLua = ` - myNameSpace = {} - function myNameSpace.declaredFunction(x) return 3*x end - `; - - const tsHeader = `declare namespace myNameSpace { function declaredFunction(this: void, x: number): number; }`; - - const source = `return myNameSpace.declaredFunction(2) + 4;`; - - const result = util.transpileAndExecute(source, undefined, libLua, tsHeader); - expect(result).toBe(10); -}); - -test("Declaration interface function call", () => { - const libLua = ` - myInterfaceInstance = {} - myInterfaceInstance.x = 10 - function myInterfaceInstance:declaredFunction(x) return self.x + 3*x end - `; - - const tsHeader = ` - declare interface MyInterface { - declaredFunction(x: number): number; - } - declare var myInterfaceInstance: MyInterface; - `; - - const source = `return myInterfaceInstance.declaredFunction(3);`; - - const result = util.transpileAndExecute(source, undefined, libLua, tsHeader); - expect(result).toBe(19); -}); - -test("Declaration function callback", () => { - const libLua = `function declaredFunction(callback) return callback(4) end`; - const tsHeader = `declare function declaredFunction(this: void, callback: (this: void, x: number) => number): number;`; - - const source = `return declaredFunction(x => 2 * x);`; - - const result = util.transpileAndExecute(source, undefined, libLua, tsHeader); - expect(result).toBe(8); -}); - -test("Declaration instance function callback", () => { - const libLua = ` - myInstance = {} - myInstance.x = 10 - function myInstance:declaredFunction(callback) return callback(self.x) end - `; - - const tsHeader = ` - declare interface MyInterface { - declaredFunction(callback: (this: void, x: number) => number): number; - } - declare var myInstance: MyInterface; - `; - - const source = `return myInstance.declaredFunction(x => 2 * x);`; - - const result = util.transpileAndExecute(source, undefined, libLua, tsHeader); - expect(result).toBe(20); -}); - -test("ImportEquals declaration", () => { - const header = ` - namespace outerNamespace { - export namespace innerNamespace { - export function func() { return "foo" } - } - }; - - import importedFunc = outerNamespace.innerNamespace.func; - `; - - const execution = `return importedFunc();`; - - const result = util.transpileAndExecute(execution, undefined, undefined, header); - expect(result).toEqual("foo"); -}); - -test("ImportEquals declaration ambient", () => { - const header = ` - declare namespace outerNamespace { - namespace innerNamespace { - function func(): string; - } - }; - - import importedFunc = outerNamespace.innerNamespace.func; - `; - - const luaHeader = `outerNamespace = { - innerNamespace = { - func = function() return "foo" end - } - } - `; - - const execution = `return importedFunc();`; - - const result = util.transpileAndExecute(execution, undefined, luaHeader, header); - expect(result).toEqual("foo"); -}); diff --git a/test/unit/decorators/compileMembersOnly.spec.ts b/test/unit/decorators/compileMembersOnly.spec.ts new file mode 100644 index 000000000..5db0fe974 --- /dev/null +++ b/test/unit/decorators/compileMembersOnly.spec.ts @@ -0,0 +1,34 @@ +import * as util from "../../util"; + +test("@compileMembersOnly", () => { + util.testFunction` + /** @compileMembersOnly */ + enum TestEnum { + A = 0, + B = 2, + C, + D = "D", + } + + return { A: TestEnum.A, B: TestEnum.B, C: TestEnum.C, D: TestEnum.D }; + ` + .tap(builder => expect(builder.getMainLuaCodeChunk()).not.toContain("TestEnum")) + .expectToMatchJsResult(); +}); + +test("@compileMembersOnly in a namespace", () => { + util.testModule` + namespace Test { + /** @compileMembersOnly */ + export enum TestEnum { + A = "A", + B = "B", + } + } + + export const A = Test.TestEnum.A; + ` + .setReturnExport("A") + .tap(builder => expect(builder.getMainLuaCodeChunk()).toContain("Test.A")) + .expectToEqual("A"); +}); diff --git a/test/unit/decoratorCustomConstructor.spec.ts b/test/unit/decorators/customConstructor.spec.ts similarity index 91% rename from test/unit/decoratorCustomConstructor.spec.ts rename to test/unit/decorators/customConstructor.spec.ts index 126c44e4f..48c473943 100644 --- a/test/unit/decoratorCustomConstructor.spec.ts +++ b/test/unit/decorators/customConstructor.spec.ts @@ -1,5 +1,5 @@ -import * as TSTLErrors from "../../src/TSTLErrors"; -import * as util from "../util"; +import * as TSTLErrors from "../../../src/TSTLErrors"; +import * as util from "../../util"; test("CustomCreate", () => { const luaHeader = ` diff --git a/test/unit/decorators/extension.spec.ts b/test/unit/decorators/extension.spec.ts new file mode 100644 index 000000000..e7717792e --- /dev/null +++ b/test/unit/decorators/extension.spec.ts @@ -0,0 +1,34 @@ +import * as TSTLErrors from "../../../src/TSTLErrors"; +import * as util from "../../util"; + +test.each(["extension", "metaExtension"])("Class extends extension (%p)", extensionType => { + const code = ` + declare class A {} + /** @${extensionType} **/ + class B extends A {} + class C extends B {} + `; + expect(() => util.transpileString(code)).toThrowExactError(TSTLErrors.InvalidExtendsExtension(util.nodeStub)); +}); + +test.each(["extension", "metaExtension"])("Class construct extension (%p)", extensionType => { + const code = ` + declare class A {} + /** @${extensionType} **/ + class B extends A {} + const b = new B(); + `; + expect(() => util.transpileString(code)).toThrowExactError( + TSTLErrors.InvalidNewExpressionOnExtension(util.nodeStub) + ); +}); + +test.each(["extension", "metaExtension"])("instanceof extension (%p)", extensionType => { + util.testModule` + declare class A {} + /** @${extensionType} **/ + class B extends A {} + declare const foo: any; + const result = foo instanceof B; + `.expectToHaveDiagnosticOfError(TSTLErrors.InvalidInstanceOfExtension(util.nodeStub)); +}); diff --git a/test/unit/decorators/forRange.spec.ts b/test/unit/decorators/forRange.spec.ts new file mode 100644 index 000000000..11f8aec85 --- /dev/null +++ b/test/unit/decorators/forRange.spec.ts @@ -0,0 +1,112 @@ +import * as ts from "typescript"; +import * as TSTLErrors from "../../../src/TSTLErrors"; +import * as util from "../../util"; + +test.each([ + { args: [1, 10], expectResult: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }, + { args: [1, 10, 2], expectResult: [1, 3, 5, 7, 9] }, + { args: [10, 1, -1], expectResult: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] }, + { args: [10, 1, -2], expectResult: [10, 8, 6, 4, 2] }, +])("@forRange loop", ({ args, expectResult }) => { + const tsHeader = "/** @forRange **/ declare function luaRange(i: number, j: number, k?: number): number[];"; + const code = ` + const results: number[] = []; + for (const i of luaRange(${args})) { + results.push(i); + } + return JSONStringify(results);`; + + const result = util.transpileAndExecute(code, undefined, undefined, tsHeader); + expect(JSON.parse(result)).toEqual(expectResult); +}); + +test("invalid non-ambient @forRange function", () => { + const code = ` + /** @forRange **/ function luaRange(i: number, j: number, k?: number): number[] { return []; } + for (const i of luaRange(1, 10, 2)) {}`; + + expect(() => util.transpileString(code)).toThrow( + TSTLErrors.InvalidForRangeCall( + ts.createEmptyStatement(), + "@forRange function can only be used as an iterable in a for...of loop." + ).message + ); +}); + +test.each([[1], [1, 2, 3, 4]])("invalid @forRange argument count", args => { + const code = ` + /** @forRange **/ declare function luaRange(...args: number[]): number[] { return []; } + for (const i of luaRange(${args})) {}`; + + expect(() => util.transpileString(code)).toThrow( + TSTLErrors.InvalidForRangeCall(ts.createEmptyStatement(), "@forRange function must take 2 or 3 arguments.") + .message + ); +}); + +test("invalid @forRange control variable", () => { + const code = ` + /** @forRange **/ declare function luaRange(i: number, j: number, k?: number): number[]; + let i: number; + for (i of luaRange(1, 10, 2)) {}`; + + expect(() => util.transpileString(code)).toThrow( + TSTLErrors.InvalidForRangeCall( + ts.createEmptyStatement(), + "@forRange loop must declare its own control variable." + ).message + ); +}); + +test("invalid @forRange argument type", () => { + const code = ` + /** @forRange **/ declare function luaRange(i: string, j: number): number[] { return []; } + for (const i of luaRange("foo", 2)) {}`; + + expect(() => util.transpileString(code)).toThrow( + TSTLErrors.InvalidForRangeCall(ts.createEmptyStatement(), "@forRange arguments must be number types.").message + ); +}); + +test("invalid @forRange destructuring", () => { + const code = ` + /** @forRange **/ declare function luaRange(i: number, j: number, k?: number): number[][]; + for (const [i] of luaRange(1, 10, 2)) {}`; + + expect(() => util.transpileString(code)).toThrow( + TSTLErrors.InvalidForRangeCall(ts.createEmptyStatement(), "@forRange loop cannot use destructuring.").message + ); +}); + +test("invalid @forRange return type", () => { + const code = ` + /** @forRange **/ declare function luaRange(i: number, j: number, k?: number): string[]; + for (const i of luaRange(1, 10)) {}`; + + expect(() => util.transpileString(code)).toThrow( + TSTLErrors.InvalidForRangeCall( + ts.createEmptyStatement(), + "@forRange function must return Iterable or Array." + ).message + ); +}); + +test.each([ + "const range = luaRange(1, 10);", + "console.log(luaRange);", + "luaRange.call(null, 0, 0, 0);", + "let array = [0, luaRange, 1];", + "const call: any; call(luaRange);", + "for (const i of [...luaRange(1, 10)]) {}", +])("invalid @forRange reference (%p)", statement => { + const code = ` + /** @forRange **/ declare function luaRange(i: number, j: number, k?: number): number[]; + ${statement}`; + + expect(() => util.transpileString(code)).toThrow( + TSTLErrors.InvalidForRangeCall( + ts.createEmptyStatement(), + "@forRange function can only be used as an iterable in a for...of loop." + ).message + ); +}); diff --git a/test/unit/decorators/luaIterator.spec.ts b/test/unit/decorators/luaIterator.spec.ts new file mode 100644 index 000000000..5aa24d8ae --- /dev/null +++ b/test/unit/decorators/luaIterator.spec.ts @@ -0,0 +1,262 @@ +import * as ts from "typescript"; +import * as tstl from "../../../src"; +import * as TSTLErrors from "../../../src/TSTLErrors"; +import * as util from "../../util"; + +test("forof lua iterator", () => { + const code = ` + const arr = ["a", "b", "c"]; + /** @luaIterator */ + interface Iter extends Iterable {} + function luaIter(): Iter { + let i = 0; + return (() => arr[i++]) as any; + } + let result = ""; + for (let e of luaIter()) { result += e; } + return result; + `; + const compilerOptions = { + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const result = util.transpileAndExecute(code, compilerOptions); + expect(result).toBe("abc"); +}); + +test("forof array lua iterator", () => { + const code = ` + const arr = ["a", "b", "c"]; + /** @luaIterator */ + interface Iter extends Array {} + function luaIter(): Iter { + let i = 0; + return (() => arr[i++]) as any; + } + let result = ""; + for (let e of luaIter()) { result += e; } + return result; + `; + const compilerOptions = { + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const result = util.transpileAndExecute(code, compilerOptions); + expect(result).toBe("abc"); +}); + +test("forof lua iterator with existing variable", () => { + const code = ` + const arr = ["a", "b", "c"]; + /** @luaIterator */ + interface Iter extends Iterable {} + function luaIter(): Iter { + let i = 0; + return (() => arr[i++]) as any; + } + let result = ""; + let e: string; + for (e of luaIter()) { result += e; } + return result; + `; + const compilerOptions = { + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const result = util.transpileAndExecute(code, compilerOptions); + expect(result).toBe("abc"); +}); + +test("forof lua iterator destructuring", () => { + const code = ` + const arr = ["a", "b", "c"]; + /** @luaIterator */ + interface Iter extends Iterable<[string, string]> {} + function luaIter(): Iter { + let i = 0; + return (() => arr[i] && [i.toString(), arr[i++]]) as any; + } + let result = ""; + for (let [a, b] of luaIter()) { result += a + b; } + return result; + `; + const compilerOptions = { + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const result = util.transpileAndExecute(code, compilerOptions); + expect(result).toBe("0a1b2c"); +}); + +test("forof lua iterator destructuring with existing variables", () => { + const code = ` + const arr = ["a", "b", "c"]; + /** @luaIterator */ + interface Iter extends Iterable<[string, string]> {} + function luaIter(): Iter { + let i = 0; + return (() => arr[i] && [i.toString(), arr[i++]]) as any; + } + let result = ""; + let a: string; + let b: string; + for ([a, b] of luaIter()) { result += a + b; } + return result; + `; + const compilerOptions = { + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const result = util.transpileAndExecute(code, compilerOptions); + expect(result).toBe("0a1b2c"); +}); + +test("forof lua iterator tuple-return", () => { + const code = ` + const arr = ["a", "b", "c"]; + /** @luaIterator */ + /** @tupleReturn */ + interface Iter extends Iterable<[string, string]> {} + function luaIter(): Iter { + let i = 0; + /** @tupleReturn */ + function iter() { return arr[i] && [i.toString(), arr[i++]] || []; } + return iter as any; + } + let result = ""; + for (let [a, b] of luaIter()) { result += a + b; } + return result; + `; + const compilerOptions = { + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const result = util.transpileAndExecute(code, compilerOptions); + expect(result).toBe("0a1b2c"); +}); + +test("forof lua iterator tuple-return with existing variables", () => { + const code = ` + const arr = ["a", "b", "c"]; + /** @luaIterator */ + /** @tupleReturn */ + interface Iter extends Iterable<[string, string]> {} + function luaIter(): Iter { + let i = 0; + /** @tupleReturn */ + function iter() { return arr[i] && [i.toString(), arr[i++]] || []; } + return iter as any; + } + let result = ""; + let a: string; + let b: string; + for ([a, b] of luaIter()) { result += a + b; } + return result; + `; + const compilerOptions = { + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const result = util.transpileAndExecute(code, compilerOptions); + expect(result).toBe("0a1b2c"); +}); + +test("forof lua iterator tuple-return single variable", () => { + const code = ` + /** @luaIterator */ + /** @tupleReturn */ + interface Iter extends Iterable<[string, string]> {} + declare function luaIter(): Iter; + for (let x of luaIter()) {} + `; + const compilerOptions = { + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + expect(() => util.transpileString(code, compilerOptions)).toThrowExactError( + TSTLErrors.UnsupportedNonDestructuringLuaIterator(util.nodeStub) + ); +}); + +test("forof lua iterator tuple-return single existing variable", () => { + const code = ` + /** @luaIterator */ + /** @tupleReturn */ + interface Iter extends Iterable<[string, string]> {} + declare function luaIter(): Iter; + let x: [string, string]; + for (x of luaIter()) {} + `; + const compilerOptions = { + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + expect(() => util.transpileString(code, compilerOptions)).toThrowExactError( + TSTLErrors.UnsupportedNonDestructuringLuaIterator(util.nodeStub) + ); +}); + +test("forof forwarded lua iterator", () => { + const code = ` + const arr = ["a", "b", "c"]; + /** @luaIterator */ + interface Iter extends Iterable {} + function luaIter(): Iter { + let i = 0; + function iter() { return arr[i++]; } + return iter as any; + } + function forward() { + const iter = luaIter(); + return iter; + } + let result = ""; + for (let a of forward()) { result += a; } + return result; + `; + const compilerOptions = { + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const result = util.transpileAndExecute(code, compilerOptions); + expect(result).toBe("abc"); +}); + +test("forof forwarded lua iterator with tupleReturn", () => { + const code = ` + const arr = ["a", "b", "c"]; + /** @luaIterator */ + /** @tupleReturn */ + interface Iter extends Iterable<[string, string]> {} + function luaIter(): Iter { + let i = 0; + /** @tupleReturn */ + function iter() { return arr[i] && [i.toString(), arr[i++]] || []; } + return iter as any; + } + function forward() { + const iter = luaIter(); + return iter; + } + let result = ""; + for (let [a, b] of forward()) { result += a + b; } + return result; + `; + const compilerOptions = { + luaLibImport: tstl.LuaLibImportKind.Require, + luaTarget: tstl.LuaTarget.Lua53, + target: ts.ScriptTarget.ES2015, + }; + const result = util.transpileAndExecute(code, compilerOptions); + expect(result).toBe("0a1b2c"); +}); diff --git a/test/unit/luaTable.spec.ts b/test/unit/decorators/luaTable.spec.ts similarity index 98% rename from test/unit/luaTable.spec.ts rename to test/unit/decorators/luaTable.spec.ts index ddc5f81a3..6419cc735 100644 --- a/test/unit/luaTable.spec.ts +++ b/test/unit/decorators/luaTable.spec.ts @@ -1,5 +1,5 @@ -import * as TSTLErrors from "../../src/TSTLErrors"; -import * as util from "../util"; +import * as TSTLErrors from "../../../src/TSTLErrors"; +import * as util from "../../util"; const tableLibClass = ` /** @luaTable */ diff --git a/test/unit/decoratorMetaExtension.spec.ts b/test/unit/decorators/metaExtension.spec.ts similarity index 93% rename from test/unit/decoratorMetaExtension.spec.ts rename to test/unit/decorators/metaExtension.spec.ts index 20bf7fb4d..cac222c97 100644 --- a/test/unit/decoratorMetaExtension.spec.ts +++ b/test/unit/decorators/metaExtension.spec.ts @@ -1,5 +1,5 @@ -import * as TSTLErrors from "../../src/TSTLErrors"; -import * as util from "../util"; +import * as TSTLErrors from "../../../src/TSTLErrors"; +import * as util from "../../util"; test("MetaExtension", () => { const tsHeader = ` diff --git a/test/unit/tuples.spec.ts b/test/unit/decorators/tupleReturn.spec.ts similarity index 52% rename from test/unit/tuples.spec.ts rename to test/unit/decorators/tupleReturn.spec.ts index 779bd8666..66df74aba 100644 --- a/test/unit/tuples.spec.ts +++ b/test/unit/decorators/tupleReturn.spec.ts @@ -1,214 +1,96 @@ -import * as util from "../util"; +import * as util from "../../util"; -test("Tuple loop", () => { - const result = util.transpileAndExecute( - `const tuple: [number, number, number] = [3,5,1]; - let count = 0; - for (const value of tuple) { count += value; } - return count;` - ); - - expect(result).toBe(9); -}); - -test("Tuple foreach", () => { - const result = util.transpileAndExecute( - `const tuple: [number, number, number] = [3,5,1]; - let count = 0; - tuple.forEach(v => count += v); - return count;` - ); - - expect(result).toBe(9); -}); - -test("Tuple access", () => { - const result = util.transpileAndExecute( - `const tuple: [number, number, number] = [3,5,1]; - return tuple[1];` - ); - - expect(result).toBe(5); -}); - -test("Readonly Tuple access", () => { - const result = util.transpileAndExecute( - `const tuple: readonly [number, number, number] = [3,5,1]; - return tuple[1];` - ); - - expect(result).toBe(5); -}); - -test("Tuple union access", () => { - const result = util.transpileAndExecute( - `function makeTuple(): [number, number, number] | [string, string, string] { return [3,5,1]; } - const tuple = makeTuple(); - return tuple[1];` - ); - expect(result).toBe(5); -}); - -test("Tuple intersection access", () => { - const result = util.transpileAndExecute( - `type I = [number, number, number] & {foo: string}; - function makeTuple(): I { - let t = [3,5,1]; - (t as I).foo = "bar"; - return (t as I); - } - const tuple = makeTuple(); - return tuple[1];` - ); - expect(result).toBe(5); -}); - -test("Tuple Destruct", () => { - const result = util.transpileAndExecute( - `function tuple(): [number, number, number] { return [3,5,1]; } - const [a,b,c] = tuple(); - return b;` - ); - - expect(result).toBe(5); -}); - -test("Tuple Destruct Array Literal", () => { - const code = ` - const [a,b,c] = [3,5,1]; - return b;`; - - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe(5); -}); - -test("Tuple Destruct Array Literal Extra Values", () => { - const code = ` - let result = ""; - const set = () => { result = "bar"; }; - const [a] = ["foo", set()]; - return a + result;`; - - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe("foobar"); -}); - -test("Tuple length", () => { - const result = util.transpileAndExecute( - `const tuple: [number, number, number] = [3,5,1]; - return tuple.length;` - ); - - expect(result).toBe(3); -}); +const expectNoUnpack: util.TapCallback = builder => expect(builder.getMainLuaCodeChunk()).not.toContain("unpack"); test("Tuple Return Access", () => { - const code = ` + util.testFunction` /** @tupleReturn */ - function tuple(): [number, number, number] { return [3,5,1]; } - return tuple()[2];`; - - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe(1); + function tuple(): [number, number, number] { return [3, 5, 1]; } + return tuple()[2]; + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Return Destruct Declaration", () => { - const code = ` + util.testFunction` /** @tupleReturn */ function tuple(): [number, number, number] { return [3,5,1]; } const [,b,c] = tuple(); - return b;`; - - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe(5); + return b; + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Return Destruct Assignment", () => { - const code = ` + util.testFunction` /** @tupleReturn */ function tuple(): [number, number] { return [3,6]; } let [a,b] = [1,2]; [b,a] = tuple(); - return a - b;`; - - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe(3); + return a - b; + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Static Method Return Destruct", () => { - const code = ` + util.testFunction` class Test { /** @tupleReturn */ static tuple(): [number, number, number] { return [3,5,1]; } } const [a,b,c] = Test.tuple(); - return b;`; - - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe(5); + return b; + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Static Function Property Return Destruct", () => { - const code = ` + util.testFunction` class Test { /** @tupleReturn */ static tuple: () => [number, number, number] = () => [3,5,1]; } const [a,b,c] = Test.tuple(); - return b;`; - - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe(5); + return b; + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Non-Static Method Return Destruct", () => { - const code = ` + util.testFunction` class Test { /** @tupleReturn */ tuple(): [number, number, number] { return [3,5,1]; } } const t = new Test(); const [a,b,c] = t.tuple(); - return b;`; - - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe(5); + return b; + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Non-Static Function Property Return Destruct", () => { - const code = ` + util.testFunction` class Test { /** @tupleReturn */ tuple: () => [number, number, number] = () => [3,5,1]; } const t = new Test(); const [a,b,c] = t.tuple(); - return b;`; - - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe(5); + return b; + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Interface Method Return Destruct", () => { - const code = ` + util.testFunction` interface Test { /** @tupleReturn */ tuple(): [number, number, number]; @@ -217,16 +99,14 @@ test("Tuple Interface Method Return Destruct", () => { tuple() { return [3,5,1]; } }; const [a,b,c] = t.tuple(); - return b;`; - - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe(5); + return b; + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Interface Function Property Return Destruct", () => { - const code = ` + util.testFunction` interface Test { /** @tupleReturn */ tuple: () => [number, number, number]; @@ -235,116 +115,100 @@ test("Tuple Interface Function Property Return Destruct", () => { tuple: () => [3,5,1] }; const [a,b,c] = t.tuple(); - return b;`; - - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe(5); + return b; + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Object Literal Method Return Destruct", () => { - const code = ` + util.testFunction` const t = { /** @tupleReturn */ tuple() { return [3,5,1]; } }; const [a,b,c] = t.tuple(); - return b;`; - - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe(5); + return b; + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Object Literal Function Property Return Destruct", () => { - const code = ` + util.testFunction` const t = { /** @tupleReturn */ tuple: () => [3,5,1] }; const [a,b,c] = t.tuple(); - return b;`; - - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe(5); + return b; + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Return on Arrow Function", () => { - const code = ` + util.testFunction` const fn = /** @tupleReturn */ (s: string) => [s, "bar"]; const [a, b] = fn("foo"); return a + b; - `; - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe("foobar"); + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Return Inference", () => { - const code = ` + util.testFunction` /** @tupleReturn */ interface Fn { (s: string): [string, string] } const fn: Fn = s => [s, "bar"]; const [a, b] = fn("foo"); return a + b; - `; - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe("foobar"); + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Return Inference as Argument", () => { - const code = ` + util.testFunction` /** @tupleReturn */ interface Fn { (s: string): [string, string] } function foo(fn: Fn) { const [a, b] = fn("foo"); return a + b; } return foo(s => [s, "bar"]); - `; - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe("foobar"); + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Return Inference as Elipsis Argument", () => { - const code = ` + util.testFunction` /** @tupleReturn */ interface Fn { (s: string): [string, string] } - function foo(a: number, ...fn: Fn[]) { + function foo(_: number, ...fn: Fn[]) { const [a, b] = fn[0]("foo"); return a + b; } - return foo(7, s => [s, "bar"]); - `; - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe("foobar"); + return foo(0, s => [s, "bar"]); + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Return Inference as Elipsis Tuple Argument", () => { - const code = ` + util.testFunction` /** @tupleReturn */ interface Fn { (s: string): [string, string] } - function foo(a: number, ...fn: [number, Fn]) { + function foo(_: number, ...fn: [number, Fn]) { const [a, b] = fn[1]("foo"); return a + b; } - return foo(7, 17, s => [s, "bar"]); - `; - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe("foobar"); + return foo(0, 0, s => [s, "bar"]); + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Return in Spread", () => { - const code = ` + util.testFunction` /** @tupleReturn */ function foo(): [string, string] { return ["foo", "bar"]; } @@ -352,56 +216,48 @@ test("Tuple Return in Spread", () => { return a + b; } return bar(...foo()); - `; - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe("foobar"); + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Return on Type Alias", () => { - const code = ` + util.testFunction` /** @tupleReturn */ type Fn = () => [number, number]; const fn: Fn = () => [1, 2]; const [a, b] = fn(); return a + b; - `; - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe(3); + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Return on Interface", () => { - const code = ` + util.testFunction` /** @tupleReturn */ interface Fn { (): [number, number]; } const fn: Fn = () => [1, 2]; const [a, b] = fn(); return a + b; - `; - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe(3); + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Return on Interface Signature", () => { - const code = ` + util.testFunction` interface Fn { /** @tupleReturn */ (): [number, number]; } const fn: Fn = () => [1, 2]; const [a, b] = fn(); return a + b; - `; - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe(3); + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Return on Overload", () => { - const code = ` + util.testFunction` function fn(a: number): number; /** @tupleReturn */ function fn(a: string, b: string): [string, string]; function fn(a: number | string, b?: string): number | [string, string] { @@ -414,15 +270,13 @@ test("Tuple Return on Overload", () => { const a = fn(3); const [b, c] = fn("foo", "bar"); return a + b + c - `; - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe("3foobar"); + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Return on Interface Overload", () => { - const code = ` + util.testFunction` interface Fn { (a: number): number; /** @tupleReturn */ (a: string, b: string): [string, string]; @@ -437,20 +291,18 @@ test("Tuple Return on Interface Overload", () => { const a = fn(3); const [b, c] = fn("foo", "bar"); return a + b + c - `; - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe("3foobar"); + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Return on Interface Method Overload", () => { - const code = ` + util.testFunction` interface Foo { foo(a: number): number; /** @tupleReturn */ foo(a: string, b: string): [string, string]; } - const bar = ({ + const bar = { foo: (a: number | string, b?: string): number | [string, string] => { if (typeof a === "number") { return a; @@ -458,15 +310,13 @@ test("Tuple Return on Interface Method Overload", () => { return [a, b as string]; } } - }) as Foo; + } as Foo; const a = bar.foo(3); const [b, c] = bar.foo("foo", "bar"); - return a + b + c - `; - const lua = util.transpileString(code); - expect(lua).not.toContain("unpack"); - const result = util.executeLua(lua); - expect(result).toBe("3foobar"); + return a + b + c; + ` + .tap(expectNoUnpack) + .expectToMatchJsResult(); }); test("Tuple Return vs Non-Tuple Return Overload", () => { @@ -479,15 +329,218 @@ test("Tuple Return vs Non-Tuple Return Overload", () => { end end `; + const tsHeader = ` declare function fn(this: void, a: number): [number, number]; /** @tupleReturn */ declare function fn(this: void, a: string, b: string): [string, string]; `; - const code = ` + + util.testFunction` const [a, b] = fn(3); const [c, d] = fn("foo", "bar"); return (a + b) + c + d; + ` + .setTsHeader(tsHeader) + .setLuaHeader(luaHeader) + .expectToEqual("7foobar"); +}); + +test("TupleReturn assignment", () => { + const code = ` + /** @tupleReturn */ + declare function abc(this: void): number[] + let [a,b] = abc(); `; - const result = util.transpileAndExecute(code, undefined, luaHeader, tsHeader); - expect(result).toBe("7foobar"); + + const lua = util.transpileString(code); + expect(lua).toBe("local a, b = abc()"); +}); + +test("TupleReturn Single assignment", () => { + const code = ` + /** @tupleReturn */ + declare function abc(this: void): [number, string]; + let a = abc(); + a = abc(); + `; + + const lua = util.transpileString(code); + expect(lua).toBe("local a = ({\n abc()\n})\na = ({\n abc()\n})"); +}); + +test("TupleReturn interface assignment", () => { + const code = ` + interface def { + /** @tupleReturn */ + abc(); + } declare const jkl : def; + let [a,b] = jkl.abc(); + `; + + const lua = util.transpileString(code); + expect(lua).toBe("local a, b = jkl:abc()"); +}); + +test("TupleReturn namespace assignment", () => { + const code = ` + declare namespace def { + /** @tupleReturn */ + function abc(this: void) {} + } + let [a,b] = def.abc(); + `; + + const lua = util.transpileString(code); + expect(lua).toBe("local a, b = def.abc()"); +}); + +test("TupleReturn method assignment", () => { + const code = ` + declare class def { + /** @tupleReturn */ + abc() { return [1,2,3]; } + } const jkl = new def(); + let [a,b] = jkl.abc(); + `; + + const lua = util.transpileString(code); + expect(lua).toBe("local jkl = def.new()\nlocal a, b = jkl:abc()"); +}); + +test("TupleReturn functional", () => { + const code = ` + /** @tupleReturn */ + function abc(): [number, string] { return [3, "a"]; } + const [a, b] = abc(); + return b + a; + `; + + const result = util.transpileAndExecute(code); + + expect(result).toBe("a3"); +}); + +test("TupleReturn single", () => { + const code = ` + /** @tupleReturn */ + function abc(): [number, string] { return [3, "a"]; } + const res = abc(); + return res.length + `; + + const result = util.transpileAndExecute(code); + + expect(result).toBe(2); +}); + +test("TupleReturn in expression", () => { + const code = ` + /** @tupleReturn */ + function abc(): [number, string] { return [3, "a"]; } + return abc()[1] + abc()[0]; + `; + + const result = util.transpileAndExecute(code); + + expect(result).toBe("a3"); +}); + +test("TupleReturn assignment", () => { + const code = ` + /** @tupleReturn */ + declare function abc(this: void): number[] + let [a,b] = abc(); + `; + + const lua = util.transpileString(code); + expect(lua).toBe("local a, b = abc()"); +}); + +test("TupleReturn Single assignment", () => { + const code = ` + /** @tupleReturn */ + declare function abc(this: void): [number, string]; + let a = abc(); + a = abc(); + `; + + const lua = util.transpileString(code); + expect(lua).toBe("local a = ({\n abc()\n})\na = ({\n abc()\n})"); +}); + +test("TupleReturn interface assignment", () => { + const code = ` + interface def { + /** @tupleReturn */ + abc(); + } declare const jkl : def; + let [a,b] = jkl.abc(); + `; + + const lua = util.transpileString(code); + expect(lua).toBe("local a, b = jkl:abc()"); +}); + +test("TupleReturn namespace assignment", () => { + const code = ` + declare namespace def { + /** @tupleReturn */ + function abc(this: void) {} + } + let [a,b] = def.abc(); + `; + + const lua = util.transpileString(code); + expect(lua).toBe("local a, b = def.abc()"); +}); + +test("TupleReturn method assignment", () => { + const code = ` + declare class def { + /** @tupleReturn */ + abc() { return [1,2,3]; } + } const jkl = new def(); + let [a,b] = jkl.abc(); + `; + + const lua = util.transpileString(code); + expect(lua).toBe("local jkl = def.new()\nlocal a, b = jkl:abc()"); +}); + +test("TupleReturn functional", () => { + const code = ` + /** @tupleReturn */ + function abc(): [number, string] { return [3, "a"]; } + const [a, b] = abc(); + return b + a; + `; + + const result = util.transpileAndExecute(code); + + expect(result).toBe("a3"); +}); + +test("TupleReturn single", () => { + const code = ` + /** @tupleReturn */ + function abc(): [number, string] { return [3, "a"]; } + const res = abc(); + return res.length + `; + + const result = util.transpileAndExecute(code); + + expect(result).toBe(2); +}); + +test("TupleReturn in expression", () => { + const code = ` + /** @tupleReturn */ + function abc(): [number, string] { return [3, "a"]; } + return abc()[1] + abc()[0]; + `; + + const result = util.transpileAndExecute(code); + + expect(result).toBe("a3"); }); diff --git a/test/unit/decorators/vararg.spec.ts b/test/unit/decorators/vararg.spec.ts new file mode 100644 index 000000000..2a99fa4a7 --- /dev/null +++ b/test/unit/decorators/vararg.spec.ts @@ -0,0 +1,55 @@ +import * as util from "../../util"; + +test.each([{}, { noHoisting: true }])("@vararg", compilerOptions => { + const code = ` + /** @vararg */ type LuaVarArg = A & { __luaVarArg?: never }; + function foo(a: unknown, ...b: LuaVarArg) { + const c = [...b]; + return c.join(""); + } + function bar(a: unknown, ...b: LuaVarArg) { + return foo(a, ...b); + } + return bar("A", "B", "C", "D"); + `; + + const lua = util.transpileString(code, compilerOptions); + expect(lua).not.toMatch("b = ({...})"); + expect(lua).not.toMatch("unpack"); + expect(util.transpileAndExecute(code, compilerOptions)).toBe("BCD"); +}); + +test.each([{}, { noHoisting: true }])("@vararg array access", compilerOptions => { + const code = ` + /** @vararg */ type LuaVarArg = A & { __luaVarArg?: never }; + function foo(a: unknown, ...b: LuaVarArg) { + const c = [...b]; + return c.join("") + b[0]; + } + return foo("A", "B", "C", "D"); + `; + + expect(util.transpileAndExecute(code, compilerOptions)).toBe("BCDB"); +}); + +test.each([{}, { noHoisting: true }])("@vararg global", compilerOptions => { + const code = ` + /** @vararg */ type LuaVarArg = A & { __luaVarArg?: never }; + declare const arg: LuaVarArg; + const arr = [...arg]; + const result = arr.join(""); + `; + + const luaBody = util.transpileString(code, compilerOptions, false); + expect(luaBody).not.toMatch("unpack"); + + const lua = ` + function test(...) + ${luaBody} + return result + end + return test("A", "B", "C", "D") + `; + + expect(util.executeLua(lua)).toBe("ABCD"); +}); diff --git a/test/unit/destructuring.spec.ts b/test/unit/destructuring.spec.ts new file mode 100644 index 000000000..b4ae3dd50 --- /dev/null +++ b/test/unit/destructuring.spec.ts @@ -0,0 +1,123 @@ +import * as util from "../util"; + +const allBindings = "x, y, z"; +const testCases = [ + { binding: "{ x }", value: { x: true } }, + { binding: "{ x, y }", value: { x: false, y: true } }, + { binding: "{ x: z, y }", value: { x: true, y: false } }, + { binding: "{ x: { x, y }, z }", value: { x: { x: true, y: false }, z: false } }, + { binding: "{ x, y = true }", value: { x: false, y: false } }, + { binding: "{ x = true }", value: {} }, + { binding: "{ x, y = true }", value: { x: false } }, + + { binding: "[]", value: [] }, + { binding: "[x, y]", value: ["x", "y"] }, + { binding: "[x, , y]", value: ["x", "", "y"] }, + { binding: "[x = true]", value: [false] }, + { binding: "[[x, y]]", value: [["x", "y"]] }, + + { binding: "{ y: [z = true] }", value: { y: [false] } }, + { binding: "{ x: [x, y] }", value: { x: ["x", "y"] } }, + { binding: "{ x: [{ y }] }", value: { x: [{ y: "y" }] } }, +].map(({ binding, value }) => ({ binding, value: util.valueToString(value) })); + +test.each([ + ...testCases, + { binding: "{ x, y }, z", value: "{ x: false, y: false }, true" }, + { binding: "{ x, y }, { z }", value: "{ x: false, y: false }, { z: true }" }, +])("in function parameter (%p)", ({ binding, value }) => { + util.testFunction` + let ${allBindings}; + function test(${binding}) { + return { ${allBindings} }; + } + + return test(${value}); + `.expectToMatchJsResult(); +}); + +test.each(testCases)("in variable declaration (%p)", ({ binding, value }) => { + util.testFunction` + let ${allBindings}; + { + const ${binding} = ${value}; + return { ${allBindings} }; + } + `.expectToMatchJsResult(); +}); + +// TODO: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/695 +test.each(testCases.filter(x => x.binding !== "[x, , y]"))( + "in exported variable declaration (%p)", + ({ binding, value }) => { + util.testModule` + export const ${binding} = ${value}; + `.expectToMatchJsResult(); + } +); + +const assignmentTestCases = [ + ...testCases, + ...[ + { binding: "{ x: obj.prop }", value: { x: true } }, + { binding: "{ x: obj.prop = true }", value: {} }, + { binding: "[{ x: obj.prop }]", value: [{ x: true }] }, + { binding: "{ obj: { prop: obj.prop } }", value: { obj: { prop: true } } }, + { binding: "{ x = true }", value: {} }, + ].map(({ binding, value }) => ({ binding, value: util.valueToString(value) })), + { binding: "{ x: { [(3).toString()]: y } }", value: "{ x: { [(3).toString()]: true } }" }, +]; + +test.each(assignmentTestCases)("in assignment expression (%p)", ({ binding, value }) => { + util.testFunction` + let ${allBindings}; + const obj = { prop: false }; + const expressionResult = (${binding} = ${value}); + return { ${allBindings}, obj, expressionResult }; + `.expectToMatchJsResult(); +}); + +describe("array destructuring optimization", () => { + // TODO: Try to generalize optimization logic between declaration and assignment and make more generic tests + + test("array", () => { + util.testFunction` + const array = [3, 5, 1]; + const [a, b, c] = array; + return { a, b, c }; + ` + .tap(builder => expect(builder.getMainLuaCodeChunk()).toContain("unpack")) + .expectToMatchJsResult(); + }); + + test("array literal", () => { + util.testFunction` + const [a, b, c] = [3, 5, 1]; + return { a, b, c }; + ` + .tap(builder => expect(builder.getMainLuaCodeChunk()).not.toContain("unpack")) + .expectToMatchJsResult(); + }); + + test("array literal with extra values", () => { + util.testFunction` + let called = false; + const set = () => { called = true; }; + const [head] = ["foo", set()]; + return { head, called }; + ` + .tap(builder => expect(builder.getMainLuaCodeChunk()).not.toContain("unpack")) + .expectToMatchJsResult(); + }); + + test("array union", () => { + util.testFunction` + const array: [string] | [] = ["bar"]; + let x: string; + [x] = array; + return x; + ` + .tap(builder => expect(builder.getMainLuaCodeChunk()).toContain("unpack")) + .expectToMatchJsResult(); + }); +}); diff --git a/test/unit/enum.spec.ts b/test/unit/enum.spec.ts index 219927f41..6fd75556b 100644 --- a/test/unit/enum.spec.ts +++ b/test/unit/enum.spec.ts @@ -1,176 +1,134 @@ import * as TSTLErrors from "../../src/TSTLErrors"; import * as util from "../util"; -test("Declare const enum", () => { - const testCode = ` - declare const enum TestEnum { - MEMBER_ONE = "test", - MEMBER_TWO = "test2" - } - - const valueOne = TestEnum.MEMBER_ONE; - `; - - expect(util.transpileString(testCode)).toBe(`local valueOne = "test"`); -}); - -test("Const enum", () => { - const testCode = ` - const enum TestEnum { - MEMBER_ONE = "test", - MEMBER_TWO = "test2" - } - - const valueOne = TestEnum.MEMBER_TWO; - `; - - expect(util.transpileString(testCode)).toBe(`local valueOne = "test2"`); -}); - -test("Const enum without initializer", () => { - const testCode = ` - const enum TestEnum { - MEMBER_ONE, - MEMBER_TWO - } - - const valueOne = TestEnum.MEMBER_TWO; - `; - - expect(util.transpileString(testCode)).toBe(`local valueOne = 1`); -}); - -test("Const enum without initializer in some values", () => { - const testCode = ` - const enum TestEnum { - MEMBER_ONE = 3, - MEMBER_TWO, - MEMBER_THREE = 5 +// TODO: string.toString() +const serializeEnum = (identifier: string) => `(() => { + const mappedTestEnum: any = {}; + for (const key in ${identifier}) { + mappedTestEnum[(key as any).toString()] = ${identifier}[key]; + } + return mappedTestEnum; +})()`; + +// TODO: Move to namespace tests? +test("in a namespace", () => { + util.testModule` + namespace Test { + export enum TestEnum { + A, + B, + } } - const valueOne = TestEnum.MEMBER_TWO; - `; - - expect(util.transpileString(testCode)).toBe(`local valueOne = 4`); + export const result = ${serializeEnum("Test.TestEnum")} + `.expectToMatchJsResult(); }); -test("Invalid heterogeneous enum", () => { - expect(() => { - util.transpileString(` +describe("initializers", () => { + test("expression", () => { + util.testFunction` + const value = 6; enum TestEnum { - a, - b = "ok", - c, + A, + B = value, } - `); - }).toThrowExactError(TSTLErrors.HeterogeneousEnum(util.nodeStub)); -}); -test("String literal name in enum", () => { - const code = ` - enum TestEnum { - ["name"] = "foo" - } - return TestEnum["name"]; - `; - const result = util.transpileAndExecute(code); - expect(result).toBe("foo"); -}); + return ${serializeEnum("TestEnum")} + `.expectToMatchJsResult(); + }); -test("Enum identifier value internal", () => { - const result = util.transpileAndExecute( - `enum testEnum { - abc, - def, - ghi = def, - jkl, - } - return \`\${testEnum.abc},\${testEnum.def},\${testEnum.ghi},\${testEnum.jkl}\`;` - ); + test("inference", () => { + util.testFunction` + enum TestEnum { + A, + B, + C, + } - expect(result).toBe("0,1,1,2"); -}); + return ${serializeEnum("TestEnum")} + `.expectToMatchJsResult(); + }); -test("Enum identifier value internal recursive", () => { - const result = util.transpileAndExecute( - `enum testEnum { - abc, - def, - ghi = def, - jkl = ghi, - } - return \`\${testEnum.abc},\${testEnum.def},\${testEnum.ghi},\${testEnum.jkl}\`;` - ); + test("partial inference", () => { + util.testFunction` + enum TestEnum { + A = 3, + B, + C = 5, + } - expect(result).toBe("0,1,1,1"); -}); + return ${serializeEnum("TestEnum")} + `.expectToMatchJsResult(); + }); -test("Enum identifier value external", () => { - const result = util.transpileAndExecute( - `const ext = 6; - enum testEnum { - abc, - def, - ghi = ext, - } - return \`\${testEnum.abc},\${testEnum.def},\${testEnum.ghi}\`;` - ); + test("other member reference", () => { + util.testFunction` + enum TestEnum { + A, + B = A, + C = B, + } - expect(result).toBe("0,1,6"); + return ${serializeEnum("TestEnum")} + `.expectToMatchJsResult(); + }); }); -test("Enum reverse mapping", () => { - const result = util.transpileAndExecute( - `enum testEnum { - abc, - def, - ghi +test("invalid heterogeneous enum", () => { + util.testFunction` + enum TestEnum { + A, + B = "B", + C, } - return testEnum[testEnum.abc] + testEnum[testEnum.ghi]` - ); - - expect(result).toBe("abcghi"); + ` + .disableSemanticCheck() + .expectToHaveDiagnosticOfError(TSTLErrors.HeterogeneousEnum(util.nodeStub)); }); -test("Const enum index", () => { - const result = util.transpileAndExecute( - `const enum testEnum { - abc, - def, - ghi - } - return testEnum["def"];` - ); - - expect(result).toBe(1); -}); +describe("const enum", () => { + const expectToBeConst: util.TapCallback = builder => + expect(builder.getMainLuaCodeChunk()).not.toContain("TestEnum"); -test("Const enum index identifier value", () => { - const result = util.transpileAndExecute( - `const enum testEnum { - abc, - def = 4, - ghi, - jkl = ghi - } - return testEnum["jkl"];` - ); + test.each(["", "declare"])("%s without initializer", () => { + util.testFunction` + const enum TestEnum { + A, + B, + } - expect(result).toBe(5); -}); + return TestEnum.A; + ` + .tap(expectToBeConst) + .expectToMatchJsResult(); + }); + + test("with initializer", () => { + util.testFunction` + const enum TestEnum { + A = "ONE", + B = "TWO", + } -test("Const enum index identifier chain", () => { - const result = util.transpileAndExecute( - `const enum testEnum { - abc = 3, - def, - ghi = def, - jkl = ghi, - } - return testEnum["ghi"];` - ); + return TestEnum.A; + ` + .tap(expectToBeConst) + .expectToMatchJsResult(); + }); + + test("access with string literal", () => { + util.testFunction` + const enum TestEnum { + A, + B, + C, + } - expect(result).toBe(4); + return TestEnum["C"]; + ` + .tap(expectToBeConst) + .expectToMatchJsResult(); + }); }); test("enum toString", () => { @@ -196,34 +154,3 @@ test("enum concat", () => { return test + "_foobar";`; expect(util.transpileAndExecute(code)).toBe("0_foobar"); }); - -test("enum value as array index", () => { - const code = ` - enum TestEnum { - A, - B, - C, - } - const arr = ["a", "b", "c"]; - let i = TestEnum.A; - return arr[i];`; - expect(util.transpileAndExecute(code)).toBe("a"); -}); - -test("enum property value as array index", () => { - const code = ` - enum TestEnum { - A, - B, - C, - } - - class Foo { - i = TestEnum.A; - } - const foo = new Foo(); - - const arr = ["a", "b", "c"]; - return arr[foo.i];`; - expect(util.transpileAndExecute(code)).toBe("a"); -}); diff --git a/test/unit/error.spec.ts b/test/unit/error.spec.ts index 5f0ec7df6..17a6bc15c 100644 --- a/test/unit/error.spec.ts +++ b/test/unit/error.spec.ts @@ -2,51 +2,47 @@ import * as TSTLErrors from "../../src/TSTLErrors"; import * as util from "../util"; test("throwString", () => { - const lua = util.transpileString(`throw "Some Error"`); - expect(lua).toBe(`error("Some Error")`); + util.testFunction` + throw "Some Error" + `.expectToEqual(new util.ExecutionError("Some Error")); }); test("throwError", () => { - expect(() => { - util.transpileString(`throw Error("Some Error")`); - }).toThrowExactError(TSTLErrors.InvalidThrowExpression(util.nodeStub)); + util.testFunction` + throw Error("Some Error") + `.expectToHaveDiagnosticOfError(TSTLErrors.InvalidThrowExpression(util.nodeStub)); }); -test.each([{ i: 0, expected: "A" }, { i: 1, expected: "B" }, { i: 2, expected: "C" }])( - "re-throw (%p)", - ({ i, expected }) => { - const source = ` - const i: number = ${i}; - function foo() { +test.skip.each([0, 1, 2])("re-throw (%p)", i => { + util.testFunction` + const i: number = ${i}; + function foo() { + try { try { - try { - if (i === 0) { throw "z"; } - } catch (e) { - throw "a"; - } finally { - if (i === 1) { throw "b"; } - } + if (i === 0) { throw "z"; } } catch (e) { - throw (e as string).toUpperCase(); + throw "a"; } finally { - throw "C"; + if (i === 1) { throw "b"; } } - } - let result: string = "x"; - try { - foo(); } catch (e) { - result = (e as string)[(e as string).length - 1]; + throw (e as string).toUpperCase(); + } finally { + throw "C"; } - return result; - `; - const result = util.transpileAndExecute(source); - expect(result).toBe(expected); - } -); + } + let result: string = "x"; + try { + foo(); + } catch (e) { + result = (e as string)[(e as string).length - 1]; + } + return result; + `.expectToMatchJsResult(); +}); test("re-throw (no catch var)", () => { - const source = ` + util.testFunction` let result = "x"; try { try { @@ -58,9 +54,7 @@ test("re-throw (no catch var)", () => { result = (e as string)[(e as string).length - 1]; } return result; - `; - const result = util.transpileAndExecute(source); - expect(result).toBe("z"); + `.expectToMatchJsResult(); }); test("return from try", () => { diff --git a/test/unit/expressions.spec.ts b/test/unit/expressions.spec.ts index a2e902352..5209bb59c 100644 --- a/test/unit/expressions.spec.ts +++ b/test/unit/expressions.spec.ts @@ -3,53 +3,44 @@ import * as tstl from "../../src"; import * as TSTLErrors from "../../src/TSTLErrors"; import * as util from "../util"; -test.each([ - { input: "i++", lua: "i = i + 1" }, - { input: "++i", lua: "i = i + 1" }, - { input: "i--", lua: "i = i - 1" }, - { input: "--i", lua: "i = i - 1" }, - { input: "!a", lua: "local ____ = not a" }, - { input: "-a", lua: "local ____ = -a" }, - { input: "+a", lua: "local ____ = a" }, - { input: "let a = delete tbl['test']", lua: "local a = (function()\n tbl.test = nil\n return true\nend)()" }, - { input: "delete tbl['test']", lua: "tbl.test = nil" }, - { input: "let a = delete tbl.test", lua: "local a = (function()\n tbl.test = nil\n return true\nend)()" }, - { input: "delete tbl.test", lua: "tbl.test = nil" }, -])("Unary expressions basic (%p)", ({ input, lua }) => { - expect(util.transpileString(input)).toBe(lua); +// TODO: +test("Block statement", () => { + util.testFunction` + let a = 4; + { let a = 42; } + return a; + `.expectToMatchJsResult(); }); test.each([ - { input: "3+4", output: 3 + 4 }, - { input: "5-2", output: 5 - 2 }, - { input: "6*3", output: 6 * 3 }, - { input: "6**3", output: 6 ** 3 }, - { input: "20/5", output: 20 / 5 }, - { input: "15/10", output: 15 / 10 }, - { input: "15%3", output: 15 % 3 }, -])("Binary expressions basic numeric (%p)", ({ input, output }) => { - const result = util.transpileAndExecute(`return ${input}`); - - expect(result).toBe(output); + "i++", + "++i", + "i--", + "--i", + "!a", + "-a", + "+a", + "let a = delete tbl['test']", + "delete tbl['test']", + "let a = delete tbl.test", + "delete tbl.test", +])("Unary expressions basic (%p)", input => { + util.testFunction(input) + .disableSemanticCheck() + .expectLuaToMatchSnapshot(); }); -test.each([ - { input: "1==1", expected: true }, - { input: "1===1", expected: true }, - { input: "1!=1", expected: false }, - { input: "1!==1", expected: false }, - { input: "1>1", expected: false }, - { input: "1>=1", expected: true }, - { input: "1<1", expected: false }, - { input: "1<=1", expected: true }, - { input: "1&&1", expected: 1 }, - { input: "1||1", expected: 1 }, -])("Binary expressions basic boolean (%p)", ({ input, expected }) => { - const result = util.transpileAndExecute(`return ${input}`); - - expect(result).toBe(expected); +test.each(["3+4", "5-2", "6*3", "6**3", "20/5", "15/10", "15%3"])("Binary expressions basic numeric (%p)", input => { + util.testExpression(input).expectToMatchJsResult(); }); +test.each(["1==1", "1===1", "1!=1", "1!==1", "1>1", "1>=1", "1<1", "1<=1", "1&&1", "1||1"])( + "Binary expressions basic boolean (%p)", + input => { + util.testExpression(input).expectToMatchJsResult(); + } +); + test.each(["'key' in obj", "'existingKey' in obj", "0 in obj", "9 in obj"])("Binary expression in (%p)", input => { const tsHeader = "declare var obj: any;"; const tsSource = `return ${input}`; @@ -59,143 +50,84 @@ test.each(["'key' in obj", "'existingKey' in obj", "0 in obj", "9 in obj"])("Bin expect(result).toBe(eval(`let obj = { existingKey: 1 }; ${input}`)); }); -test.each([ - { input: "a+=b", expected: 5 + 3 }, - { input: "a-=b", expected: 5 - 3 }, - { input: "a*=b", expected: 5 * 3 }, - { input: "a/=b", expected: 5 / 3 }, - { input: "a%=b", expected: 5 % 3 }, - { input: "a**=b", expected: 5 ** 3 }, -])("Binary expressions overridden operators (%p)", ({ input, expected }) => { - const result = util.transpileAndExecute(`let a = 5; let b = 3; ${input}; return a;`); - - expect(result).toBe(expected); +test.each(["a+=b", "a-=b", "a*=b", "a/=b", "a%=b", "a**=b"])("Binary expressions overridden operators (%p)", input => { + util.testFunction` + let a = 5; + let b = 3; + ${input}; + return a; + `.expectToMatchJsResult(); }); -test.each([ - { input: "~b" }, - { input: "a&b" }, - { input: "a&=b" }, - { input: "a|b" }, - { input: "a|=b" }, - { input: "a^b" }, - { input: "a^=b" }, - { input: "a<>b" }, - { input: "a>>=b" }, - { input: "a>>>b" }, - { input: "a>>>=b" }, -])("Bitop [5.1] (%p)", ({ input }) => { +const supportedInAll = ["~a", "a&b", "a&=b", "a|b", "a|=b", "a^b", "a^=b", "a<>>b", "a>>>=b"]; +const unsupportedIn53 = ["a>>b", "a>>=b"]; +const allBinaryOperators = [...supportedInAll, ...unsupportedIn53]; +test.each(allBinaryOperators)("Bitop [5.1] (%p)", input => { // Bit operations not supported in 5.1, expect an exception - expect(() => - util.transpileString(input, { - luaTarget: tstl.LuaTarget.Lua51, - luaLibImport: tstl.LuaLibImportKind.None, - }) - ).toThrow(); -}); - -test.each([ - { input: "~a", lua: "bit.bnot(a)" }, - { input: "a&b", lua: "bit.band(a, b)" }, - { input: "a&=b", lua: "a = bit.band(a, b)" }, - { input: "a|b", lua: "bit.bor(a, b)" }, - { input: "a|=b", lua: "a = bit.bor(a, b)" }, - { input: "a^b", lua: "bit.bxor(a, b)" }, - { input: "a^=b", lua: "a = bit.bxor(a, b)" }, - { input: "a<>b", lua: "bit.arshift(a, b)" }, - { input: "a>>=b", lua: "a = bit.arshift(a, b)" }, - { input: "a>>>b", lua: "bit.rshift(a, b)" }, - { input: "a>>>=b", lua: "a = bit.rshift(a, b)" }, -])("Bitop [JIT] (%p)", ({ input, lua }) => { - const options = { luaTarget: tstl.LuaTarget.LuaJIT, luaLibImport: tstl.LuaLibImportKind.None }; - expect(util.transpileString(input, options)).toBe(lua); -}); - -test.each([ - { input: "~a", lua: "bit32.bnot(a)" }, - { input: "a&b", lua: "bit32.band(a, b)" }, - { input: "a&=b", lua: "a = bit32.band(a, b)" }, - { input: "a|b", lua: "bit32.bor(a, b)" }, - { input: "a|=b", lua: "a = bit32.bor(a, b)" }, - { input: "a^b", lua: "bit32.bxor(a, b)" }, - { input: "a^=b", lua: "a = bit32.bxor(a, b)" }, - { input: "a<>b", lua: "bit32.arshift(a, b)" }, - { input: "a>>=b", lua: "a = bit32.arshift(a, b)" }, - { input: "a>>>b", lua: "bit32.rshift(a, b)" }, - { input: "a>>>=b", lua: "a = bit32.rshift(a, b)" }, -])("Bitop [5.2] (%p)", ({ input, lua }) => { - const options = { luaTarget: tstl.LuaTarget.Lua52, luaLibImport: tstl.LuaLibImportKind.None }; - expect(util.transpileString(input, options)).toBe(lua); -}); - -test.each([ - { input: "~a", lua: "local ____ = ~a" }, - { input: "a&b", lua: "local ____ = a & b" }, - { input: "a&=b", lua: "a = a & b" }, - { input: "a|b", lua: "local ____ = a | b" }, - { input: "a|=b", lua: "a = a | b" }, - { input: "a^b", lua: "local ____ = a ~ b" }, - { input: "a^=b", lua: "a = a ~ b" }, - { input: "a<>>b", lua: "local ____ = a >> b" }, - { input: "a>>>=b", lua: "a = a >> b" }, -])("Bitop [5.3] (%p)", ({ input, lua }) => { - const options = { luaTarget: tstl.LuaTarget.Lua53, luaLibImport: tstl.LuaLibImportKind.None }; - expect(util.transpileString(input, options)).toBe(lua); -}); - -test.each(["a>>b", "a>>=b"])("Unsupported bitop 5.3 (%p)", input => { - expect(() => - util.transpileString(input, { - luaTarget: tstl.LuaTarget.Lua53, - luaLibImport: tstl.LuaLibImportKind.None, - }) - ).toThrowExactError( - TSTLErrors.UnsupportedKind( - "right shift operator (use >>> instead)", - ts.SyntaxKind.GreaterThanGreaterThanToken, - util.nodeStub - ) - ); -}); - -test.each([ - { input: "1+1", lua: "1 + 1" }, - { input: "-1+1", lua: "-1 + 1" }, - { input: "1*30+4", lua: "1 * 30 + 4" }, - { input: "1*(3+4)", lua: "1 * (3 + 4)" }, - { input: "1*(3+4*2)", lua: "1 * (3 + 4 * 2)" }, - { input: "10-(4+5)", lua: "10 - (4 + 5)" }, -])("Binary expressions ordering parentheses (%p)", ({ input, lua }) => { - expect(util.transpileString(input)).toBe("local ____ = " + lua); -}); + util.testExpression(input) + .setOptions({ luaTarget: tstl.LuaTarget.Lua51, luaLibImport: tstl.LuaLibImportKind.None }) + .disableSemanticCheck() + .expectToHaveDiagnosticOfError( + TSTLErrors.UnsupportedForTarget("Bitwise operations", tstl.LuaTarget.Lua51, util.nodeStub) + ); +}); + +test.each(allBinaryOperators)("Bitop [JIT] (%p)", input => { + util.testExpression(input) + .setOptions({ luaTarget: tstl.LuaTarget.LuaJIT, luaLibImport: tstl.LuaLibImportKind.None }) + .disableSemanticCheck() + .expectLuaToMatchSnapshot(); +}); + +test.each(allBinaryOperators)("Bitop [5.2] (%p)", input => { + util.testExpression(input) + .setOptions({ luaTarget: tstl.LuaTarget.Lua52, luaLibImport: tstl.LuaLibImportKind.None }) + .disableSemanticCheck() + .expectLuaToMatchSnapshot(); +}); + +test.each(supportedInAll)("Bitop [5.3] (%p)", input => { + util.testExpression(input) + .setOptions({ luaTarget: tstl.LuaTarget.Lua53, luaLibImport: tstl.LuaLibImportKind.None }) + .disableSemanticCheck() + .expectLuaToMatchSnapshot(); +}); + +test.each(unsupportedIn53)("Unsupported bitop 5.3 (%p)", input => { + util.testExpression(input) + .setOptions({ luaTarget: tstl.LuaTarget.Lua53, luaLibImport: tstl.LuaLibImportKind.None }) + .disableSemanticCheck() + .expectToHaveDiagnosticOfError( + TSTLErrors.UnsupportedKind( + "right shift operator (use >>> instead)", + ts.SyntaxKind.GreaterThanGreaterThanToken, + util.nodeStub + ) + ); +}); + +test.each(["1+1", "-1+1", "1*30+4", "1*(3+4)", "1*(3+4*2)", "10-(4+5)"])( + "Binary expressions ordering parentheses (%p)", + input => { + util.testExpression(input).expectLuaToMatchSnapshot(); + } +); -test.each([ - { input: "bar(),foo()", expectResult: 1 }, - { input: "foo(),bar()", expectResult: 2 }, - { input: "foo(),bar(),baz()", expectResult: 3 }, -])("Binary Comma (%p)", ({ input, expectResult }) => { - const code = `function foo() { return 1; } +test.each(["bar(),foo()", "foo(),bar()", "foo(),bar(),baz()"])("Binary Comma (%p)", input => { + util.testFunction` + function foo() { return 1; } function bar() { return 2; }; function baz() { return 3; }; - return (${input});`; - expect(util.transpileAndExecute(code)).toBe(expectResult); + return (${input}); + `.expectToMatchJsResult(); }); test("Binary Comma Statement in For Loop", () => { - const code = ` + util.testFunction` let x: number, y: number; for (x = 0, y = 17; x < 5; ++x, --y) {} return y; - `; - expect(util.transpileAndExecute(code)).toBe(12); + `.expectToMatchJsResult(); }); test("Null Expression", () => { @@ -206,301 +138,19 @@ test("Undefined Expression", () => { expect(util.transpileString("undefined")).toBe("local ____ = nil"); }); -test.each([ - { input: "true ? 'a' : 'b'", expected: "a" }, - { input: "false ? 'a' : 'b'", expected: "b" }, - { input: "true ? false : true", expected: false }, - { input: "false ? false : true", expected: true }, - { input: "true ? literalValue : true", expected: "literal" }, - { input: "true ? variableValue : true" }, - { input: "true ? maybeUndefinedValue : true" }, - { input: "true ? maybeBooleanValue : true", expected: false }, - { input: "true ? maybeUndefinedValue : true", options: { strictNullChecks: true } }, - { input: "true ? maybeBooleanValue : true", expected: false, options: { strictNullChecks: true } }, - { input: "true ? undefined : true", options: { strictNullChecks: true } }, - { input: "true ? null : true", options: { strictNullChecks: true } }, - { input: "true ? false : true", expected: false, options: { luaTarget: tstl.LuaTarget.Lua51 } }, - { input: "false ? false : true", expected: true, options: { luaTarget: tstl.LuaTarget.Lua51 } }, - { input: "true ? undefined : true", options: { luaTarget: tstl.LuaTarget.Lua51 } }, - { input: "true ? false : true", expected: false, options: { luaTarget: tstl.LuaTarget.LuaJIT } }, - { input: "false ? false : true", expected: true, options: { luaTarget: tstl.LuaTarget.LuaJIT } }, - { input: "true ? undefined : true", options: { luaTarget: tstl.LuaTarget.LuaJIT } }, -])("Ternary operator (%p)", ({ input, expected, options }) => { - const result = util.transpileAndExecute( - `const literalValue = 'literal'; - let variableValue:string; - let maybeBooleanValue:string|boolean = false; - let maybeUndefinedValue:string|undefined; - return ${input};`, - options - ); - - expect(result).toBe(expected); -}); - -test.each([ - { expression: "inst.field", expected: 8 }, - { expression: "inst.field + 3", expected: 8 + 3 }, - { expression: "inst.field * 3", expected: 8 * 3 }, - { expression: "inst.field / 2", expected: 8 / 2 }, - { expression: "inst.field && 3", expected: 8 && 3 }, - { expression: "inst.field || 3", expected: 8 || 3 }, - { expression: "(inst.field + 3) & 3", expected: (8 + 3) & 3 }, - { expression: "inst.field | 3", expected: 8 | 3 }, - { expression: "inst.field << 3", expected: 8 << 3 }, - { expression: "inst.field >>> 1", expected: 8 >> 1 }, - { expression: "inst.field = 3", expected: 3 }, - { expression: `"abc" + inst.field`, expected: "abc8" }, -])("Get accessor expression (%p)", ({ expression, expected }) => { - const result = util.transpileAndExecute(` - class MyClass { - public _field: number; - public get field(): number { return this._field + 4; } - public set field(v: number) { this._field = v; } - } - var inst = new MyClass(); - inst._field = 4; +test.each(["i++", "i--", "++i", "--i"])("Incrementor value (%p)", expression => { + util.testFunction` + let i = 10; return ${expression}; - `); - - expect(result).toBe(expected); -}); - -test.each([ - { expression: "= 4", expected: 4 + 4 }, - { expression: "-= 3", expected: 4 - 3 + 4 }, - { expression: "+= 3", expected: 4 + 3 + 4 }, - { expression: "*= 3", expected: 4 * 3 + 4 }, - { expression: "/= 2", expected: 4 / 2 + 4 }, - { expression: "&= 3", expected: (4 & 3) + 4 }, - { expression: "|= 3", expected: (4 | 3) + 4 }, - { expression: "<<= 3", expected: (4 << 3) + 4 }, - { expression: ">>>= 3", expected: (4 >> 3) + 4 }, -])("Set accessorExpression (%p)", ({ expression, expected }) => { - const result = util.transpileAndExecute(` - class MyClass { - public _field: number = 4; - public get field(): number { return this._field; } - public set field(v: number) { this._field = v + 4; } - } - var inst = new MyClass(); - inst.field ${expression}; - return inst._field; - `); - - expect(result).toBe(expected); -}); - -test.each([ - { expression: "inst.baseField", expected: 7 }, - { expression: "inst.field", expected: 6 }, - { expression: "inst.superField", expected: 5 }, - { expression: "inst.superBaseField", expected: 4 }, -])("Inherited accessors (%p)", ({ expression, expected }) => { - const result = util.transpileAndExecute(` - class MyBaseClass { - public _baseField: number; - public get baseField(): number { return this._baseField + 6; } - public set baseField(v: number) { this._baseField = v; } - } - class MyClass extends MyBaseClass { - public _field: number; - public get field(): number { return this._field + 4; } - public set field(v: number) { this._field = v; } - } - class MySuperClass extends MyClass { - public _superField: number; - public get superField(): number { return this._superField + 2; } - public set superField(v: number) { this._superField = v; } - public get superBaseField() { return this.baseField - 3; } - } - var inst = new MySuperClass(); - inst.baseField = 1; - inst.field = 2; - inst.superField = 3; - return ${expression} - `); - expect(result).toBe(expected); -}); - -test.each([ - { expression: "return x.value;", expected: 1 }, - { expression: "x.value = 3; return x.value;", expected: 3 }, -])("Union accessors (%p)", ({ expression, expected }) => { - const result = util.transpileAndExecute( - `class A{ get value(){ return this.v || 1; } set value(v){ this.v = v; } v: number; } - class B{ get value(){ return this.v || 2; } set value(v){ this.v = v; } v: number; } - let x: A|B = new A(); - ${expression}` - ); - - expect(result).toBe(expected); -}); - -test.each([ - { expression: "i++", expected: 10 }, - { expression: "i--", expected: 10 }, - { expression: "++i", expected: 11 }, - { expression: "--i", expected: 9 }, -])("Incrementor value (%p)", ({ expression, expected }) => { - const result = util.transpileAndExecute(`let i = 10; return ${expression};`); - - expect(result).toBe(expected); -}); - -test.each([ - { lambda: "a++", expected: "val3" }, - { lambda: "a--", expected: "val3" }, - { lambda: "--a", expected: "val2" }, - { lambda: "++a", expected: "val4" }, -])("Template string expression (%p)", ({ lambda, expected }) => { - const result = util.transpileAndExecute("let a = 3; return `val${" + lambda + "}`;"); - - expect(result).toEqual(expected); -}); - -test.each([{ expression: "x = y", expected: "y" }, { expression: "x += y", expected: "xy" }])( - "Assignment expressions (%p)", - ({ expression, expected }) => { - const result = util.transpileAndExecute(`let x = "x"; let y = "y"; return ${expression};`); - expect(result).toBe(expected); - } -); - -test.each([ - { expression: "x = o.p", expected: "o" }, - { expression: "x = a[0]", expected: "a" }, - { expression: "x = y = o.p", expected: "o" }, - { expression: "x = o.p", expected: "o" }, -])("Assignment expressions using temp (%p)", ({ expression, expected }) => { - const result = util.transpileAndExecute( - `let x = "x"; - let y = "y"; - let o = {p: "o"}; - let a = ["a"]; - return ${expression};` - ); - expect(result).toBe(expected); -}); - -test.each([ - { expression: "o.p = x", expected: "x" }, - { expression: "a[0] = x", expected: "x" }, - { expression: "o.p = a[0]", expected: "a" }, - { expression: "o.p = a[0] = x", expected: "x" }, -])("Property assignment expressions (%p)", ({ expression, expected }) => { - const result = util.transpileAndExecute( - `let x = "x"; - let o = {p: "o"}; - let a = ["a"]; - return ${expression};` - ); - expect(result).toBe(expected); -}); - -test.each([ - { expression: "x = t()", expected: "t0,t1" }, - { expression: "x = tr()", expected: "tr0,tr1" }, - { expression: "[x[1], x[0]] = t()", expected: "t0,t1" }, - { expression: "[x[1], x[0]] = tr()", expected: "tr0,tr1" }, - { expression: "x = [y[1], y[0]]", expected: "y1,y0" }, - { expression: "[x[0], x[1]] = [y[1], y[0]]", expected: "y1,y0" }, -])("Tuple assignment expressions (%p)", ({ expression, expected }) => { - const result = util.transpileAndExecute( - `let x: [string, string] = ["x0", "x1"]; - let y: [string, string] = ["y0", "y1"]; - function t(): [string, string] { return ["t0", "t1"] }; - /** @tupleReturn */ - function tr(): [string, string] { return ["tr0", "tr1"] }; - const r = ${expression}; - return \`\${r[0]},\${r[1]}\`` - ); - expect(result).toBe(expected); -}); - -test("Block expression", () => { - const result = util.transpileAndExecute(`let a = 4; {let a = 42; } return a;`); - expect(result).toBe(4); + `.expectToMatchJsResult(); }); test("Non-null expression", () => { - const result = util.transpileAndExecute(` + util.testFunction` function abc(): number | undefined { return 3; } const a: number = abc()!; return a; - `); - expect(result).toBe(3); -}); - -test("Unknown unary postfix error", () => { - const transformer = util.makeTestTransformer(); - - const mockExpression: any = { - operand: ts.createLiteral(false), - operator: ts.SyntaxKind.AsteriskToken, - }; - - expect(() => - transformer.transformPostfixUnaryExpression(mockExpression as ts.PostfixUnaryExpression) - ).toThrowExactError( - TSTLErrors.UnsupportedKind("unary postfix operator", ts.SyntaxKind.AsteriskToken, util.nodeStub) - ); -}); - -test("Unknown unary postfix error", () => { - const transformer = util.makeTestTransformer(); - - const mockExpression: any = { - operand: ts.createLiteral(false), - operator: ts.SyntaxKind.AsteriskToken, - }; - - expect(() => - transformer.transformPrefixUnaryExpression(mockExpression as ts.PrefixUnaryExpression) - ).toThrowExactError( - TSTLErrors.UnsupportedKind("unary prefix operator", ts.SyntaxKind.AsteriskToken, util.nodeStub) - ); -}); - -test("Incompatible fromCodePoint expression error", () => { - expect(() => util.transpileString("const abc = String.fromCodePoint(123);")).toThrowExactError( - TSTLErrors.UnsupportedForTarget("string property fromCodePoint", tstl.LuaTarget.Lua53, util.nodeStub) - ); -}); - -test("Unknown string expression error", () => { - expect(() => util.transpileString("const abc = String.abcd();")).toThrowExactError( - TSTLErrors.UnsupportedForTarget("string property abcd", tstl.LuaTarget.Lua53, util.nodeStub) - ); -}); - -test("Unsupported array function error", () => { - expect(() => util.transpileString("const abc = [].unknownFunction();")).toThrowExactError( - TSTLErrors.UnsupportedProperty("array", "unknownFunction", util.nodeStub) - ); -}); - -test("Unsupported math property error", () => { - expect(() => util.transpileString("const abc = Math.unknownProperty;")).toThrowExactError( - TSTLErrors.UnsupportedProperty("math", "unknownProperty", util.nodeStub) - ); -}); - -test("Unsupported object literal element error", () => { - const transformer = util.makeTestTransformer(); - - const mockObject: any = { - properties: [ - { - kind: ts.SyntaxKind.FalseKeyword, - name: ts.createIdentifier("testProperty"), - }, - ], - }; - - expect(() => transformer.transformObjectLiteral(mockObject as ts.ObjectLiteralExpression)).toThrowExactError( - TSTLErrors.UnsupportedKind("object literal element", ts.SyntaxKind.FalseKeyword, util.nodeStub) - ); + `.expectToMatchJsResult(); }); test.each([ @@ -514,80 +164,15 @@ test.each([ "!foo()", "foo()", "typeof foo", - '"bar" in bar', + '"foo" in bar', "foo as Function", "Math.log2(2)", "Math.log10(2)", '"".indexOf("")', ])("Expression statements (%p)", input => { - const code = ` + util.testFunction` function foo() { return 17; } - const bar = {}; + const bar = { foo }; ${input}; - return 1; - `; - expect(util.transpileAndExecute(code)).toBe(1); -}); - -test("binary expression with 'as' type assertion wrapped in parenthesis", () => { - expect(util.transpileAndExecute("return 2 * (3 - 2 as number);")).toBe(2); -}); - -test.each([ - "(x as any).foo;", - "(y.x as any).foo;", - "(y['x'] as any).foo;", - "(z() as any).foo;", - "(y.z() as any).foo;", - "(x).foo;", - "(y.x).foo;", - "(y['x']).foo;", - "(z()).foo;", - "(y.z()).foo;", - "(x as unknown as any).foo;", - "(x as any).foo;", - "((x as unknown) as any).foo;", - "((x) as any).foo;", -])("'as' type assertion should strip parenthesis (%p)", expression => { - const code = ` - declare let x: unknown; - declare let y: { x: unknown; z(this: void): unknown; }; - declare function z(this: void): unknown; - ${expression}`; - - const lua = util.transpileString(code, undefined, false); - expect(lua).not.toMatch(/\(.+\)/); -}); - -test.each([ - "(x + 1 as any).foo;", - "(!x as any).foo;", - "(x ** 2 as any).foo;", - "(x < 2 as any).foo;", - "(x in y as any).foo;", - "(x + 1).foo;", - "(!x).foo;", - "(x + 1 as unknown as any).foo;", - "((x + 1 as unknown) as any).foo;", - "(!x as unknown as any).foo;", - "((!x as unknown) as any).foo;", - "(!x as any).foo;", - "((!x) as any).foo;", -])("'as' type assertion should not strip parenthesis (%p)", expression => { - const code = ` - declare let x: number; - declare let y: {}; - ${expression}`; - - const lua = util.transpileString(code, undefined, false); - expect(lua).toMatch(/\(.+\)/); -}); - -test("not operator precedence (%p)", () => { - const code = ` - const a = true; - const b = false; - return !a && b;`; - - expect(util.transpileAndExecute(code)).toBe(false); + `.expectNoExecutionError(); }); diff --git a/test/unit/functions.spec.ts b/test/unit/functions.spec.ts deleted file mode 100644 index b8558a3cd..000000000 --- a/test/unit/functions.spec.ts +++ /dev/null @@ -1,633 +0,0 @@ -import * as ts from "typescript"; -import * as TSTLErrors from "../../src/TSTLErrors"; -import * as util from "../util"; - -test("Arrow Function Expression", () => { - const result = util.transpileAndExecute(`let add = (a, b) => a+b; return add(1,2);`); - - expect(result).toBe(3); -}); - -test.each([ - { lambda: "i++", expected: 15 }, - { lambda: "i--", expected: 5 }, - { lambda: "++i", expected: 15 }, - { lambda: "--i", expected: 5 }, -])("Arrow function unary expression (%p)", ({ lambda, expected }) => { - const result = util.transpileAndExecute(`let i = 10; [1,2,3,4,5].forEach(() => ${lambda}); return i;`); - - expect(result).toBe(expected); -}); - -test.each([ - { lambda: "b => a = b", expected: 5 }, - { lambda: "b => a += b", expected: 15 }, - { lambda: "b => a -= b", expected: 5 }, - { lambda: "b => a *= b", expected: 50 }, - { lambda: "b => a /= b", expected: 2 }, - { lambda: "b => a **= b", expected: 100000 }, - { lambda: "b => a %= b", expected: 0 }, -])("Arrow function assignment (%p)", ({ lambda, expected }) => { - const result = util.transpileAndExecute(`let a = 10; let lambda = ${lambda}; - lambda(5); return a;`); - - expect(result).toBe(expected); -}); - -test.each([{ inp: [] }, { inp: [5] }, { inp: [1, 2] }])("Arrow Default Values (%p)", ({ inp }) => { - // Default value is 3 for v1 - const v1 = inp.length > 0 ? inp[0] : 3; - // Default value is 4 for v2 - const v2 = inp.length > 1 ? inp[1] : 4; - - const callArgs = inp.join(","); - - const result = util.transpileAndExecute( - `let add = (a: number = 3, b: number = 4) => a+b; - return add(${callArgs});` - ); - - expect(result).toBe(v1 + v2); -}); - -test("Function Expression", () => { - const result = util.transpileAndExecute(`let add = function(a, b) {return a+b}; return add(1,2);`); - - expect(result).toBe(3); -}); - -test("Function definition scope", () => { - const result = util.transpileAndExecute(`function abc() { function xyz() { return 5; } }\n - function def() { function xyz() { return 3; } abc(); return xyz(); }\n - return def();`); - - expect(result).toBe(3); -}); - -test("Function default parameter", () => { - const result = util.transpileAndExecute(`function abc(defaultParam: string = "abc") { return defaultParam; }\n - return abc() + abc("def");`); - - expect(result).toBe("abcdef"); -}); - -test.each([{ inp: [] }, { inp: [5] }, { inp: [1, 2] }])("Function Default Values (%p)", ({ inp }) => { - // Default value is 3 for v1 - const v1 = inp.length > 0 ? inp[0] : 3; - // Default value is 4 for v2 - const v2 = inp.length > 1 ? inp[1] : 4; - - const callArgs = inp.join(","); - - const result = util.transpileAndExecute( - `let add = function(a: number = 3, b: number = 4) { return a+b; }; - return add(${callArgs});` - ); - - expect(result).toBe(v1 + v2); -}); - -test("Function default array binding parameter", () => { - const code = ` - function foo([bar]: [string] = ["foobar"]) { - return bar; - } - return foo();`; - - expect(util.transpileAndExecute(code)).toBe("foobar"); -}); - -test("Function default object binding parameter", () => { - const code = ` - function foo({ bar }: { bar: string } = { bar: "foobar" }) { - return bar; - } - return foo();`; - - expect(util.transpileAndExecute(code)).toBe("foobar"); -}); - -test("Function default binding parameter maintains order", () => { - const code = ` - const resultsA = [{x: "foo"}, {x: "baz"}]; - const resultsB = ["blah", "bar"]; - let i = 0; - function a() { return resultsA[i++]; } - function b() { return resultsB[i++]; } - function foo({ x }: { x: string } = a(), y = b()) { - return x + y; - } - return foo();`; - - expect(util.transpileAndExecute(code)).toBe("foobar"); -}); - -test("Class method call", () => { - const returnValue = 4; - const source = `class TestClass { - public classMethod(): number { return ${returnValue}; } - } - - const classInstance = new TestClass(); - return classInstance.classMethod();`; - - const result = util.transpileAndExecute(source); - - expect(result).toBe(returnValue); -}); - -test("Class dot method call void", () => { - const returnValue = 4; - const source = `class TestClass { - public dotMethod: () => number = () => ${returnValue}; - } - - const classInstance = new TestClass(); - return classInstance.dotMethod();`; - - const result = util.transpileAndExecute(source); - - expect(result).toBe(returnValue); -}); - -test("Class dot method call with parameter", () => { - const returnValue = 4; - const source = `class TestClass { - public dotMethod: (x: number) => number = x => 3 * x; - } - - const classInstance = new TestClass(); - return classInstance.dotMethod(${returnValue});`; - - const result = util.transpileAndExecute(source); - - expect(result).toBe(3 * returnValue); -}); - -test("Class static dot method", () => { - const returnValue = 4; - const source = `class TestClass { - public static dotMethod: () => number = () => ${returnValue}; - } - - return TestClass.dotMethod();`; - - const result = util.transpileAndExecute(source); - - expect(result).toBe(returnValue); -}); - -test("Class static dot method with parameter", () => { - const returnValue = 4; - const source = `class TestClass { - public static dotMethod: (x: number) => number = x => 3 * x; - } - - return TestClass.dotMethod(${returnValue});`; - - const result = util.transpileAndExecute(source); - - expect(result).toBe(3 * returnValue); -}); - -test("Function bind", () => { - const source = `const abc = function (this: { a: number }, a: string, b: string) { return this.a + a + b; } - return abc.bind({ a: 4 }, "b")("c");`; - - const result = util.transpileAndExecute(source); - - expect(result).toBe("4bc"); -}); - -test("Function apply", () => { - const source = `const abc = function (this: { a: number }, a: string) { return this.a + a; } - return abc.apply({ a: 4 }, ["b"]);`; - - const result = util.transpileAndExecute(source); - - expect(result).toBe("4b"); -}); - -test("Function call", () => { - const source = `const abc = function (this: { a: number }, a: string) { return this.a + a; } - return abc.call({ a: 4 }, "b");`; - - const result = util.transpileAndExecute(source); - - expect(result).toBe("4b"); -}); - -test("Invalid property access call transpilation", () => { - const transformer = util.makeTestTransformer(); - - const mockObject: any = { - expression: ts.createLiteral("abc"), - }; - - expect(() => transformer.transformPropertyCall(mockObject as ts.CallExpression)).toThrowExactError( - TSTLErrors.InvalidPropertyCall(util.nodeStub) - ); -}); - -test("Function dead code after return", () => { - const result = util.transpileAndExecute(`function abc() { return 3; const a = 5; } return abc();`); - - expect(result).toBe(3); -}); - -test("Method dead code after return", () => { - const result = util.transpileAndExecute( - `class def { public static abc() { return 3; const a = 5; } } return def.abc();` - ); - - expect(result).toBe(3); -}); - -test("Recursive function definition", () => { - const result = util.transpileAndExecute(`function f() { return typeof f; }; return f();`); - - expect(result).toBe("function"); -}); - -test("Recursive function expression", () => { - const result = util.transpileAndExecute(`let f = function() { return typeof f; }; return f();`); - - expect(result).toBe("function"); -}); - -test("Wrapped recursive function expression", () => { - const result = util.transpileAndExecute( - `function wrap(fn: T) { return fn; } - let f = wrap(function() { return typeof f; }); return f();` - ); - - expect(result).toBe("function"); -}); - -test("Recursive arrow function", () => { - const result = util.transpileAndExecute(`let f = () => typeof f; return f();`); - - expect(result).toBe("function"); -}); - -test("Wrapped recursive arrow function", () => { - const result = util.transpileAndExecute( - `function wrap(fn: T) { return fn; } - let f = wrap(() => typeof f); return f();` - ); - - expect(result).toBe("function"); -}); - -test("Object method declaration", () => { - const result = util.transpileAndExecute( - `let o = { v: 4, m(i: number): number { return this.v * i; } }; return o.m(3);` - ); - expect(result).toBe(12); -}); - -test.each([{ args: ["bar"], expectResult: "foobar" }, { args: ["baz", "bar"], expectResult: "bazbar" }])( - "Function overload (%p)", - ({ args, expectResult }) => { - const code = ` - class O { - prop = "foo"; - method(s: string): string; - method(this: void, s1: string, s2: string): string; - method(s1: string) { - if (typeof this === "string") { - return this + s1; - } - return this.prop + s1; - } - }; - const o = new O(); - return o.method(${args.map(a => '"' + a + '"').join(", ")}); - `; - const result = util.transpileAndExecute(code); - expect(result).toBe(expectResult); - } -); - -test("Nested Function", () => { - const code = ` - class C { - private prop = "bar"; - public outer() { - const o = { - prop: "foo", - innerFunc: function() { return this.prop; }, - innerArrow: () => this.prop - }; - return o.innerFunc() + o.innerArrow(); - } - } - let c = new C(); - return c.outer(); - `; - const result = util.transpileAndExecute(code); - expect(result).toBe("foobar"); -}); - -test.each([{ s1: "abc", s2: "abc" }, { s1: "abc", s2: "def" }])("Dot vs Colon method call (%p)", ({ s1, s2 }) => { - const result = util.transpileAndExecute(` - class MyClass { - dotMethod(this: void, s: string) { - return s; - } - colonMethod(s: string) { - return s; - } - } - const inst = new MyClass(); - return inst.dotMethod("${s1}") == inst.colonMethod("${s2}"); - `); - expect(result).toBe(s1 === s2); -}); - -test("Element access call", () => { - const code = ` - class C { - prop = "bar"; - method(s: string) { return s + this.prop; } - } - const c = new C(); - return c['method']("foo"); - `; - const result = util.transpileAndExecute(code); - expect(result).toBe("foobar"); -}); - -test("Element access call no args", () => { - const code = ` - class C { - prop = "bar"; - method() { return this.prop; } - } - const c = new C(); - return c['method'](); - `; - const result = util.transpileAndExecute(code); - expect(result).toBe("bar"); -}); - -test("Complex element access call", () => { - const code = ` - class C { - prop = "bar"; - method(s: string) { return s + this.prop; } - } - function getC() { return new C(); } - return getC()['method']("foo"); - `; - const result = util.transpileAndExecute(code); - expect(result).toBe("foobar"); -}); - -test("Complex element access call no args", () => { - const code = ` - class C { - prop = "bar"; - method() { return this.prop; } - } - function getC() { return new C(); } - return getC()['method'](); - `; - const result = util.transpileAndExecute(code); - expect(result).toBe("bar"); -}); - -test("Complex element access call statement", () => { - const code = ` - let foo: string; - class C { - prop = "bar"; - method(s: string) { foo = s + this.prop; } - } - function getC() { return new C(); } - getC()['method']("foo"); - return foo; - `; - const result = util.transpileAndExecute(code); - expect(result).toBe("foobar"); -}); - -test.each([{ iterations: 1, expectedResult: 1 }, { iterations: 2, expectedResult: 42 }])( - "Generator functions value (%p)", - ({ iterations, expectedResult }) => { - const code = ` - function* seq(value: number) { - let a = yield value + 1; - return 42; - } - const gen = seq(0); - let ret: number; - for(let i = 0; i < ${iterations}; ++i) - { - ret = gen.next(i).value; - } - return ret; - `; - const result = util.transpileAndExecute(code); - expect(result).toBe(expectedResult); - } -); - -test.each([{ iterations: 1, expectedResult: false }, { iterations: 2, expectedResult: true }])( - "Generator functions done (%p)", - ({ iterations, expectedResult }) => { - const code = ` - function* seq(value: number) { - let a = yield value + 1; - return 42; - } - const gen = seq(0); - let ret: boolean; - for(let i = 0; i < ${iterations}; ++i) - { - ret = gen.next(i).done; - } - return ret; - `; - const result = util.transpileAndExecute(code); - expect(result).toBe(expectedResult); - } -); - -test("Generator for..of", () => { - const code = ` - function* seq() { - yield(1); - yield(2); - yield(3); - return 4; - } - let result = 0; - for(let i of seq()) - { - result = result * 10 + i; - } - return result - `; - const result = util.transpileAndExecute(code); - expect(result).toBe(123); -}); - -test("Function local overriding export", () => { - const code = ` - export const foo = 5; - function bar(foo: number) { - return foo; - } - export const result = bar(7); - `; - expect(util.transpileExecuteAndReturnExport(code, "result")).toBe(7); -}); - -test("Function using global as this", () => { - const code = ` - var foo = "foo"; - function bar(this: any) { - return this.foo; - } - `; - expect(util.transpileAndExecute("return foo;", undefined, undefined, code)).toBe("foo"); -}); - -test("Function rest binding pattern", () => { - const result = util.transpileAndExecute(` - function bar(foo: string, ...[bar, baz]: [string, string]) { - return bar + baz + foo; - } - return bar("abc", "def", "xyz"); - `); - - expect(result).toBe("defxyzabc"); -}); - -test.each([{}, { noHoisting: true }])("Function rest parameter", compilerOptions => { - const code = ` - function foo(a: unknown, ...b: string[]) { - return b.join(""); - } - return foo("A", "B", "C", "D"); - `; - - expect(util.transpileAndExecute(code, compilerOptions)).toBe("BCD"); -}); - -test.each([{}, { noHoisting: true }])("Function nested rest parameter", compilerOptions => { - const code = ` - function foo(a: unknown, ...b: string[]) { - function bar() { - return b.join(""); - } - return bar(); - } - return foo("A", "B", "C", "D"); - `; - - expect(util.transpileAndExecute(code, compilerOptions)).toBe("BCD"); -}); - -test.each([{}, { noHoisting: true }])("Function nested rest spread", compilerOptions => { - const code = ` - function foo(a: unknown, ...b: string[]) { - function bar() { - const c = [...b]; - return c.join(""); - } - return bar(); - } - return foo("A", "B", "C", "D"); - `; - - expect(util.transpileAndExecute(code, compilerOptions)).toBe("BCD"); -}); - -test.each([{}, { noHoisting: true }])("Function rest parameter (unreferenced)", compilerOptions => { - const code = ` - function foo(a: unknown, ...b: string[]) { - return "foobar"; - } - return foo("A", "B", "C", "D"); - `; - - expect(util.transpileString(code, compilerOptions)).not.toMatch("b = ({...})"); - expect(util.transpileAndExecute(code, compilerOptions)).toBe("foobar"); -}); - -test.each([{}, { noHoisting: true }])("Function rest parameter (referenced in property shorthand)", compilerOptions => { - const code = ` - function foo(a: unknown, ...b: string[]) { - const c = { b }; - return c.b.join(""); - } - return foo("A", "B", "C", "D"); - `; - - expect(util.transpileAndExecute(code, compilerOptions)).toBe("BCD"); -}); - -test.each([{}, { noHoisting: true }])("@vararg", compilerOptions => { - const code = ` - /** @vararg */ type LuaVarArg = A & { __luaVarArg?: never }; - function foo(a: unknown, ...b: LuaVarArg) { - const c = [...b]; - return c.join(""); - } - function bar(a: unknown, ...b: LuaVarArg) { - return foo(a, ...b); - } - return bar("A", "B", "C", "D"); - `; - - const lua = util.transpileString(code, compilerOptions); - expect(lua).not.toMatch("b = ({...})"); - expect(lua).not.toMatch("unpack"); - expect(util.transpileAndExecute(code, compilerOptions)).toBe("BCD"); -}); - -test.each([{}, { noHoisting: true }])("@vararg array access", compilerOptions => { - const code = ` - /** @vararg */ type LuaVarArg = A & { __luaVarArg?: never }; - function foo(a: unknown, ...b: LuaVarArg) { - const c = [...b]; - return c.join("") + b[0]; - } - return foo("A", "B", "C", "D"); - `; - - expect(util.transpileAndExecute(code, compilerOptions)).toBe("BCDB"); -}); - -test.each([{}, { noHoisting: true }])("@vararg global", compilerOptions => { - const code = ` - /** @vararg */ type LuaVarArg = A & { __luaVarArg?: never }; - declare const arg: LuaVarArg; - const arr = [...arg]; - const result = arr.join(""); - `; - - const luaBody = util.transpileString(code, compilerOptions, false); - expect(luaBody).not.toMatch("unpack"); - - const lua = ` - function test(...) - ${luaBody} - return result - end - return test("A", "B", "C", "D") - `; - - expect(util.executeLua(lua)).toBe("ABCD"); -}); - -test("named function expression reference", () => { - const code = ` - const y = function x(inp: string) { - return inp + typeof x; - }; - return y("foo-");`; - expect(util.transpileAndExecute(code)).toBe("foo-function"); -}); diff --git a/test/unit/functions/functions.spec.ts b/test/unit/functions/functions.spec.ts new file mode 100644 index 000000000..8214e6bfd --- /dev/null +++ b/test/unit/functions/functions.spec.ts @@ -0,0 +1,514 @@ +import * as ts from "typescript"; +import * as TSTLErrors from "../../../src/TSTLErrors"; +import * as util from "../../util"; + +test("Arrow Function Expression", () => { + util.testFunction` + const add = (a, b) => a + b; + return add(1, 2); + `.expectToMatchJsResult(); +}); + +test("Returning arrow function from arrow function", () => { + util.testFunction` + const add = (x: number) => (y: number) => x + y; + return add(1)(2); + `.expectToMatchJsResult(); +}); + +test.each(["i++", "i--", "++i", "--i"])("Arrow function unary expression (%p)", lambda => { + util.testFunction` + let i = 10; + [1,2,3,4,5].forEach(() => ${lambda}); + return i; + `.expectToMatchJsResult(); +}); + +test.each(["b => a = b", "b => a += b", "b => a -= b", "b => a *= b", "b => a /= b", "b => a **= b", "b => a %= b"])( + "Arrow function assignment (%p)", + lambda => { + util.testFunction` + let a = 10; + let lambda = ${lambda}; + lambda(5); + return a; + `.expectToMatchJsResult(); + } +); + +test.each([{ inp: [] }, { inp: [5] }, { inp: [1, 2] }])("Arrow Default Values (%p)", ({ inp }) => { + // Default value is 3 for v1 + const v1 = inp.length > 0 ? inp[0] : 3; + // Default value is 4 for v2 + const v2 = inp.length > 1 ? inp[1] : 4; + + const callArgs = inp.join(","); + + const result = util.transpileAndExecute( + `let add = (a: number = 3, b: number = 4) => a+b; + return add(${callArgs});` + ); + + expect(result).toBe(v1 + v2); +}); + +test("Function Expression", () => { + util.testFunction` + let add = function(a, b) {return a+b}; + return add(1,2); + `.expectToMatchJsResult(); +}); + +test("Function definition scope", () => { + util.testFunction` + function abc() { function xyz() { return 5; } } + function def() { function xyz() { return 3; } abc(); return xyz(); } + return def(); + `.expectToMatchJsResult(); +}); + +test("Function default parameter", () => { + util.testFunction` + function abc(defaultParam: string = "abc") { return defaultParam; } + return abc() + abc("def"); + `.expectToMatchJsResult(); +}); + +test.each([{ inp: [] }, { inp: [5] }, { inp: [1, 2] }])("Function Default Values (%p)", ({ inp }) => { + // Default value is 3 for v1 + const v1 = inp.length > 0 ? inp[0] : 3; + // Default value is 4 for v2 + const v2 = inp.length > 1 ? inp[1] : 4; + + const callArgs = inp.join(","); + + const result = util.transpileAndExecute( + `let add = function(a: number = 3, b: number = 4) { return a+b; }; + return add(${callArgs});` + ); + + expect(result).toBe(v1 + v2); +}); + +test("Function default array binding parameter", () => { + util.testFunction` + function foo([bar]: [string] = ["foobar"]) { + return bar; + } + return foo(); + `.expectToMatchJsResult(); +}); + +test("Function default object binding parameter", () => { + util.testFunction` + function foo({ bar }: { bar: string } = { bar: "foobar" }) { + return bar; + } + return foo(); + `.expectToMatchJsResult(); +}); + +test("Function default binding parameter maintains order", () => { + util.testFunction` + const resultsA = [{x: "foo"}, {x: "baz"}]; + const resultsB = ["blah", "bar"]; + let i = 0; + function a() { return resultsA[i++]; } + function b() { return resultsB[i++]; } + function foo({ x }: { x: string } = a(), y = b()) { + return x + y; + } + return foo(); + `.expectToMatchJsResult(); +}); + +test("Class method call", () => { + util.testFunction` + class TestClass { + public classMethod(): number { return 4; } + } + + const classInstance = new TestClass(); + return classInstance.classMethod(); + `.expectToMatchJsResult(); +}); + +test("Class dot method call void", () => { + util.testFunction` + class TestClass { + public dotMethod: () => number = () => 4; + } + + const classInstance = new TestClass(); + return classInstance.dotMethod(); + `.expectToMatchJsResult(); +}); + +test("Class dot method call with parameter", () => { + util.testFunction` + class TestClass { + public dotMethod: (x: number) => number = x => 3 * x; + } + + const classInstance = new TestClass(); + return classInstance.dotMethod(4); + `.expectToMatchJsResult(); +}); + +test("Class static dot method", () => { + util.testFunction` + class TestClass { + public static dotMethod: () => number = () => 4; + } + + return TestClass.dotMethod(); + `.expectToMatchJsResult(); +}); + +test("Class static dot method with parameter", () => { + util.testFunction` + class TestClass { + public static dotMethod: (x: number) => number = x => 3 * x; + } + + return TestClass.dotMethod(4); + `.expectToMatchJsResult(); +}); + +test("Function bind", () => { + util.testFunction` + const abc = function (this: { a: number }, a: string, b: string) { return this.a + a + b; } + return abc.bind({ a: 4 }, "b")("c"); + `.expectToMatchJsResult(); +}); + +test("Function apply", () => { + util.testFunction` + const abc = function (this: { a: number }, a: string) { return this.a + a; } + return abc.apply({ a: 4 }, ["b"]); + `.expectToMatchJsResult(); +}); + +test("Function call", () => { + util.testFunction` + const abc = function (this: { a: number }, a: string) { return this.a + a; } + return abc.call({ a: 4 }, "b"); + `.expectToMatchJsResult(); +}); + +test("Invalid property access call transpilation", () => { + const transformer = util.makeTestTransformer(); + + const mockObject: any = { + expression: ts.createLiteral("abc"), + }; + + expect(() => transformer.transformPropertyCall(mockObject as ts.CallExpression)).toThrowExactError( + TSTLErrors.InvalidPropertyCall(util.nodeStub) + ); +}); + +test("Recursive function definition", () => { + util.testFunction` + function f() { return typeof f; }; + return f(); + `.expectToMatchJsResult(); +}); + +test("Recursive function expression", () => { + util.testFunction` + let f = function() { return typeof f; }; + return f(); + `.expectToMatchJsResult(); +}); + +test("Wrapped recursive function expression", () => { + util.testFunction` + function wrap(fn: T) { return fn; } + let f = wrap(function() { return typeof f; }); return f(); + `.expectToMatchJsResult(); +}); + +test("Recursive arrow function", () => { + util.testFunction` + let f = () => typeof f; + return f(); + `.expectToMatchJsResult(); +}); + +test("Wrapped recursive arrow function", () => { + util.testFunction` + function wrap(fn: T) { return fn; } + let f = wrap(() => typeof f); + return f(); + `.expectToMatchJsResult(); +}); + +test("Object method declaration", () => { + util.testFunction` + let o = { v: 4, m(i: number): number { return this.v * i; } }; + return o.m(3); + `.expectToMatchJsResult(); +}); + +test.each([{ args: ["bar"], expected: "foobar" }, { args: ["baz", "bar"], expected: "bazbar" }])( + "Function overload (%p)", + ({ args, expected }) => { + util.testFunction` + class O { + prop = "foo"; + method(s: string): string; + method(this: void, s1: string, s2: string): string; + method(s1: string) { + if (typeof this === "string") { + return this + s1; + } + return this.prop + s1; + } + }; + const o = new O(); + return o.method(${util.valuesToString(args)}); + `.expectToEqual(expected); + } +); + +test("Nested Function", () => { + util.testFunction` + class C { + private prop = "bar"; + public outer() { + const o = { + prop: "foo", + innerFunc: function() { return this.prop; }, + innerArrow: () => this.prop + }; + return o.innerFunc() + o.innerArrow(); + } + } + let c = new C(); + return c.outer(); + `.expectToMatchJsResult(); +}); + +test.each([{ s1: "abc", s2: "abc" }, { s1: "abc", s2: "def" }])("Dot vs Colon method call (%p)", ({ s1, s2 }) => { + util.testFunction` + class MyClass { + dotMethod(this: void, s: string) { + return s; + } + colonMethod(s: string) { + return s; + } + } + const inst = new MyClass(); + return inst.dotMethod("${s1}") == inst.colonMethod("${s2}"); + `.expectToMatchJsResult(); +}); + +test("Element access call", () => { + util.testFunction` + class C { + prop = "bar"; + method(s: string) { return s + this.prop; } + } + const c = new C(); + return c['method']("foo"); + `.expectToMatchJsResult(); +}); + +test("Element access call no args", () => { + util.testFunction` + class C { + prop = "bar"; + method() { return this.prop; } + } + const c = new C(); + return c['method'](); + `.expectToMatchJsResult(); +}); + +test("Complex element access call", () => { + util.testFunction` + class C { + prop = "bar"; + method(s: string) { return s + this.prop; } + } + function getC() { return new C(); } + return getC()['method']("foo"); + `.expectToMatchJsResult(); +}); + +test("Complex element access call no args", () => { + util.testFunction` + class C { + prop = "bar"; + method() { return this.prop; } + } + function getC() { return new C(); } + return getC()['method'](); + `.expectToMatchJsResult(); +}); + +test("Complex element access call statement", () => { + util.testFunction` + let foo: string; + class C { + prop = "bar"; + method(s: string) { foo = s + this.prop; } + } + function getC() { return new C(); } + getC()['method']("foo"); + return foo; + `.expectToMatchJsResult(); +}); + +test.each([1, 2])("Generator functions value (%p)", iterations => { + util.testFunction` + function* seq(value: number) { + let a = yield value + 1; + return 42; + } + const gen = seq(0); + let ret: number; + for(let i = 0; i < ${iterations}; ++i) { + ret = gen.next(i).value; + } + return ret; + `.expectToMatchJsResult(); +}); + +test.each([1, 2])("Generator functions done (%p)", iterations => { + util.testFunction` + function* seq(value: number) { + let a = yield value + 1; + return 42; + } + const gen = seq(0); + let ret: boolean; + for(let i = 0; i < ${iterations}; ++i) { + ret = gen.next(i).done; + } + return ret; + `.expectToMatchJsResult(); +}); + +test("Generator for..of", () => { + util.testFunction` + function* seq() { + yield(1); + yield(2); + yield(3); + return 4; + } + let result = 0; + for(let i of seq()) { + result = result * 10 + i; + } + return result + `.expectToMatchJsResult(); +}); + +test("Function local overriding export", () => { + util.testModule` + export const foo = 5; + function bar(foo: number) { + return foo; + } + export const result = bar(7); + ` + .setReturnExport("result") + .expectToMatchJsResult(); +}); + +test("Function using global as this", () => { + const tsHeader = ` + var foo = "foo"; + function bar(this: any) { + return this.foo; + } + `; + + util.testExpression`foo`.setTsHeader(tsHeader).expectToMatchJsResult(); +}); + +test("Function rest binding pattern", () => { + util.testFunction` + function bar(foo: string, ...[bar, baz]: [string, string]) { + return bar + baz + foo; + } + return bar("abc", "def", "xyz"); + `.expectToMatchJsResult(); +}); + +test.each([{}, { noHoisting: true }])("Function rest parameter", compilerOptions => { + const code = ` + function foo(a: unknown, ...b: string[]) { + return b.join(""); + } + return foo("A", "B", "C", "D"); + `; + + expect(util.transpileAndExecute(code, compilerOptions)).toBe("BCD"); +}); + +test.each([{}, { noHoisting: true }])("Function nested rest parameter", compilerOptions => { + const code = ` + function foo(a: unknown, ...b: string[]) { + function bar() { + return b.join(""); + } + return bar(); + } + return foo("A", "B", "C", "D"); + `; + + expect(util.transpileAndExecute(code, compilerOptions)).toBe("BCD"); +}); + +test.each([{}, { noHoisting: true }])("Function nested rest spread", compilerOptions => { + const code = ` + function foo(a: unknown, ...b: string[]) { + function bar() { + const c = [...b]; + return c.join(""); + } + return bar(); + } + return foo("A", "B", "C", "D"); + `; + + expect(util.transpileAndExecute(code, compilerOptions)).toBe("BCD"); +}); + +test.each([{}, { noHoisting: true }])("Function rest parameter (unreferenced)", compilerOptions => { + const code = ` + function foo(a: unknown, ...b: string[]) { + return "foobar"; + } + return foo("A", "B", "C", "D"); + `; + + expect(util.transpileString(code, compilerOptions)).not.toMatch("b = ({...})"); + expect(util.transpileAndExecute(code, compilerOptions)).toBe("foobar"); +}); + +test.each([{}, { noHoisting: true }])("Function rest parameter (referenced in property shorthand)", compilerOptions => { + const code = ` + function foo(a: unknown, ...b: string[]) { + const c = { b }; + return c.b.join(""); + } + return foo("A", "B", "C", "D"); + `; + + expect(util.transpileAndExecute(code, compilerOptions)).toBe("BCD"); +}); + +test("named function expression reference", () => { + const code = ` + const y = function x(inp: string) { + return inp + typeof x; + }; + return y("foo-");`; + expect(util.transpileAndExecute(code)).toBe("foo-function"); +}); diff --git a/test/unit/assignments/functionExpressionTypeInference.spec.ts b/test/unit/functions/validation/functionExpressionTypeInference.spec.ts similarity index 99% rename from test/unit/assignments/functionExpressionTypeInference.spec.ts rename to test/unit/functions/validation/functionExpressionTypeInference.spec.ts index aefc24427..767fe0b30 100644 --- a/test/unit/assignments/functionExpressionTypeInference.spec.ts +++ b/test/unit/functions/validation/functionExpressionTypeInference.spec.ts @@ -1,4 +1,4 @@ -import * as util from "../../util"; +import * as util from "../../../util"; test.each(["noSelf", "noSelfInFile"])("noSelf function method argument (%p)", noSelfTag => { const header = ` diff --git a/test/unit/assignments/functionPermutations.ts b/test/unit/functions/validation/functionPermutations.ts similarity index 100% rename from test/unit/assignments/functionPermutations.ts rename to test/unit/functions/validation/functionPermutations.ts diff --git a/test/unit/assignments/invalidFunctionAssignments.spec.ts b/test/unit/functions/validation/invalidFunctionAssignments.spec.ts similarity index 98% rename from test/unit/assignments/invalidFunctionAssignments.spec.ts rename to test/unit/functions/validation/invalidFunctionAssignments.spec.ts index 97002fd7d..06050ac5e 100644 --- a/test/unit/assignments/invalidFunctionAssignments.spec.ts +++ b/test/unit/functions/validation/invalidFunctionAssignments.spec.ts @@ -1,5 +1,5 @@ -import * as TSTLErrors from "../../../src/TSTLErrors"; -import * as util from "../../util"; +import * as TSTLErrors from "../../../../src/TSTLErrors"; +import * as util from "../../../util"; import { invalidTestFunctionAssignments, invalidTestFunctionCasts } from "./functionPermutations"; test.each(invalidTestFunctionAssignments)( diff --git a/test/unit/assignments/validFunctionAssignments.spec.ts b/test/unit/functions/validation/validFunctionAssignments.spec.ts similarity index 99% rename from test/unit/assignments/validFunctionAssignments.spec.ts rename to test/unit/functions/validation/validFunctionAssignments.spec.ts index be553b956..be1130ba6 100644 --- a/test/unit/assignments/validFunctionAssignments.spec.ts +++ b/test/unit/functions/validation/validFunctionAssignments.spec.ts @@ -1,17 +1,17 @@ -import * as util from "../../util"; +import * as util from "../../../util"; import { - validTestFunctionAssignments, - validTestFunctionCasts, - selfTestFunctions, + anonTestFunctionExpressions, + anonTestFunctionType, + noSelfTestFunctionExpressions, noSelfTestFunctions, - TestFunctionAssignment, + noSelfTestFunctionType, selfTestFunctionExpressions, - noSelfTestFunctionExpressions, + selfTestFunctions, selfTestFunctionType, - anonTestFunctionType, - noSelfTestFunctionType, - anonTestFunctionExpressions, TestFunction, + TestFunctionAssignment, + validTestFunctionAssignments, + validTestFunctionCasts, } from "./functionPermutations"; test.each(validTestFunctionAssignments)("Valid function variable declaration (%p)", (testFunction, functionType) => { diff --git a/test/unit/identifiers.spec.ts b/test/unit/identifiers.spec.ts index d8b00733f..bde4477fa 100644 --- a/test/unit/identifiers.spec.ts +++ b/test/unit/identifiers.spec.ts @@ -21,56 +21,50 @@ const invalidLuaNames = [...invalidLuaCharNames, ...luaKeywords.values()]; const validTsInvalidLuaNames = [...invalidLuaCharNames, ...validTsInvalidLuaKeywordNames]; test.each(validTsInvalidLuaNames)("invalid lua identifier name (%p)", name => { - const code = ` + util.testFunction` const ${name} = "foobar"; - return ${name};`; - - expect(util.transpileAndExecute(code)).toBe("foobar"); + return ${name}; + `.expectToMatchJsResult(); }); test.each([...luaKeywords.values()])("lua keyword as property name (%p)", keyword => { - const code = ` + util.testFunction` const x = { ${keyword}: "foobar" }; - return x.${keyword};`; - - expect(util.transpileAndExecute(code)).toBe("foobar"); + return x.${keyword}; + `.expectToMatchJsResult(); }); test.each(validTsInvalidLuaKeywordNames)("destructuring lua keyword (%p)", keyword => { - const code = ` - const { foo: ${keyword} } = { foo: "foobar" }; - return ${keyword};`; - - expect(util.transpileAndExecute(code)).toBe("foobar"); + util.testFunction` + const { foo: ${keyword} } = { foo: "foobar" }; + return ${keyword}; + `.expectToMatchJsResult(); }); test.each(validTsInvalidLuaKeywordNames)("destructuring shorthand lua keyword (%p)", keyword => { - const code = ` - const { ${keyword} } = { ${keyword}: "foobar" }; - return ${keyword};`; - - expect(util.transpileAndExecute(code)).toBe("foobar"); + util.testFunction` + const { ${keyword} } = { ${keyword}: "foobar" }; + return ${keyword}; + `.expectToMatchJsResult(); }); test.each(invalidLuaNames)("lua keyword or invalid identifier as method call (%p)", name => { - const code = ` + util.testFunction` const foo = { ${name}(arg: string) { return "foo" + arg; } }; - return foo.${name}("bar");`; - - expect(util.transpileAndExecute(code)).toBe("foobar"); + return foo.${name}("bar"); + `.expectToMatchJsResult(); }); test.each(invalidLuaNames)("lua keyword or invalid identifier as complex method call (%p)", name => { - const code = ` + util.testFunction` const foo = { ${name}(arg: string) { return "foo" + arg; } }; function getFoo() { return foo; } - return getFoo().${name}("bar");`; - - expect(util.transpileAndExecute(code)).toBe("foobar"); + return getFoo().${name}("bar"); + `.expectToMatchJsResult(); }); test.each([ @@ -84,13 +78,12 @@ test.each([ "enum local {}", "function local() {}", ])("ambient identifier cannot be a lua keyword (%p)", statement => { - const code = ` + util.testModule` declare ${statement} - const foo = local;`; - - expect(() => util.transpileString(code)).toThrow( - TSTLErrors.InvalidAmbientIdentifierName(ts.createIdentifier("local")).message - ); + local; + ` + .disableSemanticCheck() + .expectToHaveDiagnosticOfError(TSTLErrors.InvalidAmbientIdentifierName(ts.createIdentifier("local"))); }); test.each([ @@ -102,42 +95,40 @@ test.each([ "namespace $$$ { export const bar: any; }", "module $$$ { export const bar: any; }", "enum $$$ {}", - "function $$$() {}", + "function $$$();", ])("ambient identifier must be a valid lua identifier (%p)", statement => { - const code = ` + util.testModule` declare ${statement} - const foo = $$$;`; - - expect(() => util.transpileString(code)).toThrow( - TSTLErrors.InvalidAmbientIdentifierName(ts.createIdentifier("$$$")).message - ); + $$$; + `.expectToHaveDiagnosticOfError(TSTLErrors.InvalidAmbientIdentifierName(ts.createIdentifier("$$$"))); }); test.each(validTsInvalidLuaNames)( "ambient identifier must be a valid lua identifier (object literal shorthand) (%p)", name => { - const code = ` - declare var ${name}: any; - const foo = { ${name} };`; - - expect(() => util.transpileString(code)).toThrow( - TSTLErrors.InvalidAmbientIdentifierName(ts.createIdentifier(name)).message - ); + util.testModule` + declare var ${name}: any; + const foo = { ${name} }; + `.expectToHaveDiagnosticOfError(TSTLErrors.InvalidAmbientIdentifierName(ts.createIdentifier(name))); } ); test.each(validTsInvalidLuaNames)("undeclared identifier must be a valid lua identifier (%p)", name => { - expect(() => util.transpileString(`const foo = ${name};`)).toThrow( - TSTLErrors.InvalidAmbientIdentifierName(ts.createIdentifier(name)).message - ); + util.testModule` + const foo = ${name}; + ` + .disableSemanticCheck() + .expectToHaveDiagnosticOfError(TSTLErrors.InvalidAmbientIdentifierName(ts.createIdentifier(name))); }); test.each(validTsInvalidLuaNames)( "undeclared identifier must be a valid lua identifier (object literal shorthand) (%p)", name => { - expect(() => util.transpileString(`const foo = { ${name} };`)).toThrow( - TSTLErrors.InvalidAmbientIdentifierName(ts.createIdentifier(name)).message - ); + util.testModule` + const foo = { ${name} }; + ` + .disableSemanticCheck() + .expectToHaveDiagnosticOfError(TSTLErrors.InvalidAmbientIdentifierName(ts.createIdentifier(name))); } ); @@ -196,15 +187,14 @@ test.each(validTsInvalidLuaNames)( ); test.each(validTsInvalidLuaNames)("class with invalid lua name has correct name property", name => { - const code = ` + util.testFunction` class ${name} {} - return ${name}.name;`; - - expect(util.transpileAndExecute(code)).toBe(name); + return ${name}.name; + `.expectToMatchJsResult(); }); test.each(validTsInvalidLuaNames)("decorated class with invalid lua name", name => { - const code = ` + util.testFunction` function decorator(c: T): T { c.bar = "foobar"; return c; @@ -212,9 +202,8 @@ test.each(validTsInvalidLuaNames)("decorated class with invalid lua name", name @decorator class ${name} {} - return (${name} as any).bar;`; - - expect(util.transpileAndExecute(code)).toBe("foobar"); + return (${name} as any).bar; + `.expectToMatchJsResult(); }); test.each(validTsInvalidLuaNames)("exported decorated class with invalid lua name", name => { @@ -788,66 +777,3 @@ test("exported variable with lua keyword as name is not renamed", () => { expect(util.transpileExecuteAndReturnExport(code, "print")).toBe("foobar"); }); - -describe("globalThis translation", () => { - test("globalThis to _G (expression)", () => { - const code = ` - var foo = "bar"; - return globalThis.foo;`; - - const lua = util.transpileString(code); - - expect(util.executeLua(lua)).toBe("bar"); - }); - - test("globalThis to _G (assign)", () => { - const code = ` - globalThis.foo = "bar"; - return globalThis.foo;`; - - expect(util.transpileAndExecute(code)).toBe("bar"); - }); - - test("globalThis to _G (reassign)", () => { - const code = ` - globalThis.foo = "bar"; - globalThis.foo = "baz"; - return globalThis.foo;`; - - expect(util.transpileAndExecute(code)).toBe("baz"); - }); - - test("globalThis to _G (function)", () => { - const code = ` - globalThis.foo = () => "bar"; - return globalThis.foo();`; - - expect(util.transpileAndExecute(code)).toBe("bar"); - }); - - test("globalThis to _G (assign + noImplicitAny)", () => { - const code = ` - (globalThis).foo = "bar"; - return (globalThis).foo;`; - - expect(util.transpileAndExecute(code)).toBe("bar"); - }); - - test("globalThis to _G (var)", () => { - const code = ` - var globalFoo = "bar"; - return globalThis.globalFoo;`; - - const lua = util.transpileString(code); - - expect(util.executeLua(lua)).toBe("bar"); - }); - - test("globalThis to _G (let)", () => { - const code = ` - let NotAGlobalFoo = "bar"; - return (globalThis).NotAGlobalFoo;`; - - expect(util.transpileAndExecute(code)).toBe(undefined); - }); -}); diff --git a/test/unit/json.spec.ts b/test/unit/json.spec.ts index 642243155..e822fc1ae 100644 --- a/test/unit/json.spec.ts +++ b/test/unit/json.spec.ts @@ -8,17 +8,16 @@ const jsonOptions = { moduleResolution: ts.ModuleResolutionKind.NodeJs, }; -test.each(["0", '""', "[]", '[1, "2", []]', '{ "a": "b" }', '{ "a": { "b": "c" } }'])("JSON (%p)", json => { - const lua = util - .transpileString({ "main.json": json }, jsonOptions, false) - .replace(/^return ([\s\S]+)$/, "return JSONStringify($1)"); - - const result = util.executeLua(lua); - expect(JSON.parse(result)).toEqual(JSON.parse(json)); +test.each([0, "", [], [1, "2", []], { a: "b" }, { a: { b: "c" } }])("JSON (%p)", json => { + util.testModule(JSON.stringify(json)) + .setOptions(jsonOptions) + .setMainFileName("main.json") + .expectToEqual(json); }); test("Empty JSON", () => { - expect(() => util.transpileString({ "main.json": "" }, jsonOptions, false)).toThrowExactError( - TSTLErrors.InvalidJsonFileContent(util.nodeStub) - ); + util.testModule("") + .setOptions(jsonOptions) + .setMainFileName("main.json") + .expectToHaveDiagnosticOfError(TSTLErrors.InvalidJsonFileContent(util.nodeStub)); }); diff --git a/test/unit/loops.spec.ts b/test/unit/loops.spec.ts index 57cd5cc11..34e92998a 100644 --- a/test/unit/loops.spec.ts +++ b/test/unit/loops.spec.ts @@ -170,53 +170,48 @@ test.each([{ inp: [0, 1, 2, 3], expected: [1, 2, 3, 4] }])("forNoCondition (%p)" expect(result).toBe(JSON.stringify(expected)); }); -test.each([{ inp: [0, 1, 2, 3], expected: [1, 2, 3, 4] }])("forNoPostExpression (%p)", ({ inp, expected }) => { - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; - let i = 0; - for (;;) { - if (i >= arrTest.length) { - break; - } - - arrTest[i] = arrTest[i] + 1; - - i++; +test("forNoPostExpression (%p)", () => { + util.testFunction` + let arrTest = [0, 1, 2, 3]; + let i = 0; + for (;;) { + if (i >= arrTest.length) { + break; } - return JSONStringify(arrTest);` - ); - expect(result).toBe(JSON.stringify(expected)); + arrTest[i] = arrTest[i] + 1; + + i++; + } + return arrTest; + `.expectToMatchJsResult(); }); test.each([ - { inp: [0, 1, 2, 3], expected: [1, 2, 3, 4], header: "let i = 0; i < arrTest.length; i++" }, - { inp: [0, 1, 2, 3], expected: [1, 2, 3, 4], header: "let i = 0; i <= arrTest.length - 1; i++" }, - { inp: [0, 1, 2, 3], expected: [1, 2, 3, 4], header: "let i = 0; arrTest.length > i; i++" }, - { inp: [0, 1, 2, 3], expected: [1, 2, 3, 4], header: "let i = 0; arrTest.length - 1 >= i; i++" }, - { inp: [0, 1, 2, 3], expected: [1, 1, 3, 3], header: "let i = 0; i < arrTest.length; i += 2" }, - { inp: [0, 1, 2, 3], expected: [1, 2, 3, 4], header: "let i = arrTest.length - 1; i >= 0; i--" }, - { inp: [0, 1, 2, 3], expected: [0, 2, 2, 4], header: "let i = arrTest.length - 1; i >= 0; i -= 2" }, - { inp: [0, 1, 2, 3], expected: [0, 2, 2, 4], header: "let i = arrTest.length - 1; i > 0; i -= 2" }, -])("forheader (%p)", ({ inp, expected, header }) => { - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; + { inp: [0, 1, 2, 3], header: "let i = 0; i < arrTest.length; i++" }, + { inp: [0, 1, 2, 3], header: "let i = 0; i <= arrTest.length - 1; i++" }, + { inp: [0, 1, 2, 3], header: "let i = 0; arrTest.length > i; i++" }, + { inp: [0, 1, 2, 3], header: "let i = 0; arrTest.length - 1 >= i; i++" }, + { inp: [0, 1, 2, 3], header: "let i = 0; i < arrTest.length; i += 2" }, + { inp: [0, 1, 2, 3], header: "let i = arrTest.length - 1; i >= 0; i--" }, + { inp: [0, 1, 2, 3], header: "let i = arrTest.length - 1; i >= 0; i -= 2" }, + { inp: [0, 1, 2, 3], header: "let i = arrTest.length - 1; i > 0; i -= 2" }, +])("forheader (%p)", ({ inp, header }) => { + util.testFunction` + let arrTest = ${JSON.stringify(inp)}; for (${header}) { arrTest[i] = arrTest[i] + 1; } - return JSONStringify(arrTest);` - ); - - expect(result).toBe(JSON.stringify(expected)); + return arrTest; + `.expectToMatchJsResult(); }); test("for scope", () => { - const code = ` + util.testFunction` let i = 42; for (let i = 0; i < 10; ++i) {} return i; - `; - expect(util.transpileAndExecute(code)).toBe(42); + `.expectToMatchJsResult(); }); test.each([ @@ -279,6 +274,15 @@ test.each([{ inp: [0, 1, 2], expected: [1, 2, 3] }])("forof (%p)", ({ inp, expec expect(result).toBe(JSON.stringify(expected)); }); +test("Tuple loop", () => { + util.testFunction` + const tuple: [number, number, number] = [3,5,1]; + let count = 0; + for (const value of tuple) { count += value; } + return count; + `.expectToMatchJsResult(); +}); + test.each([{ inp: [0, 1, 2], expected: [1, 2, 3] }])("forof existing variable (%p)", ({ inp, expected }) => { const result = util.transpileAndExecute( `let objTest = ${JSON.stringify(inp)}; @@ -496,264 +500,6 @@ test("forof with array typed as iterable", () => { expect(util.transpileAndExecute(code)).toBe("ABC"); }); -test("forof lua iterator", () => { - const code = ` - const arr = ["a", "b", "c"]; - /** @luaIterator */ - interface Iter extends Iterable {} - function luaIter(): Iter { - let i = 0; - return (() => arr[i++]) as any; - } - let result = ""; - for (let e of luaIter()) { result += e; } - return result; - `; - const compilerOptions = { - luaLibImport: tstl.LuaLibImportKind.Require, - luaTarget: tstl.LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - expect(result).toBe("abc"); -}); - -test("forof array lua iterator", () => { - const code = ` - const arr = ["a", "b", "c"]; - /** @luaIterator */ - interface Iter extends Array {} - function luaIter(): Iter { - let i = 0; - return (() => arr[i++]) as any; - } - let result = ""; - for (let e of luaIter()) { result += e; } - return result; - `; - const compilerOptions = { - luaLibImport: tstl.LuaLibImportKind.Require, - luaTarget: tstl.LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - expect(result).toBe("abc"); -}); - -test("forof lua iterator with existing variable", () => { - const code = ` - const arr = ["a", "b", "c"]; - /** @luaIterator */ - interface Iter extends Iterable {} - function luaIter(): Iter { - let i = 0; - return (() => arr[i++]) as any; - } - let result = ""; - let e: string; - for (e of luaIter()) { result += e; } - return result; - `; - const compilerOptions = { - luaLibImport: tstl.LuaLibImportKind.Require, - luaTarget: tstl.LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - expect(result).toBe("abc"); -}); - -test("forof lua iterator destructuring", () => { - const code = ` - const arr = ["a", "b", "c"]; - /** @luaIterator */ - interface Iter extends Iterable<[string, string]> {} - function luaIter(): Iter { - let i = 0; - return (() => arr[i] && [i.toString(), arr[i++]]) as any; - } - let result = ""; - for (let [a, b] of luaIter()) { result += a + b; } - return result; - `; - const compilerOptions = { - luaLibImport: tstl.LuaLibImportKind.Require, - luaTarget: tstl.LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - expect(result).toBe("0a1b2c"); -}); - -test("forof lua iterator destructuring with existing variables", () => { - const code = ` - const arr = ["a", "b", "c"]; - /** @luaIterator */ - interface Iter extends Iterable<[string, string]> {} - function luaIter(): Iter { - let i = 0; - return (() => arr[i] && [i.toString(), arr[i++]]) as any; - } - let result = ""; - let a: string; - let b: string; - for ([a, b] of luaIter()) { result += a + b; } - return result; - `; - const compilerOptions = { - luaLibImport: tstl.LuaLibImportKind.Require, - luaTarget: tstl.LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - expect(result).toBe("0a1b2c"); -}); - -test("forof lua iterator tuple-return", () => { - const code = ` - const arr = ["a", "b", "c"]; - /** @luaIterator */ - /** @tupleReturn */ - interface Iter extends Iterable<[string, string]> {} - function luaIter(): Iter { - let i = 0; - /** @tupleReturn */ - function iter() { return arr[i] && [i.toString(), arr[i++]] || []; } - return iter as any; - } - let result = ""; - for (let [a, b] of luaIter()) { result += a + b; } - return result; - `; - const compilerOptions = { - luaLibImport: tstl.LuaLibImportKind.Require, - luaTarget: tstl.LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - expect(result).toBe("0a1b2c"); -}); - -test("forof lua iterator tuple-return with existing variables", () => { - const code = ` - const arr = ["a", "b", "c"]; - /** @luaIterator */ - /** @tupleReturn */ - interface Iter extends Iterable<[string, string]> {} - function luaIter(): Iter { - let i = 0; - /** @tupleReturn */ - function iter() { return arr[i] && [i.toString(), arr[i++]] || []; } - return iter as any; - } - let result = ""; - let a: string; - let b: string; - for ([a, b] of luaIter()) { result += a + b; } - return result; - `; - const compilerOptions = { - luaLibImport: tstl.LuaLibImportKind.Require, - luaTarget: tstl.LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - expect(result).toBe("0a1b2c"); -}); - -test("forof lua iterator tuple-return single variable", () => { - const code = ` - /** @luaIterator */ - /** @tupleReturn */ - interface Iter extends Iterable<[string, string]> {} - declare function luaIter(): Iter; - for (let x of luaIter()) {} - `; - const compilerOptions = { - luaLibImport: tstl.LuaLibImportKind.Require, - luaTarget: tstl.LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - expect(() => util.transpileString(code, compilerOptions)).toThrowExactError( - TSTLErrors.UnsupportedNonDestructuringLuaIterator(util.nodeStub) - ); -}); - -test("forof lua iterator tuple-return single existing variable", () => { - const code = ` - /** @luaIterator */ - /** @tupleReturn */ - interface Iter extends Iterable<[string, string]> {} - declare function luaIter(): Iter; - let x: [string, string]; - for (x of luaIter()) {} - `; - const compilerOptions = { - luaLibImport: tstl.LuaLibImportKind.Require, - luaTarget: tstl.LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - expect(() => util.transpileString(code, compilerOptions)).toThrowExactError( - TSTLErrors.UnsupportedNonDestructuringLuaIterator(util.nodeStub) - ); -}); - -test("forof forwarded lua iterator", () => { - const code = ` - const arr = ["a", "b", "c"]; - /** @luaIterator */ - interface Iter extends Iterable {} - function luaIter(): Iter { - let i = 0; - function iter() { return arr[i++]; } - return iter as any; - } - function forward() { - const iter = luaIter(); - return iter; - } - let result = ""; - for (let a of forward()) { result += a; } - return result; - `; - const compilerOptions = { - luaLibImport: tstl.LuaLibImportKind.Require, - luaTarget: tstl.LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - expect(result).toBe("abc"); -}); - -test("forof forwarded lua iterator with tupleReturn", () => { - const code = ` - const arr = ["a", "b", "c"]; - /** @luaIterator */ - /** @tupleReturn */ - interface Iter extends Iterable<[string, string]> {} - function luaIter(): Iter { - let i = 0; - /** @tupleReturn */ - function iter() { return arr[i] && [i.toString(), arr[i++]] || []; } - return iter as any; - } - function forward() { - const iter = luaIter(); - return iter; - } - let result = ""; - for (let [a, b] of forward()) { result += a + b; } - return result; - `; - const compilerOptions = { - luaLibImport: tstl.LuaLibImportKind.Require, - luaTarget: tstl.LuaTarget.Lua53, - target: ts.ScriptTarget.ES2015, - }; - const result = util.transpileAndExecute(code, compilerOptions); - expect(result).toBe("0a1b2c"); -}); - describe("for...of empty destructuring", () => { const declareTests = (destructuringPrefix: string) => { test("array", () => { @@ -849,139 +595,6 @@ test.each([ expect(util.transpileString(loop, luajit).indexOf("::__continue1::") !== -1).toBe(true); }); -test("for dead code after return", () => { - const result = util.transpileAndExecute(`for (let i = 0; i < 10; i++) { return 3; const b = 8; }`); - - expect(result).toBe(3); -}); - -test("for..in dead code after return", () => { - const result = util.transpileAndExecute(`for (let a in {"a": 5, "b": 8}) { return 3; const b = 8; }`); - - expect(result).toBe(3); -}); - -test("for..of dead code after return", () => { - const result = util.transpileAndExecute(`for (let a of [1,2,4]) { return 3; const b = 8; }`); - - expect(result).toBe(3); -}); - -test("while dead code after return", () => { - const result = util.transpileAndExecute(`while (true) { return 3; const b = 8; }`); - - expect(result).toBe(3); -}); - -test.each([ - { args: [1, 10], expectResult: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }, - { args: [1, 10, 2], expectResult: [1, 3, 5, 7, 9] }, - { args: [10, 1, -1], expectResult: [10, 9, 8, 7, 6, 5, 4, 3, 2, 1] }, - { args: [10, 1, -2], expectResult: [10, 8, 6, 4, 2] }, -])("@forRange loop", ({ args, expectResult }) => { - const tsHeader = "/** @forRange **/ declare function luaRange(i: number, j: number, k?: number): number[];"; - const code = ` - const results: number[] = []; - for (const i of luaRange(${args})) { - results.push(i); - } - return JSONStringify(results);`; - - const result = util.transpileAndExecute(code, undefined, undefined, tsHeader); - expect(JSON.parse(result)).toEqual(expectResult); -}); - -test("invalid non-ambient @forRange function", () => { - const code = ` - /** @forRange **/ function luaRange(i: number, j: number, k?: number): number[] { return []; } - for (const i of luaRange(1, 10, 2)) {}`; - - expect(() => util.transpileString(code)).toThrow( - TSTLErrors.InvalidForRangeCall( - ts.createEmptyStatement(), - "@forRange function can only be used as an iterable in a for...of loop." - ).message - ); -}); - -test.each([[1], [1, 2, 3, 4]])("invalid @forRange argument count", args => { - const code = ` - /** @forRange **/ declare function luaRange(...args: number[]): number[] { return []; } - for (const i of luaRange(${args})) {}`; - - expect(() => util.transpileString(code)).toThrow( - TSTLErrors.InvalidForRangeCall(ts.createEmptyStatement(), "@forRange function must take 2 or 3 arguments.") - .message - ); -}); - -test("invalid @forRange control variable", () => { - const code = ` - /** @forRange **/ declare function luaRange(i: number, j: number, k?: number): number[]; - let i: number; - for (i of luaRange(1, 10, 2)) {}`; - - expect(() => util.transpileString(code)).toThrow( - TSTLErrors.InvalidForRangeCall( - ts.createEmptyStatement(), - "@forRange loop must declare its own control variable." - ).message - ); -}); - -test("invalid @forRange argument type", () => { - const code = ` - /** @forRange **/ declare function luaRange(i: string, j: number): number[] { return []; } - for (const i of luaRange("foo", 2)) {}`; - - expect(() => util.transpileString(code)).toThrow( - TSTLErrors.InvalidForRangeCall(ts.createEmptyStatement(), "@forRange arguments must be number types.").message - ); -}); - -test("invalid @forRange destructuring", () => { - const code = ` - /** @forRange **/ declare function luaRange(i: number, j: number, k?: number): number[][]; - for (const [i] of luaRange(1, 10, 2)) {}`; - - expect(() => util.transpileString(code)).toThrow( - TSTLErrors.InvalidForRangeCall(ts.createEmptyStatement(), "@forRange loop cannot use destructuring.").message - ); -}); - -test("invalid @forRange return type", () => { - const code = ` - /** @forRange **/ declare function luaRange(i: number, j: number, k?: number): string[]; - for (const i of luaRange(1, 10)) {}`; - - expect(() => util.transpileString(code)).toThrow( - TSTLErrors.InvalidForRangeCall( - ts.createEmptyStatement(), - "@forRange function must return Iterable or Array." - ).message - ); -}); - -test.each([ - "const range = luaRange(1, 10);", - "console.log(luaRange);", - "luaRange.call(null, 0, 0, 0);", - "let array = [0, luaRange, 1];", - "const call: any; call(luaRange);", - "for (const i of [...luaRange(1, 10)]) {}", -])("invalid @forRange reference (%p)", statement => { - const code = ` - /** @forRange **/ declare function luaRange(i: number, j: number, k?: number): number[]; - ${statement}`; - - expect(() => util.transpileString(code)).toThrow( - TSTLErrors.InvalidForRangeCall( - ts.createEmptyStatement(), - "@forRange function can only be used as an iterable in a for...of loop." - ).message - ); -}); - test("do...while", () => { const code = ` let result = 0; diff --git a/test/unit/lualib/array.spec.ts b/test/unit/lualib/array.spec.ts deleted file mode 100644 index ad8c64db2..000000000 --- a/test/unit/lualib/array.spec.ts +++ /dev/null @@ -1,458 +0,0 @@ -import * as util from "../../util"; - -test.each([{ inp: [0, 1, 2, 3], expected: [1, 2, 3, 4] }])("forEach (%p)", ({ inp, expected }) => { - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; - arrTest.forEach((elem, index) => { - arrTest[index] = arrTest[index] + 1; - }) - return JSONStringify(arrTest);` - ); - - expect(JSON.parse(result)).toEqual(expected); -}); - -test.each([ - { inp: [], searchEl: 3, expected: -1 }, - { inp: [0, 2, 4, 8], searchEl: 10, expected: -1 }, - { inp: [0, 2, 4, 8], searchEl: 8, expected: 3 }, -])("array.findIndex[value] (%p)", ({ inp, searchEl, expected }) => { - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; - return JSONStringify(arrTest.findIndex((elem, index) => { - return elem === ${searchEl}; - }));` - ); - - expect(result).toEqual(expected); -}); - -test.each([{ inp: [0, 2, 4, 8], expected: 3, value: 8 }, { inp: [0, 2, 4, 8], expected: 1, value: 2 }])( - "array.findIndex[index] (%p)", - ({ inp, expected, value }) => { - const result = util.transpileAndExecute( - `let arrTest = ${JSON.stringify(inp)}; - return JSONStringify(arrTest.findIndex((elem, index, arr) => { - return index === ${expected} && arr[${expected}] === ${value}; - }));` - ); - - expect(result).toEqual(expected); - } -); - -test.each([ - { inp: [], func: "x => x" }, - { inp: [0, 1, 2, 3], func: "x => x" }, - { inp: [0, 1, 2, 3], func: "x => x*2" }, - { inp: [1, 2, 3, 4], func: "x => -x" }, - { inp: [0, 1, 2, 3], func: "x => x+2" }, - { inp: [0, 1, 2, 3], func: "x => x%2 == 0 ? x + 1 : x - 1" }, -])("array.map (%p)", ({ inp, func }) => { - const result = util.transpileAndExecute(`return JSONStringify([${inp.toString()}].map(${func}))`); - - expect(JSON.parse(result)).toEqual(inp.map(eval(func))); -}); - -test.each([ - { inp: [], func: "x => x > 1" }, - { inp: [0, 1, 2, 3], func: "x => x > 1" }, - { inp: [0, 1, 2, 3], func: "x => x < 3" }, - { inp: [0, 1, 2, 3], func: "x => x < 0" }, - { inp: [0, -1, -2, -3], func: "x => x < 0" }, - { inp: [0, 1, 2, 3], func: "() => true" }, - { inp: [0, 1, 2, 3], func: "() => false" }, -])("array.filter (%p)", ({ inp, func }) => { - const result = util.transpileAndExecute(`return JSONStringify([${inp.toString()}].filter(${func}))`); - - expect(JSON.parse(result)).toEqual(inp.filter(eval(func))); -}); - -test.each([ - { inp: [], func: "x => x > 1" }, - { inp: [0, 1, 2, 3], func: "x => x > 1" }, - { inp: [false, true, false], func: "x => x" }, - { inp: [true, true, true], func: "x => x" }, -])("array.every (%p)", ({ inp, func }) => { - const result = util.transpileAndExecute(`return JSONStringify([${inp.toString()}].every(${func}))`); - - expect(JSON.parse(result)).toEqual(inp.every(eval(func))); -}); - -test.each([ - { inp: [], func: "x => x > 1" }, - { inp: [0, 1, 2, 3], func: "x => x > 1" }, - { inp: [false, true, false], func: "x => x" }, - { inp: [true, true, true], func: "x => x" }, -])("array.some (%p)", ({ inp, func }) => { - const result = util.transpileAndExecute(`return JSONStringify([${inp.toString()}].some(${func}))`); - - expect(JSON.parse(result)).toEqual(inp.some(eval(func))); -}); - -test.each([ - { inp: [], start: 1, end: 2 }, - { inp: [0, 1, 2, 3], start: 1, end: 2 }, - { inp: [0, 1, 2, 3], start: 1, end: 1 }, - { inp: [0, 1, 2, 3], start: 1, end: -1 }, - { inp: [0, 1, 2, 3], start: -3, end: -1 }, - { inp: [0, 1, 2, 3, 4, 5], start: 1, end: 3 }, - { inp: [0, 1, 2, 3, 4, 5], start: 3 }, -])("array.slice (%p)", ({ inp, start, end }) => { - const result = util.transpileAndExecute(`return JSONStringify([${inp.toString()}].slice(${start}, ${end}))`); - - expect(JSON.parse(result)).toEqual(inp.slice(start, end)); -}); - -test("array.slice no argument", () => { - const input = [2, 3, 4, 5]; - const result = util.transpileAndExecute(`return JSONStringify(${JSON.stringify(input)}.slice())`); - - expect(JSON.parse(result)).toEqual(input); -}); - -test.each([ - { inp: [], start: 0, deleteCount: 0, newElements: [9, 10, 11] }, - { inp: [0, 1, 2, 3], start: 1, deleteCount: 0, newElements: [9, 10, 11] }, - { inp: [0, 1, 2, 3], start: 2, deleteCount: 2, newElements: [9, 10, 11] }, - { inp: [0, 1, 2, 3], start: 4, deleteCount: 1, newElements: [8, 9] }, - { inp: [0, 1, 2, 3], start: 4, deleteCount: 0, newElements: [8, 9] }, - { inp: [0, 1, 2, 3], start: -2, deleteCount: 0, newElements: [8, 9] }, - { inp: [0, 1, 2, 3], start: -3, deleteCount: 0, newElements: [8, 9] }, - { inp: [0, 1, 2, 3, 4, 5], start: 5, deleteCount: 9, newElements: [10, 11] }, - { inp: [0, 1, 2, 3, 4, 5], start: 3, deleteCount: 2, newElements: [3, 4, 5] }, -])("array.splice[Insert] (%p)", ({ inp, start, deleteCount, newElements }) => { - const result = util.transpileAndExecute( - `let spliceTestTable = [${inp.toString()}]; - spliceTestTable.splice(${start}, ${deleteCount}, ${newElements}); - return JSONStringify(spliceTestTable);` - ); - - inp.splice(start, deleteCount, ...newElements); - expect(JSON.parse(result)).toEqual(inp); -}); - -test.each([ - { inp: [], start: 1, deleteCount: 1 }, - { inp: [0, 1, 2, 3], start: 1, deleteCount: 1 }, - { inp: [0, 1, 2, 3], start: 10, deleteCount: 1 }, - { inp: [0, 1, 2, 3], start: 1, deleteCount: undefined }, - { inp: [0, 1, 2, 3], start: 4 }, - { inp: [0, 1, 2, 3, 4, 5], start: 3 }, - { inp: [0, 1, 2, 3, 4, 5], start: -3 }, - { inp: [0, 1, 2, 3, 4, 5], start: -2 }, - { inp: [0, 1, 2, 3, 4, 5], start: 2, deleteCount: 2 }, - { inp: [0, 1, 2, 3, 4, 5, 6, 7, 8], start: 5, deleteCount: 9, newElements: [10, 11] }, -])("array.splice[Remove] (%p)", ({ inp, start, deleteCount, newElements = [] }) => { - let result; - if (deleteCount) { - result = util.transpileAndExecute( - `let spliceTestTable = [${inp.toString()}]; - spliceTestTable.splice(${start}, ${deleteCount}, ${newElements}); - return JSONStringify(spliceTestTable);` - ); - } else { - result = util.transpileAndExecute( - `let spliceTestTable = [${inp.toString()}]; - spliceTestTable.splice(${start}); - return JSONStringify(spliceTestTable);` - ); - } - - if (deleteCount) { - inp.splice(start, deleteCount, ...newElements); - expect(JSON.parse(result)).toEqual(inp); - } else { - inp.splice(start); - expect(JSON.parse(result)).toEqual(inp); - } -}); - -test.each([ - { arr: [], args: [[]] }, - { arr: [1, 2, 3], args: [[]] }, - { arr: [1, 2, 3], args: [[4]] }, - { arr: [1, 2, 3], args: [[4, 5]] }, - { arr: [1, 2, 3], args: [[4, 5]] }, - { arr: [1, 2, 3], args: [4, [5]] }, - { arr: [1, 2, 3], args: [4, [5, 6]] }, - { arr: [1, 2, 3], args: [4, [5, 6], 7] }, - { arr: [1, 2, 3], args: ["test", [5, 6], 7, ["test1", "test2"]] }, - { arr: [1, 2, "test"], args: ["test", ["test1", "test2"]] }, -])("array.concat (%p)", ({ arr, args }: { arr: any[]; args: any[] }) => { - const argStr = args.map(arg => JSON.stringify(arg)).join(","); - - const result = util.transpileAndExecute( - `let concatTestTable: any[] = ${JSON.stringify(arr)}; - return JSONStringify(concatTestTable.concat(${argStr}));` - ); - - const concatArr = arr.concat(...args); - expect(JSON.parse(result)).toEqual(concatArr); -}); - -test.each([ - { inp: [] }, - { inp: ["test1"] }, - { inp: ["test1", "test2"] }, - { inp: ["test1", "test2"], separator: ";" }, - { inp: ["test1", "test2"], separator: "" }, -])("array.join (%p)", ({ inp, separator }) => { - let separatorLua; - if (separator === "") { - separatorLua = '""'; - } else if (separator) { - separatorLua = '"' + separator + '"'; - } else { - separatorLua = ""; - } - const result = util.transpileAndExecute( - `let joinTestTable = ${JSON.stringify(inp)}; - return joinTestTable.join(${separatorLua});` - ); - - expect(result).toEqual(inp.join(separator)); -}); - -test.each([ - { inp: [], element: "test1" }, - { inp: ["test1"], element: "test1" }, - { inp: ["test1", "test2"], element: "test2" }, - { inp: ["test1", "test2", "test3"], element: "test3", fromIndex: 1 }, - { inp: ["test1", "test2", "test3"], element: "test1", fromIndex: 2 }, - { inp: ["test1", "test2", "test3"], element: "test1", fromIndex: -2 }, - { inp: ["test1", "test2", "test3"], element: "test1", fromIndex: 12 }, -])("array.indexOf (%p)", ({ inp, element, fromIndex }) => { - let str = `return ${JSON.stringify(inp)}.indexOf("${element}");`; - if (fromIndex) { - str = `return ${JSON.stringify(inp)}.indexOf("${element}", ${fromIndex});`; - } - - const result = util.transpileAndExecute(str); - - // Account for lua indexing (-1) - expect(result).toEqual(inp.indexOf(element, fromIndex)); -}); - -test.each([{ inp: [1, 2, 3], expected: 3 }, { inp: [1, 2, 3, 4, 5], expected: 3 }])( - "array.destructuring.simple (%p)", - ({ inp, expected }) => { - const result = util.transpileAndExecute( - `let [x, y, z] = ${JSON.stringify(inp)} - return z;` - ); - - expect(result).toEqual(expected); - } -); - -test.each([{ inp: [1] }, { inp: [1, 2, 3] }])("array.push (%p)", ({ inp }) => { - const result = util.transpileAndExecute( - `let testArray = [0]; - testArray.push(${inp.join(", ")}); - return JSONStringify(testArray);` - ); - - expect(JSON.parse(result)).toEqual([0].concat(inp)); -}); - -test.each([{ array: "[1, 2, 3]", expected: [3, 2] }, { array: "[1, 2, 3, null]", expected: [3, 2] }])( - "array.pop (%p)", - ({ array, expected }) => { - { - const result = util.transpileAndExecute( - `let testArray = ${array}; - let val = testArray.pop(); - return val` - ); - - expect(result).toEqual(expected[0]); - } - { - const result = util.transpileAndExecute( - `let testArray = ${array}; - testArray.pop(); - return testArray.length` - ); - - expect(result).toEqual(expected[1]); - } - } -); - -test.each([ - { array: "[1, 2, 3]", expected: [3, 2, 1] }, - { array: "[1, 2, 3, null]", expected: [3, 2, 1] }, - { array: "[1, 2, 3, 4]", expected: [4, 3, 2, 1] }, - { array: "[1]", expected: [1] }, - { array: "[]", expected: [] }, -])("array.reverse (%p)", ({ array, expected }) => { - const result = util.transpileAndExecute( - `let testArray = ${array}; - let val = testArray.reverse(); - return JSONStringify(testArray)` - ); - expect(JSON.parse(result)).toEqual(expected); -}); - -test.each([ - { array: "[1, 2, 3]", expectedArray: [2, 3], expectedValue: 1 }, - { array: "[1]", expectedArray: [], expectedValue: 1 }, - { array: "[]", expectedArray: [], expectedValue: undefined }, -])("array.shift (%p)", ({ array, expectedArray, expectedValue }) => { - { - // test array mutation - { - const result = util.transpileAndExecute( - `let testArray = ${array}; - let val = testArray.shift(); - return JSONStringify(testArray)` - ); - expect(JSON.parse(result)).toEqual(expectedArray); - } - // test return value - { - const result = util.transpileAndExecute( - `let testArray = ${array}; - let val = testArray.shift(); - return val` - ); - - expect(result).toEqual(expectedValue); - } - } -}); - -test.each([ - { array: "[3, 4, 5]", toUnshift: [1, 2], expected: [1, 2, 3, 4, 5] }, - { array: "[]", toUnshift: [], expected: [] }, - { array: "[1]", toUnshift: [], expected: [1] }, - { array: "[]", toUnshift: [1], expected: [1] }, -])("array.unshift (%p)", ({ array, toUnshift, expected }) => { - const result = util.transpileAndExecute( - `let testArray = ${array}; - testArray.unshift(${toUnshift}); - return JSONStringify(testArray)` - ); - - expect(JSON.parse(result)).toEqual(expected); -}); - -test.each([ - { array: "[4, 5, 3, 2, 1]", expected: [1, 2, 3, 4, 5] }, - { array: "[1]", expected: [1] }, - { array: "[1, null]", expected: [1] }, - { array: "[]", expected: [] }, -])("array.sort (%p)", ({ array, expected }) => { - const result = util.transpileAndExecute( - `let testArray = ${array}; - testArray.sort(); - return JSONStringify(testArray)` - ); - - expect(JSON.parse(result)).toEqual(expected); -}); - -test.each([ - { array: [1, 2, 3, 4, 5], compareStr: "a - b", compareFn: (a: any, b: any) => a - b }, - { - array: ["4", "5", "3", "2", "1"], - compareStr: "tonumber(a) - tonumber(b)", - compareFn: (a: any, b: any) => Number(a) - Number(b), - }, - { - array: ["4", "5", "3", "2", "1"], - compareStr: "tonumber(b) - tonumber(a)", - compareFn: (a: any, b: any) => Number(b) - Number(a), - }, -])("array.sort with compare function (%p)", ({ array, compareStr, compareFn }) => { - const result = util.transpileAndExecute( - `let testArray = ${JSON.stringify(array)}; - testArray.sort((a, b) => ${compareStr}); - return JSONStringify(testArray)`, - undefined, - undefined, - `declare function tonumber(this: void, e: any): number` - ); - - expect(JSON.parse(result)).toEqual(array.sort(compareFn)); -}); - -test.each([ - { array: [1, [2, 3], 4], expected: [1, 2, 3, 4] }, - { array: [1, [2, 3], 4], depth: 0, expected: [1, [2, 3], 4] }, - { array: [1, [[2], [3]], 4], expected: [1, [2], [3], 4] }, - { array: [1, [[[2], [3]]], 4], depth: Infinity, expected: [1, 2, 3, 4] }, -])("array.flat (%p)", ({ array, depth, expected }) => { - // TODO: Remove once `Infinity` would be implemented - const luaDepth = depth === Infinity ? "1 / 0" : depth; - const result = util.transpileAndExecute(` - return JSONStringify(${JSON.stringify(array)}.flat(${luaDepth})) - `); - - expect(JSON.parse(result)).toEqual(expected); -}); - -test.each([ - { array: [1, [2, 3], [4]], map: (value: T) => value, expected: [1, 2, 3, 4] }, - { array: [1, 2, 3], map: (v: number) => v * 2, expected: [2, 4, 6] }, - { array: [1, 2, 3], map: (v: number) => [v, v * 2], expected: [1, 2, 2, 4, 3, 6] }, - { array: [1, 2, 3], map: (v: number) => [v, [v]], expected: [1, [1], 2, [2], 3, [3]] }, - { array: [1, 2, 3], map: (v: number, i: number) => [v * 2 * i], expected: [0, 4, 12] }, -])("array.flatMap (%p)", ({ array, map, expected }) => { - const result = util.transpileAndExecute(` - const array = ${JSON.stringify(array)}; - const result = array.flatMap(${map.toString()}); - return JSONStringify(result); - `); - - // TODO(node 12): array.flatMap(map) - expect(JSON.parse(result)).toEqual(expected); -}); - -test.each([ - (total: number, currentItem: number) => total + currentItem, - (total: number, currentItem: number) => total * currentItem, -])("array reduce (%p)", reducer => { - const array = [1, 3, 5, 7]; - - const result = util.transpileAndExecute(` - const myArray = ${JSON.stringify(array)}; - return myArray.reduce(${reducer.toString()}); - `); - - expect(result).toEqual(array.reduce(reducer)); -}); - -test.each([ - (total: number, currentItem: number) => total + currentItem, - (total: number, currentItem: number) => total * currentItem, -])("array reduce with initial value (%p)", reducer => { - const array = [1, 3, 5, 7]; - const initial = 10; - - const result = util.transpileAndExecute(` - const myArray = ${JSON.stringify(array)}; - return myArray.reduce(${reducer.toString()}, ${initial}); - `); - - expect(result).toEqual(array.reduce(reducer, initial)); -}); - -test("array reduce index & array arguments (%p)", () => { - const array = [1, 3, 5, 7]; - const reducer = (total: number, _: number, index: number, array: number[]) => total + array[index]; - - const result = util.transpileAndExecute(` - const myArray = ${JSON.stringify(array)}; - return myArray.reduce(${reducer.toString()}); - `); - - expect(result).toEqual(array.reduce(reducer)); -}); - -test("array reduce index & array arguments (%p)", () => { - expect(() => { - util.transpileAndExecute("return [].reduce((a, b) => a + b);"); - }).toThrow("Reduce of empty array with no initial value"); -}); diff --git a/test/unit/lualib/inlining.spec.ts b/test/unit/lualib/inlining.spec.ts deleted file mode 100644 index b7a73fc57..000000000 --- a/test/unit/lualib/inlining.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { LuaLibImportKind, LuaTarget } from "../../../src"; -import * as util from "../../util"; - -test("map constructor", () => { - const result = util.transpileAndExecute(`let mymap = new Map(); return mymap.size;`, { - luaLibImport: LuaLibImportKind.Inline, - luaTarget: LuaTarget.Lua53, - }); - - expect(result).toBe(0); -}); - -test("map foreach keys", () => { - const result = util.transpileAndExecute( - `let mymap = new Map([[5, 2],[6, 3],[7, 4]]); - let count = 0; - mymap.forEach((value, key) => { count += key; }); - return count;`, - { luaLibImport: LuaLibImportKind.Inline } - ); - - expect(result).toBe(18); -}); - -test("set constructor", () => { - const result = util.transpileAndExecute( - `class abc {} let def = new abc(); let myset = new Set(); return myset.size;`, - { luaLibImport: LuaLibImportKind.Inline } - ); - - expect(result).toBe(0); -}); - -test("set foreach keys", () => { - const result = util.transpileAndExecute( - `let myset = new Set([2, 3, 4]); - let count = 0; - myset.forEach((value, key) => { count += key; }); - return count;`, - { luaLibImport: LuaLibImportKind.Inline } - ); - - expect(result).toBe(9); -}); diff --git a/test/unit/lualib/lualib.spec.ts b/test/unit/lualib/lualib.spec.ts deleted file mode 100644 index 5ff9e45de..000000000 --- a/test/unit/lualib/lualib.spec.ts +++ /dev/null @@ -1,169 +0,0 @@ -import * as util from "../../util"; - -test.each([ - { condition: "true", lhs: "4", rhs: "5", expected: 4 }, - { condition: "false", lhs: "4", rhs: "5", expected: 5 }, - { condition: "3", lhs: "4", rhs: "5", expected: 4 }, -])("Ternary Conditional (%p)", ({ condition, lhs, rhs, expected }) => { - const result = util.transpileAndExecute(`return ${condition} ? ${lhs} : ${rhs};`); - - expect(result).toBe(expected); -}); - -test.each([ - { condition: "true", expected: 11 }, - { condition: "false", expected: 13 }, - { condition: "a < 4", expected: 13 }, - { condition: "a == 8", expected: 11 }, -])("Ternary Conditional Delayed (%p)", ({ condition, expected }) => { - const result = util.transpileAndExecute( - `let a = 3; - let delay = () => ${condition} ? a + 3 : a + 5; - a = 8; - return delay();` - ); - - expect(result).toBe(expected); -}); - -test.each([ - { initial: "{a: 3}", parameters: "{}", expected: { a: 3 } }, - { initial: "{}", parameters: "{a: 3}", expected: { a: 3 } }, - { initial: "{a: 3}", parameters: "{a: 5}", expected: { a: 5 } }, - { initial: "{a: 3}", parameters: "{b: 5},{c: 7}", expected: { a: 3, b: 5, c: 7 } }, -])("Object.assign (%p)", ({ initial, parameters, expected }) => { - const jsonResult = util.transpileAndExecute(` - return JSONStringify(Object.assign(${initial},${parameters})); - `); - - const result = JSON.parse(jsonResult); - expect(result).toEqual(expected); -}); - -test.each([ - { obj: "{}", expected: [] }, - { obj: "{abc: 3}", expected: ["abc,3"] }, - { obj: "{abc: 3, def: 'xyz'}", expected: ["abc,3", "def,xyz"] }, -])("Object.entries (%p)", ({ obj, expected }) => { - const result = util.transpileAndExecute(` - const obj = ${obj}; - return Object.entries(obj).map(e => e.join(",")).join(";"); - `) as string; - - const foundKeys = result.split(";"); - if (expected.length === 0) { - expect(foundKeys.length).toBe(1); - expect(foundKeys[0]).toBe(""); - } else { - expect(foundKeys.length).toBe(expected.length); - for (const key of expected) { - expect(foundKeys.indexOf(key) >= 0).toBeTruthy(); - } - } -}); - -test.each([ - { obj: "{}", expected: [] }, - { obj: "{abc: 3}", expected: ["abc"] }, - { obj: "{abc: 3, def: 'xyz'}", expected: ["abc", "def"] }, -])("Object.keys (%p)", ({ obj, expected }) => { - const result = util.transpileAndExecute(` - const obj = ${obj}; - return Object.keys(obj).join(","); - `) as string; - - const foundKeys = result.split(","); - if (expected.length === 0) { - expect(foundKeys.length).toBe(1); - expect(foundKeys[0]).toBe(""); - } else { - expect(foundKeys.length).toBe(expected.length); - for (const key of expected) { - expect(foundKeys.indexOf(key) >= 0).toBeTruthy(); - } - } -}); - -test.each([ - { obj: "{}", expected: [] }, - { obj: "{abc: 'def'}", expected: ["def"] }, - { obj: "{abc: 3, def: 'xyz'}", expected: ["3", "xyz"] }, -])("Object.values (%p)", ({ obj, expected }) => { - const result = util.transpileAndExecute(` - const obj = ${obj}; - return Object.values(obj).join(","); - `) as string; - - const foundValues = result.split(","); - if (expected.length === 0) { - expect(foundValues.length).toBe(1); - expect(foundValues[0]).toBe(""); - } else { - expect(foundValues.length).toBe(expected.length); - for (const key of expected) { - expect(foundValues.indexOf(key) >= 0).toBeTruthy(); - } - } -}); - -// https://github.com/Microsoft/TypeScript/pull/26149 -const objectFromEntriesDeclaration = ` - interface ObjectConstructor { - fromEntries(entries: ReadonlyArray<[string, T]> | Iterable<[string, T]>): Record; - fromEntries(entries: ReadonlyArray<[string, any]> | Iterable<[string, any]>): Record; - } -`; - -test.each([ - { entries: [], expected: [] }, - { entries: [["a", 1], ["b", 2]], expected: { a: 1, b: 2 } }, - { entries: [["a", 1], ["a", 2]], expected: { a: 2 } }, -])("Object.fromEntries (%p)", ({ entries, expected }) => { - const result = util.transpileAndExecute( - `const obj = Object.fromEntries(${JSON.stringify(entries)}); - return JSONStringify(obj);`, - undefined, - undefined, - objectFromEntriesDeclaration - ); - - expect(JSON.parse(result)).toEqual(expected); -}); - -test("Object.fromEntries (Map)", () => { - const result = util.transpileAndExecute( - `const map = new Map([["foo", "bar"]]); - const obj = Object.fromEntries(map); - return JSONStringify(obj);`, - undefined, - undefined, - objectFromEntriesDeclaration - ); - - expect(JSON.parse(result)).toEqual({ foo: "bar" }); -}); - -test("lualibs should not include tstl header", () => { - const code = ` - const arr = [1, 2, 3]; - arr.push(4);`; - - expect(util.transpileString(code)).not.toMatch("Generated with"); -}); - -test("compileMembersOnly in namespace", () => { - const header = ` - namespace wifi { - /** @compileMembersOnly */ - export enum WifiMode { - NULLMODE = 0, - STATION = 1, - SOFTAP = 2 - } - }`; - const code = ` - return wifi.WifiMode.STATION; - `; - - expect(util.transpileAndExecute(code, undefined, undefined, header)).toBe(1); -}); diff --git a/test/unit/lualib/set.spec.ts b/test/unit/lualib/set.spec.ts deleted file mode 100644 index 6018d42cf..000000000 --- a/test/unit/lualib/set.spec.ts +++ /dev/null @@ -1,124 +0,0 @@ -import * as util from "../../util"; - -test("set constructor", () => { - const result = util.transpileAndExecute(`let myset = new Set(); return myset.size;`); - - expect(result).toBe(0); -}); - -test("set iterable constructor", () => { - const result = util.transpileAndExecute( - `let myset = new Set(["a", "b"]); - return myset.has("a") || myset.has("b");` - ); - - expect(result).toBe(true); -}); - -test("set iterable constructor set", () => { - const result = util.transpileAndExecute( - `let myset = new Set(new Set(["a", "b"])); - return myset.has("a") || myset.has("b");` - ); - - expect(result).toBe(true); -}); - -test("set add", () => { - const has = util.transpileAndExecute(`let myset = new Set(); myset.add("a"); return myset.has("a");`); - expect(has).toBe(true); -}); - -test("set clear", () => { - const setTS = `let myset = new Set(["a", "b"]); myset.clear();`; - const size = util.transpileAndExecute(setTS + `return myset.size;`); - expect(size).toBe(0); - - const contains = util.transpileAndExecute(setTS + `return !myset.has("a") && !myset.has("b");`); - expect(contains).toBe(true); -}); - -test("set delete", () => { - const setTS = `let myset = new Set(["a", "b"]); myset.delete("a");`; - const contains = util.transpileAndExecute(setTS + `return myset.has("b") && !myset.has("a");`); - expect(contains).toBe(true); -}); - -test("set entries", () => { - const result = util.transpileAndExecute( - `let myset = new Set([5, 6, 7]); - let count = 0; - for (var [key, value] of myset.entries()) { count += key + value; } - return count;` - ); - - expect(result).toBe(36); -}); - -test("set foreach", () => { - const result = util.transpileAndExecute( - `let myset = new Set([2, 3, 4]); - let count = 0; - myset.forEach(i => { count += i; }); - return count;` - ); - expect(result).toBe(9); -}); - -test("set foreach keys", () => { - const result = util.transpileAndExecute( - `let myset = new Set([2, 3, 4]); - let count = 0; - myset.forEach((value, key) => { count += key; }); - return count;` - ); - - expect(result).toBe(9); -}); - -test("set has", () => { - const contains = util.transpileAndExecute(`let myset = new Set(["a", "c"]); return myset.has("a");`); - expect(contains).toBe(true); -}); - -test("set has false", () => { - const contains = util.transpileAndExecute(`let myset = new Set(); return myset.has("a");`); - expect(contains).toBe(false); -}); - -test("set has null", () => { - const contains = util.transpileAndExecute(`let myset = new Set(["a", "c"]); return myset.has(null);`); - expect(contains).toBe(false); -}); - -test("set keys", () => { - const result = util.transpileAndExecute( - `let myset = new Set([5, 6, 7]); - let count = 0; - for (var key of myset.keys()) { count += key; } - return count;` - ); - - expect(result).toBe(18); -}); - -test("set values", () => { - const result = util.transpileAndExecute( - `let myset = new Set([5, 6, 7]); - let count = 0; - for (var value of myset.values()) { count += value; } - return count;` - ); - - expect(result).toBe(18); -}); - -test.each([ - { code: `let m = new Set(); return m.size;`, expected: 0 }, - { code: `let m = new Set(); m.add(1); return m.size;`, expected: 1 }, - { code: `let m = new Set([1, 2]); return m.size;`, expected: 2 }, - { code: `let m = new Set([1, 2]); m.clear(); return m.size;`, expected: 0 }, - { code: `let m = new Set([1, 2]); m.delete(2); return m.size;`, expected: 1 }, -])("set size (%p)", ({ code, expected }) => { - expect(util.transpileAndExecute(code)).toBe(expected); -}); diff --git a/test/unit/lualib/symbol.spec.ts b/test/unit/lualib/symbol.spec.ts deleted file mode 100644 index 857a9918a..000000000 --- a/test/unit/lualib/symbol.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import * as util from "../../util"; - -test.each([{}, { description: 1 }, { description: "name" }])("symbol.toString() (%p)", ({ description }) => { - const result = util.transpileAndExecute(` - return Symbol(${JSON.stringify(description)}).toString(); - `); - - expect(result).toBe(`Symbol(${description || ""})`); -}); - -test.each([{}, { description: 1 }, { description: "name" }])("symbol.description (%p)", ({ description }) => { - const result = util.transpileAndExecute(` - return Symbol(${JSON.stringify(description)}).description; - `); - - expect(result).toBe(description); -}); - -test("symbol uniqueness", () => { - const result = util.transpileAndExecute(` - return Symbol("a") === Symbol("a"); - `); - - expect(result).toBe(false); -}); - -test("Symbol.for", () => { - const result = util.transpileAndExecute(` - return Symbol.for("name").description; - `); - - expect(result).toBe("name"); -}); - -test("Symbol.for non-uniqueness", () => { - const result = util.transpileAndExecute(` - return Symbol.for("a") === Symbol.for("a"); - `); - - expect(result).toBe(true); -}); - -test("Symbol.keyFor", () => { - const result = util.transpileAndExecute(` - const sym = Symbol.for("a"); - Symbol.for("b"); - return Symbol.keyFor(sym); - `); - - expect(result).toBe("a"); -}); - -test("Symbol.keyFor empty", () => { - const result = util.transpileAndExecute(` - Symbol.for("a"); - return Symbol.keyFor(Symbol()); - `); - - expect(result).toBe(undefined); -}); diff --git a/test/unit/math.spec.ts b/test/unit/math.spec.ts deleted file mode 100644 index 84b1b071f..000000000 --- a/test/unit/math.spec.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as util from "../util"; - -test.each([ - { inp: "Math.cos()", expected: "math.cos()" }, - { inp: "Math.sin()", expected: "math.sin()" }, - { inp: "Math.min()", expected: "math.min()" }, - { inp: "Math.atan2(2, 3)", expected: "math.atan(2 / 3)" }, - { inp: "Math.log2(3)", expected: `local ____ = (math.log(3) / ${Math.LN2})` }, - { inp: "Math.log10(3)", expected: `local ____ = (math.log(3) / ${Math.LN10})` }, - { inp: "const x = Math.log2(3)", expected: `local x = (math.log(3) / ${Math.LN2})` }, - { inp: "const x = Math.log10(3)", expected: `local x = (math.log(3) / ${Math.LN10})` }, - { inp: "Math.log1p(3)", expected: "math.log(1 + 3)" }, - { inp: "Math.round(3.3)", expected: "math.floor(3.3 + 0.5)" }, - { inp: "Math.PI", expected: "local ____ = math.pi" }, -])("Math (%p)", ({ inp, expected }) => { - const lua = util.transpileString(inp); - - expect(lua).toBe(expected); -}); - -test.each(["E", "LN10", "LN2", "LOG10E", "LOG2E", "SQRT1_2", "SQRT2"])("Math constant (%p)", constant => { - const epsilon = 0.000001; - const jsValue: number = (Math as Math & { [key: string]: any })[constant]; - const code = `return Math.abs(Math.${constant} - ${jsValue}) <= ${epsilon}`; - expect(util.transpileAndExecute(code)).toBe(true); -}); - -test.each([ - { statement: "++x", expected: "x=4;y=6" }, - { statement: "x++", expected: "x=4;y=6" }, - { statement: "--x", expected: "x=2;y=6" }, - { statement: "x--", expected: "x=2;y=6" }, - { statement: "x += y", expected: "x=9;y=6" }, - { statement: "x -= y", expected: "x=-3;y=6" }, - { statement: "x *= y", expected: "x=18;y=6" }, - { statement: "y /= x", expected: "x=3;y=2.0" }, - { statement: "y %= x", expected: "x=3;y=0" }, - { statement: "y **= x", expected: "x=3;y=216.0" }, - { statement: "x |= y", expected: "x=7;y=6" }, - { statement: "x &= y", expected: "x=2;y=6" }, - { statement: "x ^= y", expected: "x=5;y=6" }, - { statement: "x <<= y", expected: "x=192;y=6" }, - { statement: "x >>>= y", expected: "x=0;y=6" }, -])("Operator assignment statements (%p)", ({ statement, expected }) => { - const result = util.transpileAndExecute( - `let x = 3; - let y = 6; - ${statement}; - return \`x=\${x};y=\${y}\`` - ); - expect(result).toBe(expected); -}); - -test.each([ - { statement: "++o.p", expected: "o=4;a=6" }, - { statement: "o.p++", expected: "o=4;a=6" }, - { statement: "--o.p", expected: "o=2;a=6" }, - { statement: "o.p--", expected: "o=2;a=6" }, - { statement: "o.p += a[0]", expected: "o=9;a=6" }, - { statement: "o.p -= a[0]", expected: "o=-3;a=6" }, - { statement: "o.p *= a[0]", expected: "o=18;a=6" }, - { statement: "a[0] /= o.p", expected: "o=3;a=2.0" }, - { statement: "a[0] %= o.p", expected: "o=3;a=0" }, - { statement: "a[0] **= o.p", expected: "o=3;a=216.0" }, - { statement: "o.p |= a[0]", expected: "o=7;a=6" }, - { statement: "o.p &= a[0]", expected: "o=2;a=6" }, - { statement: "o.p ^= a[0]", expected: "o=5;a=6" }, - { statement: "o.p <<= a[0]", expected: "o=192;a=6" }, - { statement: "o.p >>>= a[0]", expected: "o=0;a=6" }, -])("Operator assignment to simple property statements (%p)", ({ statement, expected }) => { - const result = util.transpileAndExecute( - `let o = {p: 3}; - let a = [6]; - ${statement}; - return \`o=\${o.p};a=\${a[0]}\`` - ); - expect(result).toBe(expected); -}); - -test.each([ - { statement: "++o.p.d", expected: "o=4;a=[6,11],[7,13]" }, - { statement: "o.p.d++", expected: "o=4;a=[6,11],[7,13]" }, - { statement: "--o.p.d", expected: "o=2;a=[6,11],[7,13]" }, - { statement: "o.p.d--", expected: "o=2;a=[6,11],[7,13]" }, - { statement: "o.p.d += a[0][0]", expected: "o=9;a=[6,11],[7,13]" }, - { statement: "o.p.d -= a[0][0]", expected: "o=-3;a=[6,11],[7,13]" }, - { statement: "o.p.d *= a[0][0]", expected: "o=18;a=[6,11],[7,13]" }, - { statement: "a[0][0] /= o.p.d", expected: "o=3;a=[2.0,11],[7,13]" }, - { statement: "a[0][0] %= o.p.d", expected: "o=3;a=[0,11],[7,13]" }, - { statement: "a[0][0] **= o.p.d", expected: "o=3;a=[216.0,11],[7,13]" }, - { statement: "o.p.d |= a[0][0]", expected: "o=7;a=[6,11],[7,13]" }, - { statement: "o.p.d &= a[0][0]", expected: "o=2;a=[6,11],[7,13]" }, - { statement: "o.p.d ^= a[0][0]", expected: "o=5;a=[6,11],[7,13]" }, - { statement: "o.p.d <<= a[0][0]", expected: "o=192;a=[6,11],[7,13]" }, - { statement: "o.p.d >>>= a[0][0]", expected: "o=0;a=[6,11],[7,13]" }, -])("Operator assignment to deep property statements (%p)", ({ statement, expected }) => { - const result = util.transpileAndExecute( - `let o = {p: {d: 3}}; - let a = [[6,11], [7,13]]; - ${statement}; - return \`o=\${o.p.d};a=[\${a[0][0]},\${a[0][1]}],[\${a[1][0]},\${a[1][1]}]\`` - ); - expect(result).toBe(expected); -}); - -test.each([ - { statement: "++of().p", expected: "o=4;a=6" }, - { statement: "of().p++", expected: "o=4;a=6" }, - { statement: "--of().p", expected: "o=2;a=6" }, - { statement: "of().p--", expected: "o=2;a=6" }, - { statement: "of().p += af()[i()]", expected: "o=9;a=6" }, - { statement: "of().p -= af()[i()]", expected: "o=-3;a=6" }, - { statement: "of().p *= af()[i()]", expected: "o=18;a=6" }, - { statement: "af()[i()] /= of().p", expected: "o=3;a=2.0" }, - { statement: "af()[i()] %= of().p", expected: "o=3;a=0" }, - { statement: "af()[i()] **= of().p", expected: "o=3;a=216.0" }, - { statement: "of().p |= af()[i()]", expected: "o=7;a=6" }, - { statement: "of().p &= af()[i()]", expected: "o=2;a=6" }, - { statement: "of().p ^= af()[i()]", expected: "o=5;a=6" }, - { statement: "of().p <<= af()[i()]", expected: "o=192;a=6" }, - { statement: "of().p >>>= af()[i()]", expected: "o=0;a=6" }, -])("Operator assignment to complex property statements (%p)", ({ statement, expected }) => { - const result = util.transpileAndExecute( - `let o = {p: 3}; - let a = [6]; - function of() { return o; } - function af() { return a; } - function i() { return 0; } - ${statement}; - return \`o=\${o.p};a=\${a[0]}\`` - ); - expect(result).toBe(expected); -}); - -test.each([ - { statement: "++of().p.d", expected: "o=4;a=[7,6],[11,13];i=0" }, - { statement: "of().p.d++", expected: "o=4;a=[7,6],[11,13];i=0" }, - { statement: "--of().p.d", expected: "o=2;a=[7,6],[11,13];i=0" }, - { statement: "of().p.d--", expected: "o=2;a=[7,6],[11,13];i=0" }, - { statement: "of().p.d += af()[i()][i()]", expected: "o=9;a=[7,6],[11,13];i=2" }, - { statement: "of().p.d -= af()[i()][i()]", expected: "o=-3;a=[7,6],[11,13];i=2" }, - { statement: "of().p.d *= af()[i()][i()]", expected: "o=18;a=[7,6],[11,13];i=2" }, - { statement: "af()[i()][i()] /= of().p.d", expected: "o=3;a=[7,2.0],[11,13];i=2" }, - { statement: "af()[i()][i()] %= of().p.d", expected: "o=3;a=[7,0],[11,13];i=2" }, - { statement: "af()[i()][i()] **= of().p.d", expected: "o=3;a=[7,216.0],[11,13];i=2" }, - { statement: "of().p.d |= af()[i()][i()]", expected: "o=7;a=[7,6],[11,13];i=2" }, - { statement: "of().p.d &= af()[i()][i()]", expected: "o=2;a=[7,6],[11,13];i=2" }, - { statement: "of().p.d ^= af()[i()][i()]", expected: "o=5;a=[7,6],[11,13];i=2" }, - { statement: "of().p.d <<= af()[i()][i()]", expected: "o=192;a=[7,6],[11,13];i=2" }, - { statement: "of().p.d >>>= af()[i()][i()]", expected: "o=0;a=[7,6],[11,13];i=2" }, -])("Operator assignment to complex deep property statements (%p)", ({ statement, expected }) => { - const result = util.transpileAndExecute( - `let o = {p: {d: 3}}; - let a = [[7, 6], [11, 13]]; - function of() { return o; } - function af() { return a; } - let _i = 0; - function i() { return _i++; } - ${statement}; - return \`o=\${o.p.d};a=[\${a[0][0]},\${a[0][1]}],[\${a[1][0]},\${a[1][1]}];i=\${_i}\`` - ); - expect(result).toBe(expected); -}); - -test.each([ - { expression: "++x", expected: "4;x=4;y=6" }, - { expression: "x++", expected: "3;x=4;y=6" }, - { expression: "--x", expected: "2;x=2;y=6" }, - { expression: "x--", expected: "3;x=2;y=6" }, - { expression: "x += y", expected: "9;x=9;y=6" }, - { expression: "x -= y", expected: "-3;x=-3;y=6" }, - { expression: "x *= y", expected: "18;x=18;y=6" }, - { expression: "y /= x", expected: "2.0;x=3;y=2.0" }, - { expression: "y %= x", expected: "0;x=3;y=0" }, - { expression: "y **= x", expected: "216.0;x=3;y=216.0" }, - { expression: "x |= y", expected: "7;x=7;y=6" }, - { expression: "x &= y", expected: "2;x=2;y=6" }, - { expression: "x ^= y", expected: "5;x=5;y=6" }, - { expression: "x <<= y", expected: "192;x=192;y=6" }, - { expression: "x >>>= y", expected: "0;x=0;y=6" }, - { expression: "x + (y += 7)", expected: "16;x=3;y=13" }, - { expression: "x + (y += 7)", expected: "16;x=3;y=13" }, - { expression: "x++ + (y += 7)", expected: "16;x=4;y=13" }, -])("Operator assignment expressions (%p)", ({ expression, expected }) => { - const result = util.transpileAndExecute( - `let x = 3; - let y = 6; - const r = ${expression}; - return \`\${r};x=\${x};y=\${y}\`` - ); - expect(result).toBe(expected); -}); - -test.each([ - { expression: "++o.p", expected: "4;o=4;a=6" }, - { expression: "o.p++", expected: "3;o=4;a=6" }, - { expression: "--o.p", expected: "2;o=2;a=6" }, - { expression: "o.p--", expected: "3;o=2;a=6" }, - { expression: "o.p += a[0]", expected: "9;o=9;a=6" }, - { expression: "o.p -= a[0]", expected: "-3;o=-3;a=6" }, - { expression: "o.p *= a[0]", expected: "18;o=18;a=6" }, - { expression: "a[0] /= o.p", expected: "2.0;o=3;a=2.0" }, - { expression: "a[0] %= o.p", expected: "0;o=3;a=0" }, - { expression: "a[0] **= o.p", expected: "216.0;o=3;a=216.0" }, - { expression: "o.p |= a[0]", expected: "7;o=7;a=6" }, - { expression: "o.p &= a[0]", expected: "2;o=2;a=6" }, - { expression: "o.p ^= a[0]", expected: "5;o=5;a=6" }, - { expression: "o.p <<= a[0]", expected: "192;o=192;a=6" }, - { expression: "o.p >>>= a[0]", expected: "0;o=0;a=6" }, - { expression: "o.p + (a[0] += 7)", expected: "16;o=3;a=13" }, - { expression: "o.p += (a[0] += 7)", expected: "16;o=16;a=13" }, - { expression: "o.p++ + (a[0] += 7)", expected: "16;o=4;a=13" }, -])("Operator assignment to simple property expressions (%p)", ({ expression, expected }) => { - const result = util.transpileAndExecute( - `let o = {p: 3}; - let a = [6]; - const r = ${expression}; - return \`\${r};o=\${o.p};a=\${a[0]}\`` - ); - expect(result).toBe(expected); -}); - -test.each([ - { expression: "++of().p", expected: "4;o=4;a=6" }, - { expression: "of().p++", expected: "3;o=4;a=6" }, - { expression: "--of().p", expected: "2;o=2;a=6" }, - { expression: "of().p--", expected: "3;o=2;a=6" }, - { expression: "of().p += af()[i()]", expected: "9;o=9;a=6" }, - { expression: "of().p -= af()[i()]", expected: "-3;o=-3;a=6" }, - { expression: "of().p *= af()[i()]", expected: "18;o=18;a=6" }, - { expression: "af()[i()] /= of().p", expected: "2.0;o=3;a=2.0" }, - { expression: "af()[i()] %= of().p", expected: "0;o=3;a=0" }, - { expression: "af()[i()] **= of().p", expected: "216.0;o=3;a=216.0" }, - { expression: "of().p |= af()[i()]", expected: "7;o=7;a=6" }, - { expression: "of().p &= af()[i()]", expected: "2;o=2;a=6" }, - { expression: "of().p ^= af()[i()]", expected: "5;o=5;a=6" }, - { expression: "of().p <<= af()[i()]", expected: "192;o=192;a=6" }, - { expression: "of().p >>>= af()[i()]", expected: "0;o=0;a=6" }, - { expression: "of().p + (af()[i()] += 7)", expected: "16;o=3;a=13" }, - { expression: "of().p += (af()[i()] += 7)", expected: "16;o=16;a=13" }, - { expression: "of().p++ + (af()[i()] += 7)", expected: "16;o=4;a=13" }, -])("Operator assignment to complex property expressions (%p)", ({ expression, expected }) => { - const result = util.transpileAndExecute( - `let o = {p: 3}; - let a = [6]; - function of() { return o; } - function af() { return a; } - function i() { return 0; } - const r = ${expression}; - return \`\${r};o=\${o.p};a=\${a[0]}\`` - ); - expect(result).toBe(expected); -}); diff --git a/test/unit/modules.spec.ts b/test/unit/modules.spec.ts deleted file mode 100644 index 53674df46..000000000 --- a/test/unit/modules.spec.ts +++ /dev/null @@ -1,165 +0,0 @@ -import * as ts from "typescript"; -import * as tstl from "../../src"; -import * as util from "../util"; - -describe("module import/export elision", () => { - const moduleDeclaration = ` - declare module "module" { - export type Type = string; - export declare const value: string; - } - `; - - const expectToElideImport = (code: string) => { - const lua = util.transpileString( - { "module.d.ts": moduleDeclaration, "main.ts": code }, - { module: ts.ModuleKind.CommonJS }, - false - ); - - expect(() => util.executeLua(lua)).not.toThrow(); - }; - - test("should elide named type imports", () => { - expectToElideImport(` - import { Type } from "module"; - const foo: Type = "bar"; - `); - }); - - test("should elide named value imports used only as a type", () => { - expectToElideImport(` - import { value } from "module"; - const foo: typeof value = "bar"; - `); - }); - - test("should elide namespace imports with unused values", () => { - expectToElideImport(` - import * as module from "module"; - const foo: module.Type = "bar"; - `); - }); - - test("should elide `import =` declarations", () => { - expectToElideImport(` - import module = require("module"); - const foo: module.Type = "bar"; - `); - }); - - test("should elide type exports", () => { - const code = ` - declare const _G: any; - - _G.foo = true; - type foo = boolean; - export { foo }; - `; - - expect(util.transpileExecuteAndReturnExport(code, "foo")).toBeUndefined(); - }); -}); - -test.each(["ke-bab", "dollar$", "singlequote'", "hash#", "s p a c e", "ɥɣɎɌͼƛಠ", "_̀ः٠‿"])( - "Import module names with invalid lua identifier characters (%p)", - name => { - const code = ` - import { foo } from "${name}"; - foo; - `; - - const lua = ` - setmetatable(package.loaded, {__index = function() return {foo = "bar"} end}) - ${util.transpileString(code)} - return foo;`; - - expect(util.executeLua(lua)).toBe("bar"); - } -); - -test("lualibRequire", () => { - const lua = util.transpileString(`let a = b instanceof c;`, { - luaLibImport: tstl.LuaLibImportKind.Require, - luaTarget: tstl.LuaTarget.LuaJIT, - }); - - expect(lua.startsWith(`require("lualib_bundle")`)); -}); - -test("lualibRequireAlways", () => { - const lua = util.transpileString(``, { - luaLibImport: tstl.LuaLibImportKind.Always, - luaTarget: tstl.LuaTarget.LuaJIT, - }); - - expect(lua).toBe(`require("lualib_bundle");`); -}); - -test("Non-exported module", () => { - const result = util.transpileAndExecute( - "return g.test();", - undefined, - undefined, - "module g { export function test() { return 3; } }" - ); - - expect(result).toBe(3); -}); - -test.each([tstl.LuaLibImportKind.Inline, tstl.LuaLibImportKind.None, tstl.LuaLibImportKind.Require])( - "LuaLib no uses? No code (%p)", - luaLibImport => { - const lua = util.transpileString(``, { luaLibImport }); - - expect(lua).toBe(``); - } -); - -test("Nested module with dot in name", () => { - const code = `module a.b { - export const foo = "foo"; - }`; - expect(util.transpileAndExecute("return a.b.foo;", undefined, undefined, code)).toBe("foo"); -}); - -test("Access this in module", () => { - const header = ` - module M { - export const foo = "foo"; - export function bar() { return this.foo + "bar"; } - } - `; - const code = `return M.bar();`; - expect(util.transpileAndExecute(code, undefined, undefined, header)).toBe("foobar"); -}); - -test("Module merged with interface", () => { - const header = ` - interface Foo {} - module Foo { - export function bar() { return "foobar"; } - }`; - const code = `return Foo.bar();`; - expect(util.transpileAndExecute(code, undefined, undefined, header)).toBe("foobar"); -}); - -test("module merged across files", () => { - const testA = ` - namespace NS { - export namespace Inner { - export const foo = "foo"; - } - } - `; - const testB = ` - namespace NS { - export namespace Inner { - export const bar = "bar"; - } - } - `; - const { transpiledFiles } = util.transpileStringsAsProject({ "testA.ts": testA, "testB.ts": testB }); - const lua = transpiledFiles.map(f => f.lua).join("\n") + "\nreturn NS.Inner.foo .. NS.Inner.bar"; - expect(util.executeLua(lua)).toBe("foobar"); -}); diff --git a/test/unit/modules/modules.spec.ts b/test/unit/modules/modules.spec.ts new file mode 100644 index 000000000..9209a0819 --- /dev/null +++ b/test/unit/modules/modules.spec.ts @@ -0,0 +1,211 @@ +import * as ts from "typescript"; +import * as util from "../../util"; + +describe("module import/export elision", () => { + const moduleDeclaration = ` + declare module "module" { + export type Type = string; + export declare const value: string; + } + `; + + const expectToElideImport: util.TapCallback = builder => { + builder.addExtraFile("module.d.ts", moduleDeclaration).setOptions({ module: ts.ModuleKind.CommonJS }); + expect(builder.getLuaExecutionResult()).not.toBeInstanceOf(util.ExecutionError); + }; + + test("should elide named type imports", () => { + util.testModule` + import { Type } from "module"; + const foo: Type = "bar"; + `.tap(expectToElideImport); + }); + + test("should elide named value imports used only as a type", () => { + util.testModule` + import { value } from "module"; + const foo: typeof value = "bar"; + `.tap(expectToElideImport); + }); + + test("should elide namespace imports with unused values", () => { + util.testModule` + import * as module from "module"; + const foo: module.Type = "bar"; + `.tap(expectToElideImport); + }); + + test("should elide `import =` declarations", () => { + util.testModule` + import module = require("module"); + const foo: module.Type = "bar"; + `.tap(expectToElideImport); + }); + + test("should elide type exports", () => { + util.testModule` + (globalThis as any).foo = true; + type foo = boolean; + export { foo }; + `.expectToEqual([]); + }); +}); + +test.each(["ke-bab", "dollar$", "singlequote'", "hash#", "s p a c e", "ɥɣɎɌͼƛಠ", "_̀ः٠‿"])( + "Import module names with invalid lua identifier characters (%p)", + name => { + util.testModule` + import { foo } from "./${name}"; + export { foo }; + ` + .disableSemanticCheck() + .setLuaHeader(`setmetatable(package.loaded, { __index = function() return { foo = "bar" } end })`) + .setReturnExport("foo") + .expectToEqual("bar"); + } +); + +test.each(["export default value;", "export { value as default };"])("Export Default From (%p)", exportStatement => { + const [result] = util.transpileAndExecuteProjectReturningMainExport( + { + "main.ts": ` + export { default } from "./module"; + `, + "module.ts": ` + export const value = true; + ${exportStatement}; + `, + }, + "default" + ); + + expect(result).toBe(true); +}); + +test("Default Import and Export Expression", () => { + const [result] = util.transpileAndExecuteProjectReturningMainExport( + { + "main.ts": ` + import defaultExport from "./module"; + export const value = defaultExport; + `, + "module.ts": ` + export default 1 + 2 + 3; + `, + }, + "value" + ); + + expect(result).toBe(6); +}); + +test("Import and Export Assignment", () => { + const [result] = util.transpileAndExecuteProjectReturningMainExport( + { + "main.ts": ` + import * as m from "./module"; + export const value = m; + `, + "module.ts": ` + export = true; + `, + }, + "value" + ); + + expect(result).toBe(true); +}); + +test("Mixed Exports, Default and Named Imports", () => { + const [result] = util.transpileAndExecuteProjectReturningMainExport( + { + "main.ts": ` + import defaultExport, { a, b, c } from "./module"; + export const value = defaultExport + b + c; + `, + "module.ts": ` + export const a = 1; + export const b = 2; + export const c = 3; + export default a; + `, + }, + "value" + ); + + expect(result).toBe(6); +}); + +test("Mixed Exports, Default and Namespace Import", () => { + const [result] = util.transpileAndExecuteProjectReturningMainExport( + { + "main.ts": ` + import defaultExport, * as ns from "./module"; + export const value = defaultExport + ns.b + ns.c; + `, + "module.ts": ` + export const a = 1; + export const b = 2; + export const c = 3; + export default a; + `, + }, + "value" + ); + + expect(result).toBe(6); +}); + +test("Export Default Function", () => { + const [result] = util.transpileAndExecuteProjectReturningMainExport( + { + "main.ts": ` + import defaultExport from "./module"; + export const value = defaultExport(); + `, + "module.ts": ` + export default function() { + return true; + } + `, + }, + "value" + ); + + expect(result).toBe(true); +}); + +test.each([ + ["Test", "export default class Test { static method() { return true; } }"], + ["default", "export default class { static method() { return true; } }"], +])("Export Default Class Name (%p)", (expectedClassName, classDeclarationStatement) => { + const [result] = util.transpileAndExecuteProjectReturningMainExport( + { + "main.ts": ` + import defaultExport from "./module"; + export const value = defaultExport.name; + `, + "module.ts": classDeclarationStatement, + }, + "value" + ); + + expect(result).toBe(expectedClassName); +}); + +test("Export Equals", () => { + const [result] = util.transpileAndExecuteProjectReturningMainExport( + { + "main.ts": ` + import * as module from "./module"; + export const value = module; + `, + "module.ts": ` + export = true; + `, + }, + "value" + ); + + expect(result).toBe(true); +}); diff --git a/test/unit/modules/resolution.spec.ts b/test/unit/modules/resolution.spec.ts new file mode 100644 index 000000000..6c1dbe105 --- /dev/null +++ b/test/unit/modules/resolution.spec.ts @@ -0,0 +1,143 @@ +import * as ts from "typescript"; +import * as util from "../../util"; + +const requireRegex = /require\("(.*?)"\)/; +const expectToRequire = (expected: string): util.TapCallback => builder => { + const match = requireRegex.exec(builder.getMainLuaCodeChunk()); + if (util.expectToBeDefined(match)) { + expect(match[1]).toBe(expected); + } +}; + +test.each([ + { + filePath: "main.ts", + usedPath: "./folder/Module", + expected: "folder.Module", + options: { rootDir: "." }, + }, + { + filePath: "main.ts", + usedPath: "./folder/Module", + expected: "folder.Module", + options: { rootDir: "./" }, + }, + { + filePath: "src/main.ts", + usedPath: "./folder/Module", + expected: "src.folder.Module", + options: { rootDir: "." }, + }, + { + filePath: "main.ts", + usedPath: "folder/Module", + expected: "folder.Module", + options: { rootDir: ".", baseUrl: "." }, + }, + { + filePath: "main.ts", + usedPath: "folder/Module", + expected: "folder.Module", + options: { rootDir: "./", baseUrl: "." }, + }, + { + filePath: "src/main.ts", + usedPath: "./folder/Module", + expected: "folder.Module", + options: { rootDir: "src" }, + }, + { + filePath: "src/main.ts", + usedPath: "./folder/Module", + expected: "folder.Module", + options: { rootDir: "./src" }, + }, + { + filePath: "src/dir/main.ts", + usedPath: "../Module", + expected: "Module", + options: { rootDir: "./src" }, + }, + { + filePath: "src/dir/dir/main.ts", + usedPath: "../../dir/Module", + expected: "dir.Module", + options: { rootDir: "./src" }, + }, +])("resolve paths with baseUrl or rootDir (%p)", ({ filePath, usedPath, expected, options }) => { + util.testModule` + import * as module from "${usedPath}"; + module; + ` + .setMainFileName(filePath) + .setOptions(options) + .tap(expectToRequire(expected)); +}); + +test("doesn't resolve paths out of root dir", () => { + util.testModule` + import * as module from "../module"; + module; + ` + .setMainFileName("src/main.ts") + .setOptions({ rootDir: "./src" }) + .disableSemanticCheck() + .expectToHaveDiagnostics(); +}); + +test.each([ + { + declarationStatement: ` + /** @noResolution */ + declare module "fake" {} + `, + mainCode: `import "fake";`, + expectedPath: "fake", + }, + { + declarationStatement: ` + /** @noResolution */ + declare module "fake" {} + `, + mainCode: `import * as fake from "fake"; fake;`, + expectedPath: "fake", + }, + { + declarationStatement: ` + /** @noResolution */ + declare module "fake" { + export const x: number; + } + `, + mainCode: `import { x } from "fake"; x;`, + expectedPath: "fake", + }, + { + declarationStatement: ` + /** @noResolution */ + declare module "fake" { + export const x: number; + } + + declare module "fake" { + export const y: number; + } + `, + mainCode: `import { y } from "fake"; y;`, + expectedPath: "fake", + }, +])("noResolution prevents any module path resolution behavior", ({ declarationStatement, mainCode, expectedPath }) => { + util.testModule(mainCode) + .setMainFileName("src/main.ts") + .addExtraFile("module.d.ts", declarationStatement) + .tap(expectToRequire(expectedPath)); +}); + +test("import = require", () => { + util.testModule` + import foo = require("./foo/bar"); + foo; + ` + .setOptions({ module: ts.ModuleKind.CommonJS }) + .tap(expectToRequire("foo.bar")); +}); diff --git a/test/unit/namespaces.spec.ts b/test/unit/namespaces.spec.ts new file mode 100644 index 000000000..21272ae8e --- /dev/null +++ b/test/unit/namespaces.spec.ts @@ -0,0 +1,121 @@ +import * as util from "../util"; + +test("legacy internal module syntax", () => { + util.testModule` + module Foo { + export const foo = "bar"; + } + + export const foo = Foo.foo; + `.expectToMatchJsResult(); +}); + +test("global scoping", () => { + const result = util.transpileAndExecute( + "return a.foo();", + undefined, + undefined, + `namespace a { export function foo() { return "bar"; } }` + ); + + expect(result).toBe("bar"); +}); + +test("nested namespace", () => { + util.testModule` + namespace A { + export namespace B { + export const foo = "foo"; + } + } + + export const foo = A.B.foo; + `.expectToMatchJsResult(); +}); + +test("nested namespace with dot in name", () => { + util.testModule` + namespace A.B { + export const foo = "foo"; + } + + export const foo = A.B.foo; + `.expectToMatchJsResult(); +}); + +test("context in namespace function", () => { + util.testModule` + namespace a { + export const foo = "foo"; + export function bar() { return this.foo + "bar"; } + } + + export const result = a.bar(); + `.expectToMatchJsResult(); +}); + +test("namespace merged with interface", () => { + util.testModule` + interface Foo {} + namespace Foo { + export function bar() { return "foobar"; } + } + + export const result = Foo.bar(); + `.expectToMatchJsResult(); +}); + +test("namespace merged across files", () => { + const testA = ` + namespace NS { + export namespace Inner { + export const foo = "foo"; + } + } + `; + + const testB = ` + namespace NS { + export namespace Inner { + export const bar = "bar"; + } + } + `; + + const { transpiledFiles } = util.transpileStringsAsProject({ "testA.ts": testA, "testB.ts": testB }); + const lua = transpiledFiles.map(f => f.lua).join("\n") + "\nreturn NS.Inner.foo .. NS.Inner.bar"; + expect(util.executeLua(lua)).toBe("foobar"); +}); + +test("declared namespace function call", () => { + const luaHeader = ` + myNameSpace = {} + function myNameSpace.declaredFunction(x) return 3*x end + `; + + util.testModule` + declare namespace myNameSpace { + function declaredFunction(this: void, x: number): number; + } + + export const result = myNameSpace.declaredFunction(2); + ` + .setReturnExport("result") + .setLuaHeader(luaHeader) + .expectToEqual(6); +}); + +test("`import =` on a namespace", () => { + util.testModule` + namespace outerNamespace { + export namespace innerNamespace { + export function func() { + return "foo"; + } + } + } + + import importedFunc = outerNamespace.innerNamespace.func; + export const result = importedFunc(); + `.expectToMatchJsResult(); +}); diff --git a/test/unit/numbers.spec.ts b/test/unit/numbers.spec.ts deleted file mode 100644 index ecda5d93a..000000000 --- a/test/unit/numbers.spec.ts +++ /dev/null @@ -1,69 +0,0 @@ -import * as util from "../util"; - -test.each([ - "NaN === NaN", - "NaN !== NaN", - "NaN + NaN", - "NaN - NaN", - "NaN * NaN", - "NaN / NaN", - "NaN + 1", - "1 + NaN", - "1 / NaN", - "NaN * 0", -])("%s", code => expect(util.transpileAndExecute(`return ${code}`)).toBe(eval(code))); - -test("NaN reassignment", () => { - const result = util.transpileAndExecute(`const NaN = 1; return NaN`); - - expect(result).toBe(NaN); -}); - -test.each(["Infinity", "Infinity - Infinity", "Infinity / -1", "Infinity * -1", "Infinity + 1", "Infinity - 1"])( - "%s", - code => expect(util.transpileAndExecute(`return ${code}`)).toBe(eval(code)) -); - -test("Infinity reassignment", () => { - const result = util.transpileAndExecute(`const Infinity = 1; return Infinity`); - - expect(result).toBe(Infinity); -}); - -const numberCases = [-1, 0, 1, 1.5, Infinity, -Infinity]; -const stringCases = ["-1", "0", "1", "1.5", "Infinity", "-Infinity"]; -const restCases: any[] = [true, false, "", " ", "\t", "\n", "foo", {}]; -const cases: any[] = [...numberCases, ...stringCases, ...restCases]; - -describe("Number", () => { - test.each(cases)("constructor(%p)", value => { - const result = util.transpileAndExecute(`return Number(${util.valueToString(value)})`); - expect(result).toBe(Number(value)); - }); - - test.each(cases)("isNaN(%p)", value => { - const result = util.transpileAndExecute(` - return Number.isNaN(${util.valueToString(value)} as any) - `); - - expect(result).toBe(Number.isNaN(value)); - }); - - test.each(cases)("isFinite(%p)", value => { - const result = util.transpileAndExecute(` - return Number.isFinite(${util.valueToString(value)} as any) - `); - - expect(result).toBe(Number.isFinite(value)); - }); -}); - -test.each(cases)("isNaN(%p)", value => { - const result = util.transpileAndExecute(`return isNaN(${util.valueToString(value)} as any)`); - expect(result).toBe(isNaN(value)); -}); - -test.each(cases)("isFinite(%p)", value => { - const result = util.transpileAndExecute(`return isFinite(${util.valueToString(value)} as any)`); - expect(result).toBe(isFinite(value)); -}); diff --git a/test/unit/printer/deadCodeAfterReturn.spec.ts b/test/unit/printer/deadCodeAfterReturn.spec.ts new file mode 100644 index 000000000..08c88658d --- /dev/null +++ b/test/unit/printer/deadCodeAfterReturn.spec.ts @@ -0,0 +1,61 @@ +import * as util from "../../util"; + +test("If dead code after return", () => { + util.testFunction` + if (true) { + return 3; + const b = 8; + } + `.expectToMatchJsResult(); +}); + +test("switch dead code after return", () => { + util.testFunction` + switch ("abc" as string) { + case "def": + return 4; + let abc = 4; + case "abc": + return 5; + let def = 6; + } + `.expectToMatchJsResult(); +}); + +test("Function dead code after return", () => { + util.testFunction` + function abc() { return 3; const a = 5; } + return abc(); + `.expectToMatchJsResult(); +}); + +test("Method dead code after return", () => { + util.testFunction` + class def { public static abc() { return 3; const a = 5; } } + return def.abc(); + `.expectToMatchJsResult(); +}); + +test("for dead code after return", () => { + const result = util.transpileAndExecute(`for (let i = 0; i < 10; i++) { return 3; const b = 8; }`); + + expect(result).toBe(3); +}); + +test("for..in dead code after return", () => { + const result = util.transpileAndExecute(`for (let a in {"a": 5, "b": 8}) { return 3; const b = 8; }`); + + expect(result).toBe(3); +}); + +test("for..of dead code after return", () => { + const result = util.transpileAndExecute(`for (let a of [1,2,4]) { return 3; const b = 8; }`); + + expect(result).toBe(3); +}); + +test("while dead code after return", () => { + const result = util.transpileAndExecute(`while (true) { return 3; const b = 8; }`); + + expect(result).toBe(3); +}); diff --git a/test/unit/printer/parenthesis.spec.ts b/test/unit/printer/parenthesis.spec.ts new file mode 100644 index 000000000..ef08a6e3d --- /dev/null +++ b/test/unit/printer/parenthesis.spec.ts @@ -0,0 +1,64 @@ +import * as util from "../../util"; + +test("binary expression with 'as' type assertion wrapped in parenthesis", () => { + expect(util.transpileAndExecute("return 2 * (3 - 2 as number);")).toBe(2); +}); + +test.each([ + "(x as any).foo;", + "(y.x as any).foo;", + "(y['x'] as any).foo;", + "(z() as any).foo;", + "(y.z() as any).foo;", + "(x).foo;", + "(y.x).foo;", + "(y['x']).foo;", + "(z()).foo;", + "(y.z()).foo;", + "(x as unknown as any).foo;", + "(x as any).foo;", + "((x as unknown) as any).foo;", + "((x) as any).foo;", +])("'as' type assertion should strip parenthesis (%p)", expression => { + const code = ` + declare let x: unknown; + declare let y: { x: unknown; z(this: void): unknown; }; + declare function z(this: void): unknown; + ${expression}`; + + const lua = util.transpileString(code, undefined, false); + expect(lua).not.toMatch(/\(.+\)/); +}); + +test.each([ + "(x + 1 as any).foo;", + "(!x as any).foo;", + "(x ** 2 as any).foo;", + "(x < 2 as any).foo;", + "(x in y as any).foo;", + "(x + 1).foo;", + "(!x).foo;", + "(x + 1 as unknown as any).foo;", + "((x + 1 as unknown) as any).foo;", + "(!x as unknown as any).foo;", + "((!x as unknown) as any).foo;", + "(!x as any).foo;", + "((!x) as any).foo;", +])("'as' type assertion should not strip parenthesis (%p)", expression => { + const code = ` + declare let x: number; + declare let y: {}; + ${expression}`; + + const lua = util.transpileString(code, undefined, false); + expect(lua).toMatch(/\(.+\)/); +}); + +test("not operator precedence (%p)", () => { + const code = ` + const a = true; + const b = false; + return !a && b;`; + + expect(util.transpileAndExecute(code)).toBe(false); +}); diff --git a/test/unit/semicolons.spec.ts b/test/unit/printer/semicolons.spec.ts similarity index 56% rename from test/unit/semicolons.spec.ts rename to test/unit/printer/semicolons.spec.ts index 2cbe3ecb6..ae3a79244 100644 --- a/test/unit/semicolons.spec.ts +++ b/test/unit/printer/semicolons.spec.ts @@ -1,15 +1,15 @@ -import * as util from "../util"; +import * as util from "../../util"; test.each(["const a = 1; const b = a;", "const a = 1; let b: number; b = a;", "{}", "function bar() {} bar();"])( "semicolon insertion (%p)", leadingStatement => { const code = ` - let result = ""; - function foo() { result = "foo"; } - ${leadingStatement} - (foo)(); - return result; - `; + let result = ""; + function foo() { result = "foo"; } + ${leadingStatement} + (foo)(); + return result; + `; expect(util.transpileAndExecute(code)).toEqual("foo"); } ); diff --git a/test/unit/sourcemaps.spec.ts b/test/unit/printer/sourcemaps.spec.ts similarity index 70% rename from test/unit/sourcemaps.spec.ts rename to test/unit/printer/sourcemaps.spec.ts index 51d01bbc8..4023dad19 100644 --- a/test/unit/sourcemaps.spec.ts +++ b/test/unit/printer/sourcemaps.spec.ts @@ -1,14 +1,15 @@ import { Position, SourceMapConsumer } from "source-map"; -import * as tstl from "../../src"; -import * as util from "../util"; +import * as tstl from "../../../src"; +import * as util from "../../util"; test.each([ { - typeScriptSource: ` + code: ` const abc = "foo"; const def = "bar"; - const xyz = "baz";`, + const xyz = "baz"; + `, assertPatterns: [ { luaPattern: "abc", typeScriptPattern: "abc" }, @@ -20,26 +21,28 @@ test.each([ ], }, { - typeScriptSource: ` + code: ` function abc() { return def(); } + function def() { return "foo"; } - return abc();`, + `, assertPatterns: [ { luaPattern: "function abc(", typeScriptPattern: "function abc() {" }, { luaPattern: "function def(", typeScriptPattern: "function def() {" }, - { luaPattern: "return abc(", typeScriptPattern: "return abc(" }, + { luaPattern: "return def(", typeScriptPattern: "return def(" }, { luaPattern: "end", typeScriptPattern: "function def() {" }, ], }, { - typeScriptSource: ` + code: ` const enum abc { foo = 2, bar = 4 }; - const xyz = abc.foo;`, + const xyz = abc.foo; + `, assertPatterns: [ { luaPattern: "xyz", typeScriptPattern: "xyz" }, @@ -47,8 +50,9 @@ test.each([ ], }, { - typeScriptSource: ` - import {Foo} from "foo"; + code: ` + // @ts-ignore + import { Foo } from "foo"; Foo; `, @@ -58,7 +62,8 @@ test.each([ ], }, { - typeScriptSource: ` + code: ` + // @ts-ignore import * as Foo from "foo"; Foo; `, @@ -69,7 +74,8 @@ test.each([ ], }, { - typeScriptSource: ` + code: ` + // @ts-ignore class Bar extends Foo { constructor() { super(); @@ -92,7 +98,7 @@ test.each([ ], }, { - typeScriptSource: ` + code: ` class Foo { } `, @@ -100,7 +106,7 @@ test.each([ assertPatterns: [{ luaPattern: "function Foo.prototype.____constructor", typeScriptPattern: "class Foo" }], }, { - typeScriptSource: ` + code: ` class Foo { bar = "baz"; } @@ -109,7 +115,7 @@ test.each([ assertPatterns: [{ luaPattern: "function Foo.prototype.____constructor", typeScriptPattern: "class Foo" }], }, { - typeScriptSource: ` + code: ` declare const arr: string[]; for (const element of arr) {} `, @@ -120,7 +126,7 @@ test.each([ ], }, { - typeScriptSource: ` + code: ` declare function getArr(this: void): string[]; for (const element of getArr()) {} `, @@ -132,7 +138,7 @@ test.each([ ], }, { - typeScriptSource: ` + code: ` declare const arr: string[] for (let i = 0; i < arr.length; ++i) {} `, @@ -143,31 +149,28 @@ test.each([ { luaPattern: "i + 1", typeScriptPattern: "++i" }, ], }, -])("Source map has correct mapping (%p)", async ({ typeScriptSource, assertPatterns }) => { - // Act - const { file } = util.transpileStringResult(typeScriptSource); - - // Assert - if (!util.expectToBeDefined(file.lua) || !util.expectToBeDefined(file.sourceMap)) return; +])("Source map has correct mapping (%p)", async ({ code, assertPatterns }) => { + const file = util + .testModule(code) + .expectToHaveNoDiagnostics() + .getMainLuaFileResult(); const consumer = await new SourceMapConsumer(file.sourceMap); for (const { luaPattern, typeScriptPattern } of assertPatterns) { const luaPosition = lineAndColumnOf(file.lua, luaPattern); const mappedPosition = consumer.originalPositionFor(luaPosition); + const typescriptPosition = lineAndColumnOf(code, typeScriptPattern); - const typescriptPosition = lineAndColumnOf(typeScriptSource, typeScriptPattern); - - const mappedLineColumn = { line: mappedPosition.line, column: mappedPosition.column }; - expect(mappedLineColumn).toEqual(typescriptPosition); + expect(mappedPosition).toMatchObject(typescriptPosition); } }); test("Source map has correct sources", async () => { - const code = `const foo = "foo"`; - - const { file } = util.transpileStringResult(code); - - if (!util.expectToBeDefined(file.lua) || !util.expectToBeDefined(file.sourceMap)) return; + const file = util.testModule` + const foo = "foo" + ` + .expectToHaveNoDiagnostics() + .getMainLuaFileResult(); const consumer = await new SourceMapConsumer(file.sourceMap); expect(consumer.sources.length).toBe(1); @@ -175,11 +178,11 @@ test("Source map has correct sources", async () => { }); test("Source map has correct source root", async () => { - const code = `const foo = "foo"`; - - const { file } = util.transpileStringResult(code); - - if (!util.expectToBeDefined(file.lua) || !util.expectToBeDefined(file.sourceMap)) return; + const file = util.testModule` + const foo = "foo" + ` + .expectToHaveNoDiagnostics() + .getMainLuaFileResult(); const sourceMap = JSON.parse(file.sourceMap); expect(sourceMap.sourceRoot).toBe("."); @@ -189,14 +192,15 @@ test.each([ { code: `const type = "foobar";`, name: "type" }, { code: `const and = "foobar";`, name: "and" }, { code: `const $$$ = "foobar";`, name: "$$$" }, - { code: `const foo = { bar() { console.log(this); } };`, name: "this" }, + { code: `const foo = { bar() { this; } };`, name: "this" }, { code: `function foo($$$: unknown) {}`, name: "$$$" }, { code: `class $$$ {}`, name: "$$$" }, { code: `namespace $$$ { const foo = "bar"; }`, name: "$$$" }, ])("Source map has correct name mappings (%p)", async ({ code, name }) => { - const { file } = util.transpileStringResult(code); - - if (!util.expectToBeDefined(file.lua) || !util.expectToBeDefined(file.sourceMap)) return; + const file = util + .testModule(code) + .expectToHaveNoDiagnostics() + .getMainLuaFileResult(); const consumer = await new SourceMapConsumer(file.sourceMap); const typescriptPosition = lineAndColumnOf(code, name); @@ -206,41 +210,30 @@ test.each([ mappedName = mapping.name; } }); + expect(mappedName).toBe(name); }); test("sourceMapTraceback saves sourcemap in _G", () => { - // Arrange - const typeScriptSource = ` + const code = ` function abc() { return "foo"; } - return JSONStringify(_G.__TS__sourcemap);`; - - const options: tstl.CompilerOptions = { - sourceMapTraceback: true, - luaLibImport: tstl.LuaLibImportKind.Inline, - }; - // Act - const transpiledLua = util.transpileString(typeScriptSource, options); + return (globalThis as any).__TS__sourcemap; + `; - const sourceMapJson = util.transpileAndExecute( - typeScriptSource, - options, - undefined, - "declare const _G: {__TS__sourcemap: any};" - ); + const builder = util + .testFunction(code) + .setOptions({ sourceMapTraceback: true, luaLibImport: tstl.LuaLibImportKind.Inline }); - // Assert - expect(sourceMapJson).toBeDefined(); - - const sourceMap = JSON.parse(sourceMapJson); + const sourceMap = builder.getLuaExecutionResult(); + const transpiledLua = builder.getMainLuaCodeChunk(); + expect(sourceMap).toEqual(expect.any(Object)); const sourceMapFiles = Object.keys(sourceMap); - - expect(sourceMapFiles.length).toBe(1); - expect(sourceMap[sourceMapFiles[0]]).toBeDefined(); + expect(sourceMapFiles).toHaveLength(1); + const mainSourceMap = sourceMap[sourceMapFiles[0]]; const assertPatterns = [ { luaPattern: "function abc(", typeScriptPattern: "function abc() {" }, @@ -249,37 +242,36 @@ test("sourceMapTraceback saves sourcemap in _G", () => { for (const { luaPattern, typeScriptPattern } of assertPatterns) { const luaPosition = lineAndColumnOf(transpiledLua, luaPattern); - const mappedLine = sourceMap[sourceMapFiles[0]][luaPosition.line.toString()]; + const mappedLine = mainSourceMap[luaPosition.line.toString()]; - const typescriptPosition = lineAndColumnOf(typeScriptSource, typeScriptPattern); - - // Add 1 to account for transpiledAndExecute-added function header - expect(mappedLine).toEqual(typescriptPosition.line + 1); + const typescriptPosition = lineAndColumnOf(code, typeScriptPattern); + expect(mappedLine).toEqual(typescriptPosition.line); } }); test("Inline sourcemaps", () => { - const typeScriptSource = ` + const code = ` function abc() { return def(); } + function def() { return "foo"; } - return abc();`; - const compilerOptions: tstl.CompilerOptions = { inlineSourceMap: true }; + return abc(); + `; - const { file } = util.transpileStringResult(typeScriptSource, compilerOptions); - if (!util.expectToBeDefined(file.lua)) return; + const file = util + .testFunction(code) + .setOptions({ inlineSourceMap: true }) + .expectToMatchJsResult() + .getMainLuaFileResult(); const inlineSourceMapMatch = file.lua.match(/--# sourceMappingURL=data:application\/json;base64,([A-Za-z0-9+/=]+)/); - if (util.expectToBeDefined(inlineSourceMapMatch)) { const inlineSourceMap = Buffer.from(inlineSourceMapMatch[1], "base64").toString(); expect(file.sourceMap).toBe(inlineSourceMap); - - expect(util.executeLua(file.lua)).toBe("foo"); } }); diff --git a/test/unit/require.spec.ts b/test/unit/require.spec.ts deleted file mode 100644 index 216f8fbc6..000000000 --- a/test/unit/require.spec.ts +++ /dev/null @@ -1,334 +0,0 @@ -import * as ts from "typescript"; -import * as util from "../util"; - -const requireRegex = /require\("(.*?)"\)/; - -test.each([ - { - filePath: "main.ts", - usedPath: "./folder/Module", - expectedPath: "folder.Module", - options: { rootDir: "." }, - throwsError: false, - }, - { - filePath: "main.ts", - usedPath: "./folder/Module", - expectedPath: "folder.Module", - options: { rootDir: "./" }, - throwsError: false, - }, - { - filePath: "src/main.ts", - usedPath: "./folder/Module", - expectedPath: "src.folder.Module", - options: { rootDir: "." }, - throwsError: false, - }, - { - filePath: "main.ts", - usedPath: "folder/Module", - expectedPath: "folder.Module", - options: { rootDir: ".", baseUrl: "." }, - throwsError: false, - }, - { - filePath: "main.ts", - usedPath: "folder/Module", - expectedPath: "folder.Module", - options: { rootDir: "./", baseUrl: "." }, - throwsError: false, - }, - { - filePath: "src/main.ts", - usedPath: "./folder/Module", - expectedPath: "folder.Module", - options: { rootDir: "src" }, - throwsError: false, - }, - { - filePath: "src/main.ts", - usedPath: "./folder/Module", - expectedPath: "folder.Module", - options: { rootDir: "./src" }, - throwsError: false, - }, - { - filePath: "main.ts", - usedPath: "../Module", - expectedPath: "", - options: { rootDir: "./src" }, - throwsError: true, - }, - { - filePath: "src/dir/main.ts", - usedPath: "../Module", - expectedPath: "Module", - options: { rootDir: "./src" }, - throwsError: false, - }, - { - filePath: "src/dir/dir/main.ts", - usedPath: "../../dir/Module", - expectedPath: "dir.Module", - options: { rootDir: "./src" }, - throwsError: false, - }, -])( - "require paths root from --baseUrl or --rootDir (%p)", - ({ filePath, usedPath, expectedPath, options, throwsError }) => { - const input = { [filePath]: `import * as module from "${usedPath}"; module;` }; - if (throwsError) { - expect(() => util.transpileString(input, options)).toThrow(); - } else { - const lua = util.transpileString(input, options); - const match = requireRegex.exec(lua); - - if (util.expectToBeDefined(match)) { - expect(match[1]).toBe(expectedPath); - } - } - } -); - -test.each([ - { - declarationStatement: ` - declare module 'fake' {} - `, - mainCode: "import * as fake from 'fake'; fake;", - expectedPath: "src.fake", - }, - { - declarationStatement: ` - /** @noResolution */ - declare module 'fake' {} - `, - mainCode: "import * as fake from 'fake'; fake;", - expectedPath: "fake", - }, - { - declarationStatement: ` - declare module 'fake' { - export const x: number; - } - `, - mainCode: "import { x } from 'fake'; x;", - expectedPath: "src.fake", - }, - { - declarationStatement: ` - /** @noResolution */ - declare module 'fake' { - export const x: number; - } - `, - mainCode: "import { x } from 'fake'; x;", - expectedPath: "fake", - }, - { - declarationStatement: ` - /** @noResolution */ - declare module 'fake' { - export const x: number; - } - declare module 'fake' { - export const y: number; - } - `, - mainCode: "import { y } from 'fake'; y;", - expectedPath: "fake", - }, - { - declarationStatement: ` - declare module 'fake' { - export const x: number; - } - declare module 'fake' { - export const y: number; - } - `, - mainCode: "import { y } from 'fake'; y;", - expectedPath: "src.fake", - }, - { - declarationStatement: ` - declare module 'fake' {} - `, - mainCode: "import 'fake';", - expectedPath: "src.fake", - }, - { - declarationStatement: ` - /** @noResolution */ - declare module 'fake' {} - `, - mainCode: "import 'fake';", - expectedPath: "fake", - }, -])("noResolution prevents any module path resolution behaviour", ({ declarationStatement, mainCode, expectedPath }) => { - const lua = util.transpileString({ - "src/main.ts": mainCode, - "module.d.ts": declarationStatement, - }); - const match = requireRegex.exec(lua); - - if (util.expectToBeDefined(match)) { - expect(match[1]).toBe(expectedPath); - } -}); - -test("ImportEquals declaration require", () => { - const input = `import foo = require("./foo/bar"); foo;`; - - const lua = util.transpileString(input, { module: ts.ModuleKind.CommonJS }); - const match = requireRegex.exec(lua); - if (util.expectToBeDefined(match)) { - expect(match[1]).toBe("foo.bar"); - } -}); - -test.each(["export default value;", "export { value as default };"])("Export Default From (%p)", exportStatement => { - const [result] = util.transpileAndExecuteProjectReturningMainExport( - { - "main.ts": ` - export { default } from "./module"; - `, - "module.ts": ` - export const value = true; - ${exportStatement}; - `, - }, - "default" - ); - - expect(result).toBe(true); -}); - -test("Default Import and Export Expression", () => { - const [result] = util.transpileAndExecuteProjectReturningMainExport( - { - "main.ts": ` - import defaultExport from "./module"; - export const value = defaultExport; - `, - "module.ts": ` - export default 1 + 2 + 3; - `, - }, - "value" - ); - - expect(result).toBe(6); -}); - -test("Import and Export Assignment", () => { - const [result] = util.transpileAndExecuteProjectReturningMainExport( - { - "main.ts": ` - import * as m from "./module"; - export const value = m; - `, - "module.ts": ` - export = true; - `, - }, - "value" - ); - - expect(result).toBe(true); -}); - -test("Mixed Exports, Default and Named Imports", () => { - const [result] = util.transpileAndExecuteProjectReturningMainExport( - { - "main.ts": ` - import defaultExport, { a, b, c } from "./module"; - export const value = defaultExport + b + c; - `, - "module.ts": ` - export const a = 1; - export default a; - export const b = 2; - export const c = 3; - `, - }, - "value" - ); - - expect(result).toBe(6); -}); - -test("Mixed Exports, Default and Namespace Import", () => { - const [result] = util.transpileAndExecuteProjectReturningMainExport( - { - "main.ts": ` - import defaultExport, * as ns from "./module"; - export const value = defaultExport + ns.b + ns.c; - `, - "module.ts": ` - export const a = 1; - export default a; - export const b = 2; - export const c = 3; - `, - }, - "value" - ); - - expect(result).toBe(6); -}); - -test("Export Default Function", () => { - const [result] = util.transpileAndExecuteProjectReturningMainExport( - { - "main.ts": ` - import defaultExport from "./module"; - export const value = defaultExport(); - `, - "module.ts": ` - export default function() { - return true; - } - `, - }, - "value" - ); - - expect(result).toBe(true); -}); - -test.each([ - ["Test", "export default class Test { static method() { return true; } }"], - ["default", "export default class { static method() { return true; } }"], -])("Export Default Class Name (%p)", (expectedClassName, classDeclarationStatement) => { - const [result] = util.transpileAndExecuteProjectReturningMainExport( - { - "main.ts": ` - import defaultExport from "./module"; - export const value = defaultExport.name; - `, - "module.ts": classDeclarationStatement, - }, - "value" - ); - - expect(result).toBe(expectedClassName); -}); - -test("Export Equals", () => { - const [result] = util.transpileAndExecuteProjectReturningMainExport( - { - "main.ts": ` - import * as module from "./module"; - export const value = module; - `, - "module.ts": ` - export = true; - `, - }, - "value" - ); - - expect(result).toBe(true); -}); diff --git a/test/unit/spread.spec.ts b/test/unit/spread.spec.ts new file mode 100644 index 000000000..9f6f19ae4 --- /dev/null +++ b/test/unit/spread.spec.ts @@ -0,0 +1,89 @@ +import * as tstl from "../../src"; +import * as util 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"); + +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.LuaJIT]: builder => builder.tap(expectUnpack), + [tstl.LuaTarget.Lua51]: builder => builder.tap(expectUnpack), + [tstl.LuaTarget.Lua52]: builder => builder.tap(expectTableUnpack), + [tstl.LuaTarget.Lua53]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), + } + ); +}); + +describe("in array literal", () => { + util.testEachVersion("of array literal", () => util.testExpression`[...[0, 1, 2]]`, { + [tstl.LuaTarget.LuaJIT]: builder => builder.tap(expectUnpack), + [tstl.LuaTarget.Lua51]: builder => builder.tap(expectUnpack), + [tstl.LuaTarget.Lua52]: builder => builder.tap(expectTableUnpack), + [tstl.LuaTarget.Lua53]: builder => builder.tap(expectTableUnpack).expectToMatchJsResult(), + }); + + test.each(["", "string", "string with spaces", "string 1 2 3"])("of string literal (%p)", str => { + util.testExpressionTemplate`[...${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 [...it] + `.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).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(); + }); +}); diff --git a/test/unit/spreadElement.spec.ts b/test/unit/spreadElement.spec.ts deleted file mode 100644 index 13e528102..000000000 --- a/test/unit/spreadElement.spec.ts +++ /dev/null @@ -1,114 +0,0 @@ -import * as tstl from "../../src"; -import * as util from "../util"; - -test.each([{ inp: [] }, { inp: [1, 2, 3] }, { inp: [1, "test", 3] }])("Spread Element Push (%p)", ({ inp }) => { - const result = util.transpileAndExecute( - `return JSONStringify(([] as Array).push(...${JSON.stringify(inp)}));` - ); - expect(result).toBe(([] as Array).push(...inp)); -}); - -test("Spread Element Lua 5.1", () => { - // Cant test functional because our VM doesn't run on 5.1 - const options: tstl.CompilerOptions = { - luaTarget: tstl.LuaTarget.Lua51, - luaLibImport: tstl.LuaLibImportKind.None, - }; - const lua = util.transpileString(`[].push(...${JSON.stringify([1, 2, 3])});`, options); - expect(lua).toBe("__TS__ArrayPush(\n {},\n unpack({1, 2, 3})\n)"); -}); - -test("Spread Element Lua 5.2", () => { - const options: tstl.CompilerOptions = { - luaTarget: tstl.LuaTarget.Lua52, - luaLibImport: tstl.LuaLibImportKind.None, - }; - const lua = util.transpileString(`[...[0, 1, 2]]`, options); - expect(lua).toBe("local ____ = {\n table.unpack({0, 1, 2})\n}"); -}); - -test("Spread Element Lua 5.3", () => { - const options: tstl.CompilerOptions = { - luaTarget: tstl.LuaTarget.Lua53, - luaLibImport: tstl.LuaLibImportKind.None, - }; - const lua = util.transpileString(`[...[0, 1, 2]]`, options); - expect(lua).toBe("local ____ = {\n table.unpack({0, 1, 2})\n}"); -}); - -test("Spread Element Lua JIT", () => { - const options: tstl.CompilerOptions = { - luaTarget: tstl.LuaTarget.LuaJIT, - luaLibImport: tstl.LuaLibImportKind.None, - }; - const lua = util.transpileString(`[...[0, 1, 2]]`, options); - expect(lua).toBe("local ____ = {\n unpack({0, 1, 2})\n}"); -}); - -test("Spread Element Iterable", () => { - const code = ` - const it = { - i: -1, - [Symbol.iterator]() { - return this; - }, - next() { - ++this.i; - return { - value: 2 ** this.i, - done: this.i == 9, - } - } - }; - const arr = [...it]; - return JSONStringify(arr)`; - expect(JSON.parse(util.transpileAndExecute(code))).toEqual([1, 2, 4, 8, 16, 32, 64, 128, 256]); -}); - -test.each(["", "string", "string with spaces", "string 1 2 3"])('Spread Element String "%s"', str => { - const code = ` - const arr = [..."${str}"]; - return JSONStringify(arr)`; - expect(JSON.parse(util.transpileAndExecute(code))).toEqual([...str]); -}); - -test.each([ - "{ value: false, ...{ value: true } }", - "{ ...{ value: false }, value: true }", - "{ ...{ value: false }, value: false, ...{ value: true } }", - "{ ...{ x: true, y: true } }", - "{ x: true, ...{ y: true, z: true } }", - "{ ...{ x: true }, ...{ y: true, z: true } }", -])('SpreadAssignment "%s"', expression => { - const code = `return JSONStringify(${expression});`; - expect(JSON.parse(util.transpileAndExecute(code))).toEqual(eval(`(${expression})`)); -}); - -test("SpreadAssignment Destructure", () => { - const code = `let obj = { x: 0, y: 1, z: 2 };`; - const luaCode = ` - ${code} - return JSONStringify({ a: 0, ...obj, b: 1, c: 2 });`; - const jsCode = ` - ${code} - ({ a: 0, ...obj, b: 1, c: 2 })`; - expect(JSON.parse(util.transpileAndExecute(luaCode))).toStrictEqual(eval(jsCode)); -}); - -test("SpreadAssignment No Mutation", () => { - const code = ` - const obj: { x: number, y: number, z?: number } = { x: 0, y: 1 }; - const merge = { ...obj, z: 2 }; - return obj.z;`; - expect(util.transpileAndExecute(code)).toBe(undefined); -}); - -test.each([ - "function spread() { return [0, 1, 2] } const object = { ...spread() };", - "const object = { ...[0, 1, 2] };", -])('SpreadAssignment Array "%s"', expressionToCreateObject => { - const code = ` - ${expressionToCreateObject} - return JSONStringify([object[0], object[1], object[2]]);`; - expect(JSON.parse(util.transpileAndExecute(code))).toEqual([0, 1, 2]); -}); diff --git a/test/unit/string.spec.ts b/test/unit/string.spec.ts deleted file mode 100644 index 9590c478d..000000000 --- a/test/unit/string.spec.ts +++ /dev/null @@ -1,380 +0,0 @@ -import * as TSTLErrors from "../../src/TSTLErrors"; -import * as util from "../util"; - -test("Unsuported string function", () => { - expect(() => { - util.transpileString(`return "test".testThisIsNoMember()`); - }).toThrowExactError(TSTLErrors.UnsupportedProperty("string", "testThisIsNoMember", util.nodeStub)); -}); - -test("Suported lua string function", () => { - expect( - util.transpileAndExecute(`return "test".upper()`, undefined, undefined, `interface String { upper(): string; }`) - ).toBe("TEST"); -}); - -test.each([{ inp: [] }, { inp: [65] }, { inp: [65, 66] }, { inp: [65, 66, 67] }])( - "String.fromCharCode (%p)", - ({ inp }) => { - const result = util.transpileAndExecute(`return String.fromCharCode(${inp.toString()})`); - - expect(result).toBe(String.fromCharCode(...inp)); - } -); - -test.each([ - { a: 12, b: 23, c: 43 }, - { a: "test", b: "hello", c: "bye" }, - { a: "test", b: 42, c: "bye" }, - { a: "test", b: 42, c: 12 }, - { a: "test", b: 42, c: true }, - { a: false, b: 42, c: 12 }, -])("Template Strings (%p)", ({ a, b, c }) => { - const a1 = typeof a === "string" ? `'${a}'` : a; - const b1 = typeof b === "string" ? `'${b}'` : b; - const c1 = typeof c === "string" ? `'${c}'` : c; - - const result = util.transpileAndExecute(` - let a = ${a1}; - let b = ${b1}; - let c = ${c1}; - return \`${a} ${b} test ${c}\`; - `); - - expect(result).toBe(`${a} ${b} test ${c}`); -}); - -test.each([ - { a: 12, b: 23, c: 43 }, - { a: "test", b: "hello", c: "bye" }, - { a: "test", b: 42, c: "bye" }, - { a: "test", b: 42, c: 12 }, - { a: "test", b: 42, c: true }, - { a: false, b: 42, c: 12 }, -])("String Concat Operator (%p)", ({ a, b, c }) => { - const a1 = typeof a === "string" ? `'${a}'` : a; - const b1 = typeof b === "string" ? `'${b}'` : b; - const c1 = typeof c === "string" ? `'${c}'` : c; - - const result = util.transpileAndExecute(` - let a = ${a1}; - let b = ${b1}; - let c = ${c1}; - return a + " " + b + " test " + c; - `); - - expect(result).toBe(`${a} ${b} test ${c}`); -}); - -test.each([ - { input: "abcd", index: 3 }, - { input: "abcde", index: 3 }, - { input: "abcde", index: 0 }, - { input: "a", index: 0 }, -])("string index (%p)", ({ input, index }) => { - const result = util.transpileAndExecute(`return "${input}"[${index}];`); - - expect(result).toBe(input[index]); -}); - -test.each([ - { inp: "hello test", searchValue: "", replaceValue: "" }, - { inp: "hello test", searchValue: " ", replaceValue: "" }, - { inp: "hello test", searchValue: "hello", replaceValue: "" }, - { inp: "hello test", searchValue: "test", replaceValue: "" }, - { inp: "hello test", searchValue: "test", replaceValue: "world" }, - { inp: "hello test", searchValue: "test", replaceValue: "%world" }, - { inp: "hello %test", searchValue: "test", replaceValue: "world" }, - { inp: "hello %test", searchValue: "%test", replaceValue: "world" }, - { inp: "hello test", searchValue: "test", replaceValue: (): string => "a" }, - { inp: "hello test", searchValue: "test", replaceValue: (): string => "%a" }, - { inp: "aaa", searchValue: "a", replaceValue: "b" }, -])("string.replace (%p)", ({ inp, searchValue, replaceValue }) => { - const replaceValueString = - typeof replaceValue === "string" ? JSON.stringify(replaceValue) : replaceValue.toString(); - const result = util.transpileAndExecute(`return "${inp}".replace("${searchValue}", ${replaceValueString});`); - - // https://github.com/Microsoft/TypeScript/issues/22378 - if (typeof replaceValue === "string") { - expect(result).toBe(inp.replace(searchValue, replaceValue)); - } else { - expect(result).toBe(inp.replace(searchValue, replaceValue)); - } -}); - -test.each([ - { inp: ["", ""], expected: "" }, - { inp: ["hello", "test"], expected: "hellotest" }, - { inp: ["hello", "test", "bye"], expected: "hellotestbye" }, - { inp: ["hello", 42], expected: "hello42" }, - { inp: [42, "hello"], expected: "42hello" }, -])("string.concat[+] (%p)", ({ inp, expected }) => { - const concatStr = inp.map(elem => (typeof elem === "string" ? `"${elem}"` : elem)).join(" + "); - - const result = util.transpileAndExecute(`return ${concatStr}`); - - expect(result).toBe(expected); -}); - -test.each([ - { str: "", param: ["", ""] }, - { str: "hello", param: ["test"] }, - { str: "hello", param: [] }, - { str: "hello", param: ["test", "bye"] }, -])("string.concatFct (%p)", ({ str, param }) => { - const paramStr = param.map(elem => `"${elem}"`).join(", "); - const result = util.transpileAndExecute(`return "${str}".concat(${paramStr})`); - expect(result).toBe(str.concat(...param)); -}); - -test.each([ - { inp: "hello test", searchValue: "" }, - { inp: "hello test", searchValue: "t" }, - { inp: "hello test", searchValue: "h" }, - { inp: "hello test", searchValue: "invalid" }, - { inp: "hello.test", searchValue: "." }, -])("string.indexOf (%p)", ({ inp, searchValue }) => { - const result = util.transpileAndExecute(`return "${inp}".indexOf("${searchValue}")`); - - expect(result).toBe(inp.indexOf(searchValue)); -}); - -test.each([ - { inp: "hello test", searchValue: "t", offset: 5 }, - { inp: "hello test", searchValue: "t", offset: 6 }, - { inp: "hello test", searchValue: "t", offset: 7 }, - { inp: "hello test", searchValue: "h", offset: 4 }, -])("string.indexOf with offset (%p)", ({ inp, searchValue, offset }) => { - const result = util.transpileAndExecute(`return "${inp}".indexOf("${searchValue}", ${offset})`); - - expect(result).toBe(inp.indexOf(searchValue, offset)); -}); - -test.each([{ inp: "hello test", searchValue: "t", x: 4, y: 3 }, { inp: "hello test", searchValue: "h", x: 3, y: 4 }])( - "string.indexOf with offset expression (%p)", - ({ inp, searchValue, x, y }) => { - const result = util.transpileAndExecute(`return "${inp}".indexOf("${searchValue}", 2 > 1 && ${x} || ${y})`); - - expect(result).toBe(inp.indexOf(searchValue, x)); - } -); - -test.each([ - { inp: "hello test" }, - { inp: "hello test", start: 0 }, - { inp: "hello test", start: 1 }, - { inp: "hello test", start: 1, end: 2 }, - { inp: "hello test", start: 1, end: 5 }, -])("string.slice (%p)", ({ inp, start, end }) => { - const paramStr = start ? (end ? `${start}, ${end}` : `${start}`) : ""; - const result = util.transpileAndExecute(`return "${inp}".slice(${paramStr})`); - - expect(result).toBe(inp.slice(start, end)); -}); - -test.each([ - { inp: "hello test", start: 0 }, - { inp: "hello test", start: 1 }, - { inp: "hello test", start: 1, end: 2 }, - { inp: "hello test", start: 1, end: 5 }, -])("string.substring (%p)", ({ inp, start, end }) => { - const paramStr = end ? `${start}, ${end}` : `${start}`; - const result = util.transpileAndExecute(`return "${inp}".substring(${paramStr})`); - - expect(result).toBe(inp.substring(start, end)); -}); - -test.each([{ inp: "hello test", start: 1, ignored: 0 }, { inp: "hello test", start: 3, ignored: 0, end: 5 }])( - "string.substring with expression (%p)", - ({ inp, start, ignored, end }) => { - const paramStr = `2 > 1 && ${start} || ${ignored}` + (end ? `, ${end}` : ""); - const result = util.transpileAndExecute(`return "${inp}".substring(${paramStr})`); - - expect(result).toBe(inp.substring(start, end)); - } -); - -test.each([ - { inp: "hello test", start: 0 }, - { inp: "hello test", start: 1 }, - { inp: "hello test", start: 1, end: 2 }, - { inp: "hello test", start: 1, end: 5 }, -])("string.substr (%p)", ({ inp, start, end }) => { - const paramStr = end ? `${start}, ${end}` : `${start}`; - const result = util.transpileAndExecute(`return "${inp}".substr(${paramStr})`); - - expect(result).toBe(inp.substr(start, end)); -}); - -test.each([{ inp: "hello test", start: 1, ignored: 0 }, { inp: "hello test", start: 3, ignored: 0, end: 2 }])( - "string.substr with expression (%p)", - ({ inp, start, ignored, end }) => { - const paramStr = `2 > 1 && ${start} || ${ignored}` + (end ? `, ${end}` : ""); - const result = util.transpileAndExecute(`return "${inp}".substr(${paramStr})`); - - expect(result).toBe(inp.substr(start, end)); - } -); - -test.each(["", "h", "hello"])("string.length (%p)", input => { - const result = util.transpileAndExecute(`return "${input}".length`); - - expect(result).toBe(input.length); -}); - -test.each(["hello TEST"])("string.toLowerCase (%p)", inp => { - const result = util.transpileAndExecute(`return "${inp}".toLowerCase()`); - - expect(result).toBe(inp.toLowerCase()); -}); - -test.each(["hello test"])("string.toUpperCase (%p)", inp => { - const result = util.transpileAndExecute(`return "${inp}".toUpperCase()`); - - expect(result).toBe(inp.toUpperCase()); -}); - -test.each([ - { inp: "hello test", separator: "" }, - { inp: "hello test", separator: " " }, - { inp: "hello test", separator: "h" }, - { inp: "hello test", separator: "t" }, - { inp: "hello test", separator: "l" }, - { inp: "hello test", separator: "invalid" }, - { inp: "hello test", separator: "hello test" }, -])("string.split (%p)", ({ inp, separator }) => { - const result = util.transpileAndExecute(`return JSONStringify("${inp}".split("${separator}"))`); - - expect(result).toBe(JSON.stringify(inp.split(separator))); -}); - -test.each([ - { inp: "hello test", index: 1 }, - { inp: "hello test", index: 2 }, - { inp: "hello test", index: 3 }, - { inp: "hello test", index: 99 }, -])("string.charAt (%p)", ({ inp, index }) => { - const result = util.transpileAndExecute(`return "${inp}".charAt(${index})`); - - expect(result).toBe(inp.charAt(index)); -}); - -test.each([{ inp: "hello test", index: 1 }, { inp: "hello test", index: 2 }, { inp: "hello test", index: 3 }])( - "string.charCodeAt (%p)", - ({ inp, index }) => { - const result = util.transpileAndExecute(`return "${inp}".charCodeAt(${index})`); - - expect(result).toBe(inp.charCodeAt(index)); - } -); - -test.each([ - { inp: "hello test", index: 1, ignored: 0 }, - { inp: "hello test", index: 1, ignored: 2 }, - { inp: "hello test", index: 3, ignored: 2 }, - { inp: "hello test", index: 3, ignored: 99 }, -])("string.charAt with expression (%p)", ({ inp, index, ignored }) => { - const result = util.transpileAndExecute(`return "${inp}".charAt(2 > 1 && ${index} || ${ignored})`); - - expect(result).toBe(inp.charAt(index)); -}); - -test.each<{ inp: string; args: Parameters }>([ - { inp: "hello test", args: [""] }, - { inp: "hello test", args: ["hello"] }, - { inp: "hello test", args: ["test"] }, - { inp: "hello test", args: ["test", 6] }, -])("string.startsWith (%p)", ({ inp, args }) => { - const argsString = util.valuesToString(args); - const result = util.transpileAndExecute(`return "${inp}".startsWith(${argsString})`); - - expect(result).toBe(inp.startsWith(...args)); -}); - -test.each<{ inp: string; args: Parameters }>([ - { inp: "hello test", args: [""] }, - { inp: "hello test", args: ["test"] }, - { inp: "hello test", args: ["hello"] }, - { inp: "hello test", args: ["hello", 5] }, -])("string.endsWith (%p)", ({ inp, args }) => { - const argsString = util.valuesToString(args); - const result = util.transpileAndExecute(`return "${inp}".endsWith(${argsString})`); - - expect(result).toBe(inp.endsWith(...args)); -}); - -test.each([ - { inp: "hello test", count: 0 }, - { inp: "hello test", count: 1 }, - { inp: "hello test", count: 2 }, - { inp: "hello test", count: 1.1 }, - { inp: "hello test", count: 1.5 }, - { inp: "hello test", count: 1.9 }, -])("string.repeat (%p)", ({ inp, count }) => { - const result = util.transpileAndExecute(`return "${inp}".repeat(${count})`); - - expect(result).toBe(inp.repeat(count)); -}); - -const padCases = [ - { inp: "foo", maxLength: 0 }, - { inp: "foo", maxLength: 3 }, - { inp: "foo", maxLength: 5 }, - { inp: "foo", maxLength: 4, fillString: " " }, - { inp: "foo", maxLength: 10, fillString: " " }, - { inp: "foo", maxLength: 5, fillString: "1234" }, - { inp: "foo", maxLength: 5.9, fillString: "1234" }, - { inp: "foo", maxLength: NaN }, -]; - -test.each(padCases)("string.padStart (%p)", ({ inp, maxLength, fillString }) => { - const argsString = util.valuesToString([maxLength, fillString]); - const result = util.transpileAndExecute(`return "${inp}".padStart(${argsString})`); - - expect(result).toBe(inp.padStart(maxLength, fillString)); -}); - -test.each(padCases)("string.padEnd (%p)", ({ inp, maxLength, fillString }) => { - const argsString = util.valuesToString([maxLength, fillString]); - const result = util.transpileAndExecute(`return "${inp}".padEnd(${argsString})`); - - expect(result).toBe(inp.padEnd(maxLength, fillString)); -}); - -test.each([`"foobar".length`, `"foobar".repeat(2)`, "`foo${'bar'}`.length", "`foo${'bar'}`.repeat(2)"])( - "string literal property access (%p)", - expression => { - const code = `return ${expression}`; - const expectResult = eval(expression); - expect(util.transpileAndExecute(code)).toBe(expectResult); - } -); - -test("scoped string-union inference", () => { - const inp = "foo"; - - const result = util.transpileAndExecute(` - const union: string = "${inp}"; - - if (union === "foo" || union === "bar") { - return union.length; - } - - return 0; - `); - - expect(result).toBe(inp.length); -}); - -test.each([ - "function generic(string: T)", - "type StringType = string; function generic(string: T)", -])("string constrained generic foreach (%p)", signature => { - const code = ` - ${signature}: number { - return string.length; - } - return generic("string"); - `; - expect(util.transpileAndExecute(code)).toBe(6); -}); diff --git a/test/unit/taggedTemplateLiterals.spec.ts b/test/unit/taggedTemplateLiterals.spec.ts deleted file mode 100644 index 6ab4e68cd..000000000 --- a/test/unit/taggedTemplateLiterals.spec.ts +++ /dev/null @@ -1,123 +0,0 @@ -import * as util from "../util"; - -const testCases = [ - { - callExpression: "func``", - joinAllResult: "", - joinRawResult: "", - }, - { - callExpression: "func`hello`", - joinAllResult: "hello", - joinRawResult: "hello", - }, - { - callExpression: "func`hello ${1} ${2} ${3}`", - joinAllResult: "hello 1 2 3", - joinRawResult: "hello ", - }, - { - callExpression: "func`hello ${(() => 'iife')()}`", - joinAllResult: "hello iife", - joinRawResult: "hello ", - }, - { - callExpression: "func`hello ${1 + 2 + 3} arithmetic`", - joinAllResult: "hello 6 arithmetic", - joinRawResult: "hello arithmetic", - }, - { - callExpression: "func`begin ${'middle'} end`", - joinAllResult: "begin middle end", - joinRawResult: "begin end", - }, - { - callExpression: "func`hello ${func`hello`}`", - joinAllResult: "hello hello", - joinRawResult: "hello ", - }, - { - callExpression: "func`hello \\u00A9`", - joinAllResult: "hello ©", - joinRawResult: "hello \\u00A9", - }, - { - callExpression: "func`hello $ { }`", - joinAllResult: "hello $ { }", - joinRawResult: "hello $ { }", - }, - { - callExpression: "func`hello { ${'brackets'} }`", - joinAllResult: "hello { brackets }", - joinRawResult: "hello { }", - }, - { - callExpression: "func`hello \\``", - joinAllResult: "hello `", - joinRawResult: "hello \\`", - }, - { - callExpression: "obj.func`hello ${'propertyAccessExpression'}`", - joinAllResult: "hello propertyAccessExpression", - joinRawResult: "hello ", - }, - { - callExpression: "obj['func']`hello ${'elementAccessExpression'}`", - joinAllResult: "hello elementAccessExpression", - joinRawResult: "hello ", - }, -]; - -test.each(testCases)("TaggedTemplateLiteral call (%p)", ({ callExpression, joinAllResult }) => { - const result = util.transpileAndExecute(` - function func(strings: TemplateStringsArray, ...expressions: any[]) { - const toJoin = []; - for (let i = 0; i < strings.length; ++i) { - if (strings[i]) { - toJoin.push(strings[i]); - } - if (expressions[i]) { - toJoin.push(expressions[i]); - } - } - return toJoin.join(""); - } - const obj = { - func - }; - return ${callExpression}; - `); - - expect(result).toBe(joinAllResult); -}); - -test.each(testCases)("TaggedTemplateLiteral raw preservation (%p)", ({ callExpression, joinRawResult }) => { - const result = util.transpileAndExecute(` - function func(strings: TemplateStringsArray, ...expressions: any[]) { - return strings.raw.join(""); - } - const obj = { - func - }; - return ${callExpression}; - `); - - expect(result).toBe(joinRawResult); -}); - -test.each(["func`noSelfParameter`", "obj.func`noSelfParameter`", "obj[`func`]`noSelfParameter`"])( - "TaggedTemplateLiteral no self parameter", - callExpression => { - const result = util.transpileAndExecute(` - function func(this: void, strings: TemplateStringsArray, ...expressions: any[]) { - return strings.join(""); - } - const obj = { - func - }; - return ${callExpression}; - `); - - expect(result).toBe("noSelfParameter"); - } -); diff --git a/test/unit/templateLiterals.spec.ts b/test/unit/templateLiterals.spec.ts new file mode 100644 index 000000000..7db192a88 --- /dev/null +++ b/test/unit/templateLiterals.spec.ts @@ -0,0 +1,62 @@ +import * as util from "../util"; + +test.each([ + { a: 12, b: 23, c: 43 }, + { a: "test", b: "hello", c: "bye" }, + { a: "test", b: 42, c: "bye" }, + { a: "test", b: 42, c: 12 }, + { a: "test", b: 42, c: true }, + { a: false, b: 42, c: 12 }, +])("template literal (%p)", ({ a, b, c }) => { + util.testExpressionTemplate`\`\${${a}} \${${b}} test \${${c}}\``.expectToMatchJsResult(); +}); + +test.each(["a++", "a--", "--a", "++a"])("template literal with expression (%p)", expression => { + util.testFunction` + let a = 3; + return \`value\${${expression}}\`; + `.expectToMatchJsResult(); +}); + +test.each(["`foo${'bar'}`.length", "`foo${'bar'}`.repeat(2)"])("template literal property access (%p)", expression => { + util.testExpression(expression).expectToMatchJsResult(); +}); + +test.each([ + "func``", + "func`hello`", + "func`hello ${1} ${2} ${3}`", + "func`hello ${(() => 'iife')()}`", + "func`hello ${1 + 2 + 3} arithmetic`", + "func`begin ${'middle'} end`", + "func`hello ${func`hello`}`", + "func`hello \\u00A9`", + "func`hello $ { }`", + "func`hello { ${'brackets'} }`", + "func`hello \\``", + "obj.func`hello ${'propertyAccessExpression'}`", + "obj['func']`hello ${'elementAccessExpression'}`", +])("tagged template literal (%p)", expression => { + util.testFunction` + function func(strings: TemplateStringsArray, ...expressions: any[]) { + return { strings: [...strings], raw: strings.raw, expressions }; + } + + const obj = { func }; + return ${expression}; + `.expectToMatchJsResult(); +}); + +test.each(["func`noSelfParameter`", "obj.func`noSelfParameter`", "obj[`func`]`noSelfParameter`"])( + "tagged template literal function context (%p)", + expression => { + util.testFunction` + function func(this: void, strings: TemplateStringsArray) { + return [...strings]; + } + + const obj = { func }; + return ${expression}; + `.expectToMatchJsResult(); + } +); diff --git a/test/unit/transformers.spec.ts b/test/unit/transformers/transformers.spec.ts similarity index 81% rename from test/unit/transformers.spec.ts rename to test/unit/transformers/transformers.spec.ts index cb7f143fe..db36c27dc 100644 --- a/test/unit/transformers.spec.ts +++ b/test/unit/transformers/transformers.spec.ts @@ -1,6 +1,6 @@ import * as path from "path"; -import * as tstl from "../../src"; -import * as util from "../util"; +import * as tstl from "../../../src"; +import * as util from "../../util"; const optionsOfTransformer = (transformer: tstl.TransformerImport): tstl.CompilerOptions => ({ plugins: [transformer], @@ -8,7 +8,7 @@ const optionsOfTransformer = (transformer: tstl.TransformerImport): tstl.Compile test("should ignore language service plugins", () => { const options: tstl.CompilerOptions = { - plugins: [{ name: path.join(__dirname, "transformers/resolve.ts") }], + plugins: [{ name: path.join(__dirname, "resolve.ts") }], }; expect(util.transpileAndExecute("return", options)).toBe(undefined); @@ -22,26 +22,26 @@ describe("resolution", () => { test("should resolve relative transformer paths", () => { jest.spyOn(process, "cwd").mockReturnValue(__dirname); - testTransform({ transform: "./transformers/resolve.ts" }); + testTransform({ transform: "./resolve.ts" }); }); test("should load js transformers", () => { - testTransform({ transform: path.join(__dirname, "transformers/resolve.js") }); + testTransform({ transform: path.join(__dirname, "resolve.js") }); }); test("should load ts transformers", () => { - testTransform({ transform: path.join(__dirname, "transformers/resolve.ts") }); + testTransform({ transform: path.join(__dirname, "resolve.ts") }); }); test('should support "import" option', () => { testTransform({ - transform: path.join(__dirname, "transformers/import.ts"), + transform: path.join(__dirname, "import.ts"), import: "transformer", }); }); test("should error if transformer could not be resolved", () => { - const transform = path.join(__dirname, "transformers/error.ts"); + const transform = path.join(__dirname, "error.ts"); const options = optionsOfTransformer({ transform }); const { diagnostics } = util.transpileStringResult("", options); expect(diagnostics).toHaveDiagnostics(); @@ -52,7 +52,7 @@ describe("factory types", () => { const value = "foo"; const getOptions = (options: Partial) => optionsOfTransformer({ - transform: path.join(__dirname, "transformers/types.ts"), + transform: path.join(__dirname, "types.ts"), ...options, }); diff --git a/test/unit/typechecking.spec.ts b/test/unit/typechecking.spec.ts deleted file mode 100644 index 500e4e094..000000000 --- a/test/unit/typechecking.spec.ts +++ /dev/null @@ -1,185 +0,0 @@ -import * as TSTLErrors from "../../src/TSTLErrors"; -import * as util from "../util"; - -test.each(["0", "30", "30_000", "30.00"])("typeof number (%p)", inp => { - const result = util.transpileAndExecute(`return typeof ${inp};`); - - expect(result).toBe("number"); -}); - -test.each(['"abc"', "`abc`"])("typeof string (%p)", inp => { - const result = util.transpileAndExecute(`return typeof ${inp};`); - - expect(result).toBe("string"); -}); - -test.each(["false", "true"])("typeof boolean (%p)", inp => { - const result = util.transpileAndExecute(`return typeof ${inp};`); - - expect(result).toBe("boolean"); -}); - -test.each(["{}", "[]"])("typeof object literal (%p)", inp => { - const result = util.transpileAndExecute(`return typeof ${inp};`); - - expect(result).toBe("object"); -}); - -test("typeof class instance", () => { - const result = util.transpileAndExecute(`class myClass {} let inst = new myClass(); return typeof inst;`); - - expect(result).toBe("object"); -}); - -test("typeof function", () => { - const result = util.transpileAndExecute(`return typeof (() => 3);`); - - expect(result).toBe("function"); -}); - -test.each(["null", "undefined"])("typeof undefined (%p)", inp => { - const result = util.transpileAndExecute(`return typeof ${inp};`); - - expect(result).toBe("undefined"); -}); - -test("instanceof", () => { - const result = util.transpileAndExecute( - "class myClass {} let inst = new myClass(); return inst instanceof myClass;" - ); - - expect(result).toBe(true); -}); - -test("instanceof inheritance", () => { - const result = util.transpileAndExecute(` - class myClass {} - class childClass extends myClass{} - let inst = new childClass(); return inst instanceof myClass; - `); - - expect(result).toBe(true); -}); - -test("instanceof inheritance false", () => { - const result = util.transpileAndExecute(` - class myClass {} - class childClass extends myClass{} - let inst = new myClass(); return inst instanceof childClass; - `); - - expect(result).toBe(false); -}); - -test("{} instanceof Object", () => { - const result = util.transpileAndExecute("return {} instanceof Object;"); - - expect(result).toBe(true); -}); - -test("function instanceof Object", () => { - const result = util.transpileAndExecute("return (() => {}) instanceof Object;"); - - expect(result).toBe(true); -}); - -test("null instanceof Object", () => { - const result = util.transpileAndExecute("return (null as any) instanceof Object;"); - - expect(result).toBe(false); -}); - -test("instanceof undefined", () => { - expect(() => { - util.transpileAndExecute("return {} instanceof (undefined as any);"); - }).toThrow("Right-hand side of 'instanceof' is not an object"); -}); - -test("null instanceof Class", () => { - const result = util.transpileAndExecute("class myClass {} return (null as any) instanceof myClass;"); - - expect(result).toBe(false); -}); - -test.each(["extension", "metaExtension"])("instanceof extension (%p)", extensionType => { - const code = ` - declare class A {} - /** @${extensionType} **/ - class B extends A {} - declare const foo: any; - const result = foo instanceof B; - `; - expect(() => util.transpileString(code)).toThrowExactError(TSTLErrors.InvalidInstanceOfExtension(util.nodeStub)); -}); - -test("instanceof export", () => { - const result = util.transpileExecuteAndReturnExport( - `export class myClass {} - let inst = new myClass(); - export const result = inst instanceof myClass;`, - "result" - ); - - expect(result).toBe(true); -}); - -test("instanceof Symbol.hasInstance", () => { - const result = util.transpileAndExecute(` - class myClass { - static [Symbol.hasInstance]() { - return false; - } - } - - const inst = new myClass(); - const isInstanceOld = inst instanceof myClass; - myClass[Symbol.hasInstance] = () => true; - const isInstanceNew = inst instanceof myClass; - return isInstanceOld !== isInstanceNew; - `); - - expect(result).toBe(true); -}); - -test.each([ - { expression: "{}", operator: "===", compareTo: "object", expectResult: true }, - { expression: "{}", operator: "!==", compareTo: "object", expectResult: false }, - { expression: "{}", operator: "==", compareTo: "object", expectResult: true }, - { expression: "{}", operator: "!=", compareTo: "object", expectResult: false }, - { expression: "{}", operator: "<=", compareTo: "object", expectResult: true }, - { expression: "{}", operator: "<", compareTo: "object", expectResult: false }, - { expression: "undefined", operator: "===", compareTo: "undefined", expectResult: true }, - { expression: "() => {}", operator: "===", compareTo: "function", expectResult: true }, - { expression: "1", operator: "===", compareTo: "number", expectResult: true }, - { expression: "true", operator: "===", compareTo: "boolean", expectResult: true }, - { expression: `"foo"`, operator: "===", compareTo: "string", expectResult: true }, -])("typeof literal comparison (%p)", ({ expression, operator, compareTo, expectResult }) => { - const code = ` - let val = ${expression}; - return typeof val ${operator} "${compareTo}";`; - - expect(util.transpileString(code)).not.toMatch("__TS__TypeOf"); - expect(util.transpileAndExecute(code)).toBe(expectResult); -}); - -test.each([ - { expression: "{}", operator: "===", compareTo: "object", expectResult: true }, - { expression: "{}", operator: "!==", compareTo: "object", expectResult: false }, - { expression: "{}", operator: "==", compareTo: "object", expectResult: true }, - { expression: "{}", operator: "!=", compareTo: "object", expectResult: false }, - { expression: "{}", operator: "<=", compareTo: "object", expectResult: true }, - { expression: "{}", operator: "<", compareTo: "object", expectResult: false }, - { expression: "undefined", operator: "===", compareTo: "undefined", expectResult: true }, - { expression: "() => {}", operator: "===", compareTo: "function", expectResult: true }, - { expression: "1", operator: "===", compareTo: "number", expectResult: true }, - { expression: "true", operator: "===", compareTo: "boolean", expectResult: true }, - { expression: `"foo"`, operator: "===", compareTo: "string", expectResult: true }, -])("typeof non-literal comparison (%p)", ({ expression, operator, compareTo, expectResult }) => { - const code = ` - let val = ${expression}; - let compareTo = "${compareTo}"; - return typeof val ${operator} compareTo;`; - - expect(util.transpileString(code)).toMatch("__TS__TypeOf"); - expect(util.transpileAndExecute(code)).toBe(expectResult); -}); diff --git a/test/unit/typeof.spec.ts b/test/unit/typeof.spec.ts new file mode 100644 index 000000000..1541884b1 --- /dev/null +++ b/test/unit/typeof.spec.ts @@ -0,0 +1,76 @@ +import * as util from "../util"; + +test.each(["0", "30", "30_000", "30.00"])("typeof number (%p)", inp => { + util.testExpression`typeof ${inp}`.expectToMatchJsResult(); +}); + +test.each(['"abc"', "`abc`"])("typeof string (%p)", inp => { + util.testExpression`typeof ${inp}`.expectToMatchJsResult(); +}); + +test.each(["false", "true"])("typeof boolean (%p)", inp => { + util.testExpression`typeof ${inp}`.expectToMatchJsResult(); +}); + +test.each(["{}", "[]"])("typeof object literal (%p)", inp => { + util.testExpression`typeof ${inp}`.expectToMatchJsResult(); +}); + +test("typeof class instance", () => { + util.testFunction` + class myClass {} + let inst = new myClass(); + return typeof inst; + `.expectToMatchJsResult(); +}); + +test("typeof function", () => { + util.testExpression`typeof (() => 3)`.expectToMatchJsResult(); +}); + +test.each(["null", "undefined"])("typeof undefined (%p)", inp => { + util.testExpression`typeof ${inp}`.expectToEqual("undefined"); +}); + +test.each([ + { expression: "{}", operator: "===", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "!==", compareTo: "object", expectResult: false }, + { expression: "{}", operator: "==", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "!=", compareTo: "object", expectResult: false }, + { expression: "{}", operator: "<=", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "<", compareTo: "object", expectResult: false }, + { expression: "undefined", operator: "===", compareTo: "undefined", expectResult: true }, + { expression: "() => {}", operator: "===", compareTo: "function", expectResult: true }, + { expression: "1", operator: "===", compareTo: "number", expectResult: true }, + { expression: "true", operator: "===", compareTo: "boolean", expectResult: true }, + { expression: `"foo"`, operator: "===", compareTo: "string", expectResult: true }, +])("typeof literal comparison (%p)", ({ expression, operator, compareTo, expectResult }) => { + const code = ` + let val = ${expression}; + return typeof val ${operator} "${compareTo}";`; + + expect(util.transpileString(code)).not.toMatch("__TS__TypeOf"); + expect(util.transpileAndExecute(code)).toBe(expectResult); +}); + +test.each([ + { expression: "{}", operator: "===", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "!==", compareTo: "object", expectResult: false }, + { expression: "{}", operator: "==", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "!=", compareTo: "object", expectResult: false }, + { expression: "{}", operator: "<=", compareTo: "object", expectResult: true }, + { expression: "{}", operator: "<", compareTo: "object", expectResult: false }, + { expression: "undefined", operator: "===", compareTo: "undefined", expectResult: true }, + { expression: "() => {}", operator: "===", compareTo: "function", expectResult: true }, + { expression: "1", operator: "===", compareTo: "number", expectResult: true }, + { expression: "true", operator: "===", compareTo: "boolean", expectResult: true }, + { expression: `"foo"`, operator: "===", compareTo: "string", expectResult: true }, +])("typeof non-literal comparison (%p)", ({ expression, operator, compareTo, expectResult }) => { + const code = ` + let val = ${expression}; + let compareTo = "${compareTo}"; + return typeof val ${operator} compareTo;`; + + expect(util.transpileString(code)).toMatch("__TS__TypeOf"); + expect(util.transpileAndExecute(code)).toBe(expectResult); +}); diff --git a/test/util.ts b/test/util.ts index f12f81d5a..42f53e58f 100644 --- a/test/util.ts +++ b/test/util.ts @@ -1,173 +1,20 @@ -import * as tsHelper from "../src/TSHelper"; import { lauxlib, lua, lualib, to_jsstring, to_luastring } from "fengari"; import * as fs from "fs"; import * as path from "path"; +import * as prettyFormat from "pretty-format"; import * as ts from "typescript"; +import * as vm from "vm"; import * as tstl from "../src"; -export const nodeStub = ts.createNode(ts.SyntaxKind.Unknown); - -export function transpileString( - str: string | { [filename: string]: string }, - options: tstl.CompilerOptions = {}, - ignoreDiagnostics = true -): string { - const { diagnostics, file } = transpileStringResult(str, options); - if (!expectToBeDefined(file) || !expectToBeDefined(file.lua)) return ""; - - const errors = diagnostics.filter(d => !ignoreDiagnostics || d.source === "typescript-to-lua"); - expect(errors).not.toHaveDiagnostics(); - - return file.lua.trim(); -} - -export function transpileStringsAsProject( - input: Record, - options: tstl.CompilerOptions = {} -): tstl.TranspileResult { - const optionsWithDefaults = { - luaTarget: tstl.LuaTarget.Lua53, - noHeader: true, - skipLibCheck: true, - target: ts.ScriptTarget.ESNext, - lib: ["lib.esnext.d.ts"], - experimentalDecorators: true, - ...options, - }; - - return tstl.transpileVirtualProject(input, optionsWithDefaults); -} - -export function transpileStringResult( - input: string | Record, - options: tstl.CompilerOptions = {} -): Required { - const { diagnostics, transpiledFiles } = transpileStringsAsProject( - typeof input === "string" ? { "main.ts": input } : input, - options - ); - - const file = transpiledFiles.find(({ fileName }) => /\bmain\.[a-z]+$/.test(fileName)); - if (file === undefined) { - throw new Error('Program should have a file named "main"'); - } - - return { diagnostics, file }; -} - -const lualibContent = fs.readFileSync(path.resolve(__dirname, "../dist/lualib/lualib_bundle.lua"), "utf8"); -const minimalTestLib = fs.readFileSync(path.join(__dirname, "json.lua"), "utf8") + "\n"; -export function executeLua(luaStr: string, withLib = true): any { - luaStr = luaStr.replace(/require\("lualib_bundle"\)/g, lualibContent); - if (withLib) { - luaStr = minimalTestLib + luaStr; - } - - const L = lauxlib.luaL_newstate(); - lualib.luaL_openlibs(L); - const status = lauxlib.luaL_dostring(L, to_luastring(luaStr)); - - if (status === lua.LUA_OK) { - // Read the return value from stack depending on its type. - if (lua.lua_isboolean(L, -1)) { - return lua.lua_toboolean(L, -1); - } else if (lua.lua_isnil(L, -1)) { - return undefined; - } else if (lua.lua_isnumber(L, -1)) { - return lua.lua_tonumber(L, -1); - } else if (lua.lua_isstring(L, -1)) { - return lua.lua_tojsstring(L, -1); - } else { - throw new Error("Unsupported lua return type: " + to_jsstring(lua.lua_typename(L, lua.lua_type(L, -1)))); - } - } else { - // If the lua VM did not terminate with status code LUA_OK an error occurred. - // Throw a JS error with the message, retrieved by reading a string from the stack. +export * from "./legacy-utils"; - // Filter control characters out of string which are in there because ???? - throw new Error("LUA ERROR: " + to_jsstring(lua.lua_tostring(L, -1).filter(c => c >= 20))); - } -} +export const nodeStub = ts.createNode(ts.SyntaxKind.Unknown); // Get a mock transformer to use for testing export function makeTestTransformer(luaTarget = tstl.LuaTarget.Lua53): tstl.LuaTransformer { return new tstl.LuaTransformer(ts.createProgram([], { luaTarget })); } -export function transpileAndExecute( - tsStr: string, - compilerOptions?: tstl.CompilerOptions, - luaHeader?: string, - tsHeader?: string -): any { - const wrappedTsString = `${tsHeader ? tsHeader : ""} - declare function JSONStringify(this: void, p: any): string; - function __runTest(this: void): any {${tsStr}}`; - - const lua = `${luaHeader ? luaHeader : ""} - ${transpileString(wrappedTsString, compilerOptions, false)} - return __runTest();`; - - return executeLua(lua); -} - -export function transpileAndExecuteProjectReturningMainExport( - typeScriptFiles: Record, - exportName: string, - options: tstl.CompilerOptions = {} -): [any, string] { - const mainFile = Object.keys(typeScriptFiles).find(typeScriptFileName => typeScriptFileName === "main.ts"); - if (!mainFile) { - throw new Error("An entry point file needs to be specified. This should be called main.ts"); - } - - const joinedTranspiledFiles = Object.keys(typeScriptFiles) - .filter(typeScriptFileName => typeScriptFileName !== "main.ts") - .map(typeScriptFileName => { - const modulePath = tsHelper.getExportPath(typeScriptFileName, options); - const tsCode = typeScriptFiles[typeScriptFileName]; - const luaCode = transpileString(tsCode, options); - return `package.preload["${modulePath}"] = function() - ${luaCode} - end`; - }) - .join("\n"); - - const luaCode = `return (function() - ${joinedTranspiledFiles} - ${transpileString(typeScriptFiles[mainFile])} - end)().${exportName}`; - - try { - return [executeLua(luaCode), luaCode]; - } catch (err) { - throw new Error(` - Encountered an error when executing the following Lua code: - - ${luaCode} - - ${err} - `); - } -} - -export function transpileExecuteAndReturnExport( - tsStr: string, - returnExport: string, - compilerOptions?: tstl.CompilerOptions, - luaHeader?: string -): any { - const wrappedTsString = `declare function JSONStringify(this: void, p: any): string; - ${tsStr}`; - - const lua = `return (function() - ${luaHeader ? luaHeader : ""} - ${transpileString(wrappedTsString, compilerOptions, false)} - end)().${returnExport}`; - - return executeLua(lua); -} - export function parseTypeScript( typescript: string, target: tstl.LuaTarget = tstl.LuaTarget.Lua53 @@ -203,8 +50,419 @@ export function expectToBeDefined(subject: T | null | undefined): subject is } export const valueToString = (value: unknown) => - value === Infinity || value === -Infinity || (typeof value === "number" && Number.isNaN(value)) + (typeof value === "number" && (!Number.isFinite(value) || Number.isNaN(value))) || typeof value === "function" ? String(value) : JSON.stringify(value); export const valuesToString = (values: unknown[]) => values.map(valueToString).join(", "); + +export function testEachVersion( + name: string | undefined, + common: () => T, + special: Record T) | false> +): void { + for (const version of Object.values(tstl.LuaTarget) as tstl.LuaTarget[]) { + const specialBuilder = special[version]; + if (specialBuilder === false) return; + + const testName = name === undefined ? version : `${name} [${version}]`; + test(testName, () => { + const builder = common(); + builder.setOptions({ luaTarget: version }); + specialBuilder(builder); + }); + } +} + +interface TranspiledJsFile { + fileName: string; + js?: string; + sourceMap?: string; +} + +interface TranspileJsResult { + diagnostics: ts.Diagnostic[]; + transpiledFiles: TranspiledJsFile[]; +} + +function transpileJs(program: ts.Program): TranspileJsResult { + const transpiledFiles: TranspiledJsFile[] = []; + const updateTranspiledFile = (fileName: string, update: Omit) => { + const file = transpiledFiles.find(f => f.fileName === fileName); + if (file) { + Object.assign(file, update); + } else { + transpiledFiles.push({ fileName, ...update }); + } + }; + + const { diagnostics } = program.emit(undefined, (fileName, data, _bom, _onError, sourceFiles = []) => { + for (const sourceFile of sourceFiles) { + const isJs = fileName.endsWith(".js"); + const isSourceMap = fileName.endsWith(".js.map"); + if (isJs || isSourceMap) { + updateTranspiledFile(sourceFile.fileName, { js: data }); + } else if (isSourceMap) { + updateTranspiledFile(sourceFile.fileName, { sourceMap: data }); + } + } + }); + + return { transpiledFiles, diagnostics: [...diagnostics] }; +} + +const memoize: MethodDecorator = (_target, _propertyKey, descriptor) => { + const originalFunction = descriptor.value as any; + const memoized = new WeakMap(); + descriptor.value = function(this: any, ...args: any[]): any { + if (!memoized.has(this)) { + memoized.set(this, originalFunction.apply(this, args)); + } + + return memoized.get(this); + } as any; + return descriptor; +}; + +export class ExecutionError extends Error { + public name = "ExecutionError"; + constructor(message: string) { + super(message); + } +} + +export type ExecutableTranspiledFile = tstl.TranspiledFile & { lua: string; sourceMap: string }; +export type TapCallback = (builder: TestBuilder) => void; +export abstract class TestBuilder { + constructor(protected _tsCode: string) {} + + // Options + + // TODO: Use testModule in these cases? + protected tsHeader = ""; + public setTsHeader(tsHeader: string): this { + expect(this.hasProgram).toBe(false); + this.tsHeader = tsHeader; + return this; + } + + private luaHeader = ""; + public setLuaHeader(luaHeader: string): this { + expect(this.hasProgram).toBe(false); + this.luaHeader += luaHeader; + return this; + } + + protected jsHeader = ""; + public setJsHeader(jsHeader: string): this { + expect(this.hasProgram).toBe(false); + this.jsHeader += jsHeader; + return this; + } + + private semanticCheck = true; + public disableSemanticCheck(): this { + expect(this.hasProgram).toBe(false); + this.semanticCheck = false; + return this; + } + + private options: tstl.CompilerOptions = { + luaTarget: tstl.LuaTarget.Lua53, + noHeader: true, + skipLibCheck: true, + target: ts.ScriptTarget.ES2017, + lib: ["lib.esnext.d.ts"], + experimentalDecorators: true, + }; + public setOptions(options: tstl.CompilerOptions = {}): this { + expect(this.hasProgram).toBe(false); + Object.assign(this.options, options); + return this; + } + + protected mainFileName = "main.ts"; + public setMainFileName(mainFileName: string): this { + expect(this.hasProgram).toBe(false); + this.mainFileName = mainFileName; + return this; + } + + private extraFiles: Record = {}; + public addExtraFile(fileName: string, code: string): this { + expect(this.hasProgram).toBe(false); + this.extraFiles[fileName] = code; + return this; + } + + // Transpilation and execution + + public getTsCode(): string { + return `${this.tsHeader}${this._tsCode}`; + } + + protected hasProgram = false; + @memoize + public getProgram(): ts.Program { + this.hasProgram = true; + return tstl.createVirtualProgram({ ...this.extraFiles, [this.mainFileName]: this.getTsCode() }, this.options); + } + + @memoize + public getLuaResult(): tstl.TranspileResult { + const program = this.getProgram(); + const result = tstl.transpile({ program }); + const diagnostics = ts.sortAndDeduplicateDiagnostics([ + ...ts.getPreEmitDiagnostics(program), + ...result.diagnostics, + ]); + + return { ...result, diagnostics: [...diagnostics] }; + } + + @memoize + public getMainLuaFileResult(): ExecutableTranspiledFile { + const { transpiledFiles } = this.getLuaResult(); + const mainFile = transpiledFiles.find(x => x.fileName === this.mainFileName); + expect(mainFile).toMatchObject({ lua: expect.any(String), sourceMap: expect.any(String) }); + return mainFile as ExecutableTranspiledFile; + } + + @memoize + public getMainLuaCodeChunk(): string { + const header = this.luaHeader ? `${this.luaHeader.trimRight()}\n` : ""; + return header + this.getMainLuaFileResult().lua.trimRight(); + } + + public abstract getLuaCodeWithWrapper(): string; + + @memoize + public getLuaExecutionResult(): any { + const code = this.getLuaCodeWithWrapper(); + const L = lauxlib.luaL_newstate(); + lualib.luaL_openlibs(L); + const status = lauxlib.luaL_dostring(L, to_luastring(code)); + + if (status === lua.LUA_OK) { + if (lua.lua_isstring(L, -1)) { + const result = eval(`(${lua.lua_tojsstring(L, -1)})`); + return result === null ? undefined : result; + } else { + const returnType = to_jsstring(lua.lua_typename(L, lua.lua_type(L, -1))); + throw new Error(`Unsupported Lua return type: ${returnType}`); + } + } else { + const message = to_jsstring(lua.lua_tostring(L, -1)).replace(/^\[string "--\.\.\."\]:\d+: /, ""); + return new ExecutionError(message); + } + } + + @memoize + public getJsResult(): TranspileJsResult { + const program = this.getProgram(); + program.getCompilerOptions().module = ts.ModuleKind.CommonJS; + return transpileJs(program); + } + + @memoize + protected getMainJsCodeChunk(): string { + const { transpiledFiles } = this.getJsResult(); + const mainFile = transpiledFiles.find(x => x.fileName === this.mainFileName); + expect(mainFile).toBeDefined(); + + const header = this.jsHeader ? `${this.jsHeader.trimRight()}\n` : ""; + return header + mainFile!.js!; + } + + protected abstract getJsCodeWithWrapper(): string; + + @memoize + public getJsExecutionResult(): any { + const exports = {}; + const context = vm.createContext({ exports, module: { exports } }); + context.global = context; + let result: unknown; + try { + result = vm.runInContext(this.getJsCodeWithWrapper(), context); + } catch (error) { + return new ExecutionError(error.message); + } + + function transform(currentValue: any): any { + if (currentValue === null) { + return undefined; + } + + if (Array.isArray(currentValue)) { + return currentValue.map(transform); + } + + if (typeof currentValue === "object") { + for (const [key, value] of Object.entries(currentValue)) { + currentValue[key] = transform(value); + if (currentValue[key] === undefined) { + delete currentValue[key]; + } + } + + if (Object.keys(currentValue).length === 0) { + return []; + } + } + + return currentValue; + } + + return transform(result); + } + + // Utilities + + private getLuaDiagnostics(): ts.Diagnostic[] { + const { diagnostics } = this.getLuaResult(); + return diagnostics.filter(d => this.semanticCheck || d.source === "typescript-to-lua"); + } + + // Actions + + public debug(): this { + const luaCode = this.getMainLuaCodeChunk().replace(/(^|\n)/g, "\n "); + const value = prettyFormat(this.getLuaExecutionResult()); + console.log(`Lua Code:${luaCode}\nValue: ${value}`); + return this; + } + + public expectToHaveDiagnostics(): this { + expect(this.getLuaDiagnostics()).toHaveDiagnostics(); + return this; + } + + public expectToHaveDiagnosticOfError(error: tstl.TranspileError): this { + this.expectToHaveDiagnostics(); + expect(this.getLuaDiagnostics()).toHaveLength(1); + const firstDiagnostic = this.getLuaDiagnostics()[0]; + expect(firstDiagnostic).toMatchObject({ messageText: error.message }); + return this; + } + + public expectToHaveNoDiagnostics(): this { + expect(this.getLuaDiagnostics()).not.toHaveDiagnostics(); + return this; + } + + public expectNoExecutionError(): this { + const luaResult = this.getLuaExecutionResult(); + if (luaResult instanceof ExecutionError) { + throw luaResult; + } + + return this; + } + + public expectToMatchJsResult(allowErrors = false): this { + this.expectToHaveNoDiagnostics(); + if (!allowErrors) this.expectNoExecutionError(); + + const luaResult = this.getLuaExecutionResult(); + const jsResult = this.getJsExecutionResult(); + expect(luaResult).toEqual(jsResult); + + return this; + } + + public expectToEqual(expected: any): this { + this.expectToHaveNoDiagnostics(); + const luaResult = this.getLuaExecutionResult(); + expect(luaResult).toEqual(expected); + return this; + } + + public expectLuaToMatchSnapshot(): this { + this.expectToHaveNoDiagnostics(); + expect(this.getMainLuaCodeChunk()).toMatchSnapshot(); + return this; + } + + public expectResultToMatchSnapshot(): this { + this.expectToHaveNoDiagnostics(); + expect(this.getLuaExecutionResult()).toMatchSnapshot(); + return this; + } + + public tap(callback: TapCallback): this { + callback(this); + return this; + } +} + +const lualibContent = fs.readFileSync(path.resolve(__dirname, "../dist/lualib/lualib_bundle.lua"), "utf8"); +const minimalTestLib = fs.readFileSync(path.join(__dirname, "json.lua"), "utf8") + "\n"; +class AccessorTestBuilder extends TestBuilder { + protected accessor = ""; + + @memoize + public getLuaCodeWithWrapper(): string { + let code = this.getMainLuaCodeChunk(); + if (code.includes('require("lualib_bundle")')) { + code = `package.preload.lualib_bundle = function()\n${lualibContent}\nend\n${code}`; + } + + return `${minimalTestLib}\nreturn JSONStringify((function()\n${code}\nend)()${this.accessor})`; + } + + @memoize + protected getJsCodeWithWrapper(): string { + return this.getMainJsCodeChunk() + `\n;module.exports = module.exports${this.accessor}`; + } +} + +class ModuleTestBuilder extends AccessorTestBuilder { + public setReturnExport(name: string): this { + expect(this.hasProgram).toBe(false); + this.accessor = `.${name}`; + return this; + } +} + +class FunctionTestBuilder extends AccessorTestBuilder { + protected accessor = ".__main()"; + public getTsCode(): string { + return `${this.tsHeader}export function __main() {${this._tsCode}}`; + } +} + +class ExpressionTestBuilder extends AccessorTestBuilder { + protected accessor = ".__result"; + public getTsCode(): string { + return `${this.tsHeader}export const __result = ${this._tsCode};`; + } +} + +const createTestBuilderFactory = ( + builder: new (_tsCode: string) => T, + serializeSubstitutions: boolean +) => (...args: [string] | [TemplateStringsArray, ...any[]]): T => { + let tsCode: string; + if (typeof args[0] === "string") { + expect(serializeSubstitutions).toBe(false); + tsCode = args[0]; + } else { + let [template, ...substitutions] = args; + if (serializeSubstitutions) { + substitutions = substitutions.map(valueToString); + } + + tsCode = template + .map((chunk, index) => (substitutions[index - 1] !== undefined ? substitutions[index - 1] : "") + chunk) + .join(""); + } + + return new builder(tsCode); +}; + +export const testModule = createTestBuilderFactory(ModuleTestBuilder, false); +export const testModuleTemplate = createTestBuilderFactory(ModuleTestBuilder, true); +export const testFunction = createTestBuilderFactory(FunctionTestBuilder, false); +export const testFunctionTemplate = createTestBuilderFactory(FunctionTestBuilder, true); +export const testExpression = createTestBuilderFactory(ExpressionTestBuilder, false); +export const testExpressionTemplate = createTestBuilderFactory(ExpressionTestBuilder, true); diff --git a/tslint.json b/tslint.json index 2bf168c59..be6ae2b58 100644 --- a/tslint.json +++ b/tslint.json @@ -22,7 +22,6 @@ "interface-name": [true, "never-prefix"], "jsdoc-format": true, "label-position": true, - "max-classes-per-file": [true, 1], "member-access": true, "no-angle-bracket-type-assertion": true, "no-any": false,