Repository: vuejs/eslint-plugin-vue
Pull request #2255 (2023-07-28 12:50:17)

Only allow modelValue in `vue/no-deprecated-model-definition`
Follow-up to #2238.

I now tested the (unreleased) rule in a large codebase that is currently prepared for upgrading to Vue 3. There, I noticed that allowing anything different than `modelValue`/`update:modelValue` or `model-value`/`update:model-value` does not make sense: You'd still need to change `v-model` to `v-model:fooBar` in the actual Vue 3 migration if your model specified `fooBar`/`update:fooBar`.

So it's better to either rename the model definition to `modelValue`/`update:modelValue` or `model-value`/`update:model-value` and keep using `v-model`, or to rename the model definition to `fooBar`/`update:fooBar` and also update all `v-model`s to `:fooBar="…" @update:fooBar="… = $event"`, and after the Vue 3 migration change it to `v-model:fooBar` with #2237.

Therefore, I changed the rule and the suggestions to only allow `modelValue`/`update:modelValue` or `model-value`/`update:model-value`.


Patch:
diff --git a/docs/rules/block-order.md b/docs/rules/block-order.md
new file mode 100644
index 00000000..fe32e99e
--- /dev/null
+++ b/docs/rules/block-order.md
@@ -0,0 +1,194 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/block-order
+description: enforce order of component top-level elements
+---
+# vue/block-order
+
+> enforce order of component top-level elements
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- :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
+
+This rule warns about the order of the top-level tags, such as `<script>`, `<template>` & `<style>`.
+
+## :wrench: Options
+
+```json
+{
+  "vue/block-order": ["error", {
+    "order": [ [ "script", "template" ], "style" ]
+  }]
+}
+```
+
+- `order` (`(string|string[])[]`) ... The order of top-level element names. default `[ [ "script", "template" ], "style" ]`. May also be CSS selectors, such as `script[setup]` and `i18n:not([locale=en])`.
+
+### `{ "order": [ [ "script", "template" ], "style" ] }` (default)
+
+<eslint-code-block fix :rules="{'vue/block-order': ['error']}">
+
+```vue
+<!-- ✓ GOOD -->
+<script>/* ... */</script>
+<template>...</template>
+<style>/* ... */</style>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/block-order': ['error']}">
+
+```vue
+<!-- ✓ GOOD -->
+<template>...</template>
+<script>/* ... */</script>
+<style>/* ... */</style>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/block-order': ['error']}">
+
+```vue
+<!-- ✗ BAD -->
+<style>/* ... */</style>
+<script>/* ... */</script>
+<template>...</template>
+```
+
+</eslint-code-block>
+
+### `{ "order": ["template", "script", "style"] }`
+
+<eslint-code-block fix :rules="{'vue/block-order': ['error', { 'order': ['template', 'script', 'style'] }]}">
+
+```vue
+<!-- ✓ GOOD -->
+<template>...</template>
+<script>/* ... */</script>
+<style>/* ... */</style>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/block-order': ['error', { 'order': ['template', 'script', 'style'] }]}">
+
+```vue
+<!-- ✗ BAD -->
+<script>/* ... */</script>
+<template>...</template>
+<style>/* ... */</style>
+```
+
+</eslint-code-block>
+
+### `{ "order": ["docs", "template", "script", "style"] }`
+
+<eslint-code-block fix :rules="{'vue/block-order': ['error', { 'order': ['docs', 'template', 'script', 'style'] }]}">
+
+```vue
+<!-- ✓ GOOD -->
+<docs> documentation </docs>
+<template>...</template>
+<script>/* ... */</script>
+<style>/* ... */</style>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/block-order': ['error', { 'order': ['docs', 'template', 'script', 'style'] }]}">
+
+```vue
+<!-- ✗ BAD -->
+<template>...</template>
+<script>/* ... */</script>
+<docs> documentation </docs>
+<style>/* ... */</style>
+```
+
+</eslint-code-block>
+
+### `{ 'order': ['template', 'script:not([setup])', 'script[setup]'] }`
+
+<eslint-code-block fix :rules="{'vue/block-order': ['error', { 'order': ['template', 'script:not([setup])', 'script[setup]'] }]}">
+
+```vue
+<!-- ✓ GOOD -->
+<template>...</template>
+<script>/* ... */</script>
+<script setup>/* ... */</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/block-order': ['error', { 'order': ['template', 'script:not([setup])', 'script[setup]'] }]}">
+
+```vue
+<!-- ✗ BAD -->
+<template>...</template>
+<script setup>/* ... */</script>
+<script>/* ... */</script>
+```
+
+</eslint-code-block>
+
+### `{ 'order': ['template', 'style:not([scoped])', 'style[scoped]'] }`
+
+<eslint-code-block fix :rules="{'vue/block-order': ['error', { 'order': ['template', 'style:not([scoped])', 'style[scoped]'] }]}">
+
+```vue
+<!-- ✓ GOOD -->
+<template>...</template>
+<style>/* ... */</style>
+<style scoped>/* ... */</style>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/block-order': ['error', { 'order': ['template', 'style:not([scoped])', 'style[scoped]'] }]}">
+
+```vue
+<!-- ✗ BAD -->
+<template>...</template>
+<style scoped>/* ... */</style>
+<style>/* ... */</style>
+```
+
+</eslint-code-block>
+
+### `{ 'order': ['template', 'i18n:not([locale=en])', 'i18n[locale=en]'] }`
+
+<eslint-code-block fix :rules="{'vue/block-order': ['error', { 'order': ['template', 'i18n:not([locale=en])', 'i18n[locale=en]'] }]}">
+
+```vue
+<!-- ✓ GOOD -->
+<template>...</template>
+<i18n locale="ja">/* ... */</i18n>
+<i18n locale="en">/* ... */</i18n>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/block-order': ['error', { 'order': ['template', 'i18n:not([locale=en])', 'i18n[locale=en]'] }]}">
+
+```vue
+<!-- ✗ BAD -->
+<template>...</template>
+<i18n locale="en">/* ... */</i18n>
+<i18n locale="ja">/* ... */</i18n>
+```
+
+</eslint-code-block>
+
+## :books: Further Reading
+
+- [Style guide - Single-file component top-level element order](https://vuejs.org/style-guide/rules-recommended.html#single-file-component-top-level-element-order)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/block-order.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/block-order.js)
diff --git a/docs/rules/component-tags-order.md b/docs/rules/component-tags-order.md
index 4fc8d245..22b161aa 100644
--- a/docs/rules/component-tags-order.md
+++ b/docs/rules/component-tags-order.md
@@ -9,6 +9,7 @@ since: v6.1.0
 
 > enforce order of component top-level elements
 
+- :no_entry_sign: This rule was **deprecated** and replaced by [vue/block-order](block-order.md) rule.
 - :gear: This rule is included in `"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.
 
diff --git a/docs/rules/index.md b/docs/rules/index.md
index bdf3a2d1..6b06a7c7 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -178,7 +178,7 @@ Rules in this category are enabled for all presets provided by eslint-plugin-vue
 | Rule ID | Description |    |    |
 |:--------|:------------|:--:|:--:|
 | [vue/attributes-order](./attributes-order.md) | enforce order of attributes | :wrench: | :three::two::hammer: |
-| [vue/component-tags-order](./component-tags-order.md) | enforce order of component top-level elements | :wrench: | :three::two::hammer: |
+| [vue/component-tags-order](./component-tags-order.md) | enforce order of component top-level elements | :wrench::no_entry_sign: | :three::two::hammer: |
 | [vue/no-lone-template](./no-lone-template.md) | disallow unnecessary `<template>` |  | :three::two::warning: |
 | [vue/no-multiple-slot-args](./no-multiple-slot-args.md) | disallow to pass multiple arguments to scoped slots |  | :three::two::warning: |
 | [vue/no-v-html](./no-v-html.md) | disallow use of v-html to prevent XSS attack |  | :three::two::hammer: |
@@ -207,6 +207,7 @@ For example:
 | Rule ID | Description |    |    |
 |:--------|:------------|:--:|:--:|
 | [vue/block-lang](./block-lang.md) | disallow use other than available `lang` |  | :hammer: |
+| [vue/block-order](./block-order.md) | enforce order of component top-level elements | :wrench: | :hammer: |
 | [vue/block-tag-newline](./block-tag-newline.md) | enforce line breaks after opening and before closing block-level tags | :wrench: | :lipstick: |
 | [vue/component-api-style](./component-api-style.md) | enforce component API style |  | :hammer: |
 | [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: | :hammer: |
@@ -336,6 +337,7 @@ The following rules extend the rules provided by ESLint itself and apply them to
 
 | Rule ID | Replaced by |
 |:--------|:------------|
+| [vue/component-tags-order](./component-tags-order.md) | [vue/block-order](./block-order.md) |
 | [vue/no-invalid-model-keys](./no-invalid-model-keys.md) | [vue/valid-model-definition](./valid-model-definition.md) |
 | [vue/script-setup-uses-vars](./script-setup-uses-vars.md) | (no replacement) |
 | [vue/v-on-function-call](./v-on-function-call.md) | [vue/v-on-handler-style](./v-on-handler-style.md) |
diff --git a/docs/rules/no-deprecated-model-definition.md b/docs/rules/no-deprecated-model-definition.md
index bb2777d6..b213c7c4 100644
--- a/docs/rules/no-deprecated-model-definition.md
+++ b/docs/rules/no-deprecated-model-definition.md
@@ -44,7 +44,7 @@ export default defineComponent({
 
 ### `"allowVue3Compat": true`
 
-Allow `model` definitions with prop/event names that match the Vue.js 3.0.0+ `v-model` syntax, e.g. `fooBar`/`update:fooBar`.
+Allow `model` definitions with prop/event names that match the Vue.js 3.0.0+ `v-model` syntax, i.e. `modelValue`/`update:modelValue` or `model-value`/`update:model-value`.
 
 <eslint-code-block :rules="{'vue/no-deprecated-model-definition': ['error', { allowVue3Compat: true }]}">
 
@@ -52,8 +52,8 @@ Allow `model` definitions with prop/event names that match the Vue.js 3.0.0+ `v-
 <script>
 export default defineComponent({
   model: {
-    prop: 'fooBar',
-    event: 'update:fooBar'
+    prop: 'modelValue',
+    event: 'update:modelValue'
   }
 })
 </script>
diff --git a/lib/index.js b/lib/index.js
index d3bdc3a8..ace37ce5 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -15,6 +15,7 @@ module.exports = {
     'attribute-hyphenation': require('./rules/attribute-hyphenation'),
     'attributes-order': require('./rules/attributes-order'),
     'block-lang': require('./rules/block-lang'),
+    'block-order': require('./rules/block-order'),
     'block-spacing': require('./rules/block-spacing'),
     'block-tag-newline': require('./rules/block-tag-newline'),
     'brace-style': require('./rules/brace-style'),
diff --git a/lib/rules/block-order.js b/lib/rules/block-order.js
new file mode 100644
index 00000000..91d3eaa6
--- /dev/null
+++ b/lib/rules/block-order.js
@@ -0,0 +1,184 @@
+/**
+ * @author Yosuke Ota
+ * issue https://github.com/vuejs/eslint-plugin-vue/issues/140
+ */
+'use strict'
+
+const utils = require('../utils')
+const { parseSelector } = require('../utils/selector')
+
+/**
+ * @typedef {import('../utils/selector').VElementSelector} VElementSelector
+ */
+
+const DEFAULT_ORDER = Object.freeze([['script', 'template'], 'style'])
+
+/**
+ * @param {VElement} element
+ * @return {string}
+ */
+function getAttributeString(element) {
+  return element.startTag.attributes
+    .map((attribute) => {
+      if (attribute.value && attribute.value.type !== 'VLiteral') {
+        return ''
+      }
+
+      return `${attribute.key.name}${
+        attribute.value && attribute.value.value
+          ? `=${attribute.value.value}`
+          : ''
+      }`
+    })
+    .join(' ')
+}
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'enforce order of component top-level elements',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/block-order.html'
+    },
+    fixable: 'code',
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          order: {
+            type: 'array',
+            items: {
+              anyOf: [
+                { type: 'string' },
+                { type: 'array', items: { type: 'string' }, uniqueItems: true }
+              ]
+            },
+            uniqueItems: true,
+            additionalItems: false
+          }
+        },
+        additionalProperties: false
+      }
+    ],
+    messages: {
+      unexpected:
+        "'<{{elementName}}{{elementAttributes}}>' should be above '<{{firstUnorderedName}}{{firstUnorderedAttributes}}>' on line {{line}}."
+    }
+  },
+  /**
+   * @param {RuleContext} context - The rule context.
+   * @returns {RuleListener} AST event handlers.
+   */
+  create(context) {
+    /**
+     * @typedef {object} OrderElement
+     * @property {string} selectorText
+     * @property {VElementSelector} selector
+     * @property {number} index
+     */
+    /** @type {OrderElement[]} */
+    const orders = []
+    /** @type {(string|string[])[]} */
+    const orderOptions =
+      (context.options[0] && context.options[0].order) || DEFAULT_ORDER
+    for (const [index, selectorOrSelectors] of orderOptions.entries()) {
+      if (Array.isArray(selectorOrSelectors)) {
+        for (const selector of selectorOrSelectors) {
+          orders.push({
+            selectorText: selector,
+            selector: parseSelector(selector, context),
+            index
+          })
+        }
+      } else {
+        orders.push({
+          selectorText: selectorOrSelectors,
+          selector: parseSelector(selectorOrSelectors, context),
+          index
+        })
+      }
+    }
+
+    /**
+     * @param {VElement} element
+     */
+    function getOrderElement(element) {
+      return orders.find((o) => o.selector.test(element))
+    }
+    const documentFragment =
+      context.parserServices.getDocumentFragment &&
+      context.parserServices.getDocumentFragment()
+
+    function getTopLevelHTMLElements() {
+      if (documentFragment) {
+        return documentFragment.children.filter(utils.isVElement)
+      }
+      return []
+    }
+
+    return {
+      Program(node) {
+        if (utils.hasInvalidEOF(node)) {
+          return
+        }
+        const elements = getTopLevelHTMLElements()
+
+        const elementsWithOrder = elements.flatMap((element) => {
+          const order = getOrderElement(element)
+          return order ? [{ order, element }] : []
+        })
+        const sourceCode = context.getSourceCode()
+        for (const [index, elementWithOrders] of elementsWithOrder.entries()) {
+          const { order: expected, element } = elementWithOrders
+          const firstUnordered = elementsWithOrder
+            .slice(0, index)
+            .filter(({ order }) => expected.index < order.index)
+            .sort((e1, e2) => e1.order.index - e2.order.index)[0]
+          if (firstUnordered) {
+            const firstUnorderedAttributes = getAttributeString(
+              firstUnordered.element
+            )
+            const elementAttributes = getAttributeString(element)
+
+            context.report({
+              node: element,
+              loc: element.loc,
+              messageId: 'unexpected',
+              data: {
+                elementName: element.name,
+                elementAttributes: elementAttributes
+                  ? ` ${elementAttributes}`
+                  : '',
+                firstUnorderedName: firstUnordered.element.name,
+                firstUnorderedAttributes: firstUnorderedAttributes
+                  ? ` ${firstUnorderedAttributes}`
+                  : '',
+                line: firstUnordered.element.loc.start.line
+              },
+              *fix(fixer) {
+                // insert element before firstUnordered
+                const fixedElements = elements.flatMap((it) => {
+                  if (it === firstUnordered.element) {
+                    return [element, it]
+                  } else if (it === element) {
+                    return []
+                  }
+                  return [it]
+                })
+                for (let i = elements.length - 1; i >= 0; i--) {
+                  if (elements[i] !== fixedElements[i]) {
+                    yield fixer.replaceTextRange(
+                      elements[i].range,
+                      sourceCode.text.slice(...fixedElements[i].range)
+                    )
+                  }
+                }
+              }
+            })
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/lib/rules/component-tags-order.js b/lib/rules/component-tags-order.js
index 23fbc031..a2a5cd65 100644
--- a/lib/rules/component-tags-order.js
+++ b/lib/rules/component-tags-order.js
@@ -1,184 +1,23 @@
-/**
- * @author Yosuke Ota
- * issue https://github.com/vuejs/eslint-plugin-vue/issues/140
- */
 'use strict'
 
-const utils = require('../utils')
-const { parseSelector } = require('../utils/selector')
-
-/**
- * @typedef {import('../utils/selector').VElementSelector} VElementSelector
- */
-
-const DEFAULT_ORDER = Object.freeze([['script', 'template'], 'style'])
-
-/**
- * @param {VElement} element
- * @return {string}
- */
-function getAttributeString(element) {
-  return element.startTag.attributes
-    .map((attribute) => {
-      if (attribute.value && attribute.value.type !== 'VLiteral') {
-        return ''
-      }
-
-      return `${attribute.key.name}${
-        attribute.value && attribute.value.value
-          ? `=${attribute.value.value}`
-          : ''
-      }`
-    })
-    .join(' ')
-}
+const baseRule = require('./block-order')
 
 module.exports = {
+  // eslint-disable-next-line eslint-plugin/require-meta-schema, eslint-plugin/prefer-message-ids -- inherit schema from base rule
   meta: {
-    type: 'suggestion',
+    ...baseRule.meta,
+    // eslint-disable-next-line eslint-plugin/meta-property-ordering
+    type: baseRule.meta.type,
     docs: {
-      description: 'enforce order of component top-level elements',
+      description: baseRule.meta.docs.description,
       categories: ['vue3-recommended', 'recommended'],
       url: 'https://eslint.vuejs.org/rules/component-tags-order.html'
     },
-    fixable: 'code',
-    schema: [
-      {
-        type: 'object',
-        properties: {
-          order: {
-            type: 'array',
-            items: {
-              anyOf: [
-                { type: 'string' },
-                { type: 'array', items: { type: 'string' }, uniqueItems: true }
-              ]
-            },
-            uniqueItems: true,
-            additionalItems: false
-          }
-        },
-        additionalProperties: false
-      }
-    ],
-    messages: {
-      unexpected:
-        "'<{{elementName}}{{elementAttributes}}>' should be above '<{{firstUnorderedName}}{{firstUnorderedAttributes}}>' on line {{line}}."
-    }
+    deprecated: true,
+    replacedBy: ['block-order']
   },
-  /**
-   * @param {RuleContext} context - The rule context.
-   * @returns {RuleListener} AST event handlers.
-   */
+  /** @param {RuleContext} context */
   create(context) {
-    /**
-     * @typedef {object} OrderElement
-     * @property {string} selectorText
-     * @property {VElementSelector} selector
-     * @property {number} index
-     */
-    /** @type {OrderElement[]} */
-    const orders = []
-    /** @type {(string|string[])[]} */
-    const orderOptions =
-      (context.options[0] && context.options[0].order) || DEFAULT_ORDER
-    for (const [index, selectorOrSelectors] of orderOptions.entries()) {
-      if (Array.isArray(selectorOrSelectors)) {
-        for (const selector of selectorOrSelectors) {
-          orders.push({
-            selectorText: selector,
-            selector: parseSelector(selector, context),
-            index
-          })
-        }
-      } else {
-        orders.push({
-          selectorText: selectorOrSelectors,
-          selector: parseSelector(selectorOrSelectors, context),
-          index
-        })
-      }
-    }
-
-    /**
-     * @param {VElement} element
-     */
-    function getOrderElement(element) {
-      return orders.find((o) => o.selector.test(element))
-    }
-    const documentFragment =
-      context.parserServices.getDocumentFragment &&
-      context.parserServices.getDocumentFragment()
-
-    function getTopLevelHTMLElements() {
-      if (documentFragment) {
-        return documentFragment.children.filter(utils.isVElement)
-      }
-      return []
-    }
-
-    return {
-      Program(node) {
-        if (utils.hasInvalidEOF(node)) {
-          return
-        }
-        const elements = getTopLevelHTMLElements()
-
-        const elementsWithOrder = elements.flatMap((element) => {
-          const order = getOrderElement(element)
-          return order ? [{ order, element }] : []
-        })
-        const sourceCode = context.getSourceCode()
-        for (const [index, elementWithOrders] of elementsWithOrder.entries()) {
-          const { order: expected, element } = elementWithOrders
-          const firstUnordered = elementsWithOrder
-            .slice(0, index)
-            .filter(({ order }) => expected.index < order.index)
-            .sort((e1, e2) => e1.order.index - e2.order.index)[0]
-          if (firstUnordered) {
-            const firstUnorderedAttributes = getAttributeString(
-              firstUnordered.element
-            )
-            const elementAttributes = getAttributeString(element)
-
-            context.report({
-              node: element,
-              loc: element.loc,
-              messageId: 'unexpected',
-              data: {
-                elementName: element.name,
-                elementAttributes: elementAttributes
-                  ? ` ${elementAttributes}`
-                  : '',
-                firstUnorderedName: firstUnordered.element.name,
-                firstUnorderedAttributes: firstUnorderedAttributes
-                  ? ` ${firstUnorderedAttributes}`
-                  : '',
-                line: firstUnordered.element.loc.start.line
-              },
-              *fix(fixer) {
-                // insert element before firstUnordered
-                const fixedElements = elements.flatMap((it) => {
-                  if (it === firstUnordered.element) {
-                    return [element, it]
-                  } else if (it === element) {
-                    return []
-                  }
-                  return [it]
-                })
-                for (let i = elements.length - 1; i >= 0; i--) {
-                  if (elements[i] !== fixedElements[i]) {
-                    yield fixer.replaceTextRange(
-                      elements[i].range,
-                      sourceCode.text.slice(...fixedElements[i].range)
-                    )
-                  }
-                }
-              }
-            })
-          }
-        }
-      }
-    }
+    return baseRule.create(context)
   }
 }
diff --git a/lib/rules/no-deprecated-model-definition.js b/lib/rules/no-deprecated-model-definition.js
index f1561c20..9540ffc0 100644
--- a/lib/rules/no-deprecated-model-definition.js
+++ b/lib/rules/no-deprecated-model-definition.js
@@ -6,16 +6,8 @@
 
 const utils = require('../utils')
 
-/**
- * @param {RuleContext} context
- * @param {ASTNode} node
- */
-function reportWithoutSuggestion(context, node) {
-  context.report({
-    node,
-    messageId: 'deprecatedModel'
-  })
-}
+const allowedPropNames = new Set(['modelValue', 'model-value'])
+const allowedEventNames = new Set(['update:modelValue', 'update:model-value'])
 
 /**
  * @param {ObjectExpression} node
@@ -39,6 +31,15 @@ function findPropertyValue(node, key) {
   return property.value
 }
 
+/**
+ * @param {RuleFixer} fixer
+ * @param {Literal} node
+ * @param {string} text
+ */
+function replaceLiteral(fixer, node, text) {
+  return fixer.replaceTextRange([node.range[0] + 1, node.range[1] - 1], text)
+}
+
 module.exports = {
   meta: {
     type: 'problem',
@@ -62,7 +63,10 @@ module.exports = {
     ],
     messages: {
       deprecatedModel: '`model` definition is deprecated.',
-      renameEvent: 'Rename event to `{{expectedEventName}}`.'
+      vue3Compat:
+        '`model` definition is deprecated. You may use the Vue 3-compatible `modelValue`/`update:modelValue` though.',
+      changeToModelValue: 'Change to `modelValue`/`update:modelValue`.',
+      changeToKebabModelValue: 'Change to `model-value`/`update:model-value`.'
     }
   },
   /** @param {RuleContext} context */
@@ -76,35 +80,50 @@ module.exports = {
       }
 
       if (!allowVue3Compat) {
-        reportWithoutSuggestion(context, modelProperty)
+        context.report({
+          node: modelProperty,
+          messageId: 'deprecatedModel'
+        })
         return
       }
 
       const propName = findPropertyValue(modelProperty.value, 'prop')
       const eventName = findPropertyValue(modelProperty.value, 'event')
 
-      if (!propName || !eventName) {
-        reportWithoutSuggestion(context, modelProperty)
-        return
-      }
-
-      const expectedEventName = `update:${propName.value}`
-      if (eventName.value !== expectedEventName) {
+      if (
+        !propName ||
+        !eventName ||
+        typeof propName.value !== 'string' ||
+        typeof eventName.value !== 'string' ||
+        !allowedPropNames.has(propName.value) ||
+        !allowedEventNames.has(eventName.value)
+      ) {
         context.report({
           node: modelProperty,
-          messageId: 'deprecatedModel',
-          suggest: [
-            {
-              messageId: 'renameEvent',
-              data: { expectedEventName },
-              fix(fixer) {
-                return fixer.replaceTextRange(
-                  [eventName.range[0] + 1, eventName.range[1] - 1],
-                  expectedEventName
-                )
-              }
-            }
-          ]
+          messageId: 'vue3Compat',
+          suggest:
+            propName && eventName
+              ? [
+                  {
+                    messageId: 'changeToModelValue',
+                    *fix(fixer) {
+                      const newPropName = 'modelValue'
+                      const newEventName = 'update:modelValue'
+                      yield replaceLiteral(fixer, propName, newPropName)
+                      yield replaceLiteral(fixer, eventName, newEventName)
+                    }
+                  },
+                  {
+                    messageId: 'changeToKebabModelValue',
+                    *fix(fixer) {
+                      const newPropName = 'model-value'
+                      const newEventName = 'update:model-value'
+                      yield replaceLiteral(fixer, propName, newPropName)
+                      yield replaceLiteral(fixer, eventName, newEventName)
+                    }
+                  }
+                ]
+              : []
         })
       }
     })
diff --git a/package.json b/package.json
index 2c1956df..450698e4 100644
--- a/package.json
+++ b/package.json
@@ -54,12 +54,12 @@
     "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0"
   },
   "dependencies": {
-    "@eslint-community/eslint-utils": "^4.3.0",
+    "@eslint-community/eslint-utils": "^4.4.0",
     "natural-compare": "^1.4.0",
-    "nth-check": "^2.0.1",
-    "postcss-selector-parser": "^6.0.9",
-    "semver": "^7.3.5",
-    "vue-eslint-parser": "^9.3.0",
+    "nth-check": "^2.1.1",
+    "postcss-selector-parser": "^6.0.13",
+    "semver": "^7.5.4",
+    "vue-eslint-parser": "^9.3.1",
     "xml-name-validator": "^4.0.0"
   },
   "devDependencies": {
diff --git a/tools/update-docs.js b/tools/update-docs.js
index 65b0eae5..aecee5fd 100644
--- a/tools/update-docs.js
+++ b/tools/update-docs.js
@@ -117,7 +117,8 @@ class DocFile {
       } else {
         notes.push(`- :no_entry_sign: This rule was **deprecated**.`)
       }
-    } else if (meta.docs.categories) {
+    }
+    if (meta.docs?.categories) {
       const presets = getPresetIds(meta.docs.categories).map(
         (categoryId) => `\`"plugin:vue/${categoryId}"\``
       )


Test patch:
diff --git a/tests/lib/rules/block-order.js b/tests/lib/rules/block-order.js
new file mode 100644
index 00000000..aefd11ce
--- /dev/null
+++ b/tests/lib/rules/block-order.js
@@ -0,0 +1,495 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const rule = require('../../../lib/rules/block-order')
+const RuleTester = require('eslint').RuleTester
+const assert = require('assert')
+const { ESLint } = require('../../eslint-compat')
+
+// Initialize linter.
+const eslint = new ESLint({
+  overrideConfig: {
+    parser: require.resolve('vue-eslint-parser'),
+    parserOptions: {
+      ecmaVersion: 2015
+    },
+    plugins: ['vue'],
+    rules: {
+      'vue/comment-directive': 'error',
+      'vue/block-order': 'error'
+    }
+  },
+  useEslintrc: false,
+  plugins: { vue: require('../../../lib/index') },
+  fix: true
+})
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser')
+})
+
+tester.run('block-order', rule, {
+  valid: [
+    // default
+    '<script></script><template></template><style></style>',
+    '<template></template><script></script><style></style>',
+    '<script> /*script*/ </script><template><div id="id">text <!--comment--> </div><br></template><style>.button{ color: red; }</style>',
+    '<docs></docs><script></script><template></template><style></style>',
+    '<script></script><docs></docs><template></template><style></style>',
+    '<docs></docs><template></template><script></script><style></style>',
+    '<template></template><script></script><docs></docs><style></style>',
+    '<script></script><template></template>',
+    '<template></template><script></script>',
+    '<script setup></script><script></script>',
+    '<docs></docs><template></template><script></script><script setup></script><style></style>',
+    `
+      <template>
+      </template>
+
+      <script>
+      </script>
+
+      <style>
+      </style>
+    `,
+    `
+      <script>
+      </script>
+
+      <template>
+      </template>
+
+      <style>
+      </style>
+    `,
+
+    // order
+    {
+      code: '<script></script><template></template><style></style>',
+      output: null,
+      options: [{ order: ['script', 'template', 'style'] }]
+    },
+    {
+      code: '<template></template><script></script><style></style>',
+      output: null,
+      options: [{ order: ['template', 'script', 'style'] }]
+    },
+    {
+      code: '<style></style><template></template><script></script>',
+      output: null,
+      options: [{ order: ['style', 'template', 'script'] }]
+    },
+    {
+      code: '<template></template><script></script><style></style>',
+      output: null,
+      options: [{ order: ['template', 'docs', 'script', 'style'] }]
+    },
+    {
+      code: '<template></template><docs></docs><script></script><style></style>',
+      output: null,
+      options: [{ order: ['template', 'script', 'style'] }]
+    },
+    {
+      code: '<docs><div id="id">text <!--comment--> </div><br></docs><script></script><template></template><style></style>',
+      output: null,
+      options: [{ order: ['docs', 'script', 'template', 'style'] }]
+    },
+    {
+      code: '<script setup></script><script></script><template></template><style></style>',
+      output: null,
+      options: [
+        { order: ['script[setup]', 'script:not([setup])', 'template', 'style'] }
+      ]
+    },
+    {
+      code: '<template></template><script setup></script><script></script><style></style>',
+      output: null,
+      options: [
+        {
+          order: [['script[setup]', 'script:not([setup])', 'template'], 'style']
+        }
+      ]
+    },
+    {
+      code: '<script></script><script setup></script><template></template><style></style>',
+      output: null,
+      options: [{ order: ['script', 'template', 'style'] }]
+    },
+    {
+      code: '<template></template><script></script><script setup></script><style></style>',
+      output: null,
+      options: [{ order: [['script', 'template'], 'style'] }]
+    },
+    {
+      code: '<script></script><script setup></script><template></template><style></style>',
+      output: null,
+      options: [
+        { order: ['script:not([setup])', 'script[setup]', 'template', 'style'] }
+      ]
+    },
+    {
+      code: '<template></template><script></script><script setup></script><style></style>',
+      output: null,
+      options: [
+        {
+          order: [['script:not([setup])', 'script[setup]', 'template'], 'style']
+        }
+      ]
+    },
+    {
+      code: '<template></template><script></script><script setup></script><style scoped></style><style></style><i18n locale="ja"></i18n><i18n locale="en"></i18n>',
+      output: null,
+      options: [
+        {
+          order: [
+            ['script:not([setup])', 'script[setup]', 'template'],
+            'style[scoped]',
+            'style:not([scoped])',
+            'i18n:not([locale=en])',
+            'i18n:not([locale=ja])'
+          ]
+        }
+      ]
+    },
+    {
+      code: '<template></template><script></script><script setup></script><style scoped></style><style></style><i18n locale="en"></i18n><i18n locale="ja"></i18n>',
+      output: null,
+      options: [
+        {
+          order: [
+            'template',
+            'script:not([setup])',
+            'script[setup]',
+            'style[scoped]',
+            'style:not([scoped])',
+            'i18n[locale=en]',
+            'i18n[locale=ja]'
+          ]
+        }
+      ]
+    },
+    {
+      code: '<template></template><docs></docs><script></script><style></style>',
+      output: null,
+      options: [{ order: [['docs', 'script', 'template'], 'style'] }]
+    },
+    {
+      code: '<i18n locale="en"></i18n><i18n locale="ja"></i18n>',
+      output: null,
+      options: [{ order: ['i18n[locale=en]', 'i18n[locale=ja]'] }]
+    },
+    {
+      code: '<style></style><style scoped></style>',
+      output: null,
+      options: [{ order: ['style:not([scoped])', 'style[scoped]'] }]
+    },
+
+    `<script></script><style></style>`,
+
+    // Invalid EOF
+    '<template><div a=">test</div></template><style></style>',
+    '<template><div><!--test</div></template><style></style>'
+  ],
+  invalid: [
+    {
+      code: '<style></style><template></template><script></script>',
+      output: '<template></template><style></style><script></script>',
+      errors: [
+        {
+          message: "'<template>' should be above '<style>' on line 1.",
+          line: 1,
+          column: 16
+        },
+        {
+          message: "'<script>' should be above '<style>' on line 1.",
+          line: 1,
+          column: 37
+        }
+      ]
+    },
+    {
+      code: '<template></template><script></script><style></style>',
+      output: '<script></script><template></template><style></style>',
+      options: [{ order: ['script', 'template', 'style'] }],
+      errors: [
+        {
+          message: "'<script>' should be above '<template>' on line 1.",
+          line: 1,
+          column: 22
+        }
+      ]
+    },
+    {
+      code: `
+        <template></template>
+
+        <style></style>
+
+        <script></script>`,
+      output:
+        '\n' +
+        '        <template></template>\n' +
+        '\n' +
+        '        <script></script>\n' +
+        '\n' +
+        '        <style></style>',
+      errors: [
+        {
+          message: "'<script>' should be above '<style>' on line 4.",
+          line: 6
+        }
+      ]
+    },
+    {
+      code: `
+        <template></template>
+        <script></script>
+        <style></style>
+      `,
+      output:
+        '\n' +
+        '        <script></script>\n' +
+        '        <template></template>\n' +
+        '        <style></style>\n' +
+        '      ',
+      options: [{ order: ['script', 'template', 'style'] }],
+      errors: [
+        {
+          message: "'<script>' should be above '<template>' on line 2.",
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+        <script></script>
+        <template></template>
+        <style></style>
+      `,
+      output:
+        '\n' +
+        '        <template></template>\n' +
+        '        <script></script>\n' +
+        '        <style></style>\n' +
+        '      ',
+      options: [{ order: ['template', 'script', 'style'] }],
+      errors: [
+        {
+          message: "'<template>' should be above '<script>' on line 2.",
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+        <template></template>
+        <docs></docs>
+        <script></script>
+        <style></style>
+      `,
+      output:
+        '\n' +
+        '        <docs></docs>\n' +
+        '        <template></template>\n' +
+        '        <script></script>\n' +
+        '        <style></style>\n' +
+        '      ',
+      options: [{ order: ['docs', 'template', 'script', 'style'] }],
+      errors: [
+        {
+          message: "'<docs>' should be above '<template>' on line 2.",
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+        <template></template>
+        <docs></docs>
+        <script></script>
+        <style></style>
+      `,
+      output:
+        '\n' +
+        '        <script></script>\n' +
+        '        <template></template>\n' +
+        '        <docs></docs>\n' +
+        '        <style></style>\n' +
+        '      ',
+      options: [{ order: ['script', 'template', 'style'] }],
+      errors: [
+        {
+          message: "'<script>' should be above '<template>' on line 2.",
+          line: 4
+        }
+      ]
+    },
+    {
+      code: `
+        <template></template>
+        <docs>
+        </docs>
+        <script></script>
+        <style></style>
+      `,
+      output:
+        '\n' +
+        '        <script></script>\n' +
+        '        <template></template>\n' +
+        '        <docs>\n' +
+        '        </docs>\n' +
+        '        <style></style>\n' +
+        '      ',
+      options: [{ order: ['script', 'template', 'style'] }],
+      errors: [
+        {
+          message: "'<script>' should be above '<template>' on line 2.",
+          line: 5
+        }
+      ]
+    },
+    {
+      code: `
+        <script></script>
+        <template></template>
+      `,
+      output:
+        '\n        <template></template>\n        <script></script>\n      ',
+      options: [{ order: ['template', 'script'] }],
+      errors: [
+        {
+          message: "'<template>' should be above '<script>' on line 2.",
+          line: 3
+        }
+      ]
+    },
+    {
+      code: `
+        <style></style>
+        <template></template>
+        <script></script>
+      `,
+      output:
+        '\n' +
+        '        <template></template>\n' +
+        '        <style></style>\n' +
+        '        <script></script>\n' +
+        '      ',
+      errors: [
+        {
+          message: "'<template>' should be above '<style>' on line 2.",
+          line: 3
+        },
+        {
+          message: "'<script>' should be above '<style>' on line 2.",
+          line: 4
+        }
+      ]
+    },
+    {
+      code: `
+        <style></style>
+        <docs></docs>
+        <template></template>
+        <script></script>
+      `,
+      output:
+        '\n' +
+        '        <template></template>\n' +
+        '        <style></style>\n' +
+        '        <docs></docs>\n' +
+        '        <script></script>\n' +
+        '      ',
+      errors: [
+        {
+          message: "'<template>' should be above '<style>' on line 2.",
+          line: 4
+        },
+        {
+          message: "'<script>' should be above '<style>' on line 2.",
+          line: 5
+        }
+      ]
+    },
+    // no <template>
+    {
+      code: `
+        <style></style>
+        <script></script>
+      `,
+      output: '\n        <script></script>\n        <style></style>\n      ',
+      errors: [
+        {
+          message: "'<script>' should be above '<style>' on line 2.",
+          line: 3
+        }
+      ]
+    },
+    {
+      code: '<i18n locale="ja"></i18n><i18n locale="en"></i18n>',
+      output: '<i18n locale="en"></i18n><i18n locale="ja"></i18n>',
+      options: [{ order: ['i18n[locale=en]', 'i18n[locale=ja]'] }],
+      errors: [
+        {
+          message:
+            "'<i18n locale=en>' should be above '<i18n locale=ja>' on line 1.",
+          line: 1
+        }
+      ]
+    },
+    {
+      code: '<style scoped></style><style></style>',
+      output: '<style></style><style scoped></style>',
+      options: [{ order: ['style:not([scoped])', 'style[scoped]'] }],
+      errors: [
+        {
+          message: "'<style>' should be above '<style scoped>' on line 1.",
+          line: 1
+        }
+      ]
+    },
+    {
+      code: '<style></style><style scoped></style>',
+      output: '<style scoped></style><style></style>',
+      options: [{ order: ['style[scoped]', 'style:not([scoped])'] }],
+      errors: [
+        {
+          message: "'<style scoped>' should be above '<style>' on line 1.",
+          line: 1
+        }
+      ]
+    },
+    {
+      code: '<style></style><script></script>',
+      output: '<script></script><style></style>',
+      options: [{ order: ['script:not([scoped])', 'style:not([scoped])'] }],
+      errors: [
+        {
+          message: "'<script>' should be above '<style>' on line 1.",
+          line: 1
+        }
+      ]
+    }
+  ]
+})
+
+describe('suppress reporting with eslint-disable-next-line', () => {
+  it('do not report if <!-- eslint-disable-next-line -->', async () => {
+    const code = `<style></style><template></template>
+    <!-- eslint-disable-next-line vue/block-order -->
+    <script></script>`
+    const [{ messages, output }] = await eslint.lintText(code, {
+      filePath: 'test.vue'
+    })
+    assert.strictEqual(messages.length, 0)
+    // should not fix <script>
+    assert.strictEqual(
+      output,
+      `<template></template><style></style>
+    <!-- eslint-disable-next-line vue/block-order -->
+    <script></script>`
+    )
+  })
+})
diff --git a/tests/lib/rules/no-deprecated-model-definition.js b/tests/lib/rules/no-deprecated-model-definition.js
index 71adb34f..908be654 100644
--- a/tests/lib/rules/no-deprecated-model-definition.js
+++ b/tests/lib/rules/no-deprecated-model-definition.js
@@ -31,8 +31,8 @@ tester.run('no-deprecated-model-definition', rule, {
         <script>
         export default {
           model: {
-            prop: 'fooBar',
-            event: 'update:fooBar'
+            prop: 'modelValue',
+            event: 'update:modelValue'
           }
         }
         </script>
@@ -45,8 +45,8 @@ tester.run('no-deprecated-model-definition', rule, {
         <script>
         export default defineComponent({
           model: {
-            prop: 'foo-bar',
-            event: 'update:foo-bar'
+            prop: 'model-value',
+            event: 'update:model-value'
           }
         })
         </script>
@@ -133,7 +133,8 @@ tester.run('no-deprecated-model-definition', rule, {
       options: [{ allowVue3Compat: true }],
       errors: [
         {
-          message: '`model` definition is deprecated.',
+          message:
+            '`model` definition is deprecated. You may use the Vue 3-compatible `modelValue`/`update:modelValue` though.',
           line: 4,
           column: 11,
           endLine: 6,
@@ -155,7 +156,8 @@ tester.run('no-deprecated-model-definition', rule, {
       options: [{ allowVue3Compat: true }],
       errors: [
         {
-          message: '`model` definition is deprecated.',
+          message:
+            '`model` definition is deprecated. You may use the Vue 3-compatible `modelValue`/`update:modelValue` though.',
           line: 4,
           column: 11,
           endLine: 6,
@@ -178,20 +180,138 @@ tester.run('no-deprecated-model-definition', rule, {
       options: [{ allowVue3Compat: true }],
       errors: [
         {
-          message: '`model` definition is deprecated.',
+          message:
+            '`model` definition is deprecated. You may use the Vue 3-compatible `modelValue`/`update:modelValue` though.',
           line: 4,
           column: 11,
           endLine: 7,
           endColumn: 12,
           suggestions: [
             {
-              desc: 'Rename event to `update:foo`.',
+              desc: 'Change to `modelValue`/`update:modelValue`.',
               output: `
         <script>
         export default defineComponent({
           model: {
-            prop: 'foo',
-            event: 'update:foo'
+            prop: 'modelValue',
+            event: 'update:modelValue'
+          }
+        })
+        </script>
+      `
+            },
+            {
+              desc: 'Change to `model-value`/`update:model-value`.',
+              output: `
+        <script>
+        export default defineComponent({
+          model: {
+            prop: 'model-value',
+            event: 'update:model-value'
+          }
+        })
+        </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default {
+          model: {
+            prop: "fooBar",
+            event: "update:fooBar"
+          }
+        }
+        </script>
+      `,
+      options: [{ allowVue3Compat: true }],
+      errors: [
+        {
+          message:
+            '`model` definition is deprecated. You may use the Vue 3-compatible `modelValue`/`update:modelValue` though.',
+          line: 4,
+          column: 11,
+          endLine: 7,
+          endColumn: 12,
+          suggestions: [
+            {
+              desc: 'Change to `modelValue`/`update:modelValue`.',
+              output: `
+        <script>
+        export default {
+          model: {
+            prop: "modelValue",
+            event: "update:modelValue"
+          }
+        }
+        </script>
+      `
+            },
+            {
+              desc: 'Change to `model-value`/`update:model-value`.',
+              output: `
+        <script>
+        export default {
+          model: {
+            prop: "model-value",
+            event: "update:model-value"
+          }
+        }
+        </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script>
+        export default defineComponent({
+          model: {
+            prop: 'foo-bar',
+            event: 'update:foo-bar'
+          }
+        })
+        </script>
+      `,
+      options: [{ allowVue3Compat: true }],
+      errors: [
+        {
+          message:
+            '`model` definition is deprecated. You may use the Vue 3-compatible `modelValue`/`update:modelValue` though.',
+          line: 4,
+          column: 11,
+          endLine: 7,
+          endColumn: 12,
+          suggestions: [
+            {
+              desc: 'Change to `modelValue`/`update:modelValue`.',
+              output: `
+        <script>
+        export default defineComponent({
+          model: {
+            prop: 'modelValue',
+            event: 'update:modelValue'
+          }
+        })
+        </script>
+      `
+            },
+            {
+              desc: 'Change to `model-value`/`update:model-value`.',
+              output: `
+        <script>
+        export default defineComponent({
+          model: {
+            prop: 'model-value',
+            event: 'update:model-value'
           }
         })
         </script>