diff --git a/src/LuaLib.ts b/src/LuaLib.ts index aa7c3e432..25cd83268 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -18,6 +18,7 @@ export enum LuaLibFeature { ArraySlice = "ArraySlice", ArraySome = "ArraySome", ArraySplice = "ArraySplice", + ArrayToObject = "ArrayToObject", ArrayFlat = "ArrayFlat", ArrayFlatMap = "ArrayFlatMap", ArraySetLength = "ArraySetLength", diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index ca398ba4c..ae8d79434 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -3836,7 +3836,8 @@ export class LuaTransformer { } public transformObjectLiteral(expression: ts.ObjectLiteralExpression): ExpressionVisitResult { - const properties: tstl.TableFieldExpression[] = []; + let properties: tstl.TableFieldExpression[] = []; + const tableExpressions: tstl.Expression[] = []; // Add all property assignments expression.properties.forEach(element => { const name = element.name ? this.transformPropertyName(element.name) : undefined; @@ -3853,12 +3854,46 @@ export class LuaTransformer { } else if (ts.isMethodDeclaration(element)) { const expression = this.transformFunctionExpression(element); properties.push(tstl.createTableFieldExpression(expression, name, element)); + } else if (ts.isSpreadAssignment(element)) { + // Create a table for preceding properties to preserve property order + // { x: 0, ...{ y: 2 }, y: 1, z: 2 } --> __TS__ObjectAssign({x = 0}, {y = 2}, {y = 1, z = 2}) + if (properties.length > 0) { + const tableExpression = tstl.createTableExpression(properties, expression); + tableExpressions.push(tableExpression); + } + properties = []; + + const type = this.checker.getTypeAtLocation(element.expression); + let tableExpression: tstl.Expression; + if (type && tsHelper.isArrayType(type, this.checker, this.program)) { + tableExpression = this.transformLuaLibFunction( + LuaLibFeature.ArrayToObject, + element.expression, + this.transformExpression(element.expression) + ); + } else { + tableExpression = this.transformExpression(element.expression); + } + tableExpressions.push(tableExpression); } else { throw TSTLErrors.UnsupportedKind("object literal element", element.kind, expression); } }); - return tstl.createTableExpression(properties, expression); + if (tableExpressions.length === 0) { + return tstl.createTableExpression(properties, expression); + } else { + if (properties.length > 0) { + const tableExpression = tstl.createTableExpression(properties, expression); + tableExpressions.push(tableExpression); + } + + if (tableExpressions[0].kind !== tstl.SyntaxKind.TableExpression) { + tableExpressions.unshift(tstl.createTableExpression(undefined, expression)); + } + + return this.transformLuaLibFunction(LuaLibFeature.ObjectAssign, expression, ...tableExpressions); + } } public transformOmittedExpression(node: ts.OmittedExpression): ExpressionVisitResult { diff --git a/src/lualib/ArrayToObject.ts b/src/lualib/ArrayToObject.ts new file mode 100644 index 000000000..385906f0a --- /dev/null +++ b/src/lualib/ArrayToObject.ts @@ -0,0 +1,7 @@ +function __TS__ArrayToObject(this: void, array: any[]): object { + const object: Record = {}; + for (let i = 0; i < array.length; i += 1) { + object[i] = array[i]; + } + return object; +} diff --git a/src/lualib/Spread.ts b/src/lualib/Spread.ts index aee6ac66b..8f88053d2 100644 --- a/src/lualib/Spread.ts +++ b/src/lualib/Spread.ts @@ -1,7 +1,13 @@ -function __TS__Spread(this: void, iterable: Iterable): T[] { - const arr: T[] = []; - for (const item of iterable) { - arr[arr.length] = item; +function __TS__Spread(this: void, iterable: string | Iterable): T[] { + const arr = []; + if (typeof iterable === "string") { + for (let i = 0; i < iterable.length; i += 1) { + arr[arr.length] = iterable[i]; + } + } else { + for (const item of iterable) { + arr[arr.length] = item; + } } return (table.unpack || unpack)(arr); } diff --git a/test/translation/__snapshots__/transformation.spec.ts.snap b/test/translation/__snapshots__/transformation.spec.ts.snap index 7e5ae77f0..a3e79fed4 100644 --- a/test/translation/__snapshots__/transformation.spec.ts.snap +++ b/test/translation/__snapshots__/transformation.spec.ts.snap @@ -562,6 +562,13 @@ exports[`Transformation (shorthandPropertyAssignment) 1`] = ` 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( diff --git a/test/translation/transformation/spreadAssignment.ts b/test/translation/transformation/spreadAssignment.ts new file mode 100644 index 000000000..0ad87be82 --- /dev/null +++ b/test/translation/transformation/spreadAssignment.ts @@ -0,0 +1,3 @@ +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/unit/spreadElement.spec.ts b/test/unit/spreadElement.spec.ts index 28201f12e..13e528102 100644 --- a/test/unit/spreadElement.spec.ts +++ b/test/unit/spreadElement.spec.ts @@ -64,3 +64,51 @@ test("Spread Element Iterable", () => { 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]); +});