Repository: vuejs/eslint-plugin-vue
Pull request #2498 (2024-07-06 13:00:05)

fix: change flat config name separator to `/`
This PR changes the all flat configs' name separator from `:` to `/`.

As ESLint recommended use `/` as the separator.

Refs:

- [Configuration Naming Conventions](https://eslint.org/docs/latest/use/configure/configuration-files#configuration-naming-conventions)


Patch:
diff --git a/docs/rules/index.md b/docs/rules/index.md
index e718603f..a75fe157 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -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: |
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/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/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
+        }
+      ]
+    }
+  ]
+})