Repository: vuejs/eslint-plugin-vue
Pull request #2529 (2024-08-15 14:24:19)

fix(no-deprecated-slot-attribute): handle v-for with dynamic slot
Fixes an edge-case where the slot is dynamic derived from a `v-for`.

Previously, it would move the `slot` directive in a new `<template>` above the `v-for`, which broke it.

Now, it also moves the `v-for` to the new `<template>`.


Patch:
diff --git a/docs/rules/define-macros-order.md b/docs/rules/define-macros-order.md
index 5d8dda56..6dbe5ef0 100644
--- a/docs/rules/define-macros-order.md
+++ b/docs/rules/define-macros-order.md
@@ -2,20 +2,20 @@
 pageClass: rule-details
 sidebarDepth: 0
 title: vue/define-macros-order
-description: enforce order of `defineEmits` and `defineProps` compiler macros
+description: enforce order of compiler macros (`defineProps`, `defineEmits`, etc.)
 since: v8.7.0
 ---
 
 # vue/define-macros-order
 
-> enforce order of `defineEmits` and `defineProps` compiler macros
+> enforce order of compiler macros (`defineProps`, `defineEmits`, etc.)
 
 - :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
 - :bulb: Some problems reported by this rule are manually fixable by editor [suggestions](https://eslint.org/docs/developer-guide/working-with-rules#providing-suggestions).
 
 ## :book: Rule Details
 
-This rule reports the `defineProps` and `defineEmits` compiler macros when they are not the first statements in `<script setup>` (after any potential import statements or type definitions) or when they are not in the correct order.
+This rule reports compiler macros (like `defineProps` or `defineEmits` but also custom ones) when they are not the first statements in `<script setup>` (after any potential import statements or type definitions) or when they are not in the correct order.
 
 ## :wrench: Options
 
@@ -28,7 +28,7 @@ This rule reports the `defineProps` and `defineEmits` compiler macros when they
 }
 ```
 
-- `order` (`string[]`) ... The order of defineEmits and defineProps macros. You can also add `"defineOptions"`, `"defineSlots"`, and `"defineModel"`.
+- `order` (`string[]`) ... The order in which the macros should appear. The default is `["defineProps", "defineEmits"]`.
 - `defineExposeLast` (`boolean`) ... Force `defineExpose` at the end.
 
 ### `{ "order": ["defineProps", "defineEmits"] }` (default)
@@ -118,6 +118,39 @@ const slots = defineSlots()
 
 </eslint-code-block>
 
+### `{ "order": ["definePage", "defineModel", "defineCustom", "defineEmits", "defineSlots"] }`
+
+<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {order: ['definePage', 'defineModel', 'defineCustom', 'defineEmits', 'defineSlots']}]}">
+
+```vue
+<!-- ✓ GOOD -->
+<script setup>
+definePage()
+const model = defineModel()
+defineCustom()
+defineEmits(/* ... */)
+const slots = defineSlots()
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {order: ['definePage', 'defineModel', 'defineCustom', 'defineEmits', 'defineSlots']}]}">
+
+```vue
+<!-- ✗ BAD -->
+<script setup>
+defineEmits(/* ... */)
+const slots = defineSlots()
+defineProps(/* ... */)
+defineCustom({/* ... */})
+const model = defineModel()
+definePage()
+</script>
+```
+
+</eslint-code-block>
+
 ### `{ "defineExposeLast": true }`
 
 <eslint-code-block fix :rules="{'vue/define-macros-order': ['error', {defineExposeLast: true}]}">
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 113ff416..9624111c 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -213,7 +213,7 @@ For example:
 | [vue/component-options-name-casing](./component-options-name-casing.md) | enforce the casing of component name in `components` options | :wrench::bulb: | :hammer: |
 | [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name |  | :hammer: |
 | [vue/define-emits-declaration](./define-emits-declaration.md) | enforce declaration style of `defineEmits` |  | :hammer: |
-| [vue/define-macros-order](./define-macros-order.md) | enforce order of `defineEmits` and `defineProps` compiler macros | :wrench::bulb: | :lipstick: |
+| [vue/define-macros-order](./define-macros-order.md) | enforce order of compiler macros (`defineProps`, `defineEmits`, etc.) | :wrench::bulb: | :lipstick: |
 | [vue/define-props-declaration](./define-props-declaration.md) | enforce declaration style of `defineProps` |  | :hammer: |
 | [vue/enforce-style-attribute](./enforce-style-attribute.md) | enforce or forbid the use of the `scoped` and `module` attributes in SFC top level style tags |  | :hammer: |
 | [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute |  | :hammer: |
@@ -224,6 +224,7 @@ For example:
 | [vue/match-component-import-name](./match-component-import-name.md) | require the registered component name to match the imported component name |  | :warning: |
 | [vue/max-lines-per-block](./max-lines-per-block.md) | enforce maximum number of lines in Vue SFC blocks |  | :warning: |
 | [vue/max-props](./max-props.md) | enforce maximum number of props in Vue component |  | :warning: |
+| [vue/max-template-depth](./max-template-depth.md) | enforce maximum depth of template |  | :warning: |
 | [vue/new-line-between-multi-line-property](./new-line-between-multi-line-property.md) | enforce new lines between multi-line properties in Vue components | :wrench: | :lipstick: |
 | [vue/next-tick-style](./next-tick-style.md) | enforce Promise or callback style in `nextTick` | :wrench: | :hammer: |
 | [vue/no-bare-strings-in-template](./no-bare-strings-in-template.md) | disallow the use of bare strings in `<template>` |  | :hammer: |
diff --git a/docs/rules/max-template-depth.md b/docs/rules/max-template-depth.md
new file mode 100644
index 00000000..94bd8b5e
--- /dev/null
+++ b/docs/rules/max-template-depth.md
@@ -0,0 +1,67 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/max-template-depth
+description: enforce maximum depth of template
+---
+
+# vue/max-template-depth
+
+> enforce maximum depth of template
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> _**This rule has not been released yet.**_ </badge>
+
+## :book: Rule Details
+
+This rule enforces a maximum depth of the template in a Vue SFC, in order to aid in maintainability and reduce complexity.
+
+## :wrench: Options
+
+This rule takes an object, where you can specify the maximum depth allowed in a Vue SFC template block.
+There is one property that can be specified for the object.
+
+- `maxDepth` ... Specify the maximum template depth `template` block.
+
+### `{ maxDepth: 3 }`
+
+<eslint-code-block :rules="{'vue/max-template-depth': ['error', { maxDepth: 3 }]}">
+
+```vue
+<!-- ✗ BAD -->
+<template>
+  <main-container>
+    <child-component>
+      <div />
+    </child-component>
+    <child-component>
+      <nested-component>
+        <div>
+          <div />
+        </div>
+      </nested-component>
+    </child-component>
+  </main-container>
+</template>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/max-template-depth': ['error', { maxDepth: 3 }]}">
+
+```vue
+<!-- ✓ GOOD -->
+<template>
+  <main-container>
+    <child-component>
+      <div />
+    </child-component>
+  </main-container>
+</template>
+```
+
+</eslint-code-block>
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/max-template-depth.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/max-template-depth.js)
diff --git a/lib/index.js b/lib/index.js
index ff3f69c6..6dbe5107 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -80,6 +80,7 @@ const plugin = {
     'max-len': require('./rules/max-len'),
     'max-lines-per-block': require('./rules/max-lines-per-block'),
     'max-props': require('./rules/max-props'),
+    'max-template-depth': require('./rules/max-template-depth'),
     'multi-word-component-names': require('./rules/multi-word-component-names'),
     'multiline-html-element-content-newline': require('./rules/multiline-html-element-content-newline'),
     'multiline-ternary': require('./rules/multiline-ternary'),
diff --git a/lib/rules/attribute-hyphenation.js b/lib/rules/attribute-hyphenation.js
index 0ef2a042..54ac7779 100644
--- a/lib/rules/attribute-hyphenation.js
+++ b/lib/rules/attribute-hyphenation.js
@@ -101,6 +101,10 @@ module.exports = {
             return null
           }
 
+          if (text.endsWith('.sync')) {
+            return null
+          }
+
           if (/^[A-Z]/.test(name)) {
             return null
           }
diff --git a/lib/rules/define-macros-order.js b/lib/rules/define-macros-order.js
index f24c58af..920b5c29 100644
--- a/lib/rules/define-macros-order.js
+++ b/lib/rules/define-macros-order.js
@@ -11,13 +11,15 @@ const MACROS_PROPS = 'defineProps'
 const MACROS_OPTIONS = 'defineOptions'
 const MACROS_SLOTS = 'defineSlots'
 const MACROS_MODEL = 'defineModel'
-const ORDER_SCHEMA = [
+const MACROS_EXPOSE = 'defineExpose'
+const KNOWN_MACROS = new Set([
   MACROS_EMITS,
   MACROS_PROPS,
   MACROS_OPTIONS,
   MACROS_SLOTS,
-  MACROS_MODEL
-]
+  MACROS_MODEL,
+  MACROS_EXPOSE
+])
 const DEFAULT_ORDER = [MACROS_PROPS, MACROS_EMITS]
 
 /**
@@ -109,6 +111,12 @@ function create(context) {
   /** @type {ASTNode} */
   let defineExposeNode
 
+  if (order.includes(MACROS_EXPOSE) && defineExposeLast) {
+    throw new Error(
+      "`defineExpose` macro can't be in the `order` array if `defineExposeLast` is true."
+    )
+  }
+
   return utils.compositingVisitors(
     utils.defineScriptSetupVisitor(context, {
       onDefinePropsExit(node) {
@@ -130,6 +138,20 @@ function create(context) {
       },
       onDefineExposeExit(node) {
         defineExposeNode = getDefineMacrosStatement(node)
+      },
+
+      /** @param {CallExpression} node */
+      'Program > ExpressionStatement > CallExpression, Program > VariableDeclaration > VariableDeclarator > CallExpression'(
+        node
+      ) {
+        if (
+          node.callee &&
+          node.callee.type === 'Identifier' &&
+          order.includes(node.callee.name) &&
+          !KNOWN_MACROS.has(node.callee.name)
+        ) {
+          macrosNodes.set(node.callee.name, [getDefineMacrosStatement(node)])
+        }
       }
     }),
     {
@@ -333,7 +355,7 @@ module.exports = {
     type: 'layout',
     docs: {
       description:
-        'enforce order of `defineEmits` and `defineProps` compiler macros',
+        'enforce order of compiler macros (`defineProps`, `defineEmits`, etc.)',
       categories: undefined,
       url: 'https://eslint.vuejs.org/rules/define-macros-order.html'
     },
@@ -346,7 +368,8 @@ module.exports = {
           order: {
             type: 'array',
             items: {
-              enum: ORDER_SCHEMA
+              type: 'string',
+              minLength: 1
             },
             uniqueItems: true,
             additionalItems: false
diff --git a/lib/rules/max-props.js b/lib/rules/max-props.js
index a7f44b8e..698757e7 100644
--- a/lib/rules/max-props.js
+++ b/lib/rules/max-props.js
@@ -43,7 +43,7 @@ module.exports = {
     function checkMaxNumberOfProps(props) {
       if (props.length > option.maxProps && props[0].node) {
         context.report({
-          node: props[0].node,
+          node: props[0].node.parent,
           messageId: 'tooManyProps',
           data: {
             propCount: props.length,
diff --git a/lib/rules/max-template-depth.js b/lib/rules/max-template-depth.js
new file mode 100644
index 00000000..899ec75c
--- /dev/null
+++ b/lib/rules/max-template-depth.js
@@ -0,0 +1,82 @@
+/**
+ * @author kevsommer Kevin Sommer
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'enforce maximum depth of template',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/max-template-depth.html'
+    },
+    fixable: null,
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          maxDepth: {
+            type: 'integer',
+            minimum: 1
+          }
+        },
+        additionalProperties: false,
+        minProperties: 1
+      }
+    ],
+    messages: {
+      templateTooDeep:
+        'Element is nested too deeply (depth of {{depth}}, maximum allowed is {{limit}}).'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    const option = context.options[0] || {}
+
+    /**
+     * @param {VElement} element
+     * @param {number} curDepth
+     */
+    function checkMaxDepth(element, curDepth) {
+      if (curDepth > option.maxDepth) {
+        context.report({
+          node: element,
+          messageId: 'templateTooDeep',
+          data: {
+            depth: curDepth,
+            limit: option.maxDepth
+          }
+        })
+      }
+
+      if (!element.children) {
+        return
+      }
+
+      for (const child of element.children) {
+        if (child.type === 'VElement') {
+          checkMaxDepth(child, curDepth + 1)
+        }
+      }
+    }
+
+    return {
+      /** @param {Program} program */
+      Program(program) {
+        const element = program.templateBody
+
+        if (element == null) {
+          return
+        }
+
+        if (element.type !== 'VElement') {
+          return
+        }
+
+        checkMaxDepth(element, 0)
+      }
+    }
+  }
+}
diff --git a/lib/rules/syntaxes/slot-attribute.js b/lib/rules/syntaxes/slot-attribute.js
index 61891df3..27087cb3 100644
--- a/lib/rules/syntaxes/slot-attribute.js
+++ b/lib/rules/syntaxes/slot-attribute.js
@@ -56,10 +56,8 @@ module.exports = {
         // parse error or empty expression
         return false
       }
-      const slotName = sourceCode.getText(slotAttr.value.expression).trim()
-      // If non-Latin characters are included it can not be converted.
-      // It does not check the space only because `a>b?c:d` should be rejected.
-      return !/[^a-z]/i.test(slotName)
+
+      return slotAttr.value.expression.type === 'Identifier'
     }
 
     /**
@@ -102,7 +100,18 @@ module.exports = {
           yield fixer.remove(scopeAttr)
         }
 
-        yield fixer.insertTextBefore(element, `<template ${replaceText}>\n`)
+        const vFor = startTag.attributes.find(
+          (attr) => attr.directive && attr.key.name.name === 'for'
+        )
+        const vForText = vFor ? `${sourceCode.getText(vFor)} ` : ''
+        if (vFor) {
+          yield fixer.remove(vFor)
+        }
+
+        yield fixer.insertTextBefore(
+          element,
+          `<template ${vForText}${replaceText}>\n`
+        )
         yield fixer.insertTextAfter(element, `\n</template>`)
       }
     }


Test patch:
diff --git a/tests/lib/rules/attribute-hyphenation.js b/tests/lib/rules/attribute-hyphenation.js
index 4d867d15..16f6e4e1 100644
--- a/tests/lib/rules/attribute-hyphenation.js
+++ b/tests/lib/rules/attribute-hyphenation.js
@@ -70,6 +70,16 @@ ruleTester.run('attribute-hyphenation', rule, {
       filename: 'test.vue',
       code: '<template><div><custom :attr_ff="prop"></custom></div></template>',
       options: ['never']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom :my-name.sync="prop"></custom></div></template>',
+      options: ['always']
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom :myName.sync="prop"></custom></div></template>',
+      options: ['never']
     }
   ],
 
@@ -367,6 +377,32 @@ ruleTester.run('attribute-hyphenation', rule, {
           line: 1
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom :myAge.sync="prop"></custom></div></template>',
+      output: null,
+      options: ['always'],
+      errors: [
+        {
+          message: "Attribute ':myAge.sync' must be hyphenated.",
+          type: 'VDirectiveKey',
+          line: 1
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: '<template><div><custom :my-age.sync="prop"></custom></div></template>',
+      output: null,
+      options: ['never'],
+      errors: [
+        {
+          message: "Attribute ':my-age.sync' can't be hyphenated.",
+          type: 'VDirectiveKey',
+          line: 1
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/define-macros-order.js b/tests/lib/rules/define-macros-order.js
index 3aa2e434..72b35986 100644
--- a/tests/lib/rules/define-macros-order.js
+++ b/tests/lib/rules/define-macros-order.js
@@ -195,6 +195,65 @@ tester.run('define-macros-order', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          import Foo from 'foo'
+          /** Page */
+          definePage()
+          /** model */
+          const model = defineModel()
+          /** emits */
+          defineEmits(['update:foo'])
+        </script>
+      `,
+      options: [
+        {
+          order: ['definePage', 'defineModel', 'defineEmits']
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          import Foo from 'foo'
+          /** model */
+          const model = defineModel()
+          /** emits */
+          defineEmits(['update:foo'])
+
+          function fn() {
+            definePage()
+          }
+        </script>
+      `,
+      options: [
+        {
+          order: ['definePage', 'defineModel', 'defineEmits']
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          import Foo from 'foo'
+          /** model */
+          const model = defineModel()
+          /** emits */
+          defineEmits(['update:foo'])
+
+          const val = () => definePage()
+        </script>
+      `,
+      options: [
+        {
+          order: ['definePage', 'defineModel', 'defineEmits']
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -254,6 +313,22 @@ tester.run('define-macros-order', rule, {
           order: ['defineModel', 'defineSlots']
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+        const page = definePage()
+
+        const first = defineModel('first')
+        const second = defineModel('second')
+        </script>
+      `,
+      options: [
+        {
+          order: ['definePage', 'defineModel']
+        }
+      ]
     }
   ],
   invalid: [
@@ -382,6 +457,40 @@ tester.run('define-macros-order', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          console.log('test1')
+          const props = defineProps({
+            test: Boolean
+          })
+          console.log('test2')
+          const page = definePage({
+            name: 'hello'
+          })
+        </script>
+      `,
+      output: `
+        <script setup>
+          const page = definePage({
+            name: 'hello'
+          })
+          const props = defineProps({
+            test: Boolean
+          })
+          console.log('test1')
+          console.log('test2')
+        </script>
+      `,
+      options: [{ order: ['definePage', 'defineProps'] }],
+      errors: [
+        {
+          message: message('definePage'),
+          line: 8
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -425,6 +534,61 @@ tester.run('define-macros-order', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <script lang="ts" setup>
+          interface Props {
+            msg?: string
+            labels?: string[]
+          }
+
+          const props = defineProps<{
+            msg?: string
+            labels?: string[]
+          }>()
+          defineCustom()
+          const emit = defineEmits<{(e: 'update:test'): void}>()
+
+          const page = definePage({
+            name: 'hello'
+          })
+        </script>
+      `,
+      output: `
+        <script lang="ts" setup>
+          interface Props {
+            msg?: string
+            labels?: string[]
+          }
+
+          const page = definePage({
+            name: 'hello'
+          })
+          defineCustom()
+          const props = defineProps<{
+            msg?: string
+            labels?: string[]
+          }>()
+          const emit = defineEmits<{(e: 'update:test'): void}>()
+
+        </script>
+      `,
+      options: [
+        { order: ['definePage', 'defineCustom', 'defineProps', 'defineEmits'] }
+      ],
+      languageOptions: {
+        parserOptions: {
+          parser: require.resolve('@typescript-eslint/parser')
+        }
+      },
+      errors: [
+        {
+          message: message('definePage'),
+          line: 15
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -537,6 +701,25 @@ tester.run('define-macros-order', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          const props = defineProps({ test: Boolean });definePage({name: 'hello'})
+        </script>
+      `,
+      output: `
+        <script setup>
+          definePage({name: 'hello'});const props = defineProps({ test: Boolean });        </script>
+      `,
+      options: [{ order: ['definePage', 'defineProps'] }],
+      errors: [
+        {
+          message: message('definePage'),
+          line: 3
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -633,6 +816,54 @@ tester.run('define-macros-order', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          definePage({name: 'hello'})
+          console.log('test1')
+          defineCustom({ test: Boolean })
+        </script>
+      `,
+      output: `
+        <script setup>
+          defineCustom({ test: Boolean })
+          definePage({name: 'hello'})
+          console.log('test1')
+        </script>
+      `,
+      options: [{ order: ['defineCustom', 'definePage'] }],
+      errors: [
+        {
+          message: message('defineCustom'),
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          definePage({name: 'hello'})
+          console.log('test1')
+          const custom = defineCustom({ test: Boolean })
+        </script>
+      `,
+      output: `
+        <script setup>
+          const custom = defineCustom({ test: Boolean })
+          definePage({name: 'hello'})
+          console.log('test1')
+        </script>
+      `,
+      options: [{ order: ['defineCustom', 'definePage'] }],
+      errors: [
+        {
+          message: message('defineCustom'),
+          line: 5
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
@@ -895,6 +1126,44 @@ tester.run('define-macros-order', rule, {
         }
       ]
     },
+    {
+      filename: 'test.vue',
+      code: `
+        <script setup>
+          defineOptions({})
+          defineCustom('second')
+          const something = defineSomething('first')
+          definePage()
+          const model = defineModel('second')
+        </script>
+      `,
+      output: `
+        <script setup>
+          const something = defineSomething('first')
+          defineCustom('second')
+          const model = defineModel('second')
+          defineOptions({})
+          definePage()
+        </script>
+      `,
+      options: [
+        {
+          order: [
+            'defineSomething',
+            'defineCustom',
+            'defineModel',
+            'defineOptions',
+            'definePage'
+          ]
+        }
+      ],
+      errors: [
+        {
+          message: message('defineSomething'),
+          line: 5
+        }
+      ]
+    },
     {
       filename: 'test.vue',
       code: `
diff --git a/tests/lib/rules/max-props.js b/tests/lib/rules/max-props.js
index 1dd42a03..1ebfb9cf 100644
--- a/tests/lib/rules/max-props.js
+++ b/tests/lib/rules/max-props.js
@@ -113,7 +113,8 @@ tester.run('max-props', rule, {
       errors: [
         {
           message: 'Component has too many props (2). Maximum allowed is 1.',
-          line: 3
+          line: 3,
+          endLine: 3
         }
       ]
     },
@@ -133,7 +134,8 @@ tester.run('max-props', rule, {
       errors: [
         {
           message: 'Component has too many props (2). Maximum allowed is 1.',
-          line: 5
+          line: 4,
+          endLine: 7
         }
       ]
     },
@@ -154,7 +156,8 @@ tester.run('max-props', rule, {
       errors: [
         {
           message: 'Component has too many props (3). Maximum allowed is 2.',
-          line: 3
+          line: 3,
+          endLine: 3
         }
       ]
     }
diff --git a/tests/lib/rules/max-template-depth.js b/tests/lib/rules/max-template-depth.js
new file mode 100644
index 00000000..4ef9e95d
--- /dev/null
+++ b/tests/lib/rules/max-template-depth.js
@@ -0,0 +1,168 @@
+/**
+ * @author kevsommer Kevin Sommer
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('../../eslint-compat').RuleTester
+const rule = require('../../../lib/rules/max-template-depth')
+
+const tester = new RuleTester({
+  languageOptions: {
+    parser: require('vue-eslint-parser'),
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('max-template-depth', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+      <main-container>
+        <child-component>
+          <div />
+        </child-component>
+      </main-container>
+      </template>
+      `,
+      options: [{ maxDepth: 3 }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+      <main-container>
+        <ul>
+          <li>Item 1</li>
+        </ul>
+      </main-container>
+      </template>
+      `,
+      options: [{ maxDepth: 4 }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+      <main-container>
+        <child-component>
+          <sub-child-component>
+            <div />
+          </sub-child-component>
+        </child-component>
+      </main-container>
+      </template>
+      `,
+      options: [{ maxDepth: 4 }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+      <div>
+      </div>
+      <main-container>
+        <child-component>
+          <ul>
+            <li />
+          </ul>
+        </child-component>
+      </main-container>
+      </template>
+      `,
+      options: [{ maxDepth: 4 }]
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+      <div>
+        <div>
+          <div>
+            <div />
+          </div>
+        </div>
+      </div>
+      </template>
+      `,
+      options: [{ maxDepth: 3 }],
+      errors: [
+        {
+          message:
+            'Element is nested too deeply (depth of 4, maximum allowed is 3).',
+          line: 6
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+      <side-container>
+        <child-component />
+      </side-container>
+      <main-container>
+        <child-component>
+          <nested-component>
+            <h1 />
+          </nested-component>
+        </child-component>
+      </main-container>
+      </template>
+      `,
+      options: [{ maxDepth: 3 }],
+      errors: [
+        {
+          message:
+            'Element is nested too deeply (depth of 4, maximum allowed is 3).',
+          line: 9
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <template>
+      <main-container>
+        <nav-bar>
+          <div />
+        </nav-bar>
+        <child-component>
+          <nested-component>
+            <div>
+              <div />
+            <div>
+          </nested-component>
+        </child-component>
+      </main-container>
+      </template>
+      `,
+      options: [{ maxDepth: 3 }],
+      errors: [
+        {
+          message:
+            'Element is nested too deeply (depth of 4, maximum allowed is 3).',
+          line: 9,
+          endLine: 12
+        },
+        {
+          message:
+            'Element is nested too deeply (depth of 5, maximum allowed is 3).',
+          line: 10,
+          endLine: 10
+        },
+        {
+          message:
+            'Element is nested too deeply (depth of 5, maximum allowed is 3).',
+          line: 11,
+          endLine: 12
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/no-deprecated-slot-attribute.js b/tests/lib/rules/no-deprecated-slot-attribute.js
index 51d10935..710472bd 100644
--- a/tests/lib/rules/no-deprecated-slot-attribute.js
+++ b/tests/lib/rules/no-deprecated-slot-attribute.js
@@ -643,6 +643,31 @@ tester.run('no-deprecated-slot-attribute', rule, {
         }
       ],
       errors: ['`slot` attributes are deprecated.']
+    },
+    {
+      code: `
+      <template>
+        <my-component>
+          <slot
+            v-for="slot in Object.keys($slots)"
+            :slot="slot"
+            :name="slot"
+          ></slot>
+        </my-component>
+      </template>`,
+      output: `
+      <template>
+        <my-component>
+          <template v-for="slot in Object.keys($slots)" v-slot:[slot]>
+<slot
+            
+            
+            :name="slot"
+          ></slot>
+</template>
+        </my-component>
+      </template>`,
+      errors: ['`slot` attributes are deprecated.']
     }
   ]
 })