From 9277afce61e490e237d936ba06df95710b4d88ea Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Tue, 26 Mar 2019 14:02:16 -0700 Subject: [PATCH] refactor(ivy): move metadata registration to its own package (#29698) Previously, metadata registration (the recording of collected metadata during analysis of directives, pipes, and NgModules) was only used to produce the `LocalModuleScope`, and thus was handled by the `LocalModuleScopeRegistry`. However, the template type-checker also needs information about registered directives, outside of the NgModule scope determinations. Rather than reuse the scope registry for an unintended purpose, this commit introduces new abstractions for metadata registration and lookups in a separate 'metadata' package, which the scope registry implements. This paves the way for a future commit to make use of this metadata for the template type-checking system. Testing strategy: this commit is a refactoring which introduces no new functionality, so existing tests are sufficient. PR Close #29698 --- packages/compiler-cli/BUILD.bazel | 1 + packages/compiler-cli/ngcc/BUILD.bazel | 1 + .../ngcc/src/analysis/decoration_analyzer.ts | 26 ++-- .../src/ngtsc/annotations/BUILD.bazel | 1 + .../src/ngtsc/annotations/src/component.ts | 16 +- .../src/ngtsc/annotations/src/directive.ts | 10 +- .../src/ngtsc/annotations/src/ng_module.ts | 11 +- .../src/ngtsc/annotations/src/pipe.ts | 7 +- .../src/ngtsc/annotations/test/BUILD.bazel | 1 + .../ngtsc/annotations/test/component_spec.ts | 9 +- .../ngtsc/annotations/test/directive_spec.ts | 5 +- .../ngtsc/annotations/test/ng_module_spec.ts | 9 +- .../src/ngtsc/metadata/BUILD.bazel | 17 ++ .../compiler-cli/src/ngtsc/metadata/index.ts | 12 ++ .../src/ngtsc/metadata/src/api.ts | 71 +++++++++ .../src/ngtsc/metadata/src/dts.ts | 118 ++++++++++++++ .../src/ngtsc/metadata/src/registry.ts | 61 ++++++++ .../src/ngtsc/{scope => metadata}/src/util.ts | 0 packages/compiler-cli/src/ngtsc/program.ts | 25 +-- .../compiler-cli/src/ngtsc/scope/BUILD.bazel | 1 + .../compiler-cli/src/ngtsc/scope/index.ts | 3 +- .../compiler-cli/src/ngtsc/scope/src/api.ts | 27 +--- .../src/ngtsc/scope/src/dependency.ts | 131 ++-------------- .../compiler-cli/src/ngtsc/scope/src/local.ts | 105 ++++++------- .../src/ngtsc/scope/test/BUILD.bazel | 1 + .../src/ngtsc/scope/test/dependency_spec.ts | 6 +- .../src/ngtsc/scope/test/local_spec.ts | 147 +++++++++++++----- 27 files changed, 532 insertions(+), 290 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel create mode 100644 packages/compiler-cli/src/ngtsc/metadata/index.ts create mode 100644 packages/compiler-cli/src/ngtsc/metadata/src/api.ts create mode 100644 packages/compiler-cli/src/ngtsc/metadata/src/dts.ts create mode 100644 packages/compiler-cli/src/ngtsc/metadata/src/registry.ts rename packages/compiler-cli/src/ngtsc/{scope => metadata}/src/util.ts (100%) 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}; }