From bb241ee28bb73915010ce0620e265daeb288cde1 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 17 Sep 2025 14:06:25 +0200 Subject: [PATCH] refactor(compiler-cli): track member metadata using output AST (#63957) (#64317) Reworks the logic that tracks the decorator metadata for members to do so using the output AST, rather than wrapping the TypeScript AST. This makes it easier to programmatically generate new members that weren't part of the TypeScript AST before. PR Close #63957 PR Close #64317 --- .../ngtsc/annotations/common/src/metadata.ts | 50 ++++++++++++------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/annotations/common/src/metadata.ts b/packages/compiler-cli/src/ngtsc/annotations/common/src/metadata.ts index e6d782d461c..6299337a745 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/common/src/metadata.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/common/src/metadata.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {ErrorCode, FatalDiagnosticError, makeRelatedInformation} from '../../../diagnostics'; +import {ErrorCode, FatalDiagnosticError} from '../../../diagnostics'; import { ArrowFunctionExpr, Expression, @@ -19,6 +19,7 @@ import { import ts from 'typescript'; import { + ClassMember, ClassMemberAccessLevel, CtorParameter, DeclarationNode, @@ -88,15 +89,32 @@ export function extractClassMetadata( const classMembers = reflection.getMembersOfClass(clazz).filter( (member) => !member.isStatic && - member.decorators !== null && - member.decorators.length > 0 && // Private fields are not supported in the metadata emit member.accessLevel !== ClassMemberAccessLevel.EcmaScriptPrivate, ); - const duplicateDecoratedMembers = classMembers.filter( - (member, i, arr) => arr.findIndex((arrayMember) => arrayMember.name === member.name) < i, - ); - if (duplicateDecoratedMembers.length > 0) { + + const decoratedMembers: {key: string; value: Expression; quoted: boolean}[] = []; + const seenMemberNames = new Set(); + let duplicateDecoratedMembers: ClassMember[] | null = null; + + for (const member of classMembers) { + if (member.decorators !== null && member.decorators.length > 0) { + decoratedMembers.push({ + key: member.name, + quoted: false, + value: decoratedClassMemberToMetadata(member.decorators!, isCore), + }); + + if (seenMemberNames.has(member.name)) { + duplicateDecoratedMembers ??= []; + duplicateDecoratedMembers.push(member); + } else { + seenMemberNames.add(member.name); + } + } + } + + if (duplicateDecoratedMembers !== null) { // This should theoretically never happen, because the only way to have duplicate instance // member names is getter/setter pairs and decorators cannot appear in both a getter and the // corresponding setter. @@ -107,13 +125,9 @@ export function extractClassMetadata( duplicateDecoratedMembers.map((member) => member.name).join(', '), ); } - const decoratedMembers = classMembers.map((member) => - classMemberToMetadata(member.nameNode ?? member.name, member.decorators!, isCore), - ); + if (decoratedMembers.length > 0) { - metaPropDecorators = new WrappedNodeExpr( - ts.factory.createObjectLiteralExpression(decoratedMembers), - ); + metaPropDecorators = literalMap(decoratedMembers); } return { @@ -153,16 +167,14 @@ function ctorParameterToMetadata(param: CtorParameter, isCore: boolean): Express /** * Convert a reflected class member to metadata. */ -function classMemberToMetadata( - name: ts.PropertyName | string, +function decoratedClassMemberToMetadata( decorators: Decorator[], isCore: boolean, -): ts.PropertyAssignment { +): LiteralArrayExpr { const ngDecorators = decorators .filter((dec) => isAngularDecorator(dec, isCore)) - .map((decorator: Decorator) => decoratorToMetadata(decorator)); - const decoratorMeta = ts.factory.createArrayLiteralExpression(ngDecorators); - return ts.factory.createPropertyAssignment(name, decoratorMeta); + .map((decorator: Decorator) => new WrappedNodeExpr(decoratorToMetadata(decorator))); + return new LiteralArrayExpr(ngDecorators); } /**