Skip to content

Commit

Permalink
Add multi-word-component-names rule (#1661)
Browse files Browse the repository at this point in the history
* Add `multi-word-component-names` rule

* Fix review comments for `multi-word-component-names` rule
  • Loading branch information
csordasmarton committed Oct 18, 2021
1 parent 928e0c6 commit a74dd59
Show file tree
Hide file tree
Showing 5 changed files with 454 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/rules/README.md
Expand Up @@ -296,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 `<template>` | |
Expand Down
84 changes: 84 additions & 0 deletions docs/rules/multi-word-component-names.md
@@ -0,0 +1,84 @@
---
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: <badge text="This rule has not been released yet." vertical="middle" type="error"> ***This rule has not been released yet.*** </badge>

## :book: Rule Details

This rule require component names to be always multi-word, except for root `App`
components, and built-in components provided by Vue, such as `<transition>` or
`<component>`. This prevents conflicts with existing and future HTML elements,
since all HTML elements are a single word.

<eslint-code-block filename="src/TodoItem.js" language="javascript" :rules="{'vue/multi-word-component-names': ['error']}">

```js
/* ✓ GOOD */
Vue.component('todo-item', {
// ...
})

/* ✗ BAD */
Vue.component('Todo', {
// ...
})
```
</eslint-code-block>

<eslint-code-block filename="src/TodoItem.js" :rules="{'vue/multi-word-component-names': ['error']}">

```vue
<script>
/* ✓ GOOD */
export default {
name: 'TodoItem',
// ...
}
</script>
```
</eslint-code-block>

<eslint-code-block filename="src/Todo.vue" :rules="{'vue/multi-word-component-names': ['error']}">

```vue
<script>
/* ✗ BAD */
export default {
name: 'Todo',
// ...
}
</script>
```
</eslint-code-block>

<eslint-code-block filename="src/Todo.vue" :rules="{'vue/multi-word-component-names': ['error']}">

```vue
<script>
/* ✗ BAD */
export default {
// ...
}
</script>
```
</eslint-code-block>

## :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)
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -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'),
Expand Down
115 changes: 115 additions & 0 deletions lib/rules/multi-word-component-names.js
@@ -0,0 +1,115 @@
/**
* @author Marton Csordas
* See LICENSE file in root directory for full license.
*/
'use strict'

// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------

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: 'suggestion',
docs: {
description: 'require component names to be always multi-word',
categories: undefined,
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 = fileName.replace(/\.[^/.]+$/, '')

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')

/** @type {SourceLocation | null} */
let loc = null

// Check if the component has a name property.
if (node) {
const valueNode = node.value
if (valueNode.type !== 'Literal') return

componentName = `${valueNode.value}`
loc = node.loc
} 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}`
loc = argument.loc
}

if (!isValidComponentName(componentName)) {
context.report({
messageId: 'unexpected',
data: {
value: componentName
},
loc: loc || { line: 1, column: 0 }
})
}
})
)
}
}

0 comments on commit a74dd59

Please sign in to comment.