Skip to content

Commit c6c41d6

Browse files
committed
Reimplement use-v-on-exact rule
1 parent d550054 commit c6c41d6

File tree

4 files changed

+392
-101
lines changed

4 files changed

+392
-101
lines changed

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

Lines changed: 121 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,100 @@
99
// ------------------------------------------------------------------------------
1010

1111
const utils = require('../utils')
12+
const keyAliases = require('../utils/key-aliases.json')
13+
14+
const KEY_MODIFIERS = new Set([
15+
'ctrl', 'shift', 'alt', 'meta', 'left', 'right', 'middle', 'esc', 'tab',
16+
'enter', 'space', 'up', 'left', 'right', 'down', 'delete'
17+
])
18+
19+
// https://www.w3.org/TR/uievents-key/
20+
const KEY_ALIASES = new Set(keyAliases)
1221

1322
// ------------------------------------------------------------------------------
14-
// Rule Definition
23+
// Helpers
24+
// ------------------------------------------------------------------------------
25+
26+
/**
27+
* Finds and returns all keys for event directives
28+
*
29+
* @param {Array} attributes Element attributes
30+
* @returns {Array}
31+
*/
32+
function getEventDirectives (attributes) {
33+
return attributes
34+
.filter(attribute =>
35+
attribute.directive &&
36+
attribute.key.name === 'on'
37+
)
38+
.map(attribute => ({
39+
name: attribute.key.argument,
40+
node: attribute.key,
41+
modifiers: attribute.key.modifiers
42+
}))
43+
}
44+
45+
function isKeyModifier (modifier) {
46+
return (
47+
KEY_MODIFIERS.has(modifier) ||
48+
// keyCode
49+
Number.isInteger(parseInt(modifier, 10)) ||
50+
// keyAlias (an Unicode character)
51+
Array.from(modifier).length === 1 ||
52+
// keyAlias (special keys)
53+
KEY_ALIASES.has(modifier)
54+
)
55+
}
56+
57+
function hasKeyModifier (modifiers) {
58+
return modifiers.some(isKeyModifier)
59+
}
60+
61+
function groupEvents (events) {
62+
return events.reduce((acc, event) => {
63+
if (acc[event.name]) {
64+
acc[event.name].push(event)
65+
} else {
66+
acc[event.name] = [event]
67+
}
68+
69+
return acc
70+
}, {})
71+
}
72+
73+
function getKeyModifiersString (modifiers) {
74+
return modifiers.filter(isKeyModifier).sort().join(',')
75+
}
76+
77+
function hasConflictedModifiers (baseEvent, event) {
78+
if (
79+
event.node === baseEvent.node ||
80+
event.modifiers.includes('exact')
81+
) return false
82+
83+
const eventModifiers = getKeyModifiersString(event.modifiers)
84+
const baseEventModifiers = getKeyModifiersString(baseEvent.modifiers)
85+
86+
return (
87+
baseEvent.modifiers.length >= 1 &&
88+
baseEventModifiers !== eventModifiers &&
89+
baseEventModifiers.indexOf(eventModifiers) > -1
90+
)
91+
}
92+
93+
function findConflictedEvents (events) {
94+
return events.reduce((acc, event) => {
95+
return [
96+
...acc,
97+
...events
98+
.filter(evt => !acc.find(e => evt === e)) // No duplicates
99+
.filter(hasConflictedModifiers.bind(null, event))
100+
]
101+
}, [])
102+
}
103+
104+
// ------------------------------------------------------------------------------
105+
// Rule details
15106
// ------------------------------------------------------------------------------
16107

17108
module.exports = {
@@ -35,31 +126,36 @@ module.exports = {
35126
create (context) {
36127
return utils.defineTemplateBodyVisitor(context, {
37128
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-
}
129+
if (node.attributes.length === 0) return
130+
131+
const isCustomComponent = utils.isCustomComponent(node.parent)
132+
let events = getEventDirectives(node.attributes)
133+
134+
if (isCustomComponent) {
135+
// For components consider only events with `native` modifier
136+
events = events.filter(event => event.modifiers.includes('native'))
62137
}
138+
139+
const grouppedEvents = groupEvents(events)
140+
141+
Object.keys(grouppedEvents).forEach(eventName => {
142+
const eventsInGroup = grouppedEvents[eventName]
143+
const hasEventWithKeyModifier = eventsInGroup.some(event =>
144+
hasKeyModifier(event.modifiers)
145+
)
146+
147+
if (!hasEventWithKeyModifier) return
148+
149+
const conflictedEvents = findConflictedEvents(eventsInGroup)
150+
151+
conflictedEvents.forEach(e => {
152+
context.report({
153+
node: e.node,
154+
loc: e.node.loc,
155+
message: "Consider to use '.exact' modifier."
156+
})
157+
})
158+
})
63159
}
64160
})
65161
}

lib/rules/valid-v-on.js

Lines changed: 2 additions & 68 deletions
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

Lines changed: 68 additions & 0 deletions
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)