From f90ff31b56f1d4360ec91b82d69e83fb1c9b5c10 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Tue, 23 Nov 2021 14:39:17 +0800 Subject: [PATCH 1/8] Add `vue/component-options-name-casing` rule --- docs/rules/README.md | 1 + docs/rules/component-options-name-casing.md | 135 ++++++ lib/index.js | 1 + lib/rules/component-options-name-casing.js | 79 ++++ .../rules/component-options-name-casing.js | 442 ++++++++++++++++++ 5 files changed, 658 insertions(+) create mode 100644 docs/rules/component-options-name-casing.md create mode 100644 lib/rules/component-options-name-casing.js create mode 100644 tests/lib/rules/component-options-name-casing.js diff --git a/docs/rules/README.md b/docs/rules/README.md index 5794cc3ed..777c5649c 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -311,6 +311,7 @@ For example: | [vue/block-tag-newline](./block-tag-newline.md) | enforce line breaks after opening and before closing block-level tags | :wrench: | | [vue/component-api-style](./component-api-style.md) | enforce component API style | | | [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: | +| [vue/component-options-name-casing](./component-options-name-casing.md) | enforce the casing of component name in `components` options | :wrench: | | [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name | | | [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute | | | [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: | diff --git a/docs/rules/component-options-name-casing.md b/docs/rules/component-options-name-casing.md new file mode 100644 index 000000000..cdf1e78cc --- /dev/null +++ b/docs/rules/component-options-name-casing.md @@ -0,0 +1,135 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/component-options-name-casing +description: enforce the casing of component name in `components` options +--- +# vue/component-options-name-casing + +> enforce the casing of component name in `components` options + +- :exclamation: ***This rule has not been released yet.*** +- :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. + +## :book: Rule Details + +This rule aims to enforce casing of the component names in `components` options. + +## :wrench: Options + +```json +{ + "vue/component-options-name-casing": ["error", "PascalCase" | "kebab-case" | "camelCase"] +} +``` + +This rule has an option which can be one of these values: + +- `"PascalCase"` (default) ... enforce component names to pascal case. +- `"kebab-case"` ... enforce component names to kebab case. +- `"camelCase"` ... enforce component names to camel case. + +### `"PascalCase"` (default) + + + +```vue + +``` + + + + + +```vue + +``` + + + +### `"kebab-case"` + + + +```vue + +``` + + + + + +```vue + +``` + + + +### `"camelCase"` + + + +```vue + +``` + + + + + +```vue + +``` + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/component-options-name-casing.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/component-options-name-casing.js) diff --git a/lib/index.js b/lib/index.js index c9969917c..0adbf3f74 100644 --- a/lib/index.js +++ b/lib/index.js @@ -24,6 +24,7 @@ module.exports = { 'component-api-style': require('./rules/component-api-style'), 'component-definition-name-casing': require('./rules/component-definition-name-casing'), 'component-name-in-template-casing': require('./rules/component-name-in-template-casing'), + 'component-options-name-casing': require('./rules/component-options-name-casing'), 'component-tags-order': require('./rules/component-tags-order'), 'custom-event-name-casing': require('./rules/custom-event-name-casing'), 'dot-location': require('./rules/dot-location'), diff --git a/lib/rules/component-options-name-casing.js b/lib/rules/component-options-name-casing.js new file mode 100644 index 000000000..bd5adba96 --- /dev/null +++ b/lib/rules/component-options-name-casing.js @@ -0,0 +1,79 @@ +/** + * @author Pig Fang + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const utils = require('../utils') +const casing = require('../utils/casing') + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: + 'enforce the casing of component name in `components` options', + categories: undefined, + url: 'https://eslint.vuejs.org/rules/component-options-name-casing.html' + }, + fixable: 'code', + schema: [{ enum: casing.allowedCaseOptions }], + messages: { + caseNotMatched: 'Component name "{{component}}" is not {{caseType}}.' + } + }, + /** @param {RuleContext} context */ + create(context) { + const caseType = context.options[0] || 'PascalCase' + + const checkCase = casing.getChecker(caseType) + const convert = casing.getConverter(caseType) + + return utils.executeOnVue(context, (obj) => { + const node = utils.findProperty(obj, 'components') + if (!node || node.value.type !== 'ObjectExpression') { + return + } + + node.value.properties.forEach((property) => { + if ( + property.type === 'Property' && + property.key.type === 'Identifier' + ) { + const { name } = property.key + if (checkCase(name)) { + return + } + + context.report({ + node: property.key, + messageId: 'caseNotMatched', + data: { + component: name, + caseType + }, + fix: (fixer) => { + const converted = convert(name) + if (caseType === 'kebab-case') { + return property.key.range[0] === property.value.range[0] && + property.key.range[1] === property.value.range[1] + ? fixer.replaceText(property, `'${converted}': ${name}`) + : fixer.replaceText(property.key, `'${converted}'`) + } else { + return fixer.replaceText(property.key, converted) + } + } + }) + } + }) + }) + } +} diff --git a/tests/lib/rules/component-options-name-casing.js b/tests/lib/rules/component-options-name-casing.js new file mode 100644 index 000000000..a618e1f37 --- /dev/null +++ b/tests/lib/rules/component-options-name-casing.js @@ -0,0 +1,442 @@ +/** + * @author Pig Fang + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/component-options-name-casing') + +const tester = new RuleTester({ + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('component-options-name-casing', rule, { + valid: [ + { + filename: 'test.vue', + code: ` + export default { + } + ` + }, + { + filename: 'test.vue', + code: ` + export default { + ...components + } + ` + }, + { + filename: 'test.vue', + code: ` + export default { + components: { + FooBar + } + } + ` + }, + { + filename: 'test.vue', + code: ` + export default { + components: { + FooBar: fooBar + } + } + ` + }, + { + filename: 'test.vue', + code: ` + export default { + components: { + FooBar + } + } + `, + options: ['PascalCase'] + }, + { + filename: 'test.vue', + code: ` + export default { + components: { + fooBar + } + } + `, + options: ['camelCase'] + }, + { + filename: 'test.vue', + code: ` + export default { + components: { + fooBar: FooBar + } + } + `, + options: ['camelCase'] + }, + { + filename: 'test.vue', + code: ` + export default { + components: { + 'foo-bar': fooBar + } + } + `, + options: ['kebab-case'] + }, + { + filename: 'test.vue', + code: ` + export default { + components: { + 'foo-bar': FooBar + } + } + `, + options: ['kebab-case'] + } + ], + invalid: [ + { + filename: 'test.vue', + code: ` + export default { + components: { + fooBar + } + } + `, + errors: [ + { + messageId: 'caseNotMatched', + data: { + component: 'fooBar', + caseType: 'PascalCase' + }, + line: 4, + column: 13, + endColumn: 19 + } + ], + output: ` + export default { + components: { + FooBar + } + } + ` + }, + { + filename: 'test.vue', + code: ` + export default { + components: { + fooBar: FooBar + } + } + `, + errors: [ + { + messageId: 'caseNotMatched', + data: { + component: 'fooBar', + caseType: 'PascalCase' + }, + line: 4, + column: 13, + endColumn: 19 + } + ], + output: ` + export default { + components: { + FooBar: FooBar + } + } + ` + }, + { + filename: 'test.vue', + code: ` + export default { + components: { + fooBar + } + } + `, + options: ['PascalCase'], + errors: [ + { + messageId: 'caseNotMatched', + data: { + component: 'fooBar', + caseType: 'PascalCase' + }, + line: 4, + column: 13, + endColumn: 19 + } + ], + output: ` + export default { + components: { + FooBar + } + } + ` + }, + { + filename: 'test.vue', + code: ` + export default { + components: { + fooBar: FooBar + } + } + `, + options: ['PascalCase'], + errors: [ + { + messageId: 'caseNotMatched', + data: { + component: 'fooBar', + caseType: 'PascalCase' + }, + line: 4, + column: 13, + endColumn: 19 + } + ], + output: ` + export default { + components: { + FooBar: FooBar + } + } + ` + }, + { + filename: 'test.vue', + code: ` + export default { + components: { + FooBar + } + } + `, + options: ['camelCase'], + errors: [ + { + messageId: 'caseNotMatched', + data: { + component: 'FooBar', + caseType: 'camelCase' + }, + line: 4, + column: 13, + endColumn: 19 + } + ], + output: ` + export default { + components: { + fooBar + } + } + ` + }, + { + filename: 'test.vue', + code: ` + export default { + components: { + FooBar: fooBar + } + } + `, + options: ['camelCase'], + errors: [ + { + messageId: 'caseNotMatched', + data: { + component: 'FooBar', + caseType: 'camelCase' + }, + line: 4, + column: 13, + endColumn: 19 + } + ], + output: ` + export default { + components: { + fooBar: fooBar + } + } + ` + }, + { + filename: 'test.vue', + code: ` + export default { + components: { + FooBar + } + } + `, + options: ['kebab-case'], + errors: [ + { + messageId: 'caseNotMatched', + data: { + component: 'FooBar', + caseType: 'kebab-case' + }, + line: 4, + column: 13, + endColumn: 19 + } + ], + output: ` + export default { + components: { + 'foo-bar': FooBar + } + } + ` + }, + { + filename: 'test.vue', + code: ` + export default { + components: { + FooBar: fooBar + } + } + `, + options: ['kebab-case'], + errors: [ + { + messageId: 'caseNotMatched', + data: { + component: 'FooBar', + caseType: 'kebab-case' + }, + line: 4, + column: 13, + endColumn: 19 + } + ], + output: ` + export default { + components: { + 'foo-bar': fooBar + } + } + ` + }, + { + filename: 'test.vue', + code: ` + export default { + components: { + fooBar + } + } + `, + options: ['kebab-case'], + errors: [ + { + messageId: 'caseNotMatched', + data: { + component: 'fooBar', + caseType: 'kebab-case' + }, + line: 4, + column: 13, + endColumn: 19 + } + ], + output: ` + export default { + components: { + 'foo-bar': fooBar + } + } + ` + }, + { + filename: 'test.vue', + code: ` + export default { + components: { + fooBar: FooBar + } + } + `, + options: ['kebab-case'], + errors: [ + { + messageId: 'caseNotMatched', + data: { + component: 'fooBar', + caseType: 'kebab-case' + }, + line: 4, + column: 13, + endColumn: 19 + } + ], + output: ` + export default { + components: { + 'foo-bar': FooBar + } + } + ` + }, + { + filename: 'test.vue', + code: ` + export default { + components: { + FooBar, + 'my-component': MyComponent + } + } + `, + options: ['kebab-case'], + errors: [ + { + messageId: 'caseNotMatched', + data: { + component: 'FooBar', + caseType: 'kebab-case' + }, + line: 4, + column: 13, + endColumn: 19 + } + ], + output: ` + export default { + components: { + 'foo-bar': FooBar, + 'my-component': MyComponent + } + } + ` + } + ] +}) From afd39abd382c7ee7637ec7c0262f37d2065c7cc6 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Tue, 23 Nov 2021 15:03:56 +0800 Subject: [PATCH 2/8] fix docs --- docs/rules/component-options-name-casing.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/rules/component-options-name-casing.md b/docs/rules/component-options-name-casing.md index cdf1e78cc..9ca9ec846 100644 --- a/docs/rules/component-options-name-casing.md +++ b/docs/rules/component-options-name-casing.md @@ -129,6 +129,8 @@ export default { ``` + + ## :mag: Implementation - [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/component-options-name-casing.js) From 095d4f4ee711306c963db61b05ab959c55d4ac1c Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Wed, 24 Nov 2021 09:27:43 +0800 Subject: [PATCH 3/8] fix demo --- docs/rules/component-options-name-casing.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/rules/component-options-name-casing.md b/docs/rules/component-options-name-casing.md index 9ca9ec846..c6af99441 100644 --- a/docs/rules/component-options-name-casing.md +++ b/docs/rules/component-options-name-casing.md @@ -31,7 +31,7 @@ This rule has an option which can be one of these values: ### `"PascalCase"` (default) - + ```vue +``` + ### `"PascalCase"` (default) diff --git a/lib/rules/component-options-name-casing.js b/lib/rules/component-options-name-casing.js index 9e398ba7e..af68e056a 100644 --- a/lib/rules/component-options-name-casing.js +++ b/lib/rules/component-options-name-casing.js @@ -19,7 +19,7 @@ const casing = require('../utils/casing') * @param {import('../../typings/eslint-plugin-vue/util-types/ast').Expression} node * @returns {string | null} */ -function getComponentOptionsName(node) { +function getOptionsComponentName(node) { if (node.type === 'Identifier') { return node.name } @@ -43,9 +43,11 @@ module.exports = { url: 'https://eslint.vuejs.org/rules/component-options-name-casing.html' }, fixable: 'code', + hasSuggestions: true, schema: [{ enum: casing.allowedCaseOptions }], messages: { - caseNotMatched: 'Component name "{{component}}" is not {{caseType}}.' + caseNotMatched: 'Component name "{{component}}" is not {{caseType}}.', + possibleRenaming: 'Rename component name to be in {{caseType}}.' } }, /** @param {RuleContext} context */ @@ -67,7 +69,7 @@ module.exports = { return } - const name = getComponentOptionsName(property.key) + const name = getOptionsComponentName(property.key) if (!name) { return } @@ -90,7 +92,26 @@ module.exports = { ? fixer.replaceText(property, `${converted}: ${name}`) : fixer.replaceText(property.key, converted) } - : undefined + : undefined, + suggest: canAutoFix + ? undefined + : [ + { + messageId: 'possibleRenaming', + data: { caseType }, + fix: (fixer) => { + const converted = convert(name) + if (caseType === 'kebab-case') { + return property.shorthand + ? fixer.replaceText(property, `'${converted}': ${name}`) + : fixer.replaceText(property.key, `'${converted}'`) + } + return property.shorthand + ? fixer.replaceText(property, `${converted}: ${name}`) + : fixer.replaceText(property.key, converted) + } + } + ] }) }) }) diff --git a/tests/lib/rules/component-options-name-casing.js b/tests/lib/rules/component-options-name-casing.js index c60d12418..504d98b0c 100644 --- a/tests/lib/rules/component-options-name-casing.js +++ b/tests/lib/rules/component-options-name-casing.js @@ -275,7 +275,20 @@ tester.run('component-options-name-casing', rule, { }, line: 4, column: 13, - endColumn: 19 + endColumn: 19, + suggestions: [ + { + messageId: 'possibleRenaming', + data: { caseType: 'camelCase' }, + output: ` + export default { + components: { + fooBar: FooBar + } + } + ` + } + ] } ], output: null @@ -299,7 +312,20 @@ tester.run('component-options-name-casing', rule, { }, line: 4, column: 13, - endColumn: 19 + endColumn: 19, + suggestions: [ + { + messageId: 'possibleRenaming', + data: { caseType: 'camelCase' }, + output: ` + export default { + components: { + fooBar: fooBar + } + } + ` + } + ] } ], output: null @@ -323,7 +349,20 @@ tester.run('component-options-name-casing', rule, { }, line: 4, column: 13, - endColumn: 22 + endColumn: 22, + suggestions: [ + { + messageId: 'possibleRenaming', + data: { caseType: 'camelCase' }, + output: ` + export default { + components: { + fooBar: fooBar + } + } + ` + } + ] } ], output: null @@ -347,7 +386,20 @@ tester.run('component-options-name-casing', rule, { }, line: 4, column: 13, - endColumn: 19 + endColumn: 19, + suggestions: [ + { + messageId: 'possibleRenaming', + data: { caseType: 'kebab-case' }, + output: ` + export default { + components: { + 'foo-bar': FooBar + } + } + ` + } + ] } ], output: null @@ -371,7 +423,20 @@ tester.run('component-options-name-casing', rule, { }, line: 4, column: 13, - endColumn: 19 + endColumn: 19, + suggestions: [ + { + messageId: 'possibleRenaming', + data: { caseType: 'kebab-case' }, + output: ` + export default { + components: { + 'foo-bar': fooBar + } + } + ` + } + ] } ], output: null @@ -395,7 +460,20 @@ tester.run('component-options-name-casing', rule, { }, line: 4, column: 13, - endColumn: 19 + endColumn: 19, + suggestions: [ + { + messageId: 'possibleRenaming', + data: { caseType: 'kebab-case' }, + output: ` + export default { + components: { + 'foo-bar': fooBar + } + } + ` + } + ] } ], output: null @@ -419,7 +497,20 @@ tester.run('component-options-name-casing', rule, { }, line: 4, column: 13, - endColumn: 19 + endColumn: 19, + suggestions: [ + { + messageId: 'possibleRenaming', + data: { caseType: 'kebab-case' }, + output: ` + export default { + components: { + 'foo-bar': FooBar + } + } + ` + } + ] } ], output: null @@ -444,7 +535,21 @@ tester.run('component-options-name-casing', rule, { }, line: 4, column: 13, - endColumn: 19 + endColumn: 19, + suggestions: [ + { + messageId: 'possibleRenaming', + data: { caseType: 'kebab-case' }, + output: ` + export default { + components: { + 'foo-bar': FooBar, + 'my-component': MyComponent + } + } + ` + } + ] } ], output: null From 714432026163e3a44454709b492bdd719b0fd521 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Wed, 24 Nov 2021 17:09:08 +0800 Subject: [PATCH 8/8] accept suggestions --- docs/rules/README.md | 2 +- docs/rules/component-options-name-casing.md | 3 ++- lib/rules/component-options-name-casing.js | 6 +----- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/rules/README.md b/docs/rules/README.md index 777c5649c..2af1c06ca 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -311,7 +311,7 @@ For example: | [vue/block-tag-newline](./block-tag-newline.md) | enforce line breaks after opening and before closing block-level tags | :wrench: | | [vue/component-api-style](./component-api-style.md) | enforce component API style | | | [vue/component-name-in-template-casing](./component-name-in-template-casing.md) | enforce specific casing for the component naming style in template | :wrench: | -| [vue/component-options-name-casing](./component-options-name-casing.md) | enforce the casing of component name in `components` options | :wrench: | +| [vue/component-options-name-casing](./component-options-name-casing.md) | enforce the casing of component name in `components` options | :wrench::bulb: | | [vue/custom-event-name-casing](./custom-event-name-casing.md) | enforce specific casing for custom event name | | | [vue/html-button-has-type](./html-button-has-type.md) | disallow usage of button without an explicit type attribute | | | [vue/html-comment-content-newline](./html-comment-content-newline.md) | enforce unified line brake in HTML comments | :wrench: | diff --git a/docs/rules/component-options-name-casing.md b/docs/rules/component-options-name-casing.md index 9d39444ff..9a956553b 100644 --- a/docs/rules/component-options-name-casing.md +++ b/docs/rules/component-options-name-casing.md @@ -10,6 +10,7 @@ description: enforce the casing of component name in `components` options - :exclamation: ***This rule has not been released yet.*** - :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. +- :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 @@ -31,7 +32,7 @@ This rule has an option which can be one of these values: Please note that if you use kebab case in `components` options, you can **only** use kebab case in template; -and, if you use camel case in `components` options, +and if you use camel case in `components` options, you **can't** use pascal case in template. For demonstration, the code example is invalid: diff --git a/lib/rules/component-options-name-casing.js b/lib/rules/component-options-name-casing.js index af68e056a..ad70c1546 100644 --- a/lib/rules/component-options-name-casing.js +++ b/lib/rules/component-options-name-casing.js @@ -70,11 +70,7 @@ module.exports = { } const name = getOptionsComponentName(property.key) - if (!name) { - return - } - - if (checkCase(name)) { + if (!name || checkCase(name)) { return }