Repository: vuejs/eslint-plugin-vue
Pull request #1281 (2020-08-22 02:01:04)

Separate rule that report <template v-for key> from no-template-key rule
This PR separates the rule reporting keys placed on `<template v-for>` from `vue/no-template-key` rule.
This PR makes the following changes.

- Change `vue/no-template-key` to allow `v-for` key.
- Add `vue/no-v-for-template-key` rule that disallow the key placed on the `<template v-for>`.
- Add `vue/no-v-for-template-key` rule to `plugin:vue/essential` config.

refs #1279 


Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index 63d538e4..dfb0a2a4 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -187,6 +187,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi
 | [vue/no-unused-components](./no-unused-components.md) | disallow registering components that are not used inside templates |  |
 | [vue/no-unused-vars](./no-unused-vars.md) | disallow unused variable definitions of v-for directives or scope attributes |  |
 | [vue/no-use-v-if-with-v-for](./no-use-v-if-with-v-for.md) | disallow use v-if on the same element as v-for |  |
+| [vue/no-v-for-template-key](./no-v-for-template-key.md) | disallow `key` attribute on `<template v-for>` |  |
 | [vue/no-v-model-argument](./no-v-model-argument.md) | disallow adding an argument to `v-model` used in custom component |  |
 | [vue/require-component-is](./require-component-is.md) | require `v-bind:is` of `<component>` elements |  |
 | [vue/require-prop-type-constructor](./require-prop-type-constructor.md) | require prop type to be a constructor | :wrench: |
diff --git a/docs/rules/no-template-key.md b/docs/rules/no-template-key.md
index ebd4b40e..3b0d8366 100644
--- a/docs/rules/no-template-key.md
+++ b/docs/rules/no-template-key.md
@@ -23,6 +23,9 @@ This rule reports the `<template>` elements which have `key` attribute.
   <div key="foo"> ... </div>
   <template> ... </template>
 
+  <!-- It's valid for Vue.js 3.x -->
+  <template v-for="item in list" :key="item.id"> ... </template>
+
   <!-- ✗ BAD -->
   <template key="foo"> ... </template>
   <template v-bind:key="bar"> ... </template>
@@ -32,10 +35,20 @@ This rule reports the `<template>` elements which have `key` attribute.
 
 </eslint-code-block>
 
+::: tip Note
+This rule does not report keys placed on `<template v-for>`. It's valid for Vue.js 3.x. If you want to report keys placed on `<template v-for>` invalid for Vue.js 2.x, use [vue/no-v-for-template-key] rule.
+:::
+
 ## :wrench: Options
 
 Nothing.
 
+## :couple: Related Rules
+
+- [vue/no-v-for-template-key]
+
+[vue/no-v-for-template-key]: ./no-v-for-template-key.md
+
 ## :books: Further Reading
 
 - [API - Special Attributes - key](https://v3.vuejs.org/api/special-attributes.html#key)
diff --git a/docs/rules/no-v-for-template-key.md b/docs/rules/no-v-for-template-key.md
new file mode 100644
index 00000000..a39709a3
--- /dev/null
+++ b/docs/rules/no-v-for-template-key.md
@@ -0,0 +1,58 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-v-for-template-key
+description: disallow `key` attribute on `<template v-for>`
+---
+# vue/no-v-for-template-key
+> disallow `key` attribute on `<template v-for>`
+
+- :gear: This rule is included in all of `"plugin:vue/essential"`, `"plugin:vue/strongly-recommended"` and `"plugin:vue/recommended"`.
+
+Vue.js disallows `key` attribute on `<template>` elements.
+
+## :book: Rule Details
+
+This rule reports the `<template v-for>` elements which have `key` attribute.
+
+<eslint-code-block :rules="{'vue/no-v-for-template-key': ['error']}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <template v-for="item in list">
+    <div :key="item.id" />
+  </template>
+
+  <!-- ✗ BAD -->
+  <template v-for="item in list" :key="item.id">
+    <div />
+  </template>
+</template>
+```
+
+</eslint-code-block>
+
+::: tip Note
+If you want to report keys placed on `<template>` without `v-for`, use the [vue/no-template-key] rule.
+:::
+
+## :wrench: Options
+
+Nothing.
+
+## :couple: Related Rules
+
+- [vue/no-template-key](./no-template-key.md)
+
+[vue/no-template-key]: ./no-template-key.md
+
+## :books: Further Reading
+
+- [API - Special Attributes - key](https://v3.vuejs.org/api/special-attributes.html#key)
+- [API (for v2) - Special Attributes - key](https://vuejs.org/v2/api/#key)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-v-for-template-key.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-v-for-template-key.js)
diff --git a/lib/configs/essential.js b/lib/configs/essential.js
index 722cf1fa..7b7a5873 100644
--- a/lib/configs/essential.js
+++ b/lib/configs/essential.js
@@ -24,6 +24,7 @@ module.exports = {
     'vue/no-unused-components': 'error',
     'vue/no-unused-vars': 'error',
     'vue/no-use-v-if-with-v-for': 'error',
+    'vue/no-v-for-template-key': 'error',
     'vue/no-v-model-argument': 'error',
     'vue/require-component-is': 'error',
     'vue/require-prop-type-constructor': 'error',
diff --git a/lib/index.js b/lib/index.js
index 71c922f4..d6c82ea2 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -109,6 +109,7 @@ module.exports = {
     'no-useless-concat': require('./rules/no-useless-concat'),
     'no-useless-mustaches': require('./rules/no-useless-mustaches'),
     'no-useless-v-bind': require('./rules/no-useless-v-bind'),
+    'no-v-for-template-key': require('./rules/no-v-for-template-key'),
     'no-v-html': require('./rules/no-v-html'),
     'no-v-model-argument': require('./rules/no-v-model-argument'),
     'no-watch-after-await': require('./rules/no-watch-after-await'),
diff --git a/lib/rules/no-template-key.js b/lib/rules/no-template-key.js
index 93511382..c3fc3f2b 100644
--- a/lib/rules/no-template-key.js
+++ b/lib/rules/no-template-key.js
@@ -24,22 +24,31 @@ module.exports = {
       url: 'https://eslint.vuejs.org/rules/no-template-key.html'
     },
     fixable: null,
-    schema: []
+    schema: [],
+    messages: {
+      disallow:
+        "'<template>' cannot be keyed. Place the key on real elements instead."
+    }
   },
   /** @param {RuleContext} context */
   create(context) {
     return utils.defineTemplateBodyVisitor(context, {
       /** @param {VElement} node */
       "VElement[name='template']"(node) {
-        if (
-          utils.hasAttribute(node, 'key') ||
-          utils.hasDirective(node, 'bind', 'key')
-        ) {
+        const keyNode =
+          utils.getAttribute(node, 'key') ||
+          utils.getDirective(node, 'bind', 'key')
+        if (keyNode) {
+          if (utils.hasDirective(node, 'for')) {
+            // It's valid for Vue.js 3.x.
+            // <template v-for="item in list" :key="item.id"> ... </template>
+            // see https://github.com/vuejs/vue-next/issues/1734
+            return
+          }
           context.report({
-            node,
-            loc: node.loc,
-            message:
-              "'<template>' cannot be keyed. Place the key on real elements instead."
+            node: keyNode,
+            loc: keyNode.loc,
+            messageId: 'disallow'
           })
         }
       }
diff --git a/lib/rules/no-v-for-template-key.js b/lib/rules/no-v-for-template-key.js
new file mode 100644
index 00000000..9a017515
--- /dev/null
+++ b/lib/rules/no-v-for-template-key.js
@@ -0,0 +1,48 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'disallow `key` attribute on `<template v-for>`',
+      categories: ['essential'],
+      url: 'https://eslint.vuejs.org/rules/no-v-for-template-key.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      disallow:
+        "'<template v-for>' cannot be keyed. Place the key on real elements instead."
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    return utils.defineTemplateBodyVisitor(context, {
+      /** @param {VDirective} node */
+      "VElement[name='template'] > VStartTag > VAttribute[directive=true][key.name.name='for']"(
+        node
+      ) {
+        const element = node.parent.parent
+        const keyNode =
+          utils.getAttribute(element, 'key') ||
+          utils.getDirective(element, 'bind', 'key')
+        if (keyNode) {
+          context.report({
+            node: keyNode,
+            loc: keyNode.loc,
+            messageId: 'disallow'
+          })
+        }
+      }
+    })
+  }
+}
diff --git a/lib/utils/keycode-to-key.js b/lib/utils/keycode-to-key.js
index 365ae435..cabf377d 100644
--- a/lib/utils/keycode-to-key.js
+++ b/lib/utils/keycode-to-key.js
@@ -1,26 +1,26 @@
 // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values
 /** @type { { [key: number]: string }  } */
 module.exports = {
-  '8': 'backspace',
-  '9': 'tab',
-  '13': 'enter',
-  '16': 'shift',
-  '17': 'ctrl',
-  '18': 'alt',
-  '19': 'pause', // windows
-  '20': 'caps-lock',
-  '27': 'escape',
-  '32': 'space', // Vue.js specially key name.
-  '33': 'page-up',
-  '34': 'page-down',
-  '35': 'end',
-  '36': 'home',
-  '37': 'arrow-left',
-  '38': 'arrow-up',
-  '39': 'arrow-right',
-  '40': 'arrow-down',
-  '45': 'insert', // windows
-  '46': 'delete',
+  8: 'backspace',
+  9: 'tab',
+  13: 'enter',
+  16: 'shift',
+  17: 'ctrl',
+  18: 'alt',
+  19: 'pause', // windows
+  20: 'caps-lock',
+  27: 'escape',
+  32: 'space', // Vue.js specially key name.
+  33: 'page-up',
+  34: 'page-down',
+  35: 'end',
+  36: 'home',
+  37: 'arrow-left',
+  38: 'arrow-up',
+  39: 'arrow-right',
+  40: 'arrow-down',
+  45: 'insert', // windows
+  46: 'delete',
 
   // If mistakenly use it in Vue.js 2.x, it will be irreversibly broken. Therefore, it will not be autofix.
   // '48': '0',
@@ -34,32 +34,32 @@ module.exports = {
   // '56': '8',
   // '57': '9',
 
-  '65': 'a',
-  '66': 'b',
-  '67': 'c',
-  '68': 'd',
-  '69': 'e',
-  '70': 'f',
-  '71': 'g',
-  '72': 'h',
-  '73': 'i',
-  '74': 'j',
-  '75': 'k',
-  '76': 'l',
-  '77': 'm',
-  '78': 'n',
-  '79': 'o',
-  '80': 'p',
-  '81': 'q',
-  '82': 'r',
-  '83': 's',
-  '84': 't',
-  '85': 'u',
-  '86': 'v',
-  '87': 'w',
-  '88': 'x',
-  '89': 'y',
-  '90': 'z',
+  65: 'a',
+  66: 'b',
+  67: 'c',
+  68: 'd',
+  69: 'e',
+  70: 'f',
+  71: 'g',
+  72: 'h',
+  73: 'i',
+  74: 'j',
+  75: 'k',
+  76: 'l',
+  77: 'm',
+  78: 'n',
+  79: 'o',
+  80: 'p',
+  81: 'q',
+  82: 'r',
+  83: 's',
+  84: 't',
+  85: 'u',
+  86: 'v',
+  87: 'w',
+  88: 'x',
+  89: 'y',
+  90: 'z',
 
   // The key value may change depending on the OS.
   // '91': 'meta' ,// Win: 'os'?
@@ -82,18 +82,18 @@ module.exports = {
   // '109': 'subtract',
   // '110': 'decimal',
   // '111': 'divide',
-  '112': 'f1',
-  '113': 'f2',
-  '114': 'f3',
-  '115': 'f4',
-  '116': 'f5',
-  '117': 'f6',
-  '118': 'f7',
-  '119': 'f8',
-  '120': 'f9',
-  '121': 'f10',
-  '122': 'f11',
-  '123': 'f12',
-  '144': 'num-lock',
-  '145': 'scroll-lock'
+  112: 'f1',
+  113: 'f2',
+  114: 'f3',
+  115: 'f4',
+  116: 'f5',
+  117: 'f6',
+  118: 'f7',
+  119: 'f8',
+  120: 'f9',
+  121: 'f10',
+  122: 'f11',
+  123: 'f12',
+  144: 'num-lock',
+  145: 'scroll-lock'
 }
diff --git a/package.json b/package.json
index 7bc47d2c..22001b26 100644
--- a/package.json
+++ b/package.json
@@ -76,7 +76,7 @@
     "lodash": "^4.17.15",
     "mocha": "^7.1.2",
     "nyc": "^15.0.1",
-    "prettier": "^2.0.5",
+    "prettier": "^2.1.1",
     "typescript": "^3.9.5",
     "vue-eslint-editor": "^1.1.0",
     "vuepress": "^1.4.1"


Test patch:
diff --git a/tests/lib/rules/no-side-effects-in-computed-properties.js b/tests/lib/rules/no-side-effects-in-computed-properties.js
index 5aefc95c..ae5ff751 100644
--- a/tests/lib/rules/no-side-effects-in-computed-properties.js
+++ b/tests/lib/rules/no-side-effects-in-computed-properties.js
@@ -180,6 +180,16 @@ ruleTester.run('no-side-effects-in-computed-properties', rule, {
         el: test.el
       })`,
       parserOptions
+    },
+    {
+      code: `Vue.component('test', {
+        computed: {
+          test () {
+            return [...this.items].reverse()
+          },
+        }
+      })`,
+      parserOptions
     }
   ],
   invalid: [
diff --git a/tests/lib/rules/no-template-key.js b/tests/lib/rules/no-template-key.js
index fb373e80..9ec20a7f 100644
--- a/tests/lib/rules/no-template-key.js
+++ b/tests/lib/rules/no-template-key.js
@@ -42,6 +42,28 @@ tester.run('no-template-key', rule, {
     {
       filename: 'test.vue',
       code: '<template><div :key="foo"></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><template v-for="item in list" :key="item.id"><div /></template></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><template v-for="(item, i) in list" :key="i"><div /></template></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><template v-for="item in list" :key="foo + item.id"><div /></template></template>'
+    },
+    {
+      filename: 'test.vue',
+      // It is probably not valid, but it works as the Vue.js 3.x compiler.
+      // We can prevent it with other rules. e.g. vue/require-v-for-key
+      code:
+        '<template><template v-for="item in list" key="foo"><div /></template></template>'
     }
   ],
   invalid: [
@@ -66,6 +88,22 @@ tester.run('no-template-key', rule, {
       errors: [
         "'<template>' cannot be keyed. Place the key on real elements instead."
       ]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><template v-slot="item" :key="item.id"><div /></template></template>',
+      errors: [
+        "'<template>' cannot be keyed. Place the key on real elements instead."
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><template v-for="item in list"><template :key="item.id"><div /></template></template></template>',
+      errors: [
+        "'<template>' cannot be keyed. Place the key on real elements instead."
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/no-v-for-template-key.js b/tests/lib/rules/no-v-for-template-key.js
new file mode 100644
index 00000000..56fdb348
--- /dev/null
+++ b/tests/lib/rules/no-v-for-template-key.js
@@ -0,0 +1,102 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-v-for-template-key')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2015 }
+})
+
+tester.run('no-v-for-template-key', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: ''
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><template></template></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div key="foo"></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div v-bind:key="foo"></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div :key="foo"></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><template key="foo"></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-bind:key="foo"></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><template :key="foo"></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><template v-slot="item" :key="item.id"><div /></template></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><template v-for="item in list"><template :key="item.id"><div /></template></template></template>'
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code:
+        '<template><template v-for="item in list" :key="item.id"><div /></template></template>',
+      errors: [
+        "'<template v-for>' cannot be keyed. Place the key on real elements instead."
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><template v-for="(item, i) in list" :key="i"><div /></template></template>',
+      errors: [
+        "'<template v-for>' cannot be keyed. Place the key on real elements instead."
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><template v-for="item in list" :key="foo + item.id"><div /></template></template>',
+      errors: [
+        "'<template v-for>' cannot be keyed. Place the key on real elements instead."
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><template v-for="item in list" key="foo"><div /></template></template>',
+      errors: [
+        "'<template v-for>' cannot be keyed. Place the key on real elements instead."
+      ]
+    }
+  ]
+})