diff --git a/src/LuaAST.ts b/src/LuaAST.ts index ee6c8002b..d88dbef09 100644 --- a/src/LuaAST.ts +++ b/src/LuaAST.ts @@ -887,6 +887,9 @@ export function createTableIndexExpression( } export type AssignmentLeftHandSideExpression = Identifier | TableIndexExpression; +export function isAssignmentLeftHandSideExpression(node: Node): node is AssignmentLeftHandSideExpression { + return isIdentifier(node) || isTableIndexExpression(node); +} export type FunctionDefinition = (VariableDeclarationStatement | AssignmentStatement) & { right: [FunctionExpression]; diff --git a/src/LuaLib.ts b/src/LuaLib.ts index 25cd83268..050e1eaf3 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -41,6 +41,7 @@ export enum LuaLibFeature { ObjectEntries = "ObjectEntries", ObjectFromEntries = "ObjectFromEntries", ObjectKeys = "ObjectKeys", + ObjectRest = "ObjectRest", ObjectValues = "ObjectValues", Set = "Set", WeakMap = "WeakMap", diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index a8a5cdd35..c14252578 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -1569,60 +1569,90 @@ export class LuaTransformer { const isObjectBindingPattern = ts.isObjectBindingPattern(pattern); for (let index = 0; index < pattern.elements.length; index++) { const element = pattern.elements[index]; - if (ts.isBindingElement(element)) { - if (ts.isArrayBindingPattern(element.name) || ts.isObjectBindingPattern(element.name)) { - // nested binding pattern - const propertyName = isObjectBindingPattern - ? element.propertyName - : ts.createNumericLiteral(String(index + 1)); - if (propertyName !== undefined) { - propertyAccessStack.push(propertyName); - } - result.push( - ...this.statementVisitResultToArray( - this.transformBindingPattern(element.name, table, propertyAccessStack) + if (ts.isOmittedExpression(element)) continue; + + if (ts.isArrayBindingPattern(element.name) || ts.isObjectBindingPattern(element.name)) { + // nested binding pattern + const propertyName = isObjectBindingPattern + ? element.propertyName + : ts.createNumericLiteral(String(index + 1)); + if (propertyName !== undefined) { + propertyAccessStack.push(propertyName); + } + result.push( + ...this.statementVisitResultToArray( + this.transformBindingPattern(element.name, table, propertyAccessStack) + ) + ); + continue; + } + + // Build the path to the table + let tableExpression: tstl.Expression = table; + propertyAccessStack.forEach(property => { + const propertyName = ts.isPropertyName(property) + ? this.transformPropertyName(property) + : this.transformNumericLiteral(property); + tableExpression = tstl.createTableIndexExpression(tableExpression, propertyName); + }); + + // The identifier of the new variable + const variableName = this.transformIdentifier(element.name as ts.Identifier); + // The field to extract + const propertyName = this.transformPropertyName(element.propertyName || element.name); + + let expression: tstl.Expression; + if (element.dotDotDotToken) { + if (index !== pattern.elements.length - 1) continue; + + if (isObjectBindingPattern) { + const elements = pattern.elements as ts.NodeArray; + const usedProperties = elements.map(e => + tstl.createTableFieldExpression( + tstl.createBooleanLiteral(true), + tstl.createStringLiteral( + ((e.propertyName || e.name) as ts.Identifier).text, + e.propertyName || e.name + ) ) ); + + expression = this.transformLuaLibFunction( + LuaLibFeature.ObjectRest, + undefined, + tableExpression, + tstl.createTableExpression(usedProperties) + ); } else { - // Disallow ellipsis destructure - if (element.dotDotDotToken) { - throw TSTLErrors.ForbiddenEllipsisDestruction(element); - } - // Build the path to the table - let tableExpression: tstl.Expression = table; - propertyAccessStack.forEach(property => { - const propertyName = ts.isPropertyName(property) - ? this.transformPropertyName(property) - : this.transformNumericLiteral(property); - tableExpression = tstl.createTableIndexExpression(tableExpression, propertyName); - }); - // The identifier of the new variable - const variableName = this.transformIdentifier(element.name as ts.Identifier); - // The field to extract - const propertyName = this.transformPropertyName(element.propertyName || element.name); - const expression = isObjectBindingPattern - ? tstl.createTableIndexExpression(tableExpression, propertyName) - : tstl.createTableIndexExpression(tableExpression, tstl.createNumericLiteral(index + 1)); - result.push(...this.createLocalOrExportedOrGlobalDeclaration(variableName, expression)); - if (element.initializer) { - const identifier = this.addExportToIdentifier(variableName); - result.push( - tstl.createIfStatement( - tstl.createBinaryExpression( - identifier, - tstl.createNilLiteral(), - tstl.SyntaxKind.EqualityOperator - ), - tstl.createBlock([ - tstl.createAssignmentStatement( - identifier, - this.transformExpression(element.initializer) - ), - ]) - ) - ); - } + expression = this.transformLuaLibFunction( + LuaLibFeature.ArraySlice, + undefined, + tableExpression, + tstl.createNumericLiteral(index) + ); } + } else { + expression = tstl.createTableIndexExpression( + tableExpression, + isObjectBindingPattern ? propertyName : tstl.createNumericLiteral(index + 1) + ); + } + + result.push(...this.createLocalOrExportedOrGlobalDeclaration(variableName, expression)); + if (element.initializer) { + const identifier = this.addExportToIdentifier(variableName); + result.push( + tstl.createIfStatement( + tstl.createBinaryExpression( + identifier, + tstl.createNilLiteral(), + tstl.SyntaxKind.EqualityOperator + ), + tstl.createBlock([ + tstl.createAssignmentStatement(identifier, this.transformExpression(element.initializer)), + ]) + ) + ); } } propertyAccessStack.pop(); @@ -2025,17 +2055,15 @@ export class LuaTransformer { } } else if (ts.isArrayBindingPattern(statement.name) || ts.isObjectBindingPattern(statement.name)) { // Destructuring types - const statements: tstl.Statement[] = []; - // For nested bindings and object bindings, fall back to transformBindingPattern + // For object, nested, omitted or rest bindings fall back to transformBindingPattern if ( ts.isObjectBindingPattern(statement.name) || statement.name.elements.some( - elem => (!ts.isBindingElement(elem) || !ts.isIdentifier(elem.name)) && !ts.isOmittedExpression(elem) + e => ts.isBindingElement(e) && (!ts.isIdentifier(e.name) || e.dotDotDotToken) ) ) { - const statements = []; let table: tstl.Identifier; if (statement.initializer !== undefined && ts.isIdentifier(statement.initializer)) { table = this.transformIdentifier(statement.initializer); @@ -2057,17 +2085,6 @@ export class LuaTransformer { return statements; } - // Disallow ellipsis destruction - if ( - statement.name.elements.some( - elem => - (!ts.isBindingElement(elem) || elem.dotDotDotToken !== undefined) && - !ts.isOmittedExpression(elem) - ) - ) { - throw TSTLErrors.ForbiddenEllipsisDestruction(statement); - } - const vars = statement.name.elements.length > 0 ? this.filterUndefinedAndCast( @@ -3146,67 +3163,49 @@ export class LuaTransformer { this.validateFunctionAssignment(expression.right, rightType, leftType); this.validatePropertyAssignment(expression); - if (tsHelper.isAssignmentPattern(expression.left)) { + if (tsHelper.isDestructuringAssignment(expression)) { // Destructuring assignment - const flattenable = tsHelper.isValidFlattenableDestructuringAssignmentLeftHandSide( - expression as ts.DestructuringAssignment, - this.checker, - this.program - ); - - if (flattenable) { - const expressionType = this.checker.getTypeAtLocation(expression.right); + if ( + ts.isArrayLiteralExpression(expression.left) && + expression.left.elements.every( + e => + (ts.isIdentifier(e) || ts.isPropertyAccessExpression(e) || ts.isElementAccessExpression(e)) && + !tsHelper.isArrayLength(e, this.checker, this.program) + ) + ) { + const rightType = 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) + tsHelper.isArrayType(rightType, this.checker, this.program) ) { right = this.createUnpackCall(right, expression.right); } - return this.transformFlattenableDestructuringAssignment( - expression as ts.DestructuringAssignment, - right + const left = this.castElements( + expression.left.elements.map(e => this.transformExpression(e)), + tstl.isAssignmentLeftHandSideExpression ); + + return tstl.createAssignmentStatement(left, right, expression); } let right = this.transformExpression(expression.right); - const rootIdentifier = tstl.createAnonymousIdentifier(expression.left); - if (tsHelper.isTupleReturnCall(expression.right, this.checker)) { right = this.wrapInTable(right); } - const rootDeclaration = tstl.createVariableDeclarationStatement(rootIdentifier, right); - - const statements = this.transformDestructuringAssignment( - expression as ts.DestructuringAssignment, - rootIdentifier - ); - statements.unshift(rootDeclaration); - - return statements; + const rootIdentifier = tstl.createAnonymousIdentifier(expression.left); + return [ + tstl.createVariableDeclarationStatement(rootIdentifier, right), + ...this.transformDestructuringAssignment(expression, rootIdentifier), + ]; } 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 @@ -3268,6 +3267,36 @@ export class LuaTransformer { return result; } + protected transformSpreadAssignment( + node: ts.SpreadAssignment, + root: tstl.Expression, + properties: ts.NodeArray + ): tstl.Statement[] { + const usedProperties: tstl.TableFieldExpression[] = []; + // TODO: .flatMap + for (const property of properties) { + if ( + (ts.isShorthandPropertyAssignment(property) || ts.isPropertyAssignment(property)) && + !ts.isComputedPropertyName(property.name) + ) { + const name = ts.isIdentifier(property.name) + ? tstl.createStringLiteral(property.name.text) + : this.transformExpression(property.name); + + usedProperties.push(tstl.createTableFieldExpression(tstl.createBooleanLiteral(true), name)); + } + } + + const extractingExpression = this.transformLuaLibFunction( + LuaLibFeature.ObjectRest, + undefined, + root, + tstl.createTableExpression(usedProperties) + ); + + return [this.transformAssignment(node.expression, extractingExpression)]; + } + protected transformObjectLiteralAssignmentPattern( node: ts.ObjectLiteralExpression, root: tstl.Expression @@ -3283,7 +3312,8 @@ export class LuaTransformer { result.push(...this.transformPropertyAssignment(property, root)); break; case ts.SyntaxKind.SpreadAssignment: - throw TSTLErrors.ForbiddenEllipsisDestruction(property); + result.push(...this.transformSpreadAssignment(property, root, node.properties)); + break; default: throw TSTLErrors.UnsupportedKind("Object Destructure Property", property.kind, property); } @@ -3299,11 +3329,7 @@ export class LuaTransformer { const result: tstl.Statement[] = []; node.elements.forEach((element, index) => { - const indexedRoot = tstl.createTableIndexExpression( - root as tstl.Expression, - tstl.createNumericLiteral(index + 1), - element - ); + const indexedRoot = tstl.createTableIndexExpression(root, tstl.createNumericLiteral(index + 1), element); switch (element.kind) { case ts.SyntaxKind.ObjectLiteralExpression: @@ -3362,6 +3388,18 @@ export class LuaTransformer { result.push(assignmentStatement); break; + case ts.SyntaxKind.SpreadElement: + if (index !== node.elements.length - 1) break; + + const restElements = this.transformLuaLibFunction( + LuaLibFeature.ArraySlice, + undefined, + root, + tstl.createNumericLiteral(index) + ); + + result.push(this.transformAssignment((element as ts.SpreadElement).expression, restElements)); + break; case ts.SyntaxKind.OmittedExpression: break; default: @@ -3388,13 +3426,7 @@ export class LuaTransformer { } } - let leftExpression: ts.Expression; - if (ts.isBinaryExpression(node.initializer)) { - leftExpression = node.initializer.left; - } else { - leftExpression = node.initializer; - } - + const leftExpression = ts.isBinaryExpression(node.initializer) ? node.initializer.left : node.initializer; const variableToExtract = this.transformPropertyName(node.name); const extractingExpression = tstl.createTableIndexExpression(root, variableToExtract); @@ -3441,7 +3473,7 @@ export class LuaTransformer { ); } - if (tsHelper.isAssignmentPattern(expression.left)) { + if (tsHelper.isDestructuringAssignment(expression)) { // Destructuring assignment const rootIdentifier = tstl.createAnonymousIdentifier(expression.left); @@ -3450,13 +3482,10 @@ export class LuaTransformer { right = this.wrapInTable(right); } - const rootDeclaration = tstl.createVariableDeclarationStatement(rootIdentifier, right); - - const statements = this.transformDestructuringAssignment( - expression as ts.DestructuringAssignment, - rootIdentifier - ); - statements.unshift(rootDeclaration); + const statements = [ + tstl.createVariableDeclarationStatement(rootIdentifier, right), + ...this.transformDestructuringAssignment(expression, rootIdentifier), + ]; return this.createImmediatelyInvokedFunctionExpression(statements, rootIdentifier, expression); } @@ -6124,6 +6153,17 @@ export class LuaTransformer { return this.filterUndefined(visitResult); } + public castElements( + items: TOriginal[], + cast: (item: TOriginal) => item is TCast + ): TCast[] { + if (items.every(cast)) { + return items as TCast[]; + } else { + throw TSTLErrors.CouldNotCast(cast.name); + } + } + protected filterUndefined(items: Array): T[] { return items.filter(i => i !== undefined) as T[]; } @@ -6132,12 +6172,7 @@ export class LuaTransformer { items: Array, cast: (item: TOriginal) => item is TCast ): TCast[] { - const filteredItems = items.filter(i => i !== undefined) as TOriginal[]; - if (filteredItems.every(i => cast(i))) { - return filteredItems as TCast[]; - } else { - throw TSTLErrors.CouldNotCast(cast.name); - } + return this.castElements(this.filterUndefined(items), cast); } protected createConstructorDecorationStatement( diff --git a/src/TSHelper.ts b/src/TSHelper.ts index cbb250e77..5b608199b 100644 --- a/src/TSHelper.ts +++ b/src/TSHelper.ts @@ -60,6 +60,14 @@ export function isAssignmentPattern(node: ts.Node): node is ts.AssignmentPattern return ts.isObjectLiteralExpression(node) || ts.isArrayLiteralExpression(node); } +export function isDestructuringAssignment(node: ts.Node): node is ts.DestructuringAssignment { + return ( + ts.isBinaryExpression(node) && + node.operatorToken.kind === ts.SyntaxKind.EqualsToken && + isAssignmentPattern(node.left) + ); +} + export function getExportable(exportSpecifiers: ts.NamedExports, resolver: EmitResolver): ts.ExportSpecifier[] { return exportSpecifiers.elements.filter(exportSpecifier => resolver.isValueAliasDeclaration(exportSpecifier)); } @@ -400,7 +408,7 @@ export function getCustomNodeDirectives(node: ts.Node): Map(); ts.getJSDocTags(node).forEach(tag => { - const tagName = tag.tagName.escapedText as string; + const tagName = tag.tagName.text; if (Decorator.isValid(tagName)) { const dec = new Decorator(tagName, tag.comment ? tag.comment.split(" ") : []); directivesMap.set(dec.kind, dec); @@ -928,34 +936,6 @@ export function moduleHasEmittedBody( return false; } -export function isValidFlattenableDestructuringAssignmentLeftHandSide( - node: ts.DestructuringAssignment, - checker: ts.TypeChecker, - program: ts.Program -): 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; - } - }); - } - } - - return false; -} - export function isArrayLength( expression: ts.Expression, checker: ts.TypeChecker, @@ -971,8 +951,10 @@ export function isArrayLength( } const name = ts.isPropertyAccessExpression(expression) - ? (expression.name.escapedText as string) - : ts.isStringLiteral(expression.argumentExpression) && expression.argumentExpression.text; + ? expression.name.text + : ts.isStringLiteral(expression.argumentExpression) + ? expression.argumentExpression.text + : undefined; return name === "length"; } diff --git a/src/TSTLErrors.ts b/src/TSTLErrors.ts index 0e53dd713..6e4d00ce4 100644 --- a/src/TSTLErrors.ts +++ b/src/TSTLErrors.ts @@ -7,9 +7,6 @@ const getLuaTargetName = (version: LuaTarget) => (version === LuaTarget.LuaJIT ? export const CouldNotCast = (castName: string) => new Error(`Failed to cast all elements to expected type using ${castName}.`); -export const ForbiddenEllipsisDestruction = (node: ts.Node) => - new TranspileError(`Ellipsis destruction is not allowed.`, node); - export const ForbiddenForIn = (node: ts.Node) => new TranspileError(`Iterating over arrays with 'for ... in' is not allowed.`, node); diff --git a/src/lualib/ObjectRest.ts b/src/lualib/ObjectRest.ts new file mode 100644 index 000000000..7da14b2fb --- /dev/null +++ b/src/lualib/ObjectRest.ts @@ -0,0 +1,14 @@ +function __TS__ObjectRest( + this: void, + target: Record, + usedProperties: Partial> +): Partial> { + const result: Partial> = {}; + for (const property in target) { + if (!usedProperties[property]) { + result[property] = target[property]; + } + } + + return result; +} diff --git a/test/unit/destructuring.spec.ts b/test/unit/destructuring.spec.ts index b4ae3dd50..35519f299 100644 --- a/test/unit/destructuring.spec.ts +++ b/test/unit/destructuring.spec.ts @@ -1,6 +1,6 @@ import * as util from "../util"; -const allBindings = "x, y, z"; +const allBindings = "x, y, z, rest"; const testCases = [ { binding: "{ x }", value: { x: true } }, { binding: "{ x, y }", value: { x: false, y: true } }, @@ -9,12 +9,17 @@ const testCases = [ { binding: "{ x, y = true }", value: { x: false, y: false } }, { binding: "{ x = true }", value: {} }, { binding: "{ x, y = true }", value: { x: false } }, + { binding: "{ ...rest }", value: {} }, + { binding: "{ x, ...rest }", value: { x: "x" } }, + { binding: "{ x, ...rest }", value: { x: "x", y: "y", z: "z" } }, { 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: "[x, ...rest]", value: ["x"] }, + { binding: "[x, ...rest]", value: ["x", "y", "z"] }, { binding: "{ y: [z = true] }", value: { y: [false] } }, { binding: "{ x: [x, y] }", value: { x: ["x", "y"] } }, @@ -47,7 +52,8 @@ test.each(testCases)("in variable declaration (%p)", ({ binding, value }) => { }); // TODO: https://github.com/TypeScriptToLua/TypeScriptToLua/issues/695 -test.each(testCases.filter(x => x.binding !== "[x, , y]"))( +// TODO: https://github.com/microsoft/TypeScript/issues/32656 +test.each(testCases.filter(x => x.binding !== "[x, , y]" && x.binding !== "{ x, ...rest }"))( "in exported variable declaration (%p)", ({ binding, value }) => { util.testModule`