diff --git a/.dependency-cruiser.json b/.dependency-cruiser.json index 213c8d31e..a1abe8cc4 100644 --- a/.dependency-cruiser.json +++ b/.dependency-cruiser.json @@ -181,6 +181,7 @@ }, { "name": "not-reachable-from-folder-index", + "comment": "(sample rule to demo reachable rules with capturing groups)", "severity": "info", "from": { "path": "^src/([^/]+)/index\\.js$" @@ -189,6 +190,13 @@ "path": "^src/$1/", "reachable": false } + }, + { + "name": "utl-module-not-shared-enough", + "comment": "(sample rule to demo demo rules based on dependents)", + "severity": "info", + "from": { "path": "^src" }, + "module": { "path": "^src/utl", "numberOfDependentsLessThan": 3 } } ], "options": { @@ -387,8 +395,6 @@ } } }, - // "progress": { "type": "cli-feedback" } "progress": { "type": "performance-log" } - // "progress": {"type": "none"} } } diff --git a/doc/options-reference.md b/doc/options-reference.md index 77f62c077..d6b57b3d8 100644 --- a/doc/options-reference.md +++ b/doc/options-reference.md @@ -25,6 +25,7 @@ - [mono repo behaviour - combinedDependencies](#mono-repo-behaviour---combinedDependencies) - [exotic ways to require modules - exoticRequireStrings](#exotic-ways-to-require-modules---exoticrequirestrings) - [enhancedResolveOptions](#enhancedresolveoptions) + - [forceDeriveDependents](#forcederivedependents) ## Filters @@ -1178,3 +1179,9 @@ E.g. to set the cache duration to 1337ms, you can use this: The cache duration is limited from 0ms (~ don't use a cache) to 1800000ms (0.5h). The cacheDuration used here overrides any that might be set in webpack configs. + +### `forceDeriveDependents` + +Dependency-cruiser will automatically determine whether it needs to derive dependents. +However, if you want to force them to be derived, you can switch this variable +to `true`. diff --git a/doc/recipes/internal-orphans/.dependency-cruiser-options-only.js b/doc/recipes/internal-orphans/.dependency-cruiser-options-only.js new file mode 100644 index 000000000..8fb560765 --- /dev/null +++ b/doc/recipes/internal-orphans/.dependency-cruiser-options-only.js @@ -0,0 +1,40 @@ +/** @type {import('dependency-cruiser').IConfiguration} */ +/** @type {import('../../..').IConfiguration} */ +module.exports = { + options: { + tsPreCompilationDeps: true, + baseDir: "src", + + reporterOptions: { + dot: { + collapsePattern: "node_modules/[^/]+", + theme: { + graph: { + splines: "ortho", + rankdir: "TD", + }, + modules: [ + { + criteria: { source: "^common" }, + attributes: { fillcolor: "#cccccc" }, + }, + ], + dependencies: [ + { + criteria: { "rules[0].severity": "error" }, + attributes: { fontcolor: "red", color: "red" }, + }, + { + criteria: { "rules[0].severity": "warn" }, + attributes: { fontcolor: "orange", color: "orange" }, + }, + { + criteria: { "rules[0].severity": "info" }, + attributes: { fontcolor: "blue", color: "blue" }, + }, + ], + }, + }, + }, + }, +}; diff --git a/doc/recipes/internal-orphans/.dependency-cruiser-with-rules.js b/doc/recipes/internal-orphans/.dependency-cruiser-with-rules.js new file mode 100644 index 000000000..1d573aaa3 --- /dev/null +++ b/doc/recipes/internal-orphans/.dependency-cruiser-with-rules.js @@ -0,0 +1,17 @@ +const options = require("./.dependency-cruiser-options-only"); + +/** @type {import('dependency-cruiser').IConfiguration} */ +module.exports = { + forbidden: [ + { + name: "no-unshared-utl", + severity: "error", + from: {}, + module: { + path: "^do-things/", + numberOfDependentsLessThan: 1, + }, + }, + ], + ...options, +}; diff --git a/doc/recipes/internal-orphans/before.svg b/doc/recipes/internal-orphans/before.svg new file mode 100644 index 000000000..5f353c8d8 --- /dev/null +++ b/doc/recipes/internal-orphans/before.svg @@ -0,0 +1,155 @@ + + + + + + +dependency-cruiser output + + +cluster_node_modules + +node_modules + + +cluster_do-things + +do-things + + + +do-things/analyze.ts + + +analyze.ts + + + + + +do-things/ingest.ts + + +ingest.ts + + + + + +do-things/main.ts + + +main.ts + + + + + +do-things/main.ts->do-things/analyze.ts + + + + + +do-things/main.ts->do-things/ingest.ts + + + + + +do-things/parse.ts + + +parse.ts + + + + + +do-things/main.ts->do-things/parse.ts + + + + + +do-things/validate.ts + + +validate.ts + + + + + +do-things/main.ts->do-things/validate.ts + + + + + +do-things/not-used-but-using-path.ts + + +not-used-but-using-path.ts + + + + + +path + +path + + + +do-things/not-used-but-using-path.ts->path + + + + + +do-things/report.ts + + +report.ts + + + + + +node_modules/snodash + + + + + +snodash + + + + + +do-things/report.ts->node_modules/snodash + + + + + +index.ts + + +index.ts + + + + + +index.ts->do-things/main.ts + + + + + diff --git a/doc/recipes/internal-orphans/rules-applied.svg b/doc/recipes/internal-orphans/rules-applied.svg new file mode 100644 index 000000000..3621f7a0a --- /dev/null +++ b/doc/recipes/internal-orphans/rules-applied.svg @@ -0,0 +1,155 @@ + + + + + + +dependency-cruiser output + + +cluster_do-things + +do-things + + +cluster_node_modules + +node_modules + + + +do-things/analyze.ts + + +analyze.ts + + + + + +do-things/ingest.ts + + +ingest.ts + + + + + +do-things/main.ts + + +main.ts + + + + + +do-things/main.ts->do-things/analyze.ts + + + + + +do-things/main.ts->do-things/ingest.ts + + + + + +do-things/parse.ts + + +parse.ts + + + + + +do-things/main.ts->do-things/parse.ts + + + + + +do-things/validate.ts + + +validate.ts + + + + + +do-things/main.ts->do-things/validate.ts + + + + + +do-things/not-used-but-using-path.ts + + +not-used-but-using-path.ts + + + + + +path + +path + + + +do-things/not-used-but-using-path.ts->path + + + + + +do-things/report.ts + + +report.ts + + + + + +node_modules/snodash + + + + + +snodash + + + + + +do-things/report.ts->node_modules/snodash + + + + + +index.ts + + +index.ts + + + + + +index.ts->do-things/main.ts + + + + + diff --git a/doc/recipes/internal-orphans/runme.sh b/doc/recipes/internal-orphans/runme.sh new file mode 100644 index 000000000..e98b2aa63 --- /dev/null +++ b/doc/recipes/internal-orphans/runme.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -e + +node ../../../bin/dependency-cruise.js . -c .dependency-cruiser-options-only.js -p -T dot | dot -T svg > before.svg +node ../../../bin/dependency-cruise.js . -c .dependency-cruiser-with-rules.js -p -T dot | dot -T svg > rules-applied.svg diff --git a/doc/recipes/internal-orphans/src/do-things/analyze.ts b/doc/recipes/internal-orphans/src/do-things/analyze.ts new file mode 100644 index 000000000..e69de29bb diff --git a/doc/recipes/internal-orphans/src/do-things/ingest.ts b/doc/recipes/internal-orphans/src/do-things/ingest.ts new file mode 100644 index 000000000..e69de29bb diff --git a/doc/recipes/internal-orphans/src/do-things/main.ts b/doc/recipes/internal-orphans/src/do-things/main.ts new file mode 100644 index 000000000..4c105dd75 --- /dev/null +++ b/doc/recipes/internal-orphans/src/do-things/main.ts @@ -0,0 +1,4 @@ +import "./ingest"; +import "./analyze"; +import "./parse"; +import "./validate"; diff --git a/doc/recipes/internal-orphans/src/do-things/not-used-but-using-path.ts b/doc/recipes/internal-orphans/src/do-things/not-used-but-using-path.ts new file mode 100644 index 000000000..1a8e03caa --- /dev/null +++ b/doc/recipes/internal-orphans/src/do-things/not-used-but-using-path.ts @@ -0,0 +1 @@ +import "path"; diff --git a/doc/recipes/internal-orphans/src/do-things/parse.ts b/doc/recipes/internal-orphans/src/do-things/parse.ts new file mode 100644 index 000000000..e69de29bb diff --git a/doc/recipes/internal-orphans/src/do-things/report.ts b/doc/recipes/internal-orphans/src/do-things/report.ts new file mode 100644 index 000000000..160f24dfe --- /dev/null +++ b/doc/recipes/internal-orphans/src/do-things/report.ts @@ -0,0 +1 @@ +import "snodash"; diff --git a/doc/recipes/internal-orphans/src/do-things/validate.ts b/doc/recipes/internal-orphans/src/do-things/validate.ts new file mode 100644 index 000000000..e69de29bb diff --git a/doc/recipes/internal-orphans/src/index.ts b/doc/recipes/internal-orphans/src/index.ts new file mode 100644 index 000000000..827f82123 --- /dev/null +++ b/doc/recipes/internal-orphans/src/index.ts @@ -0,0 +1 @@ +import "./do-things/main"; diff --git a/doc/recipes/internal-orphans/src/node_modules/snodash/index.js b/doc/recipes/internal-orphans/src/node_modules/snodash/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/doc/recipes/internal-orphans/src/node_modules/snodash/package.json b/doc/recipes/internal-orphans/src/node_modules/snodash/package.json new file mode 100644 index 000000000..f58f7ddec --- /dev/null +++ b/doc/recipes/internal-orphans/src/node_modules/snodash/package.json @@ -0,0 +1,8 @@ +{ + "name": "snodash", + "version": "1.0.0", + "description": "dummy package", + "main": "index.js", + "author": "Nigel Tufnel", + "license": "MIT" +} diff --git a/doc/recipes/internal-orphans/src/package.json b/doc/recipes/internal-orphans/src/package.json new file mode 100644 index 000000000..dd2a5a997 --- /dev/null +++ b/doc/recipes/internal-orphans/src/package.json @@ -0,0 +1,10 @@ +{ + "name": "internal-orphans", + "version": "1.0.0", + "description": "This is Spnal Tap", + "dependencies": { + "snodash": "1.0.0" + }, + "author": "David St. Hubbins", + "license": "MIT" +} diff --git a/doc/recipes/isolating-peer-folders/before.svg b/doc/recipes/isolating-peer-folders/before.svg index f64a48b38..b48655522 100644 --- a/doc/recipes/isolating-peer-folders/before.svg +++ b/doc/recipes/isolating-peer-folders/before.svg @@ -1,7 +1,7 @@ - - - - + + + + + +dependency-cruiser output + + +cluster_common + +common + + +cluster_features + +features + + + +common/customer.ts + + +customer.ts + + + + + +common/guitar.ts + + +guitar.ts + + + + + +common/icecream.ts + + +icecream.ts + + + + + +common/movie.ts + + +movie.ts + + + + + +features/check-in.ts + + +check-in.ts + + + + + +features/check-in.ts->common/customer.ts + + + + + +features/check-in.ts->common/movie.ts + + + + + +features/checkout.ts + + +checkout.ts + + + + + +features/checkout.ts->common/customer.ts + + + + + +features/checkout.ts->common/movie.ts + + + + + +features/information.ts + + +information.ts + + + + + +features/information.ts->common/icecream.ts + + + + + +features/refund.ts + + +refund.ts + + + + + +features/refund.ts->common/customer.ts + + + + + +features/refund.ts->common/movie.ts + + + + + +features/search.ts + + +search.ts + + + + + +features/search.ts->common/movie.ts + + + + + diff --git a/doc/recipes/shared-or-not/rules-applied.svg b/doc/recipes/shared-or-not/rules-applied.svg new file mode 100644 index 000000000..fc887389f --- /dev/null +++ b/doc/recipes/shared-or-not/rules-applied.svg @@ -0,0 +1,152 @@ + + + + + + +dependency-cruiser output + + +cluster_common + +common + + +cluster_features + +features + + + +common/customer.ts + + +customer.ts + + + + + +common/guitar.ts + + +guitar.ts + + + + + +common/icecream.ts + + +icecream.ts + + + + + +common/movie.ts + + +movie.ts + + + + + +features/check-in.ts + + +check-in.ts + + + + + +features/check-in.ts->common/customer.ts + + + + + +features/check-in.ts->common/movie.ts + + + + + +features/checkout.ts + + +checkout.ts + + + + + +features/checkout.ts->common/customer.ts + + + + + +features/checkout.ts->common/movie.ts + + + + + +features/information.ts + + +information.ts + + + + + +features/information.ts->common/icecream.ts + + + + + +features/refund.ts + + +refund.ts + + + + + +features/refund.ts->common/customer.ts + + + + + +features/refund.ts->common/movie.ts + + + + + +features/search.ts + + +search.ts + + + + + +features/search.ts->common/movie.ts + + + + + diff --git a/doc/recipes/shared-or-not/runme.sh b/doc/recipes/shared-or-not/runme.sh new file mode 100644 index 000000000..e98b2aa63 --- /dev/null +++ b/doc/recipes/shared-or-not/runme.sh @@ -0,0 +1,5 @@ +#!/bin/sh +set -e + +node ../../../bin/dependency-cruise.js . -c .dependency-cruiser-options-only.js -p -T dot | dot -T svg > before.svg +node ../../../bin/dependency-cruise.js . -c .dependency-cruiser-with-rules.js -p -T dot | dot -T svg > rules-applied.svg diff --git a/doc/recipes/shared-or-not/src/common/customer.ts b/doc/recipes/shared-or-not/src/common/customer.ts new file mode 100644 index 000000000..e69de29bb diff --git a/doc/recipes/shared-or-not/src/common/guitar.ts b/doc/recipes/shared-or-not/src/common/guitar.ts new file mode 100644 index 000000000..e69de29bb diff --git a/doc/recipes/shared-or-not/src/common/icecream.ts b/doc/recipes/shared-or-not/src/common/icecream.ts new file mode 100644 index 000000000..e69de29bb diff --git a/doc/recipes/shared-or-not/src/common/movie.ts b/doc/recipes/shared-or-not/src/common/movie.ts new file mode 100644 index 000000000..e69de29bb diff --git a/doc/recipes/shared-or-not/src/features/check-in.ts b/doc/recipes/shared-or-not/src/features/check-in.ts new file mode 100644 index 000000000..51020645b --- /dev/null +++ b/doc/recipes/shared-or-not/src/features/check-in.ts @@ -0,0 +1,2 @@ +import "../common/customer"; +import "../common/movie"; diff --git a/doc/recipes/shared-or-not/src/features/checkout.ts b/doc/recipes/shared-or-not/src/features/checkout.ts new file mode 100644 index 000000000..51020645b --- /dev/null +++ b/doc/recipes/shared-or-not/src/features/checkout.ts @@ -0,0 +1,2 @@ +import "../common/customer"; +import "../common/movie"; diff --git a/doc/recipes/shared-or-not/src/features/information.ts b/doc/recipes/shared-or-not/src/features/information.ts new file mode 100644 index 000000000..6dbb4a188 --- /dev/null +++ b/doc/recipes/shared-or-not/src/features/information.ts @@ -0,0 +1 @@ +import "../common/icecream"; diff --git a/doc/recipes/shared-or-not/src/features/refund.ts b/doc/recipes/shared-or-not/src/features/refund.ts new file mode 100644 index 000000000..51020645b --- /dev/null +++ b/doc/recipes/shared-or-not/src/features/refund.ts @@ -0,0 +1,2 @@ +import "../common/customer"; +import "../common/movie"; diff --git a/doc/recipes/shared-or-not/src/features/search.ts b/doc/recipes/shared-or-not/src/features/search.ts new file mode 100644 index 000000000..b31d93480 --- /dev/null +++ b/doc/recipes/shared-or-not/src/features/search.ts @@ -0,0 +1 @@ +import "../common/movie"; diff --git a/doc/rules-reference.md b/doc/rules-reference.md index 74ba362e0..835cdcae3 100644 --- a/doc/rules-reference.md +++ b/doc/rules-reference.md @@ -34,6 +34,7 @@ - [`orphan`](#orphans) - [`reachable`](#reachable---detecting-dead-wood-and-transient-dependencies) - [`couldNotResolve`](#couldnotresolve) + [rules on dependents - `numberOfDependentsLessThan`](#rules-on-dependents---numberOfDependentsLessThan) - [`circular`](#circular) - [`license` and `licenseNot`](#license-and-licensenot) - [`dependencyTypes`](#dependencytypes) @@ -422,7 +423,7 @@ improve legibility. ### orphans A Boolean indicating whether or not to match modules that have no incoming -or outgoing dependencies. Orphans might need special attention because +and no outgoing dependencies. Orphans might need special attention because they're unused leftovers from a refactoring. Or the start of some feature that never got finished but which was merged anyway. Leaving the `orphan` attribute out means you don't care about orphans in your code. @@ -546,13 +547,13 @@ happens: "comment": "Don't allow importing database implementation files for schema declaration files", "severity": "error", "from": { - "path": "\\.schema\\.ts$", + "path": "\\.schema\\.ts$" }, "to": { "path": "^src/libs/database/", - "reachable": true, - }, -}; + "reachable": true + } +} ``` #### Usage notes @@ -565,6 +566,48 @@ happens: - `path` and `pathNot` alongside the `reachable` in the `to` part of the rule (these limitations might get lifted somewhere in the future) +### rules on dependents - `numberOfDependentsLessThan` + +Matches when the number of dependents of a module is less than the provided number. +Useful to detect whether modules are 'shared' enough to your liking, or whether +they're actually used in the first place. + +E.g. to flag modules in the `shared` folder that are only used from the +`features` folder once (or not a all), you can use a rule like this in the +`forbidden` section + +```javascript +{ + name: "no-unshared-in-shared", + from: { + path: "^features/" + }, + module: { + path: "^shared/", + numberOfDependentsLessThan: 2 + } +} +``` + +#### Usage notes + +- Currently rules on dependents only work within the `forbidden` context. +- In the `from` part `path` and `pathNot` attributes work, but none other. +- Similarly the `to` part of the rule can (next to the `numberOfDependentsLessThan` + attribute) also only use `path` and `pathNot`. +- You can't use group matching with this rule. +
+ why? + + Unlike regular dependency rules this rule will not match one module at a time, + but a whole bunch of them. With one match (i.e. + `src/coolstuff/ice.ts` and a a regular expression with a group (`/^src/(^[/]+)/`) + the `$1` variable can only mean one thing (`coolstuff`). With more than one + result (`src/coolstuff/ice.ts, src/hotstuff/pepper.ts`) there's also more than + one thing `$1` means - making use in e.g. `to.path` ambiguous. + +
+ ### `couldNotResolve` Whether or not to match modules dependency-cruiser could not resolve (and diff --git a/doc/rules-tutorial.md b/doc/rules-tutorial.md index 454a6cc6c..fcacb4a90 100644 --- a/doc/rules-tutorial.md +++ b/doc/rules-tutorial.md @@ -229,3 +229,70 @@ you can see it flags the `wind-controller.ts` in red. In the svg and html versio you'll see the name of the rule when you hover over it: ![some controllers - but with the must-inherit-from-base-controller rule applied](recipes/must-use/rules-applied.svg) + +### Is a utility module shared? + +Over time central utility modules will rise and fall in use. Some shops want to +ensure there's a minimum number of modules that use those utility modules. With +dependency-cruiser you can keep track of usage of utility modules and define +rules on them. + +When you inspect these modules closely, you see the `icecream.ts` module +is only used by `information.ts`, and `guitar.ts` isn't used at all. + +![a bunch of 'shared' modules](recipes/shared-or-not/before.svg) + +We can set up a rule that flags any module that is used from the `features` folder +less than twice: + +```javascript +forbidden: [ + { + name: "no-unshared-utl", + from: { + path: "^features/", + }, + module: { + path: "^common/", + numberOfDependentsLessThan: 2, + }, + }, +]; +``` + +In graphical form the result would look like this: + +![a bunch of 'shared' modules - but with the no-unshared-utl rule appled](recipes/shared-or-not/rules-applied.svg) + +### Is a module actually used? + +If you're using dependency-cruiser to validate dependencies, you might be familiar +with the concept of [orphans](rules-reference.md#orphans) - modules that have +no dependencies and no dependents. + +![two 'internal orphans' hiding in do-things](recipes/internal-orphans/before.svg) + +However, sometimes a module only has dependencies to an external module, e.g. +node's native `path` module or to `lodash`. That module is not strictly an orphans, +but for all intents and purposes share the same characteristics as orphans. To flag +them we can add a rule that forbids the dependents of a module to drop below one - +for example: + +```javascript +forbidden: [ + { + name: "no-internal-orphans", + from: { + path: "^do-things/", + }, + module: { + path: "^do-things/", + numberOfDependentsLessThan: 1, + }, + }, +]; +``` + +And when we apply that rule to the example, we see them light up as errors: + +![two 'internal orphans' highlighted because of the rule above](recipes/internal-orphans/rules-applied.svg) diff --git a/src/enrich/clear-caches.js b/src/enrich/clear-caches.js index ec2c1b18c..45f71eaa3 100644 --- a/src/enrich/clear-caches.js +++ b/src/enrich/clear-caches.js @@ -1,5 +1,5 @@ -const findModuleByName = require("./derive/find-module-by-name"); +const utl = require("./derive/utl"); module.exports = function clearCaches() { - findModuleByName.clearCache(); + utl.clearCache(); }; diff --git a/src/enrich/derive/circular/get-cycle.js b/src/enrich/derive/circular/get-cycle.js index 79001ec5e..99d60d7f3 100644 --- a/src/enrich/derive/circular/get-cycle.js +++ b/src/enrich/derive/circular/get-cycle.js @@ -1,4 +1,4 @@ -const findModuleByName = require("../find-module-by-name"); +const { findModuleByName } = require("../utl"); /* about the absence of checks whether attributes/ objects actually * exist: * - it saves CPU cycles to the effect of being ~30% faster than with the diff --git a/src/enrich/derive/dependents/get-dependents.js b/src/enrich/derive/dependents/get-dependents.js index 557d023c7..81de3c845 100644 --- a/src/enrich/derive/dependents/get-dependents.js +++ b/src/enrich/derive/dependents/get-dependents.js @@ -1,13 +1,8 @@ -// TODO: duplicate of the function of the same nature in is-orphan -function isDependent(pResolvedName) { - return (pModule) => - pModule.dependencies.some( - (pDependency) => pDependency.resolved === pResolvedName - ); -} +const { isDependent } = require("../utl"); + module.exports = function getDependents(pModule, pModules) { - // TODO: perf - O(n^2) + // perf between O(n) in an unconnected graph and O(n^2) in a fully connected one return pModules .filter(isDependent(pModule.source)) - .map((pDependantModule) => pDependantModule.source); + .map((pDependentModule) => pDependentModule.source); }; diff --git a/src/enrich/derive/dependents/index.js b/src/enrich/derive/dependents/index.js index 95622391b..db46249c0 100644 --- a/src/enrich/derive/dependents/index.js +++ b/src/enrich/derive/dependents/index.js @@ -1,7 +1,15 @@ +const get = require("lodash/get"); const getDependents = require("./get-dependents"); +function hasDependentsRule(pOptions) { + // TODO: might want to enable this in required and allowed rules as well + return get(pOptions, "ruleSet.forbidden", []).some((pRule) => + get(pRule, "module.numberOfDependentsLessThan") + ); +} + function shouldAddDependents(pOptions) { - return Boolean(pOptions.forceDeriveDependents); + return Boolean(pOptions.forceDeriveDependents) || hasDependentsRule(pOptions); } module.exports = function addDependents(pModules, pOptions) { @@ -10,9 +18,6 @@ module.exports = function addDependents(pModules, pOptions) { return { ...pModule, dependents: getDependents(pModule, pModules), - // TODO: observation: - // an orphan is a module for which module.dependents === 0 && module.dependencies === 0 - // orphan: lDependents.length + pModule.dependencies.length === 0, }; }); } diff --git a/src/enrich/derive/orphan/is-orphan.js b/src/enrich/derive/orphan/is-orphan.js index d9bb5c0dc..0ab20f4dd 100644 --- a/src/enrich/derive/orphan/is-orphan.js +++ b/src/enrich/derive/orphan/is-orphan.js @@ -1,14 +1,14 @@ -function hasDependency(pResolvedName) { - return (pNode) => - pNode.dependencies.some( - (pToDependency) => pToDependency.resolved === pResolvedName - ); -} +const { isDependent } = require("../utl"); -module.exports = (pNode, pGraph) => { - if (pNode.dependencies.length > 0) { +module.exports = (pModule, pGraph) => { + if (pModule.dependencies.length > 0) { return false; } - return !pGraph.some(hasDependency(pNode.source)); + // when dependents already calculated take those + if (pModule.dependents) { + return pModule.dependents.length === 0; + } + // ... otherwise calculate them + return !pGraph.some(isDependent(pModule.source)); }; diff --git a/src/enrich/derive/reachable/get-path.js b/src/enrich/derive/reachable/get-path.js index 72ce8ca25..11f0b017b 100644 --- a/src/enrich/derive/reachable/get-path.js +++ b/src/enrich/derive/reachable/get-path.js @@ -1,4 +1,4 @@ -const findModuleByName = require("../find-module-by-name"); +const { findModuleByName } = require("../utl"); function getPath(pGraph, pFrom, pTo, pVisited = new Set()) { let lReturnValue = []; diff --git a/src/enrich/derive/find-module-by-name.js b/src/enrich/derive/utl.js similarity index 53% rename from src/enrich/derive/find-module-by-name.js rename to src/enrich/derive/utl.js index 8d6b70e24..ae018df9c 100644 --- a/src/enrich/derive/find-module-by-name.js +++ b/src/enrich/derive/utl.js @@ -9,8 +9,15 @@ const findModuleByName = _memoize( (_pGraph, pSource) => pSource ); -module.exports = findModuleByName; +function isDependent(pResolvedName) { + return (pModule) => + pModule.dependencies.some( + (pDependency) => pDependency.resolved === pResolvedName + ); +} -module.exports.clearCache = () => { +function clearCache() { findModuleByName.cache.clear(); -}; +} + +module.exports = { findModuleByName, clearCache, isDependent }; diff --git a/src/enrich/enrich-modules.js b/src/enrich/enrich-modules.js index 18781c3b8..fe3e23418 100644 --- a/src/enrich/enrich-modules.js +++ b/src/enrich/enrich-modules.js @@ -11,10 +11,10 @@ const addValidations = require("./add-validations"); module.exports = function enrichModules(pModules, pOptions) { bus.emit("progress", "analyzing: cycles", { level: busLogLevels.INFO }); let lModules = deriveCirculars(pModules); - bus.emit("progress", "analyzing: orphans", { level: busLogLevels.INFO }); - lModules = deriveOrphans(lModules); bus.emit("progress", "analyzing: dependents", { level: busLogLevels.INFO }); lModules = addDependents(lModules, pOptions); + bus.emit("progress", "analyzing: orphans", { level: busLogLevels.INFO }); + lModules = deriveOrphans(lModules); bus.emit("progress", "analyzing: reachables", { level: busLogLevels.INFO }); lModules = deriveReachable(lModules, pOptions.ruleSet); bus.emit("progress", "analyzing: add focus (if any)", { diff --git a/src/schema/configuration.schema.json b/src/schema/configuration.schema.json index 3c59065ca..409e5c482 100644 --- a/src/schema/configuration.schema.json +++ b/src/schema/configuration.schema.json @@ -84,7 +84,8 @@ "ForbiddenRuleType": { "oneOf": [ { "$ref": "#/definitions/RegularForbiddenRuleType" }, - { "$ref": "#/definitions/ReachabilityForbiddenRuleType" } + { "$ref": "#/definitions/ReachabilityForbiddenRuleType" }, + { "$ref": "#/definitions/DependentsForbiddenRuleType" } ] }, "RegularForbiddenRuleType": { @@ -105,6 +106,18 @@ "to": { "$ref": "#/definitions/ToRestrictionType" } } }, + "DependentsForbiddenRuleType": { + "type": "object", + "required": ["module", "from"], + "additionalProperties": false, + "properties": { + "name": { "type": "string" }, + "severity": { "$ref": "#/definitions/SeverityType" }, + "comment": { "type": "string" }, + "module": { "$ref": "#/definitions/DependentsModuleRestrictionType" }, + "from": { "$ref": "#/definitions/DependentsFromRestrictionType" } + } + }, "ReachabilityForbiddenRuleType": { "type": "object", "required": ["from", "to"], @@ -223,6 +236,44 @@ } } }, + "DependentsModuleRestrictionType": { + "description": "Criteria to select the module(s) this restriction should apply to", + "required": [], + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "description": "A regular expression or an array of regular expressions an end of a dependency should match to be caught by this rule.", + "$ref": "#/definitions/REAsStringsType" + }, + "pathNot": { + "description": "A regular expression or an array of regular expressions an end of a dependency should NOT match to be caught by this rule.", + "$ref": "#/definitions/REAsStringsType" + }, + "numberOfDependentsLessThan": { + "type": "integer", + "description": "Matches when the number of times the 'to' module is used falls below (<) this number. Caveat: only works in concert with path and pathNot restrictions in the from and to parts of the rule; other conditions will be ignored.(somewhat experimental; - syntax can change over time without a major bump)E.g. to flag modules that are used only once or not at all, use 2 here.", + "minimum": 0, + "maximum": 100 + } + } + }, + "DependentsFromRestrictionType": { + "description": "Criteria the dependents of the module should adehere to be caught by this rule rule. Leave it empty if you want any dependent to be matched.", + "required": [], + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "description": "A regular expression or an array of regular expressions an end of a dependency should match to be caught by this rule.", + "$ref": "#/definitions/REAsStringsType" + }, + "pathNot": { + "description": "A regular expression or an array of regular expressions an end of a dependency should NOT match to be caught by this rule.", + "$ref": "#/definitions/REAsStringsType" + } + } + }, "ReachabilityToRestrictionType": { "description": "Criteria the 'to' end of a dependency should match to be caught by this rule. Leave it empty if you want any module to be matched.", "required": ["reachable"], diff --git a/src/schema/cruise-result.schema.json b/src/schema/cruise-result.schema.json index c101058c3..dfc109030 100644 --- a/src/schema/cruise-result.schema.json +++ b/src/schema/cruise-result.schema.json @@ -34,7 +34,7 @@ "description": "list of modules depending on this module", "items": { "type": "string", - "description": "the (resolved) name of the dependant" + "description": "the (resolved) name of the dependent" } }, "followable": { @@ -387,7 +387,8 @@ "ForbiddenRuleType": { "oneOf": [ { "$ref": "#/definitions/RegularForbiddenRuleType" }, - { "$ref": "#/definitions/ReachabilityForbiddenRuleType" } + { "$ref": "#/definitions/ReachabilityForbiddenRuleType" }, + { "$ref": "#/definitions/DependentsForbiddenRuleType" } ] }, "RegularForbiddenRuleType": { @@ -408,6 +409,18 @@ "to": { "$ref": "#/definitions/ToRestrictionType" } } }, + "DependentsForbiddenRuleType": { + "type": "object", + "required": ["module", "from"], + "additionalProperties": false, + "properties": { + "name": { "type": "string" }, + "severity": { "$ref": "#/definitions/SeverityType" }, + "comment": { "type": "string" }, + "module": { "$ref": "#/definitions/DependentsModuleRestrictionType" }, + "from": { "$ref": "#/definitions/DependentsFromRestrictionType" } + } + }, "ReachabilityForbiddenRuleType": { "type": "object", "required": ["from", "to"], @@ -526,6 +539,44 @@ } } }, + "DependentsModuleRestrictionType": { + "description": "Criteria to select the module(s) this restriction should apply to", + "required": [], + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "description": "A regular expression or an array of regular expressions an end of a dependency should match to be caught by this rule.", + "$ref": "#/definitions/REAsStringsType" + }, + "pathNot": { + "description": "A regular expression or an array of regular expressions an end of a dependency should NOT match to be caught by this rule.", + "$ref": "#/definitions/REAsStringsType" + }, + "numberOfDependentsLessThan": { + "type": "integer", + "description": "Matches when the number of times the 'to' module is used falls below (<) this number. Caveat: only works in concert with path and pathNot restrictions in the from and to parts of the rule; other conditions will be ignored.(somewhat experimental; - syntax can change over time without a major bump)E.g. to flag modules that are used only once or not at all, use 2 here.", + "minimum": 0, + "maximum": 100 + } + } + }, + "DependentsFromRestrictionType": { + "description": "Criteria the dependents of the module should adehere to be caught by this rule rule. Leave it empty if you want any dependent to be matched.", + "required": [], + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "description": "A regular expression or an array of regular expressions an end of a dependency should match to be caught by this rule.", + "$ref": "#/definitions/REAsStringsType" + }, + "pathNot": { + "description": "A regular expression or an array of regular expressions an end of a dependency should NOT match to be caught by this rule.", + "$ref": "#/definitions/REAsStringsType" + } + } + }, "ReachabilityToRestrictionType": { "description": "Criteria the 'to' end of a dependency should match to be caught by this rule. Leave it empty if you want any module to be matched.", "required": ["reachable"], diff --git a/src/validate/match-module-rule.js b/src/validate/match-module-rule.js index 5393073e4..5b0d73bfa 100644 --- a/src/validate/match-module-rule.js +++ b/src/validate/match-module-rule.js @@ -47,12 +47,37 @@ function matchesReachesRule(pRule, pModule) { ); } +function matchesDependentsRule(pRule, pModule) { + if ( + _has(pModule, "dependents") && + _has(pRule, "module.numberOfDependentsLessThan") + ) { + return ( + // group matching seems like a nice idea, however, the 'from' part of the + // rule is going to match not one module (as with regular dependency rules) + // but a whole bunch of them, being the 'dependents'. So that match is going + // to produce not one result, but one per matching dependent. To get meaningful + // results we'd probably have to loop over these and or the + // matchers.toModulePath together. + matchers.modulePath(pRule, pModule) && + matchers.modulePathNot(pRule, pModule) && + pModule.dependents.filter( + (pDependent) => + Boolean(!pRule.from.path || pDependent.match(pRule.from.path)) && + Boolean(!pRule.from.pathNot || pDependent.match(pRule.from.pathNot)) + ).length < pRule.module.numberOfDependentsLessThan + ); + } + return false; +} + function match(pModule) { return (pRule) => { return ( matchesOrphanRule(pRule, pModule) || matchesReachableRule(pRule, pModule) || - matchesReachesRule(pRule, pModule) + matchesReachesRule(pRule, pModule) || + matchesDependentsRule(pRule, pModule) ); }; } @@ -62,6 +87,7 @@ module.exports = { matchesOrphanRule, matchesReachableRule, matchesReachesRule, + matchesDependentsRule, match, isInteresting, }; diff --git a/test/validate/match-module-rule.dependents.spec.js b/test/validate/match-module-rule.dependents.spec.js new file mode 100644 index 000000000..5fb640a5e --- /dev/null +++ b/test/validate/match-module-rule.dependents.spec.js @@ -0,0 +1,142 @@ +const { expect } = require("chai"); +const { + matchesDependentsRule, +} = require("../../src/validate/match-module-rule"); + +const EMPTY_RULE = { from: {}, module: {} }; +const ANY_DEPENDENTS = { + from: {}, + module: { numberOfDependentsLessThan: 100 }, +}; +const MUST_BE_SHARED_DONT_CARE_FROM_WHERE = { + from: {}, + module: { + path: "^src/utensils", + numberOfDependentsLessThan: 2, + }, +}; +const MUST_BE_SHARED_FROM_SNACKBAR = { + from: { + path: "^src/snackbar", + }, + module: { + path: "^src/utensils", + numberOfDependentsLessThan: 2, + }, +}; +const MUST_BE_SHARED_BUT_NOT_FROM_SNACKBAR = { + from: { + pathNot: "^src/snackbar", + }, + module: { + path: "^src/utensils", + numberOfDependentsLessThan: 2, + }, +}; +describe("validate/match-module-rule - dependents", () => { + it("rule without dependents restriction doesn't flag (implicit)", () => { + expect(matchesDependentsRule(EMPTY_RULE, {})).to.equal(false); + }); + it("rule without dependents restriction doesn't flag (explicit)", () => { + expect(matchesDependentsRule(EMPTY_RULE, { dependents: [] })).to.equal( + false + ); + }); + it("rule with dependents doesn't match a module with no dependents attribute", () => { + expect(matchesDependentsRule(ANY_DEPENDENTS, {})).to.equal(false); + }); + it("rule that matches any dependents does match a module with a dependents attribute", () => { + expect(matchesDependentsRule(ANY_DEPENDENTS, { dependents: [] })).to.equal( + true + ); + }); + it("rule that matches any dependents does match a module with a dependents attribute (>1 dependent)", () => { + expect( + matchesDependentsRule(ANY_DEPENDENTS, { + dependents: ["aap", "noot", "mies", "wim"], + }) + ).to.equal(true); + }); + + it("must-share (>=2 dependents) rule doesn't flag when there's 2 dependents", () => { + expect( + matchesDependentsRule(MUST_BE_SHARED_DONT_CARE_FROM_WHERE, { + source: "src/utensils/simsalabim.ts", + dependents: ["aap", "noot"], + }) + ).to.equal(false); + }); + + it("must-share (>=2 dependents) rule flags when there's 1 dependent", () => { + expect( + matchesDependentsRule(MUST_BE_SHARED_DONT_CARE_FROM_WHERE, { + source: "src/utensils/simsalabim.ts", + dependents: ["aap"], + }) + ).to.equal(true); + }); + + it("must-share (>=2 dependents) rule flags when there's 0 dependents", () => { + expect( + matchesDependentsRule(MUST_BE_SHARED_DONT_CARE_FROM_WHERE, { + source: "src/utensils/simsalabim.ts", + dependents: [], + }) + ).to.equal(true); + }); + + it("must-share (>=2 dependents) with a from doesn't flag when there's 2 dependents from that from", () => { + expect( + matchesDependentsRule(MUST_BE_SHARED_FROM_SNACKBAR, { + source: "src/utensils/frieten.ts", + dependents: ["src/snackbar/kapsalon.ts", "src/snackbar/zijspan.ts"], + }) + ).to.equal(false); + }); + + it("must-share (>=2 dependents) with a from doesn't flag when there's 2 dependents from that from (+ some others that don't matter)", () => { + expect( + matchesDependentsRule(MUST_BE_SHARED_FROM_SNACKBAR, { + source: "src/utensils/frieten.ts", + dependents: [ + "src/snackbar/kapsalon.ts", + "src/snackbar/zijspan.ts", + "src/fietsenwinkel/index.ts", + "src/fietsenwinkel/index.ts", + ], + }) + ).to.equal(false); + }); + + it("must-share (>=2 dependents) with a from path when there's only 1 dependents from that from path", () => { + expect( + matchesDependentsRule(MUST_BE_SHARED_FROM_SNACKBAR, { + source: "src/utensils/frieten.ts", + dependents: [ + "src/snackbar/kapsalon.ts", + "src/fietsenwinkel/zijspan.ts", + ], + }) + ).to.equal(true); + }); + + it("must-share (>=2 dependents) with a from pathNot when there's only 1 dependents from that from pathNot", () => { + expect( + matchesDependentsRule(MUST_BE_SHARED_BUT_NOT_FROM_SNACKBAR, { + source: "src/utensils/frieten.ts", + dependents: [ + "src/snackbar/kapsalon.ts", + "src/fietsenwinkel/zijspan.ts", + ], + }) + ).to.equal(true); + }); + it("must-share (>=2 dependents) with a from pathNot when there's 0 dependents from that from pathNot", () => { + expect( + matchesDependentsRule(MUST_BE_SHARED_BUT_NOT_FROM_SNACKBAR, { + source: "src/utensils/frieten.ts", + dependents: ["src/snackbar/kapsalon.ts", "src/snackbar/zijspan.ts"], + }) + ).to.equal(false); + }); +}); diff --git a/tools/schema/modules.mjs b/tools/schema/modules.mjs index 78355ad4a..e621d8fef 100644 --- a/tools/schema/modules.mjs +++ b/tools/schema/modules.mjs @@ -32,7 +32,7 @@ export default { description: "list of modules depending on this module", items: { type: "string", - description: "the (resolved) name of the dependant", + description: "the (resolved) name of the dependent", }, }, followable: { diff --git a/tools/schema/restrictions.mjs b/tools/schema/restrictions.mjs index 01986c041..f921cfb1a 100644 --- a/tools/schema/restrictions.mjs +++ b/tools/schema/restrictions.mjs @@ -124,6 +124,38 @@ export default { }, }, }, + DependentsModuleRestrictionType: { + description: + "Criteria to select the module(s) this restriction should apply to", + required: [], + type: "object", + additionalProperties: false, + properties: { + ...BASE_RESTRICTION, + numberOfDependentsLessThan: { + type: "integer", + description: + "Matches when the number of times the 'to' module is used falls below (<) " + + "this number. Caveat: only works in concert with path and pathNot restrictions " + + "in the from and to parts of the rule; other conditions will be ignored." + + "(somewhat experimental; - syntax can change over time without a major bump)" + + "E.g. to flag modules that are used only once or not at all, use 2 here.", + minimum: 0, + maximum: 100, + }, + }, + }, + DependentsFromRestrictionType: { + description: + "Criteria the dependents of the module should adehere to be caught by this rule " + + "rule. Leave it empty if you want any dependent to be matched.", + required: [], + type: "object", + additionalProperties: false, + properties: { + ...BASE_RESTRICTION, + }, + }, ReachabilityToRestrictionType: { description: "Criteria the 'to' end of a dependency should match to be caught by this " + diff --git a/tools/schema/rule-set.mjs b/tools/schema/rule-set.mjs index 90cbbeaa2..dfee908c5 100644 --- a/tools/schema/rule-set.mjs +++ b/tools/schema/rule-set.mjs @@ -100,6 +100,9 @@ export default { { $ref: "#/definitions/ReachabilityForbiddenRuleType", }, + { + $ref: "#/definitions/DependentsForbiddenRuleType", + }, ], }, RegularForbiddenRuleType: { @@ -132,6 +135,28 @@ export default { }, }, }, + DependentsForbiddenRuleType: { + type: "object", + required: ["module", "from"], + additionalProperties: false, + properties: { + name: { + type: "string", + }, + severity: { + $ref: "#/definitions/SeverityType", + }, + comment: { + type: "string", + }, + module: { + $ref: "#/definitions/DependentsModuleRestrictionType", + }, + from: { + $ref: "#/definitions/DependentsFromRestrictionType", + }, + }, + }, ReachabilityForbiddenRuleType: { type: "object", required: ["from", "to"], diff --git a/types/restrictions.d.ts b/types/restrictions.d.ts new file mode 100644 index 000000000..06d748a97 --- /dev/null +++ b/types/restrictions.d.ts @@ -0,0 +1,109 @@ +import { DependencyType } from "./shared-types"; + +export interface IBaseRestrictionType { + /** + * A regular expression or an array of regular expressions that select + * the modules this required rule should apply to. + */ + path?: string | string[]; + /** + * A regular expression or an array of regular expressions that select + * the modules this required rule should not apply to (you can use this + * to make exceptions on the `path` attribute) + */ + pathNot?: string | string[]; +} + +export interface IFromRestriction extends IBaseRestrictionType { + /** + * Whether or not to match when the module is an orphan (= has no incoming and no outgoing + * dependencies). When this property it is part of a rule, dependency-cruiser will + * ignore the 'to' part. + */ + orphan?: boolean; +} + +export interface IToRestriction extends IBaseRestrictionType { + /** + * Whether or not to match modules dependency-cruiser could not resolve (and probably + * aren't on disk). For this one too: leave out if you don't care either way. + */ + couldNotResolve?: boolean; + /** + * Whether or not to match when following to the to will ultimately end up in the from. + */ + circular?: boolean; + /** + * If following this dependency will ultimately return to the source + * (circular === true), this attribute will contain an (ordered) array of module + * names that shows (one of the) circular path(s) + */ + cycle?: string[]; + /** + * Whether or not to match when the dependency is a dynamic one. + */ + dynamic?: boolean; + /** + * Whether or not to match when the dependency is exotically required + */ + exoticallyRequired?: boolean; + /** + * A regular expression to match against any 'exotic' require strings + */ + exoticRequire?: string | string[]; + /** + * A regular expression to match against any 'exotic' require strings - when it should NOT be caught by the rule + */ + exoticRequireNot?: string | string[]; + /** + * true if this dependency only exists before compilation (like type only imports), + * false in all other cases. Only returned when the tsPreCompilationDeps is set to 'specify'. + */ + preCompilationOnly?: boolean; + /** + * Whether or not to match modules of any of these types (leaving out matches any of them) + */ + dependencyTypes?: DependencyType[]; + /** + * If true matches dependencies with more than one dependency type (e.g. defined in + * _both_ npm and npm-dev) + */ + moreThanOneDependencyType?: boolean; + /** + * Whether or not to match modules that were released under one of the mentioned + * licenses. E.g. to flag GPL-1.0, GPL-2.0 licensed modules (e.g. because your app + * is not compatible with the GPL) use "GPL" + */ + license?: string | string[]; + /** + * Whether or not to match modules that were NOT released under one of the mentioned + * licenses. E.g. to flag everyting non MIT use "MIT" here + */ + licenseNot?: string | string[]; +} + +export interface IReachabilityToRestrictionType extends IBaseRestrictionType { + /** + * Whether or not to match modules that aren't reachable from the from part of the rule. + */ + reachable: boolean; +} + +export interface IRequiredToRestrictionType { + /** + * A regular expression or an array of regular expressions at least + * one of the dependencies of the module should adhere to. + */ + path?: string | string[]; +} + +export interface IDependentsModuleRestrictionType extends IBaseRestrictionType { + /** + * Matches when the number of times the 'to' module is used falls below (<) + * this number. Caveat: only works in concert with path and pathNot restrictions + * in the from and to parts of the rule; other conditions will be ignored. + * (somewhat experimental; - syntax can change over time without a major bump) + * E.g. to flag modules that are used only once or not at all, use 2 here. + */ + numberOfDependentsLessThan?: number; +} diff --git a/types/rule-set.d.ts b/types/rule-set.d.ts index f7e92ccce..622743c38 100644 --- a/types/rule-set.d.ts +++ b/types/rule-set.d.ts @@ -1,166 +1,14 @@ -import { DependencyType, SeverityType } from "./shared-types"; +import { SeverityType } from "./shared-types"; +import { + IBaseRestrictionType, + IFromRestriction, + IToRestriction, + IReachabilityToRestrictionType, + IRequiredToRestrictionType, + IDependentsModuleRestrictionType, +} from "./restrictions"; -export interface IFromRestriction { - /** - * A regular expression or an array of regular expressions an end of a - * dependency should match to be caught by this rule. - */ - path?: string | string[]; - /** - * A regular expression or an array of regular expressions an end of a - * dependency should NOT match to be caught by this rule. - */ - pathNot?: string | string[]; - /** - * Whether or not to match when the module is an orphan (= has no incoming or outgoing - * dependencies). When this property it is part of a rule, dependency-cruiser will - * ignore the 'to' part. - */ - orphan?: boolean; -} - -export interface IToRestriction { - /** - * A regular expression or an array of regular expressions an end of a - * dependency should match to be caught by this rule. - */ - path?: string | string[]; - /** - * A regular expression or an array of regular expressions an end of a - * dependency should NOT match to be caught by this rule. - */ - pathNot?: string | string[]; - /** - * Whether or not to match modules dependency-cruiser could not resolve (and probably - * aren't on disk). For this one too: leave out if you don't care either way. - */ - couldNotResolve?: boolean; - /** - * Whether or not to match when following to the to will ultimately end up in the from. - */ - circular?: boolean; - /** - * If following this dependency will ultimately return to the source - * (circular === true), this attribute will contain an (ordered) array of module - * names that shows (one of the) circular path(s) - */ - cycle?: string[]; - /** - * Whether or not to match when the dependency is a dynamic one. - */ - dynamic?: boolean; - /** - * Whether or not to match when the dependency is exotically required - */ - exoticallyRequired?: boolean; - /** - * A regular expression to match against any 'exotic' require strings - */ - exoticRequire?: string | string[]; - /** - * A regular expression to match against any 'exotic' require strings - when it should NOT be caught by the rule - */ - exoticRequireNot?: string | string[]; - /** - * true if this dependency only exists before compilation (like type only imports), - * false in all other cases. Only returned when the tsPreCompilationDeps is set to 'specify'. - */ - preCompilationOnly?: boolean; - /** - * Whether or not to match modules of any of these types (leaving out matches any of them) - */ - dependencyTypes?: DependencyType[]; - /** - * If true matches dependencies with more than one dependency type (e.g. defined in - * _both_ npm and npm-dev) - */ - moreThanOneDependencyType?: boolean; - /** - * Whether or not to match modules that were released under one of the mentioned - * licenses. E.g. to flag GPL-1.0, GPL-2.0 licensed modules (e.g. because your app - * is not compatible with the GPL) use "GPL" - */ - license?: string | string[]; - /** - * Whether or not to match modules that were NOT released under one of the mentioned - * licenses. E.g. to flag everyting non MIT use "MIT" here - */ - licenseNot?: string | string[]; -} - -export interface IReachabilityFromRestrictionType { - /** - * A regular expression or an array of regular expressions an end of a - * dependency should match to be caught by this rule. - */ - path?: string | string[]; - /** - * A regular expression or an array of regular expressions an end of a - * dependency should NOT match to be caught by this rule. - */ - pathNot?: string | string[]; -} - -export interface IReachabilityToRestrictionType { - /** - * A regular expression or an array of regular expressions an end of a - * dependency should match to be caught by this rule. - */ - path?: string | string[]; - /** - * A regular expression or an array of regular expressions an end of a - * dependency should NOT match to be caught by this rule. - */ - pathNot?: string | string[]; - /** - * Whether or not to match modules that aren't reachable from the from part of the rule. - */ - reachable: boolean; -} - -export type IAllowedRuleType = - | IRegularAllowedRuleType - | IReachabilityAllowedRuleType; - -export interface IRegularAllowedRuleType { - /** - * You can use this field to document why the rule is there. - */ - comment?: string; - /** - * Criteria the 'from' end of a dependency should match to be caught by this rule. - * Leave it empty if you want any module to be matched. - */ - from: IFromRestriction; - /** - * Criteria the 'to' end of a dependency should match to be caught by this rule. - * Leave it empty if you want any module to be matched. - */ - to: IToRestriction; -} - -export interface IReachabilityAllowedRuleType { - /** - * You can use this field to document why the rule is there. - */ - comment?: string; - /** - * Criteria the 'from' end of a dependency should match to be caught by this rule. - * Leave it empty if you want any module to be matched. - */ - from: IReachabilityFromRestrictionType; - /** - * Criteria the 'to' end of a dependency should match to be caught by this rule. - * Leave it empty if you want any module to be matched. - */ - to: IReachabilityToRestrictionType; -} - -export type IForbiddenRuleType = - | IRegularForbiddenRuleType - | IReachabilityForbiddenRuleType; - -export interface IRegularForbiddenRuleType { +interface IBaseRuleType { /** * A short name for the rule - will appear in reporters to enable customers to * quickly identify a violated rule. Try to keep them short, eslint style. @@ -179,6 +27,9 @@ export interface IRegularForbiddenRuleType { * You can use this field to document why the rule is there. */ comment?: string; +} + +export interface IRegularForbiddenRuleType extends IBaseRuleType { /** * Criteria the 'from' end of a dependency should match to be caught by this * rule. Leave it empty if you want any module to be matched. @@ -191,30 +42,12 @@ export interface IRegularForbiddenRuleType { to: IToRestriction; } -export interface IReachabilityForbiddenRuleType { - /** - * A short name for the rule - will appear in reporters to enable customers to - * quickly identify a violated rule. Try to keep them short, eslint style. - * E.g. 'not-to-core' for a rule forbidding dependencies on core modules, or - * 'not-to-unresolvable' for one that prevents dependencies on modules that - * probably don't exist. - */ - name?: string; - /** - * How severe a violation of the rule is. The 'error' severity will make some - * reporters return a non-zero exit code, so if you want e.g. a build to stop - * when there's a rule violated: use that. - */ - severity?: SeverityType; - /** - * You can use this field to document why the rule is there. - */ - comment?: string; +export interface IReachabilityForbiddenRuleType extends IBaseRuleType { /** * Criteria the 'from' end of a dependency should match to be caught by this * rule. Leave it empty if you want any module to be matched. */ - from: IReachabilityFromRestrictionType; + from: IBaseRestrictionType; /** * Criteria the 'to' end of a dependency should match to be caught by this * rule. Leave it empty if you want any module to be matched. @@ -222,29 +55,45 @@ export interface IReachabilityForbiddenRuleType { to: IReachabilityToRestrictionType; } -export interface IRequiredRuleType { +export interface IDependentsForbiddenRuleType extends IBaseRuleType { /** - * A short name for the rule - will appear in reporters to enable customers to - * quickly identify a violated rule. Try to keep them short, eslint style. - * E.g. 'not-to-core' for a rule forbidding dependencies on core modules, or - * 'not-to-unresolvable' for one that prevents dependencies on modules that - * probably don't exist. + * Criteria to select the module(s) this restriction should apply to */ - name?: string; + module: IDependentsModuleRestrictionType; /** - * How severe a violation of the rule is. The 'error' severity will make some - * reporters return a non-zero exit code, so if you want e.g. a build to stop - * when there's a rule violated: use that. + * Criteria at least one dependency of each matching module must + * adhere to. */ - severity?: SeverityType; + from: IBaseRestrictionType; +} + +export interface IReachabilityAllowedRuleType { /** * You can use this field to document why the rule is there. */ comment?: string; + /** + * Criteria the 'from' end of a dependency should match to be caught by this rule. + * Leave it empty if you want any module to be matched. + */ + from: IBaseRestrictionType; + /** + * Criteria the 'to' end of a dependency should match to be caught by this rule. + * Leave it empty if you want any module to be matched. + */ + to: IReachabilityToRestrictionType; +} + +export type IForbiddenRuleType = + | IRegularForbiddenRuleType + | IReachabilityForbiddenRuleType + | IDependentsForbiddenRuleType; + +export interface IRequiredRuleType extends IBaseRuleType { /** * Criteria to select the module(s) this restriction should apply to */ - module: IRequiredModuleRestrictionType; + module: IBaseRestrictionType; /** * Criteria at least one dependency of each matching module must * adhere to. @@ -252,26 +101,25 @@ export interface IRequiredRuleType { to: IRequiredToRestrictionType; } -export interface IRequiredModuleRestrictionType { +export type IAllowedRuleType = + | IRegularAllowedRuleType + | IReachabilityAllowedRuleType; + +export interface IRegularAllowedRuleType { /** - * A regular expression or an array of regular expressions that select - * the modules this required rule should apply to. + * You can use this field to document why the rule is there. */ - path?: string | string[]; + comment?: string; /** - * A regular expression or an array of regular expressions that select - * the modules this required rule should not apply to (you can use this - * to make exceptions on the `path` attribute) + * Criteria the 'from' end of a dependency should match to be caught by this rule. + * Leave it empty if you want any module to be matched. */ - pathNot?: string | string[]; -} - -export interface IRequiredToRestrictionType { + from: IFromRestriction; /** - * A regular expression or an array of regular expressions at least - * one of the dependencies of the module should adhere to. + * Criteria the 'to' end of a dependency should match to be caught by this rule. + * Leave it empty if you want any module to be matched. */ - path?: string | string[]; + to: IToRestriction; } export interface IFlattenedRuleSet {