Repository: vuejs/eslint-plugin-vue (js)

Issue:
vue/no-template-key, vue/valid-v-for need to be updated for vue-next 3.0.0-rc.6
Due to a vue compiler error, I was made aware that in Vue 3, :key can/should be placed directly on <template> instead of the direct child element. I'm not sure when this changed, but a recent vue-next commit that is now part of 3.0.0-rc.6 now throws the error when compiling.

Re: https://github.com/vuejs/vue-next/commit/b0d01e9db9ffe5781cce5a2d62c8552db3d615b0

**Checklist**

- [x] I checked the [FAQ](https://eslint.vuejs.org/user-guide/#faq).

**Tell us about your environment**

- **ESLint version: 7.7.0** 
- **eslint-plugin-vue version: 7.0.0-beta.2** 
- **Node version: 14.8.0** 
- **Operating System: Ubuntu 20.04.1 LTS** 

**What did you do?**	
```vue
<template v-for="val in tree" :key="val.refId">
   <component :is="MyComponent" :item="val" />
</template>
```

**What did you expect to happen?**
No ESLint rule errors.

**What actually happened?**
```
error: '<template>' cannot be keyed. Place the key on real elements instead (vue/no-template-key) at src/ContentFrame/ContentFrame.vue:36:5:
  35 |   <div ref="root" class="ContentFrame">
> 36 |     <template v-for="val in tree" :key="val.refId">
     |     ^
  37 |       <component :is="MyComponent" :item="val"

error: Custom elements in iteration require 'v-bind:key' directives (vue/valid-v-for) at src/ContentFrame/ContentFrame.vue:37:7:
  36 |     <template v-for="val in tree" :key="val.refId">
> 37 |       <component :is="MyComponent" :item="val"
     |       ^
```

Pull request:
Add `vue/no-v-for-template-key-on-child` rule
This PR adds `vue/no-v-for-template-key-on-child` rule that reports the key of the `<template v-for>` placed on the child elements.

close #1279

Patch:
diff --git a/docs/rules/README.md b/docs/rules/README.md
index dfb0a2a4..9798e777 100644
--- a/docs/rules/README.md
+++ b/docs/rules/README.md
@@ -73,6 +73,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-on-child](./no-v-for-template-key-on-child.md) | disallow key of `<template v-for>` placed on child elements |  |
 | [vue/no-watch-after-await](./no-watch-after-await.md) | disallow asynchronously registered `watch` |  |
 | [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-v-for-template-key-on-child.md b/docs/rules/no-v-for-template-key-on-child.md
new file mode 100644
index 00000000..982ddd5e
--- /dev/null
+++ b/docs/rules/no-v-for-template-key-on-child.md
@@ -0,0 +1,54 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-v-for-template-key-on-child
+description: disallow key of `<template v-for>` placed on child elements
+---
+# vue/no-v-for-template-key-on-child
+> disallow key of `<template v-for>` placed on child elements
+
+- :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 the key of the `<template v-for>` placed on the child elements.
+
+In Vue.js 3.x, with the support for fragments, the `<template v-for>` key can be placed on the `<template>` tag.  
+
+::: warning Note
+Do not use with the [vue/no-v-for-template-key] rule for Vue.js 2.x.  
+This rule conflicts with the [vue/no-v-for-template-key] rule.
+:::
+
+<eslint-code-block :rules="{'vue/no-v-for-template-key-on-child': ['error']}">
+
+```vue
+<template>
+  <!-- ✓ GOOD -->
+  <template v-for="todo in todos" :key="todo">
+    <Foo />
+  </template>
+
+  <!-- ✗ BAD -->
+  <template v-for="todo in todos">
+    <Foo :key="todo" />
+  </template>
+</template>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+Nothing.
+
+## :couple: Related Rules
+
+- [vue/no-v-for-template-key]
+
+[vue/no-v-for-template-key]: ./no-v-for-template-key.md
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-v-for-template-key-on-child.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-v-for-template-key-on-child.js)
diff --git a/docs/rules/no-v-for-template-key.md b/docs/rules/no-v-for-template-key.md
index a39709a3..15111ce7 100644
--- a/docs/rules/no-v-for-template-key.md
+++ b/docs/rules/no-v-for-template-key.md
@@ -9,12 +9,18 @@ description: 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.
 
+In Vue.js 2.x, disallows `key` attribute on `<template>` elements.
+
+::: warning Note
+Do not use with the [vue/no-v-for-template-key-on-child] rule for Vue.js 3.x.  
+This rule conflicts with the [vue/no-v-for-template-key-on-child] rule.
+:::
+
 <eslint-code-block :rules="{'vue/no-v-for-template-key': ['error']}">
 
 ```vue
@@ -43,9 +49,11 @@ Nothing.
 
 ## :couple: Related Rules
 
-- [vue/no-template-key](./no-template-key.md)
+- [vue/no-template-key]
+- [vue/no-v-for-template-key-on-child]
 
 [vue/no-template-key]: ./no-template-key.md
+[vue/no-v-for-template-key-on-child]: ./no-v-for-template-key-on-child.md
 
 ## :books: Further Reading
 
diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js
index 8b2d6a13..7d715c18 100644
--- a/lib/configs/vue3-essential.js
+++ b/lib/configs/vue3-essential.js
@@ -41,6 +41,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-on-child': 'error',
     'vue/no-watch-after-await': 'error',
     'vue/require-component-is': 'error',
     'vue/require-prop-type-constructor': 'error',
diff --git a/lib/index.js b/lib/index.js
index d6c82ea2..556fadeb 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-on-child': require('./rules/no-v-for-template-key-on-child'),
     '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'),
diff --git a/lib/rules/no-v-for-template-key-on-child.js b/lib/rules/no-v-for-template-key-on-child.js
new file mode 100644
index 00000000..79ee1d97
--- /dev/null
+++ b/lib/rules/no-v-for-template-key-on-child.js
@@ -0,0 +1,95 @@
+/**
+ * @author Yosuke Ota
+ * This rule is based on X_V_FOR_TEMPLATE_KEY_PLACEMENT error of Vue 3.
+ * see https://github.com/vuejs/vue-next/blob/b0d01e9db9ffe5781cce5a2d62c8552db3d615b0/packages/compiler-core/src/errors.ts#L70
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+// ------------------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------------------
+
+/**
+ * Check whether the given attribute is using the variables which are defined by `v-for` directives.
+ * @param {VDirective} vFor The attribute node of `v-for` to check.
+ * @param {VDirective} vBindKey The attribute node of `v-bind:key` to check.
+ * @returns {boolean} `true` if the node is using the variables which are defined by `v-for` directives.
+ */
+function isUsingIterationVar(vFor, vBindKey) {
+  if (vBindKey.value == null) {
+    return false
+  }
+  const references = vBindKey.value.references
+  const variables = vFor.parent.parent.variables
+  return references.some((reference) =>
+    variables.some(
+      (variable) =>
+        variable.id.name === reference.id.name && variable.kind === 'v-for'
+    )
+  )
+}
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description:
+        'disallow key of `<template v-for>` placed on child elements',
+      categories: ['vue3-essential'],
+      url: 'https://eslint.vuejs.org/rules/no-v-for-template-key-on-child.html'
+    },
+    fixable: null,
+    schema: [],
+    messages: {
+      vForTemplateKeyPlacement:
+        '`<template v-for>` key should be placed on the `<template>` tag.'
+    }
+  },
+  /** @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 template = node.parent.parent
+        const vBindKeyOnTemplate = utils.getDirective(template, 'bind', 'key')
+        if (
+          vBindKeyOnTemplate &&
+          isUsingIterationVar(node, vBindKeyOnTemplate)
+        ) {
+          return
+        }
+
+        for (const child of template.children.filter(utils.isVElement)) {
+          if (
+            utils.hasDirective(child, 'if') ||
+            utils.hasDirective(child, 'else-if') ||
+            utils.hasDirective(child, 'else') ||
+            utils.hasDirective(child, 'for')
+          ) {
+            continue
+          }
+          const vBindKeyOnChild = utils.getDirective(child, 'bind', 'key')
+          if (vBindKeyOnChild && isUsingIterationVar(node, vBindKeyOnChild)) {
+            context.report({
+              node: vBindKeyOnChild,
+              loc: vBindKeyOnChild.loc,
+              messageId: 'vForTemplateKeyPlacement'
+            })
+          }
+        }
+      }
+    })
+  }
+}
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-v-for-template-key-on-child.js b/tests/lib/rules/no-v-for-template-key-on-child.js
new file mode 100644
index 00000000..36254f99
--- /dev/null
+++ b/tests/lib/rules/no-v-for-template-key-on-child.js
@@ -0,0 +1,167 @@
+/**
+ * @author Yosuke Ota
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-v-for-template-key-on-child')
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: { ecmaVersion: 2015 }
+})
+
+tester.run('no-v-for-template-key-on-child', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: ''
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list"><Foo /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" :key="x"><Foo /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" :key="x.id"><Foo :key="x.id" /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="(x, i) in list" :key="i"><Foo :key="x" /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="(x, i) in list"><Foo :key="foo" /></template></div></template>'
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <template v-for="x in list">
+            <Foo v-if="a" :key="x" />
+          </template>
+        </div>
+      </template>`
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <template v-for="x in list">
+            <Foo v-if="a" :key="x.key1" />
+            <Foo v-else-if="a" :key="x.key2" />
+            <Foo v-else :key="x.key3" />
+            <Foo v-for="y in list" :key="x.key4" />
+          </template>
+        </div>
+      </template>`
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list"><Foo :key="x" /></template></div></template>',
+      errors: [
+        {
+          message:
+            '`<template v-for>` key should be placed on the `<template>` tag.',
+          column: 49
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list"><Foo :key="x.id" /></template></div></template>',
+      errors: [
+        '`<template v-for>` key should be placed on the `<template>` tag.'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" :key="foo"><Foo :key="x.id" /></template></div></template>',
+      errors: [
+        '`<template v-for>` key should be placed on the `<template>` tag.'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" :key><Foo :key="x.id" /></template></div></template>',
+      errors: [
+        '`<template v-for>` key should be placed on the `<template>` tag.'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code:
+        '<template><div><template v-for="x in list" :key><div /><Foo :key="x.id" /></template></div></template>',
+      errors: [
+        '`<template v-for>` key should be placed on the `<template>` tag.'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `<template><div><template v-for="x in list" :key><Foo :key="'foo' + x.id" /><Bar :key="'bar' + x.id" /></template></div></template>`,
+      errors: [
+        '`<template v-for>` key should be placed on the `<template>` tag.',
+        '`<template v-for>` key should be placed on the `<template>` tag.'
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+        <div>
+          <template v-for="x in list">
+            <Foo v-if="a" :key="x.key1" />
+            <Foo v-else-if="a" :key="x.key2" />
+            <Foo v-else :key="x.key3" />
+            <Foo v-for="y in list" :key="x.key4" />
+            <Foo :key="x.error1" />
+            <div :key="x.error2" />
+            <slot :key="x.error3" ></slot>
+          </template>
+        </div>
+      </template>`,
+      errors: [
+        {
+          message:
+            '`<template v-for>` key should be placed on the `<template>` tag.',
+          line: 9
+        },
+        {
+          message:
+            '`<template v-for>` key should be placed on the `<template>` tag.',
+          line: 10
+        },
+        {
+          message:
+            '`<template v-for>` key should be placed on the `<template>` tag.',
+          line: 11
+        }
+      ]
+    }
+  ]
+})
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: