diff --git a/CHANGELOG.md b/CHANGELOG.md index 78a3ae137..6936a691c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,54 @@ - TypeScript has been updated to 3.8. See [release notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html) for details. +- Fixed class accessors not working when base class is lacking type information (#725) + +- Class extension code has been extracted to lualib + + ```ts + class A {} + class B extends A {} + ``` + + ```diff + A = __TS__Class() + B = __TS__Class() + -B.____super = A + -setmetatable(B, B.____super) + -setmetatable(B.prototype, B.____super.prototype) + +__TS__ClassExtends(A, B) + ``` + +- Generated code for class accessors is more dynamic now + + ```ts + class A { + get a() { + return true; + } + } + ``` + + ```diff + A = __TS__Class() + -A.prototype.____getters = {} + -A.prototype.__index = __TS__Index(A.prototype) + -function A.prototype.____getters.a(self) + - return true + -end + +__TS__SetDescriptor( + + A.prototype, + + "a", + + { + + get = function(self) + + return true + + end + + } + +) + ``` + + This change simplifies our codebase and opens a path to object accessors implementation + ## 0.31.0 - **Breaking:** The old annotation syntax (`/* !varArg */`) **no longer works**, the only currently supported syntax is: diff --git a/src/LuaLib.ts b/src/LuaLib.ts index 7b1926912..38e978fb3 100644 --- a/src/LuaLib.ts +++ b/src/LuaLib.ts @@ -26,20 +26,18 @@ export enum LuaLibFeature { ArrayFlatMap = "ArrayFlatMap", ArraySetLength = "ArraySetLength", Class = "Class", - ClassIndex = "ClassIndex", - ClassNewIndex = "ClassNewIndex", + ClassExtends = "ClassExtends", Decorate = "Decorate", + Descriptors = "Descriptors", Error = "Error", FunctionApply = "FunctionApply", FunctionBind = "FunctionBind", FunctionCall = "FunctionCall", - Index = "Index", InstanceOf = "InstanceOf", InstanceOfObject = "InstanceOfObject", Iterator = "Iterator", Map = "Map", New = "New", - NewIndex = "NewIndex", Number = "Number", NumberIsFinite = "NumberIsFinite", NumberIsNaN = "NumberIsNaN", diff --git a/src/lualib/Class.ts b/src/lualib/Class.ts index 14aa98587..d5d7af93d 100644 --- a/src/lualib/Class.ts +++ b/src/lualib/Class.ts @@ -1,7 +1,5 @@ function __TS__Class(): LuaClass { - const c = {} as LuaClass; - c.__index = c; - c.prototype = {}; + const c: LuaClass = { prototype: {} }; c.prototype.__index = c.prototype; c.prototype.constructor = c; return c; diff --git a/src/lualib/ClassExtends.ts b/src/lualib/ClassExtends.ts new file mode 100644 index 000000000..e297cba93 --- /dev/null +++ b/src/lualib/ClassExtends.ts @@ -0,0 +1,19 @@ +function __TS__ClassExtends(this: void, target: LuaClass, base: LuaClass): void { + target.____super = base; + + // Set base class as a metatable, because descriptors use `getmetatable` to get extended prototype + const staticMetatable: any = setmetatable({ __index: base }, base); + setmetatable(target, staticMetatable); + + const baseMetatable = getmetatable(base); + if (baseMetatable) { + // Re-add metatable events defined by descriptors + if (typeof baseMetatable.__index === "function") staticMetatable.__index = baseMetatable.__index; + if (typeof baseMetatable.__newindex === "function") staticMetatable.__newindex = baseMetatable.__newindex; + } + + setmetatable(target.prototype, base.prototype); + // Re-add metatable events defined by accessors with `__TS__SetDescriptor` + if (typeof base.prototype.__index === "function") target.prototype.__index = base.prototype.__index; + if (typeof base.prototype.__newindex === "function") target.prototype.__newindex = base.prototype.__newindex; +} diff --git a/src/lualib/ClassIndex.ts b/src/lualib/ClassIndex.ts deleted file mode 100644 index 0f753e3bb..000000000 --- a/src/lualib/ClassIndex.ts +++ /dev/null @@ -1,21 +0,0 @@ -function __TS__ClassIndex(this: void, classTable: LuaClass, key: any): any { - while (true) { - const getters = rawget(classTable, "____getters"); - if (getters) { - const getter = getters[key]; - if (getter) { - return getter(classTable); - } - } - - classTable = rawget(classTable, "____super"); - if (!classTable) { - break; - } - - const val = rawget(classTable, key); - if (val !== null) { - return val; - } - } -} diff --git a/src/lualib/ClassNewIndex.ts b/src/lualib/ClassNewIndex.ts deleted file mode 100644 index 99205093c..000000000 --- a/src/lualib/ClassNewIndex.ts +++ /dev/null @@ -1,17 +0,0 @@ -function __TS__ClassNewIndex(this: void, classTable: LuaClass, key: any, val: any): void { - let tbl = classTable; - do { - const setters = rawget(tbl, "____setters"); - if (setters) { - const setter = setters[key]; - if (setter) { - setter(tbl, val); - return; - } - } - - tbl = rawget(tbl, "____super"); - } while (tbl); - - rawset(classTable, key, val); -} diff --git a/src/lualib/Descriptors.ts b/src/lualib/Descriptors.ts new file mode 100644 index 000000000..eb5198408 --- /dev/null +++ b/src/lualib/Descriptors.ts @@ -0,0 +1,75 @@ +function ____descriptorIndex(this: any, key: string): void { + const value = rawget(this, key); + if (value !== null) { + return value; + } + + let metatable = getmetatable(this); + while (metatable) { + const rawResult = rawget(metatable, key); + if (rawResult !== undefined) { + return rawResult; + } + + const descriptors = rawget(metatable, "_descriptors"); + if (descriptors) { + const descriptor: PropertyDescriptor = descriptors[key]; + if (descriptor) { + if (descriptor.get) { + return descriptor.get.call(this); + } + + return; + } + } + + metatable = getmetatable(metatable); + } +} + +function ____descriptorNewindex(this: any, key: string, value: any): void { + let metatable = getmetatable(this); + while (metatable) { + const descriptors = rawget(metatable, "_descriptors"); + if (descriptors) { + const descriptor: PropertyDescriptor = descriptors[key]; + if (descriptor) { + if (descriptor.set) { + descriptor.set.call(this, value); + } + + return; + } + } + + metatable = getmetatable(metatable); + } + + rawset(this, key, value); +} + +// It's also used directly in class transform to add descriptors to the prototype +function __TS__SetDescriptor(this: void, metatable: Metatable, prop: string, descriptor: PropertyDescriptor): void { + if (!metatable._descriptors) metatable._descriptors = {}; + metatable._descriptors[prop] = descriptor; + + if (descriptor.get) metatable.__index = ____descriptorIndex; + if (descriptor.set) metatable.__newindex = ____descriptorNewindex; +} + +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty +function __TS__ObjectDefineProperty( + this: void, + object: T, + prop: string, + descriptor: PropertyDescriptor +): T { + let metatable = getmetatable(object); + if (!metatable) { + metatable = {}; + setmetatable(object, metatable); + } + + __TS__SetDescriptor(metatable, prop, descriptor); + return object; +} diff --git a/src/lualib/Index.ts b/src/lualib/Index.ts deleted file mode 100644 index ee7c2b9c7..000000000 --- a/src/lualib/Index.ts +++ /dev/null @@ -1,26 +0,0 @@ -function __TS__Index(this: void, classProto: LuaObject): (this: void, tbl: LuaObject, key: any) => any { - return (tbl, key) => { - let proto = classProto; - while (true) { - const val = rawget(proto, key); - if (val !== null) { - return val; - } - - const getters = rawget(proto, "____getters"); - if (getters) { - const getter = getters[key]; - if (getter) { - return getter(tbl); - } - } - - const base = rawget(rawget(proto, "constructor"), "____super"); - if (!base) { - break; - } - - proto = rawget(base, "prototype"); - } - }; -} diff --git a/src/lualib/InstanceOf.ts b/src/lualib/InstanceOf.ts index b9b47f707..b856ddaa9 100644 --- a/src/lualib/InstanceOf.ts +++ b/src/lualib/InstanceOf.ts @@ -1,4 +1,4 @@ -function __TS__InstanceOf(this: void, obj: LuaObject, classTbl: LuaClass): boolean { +function __TS__InstanceOf(this: void, obj: LuaClassInstance, classTbl: LuaClass): boolean { if (typeof classTbl !== "object") { // tslint:disable-next-line: no-string-throw throw "Right-hand side of 'instanceof' is not an object"; diff --git a/src/lualib/NewIndex.ts b/src/lualib/NewIndex.ts deleted file mode 100644 index da0d4d12a..000000000 --- a/src/lualib/NewIndex.ts +++ /dev/null @@ -1,24 +0,0 @@ -function __TS__NewIndex(this: void, classProto: LuaObject): (this: void, tbl: LuaObject, key: any, val: any) => void { - return (tbl, key, val) => { - let proto = classProto; - while (true) { - const setters = rawget(proto, "____setters"); - if (setters) { - const setter = setters[key]; - if (setter) { - setter(tbl, val); - return; - } - } - - const base = rawget(rawget(proto, "constructor"), "____super"); - if (!base) { - break; - } - - proto = rawget(base, "prototype"); - } - - rawset(tbl, key, val); - }; -} diff --git a/src/lualib/declarations/tstl.d.ts b/src/lualib/declarations/tstl.d.ts index 9216870f2..11c34c809 100644 --- a/src/lualib/declarations/tstl.d.ts +++ b/src/lualib/declarations/tstl.d.ts @@ -6,17 +6,18 @@ interface Vararg extends Array {} /** @forRange */ declare function forRange(start: number, limit: number, step?: number): number[]; -interface LuaClass { - prototype: LuaObject; - ____super?: LuaClass; - ____getters?: { [key: string]: (self: LuaClass) => any }; - ____setters?: { [key: string]: (self: LuaClass, val: any) => void }; +interface Metatable { + _descriptors?: Record; __index?: any; + __newindex?: any; } -interface LuaObject { +interface LuaClass extends Metatable { + prototype: LuaClassInstance; + [Symbol.hasInstance]?(instance: LuaClassInstance): any; + ____super?: LuaClass; +} + +interface LuaClassInstance extends Metatable { constructor: LuaClass; - ____getters?: { [key: string]: (self: LuaObject) => any }; - ____setters?: { [key: string]: (self: LuaObject, val: any) => void }; - __index?: any; } diff --git a/src/transformation/context/context.ts b/src/transformation/context/context.ts index e192348b3..0b34da260 100644 --- a/src/transformation/context/context.ts +++ b/src/transformation/context/context.ts @@ -4,11 +4,19 @@ import * as lua from "../../LuaAST"; import { unwrapVisitorResult } from "../utils/lua-ast"; import { ExpressionLikeNode, ObjectVisitor, StatementLikeNode, VisitorMap } from "./visitors"; +export interface AllAccessorDeclarations { + firstAccessor: ts.AccessorDeclaration; + secondAccessor: ts.AccessorDeclaration | undefined; + getAccessor: ts.GetAccessorDeclaration | undefined; + setAccessor: ts.SetAccessorDeclaration | undefined; +} + export interface EmitResolver { isValueAliasDeclaration(node: ts.Node): boolean; isReferencedAliasDeclaration(node: ts.Node, checkChildren?: boolean): boolean; isTopLevelValueImportEqualsWithEntityName(node: ts.ImportEqualsDeclaration): boolean; moduleExportsSomeValue(moduleReferenceExpression: ts.Expression): boolean; + getAllAccessorDeclarations(declaration: ts.AccessorDeclaration): AllAccessorDeclarations; } export interface DiagnosticsProducingTypeChecker extends ts.TypeChecker { diff --git a/src/transformation/utils/safe-names.ts b/src/transformation/utils/safe-names.ts index dbd8f3071..ed95f0d44 100644 --- a/src/transformation/utils/safe-names.ts +++ b/src/transformation/utils/safe-names.ts @@ -41,7 +41,6 @@ export const luaBuiltins: ReadonlySet = new Set([ "pcall", "print", "rawget", - "rawset", "repeat", "require", "self", diff --git a/src/transformation/visitors/class/index.ts b/src/transformation/visitors/class/index.ts index 8f4049399..bf11c8748 100644 --- a/src/transformation/visitors/class/index.ts +++ b/src/transformation/visitors/class/index.ts @@ -32,13 +32,13 @@ import { isAmbientNode } from "../../utils/typescript"; import { transformIdentifier } from "../identifier"; import { transformPropertyName } from "../literal"; import { createConstructorDecorationStatement } from "./decorators"; -import { isGetAccessorOverride, transformAccessorDeclaration } from "./members/accessors"; +import { isGetAccessorOverride, transformAccessorDeclarations } from "./members/accessors"; import { createConstructorName, transformConstructorDeclaration } from "./members/constructor"; import { transformClassInstanceFields } from "./members/fields"; import { transformMethodDeclaration } from "./members/method"; import { checkForLuaLibType } from "./new"; import { createClassSetup } from "./setup"; -import { getExtendedType, getExtendedTypeNode, isStaticNode } from "./utils"; +import { getExtendedNode, getExtendedType, isStaticNode } from "./utils"; export function transformClassAsExpression( expression: ts.ClassLikeDeclaration, @@ -105,23 +105,23 @@ export function transformClassDeclaration( } // Get type that is extended - const extendsType = getExtendedType(context, classDeclaration); + const extendedType = getExtendedType(context, classDeclaration); - if (extendsType) { - checkForLuaLibType(context, extendsType); + if (extendedType) { + checkForLuaLibType(context, extendedType); } - if (!(isExtension || isMetaExtension) && extendsType) { + if (!(isExtension || isMetaExtension) && extendedType) { // Non-extensions cannot extend extension classes - const extendsAnnotations = getTypeAnnotations(extendsType); + const extendsAnnotations = getTypeAnnotations(extendedType); if (extendsAnnotations.has(AnnotationKind.Extension) || extendsAnnotations.has(AnnotationKind.MetaExtension)) { throw InvalidExtendsExtension(classDeclaration); } } // You cannot extend LuaTable classes - if (extendsType) { - const annotations = getTypeAnnotations(extendsType); + if (extendedType) { + const annotations = getTypeAnnotations(extendedType); if (annotations.has(AnnotationKind.LuaTable)) { throw InvalidExtendsLuaTable(classDeclaration); } @@ -143,11 +143,11 @@ export function transformClassDeclaration( // Overwrite the original className with the class we are overriding for extensions if (isMetaExtension) { - if (!extendsType) { + if (!extendedType) { throw MissingMetaExtension(classDeclaration); } - const extendsName = lua.createStringLiteral(extendsType.symbol.name as string); + const extendsName = lua.createStringLiteral(extendedType.symbol.name as string); className = lua.createIdentifier("__meta__" + extendsName.value); // local className = debug.getregistry()["extendsName"] @@ -173,8 +173,8 @@ export function transformClassDeclaration( const [extensionName] = extensionDirective.args; if (extensionName) { className = lua.createIdentifier(extensionName); - } else if (extendsType) { - className = lua.createIdentifier(extendsType.symbol.name); + } else if (extendedType) { + className = lua.createIdentifier(extendedType.symbol.name); } } @@ -193,7 +193,7 @@ export function transformClassDeclaration( if (!isExtension && !isMetaExtension) { result.push( - ...createClassSetup(context, classDeclaration, className, localClassName, classNameText, extendsType) + ...createClassSetup(context, classDeclaration, className, localClassName, classNameText, extendedType) ); } else { for (const f of instanceFields) { @@ -228,7 +228,7 @@ export function transformClassDeclaration( ); if (constructorResult) result.push(constructorResult); - } else if (!extendsType) { + } else if (!extendedType) { // Generate a constructor if none was defined in a base class const constructorResult = transformConstructorDeclaration( context, @@ -276,12 +276,16 @@ export function transformClassDeclaration( } // Transform accessors - result.push( - ...classDeclaration.members - .filter(ts.isAccessor) - .map(accessor => transformAccessorDeclaration(context, accessor, localClassName)) - .filter(isNonNull) - ); + for (const member of classDeclaration.members) { + if (!ts.isAccessor(member)) continue; + const accessors = context.resolver.getAllAccessorDeclarations(member); + if (accessors.firstAccessor !== member) continue; + + const accessorsResult = transformAccessorDeclarations(context, accessors, localClassName); + if (accessorsResult) { + result.push(accessorsResult); + } + } // Transform methods result.push( @@ -316,12 +320,12 @@ export function transformClassDeclaration( export const transformSuperExpression: FunctionVisitor = (expression, context) => { const classStack = getOrUpdate(classStacks, context, () => []); const classDeclaration = classStack[classStack.length - 1]; - const typeNode = getExtendedTypeNode(context, classDeclaration); - if (typeNode === undefined) { + const extendedNode = getExtendedNode(context, classDeclaration); + if (extendedNode === undefined) { throw UnknownSuperType(expression); } - const extendsExpression = typeNode.expression; + const extendsExpression = extendedNode.expression; let baseClassName: lua.AssignmentLeftHandSideExpression | undefined; if (ts.isIdentifier(extendsExpression)) { diff --git a/src/transformation/visitors/class/members/accessors.ts b/src/transformation/visitors/class/members/accessors.ts index 2da5f0f70..50cc8a10f 100644 --- a/src/transformation/visitors/class/members/accessors.ts +++ b/src/transformation/visitors/class/members/accessors.ts @@ -1,12 +1,58 @@ import * as ts from "typescript"; import * as lua from "../../../../LuaAST"; -import { TransformationContext } from "../../../context"; +import { AllAccessorDeclarations, TransformationContext } from "../../../context"; import { createSelfIdentifier } from "../../../utils/lua-ast"; +import { importLuaLibFeature, LuaLibFeature } from "../../../utils/lualib"; import { transformFunctionBody, transformParameters } from "../../function"; -import { transformIdentifier } from "../../identifier"; +import { transformPropertyName } from "../../literal"; import { getExtendedType, isStaticNode } from "../utils"; -// TODO: Inline to `hasMemberInClassOrAncestor`? +function transformAccessor(context: TransformationContext, node: ts.AccessorDeclaration): lua.FunctionExpression { + const [params, dot, restParam] = transformParameters(context, node.parameters, createSelfIdentifier()); + const body = node.body ? transformFunctionBody(context, node.parameters, node.body, restParam)[0] : []; + return lua.createFunctionExpression( + lua.createBlock(body), + params, + dot, + restParam, + lua.FunctionExpressionFlags.Declaration + ); +} + +export function transformAccessorDeclarations( + context: TransformationContext, + { firstAccessor, getAccessor, setAccessor }: AllAccessorDeclarations, + className: lua.Identifier +): lua.Statement | undefined { + const propertyName = transformPropertyName(context, firstAccessor.name); + const descriptor = lua.createTableExpression([]); + + if (getAccessor) { + const getterFunction = transformAccessor(context, getAccessor); + descriptor.fields.push(lua.createTableFieldExpression(getterFunction, lua.createStringLiteral("get"))); + } + + if (setAccessor) { + const setterFunction = transformAccessor(context, setAccessor); + descriptor.fields.push(lua.createTableFieldExpression(setterFunction, lua.createStringLiteral("set"))); + } + + importLuaLibFeature(context, LuaLibFeature.Descriptors); + const call = isStaticNode(firstAccessor) + ? lua.createCallExpression(lua.createIdentifier(`__TS__ObjectDefineProperty`), [ + lua.cloneIdentifier(className), + propertyName, + descriptor, + ]) + : lua.createCallExpression(lua.createIdentifier(`__TS__SetDescriptor`), [ + lua.createTableIndexExpression(lua.cloneIdentifier(className), lua.createStringLiteral("prototype")), + propertyName, + descriptor, + ]); + + return lua.createExpressionStatement(call); +} + function* classWithAncestors( context: TransformationContext, classDeclaration: ts.ClassLikeDeclarationBase @@ -71,34 +117,3 @@ export function isGetAccessorOverride( m => ts.isPropertyDeclaration(m) && m.initializer !== undefined && isSamePropertyName(m.name, element.name) ); } - -export function transformAccessorDeclaration( - context: TransformationContext, - node: ts.AccessorDeclaration, - className: lua.Identifier -): lua.Statement | undefined { - if (node.body === undefined) { - return undefined; - } - - const name = transformIdentifier(context, node.name as ts.Identifier); - - const [params, dot, restParam] = transformParameters(context, node.parameters, createSelfIdentifier()); - const [body] = transformFunctionBody(context, node.parameters, node.body, restParam); - const accessorFunction = lua.createFunctionExpression( - lua.createBlock(body), - params, - dot, - restParam, - lua.FunctionExpressionFlags.Declaration - ); - - const methodTable = isStaticNode(node) - ? lua.cloneIdentifier(className) - : lua.createTableIndexExpression(lua.cloneIdentifier(className), lua.createStringLiteral("prototype")); - - const classAccessorsName = ts.isGetAccessorDeclaration(node) ? "____getters" : "____setters"; - const classAccessors = lua.createTableIndexExpression(methodTable, lua.createStringLiteral(classAccessorsName)); - const accessorPath = lua.createTableIndexExpression(classAccessors, lua.createStringLiteral(name.text)); - return lua.createAssignmentStatement(accessorPath, accessorFunction, node); -} diff --git a/src/transformation/visitors/class/members/fields.ts b/src/transformation/visitors/class/members/fields.ts index 87b91e235..9117c1519 100644 --- a/src/transformation/visitors/class/members/fields.ts +++ b/src/transformation/visitors/class/members/fields.ts @@ -27,6 +27,8 @@ export function transformClassInstanceFields( statements.push(assignClassField); } + // TODO: Remove when `useDefineForClassFields` would be `true` by default + const getOverrides = classDeclaration.members.filter((m): m is ts.GetAccessorDeclaration => isGetAccessorOverride(context, m, classDeclaration) ); diff --git a/src/transformation/visitors/class/setup.ts b/src/transformation/visitors/class/setup.ts index 2a4a1c8d0..3aec046a3 100644 --- a/src/transformation/visitors/class/setup.ts +++ b/src/transformation/visitors/class/setup.ts @@ -9,9 +9,8 @@ import { hasDefaultExportModifier, } from "../../utils/export"; import { createExportsIdentifier, createLocalOrExportedOrGlobalDeclaration } from "../../utils/lua-ast"; -import { importLuaLibFeature, LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib"; -import { hasMemberInClassOrAncestor } from "./members/accessors"; -import { getExtendedTypeNode, isStaticNode } from "./utils"; +import { LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib"; +import { getExtendedNode, getExtendsClause } from "./utils"; export function createClassSetup( context: TransformationContext, @@ -62,209 +61,23 @@ export function createClassSetup( ) ); - // localClassName.____getters = {} - if (statement.members.some(m => ts.isGetAccessor(m) && isStaticNode(m))) { - const classGetters = lua.createTableIndexExpression( - lua.cloneIdentifier(localClassName), - lua.createStringLiteral("____getters") - ); - const assignClassGetters = lua.createAssignmentStatement(classGetters, lua.createTableExpression(), statement); - result.push(assignClassGetters); - - importLuaLibFeature(context, LuaLibFeature.ClassIndex); - } - - // localClassName.____setters = {} - if (statement.members.some(m => ts.isSetAccessor(m) && isStaticNode(m))) { - const classSetters = lua.createTableIndexExpression( - lua.cloneIdentifier(localClassName), - lua.createStringLiteral("____setters") - ); - const assignClassSetters = lua.createAssignmentStatement(classSetters, lua.createTableExpression(), statement); - result.push(assignClassSetters); - - importLuaLibFeature(context, LuaLibFeature.ClassNewIndex); - } - - // localClassName.prototype - const createClassPrototype = () => - lua.createTableIndexExpression(lua.cloneIdentifier(localClassName), lua.createStringLiteral("prototype")); - - // localClassName.prototype.____getters = {} - if (statement.members.some(m => ts.isGetAccessor(m) && !isStaticNode(m))) { - const classPrototypeGetters = lua.createTableIndexExpression( - createClassPrototype(), - lua.createStringLiteral("____getters") - ); - const assignClassPrototypeGetters = lua.createAssignmentStatement( - classPrototypeGetters, - lua.createTableExpression(), - statement - ); - result.push(assignClassPrototypeGetters); - } - - if (hasMemberInClassOrAncestor(context, statement, m => ts.isGetAccessor(m) && !isStaticNode(m))) { - // localClassName.prototype.__index = __TS__Index(localClassName.prototype) - const classPrototypeIndex = lua.createTableIndexExpression( - createClassPrototype(), - lua.createStringLiteral("__index") - ); - const assignClassPrototypeIndex = lua.createAssignmentStatement( - classPrototypeIndex, - transformLuaLibFunction(context, LuaLibFeature.Index, undefined, createClassPrototype()), - statement - ); - result.push(assignClassPrototypeIndex); - } - - if (statement.members.some(m => ts.isSetAccessor(m) && !isStaticNode(m))) { - // localClassName.prototype.____setters = {} - const classPrototypeSetters = lua.createTableIndexExpression( - createClassPrototype(), - lua.createStringLiteral("____setters") - ); - const assignClassPrototypeSetters = lua.createAssignmentStatement( - classPrototypeSetters, - lua.createTableExpression(), - statement - ); - result.push(assignClassPrototypeSetters); - } - - if (hasMemberInClassOrAncestor(context, statement, m => ts.isSetAccessor(m) && !isStaticNode(m))) { - // localClassName.prototype.__newindex = __TS__NewIndex(localClassName.prototype) - const classPrototypeNewIndex = lua.createTableIndexExpression( - createClassPrototype(), - lua.createStringLiteral("__newindex") - ); - const assignClassPrototypeIndex = lua.createAssignmentStatement( - classPrototypeNewIndex, - transformLuaLibFunction(context, LuaLibFeature.NewIndex, undefined, createClassPrototype()), - statement - ); - result.push(assignClassPrototypeIndex); - } - - const hasStaticGetters = hasMemberInClassOrAncestor( - context, - statement, - m => ts.isGetAccessor(m) && isStaticNode(m) - ); - const hasStaticSetters = hasMemberInClassOrAncestor( - context, - statement, - m => ts.isSetAccessor(m) && isStaticNode(m) - ); - if (extendsType) { - const extendedTypeNode = getExtendedTypeNode(context, statement); - if (extendedTypeNode === undefined) { + const extendedNode = getExtendedNode(context, statement); + if (extendedNode === undefined) { throw UndefinedTypeNode(statement); } - // localClassName.____super = extendsExpression - const createClassBase = () => - lua.createTableIndexExpression(lua.cloneIdentifier(localClassName), lua.createStringLiteral("____super")); - const assignClassBase = lua.createAssignmentStatement( - createClassBase(), - context.transformExpression(extendedTypeNode.expression), - extendedTypeNode.expression - ); - result.push(assignClassBase); - - if (hasStaticGetters || hasStaticSetters) { - const metatableFields: lua.TableFieldExpression[] = []; - if (hasStaticGetters) { - // __index = __TS__ClassIndex - metatableFields.push( - lua.createTableFieldExpression( - lua.createIdentifier("__TS__ClassIndex"), - lua.createStringLiteral("__index"), - extendedTypeNode.expression - ) - ); - } else { - // __index = localClassName.____super - metatableFields.push( - lua.createTableFieldExpression( - createClassBase(), - lua.createStringLiteral("__index"), - extendedTypeNode.expression - ) - ); - } - - if (hasStaticSetters) { - // __newindex = __TS__ClassNewIndex - metatableFields.push( - lua.createTableFieldExpression( - lua.createIdentifier("__TS__ClassNewIndex"), - lua.createStringLiteral("__newindex"), - extendedTypeNode.expression - ) - ); - } - - const setClassMetatable = lua.createExpressionStatement( - lua.createCallExpression( - lua.createIdentifier("setmetatable"), - [lua.cloneIdentifier(localClassName), lua.createTableExpression(metatableFields)], - extendedTypeNode.expression + result.push( + lua.createExpressionStatement( + transformLuaLibFunction( + context, + LuaLibFeature.ClassExtends, + getExtendsClause(statement), + lua.cloneIdentifier(localClassName), + context.transformExpression(extendedNode.expression) ) - ); - result.push(setClassMetatable); - } else { - // setmetatable(localClassName, localClassName.____super) - const setClassMetatable = lua.createExpressionStatement( - lua.createCallExpression( - lua.createIdentifier("setmetatable"), - [lua.cloneIdentifier(localClassName), createClassBase()], - extendedTypeNode.expression - ) - ); - result.push(setClassMetatable); - } - - // setmetatable(localClassName.prototype, localClassName.____super.prototype) - const basePrototype = lua.createTableIndexExpression(createClassBase(), lua.createStringLiteral("prototype")); - const setClassPrototypeMetatable = lua.createExpressionStatement( - lua.createCallExpression(lua.createIdentifier("setmetatable"), [createClassPrototype(), basePrototype]), - extendedTypeNode.expression - ); - result.push(setClassPrototypeMetatable); - } else if (hasStaticGetters || hasStaticSetters) { - const metatableFields: lua.TableFieldExpression[] = []; - if (hasStaticGetters) { - // __index = __TS__ClassIndex - metatableFields.push( - lua.createTableFieldExpression( - lua.createIdentifier("__TS__ClassIndex"), - lua.createStringLiteral("__index"), - statement - ) - ); - } - - if (hasStaticSetters) { - // __newindex = __TS__ClassNewIndex - metatableFields.push( - lua.createTableFieldExpression( - lua.createIdentifier("__TS__ClassNewIndex"), - lua.createStringLiteral("__newindex"), - statement - ) - ); - } - - const setClassMetatable = lua.createExpressionStatement( - lua.createCallExpression(lua.createIdentifier("setmetatable"), [ - lua.cloneIdentifier(localClassName), - lua.createTableExpression(metatableFields), - ]), - statement + ) ); - result.push(setClassMetatable); } return result; diff --git a/src/transformation/visitors/class/utils.ts b/src/transformation/visitors/class/utils.ts index a1639e24b..9ba263818 100644 --- a/src/transformation/visitors/class/utils.ts +++ b/src/transformation/visitors/class/utils.ts @@ -6,20 +6,21 @@ export function isStaticNode(node: ts.Node): boolean { return (node.modifiers ?? []).some(m => m.kind === ts.SyntaxKind.StaticKeyword); } -export function getExtendedTypeNode( +export function getExtendsClause(node: ts.ClassLikeDeclarationBase): ts.HeritageClause | undefined { + return (node.heritageClauses ?? []).find(clause => clause.token === ts.SyntaxKind.ExtendsKeyword); +} + +export function getExtendedNode( context: TransformationContext, node: ts.ClassLikeDeclarationBase ): ts.ExpressionWithTypeArguments | undefined { - if (node && node.heritageClauses) { - for (const clause of node.heritageClauses) { - if (clause.token === ts.SyntaxKind.ExtendsKeyword) { - const superType = context.checker.getTypeAtLocation(clause.types[0]); - const annotations = getTypeAnnotations(superType); - if (!annotations.has(AnnotationKind.PureAbstract)) { - return clause.types[0]; - } - } - } + const extendsClause = getExtendsClause(node); + if (!extendsClause) return; + + const superType = context.checker.getTypeAtLocation(extendsClause.types[0]); + const annotations = getTypeAnnotations(superType); + if (!annotations.has(AnnotationKind.PureAbstract)) { + return extendsClause.types[0]; } } @@ -27,6 +28,6 @@ export function getExtendedType( context: TransformationContext, node: ts.ClassLikeDeclarationBase ): ts.Type | undefined { - const extendedTypeNode = getExtendedTypeNode(context, node); - return extendedTypeNode && context.checker.getTypeAtLocation(extendedTypeNode); + const extendedNode = getExtendedNode(context, node); + return extendedNode && context.checker.getTypeAtLocation(extendedNode); } diff --git a/test/unit/identifiers.spec.ts b/test/unit/identifiers.spec.ts index 5f647a013..875c196fc 100644 --- a/test/unit/identifiers.spec.ts +++ b/test/unit/identifiers.spec.ts @@ -453,21 +453,6 @@ describe("lua keyword as identifier doesn't interfere with lua's value", () => { expect(util.transpileAndExecute(code)).toBe(true); }); - test("variable (rawset)", () => { - const code = ` - const rawset = "foobar"; - class A { - prop = "prop"; - } - class B extends A { - get prop() { return rawset; } - } - const b = new B(); - return b.prop;`; - - expect(util.transpileAndExecute(code)).toBe("foobar"); - }); - test("variable (require)", () => { const code = ` const require = "foobar"; diff --git a/test/unit/printer/sourcemaps.spec.ts b/test/unit/printer/sourcemaps.spec.ts index 918d430d1..c12a89b69 100644 --- a/test/unit/printer/sourcemaps.spec.ts +++ b/test/unit/printer/sourcemaps.spec.ts @@ -86,9 +86,8 @@ test.each([ assertPatterns: [ { luaPattern: "Bar = __TS__Class()", typeScriptPattern: "class Bar" }, { luaPattern: "Bar.name =", typeScriptPattern: "class Bar" }, - { luaPattern: "Bar.____super = Foo", typeScriptPattern: "Foo {" }, - { luaPattern: "setmetatable(Bar,", typeScriptPattern: "Foo {" }, - { luaPattern: "setmetatable(Bar.prototype,", typeScriptPattern: "Foo {" }, + { luaPattern: "__TS__ClassExtends", typeScriptPattern: "extends" }, + { luaPattern: "Foo", typeScriptPattern: "Foo" }, { luaPattern: "function Bar.prototype.____constructor", typeScriptPattern: "constructor" }, ], },