diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index 7c2d673034f7b..9de9616075915 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -16,6 +16,7 @@ import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from import {DependencyTracker} from '../../incremental/api'; import {IndexingContext} from '../../indexer'; import {ClassPropertyMapping, DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../metadata'; +import {flattenInheritedDirectiveMetadata} from '../../metadata/src/inheritance'; import {EnumValue, PartialEvaluator} from '../../partial_evaluator'; import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection'; import {ComponentScopeReader, LocalModuleScopeRegistry} from '../../scope'; @@ -30,7 +31,6 @@ import {createValueHasWrongTypeError, getDirectiveDiagnostics, getProviderDiagno import {extractDirectiveMetadata, parseFieldArrayValue} from './directive'; import {compileNgFactoryDefField} from './factory'; import {generateSetClassMetadataCall} from './metadata'; -import {TypeCheckScopes} from './typecheck_scopes'; import {findAngularDecorator, isAngularCoreReference, isExpressionForwardReference, readBaseClass, resolveProvidersRequiringFactory, unwrapExpression, wrapFunctionExpressionsInParens} from './util'; const EMPTY_MAP = new Map(); @@ -95,7 +95,6 @@ export class ComponentDecoratorHandler implements private literalCache = new Map(); private elementSchemaRegistry = new DomElementSchemaRegistry(); - private typeCheckScopes = new TypeCheckScopes(this.scopeReader, this.metaReader); /** * During the asynchronous preanalyze phase, it's necessary to parse the template to extract @@ -424,15 +423,36 @@ export class ComponentDecoratorHandler implements return; } - const scope = this.typeCheckScopes.getTypeCheckScope(node); + const matcher = new SelectorMatcher(); + const pipes = new Map>>(); + let schemas: SchemaMetadata[] = []; + + const scope = this.scopeReader.getScopeForComponent(node); if (scope === 'error') { // Don't type-check components that had errors in their scopes. return; } - const binder = new R3TargetBinder(scope.matcher); + if (scope !== null) { + for (const meta of scope.compilation.directives) { + if (meta.selector !== null) { + const extMeta = flattenInheritedDirectiveMetadata(this.metaReader, meta.ref); + matcher.addSelectables(CssSelector.parse(meta.selector), extMeta); + } + } + for (const {name, ref} of scope.compilation.pipes) { + if (!ts.isClassDeclaration(ref.node)) { + throw new Error(`Unexpected non-class declaration ${ + ts.SyntaxKind[ref.node.kind]} for pipe ${ref.debugName}`); + } + pipes.set(name, ref as Reference>); + } + schemas = scope.schemas; + } + + const binder = new R3TargetBinder(matcher); ctx.addTemplate( - new Reference(node), binder, meta.template.diagNodes, scope.pipes, scope.schemas, + new Reference(node), binder, meta.template.diagNodes, pipes, schemas, meta.template.sourceMapping, meta.template.file); } @@ -475,49 +495,36 @@ 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. - type MatchedDirective = DirectiveMeta&{selector: string}; - const matcher = new SelectorMatcher(); + const matcher = new SelectorMatcher(); + const directives: {selector: string, expression: Expression}[] = []; for (const dir of scope.compilation.directives) { - if (dir.selector !== null) { - matcher.addSelectables(CssSelector.parse(dir.selector), dir as MatchedDirective); + const {ref, selector} = dir; + if (selector !== null) { + const expression = this.refEmitter.emit(ref, context); + directives.push({selector, expression}); + matcher.addSelectables(CssSelector.parse(selector), {...dir, expression}); } } - const pipes = new Map>(); + const pipes = new Map(); for (const pipe of scope.compilation.pipes) { - pipes.set(pipe.name, pipe.ref); + pipes.set(pipe.name, this.refEmitter.emit(pipe.ref, context)); } - // Next, the component template AST is bound using the R3TargetBinder. This produces a + // Next, the component template AST is bound using the R3TargetBinder. This produces an // BoundTarget, which is similar to a ts.TypeChecker. const binder = new R3TargetBinder(matcher); const bound = binder.bind({template: metadata.template.nodes}); // The BoundTarget knows which directives and pipes matched the template. - const usedDirectives = bound.getUsedDirectives().map(directive => { - return { - selector: directive.selector, - expression: this.refEmitter.emit(directive.ref, context), - }; - }); - - const usedPipes: {pipeName: string, expression: Expression}[] = []; - for (const pipeName of bound.getUsedPipes()) { - if (!pipes.has(pipeName)) { - continue; - } - const pipe = pipes.get(pipeName)!; - usedPipes.push({ - pipeName, - expression: this.refEmitter.emit(pipe, context), - }); - } + const usedDirectives = bound.getUsedDirectives(); + const usedPipes = bound.getUsedPipes().map(name => pipes.get(name)!); // Scan through the directives/pipes actually used in the template and check whether any // import which needs to be generated would create a cycle. const cycleDetected = usedDirectives.some(dir => this._isCyclicImport(dir.expression, context)) || - usedPipes.some(pipe => this._isCyclicImport(pipe.expression, context)); + usedPipes.some(pipe => this._isCyclicImport(pipe, context)); if (!cycleDetected) { // No cycle was detected. Record the imports that need to be created in the cycle detector @@ -525,8 +532,8 @@ export class ComponentDecoratorHandler implements for (const {expression} of usedDirectives) { this._recordSyntheticImport(expression, context); } - for (const {expression} of usedPipes) { - this._recordSyntheticImport(expression, context); + for (const pipe of usedPipes) { + this._recordSyntheticImport(pipe, context); } // Check whether the directive/pipe arrays in ɵcmp need to be wrapped in closures. @@ -535,11 +542,16 @@ export class ComponentDecoratorHandler implements const wrapDirectivesAndPipesInClosure = usedDirectives.some( dir => isExpressionForwardReference(dir.expression, node.name, context)) || - usedPipes.some( - pipe => isExpressionForwardReference(pipe.expression, node.name, context)); - - data.directives = usedDirectives; - data.pipes = new Map(usedPipes.map(pipe => [pipe.pipeName, pipe.expression])); + usedPipes.some(pipe => isExpressionForwardReference(pipe, node.name, context)); + + // Actual compilation still uses the full scope, not the narrowed scope determined by + // R3TargetBinder. This is a hedge against potential issues with the R3TargetBinder - right + // now the TemplateDefinitionBuilder is the "source of truth" for which directives/pipes are + // actually used (though the two should agree perfectly). + // + // TODO(alxhub): switch TemplateDefinitionBuilder over to using R3TargetBinder directly. + data.directives = directives; + data.pipes = pipes; data.wrapDirectivesAndPipesInClosure = wrapDirectivesAndPipesInClosure; } else { // Declaring the directiveDefs/pipeDefs arrays directly would require imports that would diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/typecheck_scopes.ts b/packages/compiler-cli/src/ngtsc/annotations/src/typecheck_scopes.ts deleted file mode 100644 index aada9e0df4e55..0000000000000 --- a/packages/compiler-cli/src/ngtsc/annotations/src/typecheck_scopes.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * @license - * Copyright Google LLC 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 {CssSelector, SchemaMetadata, SelectorMatcher} from '@angular/compiler'; -import * as ts from 'typescript'; - -import {Reference} from '../../imports'; -import {DirectiveMeta, flattenInheritedDirectiveMetadata, MetadataReader} from '../../metadata'; -import {ClassDeclaration} from '../../reflection'; -import {ComponentScopeReader} from '../../scope'; - -/** - * The scope that is used for type-check code generation of a component template. - */ -export interface TypeCheckScope { - /** - * A `SelectorMatcher` instance that contains the flattened directive metadata of all directives - * that are in the compilation scope of the declaring NgModule. - */ - matcher: SelectorMatcher; - - /** - * The pipes that are available in the compilation scope. - */ - pipes: Map>>; - - /** - * The schemas that are used in this scope. - */ - schemas: SchemaMetadata[]; -} - -/** - * Computes scope information to be used in template type checking. - */ -export class TypeCheckScopes { - /** - * Cache of flattened directive metadata. Because flattened metadata is scope-invariant it's - * cached individually, such that all scopes refer to the same flattened metadata. - */ - private flattenedDirectiveMetaCache = new Map(); - - /** - * Cache of the computed type check scope per NgModule declaration. - */ - private scopeCache = new Map(); - - constructor(private scopeReader: ComponentScopeReader, private metaReader: MetadataReader) {} - - /** - * Computes the type-check scope information for the component declaration. If the NgModule - * contains an error, then 'error' is returned. If the component is not declared in any NgModule, - * an empty type-check scope is returned. - */ - getTypeCheckScope(node: ClassDeclaration): TypeCheckScope|'error' { - const matcher = new SelectorMatcher(); - const pipes = new Map>>(); - - const scope = this.scopeReader.getScopeForComponent(node); - if (scope === null) { - return {matcher, pipes, schemas: []}; - } else if (scope === 'error') { - return scope; - } - - if (this.scopeCache.has(scope.ngModule)) { - return this.scopeCache.get(scope.ngModule)!; - } - - for (const meta of scope.compilation.directives) { - if (meta.selector !== null) { - const extMeta = this.getInheritedDirectiveMetadata(meta.ref); - matcher.addSelectables(CssSelector.parse(meta.selector), extMeta); - } - } - - for (const {name, ref} of scope.compilation.pipes) { - if (!ts.isClassDeclaration(ref.node)) { - throw new Error(`Unexpected non-class declaration ${ - ts.SyntaxKind[ref.node.kind]} for pipe ${ref.debugName}`); - } - pipes.set(name, ref as Reference>); - } - - const typeCheckScope: TypeCheckScope = {matcher, pipes, schemas: scope.schemas}; - this.scopeCache.set(scope.ngModule, typeCheckScope); - return typeCheckScope; - } - - private getInheritedDirectiveMetadata(ref: Reference): DirectiveMeta { - const clazz = ref.node; - if (this.flattenedDirectiveMetaCache.has(clazz)) { - return this.flattenedDirectiveMetaCache.get(clazz)!; - } - - const meta = flattenInheritedDirectiveMetadata(this.metaReader, ref); - this.flattenedDirectiveMetaCache.set(clazz, meta); - return meta; - } -} diff --git a/packages/compiler-cli/src/ngtsc/metadata/index.ts b/packages/compiler-cli/src/ngtsc/metadata/index.ts index 9ee4e1a977ac4..2f857eee0bc40 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/index.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/index.ts @@ -8,7 +8,6 @@ export * from './src/api'; export {DtsMetadataReader} from './src/dts'; -export {flattenInheritedDirectiveMetadata} from './src/inheritance'; export {CompoundMetadataRegistry, LocalMetadataRegistry, InjectableClassRegistry} from './src/registry'; export {extractDirectiveTypeCheckMeta, CompoundMetadataReader} from './src/util'; export {BindingPropertyName, ClassPropertyMapping, ClassPropertyName, InputOrOutput} from './src/property_mapping'; diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts b/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts index b923de6e2a589..f5271f4351d32 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/inheritance.ts @@ -26,9 +26,6 @@ export function flattenInheritedDirectiveMetadata( if (topMeta === null) { throw new Error(`Metadata not found for directive: ${dir.debugName}`); } - if (topMeta.baseClass === null) { - return topMeta; - } const coercedInputFields = new Set(); const undeclaredInputFields = new Set(); diff --git a/packages/compiler-cli/src/ngtsc/scope/src/local.ts b/packages/compiler-cli/src/ngtsc/scope/src/local.ts index da7f3aa9397c1..985058a157d98 100644 --- a/packages/compiler-cli/src/ngtsc/scope/src/local.ts +++ b/packages/compiler-cli/src/ngtsc/scope/src/local.ts @@ -26,7 +26,6 @@ export interface LocalNgModuleData { } export interface LocalModuleScope extends ExportScope { - ngModule: ClassDeclaration; compilation: ScopeData; reexports: Reexport[]|null; schemas: SchemaMetadata[]; @@ -434,8 +433,7 @@ export class LocalModuleScopeRegistry implements MetadataRegistry, ComponentScop } // Finally, produce the `LocalModuleScope` with both the compilation and export scopes. - const scope: LocalModuleScope = { - ngModule: ngModule.ref.node, + const scope = { compilation: { directives: Array.from(compilationDirectives.values()), pipes: Array.from(compilationPipes.values()),