v-on-event-hyphenation.js 3.2 KB

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