Skip to content

Commit

Permalink
feat(53461): Implement decorator metadata proposal (#54657)
Browse files Browse the repository at this point in the history
  • Loading branch information
a-tarasyuk committed Jun 23, 2023
1 parent f7e4f80 commit bc91920
Show file tree
Hide file tree
Showing 276 changed files with 2,926 additions and 973 deletions.
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

0 comments on commit bc91920

Please sign in to comment.