use-v-on-exact.js 5.8 KB

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