refactor(compiler-cli): error on cyclic imports in partial compilation (#40782)
Our approach for handling cyclic imports results in code that is not easy to tree-shake, so it is not suitable for publishing in a library. When compiling in partial compilation mode, we are targeting such library publication, so we now create a fatal diagnostic error instead of trying to handle the cyclic import situation. Closes #40678 PR Close #40782
This commit is contained in:
committed by
Joey Perrott
parent
a4c00c2148
commit
322951af49
@@ -9,9 +9,9 @@
|
||||
import {compileComponentFromMetadata, compileDeclareComponentFromMetadata, ConstantPool, CssSelector, DeclarationListEmitMode, DEFAULT_INTERPOLATION_CONFIG, DomElementSchemaRegistry, Expression, ExternalExpr, Identifiers, InterpolationConfig, LexerRange, makeBindingParser, ParsedTemplate, ParseSourceFile, parseTemplate, R3ComponentDef, R3ComponentMetadata, R3FactoryTarget, R3TargetBinder, R3UsedDirectiveMetadata, SelectorMatcher, Statement, TmplAstNode, WrappedNodeExpr} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {CycleAnalyzer} from '../../cycles';
|
||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||
import {absoluteFrom, AbsoluteFsPath, relative} from '../../file_system';
|
||||
import {Cycle, CycleAnalyzer, CycleHandlingStrategy} from '../../cycles';
|
||||
import {ErrorCode, FatalDiagnosticError, makeDiagnostic, makeRelatedInformation} from '../../diagnostics';
|
||||
import {absoluteFrom, relative} from '../../file_system';
|
||||
import {DefaultImportRecorder, ModuleResolver, Reference, ReferenceEmitter} from '../../imports';
|
||||
import {DependencyTracker} from '../../incremental/api';
|
||||
import {IndexingContext} from '../../indexer';
|
||||
@@ -127,7 +127,8 @@ export class ComponentDecoratorHandler implements
|
||||
private enableI18nLegacyMessageIdFormat: boolean, private usePoisonedData: boolean,
|
||||
private i18nNormalizeLineEndingsInICUs: boolean|undefined,
|
||||
private moduleResolver: ModuleResolver, private cycleAnalyzer: CycleAnalyzer,
|
||||
private refEmitter: ReferenceEmitter, private defaultImportRecorder: DefaultImportRecorder,
|
||||
private cycleHandlingStrategy: CycleHandlingStrategy, private refEmitter: ReferenceEmitter,
|
||||
private defaultImportRecorder: DefaultImportRecorder,
|
||||
private depTracker: DependencyTracker|null,
|
||||
private injectableRegistry: InjectableClassRegistry,
|
||||
private annotateForClosureCompiler: boolean) {}
|
||||
@@ -546,10 +547,12 @@ export class ComponentDecoratorHandler implements
|
||||
inputs: directive.inputs.propertyNames,
|
||||
outputs: directive.outputs.propertyNames,
|
||||
exportAs: directive.exportAs,
|
||||
isComponent: directive.isComponent,
|
||||
};
|
||||
});
|
||||
|
||||
const usedPipes: {ref: Reference, pipeName: string, expression: Expression}[] = [];
|
||||
type UsedPipe = {ref: Reference, pipeName: string, expression: Expression};
|
||||
const usedPipes: UsedPipe[] = [];
|
||||
for (const pipeName of bound.getUsedPipes()) {
|
||||
if (!pipes.has(pipeName)) {
|
||||
continue;
|
||||
@@ -564,10 +567,22 @@ export class ComponentDecoratorHandler implements
|
||||
|
||||
// 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.type, context)) ||
|
||||
usedPipes.some(pipe => this._isCyclicImport(pipe.expression, context));
|
||||
const cyclesFromDirectives = new Map<UsedDirective, Cycle>();
|
||||
for (const usedDirective of usedDirectives) {
|
||||
const cycle = this._checkForCyclicImport(usedDirective.ref, 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.ref, usedPipe.expression, context);
|
||||
if (cycle !== null) {
|
||||
cyclesFromPipes.set(usedPipe, cycle);
|
||||
}
|
||||
}
|
||||
|
||||
if (!cycleDetected) {
|
||||
if (cyclesFromDirectives.size === 0 && cyclesFromPipes.size === 0) {
|
||||
// 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} of usedDirectives) {
|
||||
@@ -592,11 +607,28 @@ export class ComponentDecoratorHandler implements
|
||||
DeclarationListEmitMode.Closure :
|
||||
DeclarationListEmitMode.Direct;
|
||||
} else {
|
||||
// Declaring the directiveDefs/pipeDefs arrays directly would require imports that would
|
||||
// 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));
|
||||
if (this.cycleHandlingStrategy === CycleHandlingStrategy.UseRemoteScoping) {
|
||||
// Declaring the directiveDefs/pipeDefs arrays directly would require imports that would
|
||||
// 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));
|
||||
} else {
|
||||
// We are not able to handle this cycle so throw an error.
|
||||
const relatedMessages: ts.DiagnosticRelatedInformation[] = [];
|
||||
for (const [dir, cycle] of cyclesFromDirectives) {
|
||||
relatedMessages.push(
|
||||
makeCyclicImportInfo(dir.ref, dir.isComponent ? 'component' : 'directive', cycle));
|
||||
}
|
||||
for (const [pipe, cycle] of cyclesFromPipes) {
|
||||
relatedMessages.push(makeCyclicImportInfo(pipe.ref, 'pipe', cycle));
|
||||
}
|
||||
throw new FatalDiagnosticError(
|
||||
ErrorCode.IMPORT_CYCLE_DETECTED, node,
|
||||
'One or more import cycles would need to be created to compile this component, ' +
|
||||
'which is not supported by the current compiler configuration.',
|
||||
relatedMessages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1052,14 +1084,20 @@ export class ComponentDecoratorHandler implements
|
||||
return this.moduleResolver.resolveModule(expr.value.moduleName!, origin.fileName);
|
||||
}
|
||||
|
||||
private _isCyclicImport(expr: Expression, origin: ts.SourceFile): boolean {
|
||||
const imported = this._expressionToImportedFile(expr, origin);
|
||||
if (imported === null) {
|
||||
return false;
|
||||
/**
|
||||
* Check whether adding an import from `origin` to the source-file corresponding to `expr` would
|
||||
* create a cyclic import.
|
||||
*
|
||||
* @returns a `Cycle` object if a cycle would be created, otherwise `null`.
|
||||
*/
|
||||
private _checkForCyclicImport(ref: Reference, expr: Expression, origin: ts.SourceFile): Cycle
|
||||
|null {
|
||||
const importedFile = this._expressionToImportedFile(expr, origin);
|
||||
if (importedFile === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check whether the import is legal.
|
||||
return this.cycleAnalyzer.wouldCreateCycle(origin, imported);
|
||||
return this.cycleAnalyzer.wouldCreateCycle(origin, importedFile);
|
||||
}
|
||||
|
||||
private _recordSyntheticImport(expr: Expression, origin: ts.SourceFile): void {
|
||||
@@ -1219,3 +1257,15 @@ interface ExternalTemplateDeclaration extends CommonTemplateDeclaration {
|
||||
* record without needing to parse the original decorator again.
|
||||
*/
|
||||
type TemplateDeclaration = InlineTemplateDeclaration|ExternalTemplateDeclaration;
|
||||
|
||||
/**
|
||||
* Generate a diagnostic related information object that describes a potential cyclic import path.
|
||||
*/
|
||||
function makeCyclicImportInfo(
|
||||
ref: Reference, type: string, cycle: Cycle): ts.DiagnosticRelatedInformation {
|
||||
const name = ref.debugName || '(unknown)';
|
||||
const path = cycle.getPath().map(sf => sf.fileName).join(' -> ');
|
||||
const message =
|
||||
`The ${type} '${name}' is used in the template but importing it would create a cycle: `;
|
||||
return makeRelatedInformation(ref.node, message + path);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import {ConstantPool} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {CycleAnalyzer, ImportGraph} from '../../cycles';
|
||||
import {CycleAnalyzer, CycleHandlingStrategy, ImportGraph} from '../../cycles';
|
||||
import {ErrorCode, FatalDiagnosticError} from '../../diagnostics';
|
||||
import {absoluteFrom} from '../../file_system';
|
||||
import {runInEachFileSystem} from '../../file_system/testing';
|
||||
@@ -73,6 +73,7 @@ function setup(program: ts.Program, options: ts.CompilerOptions, host: ts.Compil
|
||||
/* i18nNormalizeLineEndingsInICUs */ undefined,
|
||||
moduleResolver,
|
||||
cycleAnalyzer,
|
||||
CycleHandlingStrategy.UseRemoteScoping,
|
||||
refEmitter,
|
||||
NOOP_DEFAULT_IMPORT_RECORDER,
|
||||
/* depTracker */ null,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {Type} from '@angular/compiler';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, NoopReferencesRegistry, PipeDecoratorHandler, ReferencesRegistry} from '../../annotations';
|
||||
import {CycleAnalyzer, ImportGraph} from '../../cycles';
|
||||
import {CycleAnalyzer, CycleHandlingStrategy, ImportGraph} from '../../cycles';
|
||||
import {COMPILER_ERRORS_WITH_GUIDES, ERROR_DETAILS_PAGE_BASE_URL, ErrorCode, ngErrorCode} from '../../diagnostics';
|
||||
import {checkForPrivateExports, ReferenceGraph} from '../../entry_point';
|
||||
import {LogicalFileSystem, resolve} from '../../file_system';
|
||||
@@ -985,6 +985,16 @@ export class NgCompiler {
|
||||
const defaultImportTracker = new DefaultImportTracker();
|
||||
const resourceRegistry = new ResourceRegistry();
|
||||
|
||||
const compilationMode =
|
||||
this.options.compilationMode === 'partial' ? CompilationMode.PARTIAL : CompilationMode.FULL;
|
||||
|
||||
// Cycles are handled in full compilation mode by "remote scoping".
|
||||
// "Remote scoping" does not work well with tree shaking for libraries.
|
||||
// So in partial compilation mode, when building a library, a cycle will cause an error.
|
||||
const cycleHandlingStrategy = compilationMode === CompilationMode.FULL ?
|
||||
CycleHandlingStrategy.UseRemoteScoping :
|
||||
CycleHandlingStrategy.Error;
|
||||
|
||||
// Set up the IvyCompilation, which manages state for the Ivy transformer.
|
||||
const handlers: DecoratorHandler<unknown, unknown, unknown>[] = [
|
||||
new ComponentDecoratorHandler(
|
||||
@@ -994,8 +1004,8 @@ export class NgCompiler {
|
||||
this.options.i18nUseExternalIds !== false,
|
||||
this.options.enableI18nLegacyMessageIdFormat !== false, this.usePoisonedData,
|
||||
this.options.i18nNormalizeLineEndingsInICUs, this.moduleResolver, this.cycleAnalyzer,
|
||||
refEmitter, defaultImportTracker, this.incrementalDriver.depGraph, injectableRegistry,
|
||||
this.closureCompilerEnabled),
|
||||
cycleHandlingStrategy, refEmitter, defaultImportTracker, this.incrementalDriver.depGraph,
|
||||
injectableRegistry, this.closureCompilerEnabled),
|
||||
// TODO(alxhub): understand why the cast here is necessary (something to do with `null`
|
||||
// not being assignable to `unknown` when wrapped in `Readonly`).
|
||||
// clang-format off
|
||||
@@ -1022,8 +1032,6 @@ export class NgCompiler {
|
||||
this.closureCompilerEnabled, injectableRegistry, this.options.i18nInLocale),
|
||||
];
|
||||
|
||||
const compilationMode =
|
||||
this.options.compilationMode === 'partial' ? CompilationMode.PARTIAL : CompilationMode.FULL;
|
||||
const traitCompiler = new TraitCompiler(
|
||||
handlers, reflector, this.perfRecorder, this.incrementalDriver,
|
||||
this.options.compileNonExportedClasses !== false, compilationMode, dtsTransforms);
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export {CycleAnalyzer} from './src/analyzer';
|
||||
export {Cycle, CycleAnalyzer, CycleHandlingStrategy} from './src/analyzer';
|
||||
export {ImportGraph} from './src/imports';
|
||||
|
||||
@@ -17,11 +17,17 @@ export class CycleAnalyzer {
|
||||
constructor(private importGraph: ImportGraph) {}
|
||||
|
||||
/**
|
||||
* Check whether adding an import from `from` to `to` would create a cycle in the `ts.Program`.
|
||||
* Check for a cycle to be created in the `ts.Program` by adding an import between `from` and
|
||||
* `to`.
|
||||
*
|
||||
* @returns a `Cycle` object if an import between `from` and `to` would create a cycle; `null`
|
||||
* otherwise.
|
||||
*/
|
||||
wouldCreateCycle(from: ts.SourceFile, to: ts.SourceFile): boolean {
|
||||
wouldCreateCycle(from: ts.SourceFile, to: ts.SourceFile): Cycle|null {
|
||||
// Import of 'from' -> 'to' is illegal if an edge 'to' -> 'from' already exists.
|
||||
return this.importGraph.transitiveImportsOf(to).has(from);
|
||||
return this.importGraph.transitiveImportsOf(to).has(from) ?
|
||||
new Cycle(this.importGraph, from, to) :
|
||||
null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,3 +40,35 @@ export class CycleAnalyzer {
|
||||
this.importGraph.addSyntheticImport(from, to);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an import cycle between `from` and `to` in the program.
|
||||
*
|
||||
* This class allows us to do the work to compute the cyclic path between `from` and `to` only if
|
||||
* needed.
|
||||
*/
|
||||
export class Cycle {
|
||||
constructor(
|
||||
private importGraph: ImportGraph, readonly from: ts.SourceFile, readonly to: ts.SourceFile) {}
|
||||
|
||||
/**
|
||||
* Compute an array of source-files that illustrates the cyclic path between `from` and `to`.
|
||||
*
|
||||
* Note that a `Cycle` will not be created unless a path is available between `to` and `from`,
|
||||
* so `findPath()` will never return `null`.
|
||||
*/
|
||||
getPath(): ts.SourceFile[] {
|
||||
return [this.from, ...this.importGraph.findPath(this.to, this.from)!];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* What to do if a cycle is detected.
|
||||
*/
|
||||
export const enum CycleHandlingStrategy {
|
||||
/** Add "remote scoping" code to avoid creating a cycle. */
|
||||
UseRemoteScoping,
|
||||
/** Fail the compilation with an error. */
|
||||
Error,
|
||||
}
|
||||
|
||||
@@ -52,6 +52,44 @@ export class ImportGraph {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Find an import path from the `start` SourceFile to the `end` SourceFile.
|
||||
*
|
||||
* This function implements a breadth first search that results in finding the
|
||||
* shortest path between the `start` and `end` points.
|
||||
*
|
||||
* @param start the starting point of the path.
|
||||
* @param end the ending point of the path.
|
||||
* @returns an array of source files that connect the `start` and `end` source files, or `null` if
|
||||
* no path could be found.
|
||||
*/
|
||||
findPath(start: ts.SourceFile, end: ts.SourceFile): ts.SourceFile[]|null {
|
||||
if (start === end) {
|
||||
// Escape early for the case where `start` and `end` are the same.
|
||||
return [start];
|
||||
}
|
||||
|
||||
const found = new Set<ts.SourceFile>([start]);
|
||||
const queue: Found[] = [new Found(start, null)];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!;
|
||||
const imports = this.importsOf(current.sourceFile);
|
||||
for (const importedFile of imports) {
|
||||
if (!found.has(importedFile)) {
|
||||
const next = new Found(importedFile, current);
|
||||
if (next.sourceFile === end) {
|
||||
// We have hit the target `end` path so we can stop here.
|
||||
return next.toPath();
|
||||
}
|
||||
found.add(importedFile);
|
||||
queue.push(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a record of an import from `sf` to `imported`, that's not present in the original
|
||||
* `ts.Program` but will be remembered by the `ImportGraph`.
|
||||
@@ -84,3 +122,27 @@ export class ImportGraph {
|
||||
function isLocalFile(sf: ts.SourceFile): boolean {
|
||||
return !sf.fileName.endsWith('.d.ts');
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper class to track which SourceFiles are being processed when searching for a path in
|
||||
* `getPath()` above.
|
||||
*/
|
||||
class Found {
|
||||
constructor(readonly sourceFile: ts.SourceFile, readonly parent: Found|null) {}
|
||||
|
||||
/**
|
||||
* Back track through this found SourceFile and its ancestors to generate an array of
|
||||
* SourceFiles that form am import path between two SourceFiles.
|
||||
*/
|
||||
toPath(): ts.SourceFile[] {
|
||||
const array: ts.SourceFile[] = [];
|
||||
let current: Found|null = this;
|
||||
while (current !== null) {
|
||||
array.push(current.sourceFile);
|
||||
current = current.parent;
|
||||
}
|
||||
// Pushing and then reversing, O(n), rather than unshifting repeatedly, O(n^2), avoids
|
||||
// manipulating the array on every iteration: https://stackoverflow.com/a/26370620
|
||||
return array.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ import * as ts from 'typescript';
|
||||
import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../file_system';
|
||||
import {runInEachFileSystem} from '../../file_system/testing';
|
||||
import {ModuleResolver} from '../../imports';
|
||||
import {CycleAnalyzer} from '../src/analyzer';
|
||||
import {Cycle, CycleAnalyzer} from '../src/analyzer';
|
||||
import {ImportGraph} from '../src/imports';
|
||||
import {makeProgramFromGraph} from './util';
|
||||
import {importPath, makeProgramFromGraph} from './util';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
describe('cycle analyzer', () => {
|
||||
@@ -22,41 +22,53 @@ runInEachFileSystem(() => {
|
||||
const {program, analyzer} = makeAnalyzer('a:b,c;b;c');
|
||||
const b = getSourceFileOrError(program, (_('/b.ts')));
|
||||
const c = getSourceFileOrError(program, (_('/c.ts')));
|
||||
expect(analyzer.wouldCreateCycle(b, c)).toBe(false);
|
||||
expect(analyzer.wouldCreateCycle(c, b)).toBe(false);
|
||||
expect(analyzer.wouldCreateCycle(b, c)).toBe(null);
|
||||
expect(analyzer.wouldCreateCycle(c, b)).toBe(null);
|
||||
});
|
||||
|
||||
it('should detect a simple cycle between two files', () => {
|
||||
const {program, analyzer} = makeAnalyzer('a:b;b');
|
||||
const a = getSourceFileOrError(program, (_('/a.ts')));
|
||||
const b = getSourceFileOrError(program, (_('/b.ts')));
|
||||
expect(analyzer.wouldCreateCycle(a, b)).toBe(false);
|
||||
expect(analyzer.wouldCreateCycle(b, a)).toBe(true);
|
||||
expect(analyzer.wouldCreateCycle(a, b)).toBe(null);
|
||||
const cycle = analyzer.wouldCreateCycle(b, a);
|
||||
expect(cycle).toBeInstanceOf(Cycle);
|
||||
expect(importPath(cycle!.getPath())).toEqual('b,a,b');
|
||||
});
|
||||
|
||||
it('should detect a cycle with a re-export in the chain', () => {
|
||||
const {program, analyzer} = makeAnalyzer('a:*b;b:c;c');
|
||||
const a = getSourceFileOrError(program, (_('/a.ts')));
|
||||
const b = getSourceFileOrError(program, (_('/b.ts')));
|
||||
const c = getSourceFileOrError(program, (_('/c.ts')));
|
||||
expect(analyzer.wouldCreateCycle(a, c)).toBe(false);
|
||||
expect(analyzer.wouldCreateCycle(c, a)).toBe(true);
|
||||
expect(analyzer.wouldCreateCycle(a, c)).toBe(null);
|
||||
const cycle = analyzer.wouldCreateCycle(c, a);
|
||||
expect(cycle).toBeInstanceOf(Cycle);
|
||||
expect(importPath(cycle!.getPath())).toEqual('c,a,b,c');
|
||||
});
|
||||
|
||||
it('should detect a cycle in a more complex program', () => {
|
||||
const {program, analyzer} = makeAnalyzer('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f:c;g;h:g');
|
||||
const b = getSourceFileOrError(program, (_('/b.ts')));
|
||||
const c = getSourceFileOrError(program, (_('/c.ts')));
|
||||
const e = getSourceFileOrError(program, (_('/e.ts')));
|
||||
const f = getSourceFileOrError(program, (_('/f.ts')));
|
||||
const g = getSourceFileOrError(program, (_('/g.ts')));
|
||||
expect(analyzer.wouldCreateCycle(b, g)).toBe(false);
|
||||
expect(analyzer.wouldCreateCycle(g, b)).toBe(true);
|
||||
expect(analyzer.wouldCreateCycle(b, g)).toBe(null);
|
||||
const cycle = analyzer.wouldCreateCycle(g, b);
|
||||
expect(cycle).toBeInstanceOf(Cycle);
|
||||
expect(importPath(cycle!.getPath())).toEqual('g,b,f,c,g');
|
||||
});
|
||||
|
||||
it('should detect a cycle caused by a synthetic edge', () => {
|
||||
const {program, analyzer} = makeAnalyzer('a:b,c;b;c');
|
||||
const b = getSourceFileOrError(program, (_('/b.ts')));
|
||||
const c = getSourceFileOrError(program, (_('/c.ts')));
|
||||
expect(analyzer.wouldCreateCycle(b, c)).toBe(false);
|
||||
expect(analyzer.wouldCreateCycle(b, c)).toBe(null);
|
||||
analyzer.recordSyntheticImport(c, b);
|
||||
expect(analyzer.wouldCreateCycle(b, c)).toBe(true);
|
||||
const cycle = analyzer.wouldCreateCycle(b, c);
|
||||
expect(cycle).toBeInstanceOf(Cycle);
|
||||
expect(importPath(cycle!.getPath())).toEqual('b,c,b');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,46 +10,76 @@ import {absoluteFrom, getFileSystem, getSourceFileOrError} from '../../file_syst
|
||||
import {runInEachFileSystem} from '../../file_system/testing';
|
||||
import {ModuleResolver} from '../../imports';
|
||||
import {ImportGraph} from '../src/imports';
|
||||
import {makeProgramFromGraph} from './util';
|
||||
import {importPath, makeProgramFromGraph} from './util';
|
||||
|
||||
runInEachFileSystem(() => {
|
||||
describe('import graph', () => {
|
||||
describe('ImportGraph', () => {
|
||||
let _: typeof absoluteFrom;
|
||||
beforeEach(() => _ = absoluteFrom);
|
||||
|
||||
it('should record imports of a simple program', () => {
|
||||
const {program, graph} = makeImportGraph('a:b;b:c;c');
|
||||
const a = getSourceFileOrError(program, (_('/a.ts')));
|
||||
const b = getSourceFileOrError(program, (_('/b.ts')));
|
||||
const c = getSourceFileOrError(program, (_('/c.ts')));
|
||||
expect(importsToString(graph.importsOf(a))).toBe('b');
|
||||
expect(importsToString(graph.importsOf(b))).toBe('c');
|
||||
describe('importsOf()', () => {
|
||||
it('should record imports of a simple program', () => {
|
||||
const {program, graph} = makeImportGraph('a:b;b:c;c');
|
||||
const a = getSourceFileOrError(program, (_('/a.ts')));
|
||||
const b = getSourceFileOrError(program, (_('/b.ts')));
|
||||
const c = getSourceFileOrError(program, (_('/c.ts')));
|
||||
expect(importsToString(graph.importsOf(a))).toBe('b');
|
||||
expect(importsToString(graph.importsOf(b))).toBe('c');
|
||||
});
|
||||
});
|
||||
|
||||
it('should calculate transitive imports of a simple program', () => {
|
||||
const {program, graph} = makeImportGraph('a:b;b:c;c');
|
||||
const a = getSourceFileOrError(program, (_('/a.ts')));
|
||||
const b = getSourceFileOrError(program, (_('/b.ts')));
|
||||
const c = getSourceFileOrError(program, (_('/c.ts')));
|
||||
expect(importsToString(graph.transitiveImportsOf(a))).toBe('a,b,c');
|
||||
describe('transitiveImportsOf()', () => {
|
||||
it('should calculate transitive imports of a simple program', () => {
|
||||
const {program, graph} = makeImportGraph('a:b;b:c;c');
|
||||
const a = getSourceFileOrError(program, (_('/a.ts')));
|
||||
const b = getSourceFileOrError(program, (_('/b.ts')));
|
||||
const c = getSourceFileOrError(program, (_('/c.ts')));
|
||||
expect(importsToString(graph.transitiveImportsOf(a))).toBe('a,b,c');
|
||||
});
|
||||
|
||||
it('should calculate transitive imports in a more complex program (with a cycle)', () => {
|
||||
const {program, graph} = makeImportGraph('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f;g:e;h:g');
|
||||
const c = getSourceFileOrError(program, (_('/c.ts')));
|
||||
expect(importsToString(graph.transitiveImportsOf(c))).toBe('c,e,f,g,h');
|
||||
});
|
||||
|
||||
it('should reflect the addition of a synthetic import', () => {
|
||||
const {program, graph} = makeImportGraph('a:b,c,d;b;c;d:b');
|
||||
const b = getSourceFileOrError(program, (_('/b.ts')));
|
||||
const c = getSourceFileOrError(program, (_('/c.ts')));
|
||||
const d = getSourceFileOrError(program, (_('/d.ts')));
|
||||
expect(importsToString(graph.importsOf(b))).toEqual('');
|
||||
expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,d');
|
||||
graph.addSyntheticImport(b, c);
|
||||
expect(importsToString(graph.importsOf(b))).toEqual('c');
|
||||
expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,c,d');
|
||||
});
|
||||
});
|
||||
|
||||
it('should calculate transitive imports in a more complex program (with a cycle)', () => {
|
||||
const {program, graph} = makeImportGraph('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f;g:e;h:g');
|
||||
const c = getSourceFileOrError(program, (_('/c.ts')));
|
||||
expect(importsToString(graph.transitiveImportsOf(c))).toBe('c,e,f,g,h');
|
||||
});
|
||||
describe('findPath()', () => {
|
||||
it('should be able to compute the path between two source files if there is a cycle', () => {
|
||||
const {program, graph} = makeImportGraph('a:*b,*c;b:*e,*f;c:*g,*h;e:f;f;g:e;h:g');
|
||||
const a = getSourceFileOrError(program, (_('/a.ts')));
|
||||
const b = getSourceFileOrError(program, (_('/b.ts')));
|
||||
const c = getSourceFileOrError(program, (_('/c.ts')));
|
||||
const e = getSourceFileOrError(program, (_('/e.ts')));
|
||||
expect(importPath(graph.findPath(a, a)!)).toBe('a');
|
||||
expect(importPath(graph.findPath(a, b)!)).toBe('a,b');
|
||||
expect(importPath(graph.findPath(c, e)!)).toBe('c,g,e');
|
||||
expect(graph.findPath(e, c)).toBe(null);
|
||||
expect(graph.findPath(b, c)).toBe(null);
|
||||
});
|
||||
|
||||
it('should reflect the addition of a synthetic import', () => {
|
||||
const {program, graph} = makeImportGraph('a:b,c,d;b;c;d:b');
|
||||
const b = getSourceFileOrError(program, (_('/b.ts')));
|
||||
const c = getSourceFileOrError(program, (_('/c.ts')));
|
||||
const d = getSourceFileOrError(program, (_('/d.ts')));
|
||||
expect(importsToString(graph.importsOf(b))).toEqual('');
|
||||
expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,d');
|
||||
graph.addSyntheticImport(b, c);
|
||||
expect(importsToString(graph.importsOf(b))).toEqual('c');
|
||||
expect(importsToString(graph.transitiveImportsOf(d))).toEqual('b,c,d');
|
||||
it('should handle circular dependencies within the path between `from` and `to`', () => {
|
||||
// a -> b -> c -> d
|
||||
// ^----/ |
|
||||
// ^---------/
|
||||
const {program, graph} = makeImportGraph('a:b;b:a,c;c:a,d;d');
|
||||
const a = getSourceFileOrError(program, (_('/a.ts')));
|
||||
const c = getSourceFileOrError(program, (_('/c.ts')));
|
||||
const d = getSourceFileOrError(program, (_('/d.ts')));
|
||||
expect(importPath(graph.findPath(a, d)!)).toBe('a,b,c,d');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import {PathManipulation} from '../../file_system';
|
||||
import {getFileSystem, PathManipulation} from '../../file_system';
|
||||
import {TestFile} from '../../file_system/testing';
|
||||
import {makeProgram} from '../../testing';
|
||||
|
||||
@@ -56,3 +56,8 @@ export function makeProgramFromGraph(fs: PathManipulation, graph: string): {
|
||||
});
|
||||
return makeProgram(files);
|
||||
}
|
||||
|
||||
export function importPath(files: ts.SourceFile[]): string {
|
||||
const fs = getFileSystem();
|
||||
return files.map(sf => fs.basename(sf.fileName).replace('.ts', '')).join(',');
|
||||
}
|
||||
|
||||
@@ -52,6 +52,11 @@ export enum ErrorCode {
|
||||
|
||||
SYMBOL_NOT_EXPORTED = 3001,
|
||||
SYMBOL_EXPORTED_UNDER_DIFFERENT_NAME = 3002,
|
||||
/**
|
||||
* Raised when a relationship between directives and/or pipes would cause a cyclic import to be
|
||||
* created that cannot be handled, such as in partial compilation mode.
|
||||
*/
|
||||
IMPORT_CYCLE_DETECTED = 3003,
|
||||
|
||||
CONFIG_FLAT_MODULE_NO_INDEX = 4001,
|
||||
CONFIG_STRICT_TEMPLATES_IMPLIES_FULL_TEMPLATE_TYPECHECK = 4002,
|
||||
@@ -192,6 +197,7 @@ export const ERROR_DETAILS_PAGE_BASE_URL = 'https://angular.io/errors';
|
||||
*/
|
||||
export const COMPILER_ERRORS_WITH_GUIDES = new Set([
|
||||
ErrorCode.DECORATOR_ARG_NOT_LITERAL,
|
||||
ErrorCode.IMPORT_CYCLE_DETECTED,
|
||||
ErrorCode.PARAM_MISSING_TOKEN,
|
||||
ErrorCode.SCHEMA_INVALID_ELEMENT,
|
||||
ErrorCode.SCHEMA_INVALID_ATTRIBUTE,
|
||||
|
||||
Reference in New Issue
Block a user