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
This commit is contained in:
Alex Rickabaugh 2018-06-20 15:57:40 -07:00 committed by Miško Hevery
parent 104d30507a
commit fc4dc35426
2 changed files with 126 additions and 7 deletions

View File

@ -132,7 +132,7 @@ export class NgtscProgram implements api.Program {
options: this.options, options: this.options,
emitOnlyDtsFiles: false, writeFile, emitOnlyDtsFiles: false, writeFile,
customTransformers: { customTransformers: {
before: [ivyTransformFactory(compilation, coreImportsFrom)], before: [ivyTransformFactory(compilation, reflector, coreImportsFrom)],
}, },
}); });
return emitResult; return emitResult;

View File

@ -9,24 +9,30 @@
import {WrappedNodeExpr} from '@angular/compiler'; import {WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript'; 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 {VisitListEntryResult, Visitor, visit} from '../../util/src/visitor';
import {CompileResult} from './api'; import {CompileResult} from './api';
import {IvyCompilation} from './compilation'; import {IvyCompilation} from './compilation';
import {ImportManager, translateExpression, translateStatement} from './translator'; import {ImportManager, translateExpression, translateStatement} from './translator';
const NO_DECORATORS = new Set<ts.Decorator>();
export function ivyTransformFactory( export function ivyTransformFactory(
compilation: IvyCompilation, compilation: IvyCompilation, reflector: ReflectionHost,
coreImportsFrom: ts.SourceFile | null): ts.TransformerFactory<ts.SourceFile> { coreImportsFrom: ts.SourceFile | null): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => { return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
return (file: ts.SourceFile): ts.SourceFile => { return (file: ts.SourceFile): ts.SourceFile => {
return transformIvySourceFile(compilation, context, coreImportsFrom, file); return transformIvySourceFile(compilation, context, reflector, coreImportsFrom, file);
}; };
}; };
} }
class IvyVisitor extends Visitor { 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(); super();
} }
@ -62,24 +68,133 @@ class IvyVisitor extends Visitor {
// Remove the decorator which triggered this compilation, leaving the others alone. // Remove the decorator which triggered this compilation, leaving the others alone.
maybeFilterDecorator( maybeFilterDecorator(
node.decorators, this.compilation.ivyDecoratorFor(node) !.node as ts.Decorator), 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, before: statements};
} }
return {node}; 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<ts.Decorator> {
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<ts.Decorator>(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<ts.Decorator>|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<T extends ts.Node>(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`. * A transformer which operates on ts.SourceFiles and applies changes from an `IvyCompilation`.
*/ */
function transformIvySourceFile( function transformIvySourceFile(
compilation: IvyCompilation, context: ts.TransformationContext, compilation: IvyCompilation, context: ts.TransformationContext, reflector: ReflectionHost,
coreImportsFrom: ts.SourceFile | null, file: ts.SourceFile): ts.SourceFile { coreImportsFrom: ts.SourceFile | null, file: ts.SourceFile): ts.SourceFile {
const importManager = new ImportManager(coreImportsFrom !== null); const importManager = new ImportManager(coreImportsFrom !== null);
// Recursively scan through the AST and perform any updates requested by the IvyCompilation. // 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. // Generate the import statements to prepend.
const imports = importManager.getAllImports(file.fileName, coreImportsFrom).map(i => { const imports = importManager.getAllImports(file.fileName, coreImportsFrom).map(i => {
@ -108,3 +223,7 @@ function maybeFilterDecorator(
} }
return ts.createNodeArray(filtered); return ts.createNodeArray(filtered);
} }
function isFromAngularCore(decorator: Decorator): boolean {
return decorator.import !== null && decorator.import.from === '@angular/core';
}