feat(linter): show files involved in circular dependency (#7113)

This commit is contained in:
Miroslav Jonaš 2021-09-24 11:00:15 +02:00 committed by GitHub
parent 4fb351617d
commit c18a40b1f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 104 additions and 34 deletions

View File

@ -1,4 +1,4 @@
import type { ProjectGraph } from '@nrwl/devkit';
import type { FileData, ProjectGraph } from '@nrwl/devkit';
import {
DependencyType,
ProjectType,
@ -6,7 +6,6 @@ import {
import { TSESLint } from '@typescript-eslint/experimental-utils';
import * as parser from '@typescript-eslint/parser';
import { vol } from 'memfs';
import { extname } from 'path';
import enforceModuleBoundaries, {
RULE_NAME as enforceModuleBoundariesRuleName,
} from '../../src/rules/enforce-module-boundaries';
@ -936,7 +935,7 @@ describe('Enforce Module Boundaries (eslint)', () => {
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/mylib/src/main.ts`)],
files: [createFile(`libs/mylib/src/main.ts`, ['anotherlibName'])],
},
},
anotherlibName: {
@ -947,7 +946,7 @@ describe('Enforce Module Boundaries (eslint)', () => {
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/anotherlib/src/main.ts`)],
files: [createFile(`libs/anotherlib/src/main.ts`, ['mylibName'])],
},
},
myappName: {
@ -974,8 +973,11 @@ describe('Enforce Module Boundaries (eslint)', () => {
}
);
const message =
'Circular dependency between "anotherlibName" and "mylibName" detected: anotherlibName -> mylibName -> anotherlibName';
const message = `Circular dependency between "anotherlibName" and "mylibName" detected: anotherlibName -> mylibName -> anotherlibName
Circular file chain:
- libs/anotherlib/src/main.ts
- libs/mylib/src/main.ts`;
expect(failures.length).toEqual(2);
expect(failures[0].message).toEqual(message);
expect(failures[1].message).toEqual(message);
@ -999,7 +1001,9 @@ describe('Enforce Module Boundaries (eslint)', () => {
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/mylib/src/main.ts`)],
files: [
createFile(`libs/mylib/src/main.ts`, ['badcirclelibName']),
],
},
},
anotherlibName: {
@ -1010,7 +1014,10 @@ describe('Enforce Module Boundaries (eslint)', () => {
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/anotherlib/src/main.ts`)],
files: [
createFile(`libs/anotherlib/src/main.ts`, ['mylibName']),
createFile(`libs/anotherlib/src/index.ts`, ['mylibName']),
],
},
},
badcirclelibName: {
@ -1021,7 +1028,9 @@ describe('Enforce Module Boundaries (eslint)', () => {
tags: [],
implicitDependencies: [],
architect: {},
files: [createFile(`libs/badcirclelib/src/main.ts`)],
files: [
createFile(`libs/badcirclelib/src/main.ts`, ['anotherlibName']),
],
},
},
myappName: {
@ -1062,8 +1071,12 @@ describe('Enforce Module Boundaries (eslint)', () => {
}
);
const message =
'Circular dependency between "mylibName" and "badcirclelibName" detected: mylibName -> badcirclelibName -> anotherlibName -> mylibName';
const message = `Circular dependency between "mylibName" and "badcirclelibName" detected: mylibName -> badcirclelibName -> anotherlibName -> mylibName
Circular file chain:
- libs/mylib/src/main.ts
- libs/badcirclelib/src/main.ts
- [libs/anotherlib/src/main.ts,libs/anotherlib/src/index.ts]`;
expect(failures.length).toEqual(2);
expect(failures[0].message).toEqual(message);
expect(failures[1].message).toEqual(message);
@ -1525,8 +1538,8 @@ const baseConfig = {
linter.defineParser('@typescript-eslint/parser', parser);
linter.defineRule(enforceModuleBoundariesRuleName, enforceModuleBoundaries);
function createFile(f) {
return { file: f, hash: '' };
function createFile(f: string, deps?: string[]): FileData {
return { file: f, hash: '', ...(deps && { deps }) };
}
function runRule(

View File

@ -27,7 +27,10 @@ import {
} from '@nrwl/workspace/src/core/project-graph';
import { readNxJson } from '@nrwl/workspace/src/core/file-utils';
import { TargetProjectLocator } from '@nrwl/workspace/src/core/target-project-locator';
import { checkCircularPath } from '@nrwl/workspace/src/utils/graph-utils';
import {
checkCircularPath,
findFilesInCircularPath,
} from '@nrwl/workspace/src/utils/graph-utils';
import { isRelativePath } from '@nrwl/workspace/src/utilities/fileutils';
type Options = [
@ -83,7 +86,7 @@ export default createESLintRule<Options, MessageIds>({
],
messages: {
noRelativeOrAbsoluteImportsAcrossLibraries: `Libraries cannot be imported by a relative or absolute path, and must begin with a npm scope`,
noCircularDependencies: `Circular dependency between "{{sourceProjectName}}" and "{{targetProjectName}}" detected: {{path}}`,
noCircularDependencies: `Circular dependency between "{{sourceProjectName}}" and "{{targetProjectName}}" detected: {{path}}\n\nCircular file chain:\n{{filePaths}}`,
noSelfCircularDependencies: `Projects should use relative imports to import from other files within the same project. Use "./path/to/file" instead of import from "{{imp}}"`,
noImportsOfApps: 'Imports of apps are forbidden',
noImportsOfE2e: 'Imports of e2e projects are forbidden',
@ -236,11 +239,13 @@ export default createESLintRule<Options, MessageIds>({
// check constraints between libs and apps
// check for circular dependency
const circularPath = checkCircularPath(
projectGraph,
(global as any).projectGraph,
sourceProject,
targetProject
);
if (circularPath.length !== 0) {
const circularFilePath = findFilesInCircularPath(circularPath);
context.report({
node,
messageId: 'noCircularDependencies',
@ -251,6 +256,14 @@ export default createESLintRule<Options, MessageIds>({
(acc, v) => `${acc} -> ${v.name}`,
sourceProject.name
),
filePaths: circularFilePath
.map((files) =>
files.length > 1 ? `[${files.join(',')}]` : files[0]
)
.reduce(
(acc, files) => `${acc}\n- ${files}`,
`- ${sourceFilePath}`
),
},
});
return;

View File

@ -856,8 +856,8 @@ describe('Enforce Module Boundaries (tslint)', () => {
root: 'libs/mylib',
tags: [],
implicitDependencies: [],
targets: {},
files: [createFile(`libs/mylib/src/main.ts`)],
architect: {},
files: [createFile(`libs/mylib/src/main.ts`, ['anotherlibName'])],
},
},
anotherlibName: {
@ -867,8 +867,8 @@ describe('Enforce Module Boundaries (tslint)', () => {
root: 'libs/anotherlib',
tags: [],
implicitDependencies: [],
targets: {},
files: [createFile(`libs/anotherlib/src/main.ts`)],
architect: {},
files: [createFile(`libs/anotherlib/src/main.ts`, ['mylibName'])],
},
},
myappName: {
@ -878,7 +878,7 @@ describe('Enforce Module Boundaries (tslint)', () => {
root: 'apps/myapp',
tags: [],
implicitDependencies: [],
targets: {},
architect: {},
files: [createFile(`apps/myapp/src/index.ts`)],
},
},
@ -895,7 +895,11 @@ describe('Enforce Module Boundaries (tslint)', () => {
}
);
expect(failures[0].getFailure()).toEqual(
'Circular dependency between "anotherlibName" and "mylibName" detected: anotherlibName -> mylibName -> anotherlibName'
`Circular dependency between "anotherlibName" and "mylibName" detected: anotherlibName -> mylibName -> anotherlibName
Circular file chain:
- libs/anotherlib/src/main.ts
- libs/mylib/src/main.ts`
);
});
@ -913,8 +917,10 @@ describe('Enforce Module Boundaries (tslint)', () => {
root: 'libs/mylib',
tags: [],
implicitDependencies: [],
targets: {},
files: [createFile(`libs/mylib/src/main.ts`)],
architect: {},
files: [
createFile(`libs/mylib/src/main.ts`, ['badcirclelibName']),
],
},
},
anotherlibName: {
@ -924,8 +930,11 @@ describe('Enforce Module Boundaries (tslint)', () => {
root: 'libs/anotherlib',
tags: [],
implicitDependencies: [],
targets: {},
files: [createFile(`libs/anotherlib/src/main.ts`)],
architect: {},
files: [
createFile(`libs/anotherlib/src/main.ts`, ['mylibName']),
createFile(`libs/anotherlib/src/index.ts`, ['mylibName']),
],
},
},
badcirclelibName: {
@ -935,8 +944,10 @@ describe('Enforce Module Boundaries (tslint)', () => {
root: 'libs/badcirclelib',
tags: [],
implicitDependencies: [],
targets: {},
files: [createFile(`libs/badcirclelib/src/main.ts`)],
architect: {},
files: [
createFile(`libs/badcirclelib/src/main.ts`, ['anotherlibName']),
],
},
},
myappName: {
@ -946,7 +957,7 @@ describe('Enforce Module Boundaries (tslint)', () => {
root: 'apps/myapp',
tags: [],
implicitDependencies: [],
targets: {},
architect: {},
files: [createFile(`apps/myapp/index.ts`)],
},
},
@ -977,7 +988,12 @@ describe('Enforce Module Boundaries (tslint)', () => {
}
);
expect(failures[0].getFailure()).toEqual(
'Circular dependency between "mylibName" and "badcirclelibName" detected: mylibName -> badcirclelibName -> anotherlibName -> mylibName'
`Circular dependency between "mylibName" and "badcirclelibName" detected: mylibName -> badcirclelibName -> anotherlibName -> mylibName
Circular file chain:
- libs/mylib/src/main.ts
- libs/badcirclelib/src/main.ts
- [libs/anotherlib/src/main.ts,libs/anotherlib/src/index.ts]`
);
});
@ -1156,8 +1172,8 @@ describe('Enforce Module Boundaries (tslint)', () => {
});
});
function createFile(f): FileData {
return { file: f, hash: '' };
function createFile(f: string, deps?: string[]): FileData {
return { file: f, hash: '', ...(deps && { deps }) };
}
function runRule(

View File

@ -26,7 +26,10 @@ import {
import { normalize } from 'path';
import { readNxJson } from '../core/file-utils';
import { TargetProjectLocator } from '../core/target-project-locator';
import { checkCircularPath } from '../utils/graph-utils';
import {
checkCircularPath,
findFilesInCircularPath,
} from '../utils/graph-utils';
import { isRelativePath } from '../utilities/fileutils';
export class Rule extends Lint.Rules.AbstractRule {
@ -191,7 +194,13 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {
(acc, v) => `${acc} -> ${v.name}`,
sourceProject.name
);
const error = `Circular dependency between "${sourceProject.name}" and "${targetProject.name}" detected: ${path}`;
const circularFilePath = findFilesInCircularPath(circularPath);
const filePaths = circularFilePath
.map((files) => (files.length > 1 ? `[${files.join(',')}]` : files[0]))
.reduce((acc, files) => `${acc}\n- ${files}`, `- ${filePath}`);
const error = `Circular dependency between "${sourceProject.name}" and "${targetProject.name}" detected: ${path}\n\nCircular file chain:\n${filePaths}`;
this.addFailureAt(node.getStart(), node.getWidth(), error);
return;
}

View File

@ -111,5 +111,24 @@ export function checkCircularPath(
targetProject: ProjectGraphNode
): Array<ProjectGraphNode> {
if (!graph.nodes[targetProject.name]) return [];
return getPath(graph, targetProject.name, sourceProject.name);
}
export function findFilesInCircularPath(
circularPath: ProjectGraphNode[]
): Array<string[]> {
const filePathChain = [];
for (let i = 0; i < circularPath.length - 1; i++) {
const next = circularPath[i + 1].name;
const files = circularPath[i].data.files;
filePathChain.push(
Object.keys(files)
.filter((key) => files[key].deps?.indexOf(next) !== -1)
.map((key) => files[key].file)
);
}
return filePathChain;
}