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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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. messages: {
  69. incorrectCase: 'Component name "{{name}}" is not {{caseType}}.'
  70. }
  71. },
  72. /** @param {RuleContext} context */
  73. create(context) {
  74. const caseOption = context.options[0]
  75. const options = context.options[1] || {}
  76. const caseType = allowedCaseOptions.includes(caseOption)
  77. ? caseOption
  78. : defaultCase
  79. /** @type {RegExp[]} */
  80. const ignores = (options.ignores || []).map(toRegExp)
  81. /** @type {string[]} */
  82. const globals = (options.globals || []).map(casing.pascalCase)
  83. const registeredComponentsOnly = options.registeredComponentsOnly !== false
  84. const sourceCode = context.getSourceCode()
  85. const tokens =
  86. sourceCode.parserServices.getTemplateBodyTokenStore &&
  87. sourceCode.parserServices.getTemplateBodyTokenStore()
  88. /** @type { Set<string> } */
  89. const registeredComponents = new Set(globals)
  90. if (utils.isScriptSetup(context)) {
  91. // For <script setup>
  92. const globalScope = context.getSourceCode().scopeManager.globalScope
  93. if (globalScope) {
  94. // Only check find the import module
  95. const moduleScope = globalScope.childScopes.find(
  96. (scope) => scope.type === 'module'
  97. )
  98. for (const variable of (moduleScope && moduleScope.variables) || []) {
  99. if (!isTypeOnlyImport(variable)) {
  100. registeredComponents.add(variable.name)
  101. }
  102. }
  103. }
  104. }
  105. /**
  106. * Checks whether the given node is the verification target node.
  107. * @param {VElement} node element node
  108. * @returns {boolean} `true` if the given node is the verification target node.
  109. */
  110. function isVerifyTarget(node) {
  111. if (ignores.some((re) => re.test(node.rawName))) {
  112. // ignore
  113. return false
  114. }
  115. if (
  116. (!utils.isHtmlElementNode(node) &&
  117. !utils.isSvgElementNode(node) &&
  118. !utils.isMathElementNode(node)) ||
  119. utils.isHtmlWellKnownElementName(node.rawName) ||
  120. utils.isSvgWellKnownElementName(node.rawName) ||
  121. utils.isMathWellKnownElementName(node.rawName) ||
  122. utils.isVueBuiltInElementName(node.rawName)
  123. ) {
  124. return false
  125. }
  126. if (!registeredComponentsOnly) {
  127. // If the user specifies registeredComponentsOnly as false, it checks all component tags.
  128. return true
  129. }
  130. // We only verify the registered components.
  131. return registeredComponents.has(casing.pascalCase(node.rawName))
  132. }
  133. let hasInvalidEOF = false
  134. return utils.defineTemplateBodyVisitor(
  135. context,
  136. {
  137. VElement(node) {
  138. if (hasInvalidEOF) {
  139. return
  140. }
  141. if (!isVerifyTarget(node)) {
  142. return
  143. }
  144. const name = node.rawName
  145. if (!casing.getChecker(caseType)(name)) {
  146. const startTag = node.startTag
  147. const open = tokens.getFirstToken(startTag)
  148. const casingName = casing.getExactConverter(caseType)(name)
  149. context.report({
  150. node: open,
  151. loc: open.loc,
  152. messageId: 'incorrectCase',
  153. data: {
  154. name,
  155. caseType
  156. },
  157. *fix(fixer) {
  158. yield fixer.replaceText(open, `<${casingName}`)
  159. const endTag = node.endTag
  160. if (endTag) {
  161. const endTagOpen = tokens.getFirstToken(endTag)
  162. yield fixer.replaceText(endTagOpen, `</${casingName}`)
  163. }
  164. }
  165. })
  166. }
  167. }
  168. },
  169. {
  170. Program(node) {
  171. hasInvalidEOF = utils.hasInvalidEOF(node)
  172. },
  173. ...(registeredComponentsOnly
  174. ? utils.executeOnVue(context, (obj) => {
  175. for (const n of utils.getRegisteredComponents(obj)) {
  176. registeredComponents.add(n.name)
  177. }
  178. })
  179. : {})
  180. }
  181. )
  182. }
  183. }