From a7b80309a2c0707b91674d5a7e1177b7a002480b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Csord=C3=A1s?= Date: Sun, 17 Oct 2021 09:37:43 +0200 Subject: [PATCH 1/2] Add `multi-word-component-names` rule --- docs/rules/README.md | 2 + docs/rules/multi-word-component-names.md | 58 ++++ lib/configs/essential.js | 1 + lib/configs/vue3-essential.js | 1 + lib/index.js | 1 + lib/rules/multi-word-component-names.js | 111 ++++++++ tests/lib/rules/multi-word-component-names.js | 253 ++++++++++++++++++ 7 files changed, 427 insertions(+) create mode 100644 docs/rules/multi-word-component-names.md create mode 100644 lib/rules/multi-word-component-names.js create mode 100644 tests/lib/rules/multi-word-component-names.js diff --git a/docs/rules/README.md b/docs/rules/README.md index 3f65e92c4..37ad73228 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -39,6 +39,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi | Rule ID | Description | | |:--------|:------------|:---| +| [vue/multi-word-component-names](./multi-word-component-names.md) | require component names to be always multi-word | | | [vue/no-arrow-functions-in-watch](./no-arrow-functions-in-watch.md) | disallow using arrow functions to define watcher | | | [vue/no-async-in-computed-properties](./no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties | | | [vue/no-deprecated-data-object-declaration](./no-deprecated-data-object-declaration.md) | disallow using deprecated object declaration on data (in Vue.js 3.0.0+) | :wrench: | @@ -171,6 +172,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi | Rule ID | Description | | |:--------|:------------|:---| +| [vue/multi-word-component-names](./multi-word-component-names.md) | require component names to be always multi-word | | | [vue/no-arrow-functions-in-watch](./no-arrow-functions-in-watch.md) | disallow using arrow functions to define watcher | | | [vue/no-async-in-computed-properties](./no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties | | | [vue/no-custom-modifiers-on-v-model](./no-custom-modifiers-on-v-model.md) | disallow custom modifiers on v-model used on the component | | diff --git a/docs/rules/multi-word-component-names.md b/docs/rules/multi-word-component-names.md new file mode 100644 index 000000000..027422150 --- /dev/null +++ b/docs/rules/multi-word-component-names.md @@ -0,0 +1,58 @@ +--- +pageClass: rule-details +sidebarDepth: 0 +title: vue/multi-word-component-names +description: require component names to be always multi-word +--- +# vue/multi-word-component-names + +> require component names to be always multi-word + +- :exclamation: ***This rule has not been released yet.*** +- :gear: This rule is included in all of `"plugin:vue/vue3-essential"`, `"plugin:vue/essential"`, `"plugin:vue/vue3-strongly-recommended"`, `"plugin:vue/strongly-recommended"`, `"plugin:vue/vue3-recommended"` and `"plugin:vue/recommended"`. + +## :book: Rule Details + +This rule .... + + + +```vue + +``` + + + +## :wrench: Options + +Nothing. + +## :books: Further Reading + +- [Style guide - Multi-word component names](https://vuejs.org/v2/style-guide/#Multi-word-component-names-essential) + +## :mag: Implementation + +- [Rule source](https://github.com/vuejs/eslint-plugin-vue/blob/master/lib/rules/multi-word-component-names.js) +- [Test source](https://github.com/vuejs/eslint-plugin-vue/blob/master/tests/lib/rules/multi-word-component-names.js) diff --git a/lib/configs/essential.js b/lib/configs/essential.js index dca31cd2e..3f49f4bd0 100644 --- a/lib/configs/essential.js +++ b/lib/configs/essential.js @@ -6,6 +6,7 @@ module.exports = { extends: require.resolve('./base'), rules: { + 'vue/multi-word-component-names': 'error', 'vue/no-arrow-functions-in-watch': 'error', 'vue/no-async-in-computed-properties': 'error', 'vue/no-custom-modifiers-on-v-model': 'error', diff --git a/lib/configs/vue3-essential.js b/lib/configs/vue3-essential.js index 802cb62ed..0a677cfee 100644 --- a/lib/configs/vue3-essential.js +++ b/lib/configs/vue3-essential.js @@ -6,6 +6,7 @@ module.exports = { extends: require.resolve('./base'), rules: { + 'vue/multi-word-component-names': 'error', 'vue/no-arrow-functions-in-watch': 'error', 'vue/no-async-in-computed-properties': 'error', 'vue/no-deprecated-data-object-declaration': 'error', diff --git a/lib/index.js b/lib/index.js index b4f4d4a6f..068d01d40 100644 --- a/lib/index.js +++ b/lib/index.js @@ -47,6 +47,7 @@ module.exports = { 'match-component-file-name': require('./rules/match-component-file-name'), 'max-attributes-per-line': require('./rules/max-attributes-per-line'), 'max-len': require('./rules/max-len'), + 'multi-word-component-names': require('./rules/multi-word-component-names'), 'multiline-html-element-content-newline': require('./rules/multiline-html-element-content-newline'), 'mustache-interpolation-spacing': require('./rules/mustache-interpolation-spacing'), 'name-property-casing': require('./rules/name-property-casing'), diff --git a/lib/rules/multi-word-component-names.js b/lib/rules/multi-word-component-names.js new file mode 100644 index 000000000..43638d68a --- /dev/null +++ b/lib/rules/multi-word-component-names.js @@ -0,0 +1,111 @@ +/** + * @author Marton Csordas + * See LICENSE file in root directory for full license. + */ +'use strict' + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ +const path = require('path') + +const casing = require('../utils/casing') +const utils = require('../utils') + +const RESERVED_NAMES_IN_VUE3 = new Set( + require('../utils/vue3-builtin-components') +) + +// ------------------------------------------------------------------------------ +// Helpers +// ------------------------------------------------------------------------------ + +/** + * Returns true if the given component name is valid, otherwise false. + * @param {string} name + * */ +function isValidComponentName(name) { + if (name.toLowerCase() === 'app' || RESERVED_NAMES_IN_VUE3.has(name)) { + return true + } else { + const elements = casing.kebabCase(name).split('-') + return elements.length > 1 + } +} + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'require component names to be always multi-word', + categories: ['vue3-essential', 'essential'], + url: 'https://eslint.vuejs.org/rules/multi-word-component-names.html' + }, + schema: [], + messages: { + unexpected: 'Component name "{{value}}" should always be multi-word.' + } + }, + /** @param {RuleContext} context */ + create(context) { + const fileName = context.getFilename() + let componentName = path.parse(fileName).name + + return utils.compositingVisitors( + { + /** @param {Program} node */ + Program(node) { + if ( + !node.body.length && + utils.isVueFile(fileName) && + !isValidComponentName(componentName) + ) { + context.report({ + messageId: 'unexpected', + data: { + value: componentName + }, + loc: { line: 1, column: 0 } + }) + } + } + }, + + utils.executeOnVue(context, (obj) => { + const node = utils.findProperty(obj, 'name') + + // Check if the component has a name property. + if (node) { + const valueNode = node.value + if (valueNode.type !== 'Literal') return + + componentName = `${valueNode.value}` + } else if ( + obj.parent.type === 'CallExpression' && + obj.parent.arguments.length === 2 + ) { + // The component is registered globally with 'Vue.component', where + // the first paremter is the component name. + const argument = obj.parent.arguments[0] + if (argument.type !== 'Literal') return + + componentName = `${argument.value}` + } + + if (!isValidComponentName(componentName)) { + context.report({ + messageId: 'unexpected', + data: { + value: componentName + }, + node: node || obj + }) + } + }) + ) + } +} diff --git a/tests/lib/rules/multi-word-component-names.js b/tests/lib/rules/multi-word-component-names.js new file mode 100644 index 000000000..402e7d6ac --- /dev/null +++ b/tests/lib/rules/multi-word-component-names.js @@ -0,0 +1,253 @@ +/** + * @author Marton Csordas + * See LICENSE file in root directory for full license. + */ +'use strict' + +const RuleTester = require('eslint').RuleTester +const rule = require('../../../lib/rules/multi-word-component-names') + +const tester = new RuleTester({ + parser: require.resolve('vue-eslint-parser'), + parserOptions: { + ecmaVersion: 2020, + sourceType: 'module' + } +}) + +tester.run('multi-word-component-names', rule, { + valid: [ + { + filename: 'App.vue', + code: '' + }, + { + filename: 'invalid.vue', + code: ` + ` + }, + { + filename: 'invalid.vue', + code: ` + ` + }, + { + filename: 'app.vue', + code: '' + }, + { + filename: 'invalid.vue', + code: ` + ` + }, + { + filename: 'invalid.vue', + code: ` + ` + }, + { + filename: 'transition.vue', + code: '' + }, + { + filename: 'invalid.vue', + code: ` + ` + }, + { + filename: 'invalid.vue', + code: ` + ` + }, + { + filename: 'component.vue', + code: '' + }, + { + filename: 'invalid.vue', + code: ` + ` + }, + { + filename: 'invalid.vue', + code: ` + ` + }, + { + filename: 'multi-word.vue', + code: '' + }, + { + filename: 'invalid.vue', + code: ` + ` + }, + { + filename: 'invalid.vue', + code: ` + ` + }, + { + filename: 'multiWord.vue', + code: '' + }, + { + filename: 'invalid.vue', + code: ` + ` + }, + { + filename: 'invalid.vue', + code: ` + ` + }, + { + filename: 'MultiWord.vue', + code: '' + }, + { + filename: 'invalid.vue', + code: ` + ` + }, + { + filename: 'invalid.vue', + code: ` + ` + }, + { + filename: 'TheTest.vue', + code: ` + + ` + }, + { + filename: 'TheTest.vue', + code: ` + + ` + } + ], + invalid: [ + { + filename: 'test.vue', + code: '', + errors: [ + { + message: 'Component name "test" should always be multi-word.', + line: 1 + } + ] + }, + { + filename: 'test.vue', + code: ` + `, + errors: [ + { + message: 'Component name "test" should always be multi-word.', + line: 3 + } + ] + }, + { + filename: 'test.vue', + code: ` + `, + errors: [ + { + message: 'Component name "test" should always be multi-word.', + line: 3 + } + ] + }, + { + filename: 'valid-name.vue', + code: ` + `, + errors: [ + { + message: 'Component name "invalid" should always be multi-word.', + line: 3 + } + ] + }, + { + filename: 'valid-name.vue', + code: ` + `, + errors: [ + { + message: 'Component name "invalid" should always be multi-word.', + line: 3 + } + ] + }, + { + filename: 'invalid.vue', + code: ` + `, + errors: [ + { + message: 'Component name "invalid" should always be multi-word.', + line: 3 + } + ] + }, + { + filename: 'invalid.vue', + code: ` + `, + errors: [ + { + message: 'Component name "" should always be multi-word.', + line: 3 + } + ] + } + ] +}) From 824d35acfe4093cd0fdea6a45cbd301acc44088a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rton=20Csord=C3=A1s?= Date: Mon, 18 Oct 2021 17:37:07 +0200 Subject: [PATCH 2/2] Fix review comments for `multi-word-component-names` rule --- docs/rules/README.md | 3 +- docs/rules/multi-word-component-names.md | 48 ++++++++++++++----- lib/configs/essential.js | 1 - lib/configs/vue3-essential.js | 1 - lib/rules/multi-word-component-names.js | 14 ++++-- tests/lib/rules/multi-word-component-names.js | 2 +- 6 files changed, 48 insertions(+), 21 deletions(-) diff --git a/docs/rules/README.md b/docs/rules/README.md index 37ad73228..401329378 100644 --- a/docs/rules/README.md +++ b/docs/rules/README.md @@ -39,7 +39,6 @@ Enforce all the rules in this category, as well as all higher priority rules, wi | Rule ID | Description | | |:--------|:------------|:---| -| [vue/multi-word-component-names](./multi-word-component-names.md) | require component names to be always multi-word | | | [vue/no-arrow-functions-in-watch](./no-arrow-functions-in-watch.md) | disallow using arrow functions to define watcher | | | [vue/no-async-in-computed-properties](./no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties | | | [vue/no-deprecated-data-object-declaration](./no-deprecated-data-object-declaration.md) | disallow using deprecated object declaration on data (in Vue.js 3.0.0+) | :wrench: | @@ -172,7 +171,6 @@ Enforce all the rules in this category, as well as all higher priority rules, wi | Rule ID | Description | | |:--------|:------------|:---| -| [vue/multi-word-component-names](./multi-word-component-names.md) | require component names to be always multi-word | | | [vue/no-arrow-functions-in-watch](./no-arrow-functions-in-watch.md) | disallow using arrow functions to define watcher | | | [vue/no-async-in-computed-properties](./no-async-in-computed-properties.md) | disallow asynchronous actions in computed properties | | | [vue/no-custom-modifiers-on-v-model](./no-custom-modifiers-on-v-model.md) | disallow custom modifiers on v-model used on the component | | @@ -298,6 +296,7 @@ For example: | [vue/html-comment-content-spacing](./html-comment-content-spacing.md) | enforce unified spacing in HTML comments | :wrench: | | [vue/html-comment-indent](./html-comment-indent.md) | enforce consistent indentation in HTML comments | :wrench: | | [vue/match-component-file-name](./match-component-file-name.md) | require component name property to match its file name | | +| [vue/multi-word-component-names](./multi-word-component-names.md) | require component names to be always multi-word | | | [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: | | [vue/next-tick-style](./next-tick-style.md) | enforce Promise or callback style in `nextTick` | :wrench: | | [vue/no-bare-strings-in-template](./no-bare-strings-in-template.md) | disallow the use of bare strings in `