/** * @fileoverview enforce usage of `exact` modifier on `v-on`. * @author Armano */ 'use strict' // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ /** * @typedef { {name: string, node: VDirectiveKey, modifiers: string[] } } EventDirective */ const utils = require('../utils') const SYSTEM_MODIFIERS = new Set(['ctrl', 'shift', 'alt', 'meta']) const GLOBAL_MODIFIERS = new Set([ 'stop', 'prevent', 'capture', 'self', 'once', 'passive', 'native' ]) // ------------------------------------------------------------------------------ // Helpers // ------------------------------------------------------------------------------ /** * Finds and returns all keys for event directives * * @param {VStartTag} startTag Element startTag * @param {SourceCode} sourceCode The source code object. * @returns {EventDirective[]} [{ name, node, modifiers }] */ function getEventDirectives(startTag, sourceCode) { return utils.getDirectives(startTag, 'on').map((attribute) => ({ name: attribute.key.argument ? sourceCode.getText(attribute.key.argument) : '', node: attribute.key, modifiers: attribute.key.modifiers.map((modifier) => modifier.name) })) } /** * Checks whether given modifier is key modifier * * @param {string} modifier * @returns {boolean} */ function isKeyModifier(modifier) { return !GLOBAL_MODIFIERS.has(modifier) && !SYSTEM_MODIFIERS.has(modifier) } /** * Checks whether given modifier is system one * * @param {string} modifier * @returns {boolean} */ function isSystemModifier(modifier) { return SYSTEM_MODIFIERS.has(modifier) } /** * Checks whether given any of provided modifiers * has system modifier * * @param {string[]} modifiers * @returns {boolean} */ function hasSystemModifier(modifiers) { return modifiers.some(isSystemModifier) } /** * Groups all events in object, * with keys represinting each event name * * @param {EventDirective[]} events * @returns { { [key: string]: EventDirective[] } } { click: [], keypress: [] } */ function groupEvents(events) { return events.reduce((acc, event) => { if (acc[event.name]) { acc[event.name].push(event) } else { acc[event.name] = [event] } return acc }, /** @type { { [key: string]: EventDirective[] } }*/ ({})) } /** * Creates alphabetically sorted string with system modifiers * * @param {string[]} modifiers * @returns {string} e.g. "alt,ctrl,del,shift" */ function getSystemModifiersString(modifiers) { return modifiers.filter(isSystemModifier).sort().join(',') } /** * Creates alphabetically sorted string with key modifiers * * @param {string[]} modifiers * @returns {string} e.g. "enter,tab" */ function getKeyModifiersString(modifiers) { return modifiers.filter(isKeyModifier).sort().join(',') } /** * Compares two events based on their modifiers * to detect possible event leakeage * * @param {EventDirective} baseEvent * @param {EventDirective} event * @returns {boolean} */ function hasConflictedModifiers(baseEvent, event) { if (event.node === baseEvent.node || event.modifiers.includes('exact')) return false const eventKeyModifiers = getKeyModifiersString(event.modifiers) const baseEventKeyModifiers = getKeyModifiersString(baseEvent.modifiers) if ( eventKeyModifiers && baseEventKeyModifiers && eventKeyModifiers !== baseEventKeyModifiers ) return false const eventSystemModifiers = getSystemModifiersString(event.modifiers) const baseEventSystemModifiers = getSystemModifiersString(baseEvent.modifiers) return ( baseEvent.modifiers.length >= 1 && baseEventSystemModifiers !== eventSystemModifiers && baseEventSystemModifiers.indexOf(eventSystemModifiers) > -1 ) } /** * Searches for events that might conflict with each other * * @param {EventDirective[]} events * @returns {EventDirective[]} conflicted events, without duplicates */ function findConflictedEvents(events) { return events.reduce((acc, event) => { return [ ...acc, ...events .filter((evt) => !acc.find((e) => evt === e)) // No duplicates .filter(hasConflictedModifiers.bind(null, event)) ] }, /** @type {EventDirective[]} */ ([])) } // ------------------------------------------------------------------------------ // Rule details // ------------------------------------------------------------------------------ module.exports = { meta: { type: 'suggestion', docs: { description: 'enforce usage of `exact` modifier on `v-on`', categories: ['vue3-essential', 'essential'], url: 'https://eslint.vuejs.org/rules/use-v-on-exact.html' }, fixable: null, schema: [] }, /** * Creates AST event handlers for use-v-on-exact. * * @param {RuleContext} context - The rule context. * @returns {Object} AST event handlers. */ create(context) { const sourceCode = context.getSourceCode() return utils.defineTemplateBodyVisitor(context, { /** @param {VStartTag} node */ VStartTag(node) { if (node.attributes.length === 0) return const isCustomComponent = utils.isCustomComponent(node.parent) let events = getEventDirectives(node, sourceCode) if (isCustomComponent) { // For components consider only events with `native` modifier events = events.filter((event) => event.modifiers.includes('native')) } const grouppedEvents = groupEvents(events) Object.keys(grouppedEvents).forEach((eventName) => { const eventsInGroup = grouppedEvents[eventName] const hasEventWithKeyModifier = eventsInGroup.some((event) => hasSystemModifier(event.modifiers) ) if (!hasEventWithKeyModifier) return const conflictedEvents = findConflictedEvents(eventsInGroup) conflictedEvents.forEach((e) => { context.report({ node: e.node, loc: e.node.loc, message: "Consider to use '.exact' modifier." }) }) }) } }) } }