Skip to content

feat(53461): Implement decorator metadata proposal #54657

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jun 23, 2023
Merged
1 change: 1 addition & 0 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ const libEntries: [string, string][] = [
["esnext.string", "lib.es2022.string.d.ts"],
["esnext.promise", "lib.es2021.promise.d.ts"],
["esnext.weakref", "lib.es2021.weakref.d.ts"],
["esnext.decorators", "lib.esnext.decorators.d.ts"],
["decorators", "lib.decorators.d.ts"],
["decorators.legacy", "lib.decorators.legacy.d.ts"],
];
Expand Down
22 changes: 14 additions & 8 deletions src/compiler/factory/emitHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export interface ESDecorateClassContext {
* The name of the decorated element.
*/
name: Expression;

metadata: Expression;
}

/**
Expand All @@ -75,6 +77,7 @@ export interface ESDecorateClassElementContext {
static: boolean;
private: boolean;
access: ESDecorateClassElementAccess;
metadata: Expression;
}

/** @internal */
Expand Down Expand Up @@ -251,12 +254,14 @@ export function createEmitHelperFactory(context: TransformationContext): EmitHel
// ES Decorators Helpers

function createESDecorateClassContextObject(contextIn: ESDecorateClassContext) {
return factory.createObjectLiteralExpression([
const properties = [
factory.createPropertyAssignment(factory.createIdentifier("kind"), factory.createStringLiteral("class")),
factory.createPropertyAssignment(factory.createIdentifier("name"), contextIn.name)
]);
}
factory.createPropertyAssignment(factory.createIdentifier("name"), contextIn.name),
factory.createPropertyAssignment(factory.createIdentifier("metadata"), contextIn.metadata),
];

return factory.createObjectLiteralExpression(properties);
}

function createESDecorateClassElementAccessGetMethod(elementName: ESDecorateName) {
const accessor = elementName.computed ?
Expand Down Expand Up @@ -350,13 +355,15 @@ export function createEmitHelperFactory(context: TransformationContext): EmitHel
}

function createESDecorateClassElementContextObject(contextIn: ESDecorateClassElementContext) {
return factory.createObjectLiteralExpression([
const properties = [
factory.createPropertyAssignment(factory.createIdentifier("kind"), factory.createStringLiteral(contextIn.kind)),
factory.createPropertyAssignment(factory.createIdentifier("name"), contextIn.name.computed ? contextIn.name.name : factory.createStringLiteralFromNode(contextIn.name.name)),
factory.createPropertyAssignment(factory.createIdentifier("static"), contextIn.static ? factory.createTrue() : factory.createFalse()),
factory.createPropertyAssignment(factory.createIdentifier("private"), contextIn.private ? factory.createTrue() : factory.createFalse()),
factory.createPropertyAssignment(factory.createIdentifier("access"), createESDecorateClassElementAccessObject(contextIn.name, contextIn.access))
]);
factory.createPropertyAssignment(factory.createIdentifier("access"), createESDecorateClassElementAccessObject(contextIn.name, contextIn.access)),
factory.createPropertyAssignment(factory.createIdentifier("metadata"), contextIn.metadata),
];
return factory.createObjectLiteralExpression(properties);
}

function createESDecorateContextObject(contextIn: ESDecorateContext) {
Expand Down Expand Up @@ -387,7 +394,6 @@ export function createEmitHelperFactory(context: TransformationContext): EmitHel
value ? [thisArg, initializers, value] : [thisArg, initializers]
);
}

// ES2018 Helpers

function createAssignHelper(attributesSegments: Expression[]) {
Expand Down
3 changes: 1 addition & 2 deletions src/compiler/transformers/classFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1936,9 +1936,8 @@ export function transformClassFields(context: TransformationContext): (x: Source
}

const classCheckFlags = resolver.getNodeCheckFlags(node);
const isClassWithConstructorReference = classCheckFlags & NodeCheckFlags.ClassWithConstructorReference;
const requiresBlockScopedVar = classCheckFlags & NodeCheckFlags.BlockScopedBindingInLoop;
const temp = factory.createTempVariable(requiresBlockScopedVar ? addBlockScopedVariable : hoistVariableDeclaration, !!isClassWithConstructorReference);
const temp = factory.createTempVariable(requiresBlockScopedVar ? addBlockScopedVariable : hoistVariableDeclaration, /*reservedInNestedScopes*/ true);
getClassLexicalEnvironment().classConstructor = factory.cloneNode(temp);
return temp;
}
Expand Down
110 changes: 73 additions & 37 deletions src/compiler/transformers/esDecorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ interface ClassInfo {
classExtraInitializersName?: Identifier; // used in step 13
classThis?: Identifier; // `_classThis`, if needed.
classSuper?: Identifier; // `_classSuper`, if needed.
metadataReference: Identifier;

memberInfos?: Map<ClassElement, MemberInfo>; // used in step 4.a, 12, and construction

Expand Down Expand Up @@ -563,6 +564,7 @@ export function transformESDecorators(context: TransformationContext): (x: Sourc
}

function createClassInfo(node: ClassLikeDeclaration): ClassInfo {
const metadataReference = factory.createUniqueName("_metadata", GeneratedIdentifierFlags.Optimistic | GeneratedIdentifierFlags.FileLevel);
let instanceExtraInitializersName: Identifier | undefined;
let staticExtraInitializersName: Identifier | undefined;
let hasStaticInitializers = false;
Expand Down Expand Up @@ -611,6 +613,7 @@ export function transformESDecorators(context: TransformationContext): (x: Sourc

return {
class: node,
metadataReference,
instanceExtraInitializersName,
staticExtraInitializersName,
hasStaticInitializers,
Expand All @@ -619,18 +622,6 @@ export function transformESDecorators(context: TransformationContext): (x: Sourc
};
}

function containsLexicalSuperInStaticInitializer(node: ClassLikeDeclaration) {
for (const member of node.members) {
if (isClassStaticBlockDeclaration(member) ||
isPropertyDeclaration(member) && hasStaticModifier(member)) {
if (member.transformFlags & TransformFlags.ContainsLexicalSuper) {
return true;
}
}
}
return false;
}

function transformClassLike(node: ClassLikeDeclaration) {
startLexicalEnvironment();

Expand Down Expand Up @@ -681,31 +672,25 @@ export function transformESDecorators(context: TransformationContext): (x: Sourc
}

// Rewrite `super` in static initializers so that we can use the correct `this`.
if (classDecorators && containsLexicalSuperInStaticInitializer(node)) {
const extendsClause = getHeritageClause(node.heritageClauses, SyntaxKind.ExtendsKeyword);
const extendsElement = extendsClause && firstOrUndefined(extendsClause.types);
const extendsExpression = extendsElement && visitNode(extendsElement.expression, visitor, isExpression);
if (extendsExpression) {
classInfo.classSuper = factory.createUniqueName("_classSuper", GeneratedIdentifierFlags.Optimistic | GeneratedIdentifierFlags.FileLevel);
const extendsClause = getHeritageClause(node.heritageClauses, SyntaxKind.ExtendsKeyword);
const extendsElement = extendsClause && firstOrUndefined(extendsClause.types);
const extendsExpression = extendsElement && visitNode(extendsElement.expression, visitor, isExpression);
if (extendsExpression) {
classInfo.classSuper = factory.createUniqueName("_classSuper", GeneratedIdentifierFlags.Optimistic | GeneratedIdentifierFlags.FileLevel);

// Ensure we do not give the class or function an assigned name due to the variable by prefixing it
// with `0, `.
const unwrapped = skipOuterExpressions(extendsExpression);
const safeExtendsExpression =
isClassExpression(unwrapped) && !unwrapped.name ||
isFunctionExpression(unwrapped) && !unwrapped.name ||
isArrowFunction(unwrapped) ?
factory.createComma(factory.createNumericLiteral(0), extendsExpression) :
extendsExpression;
classDefinitionStatements.push(createLet(classInfo.classSuper, safeExtendsExpression));
const updatedExtendsElement = factory.updateExpressionWithTypeArguments(extendsElement, classInfo.classSuper, /*typeArguments*/ undefined);
const updatedExtendsClause = factory.updateHeritageClause(extendsClause, [updatedExtendsElement]);
heritageClauses = factory.createNodeArray([updatedExtendsClause]);
}
}
else {
// 2. ClassHeritage clause is evaluated outside of the private name scope of the class.
heritageClauses = visitNodes(node.heritageClauses, visitor, isHeritageClause);
const unwrapped = skipOuterExpressions(extendsExpression);
const safeExtendsExpression =
isClassExpression(unwrapped) && !unwrapped.name ||
isFunctionExpression(unwrapped) && !unwrapped.name ||
isArrowFunction(unwrapped) ?
factory.createComma(factory.createNumericLiteral(0), extendsExpression) :
extendsExpression;
classDefinitionStatements.push(createLet(classInfo.classSuper, safeExtendsExpression));
const updatedExtendsElement = factory.updateExpressionWithTypeArguments(extendsElement, classInfo.classSuper, /*typeArguments*/ undefined);
const updatedExtendsClause = factory.updateHeritageClause(extendsClause, [updatedExtendsElement]);
heritageClauses = factory.createNodeArray([updatedExtendsClause]);
}

const renamedClassThis = classInfo.classThis ?? factory.createThis();
Expand All @@ -724,8 +709,10 @@ export function transformESDecorators(context: TransformationContext): (x: Sourc
// - The second pass visits the constructor to add instance initializers.
//
// NOTE: If there are no constructors, but there are instance initializers, a synthetic constructor is added.

enterClass(classInfo);

leadingBlockStatements = append(leadingBlockStatements, createMetadata(classInfo.metadataReference, classInfo.classSuper));

let members = visitNodes(node.members, classElementVisitor, isClassElement);
if (pendingExpressions) {
let outerThis: Identifier | undefined;
Expand Down Expand Up @@ -840,7 +827,7 @@ export function transformESDecorators(context: TransformationContext): (x: Sourc
leadingBlockStatements ??= [];

// produces:
// __esDecorate(null, _classDescriptor = { value: this }, _classDecorators, { kind: "class", name: this.name }, _classExtraInitializers);
// __esDecorate(null, _classDescriptor = { value: this }, _classDecorators, { kind: "class", name: this.name, metadata }, _classExtraInitializers);
const valueProperty = factory.createPropertyAssignment("value", renamedClassThis);
const classDescriptor = factory.createObjectLiteralExpression([valueProperty]);
const classDescriptorAssignment = factory.createAssignment(classInfo.classDescriptorName, classDescriptor);
Expand All @@ -849,7 +836,7 @@ export function transformESDecorators(context: TransformationContext): (x: Sourc
factory.createNull(),
classDescriptorAssignment,
classInfo.classDecoratorsName,
{ kind: "class", name: classNameReference },
{ kind: "class", name: classNameReference, metadata: classInfo.metadataReference },
factory.createNull(),
classInfo.classExtraInitializersName
);
Expand All @@ -865,6 +852,9 @@ export function transformESDecorators(context: TransformationContext): (x: Sourc
leadingBlockStatements.push(factory.createExpressionStatement(classReferenceAssignment));
}

// if (metadata) Object.defineProperty(C, Symbol.metadata, { configurable: true, writable: true, value: metadata });
leadingBlockStatements.push(createSymbolMetadata(renamedClassThis, classInfo.metadataReference));

// 11. Static extra initializers are evaluated
if (classInfo.staticExtraInitializersName) {
const runStaticInitializersHelper = emitHelpers().createRunInitializersHelper(renamedClassThis, classInfo.staticExtraInitializersName);
Expand Down Expand Up @@ -1283,6 +1273,7 @@ export function transformESDecorators(context: TransformationContext): (x: Sourc
// 3. If _kind_ is ~field~, ~accessor~, or ~setter~, then ...
set: isPropertyDeclaration(member) || isSetAccessorDeclaration(member)
},
metadata: classInfo.metadataReference,
};

const extraInitializers = isStatic(member) ?
Expand Down Expand Up @@ -2371,4 +2362,49 @@ export function transformESDecorators(context: TransformationContext): (x: Sourc
])
);
}
function createMetadata(name: Identifier, classSuper: Identifier | undefined) {
const varDecl = factory.createVariableDeclaration(
name,
/*exclamationToken*/ undefined,
/*type*/ undefined,
factory.createConditionalExpression(
factory.createLogicalAnd(
factory.createTypeCheck(factory.createIdentifier("Symbol"), "function"),
factory.createPropertyAccessExpression(factory.createIdentifier("Symbol"), "metadata"),
),
factory.createToken(SyntaxKind.QuestionToken),
factory.createCallExpression(
factory.createPropertyAccessExpression(factory.createIdentifier("Object"), "create"),
/*typeArguments*/ undefined,
[classSuper ? createSymbolMetadataReference(classSuper) : factory.createNull()]
),
factory.createToken(SyntaxKind.ColonToken),
factory.createVoidZero(),
),
);
return factory.createVariableStatement(/*modifiers*/ undefined, factory.createVariableDeclarationList([varDecl], NodeFlags.Const));
}

function createSymbolMetadata(target: Identifier | ThisExpression, value: Identifier) {
const defineProperty = factory.createObjectDefinePropertyCall(
target,
factory.createPropertyAccessExpression(factory.createIdentifier("Symbol"), "metadata"),
factory.createPropertyDescriptor({ configurable: true, writable: true, enumerable: true, value }, /*singleLine*/ true)
);
return setEmitFlags(
factory.createIfStatement(value, factory.createExpressionStatement(defineProperty)),
EmitFlags.SingleLine,
);
}

function createSymbolMetadataReference(classSuper: Identifier) {
return factory.createBinaryExpression(
factory.createElementAccessExpression(
classSuper,
factory.createPropertyAccessExpression(factory.createIdentifier("Symbol"), "metadata"),
),
SyntaxKind.QuestionQuestionToken,
factory.createNull()
);
}
}
5 changes: 3 additions & 2 deletions src/harness/evaluatorImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,15 @@ for (const key of Object.getOwnPropertyNames(Symbol)) {
const symbolNames = [
"asyncIterator",
"dispose",
"asyncDispose"
"asyncDispose",
"metadata",
];

for (const symbolName of symbolNames) {
if (!ts.hasProperty(FakeSymbol, symbolName)) {
Object.defineProperty(FakeSymbol, symbolName, {
value: Symbol.for(`Symbol.${symbolName}`),
configurable: true
configurable: true,
});
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1168,6 +1168,8 @@ export namespace Completion {
typeEntry("ParameterDecorator"),
typeEntry("ClassMemberDecoratorContext"),
typeEntry("DecoratorContext"),
typeEntry("DecoratorMetadata"),
typeEntry("DecoratorMetadataObject"),
interfaceEntry("ClassDecoratorContext"),
interfaceEntry("ClassMethodDecoratorContext"),
interfaceEntry("ClassGetterDecoratorContext"),
Expand Down
17 changes: 17 additions & 0 deletions src/lib/decorators.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ type DecoratorContext =
| ClassMemberDecoratorContext
;

type DecoratorMetadataObject = Record<PropertyKey, unknown> & object;

type DecoratorMetadata =
typeof globalThis extends { Symbol: { readonly metadata: symbol } } ? DecoratorMetadataObject : DecoratorMetadataObject | undefined;

/**
* Context provided to a class decorator.
* @template Class The type of the decorated class associated with this context.
Expand Down Expand Up @@ -48,6 +53,8 @@ interface ClassDecoratorContext<
* ```
*/
addInitializer(initializer: (this: Class) => void): void;

readonly metadata: DecoratorMetadata;
}

/**
Expand Down Expand Up @@ -112,6 +119,8 @@ interface ClassMethodDecoratorContext<
* ```
*/
addInitializer(initializer: (this: This) => void): void;

readonly metadata: DecoratorMetadata;
}

/**
Expand Down Expand Up @@ -157,6 +166,8 @@ interface ClassGetterDecoratorContext<
* decorating a non-`static` element).
*/
addInitializer(initializer: (this: This) => void): void;

readonly metadata: DecoratorMetadata;
}

/**
Expand Down Expand Up @@ -202,6 +213,8 @@ interface ClassSetterDecoratorContext<
* decorating a non-`static` element).
*/
addInitializer(initializer: (this: This) => void): void;

readonly metadata: DecoratorMetadata;
}

/**
Expand Down Expand Up @@ -256,6 +269,8 @@ interface ClassAccessorDecoratorContext<
* decorating a non-`static` element).
*/
addInitializer(initializer: (this: This) => void): void;

readonly metadata: DecoratorMetadata;
}

/**
Expand Down Expand Up @@ -351,4 +366,6 @@ interface ClassFieldDecoratorContext<
* decorating a non-`static` element).
*/
addInitializer(initializer: (this: This) => void): void;

readonly metadata: DecoratorMetadata;
}
1 change: 1 addition & 0 deletions src/lib/esnext.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/// <reference lib="es2023" />
/// <reference lib="esnext.intl" />
/// <reference lib="esnext.decorators" />
/// <reference lib="esnext.disposable" />
10 changes: 10 additions & 0 deletions src/lib/esnext.decorators.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/// <reference lib="es2015.symbol" />
/// <reference lib="decorators" />

interface SymbolConstructor {
readonly metadata: unique symbol;
}

interface Function {
[Symbol.metadata]: DecoratorMetadata | null;
}
1 change: 1 addition & 0 deletions src/lib/libs.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"es2022.regexp",
"es2023.array",
"es2023.collection",
"esnext.decorators",
"esnext.intl",
"esnext.disposable",
"decorators",
Expand Down
1 change: 1 addition & 0 deletions src/testRunner/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import "./unittests/evaluation/awaiter";
import "./unittests/evaluation/destructuring";
import "./unittests/evaluation/externalModules";
import "./unittests/evaluation/esDecorators";
import "./unittests/evaluation/esDecoratorsMetadata";
import "./unittests/evaluation/forAwaitOf";
import "./unittests/evaluation/forOf";
import "./unittests/evaluation/generator";
Expand Down
Loading