Repository: not-an-aardvark/eslint-plugin-eslint-plugin
Pull request #151 (2021-06-26 15:59:56)

Breaking: Drop support for Node 10, 13, 15
We now support Node 12, 14, 16+.

* Node 10 end-of-life 2021-04-30
* Node 13 end-of-life 2020-06-01
* Node 15 end-of-life 2021-06-01

https://nodejs.org/en/about/releases/
https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V12.md
https://en.wikipedia.org/wiki/Node.js#Releases

- [x] Should we raise these versions even higher? [ESM for example](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#how-can-i-move-my-commonjs-project-to-esm) requires `"^12.20.0 || ^14.13.1 || >=16.0.0"` (update: not now, see comments)

Part of the v4 release (https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/120).


Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index 445cfc8..352444a 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -5,13 +5,9 @@ module.exports = {
   plugins: ['node'],
   extends: [
     'not-an-aardvark/node',
-    'plugin:node/recommended',
     'plugin:unicorn/recommended',
+    'plugin:node/recommended',
   ],
-  parserOptions: {
-    ecmaVersion: 2021,
-    sourceType: 'script',
-  },
   rules: {
     'comma-dangle': [
       'error',
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index ae54b4b..93d483a 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -15,9 +15,6 @@ jobs:
           - '16'
           - '14'
           - '12'
-          - '12.0'
-          - '10'
-          - '10.12'
         os:
           - ubuntu-latest
     steps:
@@ -35,3 +32,12 @@ jobs:
       - uses: actions/setup-node@v2
       - run: npm install
       - run: npm run lint
+
+  eslint6:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v2
+    - uses: actions/setup-node@v2
+    - run: npm install
+    - run: npm install --save-dev eslint@6
+    - run: npm test
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4d7fe49..7f0d496 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,59 @@
 # Changelog
 
+## v3.6.1 (2021-09-24)
+
+* Fix: Change autofix to suggestion in `require-meta-schema` rule ([#185](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/185)) ([afc1514](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/afc15149779647265b23ba8c4c181376eeb58795))
+* Fix: only autofix in require-meta-schema rule when no options present ([#184](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/184)) ([d2d165d](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/d2d165dd70686f77a7204f9e4bf7a048a5e42942))
+
+## v3.6.0 (2021-09-24)
+
+* New: Add `requireSchemaPropertyWhenOptionless` option to `require-meta-schema` rule ([#180](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/180)) ([483f78f](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/483f78fb69f074189b78916efa8bf89f084f2f8a))
+* Fix: Remove erroneous schema from require-meta-schema rule ([#178](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/178)) ([2f9b2b0](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/2f9b2b0671ebbcd3e76cf746d83371b131e375ac))
+* Test: Add CI test for ESLint 6 compatibility ([#174](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/174)) ([30bb8e2](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/30bb8e2231c79f5010cf53763482edc70ffb4507))
+
+## v3.5.3 (2021-07-30)
+
+* Fix: False negative in `prefer-message-ids` rule ([#173](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/173)) ([c5c4b62](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/c5c4b62be720768aeb13c55b003566d2c38211cb))
+
+## v3.5.2 (2021-07-29)
+
+* Fix: support eslint v6 ([#172](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/172)) ([f7384ad](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/f7384ad76a0511cde29444256e16e9d273384cb0))
+
+## v3.5.1 (2021-07-27)
+
+* Fix: Fix false positive with empty array variable in `require-meta-has-suggestions` rule ([#171](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/171)) ([fffa881](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/fffa881fdaf1575834832e0e16df2cddb913008c))
+
+## v3.5.0 (2021-07-27)
+
+* Update: Add `catchNoFixerButFixableProperty` option (default false) to catch non-fixable rules that enable the fixable property in `require-meta-fixable` rule ([#165](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/165)) ([da652aa](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/da652aa2c13f55627503067968cc843a4732eb26))
+* New: Add new rule `prefer-message-ids` ([#170](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/170)) ([95021dd](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/95021dd03baefe8f06d959d45fefd23c893fa832))
+
+## v3.4.0 (2021-07-12)
+
+* Fix: Ensure `require-meta-*` rules test null/undefined property values ([#164](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/164)) ([990f8f6](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/990f8f6ab1bb7bf60939e2455ea3fae086bb90ae))
+* Chore: Improve test coverage in a few places ([#167](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/167)) ([cb9276e](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/cb9276e8a6490bb7023d5a6cb9fc4be5971341cf))
+* Docs: Improve consistency of `require-meta-*` rule violation messages ([#166](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/166)) ([1da1acc](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/1da1acc3a0193087947b803f9f1a6233362511af))
+* Update: Add autofixer to `require-meta-has-suggestions` rule ([#168](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/168)) ([a0a39c6](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/a0a39c6bc33b74b1f399a9099f98248c0c6d9577))
+* Docs: Fix incorrect CLI option link ([#169](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/169)) ([639da89](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/639da89998bc30e68ee5bfe8c8a00671fead5c99))
+* Fix: Improve detection of static `url` strings in `require-meta-docs-url` rule ([#162](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/162)) ([0459f12](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/0459f1288cda0174ec4622862d3e6d60eaff3889))
+* Update: Fix false positives/negatives in `require-meta-fixable` rule ([#158](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/158)) ([dc29b03](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/dc29b03e4fa75827b2b009e29b9836c801f5f9f0))
+* Docs: Fix typo ([#160](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/160)) ([88cb2bf](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/88cb2bf11e5f8162b19934f7152a1a2adaef8f27))
+* Fix: Avoid crash with non-static value of `hasSuggestions` in `require-meta-has-suggestions` rule ([#163](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/163)) ([5c83cd9](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/5c83cd9d0b49adfc408936511b560f957d3cbbcb))
+* Chore: Fully adopt `messageId` in `require-meta-docs-description` rule ([#161](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/161)) ([b0b170e](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/b0b170e11c7d86bfb34a3412edacedaed35268ab))
+* Docs: Update rule descriptions for consistency ([#159](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/159)) ([c10afb8](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/c10afb833c294195e83af4d018c7bf425379fc13))
+* Fix: Use token utilities from eslint-utils ([#156](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/156)) ([5ac45f0](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/5ac45f08bb943c097d3d10ee088a4a8f4f8e4de8))
+* Fix: Fix false negatives and reporting location in `require-meta-type` ([#155](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/155)) ([7c0d1d0](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/7c0d1d06050d3e68de7623a81d4022886ae457a6))
+* Chore: remove unnecessary ignore pattern from internal js linting ([#154](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/154)) ([4aa9aca](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/4aa9aca1dc27e224bb55fb6f1d5bdb565c73c6a1))
+
+## v3.3.0 (2021-07-02)
+
+* Chore: improve test coverage of `no-identical-tests` rule ([#153](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/153)) ([281d4e5](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/281d4e58d48fc5fad6ba033df9d412cfcb8ed99c))
+* Docs: ensure rule doc titles match rule descriptions ([#147](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/147)) ([c55a956](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/c55a95616dd2e1e832fb082c4e686f4dc271d931))
+* Update: add the plugin name to plugins prop of presets ([#91](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/91)) ([e825c56](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/e825c5692361da151e75b69b9dca196d30b6d465))
+* Docs: indicate rules with suggestions ([#146](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/146)) ([cd65a5c](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/cd65a5cedc9adc5874b789ccfffe2ac2c2041abb))
+* New: add new rule `no-only-tests` ([#145](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/145)) ([f0ac31c](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/f0ac31c967eb05dd5da95e5ef15c26ce48f6d976))
+* Fix: update fixer-return rule to handle arrow function expressions ([#144](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/144)) ([6762a3f](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/6762a3fa61fde2e9ae43576bd695d31da7ab5736))
+
 ## v3.2.0 (2021-06-23)
 
 * Fix: Improve detection of fix functions that never return a fix in `fixer-return` rule ([#143](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/143)) ([65cfb2c](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/65cfb2cd78484f2072bb1f150d07c6fa299579ed))
@@ -87,7 +141,7 @@
 
 * New: require-meta-type (fixes [#67](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/67)) ([#68](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/68)) ([7f87941](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/7f8794159aae178fdd6f069ed9d4dee27367633a))
 * Update: ensure report-message-format checks formatting in meta.messages ([#72](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/72)) ([1ffb48a](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/1ffb48aec79c278562729698bff93493ee5ac20e))
-* Upgrade: dev dependcies to latest ([#69](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/69)) ([9dad54f](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/9dad54f6b3e148068f6322011f4c5c63bd4178c0))
+* Upgrade: dev dependencies to latest ([#69](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/69)) ([9dad54f](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/9dad54f6b3e148068f6322011f4c5c63bd4178c0))
 * Breaking: require node >= 6 & eslint >= 5 (fixes [#70](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/70)) ([#71](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/issues/71)) ([c1778af](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/commit/c1778af090dc88f122101e3cf6ea653b5bc49778))
 
 ## v1.4.1 (2018-10-24)
diff --git a/README.md b/README.md
index 6cd7cc6..3ae6675 100644
--- a/README.md
+++ b/README.md
@@ -47,27 +47,28 @@ Then configure the rules you want to use under the rules section.
 <!-- __BEGIN AUTOGENERATED TABLE__ -->
 Name | ✔️ | 🛠 | 💡 | Description
 ----- | ----- | ----- | ----- | -----
-[consistent-output](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/consistent-output.md) |  |  |  | enforce consistent use of output assertions in rule tests
-[fixer-return](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/fixer-return.md) | ✔️ |  |  | require fixer function to return a fix
+[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-report-api](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-deprecated-report-api.md) | ✔️ | 🛠 |  | disallow use of the deprecated context.report() API
+[no-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-unused-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-unused-placeholders.md) | ✔️ |  |  | disallow unused placeholders in rule report messages
-[no-useless-token-range](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-useless-token-range.md) | ✔️ | 🛠 |  | disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken
-[prefer-object-rule](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-object-rule.md) |  | 🛠 |  | disallow rule exports where the export is a function.
-[prefer-output-null](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-output-null.md) |  | 🛠 |  | disallow invalid RuleTester test cases with the output the same as the code.
-[prefer-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-placeholders.md) |  |  |  | disallow template literals as report messages
-[prefer-replace-text](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-replace-text.md) |  |  |  | require using replaceText instead of replaceTextRange.
+[no-useless-token-range](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-useless-token-range.md) | ✔️ | 🛠 |  | disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()`
+[prefer-message-ids](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-message-ids.md) |  |  |  | require using `messageId` instead of `message` to report rule violations
+[prefer-object-rule](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-object-rule.md) |  | 🛠 |  | disallow rule exports where the export is a function
+[prefer-output-null](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-output-null.md) |  | 🛠 |  | disallow invalid RuleTester test cases where the `output` matches the `code`
+[prefer-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-placeholders.md) |  |  |  | require using placeholders for dynamic report messages
+[prefer-replace-text](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/prefer-replace-text.md) |  |  |  | require using `replaceText()` instead of `replaceTextRange()`
 [report-message-format](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/report-message-format.md) |  |  |  | enforce a consistent format for rule report messages
-[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-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
 [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 2fddf84..72db0bc 100644
--- a/docs/rules/consistent-output.md
+++ b/docs/rules/consistent-output.md
@@ -1,4 +1,4 @@
-# Enforce consistent use of output assertions in rule tests (consistent-output)
+# Enforce consistent use of `output` assertions in rule tests (consistent-output)
 
 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.
 
diff --git a/docs/rules/fixer-return.md b/docs/rules/fixer-return.md
index 843d3b5..6926c32 100644
--- a/docs/rules/fixer-return.md
+++ b/docs/rules/fixer-return.md
@@ -1,4 +1,4 @@
-# Require fixer function to return a fix (fixer-return)
+# Require fixer functions to return a fix (fixer-return)
 
 ✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule.
 
diff --git a/docs/rules/meta-property-ordering.md b/docs/rules/meta-property-ordering.md
index e971f2e..bef8867 100644
--- a/docs/rules/meta-property-ordering.md
+++ b/docs/rules/meta-property-ordering.md
@@ -1,6 +1,6 @@
 # Enforce the order of meta properties (meta-property-ordering)
 
-⚒️ 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.
+⚒️ 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 enforces that meta properties of a rule are placed in a consistent order.
 
diff --git a/docs/rules/no-deprecated-context-methods.md b/docs/rules/no-deprecated-context-methods.md
index 9a9520e..75bdfcb 100644
--- a/docs/rules/no-deprecated-context-methods.md
+++ b/docs/rules/no-deprecated-context-methods.md
@@ -1,6 +1,6 @@
 # Disallow usage of deprecated methods on rule context objects (no-deprecated-context-methods)
 
-⚒️ 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.
+⚒️ 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/no-deprecated-report-api.md b/docs/rules/no-deprecated-report-api.md
index 175a2a0..19231d5 100644
--- a/docs/rules/no-deprecated-report-api.md
+++ b/docs/rules/no-deprecated-report-api.md
@@ -1,10 +1,15 @@
-# Disallow use of the deprecated context.report() API (no-deprecated-report-api)
+# Disallow the version of `context.report()` with multiple arguments (no-deprecated-report-api)
 
 ✔️ 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.
+⚒️ 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.
 
-ESLint has two APIs that rules can use to report problems. The [deprecated API](http://eslint.org/docs/developer-guide/working-with-rules-deprecated) accepts multiple arguments: `context.report(node, [loc], message)`. The ["new API"](http://eslint.org/docs/developer-guide/working-with-rules#contextreport) accepts a single argument: an object containing information about the reported problem. It is recommended that all rules use the new API.
+ESLint has two APIs that rules can use to report problems.
+
+* The [deprecated API](http://eslint.org/docs/developer-guide/working-with-rules-deprecated) accepts multiple arguments: `context.report(node, [loc], message)`.
+* The ["new API"](http://eslint.org/docs/developer-guide/working-with-rules#contextreport) accepts a single argument: an object containing information about the reported problem.
+
+It is recommended that all rules use the new API.
 
 ## Rule Details
 
diff --git a/docs/rules/no-unused-placeholders.md b/docs/rules/no-unused-placeholders.md
index 60b7beb..00b96c6 100644
--- a/docs/rules/no-unused-placeholders.md
+++ b/docs/rules/no-unused-placeholders.md
@@ -6,7 +6,7 @@ This rule aims to disallow unused placeholders in rule report messages.
 
 ## Rule Details
 
-Reports when a context.report call contains a data property that does not have a corresponding placeholder in the report message.
+Reports when a `context.report()` call contains a data property that does not have a corresponding placeholder in the report message.
 
 Examples of **incorrect** code for this rule:
 
diff --git a/docs/rules/no-useless-token-range.md b/docs/rules/no-useless-token-range.md
index 35d154f..6bd4de8 100644
--- a/docs/rules/no-useless-token-range.md
+++ b/docs/rules/no-useless-token-range.md
@@ -1,14 +1,14 @@
-# Disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken (no-useless-token-range)
+# Disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()` (no-useless-token-range)
 
 ✔️ 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.
+⚒️ 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.
 
 AST nodes always start and end with tokens. As a result, the start index of the first token in a node is the same as the start index of the node itself, and the end index of the last token in a node is the same as the end index of the node itself. Using code like `sourceCode.getFirstToken(node).range[0]` unnecessarily hurts the performance of your rule, and makes your code less readable.
 
 ## Rule Details
 
-This rule aims to avoid unnecessary calls to `sourceCode.getFirstToken` and `sourceCode.getLastToken`.
+This rule aims to avoid unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()`.
 
 Examples of **incorrect** code for this rule:
 
diff --git a/docs/rules/prefer-message-ids.md b/docs/rules/prefer-message-ids.md
new file mode 100644
index 0000000..1c2ce5f
--- /dev/null
+++ b/docs/rules/prefer-message-ids.md
@@ -0,0 +1,57 @@
+# Require using `messageId` instead of `message` to report rule violations (prefer-message-ids)
+
+When reporting a rule violation, it's preferred to provide the violation message with the `messageId` property instead of the `message` property. Message IDs provide the following benefits:
+
+* Rule violation messages can be stored in a central `meta.messages` object for convenient management
+* Rule violation messages do not need to be repeated in both the rule file and rule test file
+
+## Rule Details
+
+This rule catches usages of the `message` property when reporting a rule violation.
+
+Examples of **incorrect** code for this rule:
+
+```js
+/* eslint eslint-plugin/prefer-message-ids: error */
+
+module.exports = {
+  create (context) {
+    return {
+      CallExpression (node) {
+        context.report({
+          node,
+          message: 'Some error message.',
+        });
+      },
+    };
+  },
+};
+```
+
+Examples of **correct** code for this rule:
+
+```js
+/* eslint eslint-plugin/prefer-message-ids: error */
+
+module.exports = {
+  meta: {
+    messages: {
+      someMessageId: 'Some error message',
+    },
+  },
+  create (context) {
+    return {
+      CallExpression (node) {
+        context.report({
+          node,
+          messageId: 'someMessageId',
+        });
+      },
+    };
+  },
+};
+```
+
+## Further Reading
+
+* [messageIds API](https://eslint.org/docs/developer-guide/working-with-rules#messageids)
diff --git a/docs/rules/prefer-object-rule.md b/docs/rules/prefer-object-rule.md
index 2028ceb..095835a 100644
--- a/docs/rules/prefer-object-rule.md
+++ b/docs/rules/prefer-object-rule.md
@@ -1,6 +1,6 @@
-# Disallow rule exports where the export is a function. (prefer-object-rule)
+# Disallow rule exports where the export is a function (prefer-object-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.
+⚒️ 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/prefer-output-null.md b/docs/rules/prefer-output-null.md
index 4fbbe25..e45256d 100644
--- a/docs/rules/prefer-output-null.md
+++ b/docs/rules/prefer-output-null.md
@@ -1,6 +1,6 @@
-# Disallow invalid RuleTester test cases with the output the same as the code. (prefer-output-null)
+# Disallow invalid RuleTester test cases where the `output` matches the `code` (prefer-output-null)
 
-⚒️ 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.
+⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule.
 
 Instead of repeating the test case `code`, using `output: null` is more concise and makes it easier to distinguish whether a test case provides an autofix.
 
diff --git a/docs/rules/prefer-placeholders.md b/docs/rules/prefer-placeholders.md
index 5e14398..e11f1c3 100644
--- a/docs/rules/prefer-placeholders.md
+++ b/docs/rules/prefer-placeholders.md
@@ -1,4 +1,4 @@
-# Disallow template literals as report messages (prefer-placeholders)
+# Require using placeholders for dynamic report messages (prefer-placeholders)
 
 Report messages in rules can have placeholders surrounded by curly brackets.
 
diff --git a/docs/rules/prefer-replace-text.md b/docs/rules/prefer-replace-text.md
index a85e4d5..87d2855 100644
--- a/docs/rules/prefer-replace-text.md
+++ b/docs/rules/prefer-replace-text.md
@@ -1,8 +1,8 @@
-# Require using replaceText instead of replaceTextRange. (prefer-replace-text)
+# Require using `replaceText()` instead of `replaceTextRange()` (prefer-replace-text)
 
 ## Rule Details
 
-The rule reports an error if `replaceTextRange`'s first argument is an array of identical array elements. It can be easily replaced by `replaceText` to improve readability.
+The rule reports an error if `replaceTextRange()`'s first argument is an array of identical array elements. It can be easily replaced by `replaceText()` to improve readability.
 
 Examples of **incorrect** code for this rule:
 
diff --git a/docs/rules/require-meta-docs-description.md b/docs/rules/require-meta-docs-description.md
index f8008b6..1ce3cce 100644
--- a/docs/rules/require-meta-docs-description.md
+++ b/docs/rules/require-meta-docs-description.md
@@ -1,4 +1,4 @@
-# Require rules to implement a meta.docs.description property with the correct format (require-meta-docs-description)
+# Require rules to implement a `meta.docs.description` property with the correct format (require-meta-docs-description)
 
 Defining a clear and consistent description for each rule helps developers understand what they're used for.
 
diff --git a/docs/rules/require-meta-docs-url.md b/docs/rules/require-meta-docs-url.md
index 0875712..e71f1bf 100644
--- a/docs/rules/require-meta-docs-url.md
+++ b/docs/rules/require-meta-docs-url.md
@@ -1,6 +1,6 @@
-# Require rules to implement a meta.docs.url property (require-meta-docs-url)
+# Require rules to implement a `meta.docs.url` property (require-meta-docs-url)
 
-⚒️ 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.
+⚒️ 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 rule can store the URL to its documentation page in `meta.docs.url`. This enables integration tools / IDEs / editors to conveniently provide the link to developers so that they can better understand the rule.
 
diff --git a/docs/rules/require-meta-fixable.md b/docs/rules/require-meta-fixable.md
index 3894412..ef0ba9e 100644
--- a/docs/rules/require-meta-fixable.md
+++ b/docs/rules/require-meta-fixable.md
@@ -1,12 +1,12 @@
-# Require rules to implement a meta.fixable property (require-meta-fixable)
+# Require rules to implement a `meta.fixable` property (require-meta-fixable)
 
 ✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule.
 
-A fixable ESLint rule must have a valid `meta.fixable` property. A rule reports a problem with a `fix()` function but does not export a `meta.fixable` property is likely to cause an unexpected error.
+ESLint requires fixable rules to specify a valid `meta.fixable` property (with value `code` or `whitespace`).
 
 ## Rule Details
 
-This rule aims to require ESLint rules to have a `meta.fixable` property if necessary.
+This rule aims to require fixable ESLint rules to have a valid `meta.fixable` property.
 
 Examples of **incorrect** code for this rule:
 
@@ -14,7 +14,7 @@ Examples of **incorrect** code for this rule:
 /* eslint eslint-plugin/require-meta-fixable: "error" */
 
 module.exports = {
-  meta: {},
+  meta: {}, // missing `fixable` property
   create (context) {
     context.report({
       node,
@@ -45,17 +45,14 @@ module.exports = {
 ```
 
 ```js
-/* eslint eslint-plugin/require-meta-fixable: "error" */
+/* eslint eslint-plugin/require-meta-fixable: ["error", { catchNoFixerButFixableProperty: true }] */
 
-module.exports = { create (context) {
-  context.report({
-    node,
-    message: 'foo',
-    fix (fixer) {
-      return fixer.remove(node);
-    },
-  });
-} };
+module.exports = {
+  meta: { fixable: 'code' }, // property enabled but no fixer detected
+  create (context) {
+    context.report({ node, message: 'foo' });
+  },
+};
 ```
 
 Examples of **correct** code for this rule:
@@ -91,10 +88,13 @@ module.exports = {
 };
 ```
 
-## When Not To Use It
+## Options
+
+This rule takes an optional object containing:
 
-If you do not plan to implement autofixable rules, you can turn off this rule.
+* `boolean` — `catchNoFixerButFixableProperty` — default `false` - Whether the rule should attempt to detect rules that do not have a fixer but enable the `meta.fixable` property. This option is off by default because it increases the chance of false positives since fixers can't always be detected when helper functions are used.
 
 ## Further Reading
 
 * [ESLint's autofix API](http://eslint.org/docs/developer-guide/working-with-rules#applying-fixes)
+* [ESLint's rule basics mentioning `meta.fixable`](https://eslint.org/docs/developer-guide/working-with-rules#rule-basics)
diff --git a/docs/rules/require-meta-has-suggestions.md b/docs/rules/require-meta-has-suggestions.md
index 8656870..6d08487 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 `--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.
 
 Likewise, rules that do not report suggestions should not enable the `meta.hasSuggestions` property.
diff --git a/docs/rules/require-meta-schema.md b/docs/rules/require-meta-schema.md
index a818deb..d653013 100644
--- a/docs/rules/require-meta-schema.md
+++ b/docs/rules/require-meta-schema.md
@@ -1,6 +1,6 @@
-# Require rules to implement a meta.schema property (require-meta-schema)
+# Require rules to implement a `meta.schema` property (require-meta-schema)
 
-⚒️ 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.
+💡 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.
 
@@ -59,6 +59,12 @@ module.exports = {
 };
 ```
 
+## Options
+
+This rule takes an optional object containing:
+
+* `boolean` — `requireSchemaPropertyWhenOptionless` — Whether the rule should require the `meta.schema` property to be specified (with `schema: []`) for rules that have no options. Defaults to `true`.
+
 ## Further Reading
 
 * [working-with-rules#options-schemas](https://eslint.org/docs/developer-guide/working-with-rules#options-schemas)
diff --git a/docs/rules/require-meta-type.md b/docs/rules/require-meta-type.md
index 730d233..b3a3348 100644
--- a/docs/rules/require-meta-type.md
+++ b/docs/rules/require-meta-type.md
@@ -1,8 +1,8 @@
-# Require rules to implement a meta.type property (require-meta-type)
+# Require rules to implement a `meta.type` property (require-meta-type)
 
 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.
+Fixes in custom rules will not be applied when using `--fix-type` unless they include a `meta.type` field.
 
 ## Rule Details
 
diff --git a/lib/rules/consistent-output.js b/lib/rules/consistent-output.js
index ff23663..b62b6df 100644
--- a/lib/rules/consistent-output.js
+++ b/lib/rules/consistent-output.js
@@ -1,5 +1,5 @@
 /**
- * @fileoverview Enforce consistent use of output assertions in rule tests
+ * @fileoverview Enforce consistent use of `output` assertions in rule tests
  * @author Teddy Katz
  */
 
@@ -15,7 +15,7 @@ module.exports = {
   meta: {
     type: 'suggestion',
     docs: {
-      description: 'enforce consistent use of output assertions in rule tests',
+      description: 'enforce consistent use of `output` assertions in rule tests',
       category: 'Tests',
       recommended: false,
     },
@@ -26,6 +26,9 @@ module.exports = {
         enum: ['always', 'consistent'],
       },
     ],
+    messages: {
+      missingOutput: 'This test case should have an output assertion.',
+    },
   },
 
   create (context) {
@@ -48,7 +51,7 @@ module.exports = {
             casesWithoutOutput.forEach(testCase => {
               context.report({
                 node: testCase,
-                message: 'This test case should have an output assertion.',
+                messageId: 'missingOutput',
               });
             });
           }
diff --git a/lib/rules/fixer-return.js b/lib/rules/fixer-return.js
index 5ea482f..0dd7fc0 100644
--- a/lib/rules/fixer-return.js
+++ b/lib/rules/fixer-return.js
@@ -1,5 +1,5 @@
 /**
- * @fileoverview Require fixer function to return a fix
+ * @fileoverview require fixer functions to return a fix
  * @author 薛定谔的猫<hh_2013@foxmail.com>
  */
 
@@ -20,7 +20,7 @@ module.exports = {
   meta: {
     type: 'problem',
     docs: {
-      description: 'require fixer function to return a fix',
+      description: 'require fixer functions to return a fix',
       category: 'Possible Errors',
       recommended: true,
     },
diff --git a/lib/rules/meta-property-ordering.js b/lib/rules/meta-property-ordering.js
index 3f407d1..0c4a46d 100644
--- a/lib/rules/meta-property-ordering.js
+++ b/lib/rules/meta-property-ordering.js
@@ -23,13 +23,15 @@ module.exports = {
       type: 'array',
       elements: { type: 'string' },
     }],
+    messages: {
+      inconsistentOrder: 'The meta properties should be placed in a consistent order: [{{order}}].',
+    },
   },
 
   create (context) {
     const sourceCode = context.getSourceCode();
     const info = getRuleInfo(sourceCode);
 
-    const message = 'The meta properties should be placed in a consistent order: [{{order}}].';
     const order = context.options[0] || ['type', 'docs', 'fixable', 'schema', 'messages'];
 
     const orderMap = new Map(order.map((name, i) => [name, i]));
@@ -67,7 +69,7 @@ module.exports = {
         for (const violatingProp of violatingProps) {
           context.report({
             node: violatingProp,
-            message,
+            messageId: 'inconsistentOrder',
             data: {
               order: knownProps.map(getKeyName).join(', '),
             },
diff --git a/lib/rules/no-deprecated-context-methods.js b/lib/rules/no-deprecated-context-methods.js
index 1ceeca6..7677152 100644
--- a/lib/rules/no-deprecated-context-methods.js
+++ b/lib/rules/no-deprecated-context-methods.js
@@ -44,6 +44,9 @@ module.exports = {
     },
     fixable: 'code',
     schema: [],
+    messages: {
+      newFormat: 'Use `{{contextName}}.getSourceCode().{{replacement}}` instead of `{{contextName}}.{{original}}`.',
+    },
   },
 
   create (context) {
@@ -66,7 +69,7 @@ module.exports = {
             contextId =>
               context.report({
                 node: contextId.parent,
-                message: 'Use `{{contextName}}.getSourceCode().{{replacement}}` instead of `{{contextName}}.{{original}}`.',
+                messageId: 'newFormat',
                 data: {
                   contextName: contextId.name,
                   original: contextId.parent.property.name,
diff --git a/lib/rules/no-deprecated-report-api.js b/lib/rules/no-deprecated-report-api.js
index 06aeebc..d20fbb3 100644
--- a/lib/rules/no-deprecated-report-api.js
+++ b/lib/rules/no-deprecated-report-api.js
@@ -1,5 +1,5 @@
 /**
- * @fileoverview disallow use of the deprecated context.report() API
+ * @fileoverview Disallow the version of `context.report()` with multiple arguments
  * @author Teddy Katz
  */
 
@@ -15,12 +15,15 @@ module.exports = {
   meta: {
     type: 'suggestion',
     docs: {
-      description: 'disallow use of the deprecated context.report() API',
+      description: 'disallow the version of `context.report()` with multiple arguments',
       category: 'Rules',
       recommended: true,
     },
     fixable: 'code', // or "code" or "whitespace"
     schema: [],
+    messages: {
+      useNewAPI: 'Use the new-style context.report() API.',
+    },
   },
 
   create (context) {
@@ -44,7 +47,7 @@ module.exports = {
         ) {
           context.report({
             node: node.callee.property,
-            message: 'Use the new-style context.report() API.',
+            messageId: 'useNewAPI',
             fix (fixer) {
               const openingParen = sourceCode.getTokenBefore(node.arguments[0]);
               const closingParen = sourceCode.getLastToken(node);
diff --git a/lib/rules/no-missing-placeholders.js b/lib/rules/no-missing-placeholders.js
index e855e2b..77aca1b 100644
--- a/lib/rules/no-missing-placeholders.js
+++ b/lib/rules/no-missing-placeholders.js
@@ -22,6 +22,9 @@ module.exports = {
     },
     fixable: null,
     schema: [],
+    messages: {
+      placeholderDoesNotExist: 'The placeholder {{{{missingKey}}}} does not exist.',
+    },
   },
 
   create (context) {
@@ -66,7 +69,7 @@ module.exports = {
               if (!matchingProperty) {
                 context.report({
                   node: reportInfo.message,
-                  message: 'The placeholder {{{{missingKey}}}} does not exist.',
+                  messageId: 'placeholderDoesNotExist',
                   data: { missingKey: match[1] },
                 });
               }
diff --git a/lib/rules/no-unused-placeholders.js b/lib/rules/no-unused-placeholders.js
index 911e6d4..83c38a4 100644
--- a/lib/rules/no-unused-placeholders.js
+++ b/lib/rules/no-unused-placeholders.js
@@ -22,6 +22,9 @@ module.exports = {
     },
     fixable: null,
     schema: [],
+    messages: {
+      placeholderUnused: 'The placeholder {{{{unusedKey}}}} is unused.',
+    },
   },
 
   create (context) {
@@ -69,7 +72,7 @@ module.exports = {
               if (!placeholdersInMessage.has(key)) {
                 context.report({
                   node: reportInfo.message,
-                  message: 'The placeholder {{{{unusedKey}}}} is unused.',
+                  messageId: 'placeholderUnused',
                   data: { unusedKey: key },
                 });
               }
diff --git a/lib/rules/no-useless-token-range.js b/lib/rules/no-useless-token-range.js
index 8669dc6..0a8a35d 100644
--- a/lib/rules/no-useless-token-range.js
+++ b/lib/rules/no-useless-token-range.js
@@ -1,5 +1,5 @@
 /**
- * @fileoverview Disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken
+ * @fileoverview Disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()`
  * @author Teddy Katz
  */
 
@@ -15,12 +15,15 @@ module.exports = {
   meta: {
     type: 'suggestion',
     docs: {
-      description: 'disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken',
+      description: 'disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()`',
       category: 'Rules',
       recommended: true,
     },
     fixable: 'code',
     schema: [],
+    messages: {
+      useReplacement: "Use '{{replacementText}}' instead.",
+    },
   },
 
   create (context) {
@@ -33,7 +36,7 @@ module.exports = {
     /**
      * Determines whether a second argument to getFirstToken or getLastToken changes the output of the function.
      * This occurs when the second argument exists and is not an object literal, or has keys other than `includeComments`.
-     * @param {ASTNode} arg The second argument to `sourceCode.getFirstToken` or `sourceCode.getLastToken`
+     * @param {ASTNode} arg The second argument to `sourceCode.getFirstToken()` or `sourceCode.getLastToken()`
      * @returns {boolean} `true` if the argument affects the output of getFirstToken or getLastToken
      */
     function affectsGetTokenOutput (arg) {
@@ -125,7 +128,7 @@ module.exports = {
               sourceCode.text.slice(identifier.parent.parent.range[1], fullRangeAccess.range[1]);
             context.report({
               node: identifier.parent.parent,
-              message: "Use '{{replacementText}}' instead.",
+              messageId: 'useReplacement',
               data: { replacementText },
               fix (fixer) {
                 return fixer.replaceText(identifier.parent.parent, sourceCode.getText(identifier.parent.parent.arguments[0]));
diff --git a/lib/rules/prefer-message-ids.js b/lib/rules/prefer-message-ids.js
new file mode 100644
index 0000000..8fddd35
--- /dev/null
+++ b/lib/rules/prefer-message-ids.js
@@ -0,0 +1,83 @@
+'use strict';
+
+const utils = require('../utils');
+const { getStaticValue } = require('eslint-utils');
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'require using `messageId` instead of `message` to report rule violations',
+      category: 'Rules',
+      recommended: false,
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      messagesMissing: '`meta.messages` must contain at least one violation message.',
+      foundMessage: 'Use `messageId` instead of `message`.',
+    },
+  },
+
+  create (context) {
+    const sourceCode = context.getSourceCode();
+    const info = utils.getRuleInfo(sourceCode);
+
+    let contextIdentifiers;
+
+    // ----------------------------------------------------------------------
+    // Public
+    // ----------------------------------------------------------------------
+
+    return {
+      Program (ast) {
+        contextIdentifiers = utils.getContextIdentifiers(context, ast);
+
+        if (info === null || info.meta === null) {
+          return;
+        }
+
+        const metaNode = info.meta;
+        const messagesNode =
+          metaNode &&
+          metaNode.properties &&
+          metaNode.properties.find(p => p.type === 'Property' && utils.getKeyName(p) === 'messages');
+
+        if (!messagesNode) {
+          context.report({ node: metaNode, messageId: 'messagesMissing' });
+          return;
+        }
+
+        const staticValue = getStaticValue(messagesNode.value, context.getScope());
+        if (!staticValue) {
+          return;
+        }
+
+        if (typeof staticValue.value === 'object' && staticValue.value.constructor === Object && Object.keys(staticValue.value).length === 0) {
+          context.report({ node: messagesNode.value, messageId: 'messagesMissing' });
+        }
+      },
+      CallExpression (node) {
+        if (
+          node.callee.type === 'MemberExpression' &&
+             contextIdentifiers.has(node.callee.object) &&
+             node.callee.property.type === 'Identifier' && node.callee.property.name === 'report'
+        ) {
+          const reportInfo = utils.getReportInfo(node.arguments, context);
+          if (!reportInfo || !reportInfo.message) {
+            return;
+          }
+
+          context.report({
+            node: reportInfo.message.parent,
+            messageId: 'foundMessage',
+          });
+        }
+      },
+    };
+  },
+};
diff --git a/lib/rules/prefer-object-rule.js b/lib/rules/prefer-object-rule.js
index c576e3c..72b9920 100644
--- a/lib/rules/prefer-object-rule.js
+++ b/lib/rules/prefer-object-rule.js
@@ -14,7 +14,7 @@ module.exports = {
   meta: {
     type: 'suggestion',
     docs: {
-      description: 'disallow rule exports where the export is a function.',
+      description: 'disallow rule exports where the export is a function',
       category: 'Rules',
       recommended: false,
     },
@@ -52,6 +52,7 @@ module.exports = {
                 token => token.type === 'Punctuator' && token.value === '('
               );
 
+              /* istanbul ignore if */
               if (!openParenToken) {
                 // this shouldn't happen, but guarding against crashes just in case
                 return null;
diff --git a/lib/rules/prefer-output-null.js b/lib/rules/prefer-output-null.js
index b6879a4..f622f00 100644
--- a/lib/rules/prefer-output-null.js
+++ b/lib/rules/prefer-output-null.js
@@ -1,5 +1,5 @@
 /**
- * @fileoverview disallows invalid RuleTester test cases with the output the same as the code.
+ * @fileoverview disallows invalid RuleTester test cases where the `output` matches the `code`
  * @author 薛定谔的猫<hh_2013@foxmail.com>
  */
 
@@ -15,12 +15,15 @@ module.exports = {
   meta: {
     type: 'suggestion',
     docs: {
-      description: 'disallow invalid RuleTester test cases with the output the same as the code.',
+      description: 'disallow invalid RuleTester test cases where the `output` matches the `code`',
       category: 'Tests',
       recommended: false,
     },
     fixable: 'code',
     schema: [],
+    messages: {
+      useOutputNull: 'Use `output: null` to assert that a test case is not autofixed.',
+    },
   },
 
   create (context) {
@@ -28,7 +31,6 @@ module.exports = {
     // Public
     // ----------------------------------------------------------------------
 
-    const message = 'Use `output: null` to assert that a test case is not autofixed.';
     const sourceCode = context.getSourceCode();
 
     return {
@@ -54,7 +56,7 @@ module.exports = {
             if (output && sourceCode.getText(output.value) === sourceCode.getText(code.value)) {
               context.report({
                 node: output,
-                message,
+                messageId: 'useOutputNull',
                 fix: fixer => fixer.replaceText(output.value, 'null'),
               });
             }
diff --git a/lib/rules/prefer-placeholders.js b/lib/rules/prefer-placeholders.js
index ec3ce74..21cf23a 100644
--- a/lib/rules/prefer-placeholders.js
+++ b/lib/rules/prefer-placeholders.js
@@ -1,5 +1,5 @@
 /**
- * @fileoverview disallow template literals as report messages
+ * @fileoverview require using placeholders for dynamic report messages
  * @author Teddy Katz
  */
 
@@ -16,12 +16,15 @@ module.exports = {
   meta: {
     type: 'suggestion',
     docs: {
-      description: 'disallow template literals as report messages',
+      description: 'require using placeholders for dynamic report messages',
       category: 'Rules',
       recommended: false,
     },
     fixable: null,
     schema: [],
+    messages: {
+      usePlaceholders: 'Use report message placeholders instead of string concatenation.',
+    },
   },
 
   create (context) {
@@ -80,7 +83,7 @@ module.exports = {
           ) {
             context.report({
               node: messageNode,
-              message: 'Use report message placeholders instead of string concatenation.',
+              messageId: 'usePlaceholders',
             });
           }
         }
diff --git a/lib/rules/prefer-replace-text.js b/lib/rules/prefer-replace-text.js
index 9662d7b..9275883 100644
--- a/lib/rules/prefer-replace-text.js
+++ b/lib/rules/prefer-replace-text.js
@@ -1,5 +1,5 @@
 /**
- * @fileoverview prefer using replaceText instead of replaceTextRange.
+ * @fileoverview prefer using `replaceText()` instead of `replaceTextRange()`
  * @author 薛定谔的猫<hh_2013@foxmail.com>
  */
 
@@ -15,17 +15,19 @@ module.exports = {
   meta: {
     type: 'suggestion',
     docs: {
-      description: 'require using replaceText instead of replaceTextRange.',
+      description: 'require using `replaceText()` instead of `replaceTextRange()`',
       category: 'Rules',
       recommended: false,
     },
     fixable: null,
     schema: [],
+    messages: {
+      useReplaceText: 'Use replaceText instead of replaceTextRange.',
+    },
   },
 
   create (context) {
     const sourceCode = context.getSourceCode();
-    const message = 'Use replaceText instead of replaceTextRange.';
     let funcInfo = {
       upper: null,
       codePath: null,
@@ -74,7 +76,7 @@ module.exports = {
           if (isIdenticalNodeRange) {
             context.report({
               node,
-              message,
+              messageId: 'useReplaceText',
             });
           }
         }
diff --git a/lib/rules/report-message-format.js b/lib/rules/report-message-format.js
index 59eba72..343f46a 100644
--- a/lib/rules/report-message-format.js
+++ b/lib/rules/report-message-format.js
@@ -24,6 +24,9 @@ module.exports = {
     schema: [
       { type: 'string' },
     ],
+    messages: {
+      noMatch: "Report message does not match the pattern '{{pattern}}'.",
+    },
   },
 
   create (context) {
@@ -44,7 +47,7 @@ module.exports = {
       ) {
         context.report({
           node: message,
-          message: "Report message does not match the pattern '{{pattern}}'.",
+          messageId: 'noMatch',
           data: { pattern: context.options[0] || '' },
         });
       }
diff --git a/lib/rules/require-meta-docs-description.js b/lib/rules/require-meta-docs-description.js
index 1ea6f51..31a148f 100644
--- a/lib/rules/require-meta-docs-description.js
+++ b/lib/rules/require-meta-docs-description.js
@@ -13,7 +13,7 @@ module.exports = {
   meta: {
     type: 'suggestion',
     docs: {
-      description: 'require rules to implement a meta.docs.description property with the correct format',
+      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.
     },
@@ -30,9 +30,10 @@ module.exports = {
       },
     ],
     messages: {
+      extraWhitespace: '`meta.docs.description` must not have leading nor trailing whitespace.',
+      mismatch: '`meta.docs.description` must match the regexp {{pattern}}.',
       missing: '`meta.docs.description` is required.',
       wrongType: '`meta.docs.description` must be a non-empty string.',
-      extraWhitespace: '`meta.docs.description` must not have leading nor trailing whitespace.',
     },
   },
 
@@ -77,7 +78,7 @@ module.exports = {
         } else if (!pattern.test(staticValue.value)) {
           context.report({
             node: descriptionNode.value,
-            message: '`meta.docs.description` must match the regexp {{pattern}}.',
+            messageId: 'mismatch',
             data: { pattern },
           });
         }
diff --git a/lib/rules/require-meta-docs-url.js b/lib/rules/require-meta-docs-url.js
index 34f5e39..ed7d67a 100644
--- a/lib/rules/require-meta-docs-url.js
+++ b/lib/rules/require-meta-docs-url.js
@@ -10,6 +10,7 @@
 
 const path = require('path');
 const util = require('../utils');
+const { getStaticValue } = require('eslint-utils');
 
 // -----------------------------------------------------------------------------
 // Rule Definition
@@ -19,7 +20,7 @@ module.exports = {
   meta: {
     type: 'suggestion',
     docs: {
-      description: 'require rules to implement a meta.docs.url property',
+      description: 'require rules to implement a `meta.docs.url` property',
       category: 'Rules',
       recommended: false,
     },
@@ -31,6 +32,11 @@ module.exports = {
       },
       additionalProperties: false,
     }],
+    messages: {
+      mismatch: '`meta.docs.url` property must be `{{expectedUrl}}`.',
+      missing: '`meta.docs.url` property is missing.',
+      wrongType: '`meta.docs.url` property must be a string.',
+    },
   },
 
   /**
@@ -48,24 +54,22 @@ module.exports = {
       : options.pattern.replace(/{{\s*name\s*}}/g, ruleName);
 
     /**
-     * Check whether a given node is the expected URL.
-     * @param {Node} node The node of property value to check.
+     * Check whether a given URL is the expected URL.
+     * @param {string} url The URL to check.
      * @returns {boolean} `true` if the node is the expected URL.
      */
-    function isExpectedUrl (node) {
+    function isExpectedUrl (url) {
       return Boolean(
-        node &&
-        node.type === 'Literal' &&
-        typeof node.value === 'string' &&
+        typeof url === 'string' &&
         (
           expectedUrl === undefined ||
-          node.value === expectedUrl
+          url === expectedUrl
         )
       );
     }
 
     return {
-      Program (node) {
+      Program () {
         const info = util.getRuleInfo(sourceCode);
         if (info === null) {
           return;
@@ -81,40 +85,45 @@ module.exports = {
           docsPropNode.value.properties &&
           docsPropNode.value.properties.find(p => p.type === 'Property' && util.getKeyName(p) === 'url');
 
-        if (isExpectedUrl(urlPropNode && urlPropNode.value)) {
+        const staticValue = urlPropNode ? getStaticValue(urlPropNode.value, context.getScope()) : undefined;
+        if (urlPropNode && !staticValue) {
+          // Ignore non-static values since we can't determine what they look like.
+          return;
+        }
+
+        if (isExpectedUrl(staticValue && staticValue.value)) {
           return;
         }
 
         context.report({
-          loc:
-            (urlPropNode && urlPropNode.value.loc) ||
-            (docsPropNode && docsPropNode.value.loc) ||
-            (metaNode && metaNode.loc) ||
-            node.loc.start,
-
-          message:
-            !urlPropNode ? 'Rules should export a `meta.docs.url` property.' :
+          node: (urlPropNode && urlPropNode.value) || (docsPropNode && docsPropNode.value) || metaNode || info.create,
+
+          messageId:
+            !urlPropNode ? 'missing' :
               // eslint-disable-next-line unicorn/no-nested-ternary
-              !expectedUrl ? '`meta.docs.url` property must be a string.' :
-                /* otherwise */ '`meta.docs.url` property must be `{{expectedUrl}}`.',
+              !expectedUrl ? 'wrongType' :
+                /* otherwise */ 'mismatch',
 
           data: {
             expectedUrl,
           },
 
           fix (fixer) {
-            if (expectedUrl) {
-              const urlString = JSON.stringify(expectedUrl);
-              if (urlPropNode) {
+            if (!expectedUrl) {
+              return null;
+            }
+
+            const urlString = JSON.stringify(expectedUrl);
+            if (urlPropNode) {
+              if (urlPropNode.value.type === 'Literal' || (urlPropNode.value.type === 'Identifier' && urlPropNode.value.name === 'undefined')) {
                 return fixer.replaceText(urlPropNode.value, urlString);
               }
-              if (docsPropNode && docsPropNode.value.type === 'ObjectExpression') {
-                return util.insertProperty(fixer, docsPropNode.value, `url: ${urlString}`, sourceCode);
-              }
-              if (!docsPropNode && metaNode && metaNode.type === 'ObjectExpression') {
-                return util.insertProperty(fixer, metaNode, `docs: {\nurl: ${urlString}\n}`, sourceCode);
-              }
+            } else if (docsPropNode && docsPropNode.value.type === 'ObjectExpression') {
+              return util.insertProperty(fixer, docsPropNode.value, `url: ${urlString}`, sourceCode);
+            } else if (!docsPropNode && metaNode && metaNode.type === 'ObjectExpression') {
+              return util.insertProperty(fixer, metaNode, `docs: {\nurl: ${urlString}\n}`, sourceCode);
             }
+
             return null;
           },
         });
diff --git a/lib/rules/require-meta-fixable.js b/lib/rules/require-meta-fixable.js
index fb9dc50..3223ded 100644
--- a/lib/rules/require-meta-fixable.js
+++ b/lib/rules/require-meta-fixable.js
@@ -1,10 +1,11 @@
 /**
- * @fileoverview require rules to implement a meta.fixable property
+ * @fileoverview require rules to implement a `meta.fixable` property
  * @author Teddy Katz
  */
 
 'use strict';
 
+const { getStaticValue } = require('eslint-utils');
 const utils = require('../utils');
 
 // ------------------------------------------------------------------------------
@@ -15,14 +16,32 @@ module.exports = {
   meta: {
     type: 'problem',
     docs: {
-      description: 'require rules to implement a meta.fixable property',
+      description: 'require rules to implement a `meta.fixable` property',
       category: 'Rules',
       recommended: true,
     },
-    schema: [],
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          catchNoFixerButFixableProperty: {
+            type: 'boolean',
+            default: false,
+          },
+        },
+        additionalProperties: false,
+      },
+    ],
+    messages: {
+      invalid: '`meta.fixable` must be either `code`, `whitespace`, or `null`.',
+      missing: '`meta.fixable` must be either `code` or `whitespace` for fixable rules.',
+      noFixerButFixableValue: '`meta.fixable` is enabled but no fixer detected.',
+    },
   },
 
   create (context) {
+    const catchNoFixerButFixableProperty = context.options[0] && context.options[0].catchNoFixerButFixableProperty;
+
     const sourceCode = context.getSourceCode();
     const ruleInfo = utils.getRuleInfo(sourceCode);
     let contextIdentifiers;
@@ -62,19 +81,28 @@ module.exports = {
           ruleInfo.meta.properties.find(prop => utils.getKeyName(prop) === 'fixable');
 
         if (metaFixableProp) {
-          const VALID_VALUES = new Set(['code', 'whitespace', null, undefined]);
-          const valueIsValid = metaFixableProp.value.type === 'Literal'
-            ? VALID_VALUES.has(metaFixableProp.value.value)
-            // eslint-disable-next-line unicorn/no-nested-ternary
-            : metaFixableProp.value.type === 'TemplateLiteral' && metaFixableProp.value.quasis.length === 1
-              ? VALID_VALUES.has(metaFixableProp.value.quasis[0].value.cooked)
-              : metaFixableProp.value.type === 'Identifier' && metaFixableProp.value.name === 'undefined';
+          const staticValue = getStaticValue(metaFixableProp.value, context.getScope());
+          if (!staticValue) {
+            // Ignore non-static values since we can't determine what they look like.
+            return;
+          }
+
+          if (!['code', 'whitespace', null, undefined].includes(staticValue.value)) {
+            // `fixable` property has an invalid value.
+            context.report({ node: metaFixableProp.value, messageId: 'invalid' });
+            return;
+          }
 
-          if (!valueIsValid) {
-            context.report({ node: metaFixableProp, message: '`meta.fixable` must be either `code`, `whitespace` or `null`.' });
+          if (usesFixFunctions && !['code', 'whitespace'].includes(staticValue.value)) {
+            // Rule is fixable but `fixable` property does not have a fixable value.
+            context.report({ node: metaFixableProp.value, messageId: 'missing' });
+          } else if (catchNoFixerButFixableProperty && !usesFixFunctions && ['code', 'whitespace'].includes(staticValue.value)) {
+            // Rule is NOT fixable but `fixable` property has a fixable value.
+            context.report({ node: metaFixableProp.value, messageId: 'noFixerButFixableValue' });
           }
-        } else if (usesFixFunctions) {
-          context.report({ node: ruleInfo.create, message: 'Fixable rules must export a `meta.fixable` property.' });
+        } else if (!metaFixableProp && usesFixFunctions) {
+          // Rule is fixable but is missing the `fixable` property.
+          context.report({ node: ruleInfo.meta || ruleInfo.create, messageId: 'missing' });
         }
       },
     };
diff --git a/lib/rules/require-meta-has-suggestions.js b/lib/rules/require-meta-has-suggestions.js
index e9b9bdc..da15e47 100644
--- a/lib/rules/require-meta-has-suggestions.js
+++ b/lib/rules/require-meta-has-suggestions.js
@@ -15,10 +15,11 @@ module.exports = {
       category: 'Rules',
       recommended: false,
     },
+    fixable: 'code',
     schema: [],
     messages: {
-      shouldBeSuggestable: 'Suggestable rules should specify a `meta.hasSuggestions` property with value `true`.',
-      shouldNotBeSuggestable: 'Non-suggestable rules should not specify a `meta.hasSuggestions` property with value `true`.',
+      shouldBeSuggestable: '`meta.hasSuggestions` must be `true` for suggestable rules.',
+      shouldNotBeSuggestable: '`meta.hasSuggestions` cannot be `true` for non-suggestable rules.',
     },
   },
 
@@ -46,7 +47,11 @@ module.exports = {
           const suggestProp = node.arguments[0].properties.find(prop => utils.getKeyName(prop) === 'suggest');
           if (suggestProp) {
             const staticValue = getStaticValue(suggestProp.value, context.getScope());
-            if (!staticValue || (Array.isArray(staticValue.value) && staticValue.value.length > 0)) {
+            if (
+              !staticValue ||
+                (Array.isArray(staticValue.value) && staticValue.value.length > 0) ||
+                (Array.isArray(staticValue.value) && staticValue.value.length === 0 && suggestProp.value.type === 'Identifier') // Array variable can have suggestions pushed to it.
+            ) {
               // These are all considered reporting suggestions:
               //   suggest: [{...}]
               //   suggest: getSuggestions()
@@ -64,12 +69,33 @@ module.exports = {
         if (ruleReportsSuggestions) {
           if (!hasSuggestionsProperty) {
             // Rule reports suggestions but is missing the `meta.hasSuggestions` property altogether.
-            context.report({ node: metaNode ? metaNode : ruleInfo.create, messageId: 'shouldBeSuggestable' });
-          } else if (hasSuggestionsStaticValue.value !== true) {
+            context.report({
+              node: metaNode ? metaNode : ruleInfo.create,
+              messageId: 'shouldBeSuggestable',
+              fix (fixer) {
+                if (metaNode && metaNode.type === 'ObjectExpression') {
+                  if (metaNode.properties.length === 0) {
+                    // If object is empty, just replace entire object.
+                    return fixer.replaceText(metaNode, '{ hasSuggestions: true }');
+                  }
+                  // Add new property to start of property list.
+                  return fixer.insertTextBefore(metaNode.properties[0], 'hasSuggestions: true, ');
+                }
+              },
+            });
+          } else if (hasSuggestionsStaticValue && hasSuggestionsStaticValue.value !== true) {
             // Rule reports suggestions but does not have `meta.hasSuggestions` property enabled.
-            context.report({ node: hasSuggestionsProperty.value, messageId: 'shouldBeSuggestable' });
+            context.report({
+              node: hasSuggestionsProperty.value,
+              messageId: 'shouldBeSuggestable',
+              fix (fixer) {
+                if (hasSuggestionsProperty.value.type === 'Literal' || (hasSuggestionsProperty.value.type === 'Identifier' && hasSuggestionsProperty.value.name === 'undefined')) {
+                  return fixer.replaceText(hasSuggestionsProperty.value, 'true');
+                }
+              },
+            });
           }
-        } else if (!ruleReportsSuggestions && hasSuggestionsProperty && hasSuggestionsStaticValue.value === true) {
+        } else if (!ruleReportsSuggestions && hasSuggestionsProperty && hasSuggestionsStaticValue && hasSuggestionsStaticValue.value === true) {
           // Rule does not report suggestions but has the `meta.hasSuggestions` property enabled.
           context.report({ node: hasSuggestionsProperty.value, messageId: 'shouldNotBeSuggestable' });
         }
diff --git a/lib/rules/require-meta-schema.js b/lib/rules/require-meta-schema.js
index b2296a1..976aea3 100644
--- a/lib/rules/require-meta-schema.js
+++ b/lib/rules/require-meta-schema.js
@@ -11,27 +11,29 @@ module.exports = {
   meta: {
     type: 'suggestion',
     docs: {
-      description: 'require rules to implement a meta.schema property',
+      description: 'require rules to implement a `meta.schema` property',
       category: 'Rules',
       recommended: false, // TODO: enable it in a major release.
     },
-    fixable: 'code',
     schema: [
       {
         type: 'object',
         properties: {
-          exceptRange: {
+          requireSchemaPropertyWhenOptionless: {
             type: 'boolean',
+            default: true,
           },
         },
         additionalProperties: false,
       },
     ],
     messages: {
-      foundOptionsUsage: 'Found usage of rule options but no options are defined in `meta.schema`.',
+      addEmptySchema: 'Add empty schema indicating the rule has no options.',
+      foundOptionsUsage: '`meta.schema` has no schema defined but rule has options.',
       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) {
@@ -43,32 +45,30 @@ module.exports = {
     }
 
     let contextIdentifiers;
-    let hasEmptySchema = false;
+    const metaNode = info.meta;
     let schemaNode;
 
+    // Options
+    const requireSchemaPropertyWhenOptionless = !context.options[0] || context.options[0].requireSchemaPropertyWhenOptionless;
+
+    let hasEmptySchema = false;
+    let isUsingOptions = false;
+
     return {
       Program (ast) {
         contextIdentifiers = utils.getContextIdentifiers(context, ast);
 
-        const metaNode = info.meta;
         schemaNode =
           metaNode &&
           metaNode.properties &&
           metaNode.properties.find(p => p.type === 'Property' && utils.getKeyName(p) === 'schema');
 
         if (!schemaNode) {
-          context.report({
-            node: metaNode,
-            messageId: 'missing',
-            fix (fixer) {
-              return utils.insertProperty(fixer, metaNode, 'schema: []', sourceCode);
-            },
-          });
           return;
         }
 
         let { value } = schemaNode;
-        if (value.type === 'Identifier') {
+        if (value.type === 'Identifier' && value.name !== 'undefined') {
           const variable = findVariable(
             scopeManager.acquire(value) || scopeManager.globalScope,
             value
@@ -102,16 +102,34 @@ module.exports = {
         }
       },
 
+      'Program:exit' () {
+        if (!schemaNode && requireSchemaPropertyWhenOptionless) {
+          context.report({
+            node: metaNode,
+            messageId: 'missing',
+            suggest: isUsingOptions ? [] : [
+              {
+                messageId: 'addEmptySchema',
+                fix (fixer) {
+                  return utils.insertProperty(fixer, metaNode, 'schema: []', sourceCode);
+                },
+              },
+            ],
+          });
+        }
+      },
+
       MemberExpression (node) {
         // Check if `context.options` was used when no options were defined in `meta.schema`.
         if (
-          hasEmptySchema &&
+          (hasEmptySchema || !schemaNode) &&
           node.object.type === 'Identifier' &&
           contextIdentifiers.has(node.object) &&
           node.property.type === 'Identifier' &&
           node.property.name === 'options'
         ) {
-          context.report({ node: schemaNode, messageId: 'foundOptionsUsage' });
+          isUsingOptions = true;
+          context.report({ node: schemaNode || metaNode, messageId: 'foundOptionsUsage' });
         }
       },
     };
diff --git a/lib/rules/require-meta-type.js b/lib/rules/require-meta-type.js
index aa2f486..330d3e9 100644
--- a/lib/rules/require-meta-type.js
+++ b/lib/rules/require-meta-type.js
@@ -1,10 +1,11 @@
 /**
- * @fileoverview require rules to implement a meta.type property
+ * @fileoverview require rules to implement a `meta.type` property
  * @author 薛定谔的猫<weiran.zsd@outlook.com>
  */
 
 'use strict';
 
+const { getStaticValue } = require('eslint-utils');
 const utils = require('../utils');
 const VALID_TYPES = new Set(['problem', 'suggestion', 'layout']);
 
@@ -16,7 +17,7 @@ module.exports = {
   meta: {
     type: 'problem',
     docs: {
-      description: 'require rules to implement a meta.type property',
+      description: 'require rules to implement a `meta.type` property',
       category: 'Rules',
       recommended: false, // TODO: enable it in a major release.
     },
@@ -24,7 +25,7 @@ module.exports = {
     schema: [],
     messages: {
       missing: '`meta.type` is required (must be either `problem`, `suggestion`, or `layout`).',
-      unexpected: '`meta.type` must be either `problem`, `suggestion` or `layout`.',
+      unexpected: '`meta.type` must be either `problem`, `suggestion`, or `layout`.',
     },
   },
 
@@ -52,10 +53,19 @@ module.exports = {
           metaNode.properties &&
           metaNode.properties.find(p => p.type === 'Property' && utils.getKeyName(p) === 'type');
 
-        if (typeNode && typeNode.value.type === 'Literal' && !VALID_TYPES.has(typeNode.value.value)) {
-          context.report({ node: metaNode, messageId: 'unexpected' });
-        } else if (!typeNode) {
+        if (!typeNode) {
           context.report({ node: metaNode, messageId: 'missing' });
+          return;
+        }
+
+        const staticValue = getStaticValue(typeNode.value, context.getScope());
+        if (!staticValue) {
+          // Ignore non-static values since we can't determine what they look like.
+          return;
+        }
+
+        if (!VALID_TYPES.has(staticValue.value)) {
+          context.report({ node: typeNode.value, messageId: 'unexpected' });
         }
       },
     };
diff --git a/package.json b/package.json
index e26a392..4ab8b22 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-plugin-eslint-plugin",
-  "version": "3.2.0",
+  "version": "3.6.1",
   "description": "An ESLint plugin for linting ESLint plugins",
   "author": "Teddy Katz",
   "main": "lib/index.js",
@@ -8,7 +8,7 @@
   "scripts": {
     "lint": "npm-run-all --continue-on-error --aggregate-output --parallel lint:*",
     "lint:docs": "markdownlint **/*.md",
-    "lint:js": "eslint . --ignore-pattern \"!.*\"",
+    "lint:js": "eslint .",
     "generate-readme-table": "node build/generate-readme-table.js",
     "generate-release": "node-release-script",
     "test": "nyc --all --check-coverage --include lib mocha tests --recursive"
@@ -33,10 +33,10 @@
     "eslint-utils": "^2.1.0"
   },
   "nyc": {
-    "branches": 96,
+    "branches": 98,
     "functions": 98,
-    "lines": 98,
-    "statements": 98
+    "lines": 99,
+    "statements": 99
   },
   "devDependencies": {
     "@not-an-aardvark/node-release-script": "^0.1.0",
@@ -58,9 +58,9 @@
     "nyc": "^15.1.0"
   },
   "peerDependencies": {
-    "eslint": "^7.0.0"
+    "eslint": ">=6.0.0"
   },
   "engines": {
-    "node": "^10.12.0 || >=12.0.0"
+    "node": "12.* || 14.* || >= 16.*"
   }
 }


Test patch:
diff --git a/docs/rules/no-identical-tests.md b/docs/rules/no-identical-tests.md
index 935fb54..5893c2d 100644
--- a/docs/rules/no-identical-tests.md
+++ b/docs/rules/no-identical-tests.md
@@ -2,7 +2,7 @@
 
 ✔️ 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.
+⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule.
 
 When a rule has a lot of tests, it's sometimes difficult to tell if any tests are duplicates. This rule would warn if any test cases have the same properties.
 
diff --git a/docs/rules/test-case-property-ordering.md b/docs/rules/test-case-property-ordering.md
index 993fe1e..4e62d53 100644
--- a/docs/rules/test-case-property-ordering.md
+++ b/docs/rules/test-case-property-ordering.md
@@ -1,6 +1,6 @@
 # Require the properties of a test case to be placed in a consistent order (test-case-property-ordering)
 
-⚒️ 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.
+⚒️ 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 enforces that the properties of RuleTester test cases are arranged in a consistent order.
 
diff --git a/docs/rules/test-case-shorthand-strings.md b/docs/rules/test-case-shorthand-strings.md
index 14a8122..bed8206 100644
--- a/docs/rules/test-case-shorthand-strings.md
+++ b/docs/rules/test-case-shorthand-strings.md
@@ -1,6 +1,6 @@
 # Enforce consistent usage of shorthand strings for test cases with no options (test-case-shorthand-strings)
 
-⚒️ 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.
+⚒️ The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#--fix) can automatically fix some of the problems reported by this rule.
 
 When writing valid test cases for rules with `RuleTester`, one can optionally include a string as a test case instead of an object, if the the test case does not use any options.
 
diff --git a/lib/rules/no-identical-tests.js b/lib/rules/no-identical-tests.js
index 8fb756e..e32f718 100644
--- a/lib/rules/no-identical-tests.js
+++ b/lib/rules/no-identical-tests.js
@@ -21,13 +21,15 @@ module.exports = {
     },
     fixable: 'code',
     schema: [],
+    messages: {
+      identical: 'This test case is identical to another case.',
+    },
   },
 
   create (context) {
     // ----------------------------------------------------------------------
     // Public
     // ----------------------------------------------------------------------
-    const message = 'This test case is identical to another case.';
     const sourceCode = context.getSourceCode();
 
     // ----------------------------------------------------------------------
@@ -46,8 +48,8 @@ module.exports = {
         return sourceCode.getText(testA) === sourceCode.getText(testB);
       }
 
-      const propertiesA = testA.properties || [];
-      const propertiesB = testB.properties || [];
+      const propertiesA = testA.properties;
+      const propertiesB = testB.properties;
 
       // if properties length not eq; return false;
       if (propertiesA.length !== propertiesB.length) {
@@ -78,7 +80,7 @@ module.exports = {
               if (cache.some(item => eq(item, test))) {
                 context.report({
                   node: test,
-                  message,
+                  messageId: 'identical',
                   fix (fixer) {
                     const start = sourceCode.getTokenBefore(test);
                     const end = sourceCode.getTokenAfter(test);
diff --git a/lib/rules/no-only-tests.js b/lib/rules/no-only-tests.js
index 0cdb12c..78f0c6d 100644
--- a/lib/rules/no-only-tests.js
+++ b/lib/rules/no-only-tests.js
@@ -1,36 +1,7 @@
 'use strict';
 
 const utils = require('../utils');
-
-/**
- * Checks if the given token is a comma token or not.
- * From: https://github.com/eslint/eslint/blob/master/lib/rules/utils/ast-utils.js
- * @param {Token} token The token to check.
- * @returns {boolean} `true` if the token is a comma token.
- */
-function isCommaToken (token) {
-  return token.value === ',' && token.type === 'Punctuator';
-}
-
-/**
- * Checks if the given token is an opening brace token or not.
- * From: https://github.com/eslint/eslint/blob/master/lib/rules/utils/ast-utils.js
- * @param {Token} token The token to check.
- * @returns {boolean} `true` if the token is an opening brace token.
- */
-function isOpeningBraceToken (token) {
-  return token.value === '{' && token.type === 'Punctuator';
-}
-
-/**
- * Checks if the given token is a closing brace token or not.
- * From: https://github.com/eslint/eslint/blob/master/lib/rules/utils/ast-utils.js
- * @param {Token} token The token to check.
- * @returns {boolean} `true` if the token is a closing brace token.
- */
-function isClosingBraceToken (token) {
-  return token.value === '}' && token.type === 'Punctuator';
-}
+const { isCommaToken, isOpeningBraceToken, isClosingBraceToken } = require('eslint-utils');
 
 module.exports = {
   meta: {
diff --git a/lib/rules/test-case-property-ordering.js b/lib/rules/test-case-property-ordering.js
index 7e81ff5..65ce61c 100644
--- a/lib/rules/test-case-property-ordering.js
+++ b/lib/rules/test-case-property-ordering.js
@@ -24,13 +24,15 @@ module.exports = {
       type: 'array',
       elements: { type: 'string' },
     }],
+    messages: {
+      inconsistentOrder: 'The properties of a test case should be placed in a consistent order: [{{order}}].',
+    },
   },
 
   create (context) {
     // ----------------------------------------------------------------------
     // Public
     // ----------------------------------------------------------------------
-    const message = 'The properties of a test case should be placed in a consistent order: [{{order}}].';
     const order = context.options[0] || [
       'filename',
       'code',
@@ -63,7 +65,7 @@ module.exports = {
 
                   context.report({
                     node: properties[i],
-                    message,
+                    messageId: 'inconsistentOrder',
                     data: { order: orderMsg.join(', ') },
                     fix (fixer) {
                       return orderMsg.map((key, index) => {
diff --git a/lib/rules/test-case-shorthand-strings.js b/lib/rules/test-case-shorthand-strings.js
index 7498f23..540571b 100644
--- a/lib/rules/test-case-shorthand-strings.js
+++ b/lib/rules/test-case-shorthand-strings.js
@@ -21,6 +21,9 @@ module.exports = {
     },
     fixable: 'code',
     schema: [{ enum: ['as-needed', 'never', 'consistent', 'consistent-as-needed'] }],
+    messages: {
+      useShorthand: 'Use {{preferred}} for this test case instead of {{actual}}.',
+    },
   },
 
   create (context) {
@@ -62,7 +65,7 @@ module.exports = {
       }[shorthandOption]).forEach(badCaseInfo => {
         context.report({
           node: badCaseInfo.node,
-          message: 'Use {{preferred}} for this test case instead of {{actual}}.',
+          messageId: 'useShorthand',
           data: {
             preferred: badCaseInfo.shorthand ? 'an object' : 'a string',
             actual: badCaseInfo.shorthand ? 'a string' : 'an object',
diff --git a/tests/lib/rule-setup.js b/tests/lib/rule-setup.js
index 3efe9cb..856f4a5 100644
--- a/tests/lib/rule-setup.js
+++ b/tests/lib/rule-setup.js
@@ -10,7 +10,7 @@ const RULE_NAMES_RECOMMENDED = new Set(Object.keys(plugin.configs.recommended.ru
 
 const MESSAGES = {
   fixable:
-    '⚒️ 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.',
+    '⚒️ 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.',
   configRecommended:
     '✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule.',
   hasSuggestions: '💡 Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).',
diff --git a/tests/lib/rules/consistent-output.js b/tests/lib/rules/consistent-output.js
index 627eebf..535dd9c 100644
--- a/tests/lib/rules/consistent-output.js
+++ b/tests/lib/rules/consistent-output.js
@@ -1,5 +1,5 @@
 /**
- * @fileoverview Enforce consistent use of output assertions in rule tests
+ * @fileoverview Enforce consistent use of `output` assertions in rule tests
  * @author Teddy Katz
  */
 
@@ -12,7 +12,7 @@
 const rule = require('../../../lib/rules/consistent-output');
 const RuleTester = require('eslint').RuleTester;
 
-const ERROR = { message: 'This test case should have an output assertion.', type: 'ObjectExpression' };
+const ERROR = { messageId: 'missingOutput', type: 'ObjectExpression' };
 
 // ------------------------------------------------------------------------------
 // Tests
diff --git a/tests/lib/rules/fixer-return.js b/tests/lib/rules/fixer-return.js
index c9db0b9..11cf7ab 100644
--- a/tests/lib/rules/fixer-return.js
+++ b/tests/lib/rules/fixer-return.js
@@ -1,5 +1,5 @@
 /**
- * @fileoverview Require fixer function to return a fix
+ * @fileoverview require fixer functions to return a fix
  * @author 薛定谔的猫<hh_2013@foxmail.com>
  */
 
diff --git a/tests/lib/rules/meta-property-ordering.js b/tests/lib/rules/meta-property-ordering.js
index c98cea6..3d5f094 100644
--- a/tests/lib/rules/meta-property-ordering.js
+++ b/tests/lib/rules/meta-property-ordering.js
@@ -15,14 +15,6 @@ const RuleTester = require('eslint').RuleTester;
 // Tests
 // ------------------------------------------------------------------------------
 
-/**
- * @param {string[]} order
- * @returns {string}
- */
-function getMessage (order) {
-  return `The meta properties should be placed in a consistent order: [${order.join(', ')}].`;
-}
-
 const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
 ruleTester.run('test-case-property-ordering', rule, {
   valid: [
@@ -46,7 +38,7 @@ ruleTester.run('test-case-property-ordering', rule, {
 
     `
     module.exports = {
-      meta: { 
+      meta: {
         type: 'problem',
         docs: {},
         fixable: 'code',
@@ -91,7 +83,7 @@ ruleTester.run('test-case-property-ordering', rule, {
           },
           create() {},
         };`,
-      errors: [{ message: getMessage(['type', 'docs', 'fixable']) }],
+      errors: [{ messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable'].join(', ') } }],
     },
     {
       code: `
@@ -106,27 +98,27 @@ ruleTester.run('test-case-property-ordering', rule, {
           create() {},
         };`,
       errors: [
-        { message: getMessage(['type', 'docs', 'fixable', 'schema']) },
-        { message: getMessage(['type', 'docs', 'fixable', 'schema']) },
+        { messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable', 'schema'].join(', ') } },
+        { messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable', 'schema'].join(', ') } },
       ],
     },
 
     {
       code: `
         module.exports = {
-          meta: {fixable, fooooooooo, doc, type},
+          meta: {fixable, fooooooooo, docs, type},
           create() {},
         };`,
 
       output: `
         module.exports = {
-          meta: {type, doc, fixable, fooooooooo},
+          meta: {type, docs, fixable, fooooooooo},
           create() {},
         };`,
-      options: [['type', 'doc', 'fixable']],
+      options: [['type', 'docs', 'fixable']],
       errors: [
-        { message: getMessage(['type', 'doc', 'fixable']) },
-        { message: getMessage(['type', 'doc', 'fixable']) },
+        { messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable'].join(', ') } },
+        { messageId: 'inconsistentOrder', data: { order: ['type', 'docs', 'fixable'].join(', ') } },
       ],
     },
   ],
diff --git a/tests/lib/rules/no-deprecated-report-api.js b/tests/lib/rules/no-deprecated-report-api.js
index 66c4b80..d71e3df 100644
--- a/tests/lib/rules/no-deprecated-report-api.js
+++ b/tests/lib/rules/no-deprecated-report-api.js
@@ -1,5 +1,5 @@
 /**
- * @fileoverview disallow use of the deprecated context.report() API
+ * @fileoverview Disallow the version of `context.report()` with multiple arguments
  * @author Teddy Katz
  */
 
@@ -11,7 +11,6 @@
 
 const rule = require('../../../lib/rules/no-deprecated-report-api');
 const RuleTester = require('eslint').RuleTester;
-const ERROR = { message: 'Use the new-style context.report() API.', type: 'Identifier' };
 
 // ------------------------------------------------------------------------------
 // Tests
@@ -98,7 +97,7 @@ ruleTester.run('no-deprecated-report-api', rule, {
           }
         };
       `,
-      errors: [ERROR],
+      errors: [{ messageId: 'useNewAPI', type: 'Identifier' }],
     },
     {
       code: `
@@ -115,7 +114,7 @@ ruleTester.run('no-deprecated-report-api', rule, {
           }
         };
       `,
-      errors: [ERROR],
+      errors: [{ messageId: 'useNewAPI', type: 'Identifier' }],
     },
     {
       code: `
@@ -132,7 +131,7 @@ ruleTester.run('no-deprecated-report-api', rule, {
           }
         };
       `,
-      errors: [ERROR],
+      errors: [{ messageId: 'useNewAPI', type: 'Identifier' }],
     },
     {
       code: `
@@ -143,7 +142,7 @@ ruleTester.run('no-deprecated-report-api', rule, {
         };
       `,
       output: null,
-      errors: [ERROR],
+      errors: [{ messageId: 'useNewAPI', type: 'Identifier' }],
     },
     {
       code: `
@@ -160,7 +159,7 @@ ruleTester.run('no-deprecated-report-api', rule, {
           }
         };
       `,
-      errors: [ERROR],
+      errors: [{ messageId: 'useNewAPI', type: 'Identifier' }],
     },
     {
       code: `
@@ -177,7 +176,7 @@ ruleTester.run('no-deprecated-report-api', rule, {
           }
         };
       `,
-      errors: [ERROR],
+      errors: [{ messageId: 'useNewAPI', type: 'Identifier' }],
     },
     {
       // With message string in variable.
@@ -197,7 +196,7 @@ ruleTester.run('no-deprecated-report-api', rule, {
           }
         };
       `,
-      errors: [ERROR],
+      errors: [{ messageId: 'useNewAPI', type: 'Identifier' }],
     },
     {
       // With message in variable but no autofix since we can't statically determine its type.
@@ -210,7 +209,7 @@ ruleTester.run('no-deprecated-report-api', rule, {
         };
       `,
       output: null,
-      errors: [ERROR],
+      errors: [{ messageId: 'useNewAPI', type: 'Identifier' }],
     },
     {
       code: `
@@ -221,7 +220,7 @@ ruleTester.run('no-deprecated-report-api', rule, {
         };
       `,
       output: null,
-      errors: [ERROR],
+      errors: [{ messageId: 'useNewAPI', type: 'Identifier' }],
     },
     {
       code: `
@@ -234,7 +233,7 @@ ruleTester.run('no-deprecated-report-api', rule, {
           context.report({node: theNode, message: \`blah\`, data: theData, fix: theFix});
         };
       `,
-      errors: [ERROR],
+      errors: [{ messageId: 'useNewAPI', type: 'Identifier' }],
     },
     {
       code: `
@@ -247,7 +246,7 @@ ruleTester.run('no-deprecated-report-api', rule, {
           context.report({node: theNode, loc: 5, message: foo, data: bar});
         };
       `,
-      errors: [ERROR],
+      errors: [{ messageId: 'useNewAPI', type: 'Identifier' }],
     },
     {
       // Location in variable as number.
@@ -263,7 +262,7 @@ ruleTester.run('no-deprecated-report-api', rule, {
           context.report({node: theNode, loc: LOC, message: foo, data: bar});
         };
       `,
-      errors: [ERROR],
+      errors: [{ messageId: 'useNewAPI', type: 'Identifier' }],
     },
     {
       // Location in variable as object.
@@ -279,7 +278,7 @@ ruleTester.run('no-deprecated-report-api', rule, {
           context.report({node: theNode, loc: LOC, message: foo, data: bar});
         };
       `,
-      errors: [ERROR],
+      errors: [{ messageId: 'useNewAPI', type: 'Identifier' }],
     },
     {
       // Location in variable but no autofix since we can't statically determine its type.
@@ -290,7 +289,7 @@ ruleTester.run('no-deprecated-report-api', rule, {
         };
       `,
       output: null,
-      errors: [ERROR],
+      errors: [{ messageId: 'useNewAPI', type: 'Identifier' }],
     },
     {
       code: `
@@ -301,7 +300,7 @@ ruleTester.run('no-deprecated-report-api', rule, {
         };
       `,
       output: null,
-      errors: [ERROR],
+      errors: [{ messageId: 'useNewAPI', type: 'Identifier' }],
     },
     {
       code: `
@@ -312,7 +311,7 @@ ruleTester.run('no-deprecated-report-api', rule, {
         };
       `,
       output: null,
-      errors: [ERROR],
+      errors: [{ messageId: 'useNewAPI', type: 'Identifier' }],
     },
   ],
 });
diff --git a/tests/lib/rules/no-identical-tests.js b/tests/lib/rules/no-identical-tests.js
index 343c5cf..d8c7bd7 100644
--- a/tests/lib/rules/no-identical-tests.js
+++ b/tests/lib/rules/no-identical-tests.js
@@ -12,7 +12,8 @@
 const rule = require('../../../lib/rules/no-identical-tests');
 const RuleTester = require('eslint').RuleTester;
 
-const ERROR = { message: 'This test case is identical to another case.' };
+const ERROR_OBJECT_TEST = { messageId: 'identical', type: 'ObjectExpression' };
+const ERROR_STRING_TEST = { messageId: 'identical', type: 'Literal' };
 
 // ------------------------------------------------------------------------------
 // Tests
@@ -46,6 +47,26 @@ ruleTester.run('no-identical-tests', rule, {
         invalid: []
       });
     `,
+    // Object and string test.
+    `
+      new RuleTester().run('foo', bar, {
+        valid: [
+          { code: 'foo' },
+          'foo',
+        ],
+        invalid: []
+      });
+    `,
+    // One test object with more properties than the other.
+    `
+      new RuleTester().run('foo', bar, {
+        valid: [
+          { code: 'foo' },
+          { code: 'foo', options: [{}] },
+        ],
+        invalid: []
+      });
+    `,
   ],
 
   invalid: [
@@ -67,7 +88,7 @@ ruleTester.run('no-identical-tests', rule, {
           invalid: []
         });
       `,
-      errors: [ERROR],
+      errors: [ERROR_OBJECT_TEST],
     },
     {
       code: `
@@ -87,7 +108,7 @@ ruleTester.run('no-identical-tests', rule, {
           invalid: []
         });
       `,
-      errors: [ERROR],
+      errors: [ERROR_OBJECT_TEST],
     },
     {
       code: `
@@ -112,7 +133,7 @@ ruleTester.run('no-identical-tests', rule, {
           ]
         });
       `,
-      errors: [ERROR, ERROR],
+      errors: [ERROR_OBJECT_TEST, ERROR_OBJECT_TEST],
     },
     {
       code: `
@@ -132,7 +153,28 @@ ruleTester.run('no-identical-tests', rule, {
           invalid: []
         });
       `,
-      errors: [ERROR],
+      errors: [ERROR_OBJECT_TEST],
+    },
+    {
+      // Empty objects.
+      code: `
+        new RuleTester().run('foo', bar, {
+          valid: [
+            {},
+            {},
+          ],
+          invalid: []
+        });
+      `,
+      output: `
+        new RuleTester().run('foo', bar, {
+          valid: [
+            {},
+          ],
+          invalid: []
+        });
+      `,
+      errors: [ERROR_OBJECT_TEST],
     },
     {
       code: `
@@ -152,7 +194,7 @@ ruleTester.run('no-identical-tests', rule, {
           invalid: []
         });
       `,
-      errors: [ERROR],
+      errors: [ERROR_STRING_TEST],
     },
   ],
 });
diff --git a/tests/lib/rules/no-useless-token-range.js b/tests/lib/rules/no-useless-token-range.js
index c8d58b7..19671f1 100644
--- a/tests/lib/rules/no-useless-token-range.js
+++ b/tests/lib/rules/no-useless-token-range.js
@@ -1,5 +1,5 @@
 /**
- * @fileoverview Disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken
+ * @fileoverview Disallow unnecessary calls to `sourceCode.getFirstToken()` and `sourceCode.getLastToken()`
  * @author Teddy Katz
  */
 
diff --git a/tests/lib/rules/prefer-message-ids.js b/tests/lib/rules/prefer-message-ids.js
new file mode 100644
index 0000000..cfabcef
--- /dev/null
+++ b/tests/lib/rules/prefer-message-ids.js
@@ -0,0 +1,179 @@
+'use strict';
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/prefer-message-ids');
+const RuleTester = require('eslint').RuleTester;
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
+
+ruleTester.run('prefer-message-ids', rule, {
+  valid: [
+    `
+      module.exports = {
+        create(context) {
+          context.report({ node });
+        }
+      };
+    `,
+    `
+      module.exports = {
+        create(context) {
+          context.report({ node, messageId: 'foo' });
+        }
+      };
+    `,
+    `
+      module.exports = {
+        create(context) {
+          foo.report({ node, message: 'foo' }); // unrelated function
+        }
+      };
+    `,
+    `
+      module.exports = {
+        create(context) {
+          context.foo({ node, message: 'foo' }); // unrelated function
+        }
+      };
+    `,
+    `
+      context.report({ node, message: 'foo' }); // outside rule
+      module.exports = {
+        create(context) {
+        }
+      };
+    `,
+    `
+      // Tests are still allowed to use 'message' which is helpful for verifying that dynamically-constructed messages (i.e. from placeholders) look correct.
+      new RuleTester().run('foo', bar, {
+        invalid: [
+          { code: 'foo', errors: [{message: 'foo'}] },
+        ]
+      });
+    `,
+
+    // `meta.messages` has a message
+    `
+      module.exports = {
+        meta: { messages: { someMessageId: 'some message' } },
+        create(context) {
+          context.report({ node, messageId: 'someMessageId' });
+        }
+      };
+    `,
+    // `meta.messages` has a message (in variable)
+    `
+      const messages = { someMessageId: 'some message' };
+      module.exports = {
+        meta: { messages },
+        create(context) {
+          context.report({ node, messageId: 'someMessageId' });
+        }
+      };
+    `,
+  ],
+
+  invalid: [
+    {
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({ node, message: 'foo' });
+          }
+        };
+      `,
+      errors: [{ messageId: 'foundMessage', type: 'Property' }],
+    },
+    {
+      // With message in variable.
+      code: `
+        const MESSAGE = \`\${foo} is bad.\`;
+        module.exports = {
+          create(context) {
+            context.report({
+              node,
+              message: MESSAGE
+            });
+          }
+        };
+      `,
+      errors: [{ messageId: 'foundMessage', type: 'Property' }],
+    },
+    {
+      // With constructed message.
+      code: `
+        module.exports = {
+          create(context) {
+            context.report({
+              node,
+              message: foo + ' is bad.'
+            });
+          }
+        };
+      `,
+      errors: [{ messageId: 'foundMessage', type: 'Property' }],
+    },
+
+    {
+      // `meta.messages` missing
+      code: `
+        module.exports = {
+          meta: { description: 'foo' },
+          create(context) { }
+        };
+      `,
+      errors: [{ messageId: 'messagesMissing', type: 'ObjectExpression' }],
+    },
+    {
+      // `meta.messages` empty
+      code: `
+        module.exports = {
+          meta: {
+            description: 'foo',
+            messages: {},
+          },
+          create(context) { }
+        };
+      `,
+      errors: [{ messageId: 'messagesMissing', type: 'ObjectExpression' }],
+    },
+    {
+      // `meta.messages` empty (in variable)
+      code: `
+        const messages = {};
+        module.exports = {
+          meta: {
+            description: 'foo',
+            messages,
+          },
+          create(context) { }
+        };
+      `,
+      errors: [{ messageId: 'messagesMissing', type: 'Identifier' }],
+    },
+    {
+      // `meta.messages` missing and using `message`
+      code: `
+        module.exports = {
+          meta: {
+            description: 'foo',
+          },
+          create(context) {
+            context.report({ node, message: 'foo' });
+          }
+        };
+      `,
+      errors: [
+        { messageId: 'messagesMissing', type: 'ObjectExpression' },
+        { messageId: 'foundMessage', type: 'Property' },
+      ],
+    },
+  ],
+});
diff --git a/tests/lib/rules/prefer-output-null.js b/tests/lib/rules/prefer-output-null.js
index 2c85ee8..6eabb45 100644
--- a/tests/lib/rules/prefer-output-null.js
+++ b/tests/lib/rules/prefer-output-null.js
@@ -1,5 +1,5 @@
 /**
- * @fileoverview disallows invalid RuleTester test cases with the output the same as the code.
+ * @fileoverview disallows invalid RuleTester test cases where the `output` matches the `code`
  * @author 薛定谔的猫<hh_2013@foxmail.com>
  */
 
@@ -12,7 +12,7 @@
 const rule = require('../../../lib/rules/prefer-output-null');
 const RuleTester = require('eslint').RuleTester;
 
-const ERROR = { message: 'Use `output: null` to assert that a test case is not autofixed.' };
+const ERROR = { messageId: 'useOutputNull', type: 'Property' };
 
 // ------------------------------------------------------------------------------
 // Tests
diff --git a/tests/lib/rules/prefer-placeholders.js b/tests/lib/rules/prefer-placeholders.js
index ce7daed..2de61b8 100644
--- a/tests/lib/rules/prefer-placeholders.js
+++ b/tests/lib/rules/prefer-placeholders.js
@@ -1,5 +1,5 @@
 /**
- * @fileoverview disallow template literals as report messages
+ * @fileoverview require using placeholders for dynamic report messages
  * @author Teddy Katz
  */
 
@@ -17,7 +17,7 @@ const RuleTester = require('eslint').RuleTester;
 // ------------------------------------------------------------------------------
 
 const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
-const ERROR = { message: 'Use report message placeholders instead of string concatenation.' };
+const ERROR = { messageId: 'usePlaceholders' };
 
 
 ruleTester.run('prefer-placeholders', rule, {
diff --git a/tests/lib/rules/prefer-replace-text.js b/tests/lib/rules/prefer-replace-text.js
index 2c339f1..10777b2 100644
--- a/tests/lib/rules/prefer-replace-text.js
+++ b/tests/lib/rules/prefer-replace-text.js
@@ -1,5 +1,5 @@
 /**
- * @fileoverview prefer using replaceText instead of replaceTextRange
+ * @fileoverview prefer using `replaceText()` instead of `replaceTextRange()`
  * @author 薛定谔的猫<hh_2013@foxmail.com>
  */
 
@@ -17,7 +17,7 @@ const RuleTester = require('eslint').RuleTester;
 // ------------------------------------------------------------------------------
 
 const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
-const ERROR = { message: 'Use replaceText instead of replaceTextRange.' };
+const ERROR = { messageId: 'useReplaceText', type: 'CallExpression' };
 
 
 ruleTester.run('prefer-placeholders', rule, {
diff --git a/tests/lib/rules/require-meta-docs-description.js b/tests/lib/rules/require-meta-docs-description.js
index 9a0e1dc..e8c6710 100644
--- a/tests/lib/rules/require-meta-docs-description.js
+++ b/tests/lib/rules/require-meta-docs-description.js
@@ -14,6 +14,7 @@ const RuleTester = require('eslint').RuleTester;
 const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } });
 ruleTester.run('require-meta-docs-description', rule, {
   valid: [
+    'foo()',
     `
       module.exports = {
         meta: { docs: { description: 'disallow unused variables' } },
@@ -133,6 +134,26 @@ ruleTester.run('require-meta-docs-description', rule, {
       output: null,
       errors: [{ messageId: 'wrongType', type: 'Literal' }],
     },
+    {
+      code: `
+        module.exports = {
+          meta: { docs: { description: null } },
+          create(context) {}
+        };
+      `,
+      output: null,
+      errors: [{ messageId: 'wrongType', type: 'Literal' }],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: { docs: { description: undefined } },
+          create(context) {}
+        };
+      `,
+      output: null,
+      errors: [{ messageId: 'wrongType', type: 'Identifier' }],
+    },
     {
       code: `
         const DESCRIPTION = true;
diff --git a/tests/lib/rules/require-meta-docs-url.js b/tests/lib/rules/require-meta-docs-url.js
index 6513286..b7195ae 100644
--- a/tests/lib/rules/require-meta-docs-url.js
+++ b/tests/lib/rules/require-meta-docs-url.js
@@ -19,7 +19,7 @@ const rule = require('../../../lib/rules/require-meta-docs-url');
 
 const tester = new RuleTester({
   parserOptions: {
-    ecmaVersion: 2018,
+    ecmaVersion: 2020,
   },
 });
 
@@ -66,6 +66,46 @@ tester.run('require-meta-docs-url', rule, {
         pattern: 'path/to/{{name}}.md',
       }],
     },
+    {
+      // `url` in variable.
+      filename: 'test-rule',
+      code: `
+        const url = "path/to/test-rule.md";
+        module.exports = {
+          meta: {docs: {url}},
+          create() {}
+        }
+      `,
+      options: [{
+        pattern: 'path/to/{{name}}.md',
+      }],
+    },
+    {
+      // Can't determine `url` value statically.
+      filename: 'test-rule',
+      code: `
+        module.exports = {
+          meta: {docs: {url: foo }},
+          create() {}
+        }
+      `,
+      options: [{
+        pattern: 'path/to/{{name}}.md',
+      }],
+    },
+    {
+      // Can't determine `url` value statically.
+      filename: 'test-rule',
+      code: `
+        module.exports = {
+          meta: {docs: {url: getUrl() }},
+          create() {}
+        }
+      `,
+      options: [{
+        pattern: 'path/to/{{name}}.md',
+      }],
+    },
   ],
 
   invalid: [
@@ -74,7 +114,7 @@ tester.run('require-meta-docs-url', rule, {
         module.exports = function() {}
       `,
       output: null,
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'FunctionExpression' }],
     },
     {
       code: `
@@ -84,7 +124,7 @@ tester.run('require-meta-docs-url', rule, {
         }
       `,
       output: null,
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'Identifier' }],
     },
     {
       code: `
@@ -94,7 +134,7 @@ tester.run('require-meta-docs-url', rule, {
         }
       `,
       output: null,
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'Literal' }],
     },
     {
       code: `
@@ -104,7 +144,7 @@ tester.run('require-meta-docs-url', rule, {
         }
       `,
       output: null,
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       code: `
@@ -116,7 +156,7 @@ tester.run('require-meta-docs-url', rule, {
         }
       `,
       output: null,
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       code: `
@@ -128,7 +168,7 @@ tester.run('require-meta-docs-url', rule, {
         }
       `,
       output: null,
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       code: `
@@ -140,7 +180,7 @@ tester.run('require-meta-docs-url', rule, {
         }
       `,
       output: null,
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'Identifier' }],
     },
     {
       code: `
@@ -152,7 +192,7 @@ tester.run('require-meta-docs-url', rule, {
         }
       `,
       output: null,
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       code: `
@@ -166,7 +206,7 @@ tester.run('require-meta-docs-url', rule, {
         }
       `,
       output: null,
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       code: `
@@ -180,7 +220,7 @@ tester.run('require-meta-docs-url', rule, {
         }
       `,
       output: null,
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       code: `
@@ -194,7 +234,47 @@ tester.run('require-meta-docs-url', rule, {
         }
       `,
       output: null,
-      errors: ['`meta.docs.url` property must be a string.'],
+      errors: [{ messageId: 'wrongType', type: 'Literal' }],
+    },
+    {
+      // `url` is null
+      code: `
+        module.exports = {
+          meta: {
+            docs: { url: null }
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      errors: [{ messageId: 'wrongType', type: 'Literal' }],
+    },
+    {
+      // `url` is undefined
+      code: `
+        module.exports = {
+          meta: {
+            docs: { url: undefined }
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      errors: [{ messageId: 'wrongType', type: 'Identifier' }],
+    },
+    {
+      // `url` in variable.
+      code: `
+        const url = 100;
+        module.exports = {
+          meta: {
+            docs: { url }
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      errors: [{ messageId: 'wrongType', type: 'Identifier' }],
     },
     {
       code: `
@@ -208,7 +288,7 @@ tester.run('require-meta-docs-url', rule, {
         }
       `,
       output: null,
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
 
     // -------------------------------------------------------------------------
@@ -222,7 +302,7 @@ tester.run('require-meta-docs-url', rule, {
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'FunctionExpression' }],
     },
     {
       code: `
@@ -235,7 +315,7 @@ tester.run('require-meta-docs-url', rule, {
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'Identifier' }],
     },
     {
       code: `
@@ -248,7 +328,7 @@ tester.run('require-meta-docs-url', rule, {
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'Literal' }],
     },
     {
       code: `
@@ -261,7 +341,7 @@ tester.run('require-meta-docs-url', rule, {
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       code: `
@@ -276,7 +356,7 @@ tester.run('require-meta-docs-url', rule, {
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       code: `
@@ -291,7 +371,7 @@ tester.run('require-meta-docs-url', rule, {
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       code: `
@@ -306,7 +386,7 @@ tester.run('require-meta-docs-url', rule, {
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'Identifier' }],
     },
     {
       code: `
@@ -321,7 +401,7 @@ tester.run('require-meta-docs-url', rule, {
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       code: `
@@ -338,7 +418,7 @@ tester.run('require-meta-docs-url', rule, {
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       code: `
@@ -355,7 +435,7 @@ tester.run('require-meta-docs-url', rule, {
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       code: `
@@ -372,7 +452,7 @@ tester.run('require-meta-docs-url', rule, {
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['`meta.docs.url` property must be a string.'],
+      errors: [{ messageId: 'wrongType', type: 'Literal' }],
     },
     {
       code: `
@@ -389,7 +469,7 @@ tester.run('require-meta-docs-url', rule, {
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
 
     // -------------------------------------------------------------------------
@@ -404,7 +484,7 @@ tester.run('require-meta-docs-url', rule, {
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'FunctionExpression' }],
     },
     {
       filename: 'test.js',
@@ -418,7 +498,7 @@ tester.run('require-meta-docs-url', rule, {
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'Identifier' }],
     },
     {
       filename: 'test.js',
@@ -432,7 +512,7 @@ tester.run('require-meta-docs-url', rule, {
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'Literal' }],
     },
     {
       filename: 'test.js',
@@ -455,7 +535,7 @@ url: "plugin-name/test.md"
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       filename: 'test.js',
@@ -481,7 +561,7 @@ url: "plugin-name/test.md"
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       filename: 'test.js',
@@ -507,7 +587,7 @@ url: "plugin-name/test.md"
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       filename: 'test.js',
@@ -523,7 +603,7 @@ url: "plugin-name/test.md"
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'Identifier' }],
     },
     {
       filename: 'test.js',
@@ -548,7 +628,7 @@ url: "plugin-name/test.md"
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       filename: 'test.js',
@@ -576,7 +656,7 @@ url: "plugin-name/test.md"
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       filename: 'test.js',
@@ -604,7 +684,7 @@ url: "plugin-name/test.md",
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       filename: 'test.js',
@@ -631,7 +711,73 @@ url: "plugin-name/test.md",
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['`meta.docs.url` property must be `plugin-name/test.md`.'],
+      errors: [{ message: '`meta.docs.url` property must be `plugin-name/test.md`.', type: 'Literal' }],
+    },
+    {
+      // `url` in variable, can't autofix it.
+      filename: 'test.js',
+      code: `
+        const url = 'wrong-url';
+        module.exports = {
+          meta: {
+            docs: { url }
+          },
+          create() {}
+        }
+      `,
+      output: null,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: [{ message: '`meta.docs.url` property must be `plugin-name/test.md`.', type: 'Identifier' }],
+    },
+    {
+      // `url` is `null`.
+      filename: 'test.js',
+      code: `
+        module.exports = {
+          meta: {
+            docs: { url: null }
+          },
+          create() {}
+        }
+      `,
+      output: `
+        module.exports = {
+          meta: {
+            docs: { url: "plugin-name/test.md" }
+          },
+          create() {}
+        }
+      `,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: [{ message: '`meta.docs.url` property must be `plugin-name/test.md`.', type: 'Literal' }],
+    },
+    {
+      // `url` is `undefined`.
+      filename: 'test.js',
+      code: `
+        module.exports = {
+          meta: {
+            docs: { url: undefined }
+          },
+          create() {}
+        }
+      `,
+      output: `
+        module.exports = {
+          meta: {
+            docs: { url: "plugin-name/test.md" }
+          },
+          create() {}
+        }
+      `,
+      options: [{
+        pattern: 'plugin-name/{{ name }}.md',
+      }],
+      errors: [{ message: '`meta.docs.url` property must be `plugin-name/test.md`.', type: 'Identifier' }],
     },
     {
       filename: 'test.js',
@@ -659,7 +805,7 @@ url: "plugin-name/test.md"
       options: [{
         pattern: 'plugin-name/{{ name }}.md',
       }],
-      errors: ['Rules should export a `meta.docs.url` property.'],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
   ],
 });
diff --git a/tests/lib/rules/require-meta-fixable.js b/tests/lib/rules/require-meta-fixable.js
index 875e701..ce4761b 100644
--- a/tests/lib/rules/require-meta-fixable.js
+++ b/tests/lib/rules/require-meta-fixable.js
@@ -1,5 +1,5 @@
 /**
- * @fileoverview require rules to implement a meta.fixable property
+ * @fileoverview require rules to implement a `meta.fixable` property
  * @author Teddy Katz
  */
 
@@ -12,9 +12,6 @@
 const rule = require('../../../lib/rules/require-meta-fixable');
 const RuleTester = require('eslint').RuleTester;
 
-const MISSING_ERROR = { message: 'Fixable rules must export a `meta.fixable` property.', type: 'FunctionExpression' };
-const INVALID_ERROR = { message: '`meta.fixable` must be either `code`, `whitespace` or `null`.', type: 'Property' };
-
 // ------------------------------------------------------------------------------
 // Tests
 // ------------------------------------------------------------------------------
@@ -37,9 +34,11 @@ ruleTester.run('require-meta-fixable', rule, {
         }
       };
     `,
+    // Value in variable.
     `
+      const fixable = 'code';
       module.exports = {
-        meta: { fixable: 'whitespace' },
+        meta: { fixable },
         create(context) {
           context.report({node, message, fix: foo});
         }
@@ -47,7 +46,7 @@ ruleTester.run('require-meta-fixable', rule, {
     `,
     `
       module.exports = {
-        meta: { 'fixable': 'code' },
+        meta: { fixable: 'whitespace' },
         create(context) {
           context.report({node, message, fix: foo});
         }
@@ -55,7 +54,7 @@ ruleTester.run('require-meta-fixable', rule, {
     `,
     `
       module.exports = {
-        meta: { ['fixable']: 'code' },
+        meta: { 'fixable': 'code' },
         create(context) {
           context.report({node, message, fix: foo});
         }
@@ -63,7 +62,7 @@ ruleTester.run('require-meta-fixable', rule, {
     `,
     `
       module.exports = {
-        meta: { [\`fixable\`]: 'code' },
+        meta: { ['fixable']: 'code' },
         create(context) {
           context.report({node, message, fix: foo});
         }
@@ -71,9 +70,9 @@ ruleTester.run('require-meta-fixable', rule, {
     `,
     `
       module.exports = {
-        meta: { fixable: 'code' },
+        meta: { [\`fixable\`]: 'code' },
         create(context) {
-          context.report({node, message});
+          context.report({node, message, fix: foo});
         }
       };
     `,
@@ -93,6 +92,13 @@ ruleTester.run('require-meta-fixable', rule, {
         }
       };
     `,
+    // `fixable` uses variable but no static value available.
+    `
+      module.exports = {
+        meta: { fixable: foo },
+        create(context) { context.report({node, message, fix: foo}); }
+      };
+    `,
     `
       module.exports = {
         meta: {},
@@ -118,6 +124,53 @@ ruleTester.run('require-meta-fixable', rule, {
         ecmaVersion: 9,
       },
     },
+
+    // catchNoFixerButFixableProperty = false (implicitly)
+    `
+      module.exports = {
+        meta: { fixable: 'code' },
+        create(context) { context.report({node, message}); }
+      };
+    `,
+    `
+      module.exports = {
+        meta: { fixable: 'whitespace' },
+        create(context) { context.report({node, message}); }
+      };
+    `,
+    // catchNoFixerButFixableProperty = false (explicitly)
+    {
+      code: `
+        module.exports = {
+          meta: { fixable: 'code' },
+          create(context) { context.report({node, message}); }
+        };
+      `,
+      options: [{ catchNoFixerButFixableProperty: false }],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: { fixable: 'whitespace' },
+          create(context) { context.report({node, message}); }
+        };
+      `,
+      options: [{ catchNoFixerButFixableProperty: false }],
+    },
+    // catchNoFixerButFixableProperty = true
+    {
+      code: `
+        module.exports = {
+          meta: { fixable: 'code' },
+          create(context) {
+            foo
+              ? context.report({ node, message })
+              : context.report({ node, message, fix });
+          }
+        };
+      `,
+      options: [{ catchNoFixerButFixableProperty: true }],
+    },
   ],
 
   invalid: [
@@ -128,7 +181,15 @@ ruleTester.run('require-meta-fixable', rule, {
           create(context) { context.report({node, message, fix: foo}); }
         };
       `,
-      errors: [MISSING_ERROR],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
+    },
+    {
+      code: `
+        module.exports = {
+          create(context) { context.report({node, message, fix: foo}); }
+        };
+      `,
+      errors: [{ messageId: 'missing', type: 'FunctionExpression' }],
     },
     {
       code: `
@@ -137,7 +198,7 @@ ruleTester.run('require-meta-fixable', rule, {
           create(context) { context.report(node, loc, message, data, fix); }
         };
       `,
-      errors: [MISSING_ERROR],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       code: `
@@ -146,7 +207,7 @@ ruleTester.run('require-meta-fixable', rule, {
           create(context) { context.report({node, message}); }
         };
       `,
-      errors: [INVALID_ERROR],
+      errors: [{ messageId: 'invalid', type: 'Literal' }],
     },
     {
       code: `
@@ -155,16 +216,67 @@ ruleTester.run('require-meta-fixable', rule, {
           create(context) { context.report({node, message, fix: foo}); }
         };
       `,
-      errors: [INVALID_ERROR],
+      errors: [{ messageId: 'invalid', type: 'Literal' }],
+    },
+    {
+      code: `
+        const fixable = 'invalid';
+        module.exports = {
+          meta: { fixable },
+          create(context) { context.report({node, message, fix: foo}); }
+        };
+      `,
+      errors: [{ messageId: 'invalid', type: 'Identifier' }],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: { fixable: null },
+          create(context) { context.report({node, message, fix: foo}); }
+        };
+      `,
+      errors: [{ messageId: 'missing', type: 'Literal' }],
     },
     {
       code: `
         module.exports = {
-          meta: { fixable: foo },
+          meta: { fixable: undefined },
           create(context) { context.report({node, message, fix: foo}); }
         };
       `,
-      errors: [INVALID_ERROR],
+      errors: [{ messageId: 'missing', type: 'Identifier' }],
+    },
+
+    // catchNoFixerButFixableProperty = true
+    {
+      code: `
+        module.exports = {
+          meta: { fixable: 'code' },
+          create(context) { context.report({node, message}); }
+        };
+      `,
+      options: [{ catchNoFixerButFixableProperty: true }],
+      errors: [{ messageId: 'noFixerButFixableValue', type: 'Literal' }],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: { fixable: 'whitespace' },
+          create(context) { context.report({node, message}); }
+        };
+      `,
+      options: [{ catchNoFixerButFixableProperty: true }],
+      errors: [{ messageId: 'noFixerButFixableValue', type: 'Literal' }],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: { fixable: null },
+          create(context) { context.report({node, message, fix}); }
+        };
+      `,
+      options: [{ catchNoFixerButFixableProperty: true }],
+      errors: [{ messageId: 'missing', type: 'Literal' }],
     },
   ],
 });
diff --git a/tests/lib/rules/require-meta-has-suggestions.js b/tests/lib/rules/require-meta-has-suggestions.js
index f58f9d2..0695e34 100644
--- a/tests/lib/rules/require-meta-has-suggestions.js
+++ b/tests/lib/rules/require-meta-has-suggestions.js
@@ -55,13 +55,14 @@ ruleTester.run('require-meta-has-suggestions', rule, {
       }
     };
     `,
-    // No suggestions reported (empty suggest array in variable), no suggestion property.
+    // Suggestions reported (pushing to an array variable), suggestion property.
     `
-    const SUGGESTIONS = [];
+    const suggest = [];
+    suggest.push({});
     module.exports = {
-      meta: {},
+      meta: { hasSuggestions: true },
       create(context) {
-        context.report({node, message, suggest: SUGGESTIONS});
+        context.report({node, message, suggest});
       }
     };
     `,
@@ -74,6 +75,24 @@ ruleTester.run('require-meta-has-suggestions', rule, {
       }
     };
     `,
+    // No suggestions reported, hasSuggestions property set to `null`.
+    `
+    module.exports = {
+      meta: { hasSuggestions: null },
+      create(context) {
+        context.report({node, message});
+      }
+    };
+    `,
+    // No suggestions reported, hasSuggestions property set to `undefined`.
+    `
+    module.exports = {
+      meta: { hasSuggestions: undefined },
+      create(context) {
+        context.report({node, message});
+      }
+    };
+    `,
     // No suggestions reported, hasSuggestions property set to false (as variable).
     `
     const hasSuggestions = false;
@@ -112,6 +131,34 @@ ruleTester.run('require-meta-has-suggestions', rule, {
         }
       };
     `,
+    // Provides suggestions, has hasSuggestions property with no static value available.
+    `
+      module.exports = {
+        meta: { hasSuggestions: getHasSuggestions() },
+        create(context) {
+          context.report({node, message, suggest: [{}]});
+        }
+      };
+    `,
+    // Provides suggestions, has hasSuggestions property in variable with no static value available
+    `
+      const hasSuggestions = getHasSuggestions();
+      module.exports = {
+        meta: { hasSuggestions },
+        create(context) {
+          context.report({node, message, suggest: [{}]});
+        }
+      };
+    `,
+    // Does not provide suggestions, has hasSuggestions property with no static value available
+    `
+      module.exports = {
+        meta: { hasSuggestions: getHasSuggestions() },
+        create(context) {
+          context.report({node, message});
+        }
+      };
+    `,
     // Spread syntax.
     {
       code: `
@@ -136,18 +183,41 @@ ruleTester.run('require-meta-has-suggestions', rule, {
           create(context) { context.report({node, message, suggest: [{}]}); }
         };
       `,
+      output: null,
       errors: [{ messageId: 'shouldBeSuggestable', type: 'FunctionExpression', line: 3, column: 17, endLine: 3, endColumn: 78 }],
     },
     {
-      // Reports suggestions, no hasSuggestions property, violation should be on `meta` object.
+      // Reports suggestions, no hasSuggestions property, violation should be on `meta` object, empty meta object.
       code: `
         module.exports = {
           meta: {},
           create(context) { context.report({node, message, suggest: [{}]}); }
         };
       `,
+      output: `
+        module.exports = {
+          meta: { hasSuggestions: true },
+          create(context) { context.report({node, message, suggest: [{}]}); }
+        };
+      `,
       errors: [{ messageId: 'shouldBeSuggestable', type: 'ObjectExpression', line: 3, column: 17, endLine: 3, endColumn: 19 }],
     },
+    {
+      // Reports suggestions, no hasSuggestions property, violation should be on `meta` object, non-empty meta object.
+      code: `
+        module.exports = {
+          meta: { foo: 'bar' },
+          create(context) { context.report({node, message, suggest: [{}]}); }
+        };
+      `,
+      output: `
+        module.exports = {
+          meta: { hasSuggestions: true, foo: 'bar' },
+          create(context) { context.report({node, message, suggest: [{}]}); }
+        };
+      `,
+      errors: [{ messageId: 'shouldBeSuggestable', type: 'ObjectExpression', line: 3, column: 17, endLine: 3, endColumn: 31 }],
+    },
     {
       // Reports suggestions (in variable), no hasSuggestions property, violation should be on `meta` object.
       code: `
@@ -157,8 +227,35 @@ ruleTester.run('require-meta-has-suggestions', rule, {
           create(context) { context.report({node, message, suggest: SUGGESTIONS}); }
         };
       `,
+      output: `
+        const SUGGESTIONS = [{}];
+        module.exports = {
+          meta: { hasSuggestions: true },
+          create(context) { context.report({node, message, suggest: SUGGESTIONS}); }
+        };
+      `,
       errors: [{ messageId: 'shouldBeSuggestable', type: 'ObjectExpression', line: 4, column: 17, endLine: 4, endColumn: 19 }],
     },
+    {
+      // Reports suggestions (in variable, with pushing), no hasSuggestions property, violation should be on `meta` object.
+      code: `
+        const suggest = [];
+        suggest.push({});
+        module.exports = {
+          meta: {},
+          create(context) { context.report({node, message, suggest}); }
+        };
+      `,
+      output: `
+        const suggest = [];
+        suggest.push({});
+        module.exports = {
+          meta: { hasSuggestions: true },
+          create(context) { context.report({node, message, suggest}); }
+        };
+      `,
+      errors: [{ messageId: 'shouldBeSuggestable', type: 'ObjectExpression', line: 5, column: 17, endLine: 5, endColumn: 19 }],
+    },
     {
       // Reports suggestions, hasSuggestions property set to false, violation should be on `false`
       code: `
@@ -167,8 +264,46 @@ ruleTester.run('require-meta-has-suggestions', rule, {
           create(context) { context.report({node, message, suggest: [{}]}); }
         };
       `,
+      output: `
+        module.exports = {
+          meta: { hasSuggestions: true },
+          create(context) { context.report({node, message, suggest: [{}]}); }
+        };
+      `,
       errors: [{ messageId: 'shouldBeSuggestable', type: 'Literal', line: 3, column: 35, endLine: 3, endColumn: 40 }],
     },
+    {
+      // Reports suggestions, hasSuggestions property set to `null`, violation should be on `null`
+      code: `
+        module.exports = {
+          meta: { hasSuggestions: null },
+          create(context) { context.report({node, message, suggest: [{}]}); }
+        };
+      `,
+      output: `
+        module.exports = {
+          meta: { hasSuggestions: true },
+          create(context) { context.report({node, message, suggest: [{}]}); }
+        };
+      `,
+      errors: [{ messageId: 'shouldBeSuggestable', type: 'Literal', line: 3, column: 35, endLine: 3, endColumn: 39 }],
+    },
+    {
+      // Reports suggestions, hasSuggestions property set to `undefined`, violation should be on `undefined`
+      code: `
+        module.exports = {
+          meta: { hasSuggestions: undefined },
+          create(context) { context.report({node, message, suggest: [{}]}); }
+        };
+      `,
+      output: `
+        module.exports = {
+          meta: { hasSuggestions: true },
+          create(context) { context.report({node, message, suggest: [{}]}); }
+        };
+      `,
+      errors: [{ messageId: 'shouldBeSuggestable', type: 'Identifier', line: 3, column: 35, endLine: 3, endColumn: 44 }],
+    },
     {
       // Reports suggestions, hasSuggestions property set to false (as variable), violation should be on variable
       code: `
@@ -178,6 +313,7 @@ ruleTester.run('require-meta-has-suggestions', rule, {
           create(context) { context.report({node, message, suggest: [{}]}); }
         };
       `,
+      output: null,
       errors: [{ messageId: 'shouldBeSuggestable', type: 'Identifier', line: 4, column: 19, endLine: 4, endColumn: 33 }],
     },
     {
@@ -188,6 +324,7 @@ ruleTester.run('require-meta-has-suggestions', rule, {
           create(context) { context.report({node, message}); }
         };
       `,
+      output: null,
       errors: [{ messageId: 'shouldNotBeSuggestable', type: 'Literal', line: 3, column: 35, endLine: 3, endColumn: 39 }],
     },
   ],
diff --git a/tests/lib/rules/require-meta-schema.js b/tests/lib/rules/require-meta-schema.js
index 0a32c60..7450ea7 100644
--- a/tests/lib/rules/require-meta-schema.js
+++ b/tests/lib/rules/require-meta-schema.js
@@ -83,6 +83,17 @@ ruleTester.run('require-meta-schema', rule, {
         create
       };
     `,
+
+    {
+      // requireSchemaPropertyWhenOptionless = false
+      code: `
+        module.exports = {
+          meta: {},
+          create(context) {}
+        };
+      `,
+      options: [{ requireSchemaPropertyWhenOptionless: false }],
+    },
   ],
 
   invalid: [
@@ -93,7 +104,43 @@ ruleTester.run('require-meta-schema', rule, {
           create(context) {}
         };
       `,
-      output: `
+      output: null,
+      errors: [
+        {
+          messageId: 'missing',
+          type: 'ObjectExpression',
+          suggestions: [
+            {
+              messageId: 'addEmptySchema',
+              output: `
+        module.exports = {
+          meta: {
+schema: []
+},
+          create(context) {}
+        };
+      `,
+            }],
+        }],
+    },
+    {
+      // requireSchemaPropertyWhenOptionless = true.
+      code: `
+        module.exports = {
+          meta: {},
+          create(context) {}
+        };
+      `,
+      output: null,
+      options: [{ requireSchemaPropertyWhenOptionless: true }],
+      errors: [
+        {
+          messageId: 'missing',
+          type: 'ObjectExpression',
+          suggestions: [
+            {
+              messageId: 'addEmptySchema',
+              output: `
         module.exports = {
           meta: {
 schema: []
@@ -101,7 +148,10 @@ schema: []
           create(context) {}
         };
       `,
-      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
+            },
+          ],
+        },
+      ],
     },
     {
       code: `
@@ -110,16 +160,38 @@ schema: []
           create(context) {}
         };
       `,
-      output: `
+      output: null,
+      errors: [
+        {
+          messageId: 'missing',
+          type: 'ObjectExpression',
+          suggestions: [
+            {
+              messageId: 'addEmptySchema',
+              output: `
         module.exports = {
           meta: { type: 'problem',
 schema: [] },
           create(context) {}
         };
       `,
-      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
+            },
+          ],
+        },
+      ],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: { schema: null },
+          create(context) {}
+        };
+      `,
+      output: null,
+      errors: [{ messageId: 'wrongType', type: 'Literal', suggestions: [] }],
     },
     {
+      // requireSchemaPropertyWhenOptionless = false.
       code: `
         module.exports = {
           meta: { schema: null },
@@ -127,7 +199,18 @@ schema: [] },
         };
       `,
       output: null,
-      errors: [{ messageId: 'wrongType', type: 'Literal' }],
+      options: [{ requireSchemaPropertyWhenOptionless: false }],
+      errors: [{ messageId: 'wrongType', type: 'Literal', suggestions: [] }],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: { schema: undefined },
+          create(context) {}
+        };
+      `,
+      output: null,
+      errors: [{ messageId: 'wrongType', type: 'Identifier', suggestions: [] }],
     },
     {
       code: `
@@ -138,7 +221,7 @@ schema: [] },
         };
       `,
       output: null,
-      errors: [{ messageId: 'wrongType', type: 'Literal' }],
+      errors: [{ messageId: 'wrongType', type: 'Literal', suggestions: [] }],
     },
     {
       // Empty schema (array), but using rule options.
@@ -149,7 +232,7 @@ schema: [] },
         };
       `,
       output: null,
-      errors: [{ messageId: 'foundOptionsUsage', type: 'Property' }],
+      errors: [{ messageId: 'foundOptionsUsage', type: 'Property', suggestions: [] }],
     },
     {
       // Empty schema (object), but using rule options.
@@ -160,7 +243,45 @@ schema: [] },
         };
       `,
       output: null,
-      errors: [{ messageId: 'foundOptionsUsage', type: 'Property' }],
+      errors: [{ messageId: 'foundOptionsUsage', type: 'Property', suggestions: [] }],
+    },
+    {
+      // Empty schema (object), but using rule options, requireSchemaPropertyWhenOptionless = false.
+      code: `
+        module.exports = {
+          meta: { schema: {} },
+          create(context) { const options = context.options; }
+        };
+      `,
+      output: null,
+      options: [{ requireSchemaPropertyWhenOptionless: false }],
+      errors: [{ messageId: 'foundOptionsUsage', type: 'Property', suggestions: [] }],
+    },
+    {
+      // No schema, but using rule options, requireSchemaPropertyWhenOptionless = false.
+      code: `
+        module.exports = {
+          meta: {},
+          create(context) { const options = context.options; }
+        };
+      `,
+      output: null,
+      options: [{ requireSchemaPropertyWhenOptionless: false }],
+      errors: [{ messageId: 'foundOptionsUsage', type: 'ObjectExpression', suggestions: [] }],
+    },
+    {
+      // No schema, but using rule options, should have no suggestions.
+      code: `
+        module.exports = {
+          meta: {},
+          create(context) { const options = context.options; }
+        };
+      `,
+      output: null,
+      errors: [
+        { messageId: 'foundOptionsUsage', type: 'ObjectExpression', suggestions: [] },
+        { messageId: 'missing', type: 'ObjectExpression', suggestions: [] },
+      ],
     },
   ],
 });
diff --git a/tests/lib/rules/require-meta-type.js b/tests/lib/rules/require-meta-type.js
index 41bd6d9..25ca099 100644
--- a/tests/lib/rules/require-meta-type.js
+++ b/tests/lib/rules/require-meta-type.js
@@ -1,5 +1,5 @@
 /**
- * @fileoverview require rules to implement a meta.type property
+ * @fileoverview require rules to implement a `meta.type` property
  * @author 唯然<weiran.zsd@outlook.com>
  */
 
@@ -37,6 +37,25 @@ ruleTester.run('require-meta-type', rule, {
         create(context) {}
       };
     `,
+    `
+      const type = 'problem';
+      module.exports = {
+        meta: { type },
+        create(context) {}
+      };
+    `,
+    `
+      module.exports = {
+        meta: { type: getType() },
+        create(context) {}
+      };
+    `,
+    `
+      module.exports = {
+        meta: { type: FOO },
+        create(context) {}
+      };
+    `,
     `module.exports = {
       create(context) {}
     }`,
@@ -60,7 +79,7 @@ ruleTester.run('require-meta-type', rule, {
           create(context) {}
         };
       `,
-      errors: [{ messageId: 'missing' }],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       code: `
@@ -70,7 +89,7 @@ ruleTester.run('require-meta-type', rule, {
           create,
         };
       `,
-      errors: [{ messageId: 'missing' }],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       code: `
@@ -80,7 +99,7 @@ ruleTester.run('require-meta-type', rule, {
           create,
         };
       `,
-      errors: [{ messageId: 'missing' }],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       code: `
@@ -90,7 +109,7 @@ ruleTester.run('require-meta-type', rule, {
           create,
         };
       `,
-      errors: [{ messageId: 'missing' }],
+      errors: [{ messageId: 'missing', type: 'ObjectExpression' }],
     },
     {
       code: `
@@ -99,7 +118,35 @@ ruleTester.run('require-meta-type', rule, {
           create(context) {}
         };
       `,
-      errors: [{ messageId: 'unexpected' }],
+      errors: [{ messageId: 'unexpected', type: 'Literal' }],
+    },
+    {
+      code: `
+        const type = 'invalid-type';
+        module.exports = {
+          meta: { type },
+          create(context) {}
+        };
+      `,
+      errors: [{ messageId: 'unexpected', type: 'Identifier' }],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: { type: null },
+          create(context) {}
+        };
+      `,
+      errors: [{ messageId: 'unexpected', type: 'Literal' }],
+    },
+    {
+      code: `
+        module.exports = {
+          meta: { type: undefined },
+          create(context) {}
+        };
+      `,
+      errors: [{ messageId: 'unexpected', type: 'Identifier' }],
     },
   ],
 });
diff --git a/tests/lib/rules/test-case-property-ordering.js b/tests/lib/rules/test-case-property-ordering.js
index df29749..3a5a3d6 100644
--- a/tests/lib/rules/test-case-property-ordering.js
+++ b/tests/lib/rules/test-case-property-ordering.js
@@ -19,6 +19,14 @@ const RuleTester = require('eslint').RuleTester;
 const ruleTester = new RuleTester();
 ruleTester.run('test-case-property-ordering', rule, {
   valid: [
+    `
+      new RuleTester().run('foo', bar, {
+        valid: [
+          'foo',
+          RuleTester.only('foo'),
+        ]
+      });
+    `,
     `
       new RuleTester().run('foo', bar, {
         valid: [
diff --git a/tests/lib/rules/test-case-shorthand-strings.js b/tests/lib/rules/test-case-shorthand-strings.js
index c15eefb..cf29c26 100644
--- a/tests/lib/rules/test-case-shorthand-strings.js
+++ b/tests/lib/rules/test-case-shorthand-strings.js
@@ -28,7 +28,7 @@ function getTestCases (cases) {
   `;
 }
 
-const EXPECTED_SHORTHAND_ERROR = { message: 'Use a string for this test case instead of an object.' };
+const EXPECTED_SHORTHAND_ERROR = { message: 'Use a string for this test case instead of an object.', type: 'ObjectExpression' };
 const UNEXPECTED_SHORTHAND_ERROR = { message: 'Use an object for this test case instead of a string.' };
 
 // ------------------------------------------------------------------------------