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

@ -26,6 +26,7 @@ ts_library(
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/transform",
],
)

View File

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

View File

@ -9,11 +9,13 @@
import {ConstantPool, Expression, R3ComponentMetadata, R3DirectiveMetadata, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator, reflectNonStaticField, reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {Decorator, ReflectionHost} from '../../host';
import {reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {extractDirectiveMetadata} from './directive';
import {SelectorScopeRegistry} from './selector_scope';
import {isAngularCore} from './util';
const EMPTY_MAP = new Map<string, Expression>();
@ -21,14 +23,18 @@ const EMPTY_MAP = new Map<string, Expression>();
* `DecoratorHandler` which handles the `@Component` annotation.
*/
export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMetadata> {
constructor(private checker: ts.TypeChecker, private scopeRegistry: SelectorScopeRegistry) {}
constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost,
private scopeRegistry: SelectorScopeRegistry) {}
detect(decorators: Decorator[]): Decorator|undefined {
return decorators.find(
decorator => decorator.name === 'Component' && decorator.from === '@angular/core');
return decorators.find(decorator => decorator.name === 'Component' && isAngularCore(decorator));
}
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3ComponentMetadata> {
if (decorator.args === null || decorator.args.length !== 1) {
throw new Error(`Incorrect number of arguments to @Component decorator`);
}
const meta = decorator.args[0];
if (!ts.isObjectLiteralExpression(meta)) {
throw new Error(`Decorator argument must be literal.`);
@ -36,7 +42,8 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
// @Component inherits @Directive, so begin by extracting the @Directive metadata and building
// on it.
const directiveMetadata = extractDirectiveMetadata(node, decorator, this.checker);
const directiveMetadata =
extractDirectiveMetadata(node, decorator, this.checker, this.reflector);
if (directiveMetadata === undefined) {
// `extractDirectiveMetadata` returns undefined when the @Directive has `jit: true`. In this
// case, compilation of the decorator is skipped. Returning an empty object signifies
@ -93,6 +100,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
}
};
}
compile(node: ts.ClassDeclaration, analysis: R3ComponentMetadata): CompileResult {
const pool = new ConstantPool();

View File

@ -9,25 +9,27 @@
import {ConstantPool, R3DirectiveMetadata, WrappedNodeExpr, compileDirectiveFromMetadata, makeBindingParser} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator, staticallyResolve} from '../../metadata';
import {DecoratedNode, getDecoratedClassElements, reflectNonStaticField, reflectObjectLiteral} from '../../metadata/src/reflector';
import {ClassMember, ClassMemberKind, Decorator, Import, ReflectionHost} from '../../host';
import {reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {filterToMembersWithDecorator} from '../../metadata/src/reflector';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {SelectorScopeRegistry} from './selector_scope';
import {getConstructorDependencies} from './util';
import {getConstructorDependencies, isAngularCore} from './util';
const EMPTY_OBJECT: {[key: string]: string} = {};
export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMetadata> {
constructor(private checker: ts.TypeChecker, private scopeRegistry: SelectorScopeRegistry) {}
constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost,
private scopeRegistry: SelectorScopeRegistry) {}
detect(decorators: Decorator[]): Decorator|undefined {
return decorators.find(
decorator => decorator.name === 'Directive' && decorator.from === '@angular/core');
return decorators.find(decorator => decorator.name === 'Directive' && isAngularCore(decorator));
}
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3DirectiveMetadata> {
const analysis = extractDirectiveMetadata(node, decorator, this.checker);
const analysis = extractDirectiveMetadata(node, decorator, this.checker, this.reflector);
// If the directive has a selector, it should be registered with the `SelectorScopeRegistry` so
// when this directive appears in an `@NgModule` scope, its selector can be determined.
@ -54,8 +56,11 @@ export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMe
* Helper function to extract metadata from a `Directive` or `Component`.
*/
export function extractDirectiveMetadata(
clazz: ts.ClassDeclaration, decorator: Decorator, checker: ts.TypeChecker): R3DirectiveMetadata|
undefined {
clazz: ts.ClassDeclaration, decorator: Decorator, checker: ts.TypeChecker,
reflector: ReflectionHost): R3DirectiveMetadata|undefined {
if (decorator.args === null || decorator.args.length !== 1) {
throw new Error(`Incorrect number of arguments to @${decorator.name} decorator`);
}
const meta = decorator.args[0];
if (!ts.isObjectLiteralExpression(meta)) {
throw new Error(`Decorator argument must be literal.`);
@ -67,20 +72,24 @@ export function extractDirectiveMetadata(
return undefined;
}
const members = reflector.getMembersOfClass(clazz);
// Precompute a list of ts.ClassElements that have decorators. This includes things like @Input,
// @Output, @HostBinding, etc.
const decoratedElements = getDecoratedClassElements(clazz, checker);
const decoratedElements =
members.filter(member => !member.isStatic && member.decorators !== null);
// Construct the map of inputs both from the @Directive/@Component decorator, and the decorated
// Construct the map of inputs both from the @Directive/@Component
// decorator, and the decorated
// fields.
const inputsFromMeta = parseFieldToPropertyMapping(directive, 'inputs', checker);
const inputsFromFields = parseDecoratedFields(
findDecoratedFields(decoratedElements, '@angular/core', 'Input'), checker);
filterToMembersWithDecorator(decoratedElements, 'Input', '@angular/core'), checker);
// And outputs.
const outputsFromMeta = parseFieldToPropertyMapping(directive, 'outputs', checker);
const outputsFromFields = parseDecoratedFields(
findDecoratedFields(decoratedElements, '@angular/core', 'Output'), checker);
filterToMembersWithDecorator(decoratedElements, '@angular/core', 'Output'), checker);
// Parse the selector.
let selector = '';
@ -93,11 +102,13 @@ export function extractDirectiveMetadata(
}
// Determine if `ngOnChanges` is a lifecycle hook defined on the component.
const usesOnChanges = reflectNonStaticField(clazz, 'ngOnChanges') !== null;
const usesOnChanges = members.find(
member => member.isStatic && member.kind === ClassMemberKind.Method &&
member.name === 'ngOnChanges') !== undefined;
return {
name: clazz.name !.text,
deps: getConstructorDependencies(clazz, checker),
deps: getConstructorDependencies(clazz, reflector),
host: {
attributes: {},
listeners: {},
@ -123,35 +134,6 @@ function assertIsStringArray(value: any[]): value is string[] {
return true;
}
type DecoratedProperty = DecoratedNode<ts.PropertyDeclaration|ts.AccessorDeclaration>;
/**
* Find all fields in the array of `DecoratedNode`s that have a decorator of the given type.
*/
function findDecoratedFields(
elements: DecoratedNode<ts.ClassElement>[], decoratorModule: string,
decoratorName: string): DecoratedProperty[] {
return elements
.map(entry => {
const element = entry.element;
// Only consider properties and accessors. Filter out everything else.
if (!ts.isPropertyDeclaration(element) && !ts.isAccessor(element)) {
return null;
}
// Extract the array of matching decorators (there could be more than one).
const decorators = entry.decorators.filter(
decorator => decorator.name === decoratorName && decorator.from === decoratorModule);
if (decorators.length === 0) {
// No matching decorators, don't include this element.
return null;
}
return {element, decorators};
})
// Filter out nulls.
.filter(entry => entry !== null) as DecoratedProperty[];
}
/**
* Interpret property mapping fields on the decorator (e.g. inputs or outputs) and return the
* correctly shaped metadata object.
@ -185,14 +167,15 @@ function parseFieldToPropertyMapping(
* object.
*/
function parseDecoratedFields(
fields: DecoratedProperty[], checker: ts.TypeChecker): {[field: string]: string} {
fields: {member: ClassMember, decorators: Decorator[]}[],
checker: ts.TypeChecker): {[field: string]: string} {
return fields.reduce(
(results, field) => {
const fieldName = (field.element.name as ts.Identifier).text;
const fieldName = field.member.name;
field.decorators.forEach(decorator => {
// The decorator either doesn't have an argument (@Input()) in which case the property
// name is used, or it has one argument (@Output('named')).
if (decorator.args.length === 0) {
if (decorator.args == null || decorator.args.length === 0) {
results[fieldName] = fieldName;
} else if (decorator.args.length === 1) {
const property = staticallyResolve(decorator.args[0], checker);

View File

@ -9,26 +9,26 @@
import {Expression, LiteralExpr, R3DependencyMetadata, R3InjectableMetadata, R3ResolvedDependencyType, WrappedNodeExpr, compileInjectable as compileIvyInjectable} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator} from '../../metadata';
import {reflectConstructorParameters, reflectImportedIdentifier, reflectObjectLiteral} from '../../metadata/src/reflector';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform/src/api';
import {Decorator, ReflectionHost} from '../../host';
import {reflectObjectLiteral} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {getConstructorDependencies} from './util';
import {getConstructorDependencies, isAngularCore} from './util';
/**
* Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler.
*/
export class InjectableDecoratorHandler implements DecoratorHandler<R3InjectableMetadata> {
constructor(private checker: ts.TypeChecker) {}
constructor(private reflector: ReflectionHost) {}
detect(decorator: Decorator[]): Decorator|undefined {
return decorator.find(dec => dec.name === 'Injectable' && dec.from === '@angular/core');
return decorator.find(decorator => decorator.name === 'Injectable' && isAngularCore(decorator));
}
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3InjectableMetadata> {
return {
analysis: extractInjectableMetadata(node, decorator, this.checker),
analysis: extractInjectableMetadata(node, decorator, this.reflector),
};
}
@ -49,18 +49,21 @@ export class InjectableDecoratorHandler implements DecoratorHandler<R3Injectable
*/
function extractInjectableMetadata(
clazz: ts.ClassDeclaration, decorator: Decorator,
checker: ts.TypeChecker): R3InjectableMetadata {
reflector: ReflectionHost): R3InjectableMetadata {
if (clazz.name === undefined) {
throw new Error(`@Injectables must have names`);
}
const name = clazz.name.text;
const type = new WrappedNodeExpr(clazz.name);
if (decorator.args === null) {
throw new Error(`@Injectable must be called`);
}
if (decorator.args.length === 0) {
return {
name,
type,
providedIn: new LiteralExpr(null),
deps: getConstructorDependencies(clazz, checker),
deps: getConstructorDependencies(clazz, reflector),
};
} else if (decorator.args.length === 1) {
const metaNode = decorator.args[0];
@ -95,11 +98,11 @@ function extractInjectableMetadata(
if (depsExpr.elements.length > 0) {
throw new Error(`deps not yet supported`);
}
deps.push(...depsExpr.elements.map(dep => getDep(dep, checker)));
deps.push(...depsExpr.elements.map(dep => getDep(dep, reflector)));
}
return {name, type, providedIn, useFactory: factory, deps};
} else {
const deps = getConstructorDependencies(clazz, checker);
const deps = getConstructorDependencies(clazz, reflector);
return {name, type, providedIn, deps};
}
} else {
@ -109,7 +112,7 @@ function extractInjectableMetadata(
function getDep(dep: ts.Expression, checker: ts.TypeChecker): R3DependencyMetadata {
function getDep(dep: ts.Expression, reflector: ReflectionHost): R3DependencyMetadata {
const meta: R3DependencyMetadata = {
token: new WrappedNodeExpr(dep),
host: false,
@ -119,8 +122,9 @@ function getDep(dep: ts.Expression, checker: ts.TypeChecker): R3DependencyMetada
skipSelf: false,
};
function maybeUpdateDecorator(dec: ts.Identifier, token?: ts.Expression): void {
const source = reflectImportedIdentifier(dec, checker);
function maybeUpdateDecorator(
dec: ts.Identifier, reflector: ReflectionHost, token?: ts.Expression): void {
const source = reflector.getImportOfIdentifier(dec);
if (source === null || source.from !== '@angular/core') {
return;
}
@ -145,10 +149,10 @@ function getDep(dep: ts.Expression, checker: ts.TypeChecker): R3DependencyMetada
if (ts.isArrayLiteralExpression(dep)) {
dep.elements.forEach(el => {
if (ts.isIdentifier(el)) {
maybeUpdateDecorator(el);
maybeUpdateDecorator(el, reflector);
} else if (ts.isNewExpression(el) && ts.isIdentifier(el.expression)) {
const token = el.arguments && el.arguments.length > 0 && el.arguments[0] || undefined;
maybeUpdateDecorator(el.expression, token);
maybeUpdateDecorator(el.expression, reflector, token);
}
});
}

View File

@ -9,11 +9,12 @@
import {ConstantPool, Expression, R3DirectiveMetadata, R3NgModuleMetadata, WrappedNodeExpr, compileNgModule, makeBindingParser, parseTemplate} from '@angular/compiler';
import * as ts from 'typescript';
import {Decorator, Reference, ResolvedValue, reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {Decorator} from '../../host';
import {Reference, ResolvedValue, reflectObjectLiteral, staticallyResolve} from '../../metadata';
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {SelectorScopeRegistry} from './selector_scope';
import {referenceToExpression} from './util';
import {isAngularCore, referenceToExpression} from './util';
/**
* Compiles @NgModule annotations to ngModuleDef fields.
@ -24,11 +25,13 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<R3NgModuleMeta
constructor(private checker: ts.TypeChecker, private scopeRegistry: SelectorScopeRegistry) {}
detect(decorators: Decorator[]): Decorator|undefined {
return decorators.find(
decorator => decorator.name === 'NgModule' && decorator.from === '@angular/core');
return decorators.find(decorator => decorator.name === 'NgModule' && isAngularCore(decorator));
}
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3NgModuleMetadata> {
if (decorator.args === null || decorator.args.length !== 1) {
throw new Error(`Incorrect number of arguments to @NgModule decorator`);
}
const meta = decorator.args[0];
if (!ts.isObjectLiteralExpression(meta)) {
throw new Error(`Decorator argument must be literal.`);

View File

@ -9,11 +9,14 @@
import {Expression, ExternalExpr, ExternalReference} from '@angular/compiler';
import * as ts from 'typescript';
import {AbsoluteReference, Reference, reflectStaticField, reflectTypeEntityToDeclaration} from '../../metadata';
import {ReflectionHost} from '../../host';
import {AbsoluteReference, Reference, reflectTypeEntityToDeclaration} from '../../metadata';
import {reflectIdentifierOfDeclaration, reflectNameOfDeclaration} from '../../metadata/src/reflector';
import {referenceToExpression} from './util';
/**
* Metadata extracted for a given NgModule that can be used to compute selector scopes.
*/
@ -59,55 +62,55 @@ export class SelectorScopeRegistry {
/**
* Map of modules declared in the current compilation unit to their (local) metadata.
*/
private _moduleToData = new Map<ts.ClassDeclaration, ModuleData>();
private _moduleToData = new Map<ts.Declaration, ModuleData>();
/**
* Map of modules to their cached `CompilationScope`s.
*/
private _compilationScopeCache = new Map<ts.ClassDeclaration, CompilationScope<Reference>>();
private _compilationScopeCache = new Map<ts.Declaration, CompilationScope<Reference>>();
/**
* Map of components/directives to their selector.
*/
private _directiveToSelector = new Map<ts.ClassDeclaration, string>();
private _directiveToSelector = new Map<ts.Declaration, string>();
/**
* Map of pipes to their name.
*/
private _pipeToName = new Map<ts.ClassDeclaration, string>();
private _pipeToName = new Map<ts.Declaration, string>();
/**
* Map of components/directives/pipes to their module.
*/
private _declararedTypeToModule = new Map<ts.ClassDeclaration, ts.ClassDeclaration>();
private _declararedTypeToModule = new Map<ts.Declaration, ts.Declaration>();
constructor(private checker: ts.TypeChecker) {}
constructor(private checker: ts.TypeChecker, private reflector: ReflectionHost) {}
/**
* Register a module's metadata with the registry.
*/
registerModule(node: ts.ClassDeclaration, data: ModuleData): void {
node = ts.getOriginalNode(node) as ts.ClassDeclaration;
registerModule(node: ts.Declaration, data: ModuleData): void {
node = ts.getOriginalNode(node) as ts.Declaration;
if (this._moduleToData.has(node)) {
throw new Error(`Module already registered: ${node.name!.text}`);
throw new Error(`Module already registered: ${reflectNameOfDeclaration(node)}`);
}
this._moduleToData.set(node, data);
// Register all of the module's declarations in the context map as belonging to this module.
data.declarations.forEach(decl => {
this._declararedTypeToModule.set(ts.getOriginalNode(decl.node) as ts.ClassDeclaration, node);
this._declararedTypeToModule.set(ts.getOriginalNode(decl.node) as ts.Declaration, node);
});
}
/**
* Register the selector of a component or directive with the registry.
*/
registerSelector(node: ts.ClassDeclaration, selector: string): void {
node = ts.getOriginalNode(node) as ts.ClassDeclaration;
registerSelector(node: ts.Declaration, selector: string): void {
node = ts.getOriginalNode(node) as ts.Declaration;
if (this._directiveToSelector.has(node)) {
throw new Error(`Selector already registered: ${node.name!.text} ${selector}`);
throw new Error(`Selector already registered: ${reflectNameOfDeclaration(node)} ${selector}`);
}
this._directiveToSelector.set(node, selector);
}
@ -115,14 +118,14 @@ export class SelectorScopeRegistry {
/**
* Register the name of a pipe with the registry.
*/
registerPipe(node: ts.ClassDeclaration, name: string): void { this._pipeToName.set(node, name); }
registerPipe(node: ts.Declaration, name: string): void { this._pipeToName.set(node, name); }
/**
* Produce the compilation scope of a component, which is determined by the module that declares
* it.
*/
lookupCompilationScope(node: ts.ClassDeclaration): CompilationScope<Expression>|null {
node = ts.getOriginalNode(node) as ts.ClassDeclaration;
lookupCompilationScope(node: ts.Declaration): CompilationScope<Expression>|null {
node = ts.getOriginalNode(node) as ts.Declaration;
// If the component has no associated module, then it has no compilation scope.
if (!this._declararedTypeToModule.has(node)) {
@ -150,8 +153,7 @@ export class SelectorScopeRegistry {
// The initial value of ngModuleImportedFrom is 'null' which signifies that the NgModule
// was not imported from a .d.ts source.
this.lookupScopes(module !, /* ngModuleImportedFrom */ null).compilation.forEach(ref => {
const selector =
this.lookupDirectiveSelector(ts.getOriginalNode(ref.node) as ts.ClassDeclaration);
const selector = this.lookupDirectiveSelector(ts.getOriginalNode(ref.node) as ts.Declaration);
// Only directives/components with selectors get added to the scope.
if (selector != null) {
directives.set(selector, ref);
@ -174,8 +176,7 @@ export class SelectorScopeRegistry {
* (`ngModuleImportedFrom`) then all of its declarations are exported at that same path, as well
* as imports and exports from other modules that are relatively imported.
*/
private lookupScopes(node: ts.ClassDeclaration, ngModuleImportedFrom: string|null):
SelectorScopes {
private lookupScopes(node: ts.Declaration, ngModuleImportedFrom: string|null): SelectorScopes {
let data: ModuleData|null = null;
// Either this module was analyzed directly, or has a precompiled ngModuleDef.
@ -195,7 +196,7 @@ export class SelectorScopeRegistry {
}
if (data === null) {
throw new Error(`Module not registered: ${node.name!.text}`);
throw new Error(`Module not registered: ${reflectNameOfDeclaration(node)}`);
}
return {
@ -203,20 +204,18 @@ export class SelectorScopeRegistry {
...data.declarations,
// Expand imports to the exported scope of those imports.
...flatten(data.imports.map(
ref => this.lookupScopes(ref.node as ts.ClassDeclaration, absoluteModuleName(ref))
.exported)),
ref =>
this.lookupScopes(ref.node as ts.Declaration, absoluteModuleName(ref)).exported)),
// And include the compilation scope of exported modules.
...flatten(
data.exports.filter(ref => this._moduleToData.has(ref.node as ts.ClassDeclaration))
data.exports.filter(ref => this._moduleToData.has(ref.node as ts.Declaration))
.map(
ref =>
this.lookupScopes(ref.node as ts.ClassDeclaration, absoluteModuleName(ref))
.exported))
ref => this.lookupScopes(ref.node as ts.Declaration, absoluteModuleName(ref))
.exported))
],
exported: flatten(data.exports.map(ref => {
if (this._moduleToData.has(ref.node as ts.ClassDeclaration)) {
return this.lookupScopes(ref.node as ts.ClassDeclaration, absoluteModuleName(ref))
.exported;
if (this._moduleToData.has(ref.node as ts.Declaration)) {
return this.lookupScopes(ref.node as ts.Declaration, absoluteModuleName(ref)).exported;
} else {
return [ref];
}
@ -231,7 +230,7 @@ export class SelectorScopeRegistry {
* ngComponentDef/ngDirectiveDef. In this case, the type metadata of that definition is read
* to determine the selector.
*/
private lookupDirectiveSelector(node: ts.ClassDeclaration): string|null {
private lookupDirectiveSelector(node: ts.Declaration): string|null {
if (this._directiveToSelector.has(node)) {
return this._directiveToSelector.get(node) !;
} else {
@ -239,7 +238,7 @@ export class SelectorScopeRegistry {
}
}
private lookupPipeName(node: ts.ClassDeclaration): string|undefined {
private lookupPipeName(node: ts.Declaration): string|undefined {
return this._pipeToName.get(node);
}
@ -251,16 +250,17 @@ export class SelectorScopeRegistry {
* @param ngModuleImportedFrom module specifier of the import path to assume for all declarations
* stemming from this module.
*/
private _readMetadataFromCompiledClass(clazz: ts.ClassDeclaration, ngModuleImportedFrom: string):
private _readMetadataFromCompiledClass(clazz: ts.Declaration, ngModuleImportedFrom: string):
ModuleData|null {
// This operation is explicitly not memoized, as it depends on `ngModuleImportedFrom`.
// TODO(alxhub): investigate caching of .d.ts module metadata.
const ngModuleDef = reflectStaticField(clazz, 'ngModuleDef');
if (ngModuleDef === null) {
const ngModuleDef = this.reflector.getMembersOfClass(clazz).find(
member => member.name === 'ngModuleDef' && member.isStatic);
if (ngModuleDef === undefined) {
return null;
} else if (
// Validate that the shape of the ngModuleDef type is correct.
ngModuleDef.type === undefined || !ts.isTypeReferenceNode(ngModuleDef.type) ||
ngModuleDef.type === null || !ts.isTypeReferenceNode(ngModuleDef.type) ||
ngModuleDef.type.typeArguments === undefined ||
ngModuleDef.type.typeArguments.length !== 4) {
return null;
@ -279,14 +279,15 @@ export class SelectorScopeRegistry {
* Get the selector from type metadata for a class with a precompiled ngComponentDef or
* ngDirectiveDef.
*/
private _readSelectorFromCompiledClass(clazz: ts.ClassDeclaration): string|null {
const def =
reflectStaticField(clazz, 'ngComponentDef') || reflectStaticField(clazz, 'ngDirectiveDef');
if (def === null) {
private _readSelectorFromCompiledClass(clazz: ts.Declaration): string|null {
const def = this.reflector.getMembersOfClass(clazz).find(
field =>
field.isStatic && (field.name === 'ngComponentDef' || field.name === 'ngDirectiveDef'));
if (def === undefined) {
// No definition could be found.
return null;
} else if (
def.type === undefined || !ts.isTypeReferenceNode(def.type) ||
def.type === null || !ts.isTypeReferenceNode(def.type) ||
def.type.typeArguments === undefined || def.type.typeArguments.length !== 2) {
// The type metadata was the wrong shape.
return null;
@ -317,8 +318,9 @@ export class SelectorScopeRegistry {
const type = element.typeName;
const {node, from} = reflectTypeEntityToDeclaration(type, this.checker);
const moduleName = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom);
const clazz = node as ts.ClassDeclaration;
return new AbsoluteReference(node, clazz.name !, moduleName, clazz.name !.text);
const clazz = node as ts.Declaration;
const id = reflectIdentifierOfDeclaration(clazz);
return new AbsoluteReference(node, id !, moduleName, id !.text);
});
}
}
@ -331,7 +333,6 @@ function flatten<T>(array: T[][]): T[] {
}
function absoluteModuleName(ref: Reference): string|null {
const name = (ref.node as ts.ClassDeclaration).name !.text;
if (!(ref instanceof AbsoluteReference)) {
return null;
}

View File

@ -9,20 +9,20 @@
import {Expression, R3DependencyMetadata, R3ResolvedDependencyType, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {Reference, reflectConstructorParameters} from '../../metadata';
import {reflectImportedIdentifier} from '../../metadata/src/reflector';
import {Decorator, ReflectionHost} from '../../host';
import {Reference} from '../../metadata';
export function getConstructorDependencies(
clazz: ts.ClassDeclaration, checker: ts.TypeChecker): R3DependencyMetadata[] {
clazz: ts.ClassDeclaration, reflector: ReflectionHost): R3DependencyMetadata[] {
const useType: R3DependencyMetadata[] = [];
const ctorParams = (reflectConstructorParameters(clazz, checker) || []);
ctorParams.forEach(param => {
let tokenExpr = param.typeValueExpr;
const ctorParams = reflector.getConstructorParameters(clazz) || [];
ctorParams.forEach((param, idx) => {
let tokenExpr = param.type;
let optional = false, self = false, skipSelf = false, host = false;
let resolved = R3ResolvedDependencyType.Token;
param.decorators.filter(dec => dec.from === '@angular/core').forEach(dec => {
(param.decorators || []).filter(isAngularCore).forEach(dec => {
if (dec.name === 'Inject') {
if (dec.args.length !== 1) {
if (dec.args === null || dec.args.length !== 1) {
throw new Error(`Unexpected number of arguments to @Inject().`);
}
tokenExpr = dec.args[0];
@ -35,7 +35,7 @@ export function getConstructorDependencies(
} else if (dec.name === 'Host') {
host = true;
} else if (dec.name === 'Attribute') {
if (dec.args.length !== 1) {
if (dec.args === null || dec.args.length !== 1) {
throw new Error(`Unexpected number of arguments to @Attribute().`);
}
tokenExpr = dec.args[0];
@ -46,10 +46,10 @@ export function getConstructorDependencies(
});
if (tokenExpr === null) {
throw new Error(
`No suitable token for parameter ${(param.name as ts.Identifier).text} of class ${clazz.name!.text} with decorators ${param.decorators.map(dec => dec.from + '#' + dec.name).join(',')}`);
`No suitable token for parameter ${param.name || idx} of class ${clazz.name!.text}`);
}
if (ts.isIdentifier(tokenExpr)) {
const importedSymbol = reflectImportedIdentifier(tokenExpr, checker);
const importedSymbol = reflector.getImportOfIdentifier(tokenExpr);
if (importedSymbol !== null && importedSymbol.from === '@angular/core') {
switch (importedSymbol.name) {
case 'ElementRef':
@ -82,3 +82,7 @@ export function referenceToExpression(ref: Reference, context: ts.SourceFile): E
}
return exp;
}
export function isAngularCore(decorator: Decorator): boolean {
return decorator.import !== null && decorator.import.from === '@angular/core';
}

View File

@ -8,6 +8,7 @@
import * as ts from 'typescript';
import {TypeScriptReflectionHost} from '../../metadata';
import {AbsoluteReference, ResolvedReference} from '../../metadata/src/resolver';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {NgModuleDecoratorHandler} from '../src/ng_module';
@ -53,6 +54,7 @@ describe('SelectorScopeRegistry', () => {
},
]);
const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const ProgramModule =
getDeclaration(program, 'entry.ts', 'ProgramModule', ts.isClassDeclaration);
const ProgramCmp = getDeclaration(program, 'entry.ts', 'ProgramCmp', ts.isClassDeclaration);
@ -61,7 +63,7 @@ describe('SelectorScopeRegistry', () => {
expect(ProgramModule).toBeDefined();
expect(SomeModule).toBeDefined();
const registry = new SelectorScopeRegistry(checker);
const registry = new SelectorScopeRegistry(checker, host);
registry.registerModule(ProgramModule, {
declarations: [new ResolvedReference(ProgramCmp, ProgramCmp.name !)],

View File

@ -0,0 +1,12 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "ts_library")
ts_library(
name = "host",
srcs = glob([
"index.ts",
"src/**/*.ts",
]),
module_name = "@angular/compiler-cli/src/ngtsc/host",
)

View File

@ -0,0 +1,9 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export * from './src/reflection';

View File

@ -0,0 +1,223 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
/**
* Metadata extracted from an instance of a decorator on another declaration.
*/
export interface Decorator {
/**
* Name by which the decorator was invoked in the user's code.
*
* This is distinct from the name by which the decorator was imported (though in practice they
* will usually be the same).
*/
name: string;
/**
* `Import` by which the decorator was brought into the module in which it was invoked, or `null`
* if the decorator was declared in the same module and not imported.
*/
import : Import | null;
/**
* TypeScript reference to the decorator itself.
*/
node: ts.Node;
/**
* Arguments of the invocation of the decorator, if the decorator is invoked, or `null` otherwise.
*/
args: ts.Expression[]|null;
}
/**
* An enumeration of possible kinds of class members.
*/
export enum ClassMemberKind {
Constructor,
Getter,
Setter,
Property,
Method,
}
/**
* A member of a class, such as a property, method, or constructor.
*/
export interface ClassMember {
/**
* TypeScript reference to the class member itself.
*/
node: ts.Node;
/**
* Indication of which type of member this is (property, method, etc).
*/
kind: ClassMemberKind;
/**
* TypeScript `ts.TypeNode` representing the type of the member, or `null` if not present or
* applicable.
*/
type: ts.TypeNode|null;
/**
* Name of the class member.
*/
name: string;
/**
* TypeScript `ts.Identifier` representing the name of the member, or `null` if no such node
* is present.
*
* The `nameNode` is useful in writing references to this member that will be correctly source-
* mapped back to the original file.
*/
nameNode: ts.Identifier|null;
/**
* TypeScript `ts.Expression` which initializes this member, if the member is a property, or
* `null` otherwise.
*/
initializer: ts.Expression|null;
/**
* Whether the member is static or not.
*/
isStatic: boolean;
/**
* Any `Decorator`s which are present on the member, or `null` if none are present.
*/
decorators: Decorator[]|null;
}
/**
* A parameter to a function or constructor.
*/
export interface Parameter {
/**
* Name of the parameter, if available.
*
* Some parameters don't have a simple string name (for example, parameters which are destructured
* into multiple variables). In these cases, `name` can be `null`.
*/
name: string|null;
/**
* TypeScript `ts.BindingName` representing the name of the parameter.
*
* The `nameNode` is useful in writing references to this member that will be correctly source-
* mapped back to the original file.
*/
nameNode: ts.BindingName;
/**
* TypeScript `ts.Expression` representing the type of the parameter, if the type is a simple
* expression type.
*
* If the type is not present or cannot be represented as an expression, `type` is `null`.
*/
type: ts.Expression|null;
/**
* Any `Decorator`s which are present on the parameter, or `null` if none are present.
*/
decorators: Decorator[]|null;
}
/**
* The source of an imported symbol, including the original symbol name and the module from which it
* was imported.
*/
export interface Import {
/**
* The name of the imported symbol under which it was exported (not imported).
*/
name: string;
/**
* The module from which the symbol was imported.
*
* This could either be an absolute module name (@angular/core for example) or a relative path.
*/
from: string;
}
/**
* Abstracts reflection operations on a TypeScript AST.
*
* Depending on the format of the code being interpreted, different concepts are represented with
* different syntactical structures. The `ReflectionHost` abstracts over those differences and
* presents a single API by which the compiler can query specific information about the AST.
*
* All operations on the `ReflectionHost` require the use of TypeScript `ts.Node`s with binding
* information already available (that is, nodes that come from a `ts.Program` that has been
* type-checked, and are not synthetically created).
*/
export interface ReflectionHost {
/**
* Examine a declaration (for example, of a class or function) and return metadata about any
* decorators present on the declaration.
*
* @param declaration a TypeScript `ts.Declaration` node representing the class or function over
* which to reflect. For example, if the intent is to reflect the decorators of a class and the
* source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the source is in ES5
* format, this might be a `ts.VariableDeclaration` as classes in ES5 are represented as the
* result of an IIFE execution.
*
* @returns an array of `Decorator` metadata if decorators are present on the declaration, or
* `null` if either no decorators were present or if the declaration is not of a decorable type.
*/
getDecoratorsOfDeclaration(declaration: ts.Declaration): Decorator[]|null;
/**
* Examine a declaration which should be of a class, and return metadata about the members of the
* class.
*
* @param declaration a TypeScript `ts.Declaration` node representing the class over which to
* reflect. If the source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the
* source is in ES5 format, this might be a `ts.VariableDeclaration` as classes in ES5 are
* represented as the result of an IIFE execution.
*
* @returns an array of `ClassMember` metadata representing the members of the class.
*
* @throws if `declaration` does not resolve to a class declaration.
*/
getMembersOfClass(clazz: ts.Declaration): ClassMember[];
/**
* Reflect over the constructor of a class and return metadata about its parameters.
*
* This method only looks at the constructor of a class directly and not at any inherited
* constructors.
*
* @param declaration a TypeScript `ts.Declaration` node representing the class over which to
* reflect. If the source is in ES6 format, this will be a `ts.ClassDeclaration` node. If the
* source is in ES5 format, this might be a `ts.VariableDeclaration` as classes in ES5 are
* represented as the result of an IIFE execution.
*
* @returns an array of `Parameter` metadata representing the parameters of the constructor, if
* a constructor exists. If the constructor exists and has 0 parameters, this array will be empty.
* If the class has no constructor, this method returns `null`.
*/
getConstructorParameters(declaration: ts.Declaration): Parameter[]|null;
/**
* Determine if an identifier was imported from another module and return `Import` metadata
* describing its origin.
*
* @param id a TypeScript `ts.Identifer` to reflect.
*
* @returns metadata about the `Import` if the identifier was imported from another module, or
* `null` if the identifier doesn't resolve to an import but instead is locally defined.
*/
getImportOfIdentifier(id: ts.Identifier): Import|null;
}

View File

@ -12,5 +12,6 @@ ts_library(
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/host",
],
)

View File

@ -6,6 +6,5 @@
* found in the LICENSE file at https://angular.io/license
*/
export {Decorator, Parameter, reflectConstructorParameters, reflectDecorator, reflectNonStaticField, reflectObjectLiteral, reflectStaticField, reflectTypeEntityToDeclaration,} from './src/reflector';
export {TypeScriptReflectionHost, filterToMembersWithDecorator, findMember, reflectObjectLiteral, reflectTypeEntityToDeclaration} from './src/reflector';
export {AbsoluteReference, Reference, ResolvedValue, isDynamicValue, staticallyResolve} from './src/resolver';

View File

@ -8,289 +8,187 @@
import * as ts from 'typescript';
import {ClassMember, ClassMemberKind, Decorator, Import, Parameter, ReflectionHost} from '../../host';
/**
* reflector.ts implements static reflection of declarations using the TypeScript `ts.TypeChecker`.
*/
/**
* A reflected parameter of a function, method, or constructor, indicating the name, any
* decorators, and an expression representing a reference to the value side of the parameter's
* declared type, if applicable.
*/
export interface Parameter {
/**
* Name of the parameter as a `ts.BindingName`, which allows the parameter name to be identified
* via sourcemaps.
*/
name: ts.BindingName;
export class TypeScriptReflectionHost implements ReflectionHost {
constructor(protected checker: ts.TypeChecker) {}
/**
* A `ts.Expression` which represents a reference to the value side of the parameter's type.
*/
typeValueExpr: ts.Expression|null;
/**
* Array of decorators present on the parameter.
*/
decorators: Decorator[];
}
/**
* A reflected decorator, indicating the name, where it was imported from, and any arguments if the
* decorator is a call expression.
*/
export interface Decorator {
/**
* Name of the decorator, extracted from the decoration expression.
*/
name: string;
/**
* Import path (relative to the decorator's file) of the decorator itself.
*/
from: string;
/**
* The decorator node itself (useful for printing sourcemap based references to the decorator).
*/
node: ts.Decorator;
/**
* Any arguments of a call expression, if one is present. If the decorator was not a call
* expression, then this will be an empty array.
*/
args: ts.Expression[];
}
/**
* Reflect a `ts.ClassDeclaration` and determine the list of parameters.
*
* Note that this only reflects the referenced class and not any potential parent class - that must
* be handled by the caller.
*
* @param node the `ts.ClassDeclaration` to reflect
* @param checker a `ts.TypeChecker` used for reflection
* @returns a `Parameter` instance for each argument of the constructor, or `null` if no constructor
*/
export function reflectConstructorParameters(
node: ts.ClassDeclaration, checker: ts.TypeChecker): Parameter[]|null {
// Firstly, look for a constructor.
// clang-format off
const maybeCtor: ts.ConstructorDeclaration[] = node
.members
.filter(element => ts.isConstructorDeclaration(element)) as ts.ConstructorDeclaration[];
// clang-format on
if (maybeCtor.length !== 1) {
// No constructor.
return null;
}
// Reflect each parameter.
return maybeCtor[0].parameters.map(param => reflectParameter(param, checker));
}
/**
* Reflect a `ts.ParameterDeclaration` and determine its name, a token which refers to the value
* declaration of its type (if possible to statically determine), and its decorators, if any.
*/
function reflectParameter(node: ts.ParameterDeclaration, checker: ts.TypeChecker): Parameter {
// The name of the parameter is easy.
const name = node.name;
const decorators = node.decorators &&
node.decorators.map(decorator => reflectDecorator(decorator, checker))
.filter(decorator => decorator !== null) as Decorator[] ||
[];
// It may or may not be possible to write an expression that refers to the value side of the
// type named for the parameter.
let typeValueExpr: ts.Expression|null = null;
// It's not possible to get a value expression if the parameter doesn't even have a type.
if (node.type !== undefined) {
// It's only valid to convert a type reference to a value reference if the type actually has a
// value declaration associated with it.
const type = checker.getTypeFromTypeNode(node.type);
if (type.symbol !== undefined && type.symbol.valueDeclaration !== undefined) {
// The type points to a valid value declaration. Rewrite the TypeReference into an Expression
// which references the value pointed to by the TypeReference, if possible.
typeValueExpr = typeNodeToValueExpr(node.type);
getDecoratorsOfDeclaration(declaration: ts.Declaration): Decorator[]|null {
if (declaration.decorators === undefined || declaration.decorators.length === 0) {
return null;
}
return declaration.decorators.map(decorator => this._reflectDecorator(decorator))
.filter((dec): dec is Decorator => dec !== null);
}
return {
name, typeValueExpr, decorators,
};
}
/**
* Reflect a decorator and return a structure describing where it comes from and any arguments.
*
* Only imported decorators are considered, not locally defined decorators.
*/
export function reflectDecorator(decorator: ts.Decorator, checker: ts.TypeChecker): Decorator|null {
// Attempt to resolve the decorator expression into a reference to a concrete Identifier. The
// expression may contain a call to a function which returns the decorator function, in which
// case we want to return the arguments.
let decoratorOfInterest: ts.Expression = decorator.expression;
let args: ts.Expression[] = [];
// Check for call expressions.
if (ts.isCallExpression(decoratorOfInterest)) {
args = Array.from(decoratorOfInterest.arguments);
decoratorOfInterest = decoratorOfInterest.expression;
getMembersOfClass(declaration: ts.Declaration): ClassMember[] {
const clazz = castDeclarationToClassOrDie(declaration);
return clazz.members.map(member => this._reflectMember(member))
.filter((member): member is ClassMember => member !== null);
}
// The final resolved decorator should be a `ts.Identifier` - if it's not, then something is
// wrong and the decorator can't be resolved statically.
if (!ts.isIdentifier(decoratorOfInterest)) {
return null;
}
getConstructorParameters(declaration: ts.Declaration): Parameter[]|null {
const clazz = castDeclarationToClassOrDie(declaration);
const importDecl = reflectImportedIdentifier(decoratorOfInterest, checker);
if (importDecl === null) {
return null;
}
// First, find the constructor.
const ctor = clazz.members.find(ts.isConstructorDeclaration);
if (ctor === undefined) {
return null;
}
return {
...importDecl,
node: decorator, args,
};
}
return ctor.parameters.map(node => {
// The name of the parameter is easy.
const name = parameterName(node.name);
function typeNodeToValueExpr(node: ts.TypeNode): ts.Expression|null {
if (ts.isTypeReferenceNode(node)) {
return entityNameToValue(node.typeName);
} else {
return null;
}
}
const decorators = this.getDecoratorsOfDeclaration(node);
function entityNameToValue(node: ts.EntityName): ts.Expression|null {
if (ts.isQualifiedName(node)) {
const left = entityNameToValue(node.left);
return left !== null ? ts.createPropertyAccess(left, node.right) : null;
} else if (ts.isIdentifier(node)) {
return ts.getMutableClone(node);
} else {
return null;
}
}
// It may or may not be possible to write an expression that refers to the value side of the
// type named for the parameter.
let typeValueExpr: ts.Expression|null = null;
function propertyNameToValue(node: ts.PropertyName): string|null {
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
return node.text;
} else {
return null;
}
}
export function reflectObjectLiteral(node: ts.ObjectLiteralExpression): Map<string, ts.Expression> {
const map = new Map<string, ts.Expression>();
node.properties.forEach(prop => {
if (ts.isPropertyAssignment(prop)) {
const name = propertyNameToValue(prop.name);
if (name === null) {
return;
// It's not possible to get a value expression if the parameter doesn't even have a type.
if (node.type !== undefined) {
// It's only valid to convert a type reference to a value reference if the type actually has
// a
// value declaration associated with it.
const type = this.checker.getTypeFromTypeNode(node.type);
if (type.symbol !== undefined && type.symbol.valueDeclaration !== undefined) {
// The type points to a valid value declaration. Rewrite the TypeReference into an
// Expression
// which references the value pointed to by the TypeReference, if possible.
typeValueExpr = typeNodeToValueExpr(node.type);
}
}
map.set(name, prop.initializer);
} else if (ts.isShorthandPropertyAssignment(prop)) {
map.set(prop.name.text, prop.name);
return {
name,
nameNode: node.name,
type: typeValueExpr, decorators,
};
});
}
getImportOfIdentifier(id: ts.Identifier): Import|null {
const symbol = this.checker.getSymbolAtLocation(id);
if (symbol === undefined || symbol.declarations === undefined ||
symbol.declarations.length !== 1) {
return null;
}
// Ignore decorators that are defined locally (not imported).
const decl: ts.Declaration = symbol.declarations[0];
if (!ts.isImportSpecifier(decl)) {
return null;
}
// Walk back from the specifier to find the declaration, which carries the module specifier.
const importDecl = decl.parent !.parent !.parent !;
// The module specifier is guaranteed to be a string literal, so this should always pass.
if (!ts.isStringLiteral(importDecl.moduleSpecifier)) {
// Not allowed to happen in TypeScript ASTs.
return null;
}
// Read the module specifier.
const from = importDecl.moduleSpecifier.text;
// Compute the name by which the decorator was exported, not imported.
const name = (decl.propertyName !== undefined ? decl.propertyName : decl.name).text;
return {from, name};
}
isClass(node: ts.Node): node is ts.Declaration { return ts.isClassDeclaration(node); }
private _reflectDecorator(node: ts.Decorator): Decorator|null {
// Attempt to resolve the decorator expression into a reference to a concrete Identifier. The
// expression may contain a call to a function which returns the decorator function, in which
// case we want to return the arguments.
let decoratorExpr: ts.Expression = node.expression;
let args: ts.Expression[]|null = null;
// Check for call expressions.
if (ts.isCallExpression(decoratorExpr)) {
args = Array.from(decoratorExpr.arguments);
decoratorExpr = decoratorExpr.expression;
}
// The final resolved decorator should be a `ts.Identifier` - if it's not, then something is
// wrong and the decorator can't be resolved statically.
if (!ts.isIdentifier(decoratorExpr)) {
return null;
}
const importDecl = this.getImportOfIdentifier(decoratorExpr);
return {
name: decoratorExpr.text,
import: importDecl, node, args,
};
}
private _reflectMember(node: ts.ClassElement): ClassMember|null {
let kind: ClassMemberKind|null = null;
let initializer: ts.Expression|null = null;
let name: string|null = null;
let nameNode: ts.Identifier|null = null;
if (ts.isPropertyDeclaration(node)) {
kind = ClassMemberKind.Property;
initializer = node.initializer || null;
} else if (ts.isGetAccessorDeclaration(node)) {
kind = ClassMemberKind.Getter;
} else if (ts.isSetAccessorDeclaration(node)) {
kind = ClassMemberKind.Setter;
} else if (ts.isMethodDeclaration(node)) {
kind = ClassMemberKind.Method;
} else if (ts.isConstructorDeclaration(node)) {
kind = ClassMemberKind.Constructor;
} else {
return;
return null;
}
});
return map;
}
export function reflectImportedIdentifier(
id: ts.Identifier, checker: ts.TypeChecker): {name: string, from: string}|null {
const symbol = checker.getSymbolAtLocation(id);
if (ts.isConstructorDeclaration(node)) {
name = 'constructor';
} else if (ts.isIdentifier(node.name)) {
name = node.name.text;
nameNode = node.name;
} else {
return null;
}
if (symbol === undefined || symbol.declarations === undefined ||
symbol.declarations.length !== 1) {
return null;
const decorators = this.getDecoratorsOfDeclaration(node);
const isStatic = node.modifiers !== undefined &&
node.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword);
return {
node,
kind,
type: node.type || null, name, nameNode, decorators, initializer, isStatic,
};
}
}
// Ignore decorators that are defined locally (not imported).
const decl: ts.Declaration = symbol.declarations[0];
if (!ts.isImportSpecifier(decl)) {
return null;
export function reflectNameOfDeclaration(decl: ts.Declaration): string|null {
const id = reflectIdentifierOfDeclaration(decl);
return id && id.text || null;
}
export function reflectIdentifierOfDeclaration(decl: ts.Declaration): ts.Identifier|null {
if (ts.isClassDeclaration(decl) || ts.isFunctionDeclaration(decl)) {
return decl.name || null;
} else if (ts.isVariableDeclaration(decl)) {
if (ts.isIdentifier(decl.name)) {
return decl.name;
}
}
// Walk back from the specifier to find the declaration, which carries the module specifier.
const importDecl = decl.parent !.parent !.parent !;
// The module specifier is guaranteed to be a string literal, so this should always pass.
if (!ts.isStringLiteral(importDecl.moduleSpecifier)) {
// Not allowed to happen in TypeScript ASTs.
return null;
}
// Read the module specifier.
const from = importDecl.moduleSpecifier.text;
// Compute the name by which the decorator was exported, not imported.
const name = (decl.propertyName !== undefined ? decl.propertyName : decl.name).text;
return {from, name};
}
export interface DecoratedNode<T extends ts.Node> {
element: T;
decorators: Decorator[];
}
export function getDecoratedClassElements(
clazz: ts.ClassDeclaration, checker: ts.TypeChecker): DecoratedNode<ts.ClassElement>[] {
const decoratedElements: DecoratedNode<ts.ClassElement>[] = [];
clazz.members.forEach(element => {
if (element.decorators !== undefined) {
const decorators = element.decorators.map(decorator => reflectDecorator(decorator, checker))
.filter(decorator => decorator != null) as Decorator[];
if (decorators.length > 0) {
decoratedElements.push({element, decorators});
}
}
});
return decoratedElements;
}
export function reflectStaticField(
clazz: ts.ClassDeclaration, field: string): ts.PropertyDeclaration|null {
return clazz.members.find((member: ts.ClassElement): member is ts.PropertyDeclaration => {
// Check if the name matches.
if (member.name === undefined || !ts.isIdentifier(member.name) || member.name.text !== field) {
return false;
}
// Check if the property is static.
if (member.modifiers === undefined ||
!member.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword)) {
return false;
}
// Found the field.
return true;
}) ||
null;
}
export function reflectNonStaticField(
clazz: ts.ClassDeclaration, field: string): ts.PropertyDeclaration|null {
return clazz.members.find((member: ts.ClassElement): member is ts.PropertyDeclaration => {
// Check if the name matches.
if (member.name === undefined || !ts.isIdentifier(member.name) || member.name.text !== field) {
return false;
}
// Check if the property is static.
if (member.modifiers !== undefined &&
member.modifiers.some(mod => mod.kind === ts.SyntaxKind.StaticKeyword)) {
return false;
}
// Found the field.
return true;
}) ||
null;
return null;
}
export function reflectTypeEntityToDeclaration(
@ -335,4 +233,95 @@ export function reflectTypeEntityToDeclaration(
} else {
return {node, from: null};
}
}
}
export function filterToMembersWithDecorator(members: ClassMember[], name: string, module?: string):
{member: ClassMember, decorators: Decorator[]}[] {
return members.filter(member => !member.isStatic)
.map(member => {
if (member.decorators === null) {
return null;
}
const decorators = member.decorators.filter(dec => {
if (dec.import !== null) {
return dec.import.name === name && (module === undefined || dec.import.from === module);
} else {
return dec.name === name && module === undefined;
}
});
if (decorators.length === 0) {
return null;
}
return {member, decorators};
})
.filter((value): value is {member: ClassMember, decorators: Decorator[]} => value !== null);
}
export function findMember(
members: ClassMember[], name: string, isStatic: boolean = false): ClassMember|null {
return members.find(member => member.isStatic === isStatic && member.name === name) || null;
}
export function reflectObjectLiteral(node: ts.ObjectLiteralExpression): Map<string, ts.Expression> {
const map = new Map<string, ts.Expression>();
node.properties.forEach(prop => {
if (ts.isPropertyAssignment(prop)) {
const name = propertyNameToString(prop.name);
if (name === null) {
return;
}
map.set(name, prop.initializer);
} else if (ts.isShorthandPropertyAssignment(prop)) {
map.set(prop.name.text, prop.name);
} else {
return;
}
});
return map;
}
function castDeclarationToClassOrDie(declaration: ts.Declaration): ts.ClassDeclaration {
if (!ts.isClassDeclaration(declaration)) {
throw new Error(
`Reflecting on a ${ts.SyntaxKind[declaration.kind]} instead of a ClassDeclaration.`);
}
return declaration;
}
function parameterName(name: ts.BindingName): string|null {
if (ts.isIdentifier(name)) {
return name.text;
} else {
return null;
}
}
function typeNodeToValueExpr(node: ts.TypeNode): ts.Expression|null {
if (ts.isTypeReferenceNode(node)) {
return entityNameToValue(node.typeName);
} else {
return null;
}
}
function entityNameToValue(node: ts.EntityName): ts.Expression|null {
if (ts.isQualifiedName(node)) {
const left = entityNameToValue(node.left);
return left !== null ? ts.createPropertyAccess(left, node.right) : null;
} else if (ts.isIdentifier(node)) {
return ts.getMutableClone(node);
} else {
return null;
}
}
function propertyNameToString(node: ts.PropertyName): string|null {
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
return node.text;
} else {
return null;
}
}

View File

@ -12,6 +12,7 @@ ts_library(
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/host",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/testing",
],

View File

@ -8,8 +8,9 @@
import * as ts from 'typescript';
import {Parameter} from '../../host';
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
import {Parameter, reflectConstructorParameters} from '../src/reflector';
import {TypeScriptReflectionHost} from '../src/reflector';
describe('reflector', () => {
describe('ctor params', () => {
@ -26,9 +27,10 @@ describe('reflector', () => {
}]);
const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration);
const checker = program.getTypeChecker();
const args = reflectConstructorParameters(clazz, checker) !;
const host = new TypeScriptReflectionHost(checker);
const args = host.getConstructorParameters(clazz) !;
expect(args.length).toBe(1);
expectArgument(args[0], 'bar', 'Bar');
expectParameter(args[0], 'bar', 'Bar');
});
it('should reflect a decorated argument', () => {
@ -54,9 +56,10 @@ describe('reflector', () => {
]);
const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration);
const checker = program.getTypeChecker();
const args = reflectConstructorParameters(clazz, checker) !;
const host = new TypeScriptReflectionHost(checker);
const args = host.getConstructorParameters(clazz) !;
expect(args.length).toBe(1);
expectArgument(args[0], 'bar', 'Bar', 'dec', './dec');
expectParameter(args[0], 'bar', 'Bar', 'dec', './dec');
});
it('should reflect a decorated argument with a call', () => {
@ -82,9 +85,10 @@ describe('reflector', () => {
]);
const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration);
const checker = program.getTypeChecker();
const args = reflectConstructorParameters(clazz, checker) !;
const host = new TypeScriptReflectionHost(checker);
const args = host.getConstructorParameters(clazz) !;
expect(args.length).toBe(1);
expectArgument(args[0], 'bar', 'Bar', 'dec', './dec');
expectParameter(args[0], 'bar', 'Bar', 'dec', './dec');
});
it('should reflect a decorated argument with an indirection', () => {
@ -109,26 +113,31 @@ describe('reflector', () => {
]);
const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration);
const checker = program.getTypeChecker();
const args = reflectConstructorParameters(clazz, checker) !;
const host = new TypeScriptReflectionHost(checker);
const args = host.getConstructorParameters(clazz) !;
expect(args.length).toBe(2);
expectArgument(args[0], 'bar', 'Bar');
expectArgument(args[1], 'otherBar', 'star.Bar');
expectParameter(args[0], 'bar', 'Bar');
expectParameter(args[1], 'otherBar', 'star.Bar');
});
});
});
function expectArgument(
arg: Parameter, name: string, type?: string, decorator?: string, decoratorFrom?: string): void {
expect(argExpressionToString(arg.name)).toEqual(name);
function expectParameter(
param: Parameter, name: string, type?: string, decorator?: string,
decoratorFrom?: string): void {
expect(param.name !).toEqual(name);
if (type === undefined) {
expect(arg.typeValueExpr).toBeNull();
expect(param.type).toBeNull();
} else {
expect(arg.typeValueExpr).not.toBeNull();
expect(argExpressionToString(arg.typeValueExpr !)).toEqual(type);
expect(param.type).not.toBeNull();
expect(argExpressionToString(param.type !)).toEqual(type);
}
if (decorator !== undefined) {
expect(arg.decorators.length).toBeGreaterThan(0);
expect(arg.decorators.some(dec => dec.name === decorator && dec.from === decoratorFrom))
expect(param.decorators).not.toBeNull();
expect(param.decorators !.length).toBeGreaterThan(0);
expect(param.decorators !.some(
dec => dec.name === decorator && dec.import !== null &&
dec.import.from === decoratorFrom))
.toBe(true);
}
}

View File

@ -14,6 +14,7 @@ import * as api from '../transformers/api';
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, SelectorScopeRegistry} from './annotations';
import {CompilerHost} from './compiler_host';
import {TypeScriptReflectionHost} from './metadata';
import {IvyCompilation, ivyTransformFactory} from './transform';
export class NgtscProgram implements api.Program {
@ -90,16 +91,17 @@ export class NgtscProgram implements api.Program {
const mergeEmitResultsCallback = opts && opts.mergeEmitResultsCallback || mergeEmitResults;
const checker = this.tsProgram.getTypeChecker();
const scopeRegistry = new SelectorScopeRegistry(checker);
const reflector = new TypeScriptReflectionHost(checker);
const scopeRegistry = new SelectorScopeRegistry(checker, reflector);
// Set up the IvyCompilation, which manages state for the Ivy transformer.
const handlers = [
new ComponentDecoratorHandler(checker, scopeRegistry),
new DirectiveDecoratorHandler(checker, scopeRegistry),
new InjectableDecoratorHandler(checker),
new ComponentDecoratorHandler(checker, reflector, scopeRegistry),
new DirectiveDecoratorHandler(checker, reflector, scopeRegistry),
new InjectableDecoratorHandler(reflector),
new NgModuleDecoratorHandler(checker, scopeRegistry),
];
const compilation = new IvyCompilation(handlers, checker);
const compilation = new IvyCompilation(handlers, checker, reflector);
// Analyze every source file in the program.
this.tsProgram.getSourceFiles()

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));

View File

@ -488,4 +488,4 @@ export function parseHostBindings(host: {[key: string]: string}): {
});
return {attributes, listeners, properties, animations};
}
}