line_push/node_modules/eslint-plugin-unicorn/rules/prefer-event-key.js
2022-07-17 13:16:16 +08:00

228 lines
5.0 KiB
JavaScript

'use strict';
const getDocumentationUrl = require('./utils/get-documentation-url');
const quoteString = require('./utils/quote-string');
const keys = new Set([
'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 = () => true) => {
let current = node;
while (current) {
if (current.type === type && fn(current)) {
return current;
}
current = current.parent;
}
};
const getParentByLevel = (node, level) => {
let current = node;
while (current && level) {
level--;
current = current.parent;
}
/* istanbul ignore else */
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, quoteString(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;
}
if (
references &&
references.find(reference => isPropertyOf(node, reference.identifier))
) {
report(node);
}
},
Property(node) {
// Destructured case
const propertyName = node.value && node.value.name;
if (!keys.has(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
if (
references &&
references.find(reference => reference.identifier === initObject)
) {
report(node.value);
return;
}
// When the event parameter itself is destructured directly
const isEventParameterDestructured = event.type === 'ObjectPattern';
if (isEventParameterDestructured) {
// Check for properties
for (const property of event.properties) {
if (property === node) {
report(node.value);
}
}
}
}
};
};
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
url: getDocumentationUrl(__filename)
},
fixable: 'code'
}
};