Repository: not-an-aardvark/eslint-plugin-eslint-plugin
Pull request #119 (2021-06-12 18:35:19)

Breaking: Add `meta.hasSuggestions` property to `meta-property-ordering` rule
It makes most sense to order the new ESLint 8 property `meta.hasSuggestions` (whether a rule provides suggestions) after the related property `meta.fixable` (whether a rule is autofixable).

https://eslint.org/blog/2021/06/whats-coming-in-eslint-8.0.0#rules-with-suggestions-now-require-the-metahassuggestions-property

aladdin-add: it seems a breaking change to change the default options. :)
aladdin-add: we need to resolve the conflicts. :)

Patch:
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/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/prefer-object-rule.js b/lib/rules/prefer-object-rule.js
index 72b9920..8873ec0 100644
--- a/lib/rules/prefer-object-rule.js
+++ b/lib/rules/prefer-object-rule.js
@@ -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/require-meta-schema.js b/lib/rules/require-meta-schema.js
index 976aea3..29727e0 100644
--- a/lib/rules/require-meta-schema.js
+++ b/lib/rules/require-meta-schema.js
@@ -15,6 +15,7 @@ module.exports = {
       category: 'Rules',
       recommended: false, // TODO: enable it in a major release.
     },
+    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/utils.js b/lib/utils.js
index 024360b..f07d499 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -83,8 +83,93 @@ 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;
+    }, {});
+}
+
+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,63 +179,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')
-      // 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 };
-          } else if (node.right.type === 'ObjectExpression') {
-            // Check `module.exports = { create: function () {}, meta: {} }`
-
-            exportsIsFunction = false;
-            // eslint-disable-next-line unicorn/prefer-object-from-entries
-            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) {
@@ -164,7 +193,7 @@ module.exports = {
       return null;
     }
 
-    return Object.assign({ isNewStyle: !exportsIsFunction, meta: null }, exportNodes);
+    return Object.assign({ isNewStyle: true, meta: null }, exportNodes);
   },
 
   /**


Test patch:
diff --git a/lib/rules/no-only-tests.js b/lib/rules/no-only-tests.js
index 78f0c6d..59b9edb 100644
--- a/lib/rules/no-only-tests.js
+++ b/lib/rules/no-only-tests.js
@@ -11,13 +11,13 @@ module.exports = {
       category: 'Tests',
       recommended: false,
     },
+    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/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/prefer-message-ids.js b/tests/lib/rules/prefer-message-ids.js
index cfabcef..2dc5343 100644
--- a/tests/lib/rules/prefer-message-ids.js
+++ b/tests/lib/rules/prefer-message-ids.js
@@ -29,6 +29,17 @@ ruleTester.run('prefer-message-ids', rule, {
         }
       };
     `,
+    {
+      // ESM
+      code: `
+        export default {
+          create(context) {
+            context.report({ node, messageId: 'foo' });
+          }
+        };
+      `,
+      parserOptions: { sourceType: 'module' },
+    },
     `
       module.exports = {
         create(context) {
@@ -91,6 +102,18 @@ ruleTester.run('prefer-message-ids', rule, {
       `,
       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/report-message-format.js b/tests/lib/rules/report-message-format.js
index d93543d..0dff15a 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: `
@@ -164,6 +176,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: `
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..ac97bd3 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]),