dependency-cruiser 10.7.0 → 10.8.0-beta-1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/dependency-cruise.js +1 -0
- package/package.json +1 -1
- package/src/cli/normalize-cli-options.js +1 -0
- package/src/enrich/derive/dependents/index.js +6 -1
- package/src/enrich/derive/folders/get-stability-metrics.js +97 -0
- package/src/enrich/derive/folders/index.js +12 -0
- package/src/enrich/derive/folders/module-utl.js +27 -0
- package/src/enrich/derive/folders/utl.js +23 -0
- package/src/enrich/index.js +2 -0
- package/src/meta.js +1 -1
- package/src/report/index.js +2 -0
- package/src/report/metrics.js +82 -0
- package/src/schema/cruise-result.schema.js +17 -0
- package/types/cruise-result.d.ts +53 -0
- package/types/shared-types.d.ts +1 -0
package/bin/dependency-cruise.js
CHANGED
|
@@ -28,6 +28,7 @@ try {
|
|
|
28
28
|
"-T, --output-type <type>",
|
|
29
29
|
"output type; e.g. err, err-html, dot, ddot, archi, flat, baseline or json\n(default: err)"
|
|
30
30
|
)
|
|
31
|
+
.option("-m, --metrics", "calculate stability metrics", false)
|
|
31
32
|
.option(
|
|
32
33
|
"-f, --output-to <file>",
|
|
33
34
|
"file to write output to; - for stdout",
|
package/package.json
CHANGED
|
@@ -11,7 +11,12 @@ function hasDependentsRule(pOptions) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function shouldAddDependents(pOptions) {
|
|
14
|
-
return
|
|
14
|
+
return (
|
|
15
|
+
Boolean(pOptions.forceDeriveDependents) ||
|
|
16
|
+
Boolean(pOptions.metrics) ||
|
|
17
|
+
pOptions.outputType === "metrics" ||
|
|
18
|
+
hasDependentsRule(pOptions)
|
|
19
|
+
);
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
module.exports = function addDependents(pModules, pOptions) {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/* eslint-disable security/detect-object-injection */
|
|
2
|
+
const path = require("path").posix;
|
|
3
|
+
const { foldersObject2folderArray, getParentFolders } = require("./utl");
|
|
4
|
+
const {
|
|
5
|
+
getAfferentCouplings,
|
|
6
|
+
getEfferentCouplings,
|
|
7
|
+
metricsAreCalculable,
|
|
8
|
+
} = require("./module-utl");
|
|
9
|
+
|
|
10
|
+
function upsertCouplings(pAllDependents, pNewDependents) {
|
|
11
|
+
pNewDependents.forEach((pNewDependent) => {
|
|
12
|
+
pAllDependents[pNewDependent] = pAllDependents[pNewDependent] || {
|
|
13
|
+
count: 0,
|
|
14
|
+
};
|
|
15
|
+
pAllDependents[pNewDependent].count += 1;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function upsertFolderAttributes(pAllMetrics, pModule, pDirname) {
|
|
20
|
+
pAllMetrics[pDirname] = pAllMetrics[pDirname] || {
|
|
21
|
+
dependencies: {},
|
|
22
|
+
dependents: {},
|
|
23
|
+
moduleCount: 0,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
upsertCouplings(
|
|
27
|
+
pAllMetrics[pDirname].dependents,
|
|
28
|
+
getAfferentCouplings(pModule, pDirname)
|
|
29
|
+
);
|
|
30
|
+
upsertCouplings(
|
|
31
|
+
pAllMetrics[pDirname].dependencies,
|
|
32
|
+
getEfferentCouplings(pModule, pDirname).map(
|
|
33
|
+
(pDependency) => pDependency.resolved
|
|
34
|
+
)
|
|
35
|
+
);
|
|
36
|
+
pAllMetrics[pDirname].moduleCount += 1;
|
|
37
|
+
|
|
38
|
+
return pAllMetrics;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function orderFolderMetrics(pLeftMetric, pRightMetric) {
|
|
42
|
+
// return pLeft.name.localeCompare(pRight.name);
|
|
43
|
+
// For intended use in a table it's probably more useful to sort by
|
|
44
|
+
// instability. Might need to be either configurable or flexible
|
|
45
|
+
// in the output, though
|
|
46
|
+
return pRightMetric.instability - pLeftMetric.instability;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sumCounts(pAll, pCurrent) {
|
|
50
|
+
return pAll + pCurrent.count;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getFolderLevelCouplings(pCouplingArray) {
|
|
54
|
+
return Array.from(
|
|
55
|
+
new Set(
|
|
56
|
+
pCouplingArray.map((pCoupling) =>
|
|
57
|
+
path.dirname(pCoupling.name) === "."
|
|
58
|
+
? pCoupling.name
|
|
59
|
+
: path.dirname(pCoupling.name)
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function calculateFolderMetrics(pFolder) {
|
|
66
|
+
const lModuleDependents = foldersObject2folderArray(pFolder.dependents);
|
|
67
|
+
const lModuleDependencies = foldersObject2folderArray(pFolder.dependencies);
|
|
68
|
+
const lAfferentCouplings = lModuleDependents.reduce(sumCounts, 0);
|
|
69
|
+
const lEfferentCouplings = lModuleDependencies.reduce(sumCounts, 0);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
...pFolder,
|
|
73
|
+
afferentCouplings: lAfferentCouplings,
|
|
74
|
+
efferentCouplings: lEfferentCouplings,
|
|
75
|
+
// when both afferentCouplings and efferentCouplings equal 0 instability will
|
|
76
|
+
// yield NaN. Judging Bob Martin's intention, a component with no outgoing
|
|
77
|
+
// dependencies is maximum stable (0)
|
|
78
|
+
instability:
|
|
79
|
+
lEfferentCouplings / (lEfferentCouplings + lAfferentCouplings) || 0,
|
|
80
|
+
dependents: getFolderLevelCouplings(lModuleDependents),
|
|
81
|
+
dependencies: getFolderLevelCouplings(lModuleDependencies),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = function getStabilityMetrics(pModules) {
|
|
86
|
+
return foldersObject2folderArray(
|
|
87
|
+
pModules.filter(metricsAreCalculable).reduce((pAllFolders, pModule) => {
|
|
88
|
+
getParentFolders(path.dirname(pModule.source)).forEach(
|
|
89
|
+
(pParentDirectory) =>
|
|
90
|
+
upsertFolderAttributes(pAllFolders, pModule, pParentDirectory)
|
|
91
|
+
);
|
|
92
|
+
return pAllFolders;
|
|
93
|
+
}, {})
|
|
94
|
+
)
|
|
95
|
+
.map(calculateFolderMetrics)
|
|
96
|
+
.sort(orderFolderMetrics);
|
|
97
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const getStabilityMetrics = require("./get-stability-metrics");
|
|
2
|
+
|
|
3
|
+
function shouldDeriveFolders(pOptions) {
|
|
4
|
+
return pOptions.metrics || pOptions.outputType === "metrics";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
module.exports = function deriveFolders(pModules, pOptions) {
|
|
8
|
+
if (shouldDeriveFolders(pOptions)) {
|
|
9
|
+
return { folders: getStabilityMetrics(pModules) };
|
|
10
|
+
}
|
|
11
|
+
return {};
|
|
12
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const path = require("path").posix;
|
|
2
|
+
|
|
3
|
+
function getAfferentCouplings(pModule, pDirname) {
|
|
4
|
+
return pModule.dependents.filter(
|
|
5
|
+
(pDependent) => !pDependent.startsWith(pDirname.concat(path.sep))
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getEfferentCouplings(pModule, pDirname) {
|
|
10
|
+
return pModule.dependencies.filter(
|
|
11
|
+
(pDependency) => !pDependency.resolved.startsWith(pDirname.concat(path.sep))
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function metricsAreCalculable(pModule) {
|
|
16
|
+
return (
|
|
17
|
+
!pModule.coreModule &&
|
|
18
|
+
!pModule.couldNotResolve &&
|
|
19
|
+
!pModule.matchesDoNotFollow
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = {
|
|
24
|
+
getAfferentCouplings,
|
|
25
|
+
getEfferentCouplings,
|
|
26
|
+
metricsAreCalculable,
|
|
27
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/* eslint-disable security/detect-object-injection */
|
|
2
|
+
function getParentFolders(pPath) {
|
|
3
|
+
let lFragments = pPath.split("/");
|
|
4
|
+
let lReturnValue = [];
|
|
5
|
+
|
|
6
|
+
while (lFragments.length > 0) {
|
|
7
|
+
lReturnValue.push(lFragments.join("/"));
|
|
8
|
+
lFragments.pop();
|
|
9
|
+
}
|
|
10
|
+
return lReturnValue.reverse();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function foldersObject2folderArray(pObject) {
|
|
14
|
+
return Object.keys(pObject).map((pKey) => ({
|
|
15
|
+
name: pKey,
|
|
16
|
+
...pObject[pKey],
|
|
17
|
+
}));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
getParentFolders,
|
|
22
|
+
foldersObject2folderArray,
|
|
23
|
+
};
|
package/src/enrich/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const enrichModules = require("./enrich-modules");
|
|
2
|
+
const deriveFolders = require("./derive/folders");
|
|
2
3
|
const summarize = require("./summarize");
|
|
3
4
|
const clearCaches = require("./clear-caches");
|
|
4
5
|
|
|
@@ -9,6 +10,7 @@ module.exports = function enrich(pModules, pOptions, pFileAndDirectoryArray) {
|
|
|
9
10
|
clearCaches();
|
|
10
11
|
return {
|
|
11
12
|
modules: lModules,
|
|
13
|
+
...deriveFolders(lModules, pOptions),
|
|
12
14
|
summary: summarize(lModules, pOptions, pFileAndDirectoryArray),
|
|
13
15
|
};
|
|
14
16
|
};
|
package/src/meta.js
CHANGED
package/src/report/index.js
CHANGED
|
@@ -13,6 +13,7 @@ const json = require("./json");
|
|
|
13
13
|
const teamcity = require("./teamcity");
|
|
14
14
|
const text = require("./text");
|
|
15
15
|
const baseline = require("./baseline");
|
|
16
|
+
const metrics = require("./metrics");
|
|
16
17
|
const { getExternalPluginReporter } = require("./plugins");
|
|
17
18
|
|
|
18
19
|
const TYPE2REPORTER = {
|
|
@@ -32,6 +33,7 @@ const TYPE2REPORTER = {
|
|
|
32
33
|
teamcity,
|
|
33
34
|
text,
|
|
34
35
|
baseline,
|
|
36
|
+
metrics,
|
|
35
37
|
};
|
|
36
38
|
|
|
37
39
|
/**
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const os = require("os");
|
|
2
|
+
const chalk = require("chalk");
|
|
3
|
+
|
|
4
|
+
const DECIMAL_BASE = 10;
|
|
5
|
+
const METRIC_WIDTH = 4;
|
|
6
|
+
const INSTABILITY_DECIMALS = 2;
|
|
7
|
+
const YADDUM = DECIMAL_BASE ** INSTABILITY_DECIMALS;
|
|
8
|
+
|
|
9
|
+
function transformMetricsToTable(pMetrics) {
|
|
10
|
+
// TODO: should probably use a table module for this (i.e. text-table)
|
|
11
|
+
// to simplify this code; but for this poc not having a dependency (so it's
|
|
12
|
+
// copy-n-pasteable from a gist) is more important
|
|
13
|
+
const lMaxNameWidth = pMetrics
|
|
14
|
+
.map((pMetric) => pMetric.name.length)
|
|
15
|
+
.sort((pLeft, pRight) => pLeft - pRight)
|
|
16
|
+
.pop();
|
|
17
|
+
|
|
18
|
+
return [
|
|
19
|
+
chalk.bold(
|
|
20
|
+
`${"folder".padEnd(lMaxNameWidth)} ${"N".padStart(
|
|
21
|
+
METRIC_WIDTH + 1
|
|
22
|
+
)} ${"Ca".padStart(METRIC_WIDTH + 1)} ${"Ce".padStart(
|
|
23
|
+
METRIC_WIDTH + 1
|
|
24
|
+
)} ${"I".padEnd(METRIC_WIDTH + 1)}`
|
|
25
|
+
),
|
|
26
|
+
]
|
|
27
|
+
.concat(
|
|
28
|
+
`${"-".repeat(lMaxNameWidth)} ${"-".repeat(
|
|
29
|
+
METRIC_WIDTH + 1
|
|
30
|
+
)} ${"-".repeat(METRIC_WIDTH + 1)} ${"-".repeat(
|
|
31
|
+
METRIC_WIDTH + 1
|
|
32
|
+
)} ${"-".repeat(METRIC_WIDTH + 1)}`
|
|
33
|
+
)
|
|
34
|
+
.concat(
|
|
35
|
+
pMetrics.map((pMetric) => {
|
|
36
|
+
return `${pMetric.name.padEnd(
|
|
37
|
+
lMaxNameWidth,
|
|
38
|
+
" "
|
|
39
|
+
)} ${pMetric.moduleCount
|
|
40
|
+
.toString(DECIMAL_BASE)
|
|
41
|
+
.padStart(METRIC_WIDTH)} ${pMetric.afferentCouplings
|
|
42
|
+
.toString(DECIMAL_BASE)
|
|
43
|
+
.padStart(METRIC_WIDTH)} ${pMetric.efferentCouplings
|
|
44
|
+
.toString(DECIMAL_BASE)
|
|
45
|
+
.padStart(METRIC_WIDTH)} ${(
|
|
46
|
+
Math.round(YADDUM * pMetric.instability) / YADDUM
|
|
47
|
+
)
|
|
48
|
+
.toString(DECIMAL_BASE)
|
|
49
|
+
.padEnd(METRIC_WIDTH)}`;
|
|
50
|
+
})
|
|
51
|
+
)
|
|
52
|
+
.join(os.EOL)
|
|
53
|
+
.concat(os.EOL);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Metrics plugin - to test the waters. If we want to use metrics in other
|
|
58
|
+
* reporters - or use e.g. the Ca/ Ce/ I in rules (e.g. to detect violations
|
|
59
|
+
* of Uncle Bob's variable dependency principle)
|
|
60
|
+
*
|
|
61
|
+
* @param {import('../../types/dependency-cruiser').ICruiseResult} pCruiseResult -
|
|
62
|
+
* the output of a dependency-cruise adhering to dependency-cruiser's
|
|
63
|
+
* cruise result schema
|
|
64
|
+
* @return {import('../../types/dependency-cruiser').IReporterOutput} -
|
|
65
|
+
* output: some metrics on folders and dependencies
|
|
66
|
+
* exitCode: 0
|
|
67
|
+
*/
|
|
68
|
+
module.exports = (pCruiseResult) => {
|
|
69
|
+
if (pCruiseResult.folders) {
|
|
70
|
+
return {
|
|
71
|
+
output: transformMetricsToTable(pCruiseResult.folders),
|
|
72
|
+
exitCode: 0,
|
|
73
|
+
};
|
|
74
|
+
} else {
|
|
75
|
+
return {
|
|
76
|
+
output:
|
|
77
|
+
`${os.EOL}ERROR: The cruise result didn't contain any metrics - re-running the cruise with${os.EOL}` +
|
|
78
|
+
` the '--metrics' command line option should fix that.${os.EOL}${os.EOL}`,
|
|
79
|
+
exitCode: 1,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
};
|
|
@@ -9,6 +9,7 @@ module.exports = {
|
|
|
9
9
|
additionalProperties: false,
|
|
10
10
|
properties: {
|
|
11
11
|
modules: { $ref: "#/definitions/ModulesType" },
|
|
12
|
+
folders: { $ref: "#/definitions/FoldersType" },
|
|
12
13
|
summary: { $ref: "#/definitions/SummaryType" },
|
|
13
14
|
},
|
|
14
15
|
definitions: {
|
|
@@ -158,6 +159,21 @@ module.exports = {
|
|
|
158
159
|
},
|
|
159
160
|
},
|
|
160
161
|
SeverityType: { type: "string", enum: ["error", "warn", "info", "ignore"] },
|
|
162
|
+
FoldersType: { type: "array", items: { $ref: "#/definitions/FolderType" } },
|
|
163
|
+
FolderType: {
|
|
164
|
+
type: "object",
|
|
165
|
+
required: ["name", "moduleCount"],
|
|
166
|
+
additionalProperties: false,
|
|
167
|
+
properties: {
|
|
168
|
+
name: { type: "string" },
|
|
169
|
+
dependents: { type: "array", items: { type: "string" } },
|
|
170
|
+
dependencies: { type: "array", items: { type: "string" } },
|
|
171
|
+
moduleCount: { type: "number" },
|
|
172
|
+
afferentCouplings: { type: "number" },
|
|
173
|
+
efferentCouplings: { type: "number" },
|
|
174
|
+
instability: { type: "number" },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
161
177
|
SummaryType: {
|
|
162
178
|
type: "object",
|
|
163
179
|
required: [
|
|
@@ -510,6 +526,7 @@ module.exports = {
|
|
|
510
526
|
"teamcity",
|
|
511
527
|
"anon",
|
|
512
528
|
"text",
|
|
529
|
+
"metrics",
|
|
513
530
|
],
|
|
514
531
|
},
|
|
515
532
|
{ type: "string", pattern: "^plugin:[^:]+$" },
|
package/types/cruise-result.d.ts
CHANGED
|
@@ -12,6 +12,15 @@ export interface ICruiseResult {
|
|
|
12
12
|
* A list of modules, with for each module the modules it depends upon
|
|
13
13
|
*/
|
|
14
14
|
modules: IModule[];
|
|
15
|
+
/**
|
|
16
|
+
* A list of folders, as derived from the detected modules, with for each
|
|
17
|
+
* "folder a bunch of metrics (adapted from 'Agile software development:
|
|
18
|
+
* "principles, patterns, and practices' by Robert C Martin (ISBN 0-13-597444-5).
|
|
19
|
+
* "Note: these metrics substitute 'components' and 'classes' from that book
|
|
20
|
+
* "with 'folders' and 'modules'; the closest relatives that work for the most
|
|
21
|
+
* "programming styles in JavaScript (and its derivative languages).
|
|
22
|
+
*/
|
|
23
|
+
folders?: IFolder[];
|
|
15
24
|
/**
|
|
16
25
|
* Data summarizing the found dependencies
|
|
17
26
|
*/
|
|
@@ -393,3 +402,47 @@ export interface IViolation {
|
|
|
393
402
|
*/
|
|
394
403
|
via?: string[];
|
|
395
404
|
}
|
|
405
|
+
|
|
406
|
+
export interface IFolder {
|
|
407
|
+
/**
|
|
408
|
+
* The name of the folder. FOlder names are normalized to posix (so
|
|
409
|
+
* separated by forward slashes e.g.: src/things/morethings)
|
|
410
|
+
*/
|
|
411
|
+
name: string;
|
|
412
|
+
/**
|
|
413
|
+
* List of folders depending on this folder
|
|
414
|
+
*/
|
|
415
|
+
dependents?: string[];
|
|
416
|
+
/**
|
|
417
|
+
* List of folders this folder depends upon
|
|
418
|
+
*/
|
|
419
|
+
dependencies?: string[];
|
|
420
|
+
/**
|
|
421
|
+
* The total number of modules detected in this folder and its sub-folders
|
|
422
|
+
*/
|
|
423
|
+
moduleCount: number;
|
|
424
|
+
/**
|
|
425
|
+
* The number of modules outside this folder that depend on modules
|
|
426
|
+
* within this folder. Only present when dependency-cruiser was
|
|
427
|
+
* "asked to calculate it.
|
|
428
|
+
*/
|
|
429
|
+
afferentCouplings?: number;
|
|
430
|
+
/**
|
|
431
|
+
* The number of modules inside this folder that depend on modules
|
|
432
|
+
* outside this folder. Only present when dependency-cruiser was
|
|
433
|
+
* asked to calculate it.
|
|
434
|
+
*/
|
|
435
|
+
efferentCouplings?: number;
|
|
436
|
+
/**
|
|
437
|
+
* efferentCouplings/ (afferentCouplings + efferentCouplings)
|
|
438
|
+
*
|
|
439
|
+
* A measure for how stable the folder is; ranging between 0
|
|
440
|
+
* (completely stable folder) to 1 (completely instable folder)
|
|
441
|
+
* Note that while 'instability' has a negative connotation it's also
|
|
442
|
+
* (unavoidable in any meaningful system. It's the basis of Martin's
|
|
443
|
+
* variable component stability principle: 'the instability of a folder
|
|
444
|
+
* should be larger than the folders it depends on'. Only present when
|
|
445
|
+
* dependency-cruiser was asked to calculate it.,
|
|
446
|
+
*/
|
|
447
|
+
instability?: number;
|
|
448
|
+
}
|