Skip to content

Commit

Permalink
feat(linter): add project chain mapping for dependency tree
Browse files Browse the repository at this point in the history
  • Loading branch information
meeroslav committed Mar 7, 2022
1 parent c08b25b commit 8c93ae9
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 56 deletions.
Expand Up @@ -512,8 +512,10 @@ describe('Enforce Module Boundaries (eslint)', () => {
graph
);

const message =
'A project tagged with "public" can not depend on libs tagged with "private"';
const message = `A project tagged with "public" can not depend on libs tagged with "private"
Violation detected in:
- privateName`;
expect(failures.length).toEqual(2);
expect(failures[0].message).toEqual(message);
expect(failures[1].message).toEqual(message);
Expand All @@ -532,8 +534,10 @@ describe('Enforce Module Boundaries (eslint)', () => {

expect(failures.length).toEqual(2);
// TODO: Add project dependency path to message
const message =
'A project tagged with "public" can not depend on libs tagged with "private"';
const message = `A project tagged with "public" can not depend on libs tagged with "private"
Violation detected in:
- dependsOnPrivateName -> privateName`;
expect(failures[0].message).toEqual(message);
expect(failures[1].message).toEqual(message);
});
Expand Down
47 changes: 28 additions & 19 deletions packages/eslint-plugin-nx/src/rules/enforce-module-boundaries.ts
Expand Up @@ -6,8 +6,7 @@ import {
findSourceProject,
getSourceFilePath,
hasBuildExecutor,
hasNoneOfTheseTags,
hasAnyOfTheseTags,
findDependenciesWithTags,
isAbsoluteImportIntoAnotherProject,
isRelativeImportIntoAnotherProject,
mapProjectGraphFiles,
Expand All @@ -18,6 +17,7 @@ import {
isDirectDependency,
isTerminalRun,
stringifyTags,
hasNoneOfTheseTags,
} from '@nrwl/workspace/src/utils/runtime-lint-utils';
import {
AST_NODE_TYPES,
Expand Down Expand Up @@ -109,7 +109,7 @@ export default createESLintRule<Options, MessageIds>({
bannedExternalImportsViolation: `A project tagged with "{{sourceTag}}" is not allowed to import the "{{package}}" package`,
noTransitiveDependencies: `Transitive dependencies are not allowed. Only packages defined in the "package.json" can be imported`,
onlyTagsConstraintViolation: `A project tagged with "{{sourceTag}}" can only depend on libs tagged with {{tags}}`,
notTagsConstraintViolation: `A project tagged with "{{sourceTag}}" can not depend on libs tagged with {{tags}}`,
notTagsConstraintViolation: `A project tagged with "{{sourceTag}}" can not depend on libs tagged with {{tags}}\n\nViolation detected in:\n{{projects}}`,
},
},
defaultOptions: [
Expand Down Expand Up @@ -309,7 +309,7 @@ export default createESLintRule<Options, MessageIds>({

// spacer text used for indirect dependencies when printing one line per file.
// without this, we can end up with a very long line that does not display well in the terminal.
const spacer = '\n ';
const spacer = ' ';

context.report({
node,
Expand All @@ -324,7 +324,9 @@ export default createESLintRule<Options, MessageIds>({
filePaths: circularFilePath
.map((files) =>
files.length > 1
? `[${spacer}${files.join(`,${spacer}`)}\n ]`
? `[${files
.map((f) => `\n${spacer}${spacer}${f}`)
.join(',')}\n${spacer}]`
: files[0]
)
.reduce(
Expand Down Expand Up @@ -404,6 +406,8 @@ export default createESLintRule<Options, MessageIds>({

for (let constraint of constraints) {
if (
constraint.onlyDependOnLibsWithTags &&
constraint.onlyDependOnLibsWithTags.length &&
hasNoneOfTheseTags(
targetProject,
constraint.onlyDependOnLibsWithTags
Expand All @@ -420,21 +424,26 @@ export default createESLintRule<Options, MessageIds>({
return;
}
if (
hasAnyOfTheseTags(
projectGraph,
targetProject.name,
constraint.notDependOnLibsWithTags
)
constraint.notDependOnLibsWithTags &&
constraint.notDependOnLibsWithTags.length
) {
context.report({
node,
messageId: 'notTagsConstraintViolation',
data: {
sourceTag: constraint.sourceTag,
tags: stringifyTags(constraint.notDependOnLibsWithTags),
},
});
return;
const projects = findDependenciesWithTags(
targetProject,
constraint.notDependOnLibsWithTags,
projectGraph
);
if (projects.length > 0) {
context.report({
node,
messageId: 'notTagsConstraintViolation',
data: {
sourceTag: constraint.sourceTag,
tags: stringifyTags(constraint.notDependOnLibsWithTags),
projects: `- ${projects.map((p) => p.name).join(' -> ')}`,
},
});
return;
}
}
}
}
Expand Down
40 changes: 31 additions & 9 deletions packages/workspace/src/tslint/nxEnforceModuleBoundariesRule.ts
Expand Up @@ -11,13 +11,15 @@ import {
findSourceProject,
getSourceFilePath,
hasBuildExecutor,
hasNoneOfTheseTags,
findDependenciesWithTags,
isAbsoluteImportIntoAnotherProject,
isRelativeImportIntoAnotherProject,
MappedProjectGraph,
mapProjectGraphFiles,
matchImportWithWildcard,
onlyLoadChildren,
stringifyTags,
hasNoneOfTheseTags,
} from '../utils/runtime-lint-utils';
import { normalize } from 'path';
import { readNxJson } from '../core/file-utils';
Expand Down Expand Up @@ -272,18 +274,38 @@ class EnforceModuleBoundariesWalker extends Lint.RuleWalker {

for (let constraint of constraints) {
if (
hasNoneOfTheseTags(
targetProject,
constraint.onlyDependOnLibsWithTags || []
)
constraint.onlyDependOnLibsWithTags &&
hasNoneOfTheseTags(targetProject, constraint.onlyDependOnLibsWithTags)
) {
const tags = constraint.onlyDependOnLibsWithTags
.map((s) => `"${s}"`)
.join(', ');
const error = `A project tagged with "${constraint.sourceTag}" can only depend on libs tagged with ${tags}`;
const error = `A project tagged with "${
constraint.sourceTag
}" can only depend on libs tagged with ${stringifyTags(
constraint.onlyDependOnLibsWithTags
)}`;
this.addFailureAt(node.getStart(), node.getWidth(), error);
return;
}
if (
constraint.notDependOnLibsWithTags &&
constraint.notDependOnLibsWithTags.length
) {
const projects = findDependenciesWithTags(
targetProject,
constraint.notDependOnLibsWithTags,
this.projectGraph
);
if (projects.length > 0) {
const error = `A project tagged with "${
constraint.sourceTag
}" can not depend on libs tagged with ${stringifyTags(
constraint.notDependOnLibsWithTags
)}\n\nViolation detected in:\n${projects
.map((p) => p.name)
.join(' -> ')}`;
this.addFailureAt(node.getStart(), node.getWidth(), error);
return;
}
}
}
}

Expand Down
56 changes: 32 additions & 24 deletions packages/workspace/src/utils/runtime-lint-utils.ts
Expand Up @@ -35,44 +35,52 @@ export function stringifyTags(tags: string[]): string {
}

export function hasNoneOfTheseTags(
proj: ProjectGraphProjectNode<any>,
tags?: string[]
proj: ProjectGraphProjectNode,
tags: string[]
): boolean {
if (!tags) {
return false;
}
return tags.filter((tag) => hasTag(proj, tag)).length === 0;
}

export function hasAnyOfTheseTags(
/**
* Check if any of the given tags is included in the project
* @param proj ProjectGraphProjectNode
* @param tags
* @returns
*/
export function findDependenciesWithTags(
proj: ProjectGraphProjectNode,
tags: string[],
graph: ProjectGraph,
projectName: string,
tags?: string[],
visited?: string[]
): boolean {
if (!tags) {
return false;
foundPath: Array<ProjectGraphProjectNode> = [],
visited: string[] = []
): Array<ProjectGraphProjectNode> {
if (!hasNoneOfTheseTags(proj, tags)) {
return [...foundPath, proj];
}
const found =
tags.filter((tag) => hasTag(graph.nodes[projectName], tag)).length !== 0;

if (found) {
return true;
if (!graph.dependencies[proj.name]) {
return foundPath;
}

visited = visited ?? [];

for (let d of graph.dependencies[projectName] || []) {
for (let d of graph.dependencies[proj.name]) {
if (visited.indexOf(d.target) > -1) {
continue;
}
visited.push(d.target);
if (hasAnyOfTheseTags(graph, d.target, tags, visited)) {
return true;
if (graph.nodes[d.target]) {
const tempPath = [...foundPath, proj];
const newPath = findDependenciesWithTags(
graph.nodes[d.target],
tags,
graph,
tempPath,
visited
);
if (newPath !== tempPath) {
return newPath;
}
}
}

return false;
return foundPath;
}

function hasTag(proj: ProjectGraphProjectNode, tag: string) {
Expand Down

0 comments on commit 8c93ae9

Please sign in to comment.