dependency-cruiser
Version:
Validate and visualize dependencies. With your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.
254 lines (230 loc) • 7.26 kB
JavaScript
/* eslint-disable security/detect-object-injection */
import { dirname, resolve, sep } from "node:path/posix";
import { getCachedRegExp, replaceGroupPlaceholders } from "#utl/regex-util.mjs";
import { intersects } from "#utl/array-util.mjs";
// by their nature some dependency types always occur alongside other
// dependency types - aliased, type-only, import, export will always
// occur paired with 'local', 'npm' or core. Hence we only include
// a subset of dependency types where we _care_ if they are duplicates
const DEPENDENCY_TYPE_DUPLICATES_THAT_MATTER = new Set([
"core",
"local",
"localmodule",
"npm",
"npm-bundled",
"npm-dev",
"npm-no-pkg",
"npm-optional",
"npm-peer",
"npm-unknown",
]);
export function propertyEquals(pRule, pDependency, pProperty) {
// The properties can be booleans, so we can't use !pRule.to[pProperty]
if (Object.hasOwn(pRule.to, pProperty)) {
return pDependency[pProperty] === pRule.to[pProperty];
}
return true;
}
export function propertyMatches(pRule, pDependency, pRuleProperty, pProperty) {
return (
!pRule.to[pRuleProperty] ||
(pDependency[pProperty] &&
getCachedRegExp(pRule.to[pRuleProperty]).test(pDependency[pProperty]))
);
}
export function propertyMatchesNot(
pRule,
pDependency,
pRuleProperty,
pProperty,
) {
return (
!pRule.to[pRuleProperty] ||
(pDependency[pProperty] &&
!getCachedRegExp(pRule.to[pRuleProperty]).test(pDependency[pProperty]))
);
}
export function matchesFromPath(pRule, pModule) {
return (
!pRule.from.path || getCachedRegExp(pRule.from.path).test(pModule.source)
);
}
export function matchesFromPathNot(pRule, pModule) {
return (
!pRule.from.pathNot ||
!getCachedRegExp(pRule.from.pathNot).test(pModule.source)
);
}
export function matchesModulePath(pRule, pModule) {
return (
!pRule.module.path ||
getCachedRegExp(pRule.module.path).test(pModule.source)
);
}
export function matchesModulePathNot(pRule, pModule) {
return (
!pRule.module.pathNot ||
!getCachedRegExp(pRule.module.pathNot).test(pModule.source)
);
}
function _matchesToPath(pRule, pString, pGroups = []) {
return (
!pRule.to.path ||
getCachedRegExp(replaceGroupPlaceholders(pRule.to.path, pGroups)).test(
pString,
)
);
}
export function matchesToPath(pRule, pDependency, pGroups) {
return _matchesToPath(pRule, pDependency.resolved, pGroups);
}
export function matchToModulePath(pRule, pModule, pGroups) {
return _matchesToPath(pRule, pModule.source, pGroups);
}
function _matchesToPathNot(pRule, pString, pGroups = []) {
return (
!pRule.to.pathNot ||
!getCachedRegExp(replaceGroupPlaceholders(pRule.to.pathNot, pGroups)).test(
pString,
)
);
}
export function matchesToPathNot(pRule, pDependency, pGroups) {
return _matchesToPathNot(pRule, pDependency.resolved, pGroups);
}
export function matchToModulePathNot(pRule, pModule, pGroups) {
return _matchesToPathNot(pRule, pModule.source, pGroups);
}
export function matchesToDependencyTypes(pRule, pDependency) {
return (
!pRule.to.dependencyTypes ||
intersects(pDependency.dependencyTypes, pRule.to.dependencyTypes)
);
}
export function matchesToDependencyTypesNot(pRule, pDependency) {
return (
!pRule.to.dependencyTypesNot ||
!intersects(pDependency.dependencyTypes, pRule.to.dependencyTypesNot)
);
}
export function matchesToVia(pRule, pDependency, pGroups) {
let lReturnValue = true;
if (pRule.to.via && pDependency.cycle) {
if (pRule.to.via.path) {
lReturnValue = pDependency.cycle.some(({ name }) =>
getCachedRegExp(
replaceGroupPlaceholders(pRule.to.via.path, pGroups),
).test(name),
);
}
if (pRule.to.via.pathNot) {
lReturnValue = !pDependency.cycle.every(({ name }) =>
getCachedRegExp(
replaceGroupPlaceholders(pRule.to.via.pathNot, pGroups),
).test(name),
);
}
if (pRule.to.via.dependencyTypes) {
lReturnValue &&= pDependency.cycle.some(({ dependencyTypes }) =>
pRule.to.via.dependencyTypes.some((pRuleDependencyType) =>
dependencyTypes.includes(pRuleDependencyType),
),
);
}
if (pRule.to.via.dependencyTypesNot) {
lReturnValue &&= !pDependency.cycle.every(({ dependencyTypes }) =>
pRule.to.via.dependencyTypesNot.some((pRuleDependencyType) =>
dependencyTypes.includes(pRuleDependencyType),
),
);
}
}
return lReturnValue;
}
export function matchesToViaOnly(pRule, pDependency, pGroups) {
let lReturnValue = true;
if (pRule.to.viaOnly && pDependency.cycle) {
if (pRule.to.viaOnly.path) {
lReturnValue = pDependency.cycle.every(({ name }) =>
getCachedRegExp(
replaceGroupPlaceholders(pRule.to.viaOnly.path, pGroups),
).test(name),
);
}
if (pRule.to.viaOnly.pathNot) {
lReturnValue = !pDependency.cycle.some(({ name }) =>
getCachedRegExp(
replaceGroupPlaceholders(pRule.to.viaOnly.pathNot, pGroups),
).test(name),
);
}
if (pRule.to.viaOnly.dependencyTypes) {
lReturnValue &&= pDependency.cycle.every(({ dependencyTypes }) =>
pRule.to.viaOnly.dependencyTypes.some((pRuleDependencyType) =>
dependencyTypes.includes(pRuleDependencyType),
),
);
}
if (pRule.to.viaOnly.dependencyTypesNot) {
lReturnValue &&= !pDependency.cycle.some(({ dependencyTypes }) =>
pRule.to.viaOnly.dependencyTypesNot.some((pRuleDependencyType) =>
dependencyTypes.includes(pRuleDependencyType),
),
);
}
}
return lReturnValue;
}
export function matchesToIsMoreUnstable(pRule, pModule, pDependency) {
if (Object.hasOwn(pRule.to, "moreUnstable")) {
return (
(pRule.to.moreUnstable &&
pModule.instability < pDependency.instability) ||
(!pRule.to.moreUnstable && pModule.instability >= pDependency.instability)
);
}
return true;
}
export function matchesMoreThanOneDependencyType(pRule, pDependency) {
/**
* this rule exists to weed out i.e. dependencies declared in both
* dependencies and devDependencies. We, however, also use the dependencyTypes
* to specify closer to the source what kind of dependency it is (aliased via
* a subpath import => both 'aliased' and 'aliased-subpath-import').
*
* Moreover an alias per definition is also a regular dependency. So we also
* need to exclude those.
*
* Something similar goes for dependencies that are imported as 'type-only' -
* which are some sort of regular dependency as well. Hence the use of the
* DEPENDENCY_TYPE_DUPLICATES_THAT_MATTER set.
*/
if (Object.hasOwn(pRule.to, "moreThanOneDependencyType")) {
return (
pRule.to.moreThanOneDependencyType ===
pDependency.dependencyTypes.filter((pDependencyType) =>
DEPENDENCY_TYPE_DUPLICATES_THAT_MATTER.has(pDependencyType),
).length >
1
);
}
return true;
}
export function matchesAncestor(pRule, pModule, pDependency) {
if (Object.hasOwn(pRule.to, "ancestor")) {
if (pDependency.coreModule || pDependency.couldNotResolve) {
return false;
}
const lModulePath = dirname(resolve(pModule.source)) + sep;
const lDependencyPath = dirname(resolve(pDependency.resolved)) + sep;
const lDoesMatchAncestor =
lModulePath.startsWith(lDependencyPath) &&
lModulePath.length > lDependencyPath.length;
if (pRule.to.ancestor) {
return lDoesMatchAncestor;
} else {
return !lDoesMatchAncestor;
}
}
return true;
}