Repository: vuejs/eslint-plugin-vue
Pull request #1211 (2020-06-15 01:32:50)

Add `vue/no-deprecated-destroyed-lifecycle` rule
This PR adds `vue/no-deprecated-destroyed-lifecycle` rule.

The `vue/no-deprecated-destroyed-lifecycle` rule reports use of deprecated `destroyed` and `beforeDestroy` lifecycle hooks.


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 1fe65868..16e0ef98 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -42,6 +42,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 | [vue/no-arrow-functions-in-watch](./no-arrow-functions-in-watch.md) | disallow using arrow functions to define watcher |  |
 | [vue/no-async-in-computed-properties](./no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties |  |
 | [vue/no-deprecated-data-object-declaration](./no-deprecated-data-object-declaration.md) | disallow using deprecated object declaration on data (in Vue.js 3.0.0+) | :wrench: |
+| [vue/no-deprecated-destroyed-lifecycle](./no-deprecated-destroyed-lifecycle.md) | disallow using deprecated `destroyed` and `beforeDestroy` lifecycle hooks (in Vue.js 3.0.0+) |  |
 | [vue/no-deprecated-dollar-listeners-api](./no-deprecated-dollar-listeners-api.md) | disallow using deprecated `$listeners` (in Vue.js 3.0.0+) |  |
 | [vue/no-deprecated-dollar-scopedslots-api](./no-deprecated-dollar-scopedslots-api.md) | disallow using deprecated `$scopedSlots` (in Vue.js 3.0.0+) | :wrench: |
 | [vue/no-deprecated-events-api](./no-deprecated-events-api.md) | disallow using deprecated events api (in Vue.js 3.0.0+) |  |
diff --git a/docs/rules/no-bare-strings-in-template.md b/docs/rules/no-bare-strings-in-template.md
index 40041119..2edbfda6 100644
--- a/docs/rules/no-bare-strings-in-template.md
+++ b/docs/rules/no-bare-strings-in-template.md
@@ -57,7 +57,7 @@ If you want to report these string literals, enable the [vue/no-useless-v-bind]
 ```js
 {
   "vue/no-bare-strings-in-template": ["error", {
-    "whitelist": [
+    "allowlist": [
       "(", ")", ",", ".", "&", "+", "-", "=", "*", "/", "#", "%", "!", "?", ":", "[", "]", "{", "}", "<", ">", "\u00b7", "\u2022", "\u2010", "\u2013", "\u2014", "\u2212", "|"
     ],
     "attributes": {
@@ -70,7 +70,7 @@ If you want to report these string literals, enable the [vue/no-useless-v-bind]
 }
 ```
 
-- `whitelist` ... An array of whitelisted strings.
+- `allowlist` ... An array of allowed strings.
 - `attributes` ... An object whose keys are tag name or patterns and value is an array of attributes to check for that tag name.
 - `directives` ... An array of directive names to check literal value.
 
diff --git a/docs/rules/no-deprecated-destroyed-lifecycle.md b/docs/rules/no-deprecated-destroyed-lifecycle.md
new file mode 100644
index 00000000..f7f7cd8d
--- /dev/null
+++ b/docs/rules/no-deprecated-destroyed-lifecycle.md
@@ -0,0 +1,43 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-deprecated-destroyed-lifecycle
+description: disallow using deprecated `destroyed` and `beforeDestroy` lifecycle hooks (in Vue.js 3.0.0+)
+---
+# vue/no-deprecated-destroyed-lifecycle
+> disallow using deprecated `destroyed` and `beforeDestroy` lifecycle hooks (in Vue.js 3.0.0+)
+
+- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/vue3-strongly-recommended"` and `"plugin:vue/vue3-recommended"`.
+
+## :book: Rule Details
+
+This rule reports use of deprecated `destroyed` and `beforeDestroy` lifecycle hooks. (in Vue.js 3.0.0+).
+
+<eslint-code-block :rules="{'vue/no-deprecated-destroyed-lifecycle': ['error']}">
+
+```vue
+<script>
+export default {
+  /* ✓ GOOD */
+  beforeMount () {},
+  mounted () {},
+  beforeUnmount () {},
+  unmounted () {},
+
+  /* ✗ BAD */
+  beforeDestroy () {},
+  destroyed () {}
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-deprecated-destroyed-lifecycle.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-deprecated-destroyed-lifecycle.js)
diff --git a/docs/rules/require-prop-type-constructor.md b/docs/rules/require-prop-type-constructor.md
index af004aaf..9d48c071 100644
--- a/docs/rules/require-prop-type-constructor.md
+++ b/docs/rules/require-prop-type-constructor.md
@@ -14,7 +14,7 @@ description: require prop type to be a constructor
 
 This rule reports prop types that can't be presumed as constructors.
 
-It's impossible to catch every possible case and know whether the prop type is a constructor or not, hence this rule black list few types of nodes, instead of white-listing correct ones.
+It's impossible to catch every possible case and know whether the prop type is a constructor or not, hence this rule restricts few types of nodes, instead of allowing correct ones.
 
 The following types are forbidden and will be reported:
 
diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js
index b802623d..432df355 100644
--- a/lib/configs/vue3-essential.js
+++ b/lib/configs/vue3-essential.js
@@ -10,6 +10,7 @@ module.exports = {
     'vue/no-arrow-functions-in-watch': 'error',
     'vue/no-async-in-computed-properties': 'error',
     'vue/no-deprecated-data-object-declaration': 'error',
+    'vue/no-deprecated-destroyed-lifecycle': 'error',
     'vue/no-deprecated-dollar-listeners-api': 'error',
     'vue/no-deprecated-dollar-scopedslots-api': 'error',
     'vue/no-deprecated-events-api': 'error',
diff --git a/lib/index.js b/lib/index.js
index f962015c..7eda5423 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -51,6 +51,7 @@ module.exports = {
     'no-confusing-v-for-v-if': require('./rules/no-confusing-v-for-v-if'),
     'no-custom-modifiers-on-v-model': require('./rules/no-custom-modifiers-on-v-model'),
     'no-deprecated-data-object-declaration': require('./rules/no-deprecated-data-object-declaration'),
+    'no-deprecated-destroyed-lifecycle': require('./rules/no-deprecated-destroyed-lifecycle'),
     'no-deprecated-dollar-listeners-api': require('./rules/no-deprecated-dollar-listeners-api'),
     'no-deprecated-dollar-scopedslots-api': require('./rules/no-deprecated-dollar-scopedslots-api'),
     'no-deprecated-events-api': require('./rules/no-deprecated-events-api'),
diff --git a/lib/rules/no-bare-strings-in-template.js b/lib/rules/no-bare-strings-in-template.js
index d0426e3a..29bc17db 100644
--- a/lib/rules/no-bare-strings-in-template.js
+++ b/lib/rules/no-bare-strings-in-template.js
@@ -21,7 +21,7 @@ const casing = require('../utils/casing')
 // ------------------------------------------------------------------------------
 
 // https://dev.w3.org/html5/html-author/charref
-const DEFAULT_WHITELIST = [
+const DEFAULT_ALLOWLIST = [
   '(',
   ')',
   ',',
@@ -128,7 +128,7 @@ module.exports = {
       {
         type: 'object',
         properties: {
-          whitelist: {
+          allowlist: {
             type: 'array',
             items: { type: 'string' },
             uniqueItems: true
@@ -164,12 +164,12 @@ module.exports = {
      */
     const opts = context.options[0] || {}
     /** @type {string[]} */
-    const whitelist = opts.whitelist || DEFAULT_WHITELIST
+    const allowlist = opts.allowlist || DEFAULT_ALLOWLIST
     const attributes = parseTargetAttrs(opts.attributes || DEFAULT_ATTRIBUTES)
     const directives = opts.directives || DEFAULT_DIRECTIVES
 
-    const whitelistRe = new RegExp(
-      whitelist.map((w) => regexp.escape(w)).join('|'),
+    const allowlistRe = new RegExp(
+      allowlist.map((w) => regexp.escape(w)).join('|'),
       'gu'
     )
 
@@ -180,7 +180,7 @@ module.exports = {
      * @param {string} str
      */
     function getBareString(str) {
-      return str.trim().replace(whitelistRe, '').trim()
+      return str.trim().replace(allowlistRe, '').trim()
     }
 
     /**
diff --git a/lib/rules/no-deprecated-destroyed-lifecycle.js b/lib/rules/no-deprecated-destroyed-lifecycle.js
new file mode 100644
index 00000000..c3556084
--- /dev/null
+++ b/lib/rules/no-deprecated-destroyed-lifecycle.js
@@ -0,0 +1,101 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description:
+        'disallow using deprecated `destroyed` and `beforeDestroy` lifecycle hooks (in Vue.js 3.0.0+)',
+      categories: ['vue3-essential'],
+      url:
+        'https://eslint.vuejs.org/rules/no-deprecated-destroyed-lifecycle.html'
+    },
+    fixable: null,
+    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`.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    return utils.executeOnVue(context, (obj) => {
+      const destroyed = utils.findProperty(obj, 'destroyed')
+
+      if (destroyed) {
+        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')
+              }
+            }
+          ]
+        })
+      }
+
+      const beforeDestroy = utils.findProperty(obj, 'beforeDestroy')
+      if (beforeDestroy) {
+        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')
+              }
+            }
+          ]
+        })
+      }
+
+      /**
+       * @param {RuleFixer} fixer
+       * @param {Property} property
+       * @param {string} newName
+       */
+      function fix(fixer, property, newName) {
+        if (property.computed) {
+          if (
+            property.key.type === 'Literal' ||
+            property.key.type === 'TemplateLiteral'
+          ) {
+            return fixer.replaceTextRange(
+              [property.key.range[0] + 1, property.key.range[1] - 1],
+              newName
+            )
+          }
+          return null
+        }
+        if (property.shorthand) {
+          return fixer.insertTextBefore(property.key, `${newName}:`)
+        }
+        return fixer.replaceText(property.key, newName)
+      }
+    })
+  }
+}


Test patch:
diff --git a/tests/lib/rules/html-indent.js b/tests/lib/rules/html-indent.js
index d70b0395..316eb925 100644
--- a/tests/lib/rules/html-indent.js
+++ b/tests/lib/rules/html-indent.js
@@ -78,6 +78,7 @@ function loadPatterns(additionalValid, additionalInvalid) {
 
       return Object.assign({}, pattern, { code, output, errors })
     })
+    .filter((invalid) => invalid.errors.length > 0) // Empty errors cannot be verified with eslint 7.3.
     .filter(Boolean)
 
   return {
diff --git a/tests/lib/rules/no-bare-strings-in-template.js b/tests/lib/rules/no-bare-strings-in-template.js
index 3fc4a432..8856ab96 100644
--- a/tests/lib/rules/no-bare-strings-in-template.js
+++ b/tests/lib/rules/no-bare-strings-in-template.js
@@ -214,7 +214,7 @@ tester.run('no-bare-strings-in-template', rule, {
         <h1>ipsum</h1>
       </template>
       `,
-      options: [{ whitelist: ['Lorem'] }],
+      options: [{ allowlist: ['Lorem'] }],
       errors: [
         {
           line: 4,
diff --git a/tests/lib/rules/no-deprecated-destroyed-lifecycle.js b/tests/lib/rules/no-deprecated-destroyed-lifecycle.js
new file mode 100644
index 00000000..00dda6ed
--- /dev/null
+++ b/tests/lib/rules/no-deprecated-destroyed-lifecycle.js
@@ -0,0 +1,377 @@
+/**
+ * @author Yosuke Ota
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/no-deprecated-destroyed-lifecycle')
+
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2020, sourceType: 'module' }
+})
+ruleTester.run('no-deprecated-destroyed-lifecycle', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        unmounted () {},
+        beforeUnmount () {},
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        unmounted,
+        beforeUnmount,
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        beforeCreate,
+        created,
+        beforeMount,
+        mounted,
+        beforeUpdate,
+        updated,
+        activated,
+        deactivated,
+        beforeUnmount,
+        unmounted,
+        errorCaptured,
+        renderTracked,
+        renderTriggered,
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        beforeUnmount:beforeDestroy,
+        unmounted:destroyed,
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        ...beforeDestroy,
+        ...destroyed,
+      }
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        [beforeDestroy] () {},
+        [destroyed] () {},
+      }
+      </script>
+      `
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        beforeDestroy () {},
+        destroyed () {},
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
+          line: 4,
+          suggestions: [
+            {
+              desc: 'Instead, change to `beforeUnmount`.',
+              output: `
+      <script>
+      export default {
+        beforeUnmount () {},
+        destroyed () {},
+      }
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          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>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        beforeDestroy,
+        destroyed,
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
+          line: 4,
+          suggestions: [
+            {
+              desc: 'Instead, change to `beforeUnmount`.',
+              output: `
+      <script>
+      export default {
+        beforeUnmount:beforeDestroy,
+        destroyed,
+      }
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          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>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        beforeCreate,
+        created,
+        beforeMount,
+        mounted,
+        beforeUpdate,
+        updated,
+        activated,
+        deactivated,
+        beforeDestroy,
+        destroyed,
+        errorCaptured,
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
+          line: 12,
+          suggestions: [
+            {
+              desc: 'Instead, change to `beforeUnmount`.',
+              output: `
+      <script>
+      export default {
+        beforeCreate,
+        created,
+        beforeMount,
+        mounted,
+        beforeUpdate,
+        updated,
+        activated,
+        deactivated,
+        beforeUnmount:beforeDestroy,
+        destroyed,
+        errorCaptured,
+      }
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          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>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        ['beforeDestroy']() {},
+        ['destroyed']() {},
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
+          line: 4,
+          suggestions: [
+            {
+              desc: 'Instead, change to `beforeUnmount`.',
+              output: `
+      <script>
+      export default {
+        ['beforeUnmount']() {},
+        ['destroyed']() {},
+      }
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          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>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        [\`beforeDestroy\`]() {},
+        [\`destroyed\`]() {},
+      }
+      </script>
+      `,
+      errors: [
+        {
+          message:
+            'The `beforeDestroy` lifecycle hook is deprecated. Use `beforeUnmount` instead.',
+          line: 4,
+          suggestions: [
+            {
+              desc: 'Instead, change to `beforeUnmount`.',
+              output: `
+      <script>
+      export default {
+        [\`beforeUnmount\`]() {},
+        [\`destroyed\`]() {},
+      }
+      </script>
+      `
+            }
+          ]
+        },
+        {
+          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>
+      `
+            }
+          ]
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/script-indent.js b/tests/lib/rules/script-indent.js
index 42497d2d..865e7314 100644
--- a/tests/lib/rules/script-indent.js
+++ b/tests/lib/rules/script-indent.js
@@ -79,6 +79,7 @@ function loadPatterns(additionalValid, additionalInvalid) {
 
       return Object.assign({}, pattern, { code, output, errors })
     })
+    .filter((invalid) => invalid.errors.length > 0) // Empty errors cannot be verified with eslint 7.3.
     .filter(Boolean)
 
   return {