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 (nodeType === 'CallExpression') {
|
---|
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 (node.key || ['MethodDefinition', 'Property'].indexOf(node.type) !== -1) {
|
---|
152 | return node.key;
|
---|
153 | }
|
---|
154 | if (node.type === 'MemberExpression') {
|
---|
155 | return node.property;
|
---|
156 | }
|
---|
157 | return null;
|
---|
158 | }
|
---|
159 |
|
---|
160 | /**
|
---|
161 | * Get properties name
|
---|
162 | * @param {Object} node - Property.
|
---|
163 | * @returns {String} Property name.
|
---|
164 | */
|
---|
165 | function getPropertyName(node) {
|
---|
166 | const nameNode = getPropertyNameNode(node);
|
---|
167 | return nameNode ? nameNode.name : '';
|
---|
168 | }
|
---|
169 |
|
---|
170 | /**
|
---|
171 | * Get properties for a given AST node
|
---|
172 | * @param {ASTNode} node The AST node being checked.
|
---|
173 | * @returns {Array} Properties array.
|
---|
174 | */
|
---|
175 | function getComponentProperties(node) {
|
---|
176 | switch (node.type) {
|
---|
177 | case 'ClassDeclaration':
|
---|
178 | case 'ClassExpression':
|
---|
179 | return node.body.body;
|
---|
180 | case 'ObjectExpression':
|
---|
181 | return node.properties;
|
---|
182 | default:
|
---|
183 | return [];
|
---|
184 | }
|
---|
185 | }
|
---|
186 |
|
---|
187 | /**
|
---|
188 | * Gets the first node in a line from the initial node, excluding whitespace.
|
---|
189 | * @param {Object} context The node to check
|
---|
190 | * @param {ASTNode} node The node to check
|
---|
191 | * @return {ASTNode} the first node in the line
|
---|
192 | */
|
---|
193 | function getFirstNodeInLine(context, node) {
|
---|
194 | const sourceCode = getSourceCode(context);
|
---|
195 | let token = node;
|
---|
196 | let lines;
|
---|
197 | do {
|
---|
198 | token = sourceCode.getTokenBefore(token);
|
---|
199 | lines = token.type === 'JSXText'
|
---|
200 | ? token.value.split('\n')
|
---|
201 | : null;
|
---|
202 | } while (
|
---|
203 | token.type === 'JSXText'
|
---|
204 | && /^\s*$/.test(lines[lines.length - 1])
|
---|
205 | );
|
---|
206 | return token;
|
---|
207 | }
|
---|
208 |
|
---|
209 | /**
|
---|
210 | * Checks if the node is the first in its line, excluding whitespace.
|
---|
211 | * @param {Object} context The node to check
|
---|
212 | * @param {ASTNode} node The node to check
|
---|
213 | * @return {Boolean} true if it's the first node in its line
|
---|
214 | */
|
---|
215 | function isNodeFirstInLine(context, node) {
|
---|
216 | const token = getFirstNodeInLine(context, node);
|
---|
217 | const startLine = node.loc.start.line;
|
---|
218 | const endLine = token ? token.loc.end.line : -1;
|
---|
219 | return startLine !== endLine;
|
---|
220 | }
|
---|
221 |
|
---|
222 | /**
|
---|
223 | * Checks if the node is a function or arrow function expression.
|
---|
224 | * @param {ASTNode} node The node to check
|
---|
225 | * @return {Boolean} true if it's a function-like expression
|
---|
226 | */
|
---|
227 | function isFunctionLikeExpression(node) {
|
---|
228 | return node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression';
|
---|
229 | }
|
---|
230 |
|
---|
231 | /**
|
---|
232 | * Checks if the node is a function.
|
---|
233 | * @param {ASTNode} node The node to check
|
---|
234 | * @return {Boolean} true if it's a function
|
---|
235 | */
|
---|
236 | function isFunction(node) {
|
---|
237 | return node.type === 'FunctionExpression' || node.type === 'FunctionDeclaration';
|
---|
238 | }
|
---|
239 |
|
---|
240 | /**
|
---|
241 | * Checks if node is a function declaration or expression or arrow function.
|
---|
242 | * @param {ASTNode} node The node to check
|
---|
243 | * @return {Boolean} true if it's a function-like
|
---|
244 | */
|
---|
245 | function isFunctionLike(node) {
|
---|
246 | return node.type === 'FunctionDeclaration' || isFunctionLikeExpression(node);
|
---|
247 | }
|
---|
248 |
|
---|
249 | /**
|
---|
250 | * Checks if the node is a class.
|
---|
251 | * @param {ASTNode} node The node to check
|
---|
252 | * @return {Boolean} true if it's a class
|
---|
253 | */
|
---|
254 | function isClass(node) {
|
---|
255 | return node.type === 'ClassDeclaration' || node.type === 'ClassExpression';
|
---|
256 | }
|
---|
257 |
|
---|
258 | /**
|
---|
259 | * Check if we are in a class constructor
|
---|
260 | * @param {Context} context
|
---|
261 | * @param {ASTNode} node The AST node being checked.
|
---|
262 | * @return {boolean}
|
---|
263 | */
|
---|
264 | function inConstructor(context, node) {
|
---|
265 | let scope = getScope(context, node);
|
---|
266 | while (scope) {
|
---|
267 | // @ts-ignore
|
---|
268 | if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') {
|
---|
269 | return true;
|
---|
270 | }
|
---|
271 | scope = scope.upper;
|
---|
272 | }
|
---|
273 | return false;
|
---|
274 | }
|
---|
275 |
|
---|
276 | /**
|
---|
277 | * Removes quotes from around an identifier.
|
---|
278 | * @param {string} string the identifier to strip
|
---|
279 | * @returns {string}
|
---|
280 | */
|
---|
281 | function stripQuotes(string) {
|
---|
282 | return string.replace(/^'|'$/g, '');
|
---|
283 | }
|
---|
284 |
|
---|
285 | /**
|
---|
286 | * Retrieve the name of a key node
|
---|
287 | * @param {Context} context The AST node with the key.
|
---|
288 | * @param {any} node The AST node with the key.
|
---|
289 | * @return {string | undefined} the name of the key
|
---|
290 | */
|
---|
291 | function getKeyValue(context, node) {
|
---|
292 | if (node.type === 'ObjectTypeProperty') {
|
---|
293 | const tokens = getFirstTokens(context, node, 2);
|
---|
294 | return (tokens[0].value === '+' || tokens[0].value === '-'
|
---|
295 | ? tokens[1].value
|
---|
296 | : stripQuotes(tokens[0].value)
|
---|
297 | );
|
---|
298 | }
|
---|
299 | if (node.type === 'GenericTypeAnnotation') {
|
---|
300 | return node.id.name;
|
---|
301 | }
|
---|
302 | if (node.type === 'ObjectTypeAnnotation') {
|
---|
303 | return;
|
---|
304 | }
|
---|
305 | const key = node.key || node.argument;
|
---|
306 | if (!key) {
|
---|
307 | return;
|
---|
308 | }
|
---|
309 | return key.type === 'Identifier' ? key.name : key.value;
|
---|
310 | }
|
---|
311 |
|
---|
312 | /**
|
---|
313 | * Checks if a node is surrounded by parenthesis.
|
---|
314 | *
|
---|
315 | * @param {object} context - Context from the rule
|
---|
316 | * @param {ASTNode} node - Node to be checked
|
---|
317 | * @returns {boolean}
|
---|
318 | */
|
---|
319 | function isParenthesized(context, node) {
|
---|
320 | const sourceCode = getSourceCode(context);
|
---|
321 | const previousToken = sourceCode.getTokenBefore(node);
|
---|
322 | const nextToken = sourceCode.getTokenAfter(node);
|
---|
323 |
|
---|
324 | return !!previousToken && !!nextToken
|
---|
325 | && previousToken.value === '(' && previousToken.range[1] <= node.range[0]
|
---|
326 | && nextToken.value === ')' && nextToken.range[0] >= node.range[1];
|
---|
327 | }
|
---|
328 |
|
---|
329 | /**
|
---|
330 | * Checks if a node is being assigned a value: props.bar = 'bar'
|
---|
331 | * @param {ASTNode} node The AST node being checked.
|
---|
332 | * @returns {Boolean}
|
---|
333 | */
|
---|
334 | function isAssignmentLHS(node) {
|
---|
335 | return (
|
---|
336 | node.parent
|
---|
337 | && node.parent.type === 'AssignmentExpression'
|
---|
338 | && node.parent.left === node
|
---|
339 | );
|
---|
340 | }
|
---|
341 |
|
---|
342 | /**
|
---|
343 | * Extracts the expression node that is wrapped inside a TS type assertion
|
---|
344 | *
|
---|
345 | * @param {ASTNode} node - potential TS node
|
---|
346 | * @returns {ASTNode} - unwrapped expression node
|
---|
347 | */
|
---|
348 | function unwrapTSAsExpression(node) {
|
---|
349 | if (node && node.type === 'TSAsExpression') return node.expression;
|
---|
350 | return node;
|
---|
351 | }
|
---|
352 |
|
---|
353 | function isTSTypeReference(node) {
|
---|
354 | if (!node) return false;
|
---|
355 | const nodeType = node.type;
|
---|
356 | return nodeType === 'TSTypeReference';
|
---|
357 | }
|
---|
358 |
|
---|
359 | function isTSTypeAnnotation(node) {
|
---|
360 | if (!node) return false;
|
---|
361 | const nodeType = node.type;
|
---|
362 | return nodeType === 'TSTypeAnnotation';
|
---|
363 | }
|
---|
364 |
|
---|
365 | function isTSTypeLiteral(node) {
|
---|
366 | if (!node) return false;
|
---|
367 | const nodeType = node.type;
|
---|
368 | return nodeType === 'TSTypeLiteral';
|
---|
369 | }
|
---|
370 |
|
---|
371 | function isTSIntersectionType(node) {
|
---|
372 | if (!node) return false;
|
---|
373 | const nodeType = node.type;
|
---|
374 | return nodeType === 'TSIntersectionType';
|
---|
375 | }
|
---|
376 |
|
---|
377 | function isTSInterfaceHeritage(node) {
|
---|
378 | if (!node) return false;
|
---|
379 | const nodeType = node.type;
|
---|
380 | return nodeType === 'TSInterfaceHeritage';
|
---|
381 | }
|
---|
382 |
|
---|
383 | function isTSInterfaceDeclaration(node) {
|
---|
384 | if (!node) return false;
|
---|
385 | let nodeType = node.type;
|
---|
386 | if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
---|
387 | nodeType = node.declaration.type;
|
---|
388 | }
|
---|
389 | return nodeType === 'TSInterfaceDeclaration';
|
---|
390 | }
|
---|
391 |
|
---|
392 | function isTSTypeDeclaration(node) {
|
---|
393 | if (!node) return false;
|
---|
394 | let nodeType = node.type;
|
---|
395 | let nodeKind = node.kind;
|
---|
396 | if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
---|
397 | nodeType = node.declaration.type;
|
---|
398 | nodeKind = node.declaration.kind;
|
---|
399 | }
|
---|
400 | return nodeType === 'VariableDeclaration' && nodeKind === 'type';
|
---|
401 | }
|
---|
402 |
|
---|
403 | function isTSTypeAliasDeclaration(node) {
|
---|
404 | if (!node) return false;
|
---|
405 | let nodeType = node.type;
|
---|
406 | if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
---|
407 | nodeType = node.declaration.type;
|
---|
408 | return nodeType === 'TSTypeAliasDeclaration' && node.exportKind === 'type';
|
---|
409 | }
|
---|
410 | return nodeType === 'TSTypeAliasDeclaration';
|
---|
411 | }
|
---|
412 |
|
---|
413 | function isTSParenthesizedType(node) {
|
---|
414 | if (!node) return false;
|
---|
415 | const nodeType = node.type;
|
---|
416 | return nodeType === 'TSTypeAliasDeclaration';
|
---|
417 | }
|
---|
418 |
|
---|
419 | function isTSFunctionType(node) {
|
---|
420 | if (!node) return false;
|
---|
421 | const nodeType = node.type;
|
---|
422 | return nodeType === 'TSFunctionType';
|
---|
423 | }
|
---|
424 |
|
---|
425 | function isTSTypeQuery(node) {
|
---|
426 | if (!node) return false;
|
---|
427 | const nodeType = node.type;
|
---|
428 | return nodeType === 'TSTypeQuery';
|
---|
429 | }
|
---|
430 |
|
---|
431 | function isTSTypeParameterInstantiation(node) {
|
---|
432 | if (!node) return false;
|
---|
433 | const nodeType = node.type;
|
---|
434 | return nodeType === 'TSTypeParameterInstantiation';
|
---|
435 | }
|
---|
436 |
|
---|
437 | module.exports = {
|
---|
438 | traverse,
|
---|
439 | findReturnStatement,
|
---|
440 | getFirstNodeInLine,
|
---|
441 | getPropertyName,
|
---|
442 | getPropertyNameNode,
|
---|
443 | getComponentProperties,
|
---|
444 | getKeyValue,
|
---|
445 | isParenthesized,
|
---|
446 | isAssignmentLHS,
|
---|
447 | isClass,
|
---|
448 | isFunction,
|
---|
449 | isFunctionLikeExpression,
|
---|
450 | isFunctionLike,
|
---|
451 | inConstructor,
|
---|
452 | isNodeFirstInLine,
|
---|
453 | unwrapTSAsExpression,
|
---|
454 | traverseReturns,
|
---|
455 | isTSTypeReference,
|
---|
456 | isTSTypeAnnotation,
|
---|
457 | isTSTypeLiteral,
|
---|
458 | isTSIntersectionType,
|
---|
459 | isTSInterfaceHeritage,
|
---|
460 | isTSInterfaceDeclaration,
|
---|
461 | isTSTypeAliasDeclaration,
|
---|
462 | isTSParenthesizedType,
|
---|
463 | isTSFunctionType,
|
---|
464 | isTSTypeQuery,
|
---|
465 | isTSTypeParameterInstantiation,
|
---|
466 | isTSTypeDeclaration,
|
---|
467 | };
|
---|