feat(ivy): augment selector scopes to extract additional metadata (#26203)

Before type checking can be turned on in ngtsc, appropriate metadata for
each component and directive must be determined. This commit adds tracking
of the extra metadata in *DefWithMeta types to the selector scope handling,
allowing for later extraction for type-checking purposes.

PR Close #26203
This commit is contained in:
Alex Rickabaugh 2018-09-21 13:34:09 -07:00 committed by Jason Aden
parent 5f1273ba2e
commit 868047e87f
7 changed files with 191 additions and 64 deletions

View File

@ -15,5 +15,6 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/host",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/transform",
"//packages/compiler-cli/src/ngtsc/typecheck",
],
)

View File

@ -134,7 +134,14 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
// If the component has a selector, it should be registered with the `SelectorScopeRegistry` so
// when this component appears in an `@NgModule` scope, its selector can be determined.
if (metadata.selector !== null) {
this.scopeRegistry.registerSelector(node, metadata.selector);
this.scopeRegistry.registerDirective(node, {
selector: metadata.selector,
exportAs: metadata.exportAs,
inputs: metadata.inputs,
outputs: metadata.outputs,
queries: metadata.queries.map(query => query.propertyName),
isComponent: true,
});
}
// Construct the list of view queries.
@ -198,7 +205,9 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
// Replace the empty components and directives from the analyze() step with a fully expanded
// scope. This is possible now because during compile() the whole compilation unit has been
// fully analyzed.
const {directives, pipes, containsForwardDecls} = scope;
const {pipes, containsForwardDecls} = scope;
const directives = new Map<string, Expression>();
scope.directives.forEach((meta, selector) => directives.set(selector, meta.directive));
const wrapDirectivesInClosure: boolean = !!containsForwardDecls;
analysis = {...analysis, directives, pipes, wrapDirectivesInClosure};
}

View File

@ -40,7 +40,14 @@ export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMe
// 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.
if (analysis && analysis.selector !== null) {
this.scopeRegistry.registerSelector(node, analysis.selector);
this.scopeRegistry.registerDirective(node, {
selector: analysis.selector,
exportAs: analysis.exportAs,
inputs: analysis.inputs,
outputs: analysis.outputs,
queries: analysis.queries.map(query => query.propertyName),
isComponent: false,
});
}
return {analysis};

View File

@ -65,13 +65,13 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
}
// Extract the module declarations, imports, and exports.
let declarations: Reference[] = [];
let declarations: Reference<ts.Declaration>[] = [];
if (ngModule.has('declarations')) {
const expr = ngModule.get('declarations') !;
const declarationMeta = staticallyResolve(expr, this.reflector, this.checker);
declarations = this.resolveTypeList(expr, declarationMeta, 'declarations');
}
let imports: Reference[] = [];
let imports: Reference<ts.Declaration>[] = [];
if (ngModule.has('imports')) {
const expr = ngModule.get('imports') !;
const importsMeta = staticallyResolve(
@ -79,7 +79,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
ref => this._extractModuleFromModuleWithProvidersFn(ref.node));
imports = this.resolveTypeList(expr, importsMeta, 'imports');
}
let exports: Reference[] = [];
let exports: Reference<ts.Declaration>[] = [];
if (ngModule.has('exports')) {
const expr = ngModule.get('exports') !;
const exportsMeta = staticallyResolve(
@ -87,7 +87,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
ref => this._extractModuleFromModuleWithProvidersFn(ref.node));
exports = this.resolveTypeList(expr, exportsMeta, 'exports');
}
let bootstrap: Reference[] = [];
let bootstrap: Reference<ts.Declaration>[] = [];
if (ngModule.has('bootstrap')) {
const expr = ngModule.get('bootstrap') !;
const bootstrapMeta = staticallyResolve(expr, this.reflector, this.checker);
@ -198,8 +198,9 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
/**
* Compute a list of `Reference`s from a resolved metadata value.
*/
private resolveTypeList(expr: ts.Node, resolvedList: ResolvedValue, name: string): Reference[] {
const refList: Reference[] = [];
private resolveTypeList(expr: ts.Node, resolvedList: ResolvedValue, name: string):
Reference<ts.Declaration>[] {
const refList: Reference<ts.Declaration>[] = [];
if (!Array.isArray(resolvedList)) {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, expr, `Expected array when reading property ${name}`);
@ -215,7 +216,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
if (Array.isArray(entry)) {
// Recurse into nested arrays.
refList.push(...this.resolveTypeList(expr, entry, name));
} else if (entry instanceof Reference) {
} else if (isDeclarationReference(entry)) {
if (!entry.expressable) {
throw new FatalDiagnosticError(
ErrorCode.VALUE_HAS_WRONG_TYPE, expr, `One entry in ${name} is not a type`);
@ -234,3 +235,9 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
return refList;
}
}
function isDeclarationReference(ref: any): ref is Reference<ts.Declaration> {
return ref instanceof Reference &&
(ts.isClassDeclaration(ref.node) || ts.isFunctionDeclaration(ref.node) ||
ts.isVariableDeclaration(ref.node));
}

View File

@ -12,18 +12,18 @@ import * as ts from 'typescript';
import {ReflectionHost} from '../../host';
import {AbsoluteReference, Reference, ResolvedReference, reflectTypeEntityToDeclaration} from '../../metadata';
import {reflectIdentifierOfDeclaration, reflectNameOfDeclaration} from '../../metadata/src/reflector';
import {TypeCheckableDirectiveMeta} from '../../typecheck';
import {toR3Reference} from './util';
import {extractDirectiveGuards, toR3Reference} from './util';
/**
* Metadata extracted for a given NgModule that can be used to compute selector scopes.
*/
export interface ModuleData {
declarations: Reference[];
imports: Reference[];
exports: Reference[];
declarations: Reference<ts.Declaration>[];
imports: Reference<ts.Declaration>[];
exports: Reference<ts.Declaration>[];
}
/**
@ -31,11 +31,16 @@ export interface ModuleData {
* context of some module.
*/
export interface CompilationScope<T> {
directives: Map<string, T>;
directives: Map<string, ScopeDirective<T>>;
pipes: Map<string, T>;
containsForwardDecls?: boolean;
}
export interface ScopeDirective<T> extends TypeCheckableDirectiveMeta {
selector: string;
directive: T;
}
/**
* Both transitively expanded scopes for a given NgModule.
*/
@ -44,13 +49,13 @@ interface SelectorScopes {
* Set of components, directives, and pipes visible to all components being compiled in the
* context of some module.
*/
compilation: Reference[];
compilation: Reference<ts.Declaration>[];
/**
* Set of components, directives, and pipes added to the compilation scope of any module importing
* some module.
*/
exported: Reference[];
exported: Reference<ts.Declaration>[];
}
/**
@ -71,9 +76,9 @@ export class SelectorScopeRegistry {
private _compilationScopeCache = new Map<ts.Declaration, CompilationScope<Reference>>();
/**
* Map of components/directives to their selector.
* Map of components/directives to their metadata.
*/
private _directiveToSelector = new Map<ts.Declaration, string>();
private _directiveToMetadata = new Map<ts.Declaration, ScopeDirective<Reference>>();
/**
* Map of pipes to their name.
@ -105,15 +110,16 @@ export class SelectorScopeRegistry {
}
/**
* Register the selector of a component or directive with the registry.
* Register the metadata of a component or directive with the registry.
*/
registerSelector(node: ts.Declaration, selector: string): void {
registerDirective(node: ts.Declaration, metadata: ScopeDirective<Reference>): void {
node = ts.getOriginalNode(node) as ts.Declaration;
if (this._directiveToSelector.has(node)) {
throw new Error(`Selector already registered: ${reflectNameOfDeclaration(node)} ${selector}`);
if (this._directiveToMetadata.has(node)) {
throw new Error(
`Selector already registered: ${reflectNameOfDeclaration(node)} ${metadata.selector}`);
}
this._directiveToSelector.set(node, selector);
this._directiveToMetadata.set(node, metadata);
}
/**
@ -125,11 +131,7 @@ export class SelectorScopeRegistry {
this._pipeToName.set(node, name);
}
/**
* Produce the compilation scope of a component, which is determined by the module that declares
* it.
*/
lookupCompilationScope(node: ts.Declaration): CompilationScope<Expression>|null {
lookupCompilationScopeAsRefs(node: ts.Declaration): CompilationScope<Reference>|null {
node = ts.getOriginalNode(node) as ts.Declaration;
// If the component has no associated module, then it has no compilation scope.
@ -147,11 +149,11 @@ export class SelectorScopeRegistry {
// The scope as cached is in terms of References, not Expressions. Converting between them
// requires knowledge of the context file (in this case, the component node's source file).
return convertScopeToExpressions(scope, node);
return scope;
}
// This is the first time the scope for this module is being computed.
const directives = new Map<string, Reference>();
const directives = new Map<string, ScopeDirective<Reference<ts.Declaration>>>();
const pipes = new Map<string, Reference>();
// Process the declaration scope of the module, and lookup the selector of every declared type.
@ -161,10 +163,10 @@ export class SelectorScopeRegistry {
const node = ts.getOriginalNode(ref.node) as ts.Declaration;
// Either the node represents a directive or a pipe. Look for both.
const selector = this.lookupDirectiveSelector(node);
const metadata = this.lookupDirectiveMetadata(ref);
// Only directives/components with selectors get added to the scope.
if (selector != null) {
directives.set(selector, ref);
if (metadata != null) {
directives.set(metadata.selector, {...metadata, directive: ref});
return;
}
@ -180,7 +182,16 @@ export class SelectorScopeRegistry {
this._compilationScopeCache.set(node, scope);
// Convert References to Expressions in the context of the component's source file.
return convertScopeToExpressions(scope, node);
return scope;
}
/**
* Produce the compilation scope of a component, which is determined by the module that declares
* it.
*/
lookupCompilationScope(node: ts.Declaration): CompilationScope<Expression>|null {
const scope = this.lookupCompilationScopeAsRefs(node);
return scope !== null ? convertScopeToExpressions(scope, node) : null;
}
private lookupScopesOrDie(node: ts.Declaration, ngModuleImportedFrom: string|null):
@ -210,7 +221,7 @@ export class SelectorScopeRegistry {
} else {
// The module wasn't analyzed before, and probably has a precompiled ngModuleDef with a type
// annotation that specifies the needed metadata.
data = this._readMetadataFromCompiledClass(node, ngModuleImportedFrom);
data = this._readModuleDataFromCompiledClass(node, ngModuleImportedFrom);
// Note that data here could still be null, if the class didn't have a precompiled
// ngModuleDef.
}
@ -245,17 +256,18 @@ export class SelectorScopeRegistry {
}
/**
* Lookup the selector of a component or directive class.
* Lookup the metadata of a component or directive class.
*
* Potentially this class is declared in a .d.ts file or otherwise has a manually created
* ngComponentDef/ngDirectiveDef. In this case, the type metadata of that definition is read
* to determine the selector.
* to determine the metadata.
*/
private lookupDirectiveSelector(node: ts.Declaration): string|null {
if (this._directiveToSelector.has(node)) {
return this._directiveToSelector.get(node) !;
private lookupDirectiveMetadata(ref: Reference<ts.Declaration>): ScopeDirective<Reference>|null {
const node = ts.getOriginalNode(ref.node) as ts.Declaration;
if (this._directiveToMetadata.has(node)) {
return this._directiveToMetadata.get(node) !;
} else {
return this._readSelectorFromCompiledClass(node);
return this._readMetadataFromCompiledClass(ref as Reference<ts.ClassDeclaration>);
}
}
@ -275,8 +287,8 @@ export class SelectorScopeRegistry {
* @param ngModuleImportedFrom module specifier of the import path to assume for all declarations
* stemming from this module.
*/
private _readMetadataFromCompiledClass(clazz: ts.Declaration, ngModuleImportedFrom: string|null):
ModuleData|null {
private _readModuleDataFromCompiledClass(
clazz: ts.Declaration, ngModuleImportedFrom: string|null): ModuleData|null {
// This operation is explicitly not memoized, as it depends on `ngModuleImportedFrom`.
// TODO(alxhub): investigate caching of .d.ts module metadata.
const ngModuleDef = this.reflector.getMembersOfClass(clazz).find(
@ -304,7 +316,9 @@ export class SelectorScopeRegistry {
* Get the selector from type metadata for a class with a precompiled ngComponentDef or
* ngDirectiveDef.
*/
private _readSelectorFromCompiledClass(clazz: ts.Declaration): string|null {
private _readMetadataFromCompiledClass(ref: Reference<ts.ClassDeclaration>):
ScopeDirective<Reference>|null {
const clazz = ts.getOriginalNode(ref.node) as ts.ClassDeclaration;
const def = this.reflector.getMembersOfClass(clazz).find(
field =>
field.isStatic && (field.name === 'ngComponentDef' || field.name === 'ngDirectiveDef'));
@ -317,12 +331,22 @@ export class SelectorScopeRegistry {
// The type metadata was the wrong shape.
return null;
}
const type = def.type.typeArguments[1];
if (!ts.isLiteralTypeNode(type) || !ts.isStringLiteral(type.literal)) {
// The type metadata was the wrong type.
const selector = readStringType(def.type.typeArguments[1]);
if (selector === null) {
return null;
}
return type.literal.text;
return {
ref,
name: clazz.name !.text,
directive: ref,
isComponent: def.name === 'ngComponentDef', selector,
exportAs: readStringType(def.type.typeArguments[2]),
inputs: readStringMapType(def.type.typeArguments[3]),
outputs: readStringMapType(def.type.typeArguments[4]),
queries: readStringArrayType(def.type.typeArguments[5]),
...extractDirectiveGuards(clazz, this.reflector),
};
}
/**
@ -357,7 +381,7 @@ export class SelectorScopeRegistry {
* they themselves were imported from another absolute path.
*/
private _extractReferencesFromType(def: ts.TypeNode, ngModuleImportedFrom: string|null):
Reference[] {
Reference<ts.Declaration>[] {
if (!ts.isTupleTypeNode(def)) {
return [];
}
@ -394,23 +418,33 @@ function absoluteModuleName(ref: Reference): string|null {
return ref.moduleName;
}
function convertReferenceMap(
function convertDirectiveReferenceMap(
map: Map<string, ScopeDirective<Reference>>,
context: ts.SourceFile): Map<string, ScopeDirective<Expression>> {
const newMap = new Map<string, ScopeDirective<Expression>>();
map.forEach((meta, selector) => {
newMap.set(selector, {...meta, directive: toR3Reference(meta.directive, context).value});
});
return newMap;
}
function convertPipeReferenceMap(
map: Map<string, Reference>, context: ts.SourceFile): Map<string, Expression> {
return new Map<string, Expression>(Array.from(map.entries()).map(([selector, ref]): [
string, Expression
] => [selector, toR3Reference(ref, context).value]));
const newMap = new Map<string, Expression>();
map.forEach((meta, selector) => { newMap.set(selector, toR3Reference(meta, context).value); });
return newMap;
}
function convertScopeToExpressions(
scope: CompilationScope<Reference>, context: ts.Declaration): CompilationScope<Expression> {
const sourceContext = ts.getOriginalNode(context).getSourceFile();
const directives = convertReferenceMap(scope.directives, sourceContext);
const pipes = convertReferenceMap(scope.pipes, sourceContext);
const directives = convertDirectiveReferenceMap(scope.directives, sourceContext);
const pipes = convertPipeReferenceMap(scope.pipes, sourceContext);
const declPointer = maybeUnwrapNameOfDeclaration(context);
let containsForwardDecls = false;
directives.forEach(expr => {
containsForwardDecls =
containsForwardDecls || isExpressionForwardReference(expr, declPointer, sourceContext);
containsForwardDecls = containsForwardDecls ||
isExpressionForwardReference(expr.directive, declPointer, sourceContext);
});
!containsForwardDecls && pipes.forEach(expr => {
containsForwardDecls =
@ -438,4 +472,44 @@ function maybeUnwrapNameOfDeclaration(decl: ts.Declaration): ts.Declaration|ts.I
return decl.name;
}
return decl;
}
}
function readStringType(type: ts.TypeNode): string|null {
if (!ts.isLiteralTypeNode(type) || !ts.isStringLiteral(type.literal)) {
return null;
}
return type.literal.text;
}
function readStringMapType(type: ts.TypeNode): {[key: string]: string} {
if (!ts.isTypeLiteralNode(type)) {
return {};
}
const obj: {[key: string]: string} = {};
type.members.forEach(member => {
if (!ts.isPropertySignature(member) || member.type === undefined || member.name === undefined ||
!ts.isStringLiteral(member.name)) {
return;
}
const value = readStringType(member.type);
if (value === null) {
return null;
}
obj[member.name.text] = value;
});
return obj;
}
function readStringArrayType(type: ts.TypeNode): string[] {
if (!ts.isTupleTypeNode(type)) {
return [];
}
const res: string[] = [];
type.elementTypes.forEach(el => {
if (!ts.isLiteralTypeNode(el) || !ts.isStringLiteral(el.literal)) {
return;
}
res.push(el.literal.text);
});
return res;
}

View File

@ -63,6 +63,8 @@ describe('SelectorScopeRegistry', () => {
expect(ProgramModule).toBeDefined();
expect(SomeModule).toBeDefined();
const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !);
const registry = new SelectorScopeRegistry(checker, host);
registry.registerModule(ProgramModule, {
@ -71,7 +73,20 @@ describe('SelectorScopeRegistry', () => {
imports: [new AbsoluteReference(SomeModule, SomeModule.name !, 'some_library', 'SomeModule')],
});
registry.registerSelector(ProgramCmp, 'program-cmp');
const ref = new ResolvedReference(ProgramCmp, ProgramCmp.name !);
registry.registerDirective(ProgramCmp, {
name: 'ProgramCmp',
ref: ProgramCmpRef,
directive: ProgramCmpRef,
selector: 'program-cmp',
isComponent: true,
exportAs: null,
inputs: {},
outputs: {},
queries: [],
hasNgTemplateContextGuard: false,
ngTemplateGuards: [],
});
const scope = registry.lookupCompilationScope(ProgramCmp) !;
expect(scope).toBeDefined();
@ -120,6 +135,8 @@ describe('SelectorScopeRegistry', () => {
expect(ProgramModule).toBeDefined();
expect(SomeModule).toBeDefined();
const ProgramCmpRef = new ResolvedReference(ProgramCmp, ProgramCmp.name !);
const registry = new SelectorScopeRegistry(checker, host);
registry.registerModule(ProgramModule, {
@ -128,7 +145,19 @@ describe('SelectorScopeRegistry', () => {
imports: [],
});
registry.registerSelector(ProgramCmp, 'program-cmp');
registry.registerDirective(ProgramCmp, {
name: 'ProgramCmp',
ref: ProgramCmpRef,
directive: ProgramCmpRef,
selector: 'program-cmp',
isComponent: true,
exportAs: null,
inputs: {},
outputs: {},
queries: [],
hasNgTemplateContextGuard: false,
ngTemplateGuards: [],
});
const scope = registry.lookupCompilationScope(ProgramCmp) !;
expect(scope).toBeDefined();

View File

@ -183,10 +183,10 @@ export class ResolvedReference<T extends ts.Node = ts.Node> extends Reference<T>
* An `AbsoluteReference` can be resolved to an `Expression`, and if that expression is an import
* the module specifier will be an absolute module name, not a relative path.
*/
export class AbsoluteReference extends Reference {
export class AbsoluteReference<T extends ts.Node> extends Reference<T> {
private identifiers: ts.Identifier[] = [];
constructor(
node: ts.Node, private primaryIdentifier: ts.Identifier, readonly moduleName: string,
node: T, private primaryIdentifier: ts.Identifier, readonly moduleName: string,
readonly symbolName: string) {
super(node);
}