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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  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: 'enforce specific casing for the component naming style in template',
  25. category: undefined,
  26. url: 'https://eslint.vuejs.org/rules/component-name-in-template-casing.html'
  27. },
  28. fixable: 'code',
  29. schema: [
  30. {
  31. enum: allowedCaseOptions
  32. },
  33. {
  34. type: 'object',
  35. properties: {
  36. ignores: {
  37. type: 'array',
  38. items: { type: 'string' },
  39. uniqueItems: true,
  40. additionalItems: false
  41. },
  42. registeredComponentsOnly: {
  43. type: 'boolean'
  44. }
  45. },
  46. additionalProperties: false
  47. }
  48. ]
  49. },
  50. create (context) {
  51. const caseOption = context.options[0]
  52. const options = context.options[1] || {}
  53. const caseType = allowedCaseOptions.indexOf(caseOption) !== -1 ? caseOption : defaultCase
  54. const ignores = (options.ignores || []).map(toRegExp)
  55. const registeredComponentsOnly = options.registeredComponentsOnly !== false
  56. const tokens = context.parserServices.getTemplateBodyTokenStore && context.parserServices.getTemplateBodyTokenStore()
  57. const registeredComponents = []
  58. /**
  59. * Checks whether the given node is the verification target node.
  60. * @param {VElement} node element node
  61. * @returns {boolean} `true` if the given node is the verification target node.
  62. */
  63. function isVerifyTarget (node) {
  64. if (ignores.some(re => re.test(node.rawName))) {
  65. // ignore
  66. return false
  67. }
  68. if (!registeredComponentsOnly) {
  69. // If the user specifies registeredComponentsOnly as false, it checks all component tags.
  70. if ((!utils.isHtmlElementNode(node) && !utils.isSvgElementNode(node)) ||
  71. utils.isHtmlWellKnownElementName(node.rawName) ||
  72. utils.isSvgWellKnownElementName(node.rawName)
  73. ) {
  74. return false
  75. }
  76. return true
  77. }
  78. // We only verify the components registered in the component.
  79. if (registeredComponents
  80. .filter(name => casing.pascalCase(name) === name) // When defining a component with PascalCase, you can use either case
  81. .some(name => node.rawName === name || casing.pascalCase(node.rawName) === name)) {
  82. return true
  83. }
  84. return false
  85. }
  86. let hasInvalidEOF = false
  87. return utils.defineTemplateBodyVisitor(context, {
  88. 'VElement' (node) {
  89. if (hasInvalidEOF) {
  90. return
  91. }
  92. if (!isVerifyTarget(node)) {
  93. return
  94. }
  95. const name = node.rawName
  96. const casingName = casing.getConverter(caseType)(name)
  97. if (casingName !== name) {
  98. const startTag = node.startTag
  99. const open = tokens.getFirstToken(startTag)
  100. context.report({
  101. node: open,
  102. loc: open.loc,
  103. message: 'Component name "{{name}}" is not {{caseType}}.',
  104. data: {
  105. name,
  106. caseType
  107. },
  108. fix: fixer => {
  109. const endTag = node.endTag
  110. if (!endTag) {
  111. return fixer.replaceText(open, `<${casingName}`)
  112. }
  113. const endTagOpen = tokens.getFirstToken(endTag)
  114. return [
  115. fixer.replaceText(open, `<${casingName}`),
  116. fixer.replaceText(endTagOpen, `</${casingName}`)
  117. ]
  118. }
  119. })
  120. }
  121. }
  122. },
  123. Object.assign(
  124. {
  125. Program (node) {
  126. hasInvalidEOF = utils.hasInvalidEOF(node)
  127. }
  128. },
  129. registeredComponentsOnly
  130. ? utils.executeOnVue(context, (obj) => {
  131. registeredComponents.push(...utils.getRegisteredComponents(obj).map(n => n.name))
  132. })
  133. : {}
  134. ))
  135. }
  136. }