prefer-keyboard-event-key.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. 'use strict';
  2. const escapeString = require('./utils/escape-string.js');
  3. const translateToKey = require('./shared/event-keys.js');
  4. const {isNumberLiteral} = require('./ast/index.js');
  5. const MESSAGE_ID = 'prefer-keyboard-event-key';
  6. const messages = {
  7. [MESSAGE_ID]: 'Use `.key` instead of `.{{name}}`.',
  8. };
  9. const keys = new Set([
  10. 'keyCode',
  11. 'charCode',
  12. 'which',
  13. ]);
  14. const isPropertyNamedAddEventListener = node =>
  15. node?.type === 'CallExpression'
  16. && node.callee.type === 'MemberExpression'
  17. && node.callee.property.name === 'addEventListener';
  18. const getEventNodeAndReferences = (context, node) => {
  19. const eventListener = getMatchingAncestorOfType(node, 'CallExpression', isPropertyNamedAddEventListener);
  20. const callback = eventListener?.arguments[1];
  21. switch (callback?.type) {
  22. case 'ArrowFunctionExpression':
  23. case 'FunctionExpression': {
  24. const eventVariable = context.getDeclaredVariables(callback)[0];
  25. const references = eventVariable?.references;
  26. return {
  27. event: callback.params[0],
  28. references,
  29. };
  30. }
  31. default: {
  32. return {};
  33. }
  34. }
  35. };
  36. const isPropertyOf = (node, eventNode) =>
  37. node?.parent?.type === 'MemberExpression'
  38. && node.parent.object === eventNode;
  39. // The third argument is a condition function, as one passed to `Array#filter()`
  40. // Helpful if nearest node of type also needs to have some other property
  41. const getMatchingAncestorOfType = (node, type, testFunction = () => true) => {
  42. let current = node;
  43. while (current) {
  44. if (current.type === type && testFunction(current)) {
  45. return current;
  46. }
  47. current = current.parent;
  48. }
  49. };
  50. const getParentByLevel = (node, level) => {
  51. let current = node;
  52. while (current && level) {
  53. level--;
  54. current = current.parent;
  55. }
  56. /* c8 ignore next 3 */
  57. if (level === 0) {
  58. return current;
  59. }
  60. };
  61. const fix = node => fixer => {
  62. // Since we're only fixing direct property access usages, like `event.keyCode`
  63. const nearestIf = getParentByLevel(node, 3);
  64. if (!nearestIf || nearestIf.type !== 'IfStatement') {
  65. return;
  66. }
  67. const {type, operator, right} = nearestIf.test;
  68. if (
  69. !(
  70. type === 'BinaryExpression'
  71. && (operator === '==' || operator === '===')
  72. && isNumberLiteral(right)
  73. )
  74. ) {
  75. return;
  76. }
  77. // Either a meta key or a printable character
  78. const key = translateToKey[right.value] || String.fromCodePoint(right.value);
  79. // And if we recognize the `.keyCode`
  80. if (!key) {
  81. return;
  82. }
  83. // Apply fixes
  84. return [
  85. fixer.replaceText(node, 'key'),
  86. fixer.replaceText(right, escapeString(key)),
  87. ];
  88. };
  89. const getProblem = node => ({
  90. messageId: MESSAGE_ID,
  91. data: {name: node.name},
  92. node,
  93. fix: fix(node),
  94. });
  95. /** @param {import('eslint').Rule.RuleContext} context */
  96. const create = context => ({
  97. 'Identifier:matches([name="keyCode"], [name="charCode"], [name="which"])'(node) {
  98. // Normal case when usage is direct -> `event.keyCode`
  99. const {event, references} = getEventNodeAndReferences(context, node);
  100. if (!event) {
  101. return;
  102. }
  103. if (
  104. references
  105. && references.some(reference => isPropertyOf(node, reference.identifier))
  106. ) {
  107. return getProblem(node);
  108. }
  109. },
  110. Property(node) {
  111. // Destructured case
  112. const propertyName = node.value.name;
  113. if (!keys.has(propertyName)) {
  114. return;
  115. }
  116. const {event, references} = getEventNodeAndReferences(context, node);
  117. if (!event) {
  118. return;
  119. }
  120. const nearestVariableDeclarator = getMatchingAncestorOfType(
  121. node,
  122. 'VariableDeclarator',
  123. );
  124. const initObject = nearestVariableDeclarator?.init;
  125. // Make sure initObject is a reference of eventVariable
  126. if (
  127. references
  128. && references.some(reference => reference.identifier === initObject)
  129. ) {
  130. return getProblem(node.value);
  131. }
  132. // When the event parameter itself is destructured directly
  133. const isEventParameterDestructured = event.type === 'ObjectPattern';
  134. if (isEventParameterDestructured) {
  135. // Check for properties
  136. for (const property of event.properties) {
  137. if (property === node) {
  138. return getProblem(node.value);
  139. }
  140. }
  141. }
  142. },
  143. });
  144. /** @type {import('eslint').Rule.RuleModule} */
  145. module.exports = {
  146. create,
  147. meta: {
  148. type: 'suggestion',
  149. docs: {
  150. description: 'Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`.',
  151. },
  152. fixable: 'code',
  153. messages,
  154. },
  155. };