Skip to content

Commit

Permalink
Add prefer-event-key rule (#226)
Browse files Browse the repository at this point in the history
Co-authored-by: <futpib@gmail.com>
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
ankeetmaini and sindresorhus committed May 31, 2019
1 parent 3c0c7b9 commit 9bede78
Show file tree
Hide file tree
Showing 5 changed files with 1,014 additions and 0 deletions.
39 changes: 39 additions & 0 deletions docs/rules/prefer-event-key.md
@@ -0,0 +1,39 @@
# Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`

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.

This rule is partly fixable. It can only fix direct property access.


## Fail

```js
window.addEventListener('keydown', event => {
console.log(event.keyCode);
});
```

```js
window.addEventListener('keydown', event => {
if (event.keyCode === 8) {
console.log('Backspace was pressed');
}
});
```


## Pass

```js
window.addEventListener('click', event => {
console.log(event.key);
});
```

```js
window.addEventListener('keydown', event => {
if (event.key === 'Backspace') {
console.log('Backspace was pressed');
}
});
```
1 change: 1 addition & 0 deletions index.js
Expand Up @@ -39,6 +39,7 @@ module.exports = {
'unicorn/no-zero-fractions': 'error',
'unicorn/number-literal-case': 'error',
'unicorn/prefer-add-event-listener': 'error',
'unicorn/prefer-event-key': 'error',
'unicorn/prefer-exponentiation-operator': 'error',
'unicorn/prefer-flat-map': 'error',
'unicorn/prefer-includes': 'error',
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Expand Up @@ -57,6 +57,7 @@ Configure it in `package.json`.
"unicorn/no-zero-fractions": "error",
"unicorn/number-literal-case": "error",
"unicorn/prefer-add-event-listener": "error",
"unicorn/prefer-event-key": "error",
"unicorn/prefer-exponentiation-operator": "error",
"unicorn/prefer-flat-map": "error",
"unicorn/prefer-includes": "error",
Expand Down Expand Up @@ -100,6 +101,7 @@ Configure it in `package.json`.
- [no-zero-fractions](docs/rules/no-zero-fractions.md) - Disallow number literals with zero fractions or dangling dots. *(fixable)*
- [number-literal-case](docs/rules/number-literal-case.md) - Enforce lowercase identifier and uppercase value for number literals. *(fixable)*
- [prefer-add-event-listener](docs/rules/prefer-add-event-listener.md) - Prefer `.addEventListener()` and `.removeEventListener()` over `on`-functions. *(partly fixable)*
- [prefer-event-key](docs/rules/prefer-event-key.md) - Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. *(partly fixable)*
- [prefer-exponentiation-operator](docs/rules/prefer-exponentiation-operator.md) - Prefer the exponentiation operator over `Math.pow()` *(fixable)*
- [prefer-flat-map](docs/rules/prefer-flat-map.md) - Prefer `.flatMap(…)` over `.map(…).flat()`. *(fixable)*
- [prefer-includes](docs/rules/prefer-includes.md) - Prefer `.includes()` over `.indexOf()` when checking for existence or non-existence. *(fixable)*
Expand Down
225 changes: 225 additions & 0 deletions rules/prefer-event-key.js
@@ -0,0 +1,225 @@
'use strict';
const getDocsUrl = require('./utils/get-docs-url');

const keys = [
'keyCode',
'charCode',
'which'
];

// https://github.com/facebook/react/blob/b87aabd/packages/react-dom/src/events/getEventKey.js#L36
// Only meta characters which can't be deciphered from `String.fromCharCode()`
const translateToKey = {
8: 'Backspace',
9: 'Tab',
12: 'Clear',
13: 'Enter',
16: 'Shift',
17: 'Control',
18: 'Alt',
19: 'Pause',
20: 'CapsLock',
27: 'Escape',
32: ' ',
33: 'PageUp',
34: 'PageDown',
35: 'End',
36: 'Home',
37: 'ArrowLeft',
38: 'ArrowUp',
39: 'ArrowRight',
40: 'ArrowDown',
45: 'Insert',
46: 'Delete',
112: 'F1',
113: 'F2',
114: 'F3',
115: 'F4',
116: 'F5',
117: 'F6',
118: 'F7',
119: 'F8',
120: 'F9',
121: 'F10',
122: 'F11',
123: 'F12',
144: 'NumLock',
145: 'ScrollLock',
186: ';',
187: '=',
188: ',',
189: '-',
190: '.',
191: '/',
219: '[',
220: '\\',
221: ']',
222: '\'',
224: 'Meta'
};

const isPropertyNamedAddEventListener = node =>
node &&
node.type === 'CallExpression' &&
node.callee &&
node.callee.type === 'MemberExpression' &&
node.callee.property &&
node.callee.property.name === 'addEventListener';

const getEventNodeAndReferences = (context, node) => {
const eventListener = getMatchingAncestorOfType(node, 'CallExpression', isPropertyNamedAddEventListener);
const callback = eventListener && eventListener.arguments && eventListener.arguments[1];
switch (callback && callback.type) {
case 'ArrowFunctionExpression':
case 'FunctionExpression': {
const eventVariable = context.getDeclaredVariables(callback)[0];
const references = eventVariable && eventVariable.references;
return {
event: callback.params && callback.params[0],
references
};
}

default:
return {};
}
};

const isPropertyOf = (node, eventNode) => {
return (
node &&
node.parent &&
node.parent.type === 'MemberExpression' &&
node.parent.object &&
node.parent.object === eventNode
);
};

// The third argument is a condition function, as one passed to `Array#filter()`
// Helpful if nearest node of type also needs to have some other property
const getMatchingAncestorOfType = (node, type, fn = n => n || true) => {
let current = node;
while (current) {
if (current.type === type && fn(current)) {
return current;
}

current = current.parent;
}

return null;
};

const getParentByLevel = (node, level) => {
let current = node;
while (current && level) {
level--;
current = current.parent;
}

if (level === 0) {
return current;
}
};

const fix = node => fixer => {
// Since we're only fixing direct property access usages, like `event.keyCode`
const nearestIf = getParentByLevel(node, 3);
if (!nearestIf || nearestIf.type !== 'IfStatement') {
return;
}

const {right = {}, operator} = nearestIf.test;
const isTestingEquality = operator === '==' || operator === '===';
const isRightValid = isTestingEquality && right.type === 'Literal' && typeof right.value === 'number';
// Either a meta key or a printable character
const keyCode = translateToKey[right.value] || String.fromCharCode(right.value);
// And if we recognize the `.keyCode`
if (!isRightValid || !keyCode) {
return;
}

// Apply fixes
return [
fixer.replaceText(node, 'key'),
fixer.replaceText(right, `'${keyCode}'`)
];
};

const create = context => {
const report = node => {
context.report({
message: `Use \`.key\` instead of \`.${node.name}\``,
node,
fix: fix(node)
});
};

return {
'Identifier:matches([name=keyCode], [name=charCode], [name=which])'(node) {
// Normal case when usage is direct -> `event.keyCode`
const {event, references} = getEventNodeAndReferences(context, node);
if (!event) {
return;
}

const isPropertyOfEvent = Boolean(references && references.find(r => isPropertyOf(node, r.identifier)));
if (isPropertyOfEvent) {
report(node);
}
},

Property(node) {
// Destructured case
const propertyName = node.value && node.value.name;
if (!keys.includes(propertyName)) {
return;
}

const {event, references} = getEventNodeAndReferences(context, node);
if (!event) {
return;
}

const nearestVariableDeclarator = getMatchingAncestorOfType(
node,
'VariableDeclarator'
);
const initObject =
nearestVariableDeclarator &&
nearestVariableDeclarator.init &&
nearestVariableDeclarator.init;

// Make sure initObject is a reference of eventVariable
const isReferenceOfEvent = Boolean(
references && references.find(r => r.identifier === initObject)
);
if (isReferenceOfEvent) {
report(node.value);
return;
}

// When the event parameter itself is destructured directly
const isEventParamDestructured = event.type === 'ObjectPattern';
if (isEventParamDestructured) {
// Check for properties
for (const prop of event.properties) {
if (prop === node) {
report(node.value);
}
}
}
}
};
};

module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
url: getDocsUrl(__filename)
},
fixable: 'code'
}
};

0 comments on commit 9bede78

Please sign in to comment.