diff --git a/src/Decorator.ts b/src/Decorator.ts index b8a4f3ad3..d9c7a6d87 100644 --- a/src/Decorator.ts +++ b/src/Decorator.ts @@ -29,6 +29,8 @@ export class Decorator { return DecoratorKind.NoSelf; case "noselfinfile": return DecoratorKind.NoSelfInFile; + case "vararg": + return DecoratorKind.VarArg; } return undefined; @@ -61,4 +63,5 @@ export enum DecoratorKind { LuaTable = "LuaTable", NoSelf = "NoSelf", NoSelfInFile = "NoSelfInFile", + VarArg = "VarArg", } diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index 0da4e9b84..a93c26c97 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -25,14 +25,14 @@ interface SymbolInfo { } interface FunctionDefinitionInfo { - referencedSymbols: Set; + referencedSymbols: Map; definition?: tstl.VariableDeclarationStatement | tstl.AssignmentStatement; } interface Scope { type: ScopeType; id: number; - referencedSymbols?: Set; + referencedSymbols?: Map; variableDeclarations?: tstl.VariableDeclarationStatement[]; functionDefinitions?: Map; importStatements?: tstl.Statement[]; @@ -1388,12 +1388,31 @@ export class LuaTransformer { return [paramNames, dotsLiteral, restParamName]; } + protected isRestParameterReferenced(identifier: tstl.Identifier, scope: Scope): boolean { + if (!identifier.symbolId) { + return true; + } + if (scope.referencedSymbols === undefined) { + return false; + } + const references = scope.referencedSymbols.get(identifier.symbolId); + if (!references) { + return false; + } + // Ignore references to @varArg types in spread elements + return references.some( + r => !r.parent || !ts.isSpreadElement(r.parent) || !tsHelper.isVarArgType(r, this.checker) + ); + } + protected transformFunctionBody( parameters: ts.NodeArray, body: ts.Block, spreadIdentifier?: tstl.Identifier ): [tstl.Statement[], Scope] { this.pushScope(ScopeType.Function); + const bodyStatements = this.performHoisting(this.transformStatements(body.statements)); + const scope = this.popScope(); const headerStatements = []; @@ -1426,7 +1445,7 @@ export class LuaTransformer { } // Push spread operator here - if (spreadIdentifier) { + if (spreadIdentifier && this.isRestParameterReferenced(spreadIdentifier, scope)) { const spreadTable = this.wrapInTable(tstl.createDotsLiteral()); headerStatements.push(tstl.createVariableDeclarationStatement(spreadIdentifier, spreadTable)); } @@ -1434,10 +1453,6 @@ export class LuaTransformer { // Binding pattern statements need to be after spread table is declared headerStatements.push(...bindingPatternDeclarations); - const bodyStatements = this.performHoisting(this.transformStatements(body.statements)); - - const scope = this.popScope(); - return [headerStatements.concat(bodyStatements), scope]; } @@ -1844,7 +1859,7 @@ export class LuaTransformer { if (!scope.functionDefinitions) { scope.functionDefinitions = new Map(); } - const functionInfo = { referencedSymbols: functionScope.referencedSymbols || new Set() }; + const functionInfo = { referencedSymbols: functionScope.referencedSymbols || new Map() }; scope.functionDefinitions.set(name.symbolId, functionInfo); } return this.createLocalOrExportedOrGlobalDeclaration(name, functionExpression, functionDeclaration); @@ -4543,6 +4558,10 @@ export class LuaTransformer { return innerExpression; } + if (ts.isIdentifier(expression.expression) && tsHelper.isVarArgType(expression.expression, this.checker)) { + return tstl.createDotsLiteral(expression); + } + const type = this.checker.getTypeAtLocation(expression.expression); if (tsHelper.isArrayType(type, this.checker, this.program)) { return this.createUnpackCall(innerExpression, expression); @@ -5208,13 +5227,20 @@ export class LuaTransformer { if (declaration && identifier.pos < declaration.pos) { throw TSTLErrors.ReferencedBeforeDeclaration(identifier); } - } else if (symbolId !== undefined) { + } + + if (symbolId !== undefined) { //Mark symbol as seen in all current scopes for (const scope of this.scopeStack) { if (!scope.referencedSymbols) { - scope.referencedSymbols = new Set(); + scope.referencedSymbols = new Map(); + } + let references = scope.referencedSymbols.get(symbolId); + if (!references) { + references = []; + scope.referencedSymbols.set(symbolId, references); } - scope.referencedSymbols.add(symbolId); + references.push(identifier); } } } diff --git a/src/TSHelper.ts b/src/TSHelper.ts index 8d701512b..90c4cbcf6 100644 --- a/src/TSHelper.ts +++ b/src/TSHelper.ts @@ -184,6 +184,23 @@ export class TSHelper { return TSHelper.getCustomDecorators(type, checker).has(DecoratorKind.LuaIterator); } + public static isRestParameter(node: ts.Node, checker: ts.TypeChecker): boolean { + const symbol = checker.getSymbolAtLocation(node); + if (!symbol) { + return false; + } + const declarations = symbol.getDeclarations(); + if (!declarations) { + return false; + } + return declarations.some(d => ts.isParameter(d) && d.dotDotDotToken !== undefined); + } + + public static isVarArgType(node: ts.Node, checker: ts.TypeChecker): boolean { + const type = checker.getTypeAtLocation(node); + return type !== undefined && TSHelper.getCustomDecorators(type, checker).has(DecoratorKind.VarArg); + } + public static isTupleReturnCall(node: ts.Node, checker: ts.TypeChecker): boolean { if (ts.isCallExpression(node)) { const signature = checker.getResolvedSignature(node); diff --git a/src/TSTLErrors.ts b/src/TSTLErrors.ts index 26d08d1ca..f0f492e77 100644 --- a/src/TSTLErrors.ts +++ b/src/TSTLErrors.ts @@ -204,4 +204,8 @@ export class TSTLErrors { node ); }; + + public static InvalidElipsisForward = (node: ts.Node, message: string) => { + return new TranspileError(`Invalid use of @elipsisForward: ${message}`, node); + }; } diff --git a/test/translation/__snapshots__/transformation.spec.ts.snap b/test/translation/__snapshots__/transformation.spec.ts.snap index 448f18106..58e99b895 100644 --- a/test/translation/__snapshots__/transformation.spec.ts.snap +++ b/test/translation/__snapshots__/transformation.spec.ts.snap @@ -256,6 +256,7 @@ end" exports[`Transformation (functionRestArguments) 1`] = ` "function varargsFunction(self, a, ...) local b = ({...}) + local c = b end" `; @@ -319,7 +320,6 @@ end function MyClass.prototype.____constructor(self) end function MyClass.prototype.varargsFunction(self, a, ...) - local b = ({...}) end" `; diff --git a/test/translation/transformation/functionRestArguments.ts b/test/translation/transformation/functionRestArguments.ts index 3b015a4d6..63649f09b 100644 --- a/test/translation/transformation/functionRestArguments.ts +++ b/test/translation/transformation/functionRestArguments.ts @@ -1 +1,3 @@ -function varargsFunction(a: string, ...b: string[]): void {} +function varargsFunction(a: string, ...b: string[]): void { + const c = b; +} diff --git a/test/unit/functions.spec.ts b/test/unit/functions.spec.ts index cbcd4ad87..0201dd66a 100644 --- a/test/unit/functions.spec.ts +++ b/test/unit/functions.spec.ts @@ -504,3 +504,109 @@ test("Function rest binding pattern", () => { 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 }])("@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"); +});