From 05082d0b26e4e936dcb6d0f589666fed8b5cdcbe Mon Sep 17 00:00:00 2001 From: ItMaga Date: Fri, 9 Jun 2023 19:26:03 +0300 Subject: [PATCH 1/7] Add `vue/no-restricted-component-names` rule --- docs/rules/index.md | 1 + docs/rules/no-restricted-component-names.md | 95 ++++++++ lib/index.js | 1 + lib/rules/no-restricted-component-names.js | 153 ++++++++++++ .../rules/no-restricted-component-names.js | 225 ++++++++++++++++++ 5 files changed, 475 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 d6469f3e0..59f57744e 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..57a11e42f --- /dev/null +++ b/docs/rules/no-restricted-component-names.md @@ -0,0 +1,95 @@ +--- +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 don't use `Disallow` as a component name", + "suggest": "allow" + }, + { + "name": "/^custom/", + "message": "Please don't 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 6ca29d49f..01f7aef78 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..0b83fff7c --- /dev/null +++ b/lib/rules/no-restricted-component-names.js @@ -0,0 +1,153 @@ +/** + * @author ItMaga + * See LICENSE file in root directory for full license. + */ +'use strict' + +const utils = require('../utils') +const { isRegExp } = require('../utils/regexp') +const { 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 === 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 {Property | AssignmentProperty} property + */ + function verify(property) { + 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, { + Property(node) { + verify(node) + } + }), + utils.defineScriptSetupVisitor(context, { + onDefineOptionsEnter(node) { + const expression = node.arguments[0] + if (expression.type === 'ObjectExpression') { + const property = utils.findProperty(expression, 'name') + if (!property) return + + verify(property) + } + } + }) + ) + } +} 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..0caa47247 --- /dev/null +++ b/tests/lib/rules/no-restricted-component-names.js @@ -0,0 +1,225 @@ +/** + * @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 + } + ] + } + ] +}) From 3bd9aed3fac27a62c3ddce682dac26ea2e9985ec Mon Sep 17 00:00:00 2001 From: ItMaga Date: Fri, 9 Jun 2023 19:42:27 +0300 Subject: [PATCH 2/7] lint fixes --- docs/rules/no-restricted-component-names.md | 8 +------- lib/rules/no-restricted-component-names.js | 4 ++-- tests/lib/rules/no-restricted-component-names.js | 16 ++++++++-------- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/docs/rules/no-restricted-component-names.md b/docs/rules/no-restricted-component-names.md index 57a11e42f..efd340ec3 100644 --- a/docs/rules/no-restricted-component-names.md +++ b/docs/rules/no-restricted-component-names.md @@ -70,13 +70,7 @@ Alternatively, you can specify an object with a `name` property and an optional } ``` - + ```vue diff --git a/lib/rules/no-restricted-component-names.js b/lib/rules/no-restricted-component-names.js index 0b83fff7c..2404581b7 100644 --- a/lib/rules/no-restricted-component-names.js +++ b/lib/rules/no-restricted-component-names.js @@ -97,7 +97,7 @@ module.exports = { messages: { // eslint-disable-next-line eslint-plugin/report-message-format disallow: '{{message}}', - suggest: "Instead, change to '{{suggest}}'" + suggest: 'Instead, change to `{{suggest}}`' } }, /** @param {RuleContext} context */ @@ -122,7 +122,7 @@ module.exports = { data: { message: option.message || - `Using component name '${componentName}' is not allowed.` + `Using component name \`${componentName}\` is not allowed.` }, suggest: createSuggest(property, option.suggest) }) diff --git a/tests/lib/rules/no-restricted-component-names.js b/tests/lib/rules/no-restricted-component-names.js index 0caa47247..85c545378 100644 --- a/tests/lib/rules/no-restricted-component-names.js +++ b/tests/lib/rules/no-restricted-component-names.js @@ -58,7 +58,7 @@ tester.run('no-restricted-component-names', rule, { options: ['Disallow', 'Disallow2'], errors: [ { - message: "Using component name 'Disallow' is not allowed.", + message: 'Using component name `Disallow` is not allowed.', line: 4, column: 15 } @@ -76,7 +76,7 @@ tester.run('no-restricted-component-names', rule, { options: ['Disallow'], errors: [ { - message: "Using component name 'Disallow' is not allowed.", + message: 'Using component name `Disallow` is not allowed.', line: 4, column: 15 } @@ -94,7 +94,7 @@ tester.run('no-restricted-component-names', rule, { options: ['Disallow'], errors: [ { - message: "Using component name 'Disallow' is not allowed.", + message: 'Using component name `Disallow` is not allowed.', line: 4, column: 15 } @@ -112,7 +112,7 @@ tester.run('no-restricted-component-names', rule, { options: ['/^Foo(Bar|Baz)/'], errors: [ { - message: "Using component name 'FooBar' is not allowed.", + message: 'Using component name `FooBar` is not allowed.', line: 4, column: 15 } @@ -138,7 +138,7 @@ tester.run('no-restricted-component-names', rule, { column: 15, suggestions: [ { - desc: "Instead, change to 'Allow'", + desc: 'Instead, change to `Allow`', output: ` + `, + 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 + } + ] } ] }) From a716ec9ba84135ccc4248ec8cd5a2486eb6d028c Mon Sep 17 00:00:00 2001 From: ItMaga Date: Mon, 12 Jun 2023 18:07:34 +0300 Subject: [PATCH 7/7] change vue visitor --- lib/rules/no-restricted-component-names.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/rules/no-restricted-component-names.js b/lib/rules/no-restricted-component-names.js index c6b2ddf15..e7fc28608 100644 --- a/lib/rules/no-restricted-component-names.js +++ b/lib/rules/no-restricted-component-names.js @@ -108,9 +108,12 @@ module.exports = { const options = context.options.map(parseOption) /** - * @param {Property | AssignmentProperty} property + * @param {ObjectExpression} node */ - function verify(property) { + 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() @@ -137,7 +140,7 @@ module.exports = { return utils.compositingVisitors( utils.defineVueVisitor(context, { - Property(node) { + onVueObjectEnter(node) { verify(node) } }), @@ -145,10 +148,7 @@ module.exports = { onDefineOptionsEnter(node) { const expression = node.arguments[0] if (expression.type === 'ObjectExpression') { - const property = utils.findProperty(expression, 'name') - if (!property) return - - verify(property) + verify(expression) } } })