use-v-on-exact.js 5.7 KB

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