123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178 |
- '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,
- },
- };
|