From a520887a8101abafbf3ba45cc1830528a6d47439 Mon Sep 17 00:00:00 2001 From: Eric Dauenhauer Date: Fri, 9 Jun 2023 16:23:17 +0000 Subject: [PATCH] feat(linter): allow globs in onlyDependOnLibsWithTags eslint-plugin configuration option add support for globs (in addition to existing string, regex, and * options) to the onlyDependOnLibsWithTags configuration option for the eslint-plugin closed #15264 --- .../enforce-project-boundaries.md | 49 +++++++++++++++++-- .../rules/enforce-module-boundaries.spec.ts | 26 ++++++++++ .../src/utils/runtime-lint-utils.spec.ts | 49 +++++++++++++++++++ .../src/utils/runtime-lint-utils.ts | 6 +++ 4 files changed, 125 insertions(+), 5 deletions(-) diff --git a/docs/shared/core-features/enforce-project-boundaries.md b/docs/shared/core-features/enforce-project-boundaries.md index 37a4486187fd0..35ed2174b7f4d 100644 --- a/docs/shared/core-features/enforce-project-boundaries.md +++ b/docs/shared/core-features/enforce-project-boundaries.md @@ -131,7 +131,31 @@ Next you should update your root lint configuration: With these constraints in place, `scope:client` projects can only depend on other `scope:client` projects or on `scope:shared` projects. And `scope:admin` projects can only depend on other `scope:admin` projects or on `scope:shared` projects. So `scope:client` and `scope:admin` cannot depend on each other. -Projects without any tags cannot depend on any other projects. If you add the following, projects without any tags will be able to depend on any other project. +Projects without any tags cannot depend on any other projects, unless you allow all tags (see below). + +If you try to violate the constraints, you will get an error when linting: + +```shell +A project tagged with "scope:admin" can only depend on projects +tagged with "scoped:shared" or "scope:admin". +``` + +### Tag formats + +- `string`: allow exact tags + +Example: projects tagged with `scope:client` can only depend on projects tagged `scope:client` + +```json +{ + "sourceTag": "scope:client", + "onlyDependOnLibsWithTags": ["scope:client"] +} +``` + +- `*`: allow all tags + +Example: projects with any tags (including untagged) can depend on any other project. ```json { @@ -140,9 +164,24 @@ Projects without any tags cannot depend on any other projects. If you add the fo } ``` -If you try to violate the constraints, you will get an error when linting: +- `regex`: allow tags matching the regular expression -```shell -A project tagged with "scope:admin" can only depend on projects -tagged with "scoped:shared" or "scope:admin". +Example: projects tagged with `scope:client` can depend on projects with a tag matching the regular expression `/^scope.*/`. In this case `scope:a`, `scope:b`, etc are all allowed tags for dependencies. + +```json +{ + "sourceTag": "scope:client", + "onlyDependOnLibsWithTags": ["/^scope.*/"] +} +``` + +- `glob`: allow tags matching the glob + +Example: projects with a tag starting with `scope:` can depend on projects with a tag that starts with `scope:*`. In this case `scope:a`, `scope:b`, etc are all allowed tags for dependencies. + +```json +{ + "sourceTag": "scope:*", + "onlyDependOnLibsWithTags": ["scope:*"] +} ``` diff --git a/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts b/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts index 2af05c1d99c73..59e501affd764 100644 --- a/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts +++ b/packages/eslint-plugin/src/rules/enforce-module-boundaries.spec.ts @@ -877,6 +877,32 @@ Violation detected in: expect(failures.length).toEqual(0); }); + it('should support globs', () => { + const failures = runRule( + { + depConstraints: [ + { + sourceTag: 'p*', + onlyDependOnLibsWithTags: ['domain*'], + }, + ], + }, + `${process.cwd()}/proj/libs/public/src/index.ts`, + ` + import '@mycompany/impl-domain2'; + import('@mycompany/impl-domain2'); + import '@mycompany/impl-both-domains'; + import('@mycompany/impl-both-domains'); + import '@mycompany/impl'; + import('@mycompany/impl'); + `, + graph, + fileMap + ); + + expect(failures.length).toEqual(0); + }); + it('should report errors for combo source tags', () => { const failures = runRule( { diff --git a/packages/eslint-plugin/src/utils/runtime-lint-utils.spec.ts b/packages/eslint-plugin/src/utils/runtime-lint-utils.spec.ts index 6a6b75c225f13..3d10357a444c8 100644 --- a/packages/eslint-plugin/src/utils/runtime-lint-utils.spec.ts +++ b/packages/eslint-plugin/src/utils/runtime-lint-utils.spec.ts @@ -11,6 +11,7 @@ import { findTransitiveExternalDependencies, hasBannedDependencies, hasBannedImport, + hasNoneOfTheseTags, isAngularSecondaryEntrypoint, isTerminalRun, } from './runtime-lint-utils'; @@ -80,6 +81,28 @@ describe('findConstraintsFor', () => { }) ).toEqual([{ sourceTag: '/a|b/', onlyDependOnLibsWithTags: ['c'] }]); }); + + it('should find constraints matching glob', () => { + const constriants: DepConstraint[] = [ + { sourceTag: 'a:*', onlyDependOnLibsWithTags: ['b:*'] }, + { sourceTag: 'b:*', onlyDependOnLibsWithTags: ['c:*'] }, + { sourceTag: 'c:*', onlyDependOnLibsWithTags: ['a:*'] }, + ]; + expect( + findConstraintsFor(constriants, { + type: 'lib', + name: 'someLib', + data: { root: '.', tags: ['a:a'] }, + }) + ).toEqual([{ sourceTag: 'a:*', onlyDependOnLibsWithTags: ['b:*'] }]); + expect( + findConstraintsFor(constriants, { + type: 'lib', + name: 'someLib', + data: { root: '.', tags: ['a:abc'] }, + }) + ).toEqual([{ sourceTag: 'a:*', onlyDependOnLibsWithTags: ['b:*'] }]); + }); }); describe('hasBannedImport', () => { @@ -474,3 +497,29 @@ describe('isAngularSecondaryEntrypoint', () => { ).toBe(false); }); }); + +describe('hasNoneOfTheseTags', () => { + const source: ProjectGraphProjectNode = { + type: 'lib', + name: 'aLib', + data: { + tags: ['abc'], + } as any, + }; + + it.each([ + [true, ['a']], + [true, ['b']], + [true, ['c']], + [true, ['az*']], + [true, ['/[A-Z]+/']], + [false, ['ab*']], + [false, ['*']], + [false, ['/[a-z]*/']], + ])( + 'should return %s when project has tags ["abc"] and requested tags are %s', + (expected, tags) => { + expect(hasNoneOfTheseTags(source, tags)).toBe(expected); + } + ); +}); diff --git a/packages/eslint-plugin/src/utils/runtime-lint-utils.ts b/packages/eslint-plugin/src/utils/runtime-lint-utils.ts index c45fea2f9590a..dab29d3713133 100644 --- a/packages/eslint-plugin/src/utils/runtime-lint-utils.ts +++ b/packages/eslint-plugin/src/utils/runtime-lint-utils.ts @@ -103,6 +103,12 @@ function hasTag(proj: ProjectGraphProjectNode, tag: string): boolean { return (proj.data.tags || []).some((t) => regex.test(t)); } + // if the tag is a glob, check if the project matches the glob prefix + if (tag.endsWith('*')) { + const prefix = tag.substring(0, tag.length - 1); + return (proj.data.tags || []).some((t) => t.startsWith(prefix)); + } + return (proj.data.tags || []).indexOf(tag) > -1; }