Skip to content

Commit

Permalink
feat(report/dot): adds ability to match arrays for conditional colori…
Browse files Browse the repository at this point in the history
…ng (#882)

## 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] 📖

  - My change doesn't require a documentation update, or ...
  - it _does_ and I have updated it

- [x] ⚖️
- 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).
  • Loading branch information
sverweij committed Dec 6, 2023
1 parent 33978af commit 7abbe47
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 7 deletions.
4 changes: 3 additions & 1 deletion doc/options-reference.md
Expand Up @@ -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).
Expand Down
41 changes: 35 additions & 6 deletions src/report/dot/theming.mjs
Expand Up @@ -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) {
Expand Down
173 changes: 173 additions & 0 deletions test/report/dot/theming.spec.mjs
Expand Up @@ -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" },
);
});
});

0 comments on commit 7abbe47

Please sign in to comment.