Repository: not-an-aardvark/eslint-plugin-eslint-plugin
Pull request #89 (2019-12-05 17:20:44)

Add new rule `require-meta-docs-description`
The eslint core plugin has an internal lint rule to enforce this same thing: https://github.com/eslint/eslint/blob/master/tools/internal-rules/consistent-docs-description.js

bmish: Hey @not-an-aardvark, I've got several PRs open in this repository, for improvements I'd like to make use of in eslint-plugin-ember which I maintain, and which I'd also like to use in the core eslint plugin to replace some of the custom linting they have.
bmish: @not-an-aardvark it seems like an unrelated commit beafe713387f7ef2a6ff844400e231bbbe73c5fb was accidentally added to this PR? That commit was already merged in a separate PR.
not-an-aardvark: Sorry, that's probably my fault (I force-pushed to master to update a commit message.)
bmish: Okay I just rebased to fix it. 

Let me know what you think of the PR and then I'd be interested to get a new release once this is merged.
bmish: @not-an-aardvark ping?

Patch:
diff --git a/README.md b/README.md
index e068161..c1eb471 100644
--- a/README.md
+++ b/README.md
@@ -49,25 +49,26 @@ Then configure the rules you want to use under the rules section.
 <!-- __BEGIN AUTOGENERATED TABLE__ -->
 Name | ✔️ | 🛠 | Description
 ----- | ----- | ----- | -----
-[consistent-output](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/consistent-output.md) |  |  | Enforce consistent use of output assertions in rule tests
-[fixer-return](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/fixer-return.md) | ✔️ |  | Expected fixer function to always return a value.
-[meta-property-ordering](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/meta-property-ordering.md) |  | 🛠 | Enforces the order of meta properties
-[no-deprecated-context-methods](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-deprecated-context-methods.md) |  | 🛠 | Disallows usage of deprecated methods on rule context objects
+[consistent-output](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/consistent-output.md) |  |  | enforce consistent use of output assertions in rule tests
+[fixer-return](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/fixer-return.md) | ✔️ |  | require fixer function to always return a value.
+[meta-property-ordering](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/meta-property-ordering.md) |  | 🛠 | enforce the order of meta properties
+[no-deprecated-context-methods](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-deprecated-context-methods.md) |  | 🛠 | disallow usage of deprecated methods on rule context objects
 [no-deprecated-report-api](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-deprecated-report-api.md) | ✔️ | 🛠 | disallow use of the deprecated context.report() API
 [no-identical-tests](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-identical-tests.md) | ✔️ | 🛠 | disallow identical tests
-[no-missing-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-missing-placeholders.md) | ✔️ |  | Disallow missing placeholders in rule report messages
-[no-unused-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-unused-placeholders.md) | ✔️ |  | Disallow unused placeholders in rule report messages
-[no-useless-token-range](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-useless-token-range.md) | ✔️ | 🛠 | Disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken
-[prefer-output-null](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-output-null.md) |  | 🛠 | disallows invalid RuleTester test cases with the output the same as the code.
+[no-missing-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-missing-placeholders.md) | ✔️ |  | disallow missing placeholders in rule report messages
+[no-unused-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-unused-placeholders.md) | ✔️ |  | disallow unused placeholders in rule report messages
+[no-useless-token-range](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-useless-token-range.md) | ✔️ | 🛠 | disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken
+[prefer-output-null](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-output-null.md) |  | 🛠 | disallow invalid RuleTester test cases with the output the same as the code.
 [prefer-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-placeholders.md) |  |  | disallow template literals as report messages
-[prefer-replace-text](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-replace-text.md) |  |  | prefer using replaceText instead of replaceTextRange.
+[prefer-replace-text](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-replace-text.md) |  |  | require using replaceText instead of replaceTextRange.
 [report-message-format](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/report-message-format.md) |  |  | enforce a consistent format for rule report messages
+[require-meta-docs-description](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-docs-description.md) |  |  | require rules to implement a meta.docs.description property with the correct format
 [require-meta-docs-url](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-docs-url.md) |  | 🛠 | require rules to implement a meta.docs.url property
 [require-meta-fixable](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-fixable.md) | ✔️ |  | require rules to implement a meta.fixable property
 [require-meta-schema](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-schema.md) |  | 🛠 | require rules to implement a meta.schema property
 [require-meta-type](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-type.md) |  |  | require rules to implement a meta.type property
-[test-case-property-ordering](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/test-case-property-ordering.md) |  | 🛠 | Requires the properties of a test case to be placed in a consistent order
-[test-case-shorthand-strings](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/test-case-shorthand-strings.md) |  | 🛠 | Enforce consistent usage of shorthand strings for test cases with no options
+[test-case-property-ordering](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/test-case-property-ordering.md) |  | 🛠 | require the properties of a test case to be placed in a consistent order
+[test-case-shorthand-strings](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/test-case-shorthand-strings.md) |  | 🛠 | enforce consistent usage of shorthand strings for test cases with no options
 <!-- __END AUTOGENERATED TABLE__ -->
 
 ## Supported Presets
diff --git a/docs/rules/require-meta-docs-description.md b/docs/rules/require-meta-docs-description.md
new file mode 100644
index 0000000..24fa29d
--- /dev/null
+++ b/docs/rules/require-meta-docs-description.md
@@ -0,0 +1,47 @@
+# require rules to implement a meta.docs.description property (require-meta-docs-description)
+
+Defining a clear and consistent description for each rule helps developers understand what they're used for.
+
+In particular, each rule description should begin with an allowed prefix:
+* `enforce`
+* `require`
+* `disallow`
+
+## Rule Details
+
+This rule requires ESLint rules to have a valid `meta.docs.description` property.
+
+Examples of **incorrect** code for this rule:
+
+```js
+/* eslint eslint-plugin/require-meta-docs-description: error */
+module.exports = {
+    meta: {},
+    create: function(context) { /* ... */}
+};
+
+module.exports = {
+    meta: { description: 'this rule does ...' }, // missing allowed prefix
+    create: function(context) { /* ... */}
+};
+```
+
+Examples of **correct** code for this rule:
+
+```js
+/* eslint eslint-plugin/require-meta-docs-description: error */
+module.exports = {
+    meta: { description: 'disallow unused variables' },
+    create: function(context) { /* ... */}
+};
+```
+
+## Options
+
+This rule takes an optional object containing:
+
+- `String` — `pattern` — A regular expression that the description must match. Use `'.+'` to allow anything. Defaults to `^(enforce|require|disallow)`.
+
+## Further Reading
+
+* [working-with-rules#options-schemas](https://eslint.org/docs/developer-guide/working-with-rules#options-schemas)
diff --git a/lib/rules/consistent-output.js b/lib/rules/consistent-output.js
index 97bd455..35478e7 100644
--- a/lib/rules/consistent-output.js
+++ b/lib/rules/consistent-output.js
@@ -14,7 +14,7 @@ const utils = require('../utils');
 module.exports = {
   meta: {
     docs: {
-      description: 'Enforce consistent use of output assertions in rule tests',
+      description: 'enforce consistent use of output assertions in rule tests',
       category: 'Tests',
       recommended: false,
     },
diff --git a/lib/rules/fixer-return.js b/lib/rules/fixer-return.js
index 6975d98..8d4898c 100644
--- a/lib/rules/fixer-return.js
+++ b/lib/rules/fixer-return.js
@@ -18,7 +18,7 @@ const utils = require('../utils');
 module.exports = {
   meta: {
     docs: {
-      description: 'Expected fixer function to always return a value.',
+      description: 'require fixer function to always return a value.',
       category: 'Possible Errors',
       recommended: true,
     },
diff --git a/lib/rules/meta-property-ordering.js b/lib/rules/meta-property-ordering.js
index 3d56322..6fb67a5 100644
--- a/lib/rules/meta-property-ordering.js
+++ b/lib/rules/meta-property-ordering.js
@@ -13,7 +13,7 @@ const { getKeyName, getRuleInfo } = require('../utils');
 module.exports = {
   meta: {
     docs: {
-      description: 'Enforces the order of meta properties',
+      description: 'enforce the order of meta properties',
       category: 'Rules',
       recommended: false,
     },
diff --git a/lib/rules/no-deprecated-context-methods.js b/lib/rules/no-deprecated-context-methods.js
index 32f7364..27c5afc 100644
--- a/lib/rules/no-deprecated-context-methods.js
+++ b/lib/rules/no-deprecated-context-methods.js
@@ -37,7 +37,7 @@ const DEPRECATED_PASSTHROUGHS = {
 module.exports = {
   meta: {
     docs: {
-      description: 'Disallows usage of deprecated methods on rule context objects',
+      description: 'disallow usage of deprecated methods on rule context objects',
       category: 'Rules',
       recommended: false,
     },
diff --git a/lib/rules/no-missing-placeholders.js b/lib/rules/no-missing-placeholders.js
index 15dd898..c4cf742 100644
--- a/lib/rules/no-missing-placeholders.js
+++ b/lib/rules/no-missing-placeholders.js
@@ -14,7 +14,7 @@ const utils = require('../utils');
 module.exports = {
   meta: {
     docs: {
-      description: 'Disallow missing placeholders in rule report messages',
+      description: 'disallow missing placeholders in rule report messages',
       category: 'Rules',
       recommended: true,
     },
diff --git a/lib/rules/no-unused-placeholders.js b/lib/rules/no-unused-placeholders.js
index 194a9d7..af7b78d 100644
--- a/lib/rules/no-unused-placeholders.js
+++ b/lib/rules/no-unused-placeholders.js
@@ -14,7 +14,7 @@ const utils = require('../utils');
 module.exports = {
   meta: {
     docs: {
-      description: 'Disallow unused placeholders in rule report messages',
+      description: 'disallow unused placeholders in rule report messages',
       category: 'Rules',
       recommended: true,
     },
diff --git a/lib/rules/no-useless-token-range.js b/lib/rules/no-useless-token-range.js
index 1461f79..ce6f5b6 100644
--- a/lib/rules/no-useless-token-range.js
+++ b/lib/rules/no-useless-token-range.js
@@ -14,7 +14,7 @@ const utils = require('../utils');
 module.exports = {
   meta: {
     docs: {
-      description: 'Disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken',
+      description: 'disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken',
       category: 'Rules',
       recommended: true,
     },
diff --git a/lib/rules/prefer-output-null.js b/lib/rules/prefer-output-null.js
index 89a47aa..8e6c630 100644
--- a/lib/rules/prefer-output-null.js
+++ b/lib/rules/prefer-output-null.js
@@ -14,7 +14,7 @@ const utils = require('../utils');
 module.exports = {
   meta: {
     docs: {
-      description: 'disallows invalid RuleTester test cases with the output the same as the code.',
+      description: 'disallow invalid RuleTester test cases with the output the same as the code.',
       category: 'Tests',
       recommended: false,
     },
diff --git a/lib/rules/prefer-replace-text.js b/lib/rules/prefer-replace-text.js
index 69bc0a0..3b7278b 100644
--- a/lib/rules/prefer-replace-text.js
+++ b/lib/rules/prefer-replace-text.js
@@ -14,7 +14,7 @@ const utils = require('../utils');
 module.exports = {
   meta: {
     docs: {
-      description: 'prefer using replaceText instead of replaceTextRange.',
+      description: 'require using replaceText instead of replaceTextRange.',
       category: 'Rules',
       recommended: false,
     },
diff --git a/lib/rules/require-meta-docs-description.js b/lib/rules/require-meta-docs-description.js
new file mode 100644
index 0000000..dbf7e32
--- /dev/null
+++ b/lib/rules/require-meta-docs-description.js
@@ -0,0 +1,87 @@
+'use strict';
+
+const utils = require('../utils');
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+const DEFAULT_PATTERN = new RegExp('^(enforce|require|disallow)');
+
+/**
+ * Whether or not the node is a string literal
+ *
+ * @param {object} node
+ * @returns {boolean} whether or not the node is a string literal
+ */
+function isStringLiteral (node) {
+  return node.type === 'Literal' && typeof node.value === 'string';
+}
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'require rules to implement a meta.docs.description property with the correct format',
+      category: 'Rules',
+      recommended: false, // TODO: enable it in a major release.
+    },
+    type: 'suggestion',
+    fixable: null,
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          pattern: {
+            type: 'string',
+          },
+        },
+        additionalProperties: false,
+      },
+    ],
+    messages: {
+      missing: '`meta.docs.description` is required.',
+      wrongType: '`meta.docs.description` must be a non-empty string.',
+      extraWhitespace: '`meta.docs.description` must not have leading nor trailing whitespace.',
+    },
+  },
+
+  create (context) {
+    const sourceCode = context.getSourceCode();
+    const info = utils.getRuleInfo(sourceCode.ast, sourceCode.scopeManager);
+
+    return {
+      Program () {
+        if (info === null || info.meta === null) {
+          return;
+        }
+
+        const pattern = context.options[0] && context.options[0].pattern ? new RegExp(context.options[0].pattern) : DEFAULT_PATTERN;
+
+        const metaNode = info.meta;
+        const docsNode =
+          metaNode &&
+          metaNode.properties &&
+          metaNode.properties.find(p => p.type === 'Property' && utils.getKeyName(p) === 'docs');
+
+        const descriptionNode =
+          docsNode &&
+          docsNode.value.properties &&
+          docsNode.value.properties.find(p => p.type === 'Property' && utils.getKeyName(p) === 'description');
+
+        if (!descriptionNode) {
+          context.report({ node: docsNode ? docsNode : metaNode, messageId: 'missing' });
+        } else if (!isStringLiteral(descriptionNode.value) || descriptionNode.value.value === '') {
+          context.report({ node: descriptionNode.value, messageId: 'wrongType' });
+        } else if (descriptionNode.value.value !== descriptionNode.value.value.trim()) {
+          context.report({ node: descriptionNode.value, messageId: 'extraWhitespace' });
+        } else if (!pattern.test(descriptionNode.value.value)) {
+          context.report({
+            node: descriptionNode.value,
+            message: '`meta.docs.description` must match the regexp {{pattern}}.',
+            data: { pattern },
+          });
+        }
+      },
+    };
+  },
+};


Test patch:
diff --git a/lib/rules/test-case-property-ordering.js b/lib/rules/test-case-property-ordering.js
index 5a8280a..f5778bd 100644
--- a/lib/rules/test-case-property-ordering.js
+++ b/lib/rules/test-case-property-ordering.js
@@ -14,7 +14,7 @@ const utils = require('../utils');
 module.exports = {
   meta: {
     docs: {
-      description: 'Requires the properties of a test case to be placed in a consistent order',
+      description: 'require the properties of a test case to be placed in a consistent order',
       category: 'Tests',
       recommended: false,
     },
diff --git a/lib/rules/test-case-shorthand-strings.js b/lib/rules/test-case-shorthand-strings.js
index 9b46a24..edec6d2 100644
--- a/lib/rules/test-case-shorthand-strings.js
+++ b/lib/rules/test-case-shorthand-strings.js
@@ -14,7 +14,7 @@ const utils = require('../utils');
 module.exports = {
   meta: {
     docs: {
-      description: 'Enforce consistent usage of shorthand strings for test cases with no options',
+      description: 'enforce consistent usage of shorthand strings for test cases with no options',
       category: 'Tests',
       recommended: false,
     },
diff --git a/tests/lib/rules/require-meta-docs-description.js b/tests/lib/rules/require-meta-docs-description.js
new file mode 100644
index 0000000..bb5ef8f
--- /dev/null
+++ b/tests/lib/rules/require-meta-docs-description.js
@@ -0,0 +1,150 @@
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/require-meta-docs-description');
+const RuleTester = require('eslint').RuleTester;
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
+ruleTester.run('require-meta-docs-description', rule, {
+  valid: [
+    `
+      module.exports = {
+        meta: { docs: { description: 'disallow unused variables' } },
+        create(context) {}
+      };
+    `,
+    `
+      module.exports = {
+        meta: { docs: { description: 'enforce a maximum line length' } },
+        create(context) {}
+      };
+    `,
+    `
+      module.exports = {
+        meta: { docs: { description: 'require or disallow newline at the end of files' } },
+        create(context) {}
+      };
+    `,
+    {
+      code:
+        `
+          module.exports = {
+            meta: { docs: { description: 'myPrefix foo bar' } },
+            create(context) {}
+          };
+        `,
+      options: [{ pattern: '^myPrefix' }],
+    },
+    {
+      code:
+        `
+          module.exports = {
+            meta: { docs: { description: 'random message' } },
+            create(context) {}
+          };
+        `,
+      options: [{ pattern: '.+' }], // any description allowed
+    },
+  ],
+
+  invalid: [
+    {
+      code: `
+        module.exports = {
+          meta: {},
+          create(context) {}
+        };
+      `,
+      output: null,
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: { docs: {} },
+          create(context) {}
+        };
+      `,
+      output: null,
+      errors: [{ messageId: 'missing', type: 'Property' }],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: { docs: { description: [] } },
+          create(context) {}
+        };
+      `,
+      output: null,
+      errors: [{ messageId: 'wrongType', type: 'ArrayExpression' }],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: { docs: { description: \`enforce with template literal\` } },
+          create(context) {}
+        };
+      `,
+      output: null,
+      errors: [{ messageId: 'wrongType', type: 'TemplateLiteral' }],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: { docs: { description: SOME_DESCRIPTION } },
+          create(context) {}
+        };
+      `,
+      output: null,
+      errors: [{ messageId: 'wrongType', type: 'Identifier' }],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: { docs: { description: '' } },
+          create(context) {}
+        };
+      `,
+      output: null,
+      errors: [{ messageId: 'wrongType', type: 'Literal' }],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: { docs: { description: 'enforce something with trailing whitespace ' } },
+          create(context) {}
+        };
+      `,
+      output: null,
+      errors: [{ messageId: 'extraWhitespace', type: 'Literal' }],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: { docs: { description: 'this rule does ...' } },
+          create(context) {}
+        };
+      `,
+      output: null,
+      errors: [{ message: '`meta.docs.description` must match the regexp /^(enforce|require|disallow)/.', type: 'Literal' }],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: { docs: { description: 'this rule does ...' } },
+          create(context) {}
+        };
+      `,
+      output: null,
+      options: [{ pattern: '^myPrefix' }],
+      errors: [{ message: '`meta.docs.description` must match the regexp /^myPrefix/.', type: 'Literal' }],
+    },
+  ],
+});