Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/LuaLib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export enum LuaLibFeature {
CloneDescriptor = "CloneDescriptor",
CountVarargs = "CountVarargs",
Decorate = "Decorate",
DecorateLegacy = "DecorateLegacy",
DecorateParam = "DecorateParam",
Delete = "Delete",
DelegatedYield = "DelegatedYield",
Expand Down
43 changes: 8 additions & 35 deletions src/lualib/Decorate.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,20 @@
/**
* SEE: https://github.com/Microsoft/TypeScript/blob/master/src/compiler/transformers/ts.ts#L3598
* TypeScript 5.0 decorators
*/
import { __TS__ObjectGetOwnPropertyDescriptor } from "./ObjectGetOwnPropertyDescriptor";
import { __TS__SetDescriptor } from "./SetDescriptor";
import { Decorator } from "./Decorator";

export function __TS__Decorate<TTarget extends AnyTable, TKey extends keyof TTarget>(
this: void,
decorators: Array<Decorator<TTarget, TKey>>,
target: TTarget,
key?: TKey,
desc?: any
export function __TS__Decorate<TClass, TTarget>(
this: TClass,
originalValue: TTarget,
decorators: Array<Decorator<TTarget>>,
context: DecoratorContext
): TTarget {
let result = target;
let result = originalValue;

for (let i = decorators.length; i >= 0; i--) {
const decorator = decorators[i];
if (decorator !== undefined) {
const oldResult = result;

if (key === undefined) {
result = decorator(result);
} else if (desc === true) {
const value = rawget(target, key);
const descriptor = __TS__ObjectGetOwnPropertyDescriptor(target, key) ?? {
configurable: true,
writable: true,
value,
};
const desc = decorator(target, key, descriptor) || descriptor;
const isSimpleValue = desc.configurable === true && desc.writable === true && !desc.get && !desc.set;
if (isSimpleValue) {
rawset(target, key, desc.value);
} else {
__TS__SetDescriptor(target, key, { ...descriptor, ...desc });
}
} else if (desc === false) {
result = decorator(target, key, desc);
} else {
result = decorator(target, key);
}

result = result || oldResult;
result = decorator.call(this, result, context) ?? result;
}
}

Expand Down
54 changes: 54 additions & 0 deletions src/lualib/DecorateLegacy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Old-style decorators, activated by enabling the experimentalDecorators flag
*/
import { __TS__ObjectGetOwnPropertyDescriptor } from "./ObjectGetOwnPropertyDescriptor";
import { __TS__SetDescriptor } from "./SetDescriptor";

export type LegacyDecorator<TTarget extends AnyTable, TKey extends keyof TTarget> = (
target: TTarget,
key?: TKey,
descriptor?: PropertyDescriptor
) => TTarget;

export function __TS__DecorateLegacy<TTarget extends AnyTable, TKey extends keyof TTarget>(
this: void,
decorators: Array<LegacyDecorator<TTarget, TKey>>,
target: TTarget,
key?: TKey,
desc?: any
): TTarget {
let result = target;

for (let i = decorators.length; i >= 0; i--) {
const decorator = decorators[i];
if (decorator !== undefined) {
const oldResult = result;

if (key === undefined) {
result = decorator(result);
} else if (desc === true) {
const value = rawget(target, key);
const descriptor = __TS__ObjectGetOwnPropertyDescriptor(target, key) ?? {
configurable: true,
writable: true,
value,
};
const desc = decorator(target, key, descriptor) || descriptor;
const isSimpleValue = desc.configurable === true && desc.writable === true && !desc.get && !desc.set;
if (isSimpleValue) {
rawset(target, key, desc.value);
} else {
__TS__SetDescriptor(target, key, { ...descriptor, ...desc });
}
} else if (desc === false) {
result = decorator(target, key, desc);
} else {
result = decorator(target, key);
}

result = result || oldResult;
}
}

return result;
}
4 changes: 2 additions & 2 deletions src/lualib/DecorateParam.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Decorator } from "./Decorator";
import type { LegacyDecorator } from "./DecorateLegacy";

type ParamDecorator<TTarget extends AnyTable, TKey extends keyof TTarget> = (
target: TTarget,
Expand All @@ -9,6 +9,6 @@ export function __TS__DecorateParam<TTarget extends AnyTable, TKey extends keyof
this: void,
paramIndex: number,
decorator: ParamDecorator<TTarget, TKey>
): Decorator<TTarget, TKey> {
): LegacyDecorator<TTarget, TKey> {
return (target: TTarget, key?: TKey) => decorator(target, key, paramIndex);
}
6 changes: 1 addition & 5 deletions src/lualib/Decorator.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
export type Decorator<TTarget extends AnyTable, TKey extends keyof TTarget> = (
target: TTarget,
key?: TKey,
descriptor?: PropertyDescriptor
) => TTarget;
export type Decorator<TTarget> = (target: TTarget, context: DecoratorContext) => TTarget;
4 changes: 4 additions & 0 deletions src/transformation/utils/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,7 @@ export const invalidSpreadInCallExtension = createErrorDiagnosticFactory(
export const cannotAssignToNodeOfKind = createErrorDiagnosticFactory(
(kind: lua.SyntaxKind) => `Cannot create assignment assigning to a node of type ${lua.SyntaxKind[kind]}.`
);

export const incompleteFieldDecoratorWarning = createWarningDiagnosticFactory(
"You are using a class field decorator, note that tstl ignores returned value initializers!"
);
198 changes: 195 additions & 3 deletions src/transformation/visitors/class/decorators.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import * as ts from "typescript";
import * as lua from "../../../LuaAST";
import { TransformationContext } from "../../context";
import { decoratorInvalidContext } from "../../utils/diagnostics";
import { decoratorInvalidContext, incompleteFieldDecoratorWarning } from "../../utils/diagnostics";
import { ContextType, getFunctionContextType } from "../../utils/function-context";
import { LuaLibFeature, transformLuaLibFunction } from "../../utils/lualib";
import { isNonNull } from "../../../utils";
import { transformMemberExpressionOwnerName, transformMethodName } from "./members/method";
import { transformPropertyName } from "../literal";
import { isPrivateNode, isStaticNode } from "./utils";

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

export function createDecoratingExpression(
export function createClassDecoratingExpression(
context: TransformationContext,
classDeclaration: ts.ClassDeclaration | ts.ClassExpression,
className: lua.Expression
): lua.Expression {
const classDecorators =
ts.getDecorators(classDeclaration)?.map(d => transformDecoratorExpression(context, d)) ?? [];

// If experimentalDecorators flag is set, decorate with legacy decorator logic
if (context.options.experimentalDecorators) {
return createLegacyDecoratingExpression(context, classDeclaration.kind, classDecorators, className);
}

// Else: TypeScript 5.0 decorator
return createDecoratingExpression(context, className, className, classDecorators, {
kind: lua.createStringLiteral("class"),
name: lua.createStringLiteral(classDeclaration.name?.getText() ?? ""),
});
}

export function createClassMethodDecoratingExpression(
context: TransformationContext,
methodDeclaration: ts.MethodDeclaration,
originalMethod: lua.Expression,
className: lua.Identifier
): lua.Expression {
const parameterDecorators = getParameterDecorators(context, methodDeclaration);
const methodDecorators =
ts.getDecorators(methodDeclaration)?.map(d => transformDecoratorExpression(context, d)) ?? [];

const methodName = transformMethodName(context, methodDeclaration);

// If experimentalDecorators flag is set, decorate with legacy decorator logic
if (context.options.experimentalDecorators) {
const methodTable = transformMemberExpressionOwnerName(methodDeclaration, className);
return createLegacyDecoratingExpression(
context,
methodDeclaration.kind,
[...methodDecorators, ...parameterDecorators],
methodTable,
methodName
);
}

// Else: TypeScript 5.0 decorator
return createDecoratingExpression(context, className, originalMethod, methodDecorators, {
kind: lua.createStringLiteral("method"),
name: methodName,
private: lua.createBooleanLiteral(isPrivateNode(methodDeclaration)),
static: lua.createBooleanLiteral(isStaticNode(methodDeclaration)),
});
}

export function createClassAccessorDecoratingExpression(
context: TransformationContext,
accessor: ts.AccessorDeclaration,
originalAccessor: lua.Expression,
className: lua.Identifier
): lua.Expression {
const accessorDecorators = ts.getDecorators(accessor)?.map(d => transformDecoratorExpression(context, d)) ?? [];
const propertyName = transformPropertyName(context, accessor.name);

// If experimentalDecorators flag is set, decorate with legacy decorator logic
if (context.options.experimentalDecorators) {
const propertyOwnerTable = transformMemberExpressionOwnerName(accessor, className);

return createLegacyDecoratingExpression(
context,
accessor.kind,
accessorDecorators,
propertyOwnerTable,
propertyName
);
}

// Else: TypeScript 5.0 decorator
return createDecoratingExpression(context, className, originalAccessor, accessorDecorators, {
kind: lua.createStringLiteral(accessor.kind === ts.SyntaxKind.SetAccessor ? "setter" : "getter"),
name: propertyName,
private: lua.createBooleanLiteral(isPrivateNode(accessor)),
static: lua.createBooleanLiteral(isStaticNode(accessor)),
});
}

export function createClassPropertyDecoratingExpression(
context: TransformationContext,
property: ts.PropertyDeclaration,
className: lua.Identifier
): lua.Expression {
const decorators = ts.getDecorators(property) ?? [];
const propertyDecorators = decorators.map(d => transformDecoratorExpression(context, d));

// If experimentalDecorators flag is set, decorate with legacy decorator logic
if (context.options.experimentalDecorators) {
const propertyName = transformPropertyName(context, property.name);
const propertyOwnerTable = transformMemberExpressionOwnerName(property, className);

return createLegacyDecoratingExpression(
context,
property.kind,
propertyDecorators,
propertyOwnerTable,
propertyName
);
}

// Else: TypeScript 5.0 decorator

// Add a diagnostic when something is returned from a field decorator
for (const decorator of decorators) {
const signature = context.checker.getResolvedSignature(decorator);
const decoratorReturnType = signature?.getReturnType();
// If return type of decorator is NOT void
if (decoratorReturnType && (decoratorReturnType.flags & ts.TypeFlags.Void) === 0) {
context.diagnostics.push(incompleteFieldDecoratorWarning(property));
}
}

return createDecoratingExpression(context, className, lua.createNilLiteral(), propertyDecorators, {
kind: lua.createStringLiteral("field"),
name: lua.createStringLiteral(property.name.getText()),
private: lua.createBooleanLiteral(isPrivateNode(property)),
static: lua.createBooleanLiteral(isStaticNode(property)),
});
}

function createDecoratingExpression<TValue extends lua.Expression>(
context: TransformationContext,
className: lua.Expression,
originalValue: TValue,
decorators: lua.Expression[],
decoratorContext: Record<string, lua.Expression>
): lua.Expression {
const decoratorTable = lua.createTableExpression(decorators.map(d => lua.createTableFieldExpression(d)));
const decoratorContextTable = objectToLuaTableLiteral(decoratorContext);

return transformLuaLibFunction(
context,
LuaLibFeature.Decorate,
undefined,
className,
originalValue,
decoratorTable,
decoratorContextTable
);
}

function objectToLuaTableLiteral(obj: Record<string, lua.Expression>): lua.Expression {
return lua.createTableExpression(
Object.entries(obj).map(([key, value]) => lua.createTableFieldExpression(value, lua.createStringLiteral(key)))
);
}

// Legacy decorators:
function createLegacyDecoratingExpression(
context: TransformationContext,
kind: ts.SyntaxKind,
decorators: lua.Expression[],
Expand All @@ -35,5 +193,39 @@ export function createDecoratingExpression(
trailingExpressions.push(isMethodOrAccessor ? lua.createBooleanLiteral(true) : lua.createNilLiteral());
}

return transformLuaLibFunction(context, LuaLibFeature.Decorate, undefined, ...trailingExpressions);
return transformLuaLibFunction(context, LuaLibFeature.DecorateLegacy, undefined, ...trailingExpressions);
}

function getParameterDecorators(
context: TransformationContext,
node: ts.FunctionLikeDeclarationBase
): lua.CallExpression[] {
return node.parameters
.flatMap((parameter, index) =>
ts
.getDecorators(parameter)
?.map(decorator =>
transformLuaLibFunction(
context,
LuaLibFeature.DecorateParam,
node,
lua.createNumericLiteral(index),
transformDecoratorExpression(context, decorator)
)
)
)
.filter(isNonNull);
}

export function createConstructorDecoratingExpression(
context: TransformationContext,
node: ts.ConstructorDeclaration,
className: lua.Identifier
): lua.Statement | undefined {
const parameterDecorators = getParameterDecorators(context, node);

if (parameterDecorators.length > 0) {
const decorateMethod = createLegacyDecoratingExpression(context, node.kind, parameterDecorators, className);
return lua.createExpressionStatement(decorateMethod);
}
}
Loading