Skip to content

Commit c002783

Browse files
tomblindPerryvw
authored andcommitted
Compatible getters/setters (#401)
* new implementation of get/set accessors * added test and fixed discovered edge-cases * tabs bad; spaces good * addressing feedback - removed special case for numerical literals when comparing property names - added tests for accessors overriding other accessors
1 parent 9400a6d commit c002783

File tree

8 files changed

+437
-132
lines changed

8 files changed

+437
-132
lines changed

src/LuaLib.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,11 @@ export enum LuaLibFeature {
1919
FunctionApply = "FunctionApply",
2020
FunctionBind = "FunctionBind",
2121
FunctionCall = "FunctionCall",
22+
Index = "Index",
2223
InstanceOf = "InstanceOf",
2324
Iterator = "Iterator",
2425
Map = "Map",
26+
NewIndex = "NewIndex",
2527
ObjectAssign = "ObjectAssign",
2628
ObjectEntries = "ObjectEntries",
2729
ObjectKeys = "ObjectKeys",

src/LuaTransformer.ts

Lines changed: 101 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -433,12 +433,14 @@ export class LuaTransformer {
433433
instanceFields,
434434
statement
435435
));
436-
} else if (instanceFields.length > 0) {
436+
} else if (instanceFields.length > 0
437+
|| statement.members.some(m => tsHelper.isGetAccessorOverride(m, statement, this.checker)))
438+
{
437439
// Generate a constructor if none was defined in a class with instance fields that need initialization
438440
// className.prototype.____constructor = function(self, ...)
439441
// baseClassName.prototype.____constructor(self, ...)
440442
// ...
441-
const constructorBody = this.transformClassInstanceFields(instanceFields);
443+
const constructorBody = this.transformClassInstanceFields(statement, instanceFields);
442444
const superCall = tstl.createExpressionStatement(
443445
tstl.createCallExpression(
444446
tstl.createTableIndexExpression(
@@ -534,13 +536,61 @@ export class LuaTransformer {
534536
const assignClassPrototype = tstl.createAssignmentStatement(createClassPrototype(), classPrototypeTable);
535537
result.push(assignClassPrototype);
536538

537-
// className.prototype.__index = className.prototype
538539
const classPrototypeIndex = tstl.createTableIndexExpression(
539540
createClassPrototype(),
540541
tstl.createStringLiteral("__index")
541542
);
542-
const assignClassPrototypeIndex = tstl.createAssignmentStatement(classPrototypeIndex, createClassPrototype());
543-
result.push(assignClassPrototypeIndex);
543+
if (statement.members.some(ts.isGetAccessor)) {
544+
// className.prototype.____getters = {}
545+
const classPrototypeGetters = tstl.createTableIndexExpression(
546+
createClassPrototype(),
547+
tstl.createStringLiteral("____getters")
548+
);
549+
const assignClassPrototypeGetters = tstl.createAssignmentStatement(
550+
classPrototypeGetters,
551+
tstl.createTableExpression()
552+
);
553+
result.push(assignClassPrototypeGetters);
554+
555+
// className.prototype.__index = __TS_Index(className.prototype)
556+
const assignClassPrototypeIndex = tstl.createAssignmentStatement(
557+
classPrototypeIndex,
558+
this.transformLuaLibFunction(LuaLibFeature.Index, undefined, createClassPrototype())
559+
);
560+
result.push(assignClassPrototypeIndex);
561+
562+
} else {
563+
// className.prototype.__index = className.prototype
564+
const assignClassPrototypeIndex = tstl.createAssignmentStatement(
565+
classPrototypeIndex,
566+
createClassPrototype()
567+
);
568+
result.push(assignClassPrototypeIndex);
569+
}
570+
571+
if (tsHelper.hasSetAccessorInClassOrAncestor(statement, this.checker)) {
572+
// className.prototype.____setters = {}
573+
const classPrototypeSetters = tstl.createTableIndexExpression(
574+
createClassPrototype(),
575+
tstl.createStringLiteral("____setters")
576+
);
577+
const assignClassPrototypeSetters = tstl.createAssignmentStatement(
578+
classPrototypeSetters,
579+
tstl.createTableExpression()
580+
);
581+
result.push(assignClassPrototypeSetters);
582+
583+
// className.prototype.__newindex = __TS_NewIndex(className.prototype)
584+
const classPrototypeNewIndex = tstl.createTableIndexExpression(
585+
createClassPrototype(),
586+
tstl.createStringLiteral("__newindex")
587+
);
588+
const assignClassPrototypeIndex = tstl.createAssignmentStatement(
589+
classPrototypeNewIndex,
590+
this.transformLuaLibFunction(LuaLibFeature.NewIndex, undefined, createClassPrototype())
591+
);
592+
result.push(assignClassPrototypeIndex);
593+
}
544594

545595
// className.prototype.constructor = className
546596
const classPrototypeConstructor = tstl.createTableIndexExpression(
@@ -634,7 +684,11 @@ export class LuaTransformer {
634684
return result;
635685
}
636686

637-
public transformClassInstanceFields(instanceFields: ts.PropertyDeclaration[]): tstl.Statement[] {
687+
public transformClassInstanceFields(
688+
classDeclarataion: ts.ClassLikeDeclaration,
689+
instanceFields: ts.PropertyDeclaration[]
690+
): tstl.Statement[]
691+
{
638692
const statements: tstl.Statement[] = [];
639693

640694
for (const f of instanceFields) {
@@ -652,6 +706,19 @@ export class LuaTransformer {
652706
statements.push(assignClassField);
653707
}
654708

709+
const getOverrides = classDeclarataion.members.filter(
710+
m => tsHelper.isGetAccessorOverride(m, classDeclarataion, this.checker)
711+
);
712+
for (const getter of getOverrides) {
713+
const resetGetter = tstl.createExpressionStatement(
714+
tstl.createCallExpression(
715+
tstl.createIdentifier("rawset"),
716+
[this.createSelfIdentifier(), this.transformPropertyName(getter.name), tstl.createNilLiteral()]
717+
)
718+
);
719+
statements.push(resetGetter);
720+
}
721+
655722
return statements;
656723
}
657724

@@ -677,7 +744,7 @@ export class LuaTransformer {
677744
return undefined;
678745
}
679746

680-
const bodyStatements: tstl.Statement[] = this.transformClassInstanceFields(instanceFields);
747+
const bodyStatements: tstl.Statement[] = this.transformClassInstanceFields(classDeclaration, instanceFields);
681748

682749
// Check for field declarations in constructor
683750
const constructorFieldsDeclarations = statement.parameters.filter(p => p.modifiers !== undefined);
@@ -745,15 +812,20 @@ export class LuaTransformer {
745812
[this.createSelfIdentifier()]
746813
);
747814

748-
return tstl.createAssignmentStatement(
749-
tstl.createTableIndexExpression(
750-
tstl.createTableIndexExpression(
751-
this.addExportToIdentifier(tstl.cloneIdentifier(className)),
752-
tstl.createStringLiteral("prototype")
753-
),
754-
tstl.createStringLiteral("get__" + name.text)),
755-
accessorFunction
815+
const classPrototype = tstl.createTableIndexExpression(
816+
this.addExportToIdentifier(tstl.cloneIdentifier(className)),
817+
tstl.createStringLiteral("prototype")
818+
);
819+
const classGetters = tstl.createTableIndexExpression(
820+
classPrototype,
821+
tstl.createStringLiteral("____getters")
756822
);
823+
const getter = tstl.createTableIndexExpression(
824+
classGetters,
825+
tstl.createStringLiteral(name.text)
826+
);
827+
const assignGetter = tstl.createAssignmentStatement(getter, accessorFunction);
828+
return assignGetter;
757829
}
758830

759831
public transformSetAccessorDeclaration(
@@ -774,15 +846,20 @@ export class LuaTransformer {
774846
restParam
775847
);
776848

777-
return tstl.createAssignmentStatement(
778-
tstl.createTableIndexExpression(
779-
tstl.createTableIndexExpression(
780-
this.addExportToIdentifier(tstl.cloneIdentifier(className)),
781-
tstl.createStringLiteral("prototype")
782-
),
783-
tstl.createStringLiteral("set__" + name.text)),
784-
accessorFunction
849+
const classPrototype = tstl.createTableIndexExpression(
850+
this.addExportToIdentifier(tstl.cloneIdentifier(className)),
851+
tstl.createStringLiteral("prototype")
852+
);
853+
const classSetters = tstl.createTableIndexExpression(
854+
classPrototype,
855+
tstl.createStringLiteral("____setters")
785856
);
857+
const setter = tstl.createTableIndexExpression(
858+
classSetters,
859+
tstl.createStringLiteral(name.text)
860+
);
861+
const assignSetter = tstl.createAssignmentStatement(setter, accessorFunction);
862+
return assignSetter;
786863
}
787864

788865
public transformMethodDeclaration(
@@ -2155,16 +2232,6 @@ export class LuaTransformer {
21552232
}
21562233

21572234
public transformAssignment(lhs: ts.Expression, right: tstl.Expression): tstl.Statement {
2158-
if (ts.isPropertyAccessExpression(lhs)) {
2159-
const hasSetAccessor = tsHelper.hasSetAccessor(lhs, this.checker);
2160-
if (hasSetAccessor) {
2161-
return tstl.createExpressionStatement(this.transformSetAccessor(lhs, right), lhs.parent);
2162-
} else if (hasSetAccessor === undefined) {
2163-
// Undefined hasSetAccessor indicates a union with both set accessors and no accessors
2164-
// on the same field.
2165-
throw TSTLErrors.UnsupportedUnionAccessor(lhs);
2166-
}
2167-
}
21682235
return tstl.createAssignmentStatement(
21692236
this.transformExpression(lhs) as tstl.IdentifierOrTableIndexExpression,
21702237
right,
@@ -2232,10 +2299,7 @@ export class LuaTransformer {
22322299
);
22332300
}
22342301

2235-
if (
2236-
(ts.isPropertyAccessExpression(expression.left) && !tsHelper.hasSetAccessor(expression.left, this.checker))
2237-
|| ts.isElementAccessExpression(expression.left)
2238-
) {
2302+
if (ts.isPropertyAccessExpression(expression.left) || ts.isElementAccessExpression(expression.left)) {
22392303
// Left is property/element access: cache result while maintaining order of evaluation
22402304
// (function(o, i, v) o[i] = v; return v end)(${objExpression}, ${indexExpression}, ${right})
22412305
const objParameter = tstl.createIdentifier("o");
@@ -3009,14 +3073,6 @@ export class LuaTransformer {
30093073
public transformPropertyAccessExpression(node: ts.PropertyAccessExpression): tstl.Expression {
30103074
const property = node.name.text;
30113075

3012-
const hasGetAccessor = tsHelper.hasGetAccessor(node, this.checker);
3013-
if (hasGetAccessor) {
3014-
return this.transformGetAccessor(node);
3015-
} else if (hasGetAccessor === undefined) {
3016-
// Undefined hasGetAccessor indicates a union with both get accessors and no accessors on the same field.
3017-
throw TSTLErrors.UnsupportedUnionAccessor(node);
3018-
}
3019-
30203076
// Check for primitive types to override
30213077
const type = this.checker.getTypeAtLocation(node.expression);
30223078
if (tsHelper.isStringType(type)) {
@@ -3054,18 +3110,6 @@ export class LuaTransformer {
30543110
return tstl.createTableIndexExpression(callPath, tstl.createStringLiteral(property), node);
30553111
}
30563112

3057-
public transformGetAccessor(node: ts.PropertyAccessExpression): tstl.MethodCallExpression {
3058-
const name = tstl.createIdentifier(`get__${node.name.escapedText}`);
3059-
const expression = this.transformExpression(node.expression);
3060-
return tstl.createMethodCallExpression(expression, name, [], node);
3061-
}
3062-
3063-
public transformSetAccessor(node: ts.PropertyAccessExpression, value: tstl.Expression): tstl.MethodCallExpression {
3064-
const name = tstl.createIdentifier(`set__${node.name.escapedText}`);
3065-
const expression = this.transformExpression(node.expression);
3066-
return tstl.createMethodCallExpression(expression, name, [value], node);
3067-
}
3068-
30693113
// Transpile a Math._ property
30703114
public transformMathExpression(identifier: ts.Identifier): tstl.TableIndexExpression {
30713115
const translation = {

src/TSHelper.ts

Lines changed: 72 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -261,60 +261,6 @@ export class TSHelper {
261261
return undefined;
262262
}
263263

264-
public static hasExplicitGetAccessor(type: ts.Type, name: ts.__String): boolean {
265-
if (type && type.symbol && type.symbol.members) {
266-
const field = type.symbol.members.get(name);
267-
return field && (field.flags & ts.SymbolFlags.GetAccessor) !== 0;
268-
}
269-
}
270-
271-
public static typeHasGetAccessor(type: ts.Type, name: ts.__String, checker: ts.TypeChecker): boolean | undefined {
272-
if (type.isUnion()) {
273-
if (type.types.some(t => TSHelper.typeHasGetAccessor(t, name, checker))) {
274-
// undefined if only a subset of types implements the accessor
275-
return type.types.every(t => TSHelper.typeHasGetAccessor(t, name, checker)) ? true : undefined;
276-
}
277-
return false;
278-
}
279-
return TSHelper.forTypeOrAnySupertype(type, checker, t => TSHelper.hasExplicitGetAccessor(t, name));
280-
}
281-
282-
public static hasGetAccessor(node: ts.Node, checker: ts.TypeChecker): boolean | undefined {
283-
if (ts.isPropertyAccessExpression(node)) {
284-
const name = node.name.escapedText;
285-
const type = checker.getTypeAtLocation(node.expression);
286-
return TSHelper.typeHasGetAccessor(type, name, checker);
287-
}
288-
return false;
289-
}
290-
291-
public static hasExplicitSetAccessor(type: ts.Type, name: ts.__String): boolean {
292-
if (type && type.symbol && type.symbol.members) {
293-
const field = type.symbol.members.get(name);
294-
return field && (field.flags & ts.SymbolFlags.SetAccessor) !== 0;
295-
}
296-
}
297-
298-
public static typeHasSetAccessor(type: ts.Type, name: ts.__String, checker: ts.TypeChecker): boolean | undefined {
299-
if (type.isUnion()) {
300-
if (type.types.some(t => TSHelper.typeHasSetAccessor(t, name, checker))) {
301-
// undefined if only a subset of types implements the accessor
302-
return type.types.every(t => TSHelper.typeHasSetAccessor(t, name, checker)) ? true : undefined;
303-
}
304-
return false;
305-
}
306-
return TSHelper.forTypeOrAnySupertype(type, checker, t => TSHelper.hasExplicitSetAccessor(t, name));
307-
}
308-
309-
public static hasSetAccessor(node: ts.Node, checker: ts.TypeChecker): boolean {
310-
if (ts.isPropertyAccessExpression(node)) {
311-
const name = node.name.escapedText;
312-
const type = checker.getTypeAtLocation(node.expression);
313-
return TSHelper.typeHasSetAccessor(type, name, checker);
314-
}
315-
return false;
316-
}
317-
318264
public static isBinaryAssignmentToken(token: ts.SyntaxKind): [boolean, tstl.BinaryOperator] {
319265
switch (token) {
320266
case ts.SyntaxKind.BarEqualsToken:
@@ -398,6 +344,78 @@ export class TSHelper {
398344
param => ts.isIdentifier(param.name) && param.name.originalKeywordKind === ts.SyntaxKind.ThisKeyword);
399345
}
400346

347+
public static findInClassOrAncestor(
348+
classDeclaration: ts.ClassLikeDeclarationBase,
349+
callback: (classDeclaration: ts.ClassLikeDeclarationBase) => boolean,
350+
checker: ts.TypeChecker
351+
): ts.ClassLikeDeclarationBase
352+
{
353+
if (callback(classDeclaration)) {
354+
return classDeclaration;
355+
}
356+
357+
const extendsType = TSHelper.getExtendedType(classDeclaration, checker);
358+
if (!extendsType) {
359+
return undefined;
360+
}
361+
362+
const symbol = extendsType.getSymbol();
363+
const declaration = symbol.getDeclarations().find(ts.isClassLike);
364+
if (!declaration) {
365+
return undefined;
366+
}
367+
368+
return TSHelper.findInClassOrAncestor(declaration, callback, checker);
369+
}
370+
371+
public static hasSetAccessorInClassOrAncestor(
372+
classDeclaration: ts.ClassLikeDeclarationBase,
373+
checker: ts.TypeChecker
374+
): boolean
375+
{
376+
return TSHelper.findInClassOrAncestor(
377+
classDeclaration,
378+
c => c.members.some(ts.isSetAccessor),
379+
checker
380+
) !== undefined;
381+
}
382+
383+
public static getPropertyName(propertyName: ts.PropertyName): string | number | undefined {
384+
if (ts.isIdentifier(propertyName) || ts.isStringLiteral(propertyName) || ts.isNumericLiteral(propertyName)) {
385+
return propertyName.text;
386+
} else {
387+
return undefined; // TODO: how to handle computed property names?
388+
}
389+
}
390+
391+
public static isSamePropertyName(a: ts.PropertyName, b: ts.PropertyName): boolean {
392+
const aName = TSHelper.getPropertyName(a);
393+
const bName = TSHelper.getPropertyName(b);
394+
return aName !== undefined && aName === bName;
395+
}
396+
397+
public static isGetAccessorOverride(
398+
element: ts.ClassElement,
399+
classDeclaration: ts.ClassLikeDeclarationBase,
400+
checker: ts.TypeChecker
401+
): element is ts.GetAccessorDeclaration
402+
{
403+
if (!ts.isGetAccessor(element)) {
404+
return false;
405+
}
406+
407+
const hasInitializedField = (e: ts.ClassElement) =>
408+
ts.isPropertyDeclaration(e)
409+
&& e.initializer
410+
&& TSHelper.isSamePropertyName(e.name, element.name);
411+
412+
return TSHelper.findInClassOrAncestor(
413+
classDeclaration,
414+
c => c.members.some(hasInitializedField),
415+
checker
416+
) !== undefined;
417+
}
418+
401419
public static inferAssignedType(expression: ts.Expression, checker: ts.TypeChecker): ts.Type {
402420
if (ts.isParenthesizedExpression(expression.parent)) {
403421
// Ignore expressions wrapped in parenthesis

0 commit comments

Comments
 (0)