Skip to content

Commit

Permalink
⭐️New: Add vue/match-component-file-name rule (#668)
Browse files Browse the repository at this point in the history
* match-component-file-name rule

* add tests when there is no name attribute and improve error message

* apply suggestions from @armano2

* refactor to have file extensions in options

* revert using the spread operator in tests

* revert using the spread operator in invalid tests

* refactor to handle Vue.component(...) and improve options

* improved documentation with usage example

* added tests recommended by @armano2

* ignore rule when file has multiple components

* accept mixed cases between component name and file name

* apply suggestions from @mysticatea

* add quotes to file name in error message

* improve docs

* add shouldMatchCase option

* Improve docs

Co-Authored-By: rodrigopedra <rodrigo.pedra@gmail.com>

* Update match-component-file-name.js

* Update match-component-file-name.js
  • Loading branch information
rodrigopedra authored and ota-meshi committed Nov 24, 2018
1 parent 78bd936 commit c6bbd95
Show file tree
Hide file tree
Showing 5 changed files with 1,075 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -237,6 +237,7 @@ Enforce all the rules in this category, as well as all higher priority rules, wi

| | Rule ID | Description |
|:---|:--------|:------------|
| | [vue/match-component-file-name](./docs/rules/match-component-file-name.md) | require component name property to match its file name |
| :wrench: | [vue/script-indent](./docs/rules/script-indent.md) | enforce consistent indentation in `<script>` |

### Deprecated
Expand Down
204 changes: 204 additions & 0 deletions docs/rules/match-component-file-name.md
@@ -0,0 +1,204 @@
# require component name property to match its file name (vue/match-component-file-name)

This rule reports if a component `name` property does not match its file name.

You can define an array of file extensions this rule should verify for
the component's name.

## :book: Rule Details

This rule has some options.

```json
{
"vue/match-component-file-name": ["error", {
"extensions": ["jsx"],
"shouldMatchCase": false
}]
}
```

By default this rule will only verify components in a file with a `.jsx`
extension.

You can use any combination of `".jsx"`, `".vue"` and `".js"` extensions.

You can also enforce same case between the component's name and its file name.

If you are defining multiple components within the same file, this rule will be ignored.

:-1: Examples of **incorrect** code for this rule:

```jsx
// file name: src/MyComponent.jsx
export default {
name: 'MComponent', // note the missing y
render: () {
return <h1>Hello world</h1>
}
}
```

```vue
// file name: src/MyComponent.vue
// options: {extensions: ["vue"]}
<script>
export default {
name: 'MComponent',
template: '<div />'
}
</script>
```

```js
// file name: src/MyComponent.js
// options: {extensions: ["js"]}
new Vue({
name: 'MComponent',
template: '<div />'
})
```

```js
// file name: src/MyComponent.js
// options: {extensions: ["js"]}
Vue.component('MComponent', {
template: '<div />'
})
```

```jsx
// file name: src/MyComponent.jsx
// options: {shouldMatchCase: true}
export default {
name: 'my-component',
render() { return <div /> }
}
```

```jsx
// file name: src/my-component.jsx
// options: {shouldMatchCase: true}
export default {
name: 'MyComponent',
render() { return <div /> }
}
```

:+1: Examples of **correct** code for this rule:

```jsx
// file name: src/MyComponent.jsx
export default {
name: 'MyComponent',
render: () {
return <h1>Hello world</h1>
}
}
```

```jsx
// file name: src/MyComponent.jsx
// no name property defined
export default {
render: () {
return <h1>Hello world</h1>
}
}
```

```vue
// file name: src/MyComponent.vue
<script>
export default {
name: 'MyComponent',
template: '<div />'
}
</script>
```

```vue
// file name: src/MyComponent.vue
<script>
export default {
template: '<div />'
}
</script>
```

```js
// file name: src/MyComponent.js
new Vue({
name: 'MyComponent',
template: '<div />'
})
```

```js
// file name: src/MyComponent.js
new Vue({
template: '<div />'
})
```

```js
// file name: src/MyComponent.js
Vue.component('MyComponent', {
template: '<div />'
})
```

```js
// file name: src/components.js
// defines multiple components, so this rule is ignored
Vue.component('MyComponent', {
template: '<div />'
})

Vue.component('OtherComponent', {
template: '<div />'
})

new Vue({
name: 'ThirdComponent',
template: '<div />'
})
```

```jsx
// file name: src/MyComponent.jsx
// options: {shouldMatchCase: true}
export default {
name: 'MyComponent',
render() { return <div /> }
}
```

```jsx
// file name: src/my-component.jsx
// options: {shouldMatchCase: true}
export default {
name: 'my-component',
render() { return <div /> }
}
```

## :wrench: Options

```json
{
"vue/match-component-file-name": ["error", {
"extensions": ["jsx"],
"shouldMatchCase": false
}]
}
```

- `"extensions": []` ... array of file extensions to be verified. Default is set to `["jsx"]`.
- `"shouldMatchCase": false` ... boolean indicating if component's name
should also match its file name case. Default is set to `false`.

## :books: Further reading

- [Style guide - Single-file component filename casing](https://vuejs.org/v2/style-guide/#Single-file-component-filename-casing-strongly-recommended)

1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -18,6 +18,7 @@ module.exports = {
'html-quotes': require('./rules/html-quotes'),
'html-self-closing': require('./rules/html-self-closing'),
'jsx-uses-vars': require('./rules/jsx-uses-vars'),
'match-component-file-name': require('./rules/match-component-file-name'),
'max-attributes-per-line': require('./rules/max-attributes-per-line'),
'multiline-html-element-content-newline': require('./rules/multiline-html-element-content-newline'),
'mustache-interpolation-spacing': require('./rules/mustache-interpolation-spacing'),
Expand Down
140 changes: 140 additions & 0 deletions lib/rules/match-component-file-name.js
@@ -0,0 +1,140 @@
/**
* @fileoverview Require component name property to match its file name
* @author Rodrigo Pedra Brum <rodrigo.pedra@gmail.com>
*/
'use strict'

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

const utils = require('../utils')
const casing = require('../utils/casing')
const path = require('path')

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
meta: {
docs: {
description: 'require component name property to match its file name',
category: undefined,
url: 'https://github.com/vuejs/eslint-plugin-vue/blob/v5.0.0-beta.5/docs/rules/match-component-file-name.md'
},
fixable: null,
schema: [
{
type: 'object',
properties: {
extensions: {
type: 'array',
items: {
type: 'string'
},
uniqueItems: true,
additionalItems: false
},
shouldMatchCase: {
type: 'boolean'
}
},
additionalProperties: false
}
]
},

create (context) {
const options = context.options[0]
const shouldMatchCase = (options && options.shouldMatchCase) || false
const extensionsArray = options && options.extensions
const allowedExtensions = Array.isArray(extensionsArray) ? extensionsArray : ['jsx']

const extension = path.extname(context.getFilename())
const filename = path.basename(context.getFilename(), extension)

const errors = []
let componentCount = 0

if (!allowedExtensions.includes(extension.replace(/^\./, ''))) {
return {}
}

// ----------------------------------------------------------------------
// Private
// ----------------------------------------------------------------------

function compareNames (name, filename) {
if (shouldMatchCase) {
return name === filename
}

return casing.pascalCase(name) === filename || casing.kebabCase(name) === filename
}

function verifyName (node) {
let name
if (node.type === 'TemplateLiteral') {
const quasis = node.quasis[0]
name = quasis.value.cooked
} else {
name = node.value
}

if (!compareNames(name, filename)) {
errors.push({
node: node,
message: 'Component name `{{name}}` should match file name `{{filename}}`.',
data: { filename, name }
})
}
}

function canVerify (node) {
return node.type === 'Literal' || (
node.type === 'TemplateLiteral' &&
node.expressions.length === 0 &&
node.quasis.length === 1
)
}

return Object.assign({},
{
"CallExpression > MemberExpression > Identifier[name='component']" (node) {
const parent = node.parent.parent
const calleeObject = utils.unwrapTypes(parent.callee.object)

if (calleeObject.type === 'Identifier' && calleeObject.name === 'Vue') {
if (parent.arguments && parent.arguments.length === 2) {
const argument = parent.arguments[0]
if (canVerify(argument)) {
verifyName(argument)
}
}
}
}
},
utils.executeOnVue(context, (object) => {
const node = object.properties
.find(item => (
item.type === 'Property' &&
item.key.name === 'name' &&
canVerify(item.value)
))

componentCount++

if (!node) return
verifyName(node.value)
}),
{
'Program:exit' () {
if (componentCount > 1) return

errors.forEach((error) => context.report(error))
}
}
)
}
}

0 comments on commit c6bbd95

Please sign in to comment.