From f9399ed52dd656c34b14a9431ee41d56d84bf951 Mon Sep 17 00:00:00 2001 From: Rainer Hahnekamp Date: Thu, 29 Aug 2024 19:42:35 +0200 Subject: [PATCH] fix(core): dependency rules - from tags Each from tag must match one of the to tags of its dependency. The old implementation did it the opposite. It iterated over all to tags and at least one from tag had to match. That meant, that from tags could end up having no match at all. Example: Module customer-feature: ['domain:holidays', 'type:feature'] Module shared-ui: ['shared'] Dependency Rules: { 'domain:*': sameTag, 'type:feature': ['shared'] } Since shared-ui had only one tag and type:feature had clearance, the dependency worked. Not it would fail because 'domain:holidays' would also have to have access 'shared'. --- .../check-for-dependency-rule-violation.ts | 35 +-- .../lib/checks/is-dependency-allowed.spec.ts | 232 ------------------ .../src/lib/checks/is-dependency-allowed.ts | 20 +- .../check-for-deep-imports.spec.ts | 10 +- ...heck-for-dependency-rule-violation.spec.ts | 127 +++++++--- .../tests/is-dependency-allowed.spec.ts | 213 ++++++++++++++++ .../tests/__snapshots__/verify.spec.ts.snap | 2 +- packages/core/src/lib/cli/verify.ts | 2 +- .../core/src/lib/config/parse-config.spec.ts | 2 +- .../eslint/violates-dependency-rule.spec.ts | 46 ++++ .../lib/eslint/violates-dependency-rule.ts | 4 +- packages/core/src/lib/extend.extensions.d.ts | 12 + .../get-ts-paths-and-root-dir.spec.ts | 2 +- packages/core/src/lib/fs/virtual-fs.spec.ts | 2 +- .../src/lib/tags/calc-tags-for-module.spec.ts | 2 +- .../{matchers.ts => expect.extensions.ts} | 16 -- test-projects/angular-i/integration-test.sh | 17 +- test-projects/angular-i/sheriff.config.ts | 7 +- .../angular-i/sheriff.config.ts.original | 39 +++ .../tests/dynamic-import-sheriff.config.ts | 7 +- .../angular-i/tests/expected/cli-export.txt | 43 ++++ .../angular-i/tests/expected/cli-list.txt | 20 +- .../tests/expected/cli-verify-failure.txt | 35 +++ ...{cli-verify.txt => cli-verify-success.txt} | 0 .../tests/expected/dependency-rule-lint.json | 2 +- .../tests/expected/dynamic-import-lint.json | 6 +- .../angular-i/tests/sheriff.config-failure.ts | 38 +++ test-projects/angular-ii/sheriff.config.ts | 3 +- test-projects/angular-iii/sheriff.config.ts | 7 +- test-projects/angular-iv/integration-test.sh | 17 +- test-projects/angular-iv/sheriff.config.ts | 7 +- ...stomers-container.deep-import.component.ts | 2 +- .../tests/dynamic-import-sheriff.config.ts | 7 +- .../angular-iv/tests/expected/cli-export.txt | 43 ++++ .../angular-iv/tests/expected/cli-list.txt | 20 +- .../tests/expected/cli-verify-failure.txt | 35 +++ ...{cli-verify.txt => cli-verify-success.txt} | 0 .../tests/expected/deep-import-lint.json | 4 +- .../tests/expected/dependency-rule-lint.json | 2 +- .../tests/expected/dynamic-import-lint.json | 6 +- .../tests/sheriff.config-failure.ts | 38 +++ vitest.config.ts | 1 + 42 files changed, 757 insertions(+), 376 deletions(-) delete mode 100644 packages/core/src/lib/checks/is-dependency-allowed.spec.ts rename packages/core/src/lib/checks/{ => tests}/check-for-deep-imports.spec.ts (85%) rename packages/core/src/lib/checks/{ => tests}/check-for-dependency-rule-violation.spec.ts (76%) create mode 100644 packages/core/src/lib/checks/tests/is-dependency-allowed.spec.ts create mode 100644 packages/core/src/lib/extend.extensions.d.ts rename packages/core/src/lib/test/{matchers.ts => expect.extensions.ts} (75%) create mode 100644 test-projects/angular-i/sheriff.config.ts.original create mode 100644 test-projects/angular-i/tests/expected/cli-verify-failure.txt rename test-projects/angular-i/tests/expected/{cli-verify.txt => cli-verify-success.txt} (100%) create mode 100644 test-projects/angular-i/tests/sheriff.config-failure.ts create mode 100644 test-projects/angular-iv/tests/expected/cli-verify-failure.txt rename test-projects/angular-iv/tests/expected/{cli-verify.txt => cli-verify-success.txt} (100%) create mode 100644 test-projects/angular-iv/tests/sheriff.config-failure.ts diff --git a/packages/core/src/lib/checks/check-for-dependency-rule-violation.ts b/packages/core/src/lib/checks/check-for-dependency-rule-violation.ts index 214252e..ae5572c 100644 --- a/packages/core/src/lib/checks/check-for-dependency-rule-violation.ts +++ b/packages/core/src/lib/checks/check-for-dependency-rule-violation.ts @@ -7,8 +7,8 @@ export type DependencyRuleViolation = { rawImport: string; fromModulePath: FsPath; toModulePath: FsPath; - fromTags: string[]; - toTag: string; + fromTag: string; + toTags: string[]; }; export function checkForDependencyRuleViolation( @@ -42,27 +42,32 @@ export function checkForDependencyRuleViolation( importedModulePath, rawImport, ] of importedModulePathsWithRawImport) { - const toTags: string[] = calcTagsForModule( - toFsPath(importedModulePath), - rootDir, - config.tagging, - config.autoTagging, - ); - for (const toTag of toTags) { - if ( - !isDependencyAllowed(fromTags, toTag, config.depRules, { + for (const fromTag of fromTags) { + const toTags: string[] = calcTagsForModule( + toFsPath(importedModulePath), + rootDir, + config.tagging, + config.autoTagging, + ); + const isViolation = !isDependencyAllowed( + fromTag, + toTags, + config.depRules, + { fromModulePath: fromModule, toModulePath: toFsPath(importedModulePath), fromFilePath: fsPath, toFilePath: toFsPath(importedModulePath), - }) - ) { + }, + ); + + if (isViolation) { violations.push({ rawImport, fromModulePath: fromModule, toModulePath: toFsPath(importedModulePath), - fromTags, - toTag, + fromTag, + toTags, }); break; diff --git a/packages/core/src/lib/checks/is-dependency-allowed.spec.ts b/packages/core/src/lib/checks/is-dependency-allowed.spec.ts deleted file mode 100644 index d121dd7..0000000 --- a/packages/core/src/lib/checks/is-dependency-allowed.spec.ts +++ /dev/null @@ -1,232 +0,0 @@ -import { describe, expect, test, it } from 'vitest'; -import { - DependencyCheckContext, - DependencyRulesConfig, -} from '../config/dependency-rules-config'; -import { isDependencyAllowed } from './is-dependency-allowed'; -import { FsPath } from '../file-info/fs-path'; -import { sameTag } from './same-tag'; -import { noDependencies } from './no-dependencies'; -import { NoDependencyRuleForTagError } from '../error/user-error'; -import '../test/matchers'; - -type TestParams = [string, boolean][]; - -describe('check dependency rules', () => { - const dummyContext: DependencyCheckContext = { - fromModulePath: '/project/moduleFrom' as FsPath, - toModulePath: '/project/moduleTo' as FsPath, - fromFilePath: '/project/moduleFrom/some.component.ts' as FsPath, - toFilePath: '/project/cool.service.ts' as FsPath, - }; - test('single string rule', () => { - const config: DependencyRulesConfig = { - 'type:feature': 'type:ui', - 'type:ui': '', - }; - - expect( - isDependencyAllowed(['type:feature'], 'type:ui', config, dummyContext), - ).toBe(true); - expect( - isDependencyAllowed(['type:ui'], 'type:feature', config, dummyContext), - ).toBe(false); - }); - - test('multiple string rules', () => { - const config: DependencyRulesConfig = { - 'type:feature': ['type:data', 'type:ui'], - }; - - expect( - isDependencyAllowed(['type:feature'], 'type:ui', config, dummyContext), - ).toBe(true); - expect( - isDependencyAllowed(['type:feature'], 'domain:abc', config, dummyContext), - ).toBe(false); - }); - - test('same tag not automatically allowed', () => { - const config: DependencyRulesConfig = { - 'type:feature': [], - }; - - expect( - isDependencyAllowed( - ['type:feature'], - 'type:feature', - config, - dummyContext, - ), - ).toBe(false); - }); - - for (const [to, isAllowed] of [ - ['type:ui', true], - ['type:data', true], - ['domain:shared', false], - ['super-type:shared', false], - ] as TestParams) { - test(`single matcher function for ${to} should be allowed: ${isAllowed}`, () => { - const config: DependencyRulesConfig = { - 'type:feature': [({ to }) => to.startsWith('type')], - }; - - expect( - isDependencyAllowed(['type:feature'], to, config, dummyContext), - ).toBe(isAllowed); - }); - } - - for (const [to, isAllowed] of [ - ['type:ui', true], - ['domain:abc', false], - ] as TestParams) { - test(`rule with string and function for ${to}: ${isAllowed}`, () => { - const config: DependencyRulesConfig = { - 'type:feature': ['type:data', 'type:ui'], - }; - - expect( - isDependencyAllowed(['type:feature'], to, config, dummyContext), - ).toBe(isAllowed); - }); - } - - it('should throw an error if module is not configured', () => { - const config: DependencyRulesConfig = { - 'type:feature': 'test:ui', - }; - - expect(() => - isDependencyAllowed(['type:funktion'], 'noop', config, dummyContext), - ).toThrowUserError(new NoDependencyRuleForTagError('type:funktion')); - }); - - it('should pass from, to, fromModulePath, fromFilePath, toModulePath, toFilePath to function', () => { - isDependencyAllowed( - ['domain:customers'], - 'domain:holidays', - { - 'domain:customers': (context) => { - expect(context).toStrictEqual({ - from: 'domain:customers', - to: 'domain:holidays', - ...dummyContext, - }); - return true; - }, - }, - dummyContext, - ); - }); - - for (const [to, isAllowed] of [ - ['domain:customers', true], - ['domain:shared', true], - ['domain:holidays', false], - ] as TestParams) { - it(`should support both string and function for a rule and should return for ${to}: ${isAllowed}`, () => { - const config: DependencyRulesConfig = { - 'domain:customers': [ - 'domain:shared', - ({ to }) => to === 'domain:customers', - ], - }; - expect( - isDependencyAllowed(['domain:customers'], to, config, dummyContext), - ).toBe(isAllowed); - }); - } - - for (const [to, isAllowed] of [ - ['domain:customers', true], - ['domain:shared', true], - ['domain:holidays', false], - ] as TestParams) { - it(`should return ${isAllowed} for catch all for ${to}`, () => { - const config: DependencyRulesConfig = { - 'domain:*': [ - ({ from, to }) => { - return from === to || to === 'domain:shared'; - }, - ], - }; - expect( - isDependencyAllowed(['domain:customers'], to, config, dummyContext), - ).toBe(isAllowed); - }); - } - - it('should run multipe checks, if tag is configured multiple times', () => { - const config: DependencyRulesConfig = { - 'domain:*': ({ from, to }) => from === to, - 'domain:bookings': 'domain:customers:api', - }; - - expect( - isDependencyAllowed( - ['domain:bookings'], - 'domain:customers:api', - config, - dummyContext, - ), - ).toBe(true); - }); - - it('should have access to a module when of the tags allow it', () => { - const config: DependencyRulesConfig = { - 'domain:*': [({ from, to }) => from === to, 'shared'], - 'type:feature': ['type:data', 'type:ui'], - }; - - expect( - isDependencyAllowed( - ['domain:booking', 'type:feature'], - 'shared', - config, - dummyContext, - ), - ).toBe(true); - }); - - it('should allow wildcard in rule values as well', () => { - const config: DependencyRulesConfig = { - 'type:feature': ['type:data', 'type:ui', 'shared:*'], - }; - - expect( - isDependencyAllowed(['type:feature'], 'shared:ui', config, dummyContext), - ).toBe(true); - }); - - for (const [to, from, isAllowed] of [ - ['domain:customers', 'domain:customers', true], - ['domain:holidays', 'domain:holidays', true], - ['domain:customers', 'domain:holidays', false], - ['domain:holidays', 'domain:customers', false], - ] as [string, string, boolean][]) { - it(`should work with pre-defined \`sameTag\` from ${from} to ${to}`, () => { - const config: DependencyRulesConfig = { - 'domain:*': sameTag, - }; - - expect(isDependencyAllowed([from], to, config, dummyContext)).toBe( - isAllowed, - ); - }); - } - - it.each(['type:model', '', 'shared'])( - 'should allow no dependencies with `noDependencies` on %s', - (toTag) => { - const config: DependencyRulesConfig = { - 'type:model': noDependencies, - }; - - expect( - isDependencyAllowed(['type:model'], toTag, config, dummyContext), - ).toBe(false); - }, - ); -}); diff --git a/packages/core/src/lib/checks/is-dependency-allowed.ts b/packages/core/src/lib/checks/is-dependency-allowed.ts index 6538067..fcd102e 100644 --- a/packages/core/src/lib/checks/is-dependency-allowed.ts +++ b/packages/core/src/lib/checks/is-dependency-allowed.ts @@ -6,16 +6,16 @@ import { wildcardToRegex } from '../util/wildcard-to-regex'; import { NoDependencyRuleForTagError } from '../error/user-error'; export const isDependencyAllowed = ( - froms: string[], - to: string, + from: string, + tos: string[], config: DependencyRulesConfig, context: DependencyCheckContext, ): boolean => { let isAllowed: boolean | undefined; - for (const from of froms) { - isAllowed = undefined; - for (const tag in config) { - if (from.match(wildcardToRegex(tag))) { + isAllowed = undefined; + for (const tag in config) { + if (from.match(wildcardToRegex(tag))) { + for (const to of tos) { const value = config[tag]; const matchers = Array.isArray(value) ? value : [value]; for (const matcher of matchers) { @@ -31,13 +31,13 @@ export const isDependencyAllowed = ( return true; } } - isAllowed = false; } + isAllowed = false; } + } - if (isAllowed === undefined) { - throw new NoDependencyRuleForTagError(from); - } + if (isAllowed === undefined) { + throw new NoDependencyRuleForTagError(from); } return false; diff --git a/packages/core/src/lib/checks/check-for-deep-imports.spec.ts b/packages/core/src/lib/checks/tests/check-for-deep-imports.spec.ts similarity index 85% rename from packages/core/src/lib/checks/check-for-deep-imports.spec.ts rename to packages/core/src/lib/checks/tests/check-for-deep-imports.spec.ts index 1f57f02..b3b39cd 100644 --- a/packages/core/src/lib/checks/check-for-deep-imports.spec.ts +++ b/packages/core/src/lib/checks/tests/check-for-deep-imports.spec.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from 'vitest'; -import { checkForDeepImports } from './check-for-deep-imports'; -import { toFsPath } from '../file-info/fs-path'; -import { testInit } from '../test/test-init'; -import { tsConfig } from '../test/fixtures/ts-config'; +import { checkForDeepImports } from '../check-for-deep-imports'; +import { toFsPath } from '../../file-info/fs-path'; +import { testInit } from '../../test/test-init'; +import { tsConfig } from '../../test/fixtures/ts-config'; describe('check deep imports', () => { it('should check for deep imports', () => { @@ -36,7 +36,7 @@ describe('check deep imports', () => { } }); - it('should ignore unresolable imports', () => { + it('should ignore unresolvable imports', () => { const projectInfo = testInit('src/main.ts', { 'tsconfig.json': tsConfig(), src: { diff --git a/packages/core/src/lib/checks/check-for-dependency-rule-violation.spec.ts b/packages/core/src/lib/checks/tests/check-for-dependency-rule-violation.spec.ts similarity index 76% rename from packages/core/src/lib/checks/check-for-dependency-rule-violation.spec.ts rename to packages/core/src/lib/checks/tests/check-for-dependency-rule-violation.spec.ts index 68a3e9c..f4bc3a3 100644 --- a/packages/core/src/lib/checks/check-for-dependency-rule-violation.spec.ts +++ b/packages/core/src/lib/checks/tests/check-for-dependency-rule-violation.spec.ts @@ -1,17 +1,16 @@ import { describe, expect, it } from 'vitest'; -import { sheriffConfig } from '../test/project-configurator'; -import { sameTag } from './same-tag'; -import { noDependencies } from './no-dependencies'; -import { testInit } from '../test/test-init'; +import { sheriffConfig } from '../../test/project-configurator'; +import { sameTag } from '../same-tag'; +import { noDependencies } from '../no-dependencies'; +import { testInit } from '../../test/test-init'; import { checkForDependencyRuleViolation, DependencyRuleViolation, -} from './check-for-dependency-rule-violation'; -import { FsPath, toFsPath } from '../file-info/fs-path'; -import { traverseProject } from '../test/traverse-project'; -import { tsConfig } from '../test/fixtures/ts-config'; -import { NoDependencyRuleForTagError } from '../error/user-error'; -import '../test/matchers'; +} from '../check-for-dependency-rule-violation'; +import { FsPath, toFsPath } from '../../file-info/fs-path'; +import { traverseProject } from '../../test/traverse-project'; +import { tsConfig } from '../../test/fixtures/ts-config'; +import { NoDependencyRuleForTagError } from '../../error/user-error'; describe('check for dependency rule violation', () => { describe('standard checks', () => { @@ -63,7 +62,7 @@ describe('check for dependency rule violation', () => { 'src//': ['domain:', 'type:'], }, depRules: { - root: ['type:feature', 'domain:*'], + root: ['type:feature'], 'domain:*': sameTag, 'type:feature': ['type:ui', 'type:data', 'type:model'], 'type:data': 'type:model', @@ -77,33 +76,45 @@ describe('check for dependency rule violation', () => { type Project = ReturnType; type TestParam = [ - string, - Record, - (project: Project) => void, + string, //name + Record, // expected violation + (project: Project) => void, // changes to imports ]; const params: TestParam[] = [ ['no violations', {}, () => true], [ 'root -> ui', - { 'app.component.ts': [{ from: ['root'], to: 'type:ui' }] }, + { + 'app.component.ts': [ + { from: 'root', to: ['domain:customers', 'type:ui'] }, + ], + }, (project) => project.src['app.component.ts'].push('./customers/ui'), ], [ 'root -> model', - { 'app.component.ts': [{ from: ['root'], to: 'type:model' }] }, + { + 'app.component.ts': [ + { from: 'root', to: ['domain:customers', 'type:model'] }, + ], + }, (project) => project.src['app.component.ts'].push('./customers/model'), ], [ 'root -> data', - { 'app.component.ts': [{ from: ['root'], to: 'type:data' }] }, + { + 'app.component.ts': [ + { from: 'root', to: ['domain:customers', 'type:data'] }, + ], + }, (project) => project.src['app.component.ts'].push('./customers/data'), ], [ 'feature -> feature', { 'holidays/feat-admin/index.ts': [ - { from: ['domain:holidays', 'type:feature'], to: 'type:feature' }, + { from: 'type:feature', to: ['domain:holidays', 'type:feature'] }, ], }, (project) => @@ -116,8 +127,8 @@ describe('check for dependency rule violation', () => { { 'customers/feature/index.ts': [ { - from: ['domain:customers', 'type:feature'], - to: 'domain:holidays', + from: 'domain:customers', + to: ['domain:holidays', 'type:ui'], }, ], }, @@ -128,7 +139,7 @@ describe('check for dependency rule violation', () => { 'data -> ui', { 'customers/data/index.ts': [ - { from: ['domain:customers', 'type:data'], to: 'type:ui' }, + { from: 'type:data', to: ['domain:customers', 'type:ui'] }, ], }, (project) => @@ -138,7 +149,7 @@ describe('check for dependency rule violation', () => { 'ui -> data', { 'customers/ui/index.ts': [ - { from: ['domain:customers', 'type:ui'], to: 'type:data' }, + { from: 'type:ui', to: ['domain:customers', 'type:data'] }, ], }, (project) => @@ -149,13 +160,13 @@ describe('check for dependency rule violation', () => { { 'customers/model/index.ts': [ { - from: ['domain:customers', 'type:model'], - to: 'type:data', + from: 'type:model', + to: ['domain:customers', 'type:data'], }, - { from: ['domain:customers', 'type:model'], to: 'type:ui' }, + { from: 'type:model', to: ['domain:customers', 'type:ui'] }, { - from: ['domain:customers', 'type:model'], - to: 'type:feature', + from: 'type:model', + to: ['domain:customers', 'type:feature'], }, ], }, @@ -173,17 +184,17 @@ describe('check for dependency rule violation', () => { ): Record< string, { - from: string[]; - to: string; + from: string; + to: string[]; }[] > { - const result: Record = {}; + const result: Record = {}; for (const [path, violation] of Object.entries(violations)) { const shortenedPath = path.replace('/project/src/', ''); result[shortenedPath] = violation.map((v) => ({ - from: v.fromTags, - to: v.toTag, + from: v.fromTag, + to: v.toTags, })); } @@ -198,6 +209,13 @@ describe('check for dependency rule violation', () => { const violationMap: Record = {}; for (const path of traverseProject(project.src, '/project/src')) { + if ( + name.startsWith('model -> ui, data') && + path === '/project/src/customers/model/index.ts' + ) { + debugger; + } + const violations = checkForDependencyRuleViolation(path, projectInfo); if (violations.length) { @@ -226,6 +244,49 @@ describe('check for dependency rule violation', () => { expect(violations).toEqual([]); }); + it('should require that each existing tag has clearance', () => { + const project = { + 'tsconfig.json': tsConfig(), + 'sheriff.config.ts': sheriffConfig({ + tagging: { + 'src/shared/': ['shared'], + 'src//': ['domain:', 'type:'], + }, + depRules: { + root: ['type:feature'], + 'domain:*': sameTag, + 'type:feature': ['type:ui', 'shared'], + 'type:ui': noDependencies, + }, + }), + src: { + 'app.component.ts': ['./customers/feature'], + customers: { + feature: { + 'index.ts': ['../../shared/ui'], + }, + }, + shared: { + ui: { + 'index.ts': [], + }, + }, + }, + }; + + const projectInfo = testInit('src/app.component.ts', project); + const violations = checkForDependencyRuleViolation( + toFsPath('/project/src/customers/feature/index.ts'), + projectInfo, + ); + + expect(violations).toHaveLength(1); + expect(violations[0]).toMatchObject({ + fromTag: 'domain:customers', + toTags: ['shared'], + }); + }); + describe('noTag', () => { it('should allow full access with noTag', () => { const project = { @@ -410,7 +471,7 @@ describe('check for dependency rule violation', () => { 'app.component.ts': 0, 'customers/feature/index.ts': 0, 'customers/ui/index.ts': 1, - 'customers/data/index.ts': 0, + 'customers/data/index.ts': 1, 'holidays/feature/index.ts': 0, }); }); diff --git a/packages/core/src/lib/checks/tests/is-dependency-allowed.spec.ts b/packages/core/src/lib/checks/tests/is-dependency-allowed.spec.ts new file mode 100644 index 0000000..c68937d --- /dev/null +++ b/packages/core/src/lib/checks/tests/is-dependency-allowed.spec.ts @@ -0,0 +1,213 @@ +import { describe, expect, test, it } from 'vitest'; +import { + DependencyCheckContext, + DependencyRulesConfig, +} from '../../config/dependency-rules-config'; +import { isDependencyAllowed } from '../is-dependency-allowed'; +import { FsPath } from '../../file-info/fs-path'; +import { sameTag } from '../same-tag'; +import { noDependencies } from '../no-dependencies'; +import { NoDependencyRuleForTagError } from '../../error/user-error'; +import '../../test/expect.extensions'; + +type TestParams = [string, boolean][]; + +const dummyContext: DependencyCheckContext = { + fromModulePath: '/project/moduleFrom' as FsPath, + toModulePath: '/project/moduleTo' as FsPath, + fromFilePath: '/project/moduleFrom/some.component.ts' as FsPath, + toFilePath: '/project/cool.service.ts' as FsPath, +}; + +const createAssertsForConfig = (config: DependencyRulesConfig) => { + return { + assertValid(from: string, to: string | string[]) { + expect( + isDependencyAllowed( + from, + Array.isArray(to) ? to : [to], + config, + {} as DependencyCheckContext, + ), + ).toBe(true); + }, + assertInvalid(from: string, to: string | string[]) { + expect( + isDependencyAllowed( + from, + Array.isArray(to) ? to : [to], + config, + {} as DependencyCheckContext, + ), + ).toBe(false); + }, + + assert(from: string, to: string | string[], expected: boolean) { + expect( + isDependencyAllowed( + from, + Array.isArray(to) ? to : [to], + config, + {} as DependencyCheckContext, + ), + ).toBe(expected); + }, + }; +}; + +describe('check dependency rules', () => { + test('single string rule', () => { + const { assertValid, assertInvalid } = createAssertsForConfig({ + 'type:feature': 'type:ui', + 'type:ui': '', + }); + + assertValid('type:feature', 'type:ui'); + assertInvalid('type:ui', 'type:feature'); + }); + + test('multiple string rules', () => { + const { assertValid, assertInvalid } = createAssertsForConfig({ + 'type:feature': ['type:data', 'type:ui'], + }); + + assertValid('type:feature', 'type:ui'); + assertInvalid('type:feature', 'domain:abc'); + }); + + test('same tag is not automatically allowed', () => { + const { assertInvalid } = createAssertsForConfig({ 'type:feature': [] }); + + assertInvalid('type:feature', 'type:feature'); + }); + + for (const [to, isAllowed] of [ + ['type:ui', true], + ['type:data', true], + ['domain:shared', false], + ['super-type:shared', false], + ] as TestParams) { + test(`single matcher function for ${to} should be allowed: ${isAllowed}`, () => { + const { assert } = createAssertsForConfig({ + 'type:feature': [({ to }) => to.startsWith('type')], + }); + + assert('type:feature', to, isAllowed); + }); + } + + it('should throw an error if tag is not configured', () => { + const config: DependencyRulesConfig = { + 'type:feature': 'test:ui', + }; + + expect(() => + isDependencyAllowed('type:funktion', ['noop'], config, dummyContext), + ).toThrowUserError(new NoDependencyRuleForTagError('type:funktion')); + }); + + it('should pass from, to, fromModulePath, fromFilePath, toModulePath, toFilePath to function', () => { + isDependencyAllowed( + 'domain:customers', + ['domain:holidays'], + { + 'domain:customers': (context) => { + expect(context).toStrictEqual({ + from: 'domain:customers', + to: 'domain:holidays', + ...dummyContext, + }); + return true; + }, + }, + dummyContext, + ); + }); + + for (const [to, isAllowed] of [ + ['domain:customers', true], + ['domain:shared', true], + ['domain:holidays', false], + ] as TestParams) { + it(`should support both string and function for a rule and should return for ${to}: ${isAllowed}`, () => { + const { assert } = createAssertsForConfig({ + 'domain:customers': [ + 'domain:shared', + ({ to }) => to === 'domain:customers', + ], + }); + + assert('domain:customers', to, isAllowed); + }); + } + + for (const [to, isAllowed] of [ + ['domain:customers', true], + ['domain:shared', true], + ['domain:holidays', false], + ] as TestParams) { + it(`should return ${isAllowed} for catch all for ${to}`, () => { + const { assert } = createAssertsForConfig({ + 'domain:*': [ + ({ from, to }) => { + return from === to || to === 'domain:shared'; + }, + ], + }); + + assert('domain:customers', to, isAllowed); + }); + } + + it('should run multiple checks, if tag is configured multiple times', () => { + const { assertValid } = createAssertsForConfig({ + 'domain:*': ({ from, to }) => from === to, + 'domain:bookings': 'domain:customers:api', + }); + + assertValid('domain:bookings', 'domain:customers:api'); + }); + + it('should have access to a module when of the tags allow it', () => { + const { assertValid } = createAssertsForConfig({ + 'domain:*': [({ from, to }) => from === to, 'shared'], + 'type:feature': ['type:data', 'type:ui'], + }); + + assertValid('domain:bookings', ['shared', 'type:shared']); + }); + + it('should allow wildcard in rule values as well', () => { + const { assertValid } = createAssertsForConfig({ + 'type:feature': ['type:data', 'type:ui', 'shared:*'], + }); + + assertValid('type:feature', 'shared:ui'); + }); + + for (const [to, from, isAllowed] of [ + ['domain:customers', 'domain:customers', true], + ['domain:holidays', 'domain:holidays', true], + ['domain:customers', 'domain:holidays', false], + ['domain:holidays', 'domain:customers', false], + ] as [string, string, boolean][]) { + it(`should work with pre-defined \`sameTag\` from ${from} to ${to}`, () => { + const { assert } = createAssertsForConfig({ + 'domain:*': sameTag, + }); + + assert(from, to, isAllowed); + }); + } + + it.each(['type:model', '', 'shared'])( + 'should allow no dependencies with `noDependencies` on %s', + (toTag) => { + const { assertInvalid } = createAssertsForConfig({ + 'type:model': noDependencies, + }); + + assertInvalid('type:model', toTag); + }, + ); +}); diff --git a/packages/core/src/lib/cli/tests/__snapshots__/verify.spec.ts.snap b/packages/core/src/lib/cli/tests/__snapshots__/verify.spec.ts.snap index de3a659..41f86da 100644 --- a/packages/core/src/lib/cli/tests/__snapshots__/verify.spec.ts.snap +++ b/packages/core/src/lib/cli/tests/__snapshots__/verify.spec.ts.snap @@ -17,7 +17,7 @@ Issues found: | | |-- ./customers/data |-- src/holidays/holidays.component.ts | |-- Dependency Rule Violations -| | |-- from tags holidays to customers" +| | |-- from tag holidays to tags customers" `; exports[`verify > should find errors without sheriff.config.ts > error.log 1`] = `""`; diff --git a/packages/core/src/lib/cli/verify.ts b/packages/core/src/lib/cli/verify.ts index e9af506..b08115f 100644 --- a/packages/core/src/lib/cli/verify.ts +++ b/packages/core/src/lib/cli/verify.ts @@ -35,7 +35,7 @@ export function verify(args: string[]) { const dependencyRules = dependencyRuleViolations.map( (violation) => - `from tags ${violation.fromTags.join(',')} to ${violation.toTag}`, + `from tag ${violation.fromTag} to tags ${violation.toTags.join(', ')}`, ); validationsMap[fs.relativeTo(fs.cwd(), fileInfo.path)] = { diff --git a/packages/core/src/lib/config/parse-config.spec.ts b/packages/core/src/lib/config/parse-config.spec.ts index 53222ad..f27d95c 100644 --- a/packages/core/src/lib/config/parse-config.spec.ts +++ b/packages/core/src/lib/config/parse-config.spec.ts @@ -5,7 +5,7 @@ import { toFsPath } from '../file-info/fs-path'; import { TsConfig } from '../file-info/ts-config'; import getFs, { useVirtualFs } from '../fs/getFs'; import { MissingTaggingWithoutAutoTagging } from '../error/user-error'; -import '../test/matchers'; +import '../test/expect.extensions'; describe('parse Config', () => { it('should read value', () => { diff --git a/packages/core/src/lib/eslint/violates-dependency-rule.spec.ts b/packages/core/src/lib/eslint/violates-dependency-rule.spec.ts index 52a6329..da1caab 100644 --- a/packages/core/src/lib/eslint/violates-dependency-rule.spec.ts +++ b/packages/core/src/lib/eslint/violates-dependency-rule.spec.ts @@ -6,8 +6,54 @@ import getFs from '../fs/getFs'; import { toFsPath } from '../file-info/fs-path'; import { violatesDependencyRule } from './violates-dependency-rule'; import { tsConfig } from '../test/fixtures/ts-config'; +import { noDependencies, sameTag } from '@softarc/sheriff-core'; describe('violates dependency rules', () => { + it('should require that each existing tag has clearance', () => { + createProject({ + 'tsconfig.json': tsConfig(), + 'sheriff.config.ts': sheriffConfig({ + tagging: { + 'src/shared/': ['shared'], + 'src//': ['domain:', 'type:'], + }, + depRules: { + root: ['type:feature'], + 'domain:*': sameTag, + 'type:feature': ['type:ui', 'shared'], + 'type:ui': noDependencies, + }, + }), + src: { + 'app.component.ts': ['./customers/feature'], + customers: { + feature: { + 'index.ts': ['../ui', '../../shared/ui'], + }, + ui: { + 'index.ts': ['../feature'], + }, + }, + shared: { + ui: { + 'index.ts': ['./ui.component.ts'], + 'ui.component.ts': [], + }, + }, + }, + }); + + const violations = violatesDependencyRule( + '/project/src/customers/feature/index.ts', + '../../shared/ui', + true, + getFs().readFile(toFsPath('/project/src/customers/feature/index.ts')), + ); + expect(violations).toBe( + 'module /src/customers/feature cannot access /src/shared/ui. Tag domain:customers has no clearance for tags shared', + ); + }); + it('should not generate fileInfo when no config file available', () => { const spy = vitest.spyOn(fileInfoGenerator, 'generateUnassignedFileInfo'); diff --git a/packages/core/src/lib/eslint/violates-dependency-rule.ts b/packages/core/src/lib/eslint/violates-dependency-rule.ts index 68c7a19..0b52822 100644 --- a/packages/core/src/lib/eslint/violates-dependency-rule.ts +++ b/packages/core/src/lib/eslint/violates-dependency-rule.ts @@ -66,10 +66,10 @@ function formatViolation( violation: DependencyRuleViolation, rootDir: FsPath, ): string { - const { fromModulePath, toModulePath, fromTags, toTag } = violation; + const { fromModulePath, toModulePath, fromTag, toTags } = violation; return `module ${fromModulePath.substring( rootDir.length, )} cannot access ${toModulePath.substring( rootDir.length, - )}. Tags [${fromTags.join(',')}] have no clearance for ${toTag}`; + )}. Tag ${fromTag} has no clearance for tags ${toTags.join(', ')}`; } diff --git a/packages/core/src/lib/extend.extensions.d.ts b/packages/core/src/lib/extend.extensions.d.ts new file mode 100644 index 0000000..e32ca57 --- /dev/null +++ b/packages/core/src/lib/extend.extensions.d.ts @@ -0,0 +1,12 @@ +import { UserError } from '@softarc/sheriff-core'; + +declare module 'vitest' { + interface Assertion { + + toBeVfsFile(expected: string): T; + + toBeVfsFiles(expected: string[]): T; + + toThrowUserError(userError: UserError): T; + } +} diff --git a/packages/core/src/lib/file-info/get-ts-paths-and-root-dir.spec.ts b/packages/core/src/lib/file-info/get-ts-paths-and-root-dir.spec.ts index fb86056..b34c848 100644 --- a/packages/core/src/lib/file-info/get-ts-paths-and-root-dir.spec.ts +++ b/packages/core/src/lib/file-info/get-ts-paths-and-root-dir.spec.ts @@ -3,7 +3,7 @@ import { createProject } from '../test/project-creator'; import { toFsPath } from './fs-path'; import { getTsPathsAndRootDir } from './get-ts-paths-and-root-dir'; import { InvalidPathError } from '../error/user-error'; -import '../test/matchers'; +import '../test/expect.extensions'; import { tsConfig } from '../test/fixtures/ts-config'; describe('get ts paths and root dir', () => { diff --git a/packages/core/src/lib/fs/virtual-fs.spec.ts b/packages/core/src/lib/fs/virtual-fs.spec.ts index 61845db..682c01c 100644 --- a/packages/core/src/lib/fs/virtual-fs.spec.ts +++ b/packages/core/src/lib/fs/virtual-fs.spec.ts @@ -3,7 +3,7 @@ import { VirtualFs } from './virtual-fs'; import { inVfs } from '../test/in-vfs'; import { toFsPath } from '../file-info/fs-path'; import getFs, { useVirtualFs } from './getFs'; -import '../test/matchers'; +import '../test/expect.extensions'; import { EOL } from 'os'; describe('Virtual Fs', () => { diff --git a/packages/core/src/lib/tags/calc-tags-for-module.spec.ts b/packages/core/src/lib/tags/calc-tags-for-module.spec.ts index 2ef0ba3..b555771 100644 --- a/packages/core/src/lib/tags/calc-tags-for-module.spec.ts +++ b/packages/core/src/lib/tags/calc-tags-for-module.spec.ts @@ -8,7 +8,7 @@ import { NoAssignedTagError, TagWithoutValueError, } from '../error/user-error'; -import '../test/matchers'; +import '../test/expect.extensions'; describe('calc tags for module', () => { const root = '/project' as FsPath; diff --git a/packages/core/src/lib/test/matchers.ts b/packages/core/src/lib/test/expect.extensions.ts similarity index 75% rename from packages/core/src/lib/test/matchers.ts rename to packages/core/src/lib/test/expect.extensions.ts index dd90ef5..edf9c29 100644 --- a/packages/core/src/lib/test/matchers.ts +++ b/packages/core/src/lib/test/expect.extensions.ts @@ -46,19 +46,3 @@ expect.extend({ }; }, }); - -interface CustomMatchers { - toBeVfsFile(expected: string): R; - - toBeVfsFiles(expected: string[]): R; - - toThrowUserError(userError: UserError): R; -} - -declare global { - // eslint-disable-next-line @typescript-eslint/no-namespace - namespace Vi { - // eslint-disable-next-line @typescript-eslint/no-empty-object-type - interface Assertion extends CustomMatchers {} - } -} diff --git a/test-projects/angular-i/integration-test.sh b/test-projects/angular-i/integration-test.sh index 50cb2c3..eae17a1 100755 --- a/test-projects/angular-i/integration-test.sh +++ b/test-projects/angular-i/integration-test.sh @@ -2,7 +2,7 @@ set -e yarn yalc add @softarc/sheriff-core @softarc/eslint-plugin-sheriff cd node_modules/.bin # yalc doesn't create symlink in node_modules/.bin -ln -s ../@softarc/sheriff-core/src/bin/main.js ./sheriff +ln -sf ../@softarc/sheriff-core/src/bin/main.js ./sheriff cd ../../ cp sheriff.config.ts sheriff.config.ts.original @@ -16,10 +16,17 @@ echo 'checking for CLI export' npx sheriff export src/main.ts > tests/actual/cli-export.txt diff tests/actual/cli-export.txt tests/expected/cli-export.txt -# CLI Verify Check -echo 'checking for CLI verify' -npx sheriff verify src/main.ts > tests/actual/cli-verify.txt -diff tests/actual/cli-verify.txt tests/expected/cli-verify.txt +# CLI Verify Check (failure) +echo 'checking for CLI verify (failure scenario)' +cp tests/sheriff.config-failure.ts sheriff.config.ts +npx sheriff verify src/main.ts > tests/actual/cli-verify-failure.txt || true +diff tests/actual/cli-verify-failure.txt tests/expected/cli-verify-failure.txt +cp sheriff.config.ts.original sheriff.config.ts + +# CLI Verify Check (success) +echo 'checking for CLI verify (success scenario)' +npx sheriff verify src/main.ts > tests/actual/cli-verify-success.txt +diff tests/actual/cli-verify-success.txt tests/expected/cli-verify-success.txt # Dynamic Import Check echo 'checking for dynamic import error' diff --git a/test-projects/angular-i/sheriff.config.ts b/test-projects/angular-i/sheriff.config.ts index bea41a3..a5cfb03 100644 --- a/test-projects/angular-i/sheriff.config.ts +++ b/test-projects/angular-i/sheriff.config.ts @@ -4,15 +4,15 @@ export const config: SheriffConfig = { version: 1, tagging: { 'src/app': { - 'shared/': 'shared:', + 'shared/': ['shared', 'shared:'], bookings: ['domain:bookings', 'type:feature'], 'customers/api': ['type:api', 'domain:customers:api'], '/': ['domain:', 'type:'], }, }, depRules: { - root: ['type:feature', 'shared:*', 'domain:*'], - 'domain:*': sameTag, + root: ['type:feature', 'shared:*'], + 'domain:*': [sameTag, 'shared'], 'domain:bookings': 'domain:customers:api', 'domain:customers:api': 'domain:customers', 'type:api': 'type:*', @@ -32,6 +32,7 @@ export const config: SheriffConfig = { ], 'type:ui': ['type:model', 'shared:form', 'shared:ui'], 'type:model': noDependencies, + 'shared': 'shared:*', 'shared:http': ['shared:config', 'shared:ui-messaging'], 'shared:ngrx-utils': ['shared:util'], }, diff --git a/test-projects/angular-i/sheriff.config.ts.original b/test-projects/angular-i/sheriff.config.ts.original new file mode 100644 index 0000000..a5cfb03 --- /dev/null +++ b/test-projects/angular-i/sheriff.config.ts.original @@ -0,0 +1,39 @@ +import { noDependencies, sameTag, SheriffConfig } from '@softarc/sheriff-core'; + +export const config: SheriffConfig = { + version: 1, + tagging: { + 'src/app': { + 'shared/': ['shared', 'shared:'], + bookings: ['domain:bookings', 'type:feature'], + 'customers/api': ['type:api', 'domain:customers:api'], + '/': ['domain:', 'type:'], + }, + }, + depRules: { + root: ['type:feature', 'shared:*'], + 'domain:*': [sameTag, 'shared'], + 'domain:bookings': 'domain:customers:api', + 'domain:customers:api': 'domain:customers', + 'type:api': 'type:*', + 'type:feature': [ + 'type:*', + 'shared:config', + 'shared:form', + 'shared:master-data', + 'shared:ngrx-utils', + 'shared:util', + ], + 'type:data': [ + 'type:model', + 'shared:http', + 'shared:ngrx-utils', + 'shared:ui-messaging', + ], + 'type:ui': ['type:model', 'shared:form', 'shared:ui'], + 'type:model': noDependencies, + 'shared': 'shared:*', + 'shared:http': ['shared:config', 'shared:ui-messaging'], + 'shared:ngrx-utils': ['shared:util'], + }, +}; diff --git a/test-projects/angular-i/tests/dynamic-import-sheriff.config.ts b/test-projects/angular-i/tests/dynamic-import-sheriff.config.ts index 95d987c..d90071b 100644 --- a/test-projects/angular-i/tests/dynamic-import-sheriff.config.ts +++ b/test-projects/angular-i/tests/dynamic-import-sheriff.config.ts @@ -4,15 +4,15 @@ export const config: SheriffConfig = { version: 1, tagging: { 'src/app': { - 'shared/': 'shared:', + 'shared/': ['shared', 'shared:'], bookings: ['domain:bookings', 'type:feature'], 'customers/api': ['type:api', 'domain:customers:api'], '/': ['domain:', 'type:'], }, }, depRules: { - root: ['type:feature', 'shared:*'], - 'domain:*': sameTag, + root: ['shared:*'], + 'domain:*': [sameTag, 'shared'], 'domain:bookings': 'domain:customers:api', 'domain:customers:api': 'domain:customers', 'type:api': 'type:*', @@ -32,6 +32,7 @@ export const config: SheriffConfig = { ], 'type:ui': ['type:model', 'shared:form', 'shared:ui'], 'type:model': noDependencies, + 'shared': 'shared:*', 'shared:http': ['shared:config', 'shared:ui-messaging'], 'shared:ngrx-utils': ['shared:util'], }, diff --git a/test-projects/angular-i/tests/expected/cli-export.txt b/test-projects/angular-i/tests/expected/cli-export.txt index 0751ce0..0661bc0 100644 --- a/test-projects/angular-i/tests/expected/cli-export.txt +++ b/test-projects/angular-i/tests/expected/cli-export.txt @@ -38,6 +38,7 @@ "src/app/shared/security/index.ts": { "module": "src/app/shared/security", "tags": [ + "shared", "shared:security" ], "imports": [ @@ -48,6 +49,7 @@ "src/app/shared/security/security.provider.ts": { "module": "src/app/shared/security", "tags": [ + "shared", "shared:security" ], "imports": [ @@ -58,6 +60,7 @@ "src/app/shared/security/security.effects.ts": { "module": "src/app/shared/security", "tags": [ + "shared", "shared:security" ], "imports": [ @@ -68,6 +71,7 @@ "src/app/shared/security/security.actions.ts": { "module": "src/app/shared/security", "tags": [ + "shared", "shared:security" ], "imports": [ @@ -77,6 +81,7 @@ "src/app/shared/security/security.reducer.ts": { "module": "src/app/shared/security", "tags": [ + "shared", "shared:security" ], "imports": [ @@ -86,6 +91,7 @@ "src/app/shared/security/security.service.ts": { "module": "src/app/shared/security", "tags": [ + "shared", "shared:security" ], "imports": [ @@ -97,6 +103,7 @@ "src/app/shared/security/security.selectors.ts": { "module": "src/app/shared/security", "tags": [ + "shared", "shared:security" ], "imports": [ @@ -115,6 +122,7 @@ "src/app/shared/ui-messaging/index.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [ @@ -129,6 +137,7 @@ "src/app/shared/ui-messaging/shared-ui-messaging.provider.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [ @@ -138,6 +147,7 @@ "src/app/shared/ui-messaging/loader/loading.interceptor.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [ @@ -148,6 +158,7 @@ "src/app/shared/ui-messaging/loader/loading.service.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [] @@ -155,6 +166,7 @@ "src/app/shared/ui-messaging/loader/silent-load.context.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [] @@ -162,6 +174,7 @@ "src/app/shared/ui-messaging/loader/loader.component.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [ @@ -171,6 +184,7 @@ "src/app/shared/ui-messaging/message/message.service.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [ @@ -181,6 +195,7 @@ "src/app/shared/ui-messaging/message/message.store.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [ @@ -190,6 +205,7 @@ "src/app/shared/ui-messaging/message/message.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [] @@ -197,6 +213,7 @@ "src/app/shared/ui-messaging/message/confirmation.component.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [] @@ -204,6 +221,7 @@ "src/app/shared/ui-messaging/message/message.component.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [ @@ -214,6 +232,7 @@ "src/app/shared/config/index.ts": { "module": "src/app/shared/config", "tags": [ + "shared", "shared:config" ], "imports": [ @@ -223,6 +242,7 @@ "src/app/shared/config/configuration.ts": { "module": "src/app/shared/config", "tags": [ + "shared", "shared:config" ], "imports": [] @@ -230,6 +250,7 @@ "src/app/shared/http/index.ts": { "module": "src/app/shared/http", "tags": [ + "shared", "shared:http" ], "imports": [ @@ -241,6 +262,7 @@ "src/app/shared/http/error.interceptor.ts": { "module": "src/app/shared/http", "tags": [ + "shared", "shared:http" ], "imports": [ @@ -251,6 +273,7 @@ "src/app/shared/http/error-message.context.ts": { "module": "src/app/shared/http", "tags": [ + "shared", "shared:http" ], "imports": [] @@ -258,6 +281,7 @@ "src/app/shared/http/base-url.interceptor.ts": { "module": "src/app/shared/http", "tags": [ + "shared", "shared:http" ], "imports": [ @@ -267,6 +291,7 @@ "src/app/shared/http/with-error-message-context.ts": { "module": "src/app/shared/http", "tags": [ + "shared", "shared:http" ], "imports": [ @@ -276,6 +301,7 @@ "src/app/shared/master-data/index.ts": { "module": "src/app/shared/master-data", "tags": [ + "shared", "shared:master-data" ], "imports": [ @@ -286,6 +312,7 @@ "src/app/shared/master-data/shared-master-data.provider.ts": { "module": "src/app/shared/master-data", "tags": [ + "shared", "shared:master-data" ], "imports": [ @@ -295,6 +322,7 @@ "src/app/shared/master-data/+state/master.reducer.ts": { "module": "src/app/shared/master-data", "tags": [ + "shared", "shared:master-data" ], "imports": [] @@ -410,6 +438,7 @@ "src/app/shared/form/index.ts": { "module": "src/app/shared/form", "tags": [ + "shared", "shared:form" ], "imports": [ @@ -420,6 +449,7 @@ "src/app/shared/form/options.ts": { "module": "src/app/shared/form", "tags": [ + "shared", "shared:form" ], "imports": [] @@ -427,6 +457,7 @@ "src/app/shared/form/form-errors.component.ts": { "module": "src/app/shared/form", "tags": [ + "shared", "shared:form" ], "imports": [] @@ -652,6 +683,7 @@ "src/app/shared/ngrx-utils/index.ts": { "module": "src/app/shared/ngrx-utils", "tags": [ + "shared", "shared:ngrx-utils" ], "imports": [ @@ -665,6 +697,7 @@ "src/app/shared/ngrx-utils/noop.action.ts": { "module": "src/app/shared/ngrx-utils", "tags": [ + "shared", "shared:ngrx-utils" ], "imports": [] @@ -672,6 +705,7 @@ "src/app/shared/ngrx-utils/safe-concat-map.ts": { "module": "src/app/shared/ngrx-utils", "tags": [ + "shared", "shared:ngrx-utils" ], "imports": [ @@ -681,6 +715,7 @@ "src/app/shared/ngrx-utils/filter-defined.ts": { "module": "src/app/shared/ngrx-utils", "tags": [ + "shared", "shared:ngrx-utils" ], "imports": [ @@ -690,6 +725,7 @@ "src/app/shared/util/index.ts": { "module": "src/app/shared/util", "tags": [ + "shared", "shared:util" ], "imports": [ @@ -701,6 +737,7 @@ "src/app/shared/util/assert-defined.ts": { "module": "src/app/shared/util", "tags": [ + "shared", "shared:util" ], "imports": [] @@ -708,6 +745,7 @@ "src/app/shared/util/is-defined.ts": { "module": "src/app/shared/util", "tags": [ + "shared", "shared:util" ], "imports": [] @@ -715,6 +753,7 @@ "src/app/shared/util/safe-assign.ts": { "module": "src/app/shared/util", "tags": [ + "shared", "shared:util" ], "imports": [] @@ -722,6 +761,7 @@ "src/app/shared/ngrx-utils/deep-clone.ts": { "module": "src/app/shared/ngrx-utils", "tags": [ + "shared", "shared:ngrx-utils" ], "imports": [] @@ -729,6 +769,7 @@ "src/app/shared/ngrx-utils/load-status.ts": { "module": "src/app/shared/ngrx-utils", "tags": [ + "shared", "shared:ngrx-utils" ], "imports": [] @@ -867,6 +908,7 @@ "src/app/shared/ui/index.ts": { "module": "src/app/shared/ui", "tags": [ + "shared", "shared:ui" ], "imports": [ @@ -876,6 +918,7 @@ "src/app/shared/ui/blinker.directive.ts": { "module": "src/app/shared/ui", "tags": [ + "shared", "shared:ui" ], "imports": [] diff --git a/test-projects/angular-i/tests/expected/cli-list.txt b/test-projects/angular-i/tests/expected/cli-list.txt index 4678e75..c2af42c 100644 --- a/test-projects/angular-i/tests/expected/cli-list.txt +++ b/test-projects/angular-i/tests/expected/cli-list.txt @@ -15,13 +15,13 @@ This project contains 19 modules: ├── model (domain:holidays, type:model) └── ui (domain:holidays, type:ui) └── shared - ├── config (shared:config) - ├── form (shared:form) - ├── http (shared:http) - ├── master-data (shared:master-data) - ├── ngrx-utils (shared:ngrx-utils) - ├── security (shared:security) - ├── testing (shared:testing) - ├── ui (shared:ui) - ├── ui-messaging (shared:ui-messaging) - └── util (shared:util) + ├── config (shared, shared:config) + ├── form (shared, shared:form) + ├── http (shared, shared:http) + ├── master-data (shared, shared:master-data) + ├── ngrx-utils (shared, shared:ngrx-utils) + ├── security (shared, shared:security) + ├── testing (shared, shared:testing) + ├── ui (shared, shared:ui) + ├── ui-messaging (shared, shared:ui-messaging) + └── util (shared, shared:util) diff --git a/test-projects/angular-i/tests/expected/cli-verify-failure.txt b/test-projects/angular-i/tests/expected/cli-verify-failure.txt new file mode 100644 index 0000000..3db8ef4 --- /dev/null +++ b/test-projects/angular-i/tests/expected/cli-verify-failure.txt @@ -0,0 +1,35 @@ + +Verification Report + +Issues found: + Total Invalid Files: 8 + Total Deep Imports: 0 + Total Dependency Rule Violations: 10 +---------------------------------- + +|-- src/app/customers/feature/components/add-customer.component.ts +| |-- Dependency Rule Violations +| | |-- from tag domain:customers to tags shared:master-data +|-- src/app/customers/ui/customer/customer.component.ts +| |-- Dependency Rule Violations +| | |-- from tag domain:customers to tags shared:form +| | |-- from tag domain:customers to tags shared:form +|-- src/app/customers/data/customers.effects.ts +| |-- Dependency Rule Violations +| | |-- from tag domain:customers to tags shared:ui-messaging +|-- src/app/customers/feature/components/edit-customer.component.ts +| |-- Dependency Rule Violations +| | |-- from tag domain:customers to tags shared:form +| | |-- from tag domain:customers to tags shared:master-data +|-- src/app/bookings/overview/overview.component.ts +| |-- Dependency Rule Violations +| | |-- from tag domain:bookings to tags shared:ngrx-utils +|-- src/app/holidays/feature/+state/holidays.effects.ts +| |-- Dependency Rule Violations +| | |-- from tag domain:holidays to tags shared:config +|-- src/app/holidays/ui/holiday-card/holiday-card.component.ts +| |-- Dependency Rule Violations +| | |-- from tag domain:holidays to tags shared:ui +|-- src/app/holidays/feature/request-info/request-info.component.ts +| |-- Dependency Rule Violations +| | |-- from tag domain:holidays to tags shared:util diff --git a/test-projects/angular-i/tests/expected/cli-verify.txt b/test-projects/angular-i/tests/expected/cli-verify-success.txt similarity index 100% rename from test-projects/angular-i/tests/expected/cli-verify.txt rename to test-projects/angular-i/tests/expected/cli-verify-success.txt diff --git a/test-projects/angular-i/tests/expected/dependency-rule-lint.json b/test-projects/angular-i/tests/expected/dependency-rule-lint.json index 5bb8ad1..d411d56 100644 --- a/test-projects/angular-i/tests/expected/dependency-rule-lint.json +++ b/test-projects/angular-i/tests/expected/dependency-rule-lint.json @@ -5,7 +5,7 @@ { "ruleId": "@softarc/sheriff/dependency-rule", "severity": 2, - "message": "module /src/app/customers/ui cannot access /src/app/customers/data. Tags [domain:customers,type:ui] have no clearance for type:data", + "message": "module /src/app/customers/ui cannot access /src/app/customers/data. Tag type:ui has no clearance for tags domain:customers, type:data", "line": 16, "column": 1, "nodeType": "ImportDeclaration", diff --git a/test-projects/angular-i/tests/expected/dynamic-import-lint.json b/test-projects/angular-i/tests/expected/dynamic-import-lint.json index b506e0a..919a8fe 100644 --- a/test-projects/angular-i/tests/expected/dynamic-import-lint.json +++ b/test-projects/angular-i/tests/expected/dynamic-import-lint.json @@ -5,7 +5,7 @@ { "ruleId": "@softarc/sheriff/dependency-rule", "severity": 2, - "message": "module cannot access /src/app/customers/feature. Tags [root] have no clearance for domain:customers", + "message": "module cannot access /src/app/customers/feature. Tag root has no clearance for tags domain:customers, type:feature", "line": 17, "column": 11, "nodeType": "ImportExpression", @@ -15,7 +15,7 @@ { "ruleId": "@softarc/sheriff/dependency-rule", "severity": 2, - "message": "module cannot access /src/app/bookings. Tags [root] have no clearance for domain:bookings", + "message": "module cannot access /src/app/bookings. Tag root has no clearance for tags domain:bookings, type:feature", "line": 22, "column": 11, "nodeType": "ImportExpression", @@ -25,7 +25,7 @@ { "ruleId": "@softarc/sheriff/dependency-rule", "severity": 2, - "message": "module cannot access /src/app/holidays/feature. Tags [root] have no clearance for domain:holidays", + "message": "module cannot access /src/app/holidays/feature. Tag root has no clearance for tags domain:holidays, type:feature", "line": 27, "column": 11, "nodeType": "ImportExpression", diff --git a/test-projects/angular-i/tests/sheriff.config-failure.ts b/test-projects/angular-i/tests/sheriff.config-failure.ts new file mode 100644 index 0000000..bea41a3 --- /dev/null +++ b/test-projects/angular-i/tests/sheriff.config-failure.ts @@ -0,0 +1,38 @@ +import { noDependencies, sameTag, SheriffConfig } from '@softarc/sheriff-core'; + +export const config: SheriffConfig = { + version: 1, + tagging: { + 'src/app': { + 'shared/': 'shared:', + bookings: ['domain:bookings', 'type:feature'], + 'customers/api': ['type:api', 'domain:customers:api'], + '/': ['domain:', 'type:'], + }, + }, + depRules: { + root: ['type:feature', 'shared:*', 'domain:*'], + 'domain:*': sameTag, + 'domain:bookings': 'domain:customers:api', + 'domain:customers:api': 'domain:customers', + 'type:api': 'type:*', + 'type:feature': [ + 'type:*', + 'shared:config', + 'shared:form', + 'shared:master-data', + 'shared:ngrx-utils', + 'shared:util', + ], + 'type:data': [ + 'type:model', + 'shared:http', + 'shared:ngrx-utils', + 'shared:ui-messaging', + ], + 'type:ui': ['type:model', 'shared:form', 'shared:ui'], + 'type:model': noDependencies, + 'shared:http': ['shared:config', 'shared:ui-messaging'], + 'shared:ngrx-utils': ['shared:util'], + }, +}; diff --git a/test-projects/angular-ii/sheriff.config.ts b/test-projects/angular-ii/sheriff.config.ts index 4d95e5b..6cfc888 100644 --- a/test-projects/angular-ii/sheriff.config.ts +++ b/test-projects/angular-ii/sheriff.config.ts @@ -15,9 +15,10 @@ export const sheriffConfig: SheriffConfig = { }, }, depRules: { - root: ['app:state', 'app:shell', 'type:feature', 'shared', 'domain:*'], + root: ['app:shell', 'app:state', 'type:feature', 'shared'], 'domain:*': [sameTag, 'shared'], shared: 'shared', + 'type:*':'shared', 'type:feature': ['type:feature', 'type:data', 'type:ui'], 'type:ui': ['type:data'], 'type:data': noDependencies, diff --git a/test-projects/angular-iii/sheriff.config.ts b/test-projects/angular-iii/sheriff.config.ts index bea41a3..8ec3c92 100644 --- a/test-projects/angular-iii/sheriff.config.ts +++ b/test-projects/angular-iii/sheriff.config.ts @@ -4,17 +4,18 @@ export const config: SheriffConfig = { version: 1, tagging: { 'src/app': { - 'shared/': 'shared:', + 'shared/': ['shared', 'shared:'], bookings: ['domain:bookings', 'type:feature'], 'customers/api': ['type:api', 'domain:customers:api'], '/': ['domain:', 'type:'], }, }, depRules: { - root: ['type:feature', 'shared:*', 'domain:*'], - 'domain:*': sameTag, + root: ['type:feature', 'shared'], + 'domain:*': [sameTag, 'shared'], 'domain:bookings': 'domain:customers:api', 'domain:customers:api': 'domain:customers', + 'shared': 'shared', 'type:api': 'type:*', 'type:feature': [ 'type:*', diff --git a/test-projects/angular-iv/integration-test.sh b/test-projects/angular-iv/integration-test.sh index 50cb2c3..eae17a1 100755 --- a/test-projects/angular-iv/integration-test.sh +++ b/test-projects/angular-iv/integration-test.sh @@ -2,7 +2,7 @@ set -e yarn yalc add @softarc/sheriff-core @softarc/eslint-plugin-sheriff cd node_modules/.bin # yalc doesn't create symlink in node_modules/.bin -ln -s ../@softarc/sheriff-core/src/bin/main.js ./sheriff +ln -sf ../@softarc/sheriff-core/src/bin/main.js ./sheriff cd ../../ cp sheriff.config.ts sheriff.config.ts.original @@ -16,10 +16,17 @@ echo 'checking for CLI export' npx sheriff export src/main.ts > tests/actual/cli-export.txt diff tests/actual/cli-export.txt tests/expected/cli-export.txt -# CLI Verify Check -echo 'checking for CLI verify' -npx sheriff verify src/main.ts > tests/actual/cli-verify.txt -diff tests/actual/cli-verify.txt tests/expected/cli-verify.txt +# CLI Verify Check (failure) +echo 'checking for CLI verify (failure scenario)' +cp tests/sheriff.config-failure.ts sheriff.config.ts +npx sheriff verify src/main.ts > tests/actual/cli-verify-failure.txt || true +diff tests/actual/cli-verify-failure.txt tests/expected/cli-verify-failure.txt +cp sheriff.config.ts.original sheriff.config.ts + +# CLI Verify Check (success) +echo 'checking for CLI verify (success scenario)' +npx sheriff verify src/main.ts > tests/actual/cli-verify-success.txt +diff tests/actual/cli-verify-success.txt tests/expected/cli-verify-success.txt # Dynamic Import Check echo 'checking for dynamic import error' diff --git a/test-projects/angular-iv/sheriff.config.ts b/test-projects/angular-iv/sheriff.config.ts index bea41a3..a5cfb03 100644 --- a/test-projects/angular-iv/sheriff.config.ts +++ b/test-projects/angular-iv/sheriff.config.ts @@ -4,15 +4,15 @@ export const config: SheriffConfig = { version: 1, tagging: { 'src/app': { - 'shared/': 'shared:', + 'shared/': ['shared', 'shared:'], bookings: ['domain:bookings', 'type:feature'], 'customers/api': ['type:api', 'domain:customers:api'], '/': ['domain:', 'type:'], }, }, depRules: { - root: ['type:feature', 'shared:*', 'domain:*'], - 'domain:*': sameTag, + root: ['type:feature', 'shared:*'], + 'domain:*': [sameTag, 'shared'], 'domain:bookings': 'domain:customers:api', 'domain:customers:api': 'domain:customers', 'type:api': 'type:*', @@ -32,6 +32,7 @@ export const config: SheriffConfig = { ], 'type:ui': ['type:model', 'shared:form', 'shared:ui'], 'type:model': noDependencies, + 'shared': 'shared:*', 'shared:http': ['shared:config', 'shared:ui-messaging'], 'shared:ngrx-utils': ['shared:util'], }, diff --git a/test-projects/angular-iv/tests/customers-container.deep-import.component.ts b/test-projects/angular-iv/tests/customers-container.deep-import.component.ts index 71f9075..774b374 100644 --- a/test-projects/angular-iv/tests/customers-container.deep-import.component.ts +++ b/test-projects/angular-iv/tests/customers-container.deep-import.component.ts @@ -1,7 +1,7 @@ import { AsyncPipe, NgIf } from '@angular/common'; import { Component, inject } from '@angular/core'; import { CustomersComponent, CustomersViewModel } from '@eternal/customers/ui'; -import { CustomersRepository } from '../../data/customers-repository.service.ts'; +import { CustomersRepository } from '../../data/customers-repository.service'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; diff --git a/test-projects/angular-iv/tests/dynamic-import-sheriff.config.ts b/test-projects/angular-iv/tests/dynamic-import-sheriff.config.ts index 95d987c..d90071b 100644 --- a/test-projects/angular-iv/tests/dynamic-import-sheriff.config.ts +++ b/test-projects/angular-iv/tests/dynamic-import-sheriff.config.ts @@ -4,15 +4,15 @@ export const config: SheriffConfig = { version: 1, tagging: { 'src/app': { - 'shared/': 'shared:', + 'shared/': ['shared', 'shared:'], bookings: ['domain:bookings', 'type:feature'], 'customers/api': ['type:api', 'domain:customers:api'], '/': ['domain:', 'type:'], }, }, depRules: { - root: ['type:feature', 'shared:*'], - 'domain:*': sameTag, + root: ['shared:*'], + 'domain:*': [sameTag, 'shared'], 'domain:bookings': 'domain:customers:api', 'domain:customers:api': 'domain:customers', 'type:api': 'type:*', @@ -32,6 +32,7 @@ export const config: SheriffConfig = { ], 'type:ui': ['type:model', 'shared:form', 'shared:ui'], 'type:model': noDependencies, + 'shared': 'shared:*', 'shared:http': ['shared:config', 'shared:ui-messaging'], 'shared:ngrx-utils': ['shared:util'], }, diff --git a/test-projects/angular-iv/tests/expected/cli-export.txt b/test-projects/angular-iv/tests/expected/cli-export.txt index 0751ce0..0661bc0 100644 --- a/test-projects/angular-iv/tests/expected/cli-export.txt +++ b/test-projects/angular-iv/tests/expected/cli-export.txt @@ -38,6 +38,7 @@ "src/app/shared/security/index.ts": { "module": "src/app/shared/security", "tags": [ + "shared", "shared:security" ], "imports": [ @@ -48,6 +49,7 @@ "src/app/shared/security/security.provider.ts": { "module": "src/app/shared/security", "tags": [ + "shared", "shared:security" ], "imports": [ @@ -58,6 +60,7 @@ "src/app/shared/security/security.effects.ts": { "module": "src/app/shared/security", "tags": [ + "shared", "shared:security" ], "imports": [ @@ -68,6 +71,7 @@ "src/app/shared/security/security.actions.ts": { "module": "src/app/shared/security", "tags": [ + "shared", "shared:security" ], "imports": [ @@ -77,6 +81,7 @@ "src/app/shared/security/security.reducer.ts": { "module": "src/app/shared/security", "tags": [ + "shared", "shared:security" ], "imports": [ @@ -86,6 +91,7 @@ "src/app/shared/security/security.service.ts": { "module": "src/app/shared/security", "tags": [ + "shared", "shared:security" ], "imports": [ @@ -97,6 +103,7 @@ "src/app/shared/security/security.selectors.ts": { "module": "src/app/shared/security", "tags": [ + "shared", "shared:security" ], "imports": [ @@ -115,6 +122,7 @@ "src/app/shared/ui-messaging/index.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [ @@ -129,6 +137,7 @@ "src/app/shared/ui-messaging/shared-ui-messaging.provider.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [ @@ -138,6 +147,7 @@ "src/app/shared/ui-messaging/loader/loading.interceptor.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [ @@ -148,6 +158,7 @@ "src/app/shared/ui-messaging/loader/loading.service.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [] @@ -155,6 +166,7 @@ "src/app/shared/ui-messaging/loader/silent-load.context.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [] @@ -162,6 +174,7 @@ "src/app/shared/ui-messaging/loader/loader.component.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [ @@ -171,6 +184,7 @@ "src/app/shared/ui-messaging/message/message.service.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [ @@ -181,6 +195,7 @@ "src/app/shared/ui-messaging/message/message.store.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [ @@ -190,6 +205,7 @@ "src/app/shared/ui-messaging/message/message.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [] @@ -197,6 +213,7 @@ "src/app/shared/ui-messaging/message/confirmation.component.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [] @@ -204,6 +221,7 @@ "src/app/shared/ui-messaging/message/message.component.ts": { "module": "src/app/shared/ui-messaging", "tags": [ + "shared", "shared:ui-messaging" ], "imports": [ @@ -214,6 +232,7 @@ "src/app/shared/config/index.ts": { "module": "src/app/shared/config", "tags": [ + "shared", "shared:config" ], "imports": [ @@ -223,6 +242,7 @@ "src/app/shared/config/configuration.ts": { "module": "src/app/shared/config", "tags": [ + "shared", "shared:config" ], "imports": [] @@ -230,6 +250,7 @@ "src/app/shared/http/index.ts": { "module": "src/app/shared/http", "tags": [ + "shared", "shared:http" ], "imports": [ @@ -241,6 +262,7 @@ "src/app/shared/http/error.interceptor.ts": { "module": "src/app/shared/http", "tags": [ + "shared", "shared:http" ], "imports": [ @@ -251,6 +273,7 @@ "src/app/shared/http/error-message.context.ts": { "module": "src/app/shared/http", "tags": [ + "shared", "shared:http" ], "imports": [] @@ -258,6 +281,7 @@ "src/app/shared/http/base-url.interceptor.ts": { "module": "src/app/shared/http", "tags": [ + "shared", "shared:http" ], "imports": [ @@ -267,6 +291,7 @@ "src/app/shared/http/with-error-message-context.ts": { "module": "src/app/shared/http", "tags": [ + "shared", "shared:http" ], "imports": [ @@ -276,6 +301,7 @@ "src/app/shared/master-data/index.ts": { "module": "src/app/shared/master-data", "tags": [ + "shared", "shared:master-data" ], "imports": [ @@ -286,6 +312,7 @@ "src/app/shared/master-data/shared-master-data.provider.ts": { "module": "src/app/shared/master-data", "tags": [ + "shared", "shared:master-data" ], "imports": [ @@ -295,6 +322,7 @@ "src/app/shared/master-data/+state/master.reducer.ts": { "module": "src/app/shared/master-data", "tags": [ + "shared", "shared:master-data" ], "imports": [] @@ -410,6 +438,7 @@ "src/app/shared/form/index.ts": { "module": "src/app/shared/form", "tags": [ + "shared", "shared:form" ], "imports": [ @@ -420,6 +449,7 @@ "src/app/shared/form/options.ts": { "module": "src/app/shared/form", "tags": [ + "shared", "shared:form" ], "imports": [] @@ -427,6 +457,7 @@ "src/app/shared/form/form-errors.component.ts": { "module": "src/app/shared/form", "tags": [ + "shared", "shared:form" ], "imports": [] @@ -652,6 +683,7 @@ "src/app/shared/ngrx-utils/index.ts": { "module": "src/app/shared/ngrx-utils", "tags": [ + "shared", "shared:ngrx-utils" ], "imports": [ @@ -665,6 +697,7 @@ "src/app/shared/ngrx-utils/noop.action.ts": { "module": "src/app/shared/ngrx-utils", "tags": [ + "shared", "shared:ngrx-utils" ], "imports": [] @@ -672,6 +705,7 @@ "src/app/shared/ngrx-utils/safe-concat-map.ts": { "module": "src/app/shared/ngrx-utils", "tags": [ + "shared", "shared:ngrx-utils" ], "imports": [ @@ -681,6 +715,7 @@ "src/app/shared/ngrx-utils/filter-defined.ts": { "module": "src/app/shared/ngrx-utils", "tags": [ + "shared", "shared:ngrx-utils" ], "imports": [ @@ -690,6 +725,7 @@ "src/app/shared/util/index.ts": { "module": "src/app/shared/util", "tags": [ + "shared", "shared:util" ], "imports": [ @@ -701,6 +737,7 @@ "src/app/shared/util/assert-defined.ts": { "module": "src/app/shared/util", "tags": [ + "shared", "shared:util" ], "imports": [] @@ -708,6 +745,7 @@ "src/app/shared/util/is-defined.ts": { "module": "src/app/shared/util", "tags": [ + "shared", "shared:util" ], "imports": [] @@ -715,6 +753,7 @@ "src/app/shared/util/safe-assign.ts": { "module": "src/app/shared/util", "tags": [ + "shared", "shared:util" ], "imports": [] @@ -722,6 +761,7 @@ "src/app/shared/ngrx-utils/deep-clone.ts": { "module": "src/app/shared/ngrx-utils", "tags": [ + "shared", "shared:ngrx-utils" ], "imports": [] @@ -729,6 +769,7 @@ "src/app/shared/ngrx-utils/load-status.ts": { "module": "src/app/shared/ngrx-utils", "tags": [ + "shared", "shared:ngrx-utils" ], "imports": [] @@ -867,6 +908,7 @@ "src/app/shared/ui/index.ts": { "module": "src/app/shared/ui", "tags": [ + "shared", "shared:ui" ], "imports": [ @@ -876,6 +918,7 @@ "src/app/shared/ui/blinker.directive.ts": { "module": "src/app/shared/ui", "tags": [ + "shared", "shared:ui" ], "imports": [] diff --git a/test-projects/angular-iv/tests/expected/cli-list.txt b/test-projects/angular-iv/tests/expected/cli-list.txt index 4678e75..c2af42c 100644 --- a/test-projects/angular-iv/tests/expected/cli-list.txt +++ b/test-projects/angular-iv/tests/expected/cli-list.txt @@ -15,13 +15,13 @@ This project contains 19 modules: ├── model (domain:holidays, type:model) └── ui (domain:holidays, type:ui) └── shared - ├── config (shared:config) - ├── form (shared:form) - ├── http (shared:http) - ├── master-data (shared:master-data) - ├── ngrx-utils (shared:ngrx-utils) - ├── security (shared:security) - ├── testing (shared:testing) - ├── ui (shared:ui) - ├── ui-messaging (shared:ui-messaging) - └── util (shared:util) + ├── config (shared, shared:config) + ├── form (shared, shared:form) + ├── http (shared, shared:http) + ├── master-data (shared, shared:master-data) + ├── ngrx-utils (shared, shared:ngrx-utils) + ├── security (shared, shared:security) + ├── testing (shared, shared:testing) + ├── ui (shared, shared:ui) + ├── ui-messaging (shared, shared:ui-messaging) + └── util (shared, shared:util) diff --git a/test-projects/angular-iv/tests/expected/cli-verify-failure.txt b/test-projects/angular-iv/tests/expected/cli-verify-failure.txt new file mode 100644 index 0000000..3db8ef4 --- /dev/null +++ b/test-projects/angular-iv/tests/expected/cli-verify-failure.txt @@ -0,0 +1,35 @@ + +Verification Report + +Issues found: + Total Invalid Files: 8 + Total Deep Imports: 0 + Total Dependency Rule Violations: 10 +---------------------------------- + +|-- src/app/customers/feature/components/add-customer.component.ts +| |-- Dependency Rule Violations +| | |-- from tag domain:customers to tags shared:master-data +|-- src/app/customers/ui/customer/customer.component.ts +| |-- Dependency Rule Violations +| | |-- from tag domain:customers to tags shared:form +| | |-- from tag domain:customers to tags shared:form +|-- src/app/customers/data/customers.effects.ts +| |-- Dependency Rule Violations +| | |-- from tag domain:customers to tags shared:ui-messaging +|-- src/app/customers/feature/components/edit-customer.component.ts +| |-- Dependency Rule Violations +| | |-- from tag domain:customers to tags shared:form +| | |-- from tag domain:customers to tags shared:master-data +|-- src/app/bookings/overview/overview.component.ts +| |-- Dependency Rule Violations +| | |-- from tag domain:bookings to tags shared:ngrx-utils +|-- src/app/holidays/feature/+state/holidays.effects.ts +| |-- Dependency Rule Violations +| | |-- from tag domain:holidays to tags shared:config +|-- src/app/holidays/ui/holiday-card/holiday-card.component.ts +| |-- Dependency Rule Violations +| | |-- from tag domain:holidays to tags shared:ui +|-- src/app/holidays/feature/request-info/request-info.component.ts +| |-- Dependency Rule Violations +| | |-- from tag domain:holidays to tags shared:util diff --git a/test-projects/angular-iv/tests/expected/cli-verify.txt b/test-projects/angular-iv/tests/expected/cli-verify-success.txt similarity index 100% rename from test-projects/angular-iv/tests/expected/cli-verify.txt rename to test-projects/angular-iv/tests/expected/cli-verify-success.txt diff --git a/test-projects/angular-iv/tests/expected/deep-import-lint.json b/test-projects/angular-iv/tests/expected/deep-import-lint.json index 12f2386..994a88b 100644 --- a/test-projects/angular-iv/tests/expected/deep-import-lint.json +++ b/test-projects/angular-iv/tests/expected/deep-import-lint.json @@ -10,7 +10,7 @@ "column": 1, "nodeType": "ImportDeclaration", "endLine": 4, - "endColumn": 82 + "endColumn": 79 } ], "suppressedMessages": [], @@ -19,7 +19,7 @@ "warningCount": 0, "fixableErrorCount": 0, "fixableWarningCount": 0, - "source": "import { AsyncPipe, NgIf } from '@angular/common';\nimport { Component, inject } from '@angular/core';\nimport { CustomersComponent, CustomersViewModel } from '@eternal/customers/ui';\nimport { CustomersRepository } from '../../data/customers-repository.service.ts';\nimport { Observable } from 'rxjs';\nimport { map } from 'rxjs/operators';\n\n@Component({\n template: ` `,\n standalone: true,\n imports: [CustomersComponent, NgIf, AsyncPipe],\n})\nexport class CustomersContainerComponent {\n #customersRepository = inject(CustomersRepository);\n viewModel$: Observable =\n this.#customersRepository.pagedCustomers$.pipe(\n map((pagedCustomers) => ({\n customers: pagedCustomers.customers,\n pageIndex: pagedCustomers.page - 1,\n length: pagedCustomers.total,\n }))\n );\n\n setSelected(id: number) {\n this.#customersRepository.select(id);\n }\n\n setUnselected() {\n this.#customersRepository.unselect();\n }\n\n switchPage(page: number) {\n console.log('switch to page ' + page + ' is not implemented');\n }\n}\n", + "source": "import { AsyncPipe, NgIf } from '@angular/common';\nimport { Component, inject } from '@angular/core';\nimport { CustomersComponent, CustomersViewModel } from '@eternal/customers/ui';\nimport { CustomersRepository } from '../../data/customers-repository.service';\nimport { Observable } from 'rxjs';\nimport { map } from 'rxjs/operators';\n\n@Component({\n template: ` `,\n standalone: true,\n imports: [CustomersComponent, NgIf, AsyncPipe],\n})\nexport class CustomersContainerComponent {\n #customersRepository = inject(CustomersRepository);\n viewModel$: Observable =\n this.#customersRepository.pagedCustomers$.pipe(\n map((pagedCustomers) => ({\n customers: pagedCustomers.customers,\n pageIndex: pagedCustomers.page - 1,\n length: pagedCustomers.total,\n }))\n );\n\n setSelected(id: number) {\n this.#customersRepository.select(id);\n }\n\n setUnselected() {\n this.#customersRepository.unselect();\n }\n\n switchPage(page: number) {\n console.log('switch to page ' + page + ' is not implemented');\n }\n}\n", "usedDeprecatedRules": [] } ] \ No newline at end of file diff --git a/test-projects/angular-iv/tests/expected/dependency-rule-lint.json b/test-projects/angular-iv/tests/expected/dependency-rule-lint.json index 5bb8ad1..d411d56 100644 --- a/test-projects/angular-iv/tests/expected/dependency-rule-lint.json +++ b/test-projects/angular-iv/tests/expected/dependency-rule-lint.json @@ -5,7 +5,7 @@ { "ruleId": "@softarc/sheriff/dependency-rule", "severity": 2, - "message": "module /src/app/customers/ui cannot access /src/app/customers/data. Tags [domain:customers,type:ui] have no clearance for type:data", + "message": "module /src/app/customers/ui cannot access /src/app/customers/data. Tag type:ui has no clearance for tags domain:customers, type:data", "line": 16, "column": 1, "nodeType": "ImportDeclaration", diff --git a/test-projects/angular-iv/tests/expected/dynamic-import-lint.json b/test-projects/angular-iv/tests/expected/dynamic-import-lint.json index b506e0a..919a8fe 100644 --- a/test-projects/angular-iv/tests/expected/dynamic-import-lint.json +++ b/test-projects/angular-iv/tests/expected/dynamic-import-lint.json @@ -5,7 +5,7 @@ { "ruleId": "@softarc/sheriff/dependency-rule", "severity": 2, - "message": "module cannot access /src/app/customers/feature. Tags [root] have no clearance for domain:customers", + "message": "module cannot access /src/app/customers/feature. Tag root has no clearance for tags domain:customers, type:feature", "line": 17, "column": 11, "nodeType": "ImportExpression", @@ -15,7 +15,7 @@ { "ruleId": "@softarc/sheriff/dependency-rule", "severity": 2, - "message": "module cannot access /src/app/bookings. Tags [root] have no clearance for domain:bookings", + "message": "module cannot access /src/app/bookings. Tag root has no clearance for tags domain:bookings, type:feature", "line": 22, "column": 11, "nodeType": "ImportExpression", @@ -25,7 +25,7 @@ { "ruleId": "@softarc/sheriff/dependency-rule", "severity": 2, - "message": "module cannot access /src/app/holidays/feature. Tags [root] have no clearance for domain:holidays", + "message": "module cannot access /src/app/holidays/feature. Tag root has no clearance for tags domain:holidays, type:feature", "line": 27, "column": 11, "nodeType": "ImportExpression", diff --git a/test-projects/angular-iv/tests/sheriff.config-failure.ts b/test-projects/angular-iv/tests/sheriff.config-failure.ts new file mode 100644 index 0000000..bea41a3 --- /dev/null +++ b/test-projects/angular-iv/tests/sheriff.config-failure.ts @@ -0,0 +1,38 @@ +import { noDependencies, sameTag, SheriffConfig } from '@softarc/sheriff-core'; + +export const config: SheriffConfig = { + version: 1, + tagging: { + 'src/app': { + 'shared/': 'shared:', + bookings: ['domain:bookings', 'type:feature'], + 'customers/api': ['type:api', 'domain:customers:api'], + '/': ['domain:', 'type:'], + }, + }, + depRules: { + root: ['type:feature', 'shared:*', 'domain:*'], + 'domain:*': sameTag, + 'domain:bookings': 'domain:customers:api', + 'domain:customers:api': 'domain:customers', + 'type:api': 'type:*', + 'type:feature': [ + 'type:*', + 'shared:config', + 'shared:form', + 'shared:master-data', + 'shared:ngrx-utils', + 'shared:util', + ], + 'type:data': [ + 'type:model', + 'shared:http', + 'shared:ngrx-utils', + 'shared:ui-messaging', + ], + 'type:ui': ['type:model', 'shared:form', 'shared:ui'], + 'type:model': noDependencies, + 'shared:http': ['shared:config', 'shared:ui-messaging'], + 'shared:ngrx-utils': ['shared:util'], + }, +}; diff --git a/vitest.config.ts b/vitest.config.ts index 72d2158..e58444d 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -8,6 +8,7 @@ export default defineConfig({ reporter: ['text', 'html'], }, include: ['packages/**/*.spec.ts'], + setupFiles: ['packages/core/src/lib/test/expect.extensions.ts'], alias: { '@softarc/eslint-plugin-sheriff': resolve( './packages/eslint-plugin/src/index.ts',