From fc4dc35426e3d55d4ff26a50ec87665ef283d390 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Wed, 20 Jun 2018 15:57:40 -0700 Subject: [PATCH] feat(ivy): strip all Angular decorators in compiled classes (#24677) Previously ngtsc removed the class-level decorators (@Component, etc) but left all the ancillary decorators (@Input, @Optional, etc). This changes the transform to descend into the members of decorated classes and remove any Angular decorators, not just the class-level ones. PR Close #24677 --- packages/compiler-cli/src/ngtsc/program.ts | 2 +- .../src/ngtsc/transform/src/transform.ts | 131 +++++++++++++++++- 2 files changed, 126 insertions(+), 7 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 1b9793ee90..bd3dcb537c 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -132,7 +132,7 @@ export class NgtscProgram implements api.Program { options: this.options, emitOnlyDtsFiles: false, writeFile, customTransformers: { - before: [ivyTransformFactory(compilation, coreImportsFrom)], + before: [ivyTransformFactory(compilation, reflector, coreImportsFrom)], }, }); return emitResult; diff --git a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts index f5a2ee9604..8af657aa49 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts @@ -9,24 +9,30 @@ import {WrappedNodeExpr} from '@angular/compiler'; import * as ts from 'typescript'; +import {Decorator, ReflectionHost} from '../../host'; +import {relativePathBetween} from '../../util/src/path'; import {VisitListEntryResult, Visitor, visit} from '../../util/src/visitor'; import {CompileResult} from './api'; import {IvyCompilation} from './compilation'; import {ImportManager, translateExpression, translateStatement} from './translator'; +const NO_DECORATORS = new Set(); + export function ivyTransformFactory( - compilation: IvyCompilation, + compilation: IvyCompilation, reflector: ReflectionHost, coreImportsFrom: ts.SourceFile | null): ts.TransformerFactory { return (context: ts.TransformationContext): ts.Transformer => { return (file: ts.SourceFile): ts.SourceFile => { - return transformIvySourceFile(compilation, context, coreImportsFrom, file); + return transformIvySourceFile(compilation, context, reflector, coreImportsFrom, file); }; }; } class IvyVisitor extends Visitor { - constructor(private compilation: IvyCompilation, private importManager: ImportManager) { + constructor( + private compilation: IvyCompilation, private reflector: ReflectionHost, + private importManager: ImportManager, private isCore: boolean) { super(); } @@ -62,24 +68,133 @@ class IvyVisitor extends Visitor { // Remove the decorator which triggered this compilation, leaving the others alone. maybeFilterDecorator( node.decorators, this.compilation.ivyDecoratorFor(node) !.node as ts.Decorator), - node.modifiers, node.name, node.typeParameters, node.heritageClauses || [], members); + node.modifiers, node.name, node.typeParameters, node.heritageClauses || [], + // Map over the class members and remove any Angular decorators from them. + members.map(member => this._stripAngularDecorators(member))); return {node, before: statements}; } return {node}; } + + /** + * Return all decorators on a `Declaration` which are from @angular/core, or an empty set if none + * are. + */ + private _angularCoreDecorators(decl: ts.Declaration): Set { + const decorators = this.reflector.getDecoratorsOfDeclaration(decl); + if (decorators === null) { + return NO_DECORATORS; + } + const coreDecorators = decorators.filter(dec => this.isCore || isFromAngularCore(dec)) + .map(dec => dec.node as ts.Decorator); + if (coreDecorators.length > 0) { + return new Set(coreDecorators); + } else { + return NO_DECORATORS; + } + } + + /** + * Given a `ts.Node`, filter the decorators array and return a version containing only non-Angular + * decorators. + * + * If all decorators are removed (or none existed in the first place), this method returns + * `undefined`. + */ + private _nonCoreDecoratorsOnly(node: ts.Declaration): ts.NodeArray|undefined { + // Shortcut if the node has no decorators. + if (node.decorators === undefined) { + return undefined; + } + // Build a Set of the decorators on this node from @angular/core. + const coreDecorators = this._angularCoreDecorators(node); + + if (coreDecorators.size === node.decorators.length) { + // If all decorators are to be removed, return `undefined`. + return undefined; + } else if (coreDecorators.size === 0) { + // If no decorators need to be removed, return the original decorators array. + return node.decorators; + } + + // Filter out the core decorators. + const filtered = node.decorators.filter(dec => !coreDecorators.has(dec)); + + // If no decorators survive, return `undefined`. This can only happen if a core decorator is + // repeated on the node. + if (filtered.length === 0) { + return undefined; + } + + // Create a new `NodeArray` with the filtered decorators that sourcemaps back to the original. + const array = ts.createNodeArray(filtered); + array.pos = node.decorators.pos; + array.end = node.decorators.end; + return array; + } + + /** + * Remove Angular decorators from a `ts.Node` in a shallow manner. + * + * This will remove decorators from class elements (getters, setters, properties, methods) as well + * as parameters of constructors. + */ + private _stripAngularDecorators(node: T): T { + if (ts.isParameter(node)) { + // Strip decorators from parameters (probably of the constructor). + node = ts.updateParameter( + node, this._nonCoreDecoratorsOnly(node), node.modifiers, node.dotDotDotToken, + node.name, node.questionToken, node.type, node.initializer) as T & + ts.ParameterDeclaration; + } else if (ts.isMethodDeclaration(node) && node.decorators !== undefined) { + // Strip decorators of methods. + node = ts.updateMethod( + node, this._nonCoreDecoratorsOnly(node), node.modifiers, node.asteriskToken, + node.name, node.questionToken, node.typeParameters, node.parameters, node.type, + node.body) as T & + ts.MethodDeclaration; + } else if (ts.isPropertyDeclaration(node) && node.decorators !== undefined) { + // Strip decorators of properties. + node = ts.updateProperty( + node, this._nonCoreDecoratorsOnly(node), node.modifiers, node.name, + node.questionToken, node.type, node.initializer) as T & + ts.PropertyDeclaration; + } else if (ts.isGetAccessor(node)) { + // Strip decorators of getters. + node = ts.updateGetAccessor( + node, this._nonCoreDecoratorsOnly(node), node.modifiers, node.name, + node.parameters, node.type, node.body) as T & + ts.GetAccessorDeclaration; + } else if (ts.isSetAccessor(node)) { + // Strip decorators of setters. + node = ts.updateSetAccessor( + node, this._nonCoreDecoratorsOnly(node), node.modifiers, node.name, + node.parameters, node.body) as T & + ts.SetAccessorDeclaration; + } else if (ts.isConstructorDeclaration(node)) { + // For constructors, strip decorators of the parameters. + const parameters = node.parameters.map(param => this._stripAngularDecorators(param)); + node = + ts.updateConstructor(node, node.decorators, node.modifiers, parameters, node.body) as T & + ts.ConstructorDeclaration; + } + return node; + } } /** * A transformer which operates on ts.SourceFiles and applies changes from an `IvyCompilation`. */ function transformIvySourceFile( - compilation: IvyCompilation, context: ts.TransformationContext, + compilation: IvyCompilation, context: ts.TransformationContext, reflector: ReflectionHost, coreImportsFrom: ts.SourceFile | null, file: ts.SourceFile): ts.SourceFile { const importManager = new ImportManager(coreImportsFrom !== null); // Recursively scan through the AST and perform any updates requested by the IvyCompilation. - const sf = visit(file, new IvyVisitor(compilation, importManager), context); + const sf = visit( + file, new IvyVisitor(compilation, reflector, importManager, coreImportsFrom !== null), + context); // Generate the import statements to prepend. const imports = importManager.getAllImports(file.fileName, coreImportsFrom).map(i => { @@ -108,3 +223,7 @@ function maybeFilterDecorator( } return ts.createNodeArray(filtered); } + +function isFromAngularCore(decorator: Decorator): boolean { + return decorator.import !== null && decorator.import.from === '@angular/core'; +}