Skip to content

Commit

Permalink
feat: adds skipAnalysisNotInRules option (#979)
Browse files Browse the repository at this point in the history
## Description

- adds a `skipAnalysisNotInRules` option that, when switched to `true`
skips all analyses not necessary for checking the current rule set.
- Defaults the option to `false` for backwards compatibility of both cli
and api.
- Add this option with the value `true` to the --init template that
scaffolds initial .dependency-cruiser.js configurations
- Takes the new option into account in the cache-dirty check
- Adds a paragraph in the options reference

TODO:
- [x] implement for cycle analysis
- [x] implement for dependents analysis
- [x] implement for orphan check

For the _focus_, _metrics_ and _reachables_ analyses this was already in
place by default

## Motivation and Context

Addresses #978 

## How Has This Been Tested?

- [x] green ci
- [x] additional and updated automated non-regression tests

## Types of changes

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Documentation only change
- [ ] Refactor (non-breaking change which fixes an issue without
changing functionality)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
  • Loading branch information
sverweij authored Jan 7, 2025
1 parent eb7bf8a commit a00d3a0
Show file tree
Hide file tree
Showing 60 changed files with 639 additions and 170 deletions.
1 change: 1 addition & 0 deletions .dependency-cruiser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,7 @@ export default {
// parser: "tsc", // acorn, tsc
detectJSDocImports: true, // implies parser: "tsc"
experimentalStats: true,
skipAnalysisNotInRules: true,
metrics: true,
enhancedResolveOptions: {
exportsFields: ["exports"],
Expand Down
31 changes: 28 additions & 3 deletions doc/options-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
- [enhancedResolveOptions](#enhancedresolveoptions)
- [forceDeriveDependents](#forcederivedependents)
- [experimentalStats](#experimentalstats)
- [skipAnalysisNotInRules](#skipanalysisnotinrules)
- [parser](#parser)
- [cache](#cache)
- [progress](#progress)
Expand Down Expand Up @@ -1668,9 +1669,17 @@ 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`.
> [!WARNING]
> Deprecated. This optiton hasnt had any effect on dependency-cruiser's behaviour
> since a few major versions. I there's a need to maniuplate whether or not
> dependendents get derived independent of any rule (/ metric/ report) needing
> them use the [`skipAnalysisNotInRules`](#skipanalysisnotinrules) option as
> documented below.
>
> #### Previously documented behaviour
>> 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`.
### `experimentalStats`
Expand All @@ -1679,6 +1688,22 @@ in the result for each module. This feature is not yet used by any of the
reporters dependency-cruiser ships with. The feature is also _experimental_
which means it might disappear or change in the future.
### `skipAnalysisNotInRules`
When this flag is set to `true`, dependency-cruiser will skip all analysis that
don't serve a rule. E.g. if there's no 'circular' rule in the rule set it won't
analyse cycles. This flag affects cycle, dependents and orphan analysis. If you
have a rule set that doesn't use one of these features, switching it to true
will make cruises faster.
Dependency-cruiser skips other analyses (reachable, folder, metrics, focus)
automatically when they're not needed, without the need to set this flag.
Defaults to `false` for backwards compatibility. However, we recommend to switch
this option to `true`, unless you have a specific use case (i.e. use the json
output for further analysis, using the API).
### `parser`
With this _EXPERIMENTAL_ feature you can specify which parser you want to use
Expand Down
1 change: 1 addition & 0 deletions src/cache/options-compatible.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export function optionsAreCompatible(pOldOptions, pNewOptions) {
pOldOptions.combinedDependencies === pNewOptions.combinedDependencies &&
pOldOptions.experimentalStats === pNewOptions.experimentalStats &&
pOldOptions.detectJSDocImports === pNewOptions.detectJSDocImports &&
pOldOptions.skipAnalysisNotInRules === pNewOptions.skipAnalysisNotInRules &&
metricsIsCompatible(pOldOptions.metrics, pNewOptions.metrics) &&
// includeOnly suffers from a backwards compatibility disease
includeOnlyIsCompatible(pOldOptions.includeOnly, pNewOptions.includeOnly) &&
Expand Down
10 changes: 10 additions & 0 deletions src/cli/init-config/config-template.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,16 @@ module.exports = {
*/
// aliasFields: ["browser"],
},
/*
skipAnalysisNotInRules will make dependency-cruiser execute
analysis strictly necessary for checking the rule set only.
See https://github.com/sverweij/dependency-cruiser/blob/main/doc/options-reference.md#skipanalysisnotinrules
for details
*/
skipAnalysisNotInRules: true,
reporterOptions: {
dot: {
/* pattern of modules that can be consolidated in the detailed
Expand Down
47 changes: 38 additions & 9 deletions src/enrich/derive/circular.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
/* eslint-disable security/detect-object-injection */

/** @import { IFlattenedRuleSet } from "../../../types/rule-set.mjs" */

function isCycleRule(pRule) {
return (
/* c8 ignore start */
Object.hasOwn(pRule?.to ?? {}, "circular")
/* c8 ignore stop */
);
}
/**
* @param {IFlattenedRuleSet} pRuleSet
* @returns {boolean}
*/
export function hasCycleRule(pRuleSet) {
return (
(pRuleSet?.forbidden ?? []).some(isCycleRule) ||
(pRuleSet?.allowed ?? []).some(isCycleRule)
);
}

function addCircularityCheckToDependency(
pIndexedGraph,
pFrom,
Expand Down Expand Up @@ -31,17 +51,26 @@ function addCircularityCheckToDependency(
export default function detectAndAddCycles(
pNodes,
pIndexedNodes,
{ pSourceAttribute, pDependencyName },
{ pSourceAttribute, pDependencyName, pSkipAnalysisNotInRules, pRuleSet },
) {
if (!pSkipAnalysisNotInRules || hasCycleRule(pRuleSet)) {
return pNodes.map((pModule) => ({
...pModule,
dependencies: pModule.dependencies.map((pToDep) =>
addCircularityCheckToDependency(
pIndexedNodes,
pModule[pSourceAttribute],
pToDep,
pDependencyName,
),
),
}));
}
return pNodes.map((pModule) => ({
...pModule,
dependencies: pModule.dependencies.map((pToDep) =>
addCircularityCheckToDependency(
pIndexedNodes,
pModule[pSourceAttribute],
pToDep,
pDependencyName,
),
),
dependencies: pModule.dependencies.map((pToDep) => ({
...pToDep,
circular: false,
})),
}));
}
43 changes: 38 additions & 5 deletions src/enrich/derive/dependents/index.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,41 @@
import getDependents from "./get-dependents.mjs";

export default function addDependents(pModules) {
return pModules.map((pModule) => ({
...pModule,
dependents: getDependents(pModule, pModules),
}));
/** @import { IFlattenedRuleSet } from "../../../../types/rule-set.mjs" */

function isDependentsRule(pRule) {
// used in folder rules and when moreUnstable is in the 'to' => governed by
// the 'metrics' flag in options, sot not going to repeat that here

// dependents are used in the orphans analsys. hHwever, there is a fall back
// where it does its own analysis, so not going to repeat that check here.
return (
/* c8 ignore start */
Object.hasOwn(pRule?.module ?? {}, "numberOfDependentsLessThan") ||
Object.hasOwn(pRule?.module ?? {}, "numberOfDependentsMoreThan")
/* c8 ignore stop */
);
}

/**
* @param {IFlattenedRuleSet} pRuleSet
* @returns {boolean}
*/
export function hasDependentsRule(pRuleSet) {
return (
(pRuleSet?.forbidden ?? []).some(isDependentsRule) ||
(pRuleSet?.allowed ?? []).some(isDependentsRule)
);
}

export default function addDependents(
pModules,
{ skipAnalysisNotInRules, metrics, ruleSet },
) {
if (!skipAnalysisNotInRules || metrics || hasDependentsRule(ruleSet)) {
return pModules.map((pModule) => ({
...pModule,
dependents: getDependents(pModule, pModules),
}));
}
return pModules;
}
10 changes: 9 additions & 1 deletion src/enrich/derive/folders/aggregate-to-folders.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,13 @@ function getSinks(pFolders) {
return lReturnValue;
}

export default function aggregateToFolders(pModules) {
/**
*
* @param {import("../../../../types/cruise-result.d.mts").IModule} pModules
* @param {import("../../../../types/cruise-result.d.mts").IOptions} pOptions
* @returns
*/
export default function aggregateToFolders(pModules, pOptions = {}) {
let lFolders = object2Array(
pModules.filter(metricsAreCalculable).reduce(aggregateToFolder, {}),
)
Expand All @@ -145,5 +151,7 @@ export default function aggregateToFolders(pModules) {
return detectCycles(lFolders, new IndexedModuleGraph(lFolders, "name"), {
pSourceAttribute: "name",
pDependencyName: "name",
pSkipAnalysisNotInRules: pOptions.skipAnalysisNotInRules,
pRuleSet: pOptions.ruleSet,
});
}
10 changes: 5 additions & 5 deletions src/enrich/derive/folders/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import aggregateToFolders from "./aggregate-to-folders.mjs";
import { validateFolder } from "#validate/index.mjs";

/**
* @param {import("../../../../types/dependency-cruiser.js").IFolder} pFolder
* @param {import('../../../../types/dependency-cruiser.js').IOptions} pOptions
* @param {import("../../../../types/dependency-cruiser.mjs").IFolder} pFolder
* @param {import('../../../../types/dependency-cruiser.mjs').IOptions} pOptions
* @returns
*/
function validateFolderDependency(pFolder, pOptions) {
Expand All @@ -25,15 +25,15 @@ function addFolderDependencyViolations(pOptions) {

/**
*
* @param {import('../../../../types/dependency-cruiser.js').IModule[]} pModules
* @param {import('../../../../types/dependency-cruiser.js').IOptions} pOptions
* @param {import('../../../../types/dependency-cruiser.mjs').IModule[]} pModules
* @param {import('../../../../types/dependency-cruiser.mjs').IOptions} pOptions
* @returns {any}
*/
export default function deriveFolderMetrics(pModules, pOptions) {
let lReturnValue = {};
if (pOptions.metrics) {
lReturnValue = {
folders: aggregateToFolders(pModules).map(
folders: aggregateToFolders(pModules, pOptions).map(
addFolderDependencyViolations(pOptions),
),
};
Expand Down
36 changes: 31 additions & 5 deletions src/enrich/derive/orphan/index.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
import isOrphan from "./is-orphan.mjs";

export default function deriveOrphans(pModules) {
return pModules.map((pModule) => ({
...pModule,
orphan: isOrphan(pModule, pModules),
}));
/** @import { IFlattenedRuleSet } from "../../../../types/rule-set.mjs" */

function isOrphanRule(pRule) {
return (
/* c8 ignore start */
Object.hasOwn(pRule?.from ?? {}, "orphan")
/* c8 ignore stop */
);
}
/**
* @param {IFlattenedRuleSet} pRuleSet
* @returns {boolean}
*/
export function hasOrphanRule(pRuleSet) {
return (
(pRuleSet?.forbidden ?? []).some(isOrphanRule) ||
(pRuleSet?.allowed ?? []).some(isOrphanRule)
);
}

export default function deriveOrphans(
pModules,
{ skipAnalysisNotInRules, ruleSet },
) {
if (!skipAnalysisNotInRules || hasOrphanRule(ruleSet)) {
return pModules.map((pModule) => ({
...pModule,
orphan: isOrphan(pModule, pModules),
}));
}
return pModules;
}
18 changes: 7 additions & 11 deletions src/enrich/derive/reachable.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,15 @@ import {
import IndexedModuleGraph from "#graph-utl/indexed-module-graph.mjs";
import { extractGroups } from "#utl/regex-util.mjs";

function isReachableRule(pRule) {
return Object.hasOwn(pRule?.to ?? {}, "reachable");
}

function getReachableRules(pRuleSet) {
return (pRuleSet?.forbidden ?? [])
.filter((pRule) => Object.hasOwn(pRule?.to ?? {}, "reachable"))
.concat(
(pRuleSet?.allowed ?? []).filter((pRule) =>
Object.hasOwn(pRule?.to ?? {}, "reachable"),
),
)
.concat(
(pRuleSet?.required ?? []).filter((pRule) =>
Object.hasOwn(pRule?.to ?? {}, "reachable"),
),
);
.filter(isReachableRule)
.concat((pRuleSet?.allowed ?? []).filter(isReachableRule))
.concat((pRuleSet?.required ?? []).filter(isReachableRule));
}

function isModuleInRuleFrom(pRule) {
Expand Down
15 changes: 12 additions & 3 deletions src/enrich/enrich-modules.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,31 @@ import IndexedModuleGraph from "#graph-utl/indexed-module-graph.mjs";
import addFocus from "#graph-utl/add-focus.mjs";
import { bus } from "#utl/bus.mjs";

/** @import { IModule, IOptions } from "../../types/dependency-cruiser.mjs" */

/**
* @param {IModule[]} pModules
* @param {IOptions} pOptions
* @returns {IModule[]}
*/
export default function enrichModules(pModules, pOptions) {
bus.info("analyzing: cycles");
const lIndexedModules = new IndexedModuleGraph(pModules);
let lModules = deriveCycles(pModules, lIndexedModules, {
pSourceAttribute: "source",
pDependencyName: "resolved",
pSkipAnalysisNotInRules: pOptions.skipAnalysisNotInRules,
pRuleSet: pOptions.ruleSet,
});
bus.info("analyzing: dependents");
lModules = addDependents(lModules);
lModules = addDependents(lModules, pOptions);
bus.info("analyzing: orphans");
lModules = deriveOrphans(lModules);
lModules = deriveOrphans(lModules, pOptions);
bus.info("analyzing: reachables");
lModules = deriveReachable(lModules, pOptions.ruleSet);
bus.info("analyzing: module metrics");
lModules = deriveModuleMetrics(lModules, pOptions);
bus.info("analyzing: add focus (if any)");
bus.info("analyzing: focus");
lModules = addFocus(lModules, pOptions.focus);

// when validate === false we might want to skip the addValidations.
Expand Down
1 change: 1 addition & 0 deletions src/enrich/summarize/summarize-options.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const SHAREABLE_OPTIONS = [
"reaches",
"reporterOptions",
"rulesFile",
"skipAnalysisNotInRules",
"tsConfig",
"tsPreCompilationDeps",
"webpackConfig",
Expand Down
1 change: 1 addition & 0 deletions src/main/options/defaults.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export default {
maxDepth: 0,
moduleSystems: ["es6", "cjs", "tsd", "amd"],
detectJSDocImports: false,
skipAnalysisNotInRules: false,
tsPreCompilationDeps: false,
preserveSymlinks: false,
combinedDependencies: false,
Expand Down
7 changes: 6 additions & 1 deletion src/schema/configuration.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit a00d3a0

Please sign in to comment.