Skip to content

Commit 9bede78

Browse files
Add prefer-event-key rule (#226)
Co-authored-by: <[email protected]> Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 3c0c7b9 commit 9bede78

File tree

5 files changed

+1014
-0
lines changed

5 files changed

+1014
-0
lines changed

docs/rules/prefer-event-key.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`
2+
3+
Enforces the use of [`KeyboardEvent#key`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key) over [`KeyboardEvent#keyCode`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode) which is deprecated. The `.key` property is also more semantic and readable.
4+
5+
This rule is partly fixable. It can only fix direct property access.
6+
7+
8+
## Fail
9+
10+
```js
11+
window.addEventListener('keydown', event => {
12+
console.log(event.keyCode);
13+
});
14+
```
15+
16+
```js
17+
window.addEventListener('keydown', event => {
18+
if (event.keyCode === 8) {
19+
console.log('Backspace was pressed');
20+
}
21+
});
22+
```
23+
24+
25+
## Pass
26+
27+
```js
28+
window.addEventListener('click', event => {
29+
console.log(event.key);
30+
});
31+
```
32+
33+
```js
34+
window.addEventListener('keydown', event => {
35+
if (event.key === 'Backspace') {
36+
console.log('Backspace was pressed');
37+
}
38+
});
39+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ module.exports = {
3939
'unicorn/no-zero-fractions': 'error',
4040
'unicorn/number-literal-case': 'error',
4141
'unicorn/prefer-add-event-listener': 'error',
42+
'unicorn/prefer-event-key': 'error',
4243
'unicorn/prefer-exponentiation-operator': 'error',
4344
'unicorn/prefer-flat-map': 'error',
4445
'unicorn/prefer-includes': 'error',

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ Configure it in `package.json`.
5757
"unicorn/no-zero-fractions": "error",
5858
"unicorn/number-literal-case": "error",
5959
"unicorn/prefer-add-event-listener": "error",
60+
"unicorn/prefer-event-key": "error",
6061
"unicorn/prefer-exponentiation-operator": "error",
6162
"unicorn/prefer-flat-map": "error",
6263
"unicorn/prefer-includes": "error",
@@ -100,6 +101,7 @@ Configure it in `package.json`.
100101
- [no-zero-fractions](docs/rules/no-zero-fractions.md) - Disallow number literals with zero fractions or dangling dots. *(fixable)*
101102
- [number-literal-case](docs/rules/number-literal-case.md) - Enforce lowercase identifier and uppercase value for number literals. *(fixable)*
102103
- [prefer-add-event-listener](docs/rules/prefer-add-event-listener.md) - Prefer `.addEventListener()` and `.removeEventListener()` over `on`-functions. *(partly fixable)*
104+
- [prefer-event-key](docs/rules/prefer-event-key.md) - Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. *(partly fixable)*
103105
- [prefer-exponentiation-operator](docs/rules/prefer-exponentiation-operator.md) - Prefer the exponentiation operator over `Math.pow()` *(fixable)*
104106
- [prefer-flat-map](docs/rules/prefer-flat-map.md) - Prefer `.flatMap(…)` over `.map(…).flat()`. *(fixable)*
105107
- [prefer-includes](docs/rules/prefer-includes.md) - Prefer `.includes()` over `.indexOf()` when checking for existence or non-existence. *(fixable)*

rules/prefer-event-key.js

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
'use strict';
2+
const getDocsUrl = require('./utils/get-docs-url');
3+
4+
const keys = [
5+
'keyCode',
6+
'charCode',
7+
'which'
8+
];
9+
10+
// https://github.com/facebook/react/blob/b87aabd/packages/react-dom/src/events/getEventKey.js#L36
11+
// Only meta characters which can't be deciphered from `String.fromCharCode()`
12+
const translateToKey = {
13+
8: 'Backspace',
14+
9: 'Tab',
15+
12: 'Clear',
16+
13: 'Enter',
17+
16: 'Shift',
18+
17: 'Control',
19+
18: 'Alt',
20+
19: 'Pause',
21+
20: 'CapsLock',
22+
27: 'Escape',
23+
32: ' ',
24+
33: 'PageUp',
25+
34: 'PageDown',
26+
35: 'End',
27+
36: 'Home',
28+
37: 'ArrowLeft',
29+
38: 'ArrowUp',
30+
39: 'ArrowRight',
31+
40: 'ArrowDown',
32+
45: 'Insert',
33+
46: 'Delete',
34+
112: 'F1',
35+
113: 'F2',
36+
114: 'F3',
37+
115: 'F4',
38+
116: 'F5',
39+
117: 'F6',
40+
118: 'F7',
41+
119: 'F8',
42+
120: 'F9',
43+
121: 'F10',
44+
122: 'F11',
45+
123: 'F12',
46+
144: 'NumLock',
47+
145: 'ScrollLock',
48+
186: ';',
49+
187: '=',
50+
188: ',',
51+
189: '-',
52+
190: '.',
53+
191: '/',
54+
219: '[',
55+
220: '\\',
56+
221: ']',
57+
222: '\'',
58+
224: 'Meta'
59+
};
60+
61+
const isPropertyNamedAddEventListener = node =>
62+
node &&
63+
node.type === 'CallExpression' &&
64+
node.callee &&
65+
node.callee.type === 'MemberExpression' &&
66+
node.callee.property &&
67+
node.callee.property.name === 'addEventListener';
68+
69+
const getEventNodeAndReferences = (context, node) => {
70+
const eventListener = getMatchingAncestorOfType(node, 'CallExpression', isPropertyNamedAddEventListener);
71+
const callback = eventListener && eventListener.arguments && eventListener.arguments[1];
72+
switch (callback && callback.type) {
73+
case 'ArrowFunctionExpression':
74+
case 'FunctionExpression': {
75+
const eventVariable = context.getDeclaredVariables(callback)[0];
76+
const references = eventVariable && eventVariable.references;
77+
return {
78+
event: callback.params && callback.params[0],
79+
references
80+
};
81+
}
82+
83+
default:
84+
return {};
85+
}
86+
};
87+
88+
const isPropertyOf = (node, eventNode) => {
89+
return (
90+
node &&
91+
node.parent &&
92+
node.parent.type === 'MemberExpression' &&
93+
node.parent.object &&
94+
node.parent.object === eventNode
95+
);
96+
};
97+
98+
// The third argument is a condition function, as one passed to `Array#filter()`
99+
// Helpful if nearest node of type also needs to have some other property
100+
const getMatchingAncestorOfType = (node, type, fn = n => n || true) => {
101+
let current = node;
102+
while (current) {
103+
if (current.type === type && fn(current)) {
104+
return current;
105+
}
106+
107+
current = current.parent;
108+
}
109+
110+
return null;
111+
};
112+
113+
const getParentByLevel = (node, level) => {
114+
let current = node;
115+
while (current && level) {
116+
level--;
117+
current = current.parent;
118+
}
119+
120+
if (level === 0) {
121+
return current;
122+
}
123+
};
124+
125+
const fix = node => fixer => {
126+
// Since we're only fixing direct property access usages, like `event.keyCode`
127+
const nearestIf = getParentByLevel(node, 3);
128+
if (!nearestIf || nearestIf.type !== 'IfStatement') {
129+
return;
130+
}
131+
132+
const {right = {}, operator} = nearestIf.test;
133+
const isTestingEquality = operator === '==' || operator === '===';
134+
const isRightValid = isTestingEquality && right.type === 'Literal' && typeof right.value === 'number';
135+
// Either a meta key or a printable character
136+
const keyCode = translateToKey[right.value] || String.fromCharCode(right.value);
137+
// And if we recognize the `.keyCode`
138+
if (!isRightValid || !keyCode) {
139+
return;
140+
}
141+
142+
// Apply fixes
143+
return [
144+
fixer.replaceText(node, 'key'),
145+
fixer.replaceText(right, `'${keyCode}'`)
146+
];
147+
};
148+
149+
const create = context => {
150+
const report = node => {
151+
context.report({
152+
message: `Use \`.key\` instead of \`.${node.name}\``,
153+
node,
154+
fix: fix(node)
155+
});
156+
};
157+
158+
return {
159+
'Identifier:matches([name=keyCode], [name=charCode], [name=which])'(node) {
160+
// Normal case when usage is direct -> `event.keyCode`
161+
const {event, references} = getEventNodeAndReferences(context, node);
162+
if (!event) {
163+
return;
164+
}
165+
166+
const isPropertyOfEvent = Boolean(references && references.find(r => isPropertyOf(node, r.identifier)));
167+
if (isPropertyOfEvent) {
168+
report(node);
169+
}
170+
},
171+
172+
Property(node) {
173+
// Destructured case
174+
const propertyName = node.value && node.value.name;
175+
if (!keys.includes(propertyName)) {
176+
return;
177+
}
178+
179+
const {event, references} = getEventNodeAndReferences(context, node);
180+
if (!event) {
181+
return;
182+
}
183+
184+
const nearestVariableDeclarator = getMatchingAncestorOfType(
185+
node,
186+
'VariableDeclarator'
187+
);
188+
const initObject =
189+
nearestVariableDeclarator &&
190+
nearestVariableDeclarator.init &&
191+
nearestVariableDeclarator.init;
192+
193+
// Make sure initObject is a reference of eventVariable
194+
const isReferenceOfEvent = Boolean(
195+
references && references.find(r => r.identifier === initObject)
196+
);
197+
if (isReferenceOfEvent) {
198+
report(node.value);
199+
return;
200+
}
201+
202+
// When the event parameter itself is destructured directly
203+
const isEventParamDestructured = event.type === 'ObjectPattern';
204+
if (isEventParamDestructured) {
205+
// Check for properties
206+
for (const prop of event.properties) {
207+
if (prop === node) {
208+
report(node.value);
209+
}
210+
}
211+
}
212+
}
213+
};
214+
};
215+
216+
module.exports = {
217+
create,
218+
meta: {
219+
type: 'suggestion',
220+
docs: {
221+
url: getDocsUrl(__filename)
222+
},
223+
fixable: 'code'
224+
}
225+
};

0 commit comments

Comments
 (0)