Repository: vuejs/eslint-plugin-vue
Pull request #1509 (2021-06-08 03:08:05)

Add support for `is="vue:"` (Vue 3.1)


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index d38c44a2..914242d1 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -298,6 +298,7 @@ For example:
 | [vue/next-tick-style](./next-tick-style.md) | enforce Promise or callback style in `nextTick` | :wrench: |
 | [vue/no-bare-strings-in-template](./no-bare-strings-in-template.md) | disallow the use of bare strings in `<template>` |  |
 | [vue/no-boolean-default](./no-boolean-default.md) | disallow boolean defaults | :wrench: |
+| [vue/no-deprecated-v-is](./no-deprecated-v-is.md) | disallow deprecated `v-is` directive (in Vue.js 3.1.0+) | :wrench: |
 | [vue/no-duplicate-attr-inheritance](./no-duplicate-attr-inheritance.md) | enforce `inheritAttrs` to be set to `false` when using `v-bind="$attrs"` |  |
 | [vue/no-empty-component-block](./no-empty-component-block.md) | disallow the `<template>` `<script>` `<style>` block to be empty |  |
 | [vue/no-invalid-model-keys](./no-invalid-model-keys.md) | require valid keys in model option |  |
@@ -313,6 +314,7 @@ For example:
 | [vue/no-restricted-v-bind](./no-restricted-v-bind.md) | disallow specific argument in `v-bind` |  |
 | [vue/no-static-inline-styles](./no-static-inline-styles.md) | disallow static inline `style` attributes |  |
 | [vue/no-template-target-blank](./no-template-target-blank.md) | disallow target="_blank" attribute without rel="noopener noreferrer" |  |
+| [vue/no-this-in-before-route-enter](./no-this-in-before-route-enter.md) | disallow this usage in a beforeRouteEnter method |  |
 | [vue/no-unregistered-components](./no-unregistered-components.md) | disallow using components that are not registered inside templates |  |
 | [vue/no-unsupported-features](./no-unsupported-features.md) | disallow unsupported Vue.js syntax on the specified version | :wrench: |
 | [vue/no-unused-properties](./no-unused-properties.md) | disallow unused properties |  |
diff --git a/docs/rules/no-deprecated-v-is.md b/docs/rules/no-deprecated-v-is.md
new file mode 100644
index 00000000..5b16a8c8
--- /dev/null
+++ b/docs/rules/no-deprecated-v-is.md
@@ -0,0 +1,42 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-deprecated-v-is
+description: disallow deprecated `v-is` directive (in Vue.js 3.1.0+)
+---
+# vue/no-deprecated-v-is
+
+> disallow deprecated `v-is` directive (in Vue.js 3.1.0+)
+
+- :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 reports deprecated `v-is` directive in Vue.js v3.1.0+.
+
+Use [`is` attribute with `vue:` prefix](https://v3.vuejs.org/api/special-attributes.html#is) instead.
+
+<eslint-code-block fix :rules="{'vue/no-deprecated-v-is': ['error']}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <div is="vue:MyComponent" />
+  <component is="MyComponent" />
+  
+  <!-- ✗ BAD -->
+  <div v-is="'MyComponent'" />
+</template>
+```
+
+</eslint-code-block>
+
+## :books: Further Reading
+
+- [v-is](https://v3.vuejs.org/api/directives.html#v-is)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-v-is.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-deprecated-v-is.js)
diff --git a/docs/rules/no-this-in-before-route-enter.md b/docs/rules/no-this-in-before-route-enter.md
new file mode 100644
index 00000000..7409b48e
--- /dev/null
+++ b/docs/rules/no-this-in-before-route-enter.md
@@ -0,0 +1,115 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-this-in-before-route-enter
+description: disallow this usage in a beforeRouteEnter method
+---
+# vue/no-this-in-before-route-enter
+
+> disallow this usage in a beforeRouteEnter method
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+
+## Rule Details
+
+Because lack of `this` in the `beforeRouteEnter` [(docs)](https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards). This behavior isn't obvious, so it's pretty easy to make a `TypeError`. Especially during some refactor. 
+
+Bad:
+
+<eslint-code-block :rules="{'vue/no-this-in-before-route-enter': ['error']}">
+
+```vue
+<script>
+export default {
+  beforeRouteEnter() {
+    this.method(); // Uncaught TypeError: Cannot read property 'method' of undefined      
+  }   
+}
+</script>
+```
+
+</eslint-code-block>
+
+Bad:
+
+<eslint-code-block :rules="{'vue/no-this-in-before-route-enter': ['error']}">
+
+```vue
+<script>
+export default {
+  beforeRouteEnter() {
+    this.attribute = 42;
+  }   
+}
+</script>
+```
+
+</eslint-code-block>
+
+Bad:
+
+<eslint-code-block :rules="{'vue/no-this-in-before-route-enter': ['error']}">
+
+```vue
+<script>
+export default {
+  beforeRouteEnter() {
+    if (this.value === 42) {
+        
+    }
+  }   
+}
+</script>
+```
+
+</eslint-code-block>
+
+
+Bad:
+
+<eslint-code-block :rules="{'vue/no-this-in-before-route-enter': ['error']}">
+
+```vue
+<script>
+export default {
+  beforeRouteEnter() {
+    this.attribute = this.method();
+  }   
+}
+</script>
+```
+
+</eslint-code-block>
+
+Good:
+
+<eslint-code-block :rules="{'vue/no-this-in-before-route-enter': ['error']}">
+
+```vue
+<script>
+export default {
+  beforeRouteEnter() {
+    // anything without this
+  }   
+}
+</script>
+```
+
+</eslint-code-block>
+
+### Options
+
+Nothing.
+
+## When Not To Use It
+
+When [vue-router](https://router.vuejs.org/) is not installed.
+
+## Further Reading
+
+[vue-router - in-component-guards](https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-this-in-before-route-enter.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-this-in-before-route-enter.js)
diff --git a/docs/rules/no-unsupported-features.md b/docs/rules/no-unsupported-features.md
index dfb73e85..8665e9b8 100644
--- a/docs/rules/no-unsupported-features.md
+++ b/docs/rules/no-unsupported-features.md
@@ -29,6 +29,8 @@ This rule reports unsupported Vue.js syntax on the specified version.
 - `version` ... The `version` option accepts [the valid version range of `node-semver`](https://github.com/npm/node-semver#range-grammar). Set the version of Vue.js you are using. This option is required.
 - `ignores` ... You can use this `ignores` option to ignore the given features.
 The `"ignores"` option accepts an array of the following strings.
+  - Vue.js 3.1.0+
+    - `"is-attribute-with-vue-prefix"` ... [`is` attribute with `vue:` prefix](https://v3.vuejs.org/api/special-attributes.html#is)
   - Vue.js 3.0.0+
     - `"v-model-argument"` ... [argument on `v-model`][Vue RFCs - 0005-replace-v-bind-sync-with-v-model-argument]
     - `"v-model-custom-modifiers"` ... [custom modifiers on `v-model`][Vue RFCs - 0011-v-model-api-change]
diff --git a/lib/index.js b/lib/index.js
index 1379e284..313cc693 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -71,6 +71,7 @@ module.exports = {
     'no-deprecated-slot-attribute': require('./rules/no-deprecated-slot-attribute'),
     'no-deprecated-slot-scope-attribute': require('./rules/no-deprecated-slot-scope-attribute'),
     'no-deprecated-v-bind-sync': require('./rules/no-deprecated-v-bind-sync'),
+    'no-deprecated-v-is': require('./rules/no-deprecated-v-is'),
     'no-deprecated-v-on-native-modifier': require('./rules/no-deprecated-v-on-native-modifier'),
     'no-deprecated-v-on-number-modifiers': require('./rules/no-deprecated-v-on-number-modifiers'),
     'no-deprecated-vue-config-keycodes': require('./rules/no-deprecated-vue-config-keycodes'),
@@ -113,6 +114,7 @@ module.exports = {
     'no-template-shadow': require('./rules/no-template-shadow'),
     'no-template-target-blank': require('./rules/no-template-target-blank'),
     'no-textarea-mustache': require('./rules/no-textarea-mustache'),
+    'no-this-in-before-route-enter': require('./rules/no-this-in-before-route-enter'),
     'no-unregistered-components': require('./rules/no-unregistered-components'),
     'no-unsupported-features': require('./rules/no-unsupported-features'),
     'no-unused-components': require('./rules/no-unused-components'),
diff --git a/lib/rules/no-deprecated-destroyed-lifecycle.js b/lib/rules/no-deprecated-destroyed-lifecycle.js
index 6176daa9..9cb690cb 100644
--- a/lib/rules/no-deprecated-destroyed-lifecycle.js
+++ b/lib/rules/no-deprecated-destroyed-lifecycle.js
@@ -23,15 +23,13 @@ module.exports = {
       categories: ['vue3-essential'],
       url: 'https://eslint.vuejs.org/rules/no-deprecated-destroyed-lifecycle.html'
     },
-    fixable: null,
+    fixable: 'code',
     schema: [],
     messages: {
       deprecatedDestroyed:
         'The `destroyed` lifecycle hook is deprecated. Use `unmounted` instead.',
       deprecatedBeforeDestroy:
-        'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
-      insteadUnmounted: 'Instead, change to `unmounted`.',
-      insteadBeforeUnmount: 'Instead, change to `beforeUnmount`.'
+        'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.'
     }
   },
   /** @param {RuleContext} context */
@@ -43,15 +41,9 @@ module.exports = {
         context.report({
           node: destroyed.key,
           messageId: 'deprecatedDestroyed',
-          // I don't know if they have exactly the same function, so don't do autofix.
-          suggest: [
-            {
-              messageId: 'insteadUnmounted',
-              fix(fixer) {
-                return fix(fixer, destroyed, 'unmounted')
-              }
-            }
-          ]
+          fix(fixer) {
+            return fix(fixer, destroyed, 'unmounted')
+          }
         })
       }
 
@@ -60,15 +52,9 @@ module.exports = {
         context.report({
           node: beforeDestroy.key,
           messageId: 'deprecatedBeforeDestroy',
-          // I don't know if they have exactly the same function, so don't do autofix.
-          suggest: [
-            {
-              messageId: 'insteadBeforeUnmount',
-              fix(fixer) {
-                return fix(fixer, beforeDestroy, 'beforeUnmount')
-              }
-            }
-          ]
+          fix(fixer) {
+            return fix(fixer, beforeDestroy, 'beforeUnmount')
+          }
         })
       }
 
diff --git a/lib/rules/no-deprecated-html-element-is.js b/lib/rules/no-deprecated-html-element-is.js
index e07f493a..8382bb5c 100644
--- a/lib/rules/no-deprecated-html-element-is.js
+++ b/lib/rules/no-deprecated-html-element-is.js
@@ -31,16 +31,34 @@ module.exports = {
   },
   /** @param {RuleContext} context */
   create(context) {
+    /** @param {VElement} node */
+    function isValidElement(node) {
+      return (
+        !utils.isHtmlWellKnownElementName(node.rawName) &&
+        !utils.isSvgWellKnownElementName(node.rawName)
+      )
+    }
     return utils.defineTemplateBodyVisitor(context, {
-      /** @param {VDirective | VAttribute} node */
-      "VAttribute[directive=true][key.name.name='bind'][key.argument.name='is'], VAttribute[directive=false][key.name='is']"(
+      /** @param {VDirective} node */
+      "VAttribute[directive=true][key.name.name='bind'][key.argument.name='is']"(
         node
       ) {
-        const element = node.parent.parent
-        if (
-          !utils.isHtmlWellKnownElementName(element.rawName) &&
-          !utils.isSvgWellKnownElementName(element.rawName)
-        ) {
+        if (isValidElement(node.parent.parent)) {
+          return
+        }
+        context.report({
+          node,
+          loc: node.loc,
+          messageId: 'unexpected'
+        })
+      },
+      /** @param {VAttribute} node */
+      "VAttribute[directive=false][key.name='is']"(node) {
+        if (isValidElement(node.parent.parent)) {
+          return
+        }
+        if (node.value && node.value.value.startsWith('vue:')) {
+          // Usage on native elements 3.1+
           return
         }
         context.report({
diff --git a/lib/rules/no-deprecated-v-is.js b/lib/rules/no-deprecated-v-is.js
new file mode 100644
index 00000000..cc4ad0a3
--- /dev/null
+++ b/lib/rules/no-deprecated-v-is.js
@@ -0,0 +1,31 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const vIs = require('./syntaxes/v-is')
+
+module.exports = {
+  meta: {
+    type: 'suggestion',
+    docs: {
+      description: 'disallow deprecated `v-is` directive (in Vue.js 3.1.0+)',
+      // TODO Switch to `vue3-essential` in the major version.
+      // categories: ['vue3-essential'],
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-deprecated-v-is.html'
+    },
+    fixable: 'code',
+    schema: [],
+    messages: {
+      forbiddenVIs: '`v-is` directive is deprecated.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const templateBodyVisitor = vIs.createTemplateBodyVisitor(context)
+    return utils.defineTemplateBodyVisitor(context, templateBodyVisitor)
+  }
+}
diff --git a/lib/rules/no-this-in-before-route-enter.js b/lib/rules/no-this-in-before-route-enter.js
new file mode 100644
index 00000000..968f45c1
--- /dev/null
+++ b/lib/rules/no-this-in-before-route-enter.js
@@ -0,0 +1,91 @@
+/**
+ * @fileoverview Don't use this in a beforeRouteEnter method
+ * @author Przemyslaw Jan Beigert
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'disallow this usage in a beforeRouteEnter method',
+      categories: null,
+      url: 'https://eslint.vuejs.org/rules/no-this-in-before-route-enter.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      disallow:
+        "'beforeRouteEnter' does NOT have access to `this` component instance. https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards."
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    /**
+     * @typedef {object} ScopeStack
+     * @property {ScopeStack | null} upper
+     * @property {FunctionExpression | FunctionDeclaration} node
+     * @property {boolean} beforeRouteEnter
+     */
+    /** @type {Set<FunctionExpression>} */
+    const beforeRouteEnterFunctions = new Set()
+    /** @type {ScopeStack | null} */
+    let scopeStack = null
+
+    /**
+     * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
+     */
+    function onFunctionEnter(node) {
+      if (node.type === 'ArrowFunctionExpression') {
+        return
+      }
+      scopeStack = {
+        upper: scopeStack,
+        node,
+        beforeRouteEnter: beforeRouteEnterFunctions.has(
+          /** @type {never} */ (node)
+        )
+      }
+    }
+
+    /**
+     * @param {FunctionExpression | FunctionDeclaration | ArrowFunctionExpression} node
+     */
+    function onFunctionExit(node) {
+      if (scopeStack && scopeStack.node === node) {
+        scopeStack = scopeStack.upper
+      }
+    }
+    return utils.defineVueVisitor(context, {
+      onVueObjectEnter(node) {
+        const beforeRouteEnter = utils.findProperty(node, 'beforeRouteEnter')
+        if (
+          beforeRouteEnter &&
+          beforeRouteEnter.value.type === 'FunctionExpression'
+        ) {
+          beforeRouteEnterFunctions.add(beforeRouteEnter.value)
+        }
+      },
+      ':function': onFunctionEnter,
+      ':function:exit': onFunctionExit,
+      ThisExpression(node) {
+        if (scopeStack && scopeStack.beforeRouteEnter) {
+          context.report({
+            node,
+            messageId: 'disallow'
+          })
+        }
+      }
+    })
+  }
+}
diff --git a/lib/rules/no-unregistered-components.js b/lib/rules/no-unregistered-components.js
index 6e6cb2ad..07d9e800 100644
--- a/lib/rules/no-unregistered-components.js
+++ b/lib/rules/no-unregistered-components.js
@@ -119,11 +119,14 @@ module.exports = {
         /** @param {VAttribute} node */
         "VAttribute[directive=false][key.name='is']"(node) {
           if (
-            !node.value || // `<component is />`
-            utils.isHtmlWellKnownElementName(node.value.value)
+            !node.value // `<component is />`
           )
             return
-          usedComponentNodes.push({ node, name: node.value.value })
+          const value = node.value.value.startsWith('vue:') // Usage on native elements 3.1+
+            ? node.value.value.slice(4)
+            : node.value.value
+          if (utils.isHtmlWellKnownElementName(value)) return
+          usedComponentNodes.push({ node, name: value })
         },
         /** @param {VElement} node */
         "VElement[name='template'][parent.type='VDocumentFragment']:exit"() {
diff --git a/lib/rules/no-unsupported-features.js b/lib/rules/no-unsupported-features.js
index 9eb00be0..2d4caf3e 100644
--- a/lib/rules/no-unsupported-features.js
+++ b/lib/rules/no-unsupported-features.js
@@ -25,7 +25,9 @@ const FEATURES = {
   // Vue.js 3.0.0+
   'v-model-argument': require('./syntaxes/v-model-argument'),
   'v-model-custom-modifiers': require('./syntaxes/v-model-custom-modifiers'),
-  'v-is': require('./syntaxes/v-is')
+  'v-is': require('./syntaxes/v-is'),
+  // Vue.js 3.1.0+
+  'is-attribute-with-vue-prefix': require('./syntaxes/is-attribute-with-vue-prefix')
 }
 
 const SYNTAX_NAMES = /** @type {(keyof FEATURES)[]} */ (Object.keys(FEATURES))
@@ -97,7 +99,9 @@ module.exports = {
         'Argument on `v-model` is not supported until Vue.js "3.0.0".',
       forbiddenVModelCustomModifiers:
         'Custom modifiers on `v-model` are not supported until Vue.js "3.0.0".',
-      forbiddenVIs: '`v-is` are not supported until Vue.js "3.0.0".'
+      forbiddenVIs: '`v-is` are not supported until Vue.js "3.0.0".',
+      forbiddenIsAttributeWithVuePrefix:
+        '`is="vue:"` are not supported until Vue.js "3.1.0".'
     }
   },
   /** @param {RuleContext} context */
diff --git a/lib/rules/no-unused-components.js b/lib/rules/no-unused-components.js
index e067e3eb..8497dd96 100644
--- a/lib/rules/no-unused-components.js
+++ b/lib/rules/no-unused-components.js
@@ -88,7 +88,10 @@ module.exports = {
           if (!node.value) {
             return
           }
-          usedComponents.add(node.value.value)
+          const value = node.value.value.startsWith('vue:') // Usage on native elements 3.1+
+            ? node.value.value.slice(4)
+            : node.value.value
+          usedComponents.add(value)
         },
         /** @param {VElement} node */
         "VElement[name='template']"(node) {
diff --git a/lib/rules/syntaxes/is-attribute-with-vue-prefix.js b/lib/rules/syntaxes/is-attribute-with-vue-prefix.js
new file mode 100644
index 00000000..9fd2afdd
--- /dev/null
+++ b/lib/rules/syntaxes/is-attribute-with-vue-prefix.js
@@ -0,0 +1,25 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+module.exports = {
+  supported: '>=3.1.0',
+  /** @param {RuleContext} context @returns {TemplateListener} */
+  createTemplateBodyVisitor(context) {
+    return {
+      /** @param {VAttribute} node */
+      "VAttribute[directive=false][key.name='is']"(node) {
+        if (!node.value) {
+          return
+        }
+        if (node.value.value.startsWith('vue:')) {
+          context.report({
+            node: node.value,
+            messageId: 'forbiddenIsAttributeWithVuePrefix'
+          })
+        }
+      }
+    }
+  }
+}
diff --git a/lib/rules/syntaxes/v-is.js b/lib/rules/syntaxes/v-is.js
index 9aeeb8d7..f5eed36d 100644
--- a/lib/rules/syntaxes/v-is.js
+++ b/lib/rules/syntaxes/v-is.js
@@ -4,6 +4,7 @@
  */
 'use strict'
 module.exports = {
+  deprecated: '3.1.0',
   supported: '>=3.0.0',
   /** @param {RuleContext} context @returns {TemplateListener} */
   createTemplateBodyVisitor(context) {


Test patch:
diff --git a/tests/lib/rules/no-deprecated-destroyed-lifecycle.js b/tests/lib/rules/no-deprecated-destroyed-lifecycle.js
index 00dda6ed..c8c6774d 100644
--- a/tests/lib/rules/no-deprecated-destroyed-lifecycle.js
+++ b/tests/lib/rules/no-deprecated-destroyed-lifecycle.js
@@ -111,42 +111,24 @@ ruleTester.run('no-deprecated-destroyed-lifecycle', rule, {
       }
       </script>
       `,
-      errors: [
-        {
-          message:
-            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
-          line: 4,
-          suggestions: [
-            {
-              desc: 'Instead, change to `beforeUnmount`.',
-              output: `
+      output: `
       <script>
       export default {
         beforeUnmount () {},
-        destroyed () {},
+        unmounted () {},
       }
       </script>
-      `
-            }
-          ]
+      `,
+      errors: [
+        {
+          message:
+            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
+          line: 4
         },
         {
           message:
             'The `destroyed` lifecycle hook is deprecated. Use `unmounted` instead.',
-          line: 5,
-          suggestions: [
-            {
-              desc: 'Instead, change to `unmounted`.',
-              output: `
-      <script>
-      export default {
-        beforeDestroy () {},
-        unmounted () {},
-      }
-      </script>
-      `
-            }
-          ]
+          line: 5
         }
       ]
     },
@@ -160,42 +142,24 @@ ruleTester.run('no-deprecated-destroyed-lifecycle', rule, {
       }
       </script>
       `,
-      errors: [
-        {
-          message:
-            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
-          line: 4,
-          suggestions: [
-            {
-              desc: 'Instead, change to `beforeUnmount`.',
-              output: `
+      output: `
       <script>
       export default {
         beforeUnmount:beforeDestroy,
-        destroyed,
+        unmounted:destroyed,
       }
       </script>
-      `
-            }
-          ]
+      `,
+      errors: [
+        {
+          message:
+            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
+          line: 4
         },
         {
           message:
             'The `destroyed` lifecycle hook is deprecated. Use `unmounted` instead.',
-          line: 5,
-          suggestions: [
-            {
-              desc: 'Instead, change to `unmounted`.',
-              output: `
-      <script>
-      export default {
-        beforeDestroy,
-        unmounted:destroyed,
-      }
-      </script>
-      `
-            }
-          ]
+          line: 5
         }
       ]
     },
@@ -218,15 +182,7 @@ ruleTester.run('no-deprecated-destroyed-lifecycle', rule, {
       }
       </script>
       `,
-      errors: [
-        {
-          message:
-            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
-          line: 12,
-          suggestions: [
-            {
-              desc: 'Instead, change to `beforeUnmount`.',
-              output: `
+      output: `
       <script>
       export default {
         beforeCreate,
@@ -238,40 +194,21 @@ ruleTester.run('no-deprecated-destroyed-lifecycle', rule, {
         activated,
         deactivated,
         beforeUnmount:beforeDestroy,
-        destroyed,
+        unmounted:destroyed,
         errorCaptured,
       }
       </script>
-      `
-            }
-          ]
+      `,
+      errors: [
+        {
+          message:
+            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
+          line: 12
         },
         {
           message:
             'The `destroyed` lifecycle hook is deprecated. Use `unmounted` instead.',
-          line: 13,
-          suggestions: [
-            {
-              desc: 'Instead, change to `unmounted`.',
-              output: `
-      <script>
-      export default {
-        beforeCreate,
-        created,
-        beforeMount,
-        mounted,
-        beforeUpdate,
-        updated,
-        activated,
-        deactivated,
-        beforeDestroy,
-        unmounted:destroyed,
-        errorCaptured,
-      }
-      </script>
-      `
-            }
-          ]
+          line: 13
         }
       ]
     },
@@ -285,42 +222,24 @@ ruleTester.run('no-deprecated-destroyed-lifecycle', rule, {
       }
       </script>
       `,
-      errors: [
-        {
-          message:
-            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
-          line: 4,
-          suggestions: [
-            {
-              desc: 'Instead, change to `beforeUnmount`.',
-              output: `
+      output: `
       <script>
       export default {
         ['beforeUnmount']() {},
-        ['destroyed']() {},
+        ['unmounted']() {},
       }
       </script>
-      `
-            }
-          ]
+      `,
+      errors: [
+        {
+          message:
+            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
+          line: 4
         },
         {
           message:
             'The `destroyed` lifecycle hook is deprecated. Use `unmounted` instead.',
-          line: 5,
-          suggestions: [
-            {
-              desc: 'Instead, change to `unmounted`.',
-              output: `
-      <script>
-      export default {
-        ['beforeDestroy']() {},
-        ['unmounted']() {},
-      }
-      </script>
-      `
-            }
-          ]
+          line: 5
         }
       ]
     },
@@ -334,42 +253,24 @@ ruleTester.run('no-deprecated-destroyed-lifecycle', rule, {
       }
       </script>
       `,
-      errors: [
-        {
-          message:
-            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
-          line: 4,
-          suggestions: [
-            {
-              desc: 'Instead, change to `beforeUnmount`.',
-              output: `
+      output: `
       <script>
       export default {
         [\`beforeUnmount\`]() {},
-        [\`destroyed\`]() {},
+        [\`unmounted\`]() {},
       }
       </script>
-      `
-            }
-          ]
+      `,
+      errors: [
+        {
+          message:
+            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
+          line: 4
         },
         {
           message:
             'The `destroyed` lifecycle hook is deprecated. Use `unmounted` instead.',
-          line: 5,
-          suggestions: [
-            {
-              desc: 'Instead, change to `unmounted`.',
-              output: `
-      <script>
-      export default {
-        [\`beforeDestroy\`]() {},
-        [\`unmounted\`]() {},
-      }
-      </script>
-      `
-            }
-          ]
+          line: 5
         }
       ]
     }
diff --git a/tests/lib/rules/no-deprecated-html-element-is.js b/tests/lib/rules/no-deprecated-html-element-is.js
index 96164494..9b7cfdb2 100644
--- a/tests/lib/rules/no-deprecated-html-element-is.js
+++ b/tests/lib/rules/no-deprecated-html-element-is.js
@@ -37,6 +37,12 @@ ruleTester.run('no-deprecated-html-element-is', rule, {
     {
       filename: 'test.vue',
       code: '<template><component :is="\'foo\'" /></template>'
+    },
+
+    // is="vue:xxx"
+    {
+      filename: 'test.vue',
+      code: '<template><div is="vue:foo" /></template>'
     }
   ],
 
diff --git a/tests/lib/rules/no-deprecated-v-is.js b/tests/lib/rules/no-deprecated-v-is.js
new file mode 100644
index 00000000..cdc47b9a
--- /dev/null
+++ b/tests/lib/rules/no-deprecated-v-is.js
@@ -0,0 +1,36 @@
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-deprecated-v-is')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2015
+  }
+})
+
+tester.run('no-deprecated-v-is', rule, {
+  valid: [
+    `<template>
+      <div is="vue:MyComponent" />
+    </template>`,
+    `<template>
+      <component is="MyComponent" />
+    </template>`
+  ],
+  invalid: [
+    {
+      code: `
+      <template>
+        <div v-is="'MyComponent'" />
+      </template>`,
+      errors: [
+        {
+          message: '`v-is` directive is deprecated.',
+          line: 3
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-this-in-before-route-enter.js b/tests/lib/rules/no-this-in-before-route-enter.js
new file mode 100644
index 00000000..1171959a
--- /dev/null
+++ b/tests/lib/rules/no-this-in-before-route-enter.js
@@ -0,0 +1,190 @@
+/**
+ * @fileoverview  Don&#39;t use &#34;this&#34; i a beforeRouteEnter method
+ * @author Przemyslaw Jan Beigert
+ */
+'use strict'
+
+//------------------------------------------------------------------------------
+// Requirements
+//------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-this-in-before-route-enter')
+const RuleTester = require('eslint').RuleTester
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+const template = (beforeRouteEnter) => `
+<template>
+  <p>{{ greeting }} World!</p>
+</template>
+
+<script>
+export default {
+  data () {
+    return {
+      greeting: "Hello"
+    };
+  },
+  beforeRouteEnter() {
+      ${beforeRouteEnter}
+  }
+};
+</script>
+
+<style scoped>
+p {
+  font-size: 2em;
+  text-align: center;
+}
+</style>`
+
+const functionTemplate = (beforeRouteEnter) => `
+<template>
+  <p>{{ greeting }} World!</p>
+</template>
+
+<script>
+export default {
+  data () {
+    return {
+      greeting: "Hello"
+    };
+  },
+  beforeRouteEnter: function() {
+      ${beforeRouteEnter}
+  }
+};
+</script>`
+
+const ruleTester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
+})
+ruleTester.run('no-this-in-before-route-enter', rule, {
+  valid: [
+    template(''),
+    template('const variable = 42;'),
+    template('someFunction(42)'),
+    `
+<template>
+  <p>{{ greeting }} World!</p>
+</template>
+
+<script>
+export default {
+  data () {
+    return {
+      greeting: "Hello"
+    };
+  },
+};`
+  ],
+  invalid: [
+    {
+      code: template(`this.xxx();`),
+      filename: 'ValidComponent.vue',
+      errors: [
+        {
+          message:
+            "'beforeRouteEnter' does NOT have access to `this` component instance. https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards.",
+          line: 14,
+          column: 7,
+          endLine: 14,
+          endColumn: 11
+        }
+      ]
+    },
+    {
+      code: functionTemplate('this.method();'),
+      filename: 'ValidComponent.vue',
+      errors: [
+        {
+          message:
+            "'beforeRouteEnter' does NOT have access to `this` component instance. https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards.",
+          line: 14,
+          column: 7,
+          endLine: 14,
+          endColumn: 11
+        }
+      ]
+    },
+    {
+      code: template('this.attr = this.method();'),
+      filename: 'ValidComponent.vue',
+      errors: [
+        {
+          message:
+            "'beforeRouteEnter' does NOT have access to `this` component instance. https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards.",
+          line: 14,
+          column: 7,
+          endLine: 14,
+          endColumn: 11
+        },
+        {
+          message:
+            "'beforeRouteEnter' does NOT have access to `this` component instance. https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards.",
+          line: 14,
+          column: 19,
+          endLine: 14,
+          endColumn: 23
+        }
+      ]
+    },
+    {
+      code: functionTemplate('this.attr = this.method();'),
+      filename: 'ValidComponent.vue',
+      errors: [
+        {
+          message:
+            "'beforeRouteEnter' does NOT have access to `this` component instance. https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards.",
+          line: 14,
+          column: 7,
+          endLine: 14,
+          endColumn: 11
+        },
+        {
+          message:
+            "'beforeRouteEnter' does NOT have access to `this` component instance. https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards.",
+          line: 14,
+          column: 19,
+          endLine: 14,
+          endColumn: 23
+        }
+      ]
+    },
+    {
+      code: template(`
+                if (this.method()) {}
+            `),
+      filename: 'ValidComponent.vue',
+      errors: [
+        {
+          message:
+            "'beforeRouteEnter' does NOT have access to `this` component instance. https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards.",
+          line: 15,
+          column: 21,
+          endLine: 15,
+          endColumn: 25
+        }
+      ]
+    },
+    {
+      code: functionTemplate(`
+                if (true) { this.method(); }
+            `),
+      filename: 'ValidComponent.vue',
+      errors: [
+        {
+          message:
+            "'beforeRouteEnter' does NOT have access to `this` component instance. https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards.",
+          line: 15,
+          column: 29,
+          endLine: 15,
+          endColumn: 33
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-unregistered-components.js b/tests/lib/rules/no-unregistered-components.js
index 27739b78..6a51bf49 100644
--- a/tests/lib/rules/no-unregistered-components.js
+++ b/tests/lib/rules/no-unregistered-components.js
@@ -446,6 +446,19 @@ tester.run('no-unregistered-components', rule, {
         </script>
       `
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <component is="vue:CustomComponent" />
+        </template>
+        <script>
+        export default {
+          name: 'CustomComponent'
+        }
+        </script>
+      `
+    },
     {
       filename: 'test.vue',
       code: `
@@ -706,6 +719,48 @@ tester.run('no-unregistered-components', rule, {
           line: 3
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div is="vue:foo" />
+        </template>
+        <script>
+        export default {
+          components: {
+            bar
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          message: 'The "foo" component has been used but not registered.',
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div v-is="'foo'" />
+        </template>
+        <script>
+        export default {
+          components: {
+            bar
+          }
+        }
+        </script>
+      `,
+      errors: [
+        {
+          message: 'The "foo" component has been used but not registered.',
+          line: 3
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-unsupported-features/is-attribute-with-vue-prefix.js b/tests/lib/rules/no-unsupported-features/is-attribute-with-vue-prefix.js
new file mode 100644
index 00000000..adcc6483
--- /dev/null
+++ b/tests/lib/rules/no-unsupported-features/is-attribute-with-vue-prefix.js
@@ -0,0 +1,64 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../../lib/rules/no-unsupported-features')
+const utils = require('./utils')
+
+const buildOptions = utils.optionsBuilder(
+  'is-attribute-with-vue-prefix',
+  '^3.1.0'
+)
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2019
+  }
+})
+
+tester.run('no-unsupported-features/is-attribute-with-vue-prefix', rule, {
+  valid: [
+    {
+      code: `
+       <template>
+         <div is="vue:foo" />
+       </template>`,
+      options: buildOptions()
+    },
+    {
+      code: `
+       <template>
+         <div is="foo" />
+       </template>`,
+      options: buildOptions({ version: '^2.5.0' })
+    },
+    {
+      code: `
+       <template>
+         <div is="vue:foo" />
+       </template>`,
+      options: buildOptions({
+        version: '^2.5.0',
+        ignores: ['is-attribute-with-vue-prefix']
+      })
+    }
+  ],
+  invalid: [
+    {
+      code: `
+      <template>
+        <div is="vue:foo" />
+      </template>`,
+      options: buildOptions({ version: '^3.0.0' }),
+      errors: [
+        {
+          message: '`is="vue:"` are not supported until Vue.js "3.1.0".',
+          line: 3
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-unused-components.js b/tests/lib/rules/no-unused-components.js
index 02cb1a3c..f63b75f1 100644
--- a/tests/lib/rules/no-unused-components.js
+++ b/tests/lib/rules/no-unused-components.js
@@ -486,6 +486,21 @@ tester.run('no-unused-components', rule, {
         }
         </script>
       `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div is="vue:CustomComponent" />
+        </template>
+        <script>
+        export default {
+          components: {
+            CustomComponent
+          }
+        }
+        </script>
+      `
     }
   ],
   invalid: [