Skip to content

Commit 7d954df

Browse files
alxhubjasonaden
authored andcommitted
feat(ivy): detect cycles and use remote scoping of components if needed (angular#28169)
By its nature, Ivy alters the import graph of a TS program, adding imports where template dependencies exist. For example, if ComponentA uses PipeB in its template, Ivy will insert an import of PipeB into the file in which ComponentA is declared. Any insertion of an import into a program has the potential to introduce a cycle into the import graph. If for some reason the file in which PipeB is declared imports the file in which ComponentA is declared (maybe it makes use of a service or utility function that happens to be in the same file as ComponentA) then this could create an import cycle. This turns out to happen quite regularly in larger Angular codebases. TypeScript and the Ivy runtime have no issues with such cycles. However, other tools are not so accepting. In particular the Closure Compiler is very anti-cycle. To mitigate this problem, it's necessary to detect when the insertion of an import would create a cycle. ngtsc can then use a different strategy, known as "remote scoping", instead of directly writing a reference from one component to another. Under remote scoping, a function 'setComponentScope' is called after the declaration of the component's module, which does not require the addition of new imports. FW-647 #resolve PR Close angular#28169
1 parent cac9199 commit 7d954df

File tree

27 files changed

+1050
-24
lines changed

27 files changed

+1050
-24
lines changed

packages/compiler-cli/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ ts_library(
2424
deps = [
2525
"//packages/compiler",
2626
"//packages/compiler-cli/src/ngtsc/annotations",
27+
"//packages/compiler-cli/src/ngtsc/cycles",
2728
"//packages/compiler-cli/src/ngtsc/diagnostics",
2829
"//packages/compiler-cli/src/ngtsc/entry_point",
2930
"//packages/compiler-cli/src/ngtsc/imports",

packages/compiler-cli/src/ngcc/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ts_library(
1212
"//packages:types",
1313
"//packages/compiler",
1414
"//packages/compiler-cli/src/ngtsc/annotations",
15+
"//packages/compiler-cli/src/ngtsc/cycles",
1516
"//packages/compiler-cli/src/ngtsc/imports",
1617
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
1718
"//packages/compiler-cli/src/ngtsc/reflection",

packages/compiler-cli/src/ngcc/src/analysis/decoration_analyzer.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import * as fs from 'fs';
1111
import * as ts from 'typescript';
1212

1313
import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader, SelectorScopeRegistry} from '../../../ngtsc/annotations';
14-
import {TsReferenceResolver} from '../../../ngtsc/imports';
14+
import {CycleAnalyzer, ImportGraph} from '../../../ngtsc/cycles';
15+
import {ModuleResolver, TsReferenceResolver} from '../../../ngtsc/imports';
1516
import {PartialEvaluator} from '../../../ngtsc/partial_evaluator';
1617
import {CompileResult, DecoratorHandler} from '../../../ngtsc/transform';
1718
import {DecoratedClass} from '../host/decorated_class';
@@ -65,12 +66,15 @@ export class DecorationAnalyzer {
6566
resolver = new TsReferenceResolver(this.program, this.typeChecker, this.options, this.host);
6667
scopeRegistry = new SelectorScopeRegistry(this.typeChecker, this.reflectionHost, this.resolver);
6768
evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker, this.resolver);
69+
moduleResolver = new ModuleResolver(this.program, this.options, this.host);
70+
importGraph = new ImportGraph(this.moduleResolver);
71+
cycleAnalyzer = new CycleAnalyzer(this.importGraph);
6872
handlers: DecoratorHandler<any, any>[] = [
6973
new BaseDefDecoratorHandler(this.reflectionHost, this.evaluator),
7074
new ComponentDecoratorHandler(
7175
this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore, this.resourceManager,
72-
this.rootDirs, /* defaultPreserveWhitespaces */ false,
73-
/* i18nUseExternalIds */ true),
76+
this.rootDirs, /* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true,
77+
this.moduleResolver, this.cycleAnalyzer),
7478
new DirectiveDecoratorHandler(
7579
this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore),
7680
new InjectableDecoratorHandler(this.reflectionHost, this.isCore),

packages/compiler-cli/src/ngtsc/annotations/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ ts_library(
1010
]),
1111
deps = [
1212
"//packages/compiler",
13+
"//packages/compiler-cli/src/ngtsc/cycles",
1314
"//packages/compiler-cli/src/ngtsc/diagnostics",
1415
"//packages/compiler-cli/src/ngtsc/imports",
1516
"//packages/compiler-cli/src/ngtsc/partial_evaluator",

packages/compiler-cli/src/ngtsc/annotations/src/component.ts

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, ElementSchemaRegistry, Expression, InterpolationConfig, R3ComponentMetadata, R3DirectiveMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
9+
import {ConstantPool, CssSelector, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, ElementSchemaRegistry, Expression, ExternalExpr, InterpolationConfig, R3ComponentMetadata, R3DirectiveMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr, compileComponentFromMetadata, makeBindingParser, parseTemplate} from '@angular/compiler';
1010
import * as path from 'path';
1111
import * as ts from 'typescript';
1212

13+
import {CycleAnalyzer} from '../../cycles';
1314
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
14-
import {ResolvedReference} from '../../imports';
15+
import {ModuleResolver, Reference, ResolvedReference} from '../../imports';
1516
import {EnumValue, PartialEvaluator} from '../../partial_evaluator';
1617
import {Decorator, ReflectionHost, filterToMembersWithDecorator, reflectObjectLiteral} from '../../reflection';
1718
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
@@ -41,7 +42,8 @@ export class ComponentDecoratorHandler implements
4142
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
4243
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean,
4344
private resourceLoader: ResourceLoader, private rootDirs: string[],
44-
private defaultPreserveWhitespaces: boolean, private i18nUseExternalIds: boolean) {}
45+
private defaultPreserveWhitespaces: boolean, private i18nUseExternalIds: boolean,
46+
private moduleResolver: ModuleResolver, private cycleAnalyzer: CycleAnalyzer) {}
4547

4648
private literalCache = new Map<Decorator, ts.ObjectLiteralExpression>();
4749
private elementSchemaRegistry = new DomElementSchemaRegistry();
@@ -289,28 +291,39 @@ export class ComponentDecoratorHandler implements
289291
}
290292
}
291293

292-
compile(node: ts.ClassDeclaration, analysis: ComponentHandlerData, pool: ConstantPool):
293-
CompileResult {
294+
resolve(node: ts.ClassDeclaration, analysis: ComponentHandlerData): void {
294295
// Check whether this component was registered with an NgModule. If so, it should be compiled
295296
// under that module's compilation scope.
296297
const scope = this.scopeRegistry.lookupCompilationScope(node);
297298
let metadata = analysis.meta;
298299
if (scope !== null) {
299300
// Replace the empty components and directives from the analyze() step with a fully expanded
300-
// scope. This is possible now because during compile() the whole compilation unit has been
301+
// scope. This is possible now because during resolve() the whole compilation unit has been
301302
// fully analyzed.
302303
const {pipes, containsForwardDecls} = scope;
303-
const directives: {selector: string, expression: Expression}[] = [];
304-
305-
for (const meta of scope.directives) {
306-
directives.push({selector: meta.selector, expression: meta.directive});
304+
const directives =
305+
scope.directives.map(dir => ({selector: dir.selector, expression: dir.directive}));
306+
307+
// Scan through the references of the `scope.directives` array and check whether
308+
// any import which needs to be generated for the directive would create a cycle.
309+
const origin = node.getSourceFile();
310+
const cycleDetected =
311+
scope.directives.some(meta => this._isCyclicImport(meta.directive, origin)) ||
312+
Array.from(scope.pipes.values()).some(pipe => this._isCyclicImport(pipe, origin));
313+
if (!cycleDetected) {
314+
const wrapDirectivesAndPipesInClosure: boolean = !!containsForwardDecls;
315+
metadata.directives = directives;
316+
metadata.pipes = pipes;
317+
metadata.wrapDirectivesAndPipesInClosure = wrapDirectivesAndPipesInClosure;
318+
} else {
319+
this.scopeRegistry.setComponentAsRequiringRemoteScoping(node);
307320
}
308-
const wrapDirectivesAndPipesInClosure: boolean = !!containsForwardDecls;
309-
metadata = {...metadata, directives, pipes, wrapDirectivesAndPipesInClosure};
310321
}
322+
}
311323

312-
const res =
313-
compileComponentFromMetadata(metadata, pool, makeBindingParser(metadata.interpolation));
324+
compile(node: ts.ClassDeclaration, analysis: ComponentHandlerData, pool: ConstantPool):
325+
CompileResult {
326+
const res = compileComponentFromMetadata(analysis.meta, pool, makeBindingParser());
314327

315328
const statements = res.statements;
316329
if (analysis.metadataStmt !== null) {
@@ -373,4 +386,19 @@ export class ComponentDecoratorHandler implements
373386
}
374387
return styleUrls as string[];
375388
}
389+
390+
private _isCyclicImport(expr: Expression, origin: ts.SourceFile): boolean {
391+
if (!(expr instanceof ExternalExpr)) {
392+
return false;
393+
}
394+
395+
// Figure out what file is being imported.
396+
const imported = this.moduleResolver.resolveModuleName(expr.value.moduleName !, origin);
397+
if (imported === null) {
398+
return false;
399+
}
400+
401+
// Check whether the import is legal.
402+
return this.cycleAnalyzer.wouldCreateCycle(origin, imported);
403+
}
376404
}

packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Expression, LiteralArrayExpr, R3InjectorMetadata, R3NgModuleMetadata, R3Reference, Statement, WrappedNodeExpr, compileInjector, compileNgModule} from '@angular/compiler';
9+
import {Expression, ExternalExpr, InvokeFunctionExpr, LiteralArrayExpr, R3Identifiers, R3InjectorMetadata, R3NgModuleMetadata, R3Reference, Statement, WrappedNodeExpr, compileInjector, compileNgModule} from '@angular/compiler';
1010
import * as ts from 'typescript';
1111

1212
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
@@ -25,6 +25,7 @@ export interface NgModuleAnalysis {
2525
ngModuleDef: R3NgModuleMetadata;
2626
ngInjectorDef: R3InjectorMetadata;
2727
metadataStmt: Statement|null;
28+
declarations: Reference<ts.Declaration>[];
2829
}
2930

3031
/**
@@ -152,6 +153,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
152153
analysis: {
153154
ngModuleDef,
154155
ngInjectorDef,
156+
declarations,
155157
metadataStmt: generateSetClassMetadataCall(node, this.reflector, this.isCore),
156158
},
157159
factorySymbolName: node.name !== undefined ? node.name.text : undefined,
@@ -165,6 +167,28 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
165167
if (analysis.metadataStmt !== null) {
166168
ngModuleStatements.push(analysis.metadataStmt);
167169
}
170+
const context = node.getSourceFile();
171+
for (const decl of analysis.declarations) {
172+
if (this.scopeRegistry.requiresRemoteScope(decl.node)) {
173+
const scope = this.scopeRegistry.lookupCompilationScopeAsRefs(decl.node);
174+
if (scope === null) {
175+
continue;
176+
}
177+
const directives: Expression[] = [];
178+
const pipes: Expression[] = [];
179+
scope.directives.forEach(
180+
(directive, _) => { directives.push(directive.ref.toExpression(context) !); });
181+
scope.pipes.forEach(pipe => pipes.push(pipe.toExpression(context) !));
182+
const directiveArray = new LiteralArrayExpr(directives);
183+
const pipesArray = new LiteralArrayExpr(pipes);
184+
const declExpr = decl.toExpression(context) !;
185+
const setComponentScope = new ExternalExpr(R3Identifiers.setComponentScope);
186+
const callExpr =
187+
new InvokeFunctionExpr(setComponentScope, [declExpr, directiveArray, pipesArray]);
188+
189+
ngModuleStatements.push(callExpr.toStmt());
190+
}
191+
}
168192
return [
169193
{
170194
name: 'ngModuleDef',

packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ export class SelectorScopeRegistry {
8484
*/
8585
private _pipeToName = new Map<ts.Declaration, string>();
8686

87+
/**
88+
* Components that require remote scoping.
89+
*/
90+
private _requiresRemoteScope = new Set<ts.Declaration>();
91+
8792
/**
8893
* Map of components/directives/pipes to their module.
8994
*/
@@ -132,6 +137,21 @@ export class SelectorScopeRegistry {
132137
this._pipeToName.set(node, name);
133138
}
134139

140+
/**
141+
* Mark a component (identified by its `ts.Declaration`) as requiring its `directives` scope to be
142+
* set remotely, from the file of the @NgModule which declares the component.
143+
*/
144+
setComponentAsRequiringRemoteScoping(component: ts.Declaration): void {
145+
this._requiresRemoteScope.add(component);
146+
}
147+
148+
/**
149+
* Check whether the given component requires its `directives` scope to be set remotely.
150+
*/
151+
requiresRemoteScope(component: ts.Declaration): boolean {
152+
return this._requiresRemoteScope.has(ts.getOriginalNode(component) as ts.Declaration);
153+
}
154+
135155
lookupCompilationScopeAsRefs(node: ts.Declaration): CompilationScope<Reference>|null {
136156
node = ts.getOriginalNode(node) as ts.Declaration;
137157

packages/compiler-cli/src/ngtsc/annotations/test/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ ts_library(
1212
"//packages:types",
1313
"//packages/compiler",
1414
"//packages/compiler-cli/src/ngtsc/annotations",
15+
"//packages/compiler-cli/src/ngtsc/cycles",
1516
"//packages/compiler-cli/src/ngtsc/diagnostics",
1617
"//packages/compiler-cli/src/ngtsc/imports",
1718
"//packages/compiler-cli/src/ngtsc/partial_evaluator",

packages/compiler-cli/src/ngtsc/annotations/test/component_spec.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88

99
import * as ts from 'typescript';
1010

11+
import {CycleAnalyzer, ImportGraph} from '../../cycles';
1112
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
12-
import {TsReferenceResolver} from '../../imports';
13+
import {ModuleResolver, TsReferenceResolver} from '../../imports';
1314
import {PartialEvaluator} from '../../partial_evaluator';
1415
import {TypeScriptReflectionHost} from '../../reflection';
1516
import {getDeclaration, makeProgram} from '../../testing/in_memory_typescript';
@@ -45,9 +46,13 @@ describe('ComponentDecoratorHandler', () => {
4546
const reflectionHost = new TypeScriptReflectionHost(checker);
4647
const resolver = new TsReferenceResolver(program, checker, options, host);
4748
const evaluator = new PartialEvaluator(reflectionHost, checker, resolver);
49+
const moduleResolver = new ModuleResolver(program, options, host);
50+
const importGraph = new ImportGraph(moduleResolver);
51+
const cycleAnalyzer = new CycleAnalyzer(importGraph);
52+
4853
const handler = new ComponentDecoratorHandler(
4954
reflectionHost, evaluator, new SelectorScopeRegistry(checker, reflectionHost, resolver),
50-
false, new NoopResourceLoader(), [''], false, true);
55+
false, new NoopResourceLoader(), [''], false, true, moduleResolver, cycleAnalyzer);
5156
const TestCmp = getDeclaration(program, 'entry.ts', 'TestCmp', ts.isClassDeclaration);
5257
const detected = handler.detect(TestCmp, reflectionHost.getDecoratorsOfDeclaration(TestCmp));
5358
if (detected === undefined) {

packages/compiler-cli/src/ngtsc/program.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {nocollapseHack} from '../transformers/nocollapse_hack';
1414

1515
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, NoopReferencesRegistry, PipeDecoratorHandler, ReferencesRegistry, SelectorScopeRegistry} from './annotations';
1616
import {BaseDefDecoratorHandler} from './annotations/src/base_def';
17+
import {CycleAnalyzer, ImportGraph} from './cycles';
1718
import {ErrorCode, ngErrorCode} from './diagnostics';
1819
import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point';
1920
import {ImportRewriter, ModuleResolver, NoopImportRewriter, R3SymbolsImportRewriter, Reference, TsReferenceResolver} from './imports';
@@ -48,6 +49,7 @@ export class NgtscProgram implements api.Program {
4849

4950
private constructionDiagnostics: ts.Diagnostic[] = [];
5051
private moduleResolver: ModuleResolver;
52+
private cycleAnalyzer: CycleAnalyzer;
5153

5254

5355
constructor(
@@ -128,6 +130,7 @@ export class NgtscProgram implements api.Program {
128130

129131
this.entryPoint = entryPoint !== null ? this.tsProgram.getSourceFile(entryPoint) || null : null;
130132
this.moduleResolver = new ModuleResolver(this.tsProgram, options, this.host);
133+
this.cycleAnalyzer = new CycleAnalyzer(new ImportGraph(this.moduleResolver));
131134
}
132135

133136
getTsProgram(): ts.Program { return this.tsProgram; }
@@ -184,6 +187,7 @@ export class NgtscProgram implements api.Program {
184187
.filter(file => !file.fileName.endsWith('.d.ts'))
185188
.map(file => this.compilation !.analyzeAsync(file))
186189
.filter((result): result is Promise<void> => result !== undefined));
190+
this.compilation.resolve();
187191
}
188192

189193
listLazyRoutes(entryRoute?: string|undefined): api.LazyRoute[] {
@@ -213,6 +217,7 @@ export class NgtscProgram implements api.Program {
213217
this.tsProgram.getSourceFiles()
214218
.filter(file => !file.fileName.endsWith('.d.ts'))
215219
.forEach(file => this.compilation !.analyzeSync(file));
220+
this.compilation.resolve();
216221
}
217222
return this.compilation;
218223
}
@@ -307,7 +312,7 @@ export class NgtscProgram implements api.Program {
307312
new ComponentDecoratorHandler(
308313
this.reflector, evaluator, scopeRegistry, this.isCore, this.resourceManager,
309314
this.rootDirs, this.options.preserveWhitespaces || false,
310-
this.options.i18nUseExternalIds !== false),
315+
this.options.i18nUseExternalIds !== false, this.moduleResolver, this.cycleAnalyzer),
311316
new DirectiveDecoratorHandler(this.reflector, evaluator, scopeRegistry, this.isCore),
312317
new InjectableDecoratorHandler(this.reflector, this.isCore),
313318
new NgModuleDecoratorHandler(

packages/compiler-cli/src/ngtsc/transform/src/api.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ export interface DecoratorHandler<A, M> {
4444
*/
4545
analyze(node: ts.Declaration, metadata: M): AnalysisOutput<A>;
4646

47+
/**
48+
* Perform resolution on the given decorator along with the result of analysis.
49+
*
50+
* The resolution phase happens after the entire `ts.Program` has been analyzed, and gives the
51+
* `DecoratorHandler` a chance to leverage information from the whole compilation unit to enhance
52+
* the `analysis` before the emit phase.
53+
*/
54+
resolve?(node: ts.Declaration, analysis: A): void;
55+
4756
typeCheck?(ctx: TypeCheckContext, node: ts.Declaration, metadata: A): void;
4857

4958
/**

packages/compiler-cli/src/ngtsc/transform/src/compilation.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,14 @@ export class IvyCompilation {
163163
}
164164
}
165165

166+
resolve(): void {
167+
this.analysis.forEach((op, decl) => {
168+
if (op.adapter.resolve !== undefined) {
169+
op.adapter.resolve(decl, op.analysis);
170+
}
171+
});
172+
}
173+
166174
typeCheck(context: TypeCheckContext): void {
167175
this.typeCheckMap.forEach((handler, node) => {
168176
if (handler.typeCheck !== undefined) {

0 commit comments

Comments
 (0)