refactor(compiler-cli): make `TypeCheckingScopeRegistry` a general utility (#40032)

The `annotations` package in the compiler previously contained a registry
which tracks NgModule scopes for template type-checking, including unifying
all type-checking metadata across class inheritance lines.

This commit generalizes this utility and prepares it for use in the
`TemplateTypeChecker` as well, to back APIs used by the language service.

PR Close #40032
This commit is contained in:
Alex Rickabaugh 2020-12-03 10:52:42 -08:00
parent e42250f139
commit a543e69497
6 changed files with 69 additions and 35 deletions

View File

@ -16,7 +16,7 @@ import {absoluteFrom, absoluteFromSourceFile, dirname, FileSystem, LogicalFileSy
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, PrivateExportAliasingHost, Reexport, ReferenceEmitter} from '../../../src/ngtsc/imports';
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, ResourceRegistry} from '../../../src/ngtsc/metadata';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver, TypeCheckScopeRegistry} from '../../../src/ngtsc/scope';
import {DecoratorHandler} from '../../../src/ngtsc/transform';
import {NgccReflectionHost} from '../host/ngcc_host';
import {Migration} from '../migrations/migration';
@ -91,11 +91,13 @@ export class DecorationAnalyzer {
importGraph = new ImportGraph(this.moduleResolver);
cycleAnalyzer = new CycleAnalyzer(this.importGraph);
injectableRegistry = new InjectableClassRegistry(this.reflectionHost);
typeCheckScopeRegistry = new TypeCheckScopeRegistry(this.scopeRegistry, this.fullMetaReader);
handlers: DecoratorHandler<unknown, unknown, unknown>[] = [
new ComponentDecoratorHandler(
this.reflectionHost, this.evaluator, this.fullRegistry, this.fullMetaReader,
this.scopeRegistry, this.scopeRegistry, new ResourceRegistry(), this.isCore,
this.resourceManager, this.rootDirs, !!this.compilerOptions.preserveWhitespaces,
this.scopeRegistry, this.scopeRegistry, this.typeCheckScopeRegistry, new ResourceRegistry(),
this.isCore, this.resourceManager, this.rootDirs,
!!this.compilerOptions.preserveWhitespaces,
/* i18nUseExternalIds */ true, this.bundle.enableI18nLegacyMessageIdFormat,
/* usePoisonedData */ false,
/* i18nNormalizeLineEndingsInICUs */ false, this.moduleResolver, this.cycleAnalyzer,

View File

@ -18,7 +18,7 @@ import {IndexingContext} from '../../indexer';
import {ClassPropertyMapping, ComponentResources, DirectiveMeta, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, Resource, ResourceRegistry} from '../../metadata';
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
import {ClassDeclaration, DeclarationNode, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
import {ComponentScopeReader, LocalModuleScopeRegistry} from '../../scope';
import {ComponentScopeReader, LocalModuleScopeRegistry, TypeCheckScopeRegistry} from '../../scope';
import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerFlags, HandlerPrecedence, ResolveResult} from '../../transform';
import {TemplateSourceMapping, TypeCheckContext} from '../../typecheck/api';
import {getTemplateId, makeTemplateDiagnostic} from '../../typecheck/diagnostics';
@ -30,7 +30,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<string, Expression>();
@ -87,6 +86,7 @@ export class ComponentDecoratorHandler implements
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private metaRegistry: MetadataRegistry, private metaReader: MetadataReader,
private scopeReader: ComponentScopeReader, private scopeRegistry: LocalModuleScopeRegistry,
private typeCheckScopeRegistry: TypeCheckScopeRegistry,
private resourceRegistry: ResourceRegistry, private isCore: boolean,
private resourceLoader: ResourceLoader, private rootDirs: ReadonlyArray<string>,
private defaultPreserveWhitespaces: boolean, private i18nUseExternalIds: boolean,
@ -100,7 +100,6 @@ export class ComponentDecoratorHandler implements
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
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
@ -440,15 +439,14 @@ export class ComponentDecoratorHandler implements
typeCheck(ctx: TypeCheckContext, node: ClassDeclaration, meta: Readonly<ComponentAnalysisData>):
void {
if (!ts.isClassDeclaration(node)) {
if (this.typeCheckScopeRegistry === null || !ts.isClassDeclaration(node)) {
return;
}
if (meta.isPoisoned && !this.usePoisonedData) {
return;
}
const scope = this.typeCheckScopes.getTypeCheckScope(node);
const scope = this.typeCheckScopeRegistry.getTypeCheckScope(node);
if (scope.isPoisoned && !this.usePoisonedData) {
// Don't type-check components that had errors in their scopes, unless requested.
return;
@ -492,17 +490,17 @@ export class ComponentDecoratorHandler implements
// Determining this is challenging, because the TemplateDefinitionBuilder is responsible for
// matching directives and pipes in the template; however, that doesn't run until the actual
// compile() step. It's not possible to run template compilation sooner as it requires the
// ConstantPool for the overall file being compiled (which isn't available until the transform
// step).
// ConstantPool for the overall file being compiled (which isn't available until the
// transform step).
//
// Instead, directives/pipes are matched independently here, using the R3TargetBinder. This is
// an alternative implementation of template matching which is used for template type-checking
// and will eventually replace matching in the TemplateDefinitionBuilder.
// Instead, directives/pipes are matched independently here, using the R3TargetBinder. This
// is an alternative implementation of template matching which is used for template
// type-checking and will eventually replace matching in the TemplateDefinitionBuilder.
// 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.
// 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<MatchedDirective>();
@ -563,8 +561,8 @@ export class ComponentDecoratorHandler implements
}
// Check whether the directive/pipe arrays in ɵcmp need to be wrapped in closures.
// This is required if any directive/pipe reference is to a declaration in the same file but
// declared after this component.
// This is required if any directive/pipe reference is to a declaration in the same file
// but declared after this component.
const wrapDirectivesAndPipesInClosure =
usedDirectives.some(
dir => isExpressionForwardReference(dir.type, node.name, context)) ||
@ -890,8 +888,8 @@ export class ComponentDecoratorHandler implements
// 2. By default, the template parser strips leading trivia characters (like spaces, tabs, and
// newlines). This also destroys source mapping information.
//
// In order to guarantee the correctness of diagnostics, templates are parsed a second time with
// the above options set to preserve source mappings.
// In order to guarantee the correctness of diagnostics, templates are parsed a second time
// with the above options set to preserve source mappings.
const {nodes: diagNodes} = parseTemplate(templateStr, templateUrl, {
preserveWhitespaces: true,

View File

@ -5,6 +5,9 @@
* 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 {CycleAnalyzer, ImportGraph} from '../../cycles';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {absoluteFrom} from '../../file_system';
@ -13,7 +16,7 @@ import {ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '..
import {CompoundMetadataReader, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, ResourceRegistry} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator';
import {isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver, TypeCheckScopeRegistry} from '../../scope';
import {getDeclaration, makeProgram} from '../../testing';
import {ResourceLoader} from '../src/api';
import {ComponentDecoratorHandler} from '../src/component';
@ -48,17 +51,33 @@ function setup(program: ts.Program, options: ts.CompilerOptions, host: ts.Compil
const refEmitter = new ReferenceEmitter([]);
const injectableRegistry = new InjectableClassRegistry(reflectionHost);
const resourceRegistry = new ResourceRegistry();
const typeCheckScopeRegistry = new TypeCheckScopeRegistry(scopeRegistry, metaReader);
const handler = new ComponentDecoratorHandler(
reflectionHost, evaluator, metaRegistry, metaReader, scopeRegistry, scopeRegistry,
reflectionHost,
evaluator,
metaRegistry,
metaReader,
scopeRegistry,
scopeRegistry,
typeCheckScopeRegistry,
resourceRegistry,
/* isCore */ false, new StubResourceLoader(), /* rootDirs */['/'],
/* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true,
/* isCore */ false,
new StubResourceLoader(),
/* rootDirs */['/'],
/* defaultPreserveWhitespaces */ false,
/* i18nUseExternalIds */ true,
/* enableI18nLegacyMessageIdFormat */ false,
/* usePoisonedData */ false,
/* i18nNormalizeLineEndingsInICUs */ undefined, moduleResolver, cycleAnalyzer, refEmitter,
NOOP_DEFAULT_IMPORT_RECORDER, /* depTracker */ null, injectableRegistry,
/* annotateForClosureCompiler */ false);
/* i18nNormalizeLineEndingsInICUs */ undefined,
moduleResolver,
cycleAnalyzer,
refEmitter,
NOOP_DEFAULT_IMPORT_RECORDER,
/* depTracker */ null,
injectableRegistry,
/* annotateForClosureCompiler */ false,
);
return {reflectionHost, handler};
}

View File

@ -24,7 +24,7 @@ import {NOOP_PERF_RECORDER, PerfRecorder} from '../../perf';
import {DeclarationNode, isNamedClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
import {AdapterResourceLoader} from '../../resource';
import {entryPointKeyFor, NgModuleRouteAnalyzer} from '../../routing';
import {ComponentScopeReader, LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
import {ComponentScopeReader, LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver, TypeCheckScopeRegistry} from '../../scope';
import {generatedFactoryTransform} from '../../shims';
import {ivySwitchTransform} from '../../switch';
import {aliasTransformFactory, CompilationMode, declarationTransformFactory, DecoratorHandler, DtsTransformRegistry, ivyTransformFactory, TraitCompiler} from '../../transform';
@ -44,6 +44,7 @@ interface LazyCompilationState {
reflector: TypeScriptReflectionHost;
metaReader: MetadataReader;
scopeRegistry: LocalModuleScopeRegistry;
typeCheckScopeRegistry: TypeCheckScopeRegistry;
exportReferenceGraph: ReferenceGraph|null;
routeAnalyzer: NgModuleRouteAnalyzer;
dtsTransforms: DtsTransformRegistry;
@ -732,6 +733,7 @@ export class NgCompiler {
const injectableRegistry = new InjectableClassRegistry(reflector);
const metaReader = new CompoundMetadataReader([localMetaReader, dtsReader]);
const typeCheckScopeRegistry = new TypeCheckScopeRegistry(scopeReader, metaReader);
// If a flat module entrypoint was specified, then track references via a `ReferenceGraph` in
@ -761,8 +763,9 @@ export class NgCompiler {
const handlers: DecoratorHandler<unknown, unknown, unknown>[] = [
new ComponentDecoratorHandler(
reflector, evaluator, metaRegistry, metaReader, scopeReader, scopeRegistry,
resourceRegistry, isCore, this.resourceManager, this.adapter.rootDirs,
this.options.preserveWhitespaces || false, this.options.i18nUseExternalIds !== false,
typeCheckScopeRegistry, resourceRegistry, isCore, this.resourceManager,
this.adapter.rootDirs, this.options.preserveWhitespaces || false,
this.options.i18nUseExternalIds !== false,
this.options.enableI18nLegacyMessageIdFormat !== false, this.usePoisonedData,
this.options.i18nNormalizeLineEndingsInICUs, this.moduleResolver, this.cycleAnalyzer,
refEmitter, defaultImportTracker, this.incrementalDriver.depGraph, injectableRegistry,
@ -814,6 +817,7 @@ export class NgCompiler {
routeAnalyzer,
mwpScanner,
metaReader,
typeCheckScopeRegistry,
defaultImportTracker,
aliasingHost,
refEmitter,

View File

@ -10,3 +10,4 @@ export {ExportScope, ScopeData} from './src/api';
export {ComponentScopeReader, CompoundComponentScopeReader} from './src/component_scope';
export {DtsModuleScopeResolver, MetadataDtsModuleScopeResolver} from './src/dependency';
export {DeclarationData, LocalModuleScope, LocalModuleScopeRegistry, LocalNgModuleData} from './src/local';
export {TypeCheckScope, TypeCheckScopeRegistry} from './src/typecheck';

View File

@ -12,7 +12,8 @@ import * as ts from 'typescript';
import {Reference} from '../../imports';
import {DirectiveMeta, flattenInheritedDirectiveMetadata, MetadataReader} from '../../metadata';
import {ClassDeclaration} from '../../reflection';
import {ComponentScopeReader} from '../../scope';
import {ComponentScopeReader} from './component_scope';
/**
* The scope that is used for type-check code generation of a component template.
@ -24,6 +25,11 @@ export interface TypeCheckScope {
*/
matcher: SelectorMatcher<DirectiveMeta>;
/**
* All of the directives available in the compilation scope of the declaring NgModule.
*/
directives: DirectiveMeta[];
/**
* The pipes that are available in the compilation scope.
*/
@ -44,7 +50,7 @@ export interface TypeCheckScope {
/**
* Computes scope information to be used in template type checking.
*/
export class TypeCheckScopes {
export class TypeCheckScopeRegistry {
/**
* 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.
@ -65,12 +71,14 @@ export class TypeCheckScopes {
*/
getTypeCheckScope(node: ClassDeclaration): TypeCheckScope {
const matcher = new SelectorMatcher<DirectiveMeta>();
const directives: DirectiveMeta[] = [];
const pipes = new Map<string, Reference<ClassDeclaration<ts.ClassDeclaration>>>();
const scope = this.scopeReader.getScopeForComponent(node);
if (scope === null) {
return {
matcher,
directives,
pipes,
schemas: [],
isPoisoned: false,
@ -83,8 +91,9 @@ export class TypeCheckScopes {
for (const meta of scope.compilation.directives) {
if (meta.selector !== null) {
const extMeta = this.getInheritedDirectiveMetadata(meta.ref);
const extMeta = this.getTypeCheckDirectiveMetadata(meta.ref);
matcher.addSelectables(CssSelector.parse(meta.selector), extMeta);
directives.push(extMeta);
}
}
@ -98,6 +107,7 @@ export class TypeCheckScopes {
const typeCheckScope: TypeCheckScope = {
matcher,
directives,
pipes,
schemas: scope.schemas,
isPoisoned: scope.compilation.isPoisoned || scope.exported.isPoisoned,
@ -106,7 +116,7 @@ export class TypeCheckScopes {
return typeCheckScope;
}
private getInheritedDirectiveMetadata(ref: Reference<ClassDeclaration>): DirectiveMeta {
getTypeCheckDirectiveMetadata(ref: Reference<ClassDeclaration>): DirectiveMeta {
const clazz = ref.node;
if (this.flattenedDirectiveMetaCache.has(clazz)) {
return this.flattenedDirectiveMetaCache.get(clazz)!;