Skip to content

Commit ecf6d08

Browse files
authored
Ts 5.0 decorators (#1449)
* Tests for new decorators * Rename old decorate function to DecorateLegacy * Method decorators and partially accessor decorators rewritten * Modern decorators except field decorators working but with some regression to legacy * All decorators legacy and modern work, except for field decorators * Finalize property decorators * Fix unit tests
1 parent 4a3b9d0 commit ecf6d08

File tree

17 files changed

+838
-263
lines changed

17 files changed

+838
-263
lines changed

src/LuaLib.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export enum LuaLibFeature {
3838
CloneDescriptor = "CloneDescriptor",
3939
CountVarargs = "CountVarargs",
4040
Decorate = "Decorate",
41+
DecorateLegacy = "DecorateLegacy",
4142
DecorateParam = "DecorateParam",
4243
Delete = "Delete",
4344
DelegatedYield = "DelegatedYield",

src/lualib/Decorate.ts

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,20 @@
11
/**
2-
* SEE: https://github.com/Microsoft/TypeScript/blob/master/src/compiler/transformers/ts.ts#L3598
2+
* TypeScript 5.0 decorators
33
*/
4-
import { __TS__ObjectGetOwnPropertyDescriptor } from "./ObjectGetOwnPropertyDescriptor";
5-
import { __TS__SetDescriptor } from "./SetDescriptor";
64
import { Decorator } from "./Decorator";
75

8-
export function __TS__Decorate<TTarget extends AnyTable, TKey extends keyof TTarget>(
9-
this: void,
10-
decorators: Array<Decorator<TTarget, TKey>>,
11-
target: TTarget,
12-
key?: TKey,
13-
desc?: any
6+
export function __TS__Decorate<TClass, TTarget>(
7+
this: TClass,
8+
originalValue: TTarget,
9+
decorators: Array<Decorator<TTarget>>,
10+
context: DecoratorContext
1411
): TTarget {
15-
let result = target;
12+
let result = originalValue;
1613

1714
for (let i = decorators.length; i >= 0; i--) {
1815
const decorator = decorators[i];
1916
if (decorator !== undefined) {
20-
const oldResult = result;
21-
22-
if (key === undefined) {
23-
result = decorator(result);
24-
} else if (desc === true) {
25-
const value = rawget(target, key);
26-
const descriptor = __TS__ObjectGetOwnPropertyDescriptor(target, key) ?? {
27-
configurable: true,
28-
writable: true,
29-
value,
30-
};
31-
const desc = decorator(target, key, descriptor) || descriptor;
32-
const isSimpleValue = desc.configurable === true && desc.writable === true && !desc.get && !desc.set;
33-
if (isSimpleValue) {
34-
rawset(target, key, desc.value);
35-
} else {
36-
__TS__SetDescriptor(target, key, { ...descriptor, ...desc });
37-
}
38-
} else if (desc === false) {
39-
result = decorator(target, key, desc);
40-
} else {
41-
result = decorator(target, key);
42-
}
43-
44-
result = result || oldResult;
17+
result = decorator.call(this, result, context) ?? result;
4518
}
4619
}
4720

src/lualib/DecorateLegacy.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Old-style decorators, activated by enabling the experimentalDecorators flag
3+
*/
4+
import { __TS__ObjectGetOwnPropertyDescriptor } from "./ObjectGetOwnPropertyDescriptor";
5+
import { __TS__SetDescriptor } from "./SetDescriptor";
6+
7+
export type LegacyDecorator<TTarget extends AnyTable, TKey extends keyof TTarget> = (
8+
target: TTarget,
9+
key?: TKey,
10+
descriptor?: PropertyDescriptor
11+
) => TTarget;
12+
13+
export function __TS__DecorateLegacy<TTarget extends AnyTable, TKey extends keyof TTarget>(
14+
this: void,
15+
decorators: Array<LegacyDecorator<TTarget, TKey>>,
16+
target: TTarget,
17+
key?: TKey,
18+
desc?: any
19+
): TTarget {
20+
let result = target;
21+
22+
for (let i = decorators.length; i >= 0; i--) {
23+
const decorator = decorators[i];
24+
if (decorator !== undefined) {
25+
const oldResult = result;
26+
27+
if (key === undefined) {
28+
result = decorator(result);
29+
} else if (desc === true) {
30+
const value = rawget(target, key);
31+
const descriptor = __TS__ObjectGetOwnPropertyDescriptor(target, key) ?? {
32+
configurable: true,
33+
writable: true,
34+
value,
35+
};
36+
const desc = decorator(target, key, descriptor) || descriptor;
37+
const isSimpleValue = desc.configurable === true && desc.writable === true && !desc.get && !desc.set;
38+
if (isSimpleValue) {
39+
rawset(target, key, desc.value);
40+
} else {
41+
__TS__SetDescriptor(target, key, { ...descriptor, ...desc });
42+
}
43+
} else if (desc === false) {
44+
result = decorator(target, key, desc);
45+
} else {
46+
result = decorator(target, key);
47+
}
48+
49+
result = result || oldResult;
50+
}
51+
}
52+
53+
return result;
54+
}

src/lualib/DecorateParam.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Decorator } from "./Decorator";
1+
import type { LegacyDecorator } from "./DecorateLegacy";
22

33
type ParamDecorator<TTarget extends AnyTable, TKey extends keyof TTarget> = (
44
target: TTarget,
@@ -9,6 +9,6 @@ export function __TS__DecorateParam<TTarget extends AnyTable, TKey extends keyof
99
this: void,
1010
paramIndex: number,
1111
decorator: ParamDecorator<TTarget, TKey>
12-
): Decorator<TTarget, TKey> {
12+
): LegacyDecorator<TTarget, TKey> {
1313
return (target: TTarget, key?: TKey) => decorator(target, key, paramIndex);
1414
}

src/lualib/Decorator.d.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1 @@
1-
export type Decorator<TTarget extends AnyTable, TKey extends keyof TTarget> = (
2-
target: TTarget,
3-
key?: TKey,
4-
descriptor?: PropertyDescriptor
5-
) => TTarget;
1+
export type Decorator<TTarget> = (target: TTarget, context: DecoratorContext) => TTarget;

src/transformation/utils/diagnostics.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,7 @@ export const invalidSpreadInCallExtension = createErrorDiagnosticFactory(
168168
export const cannotAssignToNodeOfKind = createErrorDiagnosticFactory(
169169
(kind: lua.SyntaxKind) => `Cannot create assignment assigning to a node of type ${lua.SyntaxKind[kind]}.`
170170
);
171+
172+
export const incompleteFieldDecoratorWarning = createWarningDiagnosticFactory(
173+
"You are using a class field decorator, note that tstl ignores returned value initializers!"
174+
);

src/transformation/visitors/class/decorators.ts

Lines changed: 195 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import * as ts from "typescript";
22
import * as lua from "../../../LuaAST";
33
import { TransformationContext } from "../../context";
4-
import { decoratorInvalidContext } from "../../utils/diagnostics";
4+
import { decoratorInvalidContext, incompleteFieldDecoratorWarning } from "../../utils/diagnostics";
55
import { ContextType, getFunctionContextType } from "../../utils/function-context";
66
import { LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib";
7+
import { isNonNull } from "../../../utils";
8+
import { transformMemberExpressionOwnerName, transformMethodName } from "./members/method";
9+
import { transformPropertyName } from "../literal";
10+
import { isPrivateNode, isStaticNode } from "./utils";
711

812
export function transformDecoratorExpression(context: TransformationContext, decorator: ts.Decorator): lua.Expression {
913
const expression = decorator.expression;
@@ -16,7 +20,161 @@ export function transformDecoratorExpression(context: TransformationContext, dec
1620
return context.transformExpression(expression);
1721
}
1822

19-
export function createDecoratingExpression(
23+
export function createClassDecoratingExpression(
24+
context: TransformationContext,
25+
classDeclaration: ts.ClassDeclaration | ts.ClassExpression,
26+
className: lua.Expression
27+
): lua.Expression {
28+
const classDecorators =
29+
ts.getDecorators(classDeclaration)?.map(d => transformDecoratorExpression(context, d)) ?? [];
30+
31+
// If experimentalDecorators flag is set, decorate with legacy decorator logic
32+
if (context.options.experimentalDecorators) {
33+
return createLegacyDecoratingExpression(context, classDeclaration.kind, classDecorators, className);
34+
}
35+
36+
// Else: TypeScript 5.0 decorator
37+
return createDecoratingExpression(context, className, className, classDecorators, {
38+
kind: lua.createStringLiteral("class"),
39+
name: lua.createStringLiteral(classDeclaration.name?.getText() ?? ""),
40+
});
41+
}
42+
43+
export function createClassMethodDecoratingExpression(
44+
context: TransformationContext,
45+
methodDeclaration: ts.MethodDeclaration,
46+
originalMethod: lua.Expression,
47+
className: lua.Identifier
48+
): lua.Expression {
49+
const parameterDecorators = getParameterDecorators(context, methodDeclaration);
50+
const methodDecorators =
51+
ts.getDecorators(methodDeclaration)?.map(d => transformDecoratorExpression(context, d)) ?? [];
52+
53+
const methodName = transformMethodName(context, methodDeclaration);
54+
55+
// If experimentalDecorators flag is set, decorate with legacy decorator logic
56+
if (context.options.experimentalDecorators) {
57+
const methodTable = transformMemberExpressionOwnerName(methodDeclaration, className);
58+
return createLegacyDecoratingExpression(
59+
context,
60+
methodDeclaration.kind,
61+
[...methodDecorators, ...parameterDecorators],
62+
methodTable,
63+
methodName
64+
);
65+
}
66+
67+
// Else: TypeScript 5.0 decorator
68+
return createDecoratingExpression(context, className, originalMethod, methodDecorators, {
69+
kind: lua.createStringLiteral("method"),
70+
name: methodName,
71+
private: lua.createBooleanLiteral(isPrivateNode(methodDeclaration)),
72+
static: lua.createBooleanLiteral(isStaticNode(methodDeclaration)),
73+
});
74+
}
75+
76+
export function createClassAccessorDecoratingExpression(
77+
context: TransformationContext,
78+
accessor: ts.AccessorDeclaration,
79+
originalAccessor: lua.Expression,
80+
className: lua.Identifier
81+
): lua.Expression {
82+
const accessorDecorators = ts.getDecorators(accessor)?.map(d => transformDecoratorExpression(context, d)) ?? [];
83+
const propertyName = transformPropertyName(context, accessor.name);
84+
85+
// If experimentalDecorators flag is set, decorate with legacy decorator logic
86+
if (context.options.experimentalDecorators) {
87+
const propertyOwnerTable = transformMemberExpressionOwnerName(accessor, className);
88+
89+
return createLegacyDecoratingExpression(
90+
context,
91+
accessor.kind,
92+
accessorDecorators,
93+
propertyOwnerTable,
94+
propertyName
95+
);
96+
}
97+
98+
// Else: TypeScript 5.0 decorator
99+
return createDecoratingExpression(context, className, originalAccessor, accessorDecorators, {
100+
kind: lua.createStringLiteral(accessor.kind === ts.SyntaxKind.SetAccessor ? "setter" : "getter"),
101+
name: propertyName,
102+
private: lua.createBooleanLiteral(isPrivateNode(accessor)),
103+
static: lua.createBooleanLiteral(isStaticNode(accessor)),
104+
});
105+
}
106+
107+
export function createClassPropertyDecoratingExpression(
108+
context: TransformationContext,
109+
property: ts.PropertyDeclaration,
110+
className: lua.Identifier
111+
): lua.Expression {
112+
const decorators = ts.getDecorators(property) ?? [];
113+
const propertyDecorators = decorators.map(d => transformDecoratorExpression(context, d));
114+
115+
// If experimentalDecorators flag is set, decorate with legacy decorator logic
116+
if (context.options.experimentalDecorators) {
117+
const propertyName = transformPropertyName(context, property.name);
118+
const propertyOwnerTable = transformMemberExpressionOwnerName(property, className);
119+
120+
return createLegacyDecoratingExpression(
121+
context,
122+
property.kind,
123+
propertyDecorators,
124+
propertyOwnerTable,
125+
propertyName
126+
);
127+
}
128+
129+
// Else: TypeScript 5.0 decorator
130+
131+
// Add a diagnostic when something is returned from a field decorator
132+
for (const decorator of decorators) {
133+
const signature = context.checker.getResolvedSignature(decorator);
134+
const decoratorReturnType = signature?.getReturnType();
135+
// If return type of decorator is NOT void
136+
if (decoratorReturnType && (decoratorReturnType.flags & ts.TypeFlags.Void) === 0) {
137+
context.diagnostics.push(incompleteFieldDecoratorWarning(property));
138+
}
139+
}
140+
141+
return createDecoratingExpression(context, className, lua.createNilLiteral(), propertyDecorators, {
142+
kind: lua.createStringLiteral("field"),
143+
name: lua.createStringLiteral(property.name.getText()),
144+
private: lua.createBooleanLiteral(isPrivateNode(property)),
145+
static: lua.createBooleanLiteral(isStaticNode(property)),
146+
});
147+
}
148+
149+
function createDecoratingExpression<TValue extends lua.Expression>(
150+
context: TransformationContext,
151+
className: lua.Expression,
152+
originalValue: TValue,
153+
decorators: lua.Expression[],
154+
decoratorContext: Record<string, lua.Expression>
155+
): lua.Expression {
156+
const decoratorTable = lua.createTableExpression(decorators.map(d => lua.createTableFieldExpression(d)));
157+
const decoratorContextTable = objectToLuaTableLiteral(decoratorContext);
158+
159+
return transformLuaLibFunction(
160+
context,
161+
LuaLibFeature.Decorate,
162+
undefined,
163+
className,
164+
originalValue,
165+
decoratorTable,
166+
decoratorContextTable
167+
);
168+
}
169+
170+
function objectToLuaTableLiteral(obj: Record<string, lua.Expression>): lua.Expression {
171+
return lua.createTableExpression(
172+
Object.entries(obj).map(([key, value]) => lua.createTableFieldExpression(value, lua.createStringLiteral(key)))
173+
);
174+
}
175+
176+
// Legacy decorators:
177+
function createLegacyDecoratingExpression(
20178
context: TransformationContext,
21179
kind: ts.SyntaxKind,
22180
decorators: lua.Expression[],
@@ -35,5 +193,39 @@ export function createDecoratingExpression(
35193
trailingExpressions.push(isMethodOrAccessor ? lua.createBooleanLiteral(true) : lua.createNilLiteral());
36194
}
37195

38-
return transformLuaLibFunction(context, LuaLibFeature.Decorate, undefined, ...trailingExpressions);
196+
return transformLuaLibFunction(context, LuaLibFeature.DecorateLegacy, undefined, ...trailingExpressions);
197+
}
198+
199+
function getParameterDecorators(
200+
context: TransformationContext,
201+
node: ts.FunctionLikeDeclarationBase
202+
): lua.CallExpression[] {
203+
return node.parameters
204+
.flatMap((parameter, index) =>
205+
ts
206+
.getDecorators(parameter)
207+
?.map(decorator =>
208+
transformLuaLibFunction(
209+
context,
210+
LuaLibFeature.DecorateParam,
211+
node,
212+
lua.createNumericLiteral(index),
213+
transformDecoratorExpression(context, decorator)
214+
)
215+
)
216+
)
217+
.filter(isNonNull);
218+
}
219+
220+
export function createConstructorDecoratingExpression(
221+
context: TransformationContext,
222+
node: ts.ConstructorDeclaration,
223+
className: lua.Identifier
224+
): lua.Statement | undefined {
225+
const parameterDecorators = getParameterDecorators(context, node);
226+
227+
if (parameterDecorators.length > 0) {
228+
const decorateMethod = createLegacyDecoratingExpression(context, node.kind, parameterDecorators, className);
229+
return lua.createExpressionStatement(decorateMethod);
230+
}
39231
}

0 commit comments

Comments
 (0)