Repository: vuejs/eslint-plugin-vue
Pull request #1848 (2022-04-14 04:27:33)

Change presets configs and remove unused internal methods
### Change Vue 3 presets configs

* Change `plugin:vue/vue3-essential` config:
  - Add [`vue/no-child-content`](https://eslint.vuejs.org/rules/no-child-content.html) rule
  - Add [`vue/no-expose-after-await`](https://eslint.vuejs.org/rules/no-expose-after-await.html) rule
  - Add [`vue/no-reserved-component-names`](https://eslint.vuejs.org/rules/no-reserved-component-names.html) rule
  - Add [`vue/no-use-computed-property-like-method`](https://eslint.vuejs.org/rules/no-use-computed-property-like-method.html) rule
  - Add [`vue/no-v-text-v-html-on-component`](https://eslint.vuejs.org/rules/no-v-text-v-html-on-component.html) rule
  - Add [`vue/prefer-import-from-vue`](https://eslint.vuejs.org/rules/prefer-import-from-vue.html) rule
* Change `plugin:vue/vue3-strongly-recommended` config:
  - Same changes as above
* Change `plugin:vue/vue3-recommended` config:
  - Same changes as above

### Change Vue 2 preset configs

* Change `plugin:vue/essential` config:
  - Add [`vue/no-child-content`](https://eslint.vuejs.org/rules/no-child-content.html) rule
  - Add [`vue/no-reserved-component-names`](https://eslint.vuejs.org/rules/no-reserved-component-names.html) rule
  - Add [`vue/no-use-computed-property-like-method`](https://eslint.vuejs.org/rules/no-use-computed-property-like-method.html) rule
  - Add [`vue/no-v-text-v-html-on-component`](https://eslint.vuejs.org/rules/no-v-text-v-html-on-component.html) rule
  - Add [`vue/valid-model-definition`](https://eslint.vuejs.org/rules/valid-model-definition.html) rule
  - Add [`vue/no-export-in-script-setup`](https://eslint.vuejs.org/rules/no-export-in-script-setup.html) rule \*
  - Add [`vue/no-ref-as-operand`](https://eslint.vuejs.org/rules/no-ref-as-operand.html) rule \*
  - Add [`vue/no-setup-props-destructure`](https://eslint.vuejs.org/rules/no-setup-props-destructure.html) rule \*
  - Add [`vue/return-in-emits-validator`](https://eslint.vuejs.org/rules/return-in-emits-validator.html) rule \*
  - Add [`vue/valid-define-emits`](https://eslint.vuejs.org/rules/valid-define-emits.html) rule \*
  - Add [`vue/valid-define-props`](https://eslint.vuejs.org/rules/valid-define-props.html) rule \*
* Change `plugin:vue/strongly-recommended` config:
  - Same changes as above
* Change `plugin:vue/recommended` config:
  - Same changes as above

*Note:* The rules marked with \* are now included in the `plugin:vue/essential` config because [`@vue/composition-api`](https://github.com/vuejs/composition-api) and [`unplugin-vue2-script-setup`](https://github.com/antfu/unplugin-vue2-script-setup/) add (limited) support for these features in Vue 2. If you don't use those libraries, enabling these rules shouldn't affect you.

### Rename rules

* Rename `vue/no-invalid-model-keys` rule to [`vue/valid-model-definition`](https://eslint.vuejs.org/rules/valid-model-definition.html)

### Internal changes

Also, remove unused internal methods:

* `getComponentProps` util method: use `getComponentPropsFromOptions` instead.
* `getComponentEmits` util method: use `getComponentEmitsFromOptions` instead.


FloEdelmann: Maybe also include the following uncategorized rules in presets?

* `plugin:vue/vue3-essential`:
  - `vue/no-invalid-model-keys` (could be renamed to `vue/valid-model-definition` for consistency)
* `plugin:vue/vue3-essential` and `plugin:vue/essential`:
  - `vue/no-use-computed-property-like-method`
  - `vue/no-reserved-component-names`
  - `vue/no-child-content`
* `plugin:vue/vue3-strongly-recommended`:
  - `vue/require-expose`
  - `vue/require-emit-validator`
* `plugin:vue/vue3-recommended` and `plugin:vue/recommended`:
  - `vue/component-options-name-casing`
  - `vue/custom-event-name-casing`
  - `vue/match-component-import-name`
  - `vue/no-multiple-objects-in-class`
  - `vue/no-useless-v-bind`
  - `vue/no-useless-mustaches`
  - `vue/no-v-text`
  - `vue/prefer-separate-static-class`


ota-meshi: Thank you for checking and listing the rules.

> * `plugin:vue/vue3-essential`:
>   - `vue/no-invalid-model-keys` (could be renamed to `vue/valid-model-definition` for consistency)

I think it's a good idea to add the `no-invalid-model-keys` rule to `plugin:vue/essential`.

> * `plugin:vue/vue3-essential` and `plugin:vue/essential`:
>   - `vue/no-use-computed-property-like-method`
>   - `vue/no-reserved-component-names`
>   - `vue/no-child-content`

I think it is a good idea to include these rules in the preset since they are intended to prevent errors.

> * `plugin:vue/vue3-strongly-recommended`:
>   - `vue/require-expose`
>   - `vue/require-emit-validator`
> * `plugin:vue/vue3-recommended` and `plugin:vue/recommended`:
>   - `vue/component-options-name-casing`
>   - `vue/custom-event-name-casing`
>   - `vue/match-component-import-name`
>   - `vue/no-multiple-objects-in-class`
>   - `vue/no-useless-v-bind`
>   - `vue/no-useless-mustaches`
>   - `vue/no-v-text`
>   - `vue/prefer-separate-static-class`

I'm wondering if these should be included in the preset. Since presets are based on style guide, we may not want to include rules that aren't written in the style guide (and Vue official documentation). 
(However, the style guide has not been maintained recently 😓)

It already contains rules that are not written in the style guide. However, many users have complained when new rules (even if the rules are written in the style guide) have been added until now.

ota-meshi: I forgot to write a comment about renaming.
I think it's a good idea to rename the rule as you say :+1:

Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index e695f422..0ba3d33d 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -5,7 +5,6 @@ workflows:
       - eslint-v6
       - eslint-v7
       - ts-eslint-v4
-      - node-v12
       - node-v14
       - node-v16
       - lint
@@ -36,7 +35,7 @@ jobs:
 
   eslint-v6:
     docker:
-      - image: node:12
+      - image: node:14
     steps:
       - run:
           name: Versions
@@ -88,10 +87,6 @@ jobs:
       - run:
           name: Test
           command: npm test
-  node-v12:
-    <<: *node-base
-    docker:
-      - image: node:12
   node-v14:
     <<: *node-base
     docker:
diff --git a/.eslintrc.js b/.eslintrc.js
index 6a944b5e..da047d7b 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -3,7 +3,7 @@
 module.exports = {
   root: true,
   parserOptions: {
-    ecmaVersion: 2018
+    ecmaVersion: 'latest'
   },
   env: {
     es6: true,
@@ -137,7 +137,7 @@ module.exports = {
       files: ['./**/*.vue'],
       parser: require.resolve('vue-eslint-parser'),
       parserOptions: {
-        ecmaVersion: 2020,
+        ecmaVersion: 'latest',
         sourceType: 'module'
       }
     },
diff --git a/docs/.vuepress/components/eslint-code-block.vue b/docs/.vuepress/components/eslint-code-block.vue
index 2c6abd2c..f71e606a 100644
--- a/docs/.vuepress/components/eslint-code-block.vue
+++ b/docs/.vuepress/components/eslint-code-block.vue
@@ -90,7 +90,7 @@ export default {
         rules: this.rules,
         parser: 'vue-eslint-parser',
         parserOptions: {
-          ecmaVersion: 2020,
+          ecmaVersion: 'latest',
           sourceType: 'module',
           ecmaFeatures: {
             jsx: true
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 9a54292c..ffb8f232 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -45,6 +45,7 @@ Rules in this category are enabled for all presets provided by eslint-plugin-vue
 | [vue/multi-word-component-names](./multi-word-component-names.md) | require component names to be always multi-word |  | :three::two::hammer: |
 | [vue/no-arrow-functions-in-watch](./no-arrow-functions-in-watch.md) | disallow using arrow functions to define watcher |  | :three::two::warning: |
 | [vue/no-async-in-computed-properties](./no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties |  | :three::two::warning: |
+| [vue/no-child-content](./no-child-content.md) | disallow element's child contents which would be overwritten by a directive like `v-html` or `v-text` | :bulb: | :three::two::warning: |
 | [vue/no-computed-properties-in-data](./no-computed-properties-in-data.md) | disallow accessing computed properties in `data`. |  | :three::two::warning: |
 | [vue/no-custom-modifiers-on-v-model](./no-custom-modifiers-on-v-model.md) | disallow custom modifiers on v-model used on the component |  | :two::warning: |
 | [vue/no-deprecated-data-object-declaration](./no-deprecated-data-object-declaration.md) | disallow using deprecated object declaration on data (in Vue.js 3.0.0+) | :wrench: | :three::warning: |
@@ -69,27 +70,32 @@ Rules in this category are enabled for all presets provided by eslint-plugin-vue
 | [vue/no-dupe-keys](./no-dupe-keys.md) | disallow duplication of field names |  | :three::two::warning: |
 | [vue/no-dupe-v-else-if](./no-dupe-v-else-if.md) | disallow duplicate conditions in `v-if` / `v-else-if` chains |  | :three::two::warning: |
 | [vue/no-duplicate-attributes](./no-duplicate-attributes.md) | disallow duplication of attributes |  | :three::two::warning: |
-| [vue/no-export-in-script-setup](./no-export-in-script-setup.md) | disallow `export` in `<script setup>` |  | :three::warning: |
+| [vue/no-export-in-script-setup](./no-export-in-script-setup.md) | disallow `export` in `<script setup>` |  | :three::two::warning: |
+| [vue/no-expose-after-await](./no-expose-after-await.md) | disallow asynchronously registered `expose` |  | :three::warning: |
 | [vue/no-lifecycle-after-await](./no-lifecycle-after-await.md) | disallow asynchronously registered lifecycle hooks |  | :three::hammer: |
 | [vue/no-multiple-template-root](./no-multiple-template-root.md) | disallow adding multiple root nodes to the template |  | :two::warning: |
 | [vue/no-mutating-props](./no-mutating-props.md) | disallow mutation of component props |  | :three::two::hammer: |
 | [vue/no-parsing-error](./no-parsing-error.md) | disallow parsing errors in `<template>` |  | :three::two::warning: |
-| [vue/no-ref-as-operand](./no-ref-as-operand.md) | disallow use of value wrapped by `ref()` (Composition API) as an operand | :wrench: | :three::hammer: |
+| [vue/no-ref-as-operand](./no-ref-as-operand.md) | disallow use of value wrapped by `ref()` (Composition API) as an operand | :wrench: | :three::two::hammer: |
+| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions |  | :three::two::hammer: |
 | [vue/no-reserved-keys](./no-reserved-keys.md) | disallow overwriting reserved keys |  | :three::two::hammer: |
 | [vue/no-reserved-props](./no-reserved-props.md) | disallow reserved names in props |  | :three::two::warning: |
-| [vue/no-setup-props-destructure](./no-setup-props-destructure.md) | disallow destructuring of `props` passed to `setup` |  | :three::hammer: |
+| [vue/no-setup-props-destructure](./no-setup-props-destructure.md) | disallow destructuring of `props` passed to `setup` |  | :three::two::hammer: |
 | [vue/no-shared-component-data](./no-shared-component-data.md) | enforce component's data property to be a function | :wrench: | :three::two::warning: |
 | [vue/no-side-effects-in-computed-properties](./no-side-effects-in-computed-properties.md) | disallow side effects in computed properties |  | :three::two::warning: |
 | [vue/no-template-key](./no-template-key.md) | disallow `key` attribute on `<template>` |  | :three::two::warning: |
 | [vue/no-textarea-mustache](./no-textarea-mustache.md) | disallow mustaches in `<textarea>` |  | :three::two::warning: |
 | [vue/no-unused-components](./no-unused-components.md) | disallow registering components that are not used inside templates |  | :three::two::hammer: |
 | [vue/no-unused-vars](./no-unused-vars.md) | disallow unused variable definitions of v-for directives or scope attributes | :bulb: | :three::two::hammer: |
+| [vue/no-use-computed-property-like-method](./no-use-computed-property-like-method.md) | disallow use computed property like method |  | :three::two::warning: |
 | [vue/no-use-v-if-with-v-for](./no-use-v-if-with-v-for.md) | disallow use v-if on the same element as v-for |  | :three::two::hammer: |
 | [vue/no-useless-template-attributes](./no-useless-template-attributes.md) | disallow useless attribute on `<template>` |  | :three::two::warning: |
 | [vue/no-v-for-template-key-on-child](./no-v-for-template-key-on-child.md) | disallow key of `<template v-for>` placed on child elements |  | :three::warning: |
 | [vue/no-v-for-template-key](./no-v-for-template-key.md) | disallow `key` attribute on `<template v-for>` |  | :two::warning: |
 | [vue/no-v-model-argument](./no-v-model-argument.md) | disallow adding an argument to `v-model` used in custom component |  | :two::warning: |
+| [vue/no-v-text-v-html-on-component](./no-v-text-v-html-on-component.md) | disallow v-text / v-html on component |  | :three::two::warning: |
 | [vue/no-watch-after-await](./no-watch-after-await.md) | disallow asynchronously registered `watch` |  | :three::hammer: |
+| [vue/prefer-import-from-vue](./prefer-import-from-vue.md) | enforce import from 'vue' instead of import from '@vue/*' | :wrench: | :three::hammer: |
 | [vue/require-component-is](./require-component-is.md) | require `v-bind:is` of `<component>` elements |  | :three::two::warning: |
 | [vue/require-prop-type-constructor](./require-prop-type-constructor.md) | require prop type to be a constructor | :wrench: | :three::two::hammer: |
 | [vue/require-render-return](./require-render-return.md) | enforce render function to always return value |  | :three::two::warning: |
@@ -98,10 +104,11 @@ Rules in this category are enabled for all presets provided by eslint-plugin-vue
 | [vue/require-v-for-key](./require-v-for-key.md) | require `v-bind:key` with `v-for` directives |  | :three::two::warning: |
 | [vue/require-valid-default-prop](./require-valid-default-prop.md) | enforce props default values to be valid |  | :three::two::hammer: |
 | [vue/return-in-computed-property](./return-in-computed-property.md) | enforce that a return statement is present in computed property |  | :three::two::warning: |
-| [vue/return-in-emits-validator](./return-in-emits-validator.md) | enforce that a return statement is present in emits validator |  | :three::warning: |
+| [vue/return-in-emits-validator](./return-in-emits-validator.md) | enforce that a return statement is present in emits validator |  | :three::two::warning: |
 | [vue/use-v-on-exact](./use-v-on-exact.md) | enforce usage of `exact` modifier on `v-on` |  | :three::two::hammer: |
-| [vue/valid-define-emits](./valid-define-emits.md) | enforce valid `defineEmits` compiler macro |  | :three::warning: |
-| [vue/valid-define-props](./valid-define-props.md) | enforce valid `defineProps` compiler macro |  | :three::warning: |
+| [vue/valid-define-emits](./valid-define-emits.md) | enforce valid `defineEmits` compiler macro |  | :three::two::warning: |
+| [vue/valid-define-props](./valid-define-props.md) | enforce valid `defineProps` compiler macro |  | :three::two::warning: |
+| [vue/valid-model-definition](./valid-model-definition.md) | require valid keys in model option |  | :two::warning: |
 | [vue/valid-next-tick](./valid-next-tick.md) | enforce valid `nextTick` function calls | :wrench::bulb: | :three::two::warning: |
 | [vue/valid-template-root](./valid-template-root.md) | enforce valid template root |  | :three::two::warning: |
 | [vue/valid-v-bind-sync](./valid-v-bind-sync.md) | enforce valid `.sync` modifier on `v-bind` directives |  | :two::warning: |
@@ -210,20 +217,16 @@ For example:
 | [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: | :lipstick: |
 | [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: | :lipstick: |
 | [vue/html-comment-indent](./html-comment-indent.md) | enforce consistent indentation in HTML comments | :wrench: | :lipstick: |
-| [vue/match-component-file-name](./match-component-file-name.md) | require component name property to match its file name |  | :hammer: |
+| [vue/match-component-file-name](./match-component-file-name.md) | require component name property to match its file name | :bulb: | :hammer: |
 | [vue/match-component-import-name](./match-component-import-name.md) | require the registered component name to match the imported component name |  | :warning: |
 | [vue/new-line-between-multi-line-property](./new-line-between-multi-line-property.md) | enforce new lines between multi-line properties in Vue components | :wrench: | :lipstick: |
 | [vue/next-tick-style](./next-tick-style.md) | enforce Promise or callback style in `nextTick` | :wrench: | :hammer: |
 | [vue/no-bare-strings-in-template](./no-bare-strings-in-template.md) | disallow the use of bare strings in `<template>` |  | :hammer: |
 | [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: | :hammer: |
-| [vue/no-child-content](./no-child-content.md) | disallow element's child contents which would be overwritten by a directive like `v-html` or `v-text` | :bulb: | :warning: |
 | [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` |  | :hammer: |
 | [vue/no-empty-component-block](./no-empty-component-block.md) | disallow the `<template>` `<script>` `<style>` block to be empty |  | :hammer: |
-| [vue/no-expose-after-await](./no-expose-after-await.md) | disallow asynchronously registered `expose` |  | :warning: |
-| [vue/no-invalid-model-keys](./no-invalid-model-keys.md) | require valid keys in model option |  | :warning: |
 | [vue/no-multiple-objects-in-class](./no-multiple-objects-in-class.md) | disallow to pass multiple objects into array to class |  | :hammer: |
 | [vue/no-potential-component-option-typo](./no-potential-component-option-typo.md) | disallow a potential typo in your component property | :bulb: | :hammer: |
-| [vue/no-reserved-component-names](./no-reserved-component-names.md) | disallow the use of reserved names in component definitions |  | :hammer: |
 | [vue/no-restricted-block](./no-restricted-block.md) | disallow specific block |  | :hammer: |
 | [vue/no-restricted-call-after-await](./no-restricted-call-after-await.md) | disallow asynchronously called restricted methods |  | :hammer: |
 | [vue/no-restricted-class](./no-restricted-class.md) | disallow specific classes in Vue components |  | :warning: |
@@ -241,13 +244,10 @@ For example:
 | [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: | :hammer: |
 | [vue/no-unused-properties](./no-unused-properties.md) | disallow unused properties |  | :hammer: |
 | [vue/no-unused-refs](./no-unused-refs.md) | disallow unused refs |  | :hammer: |
-| [vue/no-use-computed-property-like-method](./no-use-computed-property-like-method.md) | disallow use computed property like method |  | :warning: |
 | [vue/no-useless-mustaches](./no-useless-mustaches.md) | disallow unnecessary mustache interpolations | :wrench: | :hammer: |
 | [vue/no-useless-v-bind](./no-useless-v-bind.md) | disallow unnecessary `v-bind` directives | :wrench: | :hammer: |
-| [vue/no-v-text-v-html-on-component](./no-v-text-v-html-on-component.md) | disallow v-text / v-html on component |  | :warning: |
 | [vue/no-v-text](./no-v-text.md) | disallow use of v-text |  | :hammer: |
 | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | :lipstick: |
-| [vue/prefer-import-from-vue](./prefer-import-from-vue.md) | enforce import from 'vue' instead of import from '@vue/*' | :wrench: | :hammer: |
 | [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: |
 | [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: | :hammer: |
 | [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: |
@@ -319,4 +319,5 @@ The following rules extend the rules provided by ESLint itself and apply them to
 | [vue/experimental-script-setup-vars](./experimental-script-setup-vars.md) | (no replacement) |
 | [vue/name-property-casing](./name-property-casing.md) | [vue/component-definition-name-casing](./component-definition-name-casing.md) |
 | [vue/no-confusing-v-for-v-if](./no-confusing-v-for-v-if.md) | [vue/no-use-v-if-with-v-for](./no-use-v-if-with-v-for.md) |
+| [vue/no-invalid-model-keys](./no-invalid-model-keys.md) | [vue/valid-model-definition](./valid-model-definition.md) |
 | [vue/no-unregistered-components](./no-unregistered-components.md) | [vue/no-undef-components](./no-undef-components.md) |
diff --git a/docs/rules/custom-event-name-casing.md b/docs/rules/custom-event-name-casing.md
index a2c4d364..29afde05 100644
--- a/docs/rules/custom-event-name-casing.md
+++ b/docs/rules/custom-event-name-casing.md
@@ -13,7 +13,7 @@ Define a style for custom event name casing for consistency purposes.
 
 ## :book: Rule Details
 
-This rule aims to warn the custom event names other than the configured casing.
+This rule aims to warn the custom event names other than the configured casing. (Default is **camelCase**.)
 
 Vue 2 recommends using kebab-case for custom event names.
 
@@ -27,28 +27,28 @@ In Vue 3, using either camelCase or kebab-case for your custom event name does n
 
 See [Guide - Custom Events] for more details.
 
-This rule enforces kebab-case by default.
+This rule enforces camelCase by default.
 
 <eslint-code-block :rules="{'vue/custom-event-name-casing': ['error']}">
 
 ```vue
 <template>
   <!-- ✓ GOOD -->
-  <button @click="$emit('my-event')" />
+  <button @click="$emit('myEvent')" />
 
   <!-- ✗ BAD -->
-  <button @click="$emit('myEvent')" />
+  <button @click="$emit('my-event')" />
 </template>
 <script>
 export default {
   methods: {
     onClick () {
       /* ✓ GOOD */
-      this.$emit('my-event')
+      this.$emit('myEvent')
       this.$emit('update:myProp', myProp)
 
       /* ✗ BAD */
-      this.$emit('myEvent')
+      this.$emit('my-event')
     }
   }
 }
@@ -62,7 +62,7 @@ export default {
 ```json
 {
   "vue/custom-event-name-casing": ["error",
-    "kebab-case" | "camelCase",
+    "camelCase" | "kebab-case",
     {
       "ignores": []
     }
@@ -70,8 +70,8 @@ export default {
 }
 ```
 
-- `"kebab-case"` (default) ... Enforce custom event names to kebab-case.
-- `"camelCase"` ... Enforce custom event names to camelCase.
+- `"camelCase"` (default) ... Enforce custom event names to camelCase.
+- `"kebab-case"` ... Enforce custom event names to kebab-case.
 - `ignores` (`string[]`) ... The event names to ignore. Sets the event name to allow. For example, custom event names, Vue components event with special name, or Vue library component event name. You can set the regexp by writing it like `"/^name/"` or `click:row` or `fooBar`.
 
 ### `"kebab-case"`
@@ -132,18 +132,18 @@ export default {
 
 </eslint-code-block>
 
-### `"ignores": ["fooBar", "/^[a-z]+(?:-[a-z]+)*:[a-z]+(?:-[a-z]+)*$/u"]`
+### `"ignores": ["foo-bar", "/^[a-z]+(?:-[a-z]+)*:[a-z]+(?:-[a-z]+)*$/u"]`
 
-<eslint-code-block :rules="{'vue/custom-event-name-casing': ['error', { ignores: ['fooBar', '/^[a-z]+(?:-[a-z]+)*:[a-z]+(?:-[a-z]+)*$/u'] }]}">
+<eslint-code-block :rules="{'vue/custom-event-name-casing': ['error', 'camelCase', { ignores: ['foo-bar', '/^[a-z]+(?:-[a-z]+)*:[a-z]+(?:-[a-z]+)*$/u'] }]}">
 
 ```vue
 <template>
   <!-- ✓ GOOD -->
   <button @click="$emit('click:row')" />
-  <button @click="$emit('fooBar')" />
+  <button @click="$emit('foo-bar')" />
 
   <!-- ✗ BAD -->
-  <button @click="$emit('myEvent')" />
+  <button @click="$emit('my-event')" />
 </template>
 <script>
 export default {
@@ -151,10 +151,10 @@ export default {
     onClick () {
       /* ✓ GOOD */
       this.$emit('click:row')
-      this.$emit('fooBar')
+      this.$emit('foo-bar')
 
       /* ✗ BAD */
-      this.$emit('myEvent')
+      this.$emit('my-event')
     }
   }
 }
diff --git a/docs/rules/match-component-file-name.md b/docs/rules/match-component-file-name.md
index afd57a39..ae27d13b 100644
--- a/docs/rules/match-component-file-name.md
+++ b/docs/rules/match-component-file-name.md
@@ -9,6 +9,8 @@ since: v5.2.0
 
 > require component name property to match its file name
 
+- :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
+
 This rule reports if a component `name` property does not match its file name.
 
 You can define an array of file extensions this rule should verify for the component's name.
diff --git a/docs/rules/no-child-content.md b/docs/rules/no-child-content.md
index 29523230..940ea183 100644
--- a/docs/rules/no-child-content.md
+++ b/docs/rules/no-child-content.md
@@ -9,6 +9,7 @@ since: v8.1.0
 
 > disallow element's child contents which would be overwritten by a directive like `v-html` or `v-text`
 
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
 - :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
 
 ## :book: Rule Details
diff --git a/docs/rules/no-export-in-script-setup.md b/docs/rules/no-export-in-script-setup.md
index b059305b..c16899c8 100644
--- a/docs/rules/no-export-in-script-setup.md
+++ b/docs/rules/no-export-in-script-setup.md
@@ -9,7 +9,7 @@ since: v7.13.0
 
 > disallow `export` in `<script setup>`
 
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
 
 ## :book: Rule Details
 
diff --git a/docs/rules/no-expose-after-await.md b/docs/rules/no-expose-after-await.md
index 3bd436f4..54c58711 100644
--- a/docs/rules/no-expose-after-await.md
+++ b/docs/rules/no-expose-after-await.md
@@ -9,6 +9,8 @@ since: v8.1.0
 
 > disallow asynchronously registered `expose`
 
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+
 ## :book: Rule Details
 
 This rule reports usages of `expose()` after an `await` expression.  
diff --git a/docs/rules/no-invalid-model-keys.md b/docs/rules/no-invalid-model-keys.md
index 1a46e9a4..34f65d88 100644
--- a/docs/rules/no-invalid-model-keys.md
+++ b/docs/rules/no-invalid-model-keys.md
@@ -9,6 +9,8 @@ since: v7.9.0
 
 > require valid keys in model option
 
+- :warning: This rule was **deprecated** and replaced by [vue/valid-model-definition](valid-model-definition.md) rule.
+
 ## :book: Rule Details
 
 This rule is aimed at preventing invalid keys in model option.
diff --git a/docs/rules/no-ref-as-operand.md b/docs/rules/no-ref-as-operand.md
index 2610d462..df3549dc 100644
--- a/docs/rules/no-ref-as-operand.md
+++ b/docs/rules/no-ref-as-operand.md
@@ -9,7 +9,7 @@ since: v7.0.0
 
 > disallow use of value wrapped by `ref()` (Composition API) as an operand
 
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
 - :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
 
 ## :book: Rule Details
diff --git a/docs/rules/no-reserved-component-names.md b/docs/rules/no-reserved-component-names.md
index a0d9c5f2..94e761ea 100644
--- a/docs/rules/no-reserved-component-names.md
+++ b/docs/rules/no-reserved-component-names.md
@@ -9,6 +9,8 @@ since: v6.1.0
 
 > disallow the use of reserved names in component definitions
 
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+
 ## :book: Rule Details
 
 This rule prevents name collisions between Vue components and standard HTML elements and built-in components.
diff --git a/docs/rules/no-restricted-class.md b/docs/rules/no-restricted-class.md
index d67e5d8b..849c71e8 100644
--- a/docs/rules/no-restricted-class.md
+++ b/docs/rules/no-restricted-class.md
@@ -35,7 +35,7 @@ in the rule configuration.
   <div :class="'forbidden'" />
   <div :class="'forbidden ' + someString" />
   <div :class="[someString, 'forbidden']" />
-  <!-- ✗ GOOD -->
+  <!-- ✓ GOOD -->
   <div class="allowed-class" />
 </template>
 
diff --git a/docs/rules/no-restricted-custom-event.md b/docs/rules/no-restricted-custom-event.md
index bc48def0..b77feac1 100644
--- a/docs/rules/no-restricted-custom-event.md
+++ b/docs/rules/no-restricted-custom-event.md
@@ -31,7 +31,7 @@ This rule takes a list of strings, where each string is a custom event name or p
 <template>
   <!-- ✗ BAD -->
   <input @input="$emit('input', $event.target.value)">
-  <!-- ✗ GOOD -->
+  <!-- ✓ GOOD -->
   <input @input="$emit('update:value', $event.target.value)">
 </template>
 <script>
diff --git a/docs/rules/no-setup-props-destructure.md b/docs/rules/no-setup-props-destructure.md
index 91504a94..d618cf01 100644
--- a/docs/rules/no-setup-props-destructure.md
+++ b/docs/rules/no-setup-props-destructure.md
@@ -9,7 +9,7 @@ since: v7.0.0
 
 > disallow destructuring of `props` passed to `setup`
 
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
 
 ## :book: Rule Details
 
diff --git a/docs/rules/no-use-computed-property-like-method.md b/docs/rules/no-use-computed-property-like-method.md
index 33e49d65..6e267697 100644
--- a/docs/rules/no-use-computed-property-like-method.md
+++ b/docs/rules/no-use-computed-property-like-method.md
@@ -9,6 +9,8 @@ since: v7.15.0
 
 > disallow use computed property like method
 
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
+
 ## :book: Rule Details
 
 This rule disallows to use computed property like method.
@@ -320,7 +322,7 @@ export default {
       this.computedReturnPropsFunction
       this.computedReturnPropsFunction()
       /* ✗ BAD */
-      /* Nope. everything is GOOD!! */
+      /* Nope. everything is good!! */
     }
   }
 }
diff --git a/docs/rules/no-v-text-v-html-on-component.md b/docs/rules/no-v-text-v-html-on-component.md
index 377217dc..420eefda 100644
--- a/docs/rules/no-v-text-v-html-on-component.md
+++ b/docs/rules/no-v-text-v-html-on-component.md
@@ -9,6 +9,8 @@ since: v8.4.0
 
 > disallow v-text / v-html on component
 
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `"plugin:vue/vue3-essential"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/recommended"` and `"plugin:vue/vue3-recommended"`.
+
 ## :book: Rule Details
 
 This rule disallows the use of v-text / v-html on component.
diff --git a/docs/rules/prefer-import-from-vue.md b/docs/rules/prefer-import-from-vue.md
index 3190beac..f1cab9f0 100644
--- a/docs/rules/prefer-import-from-vue.md
+++ b/docs/rules/prefer-import-from-vue.md
@@ -9,6 +9,7 @@ since: v8.5.0
 
 > enforce import from 'vue' instead of import from '@vue/*'
 
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
 - :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
 
 ## :book: Rule Details
diff --git a/docs/rules/return-in-emits-validator.md b/docs/rules/return-in-emits-validator.md
index f952d0a0..08b8b340 100644
--- a/docs/rules/return-in-emits-validator.md
+++ b/docs/rules/return-in-emits-validator.md
@@ -9,7 +9,7 @@ since: v7.0.0
 
 > enforce that a return statement is present in emits validator
 
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
 
 ## :book: Rule Details
 
diff --git a/docs/rules/valid-define-emits.md b/docs/rules/valid-define-emits.md
index 7e00bb14..5718794c 100644
--- a/docs/rules/valid-define-emits.md
+++ b/docs/rules/valid-define-emits.md
@@ -9,7 +9,7 @@ since: v7.13.0
 
 > enforce valid `defineEmits` compiler macro
 
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
 
 This rule checks whether `defineEmits` compiler macro is valid.
 
diff --git a/docs/rules/valid-define-props.md b/docs/rules/valid-define-props.md
index b2c47cb5..bd7d5f75 100644
--- a/docs/rules/valid-define-props.md
+++ b/docs/rules/valid-define-props.md
@@ -9,7 +9,7 @@ since: v7.13.0
 
 > enforce valid `defineProps` compiler macro
 
-- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`.
 
 This rule checks whether `defineProps` compiler macro is valid.
 
diff --git a/docs/rules/valid-model-definition.md b/docs/rules/valid-model-definition.md
new file mode 100644
index 00000000..66fe8f03
--- /dev/null
+++ b/docs/rules/valid-model-definition.md
@@ -0,0 +1,116 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/valid-model-definition
+description: require valid keys in model option
+---
+# vue/valid-model-definition
+
+> require valid keys in model option
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `"plugin:vue/strongly-recommended"` and `"plugin:vue/recommended"`.
+
+## :book: Rule Details
+
+This rule is aimed at preventing invalid keys in model option.
+
+<eslint-code-block :rules="{'vue/valid-model-definition': ['error']}">
+
+```vue
+<script>
+/* ✓ GOOD */
+export default {
+  model: {
+    prop: 'list',
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/valid-model-definition': ['error']}">
+
+```vue
+<script>
+/* ✓ GOOD */
+export default {
+  model: {
+    event: 'update'
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/valid-model-definition': ['error']}">
+
+```vue
+<script>
+/* ✓ GOOD */
+export default {
+  model: {
+    prop: 'list',
+    event: 'update'
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/valid-model-definition': ['error']}">
+
+```vue
+<script>
+/* ✗ BAD */
+export default {
+  model: {
+    prop: 'list',
+    events: 'update'
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/valid-model-definition': ['error']}">
+
+```vue
+<script>
+/* ✗ BAD */
+export default {
+  model: {
+    props: 'list',
+    events: 'update'
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/valid-model-definition': ['error']}">
+
+```vue
+<script>
+/* ✗ BAD */
+export default {
+  model: {
+    name: 'checked',
+    props: 'list',
+    event: 'update'
+  }
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/valid-model-definition.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/valid-model-definition.js)
diff --git a/docs/user-guide/README.md b/docs/user-guide/README.md
index b083a6bd..28e586d2 100644
--- a/docs/user-guide/README.md
+++ b/docs/user-guide/README.md
@@ -23,7 +23,7 @@ yarn add -D eslint eslint-plugin-vue
 ::: tip Requirements
 
 - ESLint v6.2.0 and above
-- Node.js v12.22.x, v14.17.x, v16.x and above
+- Node.js v14.17.x, v16.x and above
 
 :::
 
diff --git a/eslint-internal-rules/no-invalid-meta-docs-categories.js b/eslint-internal-rules/no-invalid-meta-docs-categories.js
index b4812548..c95691c3 100644
--- a/eslint-internal-rules/no-invalid-meta-docs-categories.js
+++ b/eslint-internal-rules/no-invalid-meta-docs-categories.js
@@ -18,10 +18,8 @@
  */
 function getPropertyFromObject(propertyName, node) {
   if (node && node.type === 'ObjectExpression') {
-    const properties = node.properties
-
-    for (const property of properties) {
-      if (property.key.name === propertyName) {
+    for (const property of node.properties) {
+      if (property.type === 'Property' && property.key.name === propertyName) {
         return property
       }
     }
diff --git a/eslint-internal-rules/no-invalid-meta.js b/eslint-internal-rules/no-invalid-meta.js
index f1877ea4..45057c9b 100644
--- a/eslint-internal-rules/no-invalid-meta.js
+++ b/eslint-internal-rules/no-invalid-meta.js
@@ -18,10 +18,8 @@
  */
 function getPropertyFromObject(propertyName, node) {
   if (node && node.type === 'ObjectExpression') {
-    const properties = node.properties
-
-    for (const property of properties) {
-      if (property.key.name === propertyName) {
+    for (const property of node.properties) {
+      if (property.type === 'Property' && property.key.name === propertyName) {
         return property
       }
     }
diff --git a/lib/configs/essential.js b/lib/configs/essential.js
index 970473a4..33cfe64e 100644
--- a/lib/configs/essential.js
+++ b/lib/configs/essential.js
@@ -9,14 +9,18 @@ module.exports = {
     'vue/multi-word-component-names': 'error',
     'vue/no-arrow-functions-in-watch': 'error',
     'vue/no-async-in-computed-properties': 'error',
+    'vue/no-child-content': 'error',
     'vue/no-computed-properties-in-data': 'error',
     'vue/no-custom-modifiers-on-v-model': 'error',
     'vue/no-dupe-keys': 'error',
     'vue/no-dupe-v-else-if': 'error',
     'vue/no-duplicate-attributes': 'error',
+    'vue/no-export-in-script-setup': 'error',
     'vue/no-multiple-template-root': 'error',
     'vue/no-mutating-props': 'error',
     'vue/no-parsing-error': 'error',
+    'vue/no-ref-as-operand': 'error',
+    'vue/no-reserved-component-names': 'error',
     'vue/no-reserved-keys': 'error',
     'vue/no-reserved-props': [
       'error',
@@ -24,23 +28,30 @@ module.exports = {
         vueVersion: 2
       }
     ],
+    'vue/no-setup-props-destructure': 'error',
     'vue/no-shared-component-data': 'error',
     'vue/no-side-effects-in-computed-properties': 'error',
     'vue/no-template-key': 'error',
     'vue/no-textarea-mustache': 'error',
     'vue/no-unused-components': 'error',
     'vue/no-unused-vars': 'error',
+    'vue/no-use-computed-property-like-method': 'error',
     'vue/no-use-v-if-with-v-for': 'error',
     'vue/no-useless-template-attributes': 'error',
     'vue/no-v-for-template-key': 'error',
     'vue/no-v-model-argument': 'error',
+    'vue/no-v-text-v-html-on-component': 'error',
     'vue/require-component-is': 'error',
     'vue/require-prop-type-constructor': 'error',
     'vue/require-render-return': 'error',
     'vue/require-v-for-key': 'error',
     'vue/require-valid-default-prop': 'error',
     'vue/return-in-computed-property': 'error',
+    'vue/return-in-emits-validator': 'error',
     'vue/use-v-on-exact': 'error',
+    'vue/valid-define-emits': 'error',
+    'vue/valid-define-props': 'error',
+    'vue/valid-model-definition': 'error',
     'vue/valid-next-tick': 'error',
     'vue/valid-template-root': 'error',
     'vue/valid-v-bind-sync': 'error',
diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js
index 15577464..1a150733 100644
--- a/lib/configs/vue3-essential.js
+++ b/lib/configs/vue3-essential.js
@@ -9,6 +9,7 @@ module.exports = {
     'vue/multi-word-component-names': 'error',
     'vue/no-arrow-functions-in-watch': 'error',
     'vue/no-async-in-computed-properties': 'error',
+    'vue/no-child-content': 'error',
     'vue/no-computed-properties-in-data': 'error',
     'vue/no-deprecated-data-object-declaration': 'error',
     'vue/no-deprecated-destroyed-lifecycle': 'error',
@@ -33,10 +34,12 @@ module.exports = {
     'vue/no-dupe-v-else-if': 'error',
     'vue/no-duplicate-attributes': 'error',
     'vue/no-export-in-script-setup': 'error',
+    'vue/no-expose-after-await': 'error',
     'vue/no-lifecycle-after-await': 'error',
     'vue/no-mutating-props': 'error',
     'vue/no-parsing-error': 'error',
     'vue/no-ref-as-operand': 'error',
+    'vue/no-reserved-component-names': 'error',
     'vue/no-reserved-keys': 'error',
     'vue/no-reserved-props': 'error',
     'vue/no-setup-props-destructure': 'error',
@@ -46,10 +49,13 @@ module.exports = {
     'vue/no-textarea-mustache': 'error',
     'vue/no-unused-components': 'error',
     'vue/no-unused-vars': 'error',
+    'vue/no-use-computed-property-like-method': 'error',
     'vue/no-use-v-if-with-v-for': 'error',
     'vue/no-useless-template-attributes': 'error',
     'vue/no-v-for-template-key-on-child': 'error',
+    'vue/no-v-text-v-html-on-component': 'error',
     'vue/no-watch-after-await': 'error',
+    'vue/prefer-import-from-vue': 'error',
     'vue/require-component-is': 'error',
     'vue/require-prop-type-constructor': 'error',
     'vue/require-render-return': 'error',
diff --git a/lib/index.js b/lib/index.js
index 39f91b51..08423c75 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -202,6 +202,7 @@ module.exports = {
     'v-slot-style': require('./rules/v-slot-style'),
     'valid-define-emits': require('./rules/valid-define-emits'),
     'valid-define-props': require('./rules/valid-define-props'),
+    'valid-model-definition': require('./rules/valid-model-definition'),
     'valid-next-tick': require('./rules/valid-next-tick'),
     'valid-template-root': require('./rules/valid-template-root'),
     'valid-v-bind-sync': require('./rules/valid-v-bind-sync'),
diff --git a/lib/rules/block-tag-newline.js b/lib/rules/block-tag-newline.js
index 1fe427af..8459f576 100644
--- a/lib/rules/block-tag-newline.js
+++ b/lib/rules/block-tag-newline.js
@@ -347,13 +347,6 @@ module.exports = {
 
     const verify = normalizeOptionValue(context.options[0])
 
-    /**
-     * @returns {VElement[]}
-     */
-    function getTopLevelHTMLElements() {
-      return documentFragment.children.filter(utils.isVElement)
-    }
-
     return utils.defineTemplateBodyVisitor(
       context,
       {},
@@ -364,8 +357,10 @@ module.exports = {
             return
           }
 
-          for (const element of getTopLevelHTMLElements()) {
-            verify(element)
+          for (const element of documentFragment.children) {
+            if (utils.isVElement(element)) {
+              verify(element)
+            }
           }
         }
       }
diff --git a/lib/rules/custom-event-name-casing.js b/lib/rules/custom-event-name-casing.js
index f59fa9c2..5db15a87 100644
--- a/lib/rules/custom-event-name-casing.js
+++ b/lib/rules/custom-event-name-casing.js
@@ -22,7 +22,7 @@ const { toRegExp } = require('../utils/regexp')
 // ------------------------------------------------------------------------------
 
 const ALLOWED_CASE_OPTIONS = ['kebab-case', 'camelCase']
-const DEFAULT_CASE = 'kebab-case'
+const DEFAULT_CASE = 'camelCase'
 
 /**
  * Get the name param node from the given CallExpression
diff --git a/lib/rules/html-closing-bracket-newline.js b/lib/rules/html-closing-bracket-newline.js
index 6963de8f..9ac1d0f5 100644
--- a/lib/rules/html-closing-bracket-newline.js
+++ b/lib/rules/html-closing-bracket-newline.js
@@ -68,7 +68,7 @@ module.exports = {
       context.parserServices.getTemplateBodyTokenStore &&
       context.parserServices.getTemplateBodyTokenStore()
 
-    return utils.defineTemplateBodyVisitor(context, {
+    return utils.defineDocumentVisitor(context, {
       /** @param {VStartTag | VEndTag} node */
       'VStartTag, VEndTag'(node) {
         const closingBracketToken = template.getLastToken(node)
diff --git a/lib/rules/html-closing-bracket-spacing.js b/lib/rules/html-closing-bracket-spacing.js
index 95de6d01..e60fba49 100644
--- a/lib/rules/html-closing-bracket-spacing.js
+++ b/lib/rules/html-closing-bracket-spacing.js
@@ -92,7 +92,7 @@ module.exports = {
       context.parserServices.getTemplateBodyTokenStore()
     const options = parseOptions(context.options[0], tokens)
 
-    return utils.defineTemplateBodyVisitor(context, {
+    return utils.defineDocumentVisitor(context, {
       /** @param {VStartTag | VEndTag} node */
       'VStartTag, VEndTag'(node) {
         const type = options.detectType(node)
diff --git a/lib/rules/match-component-file-name.js b/lib/rules/match-component-file-name.js
index 57fd4ff8..40d01ba0 100644
--- a/lib/rules/match-component-file-name.js
+++ b/lib/rules/match-component-file-name.js
@@ -31,6 +31,8 @@ function canVerify(node) {
 
 module.exports = {
   meta: {
+    // eslint-disable-next-line eslint-plugin/require-meta-has-suggestions
+    hasSuggestions: true,
     type: 'suggestion',
     docs: {
       description: 'require component name property to match its file name',
@@ -114,7 +116,17 @@ module.exports = {
           node,
           message:
             'Component name `{{name}}` should match file name `{{filename}}`.',
-          data: { filename, name }
+          data: { filename, name },
+          suggest: [
+            {
+              desc: 'Rename component to match file name.',
+              fix(fixer) {
+                const quote =
+                  node.type === 'TemplateLiteral' ? '`' : node.raw[0]
+                return fixer.replaceText(node, `${quote}${filename}${quote}`)
+              }
+            }
+          ]
         })
       }
     }
diff --git a/lib/rules/no-child-content.js b/lib/rules/no-child-content.js
index 138a425c..1eb7fbdf 100644
--- a/lib/rules/no-child-content.js
+++ b/lib/rules/no-child-content.js
@@ -79,7 +79,7 @@ module.exports = {
     docs: {
       description:
         "disallow element's child contents which would be overwritten by a directive like `v-html` or `v-text`",
-      categories: undefined,
+      categories: ['vue3-essential', 'essential'],
       url: 'https://eslint.vuejs.org/rules/no-child-content.html'
     },
     fixable: null,
diff --git a/lib/rules/no-export-in-script-setup.js b/lib/rules/no-export-in-script-setup.js
index fe825fdd..0105d52f 100644
--- a/lib/rules/no-export-in-script-setup.js
+++ b/lib/rules/no-export-in-script-setup.js
@@ -17,7 +17,7 @@ module.exports = {
     type: 'problem',
     docs: {
       description: 'disallow `export` in `<script setup>`',
-      categories: ['vue3-essential'],
+      categories: ['vue3-essential', 'essential'],
       url: 'https://eslint.vuejs.org/rules/no-export-in-script-setup.html'
     },
     fixable: null,
diff --git a/lib/rules/no-expose-after-await.js b/lib/rules/no-expose-after-await.js
index e1e3104d..9d753ab8 100644
--- a/lib/rules/no-expose-after-await.js
+++ b/lib/rules/no-expose-after-await.js
@@ -40,8 +40,7 @@ module.exports = {
     type: 'problem',
     docs: {
       description: 'disallow asynchronously registered `expose`',
-      categories: undefined,
-      // categories: ['vue3-essential'], TODO Change with the major version
+      categories: ['vue3-essential'],
       url: 'https://eslint.vuejs.org/rules/no-expose-after-await.html'
     },
     fixable: null,
diff --git a/lib/rules/no-invalid-model-keys.js b/lib/rules/no-invalid-model-keys.js
index f3a08eda..1695e6fa 100644
--- a/lib/rules/no-invalid-model-keys.js
+++ b/lib/rules/no-invalid-model-keys.js
@@ -1,57 +1,22 @@
-/**
- * @fileoverview Requires valid keys in model option.
- * @author Alex Sokolov
- */
 'use strict'
 
-const utils = require('../utils')
-
-// ------------------------------------------------------------------------------
-// Rule Definition
-// ------------------------------------------------------------------------------
-const VALID_MODEL_KEYS = new Set(['prop', 'event'])
+const baseRule = require('./valid-model-definition')
 
 module.exports = {
   meta: {
-    type: 'problem',
+    ...baseRule.meta,
+    type: baseRule.meta.type,
     docs: {
-      description: 'require valid keys in model option',
+      description: baseRule.meta.docs.description,
       categories: undefined,
       url: 'https://eslint.vuejs.org/rules/no-invalid-model-keys.html'
     },
-    fixable: null,
+    deprecated: true,
+    replacedBy: ['valid-model-definition'],
     schema: []
   },
   /** @param {RuleContext} context */
   create(context) {
-    // ----------------------------------------------------------------------
-    // Public
-    // ----------------------------------------------------------------------
-
-    return utils.executeOnVue(context, (obj) => {
-      const modelProperty = utils.findProperty(obj, 'model')
-      if (!modelProperty || modelProperty.value.type !== 'ObjectExpression') {
-        return
-      }
-
-      for (const p of modelProperty.value.properties) {
-        if (p.type !== 'Property') {
-          continue
-        }
-        const name = utils.getStaticPropertyName(p)
-        if (!name) {
-          continue
-        }
-        if (!VALID_MODEL_KEYS.has(name)) {
-          context.report({
-            node: p,
-            message: "Invalid key '{{name}}' in model option.",
-            data: {
-              name
-            }
-          })
-        }
-      }
-    })
+    return baseRule.create(context)
   }
 }
diff --git a/lib/rules/no-ref-as-operand.js b/lib/rules/no-ref-as-operand.js
index 465ec50b..4f536daf 100644
--- a/lib/rules/no-ref-as-operand.js
+++ b/lib/rules/no-ref-as-operand.js
@@ -12,7 +12,7 @@ module.exports = {
     docs: {
       description:
         'disallow use of value wrapped by `ref()` (Composition API) as an operand',
-      categories: ['vue3-essential'],
+      categories: ['vue3-essential', 'essential'],
       url: 'https://eslint.vuejs.org/rules/no-ref-as-operand.html'
     },
     fixable: 'code',
diff --git a/lib/rules/no-reserved-component-names.js b/lib/rules/no-reserved-component-names.js
index 68d2a2b7..81b03530 100644
--- a/lib/rules/no-reserved-component-names.js
+++ b/lib/rules/no-reserved-component-names.js
@@ -81,7 +81,7 @@ module.exports = {
     docs: {
       description:
         'disallow the use of reserved names in component definitions',
-      categories: undefined,
+      categories: ['vue3-essential', 'essential'],
       url: 'https://eslint.vuejs.org/rules/no-reserved-component-names.html'
     },
     fixable: null,
diff --git a/lib/rules/no-setup-props-destructure.js b/lib/rules/no-setup-props-destructure.js
index ee3699c5..4c8f6109 100644
--- a/lib/rules/no-setup-props-destructure.js
+++ b/lib/rules/no-setup-props-destructure.js
@@ -11,7 +11,7 @@ module.exports = {
     type: 'suggestion',
     docs: {
       description: 'disallow destructuring of `props` passed to `setup`',
-      categories: ['vue3-essential'],
+      categories: ['vue3-essential', 'essential'],
       url: 'https://eslint.vuejs.org/rules/no-setup-props-destructure.html'
     },
     fixable: null,
diff --git a/lib/rules/no-use-computed-property-like-method.js b/lib/rules/no-use-computed-property-like-method.js
index e865f044..957f2b6a 100644
--- a/lib/rules/no-use-computed-property-like-method.js
+++ b/lib/rules/no-use-computed-property-like-method.js
@@ -499,7 +499,7 @@ module.exports = {
     type: 'problem',
     docs: {
       description: 'disallow use computed property like method',
-      categories: undefined,
+      categories: ['vue3-essential', 'essential'],
       url: 'https://eslint.vuejs.org/rules/no-use-computed-property-like-method.html'
     },
     fixable: null,
diff --git a/lib/rules/no-v-text-v-html-on-component.js b/lib/rules/no-v-text-v-html-on-component.js
index 4a208627..d6af976b 100644
--- a/lib/rules/no-v-text-v-html-on-component.js
+++ b/lib/rules/no-v-text-v-html-on-component.js
@@ -19,9 +19,7 @@ module.exports = {
     type: 'problem',
     docs: {
       description: 'disallow v-text / v-html on component',
-      // TODO We will change it in the next major version.
-      // categories: ['essential', 'vue3-essential'],
-      categories: undefined,
+      categories: ['essential', 'vue3-essential'],
       url: 'https://eslint.vuejs.org/rules/no-v-text-v-html-on-component.html'
     },
     fixable: null,
diff --git a/lib/rules/prefer-import-from-vue.js b/lib/rules/prefer-import-from-vue.js
index ef154854..2b3fc5b8 100644
--- a/lib/rules/prefer-import-from-vue.js
+++ b/lib/rules/prefer-import-from-vue.js
@@ -54,9 +54,7 @@ module.exports = {
     type: 'suggestion',
     docs: {
       description: "enforce import from 'vue' instead of import from '@vue/*'",
-      // TODO We will change it in the next major version.
-      // categories: ['vue3-essential'],
-      categories: undefined,
+      categories: ['vue3-essential'],
       url: 'https://eslint.vuejs.org/rules/prefer-import-from-vue.html'
     },
     fixable: 'code',
diff --git a/lib/rules/return-in-emits-validator.js b/lib/rules/return-in-emits-validator.js
index a3c2e257..91c9e667 100644
--- a/lib/rules/return-in-emits-validator.js
+++ b/lib/rules/return-in-emits-validator.js
@@ -41,7 +41,7 @@ module.exports = {
     docs: {
       description:
         'enforce that a return statement is present in emits validator',
-      categories: ['vue3-essential'],
+      categories: ['vue3-essential', 'essential'],
       url: 'https://eslint.vuejs.org/rules/return-in-emits-validator.html'
     },
     fixable: null, // or "code" or "whitespace"
diff --git a/lib/rules/valid-define-emits.js b/lib/rules/valid-define-emits.js
index dc23ac3d..f225c7af 100644
--- a/lib/rules/valid-define-emits.js
+++ b/lib/rules/valid-define-emits.js
@@ -12,7 +12,7 @@ module.exports = {
     type: 'problem',
     docs: {
       description: 'enforce valid `defineEmits` compiler macro',
-      categories: ['vue3-essential'],
+      categories: ['vue3-essential', 'essential'],
       url: 'https://eslint.vuejs.org/rules/valid-define-emits.html'
     },
     fixable: null,
diff --git a/lib/rules/valid-define-props.js b/lib/rules/valid-define-props.js
index 8521bd51..60101f49 100644
--- a/lib/rules/valid-define-props.js
+++ b/lib/rules/valid-define-props.js
@@ -12,7 +12,7 @@ module.exports = {
     type: 'problem',
     docs: {
       description: 'enforce valid `defineProps` compiler macro',
-      categories: ['vue3-essential'],
+      categories: ['vue3-essential', 'essential'],
       url: 'https://eslint.vuejs.org/rules/valid-define-props.html'
     },
     fixable: null,
diff --git a/lib/rules/valid-model-definition.js b/lib/rules/valid-model-definition.js
new file mode 100644
index 00000000..5b7435f4
--- /dev/null
+++ b/lib/rules/valid-model-definition.js
@@ -0,0 +1,57 @@
+/**
+ * @fileoverview Requires valid keys in model option.
+ * @author Alex Sokolov
+ */
+'use strict'
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+const VALID_MODEL_KEYS = new Set(['prop', 'event'])
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'require valid keys in model option',
+      categories: ['essential'],
+      url: 'https://eslint.vuejs.org/rules/valid-model-definition.html'
+    },
+    fixable: null,
+    schema: []
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    // ----------------------------------------------------------------------
+    // Public
+    // ----------------------------------------------------------------------
+
+    return utils.executeOnVue(context, (obj) => {
+      const modelProperty = utils.findProperty(obj, 'model')
+      if (!modelProperty || modelProperty.value.type !== 'ObjectExpression') {
+        return
+      }
+
+      for (const p of modelProperty.value.properties) {
+        if (p.type !== 'Property') {
+          continue
+        }
+        const name = utils.getStaticPropertyName(p)
+        if (!name) {
+          continue
+        }
+        if (!VALID_MODEL_KEYS.has(name)) {
+          context.report({
+            node: p,
+            message: "Invalid key '{{name}}' in model option.",
+            data: {
+              name
+            }
+          })
+        }
+      }
+    })
+  }
+}
diff --git a/lib/utils/index.js b/lib/utils/index.js
index fba712a1..63a838bc 100644
--- a/lib/utils/index.js
+++ b/lib/utils/index.js
@@ -895,24 +895,12 @@ module.exports = {
    * @return {(ComponentArrayProp | ComponentObjectProp | ComponentUnknownProp)[]} Array of component props
    */
   getComponentPropsFromOptions,
-  // TODO Since `utils` is used in some popular packages, we will remove it in the major version.
-  /**
-   * For backward compatibility.
-   * @deprecated Use getComponentPropsFromOptions instead.
-   */
-  getComponentProps: getComponentPropsFromOptions,
   /**
    * Get all emits by looking at all component's properties
    * @param {ObjectExpression} componentObject Object with component definition
    * @return {(ComponentArrayEmit | ComponentObjectEmit | ComponentUnknownEmit)[]} Array of component emits
    */
   getComponentEmitsFromOptions,
-  // TODO Since `utils` is used in some popular packages, we will remove it in the major version.
-  /**
-   * For backward compatibility.
-   * @deprecated Use getComponentEmitsFromOptions instead.
-   */
-  getComponentEmits: getComponentEmitsFromOptions,
 
   /**
    * Get all computed properties by looking at all component's properties
diff --git a/package.json b/package.json
index 093aab99..d783f3da 100644
--- a/package.json
+++ b/package.json
@@ -48,7 +48,7 @@
     "url": "https://github.com/vuejs/eslint-plugin-vue/issues"
   },
   "engines": {
-    "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+    "node": "^14.17.0 || >=16.0.0"
   },
   "peerDependencies": {
     "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0"
diff --git a/tools/update-docs.js b/tools/update-docs.js
index c34f3e1a..80211d27 100644
--- a/tools/update-docs.js
+++ b/tools/update-docs.js
@@ -223,6 +223,25 @@ ${
 
     return this
   }
+
+  checkGoodBadSymbols() {
+    const lines = this.content.split('\n')
+    for (const [lineNumber, line] of lines.entries()) {
+      const lineNumberFilePath = `${this.filePath}:${lineNumber + 1}`
+
+      if (line.includes('GOOD') && !line.includes('✓ GOOD')) {
+        throw new Error(
+          `Expected '✓ GOOD' instead of '${line}' in '${lineNumberFilePath}'`
+        )
+      }
+
+      if (line.includes('BAD') && !line.includes('✗ BAD')) {
+        throw new Error(
+          `Expected '✗ BAD' instead of '${line}' in '${lineNumberFilePath}'`
+        )
+      }
+    }
+  }
 }
 
 for (const rule of rules) {
@@ -234,4 +253,5 @@ for (const rule of rules) {
     .adjustCodeBlocks()
     .write()
     .checkHeadings()
+    .checkGoodBadSymbols()
 }
diff --git a/typings/eslint-plugin-vue/util-types/ast/es-ast.ts b/typings/eslint-plugin-vue/util-types/ast/es-ast.ts
index bafff6d2..16a734d2 100644
--- a/typings/eslint-plugin-vue/util-types/ast/es-ast.ts
+++ b/typings/eslint-plugin-vue/util-types/ast/es-ast.ts
@@ -347,6 +347,7 @@ export interface PrivateIdentifier extends HasParentNode {
 export interface Literal extends HasParentNode {
   type: 'Literal'
   value: string | boolean | null | number | RegExp | BigInt
+  raw: string
   regex?: {
     pattern: string
     flags: string


Test patch:
diff --git a/tests/lib/rules/custom-event-name-casing.js b/tests/lib/rules/custom-event-name-casing.js
index 00b76e4e..8ad20377 100644
--- a/tests/lib/rules/custom-event-name-casing.js
+++ b/tests/lib/rules/custom-event-name-casing.js
@@ -41,7 +41,8 @@ tester.run('custom-event-name-casing', rule, {
         }
       }
       </script>
-      `
+      `,
+      options: ['kebab-case']
     },
     {
       filename: 'test.vue',
@@ -67,7 +68,8 @@ tester.run('custom-event-name-casing', rule, {
         }
       }
       </script>
-      `
+      `,
+      options: ['kebab-case']
     },
     {
       filename: 'test.vue',
@@ -92,7 +94,8 @@ tester.run('custom-event-name-casing', rule, {
         }
       }
       </script>
-      `
+      `,
+      options: ['kebab-case']
     },
     {
       filename: 'test.vue',
@@ -117,7 +120,8 @@ tester.run('custom-event-name-casing', rule, {
         }
       }
       </script>
-      `
+      `,
+      options: ['kebab-case']
     },
     {
       filename: 'test.vue',
@@ -149,7 +153,8 @@ tester.run('custom-event-name-casing', rule, {
         },
       }
       </script>
-      `
+      `,
+      options: ['kebab-case']
     },
     {
       filename: 'test.vue',
@@ -165,7 +170,8 @@ tester.run('custom-event-name-casing', rule, {
         },
       }
       </script>
-      `
+      `,
+      options: ['kebab-case']
     },
     {
       filename: 'test.vue',
@@ -269,6 +275,60 @@ tester.run('custom-event-name-casing', rule, {
       </script>
       `,
       options: ['camelCase']
+    },
+    // Default
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <input
+          @click="$emit('fooBar')">
+      </template>
+      <script>
+      export default {
+        setup(props, context) {
+          return {
+            onInput(value) {
+              context.emit('barBaz')
+            }
+          }
+        },
+        methods: {
+          onClick() {
+            this.$emit('bazQux')
+          }
+        }
+      }
+      </script>
+      `
+    },
+
+    // kebab-case
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <input
+          @click="$emit('foo-bar')">
+      </template>
+      <script>
+      export default {
+        setup(props, context) {
+          return {
+            onInput(value) {
+              context.emit('bar-baz')
+            }
+          }
+        },
+        methods: {
+          onClick() {
+            this.$emit('baz-qux')
+          }
+        }
+      }
+      </script>
+      `,
+      options: ['kebab-case']
     }
   ],
   invalid: [
@@ -296,6 +356,7 @@ tester.run('custom-event-name-casing', rule, {
       }
       </script>
       `,
+      options: ['kebab-case'],
       errors: [
         {
           message: "Custom event name 'fooBar' must be kebab-case.",
@@ -344,6 +405,7 @@ tester.run('custom-event-name-casing', rule, {
       }
       </script>
       `,
+      options: ['kebab-case'],
       errors: [
         "Custom event name 'fooBar' must be kebab-case.",
         "Custom event name 'barBaz' must be kebab-case.",
@@ -374,6 +436,7 @@ tester.run('custom-event-name-casing', rule, {
       }
       </script>
       `,
+      options: ['kebab-case'],
       errors: [
         "Custom event name 'fooBar' must be kebab-case.",
         "Custom event name 'barBaz' must be kebab-case.",
@@ -448,6 +511,7 @@ tester.run('custom-event-name-casing', rule, {
         "Custom event name 'click/row' must be kebab-case."
       ]
     },
+    // camelCase
     {
       filename: 'test.vue',
       code: `
@@ -479,6 +543,69 @@ tester.run('custom-event-name-casing', rule, {
         "Custom event name 'baz-qux' must be camelCase."
       ]
     },
+    // Default
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <input
+          @click="$emit('foo-bar')">
+      </template>
+      <script>
+      export default {
+        setup(props, context) {
+          return {
+            onInput(value) {
+              context.emit('bar-baz')
+            }
+          }
+        },
+        methods: {
+          onClick() {
+            this.$emit('baz-qux')
+          }
+        }
+      }
+      </script>
+      `,
+      errors: [
+        "Custom event name 'foo-bar' must be camelCase.",
+        "Custom event name 'bar-baz' must be camelCase.",
+        "Custom event name 'baz-qux' must be camelCase."
+      ]
+    },
+    // kebab-case
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <input
+          @click="$emit('fooBar')">
+      </template>
+      <script>
+      export default {
+        setup(props, context) {
+          return {
+            onInput(value) {
+              context.emit('barBaz')
+            }
+          }
+        },
+        methods: {
+          onClick() {
+            this.$emit('bazQux')
+          }
+        }
+      }
+      </script>
+      `,
+      options: ['kebab-case'],
+      errors: [
+        "Custom event name 'fooBar' must be kebab-case.",
+        "Custom event name 'barBaz' must be kebab-case.",
+        "Custom event name 'bazQux' must be kebab-case."
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -490,8 +617,8 @@ tester.run('custom-event-name-casing', rule, {
       `,
       errors: [
         {
-          message: "Custom event name 'fooBar' must be kebab-case.",
-          line: 4
+          message: "Custom event name 'foo-bar' must be camelCase.",
+          line: 5
         }
       ]
     }
diff --git a/tests/lib/rules/html-closing-bracket-newline.js b/tests/lib/rules/html-closing-bracket-newline.js
index 4c07c80d..b72c46b9 100644
--- a/tests/lib/rules/html-closing-bracket-newline.js
+++ b/tests/lib/rules/html-closing-bracket-newline.js
@@ -337,6 +337,156 @@ tester.run('html-closing-bracket-newline', rule, {
         'Expected 1 line break before closing bracket, but no line breaks found.',
         'Expected 1 line break before closing bracket, but no line breaks found.'
       ]
+    },
+    {
+      code: `
+        <template
+        >
+        </template
+        >
+        <script
+        >
+        </script
+        >
+        <style
+        ></style
+        >
+      `,
+      output: `
+        <template>
+        </template>
+        <script>
+        </script>
+        <style></style>
+      `,
+      options: [
+        {
+          singleline: 'never',
+          multiline: 'never'
+        }
+      ],
+      errors: [
+        {
+          message:
+            'Expected no line breaks before closing bracket, but 1 line break found.',
+          line: 2,
+          column: 18,
+          endLine: 3,
+          endColumn: 9
+        },
+        {
+          message:
+            'Expected no line breaks before closing bracket, but 1 line break found.',
+          line: 4,
+          column: 19,
+          endLine: 5,
+          endColumn: 9
+        },
+        {
+          message:
+            'Expected no line breaks before closing bracket, but 1 line break found.',
+          line: 6,
+          column: 16,
+          endLine: 7,
+          endColumn: 9
+        },
+        {
+          message:
+            'Expected no line breaks before closing bracket, but 1 line break found.',
+          line: 8,
+          column: 17,
+          endLine: 9,
+          endColumn: 9
+        },
+        {
+          message:
+            'Expected no line breaks before closing bracket, but 1 line break found.',
+          line: 10,
+          column: 15,
+          endLine: 11,
+          endColumn: 9
+        },
+        {
+          message:
+            'Expected no line breaks before closing bracket, but 1 line break found.',
+          line: 11,
+          column: 17,
+          endLine: 12,
+          endColumn: 9
+        }
+      ]
+    },
+    {
+      code: `
+        <template>
+        </template>
+        <script>
+        </script>
+        <style></style>
+      `,
+      output: `
+        <template
+>
+        </template
+>
+        <script
+>
+        </script
+>
+        <style
+></style
+>
+      `,
+      options: [
+        {
+          singleline: 'always',
+          multiline: 'always'
+        }
+      ],
+      errors: [
+        {
+          message:
+            'Expected 1 line break before closing bracket, but no line breaks found.',
+          line: 2,
+          column: 18,
+          endColumn: 18
+        },
+        {
+          message:
+            'Expected 1 line break before closing bracket, but no line breaks found.',
+          line: 3,
+          column: 19,
+          endColumn: 19
+        },
+        {
+          message:
+            'Expected 1 line break before closing bracket, but no line breaks found.',
+          line: 4,
+          column: 16,
+          endColumn: 16
+        },
+        {
+          message:
+            'Expected 1 line break before closing bracket, but no line breaks found.',
+          line: 5,
+          column: 17,
+          endColumn: 17
+        },
+        {
+          message:
+            'Expected 1 line break before closing bracket, but no line breaks found.',
+          line: 6,
+          column: 15,
+          endColumn: 15
+        },
+        {
+          message:
+            'Expected 1 line break before closing bracket, but no line breaks found.',
+          line: 6,
+          column: 23,
+          endColumn: 23
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/html-closing-bracket-spacing.js b/tests/lib/rules/html-closing-bracket-spacing.js
index 154ba87b..af03d0bf 100644
--- a/tests/lib/rules/html-closing-bracket-spacing.js
+++ b/tests/lib/rules/html-closing-bracket-spacing.js
@@ -114,6 +114,56 @@ ruleTester.run('html-closing-bracket-spacing', rule, {
         }
       ]
     },
+    {
+      code: `
+        <template ></template >
+        <script ></script >
+        <style ></style >
+      `,
+      output: `
+        <template></template>
+        <script></script>
+        <style></style>
+      `,
+      errors: [
+        {
+          message: "Expected no space before '>', but found.",
+          line: 2,
+          column: 18,
+          endColumn: 20
+        },
+        {
+          message: "Expected no space before '>', but found.",
+          line: 2,
+          column: 30,
+          endColumn: 32
+        },
+        {
+          message: "Expected no space before '>', but found.",
+          line: 3,
+          column: 16,
+          endColumn: 18
+        },
+        {
+          message: "Expected no space before '>', but found.",
+          line: 3,
+          column: 26,
+          endColumn: 28
+        },
+        {
+          message: "Expected no space before '>', but found.",
+          line: 4,
+          column: 15,
+          endColumn: 17
+        },
+        {
+          message: "Expected no space before '>', but found.",
+          line: 4,
+          column: 24,
+          endColumn: 26
+        }
+      ]
+    },
     {
       code: '<template >\n  <div>\n  </div>\n  <div />\n</template >',
       output: '<template >\n  <div >\n  </div >\n  <div/>\n</template >',
@@ -144,6 +194,63 @@ ruleTester.run('html-closing-bracket-spacing', rule, {
           endColumn: 10
         }
       ]
+    },
+    {
+      code: `
+        <template></template>
+        <script></script>
+        <style></style>
+      `,
+      output: `
+        <template ></template >
+        <script ></script >
+        <style ></style >
+      `,
+      options: [
+        {
+          startTag: 'always',
+          endTag: 'always',
+          selfClosingTag: 'never'
+        }
+      ],
+      errors: [
+        {
+          message: "Expected a space before '>', but not found.",
+          line: 2,
+          column: 18,
+          endColumn: 19
+        },
+        {
+          message: "Expected a space before '>', but not found.",
+          line: 2,
+          column: 29,
+          endColumn: 30
+        },
+        {
+          message: "Expected a space before '>', but not found.",
+          line: 3,
+          column: 16,
+          endColumn: 17
+        },
+        {
+          message: "Expected a space before '>', but not found.",
+          line: 3,
+          column: 25,
+          endColumn: 26
+        },
+        {
+          message: "Expected a space before '>', but not found.",
+          line: 4,
+          column: 15,
+          endColumn: 16
+        },
+        {
+          message: "Expected a space before '>', but not found.",
+          line: 4,
+          column: 23,
+          endColumn: 24
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/match-component-file-name.js b/tests/lib/rules/match-component-file-name.js
index d1b4c337..83610962 100644
--- a/tests/lib/rules/match-component-file-name.js
+++ b/tests/lib/rules/match-component-file-name.js
@@ -564,7 +564,18 @@ ruleTester.run('match-component-file-name', rule, {
       errors: [
         {
           message:
-            'Component name `MComponent` should match file name `MyComponent`.'
+            'Component name `MComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        export default {
+          name: 'MyComponent',
+          render() { return <div /> }
+        }
+      `
+            }
+          ]
         }
       ]
     },
@@ -581,7 +592,46 @@ ruleTester.run('match-component-file-name', rule, {
       errors: [
         {
           message:
-            'Component name `MComponent` should match file name `MyComponent`.'
+            'Component name `MComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        export default {
+          name: 'MyComponent',
+          render() { return <div /> }
+        }
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'MyComponent.jsx',
+      code: `
+        export default {
+          name: "MComponent",
+          render() { return <div /> }
+        }
+      `,
+      options: [{ extensions: ['jsx'] }],
+      parserOptions: jsxParserOptions,
+      errors: [
+        {
+          message:
+            'Component name `MComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        export default {
+          name: "MyComponent",
+          render() { return <div /> }
+        }
+      `
+            }
+          ]
         }
       ]
     },
@@ -598,7 +648,18 @@ ruleTester.run('match-component-file-name', rule, {
       errors: [
         {
           message:
-            'Component name `MComponent` should match file name `MyComponent`.'
+            'Component name `MComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        export default {
+          name: \`MyComponent\`,
+          render() { return <div /> }
+        }
+      `
+            }
+          ]
         }
       ]
     },
@@ -620,7 +681,53 @@ ruleTester.run('match-component-file-name', rule, {
       errors: [
         {
           message:
-            'Component name `MComponent` should match file name `MyComponent`.'
+            'Component name `MComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        <script>
+          export default {
+            name: 'MyComponent',
+            template: '<div />'
+          }
+        </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'MyComponent.vue',
+      code: `
+        <script>
+          export default {
+            name: "MComponent",
+            template: '<div />'
+          }
+        </script>
+      `,
+      options: [{ extensions: ['vue'] }],
+      parser: require.resolve('vue-eslint-parser'),
+      parserOptions,
+      errors: [
+        {
+          message:
+            'Component name `MComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        <script>
+          export default {
+            name: "MyComponent",
+            template: '<div />'
+          }
+        </script>
+      `
+            }
+          ]
         }
       ]
     },
@@ -640,7 +747,20 @@ ruleTester.run('match-component-file-name', rule, {
       errors: [
         {
           message:
-            'Component name `MComponent` should match file name `MyComponent`.'
+            'Component name `MComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        <script>
+          export default {
+            name: \`MyComponent\`,
+            template: '<div />'
+          }
+        </script>
+      `
+            }
+          ]
         }
       ]
     },
@@ -659,7 +779,46 @@ ruleTester.run('match-component-file-name', rule, {
       errors: [
         {
           message:
-            'Component name `MComponent` should match file name `MyComponent`.'
+            'Component name `MComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        new Vue({
+          name: 'MyComponent',
+          template: '<div />'
+        })
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'MyComponent.js',
+      code: `
+        new Vue({
+          name: "MComponent",
+          template: '<div />'
+        })
+      `,
+      options: [{ extensions: ['js'] }],
+      parserOptions,
+      errors: [
+        {
+          message:
+            'Component name `MComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        new Vue({
+          name: "MyComponent",
+          template: '<div />'
+        })
+      `
+            }
+          ]
         }
       ]
     },
@@ -676,7 +835,18 @@ ruleTester.run('match-component-file-name', rule, {
       errors: [
         {
           message:
-            'Component name `MComponent` should match file name `MyComponent`.'
+            'Component name `MComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        new Vue({
+          name: \`MyComponent\`,
+          template: '<div />'
+        })
+      `
+            }
+          ]
         }
       ]
     },
@@ -692,7 +862,43 @@ ruleTester.run('match-component-file-name', rule, {
       errors: [
         {
           message:
-            'Component name `MComponent` should match file name `MyComponent`.'
+            'Component name `MComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        Vue.mixin({
+          name: 'MyComponent',
+        })
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'MyComponent.js',
+      code: `
+        Vue.mixin({
+          name: "MComponent",
+        })
+      `,
+      options: [{ extensions: ['js'] }],
+      parserOptions,
+      errors: [
+        {
+          message:
+            'Component name `MComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        Vue.mixin({
+          name: "MyComponent",
+        })
+      `
+            }
+          ]
         }
       ]
     },
@@ -708,7 +914,17 @@ ruleTester.run('match-component-file-name', rule, {
       errors: [
         {
           message:
-            'Component name `MComponent` should match file name `MyComponent`.'
+            'Component name `MComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        Vue.mixin({
+          name: \`MyComponent\`,
+        })
+      `
+            }
+          ]
         }
       ]
     },
@@ -724,7 +940,43 @@ ruleTester.run('match-component-file-name', rule, {
       errors: [
         {
           message:
-            'Component name `MComponent` should match file name `MyComponent`.'
+            'Component name `MComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        Vue.component('MyComponent', {
+          template: '<div />'
+        })
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'MyComponent.js',
+      code: `
+        Vue.component("MComponent", {
+          template: '<div />'
+        })
+      `,
+      options: [{ extensions: ['js'] }],
+      parserOptions,
+      errors: [
+        {
+          message:
+            'Component name `MComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        Vue.component("MyComponent", {
+          template: '<div />'
+        })
+      `
+            }
+          ]
         }
       ]
     },
@@ -740,7 +992,17 @@ ruleTester.run('match-component-file-name', rule, {
       errors: [
         {
           message:
-            'Component name `MComponent` should match file name `MyComponent`.'
+            'Component name `MComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        Vue.component(\`MyComponent\`, {
+          template: '<div />'
+        })
+      `
+            }
+          ]
         }
       ]
     },
@@ -756,7 +1018,17 @@ ruleTester.run('match-component-file-name', rule, {
       errors: [
         {
           message:
-            'Component name `MComponent` should match file name `MyComponent`.'
+            'Component name `MComponent` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        app.component(\`MyComponent\`, {
+          template: '<div />'
+        })
+      `
+            }
+          ]
         }
       ]
     },
@@ -775,7 +1047,18 @@ ruleTester.run('match-component-file-name', rule, {
       errors: [
         {
           message:
-            'Component name `my-component` should match file name `MyComponent`.'
+            'Component name `my-component` should match file name `MyComponent`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        export default {
+          name: 'MyComponent',
+          render() { return <div /> }
+        }
+      `
+            }
+          ]
         }
       ]
     },
@@ -792,7 +1075,18 @@ ruleTester.run('match-component-file-name', rule, {
       errors: [
         {
           message:
-            'Component name `MyComponent` should match file name `my-component`.'
+            'Component name `MyComponent` should match file name `my-component`.',
+          suggestions: [
+            {
+              desc: 'Rename component to match file name.',
+              output: `
+        export default {
+          name: 'my-component',
+          render() { return <div /> }
+        }
+      `
+            }
+          ]
         }
       ]
     }
diff --git a/tests/lib/rules/valid-model-definition.js b/tests/lib/rules/valid-model-definition.js
new file mode 100644
index 00000000..d9d8263a
--- /dev/null
+++ b/tests/lib/rules/valid-model-definition.js
@@ -0,0 +1,151 @@
+/**
+ * @fileoverview Prevents invalid keys in model option.
+ * @author Alex Sokolov
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/valid-model-definition')
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parserOptions: {
+    ecmaVersion: 2018,
+    sourceType: 'module'
+  }
+})
+ruleTester.run('valid-model-definition', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          model: {
+            prop: 'list'
+          }
+        }
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          model: {
+            event: 'update'
+          }
+        }
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          model: {
+            prop: 'list',
+            event: 'update'
+          }
+        }
+      `
+    }
+  ],
+
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          model: {
+            props: 'list'
+          }
+        }
+      `,
+      errors: ["Invalid key 'props' in model option."]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          model: {
+            events: 'update'
+          }
+        }
+      `,
+      errors: ["Invalid key 'events' in model option."]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          model: {
+            props: 'list',
+            event: 'update'
+          }
+        }
+      `,
+      errors: ["Invalid key 'props' in model option."]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          model: {
+            prop: 'list',
+            events: 'update'
+          }
+        }
+      `,
+      errors: ["Invalid key 'events' in model option."]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          model: {
+            props: 'list',
+            events: 'update'
+          }
+        }
+      `,
+      errors: [
+        "Invalid key 'props' in model option.",
+        "Invalid key 'events' in model option."
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          model: {
+            prop: 'checked',
+            props: 'list',
+            event: 'update'
+          }
+        }
+      `,
+      errors: ["Invalid key 'props' in model option."]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        export default {
+          model: {
+            name: 'checked',
+            props: 'list',
+            event: 'update'
+          }
+        }
+      `,
+      errors: [
+        "Invalid key 'name' in model option.",
+        "Invalid key 'props' in model option."
+      ]
+    }
+  ]
+})