use-v-on-exact.js 4.8 KB

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