match-component-file-name.js 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. /**
  2. * @fileoverview Require component name property to match its file name
  3. * @author Rodrigo Pedra Brum <rodrigo.pedra@gmail.com>
  4. */
  5. 'use strict'
  6. // ------------------------------------------------------------------------------
  7. // Requirements
  8. // ------------------------------------------------------------------------------
  9. const utils = require('../utils')
  10. const casing = require('../utils/casing')
  11. const path = require('path')
  12. // ------------------------------------------------------------------------------
  13. // Rule Definition
  14. // ------------------------------------------------------------------------------
  15. module.exports = {
  16. meta: {
  17. type: 'suggestion',
  18. docs: {
  19. description: 'require component name property to match its file name',
  20. categories: undefined,
  21. url: 'https://eslint.vuejs.org/rules/match-component-file-name.html'
  22. },
  23. fixable: null,
  24. schema: [
  25. {
  26. type: 'object',
  27. properties: {
  28. extensions: {
  29. type: 'array',
  30. items: {
  31. type: 'string'
  32. },
  33. uniqueItems: true,
  34. additionalItems: false
  35. },
  36. shouldMatchCase: {
  37. type: 'boolean'
  38. }
  39. },
  40. additionalProperties: false
  41. }
  42. ]
  43. },
  44. /** @param {RuleContext} context */
  45. create(context) {
  46. const options = context.options[0]
  47. const shouldMatchCase = (options && options.shouldMatchCase) || false
  48. const extensionsArray = options && options.extensions
  49. const allowedExtensions = Array.isArray(extensionsArray)
  50. ? extensionsArray
  51. : ['jsx']
  52. const extension = path.extname(context.getFilename())
  53. const filename = path.basename(context.getFilename(), extension)
  54. /** @type {Rule.ReportDescriptor[]} */
  55. const errors = []
  56. let componentCount = 0
  57. if (!allowedExtensions.includes(extension.replace(/^\./, ''))) {
  58. return {}
  59. }
  60. // ----------------------------------------------------------------------
  61. // Private
  62. // ----------------------------------------------------------------------
  63. /**
  64. * @param {string} name
  65. * @param {string} filename
  66. */
  67. function compareNames(name, filename) {
  68. if (shouldMatchCase) {
  69. return name === filename
  70. }
  71. return (
  72. casing.pascalCase(name) === filename ||
  73. casing.kebabCase(name) === filename
  74. )
  75. }
  76. /**
  77. * @param {Literal | TemplateLiteral} node
  78. */
  79. function verifyName(node) {
  80. let name
  81. if (node.type === 'TemplateLiteral') {
  82. const quasis = node.quasis[0]
  83. name = quasis.value.cooked
  84. } else {
  85. name = `${node.value}`
  86. }
  87. if (!compareNames(name, filename)) {
  88. errors.push({
  89. node,
  90. message:
  91. 'Component name `{{name}}` should match file name `{{filename}}`.',
  92. data: { filename, name }
  93. })
  94. }
  95. }
  96. /**
  97. * @param {Expression | SpreadElement} node
  98. * @returns {node is (Literal | TemplateLiteral)}
  99. */
  100. function canVerify(node) {
  101. return (
  102. node.type === 'Literal' ||
  103. (node.type === 'TemplateLiteral' &&
  104. node.expressions.length === 0 &&
  105. node.quasis.length === 1)
  106. )
  107. }
  108. return Object.assign(
  109. {},
  110. utils.executeOnCallVueComponent(context, (node) => {
  111. if (node.arguments.length === 2) {
  112. const argument = node.arguments[0]
  113. if (canVerify(argument)) {
  114. verifyName(argument)
  115. }
  116. }
  117. }),
  118. utils.executeOnVue(context, (object) => {
  119. const node = utils.findProperty(object, 'name')
  120. componentCount++
  121. if (!node) return
  122. if (!canVerify(node.value)) return
  123. verifyName(node.value)
  124. }),
  125. {
  126. 'Program:exit'() {
  127. if (componentCount > 1) return
  128. errors.forEach((error) => context.report(error))
  129. }
  130. }
  131. )
  132. }
  133. }