Repository: javierbrea/eslint-plugin-boundaries
Pull request #204 (2022-02-22 17:56:13)

no-external rule custom error messages
### Added
- feat(#170): Support custom messages in `no-external` rule config

### Changed
- docs: Minor changes to contributing guidelines
- chore(deps): Update devDependencies


Patch:
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index f3b5582..162688b 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -62,10 +62,7 @@ This document may also be subject to pull-requests or changes by contributors wh
 
 ### Git Commit Messages
 
-* Use the present tense ("Add feature" not "Added feature")
-* Use the imperative mood ("Move cursor to..." not "Moves cursor to...")
-* Limit the first line to 72 characters or less
-* Reference issues and pull requests liberally after the first line
+* Use [semmantic commit](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716)
 
 ### JavaScript Styleguide
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a41885..91dd20a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 ### Removed
 ### BREAKING CHANGES
 
+## [unreleased]
+
+### Added
+- feat(#170): Support custom messages in `no-external` rule config
+
+### Changed
+- docs: Minor changes to contributing guidelines
+- chore(deps): Update devDependencies
+
 ## [2.8.0] - 2021-11-28
 
 ### Added
diff --git a/README.md b/README.md
index 9ca8dcb..2763167 100644
--- a/README.md
+++ b/README.md
@@ -259,9 +259,9 @@ Some rules require extra configuration, and it has to be defined in each specifi
 
 #### Main format of rules options
 
-The docs of each rule contains an specification of their own options, but __the main rules share the format in which the options have to be defined__. The format described here is valid for options of [`element-types`](docs/rules/element-types.md), [`external`](docs/rules/external.md) and [`entry-point`](docs/rules/entry-point.md) rules, __except the `message` property, which for the moment is only supported in the [`element-types`](docs/rules/element-types.md), [`entry-point`](docs/rules/entry-point.md) and [`no-private`](docs/rules/no-private.md) rules settings.
+The docs of each rule contains an specification of their own options, but __the main rules share the format in which the options have to be defined__. The format described here is valid for options of [`element-types`](docs/rules/element-types.md), [`external`](docs/rules/external.md) and [`entry-point`](docs/rules/entry-point.md) rules.
 
-Options set an "allow/disallow" value by default, and provide an array of rules. Each matching rule will override the default value and the value returned by previous matching rules. So, the final result of the options, once processed for each case, will be "allow" or "disallow", and this value will be applied by the plugin rule in the correspondant way, making it to produce an eslint error or not.
+Options set an `allow` or `disallow` value by default, and provide an array of rules. Each matching rule will override the default value and the value returned by previous matching rules. So, the final result of the options, once processed for each case, will be `allow` or `disallow`, and this value will be applied by the plugin rule in the correspondant way, making it to produce an eslint error or not.
 
 ```jsonc
 {
@@ -331,12 +331,20 @@ Available properties in error templates both from `file` or `dependency` are:
 
 * `type`: Element's type.
 * `internalPath`: File path being analyzed or imported. Relative to the element's root path.
+* `source`: Available only for `dependency`. The source of the `import` statement as it is in the code.
 * `parent`: If the element is child of another element, it is also available in this property, which contains correspondent `type`, `internalPath` and captured properties as well.
 * ...All captured properties are also available
 
-
 > Tip: Read ["Global settings"](#global-settings) for further info about how to capture values from elements.
 
+Some rules also provide extra information about the reported error. For example, `no-external` rules provides information about detected forbidden specifiers. This information is available using `${report.PROPERTY}`. Check each rule's documentation to know which report properties it provides:
+
+```jsonc
+{
+  "message": "Do not import ${report.specifiers} from ${dependency.source} in helpers"
+}
+```
+
 ##### Advanced example of a rule configuration
 
 Just to illustrate the high level of customization that the plugin supports, here is an example of advanced options for the `boundaries/element-types` rule based on the previous global `elements` settings example:
diff --git a/docs/rules/external.md b/docs/rules/external.md
index 2336536..a2c69d4 100644
--- a/docs/rules/external.md
+++ b/docs/rules/external.md
@@ -14,10 +14,12 @@ It checks `import` statements to external modules and allow or disallow them bas
 
 * `enabled`: for enabling the rule. 0=off, 1=warn, 2=error.
 * `default`: `allow` or `disallow`. If no one `rule` matches, the external dependency will be allowed or disallowed based on this value.
+* `message`: Custom message for the rule errors. Note that __the rule default message provides a lot of information about why the error was produced__, so you should define a custom message only if you are sure about what you are doing. Read ["error messages"](#error-messages) for further information.
 * `rules`: Rules to be processed in order to decide if the `import` statement has to be allowed or not.
   * `from`: `<element matchers>` If the file being analyzed matches with this, then the rule will be executed to know if it allows/disallows the `import`. If not, the rule is skipped.
   * `disallow`: `<external modules matchers>` If the element being imported matches with this, then the result of the rule will be "disallow", and the import will be notified as an `eslint` error (this value can be overwritten by a next rule returning "allow")
   * `allow`: `<external modules matchers>` If the element being imported matches with this, then the result of the rule will be "allow", and the import will not be notified as an `eslint` error (this value can be overwritten by a next rule returning "disallow")
+  * `message`: `<string>` Custom error message only for this rule. Read ["error messages"](#error-messages) for further info.
 
 ##### External modules matchers
 
@@ -25,7 +27,7 @@ It checks `import` statements to external modules and allow or disallow them bas
 
 * `<string>`. A [`micromatch` pattern](https://github.com/micromatch/micromatch) to match the name of the module.
 * `[<string>, <object>]`. An array containing a [`micromatch` pattern](https://github.com/micromatch/micromatch) as first element, and an options object, which can have next properties:
-  * `specifiers`: `<array>` Array of used specifiers when importing the library. Each specifier can be expressed also as a [`micromatch` pattern](https://github.com/micromatch/micromatch).
+  * `specifiers`: `<array>` Array of used specifiers when importing the library. Each specifier can be expressed also as a [`micromatch` pattern](https://github.com/micromatch/micromatch). Matching specifiers are available as `${report.specifiers}` when defining custom error messages.
 
 If `specifiers` option is provided, then it will only match if any of the specifiers is used in the `import` statement.
 
@@ -232,7 +234,16 @@ _Modules can import `useHistory` from `react-router-dom`:_
 import { useHistory } from 'react-router-dom'
 ```
 
+### Error messages
+
+This rule provides a lot of information about the specific option producing an error, so the user can have enough context to solve it.
+
+* If the error is produced because all imports are disallowed by default, and no rule is specificly allowing it, then the message provides information about the file and the external dependency: `No rule allows the usage of external module 'react' in elements of type 'helper'`.
+* If the error is produced by a specific option, then the message includes information about the option producing it: `Usage of external module 'react' is not allowed in elements of type 'helper' with elementName 'helper-a'. Disallowed in rule 2`
+* If the error is produced by a specific option including specifiers property, then the message includes information it: `Usage of 'useMemo, useEffect' from external module 'react' is not allowed in elements of type 'helper' with elementName 'helper-a'. Disallowed in rule 2`
+
+You can also configure a custom error message for changing this default behaviour, or even custom error messages only for a specific rule option. Read ["error messages"](../../README.md#error-messages) in the main docs for further info about how to configure messages.
+
 ## Further reading
 
 Read [how to configure the `boundaries/elements` setting](../../README.md#global-settings) to assign an element type to each project's file.
-
diff --git a/src/helpers/messages.js b/src/helpers/messages.js
index e32ea14..23c7e27 100644
--- a/src/helpers/messages.js
+++ b/src/helpers/messages.js
@@ -80,10 +80,15 @@ function ruleElementMessage(elementPatterns, elementCapturedValues) {
 }
 
 function elementPropertiesToReplaceInTemplate(element) {
-  return { ...element.capturedValues, type: element.type, internalPath: element.internalPath };
+  return {
+    ...element.capturedValues,
+    type: element.type,
+    internalPath: element.internalPath,
+    source: element.source,
+  };
 }
 
-function customErrorMessage(message, file, dependency) {
+function customErrorMessage(message, file, dependency, report = {}) {
   let replacedMessage = replaceObjectValuesInTemplates(
     replaceObjectValuesInTemplates(message, elementPropertiesToReplaceInTemplate(file), "file"),
     elementPropertiesToReplaceInTemplate(dependency),
@@ -103,7 +108,7 @@ function customErrorMessage(message, file, dependency) {
       "dependency.parent"
     );
   }
-  return replacedMessage;
+  return replaceObjectValuesInTemplates(replacedMessage, report, "report");
 }
 
 function elementCapturedValuesMessage(capturedValues) {
diff --git a/src/helpers/validations.js b/src/helpers/validations.js
index 1454ac5..107c98a 100644
--- a/src/helpers/validations.js
+++ b/src/helpers/validations.js
@@ -42,12 +42,15 @@ function elementsMatcherSchema(matcherOptions = DEFAULT_MATCHER_OPTIONS) {
   };
 }
 
-function rulesOptionsSchema(options) {
+function rulesOptionsSchema(options = {}) {
   const mainKey = rulesMainKey(options.rulesMainKey);
-  const schema = [
+  return [
     {
       type: "object",
       properties: {
+        message: {
+          type: "string",
+        },
         default: {
           type: "string",
           enum: ["allow", "disallow"],
@@ -60,6 +63,9 @@ function rulesOptionsSchema(options) {
               [mainKey]: elementsMatcherSchema(),
               allow: elementsMatcherSchema(options.targetMatcherOptions),
               disallow: elementsMatcherSchema(options.targetMatcherOptions),
+              message: {
+                type: "string",
+              },
             },
             additionalProperties: false,
             anyOf: [
@@ -79,17 +85,6 @@ function rulesOptionsSchema(options) {
       additionalProperties: false,
     },
   ];
-  if (options.customMessage) {
-    schema[0].properties.message = {
-      type: "string",
-    };
-  }
-  if (options.customRuleMessage) {
-    schema[0].properties.rules.items.properties.message = {
-      type: "string",
-    };
-  }
-  return schema;
 }
 
 function isValidElementTypesMatcher(matcher, settings) {
diff --git a/src/rules/element-types.js b/src/rules/element-types.js
index a5a4373..0582fb9 100644
--- a/src/rules/element-types.js
+++ b/src/rules/element-types.js
@@ -42,10 +42,7 @@ module.exports = dependencyRule(
   {
     ruleName: RULE_ELEMENT_TYPES,
     description: `Check allowed dependencies between element types`,
-    schema: rulesOptionsSchema({
-      customMessage: true,
-      customRuleMessage: true,
-    }),
+    schema: rulesOptionsSchema(),
   },
   function ({ dependency, file, node, context, options }) {
     if (dependency.isLocal && !dependency.isIgnored && dependency.type && !dependency.isInternal) {
diff --git a/src/rules/entry-point.js b/src/rules/entry-point.js
index 0979006..b53c27a 100644
--- a/src/rules/entry-point.js
+++ b/src/rules/entry-point.js
@@ -46,8 +46,6 @@ module.exports = dependencyRule(
     description: `Check entry point used for each element type`,
     schema: rulesOptionsSchema({
       rulesMainKey: "target",
-      customMessage: true,
-      customRuleMessage: true,
     }),
   },
   function ({ dependency, file, node, context, options }) {
diff --git a/src/rules/external.js b/src/rules/external.js
index c47e8cb..f42f208 100644
--- a/src/rules/external.js
+++ b/src/rules/external.js
@@ -6,6 +6,7 @@ const dependencyRule = require("../rules-factories/dependency-rule");
 
 const { rulesOptionsSchema } = require("../helpers/validations");
 const { dependencyLocation, elementRulesAllowDependency } = require("../helpers/rules");
+const { customErrorMessage, ruleElementMessage, elementMessage } = require("../helpers/messages");
 
 function specifiersMatch(specifiers, options) {
   const importedSpecifiersNames = specifiers
@@ -44,6 +45,32 @@ function elementRulesAllowExternalDependency(element, dependency, options) {
   });
 }
 
+function errorMessage(ruleData, file, dependency) {
+  const ruleReport = ruleData.ruleReport;
+  if (ruleReport.message) {
+    return customErrorMessage(ruleReport.message, file, dependency, {
+      specifiers: ruleData.report && ruleData.report.join(", "),
+    });
+  }
+  if (ruleReport.isDefault) {
+    return `No rule allows the usage of external module '${
+      dependency.baseModule
+    }' in elements ${elementMessage(file)}`;
+  }
+
+  const fileReport = `is not allowed in ${ruleElementMessage(
+    ruleReport.element,
+    file.capturedValues
+  )}. Disallowed in rule ${ruleReport.index + 1}`;
+
+  if (ruleData.report) {
+    return `Usage of '${ruleData.report.join(", ")}' from external module '${
+      dependency.baseModule
+    }' ${fileReport}`;
+  }
+  return `Usage of external module '${dependency.baseModule}' ${fileReport}`;
+}
+
 module.exports = dependencyRule(
   {
     ruleName: RULE_EXTERNAL,
@@ -65,27 +92,17 @@ module.exports = dependencyRule(
   },
   function ({ dependency, file, node, context, options }) {
     if (dependency.isExternal) {
-      const isAllowed = elementRulesAllowExternalDependency(
+      const ruleData = elementRulesAllowExternalDependency(
         file,
         { ...dependency, specifiers: node.source.parent.specifiers },
         options
       );
-      if (!isAllowed.result) {
-        if (isAllowed.report) {
-          context.report({
-            message: `Usage of '${isAllowed.report.join(", ")}' from external module '${
-              dependency.baseModule
-            }' is not allowed in '${file.type}'`,
-            node: node,
-            ...dependencyLocation(node, context),
-          });
-        } else {
-          context.report({
-            message: `Usage of external module '${dependency.baseModule}' is not allowed in '${file.type}'`,
-            node: node,
-            ...dependencyLocation(node, context),
-          });
-        }
+      if (!ruleData.result) {
+        context.report({
+          message: errorMessage(ruleData, file, dependency),
+          node: node,
+          ...dependencyLocation(node, context),
+        });
       }
     }
   },


Test patch:
diff --git a/test/rules/docs-examples/external.spec.js b/test/rules/docs-examples/external.spec.js
index e8c2dfd..8d58e05 100644
--- a/test/rules/docs-examples/external.spec.js
+++ b/test/rules/docs-examples/external.spec.js
@@ -1,13 +1,11 @@
 const { EXTERNAL: RULE } = require("../../../src/constants/rules");
 const { SETTINGS, createRuleTester, pathResolvers } = require("../../support/helpers");
+const { externalNoRuleMessage } = require("../../support/messages");
 
 const rule = require(`../../../src/rules/${RULE}`);
 
 const { absoluteFilePath } = pathResolvers("docs-examples");
 
-const errorMessage = (elementType, dependencyName) =>
-  `Usage of external module '${dependencyName}' is not allowed in '${elementType}'`;
-
 const settings = SETTINGS.docsExamples;
 
 const options = [
@@ -96,7 +94,25 @@ ruleTester.run(RULE, rule, {
       options,
       errors: [
         {
-          message: errorMessage("helpers", "react"),
+          message: externalNoRuleMessage({
+            file: "'helpers' with category 'data' and elementName 'parse'",
+            dep: "react",
+          }),
+          type: "ImportDeclaration",
+        },
+      ],
+    },
+    // Helpers can't import specifier from react
+    {
+      filename: absoluteFilePath("helpers/data/parse.js"),
+      code: "import { useMemo } from 'react'",
+      options,
+      errors: [
+        {
+          message: externalNoRuleMessage({
+            file: "'helpers' with category 'data' and elementName 'parse'",
+            dep: "react",
+          }),
           type: "ImportDeclaration",
         },
       ],
@@ -108,7 +124,10 @@ ruleTester.run(RULE, rule, {
       options,
       errors: [
         {
-          message: errorMessage("components", "moment"),
+          message: externalNoRuleMessage({
+            file: "'components' with family 'atoms' and elementName 'atom-a'",
+            dep: "moment",
+          }),
           type: "ImportDeclaration",
         },
       ],
@@ -120,7 +139,8 @@ ruleTester.run(RULE, rule, {
       options,
       errors: [
         {
-          message: errorMessage("components", "@material-ui/icons"),
+          message:
+            "Usage of external module '@material-ui/icons' is not allowed in elements of type 'components' with family 'molecules'. Disallowed in rule 3",
           type: "ImportDeclaration",
         },
       ],
@@ -132,7 +152,10 @@ ruleTester.run(RULE, rule, {
       options,
       errors: [
         {
-          message: errorMessage("modules", "react-router-dom"),
+          message: externalNoRuleMessage({
+            file: "'modules' with elementName 'module-a'",
+            dep: "react-router-dom",
+          }),
           type: "ImportDeclaration",
         },
       ],
diff --git a/test/rules/one-level/external.spec.js b/test/rules/one-level/external.spec.js
index 7c28e4c..6839fae 100644
--- a/test/rules/one-level/external.spec.js
+++ b/test/rules/one-level/external.spec.js
@@ -1,22 +1,16 @@
 const { EXTERNAL: RULE } = require("../../../src/constants/rules");
 const { SETTINGS, createRuleTester, pathResolvers } = require("../../support/helpers");
+const { customErrorMessage, externalNoRuleMessage } = require("../../support/messages");
 
 const rule = require(`../../../src/rules/${RULE}`);
 
 const { absoluteFilePath, codeFilePath } = pathResolvers("one-level");
 
-const errorMessage = (elementType, dependencyName) =>
-  `Usage of external module '${dependencyName}' is not allowed in '${elementType}'`;
-
-const destructuredErrorMessage = (elementType, imported, dependencyName) =>
-  `Usage of '${imported}' from external module '${dependencyName}' is not allowed in '${elementType}'`;
-
-const test = (settings, options, specifiers) => {
-  const failingSpecifiers = specifiers || ["Link", "Router"];
+const test = (settings, options, errorMessages) => {
   const ruleTester = createRuleTester(settings);
   ruleTester.run(RULE, rule, {
     valid: [
-      // Non recognized types can import whatever
+      // Non recognized types can import anything
       {
         filename: absoluteFilePath("foo/index.js"),
         code: "import React from 'react'",
@@ -27,7 +21,7 @@ const test = (settings, options, specifiers) => {
         filename: absoluteFilePath("components/component-a/ComponentA.js"),
         code: "import { withRouter } from 'react-router-dom'",
       },
-      // Ignored files can import whatever
+      // Ignored files can import anything
       {
         filename: absoluteFilePath("components/component-a/ComponentA.js"),
         code: "import { withRouter } from 'react-router-dom'",
@@ -100,7 +94,14 @@ const test = (settings, options, specifiers) => {
         options,
         errors: [
           {
-            message: errorMessage("helpers", "react"),
+            message: customErrorMessage(
+              errorMessages,
+              0,
+              externalNoRuleMessage({
+                file: "'helpers' with elementName 'helper-a'",
+                dep: "react",
+              })
+            ),
             type: "ImportDeclaration",
           },
         ],
@@ -112,7 +113,14 @@ const test = (settings, options, specifiers) => {
         options,
         errors: [
           {
-            message: errorMessage("components", "react-router-dom"),
+            message: customErrorMessage(
+              errorMessages,
+              1,
+              externalNoRuleMessage({
+                file: "'components' with elementName 'component-a'",
+                dep: "react-router-dom",
+              })
+            ),
             type: "ImportDeclaration",
           },
         ],
@@ -124,7 +132,14 @@ const test = (settings, options, specifiers) => {
         options,
         errors: [
           {
-            message: destructuredErrorMessage("helpers", failingSpecifiers[0], "foo-library"),
+            message: customErrorMessage(
+              errorMessages,
+              2,
+              externalNoRuleMessage({
+                file: "'helpers' with elementName 'helper-a'",
+                dep: "foo-library",
+              })
+            ),
             type: "ImportDeclaration",
           },
         ],
@@ -136,7 +151,14 @@ const test = (settings, options, specifiers) => {
         options,
         errors: [
           {
-            message: destructuredErrorMessage("helpers", failingSpecifiers[0], "foo-library"),
+            message: customErrorMessage(
+              errorMessages,
+              3,
+              externalNoRuleMessage({
+                file: "'helpers' with elementName 'helper-a'",
+                dep: "foo-library",
+              })
+            ),
             type: "ImportDeclaration",
           },
         ],
@@ -148,7 +170,14 @@ const test = (settings, options, specifiers) => {
         options,
         errors: [
           {
-            message: destructuredErrorMessage("helpers", failingSpecifiers[0], "foo-library"),
+            message: customErrorMessage(
+              errorMessages,
+              4,
+              externalNoRuleMessage({
+                file: "'helpers' with elementName 'helper-a'",
+                dep: "foo-library",
+              })
+            ),
             type: "ImportDeclaration",
           },
         ],
@@ -160,7 +189,14 @@ const test = (settings, options, specifiers) => {
         options,
         errors: [
           {
-            message: destructuredErrorMessage("helpers", failingSpecifiers[0], "foo-library"),
+            message: customErrorMessage(
+              errorMessages,
+              5,
+              externalNoRuleMessage({
+                file: "'helpers' with elementName 'helper-a'",
+                dep: "foo-library",
+              })
+            ),
             type: "ImportDeclaration",
           },
         ],
@@ -172,10 +208,13 @@ const test = (settings, options, specifiers) => {
         options,
         errors: [
           {
-            message: destructuredErrorMessage(
-              "helpers",
-              `${failingSpecifiers[0]}, ${failingSpecifiers[1]}`,
-              "foo-library"
+            message: customErrorMessage(
+              errorMessages,
+              6,
+              externalNoRuleMessage({
+                file: "'helpers' with elementName 'helper-a'",
+                dep: "foo-library",
+              })
             ),
             type: "ImportDeclaration",
           },
@@ -188,7 +227,14 @@ const test = (settings, options, specifiers) => {
         options,
         errors: [
           {
-            message: errorMessage("modules", "@material-ui/core"),
+            message: customErrorMessage(
+              errorMessages,
+              7,
+              externalNoRuleMessage({
+                file: "'modules' with elementName 'module-a'",
+                dep: "@material-ui/core",
+              })
+            ),
             type: "ImportDeclaration",
           },
         ],
@@ -197,7 +243,7 @@ const test = (settings, options, specifiers) => {
   });
 };
 
-const testCapture = (settings, options) => {
+const testCapture = (settings, options, errorMessages) => {
   const ruleTester = createRuleTester(settings);
   ruleTester.run(RULE, rule, {
     valid: [
@@ -228,7 +274,14 @@ const testCapture = (settings, options) => {
         options,
         errors: [
           {
-            message: errorMessage("modules", "react-router-dom"),
+            message: customErrorMessage(
+              errorMessages,
+              0,
+              externalNoRuleMessage({
+                file: "'modules'",
+                dep: "react-router-dom",
+              })
+            ),
             type: "ImportDeclaration",
           },
         ],
@@ -240,7 +293,14 @@ const testCapture = (settings, options) => {
         options,
         errors: [
           {
-            message: errorMessage("helpers", "foo-library"),
+            message: customErrorMessage(
+              errorMessages,
+              1,
+              externalNoRuleMessage({
+                file: "'helpers' with elementName 'helper-b'",
+                dep: "foo-library",
+              })
+            ),
             type: "ImportDeclaration",
           },
         ],
@@ -252,7 +312,15 @@ const testCapture = (settings, options) => {
         options,
         errors: [
           {
-            message: destructuredErrorMessage("helpers", "Link", "foo-library"),
+            message: customErrorMessage(
+              errorMessages,
+              2,
+              externalNoRuleMessage({
+                file: "'helpers' with elementName 'helper-a'",
+                dep: "foo-library",
+                specifiers: ["Link"],
+              })
+            ),
             type: "ImportDeclaration",
           },
         ],
@@ -262,51 +330,77 @@ const testCapture = (settings, options) => {
 };
 
 // deprecated settings
-test(SETTINGS.deprecated, [
+test(
+  SETTINGS.deprecated,
+  [
+    {
+      default: "allow",
+      rules: [
+        {
+          from: "helpers",
+          disallow: ["react", ["foo-library", { specifiers: ["Link", "Router"] }]],
+        },
+        {
+          from: "components",
+          disallow: ["react-router-dom"],
+        },
+        {
+          from: "modules",
+          disallow: ["@material-ui/core", ["react-router-dom", { specifiers: ["Link"] }]],
+        },
+      ],
+    },
+  ],
   {
-    default: "allow",
-    rules: [
-      {
-        from: "helpers",
-        disallow: ["react", ["foo-library", { specifiers: ["Link", "Router"] }]],
-      },
-      {
-        from: "components",
-        disallow: ["react-router-dom"],
-      },
-      {
-        from: "modules",
-        disallow: ["@material-ui/core", ["react-router-dom", { specifiers: ["Link"] }]],
-      },
-    ],
-  },
-]);
+    0: "Usage of external module 'react' is not allowed in elements of type 'helpers'. Disallowed in rule 1",
+    1: "Usage of external module 'react-router-dom' is not allowed in elements of type 'components'. Disallowed in rule 2",
+    2: "Usage of 'Link' from external module 'foo-library' is not allowed in elements of type 'helpers'. Disallowed in rule 1",
+    3: "Usage of 'Link' from external module 'foo-library' is not allowed in elements of type 'helpers'. Disallowed in rule 1",
+    4: "Usage of 'Link' from external module 'foo-library' is not allowed in elements of type 'helpers'. Disallowed in rule 1",
+    5: "Usage of 'Link' from external module 'foo-library' is not allowed in elements of type 'helpers'. Disallowed in rule 1",
+    6: "Usage of 'Link, Router' from external module 'foo-library' is not allowed in elements of type 'helpers'. Disallowed in rule 1",
+    7: "Usage of external module '@material-ui/core' is not allowed in elements of type 'modules'. Disallowed in rule 3",
+  }
+);
 
 // allow-based options
 
-test(SETTINGS.oneLevel, [
+test(
+  SETTINGS.oneLevel,
+  [
+    {
+      default: "allow",
+      rules: [
+        {
+          from: "helpers",
+          disallow: ["react", ["foo-library", { specifiers: ["Link", "Router"] }]],
+        },
+        {
+          from: "components",
+          disallow: ["react-router-dom"],
+        },
+        {
+          from: "modules",
+          disallow: ["@material-ui/*", ["react-router-dom", { specifiers: ["Link"] }]],
+        },
+        {
+          from: "modules",
+          allow: ["@material-ui/icons"],
+        },
+      ],
+    },
+  ],
   {
-    default: "allow",
-    rules: [
-      {
-        from: "helpers",
-        disallow: ["react", ["foo-library", { specifiers: ["Link", "Router"] }]],
-      },
-      {
-        from: "components",
-        disallow: ["react-router-dom"],
-      },
-      {
-        from: "modules",
-        disallow: ["@material-ui/*", ["react-router-dom", { specifiers: ["Link"] }]],
-      },
-      {
-        from: "modules",
-        allow: ["@material-ui/icons"],
-      },
-    ],
-  },
-]);
+    0: "Usage of external module 'react' is not allowed in elements of type 'helpers'. Disallowed in rule 1",
+    1: "Usage of external module 'react-router-dom' is not allowed in elements of type 'components'. Disallowed in rule 2",
+    2: "Usage of 'Link' from external module 'foo-library' is not allowed in elements of type 'helpers'. Disallowed in rule 1",
+    3: "Usage of 'Link' from external module 'foo-library' is not allowed in elements of type 'helpers'. Disallowed in rule 1",
+    4: "Usage of 'Link' from external module 'foo-library' is not allowed in elements of type 'helpers'. Disallowed in rule 1",
+    5: "Usage of 'Link' from external module 'foo-library' is not allowed in elements of type 'helpers'. Disallowed in rule 1",
+    6: "Usage of 'Link, Router' from external module 'foo-library' is not allowed in elements of type 'helpers'. Disallowed in rule 1",
+    7: "Usage of external module '@material-ui/core' is not allowed in elements of type 'modules'. Disallowed in rule 3",
+  }
+);
 
 // micromatch-based options
 
@@ -335,55 +429,126 @@ test(
       ],
     },
   ],
-  ["L*", "R*"]
+  {
+    0: "Usage of external module 'react' is not allowed in elements of type 'h*'. Disallowed in rule 1",
+    1: "Usage of external module 'react-router-dom' is not allowed in elements of type 'c*'. Disallowed in rule 2",
+    2: "Usage of 'L*' from external module 'foo-library' is not allowed in elements of type 'h*'. Disallowed in rule 1",
+    3: "Usage of 'L*' from external module 'foo-library' is not allowed in elements of type 'h*'. Disallowed in rule 1",
+    4: "Usage of 'L*' from external module 'foo-library' is not allowed in elements of type 'h*'. Disallowed in rule 1",
+    5: "Usage of 'L*' from external module 'foo-library' is not allowed in elements of type 'h*'. Disallowed in rule 1",
+    6: "Usage of 'L*, R*' from external module 'foo-library' is not allowed in elements of type 'h*'. Disallowed in rule 1",
+    7: "Usage of external module '@material-ui/core' is not allowed in elements of type 'm*'. Disallowed in rule 3",
+  }
 );
 
 // disallow-based options
 
-test(SETTINGS.oneLevel, [
+test(
+  SETTINGS.oneLevel,
+  [
+    {
+      default: "disallow",
+      rules: [
+        {
+          from: "helpers",
+          allow: ["foo-library"],
+          disallow: [["foo-library", { specifiers: ["Link", "Router"] }]],
+        },
+        {
+          from: "components",
+          allow: ["react"],
+        },
+        {
+          from: "modules",
+          allow: ["react", "react-router-dom"],
+          disallow: [["react-router-dom", { specifiers: ["Link"] }]],
+        },
+        {
+          from: "modules",
+          allow: ["@material-ui/icons"],
+        },
+      ],
+    },
+  ],
+  {
+    2: "Usage of 'Link' from external module 'foo-library' is not allowed in elements of type 'helpers'. Disallowed in rule 1",
+    3: "Usage of 'Link' from external module 'foo-library' is not allowed in elements of type 'helpers'. Disallowed in rule 1",
+    4: "Usage of 'Link' from external module 'foo-library' is not allowed in elements of type 'helpers'. Disallowed in rule 1",
+    5: "Usage of 'Link' from external module 'foo-library' is not allowed in elements of type 'helpers'. Disallowed in rule 1",
+    6: "Usage of 'Link, Router' from external module 'foo-library' is not allowed in elements of type 'helpers'. Disallowed in rule 1",
+  }
+);
+
+// custom error messages
+
+test(
+  SETTINGS.oneLevel,
+  [
+    {
+      default: "disallow",
+      message:
+        "Importing ${dependency.source} is not allowed in ${file.type} with name ${file.elementName}",
+      rules: [
+        {
+          from: "helpers",
+          allow: ["foo-library"],
+          disallow: [["foo-library", { specifiers: ["Link", "Router"] }]],
+          message: "Do not import ${report.specifiers} from ${dependency.source} in helpers",
+        },
+        {
+          from: "components",
+          allow: ["react"],
+        },
+        {
+          from: "modules",
+          allow: ["react", "react-router-dom"],
+          disallow: [["react-router-dom", { specifiers: ["Link"] }]],
+        },
+        {
+          from: "modules",
+          allow: ["@material-ui/icons"],
+        },
+      ],
+    },
+  ],
   {
-    default: "disallow",
-    rules: [
-      {
-        from: "helpers",
-        allow: ["foo-library"],
-        disallow: [["foo-library", { specifiers: ["Link", "Router"] }]],
-      },
-      {
-        from: "components",
-        allow: ["react"],
-      },
-      {
-        from: "modules",
-        allow: ["react", "react-router-dom"],
-        disallow: [["react-router-dom", { specifiers: ["Link"] }]],
-      },
-      {
-        from: "modules",
-        allow: ["@material-ui/icons"],
-      },
-    ],
-  },
-]);
+    0: "Importing react is not allowed in helpers with name helper-a",
+    1: "Importing react-router-dom is not allowed in components with name component-a",
+    2: "Do not import Link from foo-library in helpers",
+    3: "Do not import Link from foo-library in helpers",
+    4: "Do not import Link from foo-library in helpers",
+    5: "Do not import Link from foo-library in helpers",
+    6: "Do not import Link, Router from foo-library in helpers",
+    7: "Importing @material-ui/core is not allowed in modules with name module-a",
+  }
+);
 
 // options with capture allow-based
 
-testCapture(SETTINGS.oneLevel, [
+testCapture(
+  SETTINGS.oneLevel,
+  [
+    {
+      default: "allow",
+      rules: [
+        {
+          from: [["modules", { elementName: "module-b" }]],
+          disallow: ["react-router-dom"],
+        },
+        {
+          from: [["helpers", { elementName: "helper-b" }]],
+          disallow: ["foo-library"],
+        },
+        {
+          from: [["helpers", { elementName: "helper-a" }]],
+          disallow: [["foo-library", { specifiers: ["Link"] }]],
+        },
+      ],
+    },
+  ],
   {
-    default: "allow",
-    rules: [
-      {
-        from: [["modules", { elementName: "module-b" }]],
-        disallow: ["react-router-dom"],
-      },
-      {
-        from: [["helpers", { elementName: "helper-b" }]],
-        disallow: ["foo-library"],
-      },
-      {
-        from: [["helpers", { elementName: "helper-a" }]],
-        disallow: [["foo-library", { specifiers: ["Link"] }]],
-      },
-    ],
-  },
-]);
+    0: "Usage of external module 'react-router-dom' is not allowed in elements of type 'modules' with elementName 'module-b'. Disallowed in rule 1",
+    1: "Usage of external module 'foo-library' is not allowed in elements of type 'helpers' with elementName 'helper-b'. Disallowed in rule 2",
+    2: "Usage of 'Link' from external module 'foo-library' is not allowed in elements of type 'helpers' with elementName 'helper-a'. Disallowed in rule 3",
+  }
+);
diff --git a/test/rules/two-levels/external.spec.js b/test/rules/two-levels/external.spec.js
index 606af4a..519f5c8 100644
--- a/test/rules/two-levels/external.spec.js
+++ b/test/rules/two-levels/external.spec.js
@@ -1,12 +1,10 @@
 const { EXTERNAL: RULE } = require("../../../src/constants/rules");
 const { SETTINGS, createRuleTester, pathResolvers } = require("../../support/helpers");
+const { customErrorMessage, externalNoRuleMessage } = require("../../support/messages");
 
 const rule = require(`../../../src/rules/${RULE}`);
 
-const errorMessage = (elementType, dependencyName) =>
-  `Usage of external module '${dependencyName}' is not allowed in '${elementType}'`;
-
-const test = (settings, options, { absoluteFilePath }) => {
+const test = (settings, options, { absoluteFilePath }, errorMessages) => {
   const ruleTester = createRuleTester(settings);
 
   ruleTester.run(RULE, rule, {
@@ -91,7 +89,14 @@ const test = (settings, options, { absoluteFilePath }) => {
         options,
         errors: [
           {
-            message: errorMessage("helpers", "react"),
+            message: customErrorMessage(
+              errorMessages,
+              0,
+              externalNoRuleMessage({
+                file: "'helpers' with elementName 'helper-a'",
+                dep: "react",
+              })
+            ),
             type: "ImportDeclaration",
           },
         ],
@@ -103,7 +108,14 @@ const test = (settings, options, { absoluteFilePath }) => {
         options,
         errors: [
           {
-            message: errorMessage("components", "@material-ui/core"),
+            message: customErrorMessage(
+              errorMessages,
+              1,
+              externalNoRuleMessage({
+                file: "'components' with category 'atoms' and elementName 'atom-a'",
+                dep: "@material-ui/core",
+              })
+            ),
             type: "ImportDeclaration",
           },
         ],
@@ -115,7 +127,14 @@ const test = (settings, options, { absoluteFilePath }) => {
         options,
         errors: [
           {
-            message: errorMessage("components", "@material-ui/core"),
+            message: customErrorMessage(
+              errorMessages,
+              2,
+              externalNoRuleMessage({
+                file: "'components' with category 'molecules' and elementName 'molecule-a'",
+                dep: "@material-ui/core",
+              })
+            ),
             type: "ImportDeclaration",
           },
         ],
@@ -127,7 +146,14 @@ const test = (settings, options, { absoluteFilePath }) => {
         options,
         errors: [
           {
-            message: errorMessage("components", "@material-ui/core"),
+            message: customErrorMessage(
+              errorMessages,
+              3,
+              externalNoRuleMessage({
+                file: "'components' with category 'layouts' and elementName 'layout-a'",
+                dep: "@material-ui/core",
+              })
+            ),
             type: "ImportDeclaration",
           },
         ],
@@ -139,7 +165,14 @@ const test = (settings, options, { absoluteFilePath }) => {
         options,
         errors: [
           {
-            message: errorMessage("modules", "react-router-dom"),
+            message: customErrorMessage(
+              errorMessages,
+              4,
+              externalNoRuleMessage({
+                file: "'modules' with domain 'pages' and elementName 'page-a'",
+                dep: "react-router-dom",
+              })
+            ),
             type: "ImportDeclaration",
           },
         ],
@@ -151,7 +184,14 @@ const test = (settings, options, { absoluteFilePath }) => {
         options,
         errors: [
           {
-            message: errorMessage("modules", "react"),
+            message: customErrorMessage(
+              errorMessages,
+              4,
+              externalNoRuleMessage({
+                file: "'modules' with domain 'domain-b' and elementName 'module-b'",
+                dep: "react",
+              })
+            ),
             type: "ImportDeclaration",
           },
         ],
@@ -191,7 +231,8 @@ test(
       ],
     },
   ],
-  pathResolvers("two-levels")
+  pathResolvers("two-levels"),
+  {}
 );
 
 test(
@@ -223,5 +264,6 @@ test(
       ],
     },
   ],
-  pathResolvers("two-levels-with-private")
+  pathResolvers("two-levels-with-private"),
+  {}
 );
diff --git a/test/support/messages.js b/test/support/messages.js
index 8ad14b4..1dd8523 100644
--- a/test/support/messages.js
+++ b/test/support/messages.js
@@ -14,6 +14,10 @@ function entryPointNoRuleMessage({ entryPoint, dep }) {
   return `No rule allows the entry point '${entryPoint}' in dependencies of type ${dep}`;
 }
 
+function externalNoRuleMessage({ file, dep }) {
+  return `No rule allows the usage of external module '${dep}' in elements of type ${file}`;
+}
+
 function noPrivateMessage({ dep }) {
   return `Dependency is private of element of type ${dep}`;
 }
@@ -23,4 +27,5 @@ module.exports = {
   elementTypesNoRuleMessage,
   entryPointNoRuleMessage,
   noPrivateMessage,
+  externalNoRuleMessage,
 };