1 | /**
|
---|
2 | * @fileoverview Utility functions for AST
|
---|
3 | */
|
---|
4 |
|
---|
5 | 'use strict';
|
---|
6 |
|
---|
7 | const estraverse = require('estraverse');
|
---|
8 | const eslintUtil = require('./eslint');
|
---|
9 |
|
---|
10 | const getFirstTokens = eslintUtil.getFirstTokens;
|
---|
11 | const getScope = eslintUtil.getScope;
|
---|
12 | const getSourceCode = eslintUtil.getSourceCode;
|
---|
13 | // const pragmaUtil = require('./pragma');
|
---|
14 |
|
---|
15 | /**
|
---|
16 | * Wrapper for estraverse.traverse
|
---|
17 | *
|
---|
18 | * @param {ASTNode} ASTnode The AST node being checked
|
---|
19 | * @param {Object} visitor Visitor Object for estraverse
|
---|
20 | */
|
---|
21 | function traverse(ASTnode, visitor) {
|
---|
22 | const opts = Object.assign({}, {
|
---|
23 | fallback(node) {
|
---|
24 | return Object.keys(node).filter((key) => key === 'children' || key === 'argument');
|
---|
25 | },
|
---|
26 | }, visitor);
|
---|
27 |
|
---|
28 | opts.keys = Object.assign({}, visitor.keys, {
|
---|
29 | JSXElement: ['children'],
|
---|
30 | JSXFragment: ['children'],
|
---|
31 | });
|
---|
32 |
|
---|
33 | estraverse.traverse(ASTnode, opts);
|
---|
34 | }
|
---|
35 |
|
---|
36 | function loopNodes(nodes) {
|
---|
37 | for (let i = nodes.length - 1; i >= 0; i--) {
|
---|
38 | if (nodes[i].type === 'ReturnStatement') {
|
---|
39 | return nodes[i];
|
---|
40 | }
|
---|
41 | if (nodes[i].type === 'SwitchStatement') {
|
---|
42 | const j = nodes[i].cases.length - 1;
|
---|
43 | if (j >= 0) {
|
---|
44 | return loopNodes(nodes[i].cases[j].consequent);
|
---|
45 | }
|
---|
46 | }
|
---|
47 | }
|
---|
48 | return false;
|
---|
49 | }
|
---|
50 |
|
---|
51 | /**
|
---|
52 | * Find a return statement in the current node
|
---|
53 | *
|
---|
54 | * @param {ASTNode} node The AST node being checked
|
---|
55 | * @returns {ASTNode | false}
|
---|
56 | */
|
---|
57 | function findReturnStatement(node) {
|
---|
58 | if (
|
---|
59 | (!node.value || !node.value.body || !node.value.body.body)
|
---|
60 | && (!node.body || !node.body.body)
|
---|
61 | ) {
|
---|
62 | return false;
|
---|
63 | }
|
---|
64 |
|
---|
65 | const bodyNodes = node.value ? node.value.body.body : node.body.body;
|
---|
66 |
|
---|
67 | return loopNodes(bodyNodes);
|
---|
68 | }
|
---|
69 |
|
---|
70 | // eslint-disable-next-line valid-jsdoc -- valid-jsdoc cannot parse function types.
|
---|
71 | /**
|
---|
72 | * Helper function for traversing "returns" (return statements or the
|
---|
73 | * returned expression in the case of an arrow function) of a function
|
---|
74 | *
|
---|
75 | * @param {ASTNode} ASTNode The AST node being checked
|
---|
76 | * @param {Context} context The context of `ASTNode`.
|
---|
77 | * @param {(returnValue: ASTNode, breakTraverse: () => void) => void} onReturn
|
---|
78 | * Function to execute for each returnStatement found
|
---|
79 | * @returns {undefined}
|
---|
80 | */
|
---|
81 | function traverseReturns(ASTNode, context, onReturn) {
|
---|
82 | const nodeType = ASTNode.type;
|
---|
83 |
|
---|
84 | if (nodeType === 'ReturnStatement') {
|
---|
85 | onReturn(ASTNode.argument, () => {});
|
---|
86 | return;
|
---|
87 | }
|
---|
88 |
|
---|
89 | if (nodeType === 'ArrowFunctionExpression' && ASTNode.expression) {
|
---|
90 | onReturn(ASTNode.body, () => {});
|
---|
91 | return;
|
---|
92 | }
|
---|
93 |
|
---|
94 | /* TODO: properly warn on React.forwardRefs having typo properties
|
---|
95 | if (astUtil.isCallExpression(ASTNode)) {
|
---|
96 | const callee = ASTNode.callee;
|
---|
97 | const pragma = pragmaUtil.getFromContext(context);
|
---|
98 | if (
|
---|
99 | callee.type === 'MemberExpression'
|
---|
100 | && callee.object.type === 'Identifier'
|
---|
101 | && callee.object.name === pragma
|
---|
102 | && callee.property.type === 'Identifier'
|
---|
103 | && callee.property.name === 'forwardRef'
|
---|
104 | && ASTNode.arguments.length > 0
|
---|
105 | ) {
|
---|
106 | return enterFunc(ASTNode.arguments[0]);
|
---|
107 | }
|
---|
108 | return;
|
---|
109 | }
|
---|
110 | */
|
---|
111 |
|
---|
112 | if (
|
---|
113 | nodeType !== 'FunctionExpression'
|
---|
114 | && nodeType !== 'FunctionDeclaration'
|
---|
115 | && nodeType !== 'ArrowFunctionExpression'
|
---|
116 | && nodeType !== 'MethodDefinition'
|
---|
117 | ) {
|
---|
118 | return;
|
---|
119 | }
|
---|
120 |
|
---|
121 | traverse(ASTNode.body, {
|
---|
122 | enter(node) {
|
---|
123 | const breakTraverse = () => {
|
---|
124 | this.break();
|
---|
125 | };
|
---|
126 | switch (node.type) {
|
---|
127 | case 'ReturnStatement':
|
---|
128 | this.skip();
|
---|
129 | onReturn(node.argument, breakTraverse);
|
---|
130 | return;
|
---|
131 | case 'BlockStatement':
|
---|
132 | case 'IfStatement':
|
---|
133 | case 'ForStatement':
|
---|
134 | case 'WhileStatement':
|
---|
135 | case 'SwitchStatement':
|
---|
136 | case 'SwitchCase':
|
---|
137 | return;
|
---|
138 | default:
|
---|
139 | this.skip();
|
---|
140 | }
|
---|
141 | },
|
---|
142 | });
|
---|
143 | }
|
---|
144 |
|
---|
145 | /**
|
---|
146 | * Get node with property's name
|
---|
147 | * @param {Object} node - Property.
|
---|
148 | * @returns {Object} Property name node.
|
---|
149 | */
|
---|
150 | function getPropertyNameNode(node) {
|
---|
151 | if (
|
---|
152 | node.key
|
---|
153 | || node.type === 'MethodDefinition'
|
---|
154 | || node.type === 'Property'
|
---|
155 | ) {
|
---|
156 | return node.key;
|
---|
157 | }
|
---|
158 | if (node.type === 'MemberExpression') {
|
---|
159 | return node.property;
|
---|
160 | }
|
---|
161 | return null;
|
---|
162 | }
|
---|
163 |
|
---|
164 | /**
|
---|
165 | * Get properties name
|
---|
166 | * @param {Object} node - Property.
|
---|
167 | * @returns {string} Property name.
|
---|
168 | */
|
---|
169 | function getPropertyName(node) {
|
---|
170 | const nameNode = getPropertyNameNode(node);
|
---|
171 | return nameNode ? nameNode.name : '';
|
---|
172 | }
|
---|
173 |
|
---|
174 | /**
|
---|
175 | * Get properties for a given AST node
|
---|
176 | * @param {ASTNode} node The AST node being checked.
|
---|
177 | * @returns {Array} Properties array.
|
---|
178 | */
|
---|
179 | function getComponentProperties(node) {
|
---|
180 | switch (node.type) {
|
---|
181 | case 'ClassDeclaration':
|
---|
182 | case 'ClassExpression':
|
---|
183 | return node.body.body;
|
---|
184 | case 'ObjectExpression':
|
---|
185 | return node.properties;
|
---|
186 | default:
|
---|
187 | return [];
|
---|
188 | }
|
---|
189 | }
|
---|
190 |
|
---|
191 | /**
|
---|
192 | * Gets the first node in a line from the initial node, excluding whitespace.
|
---|
193 | * @param {Object} context The node to check
|
---|
194 | * @param {ASTNode} node The node to check
|
---|
195 | * @return {ASTNode} the first node in the line
|
---|
196 | */
|
---|
197 | function getFirstNodeInLine(context, node) {
|
---|
198 | const sourceCode = getSourceCode(context);
|
---|
199 | let token = node;
|
---|
200 | let lines;
|
---|
201 | do {
|
---|
202 | token = sourceCode.getTokenBefore(token);
|
---|
203 | lines = token.type === 'JSXText'
|
---|
204 | ? token.value.split('\n')
|
---|
205 | : null;
|
---|
206 | } while (
|
---|
207 | token.type === 'JSXText'
|
---|
208 | && /^\s*$/.test(lines[lines.length - 1])
|
---|
209 | );
|
---|
210 | return token;
|
---|
211 | }
|
---|
212 |
|
---|
213 | /**
|
---|
214 | * Checks if the node is the first in its line, excluding whitespace.
|
---|
215 | * @param {Object} context The node to check
|
---|
216 | * @param {ASTNode} node The node to check
|
---|
217 | * @return {boolean} true if it's the first node in its line
|
---|
218 | */
|
---|
219 | function isNodeFirstInLine(context, node) {
|
---|
220 | const token = getFirstNodeInLine(context, node);
|
---|
221 | const startLine = node.loc.start.line;
|
---|
222 | const endLine = token ? token.loc.end.line : -1;
|
---|
223 | return startLine !== endLine;
|
---|
224 | }
|
---|
225 |
|
---|
226 | /**
|
---|
227 | * Checks if the node is a function or arrow function expression.
|
---|
228 | * @param {ASTNode} node The node to check
|
---|
229 | * @return {boolean} true if it's a function-like expression
|
---|
230 | */
|
---|
231 | function isFunctionLikeExpression(node) {
|
---|
232 | return node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression';
|
---|
233 | }
|
---|
234 |
|
---|
235 | /**
|
---|
236 | * Checks if the node is a function.
|
---|
237 | * @param {ASTNode} node The node to check
|
---|
238 | * @return {boolean} true if it's a function
|
---|
239 | */
|
---|
240 | function isFunction(node) {
|
---|
241 | return node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration';
|
---|
242 | }
|
---|
243 |
|
---|
244 | /**
|
---|
245 | * Checks if node is a function declaration or expression or arrow function.
|
---|
246 | * @param {ASTNode} node The node to check
|
---|
247 | * @return {boolean} true if it's a function-like
|
---|
248 | */
|
---|
249 | function isFunctionLike(node) {
|
---|
250 | return node.type === 'FunctionDeclaration' || isFunctionLikeExpression(node);
|
---|
251 | }
|
---|
252 |
|
---|
253 | /**
|
---|
254 | * Checks if the node is a class.
|
---|
255 | * @param {ASTNode} node The node to check
|
---|
256 | * @return {boolean} true if it's a class
|
---|
257 | */
|
---|
258 | function isClass(node) {
|
---|
259 | return node.type === 'ClassDeclaration' || node.type === 'ClassExpression';
|
---|
260 | }
|
---|
261 |
|
---|
262 | /**
|
---|
263 | * Check if we are in a class constructor
|
---|
264 | * @param {Context} context
|
---|
265 | * @param {ASTNode} node The AST node being checked.
|
---|
266 | * @return {boolean}
|
---|
267 | */
|
---|
268 | function inConstructor(context, node) {
|
---|
269 | let scope = getScope(context, node);
|
---|
270 | while (scope) {
|
---|
271 | // @ts-ignore
|
---|
272 | if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') {
|
---|
273 | return true;
|
---|
274 | }
|
---|
275 | scope = scope.upper;
|
---|
276 | }
|
---|
277 | return false;
|
---|
278 | }
|
---|
279 |
|
---|
280 | /**
|
---|
281 | * Removes quotes from around an identifier.
|
---|
282 | * @param {string} string the identifier to strip
|
---|
283 | * @returns {string}
|
---|
284 | */
|
---|
285 | function stripQuotes(string) {
|
---|
286 | return string.replace(/^'|'$/g, '');
|
---|
287 | }
|
---|
288 |
|
---|
289 | /**
|
---|
290 | * Retrieve the name of a key node
|
---|
291 | * @param {Context} context The AST node with the key.
|
---|
292 | * @param {any} node The AST node with the key.
|
---|
293 | * @return {string | undefined} the name of the key
|
---|
294 | */
|
---|
295 | function getKeyValue(context, node) {
|
---|
296 | if (node.type === 'ObjectTypeProperty') {
|
---|
297 | const tokens = getFirstTokens(context, node, 2);
|
---|
298 | return (tokens[0].value === '+' || tokens[0].value === '-'
|
---|
299 | ? tokens[1].value
|
---|
300 | : stripQuotes(tokens[0].value)
|
---|
301 | );
|
---|
302 | }
|
---|
303 | if (node.type === 'GenericTypeAnnotation') {
|
---|
304 | return node.id.name;
|
---|
305 | }
|
---|
306 | if (node.type === 'ObjectTypeAnnotation') {
|
---|
307 | return;
|
---|
308 | }
|
---|
309 | const key = node.key || node.argument;
|
---|
310 | if (!key) {
|
---|
311 | return;
|
---|
312 | }
|
---|
313 | return key.type === 'Identifier' ? key.name : key.value;
|
---|
314 | }
|
---|
315 |
|
---|
316 | /**
|
---|
317 | * Checks if a node is surrounded by parenthesis.
|
---|
318 | *
|
---|
319 | * @param {object} context - Context from the rule
|
---|
320 | * @param {ASTNode} node - Node to be checked
|
---|
321 | * @returns {boolean}
|
---|
322 | */
|
---|
323 | function isParenthesized(context, node) {
|
---|
324 | const sourceCode = getSourceCode(context);
|
---|
325 | const previousToken = sourceCode.getTokenBefore(node);
|
---|
326 | const nextToken = sourceCode.getTokenAfter(node);
|
---|
327 |
|
---|
328 | return !!previousToken && !!nextToken
|
---|
329 | && previousToken.value === '(' && previousToken.range[1] <= node.range[0]
|
---|
330 | && nextToken.value === ')' && nextToken.range[0] >= node.range[1];
|
---|
331 | }
|
---|
332 |
|
---|
333 | /**
|
---|
334 | * Checks if a node is being assigned a value: props.bar = 'bar'
|
---|
335 | * @param {ASTNode} node The AST node being checked.
|
---|
336 | * @returns {boolean}
|
---|
337 | */
|
---|
338 | function isAssignmentLHS(node) {
|
---|
339 | return (
|
---|
340 | node.parent
|
---|
341 | && node.parent.type === 'AssignmentExpression'
|
---|
342 | && node.parent.left === node
|
---|
343 | );
|
---|
344 | }
|
---|
345 |
|
---|
346 | function isTSAsExpression(node) {
|
---|
347 | return node && node.type === 'TSAsExpression';
|
---|
348 | }
|
---|
349 |
|
---|
350 | /**
|
---|
351 | * Matcher used to check whether given node is a `CallExpression`
|
---|
352 | * @param {ASTNode} node The AST node
|
---|
353 | * @returns {boolean} True if node is a `CallExpression`, false if not
|
---|
354 | */
|
---|
355 | function isCallExpression(node) {
|
---|
356 | return node && node.type === 'CallExpression';
|
---|
357 | }
|
---|
358 |
|
---|
359 | /**
|
---|
360 | * Extracts the expression node that is wrapped inside a TS type assertion
|
---|
361 | *
|
---|
362 | * @param {ASTNode} node - potential TS node
|
---|
363 | * @returns {ASTNode} - unwrapped expression node
|
---|
364 | */
|
---|
365 | function unwrapTSAsExpression(node) {
|
---|
366 | return isTSAsExpression(node) ? node.expression : node;
|
---|
367 | }
|
---|
368 |
|
---|
369 | function isTSTypeReference(node) {
|
---|
370 | if (!node) return false;
|
---|
371 |
|
---|
372 | return node.type === 'TSTypeReference';
|
---|
373 | }
|
---|
374 |
|
---|
375 | function isTSTypeAnnotation(node) {
|
---|
376 | if (!node) { return false; }
|
---|
377 |
|
---|
378 | return node.type === 'TSTypeAnnotation';
|
---|
379 | }
|
---|
380 |
|
---|
381 | function isTSTypeLiteral(node) {
|
---|
382 | if (!node) { return false; }
|
---|
383 |
|
---|
384 | return node.type === 'TSTypeLiteral';
|
---|
385 | }
|
---|
386 |
|
---|
387 | function isTSIntersectionType(node) {
|
---|
388 | if (!node) { return false; }
|
---|
389 |
|
---|
390 | return node.type === 'TSIntersectionType';
|
---|
391 | }
|
---|
392 |
|
---|
393 | function isTSInterfaceHeritage(node) {
|
---|
394 | if (!node) { return false; }
|
---|
395 |
|
---|
396 | return node.type === 'TSInterfaceHeritage';
|
---|
397 | }
|
---|
398 |
|
---|
399 | function isTSInterfaceDeclaration(node) {
|
---|
400 | if (!node) { return false; }
|
---|
401 |
|
---|
402 | return (node.type === 'ExportNamedDeclaration' && node.declaration
|
---|
403 | ? node.declaration.type
|
---|
404 | : node.type
|
---|
405 | ) === 'TSInterfaceDeclaration';
|
---|
406 | }
|
---|
407 |
|
---|
408 | function isTSTypeDeclaration(node) {
|
---|
409 | if (!node) { return false; }
|
---|
410 |
|
---|
411 | const nodeToCheck = node.type === 'ExportNamedDeclaration' && node.declaration
|
---|
412 | ? node.declaration
|
---|
413 | : node;
|
---|
414 |
|
---|
415 | return nodeToCheck.type === 'VariableDeclaration' && nodeToCheck.kind === 'type';
|
---|
416 | }
|
---|
417 |
|
---|
418 | function isTSTypeAliasDeclaration(node) {
|
---|
419 | if (!node) { return false; }
|
---|
420 |
|
---|
421 | if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
---|
422 | return node.declaration.type === 'TSTypeAliasDeclaration' && node.exportKind === 'type';
|
---|
423 | }
|
---|
424 | return node.type === 'TSTypeAliasDeclaration';
|
---|
425 | }
|
---|
426 |
|
---|
427 | function isTSParenthesizedType(node) {
|
---|
428 | if (!node) { return false; }
|
---|
429 |
|
---|
430 | return node.type === 'TSTypeAliasDeclaration';
|
---|
431 | }
|
---|
432 |
|
---|
433 | function isTSFunctionType(node) {
|
---|
434 | if (!node) { return false; }
|
---|
435 |
|
---|
436 | return node.type === 'TSFunctionType';
|
---|
437 | }
|
---|
438 |
|
---|
439 | function isTSTypeQuery(node) {
|
---|
440 | if (!node) { return false; }
|
---|
441 |
|
---|
442 | return node.type === 'TSTypeQuery';
|
---|
443 | }
|
---|
444 |
|
---|
445 | function isTSTypeParameterInstantiation(node) {
|
---|
446 | if (!node) { return false; }
|
---|
447 |
|
---|
448 | return node.type === 'TSTypeParameterInstantiation';
|
---|
449 | }
|
---|
450 |
|
---|
451 | module.exports = {
|
---|
452 | findReturnStatement,
|
---|
453 | getComponentProperties,
|
---|
454 | getFirstNodeInLine,
|
---|
455 | getKeyValue,
|
---|
456 | getPropertyName,
|
---|
457 | getPropertyNameNode,
|
---|
458 | inConstructor,
|
---|
459 | isAssignmentLHS,
|
---|
460 | isCallExpression,
|
---|
461 | isClass,
|
---|
462 | isFunction,
|
---|
463 | isFunctionLike,
|
---|
464 | isFunctionLikeExpression,
|
---|
465 | isNodeFirstInLine,
|
---|
466 | isParenthesized,
|
---|
467 | isTSAsExpression,
|
---|
468 | isTSFunctionType,
|
---|
469 | isTSInterfaceDeclaration,
|
---|
470 | isTSInterfaceHeritage,
|
---|
471 | isTSIntersectionType,
|
---|
472 | isTSParenthesizedType,
|
---|
473 | isTSTypeAliasDeclaration,
|
---|
474 | isTSTypeAnnotation,
|
---|
475 | isTSTypeDeclaration,
|
---|
476 | isTSTypeLiteral,
|
---|
477 | isTSTypeParameterInstantiation,
|
---|
478 | isTSTypeQuery,
|
---|
479 | isTSTypeReference,
|
---|
480 | traverse,
|
---|
481 | traverseReturns,
|
---|
482 | unwrapTSAsExpression,
|
---|
483 | };
|
---|