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 MESSAGE_ID = 'prefer-keyboard-event-key';
  5. const messages = {
  6. [MESSAGE_ID]: 'Use `.key` instead of `.{{name}}`.',
  7. };
  8. const keys = new Set([
  9. 'keyCode',
  10. 'charCode',
  11. 'which',
  12. ]);
  13. const isPropertyNamedAddEventListener = node =>
  14. node
  15. && node.type === 'CallExpression'
  16. && node.callee
  17. && node.callee.type === 'MemberExpression'
  18. && node.callee.property
  19. && node.callee.property.name === 'addEventListener';
  20. const getEventNodeAndReferences = (context, node) => {
  21. const eventListener = getMatchingAncestorOfType(node, 'CallExpression', isPropertyNamedAddEventListener);
  22. const callback = eventListener && eventListener.arguments && eventListener.arguments[1];
  23. switch (callback && callback.type) {
  24. case 'ArrowFunctionExpression':
  25. case 'FunctionExpression': {
  26. const eventVariable = context.getDeclaredVariables(callback)[0];
  27. const references = eventVariable && eventVariable.references;
  28. return {
  29. event: callback.params && callback.params[0],
  30. references,
  31. };
  32. }
  33. default:
  34. return {};
  35. }
  36. };
  37. const isPropertyOf = (node, eventNode) =>
  38. node
  39. && node.parent
  40. && node.parent.type === 'MemberExpression'
  41. && node.parent.object
  42. && node.parent.object === eventNode;
  43. // The third argument is a condition function, as one passed to `Array#filter()`
  44. // Helpful if nearest node of type also needs to have some other property
  45. const getMatchingAncestorOfType = (node, type, testFunction = () => true) => {
  46. let current = node;
  47. while (current) {
  48. if (current.type === type && testFunction(current)) {
  49. return current;
  50. }
  51. current = current.parent;
  52. }
  53. };
  54. const getParentByLevel = (node, level) => {
  55. let current = node;
  56. while (current && level) {
  57. level--;
  58. current = current.parent;
  59. }
  60. /* istanbul ignore else */
  61. if (level === 0) {
  62. return current;
  63. }
  64. };
  65. const fix = node => fixer => {
  66. // Since we're only fixing direct property access usages, like `event.keyCode`
  67. const nearestIf = getParentByLevel(node, 3);
  68. if (!nearestIf || nearestIf.type !== 'IfStatement') {
  69. return;
  70. }
  71. const {type, operator, right} = nearestIf.test;
  72. if (
  73. !(
  74. type === 'BinaryExpression'
  75. && (operator === '==' || operator === '===')
  76. && right.type === 'Literal'
  77. && typeof right.value === 'number'
  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. };