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.
@@ -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.7.0",
3
+ "version": "10.8.0-beta-1",
4
4
  "description": "Validate and visualize dependencies. With your rules. JavaScript, TypeScript, CoffeeScript. ES6, CommonJS, AMD.",
5
5
  "keywords": [
6
6
  "static analysis",
@@ -21,6 +21,7 @@ const KNOWN_DEPCRUISE_CLI_OPTIONS = [
21
21
  "info",
22
22
  "init",
23
23
  "maxDepth",
24
+ "metrics",
24
25
  "moduleSystems",
25
26
  "outputTo",
26
27
  "outputType",
@@ -11,7 +11,12 @@ function hasDependentsRule(pOptions) {
11
11
  }
12
12
 
13
13
  function shouldAddDependents(pOptions) {
14
- return Boolean(pOptions.forceDeriveDependents) || hasDependentsRule(pOptions);
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
+ };
@@ -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
@@ -1,7 +1,7 @@
1
1
  /* generated - don't edit */
2
2
 
3
3
  module.exports = {
4
- version: "10.7.0",
4
+ version: "10.8.0-beta-1",
5
5
  engines: {
6
6
  node: "^12.20||^14||>=16",
7
7
  },
@@ -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:[^:]+$" },
@@ -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
+ }
@@ -18,6 +18,7 @@ export type OutputType =
18
18
  | "teamcity"
19
19
  | "anon"
20
20
  | "text"
21
+ | "metrics"
21
22
  | string;
22
23
 
23
24
  export type SeverityType = "error" | "warn" | "info" | "ignore";