Skip to content

Commit c304078

Browse files
thatcosmonautPerryvw
authored andcommitted
Class decorators (#558)
* class decorator tests * decorate lualib function * fix decorate emission + decorate lualib * add variable argument decorator test * add decorator with class inheritance test * forgot to remove a comment * add missing end newline * revert accidental import * use transformLuaLibFunction * revert importLuaLib refactor * add test case for applying decorators in order * change decorator name in test case * improve decorator order test * improve Decorate helper * rename generate to create * simplify decorator expression table generator * shorten test string * fix prettier style * add error if decorator function context is void * remove accidental this: void from decorator test * clarified error message * change createConstructorDecorationStatement to private * better decorator application test name
1 parent 55da476 commit c304078

6 files changed

Lines changed: 260 additions & 0 deletions

File tree

src/LuaLib.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export enum LuaLibFeature {
2222
ArraySetLength = "ArraySetLength",
2323
ClassIndex = "ClassIndex",
2424
ClassNewIndex = "ClassNewIndex",
25+
Decorate = "Decorate",
2526
FunctionApply = "FunctionApply",
2627
FunctionBind = "FunctionBind",
2728
FunctionCall = "FunctionCall",

src/LuaTransformer.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,11 @@ export class LuaTransformer {
650650
result.push(fieldAssign);
651651
}
652652

653+
const decorationStatement = this.createConstructorDecorationStatement(statement);
654+
if (decorationStatement) {
655+
result.push(decorationStatement);
656+
}
657+
653658
this.classStack.pop();
654659

655660
return result;
@@ -5356,4 +5361,39 @@ export class LuaTransformer {
53565361
return visitResult;
53575362
}
53585363
}
5364+
5365+
private createConstructorDecorationStatement(
5366+
declaration: ts.ClassLikeDeclaration
5367+
): tstl.AssignmentStatement | undefined {
5368+
const className = declaration.name !== undefined
5369+
? this.transformIdentifier(declaration.name)
5370+
: tstl.createAnonymousIdentifier();
5371+
5372+
const decorators = declaration.decorators;
5373+
if (!decorators) { return undefined; }
5374+
5375+
const decoratorExpressions = this.filterUndefined(
5376+
decorators.map(decorator => {
5377+
const expression = decorator.expression;
5378+
const type = this.checker.getTypeAtLocation(expression);
5379+
const context = tsHelper.getFunctionContextType(type, this.checker);
5380+
if (context === ContextType.Void) { throw TSTLErrors.InvalidDecoratorContext(decorator); }
5381+
return this.transformExpression(expression);
5382+
})
5383+
);
5384+
5385+
const decoratorArguments: tstl.Expression[] = [];
5386+
5387+
const decoratorTable = tstl.createTableExpression(
5388+
decoratorExpressions.map(expression => tstl.createTableFieldExpression(expression))
5389+
);
5390+
5391+
decoratorArguments.push(decoratorTable);
5392+
decoratorArguments.push(className);
5393+
5394+
return tstl.createAssignmentStatement(
5395+
className,
5396+
this.transformLuaLibFunction(LuaLibFeature.Decorate, undefined, ...decoratorArguments)
5397+
);
5398+
}
53595399
}

src/TSTLErrors.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ export class TSTLErrors {
4242
public static InvalidDecoratorArgumentNumber = (name: string, got: number, expected: number, node: ts.Node) =>
4343
new TranspileError(`${name} expects ${expected} argument(s) but got ${got}.`, node);
4444

45+
public static InvalidDecoratorContext = (node: ts.Node) =>
46+
new TranspileError(`Decorator function cannot have 'this: void'.`, node);
47+
4548
public static InvalidExtensionMetaExtension = (node: ts.Node) =>
4649
new TranspileError(`Cannot use both '@extension' and '@metaExtension' decorators on the same class.`, node);
4750

src/lualib/Decorate.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* SEE: https://github.com/Microsoft/TypeScript/blob/master/src/compiler/transformers/ts.ts#L3598
3+
*/
4+
function __TS__Decorate(this: void, decorators: Function[], target: {}, key?: string, desc?: any): {} {
5+
let result = target;
6+
7+
for (let i = decorators.length; i >= 0; i--) {
8+
const decorator = decorators[i];
9+
if (decorator) {
10+
const oldResult = result;
11+
12+
if (key === undefined) {
13+
result = decorator(result);
14+
} else if (desc !== undefined) {
15+
result = decorator(target, key, result);
16+
} else {
17+
result = decorator(target, key);
18+
}
19+
20+
result = result || oldResult;
21+
}
22+
}
23+
24+
return result;
25+
}

test/unit/classDecorator.spec.ts

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import * as util from "../util";
2+
import { TSTLErrors } from "../../src/TSTLErrors";
3+
4+
test("Class decorator with no parameters", () => {
5+
const source = `
6+
function SetBool<T extends { new(...args: any[]): {} }>(constructor: T) {
7+
return class extends constructor {
8+
decoratorBool = true;
9+
}
10+
}
11+
12+
@SetBool
13+
class TestClass {
14+
public decoratorBool = false;
15+
}
16+
17+
const classInstance = new TestClass();
18+
return classInstance.decoratorBool;
19+
`;
20+
21+
const result = util.transpileAndExecute(source);
22+
expect(result).toBe(true);
23+
});
24+
25+
test("Class decorator with parameters", () => {
26+
const source = `
27+
function SetNum(numArg: number) {
28+
return <T extends new(...args: any[]) => {}>(constructor: T) => {
29+
return class extends constructor {
30+
decoratorNum = numArg;
31+
};
32+
};
33+
}
34+
35+
@SetNum(420)
36+
class TestClass {
37+
public decoratorNum;
38+
}
39+
40+
const classInstance = new TestClass();
41+
return classInstance.decoratorNum;
42+
`;
43+
44+
const result = util.transpileAndExecute(source);
45+
expect(result).toBe(420);
46+
});
47+
48+
test("Class decorator with variable parameters", () => {
49+
const source = `
50+
function SetNumbers(...numArgs: number[]) {
51+
return <T extends new(...args: any[]) => {}>(constructor: T) => {
52+
return class extends constructor {
53+
decoratorNums = new Set<number>(numArgs);
54+
};
55+
};
56+
}
57+
58+
@SetNumbers(120, 30, 54)
59+
class TestClass {
60+
public decoratorNums;
61+
}
62+
63+
const classInstance = new TestClass();
64+
let sum = 0;
65+
for (const value of classInstance.decoratorNums) {
66+
sum += value;
67+
}
68+
return sum;
69+
`;
70+
71+
const result = util.transpileAndExecute(source);
72+
expect(result).toBe(204);
73+
});
74+
75+
test("Multiple class decorators", () => {
76+
const source = `
77+
function SetTen<T extends { new(...args: any[]): {} }>(constructor: T) {
78+
return class extends constructor {
79+
decoratorTen = 10;
80+
}
81+
}
82+
83+
function SetNum(numArg: number) {
84+
return <T extends new(...args: any[]) => {}>(constructor: T) => {
85+
return class extends constructor {
86+
decoratorNum = numArg;
87+
};
88+
};
89+
}
90+
91+
@SetTen
92+
@SetNum(410)
93+
class TestClass {
94+
public decoratorTen;
95+
public decoratorNum;
96+
}
97+
98+
const classInstance = new TestClass();
99+
return classInstance.decoratorNum + classInstance.decoratorTen;
100+
`;
101+
102+
const result = util.transpileAndExecute(source);
103+
expect(result).toBe(420);
104+
});
105+
106+
test("Class decorator with inheritance", () => {
107+
const source = `
108+
function SetTen<T extends { new(...args: any[]): {} }>(constructor: T) {
109+
return class extends constructor {
110+
decoratorTen = 10;
111+
}
112+
}
113+
114+
function SetNum(numArg: number) {
115+
return <T extends new(...args: any[]) => {}>(constructor: T) => {
116+
return class extends constructor {
117+
decoratorNum = numArg;
118+
};
119+
};
120+
}
121+
122+
class TestClass {
123+
public decoratorTen = 0;
124+
public decoratorNum = 0;
125+
}
126+
127+
@SetTen
128+
@SetNum(410)
129+
class SubTestClass extends TestClass {}
130+
131+
const classInstance = new SubTestClass();
132+
return classInstance.decoratorNum + classInstance.decoratorTen;
133+
`;
134+
135+
const result = util.transpileAndExecute(source);
136+
expect(result).toBe(420);
137+
});
138+
139+
test("Class decorators are applied in order and executed in reverse order", () => {
140+
const source = `
141+
const order = [];
142+
143+
function SetString(stringArg: string) {
144+
order.push("eval " + stringArg);
145+
return <T extends new (...args: any[]) => {}>(constructor: T) => {
146+
order.push("execute " + stringArg);
147+
return class extends constructor {
148+
decoratorString = stringArg;
149+
};
150+
};
151+
}
152+
153+
@SetString("fox")
154+
@SetString("jumped")
155+
@SetString("over dog")
156+
class TestClass {
157+
public static decoratorString = "";
158+
}
159+
160+
const inst = new TestClass();
161+
return order.join(" ");
162+
`;
163+
164+
const result = util.transpileAndExecute(source);
165+
expect(result).toBe(
166+
"eval fox eval jumped eval over dog execute over dog execute jumped execute fox",
167+
);
168+
});
169+
170+
test("Throws error if decorator function has void context", () => {
171+
const source = `
172+
function SetBool<T extends { new(...args: any[]): {} }>(this: void, constructor: T) {
173+
return class extends constructor {
174+
decoratorBool = true;
175+
}
176+
}
177+
178+
@SetBool
179+
class TestClass {
180+
public decoratorBool = false;
181+
}
182+
183+
const classInstance = new TestClass();
184+
return classInstance.decoratorBool;
185+
`;
186+
187+
expect(() => util.transpileAndExecute(source)).toThrowExactError(
188+
TSTLErrors.InvalidDecoratorContext(util.nodeStub),
189+
);
190+
});

test/util.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export function transpileStringResult(
3636
"lib.es2018.d.ts",
3737
"lib.esnext.d.ts",
3838
],
39+
experimentalDecorators: true,
3940
...options,
4041
};
4142

0 commit comments

Comments
 (0)