You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
228 lines
6.1 KiB
228 lines
6.1 KiB
/** |
|
* @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." |
|
}) |
|
}) |
|
}) |
|
} |
|
}) |
|
} |
|
}
|
|
|