Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/LuaLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export enum LuaLibFeature {
ArraySlice = "ArraySlice",
ArraySome = "ArraySome",
ArraySplice = "ArraySplice",
ArrayToObject = "ArrayToObject",
ArrayFlat = "ArrayFlat",
ArrayFlatMap = "ArrayFlatMap",
ArraySetLength = "ArraySetLength",
Expand Down
39 changes: 37 additions & 2 deletions src/LuaTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object.assign should always get a table literal as a first argument to avoid mutation of first spread element value. It probably would be better to check the type of the first element of tableExpressions to handle literal spread cases (check out TypeScript output).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really good edge cases, thanks!

}
}

public transformOmittedExpression(node: ts.OmittedExpression): ExpressionVisitResult {
Expand Down
7 changes: 7 additions & 0 deletions src/lualib/ArrayToObject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
function __TS__ArrayToObject(this: void, array: any[]): object {
const object: Record<number, any> = {};
for (let i = 0; i < array.length; i += 1) {
object[i] = array[i];
}
return object;
}
14 changes: 10 additions & 4 deletions src/lualib/Spread.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
function __TS__Spread<T>(this: void, iterable: Iterable<T>): T[] {
const arr: T[] = [];
for (const item of iterable) {
arr[arr.length] = item;
function __TS__Spread<T>(this: void, iterable: string | Iterable<T>): 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);
}
7 changes: 7 additions & 0 deletions test/translation/__snapshots__/transformation.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 3 additions & 0 deletions test/translation/transformation/spreadAssignment.ts
Original file line number Diff line number Diff line change
@@ -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 } };
48 changes: 48 additions & 0 deletions test/unit/spreadElement.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need a single-property test if we have a multi-property one?

"{ 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]);
});