diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index e1d48fc46..a03700519 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -3028,7 +3028,18 @@ export class LuaTransformer { } } - protected transformAssignment(lhs: ts.Expression, right?: tstl.Expression): tstl.Statement { + protected transformAssignment(lhs: ts.Expression, right: tstl.Expression, parent?: ts.Expression): tstl.Statement { + if (tsHelper.isArrayLength(lhs, this.checker, this.program)) { + return tstl.createExpressionStatement( + this.transformLuaLibFunction( + LuaLibFeature.ArraySetLength, + parent, + this.transformExpression(lhs.expression), + right + ) + ); + } + return tstl.createAssignmentStatement( this.transformExpression(lhs) as tstl.AssignmentLeftHandSideExpression, right, @@ -3043,44 +3054,283 @@ export class LuaTransformer { this.validateFunctionAssignment(expression.right, rightType, leftType); this.validatePropertyAssignment(expression); - if (tsHelper.isArrayLengthAssignment(expression, this.checker, this.program)) { - // array.length = x - return tstl.createExpressionStatement( - this.transformLuaLibFunction( - LuaLibFeature.ArraySetLength, - expression, - this.transformExpression(expression.left.expression), - this.transformExpression(expression.right) - ) + if (tsHelper.isAssignmentPattern(expression.left)) { + // Destructuring assignment + const flattenable = tsHelper.isValidFlattenableDestructuringAssignmentLeftHandSide( + expression as ts.DestructuringAssignment, + this.checker, + this.program ); - } - if (ts.isArrayLiteralExpression(expression.left)) { - // Destructuring assignment - const left = - expression.left.elements.length > 0 - ? expression.left.elements.map(e => this.transformArrayBindingElement(e)) - : [tstl.createAnonymousIdentifier(expression.left)]; - let right: tstl.Expression[]; - if (ts.isArrayLiteralExpression(expression.right)) { - if (expression.right.elements.length > 0) { - const visitResults = expression.right.elements.map(e => this.transformExpression(e)); - right = this.filterUndefined(visitResults); - } else { - right = [tstl.createNilLiteral()]; + if (flattenable) { + const expressionType = this.checker.getTypeAtLocation(expression.right); + let right = this.transformExpression(expression.right); + + if ( + !tsHelper.isTupleReturnCall(expression.right, this.checker) && + tsHelper.isArrayType(expressionType, this.checker, this.program) + ) { + right = this.createUnpackCall(right, expression.right); } - } else if (tsHelper.isTupleReturnCall(expression.right, this.checker)) { - right = [this.transformExpression(expression.right)]; - } else { - right = [this.createUnpackCall(this.transformExpression(expression.right), expression.right)]; + + return this.transformFlattenableDestructuringAssignment( + expression as ts.DestructuringAssignment, + right + ); + } + + let right = this.transformExpression(expression.right); + const rootIdentifier = tstl.createAnonymousIdentifier(expression.left); + + if (tsHelper.isTupleReturnCall(expression.right, this.checker)) { + right = this.wrapInTable(right); } - return tstl.createAssignmentStatement(left as tstl.AssignmentLeftHandSideExpression[], right, expression); + + const rootDeclaration = tstl.createVariableDeclarationStatement(rootIdentifier, right); + + const statements = this.transformDestructuringAssignment( + expression as ts.DestructuringAssignment, + rootIdentifier + ); + statements.unshift(rootDeclaration); + + return statements; } else { // Simple assignment return this.transformAssignment(expression.left, this.transformExpression(expression.right)); } } + protected transformFlattenableDestructuringAssignment( + node: ts.DestructuringAssignment, + right: tstl.Expression | tstl.Expression[] + ): tstl.Statement { + if (ts.isArrayLiteralExpression(node.left)) { + const left: tstl.AssignmentLeftHandSideExpression[] = node.left.elements.map( + element => this.transformExpression(element) as tstl.AssignmentLeftHandSideExpression + ); + return tstl.createAssignmentStatement(left, right, node); + } + + throw TSTLErrors.NonFlattenableDestructure(node); + } + + protected transformDestructuringAssignment( + node: ts.DestructuringAssignment, + root: tstl.Expression + ): tstl.Statement[] { + switch (node.left.kind) { + case ts.SyntaxKind.ObjectLiteralExpression: + return this.transformObjectDestructuringAssignment(node as ts.ObjectDestructuringAssignment, root); + case ts.SyntaxKind.ArrayLiteralExpression: + return this.transformArrayDestructuringAssignment(node as ts.ArrayDestructuringAssignment, root); + } + } + + protected transformObjectDestructuringAssignment( + node: ts.ObjectDestructuringAssignment, + root: tstl.Expression + ): tstl.Statement[] { + return this.transformObjectLiteralAssignmentPattern(node.left, root); + } + + protected transformArrayDestructuringAssignment( + node: ts.ArrayDestructuringAssignment, + root: tstl.Expression + ): tstl.Statement[] { + return this.transformArrayLiteralAssignmentPattern(node.left, root); + } + + protected transformShorthandPropertyAssignment( + node: ts.ShorthandPropertyAssignment, + root: tstl.Expression + ): tstl.Statement[] { + const result: tstl.Statement[] = []; + const assignmentVariableName = this.transformIdentifier(node.name); + const extractionIndex = tstl.createStringLiteral(node.name.text); + const variableExtractionAssignmentStatement = tstl.createAssignmentStatement( + assignmentVariableName, + tstl.createTableIndexExpression(root, extractionIndex) + ); + + result.push(variableExtractionAssignmentStatement); + + const defaultInitializer = node.objectAssignmentInitializer + ? this.transformExpression(node.objectAssignmentInitializer) + : undefined; + + if (defaultInitializer) { + const nilCondition = tstl.createBinaryExpression( + assignmentVariableName, + tstl.createNilLiteral(), + tstl.SyntaxKind.EqualityOperator + ); + + const assignment = tstl.createAssignmentStatement(assignmentVariableName, defaultInitializer); + + const ifBlock = tstl.createBlock([assignment]); + + result.push(tstl.createIfStatement(nilCondition, ifBlock, undefined, node)); + } + + return result; + } + + protected transformObjectLiteralAssignmentPattern( + node: ts.ObjectLiteralExpression, + root: tstl.Expression + ): tstl.Statement[] { + const result: tstl.Statement[] = []; + + for (const property of node.properties) { + switch (property.kind) { + case ts.SyntaxKind.ShorthandPropertyAssignment: + result.push(...this.transformShorthandPropertyAssignment(property, root)); + break; + case ts.SyntaxKind.PropertyAssignment: + result.push(...this.transformPropertyAssignment(property, root)); + break; + case ts.SyntaxKind.SpreadAssignment: + throw TSTLErrors.ForbiddenEllipsisDestruction(property); + default: + throw TSTLErrors.UnsupportedKind("Object Destructure Property", property.kind, property); + } + } + + return result; + } + + protected transformArrayLiteralAssignmentPattern( + node: ts.ArrayLiteralExpression, + root: tstl.Expression + ): tstl.Statement[] { + const result: tstl.Statement[] = []; + + node.elements.forEach((element, index) => { + const indexedRoot = tstl.createTableIndexExpression( + root as tstl.Expression, + tstl.createNumericLiteral(index + 1), + element + ); + + switch (element.kind) { + case ts.SyntaxKind.ObjectLiteralExpression: + result.push( + ...this.transformObjectLiteralAssignmentPattern( + element as ts.ObjectLiteralExpression, + indexedRoot + ) + ); + break; + case ts.SyntaxKind.ArrayLiteralExpression: + result.push( + ...this.transformArrayLiteralAssignmentPattern( + element as ts.ArrayLiteralExpression, + indexedRoot + ) + ); + break; + case ts.SyntaxKind.BinaryExpression: + const assignedVariable = tstl.createIdentifier("____bindingAssignmentValue"); + + const assignedVariableDeclaration = tstl.createVariableDeclarationStatement( + assignedVariable, + indexedRoot + ); + + const nilCondition = tstl.createBinaryExpression( + assignedVariable, + tstl.createNilLiteral(), + tstl.SyntaxKind.EqualityOperator + ); + + const defaultAssignmentStatement = this.transformAssignment( + (element as ts.BinaryExpression).left, + this.transformExpression((element as ts.BinaryExpression).right) + ); + + const elseAssignmentStatement = this.transformAssignment( + (element as ts.BinaryExpression).left, + assignedVariable + ); + + const ifBlock = tstl.createBlock([defaultAssignmentStatement]); + + const elseBlock = tstl.createBlock([elseAssignmentStatement]); + + const ifStatement = tstl.createIfStatement(nilCondition, ifBlock, elseBlock, node); + + result.push(assignedVariableDeclaration); + result.push(ifStatement); + break; + case ts.SyntaxKind.Identifier: + case ts.SyntaxKind.PropertyAccessExpression: + case ts.SyntaxKind.ElementAccessExpression: + const assignmentStatement = this.transformAssignment(element, indexedRoot); + + result.push(assignmentStatement); + break; + case ts.SyntaxKind.OmittedExpression: + break; + default: + throw TSTLErrors.UnsupportedKind("Array Destructure Assignment Element", element.kind, element); + } + }); + + return result; + } + + protected transformPropertyAssignment(node: ts.PropertyAssignment, root: tstl.Expression): tstl.Statement[] { + const result: tstl.Statement[] = []; + + if (tsHelper.isAssignmentPattern(node.initializer)) { + const propertyAccessString = this.transformPropertyName(node.name); + const newRootAccess = tstl.createTableIndexExpression(root, propertyAccessString); + + if (ts.isObjectLiteralExpression(node.initializer)) { + return this.transformObjectLiteralAssignmentPattern(node.initializer, newRootAccess); + } + + if (ts.isArrayLiteralExpression(node.initializer)) { + return this.transformArrayLiteralAssignmentPattern(node.initializer, newRootAccess); + } + } + + let leftExpression: ts.Expression; + if (ts.isBinaryExpression(node.initializer)) { + leftExpression = node.initializer.left; + } else { + leftExpression = node.initializer; + } + + const variableToExtract = this.transformPropertyName(node.name); + const extractingExpression = tstl.createTableIndexExpression(root, variableToExtract); + + const destructureAssignmentStatement = this.transformAssignment(leftExpression, extractingExpression); + + result.push(destructureAssignmentStatement); + + if (ts.isBinaryExpression(node.initializer)) { + const assignmentLeftHandSide = this.transformExpression(node.initializer.left); + + const nilCondition = tstl.createBinaryExpression( + assignmentLeftHandSide, + tstl.createNilLiteral(), + tstl.SyntaxKind.EqualityOperator + ); + + const assignmentStatements = this.statementVisitResultToArray( + this.transformAssignmentStatement(node.initializer) + ); + + const ifBlock = tstl.createBlock(assignmentStatements); + + result.push(tstl.createIfStatement(nilCondition, ifBlock, undefined, node)); + } + + return result; + } + protected transformAssignmentExpression( expression: ts.BinaryExpression ): tstl.CallExpression | tstl.MethodCallExpression { @@ -3089,7 +3339,7 @@ export class LuaTransformer { const leftType = this.checker.getTypeAtLocation(expression.left); this.validateFunctionAssignment(expression.right, rightType, leftType); - if (tsHelper.isArrayLengthAssignment(expression, this.checker, this.program)) { + if (tsHelper.isArrayLength(expression.left, this.checker, this.program)) { // array.length = x return this.transformLuaLibFunction( LuaLibFeature.ArraySetLength, @@ -3099,34 +3349,24 @@ export class LuaTransformer { ); } - if (ts.isArrayLiteralExpression(expression.left)) { + if (tsHelper.isAssignmentPattern(expression.left)) { // Destructuring assignment - // (function() local ${tmps} = ${right}; ${left} = ${tmps}; return {${tmps}} end)() - const left = - expression.left.elements.length > 0 - ? expression.left.elements.map(e => this.transformExpression(e)) - : [tstl.createAnonymousIdentifier(expression.left)]; - let right: tstl.Expression[]; - if (ts.isArrayLiteralExpression(expression.right)) { - right = - expression.right.elements.length > 0 - ? expression.right.elements.map(e => this.transformExpression(e)) - : [tstl.createNilLiteral()]; - } else if (tsHelper.isTupleReturnCall(expression.right, this.checker)) { - right = [this.transformExpression(expression.right)]; - } else { - right = [this.createUnpackCall(this.transformExpression(expression.right), expression.right)]; + const rootIdentifier = tstl.createAnonymousIdentifier(expression.left); + + let right = this.transformExpression(expression.right); + if (tsHelper.isTupleReturnCall(expression.right, this.checker)) { + right = this.wrapInTable(right); } - const tmps = left.map((_, i) => tstl.createIdentifier(`____tmp${i}`)); - const statements: tstl.Statement[] = [ - tstl.createVariableDeclarationStatement(tmps, right), - tstl.createAssignmentStatement(left as tstl.AssignmentLeftHandSideExpression[], tmps), - ]; - return this.createImmediatelyInvokedFunctionExpression( - statements, - tstl.createTableExpression(tmps.map(t => tstl.createTableFieldExpression(t))), - expression + + const rootDeclaration = tstl.createVariableDeclarationStatement(rootIdentifier, right); + + const statements = this.transformDestructuringAssignment( + expression as ts.DestructuringAssignment, + rootIdentifier ); + statements.unshift(rootDeclaration); + + return this.createImmediatelyInvokedFunctionExpression(statements, rootIdentifier, expression); } if (ts.isPropertyAccessExpression(expression.left) || ts.isElementAccessExpression(expression.left)) { diff --git a/src/TSHelper.ts b/src/TSHelper.ts index 53a594dc3..a95b14d01 100644 --- a/src/TSHelper.ts +++ b/src/TSHelper.ts @@ -53,6 +53,10 @@ export function getExtendedType(node: ts.ClassLikeDeclarationBase, checker: ts.T return extendedTypeNode && checker.getTypeAtLocation(extendedTypeNode); } +export function isAssignmentPattern(node: ts.Node): node is ts.AssignmentPattern { + return ts.isObjectLiteralExpression(node) || ts.isArrayLiteralExpression(node); +} + export function isFileModule(sourceFile: ts.SourceFile): boolean { return sourceFile.statements.some(isStatementExported); } @@ -865,27 +869,51 @@ export function moduleHasEmittedBody( return false; } -export function isArrayLengthAssignment( - expression: ts.BinaryExpression, +export function isValidFlattenableDestructuringAssignmentLeftHandSide( + node: ts.DestructuringAssignment, checker: ts.TypeChecker, program: ts.Program -): expression is ts.BinaryExpression & { left: ts.PropertyAccessExpression | ts.ElementAccessExpression } { - if (expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken) { - return false; +): boolean { + if (ts.isArrayLiteralExpression(node.left)) { + if (node.left.elements.length > 0) { + return !node.left.elements.some(element => { + switch (element.kind) { + case ts.SyntaxKind.Identifier: + case ts.SyntaxKind.PropertyAccessExpression: + if (isArrayLength(element, checker, program)) { + return true; + } + case ts.SyntaxKind.ElementAccessExpression: + // Can be on the left hand side of a Lua assignment statement + return false; + default: + // Cannot be + return true; + } + }); + } } - if (!ts.isPropertyAccessExpression(expression.left) && !ts.isElementAccessExpression(expression.left)) { + return false; +} + +export function isArrayLength( + expression: ts.Expression, + checker: ts.TypeChecker, + program: ts.Program +): expression is ts.PropertyAccessExpression | ts.ElementAccessExpression { + if (!ts.isPropertyAccessExpression(expression) && !ts.isElementAccessExpression(expression)) { return false; } - const type = checker.getTypeAtLocation(expression.left.expression); + const type = checker.getTypeAtLocation(expression.expression); if (!isArrayType(type, checker, program)) { return false; } - const name = ts.isPropertyAccessExpression(expression.left) - ? (expression.left.name.escapedText as string) - : ts.isStringLiteral(expression.left.argumentExpression) && expression.left.argumentExpression.text; + const name = ts.isPropertyAccessExpression(expression) + ? (expression.name.escapedText as string) + : ts.isStringLiteral(expression.argumentExpression) && expression.argumentExpression.text; return name === "length"; } diff --git a/src/TSTLErrors.ts b/src/TSTLErrors.ts index cc351f2b7..054c89425 100644 --- a/src/TSTLErrors.ts +++ b/src/TSTLErrors.ts @@ -90,6 +90,9 @@ export const MissingFunctionName = (declaration: ts.FunctionLikeDeclaration) => export const MissingMetaExtension = (node: ts.Node) => new TranspileError(`@metaExtension requires the extension of the metatable class.`, node); +export const NonFlattenableDestructure = (node: ts.Node) => + new TranspileError(`This node cannot be destructured using a standard Lua assignment statement.`, node); + export const UndefinedFunctionDefinition = (functionSymbolId: number) => new Error(`Function definition for function symbol ${functionSymbolId} is undefined.`); diff --git a/test/unit/bindingpatterns.spec.ts b/test/unit/bindingpatterns.spec.ts index aad38661b..39684ae50 100644 --- a/test/unit/bindingpatterns.spec.ts +++ b/test/unit/bindingpatterns.spec.ts @@ -18,6 +18,8 @@ const testCases = [ 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([ @@ -82,3 +84,108 @@ test.each([ ); 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); +});