Repository: vuejs/eslint-plugin-vue
Pull request #1287 (2020-08-27 05:45:33)

Change `vue/valid-v-for` and `vue/require-v-for-key` rules to not report when placing key on `<template>`
This PR makes the following changes.

- Change `vue/valid-v-for` rule to not report when placing key on `<template>`
- Change `vue/require-v-for-key` rule to not report when placing key on `<template>`

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/rules/require-v-for-key.js b/lib/rules/require-v-for-key.js
index 987cb979..3f82e696 100644
--- a/lib/rules/require-v-for-key.js
+++ b/lib/rules/require-v-for-key.js
@@ -33,16 +33,16 @@ module.exports = {
      * @param {VElement} element The element node to check.
      */
     function checkKey(element) {
+      if (utils.hasDirective(element, 'bind', 'key')) {
+        return
+      }
       if (element.name === 'template' || element.name === 'slot') {
         for (const child of element.children) {
           if (child.type === 'VElement') {
             checkKey(child)
           }
         }
-      } else if (
-        !utils.isCustomComponent(element) &&
-        !utils.hasDirective(element, 'bind', 'key')
-      ) {
+      } else if (!utils.isCustomComponent(element)) {
         context.report({
           node: element.startTag,
           loc: element.startTag.loc,
diff --git a/lib/rules/valid-v-for.js b/lib/rules/valid-v-for.js
index 81fcc8a0..0d560aa9 100644
--- a/lib/rules/valid-v-for.js
+++ b/lib/rules/valid-v-for.js
@@ -70,7 +70,9 @@ function checkChildKey(context, vFor, child) {
  * @param {VElement} element The element node to check.
  */
 function checkKey(context, vFor, element) {
-  if (element.name === 'template') {
+  const vBindKey = utils.getDirective(element, 'bind', 'key')
+
+  if (vBindKey == null && element.name === 'template') {
     for (const child of element.children) {
       if (child.type === 'VElement') {
         checkChildKey(context, vFor, child)
@@ -79,8 +81,6 @@ function checkKey(context, vFor, element) {
     return
   }
 
-  const vBindKey = utils.getDirective(element, 'bind', 'key')
-
   if (utils.isCustomComponent(element) && vBindKey == null) {
     context.report({
       node: element.startTag,


Test patch:
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."
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/require-v-for-key.js b/tests/lib/rules/require-v-for-key.js
index 4b521593..f98da8bc 100644
--- a/tests/lib/rules/require-v-for-key.js
+++ b/tests/lib/rules/require-v-for-key.js
@@ -56,6 +56,58 @@ tester.run('require-v-for-key', rule, {
       filename: 'test.vue',
       code:
         '<template><div><slot v-for="x in list" :name="x"><div :key="x"></div></slot></div></template>'
+    },
+    // key on <template> : In Vue.js 3.x, you can place key on <template>.
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" v-bind:key="x"><div /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" v-bind:key="x"><MyComp /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" :key="x"><div /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" :key="x"><MyComp /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" :key="x.id"><div /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" :key="x.id"><MyComp /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="(x, i) in list" :key="i"><div /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="(x, i) in list" :key="i"><MyComp /></template></div></template>'
+    },
+    // key on <slot> : In Vue.js 3.x, you can place key on <slot>.
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><slot v-for="x in list" :key="x"><div /></slot></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><slot v-for="x in list" :key="x"><MyComp /></slot></div></template>'
     }
   ],
   invalid: [
diff --git a/tests/lib/rules/valid-v-for.js b/tests/lib/rules/valid-v-for.js
index 94fa4346..e42fa4f4 100644
--- a/tests/lib/rules/valid-v-for.js
+++ b/tests/lib/rules/valid-v-for.js
@@ -128,6 +128,63 @@ tester.run('valid-v-for', rule, {
         </template>
       `
     },
+    // key on <template> : In Vue.js 3.x, you can place key on <template>.
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" v-bind:key="x"><div /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" v-bind:key="x"><MyComp /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" :key="x"><div /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" :key="x"><MyComp /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" :key="x.id"><div /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" :key="x.id"><MyComp /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="(x, i) in list" :key="i"><div /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="(x, i) in list" :key="i"><MyComp /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" :key="x"><custom-component></custom-component></template></div></template>'
+    },
+    // key on <slot> : In Vue.js 3.x, you can place key on <slot>.
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><slot v-for="x in list" :key="x"><div /></slot></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><slot v-for="x in list" :key="x"><MyComp /></slot></div></template>'
+    },
     // parsing error
     {
       filename: 'parsing-error.vue',
@@ -254,12 +311,6 @@ tester.run('valid-v-for', rule, {
         "Expected 'v-bind:key' directive to use the variables which are defined by the 'v-for' directive."
       ]
     },
-    {
-      filename: 'test.vue',
-      code:
-        '<template><div><template v-for="x in list" :key="x"><custom-component></custom-component></template></div></template>',
-      errors: ["Custom elements in iteration require 'v-bind:key' directives."]
-    },
     {
       filename: 'test.vue',
       code: