Repository: sverweij/dependency-cruiser (JavaScript)

Issue:
Feature request: Exclude dependencyTypes option
There was such a issue some time ago (https://github.com/sverweij/dependency-cruiser/issues/490), but at that moment there was no option for identify `type-only` import types. Now, we can make some rules for types that are `type-only`. But it will be nice (relating that issue above) to add option for excluding dependency types.

### Context
A few days ago, new dependency type was added to dependency cruiser - `type-only`. I find very usefull to setup some import restriction except type imports.

For example:
I have a TypeScript module, which is injected to class from outside. I want to prevent developer from reimporting this module, but I want to allow import of type definitions from it.

### Expected Behavior

Something like this:
```
forbidden: [
    {
      name: 'not-to-modules',
      severity: 'error',
      from: {},
      to: {
        path: [
          '^modules',
        ],
        dependencyTypesNot: [ 'type-only' ],
      },
    },
  ]
```

In this case:
```
import Foo from '~/modules/foo'; // forbidden
import type Foo from '~/modules/foo'; // allowed
```




Discussion:
hi @rklos it makes sense to have this - if only from a consistency perspective. I think it would also be possible to use [`allowed`](https://github.com/sverweij/dependency-cruiser/blob/develop/doc/rules-reference.md#allowed) rules to get what you want, but I've found there's a slight learning curve to them.
I tried to use `allowed`, but (maybe I did something wrong or I misunderstood the purpose of `allowed` rules) it doesn't work. None of `allowed` rules overrides `forbidden` rules. Perhaps it is worth adding an example to the documentation on how to get this effect in different ways, or check if there is no bug.
Hi @rklos - that's correct `allowed`, `forbidden` (and `required`) rules are independent of each other. If there's conflicting rules between these sets it's a minor headache to manage them. That said, I agree these can (/should) have better fleshed out examples in the documentation.

I've started adding the dependencyTypesNot restriction you suggested (see linked PR). That'd probably slot in a lot easier with most people's rules setups.
hi @rklos dependency-cruiser@10.5.0 (published this evening) includes the dependencyTypesNot restriction. 

Feedback welcome!



Patch:
diff --git a/doc/rules-reference.md b/doc/rules-reference.md
index 484281ae4..4a3ea56f2 100644
--- a/doc/rules-reference.md
+++ b/doc/rules-reference.md
@@ -38,7 +38,7 @@
    - [rules on dependents - `numberOfDependentsMoreThan`](#rules-on-dependents---numberOfDependentsMoreThan)
    - [`circular`](#circular)
    - [`license` and `licenseNot`](#license-and-licensenot)
-   - [`dependencyTypes`](#dependencytypes)
+   - [`dependencyTypes` and `dependencyTypesNot`](#dependencytypes-and-dependencytypesnot)
    - [`dynamic`](#dynamic)
    - [`moreThanOneDependencyType`](#more-than-one-dependencytype-per-dependency-morethanonedependencytype)
    - [`exoticRequire` and `exoticRequireNot`](#exoticallyrequired-exoticrequire-and-exoticrequirenot)
@@ -778,7 +778,7 @@ for managing your own legal stuff. To re-iterate what is in the
 > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 > SOFTWARE.
 
-### `dependencyTypes`
+### `dependencyTypes` and `dependencyTypesNot`
 
 You might have spent some time wondering why something works on your machine,
 but not on other's. Only to discover you _did_ install a dependency, but
@@ -786,7 +786,7 @@ _did not_ save it to package.json. Or you already had it in your devDependencies
 and started using it in a production source.
 
 To save you from embarrassing moments like this, you can make rules with the
-`dependencyTypes` verb. E.g. to prevent you accidentally depend on a
+`dependencyTypes` restrction. E.g. to prevent you accidentally depend on a
 `devDependency` from anything in `src` add this to your
 .dependency-cruiser.js's "forbidden" section:
 
@@ -814,8 +814,25 @@ Or to detect stuff you npm i'd without putting it in your package.json:
 }
 ```
 
-If you don't specify dependencyTypes in a rule, dependency-cruiser will ignore
-them in the evaluation of that rule.
+Likewise you can use the inverse `dependencyTypesNot` restriction. E.g. to ensure
+type-only imports (e.g. `import type { IYadda } from "./types"`) are used from
+`.d.ts` modules and/ or modules called `types.ts`:
+
+```json
+{
+  "name": "only-type-only",
+  "comment": "use explicit 'type' imports to import from type declaration modules",
+  "severity": "error",
+  "from": {},
+  "to": {
+    "path": ["types\\.ts$", "\\.d\\.ts$"],
+    "dependencyTypesNot": ["type-only"]
+  }
+}
+```
+
+If you don't specify dependencyTypes (or dependencyTypesNot) in a rule, dependency-cruiser
+will ignore them in the evaluation of that rule.
 
 #### OK - `unknown`, `npm-unknown`, `undetermined` - I'm officially weirded out - what's that about?
 
@@ -831,7 +848,7 @@ This is a list of dependency types dependency-cruiser currently detects.
 | npm-peer        | it's a module in package.json's `peerDependencies` - note: deprecated in npm 3                                                                                                | "thing-i-am-a-plugin-for" |
 | npm-bundled     | it's a module that occurs in package.json's `bundle(d)Dependencies` array                                                                                                     | "iwillgetbundled"         |
 | npm-no-pkg      | it's an npm module - but it's nowhere in your package.json                                                                                                                    | "forgetmenot"             |
-| npm-unknown     | it's an npm module - but there is no (parseable/ valid) package.json in your package                                                                                          |
+| npm-unknown     | it's an npm module - but there is no (parseable/ valid) package.json in your package                                                                                          |                           |
 | deprecated      | it's an npm module, but the version you're using or the module itself is officially deprecated                                                                                | "some-deprecated-package" |
 | core            | it's a core module                                                                                                                                                            | "fs"                      |
 | aliased         | it's a module that's linked through an aliased (webpack)                                                                                                                      | "~/hello.ts"              |
diff --git a/src/schema/configuration.schema.js b/src/schema/configuration.schema.js
index c8e2f4427..450a471d2 100644
--- a/src/schema/configuration.schema.js
+++ b/src/schema/configuration.schema.js
@@ -159,6 +159,10 @@ module.exports = {
           type: "array",
           items: { $ref: "#/definitions/DependencyTypeType" },
         },
+        dependencyTypesNot: {
+          type: "array",
+          items: { $ref: "#/definitions/DependencyTypeType" },
+        },
         moreThanOneDependencyType: { type: "boolean" },
         license: { $ref: "#/definitions/REAsStringsType" },
         licenseNot: { $ref: "#/definitions/REAsStringsType" },
diff --git a/src/schema/configuration.schema.json b/src/schema/configuration.schema.json
index 69062be0f..7850f660d 100644
--- a/src/schema/configuration.schema.json
+++ b/src/schema/configuration.schema.json
@@ -222,6 +222,11 @@
           "description": "Whether or not to match modules of any of these types (leaving out matches any of them)",
           "items": { "$ref": "#/definitions/DependencyTypeType" }
         },
+        "dependencyTypesNot": {
+          "type": "array",
+          "description": "Whether or not to match modules NOT of any of these types (leaving out matches none of them)",
+          "items": { "$ref": "#/definitions/DependencyTypeType" }
+        },
         "moreThanOneDependencyType": {
           "type": "boolean",
           "description": "If true matches dependencies with more than one dependency type (e.g. defined in _both_ npm and npm-dev)"
diff --git a/src/schema/cruise-result.schema.js b/src/schema/cruise-result.schema.js
index 240a3e6c9..30bac61b0 100644
--- a/src/schema/cruise-result.schema.js
+++ b/src/schema/cruise-result.schema.js
@@ -331,6 +331,10 @@ module.exports = {
           type: "array",
           items: { $ref: "#/definitions/DependencyTypeType" },
         },
+        dependencyTypesNot: {
+          type: "array",
+          items: { $ref: "#/definitions/DependencyTypeType" },
+        },
         moreThanOneDependencyType: { type: "boolean" },
         license: { $ref: "#/definitions/REAsStringsType" },
         licenseNot: { $ref: "#/definitions/REAsStringsType" },
diff --git a/src/schema/cruise-result.schema.json b/src/schema/cruise-result.schema.json
index ee4d3cad9..cc34c527a 100644
--- a/src/schema/cruise-result.schema.json
+++ b/src/schema/cruise-result.schema.json
@@ -543,6 +543,11 @@
           "description": "Whether or not to match modules of any of these types (leaving out matches any of them)",
           "items": { "$ref": "#/definitions/DependencyTypeType" }
         },
+        "dependencyTypesNot": {
+          "type": "array",
+          "description": "Whether or not to match modules NOT of any of these types (leaving out matches none of them)",
+          "items": { "$ref": "#/definitions/DependencyTypeType" }
+        },
         "moreThanOneDependencyType": {
           "type": "boolean",
           "description": "If true matches dependencies with more than one dependency type (e.g. defined in _both_ npm and npm-dev)"
diff --git a/src/validate/match-dependency-rule.js b/src/validate/match-dependency-rule.js
index c446edcd7..2bb941a9f 100644
--- a/src/validate/match-dependency-rule.js
+++ b/src/validate/match-dependency-rule.js
@@ -32,6 +32,7 @@ function match(pFrom, pTo) {
       matchers.toPath(pRule, pTo, lGroups) &&
       matchers.toPathNot(pRule, pTo, lGroups) &&
       matchers.toDependencyTypes(pRule, pTo) &&
+      matchers.toDependencyTypesNot(pRule, pTo) &&
       matchesMoreThanOneDependencyType(pRule.to, pTo) &&
       matchers.toLicense(pRule, pTo) &&
       matchers.toLicenseNot(pRule, pTo) &&
diff --git a/src/validate/matchers.js b/src/validate/matchers.js
index 87992a28b..d90fad331 100644
--- a/src/validate/matchers.js
+++ b/src/validate/matchers.js
@@ -70,6 +70,13 @@ function toDependencyTypes(pRule, pDependency) {
   );
 }
 
+function toDependencyTypesNot(pRule, pDependency) {
+  return Boolean(
+    !pRule.to.dependencyTypesNot ||
+      !intersects(pDependency.dependencyTypes, pRule.to.dependencyTypesNot)
+  );
+}
+
 function toLicense(pRule, pDependency) {
   return Boolean(
     !pRule.to.license ||
@@ -127,6 +134,7 @@ module.exports = {
   toPathNot,
   toModulePathNot,
   toDependencyTypes,
+  toDependencyTypesNot,
   toLicense,
   toLicenseNot,
   toExoticRequire,
diff --git a/tools/schema/restrictions.mjs b/tools/schema/restrictions.mjs
index d9c7f0975..21973992d 100644
--- a/tools/schema/restrictions.mjs
+++ b/tools/schema/restrictions.mjs
@@ -103,6 +103,15 @@ export default {
             $ref: "#/definitions/DependencyTypeType",
           },
         },
+        dependencyTypesNot: {
+          type: "array",
+          description:
+            "Whether or not to match modules NOT of any of these types (leaving out " +
+            "matches none of them)",
+          items: {
+            $ref: "#/definitions/DependencyTypeType",
+          },
+        },
         moreThanOneDependencyType: {
           type: "boolean",
           description:
diff --git a/types/restrictions.d.ts b/types/restrictions.d.ts
index 93739ae30..e44bd6f52 100644
--- a/types/restrictions.d.ts
+++ b/types/restrictions.d.ts
@@ -71,6 +71,11 @@ export interface IToRestriction extends IBaseRestrictionType {
    * Whether or not to match modules of any of these types (leaving out matches any of them)
    */
   dependencyTypes?: DependencyType[];
+  /**
+   * Whether or not to match modules NOT of any of these types (leaving out
+   * matches none of them)"
+   */
+  dependencyTypesNot?: DependencyType[];
   /**
    * If true matches dependencies with more than one dependency type (e.g. defined in
    * _both_ npm and npm-dev)


Test patch:
diff --git a/test/validate/fixtures/rules.only-to-core.allowed-with-forbidden.json b/test/validate/fixtures/rules.only-to-core.allowed-with-forbidden.json
new file mode 100644
index 000000000..3c106eb85
--- /dev/null
+++ b/test/validate/fixtures/rules.only-to-core.allowed-with-forbidden.json
@@ -0,0 +1,12 @@
+{
+  "forbidden": [
+    {
+      "name": "only-to-core",
+      "severity": "error",
+      "from": {},
+      "to": {
+        "dependencyTypesNot": ["core"]
+      }
+    }
+  ]
+}
diff --git a/test/validate/fixtures/rules.only-to-type-only.allowed-with-forbidden.json b/test/validate/fixtures/rules.only-to-type-only.allowed-with-forbidden.json
new file mode 100644
index 000000000..94fbce636
--- /dev/null
+++ b/test/validate/fixtures/rules.only-to-type-only.allowed-with-forbidden.json
@@ -0,0 +1,12 @@
+{
+  "forbidden": [
+    {
+      "name": "only-to-type-only",
+      "severity": "error",
+      "from": {},
+      "to": {
+        "dependencyTypesNot": ["type-only"]
+      }
+    }
+  ]
+}
diff --git a/test/validate/index.spec.mjs b/test/validate/index.spec.mjs
index 7ef7ee6f2..44256dc31 100644
--- a/test/validate/index.spec.mjs
+++ b/test/validate/index.spec.mjs
@@ -206,6 +206,73 @@ describe("validate/index - specific tests", () => {
     ).to.deep.equal({ valid: true });
   });
 
+  it("only to core - with dependencyTypesNot in forbidden - ok", () => {
+    expect(
+      validate.dependency(
+        readRuleSet(
+          "./test/validate/fixtures/rules.only-to-core.allowed-with-forbidden.json"
+        ),
+        { source: "koos koets" },
+        { resolved: "os", dependencyTypes: ["core"] }
+      )
+    ).to.deep.equal({ valid: true });
+  });
+
+  it("only to core - with dependencyTypesNot in forbidden - nok", () => {
+    expect(
+      validate.dependency(
+        readRuleSet(
+          "./test/validate/fixtures/rules.only-to-core.allowed-with-forbidden.json"
+        ),
+        { source: "koos koets" },
+        { resolved: "robbie kerkhof", dependencyTypes: ["local"] }
+      )
+    ).to.deep.equal({
+      valid: false,
+      rules: [
+        {
+          name: "only-to-core",
+          severity: "error",
+        },
+      ],
+    });
+  });
+
+  it("only to type-only - with dependencyTypesNot in forbidden, multiple types - ok", () => {
+    expect(
+      validate.dependency(
+        readRuleSet(
+          "./test/validate/fixtures/rules.only-to-type-only.allowed-with-forbidden.json"
+        ),
+        { source: "src/koos-koets.ts" },
+        {
+          resolved: "src/robbie-kerkhof.ts",
+          dependencyTypes: ["type-only", "local"],
+        }
+      )
+    ).to.deep.equal({ valid: true });
+  });
+
+  it("only to type-only - with dependencyTypesNot in forbidden, multiple types - nok", () => {
+    expect(
+      validate.dependency(
+        readRuleSet(
+          "./test/validate/fixtures/rules.only-to-type-only.allowed-with-forbidden.json"
+        ),
+        { source: "src/koos-koets.ts" },
+        { resolved: "src/ger-hekking.ts", dependencyTypes: ["local"] }
+      )
+    ).to.deep.equal({
+      valid: false,
+      rules: [
+        {
+          name: "only-to-type-only",
+          severity: "error",
+        },
+      ],
+    });
+  });
+
   it("only to core - via 'allowed' - violation", () => {
     expect(
       validate.dependency(