prefer-keyboard-event-key.js 4.4 KB

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