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

Issue:
`require-typed-object-prop` reports on non-TypeScript `<script setup>`
<!--
  ❗Please don't ignore this template.❗

  If you ignore it, we're just going to respond asking you to fill it out, which wastes everyone's time.
  The more relevant information you can include, the faster we can find the issue and fix it without asking you for more info.
-->

<!--
  Before posting the issue, please confirm that the problem you're getting
  is not related with your code editor configuration.
  To make sure it's not, run: yarn eslint src/your-file.vue
-->

**Checklist**

- [x] I have tried restarting my IDE and the issue persists.
- [x] I have read the [FAQ](https://eslint.vuejs.org/user-guide/#faq) and my problem is not listed.
<!-- If you do not read the FAQ and open an issue that is listed in the FAQ, we may silently close the issue. -->

**Tell us about your environment**

- **ESLint version:** 9.6.0
- **eslint-plugin-vue version:** 9.27.0
- **Vue version:** 
- **Node version:** 
- **Operating System:** 

**Please show your full configuration:**
<!-- Paste content of your .eslintrc file -->
```json5

```

**What did you do?**

Sorry for not filling out everything, I think the problem is straightforward and easily reproducible: `require-typed-object-prop` does not respect the `lang` attribute on `<script setup>` and expects type annotation to be present in plain JS code.

Just insert the following in the [playground on this rule's page](https://eslint.vuejs.org/rules/require-typed-object-prop.html#vue-require-typed-object-prop):

<!-- Please include the actual source code causing the issue. -->
```vue
<script setup>
defineProps({
  prop: Object,
})
</script>
```

I've read the limitations section on the rule's page saying it is not meant to be used in JS projects, but I have some with mixed codebase (mainly as a result of TS migration or adjusting someone else's components code written in JS) and I just felt like detecting the language is something that seems possible to me.

**What did you expect to happen?**


**What actually happened?**
<!--
  Please include the actual, raw output from ESLint.
  If you are only looking at the results of your editor extension, also check the CLI results.
-->

**Repository to reproduce this issue**
<!--
  ❗ Please share a repository that can reproduce your issue.
  ❗ If you don't share it, we just label it as requesting a repository that can reproduce your issue.
  ❗ If you don't share it for a few days, we silently close this issue.
-->



Pull request description:
Exclude JavaScript components in `vue/require-typed-object-prop`
Fixes #2503.

Patch:
diff --git a/docs/rules/index.md b/docs/rules/index.md
index e718603f..1a9e461e 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -45,7 +45,7 @@ Rules in this category are enabled for all presets provided by eslint-plugin-vue
 | [vue/no-arrow-functions-in-watch](./no-arrow-functions-in-watch.md) | disallow using arrow functions to define watcher |  | :three::two::warning: |
 | [vue/no-async-in-computed-properties](./no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties |  | :three::two::warning: |
 | [vue/no-child-content](./no-child-content.md) | disallow element's child contents which would be overwritten by a directive like `v-html` or `v-text` | :bulb: | :three::two::warning: |
-| [vue/no-computed-properties-in-data](./no-computed-properties-in-data.md) | disallow accessing computed properties in `data`. |  | :three::two::warning: |
+| [vue/no-computed-properties-in-data](./no-computed-properties-in-data.md) | disallow accessing computed properties in `data` |  | :three::two::warning: |
 | [vue/no-custom-modifiers-on-v-model](./no-custom-modifiers-on-v-model.md) | disallow custom modifiers on v-model used on the component |  | :two::warning: |
 | [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: | :three::warning: |
 | [vue/no-deprecated-destroyed-lifecycle](./no-deprecated-destroyed-lifecycle.md) | disallow using deprecated `destroyed` and `beforeDestroy` lifecycle hooks (in Vue.js 3.0.0+) | :wrench: | :three::warning: |
@@ -223,6 +223,7 @@ For example:
 | [vue/match-component-file-name](./match-component-file-name.md) | require component name property to match its file name | :bulb: | :hammer: |
 | [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/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: |
@@ -263,7 +264,7 @@ For example:
 | [vue/padding-line-between-blocks](./padding-line-between-blocks.md) | require or disallow padding lines between blocks | :wrench: | :lipstick: |
 | [vue/padding-line-between-tags](./padding-line-between-tags.md) | require or disallow newlines between sibling tags in template | :wrench: | :lipstick: |
 | [vue/padding-lines-in-component-definition](./padding-lines-in-component-definition.md) | require or disallow padding lines in component definition | :wrench: | :lipstick: |
-| [vue/prefer-define-options](./prefer-define-options.md) | enforce use of `defineOptions` instead of default export. | :wrench: | :hammer: |
+| [vue/prefer-define-options](./prefer-define-options.md) | enforce use of `defineOptions` instead of default export | :wrench: | :hammer: |
 | [vue/prefer-prop-type-boolean-first](./prefer-prop-type-boolean-first.md) | enforce `Boolean` comes first in component prop types | :bulb: | :warning: |
 | [vue/prefer-separate-static-class](./prefer-separate-static-class.md) | require static class names in template to be in a separate `class` attribute | :wrench: | :hammer: |
 | [vue/prefer-true-attribute-shorthand](./prefer-true-attribute-shorthand.md) | require shorthand form attribute when `v-bind` value is `true` | :bulb: | :hammer: |
diff --git a/docs/rules/max-props.md b/docs/rules/max-props.md
new file mode 100644
index 00000000..b6979660
--- /dev/null
+++ b/docs/rules/max-props.md
@@ -0,0 +1,62 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/max-props
+description: enforce maximum number of props in Vue component
+---
+
+# vue/max-props
+
+> enforce maximum number of props in Vue component
+
+- :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 number of props 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 number of props allowed in a Vue SFC.
+There is one property that can be specified for the object.
+
+- `maxProps` ... Specify the maximum number of props in the `script` block.
+
+### `{ maxProps: 1 }`
+
+<eslint-code-block :rules="{'vue/max-props': ['error', { maxProps: 1 }]}">
+
+```vue
+<!-- ✗ BAD -->
+<template>
+</template>
+
+<script setup>
+defineProps({ 
+  prop1: String,
+  prop2: String,
+})
+</script>
+```
+
+</eslint-code-block>
+
+### `{ maxProps: 5 }`
+
+<eslint-code-block :rules="{'vue/max-props': ['error', { maxProps: 5 }]}">
+
+```vue
+<!-- ✓ GOOD -->
+<script>
+defineProps({
+  prop1: String
+})
+</script>
+```
+
+</eslint-code-block>
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/max-props.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/max-props.js)
diff --git a/docs/rules/no-computed-properties-in-data.md b/docs/rules/no-computed-properties-in-data.md
index 35bf62f1..26c49cbb 100644
--- a/docs/rules/no-computed-properties-in-data.md
+++ b/docs/rules/no-computed-properties-in-data.md
@@ -2,13 +2,13 @@
 pageClass: rule-details
 sidebarDepth: 0
 title: vue/no-computed-properties-in-data
-description: disallow accessing computed properties in `data`.
+description: disallow accessing computed properties in `data`
 since: v7.20.0
 ---
 
 # vue/no-computed-properties-in-data
 
-> disallow accessing computed properties in `data`.
+> disallow accessing computed properties in `data`
 
 - :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `*.configs["flat/essential"]`, `"plugin:vue/essential"`, `*.configs["flat/vue2-essential"]`, `"plugin:vue/vue3-strongly-recommended"`, `*.configs["flat/strongly-recommended"]`, `"plugin:vue/strongly-recommended"`, `*.configs["flat/vue2-strongly-recommended"]`, `"plugin:vue/vue3-recommended"`, `*.configs["flat/recommended"]`, `"plugin:vue/recommended"` and `*.configs["flat/vue2-recommended"]`.
 
diff --git a/docs/rules/prefer-define-options.md b/docs/rules/prefer-define-options.md
index b6320795..5653cb01 100644
--- a/docs/rules/prefer-define-options.md
+++ b/docs/rules/prefer-define-options.md
@@ -2,13 +2,13 @@
 pageClass: rule-details
 sidebarDepth: 0
 title: vue/prefer-define-options
-description: enforce use of `defineOptions` instead of default export.
+description: enforce use of `defineOptions` instead of default export
 since: v9.13.0
 ---
 
 # vue/prefer-define-options
 
-> enforce use of `defineOptions` instead of default export.
+> enforce use of `defineOptions` instead of default export
 
 - :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.
 
diff --git a/eslint.config.js b/eslint.config.js
index deff4615..5ddca617 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -139,6 +139,10 @@ module.exports = [
       'prefer-const': 2,
 
       'prettier/prettier': 'error',
+      'eslint-plugin/require-meta-docs-description': [
+        'error',
+        { pattern: '^(enforce|require|disallow).*[^.]$' }
+      ],
       'eslint-plugin/require-meta-docs-recommended': 'off', // use `categories` instead
       'eslint-plugin/require-meta-fixable': [
         'error',
diff --git a/lib/configs/flat/base.js b/lib/configs/flat/base.js
index 6f745c33..eb2393ed 100644
--- a/lib/configs/flat/base.js
+++ b/lib/configs/flat/base.js
@@ -6,7 +6,7 @@
 const globals = require('globals')
 module.exports = [
   {
-    name: 'vue:base:setup',
+    name: 'vue/base/setup',
     plugins: {
       get vue() {
         return require('../../index')
@@ -18,7 +18,7 @@ module.exports = [
     }
   },
   {
-    name: 'vue:base:setup-for-vue',
+    name: 'vue/base/setup-for-vue',
     files: ['*.vue', '**/*.vue'],
     plugins: {
       get vue() {
diff --git a/lib/configs/flat/vue2-essential.js b/lib/configs/flat/vue2-essential.js
index 1de89dbc..0d092041 100644
--- a/lib/configs/flat/vue2-essential.js
+++ b/lib/configs/flat/vue2-essential.js
@@ -9,7 +9,7 @@ const config = require('./base.js')
 module.exports = [
   ...config,
   {
-    name: 'vue:vue2-essential:rules',
+    name: 'vue/vue2-essential/rules',
     rules: {
       'vue/multi-word-component-names': 'error',
       'vue/no-arrow-functions-in-watch': 'error',
diff --git a/lib/configs/flat/vue2-recommended.js b/lib/configs/flat/vue2-recommended.js
index 6ccde545..0d98a4dd 100644
--- a/lib/configs/flat/vue2-recommended.js
+++ b/lib/configs/flat/vue2-recommended.js
@@ -9,7 +9,7 @@ const config = require('./vue2-strongly-recommended.js')
 module.exports = [
   ...config,
   {
-    name: 'vue:vue2-recommended:rules',
+    name: 'vue/vue2-recommended/rules',
     rules: {
       'vue/attributes-order': 'warn',
       'vue/component-tags-order': 'warn',
diff --git a/lib/configs/flat/vue2-strongly-recommended.js b/lib/configs/flat/vue2-strongly-recommended.js
index dc45fea3..e4913f40 100644
--- a/lib/configs/flat/vue2-strongly-recommended.js
+++ b/lib/configs/flat/vue2-strongly-recommended.js
@@ -9,7 +9,7 @@ const config = require('./vue2-essential.js')
 module.exports = [
   ...config,
   {
-    name: 'vue:vue2-strongly-recommended:rules',
+    name: 'vue/vue2-strongly-recommended/rules',
     rules: {
       'vue/attribute-hyphenation': 'warn',
       'vue/component-definition-name-casing': 'warn',
diff --git a/lib/configs/flat/vue3-essential.js b/lib/configs/flat/vue3-essential.js
index 595af5f1..18d8c857 100644
--- a/lib/configs/flat/vue3-essential.js
+++ b/lib/configs/flat/vue3-essential.js
@@ -9,7 +9,7 @@ const config = require('./base.js')
 module.exports = [
   ...config,
   {
-    name: 'vue:essential:rules',
+    name: 'vue/essential/rules',
     rules: {
       'vue/multi-word-component-names': 'error',
       'vue/no-arrow-functions-in-watch': 'error',
diff --git a/lib/configs/flat/vue3-recommended.js b/lib/configs/flat/vue3-recommended.js
index 8ab8aa46..523718f3 100644
--- a/lib/configs/flat/vue3-recommended.js
+++ b/lib/configs/flat/vue3-recommended.js
@@ -9,7 +9,7 @@ const config = require('./vue3-strongly-recommended.js')
 module.exports = [
   ...config,
   {
-    name: 'vue:recommended:rules',
+    name: 'vue/recommended/rules',
     rules: {
       'vue/attributes-order': 'warn',
       'vue/component-tags-order': 'warn',
diff --git a/lib/configs/flat/vue3-strongly-recommended.js b/lib/configs/flat/vue3-strongly-recommended.js
index 51134ebc..fee1d3bb 100644
--- a/lib/configs/flat/vue3-strongly-recommended.js
+++ b/lib/configs/flat/vue3-strongly-recommended.js
@@ -9,7 +9,7 @@ const config = require('./vue3-essential.js')
 module.exports = [
   ...config,
   {
-    name: 'vue:strongly-recommended:rules',
+    name: 'vue/strongly-recommended/rules',
     rules: {
       'vue/attribute-hyphenation': 'warn',
       'vue/component-definition-name-casing': 'warn',
diff --git a/lib/index.js b/lib/index.js
index 956eeaa4..1bfacebe 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -79,6 +79,7 @@ const plugin = {
     'max-attributes-per-line': require('./rules/max-attributes-per-line'),
     'max-len': require('./rules/max-len'),
     'max-lines-per-block': require('./rules/max-lines-per-block'),
+    'max-props': require('./rules/max-props'),
     '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/max-props.js b/lib/rules/max-props.js
new file mode 100644
index 00000000..a7f44b8e
--- /dev/null
+++ b/lib/rules/max-props.js
@@ -0,0 +1,67 @@
+/**
+ * @author kevsommer Kevin Sommer
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+const utils = require('../utils')
+
+module.exports = {
+  meta: {
+    type: 'problem',
+    docs: {
+      description: 'enforce maximum number of props in Vue component',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/max-props.html'
+    },
+    fixable: null,
+    schema: [
+      {
+        type: 'object',
+        properties: {
+          maxProps: {
+            type: 'integer',
+            minimum: 1
+          }
+        },
+        additionalProperties: false,
+        minProperties: 1
+      }
+    ],
+    messages: {
+      tooManyProps:
+        'Component has too many props ({{propCount}}). Maximum allowed is {{limit}}.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    /** @type {Record<string, number>} */
+    const option = context.options[0] || {}
+
+    /**
+     * @param {import('../utils').ComponentProp[]} props
+     */
+    function checkMaxNumberOfProps(props) {
+      if (props.length > option.maxProps && props[0].node) {
+        context.report({
+          node: props[0].node,
+          messageId: 'tooManyProps',
+          data: {
+            propCount: props.length,
+            limit: option.maxProps
+          }
+        })
+      }
+    }
+
+    return utils.compositingVisitors(
+      utils.executeOnVue(context, (obj) => {
+        checkMaxNumberOfProps(utils.getComponentPropsFromOptions(obj))
+      }),
+      utils.defineScriptSetupVisitor(context, {
+        onDefinePropsEnter(_, props) {
+          checkMaxNumberOfProps(props)
+        }
+      })
+    )
+  }
+}
diff --git a/lib/rules/no-computed-properties-in-data.js b/lib/rules/no-computed-properties-in-data.js
index a2273947..472d45f4 100644
--- a/lib/rules/no-computed-properties-in-data.js
+++ b/lib/rules/no-computed-properties-in-data.js
@@ -14,7 +14,7 @@ module.exports = {
   meta: {
     type: 'problem',
     docs: {
-      description: 'disallow accessing computed properties in `data`.',
+      description: 'disallow accessing computed properties in `data`',
       categories: ['vue3-essential', 'vue2-essential'],
       url: 'https://eslint.vuejs.org/rules/no-computed-properties-in-data.html'
     },
diff --git a/lib/rules/prefer-define-options.js b/lib/rules/prefer-define-options.js
index 90c83fa9..e5fabb0c 100644
--- a/lib/rules/prefer-define-options.js
+++ b/lib/rules/prefer-define-options.js
@@ -10,7 +10,7 @@ module.exports = {
   meta: {
     type: 'suggestion',
     docs: {
-      description: 'enforce use of `defineOptions` instead of default export.',
+      description: 'enforce use of `defineOptions` instead of default export',
       categories: undefined,
       url: 'https://eslint.vuejs.org/rules/prefer-define-options.html'
     },
diff --git a/lib/rules/require-typed-object-prop.js b/lib/rules/require-typed-object-prop.js
index ee88a0f3..eb7a278a 100644
--- a/lib/rules/require-typed-object-prop.js
+++ b/lib/rules/require-typed-object-prop.js
@@ -129,6 +129,30 @@ module.exports = {
   },
   /** @param {RuleContext} context */
   create(context) {
+    const filename = context.getFilename()
+    if (!utils.isVueFile(filename) && !utils.isTypeScriptFile(filename)) {
+      return {}
+    }
+
+    if (utils.isVueFile(filename)) {
+      const sourceCode = context.getSourceCode()
+      const documentFragment =
+        sourceCode.parserServices.getDocumentFragment &&
+        sourceCode.parserServices.getDocumentFragment()
+      if (!documentFragment) {
+        return {}
+      }
+      const scripts = documentFragment.children.filter(
+        /** @returns {element is VElement} */
+        (element) => utils.isVElement(element) && element.name === 'script'
+      )
+      if (
+        scripts.every((script) => !utils.hasAttribute(script, 'lang', 'ts'))
+      ) {
+        return {}
+      }
+    }
+
     return utils.compositingVisitors(
       utils.defineScriptSetupVisitor(context, {
         onDefinePropsEnter(_node, props) {
diff --git a/lib/rules/v-if-else-key.js b/lib/rules/v-if-else-key.js
index 75add9ca..a83d436b 100644
--- a/lib/rules/v-if-else-key.js
+++ b/lib/rules/v-if-else-key.js
@@ -25,6 +25,28 @@ const casing = require('../utils/casing')
  * @property {VElement | null} else - The node associated with the 'v-else' directive, or null if there isn't one.
  */
 
+/**
+ * Checks if a given node has sibling nodes of the same type that are also conditionally rendered.
+ * This is used to determine if multiple instances of the same component are being conditionally
+ * rendered within the same parent scope.
+ *
+ * @param {VElement} node - The Vue component node to check for conditional rendering siblings.
+ * @param {string} componentName - The name of the component to check for sibling instances.
+ * @returns {boolean} True if there are sibling nodes of the same type and conditionally rendered, false otherwise.
+ */
+const hasConditionalRenderedSiblings = (node, componentName) => {
+  if (!node.parent || node.parent.type !== 'VElement') {
+    return false
+  }
+  return node.parent.children.some(
+    (sibling) =>
+      sibling !== node &&
+      sibling.type === 'VElement' &&
+      sibling.rawName === componentName &&
+      hasConditionalDirective(sibling)
+  )
+}
+
 /**
  * Checks for the presence of a 'key' attribute in the given node. If the 'key' attribute is missing
  * and the node is part of a conditional family a report is generated.
@@ -44,26 +66,31 @@ const checkForKey = (
   uniqueKey,
   conditionalFamilies
 ) => {
-  if (node.parent && node.parent.type === 'VElement') {
-    const conditionalFamily = conditionalFamilies.get(node.parent)
+  if (
+    !node.parent ||
+    node.parent.type !== 'VElement' ||
+    !hasConditionalRenderedSiblings(node, componentName)
+  ) {
+    return
+  }
 
-    if (
-      conditionalFamily &&
-      (utils.hasDirective(node, 'bind', 'key') ||
-        utils.hasAttribute(node, 'key') ||
-        !hasConditionalDirective(node) ||
-        !(conditionalFamily.else || conditionalFamily.elseIf.length > 0))
-    ) {
-      return
-    }
+  const conditionalFamily = conditionalFamilies.get(node.parent)
+
+  if (!conditionalFamily || utils.hasAttribute(node, 'key')) {
+    return
+  }
+
+  const needsKey =
+    conditionalFamily.if === node ||
+    conditionalFamily.else === node ||
+    conditionalFamily.elseIf.includes(node)
 
+  if (needsKey) {
     context.report({
       node: node.startTag,
       loc: node.startTag.loc,
       messageId: 'requireKey',
-      data: {
-        componentName
-      },
+      data: { componentName },
       fix(fixer) {
         const afterComponentNamePosition =
           node.startTag.range[0] + componentName.length + 1
@@ -190,13 +217,18 @@ module.exports = {
           if (node.parent && node.parent.type === 'VElement') {
             let conditionalFamily = conditionalFamilies.get(node.parent)
 
-            if (conditionType === 'if' && !conditionalFamily) {
+            if (!conditionalFamily) {
               conditionalFamily = createConditionalFamily(node)
               conditionalFamilies.set(node.parent, conditionalFamily)
             }
 
             if (conditionalFamily) {
               switch (conditionType) {
+                case 'if': {
+                  conditionalFamily = createConditionalFamily(node)
+                  conditionalFamilies.set(node.parent, conditionalFamily)
+                  break
+                }
                 case 'else-if': {
                   conditionalFamily.elseIf.push(node)
                   break
diff --git a/tools/update-lib-flat-configs.js b/tools/update-lib-flat-configs.js
index 9d1671b9..d25ba281 100644
--- a/tools/update-lib-flat-configs.js
+++ b/tools/update-lib-flat-configs.js
@@ -58,7 +58,7 @@ function formatCategory(category) {
 const globals = require('globals')
 module.exports = [
   {
-    name: 'vue:base:setup',
+    name: 'vue/base/setup',
     plugins: {
       get vue() {
         return require('../../index')
@@ -70,7 +70,7 @@ module.exports = [
     }
   },
   {
-    name: 'vue:base:setup-for-vue',
+    name: 'vue/base/setup-for-vue',
     files: ['*.vue', '**/*.vue'],
     plugins: {
       get vue() {
@@ -99,7 +99,7 @@ const config = require('./${extendsCategoryId}.js')
 module.exports = [
   ...config,
   {
-    name: 'vue:${category.categoryId.replace(/^vue3-/u, '')}:rules',
+    name: 'vue/${category.categoryId.replace(/^vue3-/u, '')}/rules',
     rules: ${formatRules(category.rules, category.categoryId)},
   }
 ]


Test patch:
diff --git a/tests/lib/rules/max-props.js b/tests/lib/rules/max-props.js
new file mode 100644
index 00000000..1dd42a03
--- /dev/null
+++ b/tests/lib/rules/max-props.js
@@ -0,0 +1,162 @@
+/**
+ * @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-props')
+
+const tester = new RuleTester({
+  languageOptions: {
+    parser: require('vue-eslint-parser'),
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('max-props', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      defineProps({ prop1: '', prop2: '' })
+      </script>
+      `,
+      options: [{ maxProps: 5 }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        props: {
+          prop1: String,
+          prop2: String
+        }
+      }
+      </script>
+      `,
+      options: [{ maxProps: 5 }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        props: {
+          prop1: String,
+          prop2: String,
+          prop3: String,
+          prop4: String,
+          prop5: String
+        }
+      }
+      </script>
+      `,
+      options: [{ maxProps: 5 }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        props: {}
+      }
+      </script>
+      `,
+      options: [{ maxProps: 5 }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      defineProps({})
+      </script>
+      `,
+      options: [{ maxProps: 5 }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      </script>
+      `,
+      options: [{ maxProps: 5 }]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      defineProps<{ prop1: string, prop2: string }>();
+      </script>
+      `,
+      options: [{ maxProps: 5 }],
+      languageOptions: {
+        parser: require('vue-eslint-parser'),
+        parserOptions: {
+          parser: require.resolve('@typescript-eslint/parser')
+        }
+      }
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      defineProps({ prop1: '', prop2: '' })
+      </script>
+      `,
+      options: [{ maxProps: 1 }],
+      errors: [
+        {
+          message: 'Component has too many props (2). Maximum allowed is 1.',
+          line: 3
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        props: {
+          prop1: String,
+          prop2: String
+        }
+      }
+      </script>
+      `,
+      options: [{ maxProps: 1 }],
+      errors: [
+        {
+          message: 'Component has too many props (2). Maximum allowed is 1.',
+          line: 5
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup lang="ts">
+      defineProps<{ prop1: string, prop2: string, prop3: string }>();
+      </script>
+      `,
+      options: [{ maxProps: 2 }],
+      languageOptions: {
+        parser: require('vue-eslint-parser'),
+        parserOptions: {
+          parser: require.resolve('@typescript-eslint/parser')
+        }
+      },
+      errors: [
+        {
+          message: 'Component has too many props (3). Maximum allowed is 2.',
+          line: 3
+        }
+      ]
+    }
+  ]
+})
diff --git a/tests/lib/rules/require-typed-object-prop.js b/tests/lib/rules/require-typed-object-prop.js
index 9f5f000a..8a16fb56 100644
--- a/tests/lib/rules/require-typed-object-prop.js
+++ b/tests/lib/rules/require-typed-object-prop.js
@@ -9,7 +9,12 @@ const rule = require('../../../lib/rules/require-typed-object-prop')
 
 const ruleTester = new RuleTester({
   languageOptions: {
-    parser: require('@typescript-eslint/parser')
+    parser: require('vue-eslint-parser'),
+    ecmaVersion: 6,
+    sourceType: 'module',
+    parserOptions: {
+      parser: require.resolve('@typescript-eslint/parser')
+    }
   }
 })
 
@@ -19,20 +24,22 @@ ruleTester.run('require-typed-object-prop', rule, {
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default {
         props: {}
       }
-      `,
-      languageOptions: { ecmaVersion: 6, sourceType: 'module' }
+      </script>
+      `
     },
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default Vue.extend({
         props: {}
       });
-      `,
-      languageOptions: { ecmaVersion: 6, sourceType: 'module' }
+      </script>
+      `
     },
     {
       filename: 'test.vue',
@@ -40,31 +47,28 @@ ruleTester.run('require-typed-object-prop', rule, {
       <script setup lang="ts">
       defineProps({});
       </script>
-      `,
-      languageOptions: {
-        parser: require('vue-eslint-parser'),
-        ecmaVersion: 6,
-        sourceType: 'module'
-      }
+      `
     },
     // array props
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default {
         props: ['foo']
       }
-      `,
-      languageOptions: { ecmaVersion: 6, sourceType: 'module' }
+      </script>
+      `
     },
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default Vue.extend({
         props: ['foo']
       });
-      `,
-      languageOptions: { ecmaVersion: 6, sourceType: 'module' }
+      </script>
+      `
     },
     {
       filename: 'test.vue',
@@ -72,31 +76,28 @@ ruleTester.run('require-typed-object-prop', rule, {
       <script setup lang="ts">
       defineProps(['foo']);
       </script>
-      `,
-      languageOptions: {
-        parser: require('vue-eslint-parser'),
-        ecmaVersion: 6,
-        sourceType: 'module'
-      }
+      `
     },
     // primitive props
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default {
         props: { foo: String }
       }
-      `,
-      languageOptions: { ecmaVersion: 6, sourceType: 'module' }
+      </script>
+      `
     },
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default Vue.extend({
         props: { foo: String }
       });
-      `,
-      languageOptions: { ecmaVersion: 6, sourceType: 'module' }
+      </script>
+      `
     },
     {
       filename: 'test.vue',
@@ -104,31 +105,28 @@ ruleTester.run('require-typed-object-prop', rule, {
       <script setup lang="ts">
       defineProps({ foo: String });
       </script>
-      `,
-      languageOptions: {
-        parser: require('vue-eslint-parser'),
-        ecmaVersion: 6,
-        sourceType: 'module'
-      }
+      `
     },
     // union
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default {
         props: { foo: [Number, String, Boolean] }
       }
-      `,
-      languageOptions: { ecmaVersion: 6, sourceType: 'module' }
+      </script>
+      `
     },
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default Vue.extend({
         props: { foo: [Number, String, Boolean] }
       });
-      `,
-      languageOptions: { ecmaVersion: 6, sourceType: 'module' }
+      </script>
+      `
     },
     {
       filename: 'test.vue',
@@ -136,31 +134,28 @@ ruleTester.run('require-typed-object-prop', rule, {
       <script setup lang="ts">
       defineProps({ foo: [Number, String, Boolean] });
       </script>
-      `,
-      languageOptions: {
-        parser: require('vue-eslint-parser'),
-        ecmaVersion: 6,
-        sourceType: 'module'
-      }
+      `
     },
     // function
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default {
         props: { foo: someFunction() }
       }
-      `,
-      languageOptions: { ecmaVersion: 6, sourceType: 'module' }
+      </script>
+      `
     },
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default Vue.extend({
         props: { foo: someFunction() }
       });
-      `,
-      languageOptions: { ecmaVersion: 6, sourceType: 'module' }
+      </script>
+      `
     },
     {
       filename: 'test.vue',
@@ -168,40 +163,38 @@ ruleTester.run('require-typed-object-prop', rule, {
       <script setup lang="ts">
       defineProps({ foo: someFunction() });
       </script>
-      `,
-      languageOptions: {
-        parser: require('vue-eslint-parser'),
-        ecmaVersion: 6,
-        sourceType: 'module'
-      }
+      `
     },
     // typed object
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default {
         props: { foo: Object as PropType<User> }
       }
-      `,
-      languageOptions: { ecmaVersion: 6, sourceType: 'module' }
+      </script>
+      `
     },
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default {
         props: { foo: Array as PropType<User[]> }
       }
-      `,
-      languageOptions: { ecmaVersion: 6, sourceType: 'module' }
+      </script>
+      `
     },
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default Vue.extend({
         props: { foo: Object as PropType<User> }
       });
-      `,
-      languageOptions: { ecmaVersion: 6, sourceType: 'module' }
+      </script>
+      `
     },
     {
       filename: 'test.vue',
@@ -209,42 +202,28 @@ ruleTester.run('require-typed-object-prop', rule, {
       <script setup lang="ts">
       defineProps({ foo: Object as PropType<User> });
       </script>
-      `,
-      languageOptions: {
-        parser: require('vue-eslint-parser'),
-        ecmaVersion: 6,
-        sourceType: 'module',
-        parserOptions: {
-          parser: require.resolve('@typescript-eslint/parser')
-        }
-      }
+      `
     },
 
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default {
         props: { foo: Object as () => User }
       }
-      `,
-      languageOptions: {
-        ecmaVersion: 6,
-        sourceType: 'module',
-        parser: require('@typescript-eslint/parser')
-      }
+      </script>
+      `
     },
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default Vue.extend({
         props: { foo: Object as () => User }
       });
-      `,
-      languageOptions: {
-        ecmaVersion: 6,
-        sourceType: 'module',
-        parser: require('@typescript-eslint/parser')
-      }
+      </script>
+      `
     },
     {
       filename: 'test.vue',
@@ -252,15 +231,7 @@ ruleTester.run('require-typed-object-prop', rule, {
       <script setup lang="ts">
       defineProps({ foo: Object as () => User });
       </script>
-      `,
-      languageOptions: {
-        parser: require('vue-eslint-parser'),
-        ecmaVersion: 6,
-        sourceType: 'module',
-        parserOptions: {
-          parser: require.resolve('@typescript-eslint/parser')
-        }
-      }
+      `
     },
     // any
     {
@@ -269,15 +240,7 @@ ruleTester.run('require-typed-object-prop', rule, {
       <script setup lang="ts">
       defineProps({ foo: { type: Object as any } });
       </script>
-      `,
-      languageOptions: {
-        parser: require('vue-eslint-parser'),
-        ecmaVersion: 6,
-        sourceType: 'module',
-        parserOptions: {
-          parser: require.resolve('@typescript-eslint/parser')
-        }
-      }
+      `
     },
     {
       filename: 'test.vue',
@@ -289,15 +252,7 @@ ruleTester.run('require-typed-object-prop', rule, {
           }
         };
         </script>
-      `,
-      languageOptions: {
-        parser: require('vue-eslint-parser'),
-        ecmaVersion: 6,
-        sourceType: 'module',
-        parserOptions: {
-          parser: require.resolve('@typescript-eslint/parser')
-        }
-      }
+      `
     },
     {
       filename: 'test.vue',
@@ -309,13 +264,7 @@ ruleTester.run('require-typed-object-prop', rule, {
           }
         });
         </script>
-      `,
-      languageOptions: {
-        parser: require('vue-eslint-parser'),
-        ecmaVersion: 6,
-        sourceType: 'module',
-        parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
-      }
+      `
     },
     // unknown
     {
@@ -324,13 +273,7 @@ ruleTester.run('require-typed-object-prop', rule, {
       <script setup lang="ts">
       defineProps({ foo: { type: Object as unknown } });
       </script>
-      `,
-      languageOptions: {
-        parser: require('vue-eslint-parser'),
-        ecmaVersion: 6,
-        sourceType: 'module',
-        parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
-      }
+      `
     },
     {
       filename: 'test.vue',
@@ -342,13 +285,7 @@ ruleTester.run('require-typed-object-prop', rule, {
           }
         };
         </script>
-      `,
-      languageOptions: {
-        parser: require('vue-eslint-parser'),
-        ecmaVersion: 6,
-        sourceType: 'module',
-        parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
-      }
+      `
     },
     {
       filename: 'test.vue',
@@ -360,13 +297,34 @@ ruleTester.run('require-typed-object-prop', rule, {
           }
         });
         </script>
-      `,
-      languageOptions: {
-        parser: require('vue-eslint-parser'),
-        ecmaVersion: 6,
-        sourceType: 'module',
-        parserOptions: { parser: require.resolve('@typescript-eslint/parser') }
-      }
+      `
+    },
+    // JavaScript components
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      defineProps({ foo: Object });
+      </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default Vue.extend({
+        props: { foo: { type: Object } }
+      });
+      </script>
+      `
+    },
+    {
+      filename: 'test.js',
+      code: `
+      export default Vue.extend({
+        props: { foo: Object }
+      });
+      `
     }
   ],
   invalid: [
@@ -377,14 +335,6 @@ ruleTester.run('require-typed-object-prop', rule, {
       defineProps({ foo: Object });
       </script>
       `,
-      languageOptions: {
-        parser: require('vue-eslint-parser'),
-        ecmaVersion: 6,
-        sourceType: 'module',
-        parserOptions: {
-          parser: require.resolve('@typescript-eslint/parser')
-        }
-      },
       errors: [
         {
           messageId: 'expectedTypeAnnotation',
@@ -422,14 +372,6 @@ ruleTester.run('require-typed-object-prop', rule, {
       defineProps({ foo: Array });
       </script>
       `,
-      languageOptions: {
-        parser: require('vue-eslint-parser'),
-        ecmaVersion: 6,
-        sourceType: 'module',
-        parserOptions: {
-          parser: require.resolve('@typescript-eslint/parser')
-        }
-      },
       errors: [
         {
           messageId: 'expectedTypeAnnotation',
@@ -463,35 +405,40 @@ ruleTester.run('require-typed-object-prop', rule, {
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default {
         props: { foo: Object }
       }
+      </script>
       `,
-      languageOptions: { ecmaVersion: 6, sourceType: 'module' },
       errors: [
         {
           messageId: 'expectedTypeAnnotation',
-          line: 3,
+          line: 4,
           column: 23,
-          endLine: 3,
+          endLine: 4,
           endColumn: 29,
           suggestions: [
             {
               messageId: 'addTypeAnnotation',
               data: { type: 'any' },
               output: `
+      <script lang="ts">
       export default {
         props: { foo: Object as PropType<any> }
       }
+      </script>
       `
             },
             {
               messageId: 'addTypeAnnotation',
               data: { type: 'unknown' },
               output: `
+      <script lang="ts">
       export default {
         props: { foo: Object as PropType<unknown> }
       }
+      </script>
       `
             }
           ]
@@ -501,35 +448,40 @@ ruleTester.run('require-typed-object-prop', rule, {
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default Vue.extend({
         props: { foo: Object }
       });
+      </script>
       `,
-      languageOptions: { ecmaVersion: 6, sourceType: 'module' },
       errors: [
         {
           messageId: 'expectedTypeAnnotation',
-          line: 3,
+          line: 4,
           column: 23,
-          endLine: 3,
+          endLine: 4,
           endColumn: 29,
           suggestions: [
             {
               messageId: 'addTypeAnnotation',
               data: { type: 'any' },
               output: `
+      <script lang="ts">
       export default Vue.extend({
         props: { foo: Object as PropType<any> }
       });
+      </script>
       `
             },
             {
               messageId: 'addTypeAnnotation',
               data: { type: 'unknown' },
               output: `
+      <script lang="ts">
       export default Vue.extend({
         props: { foo: Object as PropType<unknown> }
       });
+      </script>
       `
             }
           ]
@@ -539,35 +491,40 @@ ruleTester.run('require-typed-object-prop', rule, {
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default Vue.extend({
         props: { foo: { type: Object } }
       });
+      </script>
       `,
-      languageOptions: { ecmaVersion: 6, sourceType: 'module' },
       errors: [
         {
           messageId: 'expectedTypeAnnotation',
-          line: 3,
+          line: 4,
           column: 31,
-          endLine: 3,
+          endLine: 4,
           endColumn: 37,
           suggestions: [
             {
               messageId: 'addTypeAnnotation',
               data: { type: 'any' },
               output: `
+      <script lang="ts">
       export default Vue.extend({
         props: { foo: { type: Object as PropType<any> } }
       });
+      </script>
       `
             },
             {
               messageId: 'addTypeAnnotation',
               data: { type: 'unknown' },
               output: `
+      <script lang="ts">
       export default Vue.extend({
         props: { foo: { type: Object as PropType<unknown> } }
       });
+      </script>
       `
             }
           ]
@@ -577,35 +534,40 @@ ruleTester.run('require-typed-object-prop', rule, {
     {
       filename: 'test.vue',
       code: `
+      <script lang="ts">
       export default {
         props: { foo: { type: Object } }
       }
+      </script>
       `,
-      languageOptions: { ecmaVersion: 6, sourceType: 'module' },
       errors: [
         {
           messageId: 'expectedTypeAnnotation',
-          line: 3,
+          line: 4,
           column: 31,
-          endLine: 3,
+          endLine: 4,
           endColumn: 37,
           suggestions: [
             {
               messageId: 'addTypeAnnotation',
               data: { type: 'any' },
               output: `
+      <script lang="ts">
       export default {
         props: { foo: { type: Object as PropType<any> } }
       }
+      </script>
       `
             },
             {
               messageId: 'addTypeAnnotation',
               data: { type: 'unknown' },
               output: `
+      <script lang="ts">
       export default {
         props: { foo: { type: Object as PropType<unknown> } }
       }
+      </script>
       `
             }
           ]
@@ -619,14 +581,6 @@ ruleTester.run('require-typed-object-prop', rule, {
       defineProps({ foo: { type: Object } });
       </script>
       `,
-      languageOptions: {
-        parser: require('vue-eslint-parser'),
-        ecmaVersion: 6,
-        sourceType: 'module',
-        parserOptions: {
-          parser: require.resolve('@typescript-eslint/parser')
-        }
-      },
       errors: [
         {
           messageId: 'expectedTypeAnnotation',
@@ -656,6 +610,44 @@ ruleTester.run('require-typed-object-prop', rule, {
           ]
         }
       ]
+    },
+    {
+      // `.ts` file
+      filename: 'test.ts',
+      code: `
+      export default Vue.extend({
+        props: { foo: Object }
+      });
+      `,
+      errors: [
+        {
+          messageId: 'expectedTypeAnnotation',
+          line: 3,
+          column: 23,
+          endLine: 3,
+          endColumn: 29,
+          suggestions: [
+            {
+              messageId: 'addTypeAnnotation',
+              data: { type: 'any' },
+              output: `
+      export default Vue.extend({
+        props: { foo: Object as PropType<any> }
+      });
+      `
+            },
+            {
+              messageId: 'addTypeAnnotation',
+              data: { type: 'unknown' },
+              output: `
+      export default Vue.extend({
+        props: { foo: Object as PropType<unknown> }
+      });
+      `
+            }
+          ]
+        }
+      ]
     }
   ]
 })
diff --git a/tests/lib/rules/v-if-else-key.js b/tests/lib/rules/v-if-else-key.js
index 16d975a0..46c42f52 100644
--- a/tests/lib/rules/v-if-else-key.js
+++ b/tests/lib/rules/v-if-else-key.js
@@ -127,6 +127,90 @@ tester.run('v-if-else-key', rule, {
         }
         </script>
         `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div>
+            <div>
+              <ComponentA v-if="foo" /> 
+              <ComponentB v-else-if="bar" /> 
+            </div>
+            <div>
+              <div>
+                <ComponentA v-if="foo" />
+                <ComponentB v-else-if="baz" /> 
+              </div>
+              <div>
+                <ComponentA v-if="foo" />
+                <ComponentB v-else-if="baz" />
+              </div>
+            </div>
+            <div>
+              <ComponentA v-if="foo" />
+              <ComponentB v-else /> 
+            </div>
+            <div>
+              <div v-if="foo" />
+              <ComponentB v-else />
+            </div>
+          </div>
+        </template>
+        <script>
+        export default {
+          components: {
+            ComponentA,
+            ComponentB
+          }
+        }
+        </script>
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div>
+            <div>
+              <CustomComponent v-if="foo" />
+              <div v-else />
+            </div>
+      
+            <CustomComponent v-if="bar" />
+          </div>
+        </template>
+        <script>
+        export default {
+            components: {
+                CustomComponent
+            }
+        }
+        </script>
+        `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <tile>
+            <template v-if="state === 'foo'">
+                <ComponentA>…</ComponentA>
+                <ComponentB>…</ComponentB>
+            </template>
+            <ComponentA v-else-if="state === 'bar'" key="a">…</ComponentA>
+            <ComponentA v-else-if="state === 'bar'" key="b">…</ComponentA>
+          </tile>
+        </template>
+        <script>
+        export default {
+            components: {
+                ComponentA,
+                ComponentB
+            }
+        }
+        </script>
+        `
     }
   ],
   invalid: [
@@ -424,6 +508,74 @@ tester.run('v-if-else-key', rule, {
           line: 6
         }
       ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+        <template>
+          <div>
+            <ComponentA v-if="foo" />
+            <ComponentA v-else />
+
+            <ComponentA v-if="bar" />
+            <ComponentA v-else-if="baz" />
+            <ComponentA v-else />
+          </div>
+        </template>
+        <script>
+        export default {
+          components: {
+            ComponentA
+          }
+        }
+        </script>
+        `,
+      output: `
+        <template>
+          <div>
+            <ComponentA key="component-a-1" v-if="foo" />
+            <ComponentA key="component-a-2" v-else />
+
+            <ComponentA key="component-a-3" v-if="bar" />
+            <ComponentA key="component-a-4" v-else-if="baz" />
+            <ComponentA key="component-a-5" v-else />
+          </div>
+        </template>
+        <script>
+        export default {
+          components: {
+            ComponentA
+          }
+        }
+        </script>
+        `,
+      errors: [
+        {
+          message:
+            "Conditionally rendered repeated component 'ComponentA' expected to have a 'key' attribute.",
+          line: 4
+        },
+        {
+          message:
+            "Conditionally rendered repeated component 'ComponentA' expected to have a 'key' attribute.",
+          line: 5
+        },
+        {
+          message:
+            "Conditionally rendered repeated component 'ComponentA' expected to have a 'key' attribute.",
+          line: 7
+        },
+        {
+          message:
+            "Conditionally rendered repeated component 'ComponentA' expected to have a 'key' attribute.",
+          line: 8
+        },
+        {
+          message:
+            "Conditionally rendered repeated component 'ComponentA' expected to have a 'key' attribute.",
+          line: 9
+        }
+      ]
     }
   ]
 })