use-v-on-exact.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. /**
  2. * @fileoverview enforce usage of `exact` modifier on `v-on`.
  3. * @author Armano
  4. */
  5. 'use strict'
  6. // ------------------------------------------------------------------------------
  7. // Requirements
  8. // ------------------------------------------------------------------------------
  9. /**
  10. * @typedef { {name: string, node: VDirectiveKey, modifiers: string[] } } EventDirective
  11. */
  12. const utils = require('../utils')
  13. const SYSTEM_MODIFIERS = new Set(['ctrl', 'shift', 'alt', 'meta'])
  14. const GLOBAL_MODIFIERS = new Set([
  15. 'stop',
  16. 'prevent',
  17. 'capture',
  18. 'self',
  19. 'once',
  20. 'passive',
  21. 'native'
  22. ])
  23. // ------------------------------------------------------------------------------
  24. // Helpers
  25. // ------------------------------------------------------------------------------
  26. /**
  27. * Finds and returns all keys for event directives
  28. *
  29. * @param {VStartTag} startTag Element startTag
  30. * @param {SourceCode} sourceCode The source code object.
  31. * @returns {EventDirective[]} [{ name, node, modifiers }]
  32. */
  33. function getEventDirectives(startTag, sourceCode) {
  34. return utils.getDirectives(startTag, 'on').map((attribute) => ({
  35. name: attribute.key.argument
  36. ? sourceCode.getText(attribute.key.argument)
  37. : '',
  38. node: attribute.key,
  39. modifiers: attribute.key.modifiers.map((modifier) => modifier.name)
  40. }))
  41. }
  42. /**
  43. * Checks whether given modifier is key modifier
  44. *
  45. * @param {string} modifier
  46. * @returns {boolean}
  47. */
  48. function isKeyModifier(modifier) {
  49. return !GLOBAL_MODIFIERS.has(modifier) && !SYSTEM_MODIFIERS.has(modifier)
  50. }
  51. /**
  52. * Checks whether given modifier is system one
  53. *
  54. * @param {string} modifier
  55. * @returns {boolean}
  56. */
  57. function isSystemModifier(modifier) {
  58. return SYSTEM_MODIFIERS.has(modifier)
  59. }
  60. /**
  61. * Checks whether given any of provided modifiers
  62. * has system modifier
  63. *
  64. * @param {string[]} modifiers
  65. * @returns {boolean}
  66. */
  67. function hasSystemModifier(modifiers) {
  68. return modifiers.some(isSystemModifier)
  69. }
  70. /**
  71. * Groups all events in object,
  72. * with keys represinting each event name
  73. *
  74. * @param {EventDirective[]} events
  75. * @returns { { [key: string]: EventDirective[] } } { click: [], keypress: [] }
  76. */
  77. function groupEvents(events) {
  78. return events.reduce((acc, event) => {
  79. if (acc[event.name]) {
  80. acc[event.name].push(event)
  81. } else {
  82. acc[event.name] = [event]
  83. }
  84. return acc
  85. }, /** @type { { [key: string]: EventDirective[] } }*/ ({}))
  86. }
  87. /**
  88. * Creates alphabetically sorted string with system modifiers
  89. *
  90. * @param {string[]} modifiers
  91. * @returns {string} e.g. "alt,ctrl,del,shift"
  92. */
  93. function getSystemModifiersString(modifiers) {
  94. return modifiers.filter(isSystemModifier).sort().join(',')
  95. }
  96. /**
  97. * Creates alphabetically sorted string with key modifiers
  98. *
  99. * @param {string[]} modifiers
  100. * @returns {string} e.g. "enter,tab"
  101. */
  102. function getKeyModifiersString(modifiers) {
  103. return modifiers.filter(isKeyModifier).sort().join(',')
  104. }
  105. /**
  106. * Compares two events based on their modifiers
  107. * to detect possible event leakeage
  108. *
  109. * @param {EventDirective} baseEvent
  110. * @param {EventDirective} event
  111. * @returns {boolean}
  112. */
  113. function hasConflictedModifiers(baseEvent, event) {
  114. if (event.node === baseEvent.node || event.modifiers.includes('exact'))
  115. return false
  116. const eventKeyModifiers = getKeyModifiersString(event.modifiers)
  117. const baseEventKeyModifiers = getKeyModifiersString(baseEvent.modifiers)
  118. if (
  119. eventKeyModifiers &&
  120. baseEventKeyModifiers &&
  121. eventKeyModifiers !== baseEventKeyModifiers
  122. )
  123. return false
  124. const eventSystemModifiers = getSystemModifiersString(event.modifiers)
  125. const baseEventSystemModifiers = getSystemModifiersString(baseEvent.modifiers)
  126. return (
  127. baseEvent.modifiers.length >= 1 &&
  128. baseEventSystemModifiers !== eventSystemModifiers &&
  129. baseEventSystemModifiers.indexOf(eventSystemModifiers) > -1
  130. )
  131. }
  132. /**
  133. * Searches for events that might conflict with each other
  134. *
  135. * @param {EventDirective[]} events
  136. * @returns {EventDirective[]} conflicted events, without duplicates
  137. */
  138. function findConflictedEvents(events) {
  139. return events.reduce((acc, event) => {
  140. return [
  141. ...acc,
  142. ...events
  143. .filter((evt) => !acc.find((e) => evt === e)) // No duplicates
  144. .filter(hasConflictedModifiers.bind(null, event))
  145. ]
  146. }, /** @type {EventDirective[]} */ ([]))
  147. }
  148. // ------------------------------------------------------------------------------
  149. // Rule details
  150. // ------------------------------------------------------------------------------
  151. module.exports = {
  152. meta: {
  153. type: 'suggestion',
  154. docs: {
  155. description: 'enforce usage of `exact` modifier on `v-on`',
  156. categories: ['vue3-essential', 'essential'],
  157. url: 'https://eslint.vuejs.org/rules/use-v-on-exact.html'
  158. },
  159. fixable: null,
  160. schema: []
  161. },
  162. /**
  163. * Creates AST event handlers for use-v-on-exact.
  164. *
  165. * @param {RuleContext} context - The rule context.
  166. * @returns {Object} AST event handlers.
  167. */
  168. create(context) {
  169. const sourceCode = context.getSourceCode()
  170. return utils.defineTemplateBodyVisitor(context, {
  171. /** @param {VStartTag} node */
  172. VStartTag(node) {
  173. if (node.attributes.length === 0) return
  174. const isCustomComponent = utils.isCustomComponent(node.parent)
  175. let events = getEventDirectives(node, sourceCode)
  176. if (isCustomComponent) {
  177. // For components consider only events with `native` modifier
  178. events = events.filter((event) => event.modifiers.includes('native'))
  179. }
  180. const grouppedEvents = groupEvents(events)
  181. Object.keys(grouppedEvents).forEach((eventName) => {
  182. const eventsInGroup = grouppedEvents[eventName]
  183. const hasEventWithKeyModifier = eventsInGroup.some((event) =>
  184. hasSystemModifier(event.modifiers)
  185. )
  186. if (!hasEventWithKeyModifier) return
  187. const conflictedEvents = findConflictedEvents(eventsInGroup)
  188. conflictedEvents.forEach((e) => {
  189. context.report({
  190. node: e.node,
  191. loc: e.node.loc,
  192. message: "Consider to use '.exact' modifier."
  193. })
  194. })
  195. })
  196. }
  197. })
  198. }
  199. }