Repository: vuejs/eslint-plugin-vue
Pull request #1506 (2021-06-02 17:57:44)

feat(no-this-in-before-router-enter): create rule
close  #1417

przemyslawjanpietrzak: @ota-meshi I applied all of your suggestions. Thanks a lot for for it, it is my first PR so it was very helpful. 

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index d38c44a2..16185b23 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -313,6 +313,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-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/lib/index.js b/lib/index.js
index 1379e284..20ecde95 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -113,6 +113,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-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'
+          })
+        }
+      }
+    })
+  }
+}


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-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
+        }
+      ]
+    }
+  ]
+})