diff --git a/src/Errors.ts b/src/Errors.ts index 556bf4775..b804c3905 100644 --- a/src/Errors.ts +++ b/src/Errors.ts @@ -116,4 +116,9 @@ export class TSTLErrors { + "the TupleReturn decorator.", node); } + + public static UnsupportedUnionAccessor = (node: ts.Node) => { + return new TranspileError(`Unsupported union of accessor with non-accessor types.`, + node); + } } diff --git a/src/TSHelper.ts b/src/TSHelper.ts index 1259ab423..0cc296de9 100644 --- a/src/TSHelper.ts +++ b/src/TSHelper.ts @@ -245,11 +245,22 @@ export class TSHelper { } } - public static hasGetAccessor(node: ts.Node, checker: ts.TypeChecker): boolean { + public static typeHasGetAccessor(type: ts.Type, name: ts.__String, checker: ts.TypeChecker): boolean | undefined { + if (type.isUnion()) { + if (type.types.some(t => this.typeHasGetAccessor(t, name, checker))) { + // undefined if only a subset of types implements the accessor + return type.types.every(t => this.typeHasGetAccessor(t, name, checker)) ? true : undefined; + } + return false; + } + return this.forTypeOrAnySupertype(type, checker, t => this.hasExplicitGetAccessor(t, name)); + } + + public static hasGetAccessor(node: ts.Node, checker: ts.TypeChecker): boolean | undefined { if (ts.isPropertyAccessExpression(node)) { const name = node.name.escapedText; const type = checker.getTypeAtLocation(node.expression); - return this.forTypeOrAnySupertype(type, checker, t => this.hasExplicitGetAccessor(t, name)); + return this.typeHasGetAccessor(type, name, checker); } return false; } @@ -261,11 +272,22 @@ export class TSHelper { } } + public static typeHasSetAccessor(type: ts.Type, name: ts.__String, checker: ts.TypeChecker): boolean | undefined { + if (type.isUnion()) { + if (type.types.some(t => this.typeHasSetAccessor(t, name, checker))) { + // undefined if only a subset of types implements the accessor + return type.types.every(t => this.typeHasSetAccessor(t, name, checker)) ? true : undefined; + } + return false; + } + return this.forTypeOrAnySupertype(type, checker, t => this.hasExplicitSetAccessor(t, name)); + } + public static hasSetAccessor(node: ts.Node, checker: ts.TypeChecker): boolean { if (ts.isPropertyAccessExpression(node)) { const name = node.name.escapedText; const type = checker.getTypeAtLocation(node.expression); - return this.forTypeOrAnySupertype(type, checker, t => this.hasExplicitSetAccessor(t, name)); + return this.typeHasSetAccessor(type, name, checker); } return false; } diff --git a/src/Transpiler.ts b/src/Transpiler.ts index 39289c37e..9ea324cad 100644 --- a/src/Transpiler.ts +++ b/src/Transpiler.ts @@ -965,8 +965,11 @@ export abstract class LuaTranspiler { } public transpileAssignment(node: ts.BinaryExpression, lhs: string, rhs: string): string { - if (tsHelper.hasSetAccessor(node.left, this.checker)) { + const hasSetAccessor = tsHelper.hasSetAccessor(node.left, this.checker); + if (hasSetAccessor) { return this.transpileSetAccessor(node.left as ts.PropertyAccessExpression, rhs); + } else if (hasSetAccessor === undefined) { + throw TSTLErrors.UnsupportedUnionAccessor(node); } // Validate assignment @@ -1611,8 +1614,11 @@ export abstract class LuaTranspiler { public transpilePropertyAccessExpression(node: ts.PropertyAccessExpression): string { const property = node.name.text; - if (tsHelper.hasGetAccessor(node, this.checker)) { + const hasGetAccessor = tsHelper.hasGetAccessor(node, this.checker); + if (hasGetAccessor) { return this.transpileGetAccessor(node); + } else if (hasGetAccessor === undefined) { + throw TSTLErrors.UnsupportedUnionAccessor(node); } // Check for primitive types to override diff --git a/test/unit/expressions.spec.ts b/test/unit/expressions.spec.ts index 2e86beac7..c77722d37 100644 --- a/test/unit/expressions.spec.ts +++ b/test/unit/expressions.spec.ts @@ -317,6 +317,34 @@ export class ExpressionTests { Expect(result).toBe(expected); } + @TestCase("return x.value;", 1) + @TestCase("x.value = 3; return x.value;", 3) + @Test("Union accessors") + public unionAccessors(expression: string, expected: any): void { + const source = `class A{ get value(){ return this.v || 1; } set value(v){ this.v = v; } v: number; } + class B{ get value(){ return this.v || 2; } set value(v){ this.v = v; } v: number; } + let x: A|B = new A(); + ${expression}`; + + const lua = util.transpileString(source); + const result = util.executeLua(lua); + Expect(result).toBe(expected); + } + + @TestCase("x.value = 3;") + @TestCase("return x.value;") + @Test("Unsupported Union accessors") + public unsupportedUnionAccessors(expression: string): void { + const source = `class A{ get value(){ return 1; } } + class B{ value:number = 3; } + let x: A|B = new A(); + ${expression}`; + Expect(() => { util.transpileString(source); }).toThrowError( + TranspileError, + "Unsupported union of accessor with non-accessor types." + ); + } + @TestCase("i++", 10) @TestCase("i--", 10) @TestCase("++i", 11)