nx/packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts
Miroslav Jonaš ff3cc38b0c
feat(linter): optimize lint performance. resolves #5210 (#5576)
* feat(linter): optimize containsFile function

related to #5210

* feat(linter): optimize find project function

related to #5210

* feat(core): cleanup target project locator

* fix(core): fix false positive match on resolvedModule

* chore(core): mark old code for removal

* feat(linter): move npm check before expensive typescript resolution

* feat(linter): improve performance of extension removal

* feat(linter): improve I/O operations

* feat(linter): read ts config only once in project locator

* feat(linter): remove double path normalization from rule

normalization is already part of getSourceFilePath function

* feat(linter): run find source only once

* feat(linter): defer source file path calculation will after whitelist check

* feat(linter): map project graph node files to hash map

* fix(linter): map projectGraph only once per runtime

* feat(linter): introduce mapped project graph type
2021-05-18 16:41:12 +02:00

358 lines
10 KiB
TypeScript

import { appRootPath } from '@nrwl/workspace/src/utilities/app-root';
import {
DepConstraint,
findConstraintsFor,
findProjectUsingImport,
findSourceProject,
getSourceFilePath,
hasBuildExecutor,
hasNoneOfTheseTags,
isAbsoluteImportIntoAnotherProject,
isRelativeImportIntoAnotherProject,
mapProjectGraphFiles,
matchImportWithWildcard,
onlyLoadChildren,
MappedProjectGraph,
} from '@nrwl/workspace/src/utils/runtime-lint-utils';
import {
AST_NODE_TYPES,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import { createESLintRule } from '../utils/create-eslint-rule';
import { normalizePath } from '@nrwl/devkit';
import {
isNpmProject,
ProjectType,
} 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 { readCurrentProjectGraph } from '@nrwl/workspace/src/core/project-graph/project-graph';
import { isRelativePath } from '@nrwl/workspace/src/utilities/fileutils';
type Options = [
{
allow: string[];
depConstraints: DepConstraint[];
enforceBuildableLibDependency: boolean;
allowCircularSelfDependency: boolean;
}
];
export type MessageIds =
| 'noRelativeOrAbsoluteImportsAcrossLibraries'
| 'noSelfCircularDependencies'
| 'noCircularDependencies'
| 'noImportsOfApps'
| 'noImportsOfE2e'
| 'noImportOfNonBuildableLibraries'
| 'noImportsOfLazyLoadedLibraries'
| 'projectWithoutTagsCannotHaveDependencies'
| 'tagConstraintViolation';
export const RULE_NAME = 'enforce-module-boundaries';
export default createESLintRule<Options, MessageIds>({
name: RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: `Ensure that module boundaries are respected within the monorepo`,
category: 'Best Practices',
recommended: 'error',
},
fixable: 'code',
schema: [
{
type: 'object',
properties: {
enforceBuildableLibDependency: { type: 'boolean' },
allowCircularSelfDependency: { type: 'boolean' },
allow: [{ type: 'string' }],
depConstraints: [
{
type: 'object',
properties: {
sourceTag: { type: 'string' },
onlyDependOnLibsWithTags: [{ type: 'string' }],
},
additionalProperties: false,
},
],
},
additionalProperties: false,
},
],
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}}`,
noSelfCircularDependencies: `Only relative imports are allowed within the project. Absolute import found: {{imp}}`,
noImportsOfApps: 'Imports of apps are forbidden',
noImportsOfE2e: 'Imports of e2e projects are forbidden',
noImportOfNonBuildableLibraries:
'Buildable libraries cannot import or export from non-buildable libraries',
noImportsOfLazyLoadedLibraries: `Imports of lazy-loaded libraries are forbidden`,
projectWithoutTagsCannotHaveDependencies: `A project without tags cannot depend on any libraries`,
tagConstraintViolation: `A project tagged with "{{sourceTag}}" can only depend on libs tagged with {{allowedTags}}`,
},
},
defaultOptions: [
{
allow: [],
depConstraints: [],
enforceBuildableLibDependency: false,
allowCircularSelfDependency: false,
},
],
create(
context,
[
{
allow,
depConstraints,
enforceBuildableLibDependency,
allowCircularSelfDependency,
},
]
) {
/**
* Globally cached info about workspace
*/
const projectPath = normalizePath(
(global as any).projectPath || appRootPath
);
if (!(global as any).projectGraph) {
const nxJson = readNxJson();
(global as any).npmScope = nxJson.npmScope;
(global as any).projectGraph = mapProjectGraphFiles(
readCurrentProjectGraph()
);
}
if (!(global as any).projectGraph) {
return {};
}
const npmScope = (global as any).npmScope;
const projectGraph = (global as any).projectGraph as MappedProjectGraph;
if (!(global as any).targetProjectLocator) {
(global as any).targetProjectLocator = new TargetProjectLocator(
projectGraph.nodes
);
}
const targetProjectLocator = (global as any)
.targetProjectLocator as TargetProjectLocator;
function run(
node:
| TSESTree.ImportDeclaration
| TSESTree.ImportExpression
| TSESTree.ExportAllDeclaration
| TSESTree.ExportNamedDeclaration
) {
// Ignoring ExportNamedDeclarations like:
// export class Foo {}
if (!node.source) {
return;
}
// accept only literals because template literals have no value
if (node.source.type !== AST_NODE_TYPES.Literal) {
return;
}
const imp = node.source.value as string;
// whitelisted import
if (allow.some((a) => matchImportWithWildcard(a, imp))) {
return;
}
const sourceFilePath = getSourceFilePath(
context.getFilename(),
projectPath
);
// check for relative and absolute imports
const sourceProject = findSourceProject(projectGraph, sourceFilePath);
if (
isRelativeImportIntoAnotherProject(
imp,
projectPath,
projectGraph,
sourceFilePath,
sourceProject
) ||
isAbsoluteImportIntoAnotherProject(imp)
) {
context.report({
node,
messageId: 'noRelativeOrAbsoluteImportsAcrossLibraries',
data: {
npmScope,
},
});
return;
}
const targetProject = findProjectUsingImport(
projectGraph,
targetProjectLocator,
sourceFilePath,
imp,
npmScope
);
// If source or target are not part of an nx workspace, return.
if (!sourceProject || !targetProject) {
return;
}
// same project => allow
if (sourceProject === targetProject) {
// we only allow relative paths within the same project
if (!allowCircularSelfDependency && !isRelativePath(imp)) {
context.report({
node,
messageId: 'noSelfCircularDependencies',
data: {
imp,
},
});
}
return;
}
// project => npm package
if (isNpmProject(targetProject)) {
return;
}
// check constraints between libs and apps
// check for circular dependency
const circularPath = checkCircularPath(
projectGraph,
sourceProject,
targetProject
);
if (circularPath.length !== 0) {
context.report({
node,
messageId: 'noCircularDependencies',
data: {
sourceProjectName: sourceProject.name,
targetProjectName: targetProject.name,
path: circularPath.reduce(
(acc, v) => `${acc} -> ${v.name}`,
sourceProject.name
),
},
});
return;
}
// cannot import apps
if (targetProject.type === ProjectType.app) {
context.report({
node,
messageId: 'noImportsOfApps',
});
return;
}
// cannot import e2e projects
if (targetProject.type === ProjectType.e2e) {
context.report({
node,
messageId: 'noImportsOfE2e',
});
return;
}
// buildable-lib is not allowed to import non-buildable-lib
if (
enforceBuildableLibDependency === true &&
sourceProject.type === ProjectType.lib &&
targetProject.type === ProjectType.lib
) {
if (
hasBuildExecutor(sourceProject) &&
!hasBuildExecutor(targetProject)
) {
context.report({
node,
messageId: 'noImportOfNonBuildableLibraries',
});
return;
}
}
// if we import a library using loadChildren, we should not import it using es6imports
if (
node.type === AST_NODE_TYPES.ImportDeclaration &&
node.importKind !== 'type' &&
onlyLoadChildren(
projectGraph,
sourceProject.name,
targetProject.name,
[]
)
) {
context.report({
node,
messageId: 'noImportsOfLazyLoadedLibraries',
});
return;
}
// check that dependency constraints are satisfied
if (depConstraints.length > 0) {
const constraints = findConstraintsFor(depConstraints, sourceProject);
// when no constrains found => error. Force the user to provision them.
if (constraints.length === 0) {
context.report({
node,
messageId: 'projectWithoutTagsCannotHaveDependencies',
});
return;
}
for (let constraint of constraints) {
if (
hasNoneOfTheseTags(
targetProject,
constraint.onlyDependOnLibsWithTags || []
)
) {
const allowedTags = constraint.onlyDependOnLibsWithTags
.map((s) => `"${s}"`)
.join(', ');
context.report({
node,
messageId: 'tagConstraintViolation',
data: {
sourceTag: constraint.sourceTag,
allowedTags,
},
});
return;
}
}
}
}
return {
ImportDeclaration(node: TSESTree.ImportDeclaration) {
run(node);
},
ImportExpression(node: TSESTree.ImportExpression) {
run(node);
},
ExportAllDeclaration(node: TSESTree.ExportAllDeclaration) {
run(node);
},
ExportNamedDeclaration(node: TSESTree.ExportNamedDeclaration) {
run(node);
},
};
},
});