dependency-cruiser 10.6.0 → 10.8.0-beta-3
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 +2 -7
- package/src/cli/normalize-cli-options.js +1 -0
- package/src/cli/validate-node-environment.js +2 -2
- package/src/enrich/derive/dependents/index.js +6 -1
- package/src/enrich/derive/metrics/folder.js +9 -0
- package/src/enrich/derive/metrics/get-stability-metrics.js +97 -0
- package/src/enrich/derive/metrics/module-utl.js +27 -0
- package/src/enrich/derive/metrics/module.js +29 -0
- package/src/enrich/derive/metrics/utl.js +28 -0
- package/src/enrich/enrich-modules.js +5 -0
- package/src/enrich/index.js +2 -0
- package/src/main/options/normalize.js +3 -3
- package/src/main/report-wrap.js +7 -2
- package/src/meta.js +1 -1
- package/src/report/index.js +2 -0
- package/src/report/metrics.js +131 -0
- package/src/report/teamcity.js +49 -14
- package/src/schema/configuration.schema.js +19 -0
- package/src/schema/cruise-result.schema.js +37 -0
- package/src/utl/wrap-and-indent.js +2 -2
- package/types/cruise-result.d.ts +62 -0
- package/types/reporter-options.d.ts +17 -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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dependency-cruiser",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.8.0-beta-3",
|
|
4
4
|
"description": "Validate and visualize dependencies. With your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"static analysis",
|
|
@@ -170,7 +170,7 @@
|
|
|
170
170
|
"eslint": "^7.32.0",
|
|
171
171
|
"eslint-config-moving-meadow": "2.0.9",
|
|
172
172
|
"eslint-config-prettier": "8.3.0",
|
|
173
|
-
"eslint-plugin-budapestian": "
|
|
173
|
+
"eslint-plugin-budapestian": "3.0.1",
|
|
174
174
|
"eslint-plugin-import": "2.25.2",
|
|
175
175
|
"eslint-plugin-mocha": "9.0.0",
|
|
176
176
|
"eslint-plugin-node": "11.1.0",
|
|
@@ -199,11 +199,6 @@
|
|
|
199
199
|
"policy": "wanted",
|
|
200
200
|
"because": "some eslint plugins (eslint-plugin-budapestian) are not compatible with eslint 8 yet "
|
|
201
201
|
},
|
|
202
|
-
{
|
|
203
|
-
"package": "eslint-plugin-budapestian",
|
|
204
|
-
"policy": "pin",
|
|
205
|
-
"because": "eslint-plugin-budapestian 3.0.0 is a bit too enthousiastic flagging things it shouldn't - keeping at 2.3.0 for the time being"
|
|
206
|
-
},
|
|
207
202
|
{
|
|
208
203
|
"package": "figures",
|
|
209
204
|
"policy": "wanted",
|
|
@@ -5,7 +5,7 @@ module.exports = function validateNodeEnvironment(pNodeVersion) {
|
|
|
5
5
|
// not using default parameter here because the check should run
|
|
6
6
|
// run on node 4 as well
|
|
7
7
|
const lNodeVersion = pNodeVersion || process.versions.node;
|
|
8
|
-
const
|
|
8
|
+
const lVersionError = `\nERROR: Your node version (${lNodeVersion}) is not supported. dependency-cruiser
|
|
9
9
|
follows the node.js release cycle and runs on these node versions:
|
|
10
10
|
${engines.node}
|
|
11
11
|
See https://nodejs.org/en/about/releases/ for details.
|
|
@@ -13,6 +13,6 @@ module.exports = function validateNodeEnvironment(pNodeVersion) {
|
|
|
13
13
|
`;
|
|
14
14
|
|
|
15
15
|
if (!satisfies(lNodeVersion, engines.node)) {
|
|
16
|
-
throw new Error(
|
|
16
|
+
throw new Error(lVersionError);
|
|
17
17
|
}
|
|
18
18
|
};
|
|
@@ -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,9 @@
|
|
|
1
|
+
const getStabilityMetrics = require("./get-stability-metrics");
|
|
2
|
+
const { shouldDeriveMetrics } = require("./utl");
|
|
3
|
+
|
|
4
|
+
module.exports = function deriveMetrics(pModules, pOptions) {
|
|
5
|
+
if (shouldDeriveMetrics(pOptions)) {
|
|
6
|
+
return { folders: getStabilityMetrics(pModules) };
|
|
7
|
+
}
|
|
8
|
+
return {};
|
|
9
|
+
};
|
|
@@ -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,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,29 @@
|
|
|
1
|
+
// const { findModuleByName, clearCache } = require("../utl");
|
|
2
|
+
const { metricsAreCalculable } = require("./module-utl");
|
|
3
|
+
const { shouldDeriveMetrics } = require("./utl");
|
|
4
|
+
|
|
5
|
+
module.exports = function deriveModuleMetrics(pModules, pOptions) {
|
|
6
|
+
if (shouldDeriveMetrics(pOptions)) {
|
|
7
|
+
return pModules.map((pModule) => ({
|
|
8
|
+
...pModule,
|
|
9
|
+
...(metricsAreCalculable(pModule)
|
|
10
|
+
? {
|
|
11
|
+
instability:
|
|
12
|
+
pModule.dependencies.length /
|
|
13
|
+
(pModule.dependents.length + pModule.dependencies.length) || 0,
|
|
14
|
+
}
|
|
15
|
+
: {}),
|
|
16
|
+
}));
|
|
17
|
+
// clearCache();
|
|
18
|
+
// return lModules.map((pModule) => ({
|
|
19
|
+
// ...pModule,
|
|
20
|
+
// dependencies: pModule.dependencies.map((pDependency) => ({
|
|
21
|
+
// ...pDependency,
|
|
22
|
+
// instability:
|
|
23
|
+
// (findModuleByName(lModules, pDependency.resolved) || {})
|
|
24
|
+
// .instability || 0,
|
|
25
|
+
// })),
|
|
26
|
+
// }));
|
|
27
|
+
}
|
|
28
|
+
return pModules;
|
|
29
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
function shouldDeriveMetrics(pOptions) {
|
|
21
|
+
return pOptions.metrics || pOptions.outputType === "metrics";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = {
|
|
25
|
+
getParentFolders,
|
|
26
|
+
foldersObject2folderArray,
|
|
27
|
+
shouldDeriveMetrics,
|
|
28
|
+
};
|
|
@@ -8,6 +8,7 @@ const addDependents = require("./derive/dependents");
|
|
|
8
8
|
const deriveReachable = require("./derive/reachable");
|
|
9
9
|
const addValidations = require("./add-validations");
|
|
10
10
|
const softenKnownViolations = require("./soften-known-violations");
|
|
11
|
+
const deriveModuleMetrics = require("./derive/metrics/module");
|
|
11
12
|
|
|
12
13
|
module.exports = function enrichModules(pModules, pOptions) {
|
|
13
14
|
bus.emit("progress", "analyzing: cycles", { level: busLogLevels.INFO });
|
|
@@ -18,6 +19,10 @@ module.exports = function enrichModules(pModules, pOptions) {
|
|
|
18
19
|
lModules = deriveOrphans(lModules);
|
|
19
20
|
bus.emit("progress", "analyzing: reachables", { level: busLogLevels.INFO });
|
|
20
21
|
lModules = deriveReachable(lModules, pOptions.ruleSet);
|
|
22
|
+
bus.emit("progress", "analyzing: calculating module metrics", {
|
|
23
|
+
level: busLogLevels.INFO,
|
|
24
|
+
});
|
|
25
|
+
lModules = deriveModuleMetrics(lModules, pOptions);
|
|
21
26
|
bus.emit("progress", "analyzing: add focus (if any)", {
|
|
22
27
|
level: busLogLevels.INFO,
|
|
23
28
|
});
|
package/src/enrich/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const enrichModules = require("./enrich-modules");
|
|
2
|
+
const deriveFolderMetrics = require("./derive/metrics/folder.js");
|
|
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
|
+
...deriveFolderMetrics(lModules, pOptions),
|
|
12
14
|
summary: summarize(lModules, pOptions, pFileAndDirectoryArray),
|
|
13
15
|
};
|
|
14
16
|
};
|
|
@@ -54,12 +54,12 @@ function normalizeFilterOptions(pOptions, pFilterOptionKeys) {
|
|
|
54
54
|
function normalizeCollapse(pCollapse) {
|
|
55
55
|
let lReturnValue = pCollapse;
|
|
56
56
|
const lOneOrMoreNonSlashes = "[^/]+";
|
|
57
|
-
const
|
|
58
|
-
const
|
|
57
|
+
const lFolderPattern = `${lOneOrMoreNonSlashes}/`;
|
|
58
|
+
const lFolderBelowNodeModules = `node_modules/${lOneOrMoreNonSlashes}`;
|
|
59
59
|
const lSingleDigitRe = /^\d$/;
|
|
60
60
|
|
|
61
61
|
if (typeof pCollapse === "number" || pCollapse.match(lSingleDigitRe)) {
|
|
62
|
-
lReturnValue = `${
|
|
62
|
+
lReturnValue = `${lFolderBelowNodeModules}|^${lFolderPattern.repeat(
|
|
63
63
|
Number.parseInt(pCollapse, 10)
|
|
64
64
|
)}`;
|
|
65
65
|
}
|
package/src/main/report-wrap.js
CHANGED
|
@@ -28,13 +28,18 @@ function reSummarizeResults(pResult, pFormatOptions) {
|
|
|
28
28
|
|
|
29
29
|
module.exports = function reportWrap(pResult, pFormatOptions) {
|
|
30
30
|
const lReportFunction = report.getReporter(pFormatOptions.outputType);
|
|
31
|
+
const lReportOptions = _get(
|
|
32
|
+
pResult,
|
|
33
|
+
`summary.optionsUsed.reporterOptions.${pFormatOptions.outputType}`,
|
|
34
|
+
{}
|
|
35
|
+
);
|
|
31
36
|
|
|
32
37
|
return lReportFunction(
|
|
33
38
|
reSummarizeResults(pResult, pFormatOptions),
|
|
34
39
|
// passing format options here so reporters that read collapse patterns
|
|
35
40
|
// from the result take the one passed in the format options instead
|
|
36
41
|
_has(pFormatOptions, "collapse")
|
|
37
|
-
? { collapsePattern: pFormatOptions.collapse }
|
|
38
|
-
:
|
|
42
|
+
? { ...lReportOptions, collapsePattern: pFormatOptions.collapse }
|
|
43
|
+
: lReportOptions
|
|
39
44
|
);
|
|
40
45
|
};
|
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,131 @@
|
|
|
1
|
+
const { EOL } = 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
|
+
const COMPONENT_HEADER = "name";
|
|
9
|
+
|
|
10
|
+
function getHeader(pMaxNameWidth) {
|
|
11
|
+
return `${COMPONENT_HEADER.padEnd(pMaxNameWidth)} ${"N".padStart(
|
|
12
|
+
METRIC_WIDTH + 1
|
|
13
|
+
)} ${"Ca".padStart(METRIC_WIDTH + 1)} ${"Ce".padStart(
|
|
14
|
+
METRIC_WIDTH + 1
|
|
15
|
+
)} ${"I".padEnd(METRIC_WIDTH + 1)}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getDemarcationLine(pMaxNameWidth) {
|
|
19
|
+
return `${"-".repeat(pMaxNameWidth)} ${"-".repeat(
|
|
20
|
+
METRIC_WIDTH + 1
|
|
21
|
+
)} ${"-".repeat(METRIC_WIDTH + 1)} ${"-".repeat(
|
|
22
|
+
METRIC_WIDTH + 1
|
|
23
|
+
)} ${"-".repeat(METRIC_WIDTH + 1)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getMetricsTable(pMetrics, pMaxNameWidth) {
|
|
27
|
+
return pMetrics.map(
|
|
28
|
+
({
|
|
29
|
+
name,
|
|
30
|
+
moduleCount,
|
|
31
|
+
afferentCouplings,
|
|
32
|
+
efferentCouplings,
|
|
33
|
+
instability,
|
|
34
|
+
}) =>
|
|
35
|
+
`${name.padEnd(pMaxNameWidth, " ")} ${moduleCount
|
|
36
|
+
.toString(DECIMAL_BASE)
|
|
37
|
+
.padStart(METRIC_WIDTH)} ${afferentCouplings
|
|
38
|
+
.toString(DECIMAL_BASE)
|
|
39
|
+
.padStart(METRIC_WIDTH)} ${efferentCouplings
|
|
40
|
+
.toString(DECIMAL_BASE)
|
|
41
|
+
.padStart(METRIC_WIDTH)} ${(Math.round(YADDUM * instability) / YADDUM)
|
|
42
|
+
.toString(DECIMAL_BASE)
|
|
43
|
+
.padEnd(METRIC_WIDTH)}`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function metricifyModule({ source, dependents, dependencies, instability }) {
|
|
48
|
+
return {
|
|
49
|
+
name: source,
|
|
50
|
+
moduleCount: 1,
|
|
51
|
+
afferentCouplings: dependents.length,
|
|
52
|
+
efferentCouplings: dependencies.length,
|
|
53
|
+
instability,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function metricsAreCalculable(pModule) {
|
|
58
|
+
return Object.prototype.hasOwnProperty.call(pModule, "instability");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function orderByNumber(pAttributeName) {
|
|
62
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
63
|
+
return (pLeft, pRight) => pRight[pAttributeName] - pLeft[pAttributeName];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function orderByString(pAttributeName) {
|
|
67
|
+
return (pLeft, pRight) =>
|
|
68
|
+
// eslint-disable-next-line security/detect-object-injection
|
|
69
|
+
pLeft[pAttributeName].localeCompare(pRight[pAttributeName]);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function transformMetricsToTable(
|
|
73
|
+
{ modules, folders },
|
|
74
|
+
{ orderBy, hideFolders, hideModules }
|
|
75
|
+
) {
|
|
76
|
+
// TODO: should probably use a table module for this (i.e. text-table)
|
|
77
|
+
// to simplify this code
|
|
78
|
+
let lComponents = [];
|
|
79
|
+
lComponents = lComponents.concat(hideFolders ? [] : folders);
|
|
80
|
+
lComponents = lComponents.concat(
|
|
81
|
+
hideModules ? [] : modules.filter(metricsAreCalculable).map(metricifyModule)
|
|
82
|
+
);
|
|
83
|
+
const lMaxNameWidth = lComponents
|
|
84
|
+
.map(({ name }) => name.length)
|
|
85
|
+
.concat(COMPONENT_HEADER.length)
|
|
86
|
+
.sort((pLeft, pRight) => pLeft - pRight)
|
|
87
|
+
.pop();
|
|
88
|
+
|
|
89
|
+
return [chalk.bold(getHeader(lMaxNameWidth))]
|
|
90
|
+
.concat(getDemarcationLine(lMaxNameWidth))
|
|
91
|
+
.concat(
|
|
92
|
+
getMetricsTable(
|
|
93
|
+
lComponents
|
|
94
|
+
.sort(orderByString("name"))
|
|
95
|
+
.sort(orderByNumber(orderBy || "instability")),
|
|
96
|
+
lMaxNameWidth
|
|
97
|
+
)
|
|
98
|
+
)
|
|
99
|
+
.join(EOL)
|
|
100
|
+
.concat(EOL);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* returns stability metrics of modules & folders in an ascii table
|
|
105
|
+
*
|
|
106
|
+
* Potential future features:
|
|
107
|
+
* - additional output formats (csv?, html?)
|
|
108
|
+
*
|
|
109
|
+
* @param {import('../../types/dependency-cruiser').ICruiseResult} pCruiseResult -
|
|
110
|
+
* the output of a dependency-cruise adhering to dependency-cruiser's
|
|
111
|
+
* cruise result schema
|
|
112
|
+
* @return {import('../../types/dependency-cruiser').IReporterOutput} -
|
|
113
|
+
* output: some metrics on folders and dependencies
|
|
114
|
+
* exitCode: 0
|
|
115
|
+
*/
|
|
116
|
+
module.exports = (pCruiseResult, pReporterOptions) => {
|
|
117
|
+
const lReporterOptions = pReporterOptions || {};
|
|
118
|
+
if (pCruiseResult.folders) {
|
|
119
|
+
return {
|
|
120
|
+
output: transformMetricsToTable(pCruiseResult, lReporterOptions),
|
|
121
|
+
exitCode: 0,
|
|
122
|
+
};
|
|
123
|
+
} else {
|
|
124
|
+
return {
|
|
125
|
+
output:
|
|
126
|
+
`${EOL}ERROR: The cruise result didn't contain any metrics - re-running the cruise with${EOL}` +
|
|
127
|
+
` the '--metrics' command line option should fix that.${EOL}${EOL}`,
|
|
128
|
+
exitCode: 1,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
};
|
package/src/report/teamcity.js
CHANGED
|
@@ -45,10 +45,26 @@ function reportAllowedRule(pAllowedRule, pViolations) {
|
|
|
45
45
|
return lReturnValue;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
function
|
|
48
|
+
function reportIgnoredRules(pIgnoredCount) {
|
|
49
|
+
let lReturnValue = [];
|
|
50
|
+
|
|
51
|
+
if (pIgnoredCount > 0) {
|
|
52
|
+
lReturnValue = tsm.inspectionType({
|
|
53
|
+
id: "ignored-known-violations",
|
|
54
|
+
name: "ignored-known-violations",
|
|
55
|
+
description:
|
|
56
|
+
"some dependency violations were ignored; run without --ignore-known to see them",
|
|
57
|
+
category: CATEGORY,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return lReturnValue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function reportViolatedRules(pRuleSetUsed, pViolations, pIgnoredCount) {
|
|
49
64
|
return reportRules(_get(pRuleSetUsed, "forbidden", []), pViolations)
|
|
50
65
|
.concat(reportAllowedRule(_get(pRuleSetUsed, "allowed", []), pViolations))
|
|
51
|
-
.concat(reportRules(_get(pRuleSetUsed, "required", []), pViolations))
|
|
66
|
+
.concat(reportRules(_get(pRuleSetUsed, "required", []), pViolations))
|
|
67
|
+
.concat(reportIgnoredRules(pIgnoredCount));
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
function determineTo(pViolation) {
|
|
@@ -66,15 +82,31 @@ function bakeViolationMessage(pViolation) {
|
|
|
66
82
|
? pViolation.from
|
|
67
83
|
: `${pViolation.from} -> ${determineTo(pViolation)}`;
|
|
68
84
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
|
|
86
|
+
function reportIgnoredViolation(pIgnoredCount) {
|
|
87
|
+
let lReturnValue = [];
|
|
88
|
+
|
|
89
|
+
if (pIgnoredCount > 0) {
|
|
90
|
+
lReturnValue = tsm.inspection({
|
|
91
|
+
typeId: "ignored-known-violations",
|
|
92
|
+
message: `${pIgnoredCount} known violations ignored. Run without --ignore-known to see them.`,
|
|
93
|
+
SEVERITY: "WARNING",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return lReturnValue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function reportViolations(pViolations, pIgnoredCount) {
|
|
100
|
+
return pViolations
|
|
101
|
+
.map((pViolation) =>
|
|
102
|
+
tsm.inspection({
|
|
103
|
+
typeId: pViolation.rule.name,
|
|
104
|
+
message: bakeViolationMessage(pViolation),
|
|
105
|
+
file: pViolation.from,
|
|
106
|
+
SEVERITY: severity2teamcitySeverity(pViolation.rule.severity),
|
|
107
|
+
})
|
|
108
|
+
)
|
|
109
|
+
.concat(reportIgnoredViolation(pIgnoredCount));
|
|
78
110
|
}
|
|
79
111
|
|
|
80
112
|
/**
|
|
@@ -96,11 +128,14 @@ module.exports = (pResults) => {
|
|
|
96
128
|
tsm.stdout = false;
|
|
97
129
|
|
|
98
130
|
const lRuleSet = _get(pResults, "summary.ruleSetUsed", []);
|
|
99
|
-
const lViolations = _get(pResults, "summary.violations", [])
|
|
131
|
+
const lViolations = _get(pResults, "summary.violations", []).filter(
|
|
132
|
+
(pViolation) => pViolation.rule.severity !== "ignore"
|
|
133
|
+
);
|
|
134
|
+
const lIgnoredCount = _get(pResults, "summary.ignore", 0);
|
|
100
135
|
|
|
101
136
|
return {
|
|
102
|
-
output: reportViolatedRules(lRuleSet, lViolations)
|
|
103
|
-
.concat(reportViolations(lViolations))
|
|
137
|
+
output: reportViolatedRules(lRuleSet, lViolations, lIgnoredCount)
|
|
138
|
+
.concat(reportViolations(lViolations, lIgnoredCount))
|
|
104
139
|
.reduce((pAll, pCurrent) => `${pAll}${pCurrent}\n`, ""),
|
|
105
140
|
exitCode: pResults.summary.error,
|
|
106
141
|
};
|
|
@@ -394,6 +394,7 @@ module.exports = {
|
|
|
394
394
|
dot: { $ref: "#/definitions/DotReporterOptionsType" },
|
|
395
395
|
ddot: { $ref: "#/definitions/DotReporterOptionsType" },
|
|
396
396
|
flat: { $ref: "#/definitions/DotReporterOptionsType" },
|
|
397
|
+
metrics: { $ref: "#/definitions/MetricsReporterOptionsType" },
|
|
397
398
|
},
|
|
398
399
|
},
|
|
399
400
|
AnonReporterOptionsType: {
|
|
@@ -401,6 +402,24 @@ module.exports = {
|
|
|
401
402
|
additionalProperties: false,
|
|
402
403
|
properties: { wordlist: { type: "array", items: { type: "string" } } },
|
|
403
404
|
},
|
|
405
|
+
MetricsReporterOptionsType: {
|
|
406
|
+
type: "object",
|
|
407
|
+
additionalProperties: false,
|
|
408
|
+
properties: {
|
|
409
|
+
orderBy: {
|
|
410
|
+
type: "string",
|
|
411
|
+
enum: [
|
|
412
|
+
"instability",
|
|
413
|
+
"moduleCount",
|
|
414
|
+
"afferentCouplings",
|
|
415
|
+
"efferentCouplings",
|
|
416
|
+
"name",
|
|
417
|
+
],
|
|
418
|
+
},
|
|
419
|
+
hideModules: { type: "boolean" },
|
|
420
|
+
hideFolders: { type: "boolean" },
|
|
421
|
+
},
|
|
422
|
+
},
|
|
404
423
|
DotReporterOptionsType: {
|
|
405
424
|
type: "object",
|
|
406
425
|
additionalProperties: false,
|
|
@@ -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: {
|
|
@@ -46,6 +47,7 @@ module.exports = {
|
|
|
46
47
|
items: { $ref: "#/definitions/RuleSummaryType" },
|
|
47
48
|
},
|
|
48
49
|
consolidated: { type: "boolean" },
|
|
50
|
+
instability: { type: "number" },
|
|
49
51
|
},
|
|
50
52
|
},
|
|
51
53
|
ReachableType: {
|
|
@@ -158,6 +160,21 @@ module.exports = {
|
|
|
158
160
|
},
|
|
159
161
|
},
|
|
160
162
|
SeverityType: { type: "string", enum: ["error", "warn", "info", "ignore"] },
|
|
163
|
+
FoldersType: { type: "array", items: { $ref: "#/definitions/FolderType" } },
|
|
164
|
+
FolderType: {
|
|
165
|
+
type: "object",
|
|
166
|
+
required: ["name", "moduleCount"],
|
|
167
|
+
additionalProperties: false,
|
|
168
|
+
properties: {
|
|
169
|
+
name: { type: "string" },
|
|
170
|
+
dependents: { type: "array", items: { type: "string" } },
|
|
171
|
+
dependencies: { type: "array", items: { type: "string" } },
|
|
172
|
+
moduleCount: { type: "number" },
|
|
173
|
+
afferentCouplings: { type: "number" },
|
|
174
|
+
efferentCouplings: { type: "number" },
|
|
175
|
+
instability: { type: "number" },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
161
178
|
SummaryType: {
|
|
162
179
|
type: "object",
|
|
163
180
|
required: [
|
|
@@ -510,6 +527,7 @@ module.exports = {
|
|
|
510
527
|
"teamcity",
|
|
511
528
|
"anon",
|
|
512
529
|
"text",
|
|
530
|
+
"metrics",
|
|
513
531
|
],
|
|
514
532
|
},
|
|
515
533
|
{ type: "string", pattern: "^plugin:[^:]+$" },
|
|
@@ -553,6 +571,7 @@ module.exports = {
|
|
|
553
571
|
dot: { $ref: "#/definitions/DotReporterOptionsType" },
|
|
554
572
|
ddot: { $ref: "#/definitions/DotReporterOptionsType" },
|
|
555
573
|
flat: { $ref: "#/definitions/DotReporterOptionsType" },
|
|
574
|
+
metrics: { $ref: "#/definitions/MetricsReporterOptionsType" },
|
|
556
575
|
},
|
|
557
576
|
},
|
|
558
577
|
AnonReporterOptionsType: {
|
|
@@ -560,6 +579,24 @@ module.exports = {
|
|
|
560
579
|
additionalProperties: false,
|
|
561
580
|
properties: { wordlist: { type: "array", items: { type: "string" } } },
|
|
562
581
|
},
|
|
582
|
+
MetricsReporterOptionsType: {
|
|
583
|
+
type: "object",
|
|
584
|
+
additionalProperties: false,
|
|
585
|
+
properties: {
|
|
586
|
+
orderBy: {
|
|
587
|
+
type: "string",
|
|
588
|
+
enum: [
|
|
589
|
+
"instability",
|
|
590
|
+
"moduleCount",
|
|
591
|
+
"afferentCouplings",
|
|
592
|
+
"efferentCouplings",
|
|
593
|
+
"name",
|
|
594
|
+
],
|
|
595
|
+
},
|
|
596
|
+
hideModules: { type: "boolean" },
|
|
597
|
+
hideFolders: { type: "boolean" },
|
|
598
|
+
},
|
|
599
|
+
},
|
|
563
600
|
DotReporterOptionsType: {
|
|
564
601
|
type: "object",
|
|
565
602
|
additionalProperties: false,
|
|
@@ -4,7 +4,7 @@ const wrapAnsi = require("wrap-ansi");
|
|
|
4
4
|
const DEFAULT_INDENT = 4;
|
|
5
5
|
module.exports = function wrapAndIndent(pString, pIndent = DEFAULT_INDENT) {
|
|
6
6
|
const lDogmaticMaxConsoleWidth = 78;
|
|
7
|
-
const
|
|
7
|
+
const lMaxWidth = lDogmaticMaxConsoleWidth - pIndent;
|
|
8
8
|
|
|
9
|
-
return indentString(wrapAnsi(pString,
|
|
9
|
+
return indentString(wrapAnsi(pString, lMaxWidth), pIndent);
|
|
10
10
|
};
|
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
|
*/
|
|
@@ -97,6 +106,15 @@ export interface IModule {
|
|
|
97
106
|
* purposes - it will not be present after a regular cruise.
|
|
98
107
|
*/
|
|
99
108
|
consolidated?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* "number of dependents/ (number of dependents + number of dependencies)
|
|
111
|
+
* A measure for how stable the module is; ranging between 0 (completely
|
|
112
|
+
* stable module) to 1 (completely instable module). Derived from Uncle
|
|
113
|
+
* Bob's instability metric - but applied to a single module instead of
|
|
114
|
+
* to a group of them. This attribute is only present when dependency-cruiser
|
|
115
|
+
* was asked to calculate metrics.
|
|
116
|
+
*/
|
|
117
|
+
instability?: number;
|
|
100
118
|
}
|
|
101
119
|
|
|
102
120
|
export interface IDependency {
|
|
@@ -393,3 +411,47 @@ export interface IViolation {
|
|
|
393
411
|
*/
|
|
394
412
|
via?: string[];
|
|
395
413
|
}
|
|
414
|
+
|
|
415
|
+
export interface IFolder {
|
|
416
|
+
/**
|
|
417
|
+
* The name of the folder. FOlder names are normalized to posix (so
|
|
418
|
+
* separated by forward slashes e.g.: src/things/morethings)
|
|
419
|
+
*/
|
|
420
|
+
name: string;
|
|
421
|
+
/**
|
|
422
|
+
* List of folders depending on this folder
|
|
423
|
+
*/
|
|
424
|
+
dependents?: string[];
|
|
425
|
+
/**
|
|
426
|
+
* List of folders this folder depends upon
|
|
427
|
+
*/
|
|
428
|
+
dependencies?: string[];
|
|
429
|
+
/**
|
|
430
|
+
* The total number of modules detected in this folder and its sub-folders
|
|
431
|
+
*/
|
|
432
|
+
moduleCount: number;
|
|
433
|
+
/**
|
|
434
|
+
* The number of modules outside this folder that depend on modules
|
|
435
|
+
* within this folder. Only present when dependency-cruiser was
|
|
436
|
+
* "asked to calculate it.
|
|
437
|
+
*/
|
|
438
|
+
afferentCouplings?: number;
|
|
439
|
+
/**
|
|
440
|
+
* The number of modules inside this folder that depend on modules
|
|
441
|
+
* outside this folder. Only present when dependency-cruiser was
|
|
442
|
+
* asked to calculate it.
|
|
443
|
+
*/
|
|
444
|
+
efferentCouplings?: number;
|
|
445
|
+
/**
|
|
446
|
+
* efferentCouplings/ (afferentCouplings + efferentCouplings)
|
|
447
|
+
*
|
|
448
|
+
* A measure for how stable the folder is; ranging between 0
|
|
449
|
+
* (completely stable folder) to 1 (completely instable folder)
|
|
450
|
+
* Note that while 'instability' has a negative connotation it's also
|
|
451
|
+
* (unavoidable in any meaningful system. It's the basis of Martin's
|
|
452
|
+
* variable component stability principle: 'the instability of a folder
|
|
453
|
+
* should be larger than the folders it depends on'. Only present when
|
|
454
|
+
* dependency-cruiser was asked to calculate it.,
|
|
455
|
+
*/
|
|
456
|
+
instability?: number;
|
|
457
|
+
}
|
|
@@ -21,6 +21,10 @@ export interface IReporterOptions {
|
|
|
21
21
|
* Options to tweak the output of the flat /fdot reporter
|
|
22
22
|
*/
|
|
23
23
|
flat?: IDotReporterOptions;
|
|
24
|
+
/**
|
|
25
|
+
* Options to tweak the output of the metrics reporter
|
|
26
|
+
*/
|
|
27
|
+
metrics?: IMetricsReporterOptions;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
export interface IReporterFiltersType {
|
|
@@ -96,3 +100,16 @@ export interface IDotThemeEntry {
|
|
|
96
100
|
criteria: any;
|
|
97
101
|
attributes: any;
|
|
98
102
|
}
|
|
103
|
+
|
|
104
|
+
export interface IMetricsReporterOptions {
|
|
105
|
+
hideModules?: boolean;
|
|
106
|
+
hideFolders?: boolean;
|
|
107
|
+
oderBy?: MetricsOrderByType;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export type MetricsOrderByType =
|
|
111
|
+
| "instability"
|
|
112
|
+
| "moduleCount"
|
|
113
|
+
| "afferentCouplings"
|
|
114
|
+
| "efferentCouplings"
|
|
115
|
+
| "name";
|