From e643d449f68ea6a890ac6454dcc7a7f96b650274 Mon Sep 17 00:00:00 2001 From: Magomed Chemurziev Date: Tue, 13 Jun 2023 01:40:24 +0300 Subject: [PATCH] Add `vue/no-restricted-component-names` rule (#2210) --- docs/rules/index.md | 1 + docs/rules/no-restricted-component-names.md | 89 ++++++ lib/index.js | 1 + lib/rules/no-restricted-component-names.js | 157 +++++++++++ .../rules/no-restricted-component-names.js | 262 ++++++++++++++++++ 5 files changed, 510 insertions(+) create mode 100644 docs/rules/no-restricted-component-names.md create mode 100644 lib/rules/no-restricted-component-names.js create mode 100644 tests/lib/rules/no-restricted-component-names.js diff --git a/docs/rules/index.md b/docs/rules/index.md index 605cd4ef4..d7033432a 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 000000000..ccd7b24bc --- /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: ***This rule has not been released yet.*** +- :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. + + + +```vue + + +``` + + + + + +```vue + + +``` + + + +## :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'" + } + ] + } + ``` + + + +```vue + + +``` + + + +## :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 41c5b6b84..4329a29a2 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 000000000..e7fc28608 --- /dev/null +++ b/lib/rules/no-restricted-component-names.js @@ -0,0 +1,157 @@ +/** + * @author 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) + } + } + }) + ) + } +} 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 000000000..07030c4bb --- /dev/null +++ b/tests/lib/rules/no-restricted-component-names.js @@ -0,0 +1,262 @@ +/** + * @author 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: ` + + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + + `, + options: ['Disallow', 'Disallow2'], + errors: [ + { + message: 'Using component name `Disallow` is not allowed.', + line: 4, + column: 15 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['Disallow'], + errors: [ + { + message: 'Using component name `Disallow` is not allowed.', + line: 4, + column: 15 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['Disallow'], + errors: [ + { + message: 'Using component name `Disallow` is not allowed.', + line: 4, + column: 15 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['/^Foo(Bar|Baz)/'], + errors: [ + { + message: 'Using component name `FooBar` is not allowed.', + line: 4, + column: 15 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [ + { name: 'Disallow', message: 'Custom message', suggest: 'Allow' } + ], + errors: [ + { + message: 'Custom message', + line: 4, + column: 15, + suggestions: [ + { + desc: 'Instead, change to `Allow`.', + output: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + 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: ` + + ` + } + ] + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: [{ name: 'Disallow', message: 'Custom message' }], + errors: [ + { + message: 'Custom message', + line: 4, + column: 15 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['1'], + errors: [ + { + message: 'Using component name `1` is not allowed.', + line: 4, + column: 15 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['DisallowedComponent'], + errors: [ + { + message: + 'Using component name `disallowed-component` is not allowed.', + line: 4, + column: 15 + } + ] + }, + { + filename: 'test.vue', + code: ` + + `, + options: ['disallowed-component'], + errors: [ + { + message: 'Using component name `DisallowedComponent` is not allowed.', + line: 4, + column: 15 + } + ] + } + ] +})