Repository: not-an-aardvark/eslint-plugin-eslint-plugin
Pull request #152 (2021-06-26 16:07:49)

Breaking: Enable additional `recommended` rules for v4
* [consistent-output](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/consistent-output.md)
* [no-deprecated-context-methods](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-deprecated-context-methods.md)
* [no-only-tests](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-only-tests.md)
* [prefer-object-rule](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-object-rule.md)
* [require-meta-has-suggestions](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-docs-suggestions.md)
* [require-meta-schema](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-schema.md)
* [require-meta-type](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-type.md)

Part of v4 release (#120).

TODO:
- [x] Check for any other high-priority [rule proposals](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc) we should implement and enable.
  * I checked and implemented [no-only-tests](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-only-tests.md).
- [x] Consider enabling [require-meta-schema](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-schema.md) since `schema` is expected to [become mandatory](https://github.com/eslint/eslint/issues/14709) for rules with options in ESLint v9
- [x] Add `requireSchemaPropertyWhenOptionless` option to [require-meta-schema](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-schema.md)

aladdin-add: looks like we have a conflict here.

Patch:
diff --git a/README.md b/README.md
index 3ae6675..ae5ab34 100644
--- a/README.md
+++ b/README.md
@@ -47,18 +47,18 @@ 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
+[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 functions to return a fix
 [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-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-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-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-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-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-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()`
@@ -66,9 +66,9 @@ Name | ✔️ | 🛠 | 💡 | Description
 [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-has-suggestions](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-has-suggestions.md) |  | 🛠 |  | require suggestable rules to implement a `meta.hasSuggestions` 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
+[require-meta-has-suggestions](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/require-meta-has-suggestions.md) | ✔️ | 🛠 |  | require suggestable rules to implement a `meta.hasSuggestions` 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) |  | 🛠 |  | 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__ -->
diff --git a/docs/rules/consistent-output.md b/docs/rules/consistent-output.md
index 72db0bc..9e110a0 100644
--- a/docs/rules/consistent-output.md
+++ b/docs/rules/consistent-output.md
@@ -1,5 +1,7 @@
 # Enforce consistent use of `output` assertions in rule tests (consistent-output)
 
+✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule.
+
 When writing tests for a fixable rule with `RuleTester`, you can assert the autofix output of your test cases. However, it can be easy to forget to assert the output of a particular test case.
 
 [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.
diff --git a/docs/rules/meta-property-ordering.md b/docs/rules/meta-property-ordering.md
index bef8867..01b2ee9 100644
--- a/docs/rules/meta-property-ordering.md
+++ b/docs/rules/meta-property-ordering.md
@@ -10,7 +10,7 @@ This rule enforces that meta properties of a rule are placed in a consistent ord
 
 This rule has an array option:
 
-* `['type', 'docs', 'fixable', 'schema', 'messages', 'deprecated', 'replacedBy']` (default): The order that the properties of `meta` should be placed in.
+* `['type', 'docs', 'fixable', 'hasSuggestions', 'schema', 'messages', 'deprecated', 'replacedBy']` (default): The order that the properties of `meta` should be placed in.
 
 Examples of **incorrect** code for this rule:
 
diff --git a/docs/rules/no-deprecated-context-methods.md b/docs/rules/no-deprecated-context-methods.md
index 75bdfcb..4ac017f 100644
--- a/docs/rules/no-deprecated-context-methods.md
+++ b/docs/rules/no-deprecated-context-methods.md
@@ -1,5 +1,7 @@
 # Disallow usage of deprecated methods on rule context objects (no-deprecated-context-methods)
 
+✔️ 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.
 
 This rule disallows the use of deprecated methods on rule `context` objects.
diff --git a/docs/rules/prefer-object-rule.md b/docs/rules/prefer-object-rule.md
index 095835a..11b3460 100644
--- a/docs/rules/prefer-object-rule.md
+++ b/docs/rules/prefer-object-rule.md
@@ -1,5 +1,7 @@
 # Disallow rule exports where the export is a function (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.
 
 ## Rule Details
diff --git a/docs/rules/require-meta-has-suggestions.md b/docs/rules/require-meta-has-suggestions.md
index 6d08487..e5e2822 100644
--- a/docs/rules/require-meta-has-suggestions.md
+++ b/docs/rules/require-meta-has-suggestions.md
@@ -1,5 +1,7 @@
 # Require suggestable rules to implement a `meta.hasSuggestions` property (require-meta-has-suggestions)
 
+✔️ 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.
 
 A suggestable ESLint rule should specify the `meta.hasSuggestions` property with a value of `true`. This makes it easier for both humans and tooling to tell whether a rule provides suggestions. [As of ESLint 8](https://eslint.org/blog/2021/06/whats-coming-in-eslint-8.0.0#rules-with-suggestions-now-require-the-metahassuggestions-property), an exception will be thrown if a suggestable rule is missing this property.
diff --git a/docs/rules/require-meta-schema.md b/docs/rules/require-meta-schema.md
index d653013..6caad97 100644
--- a/docs/rules/require-meta-schema.md
+++ b/docs/rules/require-meta-schema.md
@@ -1,5 +1,7 @@
 # Require rules to implement a `meta.schema` property (require-meta-schema)
 
+✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule.
+
 💡 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.
diff --git a/docs/rules/require-meta-type.md b/docs/rules/require-meta-type.md
index b3a3348..6bc6164 100644
--- a/docs/rules/require-meta-type.md
+++ b/docs/rules/require-meta-type.md
@@ -1,5 +1,7 @@
 # Require rules to implement a `meta.type` property (require-meta-type)
 
+✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule.
+
 ESLint v5.9.0 introduces a new `--fix-type` option for the command line interface. This option allows users to filter the type of fixes applied when using `--fix`.
 
 Fixes in custom rules will not be applied when using `--fix-type` unless they include a `meta.type` field.
diff --git a/lib/index.js b/lib/index.js
index 26e28c5..32e427a 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -43,21 +43,22 @@ function loadRule (ruleName) {
 }
 
 // import all rules in lib/rules
-const allRules = fs
+const allRules = Object.fromEntries(fs
   .readdirSync(`${__dirname}/rules`)
   .filter(fileName => fileName.endsWith('.js') && /^[^._]/.test(fileName))
   .map(fileName => fileName.replace(/\.js$/, ''))
-  .reduce((rules, ruleName) => Object.assign(rules, { [ruleName]: loadRule(ruleName) }), {});
+  .map(ruleName => [ruleName, loadRule(ruleName)]));
 
 module.exports.rules = allRules;
 
+// eslint-disable-next-line unicorn/prefer-object-from-entries
 module.exports.configs = Object.keys(configFilters).reduce((configs, configName) => {
   return Object.assign(configs, {
     [configName]: {
       plugins: ['eslint-plugin'],
-      rules: Object.keys(allRules)
+      rules: Object.fromEntries(Object.keys(allRules)
         .filter(ruleName => configFilters[configName](allRules[ruleName]))
-        .reduce((rules, ruleName) => Object.assign(rules, { [`${PLUGIN_NAME}/${ruleName}`]: 'error' }), {}),
+        .map(ruleName => [`${PLUGIN_NAME}/${ruleName}`, 'error'])),
     },
   });
 }, {});
diff --git a/lib/rules/consistent-output.js b/lib/rules/consistent-output.js
index b62b6df..bdb0b69 100644
--- a/lib/rules/consistent-output.js
+++ b/lib/rules/consistent-output.js
@@ -17,7 +17,7 @@ module.exports = {
     docs: {
       description: 'enforce consistent use of `output` assertions in rule tests',
       category: 'Tests',
-      recommended: false,
+      recommended: true,
     },
     fixable: null, // or "code" or "whitespace"
     schema: [
diff --git a/lib/rules/fixer-return.js b/lib/rules/fixer-return.js
index 0dd7fc0..40dfa5d 100644
--- a/lib/rules/fixer-return.js
+++ b/lib/rules/fixer-return.js
@@ -98,22 +98,12 @@ module.exports = {
 
       // Stacks this function's information.
       onCodePathStart (codePath, node) {
-        const parent = node.parent;
-
-        // Whether we are inside the fixer function we care about.
-        const shouldCheck = ['FunctionExpression', 'ArrowFunctionExpression'].includes(node.type) &&
-          parent.parent.type === 'ObjectExpression' &&
-          parent.parent.parent.type === 'CallExpression' &&
-          contextIdentifiers.has(parent.parent.parent.callee.object) &&
-          parent.parent.parent.callee.property.name === 'report' &&
-          utils.getReportInfo(parent.parent.parent.arguments).fix === node;
-
         funcInfo = {
           upper: funcInfo,
           codePath,
           hasYieldWithFixer: false,
           hasReturnWithFixer: false,
-          shouldCheck,
+          shouldCheck: utils.isAutoFixerFunction(node, contextIdentifiers) || utils.isSuggestionFixerFunction(node, contextIdentifiers),
           node,
         };
       },
diff --git a/lib/rules/meta-property-ordering.js b/lib/rules/meta-property-ordering.js
index 0c4a46d..40a6791 100644
--- a/lib/rules/meta-property-ordering.js
+++ b/lib/rules/meta-property-ordering.js
@@ -32,7 +32,7 @@ module.exports = {
     const sourceCode = context.getSourceCode();
     const info = getRuleInfo(sourceCode);
 
-    const order = context.options[0] || ['type', 'docs', 'fixable', 'schema', 'messages'];
+    const order = context.options[0] || ['type', 'docs', 'fixable', 'hasSuggestions', 'schema', 'messages'];
 
     const orderMap = new Map(order.map((name, i) => [name, i]));
 
diff --git a/lib/rules/no-deprecated-context-methods.js b/lib/rules/no-deprecated-context-methods.js
index 7677152..58fa3e4 100644
--- a/lib/rules/no-deprecated-context-methods.js
+++ b/lib/rules/no-deprecated-context-methods.js
@@ -40,7 +40,7 @@ module.exports = {
     docs: {
       description: 'disallow usage of deprecated methods on rule context objects',
       category: 'Rules',
-      recommended: false,
+      recommended: true,
     },
     fixable: 'code',
     schema: [],
diff --git a/lib/rules/no-missing-placeholders.js b/lib/rules/no-missing-placeholders.js
index 77aca1b..1f66c2b 100644
--- a/lib/rules/no-missing-placeholders.js
+++ b/lib/rules/no-missing-placeholders.js
@@ -45,33 +45,36 @@ module.exports = {
           node.callee.property.type === 'Identifier' && node.callee.property.name === 'report'
         ) {
           const reportInfo = utils.getReportInfo(node.arguments, context);
-          if (!reportInfo || !reportInfo.message) {
+          if (!reportInfo) {
             return;
           }
 
-          const messageStaticValue = getStaticValue(reportInfo.message, context.getScope());
-          if (
-            (
-              (reportInfo.message.type === 'Literal' && typeof reportInfo.message.value === 'string') ||
-              (messageStaticValue && typeof messageStaticValue.value === 'string')
-            ) &&
-            (!reportInfo.data || reportInfo.data.type === 'ObjectExpression')
-          ) {
-            // Same regex as the one ESLint uses
-            // https://github.com/eslint/eslint/blob/e5446449d93668ccbdb79d78cc69f165ce4fde07/lib/eslint.js#L990
-            const PLACEHOLDER_MATCHER = /{{\s*([^{}]+?)\s*}}/g;
-            let match;
+          const reportMessagesAndDataArray = utils.collectReportViolationAndSuggestionData(reportInfo).filter(obj => obj.message);
+          for (const { message, data } of reportMessagesAndDataArray) {
+            const messageStaticValue = getStaticValue(message, context.getScope());
+            if (
+              (
+                (message.type === 'Literal' && typeof message.value === 'string') ||
+                (messageStaticValue && typeof messageStaticValue.value === 'string')
+              ) &&
+              (!data || data.type === 'ObjectExpression')
+            ) {
+              // Same regex as the one ESLint uses
+              // https://github.com/eslint/eslint/blob/e5446449d93668ccbdb79d78cc69f165ce4fde07/lib/eslint.js#L990
+              const PLACEHOLDER_MATCHER = /{{\s*([^{}]+?)\s*}}/g;
+              let match;
 
-            while ((match = PLACEHOLDER_MATCHER.exec(reportInfo.message.value || messageStaticValue.value))) { // eslint-disable-line no-extra-parens
-              const matchingProperty = reportInfo.data &&
-                reportInfo.data.properties.find(prop => utils.getKeyName(prop) === match[1]);
+              while ((match = PLACEHOLDER_MATCHER.exec(message.value || messageStaticValue.value))) { // eslint-disable-line no-extra-parens
+                const matchingProperty = data &&
+                  data.properties.find(prop => utils.getKeyName(prop) === match[1]);
 
-              if (!matchingProperty) {
-                context.report({
-                  node: reportInfo.message,
-                  messageId: 'placeholderDoesNotExist',
-                  data: { missingKey: match[1] },
-                });
+                if (!matchingProperty) {
+                  context.report({
+                    node: message,
+                    messageId: 'placeholderDoesNotExist',
+                    data: { missingKey: match[1] },
+                  });
+                }
               }
             }
           }
diff --git a/lib/rules/no-unused-placeholders.js b/lib/rules/no-unused-placeholders.js
index 83c38a4..9cfc158 100644
--- a/lib/rules/no-unused-placeholders.js
+++ b/lib/rules/no-unused-placeholders.js
@@ -45,38 +45,41 @@ module.exports = {
             node.callee.property.type === 'Identifier' && node.callee.property.name === 'report'
         ) {
           const reportInfo = utils.getReportInfo(node.arguments, context);
-          if (!reportInfo || !reportInfo.message) {
+          if (!reportInfo) {
             return;
           }
 
-          const messageStaticValue = getStaticValue(reportInfo.message, context.getScope());
-          if (
-            (
-              (reportInfo.message.type === 'Literal' && typeof reportInfo.message.value === 'string') ||
-              (messageStaticValue && typeof messageStaticValue.value === 'string')
-            ) &&
-            reportInfo.data &&
-            reportInfo.data.type === 'ObjectExpression'
-          ) {
-            const message = reportInfo.message.value || messageStaticValue.value;
-            // https://github.com/eslint/eslint/blob/2874d75ed8decf363006db25aac2d5f8991bd969/lib/linter.js#L986
-            const PLACEHOLDER_MATCHER = /{{\s*([^{}]+?)\s*}}/g;
-            const placeholdersInMessage = new Set();
+          const reportMessagesAndDataArray = utils.collectReportViolationAndSuggestionData(reportInfo).filter(obj => obj.message);
+          for (const { message, data } of reportMessagesAndDataArray) {
+            const messageStaticValue = getStaticValue(message, context.getScope());
+            if (
+              (
+                (message.type === 'Literal' && typeof message.value === 'string') ||
+                (messageStaticValue && typeof messageStaticValue.value === 'string')
+              ) &&
+              data &&
+              data.type === 'ObjectExpression'
+            ) {
+              const messageValue = message.value || messageStaticValue.value;
+              // https://github.com/eslint/eslint/blob/2874d75ed8decf363006db25aac2d5f8991bd969/lib/linter.js#L986
+              const PLACEHOLDER_MATCHER = /{{\s*([^{}]+?)\s*}}/g;
+              const placeholdersInMessage = new Set();
 
-            message.replace(PLACEHOLDER_MATCHER, (fullMatch, term) => {
-              placeholdersInMessage.add(term);
-            });
+              messageValue.replace(PLACEHOLDER_MATCHER, (fullMatch, term) => {
+                placeholdersInMessage.add(term);
+              });
 
-            reportInfo.data.properties.forEach(prop => {
-              const key = utils.getKeyName(prop);
-              if (!placeholdersInMessage.has(key)) {
-                context.report({
-                  node: reportInfo.message,
-                  messageId: 'placeholderUnused',
-                  data: { unusedKey: key },
-                });
-              }
-            });
+              data.properties.forEach(prop => {
+                const key = utils.getKeyName(prop);
+                if (!placeholdersInMessage.has(key)) {
+                  context.report({
+                    node: message,
+                    messageId: 'placeholderUnused',
+                    data: { unusedKey: key },
+                  });
+                }
+              });
+            }
           }
         }
       },
diff --git a/lib/rules/prefer-message-ids.js b/lib/rules/prefer-message-ids.js
index 8fddd35..e6a0dc5 100644
--- a/lib/rules/prefer-message-ids.js
+++ b/lib/rules/prefer-message-ids.js
@@ -68,14 +68,17 @@ module.exports = {
              node.callee.property.type === 'Identifier' && node.callee.property.name === 'report'
         ) {
           const reportInfo = utils.getReportInfo(node.arguments, context);
-          if (!reportInfo || !reportInfo.message) {
+          if (!reportInfo) {
             return;
           }
 
-          context.report({
-            node: reportInfo.message.parent,
-            messageId: 'foundMessage',
-          });
+          const reportMessagesAndDataArray = utils.collectReportViolationAndSuggestionData(reportInfo).filter(obj => obj.message);
+          for (const { message } of reportMessagesAndDataArray) {
+            context.report({
+              node: message.parent,
+              messageId: 'foundMessage',
+            });
+          }
         }
       },
     };
diff --git a/lib/rules/prefer-object-rule.js b/lib/rules/prefer-object-rule.js
index 72b9920..445e1a1 100644
--- a/lib/rules/prefer-object-rule.js
+++ b/lib/rules/prefer-object-rule.js
@@ -16,7 +16,7 @@ module.exports = {
     docs: {
       description: 'disallow rule exports where the export is a function',
       category: 'Rules',
-      recommended: false,
+      recommended: true,
     },
     fixable: 'code',
     schema: [],
@@ -46,7 +46,7 @@ module.exports = {
             // note - we intentionally don't worry about formatting here, as otherwise we have
             //        to indent the function correctly
 
-            if (ruleInfo.create.type === 'FunctionExpression') {
+            if (ruleInfo.create.type === 'FunctionExpression' || ruleInfo.create.type === 'FunctionDeclaration') {
               const openParenToken = sourceCode.getFirstToken(
                 ruleInfo.create,
                 token => token.type === 'Punctuator' && token.value === '('
diff --git a/lib/rules/prefer-placeholders.js b/lib/rules/prefer-placeholders.js
index 21cf23a..666d23b 100644
--- a/lib/rules/prefer-placeholders.js
+++ b/lib/rules/prefer-placeholders.js
@@ -49,42 +49,43 @@ module.exports = {
         ) {
           const reportInfo = utils.getReportInfo(node.arguments, context);
 
-          if (!reportInfo || !reportInfo.message) {
+          if (!reportInfo) {
             return;
           }
 
-          let messageNode = reportInfo.message;
+          const reportMessagesAndDataArray = utils.collectReportViolationAndSuggestionData(reportInfo).filter(obj => obj.message);
+          for (let { message: messageNode } of reportMessagesAndDataArray) {
+            if (messageNode.type === 'Identifier') {
+              // See if we can find the variable declaration.
 
-          if (messageNode.type === 'Identifier') {
-            // See if we can find the variable declaration.
+              const variable = findVariable(
+                scopeManager.acquire(messageNode) || scopeManager.globalScope,
+                messageNode
+              );
 
-            const variable = findVariable(
-              scopeManager.acquire(messageNode) || scopeManager.globalScope,
-              messageNode
-            );
+              if (
+                !variable ||
+                !variable.defs ||
+                !variable.defs[0] ||
+                !variable.defs[0].node ||
+                variable.defs[0].node.type !== 'VariableDeclarator' ||
+                !variable.defs[0].node.init
+              ) {
+                return;
+              }
+
+              messageNode = variable.defs[0].node.init;
+            }
 
             if (
-              !variable ||
-              !variable.defs ||
-              !variable.defs[0] ||
-              !variable.defs[0].node ||
-              variable.defs[0].node.type !== 'VariableDeclarator' ||
-              !variable.defs[0].node.init
+              (messageNode.type === 'TemplateLiteral' && messageNode.expressions.length > 0) ||
+              (messageNode.type === 'BinaryExpression' && messageNode.operator === '+')
             ) {
-              return;
+              context.report({
+                node: messageNode,
+                messageId: 'usePlaceholders',
+              });
             }
-
-            messageNode = variable.defs[0].node.init;
-          }
-
-          if (
-            (messageNode.type === 'TemplateLiteral' && messageNode.expressions.length > 0) ||
-            (messageNode.type === 'BinaryExpression' && messageNode.operator === '+')
-          ) {
-            context.report({
-              node: messageNode,
-              messageId: 'usePlaceholders',
-            });
           }
         }
       },
diff --git a/lib/rules/prefer-replace-text.js b/lib/rules/prefer-replace-text.js
index 9275883..a39419d 100644
--- a/lib/rules/prefer-replace-text.js
+++ b/lib/rules/prefer-replace-text.js
@@ -43,18 +43,10 @@ module.exports = {
 
       // Stacks this function's information.
       onCodePathStart (codePath, node) {
-        const parent = node.parent;
-        const shouldCheck = (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') &&
-          parent.parent.type === 'ObjectExpression' &&
-          parent.parent.parent.type === 'CallExpression' &&
-          contextIdentifiers.has(parent.parent.parent.callee.object) &&
-          parent.parent.parent.callee.property.name === 'report' &&
-          utils.getReportInfo(parent.parent.parent.arguments, context).fix === node;
-
         funcInfo = {
           upper: funcInfo,
           codePath,
-          shouldCheck,
+          shouldCheck: utils.isAutoFixerFunction(node, contextIdentifiers) || utils.isSuggestionFixerFunction(node, contextIdentifiers),
           node,
         };
       },
diff --git a/lib/rules/report-message-format.js b/lib/rules/report-message-format.js
index 343f46a..c8deecf 100644
--- a/lib/rules/report-message-format.js
+++ b/lib/rules/report-message-format.js
@@ -83,12 +83,19 @@ module.exports = {
         ) {
           const reportInfo = utils.getReportInfo(node.arguments, context);
           const message = reportInfo && reportInfo.message;
+          const suggest = reportInfo && reportInfo.suggest;
 
-          if (!message) {
-            return;
+          if (message) {
+            processMessageNode(message);
           }
 
-          processMessageNode(message);
+          if (suggest && suggest.type === 'ArrayExpression') {
+            suggest.elements
+              .flatMap(obj => obj.properties)
+              .filter(prop => prop.type === 'Property' && prop.key.type === 'Identifier' && prop.key.name === 'message')
+              .map(prop => prop.value)
+              .forEach(processMessageNode);
+          }
         }
       },
     };
diff --git a/lib/rules/require-meta-docs-description.js b/lib/rules/require-meta-docs-description.js
index 31a148f..808b362 100644
--- a/lib/rules/require-meta-docs-description.js
+++ b/lib/rules/require-meta-docs-description.js
@@ -15,7 +15,7 @@ module.exports = {
     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.
+      recommended: false,
     },
     fixable: null,
     schema: [
diff --git a/lib/rules/require-meta-has-suggestions.js b/lib/rules/require-meta-has-suggestions.js
index da15e47..73885b6 100644
--- a/lib/rules/require-meta-has-suggestions.js
+++ b/lib/rules/require-meta-has-suggestions.js
@@ -13,7 +13,7 @@ module.exports = {
     docs: {
       description: 'require suggestable rules to implement a `meta.hasSuggestions` property',
       category: 'Rules',
-      recommended: false,
+      recommended: true,
     },
     fixable: 'code',
     schema: [],
diff --git a/lib/rules/require-meta-schema.js b/lib/rules/require-meta-schema.js
index 976aea3..a4d2f88 100644
--- a/lib/rules/require-meta-schema.js
+++ b/lib/rules/require-meta-schema.js
@@ -13,8 +13,9 @@ module.exports = {
     docs: {
       description: 'require rules to implement a `meta.schema` property',
       category: 'Rules',
-      recommended: false, // TODO: enable it in a major release.
+      recommended: true,
     },
+    hasSuggestions: true,
     schema: [
       {
         type: 'object',
@@ -33,7 +34,6 @@ module.exports = {
       missing: '`meta.schema` is required (use [] if rule has no schema).',
       wrongType: '`meta.schema` should be an array or object (use [] if rule has no schema).',
     },
-    hasSuggestions: true,
   },
 
   create (context) {
diff --git a/lib/rules/require-meta-type.js b/lib/rules/require-meta-type.js
index 330d3e9..fb7f33e 100644
--- a/lib/rules/require-meta-type.js
+++ b/lib/rules/require-meta-type.js
@@ -19,7 +19,7 @@ module.exports = {
     docs: {
       description: 'require rules to implement a `meta.type` property',
       category: 'Rules',
-      recommended: false, // TODO: enable it in a major release.
+      recommended: true,
     },
     fixable: null,
     schema: [],
diff --git a/lib/utils.js b/lib/utils.js
index e00aada..24f699d 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -83,8 +83,104 @@ function isRuleTesterConstruction (node) {
   );
 }
 
-module.exports = {
+const INTERESTING_RULE_KEYS = new Set(['create', 'meta']);
+
+/**
+ * Helper for `getRuleInfo`. Handles ESM rules.
+ */
+function getRuleExportsESM (ast) {
+  return ast.body
+    .filter(statement => statement.type === 'ExportDefaultDeclaration')
+    .map(statement => statement.declaration)
+    // eslint-disable-next-line unicorn/prefer-object-from-entries
+    .reduce((currentExports, node) => {
+      if (node.type === 'ObjectExpression') {
+        // eslint-disable-next-line unicorn/prefer-object-from-entries
+        return node.properties.reduce((parsedProps, prop) => {
+          const keyValue = module.exports.getKeyName(prop);
+          if (INTERESTING_RULE_KEYS.has(keyValue)) {
+            parsedProps[keyValue] = prop.value;
+          }
+          return parsedProps;
+        }, {});
+      } else if (isNormalFunctionExpression(node)) {
+        return { create: node, meta: null, isNewStyle: false };
+      }
+      return currentExports;
+    }, {});
+}
 
+/**
+ * Helper for `getRuleInfo`. Handles CJS rules.
+ */
+function getRuleExportsCJS (ast) {
+  let exportsVarOverridden = false;
+  let exportsIsFunction = false;
+  return ast.body
+    .filter(statement => statement.type === 'ExpressionStatement')
+    .map(statement => statement.expression)
+    .filter(expression => expression.type === 'AssignmentExpression')
+    .filter(expression => expression.left.type === 'MemberExpression')
+    // eslint-disable-next-line unicorn/prefer-object-from-entries
+    .reduce((currentExports, node) => {
+      if (
+        node.left.object.type === 'Identifier' && node.left.object.name === 'module' &&
+        node.left.property.type === 'Identifier' && node.left.property.name === 'exports'
+      ) {
+        exportsVarOverridden = true;
+        if (isNormalFunctionExpression(node.right)) {
+          // Check `module.exports = function () {}`
+
+          exportsIsFunction = true;
+          return { create: node.right, meta: null, isNewStyle: false };
+        } else if (node.right.type === 'ObjectExpression') {
+          // Check `module.exports = { create: function () {}, meta: {} }`
+
+          // eslint-disable-next-line unicorn/prefer-object-from-entries
+          return node.right.properties.reduce((parsedProps, prop) => {
+            const keyValue = module.exports.getKeyName(prop);
+            if (INTERESTING_RULE_KEYS.has(keyValue)) {
+              parsedProps[keyValue] = prop.value;
+            }
+            return parsedProps;
+          }, {});
+        }
+        return {};
+      } else if (
+        !exportsIsFunction &&
+        node.left.object.type === 'MemberExpression' &&
+        node.left.object.object.type === 'Identifier' && node.left.object.object.name === 'module' &&
+        node.left.object.property.type === 'Identifier' && node.left.object.property.name === 'exports' &&
+        node.left.property.type === 'Identifier' && INTERESTING_RULE_KEYS.has(node.left.property.name)
+      ) {
+        // Check `module.exports.create = () => {}`
+
+        currentExports[node.left.property.name] = node.right;
+      } else if (
+        !exportsVarOverridden &&
+        node.left.object.type === 'Identifier' && node.left.object.name === 'exports' &&
+        node.left.property.type === 'Identifier' && INTERESTING_RULE_KEYS.has(node.left.property.name)
+      ) {
+        // Check `exports.create = () => {}`
+
+        currentExports[node.left.property.name] = node.right;
+      }
+      return currentExports;
+    }, {});
+}
+
+/**
+ * Find the value of a property in an object by its property key name.
+ * @param {Object} obj
+ * @param {String} keyName
+ * @returns property value
+ */
+function findObjectPropertyValueByKeyName (obj, keyName) {
+  const property = obj.properties.find(prop => prop.key.type === 'Identifier' && prop.key.name === keyName);
+  return property ? property.value : undefined;
+}
+
+module.exports = {
   /**
   * Performs static analysis on an AST to try to determine the final value of `module.exports`.
   * @param {{ast: ASTNode, scopeManager?: ScopeManager}} sourceCode The object contains `Program` AST node, and optional `scopeManager`
@@ -94,61 +190,7 @@ module.exports = {
   from the file, the return value will be `null`.
   */
   getRuleInfo ({ ast, scopeManager }) {
-    const INTERESTING_KEYS = new Set(['create', 'meta']);
-    let exportsVarOverridden = false;
-    let exportsIsFunction = false;
-
-    const exportNodes = ast.body
-      .filter(statement => statement.type === 'ExpressionStatement')
-      .map(statement => statement.expression)
-      .filter(expression => expression.type === 'AssignmentExpression')
-      .filter(expression => expression.left.type === 'MemberExpression')
-      .reduce((currentExports, node) => {
-        if (
-          node.left.object.type === 'Identifier' && node.left.object.name === 'module' &&
-          node.left.property.type === 'Identifier' && node.left.property.name === 'exports'
-        ) {
-          exportsVarOverridden = true;
-
-          if (isNormalFunctionExpression(node.right)) {
-            // Check `module.exports = function () {}`
-
-            exportsIsFunction = true;
-            return { create: node.right, meta: null };
-          } else if (node.right.type === 'ObjectExpression') {
-            // Check `module.exports = { create: function () {}, meta: {} }`
-
-            exportsIsFunction = false;
-            return node.right.properties.reduce((parsedProps, prop) => {
-              const keyValue = module.exports.getKeyName(prop);
-              if (INTERESTING_KEYS.has(keyValue)) {
-                parsedProps[keyValue] = prop.value;
-              }
-              return parsedProps;
-            }, {});
-          }
-          return {};
-        } else if (
-          !exportsIsFunction &&
-          node.left.object.type === 'MemberExpression' &&
-          node.left.object.object.type === 'Identifier' && node.left.object.object.name === 'module' &&
-          node.left.object.property.type === 'Identifier' && node.left.object.property.name === 'exports' &&
-          node.left.property.type === 'Identifier' && INTERESTING_KEYS.has(node.left.property.name)
-        ) {
-          // Check `module.exports.create = () => {}`
-
-          currentExports[node.left.property.name] = node.right;
-        } else if (
-          !exportsVarOverridden &&
-          node.left.object.type === 'Identifier' && node.left.object.name === 'exports' &&
-          node.left.property.type === 'Identifier' && INTERESTING_KEYS.has(node.left.property.name)
-        ) {
-          // Check `exports.create = () => {}`
-
-          currentExports[node.left.property.name] = node.right;
-        }
-        return currentExports;
-      }, {});
+    const exportNodes = ast.sourceType === 'module' ? getRuleExportsESM(ast) : getRuleExportsCJS(ast);
 
     const createExists = Object.prototype.hasOwnProperty.call(exportNodes, 'create');
     if (!createExists) {
@@ -162,7 +204,7 @@ module.exports = {
       return null;
     }
 
-    return Object.assign({ isNewStyle: !exportsIsFunction, meta: null }, exportNodes);
+    return Object.assign({ isNewStyle: true, meta: null }, exportNodes);
   },
 
   /**
@@ -276,6 +318,7 @@ module.exports = {
 
     if (reportArgs.length === 1) {
       if (reportArgs[0].type === 'ObjectExpression') {
+        // eslint-disable-next-line unicorn/prefer-object-from-entries
         return reportArgs[0].properties.reduce((reportInfo, property) => {
           const propName = module.exports.getKeyName(property);
 
@@ -308,9 +351,9 @@ module.exports = {
       return null;
     }
 
-    return keys
+    return Object.fromEntries(keys
       .slice(0, reportArgs.length)
-      .reduce((reportInfo, key, index) => Object.assign(reportInfo, { [key]: reportArgs[index] }), {});
+      .map((key, index) => [key, reportArgs[index]]));
   },
 
   /**
@@ -332,10 +375,8 @@ module.exports = {
         identifier.parent.parent === identifier.parent.parent.parent.init &&
         identifier.parent.parent.parent.id.type === 'Identifier'
       )
-      .map(identifier => context.getDeclaredVariables(identifier.parent.parent.parent))
-      .reduce((allVariables, variablesForIdentifier) => [...allVariables, ...variablesForIdentifier], [])
-      .map(variable => variable.references)
-      .reduce((allRefs, refsForVariable) => [...allRefs, ...refsForVariable], [])
+      .flatMap(identifier => context.getDeclaredVariables(identifier.parent.parent.parent))
+      .flatMap(variable => variable.references)
       .map(ref => ref.identifier));
   },
 
@@ -355,4 +396,72 @@ module.exports = {
       `,\n${propertyText}`
     );
   },
+
+  /**
+   * Collect all context.report({...}) violation/suggestion-related nodes into a standardized array for convenience.
+   * @param {Object} reportInfo - Result of getReportInfo().
+   * @returns {messageId?: String, message?: String, data?: Object, fix?: Function}[]
+   */
+  collectReportViolationAndSuggestionData (reportInfo) {
+    return [
+      // Violation message
+      {
+        messageId: reportInfo.messageId,
+        message: reportInfo.message,
+        data: reportInfo.data,
+        fix: reportInfo.fix,
+      },
+      // Suggestion messages
+      ...((reportInfo.suggest && reportInfo.suggest.elements) || [])
+        .map(suggestObjNode => {
+          return {
+            messageId: findObjectPropertyValueByKeyName(suggestObjNode, 'messageId'),
+            message: findObjectPropertyValueByKeyName(suggestObjNode, 'desc'), // Note: suggestion message named `desc`
+            data: findObjectPropertyValueByKeyName(suggestObjNode, 'data'),
+            fix: findObjectPropertyValueByKeyName(suggestObjNode, 'fix'),
+          };
+        }
+        ),
+    ];
+  },
+
+  /**
+   * Whether the provided node represents an autofixer function.
+   * @param {Node} node
+   * @param {Node[]} contextIdentifiers
+   * @returns {boolean}
+   */
+  isAutoFixerFunction (node, contextIdentifiers) {
+    const parent = node.parent;
+    return ['FunctionExpression', 'ArrowFunctionExpression'].includes(node.type) &&
+      parent.parent.type === 'ObjectExpression' &&
+      parent.parent.parent.type === 'CallExpression' &&
+      contextIdentifiers.has(parent.parent.parent.callee.object) &&
+      parent.parent.parent.callee.property.name === 'report' &&
+      module.exports.getReportInfo(parent.parent.parent.arguments).fix === node;
+  },
+
+  /**
+   * Whether the provided node represents a suggestion fixer function.
+   * @param {Node} node
+   * @param {Node[]} contextIdentifiers
+   * @returns {boolean}
+   */
+  isSuggestionFixerFunction (node, contextIdentifiers) {
+    const parent = node.parent;
+    return (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') &&
+      parent.type === 'Property' &&
+      parent.key.type === 'Identifier' &&
+      parent.key.name === 'fix' &&
+      parent.parent.type === 'ObjectExpression' &&
+      parent.parent.parent.type === 'ArrayExpression' &&
+      parent.parent.parent.parent.type === 'Property' &&
+      parent.parent.parent.parent.key.type === 'Identifier' &&
+      parent.parent.parent.parent.key.name === 'suggest' &&
+      parent.parent.parent.parent.parent.type === 'ObjectExpression' &&
+      parent.parent.parent.parent.parent.parent.type === 'CallExpression' &&
+      contextIdentifiers.has(parent.parent.parent.parent.parent.parent.callee.object) &&
+      parent.parent.parent.parent.parent.parent.callee.property.name === 'report' &&
+      module.exports.getReportInfo(parent.parent.parent.parent.parent.parent.arguments).suggest === parent.parent.parent;
+  },
 };
diff --git a/package.json b/package.json
index b15172f..3194399 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
     "eslint-plugin-markdown": "^2.0.1",
     "eslint-plugin-node": "^11.1.0",
     "eslint-plugin-self": "^1.2.1",
-    "eslint-plugin-unicorn": "^31.0.0",
+    "eslint-plugin-unicorn": "^36.0.0",
     "eslint-scope": "^5.1.1",
     "espree": "^7.3.0",
     "estraverse": "^5.0.0",


Test patch:
diff --git a/docs/rules/no-only-tests.md b/docs/rules/no-only-tests.md
index 0da555e..8f950f7 100644
--- a/docs/rules/no-only-tests.md
+++ b/docs/rules/no-only-tests.md
@@ -1,5 +1,7 @@
 # Disallow the test case property `only` (no-only-tests)
 
+✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule.
+
 💡 Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
 
 The [`only` property](https://eslint.org/docs/developer-guide/unit-tests#running-individual-tests) can be used as of [ESLint 7.29](https://eslint.org/blog/2021/06/eslint-v7.29.0-released#highlights) for running individual rule test cases with less-noisy debugging. This feature should be only used in development, as it prevents all the tests from running. Mistakenly checking-in a test case with this property can cause CI tests to incorrectly pass.
diff --git a/lib/rules/no-only-tests.js b/lib/rules/no-only-tests.js
index 78f0c6d..56ca29a 100644
--- a/lib/rules/no-only-tests.js
+++ b/lib/rules/no-only-tests.js
@@ -9,15 +9,15 @@ module.exports = {
     docs: {
       description: 'disallow the test case property `only`',
       category: 'Tests',
-      recommended: false,
+      recommended: true,
     },
+    hasSuggestions: true,
     schema: [],
     messages: {
       foundOnly:
         'The test case property `only` can be used during development, but should not be checked-in, since it prevents all the tests from running.',
       removeOnly: 'Remove `only`.',
     },
-    hasSuggestions: true,
   },
 
   create (context) {
diff --git a/tests/lib/rules/fixer-return.js b/tests/lib/rules/fixer-return.js
index 11cf7ab..4e4afec 100644
--- a/tests/lib/rules/fixer-return.js
+++ b/tests/lib/rules/fixer-return.js
@@ -221,6 +221,55 @@ ruleTester.run('fixer-return', rule, {
         }
     };
     `,
+
+    // Suggestion
+    `
+    module.exports = {
+        create: function(context) {
+            context.report( {
+                suggest: [
+                    {
+                        fix: function(fixer) {
+                            return fixer.foo();
+                        }
+                    }
+                ]
+            });
+        }
+    };
+    `,
+    // Suggestion but wrong `suggest` key
+    `
+    module.exports = {
+        create: function(context) {
+            context.report( {
+                notSuggest: [
+                    {
+                        fix: function(fixer) {
+                            fixer.foo();
+                        }
+                    }
+                ]
+            });
+        }
+    };
+    `,
+    // Suggestion but wrong `fix` key
+    `
+    module.exports = {
+        create: function(context) {
+            context.report( {
+                suggest: [
+                    {
+                        notFix: function(fixer) {
+                            fixer.foo();
+                        }
+                    }
+                ]
+            });
+        }
+    };
+    `,
   ],
 
   invalid: [
@@ -239,6 +288,25 @@ ruleTester.run('fixer-return', rule, {
         `,
       errors: [{ messageId: 'missingFix', type: 'FunctionExpression', line: 5, column: 24 }],
     },
+    {
+      // Fix but missing return (suggestion)
+      code: `
+          module.exports = {
+              create: function(context) {
+                    context.report({
+                        suggest: [
+                            {
+                                fix(fixer) {
+                                    fixer.foo();
+                                }
+                            }
+                        ]
+                    });
+              }
+          };
+          `,
+      errors: [{ messageId: 'missingFix', type: 'FunctionExpression', line: 7, column: 36 }],
+    },
     {
       // Fix but missing return (arrow function, report on arrow)
       code: `
@@ -254,6 +322,25 @@ ruleTester.run('fixer-return', rule, {
         `,
       errors: [{ messageId: 'missingFix', type: 'ArrowFunctionExpression', line: 5, endLine: 5, column: 34, endColumn: 36 }],
     },
+    {
+      // Fix but missing return (arrow function, report on arrow, suggestion)
+      code: `
+          module.exports = {
+                create: function(context) {
+                    context.report({
+                        suggest: [
+                            {
+                                fix: (fixer) => {
+                                    fixer.foo();
+                                }
+                            }
+                        ]
+                    });
+                }
+          };
+          `,
+      errors: [{ messageId: 'missingFix', type: 'ArrowFunctionExpression', line: 7, endLine: 7, column: 46, endColumn: 48 }],
+    },
     {
       // With no autofix (arrow function, explicit return, report on arrow)
       code: `
diff --git a/tests/lib/rules/meta-property-ordering.js b/tests/lib/rules/meta-property-ordering.js
index 3d5f094..bc9386c 100644
--- a/tests/lib/rules/meta-property-ordering.js
+++ b/tests/lib/rules/meta-property-ordering.js
@@ -24,6 +24,16 @@ ruleTester.run('test-case-property-ordering', rule, {
       create() {},
     };`,
 
+    {
+      // ESM
+      code: `
+        export default {
+          meta: {type, docs, fixable, schema, messages},
+          create() {},
+        };`,
+      parserOptions: { sourceType: 'module' },
+    },
+
     `
     module.exports = {
       meta: {docs, schema, messages},
@@ -42,6 +52,7 @@ ruleTester.run('test-case-property-ordering', rule, {
         type: 'problem',
         docs: {},
         fixable: 'code',
+        hasSuggestions: true,
         schema: [],
         messages: {}
       },
@@ -85,6 +96,30 @@ ruleTester.run('test-case-property-ordering', rule, {
         };`,
       errors: [{ messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable'].join(', ') } }],
     },
+    {
+      // ESM
+      code: `
+        export default {
+          meta: {
+            docs,
+            fixable,
+            type: 'problem',
+          },
+          create() {},
+        };`,
+
+      output: `
+        export default {
+          meta: {
+            type: 'problem',
+            docs,
+            fixable,
+          },
+          create() {},
+        };`,
+      parserOptions: { sourceType: 'module' },
+      errors: [{ messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable'].join(', ') } }],
+    },
     {
       code: `
         module.exports = {
diff --git a/tests/lib/rules/no-missing-placeholders.js b/tests/lib/rules/no-missing-placeholders.js
index 09b8ce3..997f236 100644
--- a/tests/lib/rules/no-missing-placeholders.js
+++ b/tests/lib/rules/no-missing-placeholders.js
@@ -128,6 +128,24 @@ ruleTester.run('no-missing-placeholders', rule, {
         context.report(node, MESSAGE, { baz: 'qux' });
       };
     `,
+    // Suggestion with placeholder
+    `
+      module.exports = {
+        create(context) {
+          context.report({
+            node,
+            suggest: [
+              {
+                desc: 'Remove {{functionName}}',
+                data: {
+                  functionName: 'foo'
+                }
+              }
+            ]
+          });
+        }
+      };
+    `,
   ],
 
   invalid: [
@@ -212,5 +230,45 @@ ruleTester.run('no-missing-placeholders', rule, {
       `,
       errors: [error('bar')],
     },
+
+    {
+      // Suggestion (no `data`)
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({
+              node,
+              suggest: [
+                {
+                  desc: 'Remove {{bar}}'
+                }
+              ]
+            });
+          }
+        };
+      `,
+      errors: [error('bar')],
+    },
+    {
+      // Suggestion (`data` but missing placeholder)
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({
+              node,
+              suggest: [
+                {
+                  desc: 'Remove {{bar}}',
+                  data: {
+                    notBar: 'abc'
+                  }
+                }
+              ]
+            });
+          }
+        };
+      `,
+      errors: [error('bar')],
+    },
   ],
 });
diff --git a/tests/lib/rules/no-unused-placeholders.js b/tests/lib/rules/no-unused-placeholders.js
index 4646de9..87b6f06 100644
--- a/tests/lib/rules/no-unused-placeholders.js
+++ b/tests/lib/rules/no-unused-placeholders.js
@@ -107,6 +107,22 @@ ruleTester.run('no-unused-placeholders', rule, {
         context.report(node, { line: 1, column: 3 }, 'foo {{bar}}', { bar: 'baz' });
       };
     `,
+    // Suggestion
+    `
+      module.exports = {
+        create(context) {
+          context.report({
+            node,
+            suggest: [
+              {
+                desc: 'foo {{bar}}',
+                data: { 'bar': 'baz' }
+              }
+            ]
+          });
+        }
+      };
+    `,
   ],
 
   invalid: [
@@ -168,5 +184,24 @@ ruleTester.run('no-unused-placeholders', rule, {
       `,
       errors: [error('baz')],
     },
+    {
+      // Suggestion
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({
+              node,
+              suggest: [
+                {
+                  desc: 'foo',
+                  data: { bar }
+                }
+              ]
+            });
+          }
+        };
+      `,
+      errors: [error('bar')],
+    },
   ],
 });
diff --git a/tests/lib/rules/prefer-message-ids.js b/tests/lib/rules/prefer-message-ids.js
index cfabcef..952509f 100644
--- a/tests/lib/rules/prefer-message-ids.js
+++ b/tests/lib/rules/prefer-message-ids.js
@@ -29,6 +29,25 @@ ruleTester.run('prefer-message-ids', rule, {
         }
       };
     `,
+    // Suggestion
+    `
+      module.exports = {
+        create(context) {
+          context.report({ node, suggest: [{messageId:'foo'}] });
+        }
+      };
+    `,
+    {
+      // ESM
+      code: `
+        export default {
+          create(context) {
+            context.report({ node, messageId: 'foo' });
+          }
+        };
+      `,
+      parserOptions: { sourceType: 'module' },
+    },
     `
       module.exports = {
         create(context) {
@@ -91,6 +110,29 @@ ruleTester.run('prefer-message-ids', rule, {
       `,
       errors: [{ messageId: 'foundMessage', type: 'Property' }],
     },
+    {
+      // Suggestion
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({ node, suggest: [{desc:'foo'}] });
+          }
+        };
+      `,
+      errors: [{ messageId: 'foundMessage', type: 'Property' }],
+    },
+    {
+      // ESM
+      code: `
+        export default {
+          create(context) {
+            context.report({ node, message: 'foo' });
+          }
+        };
+      `,
+      parserOptions: { sourceType: 'module' },
+      errors: [{ messageId: 'foundMessage', type: 'Property' }],
+    },
     {
       // With message in variable.
       code: `
diff --git a/tests/lib/rules/prefer-object-rule.js b/tests/lib/rules/prefer-object-rule.js
index 1e48ec2..77cc6f1 100644
--- a/tests/lib/rules/prefer-object-rule.js
+++ b/tests/lib/rules/prefer-object-rule.js
@@ -11,8 +11,6 @@
 const rule = require('../../../lib/rules/prefer-object-rule');
 const RuleTester = require('eslint').RuleTester;
 
-const ERROR = { messageId: 'preferObject', line: 2, column: 26 };
-
 // ------------------------------------------------------------------------------
 // Tests
 // ------------------------------------------------------------------------------
@@ -63,6 +61,18 @@ ruleTester.run('prefer-object-rule', rule, {
       };
       module.exports = rule;
     `,
+
+    {
+      // ESM
+      code: `
+        export default {
+          create(context) {
+            return { Program() { context.report() } };
+          },
+        };
+      `,
+      parserOptions: { sourceType: 'module' },
+    },
   ],
 
   invalid: [
@@ -77,7 +87,7 @@ ruleTester.run('prefer-object-rule', rule, {
           return { Program() { context.report() } };
         }};
       `,
-      errors: [ERROR],
+      errors: [{ messageId: 'preferObject', line: 2, column: 26 }],
     },
     {
       code: `
@@ -90,7 +100,7 @@ ruleTester.run('prefer-object-rule', rule, {
           return { Program() { context.report() } };
         }};
       `,
-      errors: [ERROR],
+      errors: [{ messageId: 'preferObject', line: 2, column: 26 }],
     },
     {
       code: `
@@ -103,7 +113,35 @@ ruleTester.run('prefer-object-rule', rule, {
           return { Program() { context.report() } };
         }};
       `,
-      errors: [ERROR],
+      errors: [{ messageId: 'preferObject', line: 2, column: 26 }],
+    },
+
+    // ESM
+    {
+      code: `
+        export default function (context) {
+          return { Program() { context.report() } };
+        };
+      `,
+      output: `
+        export default {create(context) {
+          return { Program() { context.report() } };
+        }};
+      `,
+      parserOptions: { sourceType: 'module' },
+      errors: [{ messageId: 'preferObject', line: 2, column: 24 }],
+    },
+    {
+      code: 'export default function create() {};',
+      output: 'export default {create() {}};',
+      parserOptions: { sourceType: 'module' },
+      errors: [{ messageId: 'preferObject', line: 1, column: 16 }],
+    },
+    {
+      code: 'export default () => {};',
+      output: 'export default {create: () => {}};',
+      parserOptions: { sourceType: 'module' },
+      errors: [{ messageId: 'preferObject', line: 1, column: 16 }],
     },
   ],
 });
diff --git a/tests/lib/rules/prefer-placeholders.js b/tests/lib/rules/prefer-placeholders.js
index 2de61b8..1bf9f40 100644
--- a/tests/lib/rules/prefer-placeholders.js
+++ b/tests/lib/rules/prefer-placeholders.js
@@ -17,8 +17,6 @@ const RuleTester = require('eslint').RuleTester;
 // ------------------------------------------------------------------------------
 
 const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
-const ERROR = { messageId: 'usePlaceholders' };
-
 
 ruleTester.run('prefer-placeholders', rule, {
   valid: [
@@ -78,6 +76,44 @@ ruleTester.run('prefer-placeholders', rule, {
         }
       };
     `,
+    // Suggestion (message + data)
+    `
+      module.exports = {
+        create(context) {
+          context.report({
+            node,
+            suggest: [
+              {
+                message: '{{foo}} is bad.',
+                data: { foo },
+              }
+            ]
+          });
+        }
+      };
+    `,
+    // Suggestion (message)
+    `
+      module.exports = {
+        create(context) {
+          context.report({
+            node,
+            suggest: [ { message: 'hello world' }]
+          });
+        }
+      };
+    `,
+    // Suggestion (messageId)
+    `
+      module.exports = {
+        create(context) {
+          context.report({
+            node,
+            suggest: [ { messageId: 'myMessageId' }]
+          });
+        }
+      };
+    `,
   ],
 
   invalid: [
@@ -92,7 +128,23 @@ ruleTester.run('prefer-placeholders', rule, {
           }
         };
       `,
-      errors: [ERROR],
+      errors: [{ messageId: 'usePlaceholders', type: 'TemplateLiteral' }],
+    },
+    {
+      // Suggestion
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({
+              node,
+              suggest: [
+                { desc: \`\${foo} is bad.\` }
+              ]
+            });
+          }
+        };
+      `,
+      errors: [{ messageId: 'usePlaceholders', type: 'TemplateLiteral' }],
     },
     {
       // With message in variable.
@@ -107,7 +159,7 @@ ruleTester.run('prefer-placeholders', rule, {
           }
         };
       `,
-      errors: [ERROR],
+      errors: [{ messageId: 'usePlaceholders', type: 'TemplateLiteral' }],
     },
     {
       code: `
@@ -120,7 +172,7 @@ ruleTester.run('prefer-placeholders', rule, {
           }
         };
       `,
-      errors: [ERROR],
+      errors: [{ messageId: 'usePlaceholders', type: 'BinaryExpression' }],
     },
     {
       code: `
@@ -130,7 +182,7 @@ ruleTester.run('prefer-placeholders', rule, {
           }
         };
       `,
-      errors: [ERROR],
+      errors: [{ messageId: 'usePlaceholders', type: 'TemplateLiteral' }],
     },
   ],
 });
diff --git a/tests/lib/rules/prefer-replace-text.js b/tests/lib/rules/prefer-replace-text.js
index 10777b2..1341c71 100644
--- a/tests/lib/rules/prefer-replace-text.js
+++ b/tests/lib/rules/prefer-replace-text.js
@@ -17,8 +17,6 @@ const RuleTester = require('eslint').RuleTester;
 // ------------------------------------------------------------------------------
 
 const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
-const ERROR = { messageId: 'useReplaceText', type: 'CallExpression' };
-
 
 ruleTester.run('prefer-placeholders', rule, {
   valid: [
@@ -52,6 +50,23 @@ ruleTester.run('prefer-placeholders', rule, {
     `
       fixer.replaceTextRange([node.range[0], node.range[1]], '');
     `,
+
+    // Suggestion
+    `
+      module.exports = {
+        create(context) {
+          context.report({
+            suggest: [
+              {
+                fix(fixer) {
+                  return fixer.replaceTextRange([start, end], '');
+                }
+              }
+            ]
+          });
+        }
+      };
+    `,
   ],
 
   invalid: [
@@ -67,7 +82,7 @@ ruleTester.run('prefer-placeholders', rule, {
           }
         };
     `,
-      errors: [ERROR],
+      errors: [{ messageId: 'useReplaceText', type: 'CallExpression' }],
     },
     {
       code: `
@@ -81,7 +96,7 @@ ruleTester.run('prefer-placeholders', rule, {
           }
         };
     `,
-      errors: [ERROR],
+      errors: [{ messageId: 'useReplaceText', type: 'CallExpression' }],
     },
     {
       code: `
@@ -95,7 +110,7 @@ ruleTester.run('prefer-placeholders', rule, {
           }
         };
     `,
-      errors: [ERROR],
+      errors: [{ messageId: 'useReplaceText', type: 'CallExpression' }],
     },
     {
       code: `
@@ -107,7 +122,7 @@ ruleTester.run('prefer-placeholders', rule, {
           }
         };
     `,
-      errors: [ERROR],
+      errors: [{ messageId: 'useReplaceText', type: 'CallExpression' }],
     },
     {
       code: `
@@ -121,7 +136,27 @@ ruleTester.run('prefer-placeholders', rule, {
           }
         };
     `,
-      errors: [ERROR],
+      errors: [{ messageId: 'useReplaceText', type: 'CallExpression' }],
+    },
+
+    {
+      // Suggestion fixer
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({
+              suggest: [
+                {
+                  fix(fixer) {
+                    return fixer.replaceTextRange([node.range[0], node.range[1]], '');
+                  }
+                }
+              ]
+            });
+          }
+        };
+    `,
+      errors: [{ messageId: 'useReplaceText', type: 'CallExpression' }],
     },
   ],
 });
diff --git a/tests/lib/rules/report-message-format.js b/tests/lib/rules/report-message-format.js
index d93543d..1f637d3 100644
--- a/tests/lib/rules/report-message-format.js
+++ b/tests/lib/rules/report-message-format.js
@@ -33,6 +33,18 @@ ruleTester.run('report-message-format', rule, {
       `,
       options: ['foo'],
     },
+    {
+      // ESM
+      code: `
+        export default {
+          create(context) {
+            context.report(node, 'foo');
+          }
+        };
+      `,
+      options: ['foo'],
+      parserOptions: { sourceType: 'module' },
+    },
     {
       // With message as variable.
       code: `
@@ -97,6 +109,28 @@ ruleTester.run('report-message-format', rule, {
       `,
       options: ['^foo$'],
     },
+    {
+      // Suggestion message
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({node, suggest: [{message: 'foo'}]});
+          }
+        };
+      `,
+      options: ['^foo$'],
+    },
+    {
+      // Suggestion message with ternary expression
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({node, suggest: foo ? []: [{}]});
+          }
+        };
+      `,
+      options: ['^foo$'],
+    },
     {
       code: `
         module.exports = {
@@ -164,6 +198,18 @@ ruleTester.run('report-message-format', rule, {
       `,
       options: ['foo'],
     },
+    {
+      // ESM
+      code: `
+        export default {
+          create(context) {
+            context.report(node, 'bar');
+          }
+        };
+      `,
+      options: ['foo'],
+      parserOptions: { sourceType: 'module' },
+    },
     {
       // With message as variable.
       code: `
@@ -226,6 +272,17 @@ ruleTester.run('report-message-format', rule, {
       `,
       options: ['foo'],
     },
+    {
+      // Suggestion message
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({node, suggest: [{message: 'FOO'}]});
+          }
+        };
+      `,
+      options: ['foo'],
+    },
     {
       code: `
         module.exports = {
diff --git a/tests/lib/rules/require-meta-docs-description.js b/tests/lib/rules/require-meta-docs-description.js
index e8c6710..6c9aceb 100644
--- a/tests/lib/rules/require-meta-docs-description.js
+++ b/tests/lib/rules/require-meta-docs-description.js
@@ -21,6 +21,16 @@ ruleTester.run('require-meta-docs-description', rule, {
         create(context) {}
       };
     `,
+    {
+      // ESM
+      code: `
+        export default {
+          meta: { docs: { description: 'disallow unused variables' } },
+          create(context) {}
+        };
+      `,
+      parserOptions: { sourceType: 'module' },
+    },
     `
       module.exports = {
         meta: { docs: { description: 'enforce a maximum line length' } },
@@ -104,6 +114,18 @@ ruleTester.run('require-meta-docs-description', rule, {
       output: null,
       errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
+    {
+      // ESM
+      code: `
+        export default {
+          meta: {},
+          create(context) {}
+        };
+      `,
+      output: null,
+      parserOptions: { sourceType: 'module' },
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
+    },
     {
       code: `
         module.exports = {
diff --git a/tests/lib/rules/require-meta-docs-url.js b/tests/lib/rules/require-meta-docs-url.js
index b7195ae..0f75594 100644
--- a/tests/lib/rules/require-meta-docs-url.js
+++ b/tests/lib/rules/require-meta-docs-url.js
@@ -66,6 +66,20 @@ tester.run('require-meta-docs-url', rule, {
         pattern: 'path/to/{{name}}.md',
       }],
     },
+    {
+      // ESM
+      filename: 'test-rule',
+      code: `
+        export default {
+          meta: {docs: {url: "path/to/test-rule.md"}},
+          create() {}
+        }
+      `,
+      options: [{
+        pattern: 'path/to/{{name}}.md',
+      }],
+      parserOptions: { sourceType: 'module' },
+    },
     {
       // `url` in variable.
       filename: 'test-rule',
@@ -537,6 +551,31 @@ url: "plugin-name/test.md"
       }],
       errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
+    {
+      // ESM
+      filename: 'test.js',
+      code: `
+        export default {
+          meta: {},
+          create() {}
+        }
+      `,
+      output: `
+        export default {
+          meta: {
+docs: {
+url: "plugin-name/test.md"
+}
+},
+          create() {}
+        }
+      `,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      parserOptions: { sourceType: 'module' },
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
+    },
     {
       filename: 'test.js',
       code: `
diff --git a/tests/lib/rules/require-meta-fixable.js b/tests/lib/rules/require-meta-fixable.js
index ce4761b..417cf42 100644
--- a/tests/lib/rules/require-meta-fixable.js
+++ b/tests/lib/rules/require-meta-fixable.js
@@ -34,6 +34,18 @@ ruleTester.run('require-meta-fixable', rule, {
         }
       };
     `,
+    {
+      // ESM
+      code: `
+        export default {
+          meta: { fixable: 'code' },
+          create(context) {
+            context.report({node, message, fix: foo});
+          }
+        };
+      `,
+      parserOptions: { sourceType: 'module' },
+    },
     // Value in variable.
     `
       const fixable = 'code';
@@ -183,6 +195,17 @@ ruleTester.run('require-meta-fixable', rule, {
       `,
       errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
+    {
+      // ESM
+      code: `
+        export default {
+          meta: {},
+          create(context) { context.report({node, message, fix: foo}); }
+        };
+      `,
+      parserOptions: { sourceType: 'module' },
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
+    },
     {
       code: `
         module.exports = {
diff --git a/tests/lib/rules/require-meta-has-suggestions.js b/tests/lib/rules/require-meta-has-suggestions.js
index 0695e34..6218a4a 100644
--- a/tests/lib/rules/require-meta-has-suggestions.js
+++ b/tests/lib/rules/require-meta-has-suggestions.js
@@ -112,6 +112,18 @@ ruleTester.run('require-meta-has-suggestions', rule, {
         }
       };
     `,
+    {
+      // ESM: Provides suggestions, has hasSuggestions property.
+      code: `
+        export default {
+          meta: { hasSuggestions: true },
+          create(context) {
+            context.report({node, message, suggest: [{}]});
+          }
+        };
+      `,
+      parserOptions: { sourceType: 'module' },
+    },
     // Provides suggestions, has hasSuggestions property (as variable).
     `
       const hasSuggestions = true;
@@ -186,6 +198,17 @@ ruleTester.run('require-meta-has-suggestions', rule, {
       output: null,
       errors: [{ messageId: 'shouldBeSuggestable', type: 'FunctionExpression', line: 3, column: 17, endLine: 3, endColumn: 78 }],
     },
+    {
+      // ESM: Reports suggestions, no meta object, violation should be on `create` function.
+      code: `
+        export default {
+          create(context) { context.report({node, message, suggest: [{}]}); }
+        };
+      `,
+      output: null,
+      parserOptions: { sourceType: 'module' },
+      errors: [{ messageId: 'shouldBeSuggestable', type: 'FunctionExpression', line: 3, column: 17, endLine: 3, endColumn: 78 }],
+    },
     {
       // Reports suggestions, no hasSuggestions property, violation should be on `meta` object, empty meta object.
       code: `
diff --git a/tests/lib/rules/require-meta-schema.js b/tests/lib/rules/require-meta-schema.js
index 7450ea7..07a0e5b 100644
--- a/tests/lib/rules/require-meta-schema.js
+++ b/tests/lib/rules/require-meta-schema.js
@@ -53,6 +53,16 @@ ruleTester.run('require-meta-schema', rule, {
         create(context) { const options = foo.options; }
       };
     `,
+    {
+    // ESM
+      code: `
+        export default {
+          meta: { schema: { "enum": ["always", "never"] } },
+          create(context) { const options = context.options; }
+        };
+      `,
+      parserOptions: { sourceType: 'module' },
+    },
     `
       const schema = [];
       module.exports = {
@@ -144,6 +154,36 @@ schema: []
         module.exports = {
           meta: {
 schema: []
+},
+          create(context) {}
+        };
+      `,
+            },
+          ],
+        },
+      ],
+    },
+    {
+      // ESM
+      code: `
+        export default {
+          meta: {},
+          create(context) {}
+        };
+      `,
+      output: null,
+      parserOptions: { sourceType: 'module' },
+      errors: [
+        {
+          messageId: 'missing',
+          type: 'ObjectExpression',
+          suggestions: [
+            {
+              messageId: 'addEmptySchema',
+              output: `
+        export default {
+          meta: {
+schema: []
 },
           create(context) {}
         };
diff --git a/tests/lib/rules/require-meta-type.js b/tests/lib/rules/require-meta-type.js
index 25ca099..33b118c 100644
--- a/tests/lib/rules/require-meta-type.js
+++ b/tests/lib/rules/require-meta-type.js
@@ -25,6 +25,16 @@ ruleTester.run('require-meta-type', rule, {
         create(context) {}
       };
     `,
+    {
+      // ESM
+      code: `
+        export default {
+          meta: { type: 'problem' },
+          create(context) {}
+        };
+      `,
+      parserOptions: { sourceType: 'module' },
+    },
     `
       module.exports = {
         meta: { type: 'suggestion' },
@@ -81,6 +91,17 @@ ruleTester.run('require-meta-type', rule, {
       `,
       errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
+    {
+      // ESM
+      code: `
+        export default {
+          meta: {},
+          create(context) {}
+        };
+      `,
+      parserOptions: { sourceType: 'module' },
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
+    },
     {
       code: `
         function create(context) {}
diff --git a/tests/lib/utils.js b/tests/lib/utils.js
index 8e28a93..9579dde 100644
--- a/tests/lib/utils.js
+++ b/tests/lib/utils.js
@@ -33,8 +33,23 @@ describe('utils', () => {
       });
     });
 
+    describe('the file does not have a valid rule (ESM)', () => {
+      [
+        '',
+        'export const foo = { create() {} }',
+        'export default { foo: {} }',
+        'const foo = {}; export default foo',
+      ].forEach(noRuleCase => {
+        it(`returns null for ${noRuleCase}`, () => {
+          const ast = espree.parse(noRuleCase, { ecmaVersion: 8, range: true, sourceType: 'module' });
+          assert.isNull(utils.getRuleInfo({ ast }), 'Expected no rule to be found');
+        });
+      });
+    });
+
     describe('the file has a valid rule', () => {
       const CASES = {
+        // CJS
         'module.exports = { create: function foo() {} };': {
           create: { type: 'FunctionExpression', id: { name: 'foo' } }, // (This property will actually contain the AST node.)
           meta: null,
@@ -110,11 +125,35 @@ describe('utils', () => {
           meta: null,
           isNewStyle: false,
         },
+
+        // ESM (object style)
+        'export default { create() {} }': {
+          create: { type: 'FunctionExpression' },
+          meta: null,
+          isNewStyle: true,
+        },
+        'export default { create() {}, meta: {} }': {
+          create: { type: 'FunctionExpression' },
+          meta: { type: 'ObjectExpression' },
+          isNewStyle: true,
+        },
+
+        // ESM (function style)
+        'export default function () {}': {
+          create: { type: 'FunctionDeclaration' },
+          meta: null,
+          isNewStyle: false,
+        },
+        'export default () => {}': {
+          create: { type: 'ArrowFunctionExpression' },
+          meta: null,
+          isNewStyle: false,
+        },
       };
 
       Object.keys(CASES).forEach(ruleSource => {
         it(ruleSource, () => {
-          const ast = espree.parse(ruleSource, { ecmaVersion: 6, range: true });
+          const ast = espree.parse(ruleSource, { ecmaVersion: 6, range: true, sourceType: ruleSource.startsWith('export default') ? 'module' : 'script' });
           const ruleInfo = utils.getRuleInfo({ ast });
           assert(
             lodash.isMatch(ruleInfo, CASES[ruleSource]),
@@ -382,4 +421,107 @@ describe('utils', () => {
       });
     });
   });
+
+  describe('collectReportViolationAndSuggestionData', () => {
+    const CASES = [
+      {
+        code: `
+          context.report({
+            node: {},
+            message: "message1",
+            messageId: "messageId1",
+            data: { foo: 'hello' },
+            fix(fixer) {},
+            suggest: [{
+              desc: "message2",
+              messageId: "messageId2",
+              data: { bar: 'world' },
+              fix(fixer) {},
+            }]
+          });
+        `,
+        shouldMatch: [
+          {
+            message: { type: 'Literal', value: 'message1' },
+            messageId: { type: 'Literal', value: 'messageId1' },
+            data: { type: 'ObjectExpression', properties: [{ key: { name: 'foo' } }] },
+            fix: { type: 'FunctionExpression' },
+          },
+          {
+            message: { type: 'Literal', value: 'message2' },
+            messageId: { type: 'Literal', value: 'messageId2' },
+            data: { type: 'ObjectExpression', properties: [{ key: { name: 'bar' } }] },
+            fix: { type: 'FunctionExpression' },
+          },
+        ],
+      },
+    ];
+
+    it('behaves correctly', () => {
+      for (const testCase of CASES) {
+        const ast = espree.parse(testCase.code, { ecmaVersion: 6, range: true });
+        const context = { getScope () {} }; // mock object
+        const reportNode = ast.body[0].expression;
+        const reportInfo = utils.getReportInfo(reportNode.arguments, context);
+        const data = utils.collectReportViolationAndSuggestionData(reportInfo);
+        assert(
+          lodash.isMatch(data, testCase.shouldMatch),
+          `Expected \n${inspect(data)}\nto match\n${inspect(testCase.shouldMatch)}`
+        );
+      }
+    });
+  });
+
+  describe('isAutoFixerFunction / isSuggestionFixerFunction', () => {
+    const CASES = {
+      // isAutoFixerFunction
+      'context.report({ fix(fixer) {} });' (ast) {
+        return { expected: true, node: ast.body[0].expression.arguments[0].properties[0].value, context: ast.body[0].expression.callee.object, fn: utils.isAutoFixerFunction };
+      },
+      'context.notReport({ fix(fixer) {} });' (ast) {
+        return { expected: false, node: ast.body[0].expression.arguments[0].properties[0].value, context: ast.body[0].expression.callee.object, fn: utils.isAutoFixerFunction };
+      },
+      'context.report({ notFix(fixer) {} });' (ast) {
+        return { expected: false, node: ast.body[0].expression.arguments[0].properties[0].value, context: ast.body[0].expression.callee.object, fn: utils.isAutoFixerFunction };
+      },
+      'notContext.report({ notFix(fixer) {} });' (ast) {
+        return { expected: false, node: ast.body[0].expression.arguments[0].properties[0].value, context: undefined, fn: utils.isAutoFixerFunction };
+      },
+
+      // isSuggestionFixerFunction
+      'context.report({ suggest: [{ fix(fixer) {} }] });' (ast) {
+        return { expected: true, node: ast.body[0].expression.arguments[0].properties[0].value.elements[0].properties[0].value, context: ast.body[0].expression.callee.object, fn: utils.isSuggestionFixerFunction };
+      },
+      'context.notReport({ suggest: [{ fix(fixer) {} }] });' (ast) {
+        return { expected: false, node: ast.body[0].expression.arguments[0].properties[0].value.elements[0].properties[0].value, context: ast.body[0].expression.callee.object, fn: utils.isSuggestionFixerFunction };
+      },
+      'context.report({ notSuggest: [{ fix(fixer) {} }] });' (ast) {
+        return { expected: false, node: ast.body[0].expression.arguments[0].properties[0].value.elements[0].properties[0].value, context: ast.body[0].expression.callee.object, fn: utils.isSuggestionFixerFunction };
+      },
+      'context.report({ suggest: [{ notFix(fixer) {} }] });' (ast) {
+        return { expected: false, node: ast.body[0].expression.arguments[0].properties[0].value.elements[0].properties[0].value, context: ast.body[0].expression.callee.object, fn: utils.isSuggestionFixerFunction };
+      },
+      'notContext.report({ suggest: [{ fix(fixer) {} }] });' (ast) {
+        return { expected: false, node: ast.body[0].expression.arguments[0].properties[0].value, context: undefined, fn: utils.isSuggestionFixerFunction };
+      },
+    };
+
+    Object.keys(CASES).forEach(ruleSource => {
+      it(ruleSource, () => {
+        const ast = espree.parse(ruleSource, { ecmaVersion: 6, range: true });
+
+        // Add parent to each node.
+        estraverse.traverse(ast, {
+          enter (node, parent) {
+            node.parent = parent;
+          },
+        });
+
+        const testCase = CASES[ruleSource](ast);
+        const contextIdentifiers = new Set([testCase.context]);
+        const result = testCase.fn(testCase.node, contextIdentifiers);
+        assert.strictEqual(result, testCase.expected);
+      });
+    });
+  });
 });