v-on-event-hyphenation.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138
  1. 'use strict'
  2. const utils = require('../utils')
  3. const casing = require('../utils/casing')
  4. const { toRegExp } = require('../utils/regexp')
  5. module.exports = {
  6. meta: {
  7. type: 'suggestion',
  8. docs: {
  9. description:
  10. 'enforce v-on event naming style on custom components in template',
  11. categories: ['vue3-strongly-recommended'],
  12. url: 'https://eslint.vuejs.org/rules/v-on-event-hyphenation.html',
  13. defaultOptions: {
  14. vue3: ['always', { autofix: true }]
  15. }
  16. },
  17. fixable: 'code',
  18. schema: [
  19. {
  20. enum: ['always', 'never']
  21. },
  22. {
  23. type: 'object',
  24. properties: {
  25. autofix: { type: 'boolean' },
  26. ignore: {
  27. type: 'array',
  28. items: {
  29. allOf: [
  30. { type: 'string' },
  31. { not: { type: 'string', pattern: ':exit$' } },
  32. { not: { type: 'string', pattern: String.raw`^\s*$` } }
  33. ]
  34. },
  35. uniqueItems: true,
  36. additionalItems: false
  37. },
  38. ignoreTags: {
  39. type: 'array',
  40. items: { type: 'string' },
  41. uniqueItems: true,
  42. additionalItems: false
  43. }
  44. },
  45. additionalProperties: false
  46. }
  47. ],
  48. messages: {
  49. // eslint-disable-next-line eslint-plugin/report-message-format
  50. mustBeHyphenated: "v-on event '{{text}}' must be hyphenated.",
  51. // eslint-disable-next-line eslint-plugin/report-message-format
  52. cannotBeHyphenated: "v-on event '{{text}}' can't be hyphenated."
  53. }
  54. },
  55. /** @param {RuleContext} context */
  56. create(context) {
  57. const sourceCode = context.getSourceCode()
  58. const option = context.options[0]
  59. const optionsPayload = context.options[1]
  60. const useHyphenated = option !== 'never'
  61. /** @type {string[]} */
  62. const ignoredAttributes = (optionsPayload && optionsPayload.ignore) || []
  63. /** @type {RegExp[]} */
  64. const ignoredTagsRegexps = (
  65. (optionsPayload && optionsPayload.ignoreTags) ||
  66. []
  67. ).map(toRegExp)
  68. const autofix = Boolean(optionsPayload && optionsPayload.autofix)
  69. const caseConverter = casing.getConverter(
  70. useHyphenated ? 'kebab-case' : 'camelCase'
  71. )
  72. /**
  73. * @param {VDirective} node
  74. * @param {VIdentifier} argument
  75. * @param {string} name
  76. */
  77. function reportIssue(node, argument, name) {
  78. const text = sourceCode.getText(node.key)
  79. context.report({
  80. node: node.key,
  81. loc: node.loc,
  82. messageId: useHyphenated ? 'mustBeHyphenated' : 'cannotBeHyphenated',
  83. data: {
  84. text
  85. },
  86. fix:
  87. autofix &&
  88. // It cannot be converted in snake_case.
  89. !name.includes('_')
  90. ? (fixer) => fixer.replaceText(argument, caseConverter(name))
  91. : null
  92. })
  93. }
  94. /**
  95. * @param {string} value
  96. */
  97. function isIgnoredAttribute(value) {
  98. const isIgnored = ignoredAttributes.some((attr) => value.includes(attr))
  99. if (isIgnored) {
  100. return true
  101. }
  102. return useHyphenated ? value.toLowerCase() === value : !/-/.test(value)
  103. }
  104. /** @param {string} name */
  105. function isIgnoredTagName(name) {
  106. return ignoredTagsRegexps.some((re) => re.test(name))
  107. }
  108. return utils.defineTemplateBodyVisitor(context, {
  109. "VAttribute[directive=true][key.name.name='on']"(node) {
  110. const element = node.parent.parent
  111. if (
  112. !utils.isCustomComponent(element) ||
  113. isIgnoredTagName(element.rawName)
  114. ) {
  115. return
  116. }
  117. if (!node.key.argument || node.key.argument.type !== 'VIdentifier') {
  118. return
  119. }
  120. const name = node.key.argument.rawName
  121. if (!name || isIgnoredAttribute(name)) return
  122. reportIssue(node, node.key.argument, name)
  123. }
  124. })
  125. }
  126. }