diff --git a/packages/eslint-plugin-nx/tests/rules/enforce-module-boundaries.spec.ts b/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.spec.ts similarity index 97% rename from packages/eslint-plugin-nx/tests/rules/enforce-module-boundaries.spec.ts rename to packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.spec.ts index fc5cc31145..bb68df2747 100644 --- a/packages/eslint-plugin-nx/tests/rules/enforce-module-boundaries.spec.ts +++ b/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.spec.ts @@ -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( diff --git a/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts b/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts index 13b2c18168..5f720e7a76 100644 --- a/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts +++ b/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts @@ -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({ ], 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({ // 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({ (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; diff --git a/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.spec.ts b/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.spec.ts index bba77fbd05..43a5e2e1cd 100644 --- a/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.spec.ts +++ b/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.spec.ts @@ -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( diff --git a/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.ts b/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.ts index ee00a6ef63..024dd98cf8 100644 --- a/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.ts +++ b/packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.ts @@ -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; } diff --git a/packages/workspace/src/utils/graph-utils.ts b/packages/workspace/src/utils/graph-utils.ts index 288a441401..f946488ce9 100644 --- a/packages/workspace/src/utils/graph-utils.ts +++ b/packages/workspace/src/utils/graph-utils.ts @@ -111,5 +111,24 @@ export function checkCircularPath( targetProject: ProjectGraphNode ): Array { if (!graph.nodes[targetProject.name]) return []; + return getPath(graph, targetProject.name, sourceProject.name); } + +export function findFilesInCircularPath( + circularPath: ProjectGraphNode[] +): Array { + 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; +}