diff --git a/src/LuaAST.ts b/src/LuaAST.ts index 2639dbd85..a973354ca 100644 --- a/src/LuaAST.ts +++ b/src/LuaAST.ts @@ -813,6 +813,10 @@ export function createIdentifier( return expression; } +export function cloneIdentifier(identifier: Identifier): Identifier { + return createIdentifier(identifier.text, undefined, identifier.symbolId); +} + export function createAnnonymousIdentifier(tsOriginal?: ts.Node, parent?: Node): Identifier { const expression = createNode(SyntaxKind.Identifier, tsOriginal, parent) as Identifier; expression.text = "____"; diff --git a/src/LuaTransformer.ts b/src/LuaTransformer.ts index 5aa73cde1..cbcccbe42 100644 --- a/src/LuaTransformer.ts +++ b/src/LuaTransformer.ts @@ -39,8 +39,8 @@ interface Scope { export class LuaTransformer { public luaKeywords: Set = new Set([ - "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", - "in", "local", "nil", "not", "or", "repeat", "return", "then", "until", "while", + "and", "break", "do", "else", "elseif", "end", "false", "for", "function", "if", "in", "local", "new", "nil", + "not", "or", "repeat", "return", "self", "then", "until", "while", ]); private isStrict = true; @@ -52,7 +52,7 @@ export class LuaTransformer { private currentSourceFile?: ts.SourceFile; private currentNamespace: ts.ModuleDeclaration; - private classStack: tstl.Identifier[]; + private classStack: ts.ClassLikeDeclaration[]; private scopeStack: Scope[]; private genVarCounter: number; @@ -278,6 +278,8 @@ export class LuaTransformer { nameOverride?: tstl.Identifier ): tstl.Statement[] { + this.classStack.push(statement); + let className = statement.name ? this.transformIdentifier(statement.name) : nameOverride; if (!className) { throw TSTLErrors.MissingClassName(statement); @@ -294,9 +296,22 @@ export class LuaTransformer { throw TSTLErrors.InvalidExtensionMetaExtension(statement); } + if ((isExtension || isMetaExtension) && this.isIdentifierExported(className)) { + // Cannot export extension classes + throw TSTLErrors.InvalidExportsExtension(statement); + } + // Get type that is extended const extendsType = tsHelper.getExtendedType(statement, this.checker); + if (!(isExtension || isMetaExtension) && extendsType) { + // Non-extensions cannot extend extension classes + const extendsDecorators = tsHelper.getCustomDecorators(extendsType, this.checker); + if (extendsDecorators.has(DecoratorKind.Extension) || extendsDecorators.has(DecoratorKind.MetaExtension)) { + throw TSTLErrors.InvalidExtendsExtension(statement); + } + } + // Get all properties with value const properties = statement.members.filter(ts.isPropertyDeclaration).filter(member => member.initializer); @@ -346,7 +361,6 @@ export class LuaTransformer { const classCreationMethods = this.createClassCreationMethods( statement, className, - instanceFields, extendsType ); result.push(...classCreationMethods); @@ -358,7 +372,7 @@ export class LuaTransformer { // className["fieldName"] const classField = tstl.createTableIndexExpression( - className, + tstl.cloneIdentifier(className), fieldName); // className["fieldName"] = value; @@ -374,7 +388,7 @@ export class LuaTransformer { const value = this.transformExpression(field.initializer); const classField = tstl.createTableIndexExpression( - this.addExportToIdentifier(className), + this.addExportToIdentifier(tstl.cloneIdentifier(className)), fieldName ); @@ -387,18 +401,47 @@ export class LuaTransformer { } // Find first constructor with body - const constructor = statement.members - .filter(n => ts.isConstructorDeclaration(n) && n.body)[0] as ts.ConstructorDeclaration; - if (constructor) { - // Add constructor plus initialization of instance fields - result.push(this.transformConstructor(constructor, className, statement)); - } else if (!isExtension && !extendsType) { - // Generate a constructor if none was defined - result.push(this.transformConstructor( - ts.createConstructor([], [], [], ts.createBlock([], true)), - className, - statement - )); + if (!isExtension && !isMetaExtension) { + const constructor = statement.members + .filter(n => ts.isConstructorDeclaration(n) && n.body)[0] as ts.ConstructorDeclaration; + if (constructor) { + // Add constructor plus initialization of instance fields + result.push(this.transformConstructor(constructor, className, instanceFields, statement)); + } else if (!extendsType) { + // Generate a constructor if none was defined in a base class + result.push(this.transformConstructor( + ts.createConstructor([], [], [], ts.createBlock([], true)), + className, + instanceFields, + statement + )); + } else if (instanceFields.length > 0) { + // Generate a constructor if none was defined in a class with instance fields that need initialization + // className.prototype.____constructor = function(self, ...) + // baseClassName.prototype.____constructor(self, ...) + // ... + const constructorBody = this.transformClassInstanceFields(instanceFields); + const superCall = tstl.createExpressionStatement( + tstl.createCallExpression( + tstl.createTableIndexExpression( + this.transformSuperKeyword(ts.createSuper()), + tstl.createStringLiteral("____constructor") + ), + [this.createSelfIdentifier(), tstl.createDotsLiteral()] + ) + ); + constructorBody.unshift(superCall); + const constructorFunction = tstl.createFunctionExpression( + tstl.createBlock(constructorBody), + [this.createSelfIdentifier()], + tstl.createDotsLiteral() + ); + result.push(tstl.createAssignmentStatement( + this.createConstructorName(className), + constructorFunction, + statement + )); + } } // Transform get accessors @@ -416,14 +459,17 @@ export class LuaTransformer { result.push(this.transformMethodDeclaration(method, className)); }); + this.classStack.pop(); + return result; } public createClassCreationMethods( statement: ts.ClassLikeDeclarationBase, className: tstl.Identifier, - instanceFields: ts.PropertyDeclaration[], - extendsType: ts.Type): tstl.Statement[] { + extendsType: ts.Type + ): tstl.Statement[] + { let noClassOr = false; if (extendsType) { const decorators = tsHelper.getCustomDecorators(extendsType, this.checker); @@ -432,140 +478,179 @@ export class LuaTransformer { const result: tstl.Statement[] = []; - const classNameWithExport = this.addExportToIdentifier(className); - - // Write class declaration - if (extendsType) { - const extendedTypeNode = tsHelper.getExtendedTypeNode(statement, this.checker); - const baseName = this.transformExpression(extendedTypeNode.expression); - - // baseName.new - const newIndex = tstl.createTableIndexExpression(baseName, tstl.createStringLiteral("new")); - - // baseName.new() - let rhs: tstl.Expression = tstl.createCallExpression(newIndex, []); - - if (!noClassOr) { - // className or baseName.new() - rhs = tstl.createBinaryExpression(classNameWithExport, rhs, tstl.SyntaxKind.OrOperator); - } + // className = className or {} + let classTable: tstl.Expression = tstl.createTableExpression(); + if (!noClassOr) { + classTable = tstl.createBinaryExpression( + this.addExportToIdentifier(className), // Use original identifier node in declaration + classTable, + tstl.SyntaxKind.OrOperator + ); + } - // (local) className = className or baseName.new() - // (local) className = baseName.new() - // exports.className = baseName.new() - const classVar = this.createLocalOrExportedOrGlobalDeclaration(className, rhs, statement); + const classVar = this.createLocalOrExportedOrGlobalDeclaration(className, classTable, statement); + result.push(...classVar); - result.push(...classVar); - } else { - // {} - let rhs: tstl.Expression = tstl.createTableExpression(); + const createClassNameWithExport = () => this.addExportToIdentifier(tstl.cloneIdentifier(className)); - if (!noClassOr) { - // className or {} - rhs = tstl.createBinaryExpression(classNameWithExport, rhs, tstl.SyntaxKind.OrOperator); - } - - // (local) className = className or {} - // (local) className = {} - // exports.className = {} - const classVar = this.createLocalOrExportedOrGlobalDeclaration(className, rhs, statement); + // className.__index = className + const classIndex = tstl.createTableIndexExpression( + createClassNameWithExport(), + tstl.createStringLiteral("__index") + ); + const assignClassIndex = tstl.createAssignmentStatement(classIndex, createClassNameWithExport(), statement); + result.push(assignClassIndex); - result.push(...classVar); - } + // className.prototype = className.prototype or {} + const createClassPrototype = () => tstl.createTableIndexExpression( + createClassNameWithExport(), + tstl.createStringLiteral("prototype") + ); + const classPrototypeTable = noClassOr + ? tstl.createTableExpression() + : tstl.createBinaryExpression( + createClassPrototype(), + tstl.createTableExpression(), + tstl.SyntaxKind.OrOperator + ); + const assignClassPrototype = tstl.createAssignmentStatement(createClassPrototype(), classPrototypeTable); + result.push(assignClassPrototype); - // className.__index - const classIndex = tstl.createTableIndexExpression(classNameWithExport, tstl.createStringLiteral("__index")); - // className.__index = className - const assignClassIndex = tstl.createAssignmentStatement(classIndex, classNameWithExport, statement); + // className.prototype.__index = className.prototype + const classPrototypeIndex = tstl.createTableIndexExpression( + createClassPrototype(), + tstl.createStringLiteral("__index") + ); + const assignClassPrototypeIndex = tstl.createAssignmentStatement(classPrototypeIndex, createClassPrototype()); + result.push(assignClassPrototypeIndex); - result.push(assignClassIndex); + // className.prototype.constructor = className + const classPrototypeConstructor = tstl.createTableIndexExpression( + createClassPrototype(), + tstl.createStringLiteral("constructor") + ); + const assignClassPrototypeConstructor = tstl.createAssignmentStatement( + classPrototypeConstructor, + className, + statement + ); + result.push(assignClassPrototypeConstructor); if (extendsType) { const extendedTypeNode = tsHelper.getExtendedTypeNode(statement, this.checker); const baseName = this.transformExpression(extendedTypeNode.expression); - // className.__base = baseName - const classBase = tstl.createTableIndexExpression(classNameWithExport, tstl.createStringLiteral("__base")); + // className.____super = baseName + const createClassBase = () => tstl.createTableIndexExpression( + createClassNameWithExport(), + tstl.createStringLiteral("____super") + ); + const assignClassBase = tstl.createAssignmentStatement(createClassBase(), baseName, statement); + result.push(assignClassBase); - const assignClassBase = tstl.createAssignmentStatement(classBase, baseName, statement); + // setmetatable(className, className.____super) + const setClassMetatable = tstl.createExpressionStatement( + tstl.createCallExpression( + tstl.createIdentifier("setmetatable"), + [createClassNameWithExport(), createClassBase()] + ) + ); + result.push(setClassMetatable); - result.push(assignClassBase); + // setmetatable(className.prototype, className.____super.prototype) + const basePrototype = tstl.createTableIndexExpression( + createClassBase(), + tstl.createStringLiteral("prototype") + ); + const setClassPrototypeMetatable = tstl.createExpressionStatement( + tstl.createCallExpression( + tstl.createIdentifier("setmetatable"), + [createClassPrototype(), basePrototype] + ) + ); + result.push(setClassPrototypeMetatable); } const newFuncStatements: tstl.Statement[] = []; - // local self = setmetatable({}, className) + // local self = setmetatable({}, className.prototype) const assignSelf = tstl.createVariableDeclarationStatement( this.createSelfIdentifier(), tstl.createCallExpression( tstl.createIdentifier("setmetatable"), - [tstl.createTableExpression(), classNameWithExport] + [tstl.createTableExpression(), createClassPrototype()] ) ); - newFuncStatements.push(assignSelf); - for (const f of instanceFields) { - // Get identifier - const fieldName = this.transformPropertyName(f.name); - - const value = this.transformExpression(f.initializer); - - // self[fieldName] - const selfIndex = tstl.createTableIndexExpression(this.createSelfIdentifier(), fieldName); - - // self[fieldName] = value - const assignClassField = tstl.createAssignmentStatement(selfIndex, value); - - newFuncStatements.push(assignClassField); - } - - /* - if construct and className.constructor then - className.constructor(self, ...) - end - */ - const ifConstructor = tstl.createIfStatement( - tstl.createBinaryExpression( - tstl.createIdentifier("construct"), - tstl.createTableIndexExpression(classNameWithExport, tstl.createStringLiteral("constructor")), - tstl.SyntaxKind.AndOperator), - tstl.createBlock([ - tstl.createExpressionStatement(tstl.createCallExpression( - tstl.createTableIndexExpression(classNameWithExport, tstl.createStringLiteral("constructor")), - [this.createSelfIdentifier(), tstl.createDotsLiteral()])), - ])); - - newFuncStatements.push(ifConstructor); + // self:____constructor(...) + const callConstructor = tstl.createExpressionStatement( + tstl.createMethodCallExpression( + this.createSelfIdentifier(), + tstl.createIdentifier("____constructor"), + [tstl.createDotsLiteral()] + ) + ); + newFuncStatements.push(callConstructor); // return self const returnSelf = tstl.createReturnStatement([this.createSelfIdentifier()]); - newFuncStatements.push(returnSelf); // function className.new(construct, ...) ... end // or function export.className.new(construct, ...) ... end const newFunc = tstl.createAssignmentStatement( tstl.createTableIndexExpression( - classNameWithExport, + createClassNameWithExport(), tstl.createStringLiteral("new")), tstl.createFunctionExpression( tstl.createBlock(newFuncStatements), - [tstl.createIdentifier("construct")], + undefined, tstl.createDotsLiteral(), undefined, statement ) ); - result.push(newFunc); return result; } + public transformClassInstanceFields(instanceFields: ts.PropertyDeclaration[]): tstl.Statement[] { + const statements: tstl.Statement[] = []; + + for (const f of instanceFields) { + // Get identifier + const fieldName = this.transformPropertyName(f.name); + + const value = this.transformExpression(f.initializer); + + // self[fieldName] + const selfIndex = tstl.createTableIndexExpression(this.createSelfIdentifier(), fieldName); + + // self[fieldName] = value + const assignClassField = tstl.createAssignmentStatement(selfIndex, value); + + statements.push(assignClassField); + } + + return statements; + } + + public createConstructorName(className: tstl.Identifier): tstl.TableIndexExpression { + return tstl.createTableIndexExpression( + tstl.createTableIndexExpression( + this.addExportToIdentifier(tstl.cloneIdentifier(className)), + tstl.createStringLiteral("prototype") + ), + tstl.createStringLiteral("____constructor") + ); + } + public transformConstructor( statement: ts.ConstructorDeclaration, className: tstl.Identifier, + instanceFields: ts.PropertyDeclaration[], classDeclaration: ts.ClassLikeDeclaration ): tstl.AssignmentStatement { @@ -574,14 +659,11 @@ export class LuaTransformer { return undefined; } + const bodyStatements: tstl.Statement[] = this.transformClassInstanceFields(instanceFields); + // Check for field declarations in constructor const constructorFieldsDeclarations = statement.parameters.filter(p => p.modifiers !== undefined); - // Transform constructor body - this.classStack.push(className); - - const bodyStatements: tstl.Statement[] = []; - // Add in instance field declarations for (const declaration of constructorFieldsDeclarations) { const declarationName = this.transformIdentifier(declaration.name as ts.Identifier); @@ -610,7 +692,7 @@ export class LuaTransformer { } } - // function className.constructor(params) ... end + // function className.constructor(self, params) ... end const [params, dotsLiteral, restParamName] = this.transformParameters( statement.parameters, @@ -623,13 +705,10 @@ export class LuaTransformer { const block: tstl.Block = tstl.createBlock(bodyStatements); const result = tstl.createAssignmentStatement( - tstl.createTableIndexExpression( - this.addExportToIdentifier(className), - tstl.createStringLiteral("constructor")), + this.createConstructorName(className), tstl.createFunctionExpression(block, params, dotsLiteral, restParamName, undefined, undefined), - statement); - - this.classStack.pop(); + statement + ); return result; } @@ -650,7 +729,10 @@ export class LuaTransformer { return tstl.createAssignmentStatement( tstl.createTableIndexExpression( - this.addExportToIdentifier(className), + tstl.createTableIndexExpression( + this.addExportToIdentifier(tstl.cloneIdentifier(className)), + tstl.createStringLiteral("prototype") + ), tstl.createStringLiteral("get__" + name.text)), accessorFunction ); @@ -676,7 +758,10 @@ export class LuaTransformer { return tstl.createAssignmentStatement( tstl.createTableIndexExpression( - this.addExportToIdentifier(className), + tstl.createTableIndexExpression( + this.addExportToIdentifier(tstl.cloneIdentifier(className)), + tstl.createStringLiteral("prototype") + ), tstl.createStringLiteral("set__" + name.text)), accessorFunction ); @@ -711,10 +796,15 @@ export class LuaTransformer { restParamName ); - const parent = node.parent as ts.ClassLikeDeclaration; + const isStatic = node.modifiers && node.modifiers.some(m => m.kind === ts.SyntaxKind.StaticKeyword); + const classNameWithExport = this.addExportToIdentifier(tstl.cloneIdentifier(className)); + const methodTable = isStatic + ? classNameWithExport + : tstl.createTableIndexExpression(classNameWithExport, tstl.createStringLiteral("prototype")); + return tstl.createAssignmentStatement( tstl.createTableIndexExpression( - this.addExportToIdentifier(className), + methodTable, methodName), functionExpression, node @@ -1997,6 +2087,14 @@ export class LuaTransformer { ); case ts.SyntaxKind.InstanceOfKeyword: + const decorators = tsHelper.getCustomDecorators( + this.checker.getTypeAtLocation(expression.right), + this.checker + ); + if (decorators.has(DecoratorKind.Extension) || decorators.has(DecoratorKind.MetaExtension)) { + // Cannot use instanceof on extension classes + throw TSTLErrors.InvalidInstanceOfExtension(expression); + } return this.transformLuaLibFunction(LuaLibFeature.InstanceOf, lhs, rhs); case ts.SyntaxKind.CommaToken: @@ -2579,7 +2677,7 @@ export class LuaTransformer { const name = this.transformExpression(node.expression); const sig = this.checker.getResolvedSignature(node); const params = node.arguments - ? this.transformArguments(node.arguments, sig, ts.createTrue()) + ? this.transformArguments(node.arguments, sig) : [tstl.createBooleanLiteral(true)]; const type = this.checker.getTypeAtLocation(node); @@ -2618,11 +2716,21 @@ export class LuaTransformer { } public transformSuperKeyword(expression: ts.SuperExpression): tstl.Expression { - return tstl.createTableIndexExpression( - this.createSelfIdentifier(), - tstl.createStringLiteral("__base"), - expression - ); + const classDeclaration = this.classStack[this.classStack.length - 1]; + const extendsExpression = tsHelper.getExtendedTypeNode(classDeclaration, this.checker).expression; + let baseClassName: tstl.IdentifierOrTableIndexExpression; + if (ts.isIdentifier(extendsExpression)) { + // Use "baseClassName" if base is a simple identifier + baseClassName = this.addExportToIdentifier(this.transformIdentifier(extendsExpression)); + } else { + // Use "className.____super" if the base is not a simple identifier + baseClassName = tstl.createTableIndexExpression( + this.addExportToIdentifier(this.transformIdentifier(classDeclaration.name)), + tstl.createStringLiteral("____super"), + expression + ); + } + return tstl.createTableIndexExpression(baseClassName, tstl.createStringLiteral("prototype")); } public transformCallExpression(node: ts.CallExpression): tstl.Expression { @@ -2653,14 +2761,11 @@ export class LuaTransformer { // Handle super calls properly if (node.expression.kind === ts.SyntaxKind.SuperKeyword) { parameters = this.transformArguments(node.arguments, signature, ts.createThis()); - const classIdentifier = this.classStack[this.classStack.length - 1]; - const baseIdentifier = tstl.createStringLiteral("__base"); - const constructorIdentifier = tstl.createStringLiteral("constructor"); return tstl.createCallExpression( tstl.createTableIndexExpression( - tstl.createTableIndexExpression(this.addExportToIdentifier(classIdentifier), baseIdentifier), - constructorIdentifier + this.transformSuperKeyword(ts.createSuper()), + tstl.createStringLiteral("____constructor") ), parameters ); diff --git a/src/TSTLErrors.ts b/src/TSTLErrors.ts index b5719bc90..5adc79b03 100644 --- a/src/TSTLErrors.ts +++ b/src/TSTLErrors.ts @@ -25,10 +25,19 @@ export class TSTLErrors { new TranspileError(`${name} expects ${expected} argument(s) but got ${got}.`, node); public static InvalidExtensionMetaExtension = (node: ts.Node) => - new TranspileError(`Cannot use both '!Extension' and '!MetaExtension' decorators on the same class.`, node); + new TranspileError(`Cannot use both '@extension' and '@metaExtension' decorators on the same class.`, node); public static InvalidNewExpressionOnExtension = (node: ts.Node) => - new TranspileError(`Cannot construct classes with decorator '!Extension' or '!MetaExtension'.`, node); + new TranspileError(`Cannot construct classes with decorator '@extension' or '@metaExtension'.`, node); + + public static InvalidExtendsExtension = (node: ts.Node) => + new TranspileError(`Cannot extend classes with decorator '@extension' or '@metaExtension'.`, node); + + public static InvalidExportsExtension = (node: ts.Node) => + new TranspileError(`Cannot export classes with decorator '@extension' or '@metaExtension'.`, node); + + public static InvalidInstanceOfExtension = (node: ts.Node) => + new TranspileError(`Cannot use instanceof on classes with decorator '@extension' or '@metaExtension'.`, node); public static InvalidPropertyCall = (node: ts.Node) => new TranspileError(`Tried to transpile a non-property call as property call.`, node); diff --git a/src/lualib/InstanceOf.ts b/src/lualib/InstanceOf.ts index 3b6a12dab..c23c3604a 100644 --- a/src/lualib/InstanceOf.ts +++ b/src/lualib/InstanceOf.ts @@ -1,14 +1,20 @@ interface LuaClass { - __index: LuaClass; - __base: LuaClass; + ____super?: LuaClass; } -function __TS__InstanceOf(obj: LuaClass, classTbl: LuaClass): boolean { - while (obj !== undefined) { - if (obj.__index === classTbl) { - return true; +interface LuaObject { + constructor: LuaClass; +} + +function __TS__InstanceOf(obj: LuaObject, classTbl: LuaClass): boolean { + if (obj !== undefined) { + let luaClass = obj.constructor; + while (luaClass !== undefined) { + if (luaClass === classTbl) { + return true; + } + luaClass = luaClass.____super; } - obj = obj.__base; } return false; } diff --git a/test/translation/lua/classExtension1.lua b/test/translation/lua/classExtension1.lua index ff9e5bb5d..6f9f350b2 100644 --- a/test/translation/lua/classExtension1.lua +++ b/test/translation/lua/classExtension1.lua @@ -1,2 +1,2 @@ -MyClass.myFunction = function(self) +MyClass.prototype.myFunction = function(self) end; diff --git a/test/translation/lua/classExtension2.lua b/test/translation/lua/classExtension2.lua index 94e8f655f..cc6622932 100644 --- a/test/translation/lua/classExtension2.lua +++ b/test/translation/lua/classExtension2.lua @@ -1,2 +1,2 @@ -TestClass.myFunction = function(self) +TestClass.prototype.myFunction = function(self) end; diff --git a/test/translation/lua/classExtension3.lua b/test/translation/lua/classExtension3.lua index 008d45956..2a476b215 100644 --- a/test/translation/lua/classExtension3.lua +++ b/test/translation/lua/classExtension3.lua @@ -1,4 +1,4 @@ -RenamedTestClass.myFunction = function(self) +RenamedTestClass.prototype.myFunction = function(self) end; -RenamedMyClass.myFunction = function(self) +RenamedMyClass.prototype.myFunction = function(self) end; diff --git a/test/translation/lua/classExtension4.lua b/test/translation/lua/classExtension4.lua index 8fcb73124..07317d0f0 100644 --- a/test/translation/lua/classExtension4.lua +++ b/test/translation/lua/classExtension4.lua @@ -1,4 +1,4 @@ MyClass.test = "test"; MyClass.testP = "testP"; -MyClass.myFunction = function(self) +MyClass.prototype.myFunction = function(self) end; diff --git a/test/translation/lua/classPureAbstract.lua b/test/translation/lua/classPureAbstract.lua index 63d61f0db..70ba4efb3 100644 --- a/test/translation/lua/classPureAbstract.lua +++ b/test/translation/lua/classPureAbstract.lua @@ -1,11 +1,12 @@ ClassB = ClassB or {}; ClassB.__index = ClassB; -ClassB.new = function(construct, ...) - local self = setmetatable({}, ClassB); - if construct and ClassB.constructor then - ClassB.constructor(self, ...); - end +ClassB.prototype = ClassB.prototype or {}; +ClassB.prototype.__index = ClassB.prototype; +ClassB.prototype.constructor = ClassB; +ClassB.new = function(...) + local self = setmetatable({}, ClassB.prototype); + self:____constructor(...); return self; end; -ClassB.constructor = function(self) +ClassB.prototype.____constructor = function(self) end; diff --git a/test/translation/lua/getSetAccessors.lua b/test/translation/lua/getSetAccessors.lua index 9fe62bbb6..c1c16ff51 100644 --- a/test/translation/lua/getSetAccessors.lua +++ b/test/translation/lua/getSetAccessors.lua @@ -1,21 +1,22 @@ MyClass = MyClass or {}; MyClass.__index = MyClass; -MyClass.new = function(construct, ...) - local self = setmetatable({}, MyClass); - if construct and MyClass.constructor then - MyClass.constructor(self, ...); - end +MyClass.prototype = MyClass.prototype or {}; +MyClass.prototype.__index = MyClass.prototype; +MyClass.prototype.constructor = MyClass; +MyClass.new = function(...) + local self = setmetatable({}, MyClass.prototype); + self:____constructor(...); return self; end; -MyClass.constructor = function(self) +MyClass.prototype.____constructor = function(self) end; -MyClass.get__field = function(self) +MyClass.prototype.get__field = function(self) return self._field + 4; end; -MyClass.set__field = function(self, v) +MyClass.prototype.set__field = function(self, v) self._field = v * 2; end; -local instance = MyClass.new(true); +local instance = MyClass.new(); instance:set__field(4); local b = instance:get__field(); local c = (4 + instance:get__field()) * 3; diff --git a/test/translation/lua/methodRestArguments.lua b/test/translation/lua/methodRestArguments.lua index 236560407..0f2ba1555 100644 --- a/test/translation/lua/methodRestArguments.lua +++ b/test/translation/lua/methodRestArguments.lua @@ -1,14 +1,15 @@ MyClass = MyClass or {}; MyClass.__index = MyClass; -MyClass.new = function(construct, ...) - local self = setmetatable({}, MyClass); - if construct and MyClass.constructor then - MyClass.constructor(self, ...); - end +MyClass.prototype = MyClass.prototype or {}; +MyClass.prototype.__index = MyClass.prototype; +MyClass.prototype.constructor = MyClass; +MyClass.new = function(...) + local self = setmetatable({}, MyClass.prototype); + self:____constructor(...); return self; end; -MyClass.constructor = function(self) +MyClass.prototype.____constructor = function(self) end; -MyClass.varargsFunction = function(self, a, ...) +MyClass.prototype.varargsFunction = function(self, a, ...) local b = ({...}); end; diff --git a/test/translation/lua/modulesClassExport.lua b/test/translation/lua/modulesClassExport.lua index a339f9482..4f145ed02 100644 --- a/test/translation/lua/modulesClassExport.lua +++ b/test/translation/lua/modulesClassExport.lua @@ -1,13 +1,14 @@ local exports = exports or {}; exports.TestClass = exports.TestClass or {}; exports.TestClass.__index = exports.TestClass; -exports.TestClass.new = function(construct, ...) - local self = setmetatable({}, exports.TestClass); - if construct and exports.TestClass.constructor then - exports.TestClass.constructor(self, ...); - end +exports.TestClass.prototype = exports.TestClass.prototype or {}; +exports.TestClass.prototype.__index = exports.TestClass.prototype; +exports.TestClass.prototype.constructor = TestClass; +exports.TestClass.new = function(...) + local self = setmetatable({}, exports.TestClass.prototype); + self:____constructor(...); return self; end; -exports.TestClass.constructor = function(self) +exports.TestClass.prototype.____constructor = function(self) end; return exports; diff --git a/test/translation/lua/modulesClassWithMemberExport.lua b/test/translation/lua/modulesClassWithMemberExport.lua index 655d142c8..40a5d8539 100644 --- a/test/translation/lua/modulesClassWithMemberExport.lua +++ b/test/translation/lua/modulesClassWithMemberExport.lua @@ -1,15 +1,16 @@ local exports = exports or {}; exports.TestClass = exports.TestClass or {}; exports.TestClass.__index = exports.TestClass; -exports.TestClass.new = function(construct, ...) - local self = setmetatable({}, exports.TestClass); - if construct and exports.TestClass.constructor then - exports.TestClass.constructor(self, ...); - end +exports.TestClass.prototype = exports.TestClass.prototype or {}; +exports.TestClass.prototype.__index = exports.TestClass.prototype; +exports.TestClass.prototype.constructor = TestClass; +exports.TestClass.new = function(...) + local self = setmetatable({}, exports.TestClass.prototype); + self:____constructor(...); return self; end; -exports.TestClass.constructor = function(self) +exports.TestClass.prototype.____constructor = function(self) end; -exports.TestClass.memberFunc = function(self) +exports.TestClass.prototype.memberFunc = function(self) end; return exports; diff --git a/test/translation/lua/namespaceMerge.lua b/test/translation/lua/namespaceMerge.lua index e585c3c83..3c515447c 100644 --- a/test/translation/lua/namespaceMerge.lua +++ b/test/translation/lua/namespaceMerge.lua @@ -1,24 +1,25 @@ MergedClass = MergedClass or {}; MergedClass.__index = MergedClass; -MergedClass.new = function(construct, ...) - local self = setmetatable({}, MergedClass); - self.propertyFunc = function(____) - end; - if construct and MergedClass.constructor then - MergedClass.constructor(self, ...); - end +MergedClass.prototype = MergedClass.prototype or {}; +MergedClass.prototype.__index = MergedClass.prototype; +MergedClass.prototype.constructor = MergedClass; +MergedClass.new = function(...) + local self = setmetatable({}, MergedClass.prototype); + self:____constructor(...); return self; end; -MergedClass.constructor = function(self) +MergedClass.prototype.____constructor = function(self) + self.propertyFunc = function(____) + end; end; MergedClass.staticMethodA = function(self) end; MergedClass.staticMethodB = function(self) self:staticMethodA(); end; -MergedClass.methodA = function(self) +MergedClass.prototype.methodA = function(self) end; -MergedClass.methodB = function(self) +MergedClass.prototype.methodB = function(self) self:methodA(); self:propertyFunc(); end; @@ -27,7 +28,7 @@ do MergedClass.namespaceFunc = function() end; end -local mergedClass = MergedClass.new(true); +local mergedClass = MergedClass.new(); mergedClass:methodB(); mergedClass:propertyFunc(); MergedClass:staticMethodB(); diff --git a/test/unit/assignments.spec.ts b/test/unit/assignments.spec.ts index eb1a13a29..dda85cc20 100644 --- a/test/unit/assignments.spec.ts +++ b/test/unit/assignments.spec.ts @@ -152,7 +152,7 @@ export class AssignmentTests { + `let [a,b] = jkl.abc();`; const lua = util.transpileString(code); - Expect(lua).toBe("local jkl = def.new(true);\nlocal a, b = jkl:abc();"); + Expect(lua).toBe("local jkl = def.new();\nlocal a, b = jkl:abc();"); } @Test("TupleReturn functional") diff --git a/test/unit/class.spec.ts b/test/unit/class.spec.ts index 384d12fdd..5592933e9 100644 --- a/test/unit/class.spec.ts +++ b/test/unit/class.spec.ts @@ -1,7 +1,8 @@ -import { Expect, Test } from "alsatian"; +import { Expect, Test, TestCase } from "alsatian"; import * as ts from "typescript"; import * as util from "../src/util"; +import { TranspileError } from "../../src/TranspileError"; export class ClassTests { @@ -277,6 +278,29 @@ export class ClassTests { Expect(result).toBe(10); } + @Test("classSuperSkip") + public classSuperSkip(): void { + const result = util.transpileAndExecute( + `class a { + public field: number = 4; + constructor(n: number) { + this.field = n; + } + } + class b extends a { + } + class c extends b { + constructor() { + super(5); + } + } + return new c().field;` + ); + + // Assert + Expect(result).toBe(5); + } + @Test("renamedClassExtends") public renamedClassExtends(): void { @@ -603,6 +627,68 @@ export class ClassTests { Expect(result).toBe(10); } + @Test("CallSuperExpressionMethod") + public callSuperExpressionMethod(): void { + const result = util.transpileAndExecute( + `let i = 0; + function make() { + const j = i++; + return class { + constructor() {} + method() {} + }; + } + class B extends make() { + constructor() { super(); } + method() { super.method(); } + } + const inst = new B(); + inst.method(); + inst.method(); + inst.method(); + return i;` + ); + + // Assert + Expect(result).toBe(1); + } + + @Test("CallSuperSuperMethod") + public callSuperSuperMethod(): void { + const result = util.transpileAndExecute( + `class a { + a: number + constructor(n: number) { + this.a = n; + } + public method() { + return this.a; + } + } + class b extends a { + constructor(n: number) { + super(n); + } + public method() { + return super.method(); + } + } + class c extends b { + constructor(n: number) { + super(n); + } + public method() { + return super.method(); + } + } + let inst = new c(6); + return inst.method();` + ); + + // Assert + Expect(result).toBe(6); + } + @Test("classExpression") public classExpression(): void { const result = util.transpileAndExecute( @@ -662,7 +748,7 @@ export class ClassTests { } @Test("Exported class super call") - public exportedClassSupercAll(): void { + public exportedClassSuperCall(): void { const code = `export class Foo { prop: string; @@ -676,4 +762,47 @@ export class ClassTests { export const baz = (new Bar()).prop;`; Expect(util.transpileExecuteAndReturnExport(code, "baz")).toBe("bar"); } + + @TestCase("(new Foo())", "foo") + @TestCase("Foo", "bar") + @Test("Class method name collision") + public classMethodNameCollisiom(input: string, expectResult: string): void { + const code = + `class Foo { + public method() { return "foo"; } + public static method() { return "bar"; } + } + return ${input}.method();`; + Expect(util.transpileAndExecute(code)).toBe(expectResult); + } + + @TestCase("extension") + @TestCase("metaExtension") + @Test("Class extends extension") + public classExtendsExtension(extensionType: string): void { + const code = + `declare class A {} + /** @${extensionType} **/ + class B extends A {} + class C extends B {}`; + Expect(() => util.transpileString(code)).toThrowError( + TranspileError, + "Cannot extend classes with decorator '@extension' or '@metaExtension'." + ); + } + + @TestCase("extension") + @TestCase("metaExtension") + @Test("Class construct extension") + public classConstructExtension(extensionType: string): void { + const code = + `declare class A {} + /** @${extensionType} **/ + class B extends A {} + const b = new B();`; + Expect(() => util.transpileString(code)).toThrowError( + TranspileError, + "Cannot construct classes with decorator '@extension' or '@metaExtension'." + ); + } } diff --git a/test/unit/compiler/configuration/options.spec.ts b/test/unit/compiler/configuration/options.spec.ts index 262b80085..28324c10a 100644 --- a/test/unit/compiler/configuration/options.spec.ts +++ b/test/unit/compiler/configuration/options.spec.ts @@ -24,6 +24,6 @@ export class ObjectLiteralTests const options = {LuaLibImportKind: importKind}; const result = util.transpileString("const a = new Map();", options); - Expect(result).toBe("local a = Map.new(true);"); + Expect(result).toBe("local a = Map.new();"); } } diff --git a/test/unit/decoratorMetaExtension.spec.ts b/test/unit/decoratorMetaExtension.spec.ts index c58fde638..f0a5eabdc 100644 --- a/test/unit/decoratorMetaExtension.spec.ts +++ b/test/unit/decoratorMetaExtension.spec.ts @@ -57,6 +57,6 @@ export class DecoratorMetaExtension { ` ); }).toThrowError(TranspileError, - "Cannot construct classes with decorator '!Extension' or '!MetaExtension'."); + "Cannot construct classes with decorator '@extension' or '@metaExtension'."); } } diff --git a/test/unit/typechecking.spec.ts b/test/unit/typechecking.spec.ts index 095b87afd..a08740cad 100644 --- a/test/unit/typechecking.spec.ts +++ b/test/unit/typechecking.spec.ts @@ -1,6 +1,7 @@ import { Expect, Test, TestCase } from "alsatian"; import * as util from "../src/util"; +import { TranspileError } from "../../src/TranspileError"; export class TypeCheckingTests { @TestCase("0") @@ -116,4 +117,20 @@ export class TypeCheckingTests { Expect(result).toBe(false); } + + @TestCase("extension") + @TestCase("metaExtension") + @Test("instanceof extension") + public instanceOfExtension(extensionType: string): void { + const code = + `declare class A {} + /** @${extensionType} **/ + class B extends A {} + declare const foo: any; + const result = foo instanceof B;`; + Expect(() => util.transpileString(code)).toThrowError( + TranspileError, + "Cannot use instanceof on classes with decorator '@extension' or '@metaExtension'." + ); + } }