Repository: javierbrea/eslint-plugin-boundaries
Pull request #174 (2021-11-28 10:02:55)

Improve messages of no-private rule and support custom
- feat(#171): Support custom messages in `no-private` rule config


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c8ffc7d..4a6e9a7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 ### Removed
 ### BREAKING CHANGES
 
+## [unreleased]
+
+### Added
+- feat(#171): Support custom messages in `no-private` rule config
+
 ## [2.7.0] - 2021-11-27
 
 ### Added
diff --git a/README.md b/README.md
index 0cb8f84..9ca8dcb 100644
--- a/README.md
+++ b/README.md
@@ -259,7 +259,7 @@ 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) and [`entry-point`](docs/rules/entry-point.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, __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.
 
 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.
 
@@ -331,6 +331,7 @@ 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.
+* `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
 
 
diff --git a/docs/rules/element-types.md b/docs/rules/element-types.md
index 0a737d9..84bf99e 100644
--- a/docs/rules/element-types.md
+++ b/docs/rules/element-types.md
@@ -9,7 +9,7 @@ It checks `import` statements between the element types of the project based on
 ### Options
 
 ```
-"boundaries/element-types": [<enabled>, { "default": <string>, "rules": <object> }]
+"boundaries/element-types": [<enabled>, { "default": <string>, "message": <string>, "rules": <object> }]
 ```
 
 * `enabled`: for enabling the rule. 0=off, 1=warn, 2=error.
diff --git a/docs/rules/entry-point.md b/docs/rules/entry-point.md
index 8478a59..2f1e284 100644
--- a/docs/rules/entry-point.md
+++ b/docs/rules/entry-point.md
@@ -9,7 +9,7 @@ It checks `import` statements to the elements of the project and ensure that eac
 ### Options
 
 ```
-"boundaries/entry-point": [<enabled>, { "default": <string>, "rules": <object> }]
+"boundaries/entry-point": [<enabled>, { "default": <string>, "message": <string>, "rules": <object> }]
 ```
 
 * `enabled`: for enabling the rule. 0=off, 1=warn, 2=error.
diff --git a/docs/rules/no-private.md b/docs/rules/no-private.md
index c30d723..d399996 100644
--- a/docs/rules/no-private.md
+++ b/docs/rules/no-private.md
@@ -10,16 +10,17 @@ This rule follow the next principles:
 * Private elements can't be used by anyone except its parent. _(and any other descendant of the parent when `allowUncles` option is enabled)_
 * Private elements can import public elements.
 * Private elements can import another private element when both have the same parent _("brother elements")_
-* Private elements can import anoother private element if it is a direct child of a common ancestor, and the `allowUncles` option is enabled.
+* Private elements can import another private element if it is a direct child of a common ancestor, and the `allowUncles` option is enabled.
 
 ### Options
 
 ```
-"boundaries/no-private": [<enabled>, { "allowUncles": <boolean> }]
+"boundaries/no-private": [<enabled>, { "allowUncles": <boolean>, "message": <string> }]
 ```
 
 * `enabled`: for enabling the rule. 0=off, 1=warn, 2=error.
 * `allowUncles`: Optional. If set to `false`, it disallows importing "uncle elements". Default is `true`.
+* `message`: Custom message for the rule errors. Note that __the rule default message provides enough 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.
 
 ##### Options example
 
@@ -129,6 +130,12 @@ _`module-e` can import `module-d` because it is his uncle_
 import ModuleD from 'modules/module-b/modules/module-d'
 ```
 
+### Error messages
+
+This rule provides information about the parent element of the imported one in case it is private, e.g. `Dependency is private of element of type 'modules' with elementName 'module-b'`
+
+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 23f599e..e32ea14 100644
--- a/src/helpers/messages.js
+++ b/src/helpers/messages.js
@@ -79,12 +79,31 @@ function ruleElementMessage(elementPatterns, elementCapturedValues) {
   return elementMatcherMessage(elementPatterns, elementCapturedValues);
 }
 
+function elementPropertiesToReplaceInTemplate(element) {
+  return { ...element.capturedValues, type: element.type, internalPath: element.internalPath };
+}
+
 function customErrorMessage(message, file, dependency) {
-  return replaceObjectValuesInTemplates(
-    replaceObjectValuesInTemplates(message, { ...file.capturedValues, type: file.type }, "file"),
-    { ...dependency.capturedValues, type: dependency.type, internalPath: dependency.internalPath },
+  let replacedMessage = replaceObjectValuesInTemplates(
+    replaceObjectValuesInTemplates(message, elementPropertiesToReplaceInTemplate(file), "file"),
+    elementPropertiesToReplaceInTemplate(dependency),
     "dependency"
   );
+  if (file.parents[0]) {
+    replacedMessage = replaceObjectValuesInTemplates(
+      replacedMessage,
+      elementPropertiesToReplaceInTemplate(file.parents[0]),
+      "file.parent"
+    );
+  }
+  if (dependency.parents[0]) {
+    replacedMessage = replaceObjectValuesInTemplates(
+      replacedMessage,
+      elementPropertiesToReplaceInTemplate(dependency.parents[0]),
+      "dependency.parent"
+    );
+  }
+  return replacedMessage;
 }
 
 function elementCapturedValuesMessage(capturedValues) {
diff --git a/src/rules/no-private.js b/src/rules/no-private.js
index 0fd9d33..d77605a 100644
--- a/src/rules/no-private.js
+++ b/src/rules/no-private.js
@@ -3,6 +3,14 @@ const { RULE_NO_PRIVATE } = require("../constants/settings");
 const dependencyRule = require("../rules-factories/dependency-rule");
 
 const { dependencyLocation } = require("../helpers/rules");
+const { customErrorMessage, elementMessage } = require("../helpers/messages");
+
+function errorMessage(file, dependency, options) {
+  if (options.message) {
+    return customErrorMessage(options.message, file, dependency);
+  }
+  return `Dependency is private of element ${elementMessage(dependency.parents[0])}`;
+}
 
 module.exports = dependencyRule(
   {
@@ -15,12 +23,15 @@ module.exports = dependencyRule(
           allowUncles: {
             type: "boolean",
           },
+          message: {
+            type: "string",
+          },
         },
         additionalProperties: false,
       },
     ],
   },
-  function ({ dependency, node, context, options }) {
+  function ({ file, dependency, node, context, options }) {
     if (
       !dependency.isIgnored &&
       dependency.isLocal &&
@@ -32,7 +43,7 @@ module.exports = dependencyRule(
       (!options || !options.allowUncles || dependency.relationship !== "uncle")
     ) {
       context.report({
-        message: `Dependency is private of another element`,
+        message: errorMessage(file, dependency, options),
         node: node,
         ...dependencyLocation(node, context),
       });


Test patch:
diff --git a/test/rules/docs-examples/no-private.spec.js b/test/rules/docs-examples/no-private.spec.js
index d35ae75..b52b50f 100644
--- a/test/rules/docs-examples/no-private.spec.js
+++ b/test/rules/docs-examples/no-private.spec.js
@@ -1,13 +1,12 @@
 const { NO_PRIVATE: RULE } = require("../../../src/constants/rules");
 const { SETTINGS, createRuleTester, pathResolvers } = require("../../support/helpers");
+const { noPrivateMessage } = require("../../support/messages");
 
 const rule = require(`../../../src/rules/${RULE}`);
 
 const settings = SETTINGS.docsExamples;
 const { absoluteFilePath } = pathResolvers("docs-examples");
 
-const ERROR_MESSAGE = "Dependency is private of another element";
-
 const options = [
   {
     allowUncles: true,
@@ -51,7 +50,9 @@ ruleTester.run(RULE, rule, {
       options,
       errors: [
         {
-          message: ERROR_MESSAGE,
+          message: noPrivateMessage({
+            dep: "'modules' with elementName 'module-b'",
+          }),
           type: "ImportDeclaration",
         },
       ],
@@ -63,7 +64,9 @@ ruleTester.run(RULE, rule, {
       options,
       errors: [
         {
-          message: ERROR_MESSAGE,
+          message: noPrivateMessage({
+            dep: "'modules' with elementName 'module-c'",
+          }),
           type: "ImportDeclaration",
         },
       ],
@@ -79,7 +82,9 @@ ruleTester.run(RULE, rule, {
       ],
       errors: [
         {
-          message: ERROR_MESSAGE,
+          message: noPrivateMessage({
+            dep: "'modules' with elementName 'module-b'",
+          }),
           type: "ImportDeclaration",
         },
       ],
diff --git a/test/rules/one-level/no-private.spec.js b/test/rules/one-level/no-private.spec.js
index 1d9df7a..4d28424 100644
--- a/test/rules/one-level/no-private.spec.js
+++ b/test/rules/one-level/no-private.spec.js
@@ -1,12 +1,11 @@
 const { NO_PRIVATE: RULE } = require("../../../src/constants/rules");
 const { SETTINGS, createRuleTester, pathResolvers } = require("../../support/helpers");
+const { noPrivateMessage } = require("../../support/messages");
 
 const rule = require(`../../../src/rules/${RULE}`);
 
 const { absoluteFilePath, codeFilePath } = pathResolvers("one-level");
 
-const errorMessage = () => `Dependency is private of another element`;
-
 const options = [
   {
     allowUncles: true,
@@ -116,7 +115,9 @@ const test = (settings) => {
         options,
         errors: [
           {
-            message: errorMessage(),
+            message: noPrivateMessage({
+              dep: "'components' with elementName 'component-c'",
+            }),
             type: "ImportDeclaration",
           },
         ],
@@ -128,7 +129,9 @@ const test = (settings) => {
         options,
         errors: [
           {
-            message: errorMessage(),
+            message: noPrivateMessage({
+              dep: "'components' with elementName 'component-a'",
+            }),
             type: "ImportDeclaration",
           },
         ],
@@ -140,7 +143,9 @@ const test = (settings) => {
         options,
         errors: [
           {
-            message: errorMessage(),
+            message: noPrivateMessage({
+              dep: "'helpers' with elementName 'helper-a'",
+            }),
             type: "ImportDeclaration",
           },
         ],
@@ -152,7 +157,9 @@ const test = (settings) => {
         options,
         errors: [
           {
-            message: errorMessage(),
+            message: noPrivateMessage({
+              dep: "'helpers' with elementName 'helper-a'",
+            }),
             type: "ImportDeclaration",
           },
         ],
@@ -172,7 +179,51 @@ const test = (settings) => {
         ],
         errors: [
           {
-            message: errorMessage(),
+            message: noPrivateMessage({
+              dep: "'components' with elementName 'component-a'",
+            }),
+            type: "ImportDeclaration",
+          },
+        ],
+      },
+      /* Custom message */
+      {
+        filename: absoluteFilePath(
+          "components/component-a/components/component-c/components/component-d/ComponentD.js"
+        ),
+        code: 'import HelperA from "components/component-a/helpers/helper-a"',
+        options: [
+          {
+            allowUncles: false,
+            message:
+              "The element of type '${dependency.type}' with name '${dependency.elementName}' is child of element of type '${dependency.parent.type}' with name '${dependency.parent.elementName}'",
+          },
+        ],
+        errors: [
+          {
+            message:
+              "The element of type 'helpers' with name 'helper-a' is child of element of type 'components' with name 'component-a'",
+            type: "ImportDeclaration",
+          },
+        ],
+      },
+      /* Custom message with file info*/
+      {
+        filename: absoluteFilePath(
+          "components/component-a/components/component-c/components/component-d/ComponentD.js"
+        ),
+        code: 'import HelperA from "components/component-a/helpers/helper-a"',
+        options: [
+          {
+            allowUncles: false,
+            message:
+              "This element is of type '${file.type}' with name '${file.elementName}', and it is child of element of type '${file.parent.type}' with name '${file.parent.elementName}'",
+          },
+        ],
+        errors: [
+          {
+            message:
+              "This element is of type 'components' with name 'component-d', and it is child of element of type 'components' with name 'component-c'",
             type: "ImportDeclaration",
           },
         ],
diff --git a/test/rules/two-levels/no-private.spec.js b/test/rules/two-levels/no-private.spec.js
index 30fc39c..24185cd 100644
--- a/test/rules/two-levels/no-private.spec.js
+++ b/test/rules/two-levels/no-private.spec.js
@@ -1,10 +1,9 @@
 const { NO_PRIVATE: RULE } = require("../../../src/constants/rules");
 const { SETTINGS, createRuleTester, pathResolvers } = require("../../support/helpers");
+const { noPrivateMessage } = require("../../support/messages");
 
 const rule = require(`../../../src/rules/${RULE}`);
 
-const errorMessage = () => `Dependency is private of another element`;
-
 const options = [
   {
     allowUncles: true,
@@ -80,7 +79,9 @@ const test = (settings, { absoluteFilePath }) => {
         options,
         errors: [
           {
-            message: errorMessage(),
+            message: noPrivateMessage({
+              dep: "'components' with category 'molecules' and elementName 'molecule-c'",
+            }),
             type: "ImportDeclaration",
           },
         ],
@@ -91,7 +92,9 @@ const test = (settings, { absoluteFilePath }) => {
         options,
         errors: [
           {
-            message: errorMessage(),
+            message: noPrivateMessage({
+              dep: "'components' with category 'molecules' and elementName 'molecule-a'",
+            }),
             type: "ImportDeclaration",
           },
         ],
@@ -103,7 +106,9 @@ const test = (settings, { absoluteFilePath }) => {
         options,
         errors: [
           {
-            message: errorMessage(),
+            message: noPrivateMessage({
+              dep: "'helpers' with elementName 'helper-a'",
+            }),
             type: "ImportDeclaration",
           },
         ],
@@ -115,7 +120,9 @@ const test = (settings, { absoluteFilePath }) => {
         options,
         errors: [
           {
-            message: errorMessage(),
+            message: noPrivateMessage({
+              dep: "'components' with category 'molecules' and elementName 'molecule-c'",
+            }),
             type: "ImportDeclaration",
           },
         ],
@@ -135,7 +142,9 @@ const test = (settings, { absoluteFilePath }) => {
         ],
         errors: [
           {
-            message: errorMessage(),
+            message: noPrivateMessage({
+              dep: "'components' with category 'molecules' and elementName 'molecule-a'",
+            }),
             type: "ImportDeclaration",
           },
         ],
diff --git a/test/support/messages.js b/test/support/messages.js
index eaf89f6..8ad14b4 100644
--- a/test/support/messages.js
+++ b/test/support/messages.js
@@ -14,8 +14,13 @@ function entryPointNoRuleMessage({ entryPoint, dep }) {
   return `No rule allows the entry point '${entryPoint}' in dependencies of type ${dep}`;
 }
 
+function noPrivateMessage({ dep }) {
+  return `Dependency is private of element of type ${dep}`;
+}
+
 module.exports = {
   customErrorMessage,
   elementTypesNoRuleMessage,
   entryPointNoRuleMessage,
+  noPrivateMessage,
 };