Repository: sverweij/dependency-cruiser
Pull request #882 (2023-12-06 13:25:09)

feat(report/dot): adds ability to match arrays for conditional coloring
## Description

- adds ability to match arrays for conditional coloring on the dot reporter's theming

## Motivation and Context

So it's possible to add formatting based on array like properties (like `dependencyTypes`) and to have a convenient way to match against multiple criteria at the same time.

## How Has This Been Tested?

- [x] green ci
- [x] additional automated non-regression tests

## Screenshots

E.g. to color based on types of aliased dependency types like this:

![colored-by-dependency-types](https://github.com/sverweij/dependency-cruiser/assets/4822597/3a11cb97-341d-41e4-8774-53331102cbf9)

(from the reproduction example for #863 on https://github.com/sverweij/dependency-cruiser-repro-repo/tree/main/863)


... you can use this configuration that uses most features enabled by this PR:

```javascript
/** @type {import('dependency-cruiser').IConfiguration} */
module.exports = {
  options: {
    doNotFollow: { path: "node_modules" },
    moduleSystems: ["es6", "cjs"],
    tsPreCompilationDeps: true,
    combinedDependencies: true,
    tsConfig: { fileName: "tsconfig.json" },
    reporterOptions: {
      dot: {
        theme: {
          graph: {
            rankdir: "TD",
            splines: "ortho",
          },
          dependencies: [
            {
              // if the dependency type is one of the tsconfig type aliases ...
              criteria: {
                dependencyTypes: [
                  "aliased-tsconfig-",
                  "aliased-tsconfig-base-url",
                ],
              },
              // ... color the line teal
              attributes: { color: "blue" },
            },
            {
              criteria: { dependencyTypes: ["aliased-workspace"] },
              attributes: { color: "purple" },
            },
            {
              // if you just want one of the dependency types to match, a string suffices
              criteria: { dependencyTypes: "aliased-subpath-import" },
              attributes: { color: "green" },
            },
          ],
        },
      },
    },
  },
};
```

## Types of changes

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Documentation only change
- [ ] Refactor (non-breaking change which fixes an issue without changing functionality)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist

- [x] :book:

  - My change doesn't require a documentation update, or ...
  - it _does_ and I have updated it

- [x] :balance_scale:
  - The contribution will be subject to [The MIT license](https://github.com/sverweij/dependency-cruiser/blob/main/LICENSE), and I'm OK with that.
  - The contribution is my own original work.
  - I am ok with the stuff in [**CONTRIBUTING.md**](https://github.com/sverweij/dependency-cruiser/blob/main/.github/CONTRIBUTING.md).


codeclimate[bot]: Code Climate has analyzed commit 64eb79dd and detected **0 issues** on this pull request.

View more on [Code Climate](https://codeclimate.com/github/sverweij/dependency-cruiser/pull/882).


Patch:
diff --git a/doc/options-reference.md b/doc/options-reference.md
index be29a09e..3155f105 100644
--- a/doc/options-reference.md
+++ b/doc/options-reference.md
@@ -841,7 +841,9 @@ Most representational aspects of the 'dot' reporter are customizable:
   - You can use any
     [module attribute](https://github.com/sverweij/dependency-cruiser/blob/main/types/cruise-result.d.ts#L16)
     and any [dependency attribute](https://github.com/sverweij/dependency-cruiser/blob/main/types/cruise-result.d.ts#L73)
-    for dependencies.
+    for dependencies in the criteria of those.
+  - If you provide an array of criteria for the attributes the module/ dependency
+    will be matched if it matches _any_ of the criteria.
   - For attributes you can use anything GraphViz dot can understand as an attribute
     (see their [attributes](https://graphviz.gitlab.io/_pages/doc/info/attrs.html)
     documentation for a complete overview).
diff --git a/src/report/dot/theming.mjs b/src/report/dot/theming.mjs
index d1e153a0..78ee6713 100644
--- a/src/report/dot/theming.mjs
+++ b/src/report/dot/theming.mjs
@@ -8,13 +8,42 @@ function matchesRE(pValue, pRE) {
   return Boolean(lMatchResult) && lMatchResult.length > 0;
 }
 
+function matchesCriterion(pModuleKey, pCriterion) {
+  return pModuleKey === pCriterion || matchesRE(pModuleKey, pCriterion);
+}
+
 function moduleOrDependencyMatchesCriteria(pSchemeEntry, pModule) {
-  return Object.keys(pSchemeEntry.criteria).every(
-    (pKey) =>
-      (get(pModule, pKey) || has(pModule, pKey)) &&
-      (get(pModule, pKey) === get(pSchemeEntry.criteria, pKey) ||
-        matchesRE(get(pModule, pKey), get(pSchemeEntry.criteria, pKey))),
-  );
+  return Object.keys(pSchemeEntry.criteria).every((pKey) => {
+    // we use lodash.get here because in the criteria you can enter
+    // nested keys like "rules[0].severity" : "error", and lodash.get handles
+    // that for us
+    const lCriterion = get(pSchemeEntry.criteria, pKey);
+    const lModuleKey = get(pModule, pKey);
+
+    if (!(lModuleKey || has(pModule, pKey))) {
+      return false;
+    }
+
+    if (Array.isArray(lModuleKey)) {
+      if (Array.isArray(lCriterion)) {
+        return lCriterion.some((pCriterionEntry) =>
+          lModuleKey.some((pModuleKeyEntry) =>
+            matchesCriterion(pModuleKeyEntry, pCriterionEntry),
+          ),
+        );
+      } else {
+        return lModuleKey.some((pModuleKeyEntry) =>
+          matchesCriterion(pModuleKeyEntry, lCriterion),
+        );
+      }
+    }
+    if (Array.isArray(lCriterion)) {
+      return lCriterion.some((pCriterionEntry) =>
+        matchesCriterion(lModuleKey, pCriterionEntry),
+      );
+    }
+    return matchesCriterion(lModuleKey, lCriterion);
+  });
 }
 
 function determineAttributes(pModuleOrDependency, pAttributeCriteria) {


Test patch:
diff --git a/test/report/dot/theming.spec.mjs b/test/report/dot/theming.spec.mjs
index ea46a0af..2c614f61 100644
--- a/test/report/dot/theming.spec.mjs
+++ b/test/report/dot/theming.spec.mjs
@@ -45,4 +45,177 @@ describe("[U] report/dot/theming - determineModuleColors - default theme", () =>
     theming.normalizeTheme({ graph: { someAttribute: 1234 } });
     deepEqual(theming.normalizeTheme(), lOriginalDefaultTheme);
   });
+
+  it("determines attributes when the property is a string and one of the criteria is an array", () => {
+    deepEqual(
+      theming.determineAttributes(
+        { source: "package.json" },
+        theming.normalizeTheme({
+          modules: [
+            {
+              criteria: { source: ["package.json", "package-lock.json"] },
+              attributes: { fillcolor: "red" },
+            },
+          ],
+        }).modules,
+      ),
+      { fillcolor: "red" },
+    );
+  });
+
+  it("determines attributes when the property is an array and one of the criteria is an array", () => {
+    deepEqual(
+      theming.determineAttributes(
+        {
+          source: "src/heide/does.js",
+          dependencyTypes: [
+            "local",
+            "aliased",
+            "aliased-tsconfig",
+            "aliased-tsconfig-base-url",
+          ],
+        },
+        theming.normalizeTheme({
+          modules: [
+            {
+              criteria: { dependencyTypes: ["aliased-tsconfig"] },
+              attributes: { fillcolor: "blue" },
+            },
+          ],
+        }).modules,
+      ),
+      { fillcolor: "blue" },
+    );
+  });
+
+  it("determines attributes when the property is an array and one of the criteria is an array - on multiple it takes the logical OR", () => {
+    deepEqual(
+      theming.determineAttributes(
+        {
+          source: "src/heide/does.js",
+          dependencyTypes: [
+            "local",
+            "aliased",
+            "aliased-tsconfig",
+            "aliased-tsconfig-base-url",
+          ],
+        },
+        theming.normalizeTheme({
+          modules: [
+            {
+              criteria: {
+                dependencyTypes: [
+                  "npm",
+                  "aliased-workspace",
+                  "aliased-tsconfig",
+                ],
+              },
+              attributes: { fillcolor: "blue" },
+            },
+          ],
+        }).modules,
+      ),
+      { fillcolor: "blue" },
+    );
+  });
+
+  it("determines attributes when the property is an array and one of the criteria is a regexy array", () => {
+    deepEqual(
+      theming.determineAttributes(
+        {
+          source: "src/heide/does.js",
+          dependencyTypes: [
+            "local",
+            "aliased",
+            "aliased-tsconfig",
+            "aliased-tsconfig-base-url",
+          ],
+        },
+        theming.normalizeTheme({
+          modules: [
+            {
+              criteria: { dependencyTypes: ["aliased-t.+"] },
+              attributes: { fillcolor: "blue" },
+            },
+          ],
+        }).modules,
+      ),
+      { fillcolor: "blue" },
+    );
+  });
+
+  it("determines attributes when the property is an array and one of the criteria is a regexy array but there's no match", () => {
+    deepEqual(
+      theming.determineAttributes(
+        {
+          source: "src/heide/does.js",
+          dependencyTypes: [
+            "local",
+            "aliased",
+            "aliased-tsconfig",
+            "aliased-tsconfig-base-url",
+          ],
+        },
+        theming.normalizeTheme({
+          modules: [
+            {
+              criteria: { dependencyTypes: ["npm"] },
+              attributes: { fillcolor: "blue" },
+            },
+          ],
+        }).modules,
+      ),
+      {},
+    );
+  });
+
+  it("determines attributes when the property is an array and one of the criteria is a string", () => {
+    deepEqual(
+      theming.determineAttributes(
+        {
+          source: "src/heide/does.js",
+          dependencyTypes: [
+            "local",
+            "aliased",
+            "aliased-tsconfig",
+            "aliased-tsconfig-base-url",
+          ],
+        },
+        theming.normalizeTheme({
+          modules: [
+            {
+              criteria: { dependencyTypes: "aliased-tsconfig" },
+              attributes: { fillcolor: "blue" },
+            },
+          ],
+        }).modules,
+      ),
+      { fillcolor: "blue" },
+    );
+  });
+
+  it("determines attributes when the property is an array and one of the criteria is a regexy string", () => {
+    deepEqual(
+      theming.determineAttributes(
+        {
+          source: "src/heide/does.js",
+          dependencyTypes: [
+            "local",
+            "aliased",
+            "aliased-tsconfig",
+            "aliased-tsconfig-base-url",
+          ],
+        },
+        theming.normalizeTheme({
+          modules: [
+            {
+              criteria: { dependencyTypes: "aliased-tsconfig" },
+              attributes: { fillcolor: "blue" },
+            },
+          ],
+        }).modules,
+      ),
+      { fillcolor: "blue" },
+    );
+  });
 });