Repository: sverweij/dependency-cruiser (JavaScript)

Issue:
Issue: viaNot is now reporting wrong cycles
<!--- Provide a general summary of the issue in the Title above -->

### Expected Behavior

I updated dependency cruiser to last version 11.7.1, I have now new failures on `viaNot`, because it reports cycle when testing "knots" that I wanted to exclude. 
Here is my rule : 

```
  {
    name: 'no-circular',
    severity: 'error',
    comment: `This dependency is part of a circular relationship.
      You might want to revise your solution (i.e. use dependency inversion,
       make sure the modules have a single responsibility) `,
    from: {},
    to: {
      circular: true,
      viaNot: [
        '^app/javascript/tables/index.ts',
        '^app/javascript/ui/index.tsx',
        '^app/javascript/hooks/index.tsx',
        '^app/javascript/deprecated_types/index.ts',
        'spec/javascript/factory/index.ts',
      ],
    },
  },
```

Before 11.7.1, I did not have any errors. I expect "knot files" to be ignored by the rule since they appear in the `viaNot`.

### Current Behavior

<!--- Tell what happens instead -->

I have now some errors like : 
```
  error no-circular: app/javascript/ui/index.tsx → 
      app/javascript/ui/Checkbox.tsx →
      app/javascript/ui/index.tsx
  error no-circular: app/javascript/ui/index.tsx → 
      app/javascript/ui/Breadcrumb.tsx →
      app/javascript/patterns/PageHeader.tsx →
      app/javascript/ui/index.tsx
```

So basically, it is now reporting errors when testing circular inside "knots file".

### Possible Solution

<!--- Not obligatory, but suggestions how to fix the bug, or ideas what the reason for the bug is are welcome. -->

As a workaround, I added all these files to `pathNot` in `from` : 
```
    {
      name: 'no-circular',
      severity: 'error',
      comment: `This dependency is part of a circular relationship.
        You might want to revise your solution (i.e. use dependency inversion,
         make sure the modules have a single responsibility) `,
      from: {
        pathNot: [
          '^app/javascript/tables/index.ts',
          '^app/javascript/ui/index.tsx',
          '^app/javascript/hooks/index.tsx',
          '^app/javascript/deprecated_types/index.ts',
          'spec/javascript/factory/index.ts',
        ],
      },
      to: {
        circular: true,
        viaNot: [
          '^app/javascript/tables/index.ts',
          '^app/javascript/ui/index.tsx',
          '^app/javascript/hooks/index.tsx',
          '^app/javascript/deprecated_types/index.ts',
          'spec/javascript/factory/index.ts',
        ],
      },
    },
```

Is it an expected behaviour or a bug ? 

### Your Environment

<!--- Include as many relevant details about the environment you experienced the bug in -->

- Version used: 11.7.1
- Node version: 16.3.0
- Operating System and version: MacOS Monterey 12.0.1



Discussion:
Hi @epaillous this looks like a bug, thanks for raising it!

 Recently some code changed around _via_ like rules - and it looks like one or more non-regression tests are missing. I'll make a minimal reproduction sample & fix/ revert where necessary.
Hi - found the bug and fixed it (see linked PR #605 above). It was introduced in 11.7.0 (with PR #589/ commit b71566d1c). 

Will release the fix before the end of the weekend in v11.8.0.



Patch:
diff --git a/doc/rules-reference.md b/doc/rules-reference.md
index f382a5588..59d6e4af2 100644
--- a/doc/rules-reference.md
+++ b/doc/rules-reference.md
@@ -727,6 +727,10 @@ to check against multiple paths; all the "via"'s in the cycle. The variants
 exist to enable matching against only _some_ of the modules in the cycle or
 against _all_ of them.
 
+All these restrictions take the whole cycle into account; _including_ the tested
+'from'; if `a/aa.js` has a cycle via `a/ab.js` and `b/bb/js` back to `a/aa.js`
+the via-like restrictions also take `a/aa.js` into account.
+
 The examples below refer to this cycle: `a/aa.js`, `a/ab.js`, `b/bb.js`, `a/aa.js`
 
 | restriction  | what it does                                                        | example input | match?  | because...                    |
diff --git a/src/validate/matchers.js b/src/validate/matchers.js
index a0ebb0c34..2b6e2ee38 100644
--- a/src/validate/matchers.js
+++ b/src/validate/matchers.js
@@ -91,19 +91,13 @@ function toDependencyTypesNot(pRule, pDependency) {
   );
 }
 
-const removeLast = (_via, pIndex, pArray) => pIndex !== pArray.length - 1;
-
 function toVia(pRule, pDependency, pGroups) {
   return Boolean(
     !pRule.to.via ||
       (pDependency.cycle &&
-        pDependency.cycle
-          // the last in the cycle is always the module itself, which, for
-          // via & viaNot checks isn't very useful
-          .filter(removeLast)
-          .some((pVia) =>
-            pVia.match(replaceGroupPlaceholders(pRule.to.via, pGroups))
-          ))
+        pDependency.cycle.some((pVia) =>
+          pVia.match(replaceGroupPlaceholders(pRule.to.via, pGroups))
+        ))
   );
 }
 
@@ -111,13 +105,9 @@ function toViaOnly(pRule, pDependency, pGroups) {
   return Boolean(
     !pRule.to.viaOnly ||
       (pDependency.cycle &&
-        pDependency.cycle
-          // the last in the cycle is always the module itself, which, for
-          // via & viaNot checks isn't very useful
-          .filter(removeLast)
-          .every((pVia) =>
-            pVia.match(replaceGroupPlaceholders(pRule.to.viaOnly, pGroups))
-          ))
+        pDependency.cycle.every((pVia) =>
+          pVia.match(replaceGroupPlaceholders(pRule.to.viaOnly, pGroups))
+        ))
   );
 }
 
@@ -125,13 +115,9 @@ function toViaNot(pRule, pDependency, pGroups) {
   return Boolean(
     !pRule.to.viaNot ||
       (pDependency.cycle &&
-        !pDependency.cycle
-          // the last in the cycle is always the module itself, which, for
-          // via & viaNot checks isn't very useful
-          .filter(removeLast)
-          .some((pVia) =>
-            pVia.match(replaceGroupPlaceholders(pRule.to.viaNot, pGroups))
-          ))
+        !pDependency.cycle.some((pVia) =>
+          pVia.match(replaceGroupPlaceholders(pRule.to.viaNot, pGroups))
+        ))
   );
 }
 
@@ -139,13 +125,9 @@ function toviaSomeNot(pRule, pDependency, pGroups) {
   return Boolean(
     !pRule.to.viaSomeNot ||
       (pDependency.cycle &&
-        !pDependency.cycle
-          // the last in the cycle is always the module itself, which, for
-          // via & viaNot checks isn't very useful
-          .filter(removeLast)
-          .every((pVia) =>
-            pVia.match(replaceGroupPlaceholders(pRule.to.viaSomeNot, pGroups))
-          ))
+        !pDependency.cycle.every((pVia) =>
+          pVia.match(replaceGroupPlaceholders(pRule.to.viaSomeNot, pGroups))
+        ))
   );
 }
 


Test patch:
diff --git a/test/validate/index.cycle-via-not.spec.mjs b/test/validate/index.cycle-via-not.spec.mjs
index 06172138a..77483cc1b 100644
--- a/test/validate/index.cycle-via-not.spec.mjs
+++ b/test/validate/index.cycle-via-not.spec.mjs
@@ -1,4 +1,3 @@
-/* eslint-disable max-statements */
 import { expect } from "chai";
 import validate from "../../src/validate/index.js";
 import parseRuleSet from "./parse-ruleset.utl.mjs";
@@ -77,13 +76,7 @@ describe("[I] validate/index dependency - cycle viaNot - with group matching", (
         }
       )
     ).to.deep.equal({
-      valid: false,
-      rules: [
-        {
-          name: "no-circular-dependency-of-modules",
-          severity: "warn",
-        },
-      ],
+      valid: true,
     });
   });
 
@@ -129,123 +122,3 @@ describe("[I] validate/index dependency - cycle viaNot - with group matching", (
     });
   });
 });
-
-describe("[I] validate/index dependency - cycle viaSomeNot - with group matching", () => {
-  const lCycleButNotViaGroupMatchRuleSet = {
-    forbidden: [
-      {
-        name: "no-circular-dependency-of-modules",
-        from: { path: "^src/([^/]+)/.+" },
-        to: { viaSomeNot: "^src/$1/.+", circular: true },
-      },
-    ],
-  };
-
-  it("flags when all of the cycle (except the root) is outside the group-matched viaSomeNot", () => {
-    expect(
-      validate.dependency(
-        parseRuleSet(lCycleButNotViaGroupMatchRuleSet),
-        { source: "src/module-a/a.js" },
-        {
-          resolved: "src/module-b/ba.js",
-          circular: true,
-          cycle: [
-            "src/module-b/ba.js",
-            "src/module-b/bb.js",
-            "src/module-b/bc.js",
-            "src/module-a/a.js",
-          ],
-        }
-      )
-    ).to.deep.equal({
-      valid: false,
-      rules: [
-        {
-          name: "no-circular-dependency-of-modules",
-          severity: "warn",
-        },
-      ],
-    });
-  });
-
-  it("flags when only one of the cycle is outside the group-matched viaNot", () => {
-    expect(
-      validate.dependency(
-        parseRuleSet(lCycleButNotViaGroupMatchRuleSet),
-        { source: "src/module-a/a.js" },
-        {
-          resolved: "src/module-a/aa.js",
-          circular: true,
-          cycle: [
-            "src/module-a/aa.js",
-            "src/module-a/ab.js",
-            "src/module-b/bc.js",
-            "src/module-a/a.js",
-          ],
-        }
-      )
-    ).to.deep.equal({
-      valid: false,
-      rules: [
-        {
-          name: "no-circular-dependency-of-modules",
-          severity: "warn",
-        },
-      ],
-    });
-  });
-
-  it("does not flag when all of the cycle is inside the group-matched viaNot", () => {
-    expect(
-      validate.dependency(
-        parseRuleSet(lCycleButNotViaGroupMatchRuleSet),
-        { source: "src/module-a/a.js" },
-        {
-          resolved: "src/module-a/aa.js",
-          circular: true,
-          cycle: [
-            "src/module-a/aa.js",
-            "src/module-a/ab.js",
-            "src/module-a/ac.js",
-            "src/module-a/a.js",
-          ],
-        }
-      )
-    ).to.deep.equal({
-      valid: true,
-    });
-  });
-
-  it("does not flag when all of the cycle is inside the group-matched viaNot that's represented as an array", () => {
-    const lRuleSet = {
-      forbidden: [
-        {
-          name: "no-circular-dependency-of-modules",
-          from: { path: "^src/([^/]+)/.+" },
-          to: {
-            viaSomeNot: ["something", "^src/$1/.+", "somethingelse"],
-            circular: true,
-          },
-        },
-      ],
-    };
-    expect(
-      validate.dependency(
-        parseRuleSet(lRuleSet),
-        { source: "src/module-a/a.js" },
-        {
-          resolved: "src/module-a/aa.js",
-          circular: true,
-          cycle: [
-            "src/module-a/aa.js",
-            "src/module-a/ab.js",
-            "src/module-a/ac.js",
-            "src/module-a/a.js",
-          ],
-        }
-      )
-    ).to.deep.equal({
-      valid: true,
-    });
-  });
-});
diff --git a/test/validate/index.cycle-via-some-not.spec.mjs b/test/validate/index.cycle-via-some-not.spec.mjs
new file mode 100644
index 000000000..e2fda874c
--- /dev/null
+++ b/test/validate/index.cycle-via-some-not.spec.mjs
@@ -0,0 +1,123 @@
+import { expect } from "chai";
+import validate from "../../src/validate/index.js";
+import parseRuleSet from "./parse-ruleset.utl.mjs";
+
+describe("[I] validate/index dependency - cycle viaSomeNot - with group matching", () => {
+  const lCycleButNotViaGroupMatchRuleSet = {
+    forbidden: [
+      {
+        name: "no-circular-dependency-of-modules",
+        from: { path: "^src/([^/]+)/.+" },
+        to: { viaSomeNot: "^src/$1/.+", circular: true },
+      },
+    ],
+  };
+
+  it("flags when all of the cycle (except the root) is outside the group-matched viaSomeNot", () => {
+    expect(
+      validate.dependency(
+        parseRuleSet(lCycleButNotViaGroupMatchRuleSet),
+        { source: "src/module-a/a.js" },
+        {
+          resolved: "src/module-b/ba.js",
+          circular: true,
+          cycle: [
+            "src/module-b/ba.js",
+            "src/module-b/bb.js",
+            "src/module-b/bc.js",
+            "src/module-a/a.js",
+          ],
+        }
+      )
+    ).to.deep.equal({
+      valid: false,
+      rules: [
+        {
+          name: "no-circular-dependency-of-modules",
+          severity: "warn",
+        },
+      ],
+    });
+  });
+
+  it("flags when only one of the cycle is outside the group-matched viaNot", () => {
+    expect(
+      validate.dependency(
+        parseRuleSet(lCycleButNotViaGroupMatchRuleSet),
+        { source: "src/module-a/a.js" },
+        {
+          resolved: "src/module-a/aa.js",
+          circular: true,
+          cycle: [
+            "src/module-a/aa.js",
+            "src/module-a/ab.js",
+            "src/module-b/bc.js",
+            "src/module-a/a.js",
+          ],
+        }
+      )
+    ).to.deep.equal({
+      valid: false,
+      rules: [
+        {
+          name: "no-circular-dependency-of-modules",
+          severity: "warn",
+        },
+      ],
+    });
+  });
+
+  it("does not flag when all of the cycle is inside the group-matched viaNot", () => {
+    expect(
+      validate.dependency(
+        parseRuleSet(lCycleButNotViaGroupMatchRuleSet),
+        { source: "src/module-a/a.js" },
+        {
+          resolved: "src/module-a/aa.js",
+          circular: true,
+          cycle: [
+            "src/module-a/aa.js",
+            "src/module-a/ab.js",
+            "src/module-a/ac.js",
+            "src/module-a/a.js",
+          ],
+        }
+      )
+    ).to.deep.equal({
+      valid: true,
+    });
+  });
+
+  it("does not flag when all of the cycle is inside the group-matched viaSomeNot that's represented as an array", () => {
+    const lRuleSet = {
+      forbidden: [
+        {
+          name: "no-circular-dependency-of-modules",
+          from: { path: "^src/([^/]+)/.+" },
+          to: {
+            viaSomeNot: ["something", "^src/$1/.+", "somethingelse"],
+            circular: true,
+          },
+        },
+      ],
+    };
+    expect(
+      validate.dependency(
+        parseRuleSet(lRuleSet),
+        { source: "src/module-a/a.js" },
+        {
+          resolved: "src/module-a/aa.js",
+          circular: true,
+          cycle: [
+            "src/module-a/aa.js",
+            "src/module-a/ab.js",
+            "src/module-a/ac.js",
+            "src/module-a/a.js",
+          ],
+        }
+      )
+    ).to.deep.equal({
+      valid: true,
+    });
+  });
+});
diff --git a/test/validate/index.cycle-via.spec.mjs b/test/validate/index.cycle-via.spec.mjs
index 4aa3560de..d38c6445f 100644
--- a/test/validate/index.cycle-via.spec.mjs
+++ b/test/validate/index.cycle-via.spec.mjs
@@ -228,8 +228,7 @@ describe("[I] validate/index dependency - cycle viaOnly", () => {
         }
       )
     ).to.deep.equal({
-      valid: false,
-      rules: [{ name: "unnamed", severity: "warn" }],
+      valid: true,
     });
   });
 
@@ -320,8 +319,7 @@ describe("[I] validate/index dependency - cycle viaOnly - with group matching",
         }
       )
     ).to.deep.equal({
-      valid: false,
-      rules: [{ name: "unnamed", severity: "warn" }],
+      valid: true,
     });
   });