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

Issue:
Rule Proposal: `vue/no-restricted-component-names`
<!--
  Before proposing new rule, please make sure it hasn't been posted already.
  You can see all open propositions here:
  https://github.com/vuejs/eslint-plugin-vue/issues?q=is%3Aopen+is%3Aissue+label%3A%22new+rule+proposition%22
-->

**Please describe what the rule should do:**
<!-- A clear and concise description of what the rule should do. -->

Restrict some component names via a list of pattern.

**What category should the rule belong to?**
<!-- (place an "X" next to just one item) -->
[ ] Enforces code style (layout)
[x] Warns about a potential error (problem)
[ ] Suggests an alternate way of doing something (suggestion)
[ ] Other (please specify:)

**Provide 2-3 code examples that this rule should warn about:**

Options:

```json
{
  "vue/no-restricted-component-names": ["error", "Foo", "/^Baz(One|Two)/"]
}
```

```vue
<script>
<!-- ✗ BAD -->
export default {
  name: 'Foo'
}

<!-- ✗ BAD -->
export default {
  name: 'BazTwo'
}

<!-- ✓ GOOD-->
export default {
  name: 'Allow'
}
</script>
```

**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->

Related https://github.com/vuejs/eslint-plugin-vue/issues/2180#issuecomment-1551298495.

Pull request description:
Add `vue/no-restricted-component-names` rule
closes #2182 

---

I slightly changed the schema format that was proposed in the issue, as in [no-restricted-props](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-props.js) rule.

Patch:
diff --git a/docs/rules/index.md b/docs/rules/index.md
index 605cd4ef..d7033432 100644
--- a/docs/rules/index.md
+++ b/docs/rules/index.md
@@ -234,6 +234,7 @@ For example:
 | [vue/no-restricted-block](./no-restricted-block.md) | disallow specific block |  | :hammer: |
 | [vue/no-restricted-call-after-await](./no-restricted-call-after-await.md) | disallow asynchronously called restricted methods |  | :hammer: |
 | [vue/no-restricted-class](./no-restricted-class.md) | disallow specific classes in Vue components |  | :warning: |
+| [vue/no-restricted-component-names](./no-restricted-component-names.md) | disallow specific component names | :bulb: | :hammer: |
 | [vue/no-restricted-component-options](./no-restricted-component-options.md) | disallow specific component option |  | :hammer: |
 | [vue/no-restricted-custom-event](./no-restricted-custom-event.md) | disallow specific custom event | :bulb: | :hammer: |
 | [vue/no-restricted-html-elements](./no-restricted-html-elements.md) | disallow specific HTML elements |  | :hammer: |
diff --git a/docs/rules/no-restricted-component-names.md b/docs/rules/no-restricted-component-names.md
new file mode 100644
index 00000000..ccd7b24b
--- /dev/null
+++ b/docs/rules/no-restricted-component-names.md
@@ -0,0 +1,89 @@
+---
+pageClass: rule-details
+sidebarDepth: 0
+title: vue/no-restricted-component-names
+description: disallow specific component names
+---
+# vue/no-restricted-component-names
+
+> disallow specific component names
+
+- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>
+- :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 allows you to specify component names that you don't want to use in your application.
+
+<eslint-code-block :rules="{'vue/no-restricted-component-names': ['error', 'Disallow']}">
+
+```vue
+<!-- ✗ BAD -->
+<script>
+export default {
+  name: 'Disallow',
+}
+</script>
+```
+
+</eslint-code-block>
+
+<eslint-code-block :rules="{'vue/no-restricted-component-names': ['error', 'Disallow']}">
+
+```vue
+<!-- ✓ GOOD -->
+<script>
+export default {
+  name: 'Allow',
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :wrench: Options
+
+This rule takes a list of strings, where each string is a component name or pattern to be restricted:
+
+```json
+{
+  "vue/no-restricted-component-names": ["error", "foo", "/^Disallow/"]
+}
+```
+
+Alternatively, you can specify an object with a `name` property and an optional `message` and `suggest` property:
+  
+```json
+  {
+    "vue/no-restricted-component-names": [
+      "error",
+      {
+        "name": "Disallow",
+        "message": "Please do not use `Disallow` as a component name",
+        "suggest": "allow"
+      },
+      {
+        "name": "/^custom/",
+        "message": "Please do not use component names starting with 'custom'"
+      }
+    ]
+  }
+  ```
+
+<eslint-code-block :rules="{'vue/no-restricted-component-names': ['error', { name: 'Disallow', message: 'Please do not use \'Disallow\' as a component name', suggest: 'allow'}]}">
+
+```vue
+<!-- ✗ BAD -->
+<script>
+export default {
+  name: 'Disallow',
+}
+</script>
+```
+
+</eslint-code-block>
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/no-restricted-component-names.js)
+- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/no-restricted-component-names.js)
diff --git a/lib/index.js b/lib/index.js
index 41c5b6b8..4329a29a 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -118,6 +118,7 @@ module.exports = {
     'no-restricted-block': require('./rules/no-restricted-block'),
     'no-restricted-call-after-await': require('./rules/no-restricted-call-after-await'),
     'no-restricted-class': require('./rules/no-restricted-class'),
+    'no-restricted-component-names': require('./rules/no-restricted-component-names'),
     'no-restricted-component-options': require('./rules/no-restricted-component-options'),
     'no-restricted-custom-event': require('./rules/no-restricted-custom-event'),
     'no-restricted-html-elements': require('./rules/no-restricted-html-elements'),
diff --git a/lib/rules/no-restricted-component-names.js b/lib/rules/no-restricted-component-names.js
new file mode 100644
index 00000000..e7fc2860
--- /dev/null
+++ b/lib/rules/no-restricted-component-names.js
@@ -0,0 +1,157 @@
+/**
+ * @author ItMaga <https://github.com/ItMaga>
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const utils = require('../utils')
+const casing = require('../utils/casing')
+const { isRegExp, toRegExp } = require('../utils/regexp')
+
+/**
+ * @typedef {object} OptionParsed
+ * @property { (name: string) => boolean } test
+ * @property {string|undefined} [message]
+ * @property {string|undefined} [suggest]
+ */
+
+/**
+ * @param {string} str
+ * @returns {(str: string) => boolean}
+ * @private
+ */
+function buildMatcher(str) {
+  if (isRegExp(str)) {
+    const regex = toRegExp(str)
+    return (s) => regex.test(s)
+  }
+  return (s) => s === casing.pascalCase(str) || s === casing.kebabCase(str)
+}
+
+/**
+ * @param {string|{name: string, message?: string, suggest?: string}} option
+ * @returns {OptionParsed}
+ * @private
+ * */
+function parseOption(option) {
+  if (typeof option === 'string') {
+    const matcher = buildMatcher(option)
+    return { test: matcher }
+  }
+  const parsed = parseOption(option.name)
+  parsed.message = option.message
+  parsed.suggest = option.suggest
+  return parsed
+}
+
+/**
+ * @param {Property | AssignmentProperty} property
+ * @param {string | undefined} suggest
+ * @returns {Rule.SuggestionReportDescriptor[]}
+ * @private
+ * */
+function createSuggest(property, suggest) {
+  if (!suggest) {
+    return []
+  }
+
+  return [
+    {
+      fix(fixer) {
+        return fixer.replaceText(property.value, JSON.stringify(suggest))
+      },
+      messageId: 'suggest',
+      data: { suggest }
+    }
+  ]
+}
+
+module.exports = {
+  meta: {
+    hasSuggestions: true,
+    type: 'suggestion',
+    docs: {
+      description: 'disallow specific component names',
+      categories: undefined,
+      url: 'https://eslint.vuejs.org/rules/no-restricted-component-names.html'
+    },
+    fixable: null,
+    schema: {
+      type: 'array',
+      items: {
+        oneOf: [
+          { type: 'string' },
+          {
+            type: 'object',
+            properties: {
+              name: { type: 'string' },
+              message: { type: 'string', minLength: 1 },
+              suggest: { type: 'string' }
+            },
+            required: ['name'],
+            additionalProperties: false
+          }
+        ]
+      },
+      uniqueItems: true,
+      minItems: 0
+    },
+    messages: {
+      // eslint-disable-next-line eslint-plugin/report-message-format
+      disallow: '{{message}}',
+      suggest: 'Instead, change to `{{suggest}}`.'
+    }
+  },
+  /** @param {RuleContext} context */
+  create(context) {
+    /** @type {OptionParsed[]} */
+    const options = context.options.map(parseOption)
+
+    /**
+     * @param {ObjectExpression} node
+     */
+    function verify(node) {
+      const property = utils.findProperty(node, 'name')
+      if (!property) return
+
+      const propertyName = utils.getStaticPropertyName(property)
+      if (propertyName === 'name' && property.value.type === 'Literal') {
+        const componentName = property.value.value?.toString()
+        if (!componentName) {
+          return
+        }
+
+        for (const option of options) {
+          if (option.test(componentName)) {
+            context.report({
+              node: property.value,
+              messageId: 'disallow',
+              data: {
+                message:
+                  option.message ||
+                  `Using component name \`${componentName}\` is not allowed.`
+              },
+              suggest: createSuggest(property, option.suggest)
+            })
+          }
+        }
+      }
+    }
+
+    return utils.compositingVisitors(
+      utils.defineVueVisitor(context, {
+        onVueObjectEnter(node) {
+          verify(node)
+        }
+      }),
+      utils.defineScriptSetupVisitor(context, {
+        onDefineOptionsEnter(node) {
+          const expression = node.arguments[0]
+          if (expression.type === 'ObjectExpression') {
+            verify(expression)
+          }
+        }
+      })
+    )
+  }
+}


Test patch:
diff --git a/tests/lib/rules/no-restricted-component-names.js b/tests/lib/rules/no-restricted-component-names.js
new file mode 100644
index 00000000..07030c4b
--- /dev/null
+++ b/tests/lib/rules/no-restricted-component-names.js
@@ -0,0 +1,262 @@
+/**
+ * @author ItMaga <https://github.com/ItMaga>
+ * See LICENSE file in root directory for full license.
+ */
+'use strict'
+
+const RuleTester = require('eslint').RuleTester
+const rule = require('../../../lib/rules/no-restricted-component-names')
+
+const tester = new RuleTester({
+  parser: require.resolve('vue-eslint-parser'),
+  parserOptions: {
+    ecmaVersion: 2020,
+    sourceType: 'module'
+  }
+})
+
+tester.run('no-restricted-component-names', rule, {
+  valid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        name: 'Allow'
+      }
+      `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      new Vue({
+        name: 'Allow',
+      })
+    `
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      defineOptions({
+        name: 'Allow'
+      })
+      </script>
+      `
+    }
+  ],
+  invalid: [
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      export default {
+        name: 'Disallow'
+      }
+      </script>
+      `,
+      options: ['Disallow', 'Disallow2'],
+      errors: [
+        {
+          message: 'Using component name `Disallow` is not allowed.',
+          line: 4,
+          column: 15
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script>
+      new Vue({
+        name: 'Disallow',
+      })
+      </script>
+      `,
+      options: ['Disallow'],
+      errors: [
+        {
+          message: 'Using component name `Disallow` is not allowed.',
+          line: 4,
+          column: 15
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      defineOptions({
+        name: 'Disallow'
+      })
+      </script>
+      `,
+      options: ['Disallow'],
+      errors: [
+        {
+          message: 'Using component name `Disallow` is not allowed.',
+          line: 4,
+          column: 15
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      defineOptions({
+        name: 'FooBar'
+      })
+      </script>
+      `,
+      options: ['/^Foo(Bar|Baz)/'],
+      errors: [
+        {
+          message: 'Using component name `FooBar` is not allowed.',
+          line: 4,
+          column: 15
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      defineOptions({
+        name: 'Disallow',
+        inheritAttrs: false,
+      })
+      </script>
+      `,
+      options: [
+        { name: 'Disallow', message: 'Custom message', suggest: 'Allow' }
+      ],
+      errors: [
+        {
+          message: 'Custom message',
+          line: 4,
+          column: 15,
+          suggestions: [
+            {
+              desc: 'Instead, change to `Allow`.',
+              output: `
+      <script setup>
+      defineOptions({
+        name: "Allow",
+        inheritAttrs: false,
+      })
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      defineOptions({
+        name: 'Disallow',
+        inheritAttrs: false,
+      })
+      </script>
+      `,
+      options: [{ name: 'Disallow', suggest: 'Allow' }],
+      errors: [
+        {
+          message: 'Using component name `Disallow` is not allowed.',
+          line: 4,
+          column: 15,
+          suggestions: [
+            {
+              desc: 'Instead, change to `Allow`.',
+              output: `
+      <script setup>
+      defineOptions({
+        name: "Allow",
+        inheritAttrs: false,
+      })
+      </script>
+      `
+            }
+          ]
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      defineOptions({
+        name: 'Disallow',
+        inheritAttrs: false,
+      })
+      </script>
+      `,
+      options: [{ name: 'Disallow', message: 'Custom message' }],
+      errors: [
+        {
+          message: 'Custom message',
+          line: 4,
+          column: 15
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      defineOptions({
+        name: 1
+      })
+      </script>
+      `,
+      options: ['1'],
+      errors: [
+        {
+          message: 'Using component name `1` is not allowed.',
+          line: 4,
+          column: 15
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      defineOptions({
+        name: 'disallowed-component',
+      })
+      </script>
+      `,
+      options: ['DisallowedComponent'],
+      errors: [
+        {
+          message:
+            'Using component name `disallowed-component` is not allowed.',
+          line: 4,
+          column: 15
+        }
+      ]
+    },
+    {
+      filename: 'test.vue',
+      code: `
+      <script setup>
+      defineOptions({
+        name: 'DisallowedComponent',
+      })
+      </script>
+      `,
+      options: ['disallowed-component'],
+      errors: [
+        {
+          message: 'Using component name `DisallowedComponent` is not allowed.',
+          line: 4,
+          column: 15
+        }
+      ]
+    }
+  ]
+})