diff --git a/README.md b/README.md
index 04a8425e6..b1e96c050 100644
--- a/README.md
+++ b/README.md
@@ -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 `
+```
+
+```js
+// file name: src/MyComponent.js
+// options: {extensions: ["js"]}
+new Vue({
+ name: 'MComponent',
+ template: '
'
+})
+```
+
+```js
+// file name: src/MyComponent.js
+// options: {extensions: ["js"]}
+Vue.component('MComponent', {
+ template: ''
+})
+```
+
+```jsx
+// file name: src/MyComponent.jsx
+// options: {shouldMatchCase: true}
+export default {
+ name: 'my-component',
+ render() { return }
+}
+```
+
+```jsx
+// file name: src/my-component.jsx
+// options: {shouldMatchCase: true}
+export default {
+ name: 'MyComponent',
+ render() { return }
+}
+```
+
+:+1: Examples of **correct** code for this rule:
+
+```jsx
+// file name: src/MyComponent.jsx
+export default {
+ name: 'MyComponent',
+ render: () {
+ return Hello world
+ }
+}
+```
+
+```jsx
+// file name: src/MyComponent.jsx
+// no name property defined
+export default {
+ render: () {
+ return Hello world
+ }
+}
+```
+
+```vue
+// file name: src/MyComponent.vue
+
+```
+
+```vue
+// file name: src/MyComponent.vue
+
+```
+
+```js
+// file name: src/MyComponent.js
+new Vue({
+ name: 'MyComponent',
+ template: ''
+})
+```
+
+```js
+// file name: src/MyComponent.js
+new Vue({
+ template: ''
+})
+```
+
+```js
+// file name: src/MyComponent.js
+Vue.component('MyComponent', {
+ template: ''
+})
+```
+
+```js
+// file name: src/components.js
+// defines multiple components, so this rule is ignored
+Vue.component('MyComponent', {
+ template: ''
+})
+
+Vue.component('OtherComponent', {
+ template: ''
+})
+
+new Vue({
+ name: 'ThirdComponent',
+ template: ''
+})
+```
+
+```jsx
+// file name: src/MyComponent.jsx
+// options: {shouldMatchCase: true}
+export default {
+ name: 'MyComponent',
+ render() { return }
+}
+```
+
+```jsx
+// file name: src/my-component.jsx
+// options: {shouldMatchCase: true}
+export default {
+ name: 'my-component',
+ render() { return }
+}
+```
+
+## :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)
+
diff --git a/lib/index.js b/lib/index.js
index 5952af45a..e6e5a8904 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -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'),
diff --git a/lib/rules/match-component-file-name.js b/lib/rules/match-component-file-name.js
new file mode 100644
index 000000000..b206d46bf
--- /dev/null
+++ b/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
+ */
+'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))
+ }
+ }
+ )
+ }
+}
diff --git a/tests/lib/rules/match-component-file-name.js b/tests/lib/rules/match-component-file-name.js
new file mode 100644
index 000000000..1a56e45ee
--- /dev/null
+++ b/tests/lib/rules/match-component-file-name.js
@@ -0,0 +1,729 @@
+/**
+ * @fileoverview Require component name property to match its file name
+ * @author Rodrigo Pedra Brum
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/match-component-file-name')
+const RuleTester = require('eslint').RuleTester
+
+const jsxParserOptions = {
+ ecmaVersion: 2018,
+ sourceType: 'module',
+ ecmaFeatures: { jsx: true }
+}
+
+const parserOptions = {
+ ecmaVersion: 2018,
+ sourceType: 'module'
+}
+
+const ruleTester = new RuleTester()
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+ruleTester.run('match-component-file-name', rule, {
+ valid: [
+ // .jsx
+ {
+ filename: 'MyComponent.jsx',
+ code: `
+ export default {
+ render() { return }
+ }
+ `,
+ parserOptions: jsxParserOptions
+ },
+ {
+ filename: 'MyComponent.jsx',
+ code: `
+ export default {
+ name: 'MyComponent',
+ render() { return }
+ }
+ `,
+ parserOptions: jsxParserOptions
+ },
+ {
+ filename: 'MyComponent.jsx',
+ code: `
+ export default {
+ name: 'MyComponent',
+ render() { return }
+ }
+ `,
+ options: [{ extensions: ['jsx'] }],
+ parserOptions: jsxParserOptions
+ },
+ {
+ filename: 'MyComponent.jsx',
+ code: `
+ export default {
+ name: myComponent,
+ render() { return }
+ }
+ `,
+ options: [{ extensions: ['jsx'] }],
+ parserOptions: jsxParserOptions
+ },
+ {
+ filename: 'MyComponent.jsx',
+ code: `
+ export default {
+ name,
+ render() { return }
+ }
+ `,
+ options: [{ extensions: ['jsx'] }],
+ parserOptions: jsxParserOptions
+ },
+ {
+ filename: 'MyComponent.jsx',
+ code: `
+ export default {
+ name: \`MyComponent\`,
+ render() { return }
+ }
+ `,
+ options: [{ extensions: ['jsx'] }],
+ parserOptions: jsxParserOptions
+ },
+ {
+ filename: 'MyComponent.jsx',
+ code: `
+ export default {
+ name: \`My\${foo}\`,
+ render() { return }
+ }
+ `,
+ options: [{ extensions: ['jsx'] }],
+ parserOptions: jsxParserOptions
+ },
+ {
+ filename: 'MyComponent.jsx',
+ code: `
+ export default {
+ name: 'MComponent',
+ render() { return }
+ }
+ `,
+ options: [{ extensions: ['vue'] }], // missing jsx in options
+ parserOptions: jsxParserOptions
+ },
+
+ // .vue
+ {
+ filename: 'MyComponent.vue',
+ code: `
+
+ `,
+ parser: 'vue-eslint-parser',
+ parserOptions // options default to [['jsx']]
+ },
+ {
+ filename: 'MyComponent.vue',
+ code: `
+
+ `,
+ options: [{ extensions: ['jsx'] }], // missing jsx in options
+ parser: 'vue-eslint-parser',
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.vue',
+ code: `
+
+ `,
+ options: [{ extensions: ['vue'] }],
+ parser: 'vue-eslint-parser',
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.vue',
+ code: `
+
+
+
+ `,
+ options: [{ extensions: ['vue'] }],
+ parser: 'vue-eslint-parser',
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.vue',
+ code: `
+
+ `,
+ options: [{ extensions: ['vue'] }],
+ parser: 'vue-eslint-parser',
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.vue',
+ code: `
+
+ `,
+ options: [{ extensions: ['vue'] }],
+ parser: 'vue-eslint-parser',
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.vue',
+ code: `
+
+ `,
+ options: [{ extensions: ['vue'] }],
+ parser: 'vue-eslint-parser',
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.vue',
+ code: `
+
+ `,
+ options: [{ extensions: ['vue'] }],
+ parser: 'vue-eslint-parser',
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.vue',
+ code: `
+
+ `,
+ options: [{ extensions: ['vue'] }],
+ parser: 'vue-eslint-parser',
+ parserOptions
+ },
+
+ // .js
+ {
+ filename: 'MyComponent.js',
+ code: `
+ new Vue({
+ name: 'MComponent',
+ template: ''
+ })
+ `,
+ parserOptions // options default to [['jsx']]
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ Vue.mixin({})
+ `,
+ parserOptions // options default to [['jsx']]
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ Vue.component('MComponent', {
+ template: ''
+ })
+ `,
+ parserOptions // options default to [['jsx']]
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ new Vue({
+ name: 'MComponent',
+ template: ''
+ })
+ `,
+ options: [{ extensions: ['vue'] }], // missing 'js' in options
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ Vue.mixin({})
+ `,
+ options: [{ extensions: ['vue'] }], // missing 'js' in options
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ Vue.component('MComponent', {
+ template: ''
+ })
+ `,
+ options: [{ extensions: ['vue'] }], // missing 'js' in options
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ new Vue({
+ template: ''
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ new Vue({
+ name: 'MyComponent',
+ template: ''
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ new Vue({
+ name: myComponent,
+ template: ''
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ new Vue({
+ name,
+ template: ''
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ new Vue({
+ name: \`MyComponent\`,
+ template: ''
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ new Vue({
+ name: \`My\${foo}\`,
+ template: ''
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ Vue.mixin({})
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ new Vue.mixin({
+ name: 'MyComponent',
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ new Vue.mixin({
+ name: myComponent,
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ new Vue.mixin({
+ name
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ new Vue.mixin({
+ name: \`MyComponent\`,
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ new Vue.mixin({
+ name: \`My\${foo}\`,
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ Vue.component('MyComponent', {
+ template: ''
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ Vue.component(myComponent, {
+ template: ''
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ Vue.component(\`MyComponent\`, {
+ template: ''
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ Vue.component(\`My\${foo}\`, {
+ template: ''
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions
+ },
+ {
+ filename: 'index.js',
+ code: `
+ Vue.component('MyComponent', {
+ template: ''
+ })
+
+ Vue.component('OtherComponent', {
+ template: ''
+ })
+
+ new Vue('OtherComponent', {
+ name: 'ThirdComponent',
+ template: ''
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions
+ },
+
+ // casing
+ {
+ filename: 'my-component.jsx',
+ code: `
+ export default {
+ name: 'my-component',
+ render() { return }
+ }
+ `,
+ parserOptions: jsxParserOptions
+ },
+ {
+ filename: 'my-component.jsx',
+ code: `
+ export default {
+ name: 'MyComponent',
+ render() { return }
+ }
+ `,
+ parserOptions: jsxParserOptions
+ },
+ {
+ filename: 'MyComponent.jsx',
+ code: `
+ export default {
+ name: 'my-component',
+ render() { return }
+ }
+ `,
+ parserOptions: jsxParserOptions
+ },
+ {
+ filename: 'my-component.jsx',
+ code: `
+ export default {
+ name: 'my-component',
+ render() { return }
+ }
+ `,
+ options: [{ shouldMatchCase: true }],
+ parserOptions: jsxParserOptions
+ },
+ {
+ filename: 'MyComponent.jsx',
+ code: `
+ export default {
+ name: 'MyComponent',
+ render() { return }
+ }
+ `,
+ options: [{ shouldMatchCase: true }],
+ parserOptions: jsxParserOptions
+ }
+ ],
+
+ invalid: [
+ // .jsx
+ {
+ filename: 'MyComponent.jsx',
+ code: `
+ export default {
+ name: 'MComponent',
+ render() { return }
+ }
+ `,
+ parserOptions: jsxParserOptions,
+ errors: [{
+ message: 'Component name `MComponent` should match file name `MyComponent`.'
+ }]
+ },
+ {
+ filename: 'MyComponent.jsx',
+ code: `
+ export default {
+ name: 'MComponent',
+ render() { return }
+ }
+ `,
+ options: [{ extensions: ['jsx'] }],
+ parserOptions: jsxParserOptions,
+ errors: [{
+ message: 'Component name `MComponent` should match file name `MyComponent`.'
+ }]
+ },
+ {
+ filename: 'MyComponent.jsx',
+ code: `
+ export default {
+ name: \`MComponent\`,
+ render() { return }
+ }
+ `,
+ options: [{ extensions: ['jsx'] }],
+ parserOptions: jsxParserOptions,
+ errors: [{
+ message: 'Component name `MComponent` should match file name `MyComponent`.'
+ }]
+ },
+
+ // .vue
+ {
+ filename: 'MyComponent.vue',
+ code: `
+
+ `,
+ options: [{ extensions: ['vue'] }],
+ parser: 'vue-eslint-parser',
+ parserOptions,
+ errors: [{
+ message: 'Component name `MComponent` should match file name `MyComponent`.'
+ }]
+ },
+ {
+ filename: 'MyComponent.vue',
+ code: `
+
+ `,
+ options: [{ extensions: ['vue'] }],
+ parser: 'vue-eslint-parser',
+ parserOptions,
+ errors: [{
+ message: 'Component name `MComponent` should match file name `MyComponent`.'
+ }]
+ },
+
+ // .js
+ {
+ filename: 'MyComponent.js',
+ code: `
+ new Vue({
+ name: 'MComponent',
+ template: ''
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions,
+ errors: [{
+ message: 'Component name `MComponent` should match file name `MyComponent`.'
+ }]
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ new Vue({
+ name: \`MComponent\`,
+ template: ''
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions,
+ errors: [{
+ message: 'Component name `MComponent` should match file name `MyComponent`.'
+ }]
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ Vue.mixin({
+ name: 'MComponent',
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions,
+ errors: [{
+ message: 'Component name `MComponent` should match file name `MyComponent`.'
+ }]
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ Vue.mixin({
+ name: \`MComponent\`,
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions,
+ errors: [{
+ message: 'Component name `MComponent` should match file name `MyComponent`.'
+ }]
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ Vue.component('MComponent', {
+ template: ''
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions,
+ errors: [{
+ message: 'Component name `MComponent` should match file name `MyComponent`.'
+ }]
+ },
+ {
+ filename: 'MyComponent.js',
+ code: `
+ Vue.component(\`MComponent\`, {
+ template: ''
+ })
+ `,
+ options: [{ extensions: ['js'] }],
+ parserOptions,
+ errors: [{
+ message: 'Component name `MComponent` should match file name `MyComponent`.'
+ }]
+ },
+
+ // casing
+ {
+ filename: 'MyComponent.jsx',
+ code: `
+ export default {
+ name: 'my-component',
+ render() { return }
+ }
+ `,
+ options: [{ shouldMatchCase: true }],
+ parserOptions: jsxParserOptions,
+ errors: [{
+ message: 'Component name `my-component` should match file name `MyComponent`.'
+ }]
+ },
+ {
+ filename: 'my-component.jsx',
+ code: `
+ export default {
+ name: 'MyComponent',
+ render() { return }
+ }
+ `,
+ options: [{ shouldMatchCase: true }],
+ parserOptions: jsxParserOptions,
+ errors: [{
+ message: 'Component name `MyComponent` should match file name `my-component`.'
+ }]
+ }
+ ]
+})