component-name-in-template-casing.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. /**
  2. * @author Yosuke Ota
  3. * issue https://github.com/vuejs/eslint-plugin-vue/issues/250
  4. */
  5. 'use strict'
  6. // ------------------------------------------------------------------------------
  7. // Requirements
  8. // ------------------------------------------------------------------------------
  9. const utils = require('../utils')
  10. const casing = require('../utils/casing')
  11. const { toRegExp } = require('../utils/regexp')
  12. // -----------------------------------------------------------------------------
  13. // Helpers
  14. // -----------------------------------------------------------------------------
  15. const allowedCaseOptions = ['PascalCase', 'kebab-case']
  16. const defaultCase = 'PascalCase'
  17. // ------------------------------------------------------------------------------
  18. // Rule Definition
  19. // ------------------------------------------------------------------------------
  20. module.exports = {
  21. meta: {
  22. type: 'suggestion',
  23. docs: {
  24. description:
  25. 'enforce specific casing for the component naming style in template',
  26. categories: undefined,
  27. url:
  28. 'https://eslint.vuejs.org/rules/component-name-in-template-casing.html'
  29. },
  30. fixable: 'code',
  31. schema: [
  32. {
  33. enum: allowedCaseOptions
  34. },
  35. {
  36. type: 'object',
  37. properties: {
  38. ignores: {
  39. type: 'array',
  40. items: { type: 'string' },
  41. uniqueItems: true,
  42. additionalItems: false
  43. },
  44. registeredComponentsOnly: {
  45. type: 'boolean'
  46. }
  47. },
  48. additionalProperties: false
  49. }
  50. ]
  51. },
  52. /** @param {RuleContext} context */
  53. create(context) {
  54. const caseOption = context.options[0]
  55. const options = context.options[1] || {}
  56. const caseType =
  57. allowedCaseOptions.indexOf(caseOption) !== -1 ? caseOption : defaultCase
  58. /** @type {RegExp[]} */
  59. const ignores = (options.ignores || []).map(toRegExp)
  60. const registeredComponentsOnly = options.registeredComponentsOnly !== false
  61. const tokens =
  62. context.parserServices.getTemplateBodyTokenStore &&
  63. context.parserServices.getTemplateBodyTokenStore()
  64. /** @type { string[] } */
  65. const registeredComponents = []
  66. /**
  67. * Checks whether the given node is the verification target node.
  68. * @param {VElement} node element node
  69. * @returns {boolean} `true` if the given node is the verification target node.
  70. */
  71. function isVerifyTarget(node) {
  72. if (ignores.some((re) => re.test(node.rawName))) {
  73. // ignore
  74. return false
  75. }
  76. if (!registeredComponentsOnly) {
  77. // If the user specifies registeredComponentsOnly as false, it checks all component tags.
  78. if (
  79. (!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
  80. utils.isHtmlWellKnownElementName(node.rawName) ||
  81. utils.isSvgWellKnownElementName(node.rawName)
  82. ) {
  83. return false
  84. }
  85. return true
  86. }
  87. // We only verify the components registered in the component.
  88. if (
  89. registeredComponents
  90. .filter((name) => casing.isPascalCase(name)) // When defining a component with PascalCase, you can use either case
  91. .some(
  92. (name) =>
  93. node.rawName === name || casing.pascalCase(node.rawName) === name
  94. )
  95. ) {
  96. return true
  97. }
  98. return false
  99. }
  100. let hasInvalidEOF = false
  101. return utils.defineTemplateBodyVisitor(
  102. context,
  103. {
  104. VElement(node) {
  105. if (hasInvalidEOF) {
  106. return
  107. }
  108. if (!isVerifyTarget(node)) {
  109. return
  110. }
  111. const name = node.rawName
  112. if (!casing.getChecker(caseType)(name)) {
  113. const startTag = node.startTag
  114. const open = tokens.getFirstToken(startTag)
  115. const casingName = casing.getExactConverter(caseType)(name)
  116. context.report({
  117. node: open,
  118. loc: open.loc,
  119. message: 'Component name "{{name}}" is not {{caseType}}.',
  120. data: {
  121. name,
  122. caseType
  123. },
  124. *fix(fixer) {
  125. yield fixer.replaceText(open, `<${casingName}`)
  126. const endTag = node.endTag
  127. if (endTag) {
  128. const endTagOpen = tokens.getFirstToken(endTag)
  129. yield fixer.replaceText(endTagOpen, `</${casingName}`)
  130. }
  131. }
  132. })
  133. }
  134. }
  135. },
  136. {
  137. Program(node) {
  138. hasInvalidEOF = utils.hasInvalidEOF(node)
  139. },
  140. ...(registeredComponentsOnly
  141. ? utils.executeOnVue(context, (obj) => {
  142. registeredComponents.push(
  143. ...utils.getRegisteredComponents(obj).map((n) => n.name)
  144. )
  145. })
  146. : {})
  147. }
  148. )
  149. }
  150. }