Skip to content

Instantly share code, notes, and snippets.

@sverweij
Last active November 15, 2021 14:03
Show Gist options
  • Save sverweij/06487b592b39c2e9b4cd9e4ff0a6dc08 to your computer and use it in GitHub Desktop.
Save sverweij/06487b592b39c2e9b4cd9e4ff0a6dc08 to your computer and use it in GitHub Desktop.
getting dependency metrics from dependency-cruiser

What's this?

A dependency-cruiser reporter plugin to calculate Robert C. Martin's dependency metrics with dependency-cruiser.

How do I run it?

  • copy depcruise-config-force-dependents.js and metrics-reporter-plugin.js to the working directory
  • run this:
npx dependency-cruiser src -c depcruise-config-force-dependents.js -T $(pwd)/metrics-reporter-plugin.js

This script assumes

  • 'src' is where source files live
  • metrics-reporter-plugin.js resides in the current folder
  • dependency-cruiser is installed in the folder that needs to be traversed
/* eslint-disable unicorn/prevent-abbreviations */
/** @type {import('dependency-cruiser').IConfiguration} */
module.exports = {
// a rule concerning dependents (like this one) will ensure dependency-cruiser
// adds dependents to its output. Obviously a hack for demonstration purposes
// only - when metrics calculation is incorporated into dependency-cruiser
// itself this hack won't be necessary anymore.
forbidden: [
{
name: "utl-module-not-shared-enough",
comment: "(sample rule to demo demo rules based on dependents)",
severity: "info",
from: {},
module: { numberOfDependentsLessThan: 3 },
},
],
options: {
doNotFollow: "node_modules",
progress: { type: "cli-feedback" },
},
};
/* eslint-disable security/detect-object-injection */
const path = require("path").posix;
const os = require("os");
const DECIMAL_BASE = 10;
const METRIC_WIDTH = 4;
function getAfferentCouplings(pModule, pDirname) {
// TODO need to shovel in some tests (and a bit of mod to the code)
// so we're sure a bla-x/x.js is not confused to be internal to bla/y.js
return pModule.dependents.filter(
(pDependent) => !path.dirname(pDependent).startsWith(pDirname)
).length;
}
function metricsAreCalculable(pModule) {
return (
!pModule.coreModule &&
!pModule.couldNotResolve &&
!pModule.matchesDoNotFollow
);
}
function getEfferentCouplings(pModule, pDirname) {
return pModule.dependencies.filter(
(pDependency) =>
!path.dirname(pDependency.resolved).startsWith(pDirname) &&
// TODO: just to make manual validation easier ignore external stuff.
// node_modules & node builtin modules should likely count for efferent
// couplings as well (or should we make this configurable?)
metricsAreCalculable(pDependency)
).length;
}
function upsertMetrics(pAllMetrics, pModule, pDirname) {
pAllMetrics[pDirname] = pAllMetrics[pDirname] || {
afferentCouplings: 0,
efferentCouplings: 0,
moduleCount: 0,
};
pAllMetrics[pDirname].afferentCouplings += getAfferentCouplings(
pModule,
pDirname
);
pAllMetrics[pDirname].efferentCouplings += getEfferentCouplings(
pModule,
pDirname
);
pAllMetrics[pDirname].moduleCount += 1;
// when both afferentCouplings and efferentCouplings equal 0 instability will
// yield NaN. Judging Bob Martin's intention, a component with no incoming
// and no outgoing dependencies is maximum stable (0)
// Also: it's not terribly efficient to calculate instabilityon each upsert - but the
// overhead is low (compared to the other things we do) and doing it later
// on seems to be less clear
pAllMetrics[pDirname].instability =
pAllMetrics[pDirname].efferentCouplings /
(pAllMetrics[pDirname].efferentCouplings +
pAllMetrics[pDirname].afferentCouplings) || 0;
return pAllMetrics;
}
function getParentDirectories(pPath) {
let lFragments = pPath.split("/");
let lReturnValue = [];
while (lFragments.length > 0) {
lReturnValue.push(lFragments.join("/"));
lFragments.pop();
}
return lReturnValue.reverse();
}
function foldersObject2folderArray(pObject) {
return Object.keys(pObject).map((pKey) => ({
folderName: pKey,
...pObject[pKey],
}));
}
function orderFolderMetrics(pLeftMetric, pRightMetric) {
// return pLeft.folderName.localeCompare(pRight.folderName);
// For intended use in a table it's probably more useful to sorty by
// instability. Might need to be either configurable or flexible
// in the output, though
return pRightMetric.instability - pLeftMetric.instability;
}
function calculateFolderMetrics(pModules) {
return foldersObject2folderArray(
pModules.filter(metricsAreCalculable).reduce((pAllMetrics, pModule) => {
getParentDirectories(path.dirname(pModule.source)).forEach(
(pParentDirectory) =>
upsertMetrics(pAllMetrics, pModule, pParentDirectory)
);
return pAllMetrics;
}, {})
).sort(orderFolderMetrics);
}
function transformMetricsToTable(pMetrics) {
// TODO: should probably use a table module for this (i.e. text-table)
// to simplify this code; but for this poc not having a dependency (so it's
// copy-n-pasteable from a gist) is more important
const lMaxNameWidth = pMetrics
.map((pMetric) => pMetric.folderName.length)
.sort((pLeft, pRight) => pLeft - pRight)
.pop();
return [
`${"folder".padEnd(lMaxNameWidth)} ${"N".padStart(
METRIC_WIDTH + 1
)} ${"Ca".padStart(METRIC_WIDTH + 1)} ${"Ca".padStart(
METRIC_WIDTH + 1
)} ${"I".padEnd(METRIC_WIDTH + 1)}`,
]
.concat(
`${"-".repeat(lMaxNameWidth)} ${"-".repeat(
METRIC_WIDTH + 1
)} ${"-".repeat(METRIC_WIDTH + 1)} ${"-".repeat(
METRIC_WIDTH + 1
)} ${"-".repeat(METRIC_WIDTH + 1)}`
)
.concat(
pMetrics.map((pMetric) => {
return `${pMetric.folderName.padEnd(
lMaxNameWidth,
" "
)} ${pMetric.moduleCount
.toString(DECIMAL_BASE)
.padStart(METRIC_WIDTH)} ${pMetric.afferentCouplings
.toString(DECIMAL_BASE)
.padStart(METRIC_WIDTH)} ${pMetric.efferentCouplings
.toString(DECIMAL_BASE)
.padStart(METRIC_WIDTH)} ${(
Math.round(100 * pMetric.instability) / 100
)
.toString(DECIMAL_BASE)
.padEnd(METRIC_WIDTH)}`;
})
)
.join(os.EOL);
}
/**
* Metrics plugin - to test the waters. If we want to use metrics in other
* reporters - or use e.g. the Ca/ Ce/ I in rules (e.g. to detect violations
* of Uncle Bob's variable dependency principle)
*
* @param {import('dependency-cruiser').ICruiseResult} pCruiseResult -
* the output of a dependency-cruise adhering to dependency-cruiser's
* cruise result schema
* @return {import('dependency-cruiser').IReporterOutput} -
* output: some metrics on folders and dependencies
* exitCode: 0
*/
module.exports = (pCruiseResult) => ({
output: transformMetricsToTable(
calculateFolderMetrics(pCruiseResult.modules)
),
exitCode: 0,
});
folder N Ca Ca I
--------------------------------------- ----- ----- ----- -----
bin 4 0 10 1
tools 27 0 5 1
src/enrich 19 2 10 0.83
src/cli/init-config 10 1 5 0.83
src/report/dot 8 2 8 0.8
src/extract 40 4 15 0.79
src/main 10 5 15 0.75
src/enrich/derive/reachable 2 1 3 0.75
src/cli 24 7 14 0.67
src/extract/transpile 10 4 8 0.67
src/config-utl/extract-depcruise-config 3 1 2 0.67
src/report 28 6 11 0.65
src/main/rule-set 2 2 3 0.6
src/enrich/derive/circular 2 1 1 0.5
src/enrich/derive/dependents 2 1 1 0.5
src/enrich/derive/orphan 2 1 1 0.5
src/extract/resolve 10 6 6 0.5
src/report/error-html 3 1 1 0.5
src/main/resolve-options 1 2 2 0.5
src/cli/listeners 4 2 2 0.5
src/cli/listeners/cli-feedback 1 1 1 0.5
src/cli/listeners/performance-log 3 1 1 0.5
src/config-utl 8 5 4 0.44
src/enrich/summarize 5 3 2 0.4
src/main/options 3 3 2 0.4
src/extract/parse 3 7 4 0.36
src/report/html 2 2 1 0.33
src/enrich/derive 9 5 2 0.29
src/extract/ast-extractors 7 5 2 0.29
src/validate 7 3 1 0.25
src 154 15 0 0
src/graph-utl 10 18 0 0
src/utl 4 14 0 0
src/extract/utl 6 10 0 0
src/extract/resolve/get-manifest 2 2 0 0
src/schema 3 2 0 0
src/main/files-and-dirs 1 1 0 0
src/main/utl 1 2 0 0
src/report/anon 4 1 0 0
src/report/utl 1 2 0 0
src/report/plugins 1 1 0 0
src/cli/utl 2 4 0 0
src/cli/tools 2 1 0 0
src/cli/tools/svg-in-html-snippets 1 0 0 0
tools/schema 23 0 0 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment