|
/* 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, |
|
}); |