Skip to content

Commit 3de430d

Browse files
hazzard993Perryvw
authored andcommitted
Lua table (#516)
* LuaTable draft * Refactor and added tests * Fixed tripped watch file * Missing semicolon * Prettified * Cannot extend LuaTable classes * luaTable classes can only be declared * Restricted LuaTable parameter length * string -> LuaTable correction * LuaTable length cannot be re-assigned * Prettier * LuaTable interface tests * LuaTables cannot be constructed with arguments * Test splitting and more interface tests * instanceof LuaTable not allowed * Purpose in life for exception * No more other methods * Broke down transformLuaTableCallExpression into other methods * LuaTable -> luaTable * luaTables must be within an ambient context * Added isAmbient and disallowed argument spreading * Unsupported method consistency * tsHelper.isDeclared is unused * public -> private * Missed one more public * Allowed transpileAndExecute to use ambient code * luaTable functional tests * transpileString signature change * Revert util.ts changes
1 parent 720a4dc commit 3de430d

File tree

7 files changed

+366
-0
lines changed

7 files changed

+366
-0
lines changed

src/Decorator.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export class Decorator {
2323
return DecoratorKind.TupleReturn;
2424
case "luaiterator":
2525
return DecoratorKind.LuaIterator;
26+
case "luatable":
27+
return DecoratorKind.LuaTable;
2628
case "noself":
2729
return DecoratorKind.NoSelf;
2830
case "noselfinfile":
@@ -56,6 +58,7 @@ export enum DecoratorKind {
5658
Phantom = "Phantom",
5759
TupleReturn = "TupleReturn",
5860
LuaIterator = "LuaIterator",
61+
LuaTable = "LuaTable",
5962
NoSelf = "NoSelf",
6063
NoSelfInFile = "NoSelfInFile",
6164
}

src/LuaTransformer.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,19 @@ export class LuaTransformer {
475475
}
476476
}
477477

478+
// You cannot extend LuaTable classes
479+
if (extendsType) {
480+
const decorators = tsHelper.getCustomDecorators(extendsType, this.checker);
481+
if (decorators.has(DecoratorKind.LuaTable)) {
482+
throw TSTLErrors.InvalidExtendsLuaTable(statement);
483+
}
484+
}
485+
486+
// LuaTable classes must be ambient
487+
if (decorators.has(DecoratorKind.LuaTable) && !tsHelper.isAmbient(statement)) {
488+
throw TSTLErrors.ForbiddenLuaTableNonDeclaration(statement);
489+
}
490+
478491
// Get all properties with value
479492
const properties = statement.members.filter(ts.isPropertyDeclaration).filter(member => member.initializer);
480493

@@ -1981,6 +1994,22 @@ export class LuaTransformer {
19811994
);
19821995
}
19831996

1997+
if (ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression)) {
1998+
const ownerType = this.checker.getTypeAtLocation(expression.expression.expression);
1999+
const classDecorators = tsHelper.getCustomDecorators(ownerType, this.checker);
2000+
if (classDecorators.has(DecoratorKind.LuaTable)) {
2001+
this.validateLuaTableCall(
2002+
expression as ts.CallExpression & { expression: ts.PropertyAccessExpression },
2003+
true
2004+
);
2005+
return this.transformLuaTableExpressionStatement(
2006+
statement as ts.ExpressionStatement
2007+
& { expression: ts.CallExpression }
2008+
& { expression: { expression: ts.PropertyAccessExpression } }
2009+
);
2010+
}
2011+
}
2012+
19842013
return tstl.createExpressionStatement(this.expectExpression(this.transformExpression(expression)));
19852014
}
19862015

@@ -2666,6 +2695,10 @@ export class LuaTransformer {
26662695
throw TSTLErrors.InvalidInstanceOfExtension(expression);
26672696
}
26682697

2698+
if (decorators.has(DecoratorKind.LuaTable)) {
2699+
throw TSTLErrors.InvalidInstanceOfLuaTable(expression);
2700+
}
2701+
26692702
if (tsHelper.isStandardLibraryType(rhsType, "ObjectConstructor", this.program)) {
26702703
return this.transformLuaLibFunction(LuaLibFeature.InstanceOfObject, expression, lhs);
26712704
}
@@ -2697,6 +2730,7 @@ export class LuaTransformer {
26972730
const rightType = this.checker.getTypeAtLocation(expression.right);
26982731
const leftType = this.checker.getTypeAtLocation(expression.left);
26992732
this.validateFunctionAssignment(expression.right, rightType, leftType);
2733+
this.validatePropertyAssignment(expression);
27002734

27012735
if (tsHelper.isArrayLengthAssignment(expression, this.checker, this.program)) {
27022736
// array.length = x
@@ -3393,6 +3427,17 @@ export class LuaTransformer {
33933427
);
33943428
}
33953429

3430+
if (classDecorators.has(DecoratorKind.LuaTable)) {
3431+
if (node.arguments && node.arguments.length > 0) {
3432+
throw TSTLErrors.ForbiddenLuaTableUseException(
3433+
"No parameters are allowed when constructing a LuaTable object.",
3434+
node
3435+
);
3436+
} else {
3437+
return tstl.createTableExpression();
3438+
}
3439+
}
3440+
33963441
return tstl.createCallExpression(
33973442
tstl.createTableIndexExpression(name, tstl.createStringLiteral("new")),
33983443
params,
@@ -3535,6 +3580,18 @@ export class LuaTransformer {
35353580
return this.transformSymbolCallExpression(node);
35363581
}
35373582

3583+
const classDecorators = tsHelper.getCustomDecorators(ownerType, this.checker);
3584+
3585+
if (classDecorators.has(DecoratorKind.LuaTable)) {
3586+
this.validateLuaTableCall(
3587+
node as ts.CallExpression & { expression: ts.PropertyAccessExpression },
3588+
false
3589+
);
3590+
return this.transformLuaTableCallExpression(
3591+
node as ts.CallExpression & { expression: ts.PropertyAccessExpression }
3592+
);
3593+
}
3594+
35383595
switch (ownerType.flags) {
35393596
case ts.TypeFlags.String:
35403597
case ts.TypeFlags.StringLiteral:
@@ -3712,6 +3769,10 @@ export class LuaTransformer {
37123769
return tstl.createIdentifier(property, node);
37133770
}
37143771

3772+
if (decorators.has(DecoratorKind.LuaTable)) {
3773+
return this.transformLuaTableProperty(node);
3774+
}
3775+
37153776
// Catch math expressions
37163777
if (ts.isIdentifier(node.expression)) {
37173778
const ownerType = this.checker.getTypeAtLocation(node.expression);
@@ -3851,6 +3912,16 @@ export class LuaTransformer {
38513912
}
38523913
}
38533914

3915+
private transformLuaTableProperty(node: ts.PropertyAccessExpression): tstl.UnaryExpression | undefined {
3916+
switch (node.name.escapedText) {
3917+
case "length":
3918+
const propertyAccessExpression = this.expectExpression(this.transformExpression(node.expression));
3919+
return tstl.createUnaryExpression(propertyAccessExpression, tstl.SyntaxKind.LengthOperator, node);
3920+
default:
3921+
throw TSTLErrors.UnsupportedProperty("LuaTable", node.name.escapedText as string, node);
3922+
}
3923+
}
3924+
38543925
public transformElementAccessExpression(expression: ts.ElementAccessExpression): ExpressionVisitResult {
38553926
const table = this.expectExpression(this.transformExpression(expression.expression));
38563927
const index = this.expectExpression(this.transformExpression(expression.argumentExpression));
@@ -4219,6 +4290,91 @@ export class LuaTransformer {
42194290
}
42204291
}
42214292

4293+
private validateLuaTableCall(
4294+
expression: ts.CallExpression & { expression: ts.PropertyAccessExpression },
4295+
isWithinExpressionStatement: boolean
4296+
): void {
4297+
const methodName = expression.expression.name.escapedText;
4298+
if (expression.arguments.some(argument => ts.isSpreadElement(argument))) {
4299+
throw TSTLErrors.ForbiddenLuaTableUseException("Arguments cannot be spread.", expression);
4300+
}
4301+
4302+
switch (methodName) {
4303+
case "get":
4304+
if (expression.arguments.length !== 1) {
4305+
throw TSTLErrors.ForbiddenLuaTableUseException(
4306+
"One parameter is required for get().",
4307+
expression
4308+
);
4309+
}
4310+
break;
4311+
case "set":
4312+
if (expression.arguments.length !== 2) {
4313+
throw TSTLErrors.ForbiddenLuaTableUseException(
4314+
"Two parameters are required for set().",
4315+
expression
4316+
);
4317+
}
4318+
if (!isWithinExpressionStatement) {
4319+
throw TSTLErrors.ForbiddenLuaTableSetExpression(expression);
4320+
}
4321+
break;
4322+
}
4323+
}
4324+
4325+
private transformLuaTableExpressionStatement(
4326+
node: ts.ExpressionStatement
4327+
& { expression: ts.CallExpression }
4328+
& { expression: { expression: ts.PropertyAccessExpression }}
4329+
): tstl.VariableDeclarationStatement | tstl.AssignmentStatement {
4330+
const methodName = node.expression.expression.name.escapedText;
4331+
const signature = this.checker.getResolvedSignature(node.expression);
4332+
const tableName = (node.expression.expression.expression as ts.Identifier).escapedText;
4333+
const luaTable = tstl.createIdentifier(tableName);
4334+
const params = this.transformArguments((node.expression as ts.CallExpression).arguments, signature);
4335+
4336+
switch (methodName) {
4337+
case "get":
4338+
return tstl.createVariableDeclarationStatement(
4339+
tstl.createAnonymousIdentifier(node.expression),
4340+
tstl.createTableIndexExpression(luaTable, params[0], node.expression),
4341+
node.expression
4342+
);
4343+
case "set":
4344+
return tstl.createAssignmentStatement(
4345+
tstl.createTableIndexExpression(luaTable, params[0], node.expression),
4346+
params.splice(1),
4347+
node.expression
4348+
);
4349+
default:
4350+
throw TSTLErrors.ForbiddenLuaTableUseException(
4351+
"Unsupported method.",
4352+
node.expression
4353+
);
4354+
}
4355+
}
4356+
4357+
private transformLuaTableCallExpression(
4358+
expression: ts.CallExpression & { expression: ts.PropertyAccessExpression }
4359+
): tstl.Expression {
4360+
const method = expression.expression;
4361+
const methodName = method.name.escapedText;
4362+
const signature = this.checker.getResolvedSignature(expression);
4363+
const tableName = (method.expression as ts.Identifier).escapedText;
4364+
const luaTable = tstl.createIdentifier(tableName);
4365+
const params = this.transformArguments(expression.arguments, signature);
4366+
4367+
switch (methodName) {
4368+
case "get":
4369+
return tstl.createTableIndexExpression(luaTable, params[0], expression);
4370+
default:
4371+
throw TSTLErrors.ForbiddenLuaTableUseException(
4372+
"Unsupported method.",
4373+
expression
4374+
);
4375+
}
4376+
}
4377+
42224378
private transformArrayCallExpression(node: ts.CallExpression): tstl.CallExpression {
42234379
const expression = node.expression as ts.PropertyAccessExpression;
42244380
const signature = this.checker.getResolvedSignature(node);
@@ -4823,6 +4979,22 @@ export class LuaTransformer {
48234979
}
48244980
}
48254981

4982+
private validatePropertyAssignment(node: ts.Node): void {
4983+
if (ts.isBinaryExpression(node) && ts.isPropertyAccessExpression(node.left)) {
4984+
const leftType = this.checker.getTypeAtLocation(node.left.expression);
4985+
const decorators = tsHelper.getCustomDecorators(leftType, this.checker);
4986+
if (decorators.has(DecoratorKind.LuaTable)) {
4987+
switch (node.left.name.escapedText as string) {
4988+
case "length":
4989+
throw TSTLErrors.ForbiddenLuaTableUseException(
4990+
`A LuaTable object's length cannot be re-assigned.`,
4991+
node
4992+
);
4993+
}
4994+
}
4995+
}
4996+
}
4997+
48264998
private wrapInFunctionCall(expression: tstl.Expression): tstl.FunctionExpression {
48274999
const returnStatement = tstl.createReturnStatement([expression]);
48285000
return tstl.createFunctionExpression(

src/TSHelper.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ export class TSHelper {
128128
return false;
129129
}
130130

131+
public static isAmbient(node: ts.Declaration): boolean {
132+
return !((ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Ambient) === 0);
133+
}
134+
131135
public static isStatic(node: ts.Node): boolean {
132136
return node.modifiers !== undefined && node.modifiers.some(m => m.kind === ts.SyntaxKind.StaticKeyword);
133137
}

src/TSTLErrors.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,22 @@ export class TSTLErrors {
1818
public static ForbiddenForIn = (node: ts.Node) =>
1919
new TranspileError(`Iterating over arrays with 'for ... in' is not allowed.`, node);
2020

21+
public static ForbiddenLuaTableSetExpression = (node: ts.Node) => new TranspileError(
22+
`A '@luaTable' object's 'set()' method can only be used as a Statement, not an Expression.`,
23+
node);
24+
25+
public static ForbiddenLuaTableNonDeclaration = (node: ts.Node) =>
26+
new TranspileError(`Classes with the '@luaTable' decorator must be declared.`, node);
27+
28+
public static InvalidExtendsLuaTable = (node: ts.Node) =>
29+
new TranspileError(`Cannot extend classes with the decorator '@luaTable'.`, node);
30+
31+
public static InvalidInstanceOfLuaTable = (node: ts.Node) =>
32+
new TranspileError(`The instanceof operator cannot be used with a '@luaTable' class.`, node);
33+
34+
public static ForbiddenLuaTableUseException = (description: string, node: ts.Node) =>
35+
new TranspileError(`Invalid @luaTable usage: ${description}`, node);
36+
2137
public static HeterogeneousEnum = (node: ts.Node) => new TranspileError(
2238
`Invalid heterogeneous enum. Enums should either specify no member values, ` +
2339
`or specify values (of the same type) for all members.`,

test/translation/__snapshots__/transformation.spec.ts.snap

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,16 @@ exports[`Transformation (interfaceIndex) 1`] = `
296296
a.abc = \\"def\\""
297297
`;
298298

299+
exports[`Transformation (luaTable) 1`] = `
300+
"tbl = {}
301+
tbl.value = 5
302+
local value = tbl.value
303+
local tblLength = #tbl
304+
itbl.value = 5
305+
local ivalue = itbl.value
306+
local ilength = #tbl"
307+
`;
308+
299309
exports[`Transformation (methodRestArguments) 1`] = `
300310
"MyClass = {}
301311
MyClass.name = \\"MyClass\\"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/** @luaTable */
2+
declare class Table<K extends {} = {}, V = any> {
3+
public readonly length: number;
4+
public set(key: K, value: V): void;
5+
public get(key: K): V;
6+
}
7+
declare let tbl: Table;
8+
tbl = new Table();
9+
tbl.set("value", 5);
10+
const value = tbl.get("value");
11+
const tblLength = tbl.length;
12+
13+
/** @luaTable */
14+
declare interface InterfaceTable<K extends {} = {}, V = any> {
15+
readonly length: number;
16+
set(key: K, value: V): void;
17+
get(key: K): V;
18+
}
19+
20+
declare const itbl: InterfaceTable;
21+
itbl.set("value", 5);
22+
const ivalue = itbl.get("value");
23+
const ilength = tbl.length;

0 commit comments

Comments
 (0)