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
107 changes: 52 additions & 55 deletions src/LuaTransformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -4974,78 +4978,63 @@ export class LuaTransformer {
}

protected validateLuaTableCall(
expression: ts.CallExpression & { expression: ts.PropertyAccessExpression },
isWithinExpressionStatement: boolean
methodName: string,
callArguments: ts.NodeArray<ts.Expression>,
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);
}
}

Expand Down Expand Up @@ -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,
Expand Down
6 changes: 0 additions & 6 deletions src/TSTLErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
46 changes: 37 additions & 9 deletions test/unit/decorators/luaTable.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as ts from "typescript";
import * as TSTLErrors from "../../../src/TSTLErrors";
import * as util from "../../util";

Expand Down Expand Up @@ -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)
);
});

Expand Down Expand Up @@ -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);
});
Expand Down