Skip to content

Commit 5dd07bf

Browse files
authored
fix(use-v-on-exact): Reimplement algorithm to catch cases more properly (#750)
* Reimplement use-v-on-exact rule * Document methods in use-v-on-exact * Check only system modifiers
1 parent 9bf6098 commit 5dd07bf

File tree

4 files changed

+426
-101
lines changed

4 files changed

+426
-101
lines changed

lib/rules/use-v-on-exact.js

+146-25
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,124 @@
1010

1111
const utils = require('../utils')
1212

13+
const SYSTEM_MODIFIERS = new Set(['ctrl', 'shift', 'alt', 'meta'])
14+
15+
// ------------------------------------------------------------------------------
16+
// Helpers
17+
// ------------------------------------------------------------------------------
18+
19+
/**
20+
* Finds and returns all keys for event directives
21+
*
22+
* @param {array} attributes Element attributes
23+
* @returns {array[object]} [{ name, node, modifiers }]
24+
*/
25+
function getEventDirectives (attributes) {
26+
return attributes
27+
.filter(attribute =>
28+
attribute.directive &&
29+
attribute.key.name === 'on'
30+
)
31+
.map(attribute => ({
32+
name: attribute.key.argument,
33+
node: attribute.key,
34+
modifiers: attribute.key.modifiers
35+
}))
36+
}
37+
38+
/**
39+
* Checks whether given modifier is system one
40+
*
41+
* @param {string} modifier
42+
* @returns {boolean}
43+
*/
44+
function isSystemModifier (modifier) {
45+
return SYSTEM_MODIFIERS.has(modifier)
46+
}
47+
48+
/**
49+
* Checks whether given any of provided modifiers
50+
* has system modifier
51+
*
52+
* @param {array} modifiers
53+
* @returns {boolean}
54+
*/
55+
function hasSystemModifier (modifiers) {
56+
return modifiers.some(isSystemModifier)
57+
}
58+
59+
/**
60+
* Groups all events in object,
61+
* with keys represinting each event name
62+
*
63+
* @param {array} events
64+
* @returns {object} { click: [], keypress: [] }
65+
*/
66+
function groupEvents (events) {
67+
return events.reduce((acc, event) => {
68+
if (acc[event.name]) {
69+
acc[event.name].push(event)
70+
} else {
71+
acc[event.name] = [event]
72+
}
73+
74+
return acc
75+
}, {})
76+
}
77+
78+
/**
79+
* Creates alphabetically sorted string with system modifiers
80+
*
81+
* @param {array[string]} modifiers
82+
* @returns {string} e.g. "alt,ctrl,del,shift"
83+
*/
84+
function getSystemModifiersString (modifiers) {
85+
return modifiers.filter(isSystemModifier).sort().join(',')
86+
}
87+
88+
/**
89+
* Compares two events based on their modifiers
90+
* to detect possible event leakeage
91+
*
92+
* @param {object} baseEvent
93+
* @param {object} event
94+
* @returns {boolean}
95+
*/
96+
function hasConflictedModifiers (baseEvent, event) {
97+
if (
98+
event.node === baseEvent.node ||
99+
event.modifiers.includes('exact')
100+
) return false
101+
102+
const eventModifiers = getSystemModifiersString(event.modifiers)
103+
const baseEventModifiers = getSystemModifiersString(baseEvent.modifiers)
104+
105+
return (
106+
baseEvent.modifiers.length >= 1 &&
107+
baseEventModifiers !== eventModifiers &&
108+
baseEventModifiers.indexOf(eventModifiers) > -1
109+
)
110+
}
111+
112+
/**
113+
* Searches for events that might conflict with each other
114+
*
115+
* @param {array} events
116+
* @returns {array} conflicted events, without duplicates
117+
*/
118+
function findConflictedEvents (events) {
119+
return events.reduce((acc, event) => {
120+
return [
121+
...acc,
122+
...events
123+
.filter(evt => !acc.find(e => evt === e)) // No duplicates
124+
.filter(hasConflictedModifiers.bind(null, event))
125+
]
126+
}, [])
127+
}
128+
13129
// ------------------------------------------------------------------------------
14-
// Rule Definition
130+
// Rule details
15131
// ------------------------------------------------------------------------------
16132

17133
module.exports = {
@@ -35,31 +151,36 @@ module.exports = {
35151
create (context) {
36152
return utils.defineTemplateBodyVisitor(context, {
37153
VStartTag (node) {
38-
if (node.attributes.length > 1) {
39-
const groups = node.attributes
40-
.map(item => item.key)
41-
.filter(item => item && item.type === 'VDirectiveKey' && item.name === 'on')
42-
.reduce((rv, item) => {
43-
(rv[item.argument] = rv[item.argument] || []).push(item)
44-
return rv
45-
}, {})
46-
47-
const directives = Object.keys(groups).map(key => groups[key])
48-
// const directives = Object.values(groups) // Uncomment after node 6 is dropped
49-
.filter(item => item.length > 1)
50-
for (const group of directives) {
51-
if (group.some(item => item.modifiers.length > 0)) { // check if there are directives with modifiers
52-
const invalid = group.filter(item => item.modifiers.length === 0)
53-
for (const node of invalid) {
54-
context.report({
55-
node,
56-
loc: node.loc,
57-
message: "Consider to use '.exact' modifier."
58-
})
59-
}
60-
}
61-
}
154+
if (node.attributes.length === 0) return
155+
156+
const isCustomComponent = utils.isCustomComponent(node.parent)
157+
let events = getEventDirectives(node.attributes)
158+
159+
if (isCustomComponent) {
160+
// For components consider only events with `native` modifier
161+
events = events.filter(event => event.modifiers.includes('native'))
62162
}
163+
164+
const grouppedEvents = groupEvents(events)
165+
166+
Object.keys(grouppedEvents).forEach(eventName => {
167+
const eventsInGroup = grouppedEvents[eventName]
168+
const hasEventWithKeyModifier = eventsInGroup.some(event =>
169+
hasSystemModifier(event.modifiers)
170+
)
171+
172+
if (!hasEventWithKeyModifier) return
173+
174+
const conflictedEvents = findConflictedEvents(eventsInGroup)
175+
176+
conflictedEvents.forEach(e => {
177+
context.report({
178+
node: e.node,
179+
loc: e.node.loc,
180+
message: "Consider to use '.exact' modifier."
181+
})
182+
})
183+
})
63184
}
64185
})
65186
}

lib/rules/valid-v-on.js

+2-68
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
// ------------------------------------------------------------------------------
1111

1212
const utils = require('../utils')
13+
const keyAliases = require('../utils/key-aliases.json')
1314

1415
// ------------------------------------------------------------------------------
1516
// Helpers
@@ -24,74 +25,7 @@ const VERB_MODIFIERS = new Set([
2425
'stop', 'prevent'
2526
])
2627
// https://www.w3.org/TR/uievents-key/
27-
const KEY_ALIASES = new Set([
28-
'unidentified', 'alt', 'alt-graph', 'caps-lock', 'control', 'fn', 'fn-lock',
29-
'meta', 'num-lock', 'scroll-lock', 'shift', 'symbol', 'symbol-lock', 'hyper',
30-
'super', 'enter', 'tab', 'arrow-down', 'arrow-left', 'arrow-right',
31-
'arrow-up', 'end', 'home', 'page-down', 'page-up', 'backspace', 'clear',
32-
'copy', 'cr-sel', 'cut', 'delete', 'erase-eof', 'ex-sel', 'insert', 'paste',
33-
'redo', 'undo', 'accept', 'again', 'attn', 'cancel', 'context-menu', 'escape',
34-
'execute', 'find', 'help', 'pause', 'select', 'zoom-in', 'zoom-out',
35-
'brightness-down', 'brightness-up', 'eject', 'log-off', 'power',
36-
'print-screen', 'hibernate', 'standby', 'wake-up', 'all-candidates',
37-
'alphanumeric', 'code-input', 'compose', 'convert', 'dead', 'final-mode',
38-
'group-first', 'group-last', 'group-next', 'group-previous', 'mode-change',
39-
'next-candidate', 'non-convert', 'previous-candidate', 'process',
40-
'single-candidate', 'hangul-mode', 'hanja-mode', 'junja-mode', 'eisu',
41-
'hankaku', 'hiragana', 'hiragana-katakana', 'kana-mode', 'kanji-mode',
42-
'katakana', 'romaji', 'zenkaku', 'zenkaku-hankaku', 'f1', 'f2', 'f3', 'f4',
43-
'f5', 'f6', 'f7', 'f8', 'f9', 'f10', 'f11', 'f12', 'soft1', 'soft2', 'soft3',
44-
'soft4', 'channel-down', 'channel-up', 'close', 'mail-forward', 'mail-reply',
45-
'mail-send', 'media-close', 'media-fast-forward', 'media-pause',
46-
'media-play-pause', 'media-record', 'media-rewind', 'media-stop',
47-
'media-track-next', 'media-track-previous', 'new', 'open', 'print', 'save',
48-
'spell-check', 'key11', 'key12', 'audio-balance-left', 'audio-balance-right',
49-
'audio-bass-boost-down', 'audio-bass-boost-toggle', 'audio-bass-boost-up',
50-
'audio-fader-front', 'audio-fader-rear', 'audio-surround-mode-next',
51-
'audio-treble-down', 'audio-treble-up', 'audio-volume-down',
52-
'audio-volume-up', 'audio-volume-mute', 'microphone-toggle',
53-
'microphone-volume-down', 'microphone-volume-up', 'microphone-volume-mute',
54-
'speech-correction-list', 'speech-input-toggle', 'launch-application1',
55-
'launch-application2', 'launch-calendar', 'launch-contacts', 'launch-mail',
56-
'launch-media-player', 'launch-music-player', 'launch-phone',
57-
'launch-screen-saver', 'launch-spreadsheet', 'launch-web-browser',
58-
'launch-web-cam', 'launch-word-processor', 'browser-back',
59-
'browser-favorites', 'browser-forward', 'browser-home', 'browser-refresh',
60-
'browser-search', 'browser-stop', 'app-switch', 'call', 'camera',
61-
'camera-focus', 'end-call', 'go-back', 'go-home', 'headset-hook',
62-
'last-number-redial', 'notification', 'manner-mode', 'voice-dial', 't-v',
63-
't-v3-d-mode', 't-v-antenna-cable', 't-v-audio-description',
64-
't-v-audio-description-mix-down', 't-v-audio-description-mix-up',
65-
't-v-contents-menu', 't-v-data-service', 't-v-input', 't-v-input-component1',
66-
't-v-input-component2', 't-v-input-composite1', 't-v-input-composite2',
67-
't-v-input-h-d-m-i1', 't-v-input-h-d-m-i2', 't-v-input-h-d-m-i3',
68-
't-v-input-h-d-m-i4', 't-v-input-v-g-a1', 't-v-media-context', 't-v-network',
69-
't-v-number-entry', 't-v-power', 't-v-radio-service', 't-v-satellite',
70-
't-v-satellite-b-s', 't-v-satellite-c-s', 't-v-satellite-toggle',
71-
't-v-terrestrial-analog', 't-v-terrestrial-digital', 't-v-timer',
72-
'a-v-r-input', 'a-v-r-power', 'color-f0-red', 'color-f1-green',
73-
'color-f2-yellow', 'color-f3-blue', 'color-f4-grey', 'color-f5-brown',
74-
'closed-caption-toggle', 'dimmer', 'display-swap', 'd-v-r', 'exit',
75-
'favorite-clear0', 'favorite-clear1', 'favorite-clear2', 'favorite-clear3',
76-
'favorite-recall0', 'favorite-recall1', 'favorite-recall2',
77-
'favorite-recall3', 'favorite-store0', 'favorite-store1', 'favorite-store2',
78-
'favorite-store3', 'guide', 'guide-next-day', 'guide-previous-day', 'info',
79-
'instant-replay', 'link', 'list-program', 'live-content', 'lock',
80-
'media-apps', 'media-last', 'media-skip-backward', 'media-skip-forward',
81-
'media-step-backward', 'media-step-forward', 'media-top-menu', 'navigate-in',
82-
'navigate-next', 'navigate-out', 'navigate-previous', 'next-favorite-channel',
83-
'next-user-profile', 'on-demand', 'pairing', 'pin-p-down', 'pin-p-move',
84-
'pin-p-toggle', 'pin-p-up', 'play-speed-down', 'play-speed-reset',
85-
'play-speed-up', 'random-toggle', 'rc-low-battery', 'record-speed-next',
86-
'rf-bypass', 'scan-channels-toggle', 'screen-mode-next', 'settings',
87-
'split-screen-toggle', 's-t-b-input', 's-t-b-power', 'subtitle', 'teletext',
88-
'video-mode-next', 'wink', 'zoom-toggle', 'audio-volume-down',
89-
'audio-volume-up', 'audio-volume-mute', 'browser-back', 'browser-forward',
90-
'channel-down', 'channel-up', 'context-menu', 'eject', 'end', 'enter', 'home',
91-
'media-fast-forward', 'media-play', 'media-play-pause', 'media-record',
92-
'media-rewind', 'media-stop', 'media-next-track', 'media-pause',
93-
'media-previous-track', 'power', 'unidentified'
94-
])
28+
const KEY_ALIASES = new Set(keyAliases)
9529

9630
function isValidModifier (modifier, customModifiers) {
9731
return (

lib/utils/key-aliases.json

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
[
2+
"unidentified", "alt", "alt-graph", "caps-lock", "control", "fn", "fn-lock",
3+
"meta", "num-lock", "scroll-lock", "shift", "symbol", "symbol-lock", "hyper",
4+
"super", "enter", "tab", "arrow-down", "arrow-left", "arrow-right",
5+
"arrow-up", "end", "home", "page-down", "page-up", "backspace", "clear",
6+
"copy", "cr-sel", "cut", "delete", "erase-eof", "ex-sel", "insert", "paste",
7+
"redo", "undo", "accept", "again", "attn", "cancel", "context-menu", "escape",
8+
"execute", "find", "help", "pause", "select", "zoom-in", "zoom-out",
9+
"brightness-down", "brightness-up", "eject", "log-off", "power",
10+
"print-screen", "hibernate", "standby", "wake-up", "all-candidates",
11+
"alphanumeric", "code-input", "compose", "convert", "dead", "final-mode",
12+
"group-first", "group-last", "group-next", "group-previous", "mode-change",
13+
"next-candidate", "non-convert", "previous-candidate", "process",
14+
"single-candidate", "hangul-mode", "hanja-mode", "junja-mode", "eisu",
15+
"hankaku", "hiragana", "hiragana-katakana", "kana-mode", "kanji-mode",
16+
"katakana", "romaji", "zenkaku", "zenkaku-hankaku", "f1", "f2", "f3", "f4",
17+
"f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "soft1", "soft2", "soft3",
18+
"soft4", "channel-down", "channel-up", "close", "mail-forward", "mail-reply",
19+
"mail-send", "media-close", "media-fast-forward", "media-pause",
20+
"media-play-pause", "media-record", "media-rewind", "media-stop",
21+
"media-track-next", "media-track-previous", "new", "open", "print", "save",
22+
"spell-check", "key11", "key12", "audio-balance-left", "audio-balance-right",
23+
"audio-bass-boost-down", "audio-bass-boost-toggle", "audio-bass-boost-up",
24+
"audio-fader-front", "audio-fader-rear", "audio-surround-mode-next",
25+
"audio-treble-down", "audio-treble-up", "audio-volume-down",
26+
"audio-volume-up", "audio-volume-mute", "microphone-toggle",
27+
"microphone-volume-down", "microphone-volume-up", "microphone-volume-mute",
28+
"speech-correction-list", "speech-input-toggle", "launch-application1",
29+
"launch-application2", "launch-calendar", "launch-contacts", "launch-mail",
30+
"launch-media-player", "launch-music-player", "launch-phone",
31+
"launch-screen-saver", "launch-spreadsheet", "launch-web-browser",
32+
"launch-web-cam", "launch-word-processor", "browser-back",
33+
"browser-favorites", "browser-forward", "browser-home", "browser-refresh",
34+
"browser-search", "browser-stop", "app-switch", "call", "camera",
35+
"camera-focus", "end-call", "go-back", "go-home", "headset-hook",
36+
"last-number-redial", "notification", "manner-mode", "voice-dial", "t-v",
37+
"t-v3-d-mode", "t-v-antenna-cable", "t-v-audio-description",
38+
"t-v-audio-description-mix-down", "t-v-audio-description-mix-up",
39+
"t-v-contents-menu", "t-v-data-service", "t-v-input", "t-v-input-component1",
40+
"t-v-input-component2", "t-v-input-composite1", "t-v-input-composite2",
41+
"t-v-input-h-d-m-i1", "t-v-input-h-d-m-i2", "t-v-input-h-d-m-i3",
42+
"t-v-input-h-d-m-i4", "t-v-input-v-g-a1", "t-v-media-context", "t-v-network",
43+
"t-v-number-entry", "t-v-power", "t-v-radio-service", "t-v-satellite",
44+
"t-v-satellite-b-s", "t-v-satellite-c-s", "t-v-satellite-toggle",
45+
"t-v-terrestrial-analog", "t-v-terrestrial-digital", "t-v-timer",
46+
"a-v-r-input", "a-v-r-power", "color-f0-red", "color-f1-green",
47+
"color-f2-yellow", "color-f3-blue", "color-f4-grey", "color-f5-brown",
48+
"closed-caption-toggle", "dimmer", "display-swap", "d-v-r", "exit",
49+
"favorite-clear0", "favorite-clear1", "favorite-clear2", "favorite-clear3",
50+
"favorite-recall0", "favorite-recall1", "favorite-recall2",
51+
"favorite-recall3", "favorite-store0", "favorite-store1", "favorite-store2",
52+
"favorite-store3", "guide", "guide-next-day", "guide-previous-day", "info",
53+
"instant-replay", "link", "list-program", "live-content", "lock",
54+
"media-apps", "media-last", "media-skip-backward", "media-skip-forward",
55+
"media-step-backward", "media-step-forward", "media-top-menu", "navigate-in",
56+
"navigate-next", "navigate-out", "navigate-previous", "next-favorite-channel",
57+
"next-user-profile", "on-demand", "pairing", "pin-p-down", "pin-p-move",
58+
"pin-p-toggle", "pin-p-up", "play-speed-down", "play-speed-reset",
59+
"play-speed-up", "random-toggle", "rc-low-battery", "record-speed-next",
60+
"rf-bypass", "scan-channels-toggle", "screen-mode-next", "settings",
61+
"split-screen-toggle", "s-t-b-input", "s-t-b-power", "subtitle", "teletext",
62+
"video-mode-next", "wink", "zoom-toggle", "audio-volume-down",
63+
"audio-volume-up", "audio-volume-mute", "browser-back", "browser-forward",
64+
"channel-down", "channel-up", "context-menu", "eject", "end", "enter", "home",
65+
"media-fast-forward", "media-play", "media-play-pause", "media-record",
66+
"media-rewind", "media-stop", "media-next-track", "media-pause",
67+
"media-previous-track", "power", "unidentified"
68+
]

0 commit comments

Comments
 (0)