Skip to content

Commit

Permalink
refactor(compiler-cli): unify tracked template scope dependencies (#4…
Browse files Browse the repository at this point in the history
…5672)

Previously, the compiler tracked directives and pipes in template scopes
separately. This commit refactors the scope system to unify them into a
single data structure, disambiguated by a `kind` field.

PR Close #45672
  • Loading branch information
alxhub committed Apr 20, 2022
1 parent 1527e8f commit 9b35787
Show file tree
Hide file tree
Showing 23 changed files with 227 additions and 233 deletions.
165 changes: 94 additions & 71 deletions packages/compiler-cli/src/ngtsc/annotations/component/src/handler.ts
Expand Up @@ -16,7 +16,7 @@ import {assertSuccessfulReferenceEmit, ImportedFile, ModuleResolver, Reference,
import {DependencyTracker} from '../../../incremental/api';
import {extractSemanticTypeParameters, SemanticDepGraphUpdater} from '../../../incremental/semantic_graph';
import {IndexingContext} from '../../../indexer';
import {DirectiveMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, MetaType, ResourceRegistry} from '../../../metadata';
import {DirectiveMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, MetaKind, PipeMeta, ResourceRegistry} from '../../../metadata';
import {PartialEvaluator} from '../../../partial_evaluator';
import {PerfEvent, PerfRecorder} from '../../../perf';
import {ClassDeclaration, DeclarationNode, Decorator, ReflectionHost, reflectObjectLiteral} from '../../../reflection';
Expand Down Expand Up @@ -446,7 +446,7 @@ export class ComponentDecoratorHandler implements
// the information about the component is available during the compile() phase.
const ref = new Reference(node);
this.metaRegistry.registerDirectiveMetadata({
type: MetaType.Directive,
kind: MetaKind.Directive,
ref,
name: node.name.text,
selector: analysis.meta.selector,
Expand Down Expand Up @@ -482,9 +482,9 @@ export class ComponentDecoratorHandler implements
return null;
}

for (const directive of scope.compilation.directives) {
if (directive.selector !== null) {
matcher.addSelectables(CssSelector.parse(directive.selector), directive);
for (const dep of scope.compilation.dependencies) {
if (dep.kind === MetaKind.Directive && dep.selector !== null) {
matcher.addSelectables(CssSelector.parse(dep.selector), dep);
}
}
}
Expand Down Expand Up @@ -584,15 +584,15 @@ export class ComponentDecoratorHandler implements
type MatchedDirective = DirectiveMeta&{selector: string};
const matcher = new SelectorMatcher<MatchedDirective>();

for (const dir of scope.directives) {
if (dir.selector !== null) {
matcher.addSelectables(CssSelector.parse(dir.selector), dir as MatchedDirective);
const pipes = new Map<string, PipeMeta>();

for (const dep of scope.dependencies) {
if (dep.kind === MetaKind.Directive && dep.selector !== null) {
matcher.addSelectables(CssSelector.parse(dep.selector), dep as MatchedDirective);
} else if (dep.kind === MetaKind.Pipe) {
pipes.set(dep.name, dep);
}
}
const pipes = new Map<string, Reference<ClassDeclaration>>();
for (const pipe of scope.pipes) {
pipes.set(pipe.name, pipe.ref);
}

// Next, the component template AST is bound using the R3TargetBinder. This produces a
// BoundTarget, which is similar to a ts.TypeChecker.
Expand All @@ -602,91 +602,113 @@ export class ComponentDecoratorHandler implements
// The BoundTarget knows which directives and pipes matched the template.
type UsedDirective = R3DirectiveDependencyMetadata&
{ref: Reference<ClassDeclaration>, importedFile: ImportedFile};
const usedDirectives: UsedDirective[] = bound.getUsedDirectives().map(directive => {
const type = this.refEmitter.emit(directive.ref, context);
assertSuccessfulReferenceEmit(
type, node.name, directive.isComponent ? 'component' : 'directive');
return {
kind: R3TemplateDependencyKind.Directive,
ref: directive.ref,
type: type.expression,
importedFile: type.importedFile,
selector: directive.selector,
inputs: directive.inputs.propertyNames,
outputs: directive.outputs.propertyNames,
exportAs: directive.exportAs,
isComponent: directive.isComponent,
};
});

const used = new Set<ClassDeclaration>();
for (const dir of bound.getUsedDirectives()) {
used.add(dir.ref.node);
}
for (const name of bound.getUsedPipes()) {
if (!pipes.has(name)) {
continue;
}
used.add(pipes.get(name)!.ref.node);
}

type UsedPipe = R3PipeDependencyMetadata&{
ref: Reference<ClassDeclaration>,
importedFile: ImportedFile,
};
const usedPipes: UsedPipe[] = [];
for (const pipeName of bound.getUsedPipes()) {
if (!pipes.has(pipeName)) {
continue;

const declarations: (UsedPipe|UsedDirective)[] = [];

// Transform the dependencies list, filtering out unused dependencies.
for (const dep of scope.dependencies) {
switch (dep.kind) {
case MetaKind.Directive:
if (!used.has(dep.ref.node)) {
continue;
}
const dirType = this.refEmitter.emit(dep.ref, context);
assertSuccessfulReferenceEmit(
dirType, node.name, dep.isComponent ? 'component' : 'directive');

declarations.push({
kind: R3TemplateDependencyKind.Directive,
ref: dep.ref,
type: dirType.expression,
importedFile: dirType.importedFile,
selector: dep.selector!,
inputs: dep.inputs.propertyNames,
outputs: dep.outputs.propertyNames,
exportAs: dep.exportAs,
isComponent: dep.isComponent,
});
break;
case MetaKind.Pipe:
if (!used.has(dep.ref.node)) {
continue;
}

const pipeType = this.refEmitter.emit(dep.ref, context);
assertSuccessfulReferenceEmit(pipeType, node.name, 'pipe');

declarations.push({
kind: R3TemplateDependencyKind.Pipe,
type: pipeType.expression,
name: dep.name,
ref: dep.ref,
importedFile: pipeType.importedFile,
});
break;
}
const pipe = pipes.get(pipeName)!;
const type = this.refEmitter.emit(pipe, context);
assertSuccessfulReferenceEmit(type, node.name, 'pipe');
usedPipes.push({
kind: R3TemplateDependencyKind.Pipe,
type: type.expression,
name: pipeName,
ref: pipe,
importedFile: type.importedFile,
});
}

const isUsedDirective = (decl: UsedDirective|UsedPipe): decl is UsedDirective =>
decl.kind === R3TemplateDependencyKind.Directive;
const isUsedPipe = (decl: UsedDirective|UsedPipe): decl is UsedPipe =>
decl.kind === R3TemplateDependencyKind.Pipe;

const getSemanticReference = (decl: UsedDirective|UsedPipe) =>
this.semanticDepGraphUpdater!.getSemanticReference(decl.ref.node, decl.type);

if (this.semanticDepGraphUpdater !== null) {
symbol.usedDirectives = usedDirectives.map(
dir => this.semanticDepGraphUpdater!.getSemanticReference(dir.ref.node, dir.type));
symbol.usedPipes = usedPipes.map(
pipe => this.semanticDepGraphUpdater!.getSemanticReference(pipe.ref.node, pipe.type));
symbol.usedDirectives = declarations.filter(isUsedDirective).map(getSemanticReference);
symbol.usedPipes = declarations.filter(isUsedPipe).map(getSemanticReference);
}

// 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 cyclesFromDirectives = new Map<UsedDirective, Cycle>();
for (const usedDirective of usedDirectives) {
const cycle =
this._checkForCyclicImport(usedDirective.importedFile, usedDirective.type, context);
if (cycle !== null) {
cyclesFromDirectives.set(usedDirective, cycle);
}
}
const cyclesFromPipes = new Map<UsedPipe, Cycle>();
for (const usedPipe of usedPipes) {
const cycle = this._checkForCyclicImport(usedPipe.importedFile, usedPipe.type, context);
for (const usedDep of declarations) {
const cycle = this._checkForCyclicImport(usedDep.importedFile, usedDep.type, context);
if (cycle !== null) {
cyclesFromPipes.set(usedPipe, cycle);
switch (usedDep.kind) {
case R3TemplateDependencyKind.Directive:
cyclesFromDirectives.set(usedDep, cycle);
break;
case R3TemplateDependencyKind.Pipe:
cyclesFromPipes.set(usedDep, cycle);
break;
}
}
}

const cycleDetected = cyclesFromDirectives.size !== 0 || cyclesFromPipes.size !== 0;
if (!cycleDetected) {
// No cycle was detected. Record the imports that need to be created in the cycle detector
// so that future cyclic import checks consider their production.
for (const {type, importedFile} of usedDirectives) {
this._recordSyntheticImport(importedFile, type, context);
}
for (const {type, importedFile} of usedPipes) {
for (const {type, importedFile} of declarations) {
this._recordSyntheticImport(importedFile, type, context);
}

// 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
// Check whether the dependencies arrays in ɵcmp need to be wrapped in a closure.
// This is required if any dependency 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)) ||
usedPipes.some(pipe => isExpressionForwardReference(pipe.type, node.name, context));

data.declarations = [
...usedDirectives,
...usedPipes,
];
declarations.some(decl => isExpressionForwardReference(decl.type, node.name, context));

data.declarations = declarations;
data.declarationListEmitMode = wrapDirectivesAndPipesInClosure ?
DeclarationListEmitMode.Closure :
DeclarationListEmitMode.Direct;
Expand All @@ -696,7 +718,8 @@ export class ComponentDecoratorHandler implements
// create a cycle. Instead, mark this component as requiring remote scoping, so that the
// NgModule file will take care of setting the directives for the component.
this.scopeRegistry.setComponentRemoteScope(
node, usedDirectives.map(dir => dir.ref), usedPipes.map(pipe => pipe.ref));
node, declarations.filter(isUsedDirective).map(dir => dir.ref),
declarations.filter(isUsedPipe).map(pipe => pipe.ref));
symbol.isRemotelyScoped = true;

// If a semantic graph is being tracked, record the fact that this component is remotely
Expand Down
47 changes: 19 additions & 28 deletions packages/compiler-cli/src/ngtsc/annotations/component/src/scope.ts
Expand Up @@ -15,9 +15,10 @@ import {ComponentScopeReader, DtsModuleScopeResolver, ExportScope, LocalModuleSc
import {ComponentAnalysisData} from './metadata';


export type DependencyMeta = DirectiveMeta|PipeMeta;

export interface ScopeTemplateResult {
directives: DirectiveMeta[];
pipes: PipeMeta[];
dependencies: DependencyMeta[];
diagnostics: ts.Diagnostic[];
ngModule: ClassDeclaration|null;
}
Expand All @@ -43,8 +44,7 @@ export function scopeTemplate(
if (scope !== null) {
// This is an NgModule-ful component, so use scope information coming from the NgModule.
return {
directives: scope.compilation.directives,
pipes: scope.compilation.pipes,
dependencies: scope.compilation.dependencies,
ngModule: scope.ngModule,
diagnostics: [],
};
Expand All @@ -53,8 +53,7 @@ export function scopeTemplate(
if (analysis.imports === null) {
// Early exit for standalone components that don't declare imports (empty scope).
return {
directives: [],
pipes: [],
dependencies: [],
ngModule: null,
diagnostics: [],
};
Expand All @@ -64,10 +63,16 @@ export function scopeTemplate(

// We need to deduplicate directives/pipes in `imports`, as any given directive/pipe may be
// present in the export scope of more than one NgModule (or just listed more than once).
const directives = new Map<ClassDeclaration, DirectiveMeta>();
const pipes = new Map<ClassDeclaration, PipeMeta>();
const seen = new Set<ClassDeclaration>();
const dependencies: DependencyMeta[] = [];

for (const ref of analysis.imports.resolved) {
if (seen.has(ref.node)) {
// This imported type has already been processed, so don't process it a second time.
continue;
}
seen.add(ref.node);

// Determine if this import is a component/directive/pipe/NgModule.
const dirMeta = metaReader.getDirectiveMetadata(ref);
if (dirMeta !== null) {
Expand All @@ -79,9 +84,7 @@ export function scopeTemplate(
continue;
}

if (!directives.has(ref.node)) {
directives.set(ref.node, dirMeta);
}
dependencies.push(dirMeta);
continue;
}

Expand Down Expand Up @@ -114,33 +117,21 @@ export function scopeTemplate(
return null;
}

for (const dir of scope.exported.directives) {
if (!directives.has(dir.ref.node)) {
directives.set(dir.ref.node, dir);
}
}

for (const pipe of scope.exported.pipes) {
if (!pipes.has(pipe.ref.node)) {
pipes.set(pipe.ref.node, pipe);
}
}
dependencies.push(...scope.exported.dependencies);
}
}

return {
directives: Array.from(directives.values()),
pipes: Array.from(pipes.values()),
diagnostics,
dependencies,
ngModule: null,
diagnostics,
};
} else {
// This is a "free" component, and is neither standalone nor declared in an NgModule.
// This should probably be an error now that we have standalone components, but that would be a
// breaking change. For now, preserve the old behavior (treat is as having an empty scope).
// breaking change. For now, preserve the old behavior (treat it as having an empty scope).
return {
directives: [],
pipes: [],
dependencies: [],
ngModule: null,
diagnostics: [],
};
Expand Down
Expand Up @@ -11,7 +11,7 @@ import ts from 'typescript';

import {Reference} from '../../../imports';
import {extractSemanticTypeParameters, SemanticDepGraphUpdater} from '../../../incremental/semantic_graph';
import {ClassPropertyMapping, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, MetaType} from '../../../metadata';
import {ClassPropertyMapping, DirectiveTypeCheckMeta, extractDirectiveTypeCheckMeta, InjectableClassRegistry, MetadataReader, MetadataRegistry, MetaKind} from '../../../metadata';
import {PartialEvaluator} from '../../../partial_evaluator';
import {PerfEvent, PerfRecorder} from '../../../perf';
import {ClassDeclaration, ClassMember, ClassMemberKind, Decorator, ReflectionHost} from '../../../reflection';
Expand Down Expand Up @@ -132,7 +132,7 @@ export class DirectiveDecoratorHandler implements
// the information about the directive is available during the compile() phase.
const ref = new Reference(node);
this.metaRegistry.registerDirectiveMetadata({
type: MetaType.Directive,
kind: MetaKind.Directive,
ref,
name: node.name.text,
selector: analysis.meta.selector,
Expand Down
Expand Up @@ -12,7 +12,7 @@ import ts from 'typescript';
import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../../diagnostics';
import {assertSuccessfulReferenceEmit, Reference, ReferenceEmitter} from '../../../imports';
import {isArrayEqual, isReferenceEqual, isSymbolEqual, SemanticReference, SemanticSymbol} from '../../../incremental/semantic_graph';
import {InjectableClassRegistry, MetadataReader, MetadataRegistry} from '../../../metadata';
import {InjectableClassRegistry, MetadataReader, MetadataRegistry, MetaKind} from '../../../metadata';
import {PartialEvaluator, ResolvedValue} from '../../../partial_evaluator';
import {PerfEvent, PerfRecorder} from '../../../perf';
import {ClassDeclaration, Decorator, isNamedClassDeclaration, ReflectionHost, reflectObjectLiteral, typeNodeToValueExpr} from '../../../reflection';
Expand Down Expand Up @@ -738,8 +738,7 @@ export class NgModuleDecoratorHandler implements
}

function isNgModule(node: ClassDeclaration, compilation: ScopeData): boolean {
return !compilation.directives.some(directive => directive.ref.node === node) &&
!compilation.pipes.some(pipe => pipe.ref.node === node);
return !compilation.dependencies.some(dep => dep.ref.node === node);
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/compiler-cli/src/ngtsc/annotations/src/pipe.ts
Expand Up @@ -12,7 +12,7 @@ import ts from 'typescript';
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
import {Reference} from '../../imports';
import {SemanticSymbol} from '../../incremental/semantic_graph';
import {InjectableClassRegistry, MetadataRegistry, MetaType} from '../../metadata';
import {InjectableClassRegistry, MetadataRegistry, MetaKind} from '../../metadata';
import {PartialEvaluator} from '../../partial_evaluator';
import {PerfEvent, PerfRecorder} from '../../perf';
import {ClassDeclaration, Decorator, ReflectionHost, reflectObjectLiteral} from '../../reflection';
Expand Down Expand Up @@ -154,7 +154,7 @@ export class PipeDecoratorHandler implements
register(node: ClassDeclaration, analysis: Readonly<PipeHandlerData>): void {
const ref = new Reference(node);
this.metaRegistry.registerPipeMetadata({
type: MetaType.Pipe,
kind: MetaKind.Pipe,
ref,
name: analysis.meta.pipeName,
nameExpr: analysis.pipeNameExpr,
Expand Down

0 comments on commit 9b35787

Please sign in to comment.