diff --git a/packages/compiler-cli/BUILD.bazel b/packages/compiler-cli/BUILD.bazel index 91b4e5b48c..65550129bb 100644 --- a/packages/compiler-cli/BUILD.bazel +++ b/packages/compiler-cli/BUILD.bazel @@ -32,6 +32,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/routing", + "//packages/compiler-cli/src/ngtsc/scope", "//packages/compiler-cli/src/ngtsc/shims", "//packages/compiler-cli/src/ngtsc/switch", "//packages/compiler-cli/src/ngtsc/transform", diff --git a/packages/compiler-cli/src/ngcc/BUILD.bazel b/packages/compiler-cli/src/ngcc/BUILD.bazel index 1915c0e0fd..54b7ed1a8e 100644 --- a/packages/compiler-cli/src/ngcc/BUILD.bazel +++ b/packages/compiler-cli/src/ngcc/BUILD.bazel @@ -17,6 +17,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/reflection", + "//packages/compiler-cli/src/ngtsc/scope", "//packages/compiler-cli/src/ngtsc/transform", "//packages/compiler-cli/src/ngtsc/translator", "@ngdeps//@types/convert-source-map", diff --git a/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts b/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts index a5dc35592b..7b0d5338eb 100644 --- a/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts +++ b/packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts @@ -10,11 +10,12 @@ import * as path from 'canonical-path'; import * as fs from 'fs'; import * as ts from 'typescript'; -import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader, SelectorScopeRegistry} from '../../../ngtsc/annotations'; +import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../ngtsc/annotations'; import {CycleAnalyzer, ImportGraph} from '../../../ngtsc/cycles'; import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, ReferenceEmitter} from '../../../ngtsc/imports'; import {PartialEvaluator} from '../../../ngtsc/partial_evaluator'; import {AbsoluteFsPath, LogicalFileSystem} from '../../../ngtsc/path'; +import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../ngtsc/scope'; import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../ngtsc/transform'; import {DecoratedClass} from '../host/decorated_class'; import {NgccReflectionHost} from '../host/ngcc_host'; @@ -71,7 +72,9 @@ export class DecorationAnalyzer { // on whether a bestGuessOwningModule is present in the Reference. new LogicalProjectStrategy(this.typeChecker, new LogicalFileSystem(this.rootDirs)), ]); - scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.reflectionHost, this.refEmitter); + dtsModuleScopeResolver = + new MetadataDtsModuleScopeResolver(this.typeChecker, this.reflectionHost); + scopeRegistry = new LocalModuleScopeRegistry(this.dtsModuleScopeResolver); evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker); moduleResolver = new ModuleResolver(this.program, this.options, this.host); importGraph = new ImportGraph(this.moduleResolver); @@ -81,7 +84,7 @@ export class DecorationAnalyzer { new ComponentDecoratorHandler( this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore, this.resourceManager, this.rootDirs, /* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true, - this.moduleResolver, this.cycleAnalyzer), + this.moduleResolver, this.cycleAnalyzer, this.refEmitter), new DirectiveDecoratorHandler( this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore), new InjectableDecoratorHandler(this.reflectionHost, this.isCore, /* strictCtorDeps */ false), diff --git a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel index 91c845e134..a9e04b3cdc 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel @@ -16,6 +16,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/routing", + "//packages/compiler-cli/src/ngtsc/scope", "//packages/compiler-cli/src/ngtsc/transform", "//packages/compiler-cli/src/ngtsc/typecheck", "//packages/compiler-cli/src/ngtsc/util", diff --git a/packages/compiler-cli/src/ngtsc/annotations/index.ts b/packages/compiler-cli/src/ngtsc/annotations/index.ts index ac736ce264..2790a74f86 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/index.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/index.ts @@ -16,4 +16,3 @@ export {InjectableDecoratorHandler} from './src/injectable'; export {NgModuleDecoratorHandler} from './src/ng_module'; export {PipeDecoratorHandler} from './src/pipe'; export {NoopReferencesRegistry, ReferencesRegistry} from './src/references_registry'; -export {CompilationScope, SelectorScopeRegistry} from './src/selector_scope'; diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 20650763d4..aefaf5205a 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -12,9 +12,10 @@ import * as ts from 'typescript'; import {CycleAnalyzer} from '../../cycles'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; -import {ModuleResolver, Reference} from '../../imports'; +import {ModuleResolver, Reference, ReferenceEmitter} from '../../imports'; import {EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection'; +import {LocalModuleScopeRegistry, ScopeDirective, extractDirectiveGuards} from '../../scope'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; import {TypeCheckContext} from '../../typecheck'; import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300'; @@ -22,8 +23,7 @@ import {tsSourceMapBug29300Fixed} from '../../util/src/ts_source_map_bug_29300'; import {ResourceLoader} from './api'; import {extractDirectiveMetadata, extractQueriesFromDecorator, parseFieldArrayValue, queriesFromFields} from './directive'; import {generateSetClassMetadataCall} from './metadata'; -import {ScopeDirective, SelectorScopeRegistry} from './selector_scope'; -import {extractDirectiveGuards, isAngularCore, isAngularCoreReference, unwrapExpression} from './util'; +import {isAngularCore, isAngularCoreReference, unwrapExpression} from './util'; const EMPTY_MAP = new Map(); const EMPTY_ARRAY: any[] = []; @@ -41,10 +41,11 @@ export class ComponentDecoratorHandler implements DecoratorHandler { constructor( private reflector: ReflectionHost, private evaluator: PartialEvaluator, - private scopeRegistry: SelectorScopeRegistry, private isCore: boolean, + 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 moduleResolver: ModuleResolver, private cycleAnalyzer: CycleAnalyzer, + private refEmitter: ReferenceEmitter) {} private literalCache = new Map(); private elementSchemaRegistry = new DomElementSchemaRegistry(); @@ -222,14 +223,13 @@ export class ComponentDecoratorHandler implements `Errors parsing template: ${template.errors.map(e => e.toString()).join(', ')}`); } - // If the component has a selector, it should be registered with the `SelectorScopeRegistry` so - // when this component appears in an `@NgModule` scope, its selector can be determined. + // If the component has a selector, it should be registered with the `LocalModuleScopeRegistry` + // so that when this component appears in an `@NgModule` scope, its selector can be determined. if (metadata.selector !== null) { const ref = new Reference(node); - this.scopeRegistry.registerDirective(node, { + this.scopeRegistry.registerDirective({ ref, name: node.name !.text, - directive: ref, selector: metadata.selector, exportAs: metadata.exportAs, inputs: metadata.inputs, @@ -313,10 +313,13 @@ export class ComponentDecoratorHandler implements } typeCheck(ctx: TypeCheckContext, node: ts.Declaration, meta: ComponentHandlerData): void { - const scope = this.scopeRegistry.lookupCompilationScopeAsRefs(node); - const matcher = new SelectorMatcher>(); + if (!ts.isClassDeclaration(node)) { + return; + } + const scope = this.scopeRegistry.getScopeForComponent(node); + const matcher = new SelectorMatcher(); if (scope !== null) { - for (const meta of scope.directives) { + for (const meta of scope.compilation.directives) { matcher.addSelectables(CssSelector.parse(meta.selector), meta); } ctx.addTemplate(node as ts.ClassDeclaration, meta.parsedTemplate, matcher); @@ -324,26 +327,33 @@ export class ComponentDecoratorHandler implements } resolve(node: ts.ClassDeclaration, analysis: ComponentHandlerData): void { + const context = node.getSourceFile(); // Check whether this component was registered with an NgModule. If so, it should be compiled // under that module's compilation scope. - const scope = this.scopeRegistry.lookupCompilationScope(node); + const scope = this.scopeRegistry.getScopeForComponent(node); let metadata = analysis.meta; if (scope !== null) { // Replace the empty components and directives from the analyze() step with a fully expanded // scope. This is possible now because during resolve() the whole compilation unit has been // fully analyzed. - const {pipes, containsForwardDecls} = scope; - const directives = - scope.directives.map(dir => ({selector: dir.selector, expression: dir.directive})); + const directives = scope.compilation.directives.map( + dir => ({selector: dir.selector, expression: this.refEmitter.emit(dir.ref, context)})); + const pipes = new Map(); + for (const pipe of scope.compilation.pipes) { + pipes.set(pipe.name, this.refEmitter.emit(pipe.ref, context)); + } // Scan through the references of the `scope.directives` array and check whether // any import which needs to be generated for the directive would create a cycle. const origin = node.getSourceFile(); - const cycleDetected = - scope.directives.some(meta => this._isCyclicImport(meta.directive, origin)) || - Array.from(scope.pipes.values()).some(pipe => this._isCyclicImport(pipe, origin)); + const cycleDetected = directives.some(dir => this._isCyclicImport(dir.expression, origin)) || + Array.from(pipes.values()).some(pipe => this._isCyclicImport(pipe, origin)); if (!cycleDetected) { - const wrapDirectivesAndPipesInClosure: boolean = !!containsForwardDecls; + const wrapDirectivesAndPipesInClosure = + directives.some( + dir => isExpressionForwardReference(dir.expression, node.name !, origin)) || + Array.from(pipes.values()) + .some(pipe => isExpressionForwardReference(pipe, node.name !, origin)); metadata.directives = directives; metadata.pipes = pipes; metadata.wrapDirectivesAndPipesInClosure = wrapDirectivesAndPipesInClosure; @@ -446,3 +456,17 @@ function getTemplateRange(templateExpr: ts.Expression) { endPos: templateExpr.getEnd() - 1, }; } + +function isExpressionForwardReference( + expr: Expression, context: ts.Node, contextSource: ts.SourceFile): boolean { + if (isWrappedTsNodeExpr(expr)) { + const node = ts.getOriginalNode(expr.node); + return node.getSourceFile() === contextSource && context.pos < node.pos; + } else { + return false; + } +} + +function isWrappedTsNodeExpr(expr: Expression): expr is WrappedNodeExpr { + return expr instanceof WrappedNodeExpr; +} diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts index c87c47c560..bbcd000f44 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/directive.ts @@ -13,11 +13,12 @@ import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {Reference} from '../../imports'; import {EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {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'; -import {SelectorScopeRegistry} from './selector_scope'; -import {extractDirectiveGuards, getValidConstructorDependencies, isAngularCore, unwrapExpression, unwrapForwardRef} from './util'; +import {getValidConstructorDependencies, isAngularCore, unwrapExpression, unwrapForwardRef} from './util'; const EMPTY_OBJECT: {[key: string]: string} = {}; @@ -29,7 +30,7 @@ export class DirectiveDecoratorHandler implements DecoratorHandler { constructor( private reflector: ReflectionHost, private evaluator: PartialEvaluator, - private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {} + private scopeRegistry: LocalModuleScopeRegistry, private isCore: boolean) {} readonly precedence = HandlerPrecedence.PRIMARY; @@ -58,9 +59,8 @@ 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(node, { + this.scopeRegistry.registerDirective({ ref, - directive: ref, name: node.name !.text, selector: analysis.selector, exportAs: analysis.exportAs, 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 b8cb380501..6b5510bdcd 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -14,12 +14,12 @@ import {Reference, ReferenceEmitter} from '../../imports'; import {PartialEvaluator, ResolvedValue} from '../../partial_evaluator'; import {Decorator, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../reflection'; import {NgModuleRouteAnalyzer} from '../../routing'; +import {LocalModuleScopeRegistry} from '../../scope'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; import {getSourceFile} from '../../util/src/typescript'; import {generateSetClassMetadataCall} from './metadata'; import {ReferencesRegistry} from './references_registry'; -import {SelectorScopeRegistry} from './selector_scope'; import {getValidConstructorDependencies, isAngularCore, toR3Reference, unwrapExpression} from './util'; export interface NgModuleAnalysis { @@ -37,9 +37,9 @@ export interface NgModuleAnalysis { export class NgModuleDecoratorHandler implements DecoratorHandler { constructor( private reflector: ReflectionHost, private evaluator: PartialEvaluator, - private scopeRegistry: SelectorScopeRegistry, private referencesRegistry: ReferencesRegistry, - private isCore: boolean, private routeAnalyzer: NgModuleRouteAnalyzer|null, - private refEmitter: ReferenceEmitter) {} + private scopeRegistry: LocalModuleScopeRegistry, + private referencesRegistry: ReferencesRegistry, private isCore: boolean, + private routeAnalyzer: NgModuleRouteAnalyzer|null, private refEmitter: ReferenceEmitter) {} readonly precedence = HandlerPrecedence.PRIMARY; @@ -114,9 +114,10 @@ export class NgModuleDecoratorHandler implements DecoratorHandler { directives.push(this.refEmitter.emit(directive.ref, context) !); }); - scope.pipes.forEach(pipe => pipes.push(this.refEmitter.emit(pipe, context) !)); + const directives = scope.compilation.directives.map( + directive => this.refEmitter.emit(directive.ref, context)); + const pipes = scope.compilation.pipes.map(pipe => this.refEmitter.emit(pipe.ref, context)); const directiveArray = new LiteralArrayExpr(directives); const pipesArray = new LiteralArrayExpr(pipes); const declExpr = this.refEmitter.emit(decl, context) !; diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts index 643a0da6e4..e00c3c3c4f 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts @@ -10,12 +10,13 @@ import {LiteralExpr, R3PipeMetadata, Statement, WrappedNodeExpr, compilePipeFrom import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; +import {Reference} from '../../imports'; import {PartialEvaluator} from '../../partial_evaluator'; import {Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; +import {LocalModuleScopeRegistry} from '../../scope/src/local'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../transform'; import {generateSetClassMetadataCall} from './metadata'; -import {SelectorScopeRegistry} from './selector_scope'; import {getValidConstructorDependencies, isAngularCore, unwrapExpression} from './util'; export interface PipeHandlerData { @@ -26,7 +27,7 @@ export interface PipeHandlerData { export class PipeDecoratorHandler implements DecoratorHandler { constructor( private reflector: ReflectionHost, private evaluator: PartialEvaluator, - private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {} + private scopeRegistry: LocalModuleScopeRegistry, private isCore: boolean) {} readonly precedence = HandlerPrecedence.PRIMARY; @@ -78,7 +79,8 @@ export class PipeDecoratorHandler implements DecoratorHandler[]; - imports: Reference[]; - exports: Reference[]; -} - -/** - * Transitively expanded maps of directives and pipes visible to a component being compiled in the - * context of some module. - */ -export interface CompilationScope { - directives: ScopeDirective[]; - pipes: Map; - containsForwardDecls?: boolean; -} - -export interface ScopeDirective extends TypeCheckableDirectiveMeta { - selector: string; - directive: T; -} - -/** - * Both transitively expanded scopes for a given NgModule. - */ -interface SelectorScopes { - /** - * Set of components, directives, and pipes visible to all components being compiled in the - * context of some module. - */ - compilation: Reference[]; - - /** - * Set of components, directives, and pipes added to the compilation scope of any module importing - * some module. - */ - exported: Reference[]; -} - -/** - * Registry which records and correlates static analysis information of Angular types. - * - * Once a compilation unit's information is fed into the SelectorScopeRegistry, it can be asked to - * produce transitive `CompilationScope`s for components. - */ -export class SelectorScopeRegistry { - /** - * Map of modules declared in the current compilation unit to their (local) metadata. - */ - private _moduleToData = new Map(); - - /** - * Map of modules to their cached `CompilationScope`s. - */ - private _compilationScopeCache = new Map>(); - - /** - * Map of components/directives to their metadata. - */ - private _directiveToMetadata = new Map>(); - - /** - * Map of pipes to their name. - */ - private _pipeToName = new Map(); - - /** - * Components that require remote scoping. - */ - private _requiresRemoteScope = new Set(); - - /** - * Map of components/directives/pipes to their module. - */ - private _declararedTypeToModule = new Map(); - - constructor( - private checker: ts.TypeChecker, private reflector: ReflectionHost, - private refEmitter: ReferenceEmitter) {} - - /** - * Register a module's metadata with the registry. - */ - registerModule(node: ts.Declaration, data: ModuleData): void { - node = ts.getOriginalNode(node) as ts.Declaration; - - if (this._moduleToData.has(node)) { - throw new Error(`Module already registered: ${reflectNameOfDeclaration(node)}`); - } - this._moduleToData.set(node, data); - - // Register all of the module's declarations in the context map as belonging to this module. - data.declarations.forEach(decl => { - this._declararedTypeToModule.set(ts.getOriginalNode(decl.node) as ts.Declaration, node); - }); - } - - /** - * Register the metadata of a component or directive with the registry. - */ - registerDirective(node: ts.Declaration, metadata: ScopeDirective): void { - node = ts.getOriginalNode(node) as ts.Declaration; - - if (this._directiveToMetadata.has(node)) { - throw new Error( - `Selector already registered: ${reflectNameOfDeclaration(node)} ${metadata.selector}`); - } - this._directiveToMetadata.set(node, metadata); - } - - /** - * Register the name of a pipe with the registry. - */ - registerPipe(node: ts.Declaration, name: string): void { - node = ts.getOriginalNode(node) as ts.Declaration; - - this._pipeToName.set(node, name); - } - - /** - * Mark a component (identified by its `ts.Declaration`) as requiring its `directives` scope to be - * set remotely, from the file of the @NgModule which declares the component. - */ - setComponentAsRequiringRemoteScoping(component: ts.Declaration): void { - this._requiresRemoteScope.add(component); - } - - /** - * Check whether the given component requires its `directives` scope to be set remotely. - */ - requiresRemoteScope(component: ts.Declaration): boolean { - return this._requiresRemoteScope.has(ts.getOriginalNode(component) as ts.Declaration); - } - - lookupCompilationScopeAsRefs(node: ts.Declaration): CompilationScope|null { - node = ts.getOriginalNode(node) as ts.Declaration; - - // If the component has no associated module, then it has no compilation scope. - if (!this._declararedTypeToModule.has(node)) { - return null; - } - - const module = this._declararedTypeToModule.get(node) !; - - // Compilation scope computation is somewhat expensive, so it's cached. Check the cache for - // the module. - if (this._compilationScopeCache.has(module)) { - // The compilation scope was cached. - const scope = this._compilationScopeCache.get(module) !; - - // The scope as cached is in terms of References, not Expressions. Converting between them - // requires knowledge of the context file (in this case, the component node's source file). - return scope; - } - - // This is the first time the scope for this module is being computed. - const directives: ScopeDirective>[] = []; - const pipes = new Map>(); - - // Tracks which declarations already appear in the `CompilationScope`. - const seenSet = new Set(); - - // Process the declaration scope of the module, and lookup the selector of every declared type. - // The initial value of ngModuleImportedFrom is 'null' which signifies that the NgModule - // was not imported from a .d.ts source. - for (const ref of this - .lookupScopesOrDie( - module !, /* ngModuleImportedFrom */ null, node.getSourceFile().fileName) - .compilation) { - const node = ts.getOriginalNode(ref.node) as ts.Declaration; - - // Track whether this `ts.Declaration` has been seen before. - if (seenSet.has(node)) { - continue; - } else { - seenSet.add(node); - } - - // Either the node represents a directive or a pipe. Look for both. - const metadata = this.lookupDirectiveMetadata(ref); - // Only directives/components with selectors get added to the scope. - if (metadata !== null) { - directives.push({...metadata, directive: ref}); - } else { - const name = this.lookupPipeName(node); - if (name !== null) { - pipes.set(name, ref); - } - } - } - - const scope: CompilationScope = {directives, pipes}; - - // Many components may be compiled in the same scope, so cache it. - this._compilationScopeCache.set(node, scope); - - // Convert References to Expressions in the context of the component's source file. - return scope; - } - - /** - * Produce the compilation scope of a component, which is determined by the module that declares - * it. - */ - lookupCompilationScope(node: ts.Declaration): CompilationScope|null { - const scope = this.lookupCompilationScopeAsRefs(node); - return scope !== null ? convertScopeToExpressions(scope, node, this.refEmitter) : null; - } - - private lookupScopesOrDie( - node: ts.Declaration, ngModuleImportedFrom: string|null, - resolutionContext: string): SelectorScopes { - const result = this.lookupScopes(node, ngModuleImportedFrom, resolutionContext); - if (result === null) { - throw new Error(`Module not found: ${reflectNameOfDeclaration(node)}`); - } - return result; - } - - /** - * Lookup `SelectorScopes` for a given module. - * - * This function assumes that if the given module was imported from an absolute path - * (`ngModuleImportedFrom`) then all of its declarations are exported at that same path, as well - * as imports and exports from other modules that are relatively imported. - */ - private lookupScopes( - node: ts.Declaration, ngModuleImportedFrom: string|null, - resolutionContext: string): SelectorScopes|null { - let data: ModuleData|null = null; - - // Either this module was analyzed directly, or has a precompiled ngModuleDef. - if (this._moduleToData.has(node)) { - // The module was analyzed before, and thus its data is available. - data = this._moduleToData.get(node) !; - } else { - // The module wasn't analyzed before, and probably has a precompiled ngModuleDef with a type - // annotation that specifies the needed metadata. - data = this._readModuleDataFromCompiledClass(node, ngModuleImportedFrom, resolutionContext); - // Note that data here could still be null, if the class didn't have a precompiled - // ngModuleDef. - } - - if (data === null) { - return null; - } - - const context = node.getSourceFile().fileName; - - return { - compilation: [ - ...data.declarations, - // Expand imports to the exported scope of those imports. - ...flatten(data.imports.map( - ref => - this.lookupScopesOrDie(ref.node as ts.Declaration, ref.ownedByModuleGuess, context) - .exported)), - // And include the compilation scope of exported modules. - ...flatten( - data.exports - .map( - ref => this.lookupScopes( - ref.node as ts.Declaration, ref.ownedByModuleGuess, context)) - .filter((scope: SelectorScopes | null): scope is SelectorScopes => scope !== null) - .map(scope => scope.exported)) - ], - exported: flatten(data.exports.map(ref => { - const scope = - this.lookupScopes(ref.node as ts.Declaration, ref.ownedByModuleGuess, context); - if (scope !== null) { - return scope.exported; - } else { - return [ref]; - } - })), - }; - } - - /** - * Lookup the metadata of a component or directive class. - * - * Potentially this class is declared in a .d.ts file or otherwise has a manually created - * ngComponentDef/ngDirectiveDef. In this case, the type metadata of that definition is read - * to determine the metadata. - */ - private lookupDirectiveMetadata(ref: Reference): ScopeDirective|null { - const node = ts.getOriginalNode(ref.node) as ts.Declaration; - if (this._directiveToMetadata.has(node)) { - return this._directiveToMetadata.get(node) !; - } else { - return this._readMetadataFromCompiledClass(ref as Reference); - } - } - - private lookupPipeName(node: ts.Declaration): string|null { - if (this._pipeToName.has(node)) { - return this._pipeToName.get(node) !; - } else { - return this._readNameFromCompiledClass(node); - } - } - - /** - * 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 clazz the class of interest - * @param ngModuleImportedFrom module specifier of the import path to assume for all declarations - * stemming from this module. - */ - private _readModuleDataFromCompiledClass( - clazz: ts.Declaration, ngModuleImportedFrom: string|null, - resolutionContext: string): ModuleData|null { - // This operation is explicitly not memoized, as it depends on `ngModuleImportedFrom`. - // TODO(alxhub): investigate caching of .d.ts module metadata. - const ngModuleDef = 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: this._extractReferencesFromType( - declarationMetadata, ngModuleImportedFrom, resolutionContext), - exports: - this._extractReferencesFromType(exportMetadata, ngModuleImportedFrom, resolutionContext), - imports: - this._extractReferencesFromType(importMetadata, ngModuleImportedFrom, resolutionContext), - }; - } - - /** - * Get the selector from type metadata for a class with a precompiled ngComponentDef or - * ngDirectiveDef. - */ - private _readMetadataFromCompiledClass(ref: Reference): - ScopeDirective|null { - const clazz = ts.getOriginalNode(ref.node) as ts.ClassDeclaration; - const def = this.reflector.getMembersOfClass(clazz).find( - field => - field.isStatic && (field.name === 'ngComponentDef' || field.name === 'ngDirectiveDef')); - 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, - directive: ref, - 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), - }; - } - - /** - * Get the selector from type metadata for a class with a precompiled ngComponentDef or - * ngDirectiveDef. - */ - private _readNameFromCompiledClass(clazz: ts.Declaration): string|null { - const def = this.reflector.getMembersOfClass(clazz).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; - } - return type.literal.text; - } - - /** - * Process a `TypeNode` which is a tuple of references to other types, and return `Reference`s to - * them. - * - * This operation assumes that these types should be imported from `ngModuleImportedFrom` unless - * they themselves were imported from another absolute path. - */ - private _extractReferencesFromType( - def: ts.TypeNode, ngModuleImportedFrom: string|null, - resolutionContext: string): Reference[] { - if (!ts.isTupleTypeNode(def)) { - return []; - } - return def.elementTypes.map(element => { - if (!ts.isTypeQueryNode(element)) { - throw new Error(`Expected TypeQueryNode`); - } - const type = element.exprName; - if (ngModuleImportedFrom !== null) { - const {node, from} = reflectTypeEntityToDeclaration(type, this.checker); - const specifier = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom); - return new Reference(node, {specifier, resolutionContext}); - } else { - const {node} = reflectTypeEntityToDeclaration(type, this.checker); - return new Reference(node); - } - }); - } -} - -function flatten(array: T[][]): T[] { - return array.reduce((accum, subArray) => { - accum.push(...subArray); - return accum; - }, [] as T[]); -} - -function convertDirectiveReferenceList( - input: ScopeDirective[], context: ts.SourceFile, - refEmitter: ReferenceEmitter): ScopeDirective[] { - return input.map(meta => { - const directive = refEmitter.emit(meta.directive, context); - if (directive === null) { - throw new Error(`Could not write expression to reference ${meta.directive.node}`); - } - return {...meta, directive}; - }); -} - -function convertPipeReferenceMap( - map: Map, context: ts.SourceFile, - refEmitter: ReferenceEmitter): Map { - const newMap = new Map(); - map.forEach((meta, selector) => { - const pipe = refEmitter.emit(meta, context); - if (pipe === null) { - throw new Error(`Could not write expression to reference ${meta.node}`); - } - newMap.set(selector, pipe); - }); - return newMap; -} - -function convertScopeToExpressions( - scope: CompilationScope, context: ts.Declaration, - refEmitter: ReferenceEmitter): CompilationScope { - const sourceContext = ts.getOriginalNode(context).getSourceFile(); - const directives = convertDirectiveReferenceList(scope.directives, sourceContext, refEmitter); - const pipes = convertPipeReferenceMap(scope.pipes, sourceContext, refEmitter); - const declPointer = maybeUnwrapNameOfDeclaration(context); - let containsForwardDecls = false; - directives.forEach(meta => { - containsForwardDecls = containsForwardDecls || - isExpressionForwardReference(meta.directive, declPointer, sourceContext); - }); - !containsForwardDecls && pipes.forEach(expr => { - containsForwardDecls = - containsForwardDecls || isExpressionForwardReference(expr, declPointer, sourceContext); - }); - return {directives, pipes, containsForwardDecls}; -} - -function isExpressionForwardReference( - expr: Expression, context: ts.Node, contextSource: ts.SourceFile): boolean { - if (isWrappedTsNodeExpr(expr)) { - const node = ts.getOriginalNode(expr.node); - return node.getSourceFile() === contextSource && context.pos < node.pos; - } - return false; -} - -function isWrappedTsNodeExpr(expr: Expression): expr is WrappedNodeExpr { - return expr instanceof WrappedNodeExpr; -} - -function maybeUnwrapNameOfDeclaration(decl: ts.Declaration): ts.Declaration|ts.Identifier { - if ((ts.isClassDeclaration(decl) || ts.isVariableDeclaration(decl)) && decl.name !== undefined && - ts.isIdentifier(decl.name)) { - return decl.name; - } - return decl; -} - -function readStringType(type: ts.TypeNode): string|null { - if (!ts.isLiteralTypeNode(type) || !ts.isStringLiteral(type.literal)) { - return null; - } - return type.literal.text; -} - -function readStringMapType(type: ts.TypeNode): {[key: string]: string} { - if (!ts.isTypeLiteralNode(type)) { - return {}; - } - const obj: {[key: string]: string} = {}; - type.members.forEach(member => { - if (!ts.isPropertySignature(member) || member.type === undefined || member.name === undefined || - !ts.isStringLiteral(member.name)) { - return; - } - const value = readStringType(member.type); - if (value === null) { - return null; - } - obj[member.name.text] = value; - }); - return obj; -} - -function readStringArrayType(type: ts.TypeNode): string[] { - if (!ts.isTupleTypeNode(type)) { - return []; - } - const res: string[] = []; - type.elementTypes.forEach(el => { - if (!ts.isLiteralTypeNode(el) || !ts.isStringLiteral(el.literal)) { - return; - } - res.push(el.literal.text); - }); - return res; -} diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts index 1bd49291ce..0dc41be777 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -213,20 +213,3 @@ export function forwardRefResolver( } return expandForwardRef(args[0]); } - -export function extractDirectiveGuards(node: ts.Declaration, reflector: ReflectionHost): { - ngTemplateGuards: string[], - hasNgTemplateContextGuard: boolean, -} { - const methods = nodeStaticMethodNames(node, reflector); - const ngTemplateGuards = methods.filter(method => method.startsWith('ngTemplateGuard_')) - .map(method => method.split('_', 2)[1]); - const hasNgTemplateContextGuard = methods.some(name => name === 'ngTemplateContextGuard'); - return {hasNgTemplateContextGuard, ngTemplateGuards}; -} - -function nodeStaticMethodNames(node: ts.Declaration, reflector: ReflectionHost): string[] { - return reflector.getMembersOfClass(node) - .filter(member => member.kind === ClassMemberKind.Method && member.isStatic) - .map(member => member.name); -} diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel index b179d74b6b..cf13057e6a 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel @@ -18,6 +18,7 @@ ts_library( "//packages/compiler-cli/src/ngtsc/partial_evaluator", "//packages/compiler-cli/src/ngtsc/path", "//packages/compiler-cli/src/ngtsc/reflection", + "//packages/compiler-cli/src/ngtsc/scope", "//packages/compiler-cli/src/ngtsc/testing", "//packages/compiler-cli/src/ngtsc/translator", "//packages/compiler-cli/src/ngtsc/util", diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts index f8a65e2a4b..fb584e301c 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts @@ -13,10 +13,10 @@ import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {ModuleResolver, ReferenceEmitter} from '../../imports'; import {PartialEvaluator} from '../../partial_evaluator'; import {TypeScriptReflectionHost} from '../../reflection'; +import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {ResourceLoader} from '../src/api'; import {ComponentDecoratorHandler} from '../src/component'; -import {SelectorScopeRegistry} from '../src/selector_scope'; export class NoopResourceLoader implements ResourceLoader { resolve(): string { throw new Error('Not implemented.'); } @@ -48,11 +48,13 @@ describe('ComponentDecoratorHandler', () => { const moduleResolver = new ModuleResolver(program, options, host); const importGraph = new ImportGraph(moduleResolver); const cycleAnalyzer = new CycleAnalyzer(importGraph); + const scopeRegistry = + new LocalModuleScopeRegistry(new MetadataDtsModuleScopeResolver(checker, reflectionHost)); + const refEmitter = new ReferenceEmitter([]); const handler = new ComponentDecoratorHandler( - reflectionHost, evaluator, - new SelectorScopeRegistry(checker, reflectionHost, new ReferenceEmitter([])), false, - new NoopResourceLoader(), [''], false, true, moduleResolver, cycleAnalyzer); + reflectionHost, evaluator, scopeRegistry, false, new NoopResourceLoader(), [''], false, + true, moduleResolver, cycleAnalyzer, refEmitter); const TestCmp = getDeclaration(program, 'entry.ts', 'TestCmp', ts.isClassDeclaration); 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 769ac30700..87f8ca8c90 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/test/directive_spec.ts @@ -8,17 +8,16 @@ import * as ts from 'typescript'; -import {ReferenceEmitter} from '../../imports'; import {PartialEvaluator} from '../../partial_evaluator'; import {TypeScriptReflectionHost} from '../../reflection'; +import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope'; import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; import {DirectiveDecoratorHandler} from '../src/directive'; -import {SelectorScopeRegistry} from '../src/selector_scope'; describe('DirectiveDecoratorHandler', () => { it('should use the `ReflectionHost` to detect class inheritance', () => { - const {program, options, host} = makeProgram([ + const {program} = makeProgram([ { name: 'node_modules/@angular/core/index.d.ts', contents: 'export const Directive: any;', @@ -40,9 +39,9 @@ describe('DirectiveDecoratorHandler', () => { const checker = program.getTypeChecker(); const reflectionHost = new TestReflectionHost(checker); const evaluator = new PartialEvaluator(reflectionHost, checker); - const handler = new DirectiveDecoratorHandler( - reflectionHost, evaluator, - new SelectorScopeRegistry(checker, reflectionHost, new ReferenceEmitter([])), false); + const scopeRegistry = + new LocalModuleScopeRegistry(new MetadataDtsModuleScopeResolver(checker, reflectionHost)); + const handler = new DirectiveDecoratorHandler(reflectionHost, evaluator, scopeRegistry, false); const analyzeDirective = (dirName: string) => { const DirNode = getDeclaration(program, 'entry.ts', dirName, ts.isClassDeclaration); diff --git a/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts b/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts deleted file mode 100644 index 92f231950f..0000000000 --- a/packages/compiler-cli/src/ngtsc/annotations/test/selector_scope_spec.ts +++ /dev/null @@ -1,184 +0,0 @@ -/** - * @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 {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, Reference, ReferenceEmitter} from '../../imports'; -import {LogicalFileSystem} from '../../path'; -import {TypeScriptReflectionHost} from '../../reflection'; -import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript'; -import {getRootDirs} from '../../util/src/typescript'; -import {SelectorScopeRegistry} from '../src/selector_scope'; - -describe('SelectorScopeRegistry', () => { - it('absolute imports work', () => { - const {program, options, host} = makeProgram([ - { - name: 'node_modules/@angular/core/index.d.ts', - contents: ` - export interface NgComponentDefWithMeta {} - export interface NgModuleDef {} - ` - }, - { - name: 'node_modules/some_library/index.d.ts', - contents: ` - import {NgModuleDef} from '@angular/core'; - import * as i0 from './component'; - export {SomeCmp} from './component'; - - export declare class SomeModule { - static ngModuleDef: NgModuleDef; - } - ` - }, - { - name: 'node_modules/some_library/component.d.ts', - contents: ` - import {NgComponentDefWithMeta} from '@angular/core'; - - export declare class SomeCmp { - static ngComponentDef: NgComponentDefWithMeta; - } - ` - }, - { - name: 'entry.ts', - contents: ` - export class ProgramCmp {} - export class ProgramModule {} - ` - }, - ]); - const checker = program.getTypeChecker(); - const reflectionHost = new TypeScriptReflectionHost(checker); - const ProgramModule = - getDeclaration(program, 'entry.ts', 'ProgramModule', ts.isClassDeclaration); - const ProgramCmp = getDeclaration(program, 'entry.ts', 'ProgramCmp', ts.isClassDeclaration); - const SomeModule = getDeclaration( - program, 'node_modules/some_library/index.d.ts', 'SomeModule', ts.isClassDeclaration); - expect(ProgramModule).toBeDefined(); - expect(SomeModule).toBeDefined(); - - const ProgramCmpRef = new Reference(ProgramCmp); - const refEmitter = makeReferenceEmitter(program, checker, options, host); - const registry = new SelectorScopeRegistry(checker, reflectionHost, refEmitter); - - registry.registerModule(ProgramModule, { - declarations: [new Reference(ProgramCmp)], - exports: [], - imports: [new Reference( - SomeModule, - {specifier: 'some_library', resolutionContext: '/node_modules/some_library/index.d.ts'})], - }); - - const ref = new Reference(ProgramCmp); - registry.registerDirective(ProgramCmp, { - name: 'ProgramCmp', - ref: ProgramCmpRef, - directive: ProgramCmpRef, - selector: 'program-cmp', - isComponent: true, - exportAs: null, - inputs: {}, - outputs: {}, - queries: [], - hasNgTemplateContextGuard: false, - ngTemplateGuards: [], - }); - - const scope = registry.lookupCompilationScope(ProgramCmp) !; - expect(scope).toBeDefined(); - expect(scope.directives).toBeDefined(); - expect(scope.directives.length).toBe(2); - }); - - it('exports of third-party libs work', () => { - const {program, options, host} = makeProgram([ - { - name: 'node_modules/@angular/core/index.d.ts', - contents: ` - export interface NgComponentDefWithMeta {} - export interface NgModuleDef {} - ` - }, - { - name: 'node_modules/some_library/index.d.ts', - contents: ` - import {NgComponentDefWithMeta, NgModuleDef} from '@angular/core'; - - export declare class SomeModule { - static ngModuleDef: NgModuleDef; - } - - export declare class SomeCmp { - static ngComponentDef: NgComponentDefWithMeta; - } - ` - }, - { - name: 'entry.ts', - contents: ` - export class ProgramCmp {} - export class ProgramModule {} - ` - }, - ]); - const checker = program.getTypeChecker(); - const reflectionHost = new TypeScriptReflectionHost(checker); - const ProgramModule = - getDeclaration(program, 'entry.ts', 'ProgramModule', ts.isClassDeclaration); - const ProgramCmp = getDeclaration(program, 'entry.ts', 'ProgramCmp', ts.isClassDeclaration); - const SomeModule = getDeclaration( - program, 'node_modules/some_library/index.d.ts', 'SomeModule', ts.isClassDeclaration); - expect(ProgramModule).toBeDefined(); - expect(SomeModule).toBeDefined(); - - const ProgramCmpRef = new Reference(ProgramCmp); - const refEmitter = makeReferenceEmitter(program, checker, options, host); - const registry = new SelectorScopeRegistry(checker, reflectionHost, refEmitter); - - registry.registerModule(ProgramModule, { - declarations: [new Reference(ProgramCmp)], - exports: [new Reference( - SomeModule, - {specifier: 'some_library', resolutionContext: '/node_modules/some_library/index.d.ts'})], - imports: [], - }); - - registry.registerDirective(ProgramCmp, { - name: 'ProgramCmp', - ref: ProgramCmpRef, - directive: ProgramCmpRef, - selector: 'program-cmp', - isComponent: true, - exportAs: null, - inputs: {}, - outputs: {}, - queries: [], - hasNgTemplateContextGuard: false, - ngTemplateGuards: [], - }); - - const scope = registry.lookupCompilationScope(ProgramCmp) !; - expect(scope).toBeDefined(); - expect(scope.directives).toBeDefined(); - expect(scope.directives.length).toBe(2); - }); -}); - -function makeReferenceEmitter( - program: ts.Program, checker: ts.TypeChecker, options: ts.CompilerOptions, - host: ts.CompilerHost): ReferenceEmitter { - const rootDirs = getRootDirs(host, options); - return new ReferenceEmitter([ - new LocalIdentifierStrategy(), - new AbsoluteModuleStrategy(program, checker, options, host), - new LogicalProjectStrategy(checker, new LogicalFileSystem(rootDirs)), - ]); -} diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index ed44cdb0a7..f6ea81d4a7 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -12,18 +12,18 @@ import * as ts from 'typescript'; import * as api from '../transformers/api'; import {nocollapseHack} from '../transformers/nocollapse_hack'; -import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, NoopReferencesRegistry, PipeDecoratorHandler, ReferencesRegistry, SelectorScopeRegistry} from './annotations'; +import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, NoopReferencesRegistry, PipeDecoratorHandler, ReferencesRegistry} from './annotations'; import {BaseDefDecoratorHandler} from './annotations/src/base_def'; import {CycleAnalyzer, ImportGraph} from './cycles'; import {ErrorCode, ngErrorCode} from './diagnostics'; import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point'; -import {AbsoluteModuleStrategy, FileToModuleHost, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports'; -import {FileToModuleStrategy} from './imports/src/emitter'; +import {AbsoluteModuleStrategy, FileToModuleHost, FileToModuleStrategy, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, ReferenceEmitter} from './imports'; import {PartialEvaluator} from './partial_evaluator'; import {AbsoluteFsPath, LogicalFileSystem} from './path'; import {TypeScriptReflectionHost} from './reflection'; import {HostResourceLoader} from './resource_loader'; import {NgModuleRouteAnalyzer, entryPointKeyFor} from './routing'; +import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from './scope'; import {FactoryGenerator, FactoryInfo, GeneratedShimsHostWrapper, ShimGenerator, SummaryGenerator, generatedFactoryTransform} from './shims'; import {ivySwitchTransform} from './switch'; import {IvyCompilation, declarationTransformFactory, ivyTransformFactory} from './transform'; @@ -339,7 +339,9 @@ export class NgtscProgram implements api.Program { } const evaluator = new PartialEvaluator(this.reflector, checker); - const scopeRegistry = new SelectorScopeRegistry(checker, this.reflector, this.refEmitter); + const depScopeReader = new MetadataDtsModuleScopeResolver(checker, this.reflector); + const scopeRegistry = new LocalModuleScopeRegistry(depScopeReader); + // 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 @@ -360,7 +362,8 @@ export class NgtscProgram implements api.Program { new ComponentDecoratorHandler( this.reflector, evaluator, scopeRegistry, this.isCore, this.resourceManager, this.rootDirs, this.options.preserveWhitespaces || false, - this.options.i18nUseExternalIds !== false, this.moduleResolver, this.cycleAnalyzer), + this.options.i18nUseExternalIds !== false, this.moduleResolver, this.cycleAnalyzer, + this.refEmitter), new DirectiveDecoratorHandler(this.reflector, evaluator, scopeRegistry, this.isCore), new InjectableDecoratorHandler( this.reflector, this.isCore, this.options.strictInjectionParameters || false), diff --git a/packages/compiler-cli/src/ngtsc/scope/BUILD.bazel b/packages/compiler-cli/src/ngtsc/scope/BUILD.bazel new file mode 100644 index 0000000000..b1ae56776f --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/scope/BUILD.bazel @@ -0,0 +1,19 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") + +ts_library( + name = "scope", + srcs = glob([ + "index.ts", + "src/**/*.ts", + ]), + deps = [ + "//packages/compiler", + "//packages/compiler-cli/src/ngtsc/imports", + "//packages/compiler-cli/src/ngtsc/reflection", + "//packages/compiler-cli/src/ngtsc/typecheck", + "//packages/compiler-cli/src/ngtsc/util", + "@ngdeps//typescript", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/scope/index.ts b/packages/compiler-cli/src/ngtsc/scope/index.ts new file mode 100644 index 0000000000..30d09bc1a1 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/scope/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 {ExportScope, ScopeData, ScopeDirective, ScopePipe} 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 new file mode 100644 index 0000000000..deccd78622 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/scope/src/api.ts @@ -0,0 +1,56 @@ +/** + * @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 {TypeCheckableDirectiveMeta} from '../../typecheck'; + +/** + * Data for one of a given NgModule's scopes (either compilation scope or export scopes). + */ +export interface ScopeData { + /** + * Directives in the exported scope of the module. + */ + directives: ScopeDirective[]; + + /** + * Pipes in the exported scope of the module. + */ + pipes: ScopePipe[]; +} + +/** + * An export scope of an NgModule, containing the directives/pipes it contributes to other NgModules + * which import it. + */ +export interface ExportScope { + /** + * The scope exported by an NgModule, and available for import. + */ + 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 new file mode 100644 index 0000000000..bf557d7975 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/scope/src/dependency.ts @@ -0,0 +1,202 @@ +/** + * @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 {ReflectionHost} from '../../reflection'; + +import {ExportScope, ScopeDirective, ScopePipe} from './api'; +import {extractDirectiveGuards, extractReferencesFromType, readStringArrayType, readStringMapType, readStringType} from './util'; + +export interface DtsModuleScopeResolver { + resolve(ref: Reference): ExportScope|null; +} + +/** + * Reads Angular metadata from classes declared in .d.ts files and computes an `ExportScope`. + * + * Given an NgModule declared in a .d.ts file, this resolver can produce a transitive `ExportScope` + * of all of the directives/pipes it exports. It does this by reading metadata off of Ivy static + * fields on directives, components, pipes, and NgModules. + */ +export class MetadataDtsModuleScopeResolver { + /** + * Cache which holds fully resolved scopes for NgModule classes from .d.ts files. + */ + private cache = new Map(); + + constructor(private checker: ts.TypeChecker, private reflector: ReflectionHost) {} + + /** + * Resolve a `Reference`'d NgModule from a .d.ts file and produce a transitive `ExportScope` + * listing the directives and pipes which that NgModule exports to others. + * + * This operation relies on a `Reference` instead of a direct TypeScrpt node as the `Reference`s + * produced depend on how the original NgModule was imported. + */ + resolve(ref: Reference): ExportScope|null { + const clazz = ref.node; + if (!clazz.getSourceFile().isDeclarationFile) { + throw new Error( + `Debug error: DtsModuleScopeResolver.read(${ref.debugName} from ${clazz.getSourceFile().fileName}), but not a .d.ts file`); + } + + if (this.cache.has(clazz)) { + return this.cache.get(clazz) !; + } + + // Build up the export scope - those directives and pipes made visible by this module. + const directives: ScopeDirective[] = []; + const pipes: ScopePipe[] = []; + + const meta = this.readModuleMetadataFromClass(ref); + if (meta === null) { + this.cache.set(clazz, null); + return null; + } + + // Only the 'exports' field of the NgModule's metadata is important. Imports and declarations + // 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); + if (directive !== null) { + directives.push(directive); + continue; + } + + // Attempt to process the export as a pipe. + const pipe = this.readScopePipeFromClassWithDef(exportRef); + if (pipe !== null) { + pipes.push(pipe); + continue; + } + + // Attempt to process the export as a module. + const exportScope = this.resolve(exportRef); + if (exportScope !== null) { + // It is a module. Add exported directives and pipes to the current scope. + directives.push(...exportScope.exported.directives); + pipes.push(...exportScope.exported.pipes); + continue; + } + + // The export was not a directive, a pipe, or a module. This is an error. + // TODO(alxhub): produce a ts.Diagnostic + throw new Error(`Exported value ${exportRef.debugName} was not a directive, pipe, or module`); + } + + return { + exported: {directives, pipes}, + }; + } + + /** + * 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}; + } +} + +/** + * 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 new file mode 100644 index 0000000000..c2c4fb9e23 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/scope/src/local.ts @@ -0,0 +1,282 @@ +/** + * @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 {ExportScope, ScopeData, ScopeDirective, ScopePipe} from './api'; +import {DtsModuleScopeResolver} from './dependency'; + +export interface LocalNgModuleData { + declarations: Reference[]; + imports: Reference[]; + exports: Reference[]; +} + +/** + * A scope produced for an NgModule declared locally (in the current program being compiled). + * + * The `LocalModuleScope` contains the compilation scope, the transitive set of directives and pipes + * visible to any component declared in this module. It also contains an `ExportScope`, the + * transitive set of directives and pipes + */ +export interface LocalModuleScope extends ExportScope { compilation: ScopeData; } + +/** + * A registry which collects information about NgModules, Directives, Components, and Pipes which + * are local (declared in the ts.Program being compiled), and can produce `LocalModuleScope`s + * which summarize the compilation scope of a component. + * + * This class implements the logic of NgModule declarations, imports, and exports and can produce, + * for a given component, the set of directives and pipes which are "visible" in that component's + * template. + * + * The `LocalModuleScopeRegistry` has two "modes" of operation. During analysis, data for each + * individual NgModule, Directive, Component, and Pipe is added to the registry. No attempt is made + * to traverse or validate the NgModule graph (imports, exports, etc). After analysis, one of + * `getScopeOfModule` or `getScopeForComponent` can be called, which traverses the NgModule graph + * and applies the NgModule logic to generate a `LocalModuleScope`, the full scope for the given + * module or component. + */ +export class LocalModuleScopeRegistry { + /** + * 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 + * would invalidate the cached scope data. + */ + 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. + * + * As components and directives are not distinguished at the NgModule level, this map may also + * contain directives. This doesn't cause any problems but isn't useful as there is no concept of + * a directive's compilation scope. + */ + private declarationToModule = new Map(); + + /** + * A cache of calculated `LocalModuleScope`s for each NgModule declared in the current program. + */ + private cache = new Map(); + + /** + * Tracks whether a given component requires "remote scoping". + * + * Remote scoping is when the set of directives which apply to a given component is set in the + * NgModule's file instead of directly on the ngComponentDef (which is sometimes needed to get + * around cyclic import issues). This is not used in calculation of `LocalModuleScope`s, but is + * tracked here for convenience. + */ + private remoteScoping = new Set(); + + constructor(private dependencyScopeReader: DtsModuleScopeResolver) {} + + /** + * Add an NgModule's data to the registry. + */ + registerNgModule(clazz: ts.Declaration, data: LocalNgModuleData): void { + this.assertCollecting(); + this.ngModuleData.set(clazz, data); + for (const decl of data.declarations) { + this.declarationToModule.set(decl.node, clazz); + } + } + + registerDirective(directive: ScopeDirective): void { + this.assertCollecting(); + this.directiveData.set(directive.ref.node, directive); + } + + registerPipe(pipe: ScopePipe): void { + this.assertCollecting(); + this.pipeData.set(pipe.ref.node, pipe); + } + + getScopeForComponent(clazz: ts.ClassDeclaration): LocalModuleScope|null { + if (!this.declarationToModule.has(clazz)) { + return null; + } + return this.getScopeOfModule(this.declarationToModule.get(clazz) !); + } + + /** + * Collects registered data for a module and its directives/pipes and convert it into a full + * `LocalModuleScope`. + * + * This method implements the logic of NgModule imports and exports. + */ + getScopeOfModule(clazz: ts.Declaration): LocalModuleScope|null { + // 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 + // cannot be produced. + if (!this.ngModuleData.has(clazz)) { + return null; + } + const ngModule = this.ngModuleData.get(clazz) !; + + // At this point, the goal is to produce two distinct transitive sets: + // - the directives and pipes which are visible to components declared in the NgModule. + // - 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(); + + // Directives and pipes exported to any importing NgModules. + 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. + // 2) Add all of the directives/pipes from each NgModule imported into the current one to the + // compilation scope. At this point, the compilation scope is complete. + // 3) For each entry in the NgModule's exports: + // a) Attempt to resolve it as an NgModule with its own exported directives/pipes. If it is + // one, add them to the export scope of this NgModule. + // b) Otherwise, it should be a class in the compilation scope of this NgModule. If it is, + // add it to the export scope. + // c) If it's neither an NgModule nor a directive/pipe in the compilation scope, then this + // is an error. + + // 1) add declarations. + for (const decl of ngModule.declarations) { + if (this.directiveData.has(decl.node)) { + const directive = this.directiveData.get(decl.node) !; + compilationDirectives.set( + decl.node, {...directive, ref: decl as Reference}); + } else if (this.pipeData.has(decl.node)) { + const pipe = this.pipeData.get(decl.node) !; + compilationPipes.set(decl.node, {...pipe, ref: decl}); + } else { + // TODO(alxhub): produce a ts.Diagnostic. This can't be an error right now since some + // ngtools tests rely on analysis of broken components. + continue; + } + } + + // 2) process imports. + for (const decl of ngModule.imports) { + const importScope = this.getExportedScope(decl); + if (importScope === null) { + // TODO(alxhub): produce a ts.Diagnostic + throw new Error(`Unknown import: ${decl.debugName}`); + } + for (const directive of importScope.exported.directives) { + compilationDirectives.set(directive.ref.node, directive); + } + for (const pipe of importScope.exported.pipes) { + compilationPipes.set(pipe.ref.node, pipe); + } + } + + // 3) process exports. + // Exports can contain modules, components, or directives. They're processed differently. + // Modules are straightforward. Directives and pipes from exported modules are added to the + // export maps. Directives/pipes are different - they might be exports of declared types or + // imported types. + for (const decl of ngModule.exports) { + // Attempt to resolve decl as an NgModule. + const importScope = this.getExportedScope(decl); + if (importScope !== null) { + // decl is an NgModule. + for (const directive of importScope.exported.directives) { + exportDirectives.set(directive.ref.node, directive); + } + for (const pipe of importScope.exported.pipes) { + exportPipes.set(pipe.ref.node, pipe); + } + } else if (compilationDirectives.has(decl.node)) { + // decl is a directive or component in the compilation scope of this NgModule. + const directive = compilationDirectives.get(decl.node) !; + exportDirectives.set(decl.node, directive); + } else if (compilationPipes.has(decl.node)) { + // decl is a pipe in the compilation scope of this NgModule. + const pipe = compilationPipes.get(decl.node) !; + exportPipes.set(decl.node, pipe); + } else { + // decl is an unknown export. + // TODO(alxhub): produce a ts.Diagnostic + throw new Error(`Unknown export: ${decl.debugName}`); + } + } + + // Finally, produce the `LocalModuleScope` with both the compilation and export scopes. + const scope = { + compilation: { + directives: Array.from(compilationDirectives.values()), + pipes: Array.from(compilationPipes.values()), + }, + exported: { + directives: Array.from(exportDirectives.values()), + pipes: Array.from(exportPipes.values()), + }, + }; + this.cache.set(clazz, scope); + return scope; + } + + /** + * Check whether a component requires remote scoping. + */ + getRequiresRemoteScope(node: ts.Declaration): boolean { return this.remoteScoping.has(node); } + + /** + * Set a component as requiring remote scoping. + */ + setComponentAsRequiringRemoteScoping(node: ts.Declaration): void { this.remoteScoping.add(node); } + + /** + * Look up the `ExportScope` of a given `Reference` to an NgModule. + * + * The NgModule in question may be declared locally in the current ts.Program, or it may be + * declared in a .d.ts file. + */ + private getExportedScope(ref: Reference): ExportScope|null { + if (ref.node.getSourceFile().isDeclarationFile) { + // The NgModule is declared in a .d.ts file. Resolve it with the `DependencyScopeReader`. + if (!ts.isClassDeclaration(ref.node)) { + // TODO(alxhub): produce a ts.Diagnostic + throw new Error(`Reference to an NgModule ${ref.debugName} which isn't a class?`); + } + return this.dependencyScopeReader.resolve(ref as Reference); + } else { + // The NgModule is declared locally in the current program. Resolve it from the registry. + return this.getScopeOfModule(ref.node); + } + } + + private assertCollecting(): void { + if (this.sealed) { + throw new Error(`Assertion: LocalModuleScopeRegistry is not COLLECTING`); + } + } +} diff --git a/packages/compiler-cli/src/ngtsc/scope/src/util.ts b/packages/compiler-cli/src/ngtsc/scope/src/util.ts new file mode 100644 index 0000000000..fbff69b526 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/scope/src/util.ts @@ -0,0 +1,95 @@ +/** + * @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 {ClassMemberKind, ReflectionHost, reflectTypeEntityToDeclaration} from '../../reflection'; +import {nodeDebugInfo} from '../../util/src/typescript'; + +export function extractReferencesFromType( + checker: ts.TypeChecker, def: ts.TypeNode, ngModuleImportedFrom: string | null, + resolutionContext: string): Reference[] { + if (!ts.isTupleTypeNode(def)) { + return []; + } + return def.elementTypes.map(element => { + if (!ts.isTypeQueryNode(element)) { + throw new Error(`Expected TypeQueryNode: ${nodeDebugInfo(element)}`); + } + const type = element.exprName; + const {node, from} = reflectTypeEntityToDeclaration(type, checker); + if (!ts.isClassDeclaration(node)) { + throw new Error(`Expected ClassDeclaration: ${nodeDebugInfo(node)}`); + } + const specifier = (from !== null && !from.startsWith('.') ? from : ngModuleImportedFrom); + if (specifier !== null) { + return new Reference(node, {specifier, resolutionContext}); + } else { + return new Reference(node); + } + }); +} + +export function readStringType(type: ts.TypeNode): string|null { + if (!ts.isLiteralTypeNode(type) || !ts.isStringLiteral(type.literal)) { + return null; + } + return type.literal.text; +} + +export function readStringMapType(type: ts.TypeNode): {[key: string]: string} { + if (!ts.isTypeLiteralNode(type)) { + return {}; + } + const obj: {[key: string]: string} = {}; + type.members.forEach(member => { + if (!ts.isPropertySignature(member) || member.type === undefined || member.name === undefined || + !ts.isStringLiteral(member.name)) { + return; + } + const value = readStringType(member.type); + if (value === null) { + return null; + } + obj[member.name.text] = value; + }); + return obj; +} + +export function readStringArrayType(type: ts.TypeNode): string[] { + if (!ts.isTupleTypeNode(type)) { + return []; + } + const res: string[] = []; + type.elementTypes.forEach(el => { + if (!ts.isLiteralTypeNode(el) || !ts.isStringLiteral(el.literal)) { + return; + } + res.push(el.literal.text); + }); + return res; +} + + +export function extractDirectiveGuards(node: ts.Declaration, reflector: ReflectionHost): { + ngTemplateGuards: string[], + hasNgTemplateContextGuard: boolean, +} { + const methods = nodeStaticMethodNames(node, reflector); + const ngTemplateGuards = methods.filter(method => method.startsWith('ngTemplateGuard_')) + .map(method => method.split('_', 2)[1]); + const hasNgTemplateContextGuard = methods.some(name => name === 'ngTemplateContextGuard'); + return {hasNgTemplateContextGuard, ngTemplateGuards}; +} + +function nodeStaticMethodNames(node: ts.Declaration, reflector: ReflectionHost): string[] { + return reflector.getMembersOfClass(node) + .filter(member => member.kind === ClassMemberKind.Method && member.isStatic) + .map(member => member.name); +} diff --git a/packages/compiler-cli/src/ngtsc/scope/test/BUILD.bazel b/packages/compiler-cli/src/ngtsc/scope/test/BUILD.bazel new file mode 100644 index 0000000000..07e44cf254 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/scope/test/BUILD.bazel @@ -0,0 +1,28 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "jasmine_node_test", "ts_library") + +ts_library( + name = "test_lib", + testonly = True, + srcs = glob([ + "**/*.ts", + ]), + deps = [ + "//packages:types", + "//packages/compiler-cli/src/ngtsc/imports", + "//packages/compiler-cli/src/ngtsc/reflection", + "//packages/compiler-cli/src/ngtsc/scope", + "//packages/compiler-cli/src/ngtsc/testing", + "@ngdeps//typescript", + ], +) + +jasmine_node_test( + name = "test", + bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"], + deps = [ + ":test_lib", + "//tools/testing:node_no_angular", + ], +) diff --git a/packages/compiler-cli/src/ngtsc/scope/test/dependency_spec.ts b/packages/compiler-cli/src/ngtsc/scope/test/dependency_spec.ts new file mode 100644 index 0000000000..f045170d92 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/scope/test/dependency_spec.ts @@ -0,0 +1,144 @@ +/** + * @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 {TypeScriptReflectionHost} from '../../reflection'; +import {makeProgram} from '../../testing/in_memory_typescript'; + +import {ExportScope} from '../src/api'; +import {MetadataDtsModuleScopeResolver} from '../src/dependency'; + +const MODULE_FROM_NODE_MODULES_PATH = /.*node_modules\/(\w+)\/index\.d\.ts$/; + +/** + * Simple metadata types are added to the top of each testing file, for convenience. + */ +const PROLOG = ` +export declare type ModuleMeta = never; +export declare type ComponentMeta = never; +export declare type DirectiveMeta = never; +export declare type PipeMeta = never; +`; + +/** + * Construct the testing environment with a given set of absolute modules and their contents. + * + * This returns both the `MetadataDtsModuleScopeResolver` and a `refs` object which can be + * destructured to retrieve references to specific declared classes. + */ +function makeTestEnv(modules: {[module: string]: string}): { + refs: {[name: string]: Reference}, + resolver: MetadataDtsModuleScopeResolver, +} { + // Map the modules object to an array of files for `makeProgram`. + const files = Object.keys(modules).map(moduleName => { + return { + name: `node_modules/${moduleName}/index.d.ts`, + contents: PROLOG + (modules as any)[moduleName], + }; + }); + const {program} = makeProgram(files); + const checker = program.getTypeChecker(); + const resolver = + new MetadataDtsModuleScopeResolver(checker, new TypeScriptReflectionHost(checker)); + + // Resolver for the refs object. + const get = (target: {}, name: string): Reference => { + for (const sf of program.getSourceFiles()) { + const symbol = checker.getSymbolAtLocation(sf) !; + const exportedSymbol = symbol.exports !.get(name as ts.__String); + if (exportedSymbol !== undefined) { + const decl = exportedSymbol.valueDeclaration as ts.ClassDeclaration; + const specifier = MODULE_FROM_NODE_MODULES_PATH.exec(sf.fileName) ![1]; + return new Reference(decl, {specifier, resolutionContext: sf.fileName}); + } + } + throw new Error('Class not found: ' + name); + }; + + return { + resolver, + refs: new Proxy({}, {get}), + }; +} + +describe('MetadataDtsModuleScopeResolver', () => { + it('should produce an accurate scope for a basic NgModule', () => { + const {resolver, refs} = makeTestEnv({ + 'test': ` + export declare class Dir { + static ngDirectiveDef: DirectiveMeta; + } + + export declare class Module { + static ngModuleDef: ModuleMeta; + } + ` + }); + const {Dir, Module} = refs; + const scope = resolver.resolve(Module) !; + expect(scopeToRefs(scope)).toEqual([Dir]); + }); + + it('should produce an accurate scope when a module is exported', () => { + const {resolver, refs} = makeTestEnv({ + 'test': ` + export declare class Dir { + static ngDirectiveDef: DirectiveMeta; + } + + export declare class ModuleA { + static ngModuleDef: ModuleMeta; + } + + export declare class ModuleB { + static ngModuleDef: ModuleMeta; + } + ` + }); + const {Dir, ModuleB} = refs; + const scope = resolver.resolve(ModuleB) !; + expect(scopeToRefs(scope)).toEqual([Dir]); + }); + + it('should resolve correctly across modules', () => { + const {resolver, refs} = makeTestEnv({ + 'declaration': ` + export declare class Dir { + static ngDirectiveDef: DirectiveMeta; + } + + export declare class ModuleA { + static ngModuleDef: ModuleMeta; + } + `, + 'exported': ` + import * as d from 'declaration'; + + export declare class ModuleB { + static ngModuleDef: ModuleMeta; + } + ` + }); + const {Dir, ModuleB} = refs; + const scope = resolver.resolve(ModuleB) !; + expect(scopeToRefs(scope)).toEqual([Dir]); + + // Explicitly verify that the directive has the correct owning module. + expect(scope.exported.directives[0].ref.ownedByModuleGuess).toBe('declaration'); + }); +}); + +function scopeToRefs(scope: ExportScope): Reference[] { + const directives = scope.exported.directives.map(dir => dir.ref); + const pipes = scope.exported.pipes.map(pipe => pipe.ref); + return [...directives, ...pipes].sort((a, b) => a.debugName !.localeCompare(b.debugName !)); +} diff --git a/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts b/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts new file mode 100644 index 0000000000..94803ce348 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/scope/test/local_spec.ts @@ -0,0 +1,159 @@ +/** + * @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 {ScopeData, ScopeDirective, ScopePipe} from '../src/api'; +import {DtsModuleScopeResolver} from '../src/dependency'; +import {LocalModuleScopeRegistry} from '../src/local'; + +function registerFakeRefs(registry: LocalModuleScopeRegistry): + {[name: string]: Reference} { + const get = (target: {}, name: string): Reference => { + const sf = ts.createSourceFile( + name + '.ts', `export class ${name} {}`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); + const clazz = sf.statements[0] as ts.ClassDeclaration; + const ref = new Reference(clazz); + if (name.startsWith('Dir') || name.startsWith('Cmp')) { + registry.registerDirective(fakeDirective(ref)); + } else if (name.startsWith('Pipe')) { + registry.registerPipe(fakePipe(ref)); + } + return ref; + }; + return new Proxy({}, {get}); +} + +describe('LocalModuleScopeRegistry', () => { + let registry !: LocalModuleScopeRegistry; + + beforeEach(() => { registry = new LocalModuleScopeRegistry(new MockDtsModuleScopeResolver()); }); + + it('should produce an accurate LocalModuleScope for a basic NgModule', () => { + const {Dir1, Dir2, Pipe1, Module} = registerFakeRefs(registry); + + registry.registerNgModule(Module.node, { + imports: [], + declarations: [Dir1, Dir2, Pipe1], + exports: [Dir1, Pipe1], + }); + + const scope = registry.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); + + 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: []}); + + const scopeA = registry.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); + + registry.registerNgModule(ModuleA.node, {exports: [ModuleB], imports: [], declarations: []}); + registry.registerNgModule(ModuleB.node, {declarations: [Dir], exports: [Dir], imports: []}); + + const scopeA = registry.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); + + registry.registerNgModule(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]}); + + const scope = registry.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 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 + // using it. This emulates what happens when an NgModule declares a Directive. + const idVar = idSf.statements[0] as ts.VariableStatement; + 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]}); + + const scope = registry.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); + + registry.registerNgModule(ModuleA.node, {exports: [Dir], imports: [ModuleB], declarations: []}); + registry.registerNgModule(ModuleB.node, {declarations: [Dir], exports: [Dir], imports: []}); + + const scopeA = registry.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); + + registry.registerNgModule(ModuleA.node, {exports: [Dir], imports: [], declarations: []}); + registry.registerNgModule(ModuleB.node, {declarations: [Dir], exports: [Dir], imports: []}); + + expect(() => registry.getScopeOfModule(ModuleA.node)).toThrow(); + }); +}); + +function fakeDirective(ref: Reference): ScopeDirective { + const name = ref.debugName !; + return { + ref, + name, + selector: `[${ref.debugName}]`, + isComponent: name.startsWith('Cmp'), + inputs: {}, + outputs: {}, + exportAs: null, + queries: [], + hasNgTemplateContextGuard: false, + ngTemplateGuards: [], + }; +} + +function fakePipe(ref: Reference): ScopePipe { + const name = ref.debugName !; + return {ref, name}; +} + +class MockDtsModuleScopeResolver implements DtsModuleScopeResolver { + resolve(ref: Reference): null { return null; } +} + +function scopeToRefs(scopeData: ScopeData): Reference[] { + const directives = scopeData.directives.map(dir => dir.ref); + const pipes = scopeData.pipes.map(pipe => pipe.ref); + return [...directives, ...pipes].sort((a, b) => a.debugName !.localeCompare(b.debugName !)); +} diff --git a/packages/compiler-cli/src/ngtsc/util/src/typescript.ts b/packages/compiler-cli/src/ngtsc/util/src/typescript.ts index db4860a429..d672cc651b 100644 --- a/packages/compiler-cli/src/ngtsc/util/src/typescript.ts +++ b/packages/compiler-cli/src/ngtsc/util/src/typescript.ts @@ -80,3 +80,9 @@ export function getRootDirs(host: ts.CompilerHost, options: ts.CompilerOptions): } return rootDirs.map(rootDir => AbsoluteFsPath.fromUnchecked(rootDir)); } + +export function nodeDebugInfo(node: ts.Node): string { + const sf = getSourceFile(node); + const {line, character} = ts.getLineAndCharacterOfPosition(sf, node.pos); + return `[${sf.fileName}: ${ts.SyntaxKind[node.kind]} @ ${line}:${character}]`; +} diff --git a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts index 98bfd8595f..007bd762a8 100644 --- a/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts +++ b/packages/compiler-cli/test/compliance/r3_view_compiler_styling_spec.ts @@ -1173,9 +1173,7 @@ describe('compiler compliance: styling', () => { @Component({ selector: 'my-component', - template: ' -
- ', + template: '
', }) export class MyComponent { } diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 14dcf54386..b26110382a 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -1913,8 +1913,8 @@ describe('ngtsc behavioral tests', () => { env.driveMain(); const jsContents = env.getContents('test.js'); expect(jsContents) - .toContain( - 'i0.ɵsetComponentScope(NormalComponent, [i1.NormalComponent, CyclicComponent], [])'); + .toMatch( + /i\d\.ɵsetComponentScope\(NormalComponent,\s+\[NormalComponent,\s+CyclicComponent\],\s+\[\]\)/); expect(jsContents).not.toContain('/*__PURE__*/ i0.ɵsetComponentScope'); }); }); @@ -2565,12 +2565,13 @@ describe('ngtsc behavioral tests', () => { beforeEach(() => { env.tsconfig(); env.write('node_modules/@angular/router/index.d.ts', ` - import {ModuleWithProviders} from '@angular/core'; + import {ModuleWithProviders, ɵNgModuleDefWithMeta as NgModuleDefWithMeta} from '@angular/core'; export declare var ROUTES; export declare class RouterModule { static forRoot(arg1: any, arg2: any): ModuleWithProviders; static forChild(arg1: any): ModuleWithProviders; + static ngModuleDef: NgModuleDefWithMeta } `); });