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:
Pete Bacon Darwin
2021-02-10 17:51:42 +00:00
committed by Joey Perrott
parent a4c00c2148
commit 322951af49
19 changed files with 416 additions and 75 deletions
@@ -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,