Repository: not-an-aardvark/eslint-plugin-eslint-plugin
Pull request #252 (2022-06-20 02:08:22)

Update `no-missing-placeholders` and `no-unused-placeholders` to handle messageIds
Fixes the second part of #57.

Correct placeholder with messageId example: 
```js
module.exports = {
    meta: {
        messages: { myMessageId: 'foo {{bar}}' } 
    },
    create(context) {
        context.report({
            node,
            messageId: 'myMessageId',
            data: { bar: 'baz' }
        });
    }
};
```

- [x] Implement this
- [x] Ensure it applies to suggestions too
- [x] Refactor/DRY
   - [x] Reuse util functions from https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/pull/254

Part of the V5 release (#230).


Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 35da37d..8e479e6 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -13,10 +13,8 @@ jobs:
       matrix:
         node-version:
           - '18'
-          - '17'
           - '16'
           - '14'
-          - '12'
         os:
           - ubuntu-latest
     steps:
@@ -33,28 +31,17 @@ jobs:
       - uses: actions/checkout@v3
       - uses: actions/setup-node@v3
         with:
-          node-version: "16.x"
+          node-version: "18.x"
       - run: npm install
       - run: npm run lint
 
-  eslint6:
-    runs-on: ubuntu-latest
-    steps:
-    - uses: actions/checkout@v3
-    - uses: actions/setup-node@v3
-      with:
-        node-version: "16.x"
-    - run: npm install
-    - run: npm install --save-dev eslint@6
-    - run: npm test
-      
   eslint7:
     runs-on: ubuntu-latest
     steps:
     - uses: actions/checkout@v3
     - uses: actions/setup-node@v3
       with:
-        node-version: "16.x"
+        node-version: "18.x"
     - run: npm install
     - run: npm install --save-dev eslint@7
     - run: npm test
diff --git a/.npmpackagejsonlintrc.json b/.npmpackagejsonlintrc.json
new file mode 100644
index 0000000..40af081
--- /dev/null
+++ b/.npmpackagejsonlintrc.json
@@ -0,0 +1,15 @@
+{
+  "rules": {
+    "no-duplicate-properties": "error",
+    "no-repeated-dependencies": "error",
+    "prefer-alphabetical-bundledDependencies": "error",
+    "prefer-alphabetical-dependencies": "error",
+    "prefer-alphabetical-devDependencies": "error",
+    "prefer-alphabetical-optionalDependencies": "error",
+    "prefer-alphabetical-scripts": "error",
+    "prefer-caret-version-dependencies": "error",
+    "prefer-caret-version-devDependencies": ["error", {
+      "exceptions": ["eslint-plugin-eslint-plugin"]
+    }]
+  }
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 29bc59e..1e7858f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
 
 
+### [4.4.1](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/compare/v4.4.0...v4.4.1) (2022-07-11)
+
+
+### Bug Fixes
+
+* detect rules with TypeScript `export = rule` syntax ([#267](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/267)) ([8e66bf7](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/8e66bf744448b8a3c64bf32c5ba893d16fa7f3ac))
+
 ## [4.4.0](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/compare/v4.3.0...v4.4.0) (2022-07-02)
 
 
diff --git a/README.md b/README.md
index cbb6bd9..59f09c6 100644
--- a/README.md
+++ b/README.md
@@ -67,15 +67,15 @@ Name | ✔️ | 🛠 | 💡 | Description
 [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 the version of `context.report()` with multiple arguments
 [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-message-ids](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-missing-message-ids.md) |  |  |  | disallow `messageId`s that are missing from `meta.messages`
+[no-missing-message-ids](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-missing-message-ids.md) | ✔️ |  |  | disallow `messageId`s that are missing from `meta.messages`
 [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-only-tests](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-only-tests.md) | ✔️ |  | 💡 | disallow the test case property `only`
-[no-unused-message-ids](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-unused-message-ids.md) |  |  |  | disallow unused `messageId`s in `meta.messages`
+[no-unused-message-ids](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-unused-message-ids.md) | ✔️ |  |  | disallow unused `messageId`s in `meta.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-message-ids](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-message-ids.md) |  |  |  | require using `messageId` instead of `message` to report rule violations
-[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 where the `output` matches the `code`
+[prefer-message-ids](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-message-ids.md) | ✔️ |  |  | require using `messageId` instead of `message` to report rule violations
+[prefer-object-rule](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-object-rule.md) | ✔️ | 🛠 |  | disallow function-style rules
+[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 where the `output` matches the `code`
 [prefer-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-placeholders.md) |  |  |  | require using placeholders for dynamic 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()`
 [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
diff --git a/docs/rules/consistent-output.md b/docs/rules/consistent-output.md
index a6d8858..38fb712 100644
--- a/docs/rules/consistent-output.md
+++ b/docs/rules/consistent-output.md
@@ -2,11 +2,11 @@
 
 ✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule.
 
-When writing tests for fixable rules, it's a best practice to use the `output` property on each test case to assert what autofixed code is produced, or to assert that no autofix is produced using `output: null`.
+When writing tests for fixable rules, the `output` property on each test case can be used to assert what autofixed code is produced, or to assert that no autofix is produced using `output: null`.
 
 Prior to ESLint 7, it was easy to forget to assert the autofix output of a particular test case, resulting in incomplete test coverage and a greater chance of unexpected behavior / bugs.
 
-[As of ESLint 7](https://eslint.org/docs/user-guide/migrating-to-7.0.0#additional-validation-added-to-the-ruletester-class), test cases that trigger an autofix are required to provide the `output` property.
+[As of ESLint 7](https://eslint.org/docs/user-guide/migrating-to-7.0.0#additional-validation-added-to-the-ruletester-class), test cases that trigger an autofix are required by ESLint to provide the `output` property. Thus, it's now acceptable and more concise to omit this property when there's no autofix.
 
 ## Rule Details
 
@@ -71,6 +71,8 @@ This rule takes an optional string enum option with one of the following values:
 
 If you're not writing fixable rules, or you want to write test cases without output assertions, do not enable this rule.
 
+As mentioned in the introduction, the need for this rule is reduced as of ESLint v7.
+
 ## Further Reading
 
 * [`RuleTester` documentation](http://eslint.org/docs/developer-guide/working-with-plugins#testing)
diff --git a/docs/rules/no-missing-message-ids.md b/docs/rules/no-missing-message-ids.md
index 76ee359..268c42b 100644
--- a/docs/rules/no-missing-message-ids.md
+++ b/docs/rules/no-missing-message-ids.md
@@ -1,5 +1,7 @@
 # Disallow `messageId`s that are missing from `meta.messages` (no-missing-message-ids)
 
+✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule.
+
 When using `meta.messages` and `messageId` to report rule violations, it's possible to mistakenly use a `messageId` that doesn't exist in `meta.messages`.
 
 ## Rule Details
diff --git a/docs/rules/no-unused-message-ids.md b/docs/rules/no-unused-message-ids.md
index 758140a..942d33d 100644
--- a/docs/rules/no-unused-message-ids.md
+++ b/docs/rules/no-unused-message-ids.md
@@ -1,5 +1,7 @@
 # Disallow unused `messageId`s in `meta.messages` (no-unused-message-ids)
 
+✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule.
+
 When using `meta.messages` and `messageId` to report rule violations, it's possible to mistakenly leave a message in `meta.messages` that is never used.
 
 ## Rule Details
diff --git a/docs/rules/prefer-message-ids.md b/docs/rules/prefer-message-ids.md
index 4455df2..4f1696c 100644
--- a/docs/rules/prefer-message-ids.md
+++ b/docs/rules/prefer-message-ids.md
@@ -1,5 +1,7 @@
 # Require using `messageId` instead of `message` to report rule violations (prefer-message-ids)
 
+✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule.
+
 When reporting a rule violation, it's preferred to provide the violation message with the `messageId` property instead of the `message` property. Message IDs provide the following benefits:
 
 * Rule violation messages can be stored in a central `meta.messages` object for convenient management
diff --git a/docs/rules/prefer-object-rule.md b/docs/rules/prefer-object-rule.md
index cd853ee..2582d61 100644
--- a/docs/rules/prefer-object-rule.md
+++ b/docs/rules/prefer-object-rule.md
@@ -1,26 +1,22 @@
-# Disallow rule exports where the export is a function (prefer-object-rule)
+# Disallow function-style rules (prefer-object-rule)
 
 ✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule.
 
 ⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule.
 
+Prior to ESLint v9, ESLint supported both [function-style](https://eslint.org/docs/developer-guide/working-with-rules-deprecated) and [object-style](https://eslint.org/docs/developer-guide/working-with-rules) rules. However, function-style rules have been deprecated since 2016, and do not support newer features like autofixing and suggestions.
+
+As of [ESLint v9](https://github.com/eslint/rfcs/tree/main/designs/2021-schema-object-rules#motivation-for-requiring-object-style-rules), ESLint supports only object-style rules.
+
 ## Rule Details
 
-The rule reports an error if it encounters a rule that's defined using the [deprecated style](https://eslint.org/docs/developer-guide/working-with-rules-deprecated) of just a `create` function instead of the newer [object style](https://eslint.org/docs/developer-guide/working-with-rules).
+The rule reports an error if it encounters a rule that's defined using the deprecated function-style format.
 
 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() {
@@ -28,14 +24,6 @@ module.exports = function create(context) {
     },
   };
 };
-
-module.exports = (context) => {
-  return {
-    Program() {
-      context.report();
-    },
-  };
-};
 ```
 
 Examples of **correct** code for this rule:
@@ -44,6 +32,7 @@ Examples of **correct** code for this rule:
 /* eslint eslint-plugin/prefer-object-rule: error */
 
 module.exports = {
+  meta: { /* ... */ },
   create(context) {
     return {
       Program() {
@@ -52,24 +41,4 @@ module.exports = {
     };
   },
 };
-
-module.exports = {
-  create(context) {
-    return {
-      Program() {
-        context.report();
-      },
-    };
-  },
-};
-
-module.exports = {
-  create: (context) => {
-    return {
-      Program() {
-        context.report();
-      },
-    };
-  },
-};
 ```
diff --git a/docs/rules/prefer-output-null.md b/docs/rules/prefer-output-null.md
index 31d6ceb..e5e0a5d 100644
--- a/docs/rules/prefer-output-null.md
+++ b/docs/rules/prefer-output-null.md
@@ -1,5 +1,7 @@
 # Disallow invalid RuleTester test cases where the `output` matches the `code` (prefer-output-null)
 
+✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule.
+
 ⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule.
 
 Instead of repeating the test case `code`, using `output: null` is more concise and makes it easier to distinguish whether a test case provides an autofix.
diff --git a/docs/rules/require-meta-schema.md b/docs/rules/require-meta-schema.md
index ad8b969..c10e2d5 100644
--- a/docs/rules/require-meta-schema.md
+++ b/docs/rules/require-meta-schema.md
@@ -4,7 +4,9 @@
 
 💡 Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
 
-Defining a schema for each rule allows eslint to validate that configuration options are passed correctly. Even when there are no options for a rule, a schema should still be defined (as an empty array) so that eslint can validate that no data is mistakenly passed to the rule.
+Defining a schema for each rule allows eslint to validate that configuration options are passed correctly. Even when there are no options for a rule, a schema can still be defined as an empty array to validate that no data is mistakenly passed to the rule.
+
+As of [ESLint v9](https://github.com/eslint/rfcs/tree/main/designs/2021-schema-object-rules#motivation-for-requiring-schemas), ESLint will validate that options are not provided to a rule when a schema is omitted.
 
 ## Rule Details
 
@@ -75,6 +77,10 @@ This rule takes an optional object containing:
 
 * `boolean` — `requireSchemaPropertyWhenOptionless` — Whether the rule should require the `meta.schema` property to be specified (with `schema: []`) for rules that have no options. Defaults to `true`.
 
+## When Not To Use It
+
+As mentioned in the introduction, the need for this rule is reduced as of ESLint v9.
+
 ## Further Reading
 
 * [working-with-rules#options-schemas](https://eslint.org/docs/developer-guide/working-with-rules#options-schemas)
diff --git a/lib/rules/no-missing-message-ids.js b/lib/rules/no-missing-message-ids.js
index f10c950..e23382f 100644
--- a/lib/rules/no-missing-message-ids.js
+++ b/lib/rules/no-missing-message-ids.js
@@ -14,7 +14,7 @@ module.exports = {
       description:
         'disallow `messageId`s that are missing from `meta.messages`',
       category: 'Rules',
-      recommended: false,
+      recommended: true,
       url: 'https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-missing-message-ids.md',
     },
     fixable: null,
@@ -71,7 +71,7 @@ module.exports = {
             values.forEach((val) => {
               if (
                 val.type === 'Literal' &&
-                val.value !== null &&
+                typeof val.value === 'string' &&
                 val.value !== '' &&
                 !utils.getMessageIdNodeById(
                   val.value,
diff --git a/lib/rules/no-missing-placeholders.js b/lib/rules/no-missing-placeholders.js
index e598e88..6523d2a 100644
--- a/lib/rules/no-missing-placeholders.js
+++ b/lib/rules/no-missing-placeholders.js
@@ -31,19 +31,17 @@ module.exports = {
   },
 
   create(context) {
+    const sourceCode = context.getSourceCode();
+    const { scopeManager } = sourceCode;
+
     let contextIdentifiers;
 
-    // ----------------------------------------------------------------------
-    // Public
-    // ----------------------------------------------------------------------
+    const ruleInfo = utils.getRuleInfo(sourceCode);
+    const messagesNode = utils.getMessagesNode(ruleInfo, scopeManager);
 
     return {
       Program(ast) {
-        const sourceCode = context.getSourceCode();
-        contextIdentifiers = utils.getContextIdentifiers(
-          sourceCode.scopeManager,
-          ast
-        );
+        contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
       },
       CallExpression(node) {
         if (
@@ -57,10 +55,34 @@ module.exports = {
             return;
           }
 
-          const reportMessagesAndDataArray = utils
-            .collectReportViolationAndSuggestionData(reportInfo)
-            .filter((obj) => obj.message);
-          for (const { message, data } of reportMessagesAndDataArray) {
+          const reportMessagesAndDataArray =
+            utils.collectReportViolationAndSuggestionData(reportInfo);
+
+          if (messagesNode) {
+            // Check for any potential instances where we can use the messageId to fill in the message for convenience.
+            reportMessagesAndDataArray.forEach((obj) => {
+              if (
+                !obj.message &&
+                obj.messageId &&
+                obj.messageId.type === 'Literal' &&
+                typeof obj.messageId.value === 'string'
+              ) {
+                const correspondingMessage = utils.getMessageIdNodeById(
+                  obj.messageId.value,
+                  ruleInfo,
+                  scopeManager,
+                  context.getScope()
+                );
+                if (correspondingMessage) {
+                  obj.message = correspondingMessage.value;
+                }
+              }
+            });
+          }
+
+          for (const { message, data } of reportMessagesAndDataArray.filter(
+            (obj) => obj.message
+          )) {
             const messageStaticValue = getStaticValue(
               message,
               context.getScope()
diff --git a/lib/rules/no-unused-message-ids.js b/lib/rules/no-unused-message-ids.js
index a1724a6..df79360 100644
--- a/lib/rules/no-unused-message-ids.js
+++ b/lib/rules/no-unused-message-ids.js
@@ -13,7 +13,7 @@ module.exports = {
     docs: {
       description: 'disallow unused `messageId`s in `meta.messages`',
       category: 'Rules',
-      recommended: false,
+      recommended: true,
       url: 'https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/no-unused-message-ids.md',
     },
     fixable: null,
diff --git a/lib/rules/no-unused-placeholders.js b/lib/rules/no-unused-placeholders.js
index c552bfe..61f17ad 100644
--- a/lib/rules/no-unused-placeholders.js
+++ b/lib/rules/no-unused-placeholders.js
@@ -30,19 +30,17 @@ module.exports = {
   },
 
   create(context) {
+    const sourceCode = context.getSourceCode();
+    const { scopeManager } = sourceCode;
+
     let contextIdentifiers;
 
-    // ----------------------------------------------------------------------
-    // Public
-    // ----------------------------------------------------------------------
+    const ruleInfo = utils.getRuleInfo(sourceCode);
+    const messagesNode = utils.getMessagesNode(ruleInfo, scopeManager);
 
     return {
       Program(ast) {
-        const sourceCode = context.getSourceCode();
-        contextIdentifiers = utils.getContextIdentifiers(
-          sourceCode.scopeManager,
-          ast
-        );
+        contextIdentifiers = utils.getContextIdentifiers(scopeManager, ast);
       },
       CallExpression(node) {
         if (
@@ -56,10 +54,34 @@ module.exports = {
             return;
           }
 
-          const reportMessagesAndDataArray = utils
-            .collectReportViolationAndSuggestionData(reportInfo)
-            .filter((obj) => obj.message);
-          for (const { message, data } of reportMessagesAndDataArray) {
+          const reportMessagesAndDataArray =
+            utils.collectReportViolationAndSuggestionData(reportInfo);
+
+          if (messagesNode) {
+            // Check for any potential instances where we can use the messageId to fill in the message for convenience.
+            reportMessagesAndDataArray.forEach((obj) => {
+              if (
+                !obj.message &&
+                obj.messageId &&
+                obj.messageId.type === 'Literal' &&
+                typeof obj.messageId.value === 'string'
+              ) {
+                const correspondingMessage = utils.getMessageIdNodeById(
+                  obj.messageId.value,
+                  ruleInfo,
+                  scopeManager,
+                  context.getScope()
+                );
+                if (correspondingMessage) {
+                  obj.message = correspondingMessage.value;
+                }
+              }
+            });
+          }
+
+          for (const { message, data } of reportMessagesAndDataArray.filter(
+            (obj) => obj.message
+          )) {
             const messageStaticValue = getStaticValue(
               message,
               context.getScope()
diff --git a/lib/rules/prefer-message-ids.js b/lib/rules/prefer-message-ids.js
index 9fca98a..8d80860 100644
--- a/lib/rules/prefer-message-ids.js
+++ b/lib/rules/prefer-message-ids.js
@@ -15,7 +15,7 @@ module.exports = {
       description:
         'require using `messageId` instead of `message` to report rule violations',
       category: 'Rules',
-      recommended: false,
+      recommended: true,
       url: 'https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/prefer-message-ids.md',
     },
     fixable: null,
diff --git a/lib/rules/prefer-object-rule.js b/lib/rules/prefer-object-rule.js
index bb773b0..edcada1 100644
--- a/lib/rules/prefer-object-rule.js
+++ b/lib/rules/prefer-object-rule.js
@@ -15,7 +15,7 @@ module.exports = {
   meta: {
     type: 'suggestion',
     docs: {
-      description: 'disallow rule exports where the export is a function',
+      description: 'disallow function-style rules',
       category: 'Rules',
       recommended: true,
       url: 'https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/prefer-object-rule.md',
diff --git a/lib/rules/prefer-output-null.js b/lib/rules/prefer-output-null.js
index 87fe877..0716d91 100644
--- a/lib/rules/prefer-output-null.js
+++ b/lib/rules/prefer-output-null.js
@@ -19,7 +19,7 @@ module.exports = {
       description:
         'disallow invalid RuleTester test cases where the `output` matches the `code`',
       category: 'Tests',
-      recommended: false,
+      recommended: true,
       url: 'https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/tree/HEAD/docs/rules/prefer-output-null.md',
     },
     fixable: 'code',
diff --git a/lib/utils.js b/lib/utils.js
index 5b74496..d2bc650 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -121,8 +121,13 @@ function isTypeScriptRuleHelper(node) {
 function getRuleExportsESM(ast, scopeManager) {
   return (
     ast.body
-      .filter((statement) => statement.type === 'ExportDefaultDeclaration')
-      .map((statement) => statement.declaration)
+      .filter((statement) =>
+        [
+          'ExportDefaultDeclaration', // export default rule;
+          'TSExportAssignment', // export = rule;
+        ].includes(statement.type)
+      )
+      .map((statement) => statement.declaration || statement.expression)
       // eslint-disable-next-line unicorn/prefer-object-from-entries
       .reduce((currentExports, node) => {
         if (node.type === 'ObjectExpression') {
diff --git a/package.json b/package.json
index 8fc34db..5a6e3ea 100644
--- a/package.json
+++ b/package.json
@@ -1,15 +1,17 @@
 {
   "name": "eslint-plugin-eslint-plugin",
-  "version": "4.4.0",
+  "version": "4.4.1",
   "description": "An ESLint plugin for linting ESLint plugins",
   "author": "Teddy Katz",
-  "main": "lib/index.js",
+  "main": "./lib/index.js",
+  "exports": "./lib/index.js",
   "license": "MIT",
   "scripts": {
+    "generate-readme-table": "node build/generate-readme-table.js",
     "lint": "npm-run-all --continue-on-error --aggregate-output --parallel lint:*",
     "lint:docs": "markdownlint '**/*.md'",
     "lint:js": "eslint --cache .",
-    "generate-readme-table": "node build/generate-readme-table.js",
+    "lint:package-json": "npmPkgJsonLint .",
     "release": "release-it",
     "test": "nyc --all --check-coverage --include lib mocha tests --recursive"
   },
@@ -60,6 +62,7 @@
     "lodash": "^4.17.2",
     "markdownlint-cli": "^0.30.0",
     "mocha": "^9.1.4",
+    "npm-package-json-lint": "^6.3.0",
     "npm-run-all": "^4.1.5",
     "nyc": "^15.1.0",
     "prettier": "^2.5.1",
@@ -67,10 +70,10 @@
     "typescript": "^4.4.3"
   },
   "peerDependencies": {
-    "eslint": ">=6.0.0"
+    "eslint": ">=7.0.0"
   },
   "engines": {
-    "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+    "node": "^14.17.0 || ^16.0.0 || >= 18.0.0"
   },
   "release-it": {
     "git": {


Test patch:
diff --git a/docs/rules/no-identical-tests.md b/docs/rules/no-identical-tests.md
index f74519b..1663e1c 100644
--- a/docs/rules/no-identical-tests.md
+++ b/docs/rules/no-identical-tests.md
@@ -4,18 +4,34 @@
 
 ⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule.
 
-When a rule has a lot of tests, it's sometimes difficult to tell if any tests are duplicates. This rule would warn if any test cases have the same properties.
+Duplicate test cases can cause confusion, can be hard to detect manually in a long file, and serve no purpose.
+
+As of [ESLint v9](https://github.com/eslint/rfcs/tree/main/designs/2021-stricter-rule-test-validation#disallow-identical-test-cases), ESLint attempts to detect and disallow duplicate tests.
 
 ## Rule Details
 
+This rule detects duplicate test cases.
+
 Examples of **incorrect** code for this rule:
 
 ```js
 /* eslint eslint-plugin/no-identical-tests: error */
 
 new RuleTester().run('foo', bar, {
-  valid: [{ code: 'foo' }, { code: 'foo' }],
-  invalid: [],
+  valid: [
+    'foo',
+    'foo', // duplicate of previous
+  ],
+  invalid: [
+    {
+      code: 'bar',
+      errors: [{ messageId: 'my-message', type: 'CallExpression' }],
+    },
+    {
+      code: 'bar',
+      errors: [{ messageId: 'my-message', type: 'CallExpression' }],
+    }, // duplicate of previous
+  ],
 });
 ```
 
@@ -25,11 +41,12 @@ Examples of **correct** code for this rule:
 /* eslint eslint-plugin/no-identical-tests: error */
 
 new RuleTester().run('foo', bar, {
-  valid: [{ code: 'foo' }, { code: 'bar' }],
-  invalid: [],
+  valid: ['foo', 'bar'],
+  invalid: [
+    {
+      code: 'baz',
+      errors: [{ messageId: 'my-message', type: 'CallExpression' }],
+    },
+  ],
 });
 ```
-
-## When Not To Use It
-
-If you want to allow identical tests, do not enable this rule.
diff --git a/tests/lib/rules/no-missing-placeholders.js b/tests/lib/rules/no-missing-placeholders.js
index 78d9a64..fb2aafb 100644
--- a/tests/lib/rules/no-missing-placeholders.js
+++ b/tests/lib/rules/no-missing-placeholders.js
@@ -113,6 +113,60 @@ ruleTester.run('no-missing-placeholders', rule, {
         }
       };
     `,
+    // messageId but no placeholder.
+    `
+      module.exports = {
+        meta: {
+          messages: { myMessageId: 'foo' }
+        },
+        create(context) {
+          context.report({ node, messageId: 'myMessageId' });
+        }
+      };
+    `,
+    // messageId but the message doesn't exist in `meta.messages`.
+    `
+      module.exports = {
+        meta: {
+          messages: { }
+        },
+        create(context) {
+          context.report({ node, messageId: 'myMessageId' });
+        }
+      };
+    `,
+    // messageId but no `meta.messages`.
+    `
+      module.exports = {
+        meta: { },
+        create(context) {
+          context.report({ node, messageId: 'myMessageId' });
+        }
+      };
+    `,
+    // messageId but no `meta`.
+    `
+      module.exports = {
+        create(context) {
+          context.report({ node, messageId: 'myMessageId' });
+        }
+      };
+    `,
+    // messageId with correctly-used placeholder.
+    `
+      module.exports = {
+        meta: {
+          messages: { myMessageId: 'foo {{bar}}' }
+        },
+        create(context) {
+          context.report({
+            node,
+            messageId: 'myMessageId',
+            data: { bar: 'baz' }
+          });
+        }
+      };
+    `,
     // Message in variable.
     `
       const MESSAGE = 'foo {{bar}}';
@@ -145,6 +199,25 @@ ruleTester.run('no-missing-placeholders', rule, {
         }
       };
     `,
+    // Suggestion with messageId
+    `
+      module.exports = {
+        meta: { messages: { myMessageId: 'Remove {{functionName}}' } },
+        create(context) {
+          context.report({
+            node,
+            suggest: [
+              {
+                messageId: 'myMessageId',
+                data: {
+                  functionName: 'foo'
+                }
+              }
+            ]
+          });
+        }
+      };
+    `,
   ],
 
   invalid: [
@@ -269,6 +342,25 @@ ruleTester.run('no-missing-placeholders', rule, {
       `,
       errors: [error('bar')],
     },
+    {
+      // Suggestion and messageId
+      code: `
+        module.exports = {
+          meta: { messages: { myMessageId: 'foo {{bar}}' } },
+          create(context) {
+            context.report({
+              node,
+              suggest: [
+                {
+                  messageId: 'myMessageId',
+                }
+              ]
+            });
+          }
+        };
+      `,
+      errors: [error('bar')],
+    },
     {
       // `create` in variable.
       code: `
@@ -283,5 +375,20 @@ ruleTester.run('no-missing-placeholders', rule, {
       `,
       errors: [error('hasOwnProperty')],
     },
+    {
+      // messageId.
+      code: `
+        module.exports = {
+          meta: { messages: { myMessageId: 'foo {{bar}}' } },
+          create(context) {
+            context.report({
+              node,
+              messageId: 'myMessageId'
+            });
+          }
+        };
+      `,
+      errors: [error('bar')],
+    },
   ],
 });
diff --git a/tests/lib/rules/no-unused-placeholders.js b/tests/lib/rules/no-unused-placeholders.js
index cabdbc2..8679d0c 100644
--- a/tests/lib/rules/no-unused-placeholders.js
+++ b/tests/lib/rules/no-unused-placeholders.js
@@ -122,6 +122,77 @@ ruleTester.run('no-unused-placeholders', rule, {
         }
       };
     `,
+    // Suggestion with messageId
+    `
+      module.exports = {
+        meta: { messages: { myMessageId: 'foo {{bar}}' } },
+        create(context) {
+          context.report({
+            node,
+            suggest: [
+              {
+                messageId: 'myMessageId',
+                data: { 'bar': 'baz' }
+              }
+            ]
+          });
+        }
+      };
+    `,
+    // messageId but no placeholder.
+    `
+      module.exports = {
+        meta: {
+          messages: { myMessageId: 'foo' }
+        },
+        create(context) {
+          context.report({ node, messageId: 'myMessageId' });
+        }
+      };
+    `,
+    // messageId but the message property doesn't exist yet.
+    `
+      module.exports = {
+        meta: {
+          messages: { }
+        },
+        create(context) {
+          context.report({ node, messageId: 'myMessageId' });
+        }
+      };
+    `,
+    // messageId but no `meta.messages`.
+    `
+      module.exports = {
+        meta: {},
+        create(context) {
+          context.report({ node, messageId: 'myMessageId' });
+        }
+      };
+    `,
+    // messageId but no `meta`.
+    `
+      module.exports = {
+        create(context) {
+          context.report({ node, messageId: 'myMessageId' });
+        }
+      };
+    `,
+    // messageId with correctly-used placeholder.
+    `
+      module.exports = {
+        meta: {
+          messages: { myMessageId: 'foo {{bar}}' }
+        },
+        create(context) {
+          context.report({
+            node,
+            messageId: 'myMessageId',
+            data: { bar: 'baz' }
+          });
+        }
+      };
+    `,
   ],
 
   invalid: [
@@ -197,6 +268,22 @@ ruleTester.run('no-unused-placeholders', rule, {
       `,
       errors: [error('baz')],
     },
+    {
+      // messageId.
+      code: `
+        module.exports = {
+          meta: { messages: { myMessageId: 'foo' } },
+          create(context) {
+            context.report({
+              node,
+              messageId: 'myMessageId',
+              data: { bar }
+            });
+          }
+        };
+      `,
+      errors: [error('bar')],
+    },
     {
       // Suggestion
       code: `
@@ -216,5 +303,25 @@ ruleTester.run('no-unused-placeholders', rule, {
       `,
       errors: [error('bar')],
     },
+    {
+      // Suggestion and messageId
+      code: `
+        module.exports = {
+          meta: { messages: { myMessageId: 'foo' } },
+          create(context) {
+            context.report({
+              node,
+              suggest: [
+                {
+                  messageId: 'myMessageId',
+                  data: { bar }
+                }
+              ]
+            });
+          }
+        };
+      `,
+      errors: [error('bar')],
+    },
   ],
 });
diff --git a/tests/lib/utils.js b/tests/lib/utils.js
index c6ecb38..66e1b02 100644
--- a/tests/lib/utils.js
+++ b/tests/lib/utils.js
@@ -119,6 +119,9 @@ describe('utils', () => {
         'const notRule = foo(); export default notRule;',
         'const notRule = function(){}; export default notRule;',
         'const notRule = {}; export default notRule;',
+        'const notRule = {}; export = notRule;',
+        'const notRule: Rule.RuleModule = {}; export = notRule;',
+        'export = {};',
       ].forEach((noRuleCase) => {
         it(`returns null for ${noRuleCase}`, () => {
           const ast = typescriptEslintParser.parse(noRuleCase, {
@@ -188,6 +191,65 @@ describe('utils', () => {
             isNewStyle: true,
           },
 
+        // No helper.
+        'export default { create() {}, meta: {} };': {
+          create: { type: 'FunctionExpression' },
+          meta: { type: 'ObjectExpression' },
+          isNewStyle: true,
+        },
+        // No helper, `export =` syntax.
+        'export = { create() {}, meta: {} };': {
+          create: { type: 'FunctionExpression' },
+          meta: { type: 'ObjectExpression' },
+          isNewStyle: true,
+        },
+        // No helper, variable.
+        'const rule = { create() {}, meta: {} }; export default rule;': {
+          create: { type: 'FunctionExpression' },
+          meta: { type: 'ObjectExpression' },
+          isNewStyle: true,
+        },
+        // no helper, variable with type.
+        'const rule: Rule.RuleModule = { create() {}, meta: {} }; export default rule;':
+          {
+            create: { type: 'FunctionExpression' },
+            meta: { type: 'ObjectExpression' },
+            isNewStyle: true,
+          },
+        // No helper, variable, `export =` syntax.
+        'const rule = { create() {}, meta: {} }; export = rule;': {
+          create: { type: 'FunctionExpression' },
+          meta: { type: 'ObjectExpression' },
+          isNewStyle: true,
+        },
+        // No helper, variable with type, `export =` syntax.
+        'const rule: Rule.RuleModule = { create() {}, meta: {} }; export = rule;':
+          {
+            create: { type: 'FunctionExpression' },
+            meta: { type: 'ObjectExpression' },
+            isNewStyle: true,
+          },
+        // Helper, variable, `export =` syntax.
+        'const rule = createESLintRule({ create() {}, meta: {} }); export = rule;':
+          {
+            create: { type: 'FunctionExpression' },
+            meta: { type: 'ObjectExpression' },
+            isNewStyle: true,
+          },
+        // Helper, variable with type, `export =` syntax.
+        'const rule: Rule.RuleModule = createESLintRule({ create() {}, meta: {} }); export = rule;':
+          {
+            create: { type: 'FunctionExpression' },
+            meta: { type: 'ObjectExpression' },
+            isNewStyle: true,
+          },
+        // Helper, `export =` syntax.
+        'export = createESLintRule({ create() {}, meta: {} });': {
+          create: { type: 'FunctionExpression' },
+          meta: { type: 'ObjectExpression' },
+          isNewStyle: true,
+        },
+
         // Util function with "{} as const".
         'export default createESLintRule({ create() {}, meta: {} as const });':
           {