diff --git a/packages/compiler-cli/BUILD.bazel b/packages/compiler-cli/BUILD.bazel index d6aed05f7a..4ceb6afa7b 100644 --- a/packages/compiler-cli/BUILD.bazel +++ b/packages/compiler-cli/BUILD.bazel @@ -29,6 +29,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/entry_point", "//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/incremental", + "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/perf", diff --git a/packages/compiler-cli/ngcc/BUILD.bazel b/packages/compiler-cli/ngcc/BUILD.bazel index 60a07b34a0..5084e4988a 100644 --- a/packages/compiler-cli/ngcc/BUILD.bazel +++ b/packages/compiler-cli/ngcc/BUILD.bazel @@ -14,6 +14,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/annotations", "//packages/compiler-cli/src/ngtsc/cycles", "//packages/compiler-cli/src/ngtsc/imports", + "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/perf", diff --git a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts index 9f9be2ae98..4713df1078 100644 --- a/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts @@ -14,6 +14,7 @@ import * as ts from 'typescript'; import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations'; import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles'; import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../../src/ngtsc/imports'; +import {CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry} from '../../../src/ngtsc/metadata'; import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator'; import {AbsoluteFsPath, LogicalFileSystem} from '../../../src/ngtsc/path'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope'; @@ -65,6 +66,8 @@ class NgccResourceLoader implements ResourceLoader { */ export class DecorationAnalyzer { resourceManager = new NgccResourceLoader(); + metaRegistry = new LocalMetadataRegistry(); + dtsMetaReader = new DtsMetadataReader(this.typeChecker, this.reflectionHost); refEmitter = new ReferenceEmitter([ new LocalIdentifierStrategy(), new AbsoluteModuleStrategy(this.program, this.typeChecker, this.options, this.host), @@ -73,10 +76,11 @@ export class DecorationAnalyzer { // on whether a bestGuessOwningModule is present in the Reference. new LogicalProjectStrategy(this.typeChecker, new LogicalFileSystem(this.rootDirs)), ]); - dtsModuleScopeResolver = new MetadataDtsModuleScopeResolver( - this.typeChecker, this.reflectionHost, /* aliasGenerator */ null); + dtsModuleScopeResolver = + new MetadataDtsModuleScopeResolver(this.dtsMetaReader, /* aliasGenerator */ null); scopeRegistry = new LocalModuleScopeRegistry( - this.dtsModuleScopeResolver, this.refEmitter, /* aliasGenerator */ null); + this.metaRegistry, this.dtsModuleScopeResolver, this.refEmitter, /* aliasGenerator */ null); + fullRegistry = new CompoundMetadataRegistry([this.metaRegistry, this.scopeRegistry]); evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker); moduleResolver = new ModuleResolver(this.program, this.options, this.host); importGraph = new ImportGraph(this.moduleResolver); @@ -84,20 +88,22 @@ export class DecorationAnalyzer { handlers: DecoratorHandler[] = [ new BaseDefDecoratorHandler(this.reflectionHost, this.evaluator, this.isCore), new ComponentDecoratorHandler( - this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore, this.resourceManager, - this.rootDirs, /* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true, - this.moduleResolver, this.cycleAnalyzer, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER), + this.reflectionHost, this.evaluator, this.fullRegistry, this.scopeRegistry, this.isCore, + this.resourceManager, this.rootDirs, /* defaultPreserveWhitespaces */ false, + /* i18nUseExternalIds */ true, this.moduleResolver, this.cycleAnalyzer, this.refEmitter, + NOOP_DEFAULT_IMPORT_RECORDER), new DirectiveDecoratorHandler( - this.reflectionHost, this.evaluator, this.scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER, + this.reflectionHost, this.evaluator, this.fullRegistry, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore), new InjectableDecoratorHandler( this.reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore, /* strictCtorDeps */ false), new NgModuleDecoratorHandler( - this.reflectionHost, this.evaluator, this.scopeRegistry, this.referencesRegistry, - this.isCore, /* routeAnalyzer */ null, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER), + this.reflectionHost, this.evaluator, this.fullRegistry, this.scopeRegistry, + this.referencesRegistry, this.isCore, /* routeAnalyzer */ null, this.refEmitter, + NOOP_DEFAULT_IMPORT_RECORDER), new PipeDecoratorHandler( - this.reflectionHost, this.evaluator, this.scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER, + this.reflectionHost, this.evaluator, this.metaRegistry, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore), ]; diff --git a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel index 139a1db6a3..2b33edbd71 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel @@ -12,6 +12,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/cycles", "//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/imports", + "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/routing", diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 6504c29238..efa16911df 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -13,9 +13,10 @@ import * as ts from 'typescript'; import {CycleAnalyzer} from '../../cycles'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports'; +import {DirectiveMeta, MetadataRegistry, extractDirectiveGuards} from '../../metadata'; import {EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {ClassDeclaration, Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection'; -import {LocalModuleScopeRegistry, ScopeDirective, extractDirectiveGuards} from '../../scope'; +import {LocalModuleScopeRegistry} from '../../scope'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence, ResolveResult} from '../../transform'; import {TypeCheckContext} from '../../typecheck'; import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300'; @@ -41,14 +42,14 @@ export class ComponentDecoratorHandler implements DecoratorHandler { constructor( private reflector: ReflectionHost, private evaluator: PartialEvaluator, - private scopeRegistry: LocalModuleScopeRegistry, private isCore: boolean, - private resourceLoader: ResourceLoader, private rootDirs: string[], + private metaRegistry: MetadataRegistry, private scopeRegistry: LocalModuleScopeRegistry, + private isCore: boolean, private resourceLoader: ResourceLoader, private rootDirs: string[], private defaultPreserveWhitespaces: boolean, private i18nUseExternalIds: boolean, private moduleResolver: ModuleResolver, private cycleAnalyzer: CycleAnalyzer, private refEmitter: ReferenceEmitter, private defaultImportRecorder: DefaultImportRecorder) {} private literalCache = new Map(); - private boundTemplateCache = new Map>(); + private boundTemplateCache = new Map>(); private elementSchemaRegistry = new DomElementSchemaRegistry(); /** @@ -211,7 +212,7 @@ export class ComponentDecoratorHandler implements // determined. if (metadata.selector !== null) { const ref = new Reference(node); - this.scopeRegistry.registerDirective({ + this.metaRegistry.registerDirectiveMetadata({ ref, name: node.name.text, selector: metadata.selector, @@ -220,6 +221,7 @@ export class ComponentDecoratorHandler implements outputs: metadata.outputs, queries: metadata.queries.map(query => query.propertyName), isComponent: true, ...extractDirectiveGuards(node, this.reflector), + baseClass: null, }); } @@ -302,7 +304,7 @@ export class ComponentDecoratorHandler implements return; } const scope = this.scopeRegistry.getScopeForComponent(node); - const matcher = new SelectorMatcher(); + const matcher = new SelectorMatcher(); if (scope !== null) { for (const meta of scope.compilation.directives) { matcher.addSelectables(CssSelector.parse(meta.selector), meta); @@ -343,7 +345,7 @@ export class ComponentDecoratorHandler implements // Set up the R3TargetBinder, as well as a 'directives' array and a 'pipes' map that are later // fed to the TemplateDefinitionBuilder. First, a SelectorMatcher is constructed to match // directives that are in scope. - const matcher = new SelectorMatcher(); + const matcher = new SelectorMatcher(); const directives: {selector: string, expression: Expression}[] = []; for (const dir of scope.compilation.directives) { diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index 5afef7c479..938fff0287 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -11,10 +11,11 @@ import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {DefaultImportRecorder, Reference} from '../../imports'; +import {MetadataRegistry} from '../../metadata'; +import {extractDirectiveGuards} from '../../metadata/src/util'; import {DynamicValue, EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection'; import {LocalModuleScopeRegistry} from '../../scope/src/local'; -import {extractDirectiveGuards} from '../../scope/src/util'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; import {generateSetClassMetadataCall} from './metadata'; @@ -30,8 +31,8 @@ export class DirectiveDecoratorHandler implements DecoratorHandler { constructor( private reflector: ReflectionHost, private evaluator: PartialEvaluator, - private scopeRegistry: LocalModuleScopeRegistry, - private defaultImportRecorder: DefaultImportRecorder, private isCore: boolean) {} + private metaRegistry: MetadataRegistry, private defaultImportRecorder: DefaultImportRecorder, + private isCore: boolean) {} readonly precedence = HandlerPrecedence.PRIMARY; @@ -59,7 +60,7 @@ export class DirectiveDecoratorHandler implements // when this directive appears in an `@NgModule` scope, its selector can be determined. if (analysis && analysis.selector !== null) { const ref = new Reference(node); - this.scopeRegistry.registerDirective({ + this.metaRegistry.registerDirectiveMetadata({ ref, name: node.name.text, selector: analysis.selector, @@ -68,6 +69,7 @@ export class DirectiveDecoratorHandler implements outputs: analysis.outputs, queries: analysis.queries.map(query => query.propertyName), isComponent: false, ...extractDirectiveGuards(node, this.reflector), + baseClass: null, }); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index e558ed578a..97691411bc 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -11,6 +11,7 @@ import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {DefaultImportRecorder, Reference, ReferenceEmitter} from '../../imports'; +import {MetadataRegistry} from '../../metadata'; import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection'; import {NgModuleRouteAnalyzer} from '../../routing'; @@ -39,7 +40,7 @@ export interface NgModuleAnalysis { export class NgModuleDecoratorHandler implements DecoratorHandler { constructor( private reflector: ReflectionHost, private evaluator: PartialEvaluator, - private scopeRegistry: LocalModuleScopeRegistry, + private metaRegistry: MetadataRegistry, private scopeRegistry: LocalModuleScopeRegistry, private referencesRegistry: ReferencesRegistry, private isCore: boolean, private routeAnalyzer: NgModuleRouteAnalyzer|null, private refEmitter: ReferenceEmitter, private defaultImportRecorder: DefaultImportRecorder) {} @@ -134,8 +135,12 @@ export class NgModuleDecoratorHandler implements DecoratorHandler { constructor( private reflector: ReflectionHost, private evaluator: PartialEvaluator, - private scopeRegistry: LocalModuleScopeRegistry, - private defaultImportRecorder: DefaultImportRecorder, private isCore: boolean) {} + private metaRegistry: MetadataRegistry, private defaultImportRecorder: DefaultImportRecorder, + private isCore: boolean) {} readonly precedence = HandlerPrecedence.PRIMARY; @@ -76,7 +77,7 @@ export class PipeDecoratorHandler implements DecoratorHandler { const moduleResolver = new ModuleResolver(program, options, host); const importGraph = new ImportGraph(moduleResolver); const cycleAnalyzer = new CycleAnalyzer(importGraph); + const metaRegistry = new LocalMetadataRegistry(); + const dtsReader = new DtsMetadataReader(checker, reflectionHost); const scopeRegistry = new LocalModuleScopeRegistry( - new MetadataDtsModuleScopeResolver(checker, reflectionHost, null), new ReferenceEmitter([]), + metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]), null); const refEmitter = new ReferenceEmitter([]); const handler = new ComponentDecoratorHandler( - reflectionHost, evaluator, scopeRegistry, false, new NoopResourceLoader(), [''], false, - true, moduleResolver, cycleAnalyzer, refEmitter, NOOP_DEFAULT_IMPORT_RECORDER); + reflectionHost, evaluator, metaRegistry, scopeRegistry, false, new NoopResourceLoader(), + [''], false, true, moduleResolver, cycleAnalyzer, refEmitter, NOOP_DEFAULT_IMPORT_RECORDER); const TestCmp = getDeclaration(program, 'entry.ts', 'TestCmp', isNamedClassDeclaration); const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp)); if (detected === undefined) { diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts index a82dd30c52..a046fd5456 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts @@ -9,6 +9,7 @@ import * as ts from 'typescript'; import {NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports'; +import {DtsMetadataReader, LocalMetadataRegistry} from '../../metadata'; import {PartialEvaluator} from '../../partial_evaluator'; import {ClassDeclaration, TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; @@ -40,8 +41,10 @@ describe('DirectiveDecoratorHandler', () => { const checker = program.getTypeChecker(); const reflectionHost = new TestReflectionHost(checker); const evaluator = new PartialEvaluator(reflectionHost, checker); + const metaReader = new LocalMetadataRegistry(); + const dtsReader = new DtsMetadataReader(checker, reflectionHost); const scopeRegistry = new LocalModuleScopeRegistry( - new MetadataDtsModuleScopeResolver(checker, reflectionHost, null), new ReferenceEmitter([]), + metaReader, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]), null); const handler = new DirectiveDecoratorHandler( reflectionHost, evaluator, scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER, false); diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts index 213ead9276..6662c645ba 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/ng_module_spec.ts @@ -11,6 +11,7 @@ import {R3Reference} from '@angular/compiler/src/compiler'; import * as ts from 'typescript'; import {LocalIdentifierStrategy, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../imports'; +import {DtsMetadataReader, LocalMetadataRegistry} from '../../metadata'; import {PartialEvaluator} from '../../partial_evaluator'; import {TypeScriptReflectionHost, isNamedClassDeclaration} from '../../reflection'; import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; @@ -55,14 +56,16 @@ describe('NgModuleDecoratorHandler', () => { const reflectionHost = new TypeScriptReflectionHost(checker); const evaluator = new PartialEvaluator(reflectionHost, checker); const referencesRegistry = new NoopReferencesRegistry(); + const metaRegistry = new LocalMetadataRegistry(); + const dtsReader = new DtsMetadataReader(checker, reflectionHost); const scopeRegistry = new LocalModuleScopeRegistry( - new MetadataDtsModuleScopeResolver(checker, reflectionHost, null), new ReferenceEmitter([]), + metaRegistry, new MetadataDtsModuleScopeResolver(dtsReader, null), new ReferenceEmitter([]), null); const refEmitter = new ReferenceEmitter([new LocalIdentifierStrategy()]); const handler = new NgModuleDecoratorHandler( - reflectionHost, evaluator, scopeRegistry, referencesRegistry, false, null, refEmitter, - NOOP_DEFAULT_IMPORT_RECORDER); + reflectionHost, evaluator, metaRegistry, scopeRegistry, referencesRegistry, false, null, + refEmitter, NOOP_DEFAULT_IMPORT_RECORDER); const TestModule = getDeclaration(program, 'entry.ts', 'TestModule', isNamedClassDeclaration); const detected = handler.detect(TestModule, reflectionHost.getDecoratorsOfDeclaration(TestModule)); diff --git a/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel b/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel new file mode 100644 index 0000000000..6c36eb70d3 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel @@ -0,0 +1,17 @@ +load("//tools:defaults.bzl", "ts_library") + +package(default_visibility = ["//visibility:public"]) + +ts_library( + name = "metadata", + srcs = ["index.ts"] + glob([ + "src/**/*.ts", + ]), + deps = [ + "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/imports", + "//packages/compiler-cli/src/ngtsc/reflection", + "//packages/compiler-cli/src/ngtsc/util", + "@npm//typescript", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/metadata/index.ts b/packages/compiler-cli/src/ngtsc/metadata/index.ts new file mode 100644 index 0000000000..4dc4c026f2 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/metadata/index.ts @@ -0,0 +1,12 @@ +/** + * @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/api'; +export {DtsMetadataReader} from './src/dts'; +export {CompoundMetadataRegistry, LocalMetadataRegistry} from './src/registry'; +export {extractDirectiveGuards} from './src/util'; diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/api.ts b/packages/compiler-cli/src/ngtsc/metadata/src/api.ts new file mode 100644 index 0000000000..66c9f9f737 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/metadata/src/api.ts @@ -0,0 +1,71 @@ +/** + * @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 {DirectiveMeta as T2DirectiveMeta} from '@angular/compiler'; + +import {Reference} from '../../imports'; +import {ClassDeclaration} from '../../reflection'; + +/** + * Metadata collected for an `NgModule`. + */ +export interface NgModuleMeta { + ref: Reference; + declarations: Reference[]; + imports: Reference[]; + exports: Reference[]; +} + +/** + * Metadata collected for a directive within an NgModule's scope. + */ +export interface DirectiveMeta extends T2DirectiveMeta { + ref: Reference; + /** + * Unparsed selector of the directive. + */ + selector: string; + queries: string[]; + ngTemplateGuards: string[]; + hasNgTemplateContextGuard: boolean; + + /** + * A `Reference` to the base class for the directive, if one was detected. + * + * A value of `'dynamic'` indicates that while the analyzer detected that this directive extends + * another type, it could not statically determine the base class. + */ + baseClass: Reference|'dynamic'|null; +} + +/** + * Metadata for a pipe within an NgModule's scope. + */ +export interface PipeMeta { + ref: Reference; + name: string; +} + +/** + * Reads metadata for directives, pipes, and modules from a particular source, such as .d.ts files + * or a registry. + */ +export interface MetadataReader { + getDirectiveMetadata(node: Reference): DirectiveMeta|null; + getNgModuleMetadata(node: Reference): NgModuleMeta|null; + getPipeMetadata(node: Reference): PipeMeta|null; +} + +/** + * Registers new metadata for directives, pipes, and modules. + */ +export interface MetadataRegistry { + registerDirectiveMetadata(meta: DirectiveMeta): void; + registerNgModuleMetadata(meta: NgModuleMeta): void; + registerPipeMetadata(meta: PipeMeta): void; +} diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts b/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts new file mode 100644 index 0000000000..92d38b482e --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/metadata/src/dts.ts @@ -0,0 +1,118 @@ +/** + * @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'; + +import {Reference} from '../../imports'; +import {ClassDeclaration, ReflectionHost} from '../../reflection'; + +import {DirectiveMeta, MetadataReader, NgModuleMeta, PipeMeta} from './api'; +import {extractDirectiveGuards, extractReferencesFromType, readStringArrayType, readStringMapType, readStringType} from './util'; + +/** + * A `MetadataReader` that can read metadata from `.d.ts` files, which have static Ivy properties + * from an upstream compilation already. + */ +export class DtsMetadataReader implements MetadataReader { + constructor(private checker: ts.TypeChecker, private reflector: ReflectionHost) {} + + /** + * Read the metadata from a class that has already been compiled somehow (either it's in a .d.ts + * file, or in a .ts file with a handwritten definition). + * + * @param ref `Reference` to the class of interest, with the context of how it was obtained. + */ + getNgModuleMetadata(ref: Reference): NgModuleMeta|null { + const clazz = ref.node; + const resolutionContext = clazz.getSourceFile().fileName; + // This operation is explicitly not memoized, as it depends on `ref.ownedByModuleGuess`. + // TODO(alxhub): investigate caching of .d.ts module metadata. + 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 === null || !ts.isTypeReferenceNode(ngModuleDef.type) || + ngModuleDef.type.typeArguments === undefined || + ngModuleDef.type.typeArguments.length !== 4) { + return null; + } + + // Read the ModuleData out of the type arguments. + const [_, declarationMetadata, importMetadata, exportMetadata] = ngModuleDef.type.typeArguments; + return { + ref, + declarations: extractReferencesFromType( + this.checker, declarationMetadata, ref.ownedByModuleGuess, resolutionContext), + exports: extractReferencesFromType( + this.checker, exportMetadata, ref.ownedByModuleGuess, resolutionContext), + imports: extractReferencesFromType( + this.checker, importMetadata, ref.ownedByModuleGuess, resolutionContext), + }; + } + + /** + * Read directive (or component) metadata from a referenced class in a .d.ts file. + */ + getDirectiveMetadata(ref: Reference): DirectiveMeta|null { + const clazz = ref.node; + 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 === null || !ts.isTypeReferenceNode(def.type) || + def.type.typeArguments === undefined || def.type.typeArguments.length < 2) { + // The type metadata was the wrong shape. + return null; + } + const selector = readStringType(def.type.typeArguments[1]); + if (selector === null) { + return null; + } + + return { + ref, + name: clazz.name.text, + isComponent: def.name === 'ngComponentDef', selector, + exportAs: readStringArrayType(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), + baseClass: null, + }; + } + + /** + * Read pipe metadata from a referenced class in a .d.ts file. + */ + getPipeMetadata(ref: Reference): PipeMeta|null { + const def = this.reflector.getMembersOfClass(ref.node).find( + field => field.isStatic && field.name === 'ngPipeDef'); + if (def === undefined) { + // No definition could be found. + return null; + } else if ( + 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; + } + const type = def.type.typeArguments[1]; + if (!ts.isLiteralTypeNode(type) || !ts.isStringLiteral(type.literal)) { + // The type metadata was the wrong type. + return null; + } + const name = type.literal.text; + return {ref, name}; + } +} diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/registry.ts b/packages/compiler-cli/src/ngtsc/metadata/src/registry.ts new file mode 100644 index 0000000000..f834218efd --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/metadata/src/registry.ts @@ -0,0 +1,61 @@ +/** + * @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 {Reference} from '../../imports'; +import {ClassDeclaration} from '../../reflection'; + +import {DirectiveMeta, MetadataReader, MetadataRegistry, NgModuleMeta, PipeMeta} from './api'; + +/** + * A registry of directive, pipe, and module metadata for types defined in the current compilation + * unit, which supports both reading and registering. + */ +export class LocalMetadataRegistry implements MetadataRegistry, MetadataReader { + private directives = new Map(); + private ngModules = new Map(); + private pipes = new Map(); + + getDirectiveMetadata(ref: Reference): DirectiveMeta|null { + return this.directives.has(ref.node) ? this.directives.get(ref.node) ! : null; + } + getNgModuleMetadata(ref: Reference): NgModuleMeta|null { + return this.ngModules.has(ref.node) ? this.ngModules.get(ref.node) ! : null; + } + getPipeMetadata(ref: Reference): PipeMeta|null { + return this.pipes.has(ref.node) ? this.pipes.get(ref.node) ! : null; + } + + registerDirectiveMetadata(meta: DirectiveMeta): void { this.directives.set(meta.ref.node, meta); } + registerNgModuleMetadata(meta: NgModuleMeta): void { this.ngModules.set(meta.ref.node, meta); } + registerPipeMetadata(meta: PipeMeta): void { this.pipes.set(meta.ref.node, meta); } +} + +/** + * A `MetadataRegistry` which registers metdata with multiple delegate `MetadataRegistry` instances. + */ +export class CompoundMetadataRegistry implements MetadataRegistry { + constructor(private registries: MetadataRegistry[]) {} + + registerDirectiveMetadata(meta: DirectiveMeta): void { + for (const registry of this.registries) { + registry.registerDirectiveMetadata(meta); + } + } + + registerNgModuleMetadata(meta: NgModuleMeta): void { + for (const registry of this.registries) { + registry.registerNgModuleMetadata(meta); + } + } + + registerPipeMetadata(meta: PipeMeta): void { + for (const registry of this.registries) { + registry.registerPipeMetadata(meta); + } + } +} diff --git a/packages/compiler-cli/src/ngtsc/scope/src/util.ts b/packages/compiler-cli/src/ngtsc/metadata/src/util.ts similarity index 100% rename from packages/compiler-cli/src/ngtsc/scope/src/util.ts rename to packages/compiler-cli/src/ngtsc/metadata/src/util.ts diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 2698cb4515..626c8a1ccb 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -19,6 +19,7 @@ import {ErrorCode, ngErrorCode} from './diagnostics'; import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point'; import {AbsoluteModuleStrategy, AliasGenerator, AliasStrategy, DefaultImportTracker, FileToModuleHost, FileToModuleStrategy, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports'; import {IncrementalState} from './incremental'; +import {CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry} from './metadata'; import {PartialEvaluator} from './partial_evaluator'; import {AbsoluteFsPath, LogicalFileSystem} from './path'; import {NOOP_PERF_RECORDER, PerfRecorder, PerfTracker} from './perf'; @@ -413,14 +414,18 @@ export class NgtscProgram implements api.Program { } const evaluator = new PartialEvaluator(this.reflector, checker); - const depScopeReader = - new MetadataDtsModuleScopeResolver(checker, this.reflector, aliasGenerator); - const scopeRegistry = - new LocalModuleScopeRegistry(depScopeReader, this.refEmitter, aliasGenerator); + const dtsReader = new DtsMetadataReader(checker, this.reflector); + const localMetaRegistry = new LocalMetadataRegistry(); + const depScopeReader = new MetadataDtsModuleScopeResolver(dtsReader, aliasGenerator); + const scopeRegistry = new LocalModuleScopeRegistry( + localMetaRegistry, depScopeReader, this.refEmitter, aliasGenerator); + const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, scopeRegistry]); - // If a flat module entrypoint was specified, then track references via a `ReferenceGraph` in - // order to produce proper diagnostics for incorrectly exported directives/pipes/etc. If there + // If a flat module entrypoint was specified, then track references via a `ReferenceGraph` + // in + // order to produce proper diagnostics for incorrectly exported directives/pipes/etc. If + // there // is no flat module entrypoint then don't pay the cost of tracking references. let referencesRegistry: ReferencesRegistry; if (this.entryPoint !== null) { @@ -436,20 +441,20 @@ export class NgtscProgram implements api.Program { const handlers = [ new BaseDefDecoratorHandler(this.reflector, evaluator, this.isCore), new ComponentDecoratorHandler( - this.reflector, evaluator, scopeRegistry, this.isCore, this.resourceManager, + this.reflector, evaluator, metaRegistry, scopeRegistry, this.isCore, this.resourceManager, this.rootDirs, this.options.preserveWhitespaces || false, this.options.i18nUseExternalIds !== false, this.moduleResolver, this.cycleAnalyzer, this.refEmitter, this.defaultImportTracker), new DirectiveDecoratorHandler( - this.reflector, evaluator, scopeRegistry, this.defaultImportTracker, this.isCore), + this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore), new InjectableDecoratorHandler( this.reflector, this.defaultImportTracker, this.isCore, this.options.strictInjectionParameters || false), new NgModuleDecoratorHandler( - this.reflector, evaluator, scopeRegistry, referencesRegistry, this.isCore, + this.reflector, evaluator, metaRegistry, scopeRegistry, referencesRegistry, this.isCore, this.routeAnalyzer, this.refEmitter, this.defaultImportTracker), new PipeDecoratorHandler( - this.reflector, evaluator, scopeRegistry, this.defaultImportTracker, this.isCore), + this.reflector, evaluator, metaRegistry, this.defaultImportTracker, this.isCore), ]; return new IvyCompilation( diff --git a/packages/compiler-cli/src/ngtsc/scope/BUILD.bazel b/packages/compiler-cli/src/ngtsc/scope/BUILD.bazel index 13584d2203..2c5854c872 100644 --- a/packages/compiler-cli/src/ngtsc/scope/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/scope/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( "//packages/compiler", "//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/imports", + "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/typecheck", "//packages/compiler-cli/src/ngtsc/util", diff --git a/packages/compiler-cli/src/ngtsc/scope/index.ts b/packages/compiler-cli/src/ngtsc/scope/index.ts index 30d09bc1a1..ddebbe51de 100644 --- a/packages/compiler-cli/src/ngtsc/scope/index.ts +++ b/packages/compiler-cli/src/ngtsc/scope/index.ts @@ -6,7 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -export {ExportScope, ScopeData, ScopeDirective, ScopePipe} from './src/api'; +export {ExportScope, ScopeData} from './src/api'; export {DtsModuleScopeResolver, MetadataDtsModuleScopeResolver} from './src/dependency'; export {LocalModuleScope, LocalModuleScopeRegistry, LocalNgModuleData} from './src/local'; -export {extractDirectiveGuards} from './src/util'; diff --git a/packages/compiler-cli/src/ngtsc/scope/src/api.ts b/packages/compiler-cli/src/ngtsc/scope/src/api.ts index 4a8a260197..774277345e 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/api.ts @@ -6,9 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {Reference} from '../../imports'; -import {ClassDeclaration} from '../../reflection'; -import {TypeCheckableDirectiveMeta} from '../../typecheck'; +import {DirectiveMeta, PipeMeta} from '../../metadata'; + /** * Data for one of a given NgModule's scopes (either compilation scope or export scopes). @@ -17,12 +16,12 @@ export interface ScopeData { /** * Directives in the exported scope of the module. */ - directives: ScopeDirective[]; + directives: DirectiveMeta[]; /** * Pipes in the exported scope of the module. */ - pipes: ScopePipe[]; + pipes: PipeMeta[]; } /** @@ -35,21 +34,3 @@ export interface ExportScope { */ exported: ScopeData; } - -/** - * Metadata for a given directive within an NgModule's scope. - */ -export interface ScopeDirective extends TypeCheckableDirectiveMeta { - /** - * Unparsed selector of the directive. - */ - selector: string; -} - -/** - * Metadata for a given pipe within an NgModule's scope. - */ -export interface ScopePipe { - ref: Reference; - name: string; -} diff --git a/packages/compiler-cli/src/ngtsc/scope/src/dependency.ts b/packages/compiler-cli/src/ngtsc/scope/src/dependency.ts index 8f40725dc7..245bdfc19e 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/dependency.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/dependency.ts @@ -9,10 +9,10 @@ import * as ts from 'typescript'; import {AliasGenerator, Reference} from '../../imports'; -import {ClassDeclaration, ReflectionHost} from '../../reflection'; +import {DirectiveMeta, MetadataReader, PipeMeta} from '../../metadata'; +import {ClassDeclaration} from '../../reflection'; -import {ExportScope, ScopeDirective, ScopePipe} from './api'; -import {extractDirectiveGuards, extractReferencesFromType, readStringArrayType, readStringMapType, readStringType} from './util'; +import {ExportScope} from './api'; export interface DtsModuleScopeResolver { resolve(ref: Reference): ExportScope|null; @@ -31,9 +31,10 @@ export class MetadataDtsModuleScopeResolver implements DtsModuleScopeResolver { */ private cache = new Map(); - constructor( - private checker: ts.TypeChecker, private reflector: ReflectionHost, - private aliasGenerator: AliasGenerator|null) {} + /** + * @param dtsMetaReader a `MetadataReader` which can read metadata from `.d.ts` files. + */ + constructor(private dtsMetaReader: MetadataReader, private aliasGenerator: AliasGenerator|null) {} /** * Resolve a `Reference`'d NgModule from a .d.ts file and produce a transitive `ExportScope` @@ -55,10 +56,10 @@ export class MetadataDtsModuleScopeResolver implements DtsModuleScopeResolver { } // Build up the export scope - those directives and pipes made visible by this module. - const directives: ScopeDirective[] = []; - const pipes: ScopePipe[] = []; + const directives: DirectiveMeta[] = []; + const pipes: PipeMeta[] = []; - const meta = this.readModuleMetadataFromClass(ref); + const meta = this.dtsMetaReader.getNgModuleMetadata(ref); if (meta === null) { this.cache.set(clazz, null); return null; @@ -73,7 +74,7 @@ export class MetadataDtsModuleScopeResolver implements DtsModuleScopeResolver { // don't affect the export scope. for (const exportRef of meta.exports) { // Attempt to process the export as a directive. - const directive = this.readScopeDirectiveFromClassWithDef(exportRef); + const directive = this.dtsMetaReader.getDirectiveMetadata(exportRef); if (directive !== null) { if (!declarations.has(exportRef.node)) { directives.push(this.maybeAlias(directive, sourceFile)); @@ -84,7 +85,7 @@ export class MetadataDtsModuleScopeResolver implements DtsModuleScopeResolver { } // Attempt to process the export as a pipe. - const pipe = this.readScopePipeFromClassWithDef(exportRef); + const pipe = this.dtsMetaReader.getPipeMetadata(exportRef); if (pipe !== null) { if (!declarations.has(exportRef.node)) { pipes.push(this.maybeAlias(pipe, sourceFile)); @@ -133,103 +134,8 @@ export class MetadataDtsModuleScopeResolver implements DtsModuleScopeResolver { }; } - /** - * Read the metadata from a class that has already been compiled somehow (either it's in a .d.ts - * file, or in a .ts file with a handwritten definition). - * - * @param ref `Reference` to the class of interest, with the context of how it was obtained. - */ - private readModuleMetadataFromClass(ref: Reference): RawDependencyMetadata - |null { - const clazz = ref.node; - const resolutionContext = clazz.getSourceFile().fileName; - // This operation is explicitly not memoized, as it depends on `ref.ownedByModuleGuess`. - // TODO(alxhub): investigate caching of .d.ts module metadata. - 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 === null || !ts.isTypeReferenceNode(ngModuleDef.type) || - ngModuleDef.type.typeArguments === undefined || - ngModuleDef.type.typeArguments.length !== 4) { - return null; - } - - // Read the ModuleData out of the type arguments. - const [_, declarationMetadata, importMetadata, exportMetadata] = ngModuleDef.type.typeArguments; - return { - declarations: extractReferencesFromType( - this.checker, declarationMetadata, ref.ownedByModuleGuess, resolutionContext), - exports: extractReferencesFromType( - this.checker, exportMetadata, ref.ownedByModuleGuess, resolutionContext), - imports: extractReferencesFromType( - this.checker, importMetadata, ref.ownedByModuleGuess, resolutionContext), - }; - } - - /** - * Read directive (or component) metadata from a referenced class in a .d.ts file. - */ - private readScopeDirectiveFromClassWithDef(ref: Reference): ScopeDirective - |null { - const clazz = ref.node; - 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 === null || !ts.isTypeReferenceNode(def.type) || - def.type.typeArguments === undefined || def.type.typeArguments.length < 2) { - // The type metadata was the wrong shape. - return null; - } - const selector = readStringType(def.type.typeArguments[1]); - if (selector === null) { - return null; - } - - return { - ref, - name: clazz.name.text, - isComponent: def.name === 'ngComponentDef', selector, - exportAs: readStringArrayType(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), - }; - } - - /** - * Read pipe metadata from a referenced class in a .d.ts file. - */ - private readScopePipeFromClassWithDef(ref: Reference): ScopePipe|null { - const def = this.reflector.getMembersOfClass(ref.node).find( - field => field.isStatic && field.name === 'ngPipeDef'); - if (def === undefined) { - // No definition could be found. - return null; - } else if ( - 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; - } - const type = def.type.typeArguments[1]; - if (!ts.isLiteralTypeNode(type) || !ts.isStringLiteral(type.literal)) { - // The type metadata was the wrong type. - return null; - } - const name = type.literal.text; - return {ref, name}; - } - - private maybeAlias( - dirOrPipe: T, maybeAliasFrom: ts.SourceFile): T { + private maybeAlias(dirOrPipe: T, maybeAliasFrom: ts.SourceFile): + T { if (this.aliasGenerator === null) { return dirOrPipe; } @@ -244,12 +150,3 @@ export class MetadataDtsModuleScopeResolver implements DtsModuleScopeResolver { } } } - -/** - * Raw metadata read from the .d.ts info of an ngModuleDef field on a compiled NgModule class. - */ -interface RawDependencyMetadata { - declarations: Reference[]; - imports: Reference[]; - exports: Reference[]; -} diff --git a/packages/compiler-cli/src/ngtsc/scope/src/local.ts b/packages/compiler-cli/src/ngtsc/scope/src/local.ts index a1ba8fd12e..504c1f3971 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/local.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/local.ts @@ -11,10 +11,11 @@ import * as ts from 'typescript'; import {ErrorCode, makeDiagnostic} from '../../diagnostics'; import {AliasGenerator, Reexport, Reference, ReferenceEmitter} from '../../imports'; +import {DirectiveMeta, LocalMetadataRegistry, MetadataReader, MetadataRegistry, NgModuleMeta, PipeMeta} from '../../metadata'; import {ClassDeclaration} from '../../reflection'; import {identifierOfNode, nodeNameForError} from '../../util/src/typescript'; -import {ExportScope, ScopeData, ScopeDirective, ScopePipe} from './api'; +import {ExportScope, ScopeData} from './api'; import {DtsModuleScopeResolver} from './dependency'; export interface LocalNgModuleData { @@ -47,7 +48,7 @@ export interface LocalModuleScope extends ExportScope { * The `LocalModuleScopeRegistry` is also capable of producing `ts.Diagnostic` errors when Angular * semantics are violated. */ -export class LocalModuleScopeRegistry { +export class LocalModuleScopeRegistry implements MetadataRegistry { /** * Tracks whether the registry has been asked to produce scopes for a module or component. Once * this is true, the registry cannot accept registrations of new directives/pipes/modules as it @@ -55,21 +56,6 @@ export class LocalModuleScopeRegistry { */ private sealed = false; - /** - * Metadata for each local NgModule registered. - */ - private ngModuleData = new Map(); - - /** - * Metadata for each local directive registered. - */ - private directiveData = new Map(); - - /** - * Metadata for each local pipe registered. - */ - private pipeData = new Map(); - /** * A map of components from the current compilation unit to the NgModule which declared them. * @@ -79,6 +65,8 @@ export class LocalModuleScopeRegistry { */ private declarationToModule = new Map(); + private moduleToRef = new Map>(); + /** * A cache of calculated `LocalModuleScope`s for each NgModule declared in the current program. * @@ -103,29 +91,23 @@ export class LocalModuleScopeRegistry { private scopeErrors = new Map(); constructor( - private dependencyScopeReader: DtsModuleScopeResolver, private refEmitter: ReferenceEmitter, - private aliasGenerator: AliasGenerator|null) {} + private localReader: MetadataReader, private dependencyScopeReader: DtsModuleScopeResolver, + private refEmitter: ReferenceEmitter, private aliasGenerator: AliasGenerator|null) {} /** * Add an NgModule's data to the registry. */ - registerNgModule(clazz: ClassDeclaration, data: LocalNgModuleData): void { + registerNgModuleMetadata(data: NgModuleMeta): void { this.assertCollecting(); - this.ngModuleData.set(clazz, data); + this.moduleToRef.set(data.ref.node, data.ref); for (const decl of data.declarations) { - this.declarationToModule.set(decl.node, clazz); + this.declarationToModule.set(decl.node, data.ref.node); } } - registerDirective(directive: ScopeDirective): void { - this.assertCollecting(); - this.directiveData.set(directive.ref.node, directive); - } + registerDirectiveMetadata(directive: DirectiveMeta): void {} - registerPipe(pipe: ScopePipe): void { - this.assertCollecting(); - this.pipeData.set(pipe.ref.node, pipe); - } + registerPipeMetadata(pipe: PipeMeta): void {} getScopeForComponent(clazz: ClassDeclaration): LocalModuleScope|null { if (!this.declarationToModule.has(clazz)) { @@ -143,7 +125,11 @@ export class LocalModuleScopeRegistry { * available or the scope contains errors. */ getScopeOfModule(clazz: ClassDeclaration): LocalModuleScope|null { - const scope = this.getScopeOfModuleInternal(clazz); + if (!this.moduleToRef.has(clazz)) { + return null; + } + + const scope = this.getScopeOfModuleInternal(this.moduleToRef.get(clazz) !); // Translate undefined -> null. return scope !== undefined ? scope : null; } @@ -168,21 +154,17 @@ export class LocalModuleScopeRegistry { * Implementation of `getScopeOfModule` which differentiates between no scope being available * (returns `null`) and a scope being produced with errors (returns `undefined`). */ - private getScopeOfModuleInternal(clazz: ClassDeclaration): LocalModuleScope|null|undefined { + private getScopeOfModuleInternal(ref: Reference): LocalModuleScope|null + |undefined { // Seal the registry to protect the integrity of the `LocalModuleScope` cache. this.sealed = true; - // Look for cached data if available. - if (this.cache.has(clazz)) { - return this.cache.get(clazz); - } - - // `clazz` should be an NgModule previously added to the registry. If not, a scope for it + // `ref` should be an NgModule previously added to the registry. If not, a scope for it // cannot be produced. - if (!this.ngModuleData.has(clazz)) { + const ngModule = this.localReader.getNgModuleMetadata(ref); + if (ngModule === null) { return null; } - const ngModule = this.ngModuleData.get(clazz) !; // Errors produced during computation of the scope are recorded here. At the end, if this array // isn't empty then `undefined` will be cached and returned to indicate this scope is invalid. @@ -193,15 +175,15 @@ export class LocalModuleScopeRegistry { // - the directives and pipes which are exported to any NgModules which import this one. // Directives and pipes in the compilation scope. - const compilationDirectives = new Map(); - const compilationPipes = new Map(); + const compilationDirectives = new Map(); + const compilationPipes = new Map(); const declared = new Set(); - const sourceFile = clazz.getSourceFile(); + const sourceFile = ref.node.getSourceFile(); // Directives and pipes exported to any importing NgModules. - const exportDirectives = new Map(); - const exportPipes = new Map(); + const exportDirectives = new Map(); + const exportPipes = new Map(); // The algorithm is as follows: // 1) Add directives/pipes declared in the NgModule to the compilation scope. @@ -217,11 +199,11 @@ export class LocalModuleScopeRegistry { // 1) add declarations. for (const decl of ngModule.declarations) { - if (this.directiveData.has(decl.node)) { - const directive = this.directiveData.get(decl.node) !; + const directive = this.localReader.getDirectiveMetadata(decl); + const pipe = this.localReader.getPipeMetadata(decl); + if (directive !== null) { compilationDirectives.set(decl.node, {...directive, ref: decl}); - } else if (this.pipeData.has(decl.node)) { - const pipe = this.pipeData.get(decl.node) !; + } else if (pipe !== null) { compilationPipes.set(decl.node, {...pipe, ref: decl}); } else { // TODO(alxhub): produce a ts.Diagnostic. This can't be an error right now since some @@ -234,16 +216,16 @@ export class LocalModuleScopeRegistry { // 2) process imports. for (const decl of ngModule.imports) { - const importScope = this.getExportedScope(decl, diagnostics, clazz, 'import'); + const importScope = this.getExportedScope(decl, diagnostics, ref.node, 'import'); if (importScope === null) { // An import wasn't an NgModule, so record an error. - diagnostics.push(invalidRef(clazz, decl, 'import')); + diagnostics.push(invalidRef(ref.node, decl, 'import')); continue; } else if (importScope === undefined) { // An import was an NgModule but contained errors of its own. Record this as an error too, // because this scope is always going to be incorrect if one of its imports could not be // read. - diagnostics.push(invalidTransitiveNgModuleRef(clazz, decl, 'import')); + diagnostics.push(invalidTransitiveNgModuleRef(ref.node, decl, 'import')); continue; } for (const directive of importScope.exported.directives) { @@ -261,12 +243,12 @@ export class LocalModuleScopeRegistry { // imported types. for (const decl of ngModule.exports) { // Attempt to resolve decl as an NgModule. - const importScope = this.getExportedScope(decl, diagnostics, clazz, 'export'); + const importScope = this.getExportedScope(decl, diagnostics, ref.node, 'export'); if (importScope === undefined) { // An export was an NgModule but contained errors of its own. Record this as an error too, // because this scope is always going to be incorrect if one of its exports could not be // read. - diagnostics.push(invalidTransitiveNgModuleRef(clazz, decl, 'export')); + diagnostics.push(invalidTransitiveNgModuleRef(ref.node, decl, 'export')); continue; } else if (importScope !== null) { // decl is an NgModule. @@ -286,10 +268,11 @@ export class LocalModuleScopeRegistry { exportPipes.set(decl.node, pipe); } else { // decl is an unknown export. - if (this.directiveData.has(decl.node) || this.pipeData.has(decl.node)) { - diagnostics.push(invalidReexport(clazz, decl)); + if (this.localReader.getDirectiveMetadata(decl) !== null || + this.localReader.getPipeMetadata(decl) !== null) { + diagnostics.push(invalidReexport(ref.node, decl)); } else { - diagnostics.push(invalidRef(clazz, decl, 'export')); + diagnostics.push(invalidRef(ref.node, decl, 'export')); } continue; } @@ -337,10 +320,10 @@ export class LocalModuleScopeRegistry { // Check if this scope had any errors during production. if (diagnostics.length > 0) { // Cache undefined, to mark the fact that the scope is invalid. - this.cache.set(clazz, undefined); + this.cache.set(ref.node, undefined); // Save the errors for retrieval. - this.scopeErrors.set(clazz, diagnostics); + this.scopeErrors.set(ref.node, diagnostics); // Return undefined to indicate the scope is invalid. return undefined; @@ -355,7 +338,7 @@ export class LocalModuleScopeRegistry { exported, reexports, }; - this.cache.set(clazz, scope); + this.cache.set(ref.node, scope); return scope; } @@ -399,7 +382,7 @@ export class LocalModuleScopeRegistry { return this.dependencyScopeReader.resolve(ref); } else { // The NgModule is declared locally in the current program. Resolve it from the registry. - return this.getScopeOfModuleInternal(ref.node); + return this.getScopeOfModuleInternal(ref); } } diff --git a/packages/compiler-cli/src/ngtsc/scope/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/scope/test/BUILD.bazel index 307b55610c..a322b229ab 100644 --- a/packages/compiler-cli/src/ngtsc/scope/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/scope/test/BUILD.bazel @@ -12,6 +12,7 @@ ts_library( "//packages:types", "//packages/compiler", "//packages/compiler-cli/src/ngtsc/imports", + "//packages/compiler-cli/src/ngtsc/metadata", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/scope", "//packages/compiler-cli/src/ngtsc/testing", diff --git a/packages/compiler-cli/src/ngtsc/scope/test/dependency_spec.ts b/packages/compiler-cli/src/ngtsc/scope/test/dependency_spec.ts index 05e53e34ed..66f942173b 100644 --- a/packages/compiler-cli/src/ngtsc/scope/test/dependency_spec.ts +++ b/packages/compiler-cli/src/ngtsc/scope/test/dependency_spec.ts @@ -10,6 +10,7 @@ import {ExternalExpr, ExternalReference} from '@angular/compiler'; import * as ts from 'typescript'; import {AliasGenerator, FileToModuleHost, Reference} from '../../imports'; +import {DtsMetadataReader} from '../../metadata'; import {ClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {makeProgram} from '../../testing/in_memory_typescript'; import {ExportScope} from '../src/api'; @@ -54,8 +55,9 @@ function makeTestEnv( }); const {program} = makeProgram(files); const checker = program.getTypeChecker(); - const resolver = new MetadataDtsModuleScopeResolver( - checker, new TypeScriptReflectionHost(checker), aliasGenerator); + const reflector = new TypeScriptReflectionHost(checker); + const resolver = + new MetadataDtsModuleScopeResolver(new DtsMetadataReader(checker, reflector), aliasGenerator); // Resolver for the refs object. const get = (target: {}, name: string): Reference => { diff --git a/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts b/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts index a414fc15d0..37fbff8e0c 100644 --- a/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts +++ b/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts @@ -9,12 +9,13 @@ import * as ts from 'typescript'; import {Reference, ReferenceEmitter} from '../../imports'; +import {CompoundMetadataRegistry, DirectiveMeta, LocalMetadataRegistry, MetadataRegistry, PipeMeta} from '../../metadata'; import {ClassDeclaration} from '../../reflection'; -import {ScopeData, ScopeDirective, ScopePipe} from '../src/api'; +import {ScopeData} from '../src/api'; import {DtsModuleScopeResolver} from '../src/dependency'; import {LocalModuleScopeRegistry} from '../src/local'; -function registerFakeRefs(registry: LocalModuleScopeRegistry): +function registerFakeRefs(registry: MetadataRegistry): {[name: string]: Reference} { const get = (target: {}, name: string): Reference => { const sf = ts.createSourceFile( @@ -22,9 +23,9 @@ function registerFakeRefs(registry: LocalModuleScopeRegistry): const clazz = sf.statements[0] as unknown as ClassDeclaration; const ref = new Reference(clazz); if (name.startsWith('Dir') || name.startsWith('Cmp')) { - registry.registerDirective(fakeDirective(ref)); + registry.registerDirectiveMetadata(fakeDirective(ref)); } else if (name.startsWith('Pipe')) { - registry.registerPipe(fakePipe(ref)); + registry.registerPipeMetadata(fakePipe(ref)); } return ref; }; @@ -33,70 +34,108 @@ function registerFakeRefs(registry: LocalModuleScopeRegistry): describe('LocalModuleScopeRegistry', () => { const refEmitter = new ReferenceEmitter([]); - let registry !: LocalModuleScopeRegistry; + let scopeRegistry !: LocalModuleScopeRegistry; + let metaRegistry !: MetadataRegistry; beforeEach(() => { - registry = new LocalModuleScopeRegistry(new MockDtsModuleScopeResolver(), refEmitter, null); + const localRegistry = new LocalMetadataRegistry(); + scopeRegistry = new LocalModuleScopeRegistry( + localRegistry, new MockDtsModuleScopeResolver(), refEmitter, null); + metaRegistry = new CompoundMetadataRegistry([localRegistry, scopeRegistry]); }); it('should produce an accurate LocalModuleScope for a basic NgModule', () => { - const {Dir1, Dir2, Pipe1, Module} = registerFakeRefs(registry); + const {Dir1, Dir2, Pipe1, Module} = registerFakeRefs(metaRegistry); - registry.registerNgModule(Module.node, { + metaRegistry.registerNgModuleMetadata({ + ref: new Reference(Module.node), imports: [], declarations: [Dir1, Dir2, Pipe1], exports: [Dir1, Pipe1], }); - const scope = registry.getScopeOfModule(Module.node) !; + const scope = scopeRegistry.getScopeOfModule(Module.node) !; expect(scopeToRefs(scope.compilation)).toEqual([Dir1, Dir2, Pipe1]); expect(scopeToRefs(scope.exported)).toEqual([Dir1, Pipe1]); }); it('should produce accurate LocalModuleScopes for a complex module chain', () => { - const {DirA, DirB, DirCI, DirCE, ModuleA, ModuleB, ModuleC} = registerFakeRefs(registry); + const {DirA, DirB, DirCI, DirCE, ModuleA, ModuleB, ModuleC} = registerFakeRefs(metaRegistry); - registry.registerNgModule( - ModuleA.node, {imports: [ModuleB], declarations: [DirA], exports: []}); - registry.registerNgModule( - ModuleB.node, {exports: [ModuleC, DirB], declarations: [DirB], imports: []}); - registry.registerNgModule( - ModuleC.node, {declarations: [DirCI, DirCE], exports: [DirCE], imports: []}); + metaRegistry.registerNgModuleMetadata({ + ref: new Reference(ModuleA.node), + imports: [ModuleB], + declarations: [DirA], + exports: [], + }); + metaRegistry.registerNgModuleMetadata({ + ref: new Reference(ModuleB.node), + exports: [ModuleC, DirB], + declarations: [DirB], + imports: [] + }); + metaRegistry.registerNgModuleMetadata({ + ref: new Reference(ModuleC.node), + declarations: [DirCI, DirCE], + exports: [DirCE], + imports: [] + }); - const scopeA = registry.getScopeOfModule(ModuleA.node) !; + const scopeA = scopeRegistry.getScopeOfModule(ModuleA.node) !; expect(scopeToRefs(scopeA.compilation)).toEqual([DirA, DirB, DirCE]); expect(scopeToRefs(scopeA.exported)).toEqual([]); }); it('should not treat exported modules as imported', () => { - const {Dir, ModuleA, ModuleB} = registerFakeRefs(registry); + const {Dir, ModuleA, ModuleB} = registerFakeRefs(metaRegistry); - registry.registerNgModule(ModuleA.node, {exports: [ModuleB], imports: [], declarations: []}); - registry.registerNgModule(ModuleB.node, {declarations: [Dir], exports: [Dir], imports: []}); + metaRegistry.registerNgModuleMetadata({ + ref: new Reference(ModuleA.node), + exports: [ModuleB], + imports: [], + declarations: [], + }); + metaRegistry.registerNgModuleMetadata({ + ref: new Reference(ModuleB.node), + declarations: [Dir], + exports: [Dir], + imports: [], + }); - const scopeA = registry.getScopeOfModule(ModuleA.node) !; + const scopeA = scopeRegistry.getScopeOfModule(ModuleA.node) !; expect(scopeToRefs(scopeA.compilation)).toEqual([]); expect(scopeToRefs(scopeA.exported)).toEqual([Dir]); }); it('should deduplicate declarations and exports', () => { - const {DirA, ModuleA, DirB, ModuleB, ModuleC} = registerFakeRefs(registry); + const {DirA, ModuleA, DirB, ModuleB, ModuleC} = registerFakeRefs(metaRegistry); - registry.registerNgModule(ModuleA.node, { + metaRegistry.registerNgModuleMetadata({ + ref: new Reference(ModuleA.node), declarations: [DirA, DirA], imports: [ModuleB, ModuleC], exports: [DirA, DirA, DirB, ModuleB], }); - registry.registerNgModule(ModuleB.node, {declarations: [DirB], imports: [], exports: [DirB]}); - registry.registerNgModule(ModuleC.node, {declarations: [], imports: [], exports: [ModuleB]}); + metaRegistry.registerNgModuleMetadata({ + ref: new Reference(ModuleB.node), + declarations: [DirB], + imports: [], + exports: [DirB], + }); + metaRegistry.registerNgModuleMetadata({ + ref: new Reference(ModuleC.node), + declarations: [], + imports: [], + exports: [ModuleB], + }); - const scope = registry.getScopeOfModule(ModuleA.node) !; + const scope = scopeRegistry.getScopeOfModule(ModuleA.node) !; expect(scopeToRefs(scope.compilation)).toEqual([DirA, DirB]); expect(scopeToRefs(scope.exported)).toEqual([DirA, DirB]); }); it('should preserve reference identities in module metadata', () => { - const {Dir, Module} = registerFakeRefs(registry); + const {Dir, Module} = registerFakeRefs(metaRegistry); const idSf = ts.createSourceFile('id.ts', 'var id;', ts.ScriptTarget.Latest, true); // Create a new Reference to Dir, with a special `ts.Identifier`, and register the directive @@ -105,39 +144,64 @@ describe('LocalModuleScopeRegistry', () => { const id = idVar.declarationList.declarations[0].name as ts.Identifier; const DirInModule = new Reference(Dir.node); DirInModule.addIdentifier(id); - registry.registerNgModule(Module.node, {exports: [], imports: [], declarations: [DirInModule]}); + metaRegistry.registerNgModuleMetadata({ + ref: new Reference(Module.node), + exports: [], + imports: [], + declarations: [DirInModule], + }); - const scope = registry.getScopeOfModule(Module.node) !; + const scope = scopeRegistry.getScopeOfModule(Module.node) !; expect(scope.compilation.directives[0].ref.getIdentityIn(idSf)).toBe(id); }); it('should allow directly exporting a directive that\'s not imported', () => { - const {Dir, ModuleA, ModuleB} = registerFakeRefs(registry); + const {Dir, ModuleA, ModuleB} = registerFakeRefs(metaRegistry); - registry.registerNgModule(ModuleA.node, {exports: [Dir], imports: [ModuleB], declarations: []}); - registry.registerNgModule(ModuleB.node, {declarations: [Dir], exports: [Dir], imports: []}); + metaRegistry.registerNgModuleMetadata({ + ref: new Reference(ModuleA.node), + exports: [Dir], + imports: [ModuleB], + declarations: [], + }); + metaRegistry.registerNgModuleMetadata({ + ref: new Reference(ModuleB.node), + declarations: [Dir], + exports: [Dir], + imports: [], + }); - const scopeA = registry.getScopeOfModule(ModuleA.node) !; + const scopeA = scopeRegistry.getScopeOfModule(ModuleA.node) !; expect(scopeToRefs(scopeA.exported)).toEqual([Dir]); }); it('should not allow directly exporting a directive that\'s not imported', () => { - const {Dir, ModuleA, ModuleB} = registerFakeRefs(registry); + const {Dir, ModuleA, ModuleB} = registerFakeRefs(metaRegistry); - registry.registerNgModule(ModuleA.node, {exports: [Dir], imports: [], declarations: []}); - registry.registerNgModule(ModuleB.node, {declarations: [Dir], exports: [Dir], imports: []}); + metaRegistry.registerNgModuleMetadata({ + ref: new Reference(ModuleA.node), + exports: [Dir], + imports: [], + declarations: [], + }); + metaRegistry.registerNgModuleMetadata({ + ref: new Reference(ModuleB.node), + declarations: [Dir], + exports: [Dir], + imports: [], + }); - expect(registry.getScopeOfModule(ModuleA.node)).toBe(null); + expect(scopeRegistry.getScopeOfModule(ModuleA.node)).toBe(null); // ModuleA should have associated diagnostics as it exports `Dir` without declaring it. - expect(registry.getDiagnosticsOfModule(ModuleA.node)).not.toBeNull(); + expect(scopeRegistry.getDiagnosticsOfModule(ModuleA.node)).not.toBeNull(); // ModuleB should have no diagnostics as it correctly declares `Dir`. - expect(registry.getDiagnosticsOfModule(ModuleB.node)).toBeNull(); + expect(scopeRegistry.getDiagnosticsOfModule(ModuleB.node)).toBeNull(); }); }); -function fakeDirective(ref: Reference): ScopeDirective { +function fakeDirective(ref: Reference): DirectiveMeta { const name = ref.debugName !; return { ref, @@ -150,10 +214,11 @@ function fakeDirective(ref: Reference): ScopeDirective { queries: [], hasNgTemplateContextGuard: false, ngTemplateGuards: [], + baseClass: null, }; } -function fakePipe(ref: Reference): ScopePipe { +function fakePipe(ref: Reference): PipeMeta { const name = ref.debugName !; return {ref, name}; }