Repository: not-an-aardvark/eslint-plugin-eslint-plugin
Pull request #101 (2020-06-19 16:54:36)

Add rule `prefer-object-rule`
Adds a simple rule to help enforce that a rule uses object style rule definitions instead of function style.

I wanted this to help me migrate a legacy codebase of ~300 rules.

This rule pairs well and help with the `require-meta-*` rules as it allows their fixers to work.
Currently those rules will report an error, but not fix function style rules.


Patch:
diff --git a/README.md b/README.md
index c1eb471..1e4ae46 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,7 @@ Name | ✔️ | 🛠 | Description
 [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-object-rule](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-object-rule.md) |  | 🛠 | disallow rule exports where the export is a function.
 [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) |  |  | require using replaceText instead of replaceTextRange.
diff --git a/docs/rules/prefer-object-rule.md b/docs/rules/prefer-object-rule.md
new file mode 100644
index 0000000..5ddd6de
--- /dev/null
+++ b/docs/rules/prefer-object-rule.md
@@ -0,0 +1,49 @@
+# Disallow rule exports where the export is a function. (prefer-object-rule)
+
+(fixable) The `--fix` option on the [command line](../user-guide/command-line-interface#fix) automatically fixes problems reported by this rule.
+
+## Rule Details
+
+The rule reports an error if it encounters a rule that's defined using the old style of just a `create` function.
+
+Examples of **incorrect** code for this rule:
+
+```js
+/* eslint eslint-plugin/prefer-object-rule: error */
+
+module.exports = function (context) {
+  return { Program() { context.report() } };
+};
+
+module.exports = function create(context) {
+  return { Program() { context.report() } };
+};
+
+module.exports = (context) => {
+  return { Program() { context.report() } };
+};
+```
+
+Examples of **correct** code for this rule:
+
+```js
+/* eslint eslint-plugin/prefer-object-rule: error */
+
+module.exports = {
+  create(context) {
+    return { Program() { context.report() } };
+  },
+};
+
+module.exports = {
+  create(context) {
+    return { Program() { context.report() } };
+  },
+};
+
+module.exports = {
+  create: (context) => {
+    return { Program() { context.report() } };
+  },
+};
+```
diff --git a/lib/rules/prefer-object-rule.js b/lib/rules/prefer-object-rule.js
new file mode 100644
index 0000000..4cd781a
--- /dev/null
+++ b/lib/rules/prefer-object-rule.js
@@ -0,0 +1,74 @@
+/**
+ * @author Brad Zacher <https://github.com/bradzacher>
+ */
+
+'use strict';
+
+const utils = require('../utils');
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    docs: {
+      description: 'disallow rule exports where the export is a function.',
+      category: 'Rules',
+      recommended: false,
+    },
+    messages: {
+      preferObject: 'Rules should be declared using the object style.',
+    },
+    type: 'suggestion',
+    fixable: 'code',
+    schema: [],
+  },
+
+  create (context) {
+    // ----------------------------------------------------------------------
+    // Public
+    // ----------------------------------------------------------------------
+
+    const sourceCode = context.getSourceCode();
+    const ruleInfo = utils.getRuleInfo(sourceCode.ast);
+
+    return {
+      Program () {
+        if (!ruleInfo || ruleInfo.isNewStyle) {
+          return;
+        }
+
+        context.report({
+          node: ruleInfo.create,
+          messageId: 'preferObject',
+          *fix (fixer) {
+            // note - we intentionally don't worry about formatting here, as otherwise we have
+            //        to indent the function correctly
+
+            if (ruleInfo.create.type === 'FunctionExpression') {
+              const openParenToken = sourceCode.getFirstToken(
+                ruleInfo.create,
+                token => token.type === 'Punctuator' && token.value === '('
+              );
+
+              if (!openParenToken) {
+                // this shouldn't happen, but guarding against crashes just in case
+                return null;
+              }
+
+              yield fixer.replaceTextRange(
+                [ruleInfo.create.range[0], openParenToken.range[0]],
+                '{create'
+              );
+              yield fixer.insertTextAfter(ruleInfo.create, '}');
+            } else if (ruleInfo.create.type === 'ArrowFunctionExpression') {
+              yield fixer.insertTextBefore(ruleInfo.create, '{create: ');
+              yield fixer.insertTextAfter(ruleInfo.create, '}');
+            }
+          },
+        });
+      },
+    };
+  },
+};


Test patch:
diff --git a/tests/lib/rules/prefer-object-rule.js b/tests/lib/rules/prefer-object-rule.js
new file mode 100644
index 0000000..1e48ec2
--- /dev/null
+++ b/tests/lib/rules/prefer-object-rule.js
@@ -0,0 +1,109 @@
+/**
+ * @author Brad Zacher <https://github.com/bradzacher>
+ */
+
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/prefer-object-rule');
+const RuleTester = require('eslint').RuleTester;
+
+const ERROR = { messageId: 'preferObject', line: 2, column: 26 };
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
+ruleTester.run('prefer-object-rule', rule, {
+  valid: [
+    `
+      module.exports = {
+        create(context) {
+          return { Program() { context.report() } };
+        },
+      };
+    `,
+    `
+      module.exports = {
+        create: (context) => {
+          return { Program() { context.report() } };
+        },
+      };
+    `,
+    `
+      module.exports.create = (context) => {
+        return { Program() { context.report() } };
+      };
+    `,
+    `
+      module.exports.create = function (context) {
+        return { Program() { context.report() } };
+      };
+    `,
+    `
+      module.exports.create = function create(context) {
+        return { Program() { context.report() } };
+      };
+    `,
+    `
+      function create(context) {
+        return { Program() { context.report() } };
+      };
+      module.exports.create = create;
+    `,
+    `
+      const rule = {
+        create(context) {
+          return { Program() { context.report() } };
+        },
+      };
+      module.exports = rule;
+    `,
+  ],
+
+  invalid: [
+    {
+      code: `
+        module.exports = function (context) {
+          return { Program() { context.report() } };
+        };
+      `,
+      output: `
+        module.exports = {create(context) {
+          return { Program() { context.report() } };
+        }};
+      `,
+      errors: [ERROR],
+    },
+    {
+      code: `
+        module.exports = function create(context) {
+          return { Program() { context.report() } };
+        };
+      `,
+      output: `
+        module.exports = {create(context) {
+          return { Program() { context.report() } };
+        }};
+      `,
+      errors: [ERROR],
+    },
+    {
+      code: `
+        module.exports = (context) => {
+          return { Program() { context.report() } };
+        };
+      `,
+      output: `
+        module.exports = {create: (context) => {
+          return { Program() { context.report() } };
+        }};
+      `,
+      errors: [ERROR],
+    },
+  ],
+});