refactor(ivy): first pass at extracting ReflectionHost for abstract reflection (#24541)

ngtsc needs to reflect over code to property compile it. It performs operations
such as enumerating decorators on a type, reading metadata from constructor
parameters, etc.

Depending on the format (ES5, ES6, etc) of the underlying code, the AST
structures over which this reflection takes place can be very different. For
example, in TS/ES6 code `class` declarations are `ts.ClassDeclaration` nodes,
but in ES5 code they've been downleveled to `ts.VariableDeclaration` nodes that
are initialized to IIFEs that build up the classes being defined.

The ReflectionHost abstraction allows ngtsc to perform these operations without
directly querying the AST. Different implementations of ReflectionHost allow
support for different code formats.

PR Close #24541
This commit is contained in:
Alex Rickabaugh
2018-06-13 10:33:04 -07:00
committed by Miško Hevery
parent 84272e2227
commit 10da6a45c6
23 changed files with 724 additions and 465 deletions

View File

@ -11,6 +11,7 @@ ts_library(
module_name = "@angular/compiler-cli/src/ngtsc/transform",
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/host",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/util",
],

View File

@ -9,7 +9,7 @@
import {Expression, Statement, Type} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator} from '../../metadata';
import {Decorator} from '../../host';
/**
* Provides the interface between a decorator compiler from @angular/compiler and the Typescript
@ -31,13 +31,13 @@ export interface DecoratorHandler<A> {
* if successful, or an array of diagnostic messages if the analysis fails or the decorator
* isn't valid.
*/
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<A>;
analyze(node: ts.Declaration, decorator: Decorator): AnalysisOutput<A>;
/**
* Generate a description of the field which should be added to the class, including any
* initialization code to be generated.
*/
compile(node: ts.ClassDeclaration, analysis: A): CompileResult;
compile(node: ts.Declaration, analysis: A): CompileResult;
}
/**

View File

@ -9,7 +9,8 @@
import {Expression, Type} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator, reflectDecorator} from '../../metadata';
import {Decorator, ReflectionHost} from '../../host';
import {reflectNameOfDeclaration} from '../../metadata/src/reflector';
import {AnalysisOutput, CompileResult, DecoratorHandler} from './api';
import {DtsFileTransformer} from './declaration';
@ -24,7 +25,7 @@ import {ImportManager, translateType} from './translator';
interface EmitFieldOperation<T> {
adapter: DecoratorHandler<T>;
analysis: AnalysisOutput<T>;
decorator: ts.Decorator;
decorator: Decorator;
}
/**
@ -38,59 +39,63 @@ export class IvyCompilation {
* Tracks classes which have been analyzed and found to have an Ivy decorator, and the
* information recorded about them for later compilation.
*/
private analysis = new Map<ts.ClassDeclaration, EmitFieldOperation<any>>();
private analysis = new Map<ts.Declaration, EmitFieldOperation<any>>();
/**
* Tracks the `DtsFileTransformer`s for each TS file that needs .d.ts transformations.
*/
private dtsMap = new Map<string, DtsFileTransformer>();
constructor(private handlers: DecoratorHandler<any>[], private checker: ts.TypeChecker) {}
constructor(
private handlers: DecoratorHandler<any>[], private checker: ts.TypeChecker,
private reflector: ReflectionHost) {}
/**
* Analyze a source file and produce diagnostics for it (if any).
*/
analyze(sf: ts.SourceFile): ts.Diagnostic[] {
const diagnostics: ts.Diagnostic[] = [];
const visit = (node: ts.Node) => {
// Process nodes recursively, and look for class declarations with decorators.
if (ts.isClassDeclaration(node) && node.decorators !== undefined) {
// The first step is to reflect the decorators, which will identify decorators
// that are imported from another module.
const decorators =
node.decorators.map(decorator => reflectDecorator(decorator, this.checker))
.filter(decorator => decorator !== null) as Decorator[];
// Look through the DecoratorHandlers to see if any are relevant.
this.handlers.forEach(adapter => {
// An adapter is relevant if it matches one of the decorators on the class.
const decorator = adapter.detect(decorators);
if (decorator === undefined) {
return;
}
// Check for multiple decorators on the same node. Technically speaking this
// could be supported, but right now it's an error.
if (this.analysis.has(node)) {
throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.');
}
// Run analysis on the decorator. This will produce either diagnostics, an
// analysis result, or both.
const analysis = adapter.analyze(node, decorator);
if (analysis.diagnostics !== undefined) {
diagnostics.push(...analysis.diagnostics);
}
if (analysis.analysis !== undefined) {
this.analysis.set(node, {
adapter,
analysis: analysis.analysis,
decorator: decorator.node,
});
}
});
const analyzeClass = (node: ts.Declaration): void => {
// The first step is to reflect the decorators.
const decorators = this.reflector.getDecoratorsOfDeclaration(node);
if (decorators === null) {
return;
}
// Look through the DecoratorHandlers to see if any are relevant.
this.handlers.forEach(adapter => {
// An adapter is relevant if it matches one of the decorators on the class.
const decorator = adapter.detect(decorators);
if (decorator === undefined) {
return;
}
// Check for multiple decorators on the same node. Technically speaking this
// could be supported, but right now it's an error.
if (this.analysis.has(node)) {
throw new Error('TODO.Diagnostic: Class has multiple Angular decorators.');
}
// Run analysis on the decorator. This will produce either diagnostics, an
// analysis result, or both.
const analysis = adapter.analyze(node, decorator);
if (analysis.diagnostics !== undefined) {
diagnostics.push(...analysis.diagnostics);
}
if (analysis.analysis !== undefined) {
this.analysis.set(node, {
adapter,
analysis: analysis.analysis, decorator,
});
}
});
};
const visit = (node: ts.Node): void => {
// Process nodes recursively, and look for class declarations with decorators.
if (ts.isClassDeclaration(node)) {
analyzeClass(node);
}
ts.forEachChild(node, visit);
};
@ -102,9 +107,9 @@ export class IvyCompilation {
* Perform a compilation operation on the given class declaration and return instructions to an
* AST transformer if any are available.
*/
compileIvyFieldFor(node: ts.ClassDeclaration): CompileResult|undefined {
compileIvyFieldFor(node: ts.Declaration): CompileResult|undefined {
// Look to see whether the original node was analyzed. If not, there's nothing to do.
const original = ts.getOriginalNode(node) as ts.ClassDeclaration;
const original = ts.getOriginalNode(node) as ts.Declaration;
if (!this.analysis.has(original)) {
return undefined;
}
@ -117,7 +122,7 @@ export class IvyCompilation {
// which will allow the .d.ts to be transformed later.
const fileName = node.getSourceFile().fileName;
const dtsTransformer = this.getDtsTransformer(fileName);
dtsTransformer.recordStaticField(node.name !.text, res);
dtsTransformer.recordStaticField(reflectNameOfDeclaration(node) !, res);
// Return the instruction to the transformer so the field will be added.
return res;
@ -126,8 +131,8 @@ export class IvyCompilation {
/**
* Lookup the `ts.Decorator` which triggered transformation of a particular class declaration.
*/
ivyDecoratorFor(node: ts.ClassDeclaration): ts.Decorator|undefined {
const original = ts.getOriginalNode(node) as ts.ClassDeclaration;
ivyDecoratorFor(node: ts.Declaration): Decorator|undefined {
const original = ts.getOriginalNode(node) as ts.Declaration;
if (!this.analysis.has(original)) {
return undefined;
}

View File

@ -46,7 +46,8 @@ class IvyVisitor extends Visitor {
node = ts.updateClassDeclaration(
node,
// Remove the decorator which triggered this compilation, leaving the others alone.
maybeFilterDecorator(node.decorators, this.compilation.ivyDecoratorFor(node) !),
maybeFilterDecorator(
node.decorators, this.compilation.ivyDecoratorFor(node) !.node as ts.Decorator),
node.modifiers, node.name, node.typeParameters, node.heritageClauses || [],
[...node.members, property]);
const statements = res.statements.map(stmt => translateStatement(stmt, this.importManager));