feat(53461): Implement decorator metadata proposal (#54657)

This commit is contained in:
Oleksandr T
2023-06-23 10:55:22 -04:00
committed by GitHub
parent f7e4f809ac
commit bc91920cc6
276 changed files with 2926 additions and 973 deletions
+1
View File
@@ -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"],
];
+14 -8
View File
@@ -59,6 +59,8 @@ export interface ESDecorateClassContext {
* The name of the decorated element.
*/
name: Expression;
metadata: Expression;
}
/**
@@ -75,6 +77,7 @@ export interface ESDecorateClassElementContext {
static: boolean;
private: boolean;
access: ESDecorateClassElementAccess;
metadata: Expression;
}
/** @internal */
@@ -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 ?
@@ -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) {
@@ -387,7 +394,6 @@ export function createEmitHelperFactory(context: TransformationContext): EmitHel
value ? [thisArg, initializers, value] : [thisArg, initializers]
);
}
// ES2018 Helpers
function createAssignHelper(attributesSegments: Expression[]) {
+1 -2
View File
@@ -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;
}
+73 -37
View File
@@ -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
@@ -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;
@@ -611,6 +613,7 @@ export function transformESDecorators(context: TransformationContext): (x: Sourc
return {
class: node,
metadataReference,
instanceExtraInitializersName,
staticExtraInitializersName,
hasStaticInitializers,
@@ -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();
@@ -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();
@@ -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;
@@ -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);
@@ -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
);
@@ -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);
@@ -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) ?
@@ -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()
);
}
}
+3 -2
View File
@@ -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,
});
}
}
+2
View File
@@ -1168,6 +1168,8 @@ export namespace Completion {
typeEntry("ParameterDecorator"),
typeEntry("ClassMemberDecoratorContext"),
typeEntry("DecoratorContext"),
typeEntry("DecoratorMetadata"),
typeEntry("DecoratorMetadataObject"),
interfaceEntry("ClassDecoratorContext"),
interfaceEntry("ClassMethodDecoratorContext"),
interfaceEntry("ClassGetterDecoratorContext"),
+17
View File
@@ -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.
@@ -48,6 +53,8 @@ interface ClassDecoratorContext<
* ```
*/
addInitializer(initializer: (this: Class) => void): void;
readonly metadata: DecoratorMetadata;
}
/**
@@ -112,6 +119,8 @@ interface ClassMethodDecoratorContext<
* ```
*/
addInitializer(initializer: (this: This) => void): void;
readonly metadata: DecoratorMetadata;
}
/**
@@ -157,6 +166,8 @@ interface ClassGetterDecoratorContext<
* decorating a non-`static` element).
*/
addInitializer(initializer: (this: This) => void): void;
readonly metadata: DecoratorMetadata;
}
/**
@@ -202,6 +213,8 @@ interface ClassSetterDecoratorContext<
* decorating a non-`static` element).
*/
addInitializer(initializer: (this: This) => void): void;
readonly metadata: DecoratorMetadata;
}
/**
@@ -256,6 +269,8 @@ interface ClassAccessorDecoratorContext<
* decorating a non-`static` element).
*/
addInitializer(initializer: (this: This) => void): void;
readonly metadata: DecoratorMetadata;
}
/**
@@ -351,4 +366,6 @@ interface ClassFieldDecoratorContext<
* decorating a non-`static` element).
*/
addInitializer(initializer: (this: This) => void): void;
readonly metadata: DecoratorMetadata;
}
+1
View File
@@ -1,3 +1,4 @@
/// <reference lib="es2023" />
/// <reference lib="esnext.intl" />
/// <reference lib="esnext.decorators" />
/// <reference lib="esnext.disposable" />
+10
View File
@@ -0,0 +1,10 @@
/// <reference lib="es2015.symbol" />
/// <reference lib="decorators" />
interface SymbolConstructor {
readonly metadata: unique symbol;
}
interface Function {
[Symbol.metadata]: DecoratorMetadata | null;
}
+1
View File
@@ -67,6 +67,7 @@
"es2022.regexp",
"es2023.array",
"es2023.collection",
"esnext.decorators",
"esnext.intl",
"esnext.disposable",
"decorators",
+1
View File
@@ -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";
@@ -0,0 +1,141 @@
import * as evaluator from "../../_namespaces/evaluator";
import * as ts from "../../_namespaces/ts";
import { ScriptTarget } from "../../_namespaces/ts";
describe("unittests:: evaluation:: esDecoratorsMetadata", () => {
const nodeVersion = new ts.Version(process.versions.node);
const supportsClassStaticBlock = nodeVersion.major >= 16;
const targets = [
// NOTE: Class static blocks weren't supported in Node v14
...(supportsClassStaticBlock ? [ScriptTarget.ES2022] : []),
ScriptTarget.ES2021,
ScriptTarget.ES2015,
];
for (const target of targets) {
const targetName = ts.Debug.formatEnum(target, (ts as any).ScriptTarget);
const options: ts.CompilerOptions = { target };
const exec = (array: TemplateStringsArray) => evaluator.evaluateTypeScript(array[0], options);
describe("examples", () => {
// see https://github.com/tc39/proposal-decorator-metadata
it(`@meta (${targetName})`, () => {
const { output } = exec`
export const output: unknown[] = [];
function meta(key: string, value: string) {
return (_, context) => {
context.metadata[key] = value;
};
}
@meta('a', 'x')
class C {
@meta('b', 'y')
m() { }
}
output.push(C[Symbol.metadata].a);
output.push(C[Symbol.metadata].b);
`;
assert.deepEqual(output, ["x", "y"]);
});
// see https://github.com/tc39/proposal-decorator-metadata#inheritance
it(`inheritance (${targetName})`, () => {
const { output } = exec`
export const output: unknown[] = [];
function meta(key: string, value: string) {
return (_, context) => {
context.metadata[key] = value;
};
}
@meta('a', 'x')
class C {
@meta('b', 'y')
m() {}
}
output.push(C[Symbol.metadata].a);
output.push(C[Symbol.metadata].b);
class D extends C {
@meta('b', 'z')
m() {}
}
output.push(D[Symbol.metadata].a);
output.push(D[Symbol.metadata].b);
`;
assert.deepEqual(output, [
"x",
"y",
"x",
"z",
]);
});
// see https://github.com/tc39/proposal-decorator-metadata#inheritance
it(`inheritance append(${targetName})`, () => {
const { output } = exec`
export const output: unknown[] = [];
function appendMeta(key: string, value: string) {
return (_, context) => {
const existing = context.metadata[key] ?? [];
context.metadata[key] = [...existing, value];
};
}
@appendMeta('a', 'x')
class C { }
@appendMeta('a', 'z')
class D extends C { }
output.push(C[Symbol.metadata].a);
output.push(D[Symbol.metadata].a);
`;
assert.deepEqual(output, [
["x"],
["x", "z"],
]);
});
// see https://github.com/tc39/proposal-decorator-metadata#private-metadata
it(`private metadata (${targetName})`, () => {
const { output } = exec`
export const output: unknown[] = [];
const PRIVATE_METADATA = new WeakMap();
function meta(key: string, value: string) {
return (_, context) => {
let metadata = PRIVATE_METADATA.get(context.metadata);
if (!metadata) {
metadata = {};
PRIVATE_METADATA.set(context.metadata, metadata);
}
metadata[key] = value;
};
}
@meta('a', 'x')
class C {
@meta('b', 'y')
m() { }
}
output.push(PRIVATE_METADATA.get(C[Symbol.metadata]).a);
output.push(PRIVATE_METADATA.get(C[Symbol.metadata]).b);
`;
assert.deepEqual(output, [
"x",
"y",
]);
});
});
}
});