Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ignore option to vue/multi-word-component-names rule #1681

Merged
merged 1 commit into from Oct 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
41 changes: 39 additions & 2 deletions docs/rules/multi-word-component-names.md
Expand Up @@ -65,6 +65,7 @@ export default {
<eslint-code-block filename="src/Todo.vue" :rules="{'vue/multi-word-component-names': ['error']}">

```vue
<!-- filename: Todo.vue -->
<script>
/* ✗ BAD */
export default {
Expand All @@ -77,11 +78,47 @@ export default {

## :wrench: Options

Nothing.
```json
{
"vue/multi-word-component-names": ["error", {
"ignores": []
}]
}
```

- `ignores` (`string[]`) ... The component names to ignore. Sets the component name to allow.

### `ignores: ["Todo"]`

<eslint-code-block fix :rules="{'vue/multi-word-component-names': ['error', {ignores: ['Todo']}]}">

```vue
<script>
export default {
/* ✓ GOOD */
name: 'Todo'
}
</script>
```

</eslint-code-block>

<eslint-code-block fix :rules="{'vue/multi-word-component-names': ['error', {ignores: ['Todo']}]}">

```vue
<script>
export default {
/* ✗ BAD */
name: 'Item'
}
</script>
```

</eslint-code-block>

## :books: Further Reading

- [Style guide - Multi-word component names](https://vuejs.org/v2/style-guide/#Multi-word-component-names-essential)
- [Style guide - Multi-word component names](https://v3.vuejs.org/style-guide/#multi-word-component-names-essential)

## :rocket: Version

Expand Down
136 changes: 76 additions & 60 deletions lib/rules/multi-word-component-names.js
Expand Up @@ -15,23 +15,6 @@ 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
// ------------------------------------------------------------------------------
Expand All @@ -44,22 +27,92 @@ module.exports = {
categories: ['vue3-essential', 'essential'],
url: 'https://eslint.vuejs.org/rules/multi-word-component-names.html'
},
schema: [],
schema: [
{
type: 'object',
properties: {
ignores: {
type: 'array',
items: { type: 'string' },
uniqueItems: true,
additionalItems: false
}
},
additionalProperties: false
}
],
messages: {
unexpected: 'Component name "{{value}}" should always be multi-word.'
}
},
/** @param {RuleContext} context */
create(context) {
const fileName = context.getFilename()
let componentName = fileName.replace(/\.[^/.]+$/, '')
/** @type {Set<string>} */
const ignores = new Set()
ignores.add('App')
ignores.add('app')
for (const ignore of (context.options[0] && context.options[0].ignores) ||
[]) {
ignores.add(ignore)
if (casing.isPascalCase(ignore)) {
// PascalCase
ignores.add(casing.kebabCase(ignore))
}
}
let hasVue = false
let hasName = false

/**
* Returns true if the given component name is valid, otherwise false.
* @param {string} name
* */
function isValidComponentName(name) {
if (ignores.has(name) || RESERVED_NAMES_IN_VUE3.has(name)) {
return true
}
const elements = casing.kebabCase(name).split('-')
return elements.length > 1
}

/**
* @param {Expression | SpreadElement} nameNode
*/
function validateName(nameNode) {
if (nameNode.type !== 'Literal') return
const componentName = `${nameNode.value}`
if (!isValidComponentName(componentName)) {
context.report({
node: nameNode,
messageId: 'unexpected',
data: {
value: componentName
}
})
}
}

return utils.compositingVisitors(
utils.executeOnCallVueComponent(context, (node) => {
hasVue = true
if (node.arguments.length !== 2) return
hasName = true
validateName(node.arguments[0])
}),
utils.executeOnVue(context, (obj) => {
hasVue = true
const node = utils.findProperty(obj, 'name')
if (!node) return
hasName = true
validateName(node.value)
}),
{
/** @param {Program} node */
Program(node) {
'Program:exit'(node) {
if (hasName) return
if (!hasVue && node.body.length > 0) return
const fileName = context.getFilename()
const componentName = fileName.replace(/\.[^/.]+$/, '')
if (
!node.body.length &&
utils.isVueFile(fileName) &&
!isValidComponentName(componentName)
) {
Expand All @@ -72,44 +125,7 @@ module.exports = {
})
}
}
},

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 }
})
}
})
}
)
}
}
28 changes: 28 additions & 0 deletions tests/lib/rules/multi-word-component-names.js
Expand Up @@ -158,6 +158,17 @@ tester.run('multi-word-component-names', rule, {
Vue.component('TheTest', {})
</script>
`
},
{
filename: 'test.vue',
options: [{ ignores: ['Todo'] }],
code: `
<script>
export default {
name: 'Todo'
}
</script>
`
}
],
invalid: [
Expand Down Expand Up @@ -248,6 +259,23 @@ tester.run('multi-word-component-names', rule, {
line: 3
}
]
},
{
filename: 'test.vue',
options: [{ ignores: ['Todo'] }],
code: `
<script>
export default {
name: 'Item'
}
</script>
`,
errors: [
{
message: 'Component name "Item" should always be multi-word.',
line: 4
}
]
}
]
})