UNPKG

5.15 kBJavaScriptView Raw
1'use strict';
2const getDocumentationUrl = require('./utils/get-documentation-url');
3const quoteString = require('./utils/quote-string');
4
5const keys = new Set([
6 'keyCode',
7 'charCode',
8 'which'
9]);
10
11// https://github.com/facebook/react/blob/b87aabd/packages/react-dom/src/events/getEventKey.js#L36
12// Only meta characters which can't be deciphered from `String.fromCharCode()`
13const translateToKey = {
14 8: 'Backspace',
15 9: 'Tab',
16 12: 'Clear',
17 13: 'Enter',
18 16: 'Shift',
19 17: 'Control',
20 18: 'Alt',
21 19: 'Pause',
22 20: 'CapsLock',
23 27: 'Escape',
24 32: ' ',
25 33: 'PageUp',
26 34: 'PageDown',
27 35: 'End',
28 36: 'Home',
29 37: 'ArrowLeft',
30 38: 'ArrowUp',
31 39: 'ArrowRight',
32 40: 'ArrowDown',
33 45: 'Insert',
34 46: 'Delete',
35 112: 'F1',
36 113: 'F2',
37 114: 'F3',
38 115: 'F4',
39 116: 'F5',
40 117: 'F6',
41 118: 'F7',
42 119: 'F8',
43 120: 'F9',
44 121: 'F10',
45 122: 'F11',
46 123: 'F12',
47 144: 'NumLock',
48 145: 'ScrollLock',
49 186: ';',
50 187: '=',
51 188: ',',
52 189: '-',
53 190: '.',
54 191: '/',
55 219: '[',
56 220: '\\',
57 221: ']',
58 222: '\'',
59 224: 'Meta'
60};
61
62const isPropertyNamedAddEventListener = node =>
63 node &&
64 node.type === 'CallExpression' &&
65 node.callee &&
66 node.callee.type === 'MemberExpression' &&
67 node.callee.property &&
68 node.callee.property.name === 'addEventListener';
69
70const getEventNodeAndReferences = (context, node) => {
71 const eventListener = getMatchingAncestorOfType(node, 'CallExpression', isPropertyNamedAddEventListener);
72 const callback = eventListener && eventListener.arguments && eventListener.arguments[1];
73 switch (callback && callback.type) {
74 case 'ArrowFunctionExpression':
75 case 'FunctionExpression': {
76 const eventVariable = context.getDeclaredVariables(callback)[0];
77 const references = eventVariable && eventVariable.references;
78 return {
79 event: callback.params && callback.params[0],
80 references
81 };
82 }
83
84 default:
85 return {};
86 }
87};
88
89const isPropertyOf = (node, eventNode) => {
90 return (
91 node &&
92 node.parent &&
93 node.parent.type === 'MemberExpression' &&
94 node.parent.object &&
95 node.parent.object === eventNode
96 );
97};
98
99// The third argument is a condition function, as one passed to `Array#filter()`
100// Helpful if nearest node of type also needs to have some other property
101const getMatchingAncestorOfType = (node, type, fn = () => true) => {
102 let current = node;
103 while (current) {
104 if (current.type === type && fn(current)) {
105 return current;
106 }
107
108 current = current.parent;
109 }
110};
111
112const getParentByLevel = (node, level) => {
113 let current = node;
114 while (current && level) {
115 level--;
116 current = current.parent;
117 }
118
119 /* istanbul ignore else */
120 if (level === 0) {
121 return current;
122 }
123};
124
125const fix = node => fixer => {
126 // Since we're only fixing direct property access usages, like `event.keyCode`
127 const nearestIf = getParentByLevel(node, 3);
128 if (!nearestIf || nearestIf.type !== 'IfStatement') {
129 return;
130 }
131
132 const {right = {}, operator} = nearestIf.test;
133 const isTestingEquality = operator === '==' || operator === '===';
134 const isRightValid = isTestingEquality && right.type === 'Literal' && typeof right.value === 'number';
135 // Either a meta key or a printable character
136 const keyCode = translateToKey[right.value] || String.fromCharCode(right.value);
137 // And if we recognize the `.keyCode`
138 if (!isRightValid || !keyCode) {
139 return;
140 }
141
142 // Apply fixes
143 return [
144 fixer.replaceText(node, 'key'),
145 fixer.replaceText(right, quoteString(keyCode))
146 ];
147};
148
149const create = context => {
150 const report = node => {
151 context.report({
152 message: `Use \`.key\` instead of \`.${node.name}\``,
153 node,
154 fix: fix(node)
155 });
156 };
157
158 return {
159 'Identifier:matches([name="keyCode"], [name="charCode"], [name="which"])'(node) {
160 // Normal case when usage is direct -> `event.keyCode`
161 const {event, references} = getEventNodeAndReferences(context, node);
162 if (!event) {
163 return;
164 }
165
166 if (
167 references &&
168 references.find(reference => isPropertyOf(node, reference.identifier))
169 ) {
170 report(node);
171 }
172 },
173
174 Property(node) {
175 // Destructured case
176 const propertyName = node.value && node.value.name;
177 if (!keys.has(propertyName)) {
178 return;
179 }
180
181 const {event, references} = getEventNodeAndReferences(context, node);
182 if (!event) {
183 return;
184 }
185
186 const nearestVariableDeclarator = getMatchingAncestorOfType(
187 node,
188 'VariableDeclarator'
189 );
190 const initObject =
191 nearestVariableDeclarator &&
192 nearestVariableDeclarator.init &&
193 nearestVariableDeclarator.init;
194
195 // Make sure initObject is a reference of eventVariable
196 if (
197 references &&
198 references.find(reference => reference.identifier === initObject)
199 ) {
200 report(node.value);
201 return;
202 }
203
204 // When the event parameter itself is destructured directly
205 const isEventParameterDestructured = event.type === 'ObjectPattern';
206 if (isEventParameterDestructured) {
207 // Check for properties
208 for (const property of event.properties) {
209 if (property === node) {
210 report(node.value);
211 }
212 }
213 }
214 }
215 };
216};
217
218module.exports = {
219 create,
220 meta: {
221 type: 'suggestion',
222 docs: {
223 url: getDocumentationUrl(__filename)
224 },
225 fixable: 'code'
226 }
227};