diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index d8aee1a01..180985bdd 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -2169,13 +2169,7 @@ export class LuaTransformer { const ownerType = this.checker.getTypeAtLocation(expression.expression.expression); const classDecorators = tsHelper.getCustomDecorators(ownerType, this.checker); if (classDecorators.has(DecoratorKind.LuaTable)) { - this.validateLuaTableCall( - expression as ts.CallExpression & { expression: ts.PropertyAccessExpression }, - true - ); - return this.transformLuaTableExpressionStatement(statement as ts.ExpressionStatement & { - expression: ts.CallExpression; - } & { expression: { expression: ts.PropertyAccessExpression } }); + return this.transformLuaTableExpressionAsExpressionStatement(expression); } } @@ -4254,10 +4248,7 @@ export class LuaTransformer { const classDecorators = tsHelper.getCustomDecorators(ownerType, this.checker); if (classDecorators.has(DecoratorKind.LuaTable)) { - this.validateLuaTableCall(node as ts.CallExpression & { expression: ts.PropertyAccessExpression }, false); - return this.transformLuaTableCallExpression(node as ts.CallExpression & { - expression: ts.PropertyAccessExpression; - }); + return this.transformLuaTableCallExpression(node); } if (tsHelper.isStringType(ownerType, this.checker, this.program)) { @@ -4341,6 +4332,13 @@ export class LuaTransformer { throw TSTLErrors.InvalidElementCall(node); } + const ownerType = this.checker.getTypeAtLocation(node.expression.expression); + const ownerDecorators = tsHelper.getCustomDecorators(ownerType, this.checker); + + if (ownerDecorators.has(DecoratorKind.LuaTable)) { + return this.transformLuaTableCallExpression(node); + } + const signature = this.checker.getResolvedSignature(node); const signatureDeclaration = signature && signature.getDeclaration(); const parameters = this.transformArguments(node.arguments, signature); @@ -4619,13 +4617,14 @@ export class LuaTransformer { } } - protected transformLuaTableProperty(node: ts.PropertyAccessExpression): tstl.UnaryExpression { - switch (node.name.text) { + protected transformLuaTableProperty(node: ts.PropertyAccessExpression): tstl.Expression { + const [luaTable, propertyName] = this.parseLuaTableExpression(node); + switch (propertyName) { case "length": - const propertyAccessExpression = this.transformExpression(node.expression); - return tstl.createUnaryExpression(propertyAccessExpression, tstl.SyntaxKind.LengthOperator, node); + const unaryExpression = tstl.createUnaryExpression(luaTable, tstl.SyntaxKind.LengthOperator, node); + return unaryExpression; default: - throw TSTLErrors.UnsupportedProperty("LuaTable", node.name.text, node); + throw TSTLErrors.UnsupportedProperty("LuaTable", propertyName, node); } } @@ -4656,6 +4655,11 @@ export class LuaTransformer { const argumentType = this.checker.getTypeAtLocation(expression.argumentExpression); const type = this.checker.getTypeAtLocation(expression.expression); + const decorators = tsHelper.getCustomDecorators(type, this.checker); + if (decorators.has(DecoratorKind.LuaTable)) { + throw TSTLErrors.UnsupportedKind("LuaTable access expression", expression.kind, expression); + } + if ( tsHelper.isNumberType(argumentType, this.checker, this.program) && tsHelper.isStringType(type, this.checker, this.program) @@ -4974,78 +4978,63 @@ export class LuaTransformer { } protected validateLuaTableCall( - expression: ts.CallExpression & { expression: ts.PropertyAccessExpression }, - isWithinExpressionStatement: boolean + methodName: string, + callArguments: ts.NodeArray, + original: ts.Node ): void { - const methodName = expression.expression.name.text; - if (expression.arguments.some(argument => ts.isSpreadElement(argument))) { - throw TSTLErrors.ForbiddenLuaTableUseException("Arguments cannot be spread.", expression); + if (callArguments.some(argument => ts.isSpreadElement(argument))) { + throw TSTLErrors.ForbiddenLuaTableUseException("Arguments cannot be spread.", original); } switch (methodName) { case "get": - if (expression.arguments.length !== 1) { - throw TSTLErrors.ForbiddenLuaTableUseException("One parameter is required for get().", expression); + if (callArguments.length !== 1) { + throw TSTLErrors.ForbiddenLuaTableUseException("One parameter is required for get().", original); } break; case "set": - if (expression.arguments.length !== 2) { - throw TSTLErrors.ForbiddenLuaTableUseException( - "Two parameters are required for set().", - expression - ); - } - if (!isWithinExpressionStatement) { - throw TSTLErrors.ForbiddenLuaTableSetExpression(expression); + if (callArguments.length !== 2) { + throw TSTLErrors.ForbiddenLuaTableUseException("Two parameters are required for set().", original); } break; } } - protected transformLuaTableExpressionStatement( - node: ts.ExpressionStatement & { expression: ts.CallExpression } & { - expression: { expression: ts.PropertyAccessExpression }; - } - ): tstl.VariableDeclarationStatement | tstl.AssignmentStatement { - const methodName = node.expression.expression.name.text; - const signature = this.checker.getResolvedSignature(node.expression); - const tableName = (node.expression.expression.expression as ts.Identifier).text; - const luaTable = tstl.createIdentifier(tableName); - const params = this.transformArguments((node.expression as ts.CallExpression).arguments, signature); + protected transformLuaTableExpressionAsExpressionStatement(expression: ts.CallExpression): tstl.Statement { + const [luaTable, methodName] = this.parseLuaTableExpression(expression.expression); + this.validateLuaTableCall(methodName, expression.arguments, expression); + const signature = this.checker.getResolvedSignature(expression); + const params = this.transformArguments(expression.arguments, signature); switch (methodName) { case "get": return tstl.createVariableDeclarationStatement( - tstl.createAnonymousIdentifier(node.expression), - tstl.createTableIndexExpression(luaTable, params[0], node.expression), - node.expression + tstl.createAnonymousIdentifier(expression), + tstl.createTableIndexExpression(luaTable, params[0], expression), + expression ); case "set": return tstl.createAssignmentStatement( - tstl.createTableIndexExpression(luaTable, params[0], node.expression), + tstl.createTableIndexExpression(luaTable, params[0], expression), params.splice(1), - node.expression + expression ); default: - throw TSTLErrors.ForbiddenLuaTableUseException("Unsupported method.", node.expression); + throw TSTLErrors.UnsupportedProperty("LuaTable", methodName, expression); } } - protected transformLuaTableCallExpression( - expression: ts.CallExpression & { expression: ts.PropertyAccessExpression } - ): tstl.Expression { - const method = expression.expression; - const methodName = method.name.text; + protected transformLuaTableCallExpression(expression: ts.CallExpression): tstl.Expression { + const [luaTable, methodName] = this.parseLuaTableExpression(expression.expression); + this.validateLuaTableCall(methodName, expression.arguments, expression); const signature = this.checker.getResolvedSignature(expression); - const tableName = (method.expression as ts.Identifier).text; - const luaTable = tstl.createIdentifier(tableName); const params = this.transformArguments(expression.arguments, signature); switch (methodName) { case "get": return tstl.createTableIndexExpression(luaTable, params[0], expression); default: - throw TSTLErrors.ForbiddenLuaTableUseException("Unsupported method.", expression); + throw TSTLErrors.UnsupportedProperty("LuaTable", methodName, expression); } } @@ -5444,6 +5433,14 @@ export class LuaTransformer { return scope; } + protected parseLuaTableExpression(node: ts.LeftHandSideExpression): [tstl.Expression, string] { + if (ts.isPropertyAccessExpression(node)) { + return [this.transformExpression(node.expression), node.name.text]; + } else { + throw TSTLErrors.UnsupportedKind("LuaTable access expression", node.kind, node); + } + } + protected transformLuaLibFunction( func: LuaLibFeature, tsParent?: ts.Expression, diff --git a/src/TSTLErrors.ts b/src/TSTLErrors.ts index 8d9966c0b..279e86782 100644 --- a/src/TSTLErrors.ts +++ b/src/TSTLErrors.ts @@ -13,12 +13,6 @@ export const ForbiddenEllipsisDestruction = (node: ts.Node) => export const ForbiddenForIn = (node: ts.Node) => new TranspileError(`Iterating over arrays with 'for ... in' is not allowed.`, node); -export const ForbiddenLuaTableSetExpression = (node: ts.Node) => - new TranspileError( - `A '@luaTable' object's 'set()' method can only be used as a Statement, not an Expression.`, - node - ); - export const ForbiddenLuaTableNonDeclaration = (node: ts.Node) => new TranspileError(`Classes with the '@luaTable' decorator must be declared.`, node); diff --git a/test/unit/decorators/luaTable.spec.ts b/test/unit/decorators/luaTable.spec.ts index 6419cc735..e7559f7c0 100644 --- a/test/unit/decorators/luaTable.spec.ts +++ b/test/unit/decorators/luaTable.spec.ts @@ -1,3 +1,4 @@ +import * as ts from "typescript"; import * as TSTLErrors from "../../../src/TSTLErrors"; import * as util from "../../util"; @@ -34,21 +35,24 @@ test.each([tableLibClass])("LuaTables cannot be constructed with arguments", tab ); }); -test.each([tableLibClass, tableLibInterface])("LuaTable set() cannot be used in an expression position", tableLib => { - expect(() => util.transpileString(tableLib + `const exp = tbl.set("value", 5)`)).toThrowExactError( - TSTLErrors.ForbiddenLuaTableSetExpression(util.nodeStub) - ); -}); +test.each([tableLibClass, tableLibInterface])( + "LuaTable set() cannot be used in a LuaTable call expression", + tableLib => { + expect(() => util.transpileString(tableLib + `const exp = tbl.set("value", 5)`)).toThrowExactError( + TSTLErrors.UnsupportedProperty("LuaTable", "set", util.nodeStub) + ); + } +); -test.each([tableLibClass, tableLibInterface])("LuaTables cannot have other methods", tableLib => { +test.each([tableLibClass, tableLibInterface])("LuaTables cannot have other members", tableLib => { expect(() => util.transpileString(tableLib + `tbl.other()`)).toThrowExactError( - TSTLErrors.ForbiddenLuaTableUseException("Unsupported method.", util.nodeStub) + TSTLErrors.UnsupportedProperty("LuaTable", "other", util.nodeStub) ); }); -test.each([tableLibClass, tableLibInterface])("LuaTables cannot have other methods", tableLib => { +test.each([tableLibClass, tableLibInterface])("LuaTables cannot have other members", tableLib => { expect(() => util.transpileString(tableLib + `let x = tbl.other()`)).toThrowExactError( - TSTLErrors.ForbiddenLuaTableUseException("Unsupported method.", util.nodeStub) + TSTLErrors.UnsupportedProperty("LuaTable", "other", util.nodeStub) ); }); @@ -113,12 +117,36 @@ test.each([tableLibClass])("Cannot extend LuaTable class", tableLib => { }); }); +test.each([tableLibClass, tableLibInterface])("Cannot use ElementAccessExpression on a LuaTable", tableLib => { + test.each([`tbl["get"]("field")`, `tbl["set"]("field")`, `tbl["length"]`])( + "Cannot use ElementAccessExpression on a LuaTable (%p)", + code => { + expect(() => util.transpileString(tableLib + code)).toThrowExactError( + TSTLErrors.UnsupportedKind( + "LuaTable access expression", + ts.SyntaxKind.ElementAccessExpression, + util.nodeStub + ) + ); + } + ); +}); + +test.each([tableLibClass, tableLibInterface])("Cannot isolate LuaTable methods", tableLib => { + test.each([`set`, `get`])("Cannot isolate LuaTable method (%p)", propertyName => { + expect(() => util.transpileString(`${tableLib} let property = tbl.${propertyName}`)).toThrowExactError( + TSTLErrors.UnsupportedProperty("LuaTable", propertyName, util.nodeStub) + ); + }); +}); + test.each([tableLibClass])("LuaTable functional tests", tableLib => { test.each<[string, any]>([ [`const t = new Table(); t.set("field", "value"); return t.get("field");`, "value"], [`const t = new Table(); t.set("field", 0); return t.get("field");`, 0], [`const t = new Table(); t.set(1, true); return t.length`, 1], [`const t = new Table(); t.set(t.length + 1, true); t.set(t.length + 1, true); return t.length`, 2], + [`const k = "k"; const t = { data: new Table() }; t.data.set(k, 3); return t.data.get(k);`, 3], ])("LuaTable test (%p)", (code, expectedReturnValue) => { expect(util.transpileAndExecute(code, undefined, undefined, tableLib)).toBe(expectedReturnValue); });