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:

committed by
Miško Hevery

parent
84272e2227
commit
10da6a45c6
@ -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",
|
||||
],
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -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.`);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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';
|
||||
}
|
||||
|
@ -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 !)],
|
||||
|
Reference in New Issue
Block a user