Skip to content

Commit 666e7eb

Browse files
authored
Merge pull request #144 from Perryvw/feature/fix-extension-issue
Reworked class transpilation and tests, should fix #142
2 parents b666d13 + 9a53d8c commit 666e7eb

31 files changed

+355
-271
lines changed

src/TSHelper.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,20 @@ export class TSHelper {
2727
return statements.some(statement => statement.kind === kind);
2828
}
2929

30+
public static getExtendedType(node: ts.ClassDeclaration, checker: ts.TypeChecker): ts.Type | undefined {
31+
if (node.heritageClauses) {
32+
for (const clause of node.heritageClauses) {
33+
if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
34+
const superType = checker.getTypeAtLocation(clause.types[0]);
35+
if (!this.isPureAbstractClass(superType, checker)) {
36+
return superType;
37+
}
38+
}
39+
}
40+
}
41+
return undefined;
42+
}
43+
3044
public static isFileModule(sourceFile: ts.SourceFile) {
3145
if (sourceFile) {
3246
// Vanilla ts flags files as external module if they have an import or

src/Transpiler.ts

Lines changed: 81 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export abstract class LuaTranspiler {
3737
public isModule: boolean;
3838
public sourceFile: ts.SourceFile;
3939
public loopStack: number[];
40+
public classStack: string[];
4041

4142
constructor(checker: ts.TypeChecker, options: ts.CompilerOptions, sourceFile: ts.SourceFile) {
4243
this.indent = "";
@@ -49,6 +50,7 @@ export abstract class LuaTranspiler {
4950
this.sourceFile = sourceFile;
5051
this.isModule = tsHelper.isFileModule(sourceFile);
5152
this.loopStack = [];
53+
this.classStack = [];
5254
}
5355

5456
public pushIndent(): void {
@@ -131,28 +133,19 @@ export abstract class LuaTranspiler {
131133
// Shadow exports if it already exists
132134
result += "local exports = exports or {}\n";
133135
}
134-
result += this.transpileBlock(this.sourceFile);
136+
137+
// Transpile content statements
138+
this.sourceFile.statements.forEach(s => result += this.transpileNode(s));
139+
135140
if (this.isModule) {
136141
result += "return exports\n";
137142
}
138143
return result;
139144
}
140145

141146
// Transpile a block
142-
public transpileBlock(node: ts.Node): string {
143-
let result = "";
144-
145-
if (ts.isBlock(node)) {
146-
node.statements.forEach(statement => {
147-
result += this.transpileNode(statement);
148-
});
149-
} else {
150-
node.forEachChild(child => {
151-
result += this.transpileNode(child);
152-
});
153-
}
154-
155-
return result;
147+
public transpileBlock(block: ts.Block): string {
148+
return block.statements.map(statement => this.transpileNode(statement)).join("");
156149
}
157150

158151
// Transpile a node of unknown kind.
@@ -857,10 +850,9 @@ export abstract class LuaTranspiler {
857850

858851
// Handle super calls properly
859852
if (node.expression.kind === ts.SyntaxKind.SuperKeyword) {
860-
callPath = this.transpileExpression(node.expression);
861-
params =
862-
this.transpileArguments(node.arguments, ts.createNode(ts.SyntaxKind.ThisKeyword) as ts.Expression);
863-
return `self.__base.constructor(${params})`;
853+
params = this.transpileArguments(node.arguments, ts.createNode(ts.SyntaxKind.ThisKeyword) as ts.Expression);
854+
const className = this.classStack[this.classStack.length - 1];
855+
return `${className}.__base.constructor(${params})`;
864856
}
865857

866858
callPath = this.transpileExpression(node.expression);
@@ -1288,60 +1280,17 @@ export abstract class LuaTranspiler {
12881280

12891281
// Transpile a class declaration
12901282
public transpileClass(node: ts.ClassDeclaration): string {
1291-
// Find extends class, ignore implements
1292-
let extendsType: ts.ExpressionWithTypeArguments | undefined;
1293-
let noClassOr = false;
1294-
if (node.heritageClauses) { node.heritageClauses.forEach(clause => {
1295-
if (clause.token === ts.SyntaxKind.ExtendsKeyword) {
1296-
const superType = this.checker.getTypeAtLocation(clause.types[0]);
1297-
// Ignore purely abstract types (decorated with /** @PureAbstract */)
1298-
if (!tsHelper.isPureAbstractClass(superType, this.checker)) {
1299-
extendsType = clause.types[0];
1300-
}
1301-
noClassOr = tsHelper.hasCustomDecorator(superType, this.checker, "!NoClassOr");
1302-
}
1303-
});
1304-
}
1305-
13061283
if (!node.name) {
1307-
throw new TranspileError("Unexpected Error: Node has no Name", node);
1284+
throw new TranspileError("Class declaration has no name.", node);
13081285
}
13091286

13101287
let className = node.name.escapedText as string;
1311-
let result = "";
13121288

1313-
// Skip header if this is an extension class
1289+
// Find out if this class is extension of exising class
13141290
const isExtension = tsHelper.isExtensionClass(this.checker.getTypeAtLocation(node), this.checker);
1315-
if (!isExtension) {
1316-
// Write class declaration
1317-
const classOr = noClassOr ? "" : `${className} or `;
1318-
if (!extendsType) {
1319-
result += this.indent + this.accessPrefix(node) + `${className} = ${classOr}{}\n`;
1320-
result += this.makeExport(className, node);
1321-
} else {
1322-
const baseName = (extendsType.expression as ts.Identifier).escapedText;
1323-
result += this.indent + this.accessPrefix(node) + `${className} = ${classOr}${baseName}.new()\n`;
1324-
result += this.makeExport(className, node);
1325-
}
1326-
result += this.indent + `${className}.__index = ${className}\n`;
1327-
if (extendsType) {
1328-
const baseName = (extendsType.expression as ts.Identifier).escapedText;
1329-
result += this.indent + `${className}.__base = ${baseName}\n`;
1330-
}
1331-
result += this.indent + `function ${className}.new(construct, ...)\n`;
1332-
result += this.indent + ` local instance = setmetatable({}, ${className})\n`;
1333-
result += this.indent + ` if construct and ${className}.constructor then `
1334-
+ `${className}.constructor(instance, ...) end\n`;
1335-
result += this.indent + ` return instance\n`;
1336-
result += this.indent + `end\n`;
1337-
} else {
1338-
// export empty table
1339-
result += this.makeExport(className, node, true);
1340-
// Overwrite the original className with the class we are overriding for extensions
1341-
if (extendsType) {
1342-
className = (extendsType.expression as ts.Identifier).escapedText as string;
1343-
}
1344-
}
1291+
1292+
// Get type that is extended
1293+
const extendsType = tsHelper.getExtendedType(node, this.checker);
13451294

13461295
// Get all properties with value
13471296
const properties = node.members.filter(ts.isPropertyDeclaration)
@@ -1352,6 +1301,20 @@ export abstract class LuaTranspiler {
13521301
const staticFields = properties.filter(isStatic);
13531302
const instanceFields = properties.filter(prop => !isStatic(prop));
13541303

1304+
let result = "";
1305+
1306+
if (!isExtension) {
1307+
result += this.transpileClassCreationMethods(node, instanceFields, extendsType);
1308+
} else {
1309+
// export empty table
1310+
result += this.makeExport(className, node, true);
1311+
}
1312+
1313+
// Overwrite the original className with the class we are overriding for extensions
1314+
if (isExtension && extendsType) {
1315+
className = extendsType.symbol.escapedName as string;
1316+
}
1317+
13551318
// Add static declarations
13561319
for (const field of staticFields) {
13571320
const fieldName = (field.name as ts.Identifier).escapedText;
@@ -1363,14 +1326,11 @@ export abstract class LuaTranspiler {
13631326
const constructor = node.members.filter(ts.isConstructorDeclaration)[0];
13641327
if (constructor) {
13651328
// Add constructor plus initialisation of instance fields
1366-
result += this.transpileConstructor(constructor, className, instanceFields);
1329+
result += this.transpileConstructor(constructor, className);
13671330
} else if (!isExtension) {
13681331
// Generate a constructor if none was defined
1369-
result += this.transpileConstructor(
1370-
ts.createConstructor([], [], [], ts.createBlock([], true)),
1371-
className,
1372-
instanceFields
1373-
);
1332+
result += this.transpileConstructor(ts.createConstructor([], [], [], ts.createBlock([], true)),
1333+
className);
13741334
}
13751335

13761336
// Transpile get accessors
@@ -1391,6 +1351,50 @@ export abstract class LuaTranspiler {
13911351
return result;
13921352
}
13931353

1354+
public transpileClassCreationMethods(node: ts.ClassDeclaration, instanceFields: ts.PropertyDeclaration[],
1355+
extendsType: ts.Type): string {
1356+
const className = node.name.escapedText as string;
1357+
1358+
const noClassOr = extendsType && tsHelper.hasCustomDecorator(extendsType, this.checker, "!NoClassOr");
1359+
1360+
let result = "";
1361+
1362+
// Write class declaration
1363+
const classOr = noClassOr ? "" : `${className} or `;
1364+
if (!extendsType) {
1365+
result += this.indent + this.accessPrefix(node) + `${className} = ${classOr}{}\n`;
1366+
result += this.makeExport(className, node);
1367+
} else {
1368+
const baseName = extendsType.symbol.escapedName;
1369+
result += this.indent + this.accessPrefix(node) + `${className} = ${classOr}${baseName}.new()\n`;
1370+
result += this.makeExport(className, node);
1371+
}
1372+
result += this.indent + `${className}.__index = ${className}\n`;
1373+
if (extendsType) {
1374+
const baseName = extendsType.symbol.escapedName;
1375+
result += this.indent + `${className}.__base = ${baseName}\n`;
1376+
}
1377+
result += this.indent + `function ${className}.new(construct, ...)\n`;
1378+
result += this.indent + ` local instance = setmetatable({}, ${className})\n`;
1379+
result += this.indent + ` if construct and ${className}.constructor then `
1380+
+ `${className}.constructor(instance, ...) end\n`;
1381+
1382+
for (const f of instanceFields) {
1383+
// Get identifier
1384+
const fieldIdentifier = f.name as ts.Identifier;
1385+
const fieldName = fieldIdentifier.escapedText;
1386+
1387+
const value = this.transpileExpression(f.initializer);
1388+
1389+
result += this.indent + ` instance.${fieldName} = ${value}\n`;
1390+
}
1391+
1392+
result += this.indent + ` return instance\n`;
1393+
result += this.indent + `end\n`;
1394+
1395+
return result;
1396+
}
1397+
13941398
public transpileGetAccessorDeclaration(getAccessor: ts.GetAccessorDeclaration, className: string): string {
13951399
const name = (getAccessor.name as ts.Identifier).escapedText;
13961400

@@ -1425,8 +1429,7 @@ export abstract class LuaTranspiler {
14251429
}
14261430

14271431
public transpileConstructor(node: ts.ConstructorDeclaration,
1428-
className: string,
1429-
instanceFields: ts.PropertyDeclaration[]): string {
1432+
className: string): string {
14301433
const extraInstanceFields = [];
14311434

14321435
const parameters = ["self"];
@@ -1446,19 +1449,11 @@ export abstract class LuaTranspiler {
14461449
result += this.indent + ` self.${f} = ${f}\n`;
14471450
}
14481451

1449-
for (const f of instanceFields) {
1450-
// Get identifier
1451-
const fieldIdentifier = f.name as ts.Identifier;
1452-
const fieldName = fieldIdentifier.escapedText;
1453-
1454-
const value = this.transpileExpression(f.initializer);
1455-
1456-
result += this.indent + ` self.${fieldName} = ${value}\n`;
1457-
}
1458-
14591452
// Transpile constructor body
14601453
this.pushIndent();
1454+
this.classStack.push(className);
14611455
result += this.transpileBlock(node.body);
1456+
this.classStack.pop();
14621457
this.popIndent();
14631458

14641459
return result + this.indent + "end\n";
@@ -1512,7 +1507,7 @@ export abstract class LuaTranspiler {
15121507
let result = `function(${paramNames.join(",")})\n`;
15131508
this.pushIndent();
15141509
result += this.transpileParameterDefaultValues(defaultValueParams);
1515-
result += this.transpileBlock(node.body);
1510+
result += this.transpileBlock(node.body as ts.Block);
15161511
this.popIndent();
15171512
return result + this.indent + "end\n";
15181513
} else {

test/translation/lua/class.lua

Lines changed: 0 additions & 10 deletions
This file was deleted.

test/translation/lua/classConstructorAssignment.lua

Lines changed: 0 additions & 10 deletions
This file was deleted.

test/translation/lua/classDefaultConstructor.lua

Lines changed: 0 additions & 9 deletions
This file was deleted.

test/translation/lua/classEmptyConstructor.lua

Lines changed: 0 additions & 10 deletions
This file was deleted.

test/translation/lua/classEmptyNew.lua

Lines changed: 0 additions & 10 deletions
This file was deleted.

test/translation/lua/classInstanceCall1.lua

Lines changed: 0 additions & 1 deletion
This file was deleted.

test/translation/lua/classInstanceCall2.lua

Lines changed: 0 additions & 1 deletion
This file was deleted.

test/translation/lua/classInstanceCall3.lua

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)