228 lines
5.0 KiB
JavaScript
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'
|
|
}
|
|
};
|