v-on-event-hyphenation.js 3.1 KB

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