diff --git a/src/LuaLib.ts b/src/LuaLib.ts index 7515d8d4b..5b259f66b 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -19,6 +19,7 @@ export enum LuaLibFeature { ArraySplice = "ArraySplice", ArrayFlat = "ArrayFlat", ArrayFlatMap = "ArrayFlatMap", + ArraySetLength = "ArraySetLength", ClassIndex = "ClassIndex", ClassNewIndex = "ClassNewIndex", FunctionApply = "FunctionApply", diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index 921ce48a5..eede8ed9e 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -2494,6 +2494,18 @@ export class LuaTransformer { const leftType = this.checker.getTypeAtLocation(expression.left); this.validateFunctionAssignment(expression.right, rightType, leftType); + 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 (ts.isArrayLiteralExpression(expression.left)) { // Destructuring assignment const left = expression.left.elements.length > 0 @@ -2528,6 +2540,16 @@ export class LuaTransformer { const leftType = this.checker.getTypeAtLocation(expression.left); this.validateFunctionAssignment(expression.right, rightType, leftType); + if (tsHelper.isArrayLengthAssignment(expression, this.checker, this.program)) { + // array.length = x + return this.transformLuaLibFunction( + LuaLibFeature.ArraySetLength, + expression, + this.transformExpression(expression.left.expression), + this.transformExpression(expression.right) + ); + } + if (ts.isArrayLiteralExpression(expression.left)) { // Destructuring assignment // (function() local ${tmps} = ${right}; ${left} = ${tmps}; return {${tmps}} end)() diff --git a/src/TSHelper.ts b/src/TSHelper.ts index 521b06f22..d50523fa1 100644 --- a/src/TSHelper.ts +++ b/src/TSHelper.ts @@ -752,4 +752,30 @@ export class TSHelper { } return false; } + + public static isArrayLengthAssignment( + expression: ts.BinaryExpression, + checker: ts.TypeChecker, + program: ts.Program + ): expression is ts.BinaryExpression & { left: ts.PropertyAccessExpression | ts.ElementAccessExpression; } + { + if (expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken) { + return false; + } + + if (!ts.isPropertyAccessExpression(expression.left) && !ts.isElementAccessExpression(expression.left)) { + return false; + } + + const type = checker.getTypeAtLocation(expression.left.expression); + if (!TSHelper.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; + + return name === "length"; + } } diff --git a/src/lualib/ArraySetLength.ts b/src/lualib/ArraySetLength.ts new file mode 100644 index 000000000..3efcf1e2f --- /dev/null +++ b/src/lualib/ArraySetLength.ts @@ -0,0 +1,14 @@ +function __TS__ArraySetLength(this: void, arr: T[], length: number): number { + if (length < 0 + || length !== length // NaN + || length === (1 / 0) // Infinity + || Math.floor(length) !== length) // non-integer + { + // tslint:disable-next-line:no-string-throw + throw `invalid array length: ${length}`; + } + for (let i = arr.length - 1; i >= length; --i) { + arr[i] = undefined; + } + return length; +} diff --git a/test/unit/array.spec.ts b/test/unit/array.spec.ts index 85dcb5dcb..9da08ce1c 100644 --- a/test/unit/array.spec.ts +++ b/test/unit/array.spec.ts @@ -136,3 +136,43 @@ test("Array property access", () => { `; expect(util.transpileAndExecute(code)).toBe("bar123"); }); + +test.each([{ length: 0, result: 0 }, { length: 1, result: 1 }, { length: 7, result: 3 }])( + "Array length set", + ({ length, result }) => { + const code = ` + const arr = [1, 2, 3]; + arr.length = ${length}; + return arr.length; + `; + expect(util.transpileAndExecute(code)).toBe(result); + }, +); + +test.each([ + { length: 0, result: "0/0" }, + { length: 1, result: "1/1" }, + { length: 7, result: "7/3" }, +])("Array length set as expression", ({ length, result }) => { + const code = ` + const arr = [1, 2, 3]; + const l = arr.length = ${length}; + return \`\${l}/\${arr.length}\`; + `; + expect(util.transpileAndExecute(code)).toBe(result); +}); + +test.each([ + { length: -1, result: -1 }, + { length: -7, result: -7 }, + { length: 0.1, result: 0.1 }, + { length: "0 / 0", result: "NaN" }, + { length: "1 / 0", result: "Infinity" }, + { length: "-1 / 0", result: "-Infinity" }, +])("Invalid array length set", ({ length, result }) => { + const code = ` + const arr = [1, 2, 3]; + arr.length = ${length}; + `; + expect(() => util.transpileAndExecute(code)).toThrowError(`invalid array length: ${result}`); +});