1 | 'use strict';
|
2 | const getDocumentationUrl = require('./utils/get-documentation-url');
|
3 | const quoteString = require('./utils/quote-string');
|
4 |
|
5 | const keys = new Set([
|
6 | 'keyCode',
|
7 | 'charCode',
|
8 | 'which'
|
9 | ]);
|
10 |
|
11 |
|
12 |
|
13 | const 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 |
|
62 | const 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 |
|
70 | const 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 |
|
89 | const 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 |
|
100 |
|
101 | const 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 |
|
112 | const getParentByLevel = (node, level) => {
|
113 | let current = node;
|
114 | while (current && level) {
|
115 | level--;
|
116 | current = current.parent;
|
117 | }
|
118 |
|
119 |
|
120 | if (level === 0) {
|
121 | return current;
|
122 | }
|
123 | };
|
124 |
|
125 | const fix = node => fixer => {
|
126 |
|
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 |
|
136 | const keyCode = translateToKey[right.value] || String.fromCharCode(right.value);
|
137 |
|
138 | if (!isRightValid || !keyCode) {
|
139 | return;
|
140 | }
|
141 |
|
142 |
|
143 | return [
|
144 | fixer.replaceText(node, 'key'),
|
145 | fixer.replaceText(right, quoteString(keyCode))
|
146 | ];
|
147 | };
|
148 |
|
149 | const 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 |
|
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 |
|
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 |
|
196 | if (
|
197 | references &&
|
198 | references.find(reference => reference.identifier === initObject)
|
199 | ) {
|
200 | report(node.value);
|
201 | return;
|
202 | }
|
203 |
|
204 |
|
205 | const isEventParameterDestructured = event.type === 'ObjectPattern';
|
206 | if (isEventParameterDestructured) {
|
207 |
|
208 | for (const property of event.properties) {
|
209 | if (property === node) {
|
210 | report(node.value);
|
211 | }
|
212 | }
|
213 | }
|
214 | }
|
215 | };
|
216 | };
|
217 |
|
218 | module.exports = {
|
219 | create,
|
220 | meta: {
|
221 | type: 'suggestion',
|
222 | docs: {
|
223 | url: getDocumentationUrl(__filename)
|
224 | },
|
225 | fixable: 'code'
|
226 | }
|
227 | };
|