securityos/node_modules/eslint-plugin-unicorn/rules/prefer-keyboard-event-key.js

179 lines
4.2 KiB
JavaScript
Raw Normal View History

2024-09-06 15:32:35 +00:00
'use strict';
const escapeString = require('./utils/escape-string.js');
const translateToKey = require('./shared/event-keys.js');
const {isNumberLiteral} = require('./ast/index.js');
const MESSAGE_ID = 'prefer-keyboard-event-key';
const messages = {
[MESSAGE_ID]: 'Use `.key` instead of `.{{name}}`.',
};
const keys = new Set([
'keyCode',
'charCode',
'which',
]);
const isPropertyNamedAddEventListener = node =>
node?.type === 'CallExpression'
&& node.callee.type === 'MemberExpression'
&& node.callee.property.name === 'addEventListener';
const getEventNodeAndReferences = (context, node) => {
const eventListener = getMatchingAncestorOfType(node, 'CallExpression', isPropertyNamedAddEventListener);
const callback = eventListener?.arguments[1];
switch (callback?.type) {
case 'ArrowFunctionExpression':
case 'FunctionExpression': {
const eventVariable = context.getDeclaredVariables(callback)[0];
const references = eventVariable?.references;
return {
event: callback.params[0],
references,
};
}
default: {
return {};
}
}
};
const isPropertyOf = (node, eventNode) =>
node?.parent?.type === 'MemberExpression'
&& 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, testFunction = () => true) => {
let current = node;
while (current) {
if (current.type === type && testFunction(current)) {
return current;
}
current = current.parent;
}
};
const getParentByLevel = (node, level) => {
let current = node;
while (current && level) {
level--;
current = current.parent;
}
/* c8 ignore next 3 */
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 {type, operator, right} = nearestIf.test;
if (
!(
type === 'BinaryExpression'
&& (operator === '==' || operator === '===')
&& isNumberLiteral(right)
)
) {
return;
}
// Either a meta key or a printable character
const key = translateToKey[right.value] || String.fromCodePoint(right.value);
// And if we recognize the `.keyCode`
if (!key) {
return;
}
// Apply fixes
return [
fixer.replaceText(node, 'key'),
fixer.replaceText(right, escapeString(key)),
];
};
const getProblem = node => ({
messageId: MESSAGE_ID,
data: {name: node.name},
node,
fix: fix(node),
});
/** @param {import('eslint').Rule.RuleContext} context */
const create = context => ({
'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.some(reference => isPropertyOf(node, reference.identifier))
) {
return getProblem(node);
}
},
Property(node) {
// Destructured case
const propertyName = 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?.init;
// Make sure initObject is a reference of eventVariable
if (
references
&& references.some(reference => reference.identifier === initObject)
) {
return getProblem(node.value);
}
// 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) {
return getProblem(node.value);
}
}
}
},
});
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
create,
meta: {
type: 'suggestion',
docs: {
description: 'Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`.',
},
fixable: 'code',
messages,
},
};