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

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