1 | /**
|
---|
2 | * @fileoverview Utility class and functions for React components detection
|
---|
3 | * @author Yannick Croissant
|
---|
4 | */
|
---|
5 |
|
---|
6 | 'use strict';
|
---|
7 |
|
---|
8 | const arrayIncludes = require('array-includes');
|
---|
9 | const fromEntries = require('object.fromentries');
|
---|
10 | const values = require('object.values');
|
---|
11 | const iterFrom = require('es-iterator-helpers/Iterator.from');
|
---|
12 | const map = require('es-iterator-helpers/Iterator.prototype.map');
|
---|
13 |
|
---|
14 | const variableUtil = require('./variable');
|
---|
15 | const pragmaUtil = require('./pragma');
|
---|
16 | const astUtil = require('./ast');
|
---|
17 | const componentUtil = require('./componentUtil');
|
---|
18 | const propTypesUtil = require('./propTypes');
|
---|
19 | const jsxUtil = require('./jsx');
|
---|
20 | const usedPropTypesUtil = require('./usedPropTypes');
|
---|
21 | const defaultPropsUtil = require('./defaultProps');
|
---|
22 | const isFirstLetterCapitalized = require('./isFirstLetterCapitalized');
|
---|
23 | const isDestructuredFromPragmaImport = require('./isDestructuredFromPragmaImport');
|
---|
24 | const eslintUtil = require('./eslint');
|
---|
25 |
|
---|
26 | const getScope = eslintUtil.getScope;
|
---|
27 | const getText = eslintUtil.getText;
|
---|
28 |
|
---|
29 | function getId(node) {
|
---|
30 | return node ? `${node.range[0]}:${node.range[1]}` : '';
|
---|
31 | }
|
---|
32 |
|
---|
33 | function usedPropTypesAreEquivalent(propA, propB) {
|
---|
34 | if (propA.name === propB.name) {
|
---|
35 | if (!propA.allNames && !propB.allNames) {
|
---|
36 | return true;
|
---|
37 | }
|
---|
38 | if (Array.isArray(propA.allNames) && Array.isArray(propB.allNames) && propA.allNames.join('') === propB.allNames.join('')) {
|
---|
39 | return true;
|
---|
40 | }
|
---|
41 | return false;
|
---|
42 | }
|
---|
43 | return false;
|
---|
44 | }
|
---|
45 |
|
---|
46 | function mergeUsedPropTypes(propsList, newPropsList) {
|
---|
47 | const propsToAdd = newPropsList.filter((newProp) => {
|
---|
48 | const newPropIsAlreadyInTheList = propsList.some((prop) => usedPropTypesAreEquivalent(prop, newProp));
|
---|
49 | return !newPropIsAlreadyInTheList;
|
---|
50 | });
|
---|
51 |
|
---|
52 | return propsList.concat(propsToAdd);
|
---|
53 | }
|
---|
54 |
|
---|
55 | const USE_HOOK_PREFIX_REGEX = /^use[A-Z]/;
|
---|
56 |
|
---|
57 | const Lists = new WeakMap();
|
---|
58 | const ReactImports = new WeakMap();
|
---|
59 |
|
---|
60 | /**
|
---|
61 | * Components
|
---|
62 | */
|
---|
63 | class Components {
|
---|
64 | constructor() {
|
---|
65 | Lists.set(this, {});
|
---|
66 | ReactImports.set(this, {});
|
---|
67 | }
|
---|
68 |
|
---|
69 | /**
|
---|
70 | * Add a node to the components list, or update it if it's already in the list
|
---|
71 | *
|
---|
72 | * @param {ASTNode} node The AST node being added.
|
---|
73 | * @param {Number} confidence Confidence in the component detection (0=banned, 1=maybe, 2=yes)
|
---|
74 | * @returns {Object} Added component object
|
---|
75 | */
|
---|
76 | add(node, confidence) {
|
---|
77 | const id = getId(node);
|
---|
78 | const list = Lists.get(this);
|
---|
79 | if (list[id]) {
|
---|
80 | if (confidence === 0 || list[id].confidence === 0) {
|
---|
81 | list[id].confidence = 0;
|
---|
82 | } else {
|
---|
83 | list[id].confidence = Math.max(list[id].confidence, confidence);
|
---|
84 | }
|
---|
85 | return list[id];
|
---|
86 | }
|
---|
87 | list[id] = {
|
---|
88 | node,
|
---|
89 | confidence,
|
---|
90 | };
|
---|
91 | return list[id];
|
---|
92 | }
|
---|
93 |
|
---|
94 | /**
|
---|
95 | * Find a component in the list using its node
|
---|
96 | *
|
---|
97 | * @param {ASTNode} node The AST node being searched.
|
---|
98 | * @returns {Object} Component object, undefined if the component is not found or has confidence value of 0.
|
---|
99 | */
|
---|
100 | get(node) {
|
---|
101 | const id = getId(node);
|
---|
102 | const item = Lists.get(this)[id];
|
---|
103 | if (item && item.confidence >= 1) {
|
---|
104 | return item;
|
---|
105 | }
|
---|
106 | return null;
|
---|
107 | }
|
---|
108 |
|
---|
109 | /**
|
---|
110 | * Update a component in the list
|
---|
111 | *
|
---|
112 | * @param {ASTNode} node The AST node being updated.
|
---|
113 | * @param {Object} props Additional properties to add to the component.
|
---|
114 | */
|
---|
115 | set(node, props) {
|
---|
116 | const list = Lists.get(this);
|
---|
117 | let component = list[getId(node)];
|
---|
118 | while (!component || component.confidence < 1) {
|
---|
119 | node = node.parent;
|
---|
120 | if (!node) {
|
---|
121 | return;
|
---|
122 | }
|
---|
123 | component = list[getId(node)];
|
---|
124 | }
|
---|
125 |
|
---|
126 | Object.assign(
|
---|
127 | component,
|
---|
128 | props,
|
---|
129 | {
|
---|
130 | usedPropTypes: mergeUsedPropTypes(
|
---|
131 | component.usedPropTypes || [],
|
---|
132 | props.usedPropTypes || []
|
---|
133 | ),
|
---|
134 | }
|
---|
135 | );
|
---|
136 | }
|
---|
137 |
|
---|
138 | /**
|
---|
139 | * Return the components list
|
---|
140 | * Components for which we are not confident are not returned
|
---|
141 | *
|
---|
142 | * @returns {Object} Components list
|
---|
143 | */
|
---|
144 | list() {
|
---|
145 | const thisList = Lists.get(this);
|
---|
146 | const list = {};
|
---|
147 | const usedPropTypes = {};
|
---|
148 |
|
---|
149 | // Find props used in components for which we are not confident
|
---|
150 | Object.keys(thisList).filter((i) => thisList[i].confidence < 2).forEach((i) => {
|
---|
151 | let component = null;
|
---|
152 | let node = null;
|
---|
153 | node = thisList[i].node;
|
---|
154 | while (!component && node.parent) {
|
---|
155 | node = node.parent;
|
---|
156 | // Stop moving up if we reach a decorator
|
---|
157 | if (node.type === 'Decorator') {
|
---|
158 | break;
|
---|
159 | }
|
---|
160 | component = this.get(node);
|
---|
161 | }
|
---|
162 | if (component) {
|
---|
163 | const newUsedProps = (thisList[i].usedPropTypes || []).filter((propType) => !propType.node || propType.node.kind !== 'init');
|
---|
164 |
|
---|
165 | const componentId = getId(component.node);
|
---|
166 |
|
---|
167 | usedPropTypes[componentId] = mergeUsedPropTypes(usedPropTypes[componentId] || [], newUsedProps);
|
---|
168 | }
|
---|
169 | });
|
---|
170 |
|
---|
171 | // Assign used props in not confident components to the parent component
|
---|
172 | Object.keys(thisList).filter((j) => thisList[j].confidence >= 2).forEach((j) => {
|
---|
173 | const id = getId(thisList[j].node);
|
---|
174 | list[j] = thisList[j];
|
---|
175 | if (usedPropTypes[id]) {
|
---|
176 | list[j].usedPropTypes = mergeUsedPropTypes(list[j].usedPropTypes || [], usedPropTypes[id]);
|
---|
177 | }
|
---|
178 | });
|
---|
179 | return list;
|
---|
180 | }
|
---|
181 |
|
---|
182 | /**
|
---|
183 | * Return the length of the components list
|
---|
184 | * Components for which we are not confident are not counted
|
---|
185 | *
|
---|
186 | * @returns {Number} Components list length
|
---|
187 | */
|
---|
188 | length() {
|
---|
189 | const list = Lists.get(this);
|
---|
190 | return values(list).filter((component) => component.confidence >= 2).length;
|
---|
191 | }
|
---|
192 |
|
---|
193 | /**
|
---|
194 | * Return the node naming the default React import
|
---|
195 | * It can be used to determine the local name of import, even if it's imported
|
---|
196 | * with an unusual name.
|
---|
197 | *
|
---|
198 | * @returns {ASTNode} React default import node
|
---|
199 | */
|
---|
200 | getDefaultReactImports() {
|
---|
201 | return ReactImports.get(this).defaultReactImports;
|
---|
202 | }
|
---|
203 |
|
---|
204 | /**
|
---|
205 | * Return the nodes of all React named imports
|
---|
206 | *
|
---|
207 | * @returns {Object} The list of React named imports
|
---|
208 | */
|
---|
209 | getNamedReactImports() {
|
---|
210 | return ReactImports.get(this).namedReactImports;
|
---|
211 | }
|
---|
212 |
|
---|
213 | /**
|
---|
214 | * Add the default React import specifier to the scope
|
---|
215 | *
|
---|
216 | * @param {ASTNode} specifier The AST Node of the default React import
|
---|
217 | * @returns {void}
|
---|
218 | */
|
---|
219 | addDefaultReactImport(specifier) {
|
---|
220 | const info = ReactImports.get(this);
|
---|
221 | ReactImports.set(this, Object.assign({}, info, {
|
---|
222 | defaultReactImports: (info.defaultReactImports || []).concat(specifier),
|
---|
223 | }));
|
---|
224 | }
|
---|
225 |
|
---|
226 | /**
|
---|
227 | * Add a named React import specifier to the scope
|
---|
228 | *
|
---|
229 | * @param {ASTNode} specifier The AST Node of a named React import
|
---|
230 | * @returns {void}
|
---|
231 | */
|
---|
232 | addNamedReactImport(specifier) {
|
---|
233 | const info = ReactImports.get(this);
|
---|
234 | ReactImports.set(this, Object.assign({}, info, {
|
---|
235 | namedReactImports: (info.namedReactImports || []).concat(specifier),
|
---|
236 | }));
|
---|
237 | }
|
---|
238 | }
|
---|
239 |
|
---|
240 | function getWrapperFunctions(context, pragma) {
|
---|
241 | const componentWrapperFunctions = context.settings.componentWrapperFunctions || [];
|
---|
242 |
|
---|
243 | // eslint-disable-next-line arrow-body-style
|
---|
244 | return componentWrapperFunctions.map((wrapperFunction) => {
|
---|
245 | return typeof wrapperFunction === 'string'
|
---|
246 | ? { property: wrapperFunction }
|
---|
247 | : Object.assign({}, wrapperFunction, {
|
---|
248 | object: wrapperFunction.object === '<pragma>' ? pragma : wrapperFunction.object,
|
---|
249 | });
|
---|
250 | }).concat([
|
---|
251 | { property: 'forwardRef', object: pragma },
|
---|
252 | { property: 'memo', object: pragma },
|
---|
253 | ]);
|
---|
254 | }
|
---|
255 |
|
---|
256 | // eslint-disable-next-line valid-jsdoc
|
---|
257 | /**
|
---|
258 | * Merge many eslint rules into one
|
---|
259 | * @param {{[_: string]: Function}[]} rules the returned values for eslint rule.create(context)
|
---|
260 | * @returns {{[_: string]: Function}} merged rule
|
---|
261 | */
|
---|
262 | function mergeRules(rules) {
|
---|
263 | /** @type {Map<string, Function[]>} */
|
---|
264 | const handlersByKey = new Map();
|
---|
265 | rules.forEach((rule) => {
|
---|
266 | Object.keys(rule).forEach((key) => {
|
---|
267 | const fns = handlersByKey.get(key);
|
---|
268 | if (!fns) {
|
---|
269 | handlersByKey.set(key, [rule[key]]);
|
---|
270 | } else {
|
---|
271 | fns.push(rule[key]);
|
---|
272 | }
|
---|
273 | });
|
---|
274 | });
|
---|
275 |
|
---|
276 | /** @type {{ [key: string]: Function }} */
|
---|
277 | return fromEntries(map(iterFrom(handlersByKey), (entry) => [
|
---|
278 | entry[0],
|
---|
279 | function mergedHandler(node) {
|
---|
280 | entry[1].forEach((fn) => {
|
---|
281 | fn(node);
|
---|
282 | });
|
---|
283 | },
|
---|
284 | ]));
|
---|
285 | }
|
---|
286 |
|
---|
287 | function componentRule(rule, context) {
|
---|
288 | const pragma = pragmaUtil.getFromContext(context);
|
---|
289 | const components = new Components();
|
---|
290 | const wrapperFunctions = getWrapperFunctions(context, pragma);
|
---|
291 |
|
---|
292 | // Utilities for component detection
|
---|
293 | const utils = {
|
---|
294 | /**
|
---|
295 | * Check if variable is destructured from pragma import
|
---|
296 | *
|
---|
297 | * @param {ASTNode} node The AST node to check
|
---|
298 | * @param {string} variable The variable name to check
|
---|
299 | * @returns {boolean} True if createElement is destructured from the pragma
|
---|
300 | */
|
---|
301 | isDestructuredFromPragmaImport(node, variable) {
|
---|
302 | return isDestructuredFromPragmaImport(context, node, variable);
|
---|
303 | },
|
---|
304 |
|
---|
305 | /**
|
---|
306 | * @param {ASTNode} ASTNode
|
---|
307 | * @param {boolean=} strict
|
---|
308 | * @returns {boolean}
|
---|
309 | */
|
---|
310 | isReturningJSX(ASTNode, strict) {
|
---|
311 | return jsxUtil.isReturningJSX(context, ASTNode, strict, true);
|
---|
312 | },
|
---|
313 |
|
---|
314 | isReturningJSXOrNull(ASTNode, strict) {
|
---|
315 | return jsxUtil.isReturningJSX(context, ASTNode, strict);
|
---|
316 | },
|
---|
317 |
|
---|
318 | isReturningOnlyNull(ASTNode) {
|
---|
319 | return jsxUtil.isReturningOnlyNull(ASTNode, context);
|
---|
320 | },
|
---|
321 |
|
---|
322 | getPragmaComponentWrapper(node) {
|
---|
323 | let isPragmaComponentWrapper;
|
---|
324 | let currentNode = node;
|
---|
325 | let prevNode;
|
---|
326 | do {
|
---|
327 | currentNode = currentNode.parent;
|
---|
328 | isPragmaComponentWrapper = this.isPragmaComponentWrapper(currentNode);
|
---|
329 | if (isPragmaComponentWrapper) {
|
---|
330 | prevNode = currentNode;
|
---|
331 | }
|
---|
332 | } while (isPragmaComponentWrapper);
|
---|
333 |
|
---|
334 | return prevNode;
|
---|
335 | },
|
---|
336 |
|
---|
337 | getComponentNameFromJSXElement(node) {
|
---|
338 | if (node.type !== 'JSXElement') {
|
---|
339 | return null;
|
---|
340 | }
|
---|
341 | if (node.openingElement && node.openingElement.name && node.openingElement.name.name) {
|
---|
342 | return node.openingElement.name.name;
|
---|
343 | }
|
---|
344 | return null;
|
---|
345 | },
|
---|
346 |
|
---|
347 | /**
|
---|
348 | * Getting the first JSX element's name.
|
---|
349 | * @param {object} node
|
---|
350 | * @returns {string | null}
|
---|
351 | */
|
---|
352 | getNameOfWrappedComponent(node) {
|
---|
353 | if (node.length < 1) {
|
---|
354 | return null;
|
---|
355 | }
|
---|
356 | const body = node[0].body;
|
---|
357 | if (!body) {
|
---|
358 | return null;
|
---|
359 | }
|
---|
360 | if (body.type === 'JSXElement') {
|
---|
361 | return this.getComponentNameFromJSXElement(body);
|
---|
362 | }
|
---|
363 | if (body.type === 'BlockStatement') {
|
---|
364 | const jsxElement = body.body.find((item) => item.type === 'ReturnStatement');
|
---|
365 | return jsxElement
|
---|
366 | && jsxElement.argument
|
---|
367 | && this.getComponentNameFromJSXElement(jsxElement.argument);
|
---|
368 | }
|
---|
369 | return null;
|
---|
370 | },
|
---|
371 |
|
---|
372 | /**
|
---|
373 | * Get the list of names of components created till now
|
---|
374 | * @returns {string | boolean}
|
---|
375 | */
|
---|
376 | getDetectedComponents() {
|
---|
377 | const list = components.list();
|
---|
378 | return values(list).filter((val) => {
|
---|
379 | if (val.node.type === 'ClassDeclaration') {
|
---|
380 | return true;
|
---|
381 | }
|
---|
382 | if (
|
---|
383 | val.node.type === 'ArrowFunctionExpression'
|
---|
384 | && val.node.parent
|
---|
385 | && val.node.parent.type === 'VariableDeclarator'
|
---|
386 | && val.node.parent.id
|
---|
387 | ) {
|
---|
388 | return true;
|
---|
389 | }
|
---|
390 | return false;
|
---|
391 | }).map((val) => {
|
---|
392 | if (val.node.type === 'ArrowFunctionExpression') return val.node.parent.id.name;
|
---|
393 | return val.node.id && val.node.id.name;
|
---|
394 | });
|
---|
395 | },
|
---|
396 |
|
---|
397 | /**
|
---|
398 | * It will check whether memo/forwardRef is wrapping existing component or
|
---|
399 | * creating a new one.
|
---|
400 | * @param {object} node
|
---|
401 | * @returns {boolean}
|
---|
402 | */
|
---|
403 | nodeWrapsComponent(node) {
|
---|
404 | const childComponent = this.getNameOfWrappedComponent(node.arguments);
|
---|
405 | const componentList = this.getDetectedComponents();
|
---|
406 | return !!childComponent && arrayIncludes(componentList, childComponent);
|
---|
407 | },
|
---|
408 |
|
---|
409 | isPragmaComponentWrapper(node) {
|
---|
410 | if (!node || node.type !== 'CallExpression') {
|
---|
411 | return false;
|
---|
412 | }
|
---|
413 |
|
---|
414 | return wrapperFunctions.some((wrapperFunction) => {
|
---|
415 | if (node.callee.type === 'MemberExpression') {
|
---|
416 | return wrapperFunction.object
|
---|
417 | && wrapperFunction.object === node.callee.object.name
|
---|
418 | && wrapperFunction.property === node.callee.property.name
|
---|
419 | && !this.nodeWrapsComponent(node);
|
---|
420 | }
|
---|
421 | return wrapperFunction.property === node.callee.name
|
---|
422 | && (!wrapperFunction.object
|
---|
423 | // Functions coming from the current pragma need special handling
|
---|
424 | || (wrapperFunction.object === pragma && this.isDestructuredFromPragmaImport(node, node.callee.name))
|
---|
425 | );
|
---|
426 | });
|
---|
427 | },
|
---|
428 |
|
---|
429 | /**
|
---|
430 | * Find a return statement in the current node
|
---|
431 | *
|
---|
432 | * @param {ASTNode} node The AST node being checked
|
---|
433 | */
|
---|
434 | findReturnStatement: astUtil.findReturnStatement,
|
---|
435 |
|
---|
436 | /**
|
---|
437 | * Get the parent component node from the current scope
|
---|
438 | * @param {ASTNode} node
|
---|
439 | *
|
---|
440 | * @returns {ASTNode} component node, null if we are not in a component
|
---|
441 | */
|
---|
442 | getParentComponent(node) {
|
---|
443 | return (
|
---|
444 | componentUtil.getParentES6Component(context, node)
|
---|
445 | || componentUtil.getParentES5Component(context, node)
|
---|
446 | || utils.getParentStatelessComponent(node)
|
---|
447 | );
|
---|
448 | },
|
---|
449 |
|
---|
450 | /**
|
---|
451 | * @param {ASTNode} node
|
---|
452 | * @returns {boolean}
|
---|
453 | */
|
---|
454 | isInAllowedPositionForComponent(node) {
|
---|
455 | switch (node.parent.type) {
|
---|
456 | case 'VariableDeclarator':
|
---|
457 | case 'AssignmentExpression':
|
---|
458 | case 'Property':
|
---|
459 | case 'ReturnStatement':
|
---|
460 | case 'ExportDefaultDeclaration':
|
---|
461 | case 'ArrowFunctionExpression': {
|
---|
462 | return true;
|
---|
463 | }
|
---|
464 | case 'SequenceExpression': {
|
---|
465 | return utils.isInAllowedPositionForComponent(node.parent)
|
---|
466 | && node === node.parent.expressions[node.parent.expressions.length - 1];
|
---|
467 | }
|
---|
468 | default:
|
---|
469 | return false;
|
---|
470 | }
|
---|
471 | },
|
---|
472 |
|
---|
473 | /**
|
---|
474 | * Get node if node is a stateless component, or node.parent in cases like
|
---|
475 | * `React.memo` or `React.forwardRef`. Otherwise returns `undefined`.
|
---|
476 | * @param {ASTNode} node
|
---|
477 | * @returns {ASTNode | undefined}
|
---|
478 | */
|
---|
479 | getStatelessComponent(node) {
|
---|
480 | const parent = node.parent;
|
---|
481 | if (
|
---|
482 | node.type === 'FunctionDeclaration'
|
---|
483 | && (!node.id || isFirstLetterCapitalized(node.id.name))
|
---|
484 | && utils.isReturningJSXOrNull(node)
|
---|
485 | ) {
|
---|
486 | return node;
|
---|
487 | }
|
---|
488 |
|
---|
489 | if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
|
---|
490 | const isPropertyAssignment = parent.type === 'AssignmentExpression'
|
---|
491 | && parent.left.type === 'MemberExpression';
|
---|
492 | const isModuleExportsAssignment = isPropertyAssignment
|
---|
493 | && parent.left.object.name === 'module'
|
---|
494 | && parent.left.property.name === 'exports';
|
---|
495 |
|
---|
496 | if (node.parent.type === 'ExportDefaultDeclaration') {
|
---|
497 | if (utils.isReturningJSX(node)) {
|
---|
498 | return node;
|
---|
499 | }
|
---|
500 | return undefined;
|
---|
501 | }
|
---|
502 |
|
---|
503 | if (node.parent.type === 'VariableDeclarator' && utils.isReturningJSXOrNull(node)) {
|
---|
504 | if (isFirstLetterCapitalized(node.parent.id.name)) {
|
---|
505 | return node;
|
---|
506 | }
|
---|
507 | return undefined;
|
---|
508 | }
|
---|
509 |
|
---|
510 | // case: const any = () => { return (props) => null }
|
---|
511 | // case: const any = () => (props) => null
|
---|
512 | if (
|
---|
513 | (node.parent.type === 'ReturnStatement' || (node.parent.type === 'ArrowFunctionExpression' && node.parent.expression))
|
---|
514 | && !utils.isReturningJSX(node)
|
---|
515 | ) {
|
---|
516 | return undefined;
|
---|
517 | }
|
---|
518 |
|
---|
519 | // case: any = () => { return => null }
|
---|
520 | // case: any = () => null
|
---|
521 | if (node.parent.type === 'AssignmentExpression' && !isPropertyAssignment && utils.isReturningJSXOrNull(node)) {
|
---|
522 | if (isFirstLetterCapitalized(node.parent.left.name)) {
|
---|
523 | return node;
|
---|
524 | }
|
---|
525 | return undefined;
|
---|
526 | }
|
---|
527 |
|
---|
528 | // case: any = () => () => null
|
---|
529 | if (node.parent.type === 'ArrowFunctionExpression' && node.parent.parent.type === 'AssignmentExpression' && !isPropertyAssignment && utils.isReturningJSXOrNull(node)) {
|
---|
530 | if (isFirstLetterCapitalized(node.parent.parent.left.name)) {
|
---|
531 | return node;
|
---|
532 | }
|
---|
533 | return undefined;
|
---|
534 | }
|
---|
535 |
|
---|
536 | // case: { any: () => () => null }
|
---|
537 | if (node.parent.type === 'ArrowFunctionExpression' && node.parent.parent.type === 'Property' && !isPropertyAssignment && utils.isReturningJSXOrNull(node)) {
|
---|
538 | if (isFirstLetterCapitalized(node.parent.parent.key.name)) {
|
---|
539 | return node;
|
---|
540 | }
|
---|
541 | return undefined;
|
---|
542 | }
|
---|
543 |
|
---|
544 | // case: any = function() {return function() {return null;};}
|
---|
545 | if (node.parent.type === 'ReturnStatement') {
|
---|
546 | if (isFirstLetterCapitalized(node.id && node.id.name)) {
|
---|
547 | return node;
|
---|
548 | }
|
---|
549 | const functionExpr = node.parent.parent.parent;
|
---|
550 | if (functionExpr.parent.type === 'AssignmentExpression' && !isPropertyAssignment && utils.isReturningJSXOrNull(node)) {
|
---|
551 | if (isFirstLetterCapitalized(functionExpr.parent.left.name)) {
|
---|
552 | return node;
|
---|
553 | }
|
---|
554 | return undefined;
|
---|
555 | }
|
---|
556 | }
|
---|
557 |
|
---|
558 | // case: { any: function() {return function() {return null;};} }
|
---|
559 | if (node.parent.type === 'ReturnStatement') {
|
---|
560 | const functionExpr = node.parent.parent.parent;
|
---|
561 | if (functionExpr.parent.type === 'Property' && !isPropertyAssignment && utils.isReturningJSXOrNull(node)) {
|
---|
562 | if (isFirstLetterCapitalized(functionExpr.parent.key.name)) {
|
---|
563 | return node;
|
---|
564 | }
|
---|
565 | return undefined;
|
---|
566 | }
|
---|
567 | }
|
---|
568 |
|
---|
569 | // for case abc = { [someobject.somekey]: props => { ... return not-jsx } }
|
---|
570 | if (node.parent && node.parent.key && node.parent.key.type === 'MemberExpression' && !utils.isReturningJSX(node) && !utils.isReturningOnlyNull(node)) {
|
---|
571 | return undefined;
|
---|
572 | }
|
---|
573 |
|
---|
574 | if (
|
---|
575 | node.parent.type === 'Property' && (
|
---|
576 | (node.parent.method && !node.parent.computed) // case: { f() { return ... } }
|
---|
577 | || (!node.id && !node.parent.computed) // case: { f: () => ... }
|
---|
578 | )
|
---|
579 | ) {
|
---|
580 | if (isFirstLetterCapitalized(node.parent.key.name) && utils.isReturningJSX(node)) {
|
---|
581 | return node;
|
---|
582 | }
|
---|
583 | return undefined;
|
---|
584 | }
|
---|
585 |
|
---|
586 | // Case like `React.memo(() => <></>)` or `React.forwardRef(...)`
|
---|
587 | const pragmaComponentWrapper = utils.getPragmaComponentWrapper(node);
|
---|
588 | if (pragmaComponentWrapper && utils.isReturningJSXOrNull(node)) {
|
---|
589 | return pragmaComponentWrapper;
|
---|
590 | }
|
---|
591 |
|
---|
592 | if (!(utils.isInAllowedPositionForComponent(node) && utils.isReturningJSXOrNull(node))) {
|
---|
593 | return undefined;
|
---|
594 | }
|
---|
595 |
|
---|
596 | if (utils.isParentComponentNotStatelessComponent(node)) {
|
---|
597 | return undefined;
|
---|
598 | }
|
---|
599 |
|
---|
600 | if (node.id) {
|
---|
601 | return isFirstLetterCapitalized(node.id.name) ? node : undefined;
|
---|
602 | }
|
---|
603 |
|
---|
604 | if (
|
---|
605 | isPropertyAssignment
|
---|
606 | && !isModuleExportsAssignment
|
---|
607 | && !isFirstLetterCapitalized(parent.left.property.name)
|
---|
608 | ) {
|
---|
609 | return undefined;
|
---|
610 | }
|
---|
611 |
|
---|
612 | if (parent.type === 'Property' && utils.isReturningOnlyNull(node)) {
|
---|
613 | return undefined;
|
---|
614 | }
|
---|
615 |
|
---|
616 | return node;
|
---|
617 | }
|
---|
618 |
|
---|
619 | return undefined;
|
---|
620 | },
|
---|
621 |
|
---|
622 | /**
|
---|
623 | * Get the parent stateless component node from the current scope
|
---|
624 | *
|
---|
625 | * @param {ASTNode} node The AST node being checked
|
---|
626 | * @returns {ASTNode} component node, null if we are not in a component
|
---|
627 | */
|
---|
628 | getParentStatelessComponent(node) {
|
---|
629 | let scope = getScope(context, node);
|
---|
630 | while (scope) {
|
---|
631 | const statelessComponent = utils.getStatelessComponent(scope.block);
|
---|
632 | if (statelessComponent) {
|
---|
633 | return statelessComponent;
|
---|
634 | }
|
---|
635 | scope = scope.upper;
|
---|
636 | }
|
---|
637 | return null;
|
---|
638 | },
|
---|
639 |
|
---|
640 | /**
|
---|
641 | * Get the related component from a node
|
---|
642 | *
|
---|
643 | * @param {ASTNode} node The AST node being checked (must be a MemberExpression).
|
---|
644 | * @returns {ASTNode} component node, null if we cannot find the component
|
---|
645 | */
|
---|
646 | getRelatedComponent(node) {
|
---|
647 | let i;
|
---|
648 | let j;
|
---|
649 | let k;
|
---|
650 | let l;
|
---|
651 | let componentNode;
|
---|
652 | // Get the component path
|
---|
653 | const componentPath = [];
|
---|
654 | let nodeTemp = node;
|
---|
655 | while (nodeTemp) {
|
---|
656 | if (nodeTemp.property && nodeTemp.property.type === 'Identifier') {
|
---|
657 | componentPath.push(nodeTemp.property.name);
|
---|
658 | }
|
---|
659 | if (nodeTemp.object && nodeTemp.object.type === 'Identifier') {
|
---|
660 | componentPath.push(nodeTemp.object.name);
|
---|
661 | }
|
---|
662 | nodeTemp = nodeTemp.object;
|
---|
663 | }
|
---|
664 | componentPath.reverse();
|
---|
665 | const componentName = componentPath.slice(0, componentPath.length - 1).join('.');
|
---|
666 |
|
---|
667 | // Find the variable in the current scope
|
---|
668 | const variableName = componentPath.shift();
|
---|
669 | if (!variableName) {
|
---|
670 | return null;
|
---|
671 | }
|
---|
672 | const variableInScope = variableUtil.getVariableFromContext(context, node, variableName);
|
---|
673 | if (!variableInScope) {
|
---|
674 | return null;
|
---|
675 | }
|
---|
676 |
|
---|
677 | // Try to find the component using variable references
|
---|
678 | variableInScope.references.some((ref) => {
|
---|
679 | let refId = ref.identifier;
|
---|
680 | if (refId.parent && refId.parent.type === 'MemberExpression') {
|
---|
681 | refId = refId.parent;
|
---|
682 | }
|
---|
683 | if (getText(context, refId) !== componentName) {
|
---|
684 | return false;
|
---|
685 | }
|
---|
686 | if (refId.type === 'MemberExpression') {
|
---|
687 | componentNode = refId.parent.right;
|
---|
688 | } else if (
|
---|
689 | refId.parent
|
---|
690 | && refId.parent.type === 'VariableDeclarator'
|
---|
691 | && refId.parent.init
|
---|
692 | && refId.parent.init.type !== 'Identifier'
|
---|
693 | ) {
|
---|
694 | componentNode = refId.parent.init;
|
---|
695 | }
|
---|
696 | return true;
|
---|
697 | });
|
---|
698 |
|
---|
699 | if (componentNode) {
|
---|
700 | // Return the component
|
---|
701 | return components.add(componentNode, 1);
|
---|
702 | }
|
---|
703 |
|
---|
704 | // Try to find the component using variable declarations
|
---|
705 | const defs = variableInScope.defs;
|
---|
706 | const defInScope = defs.find((def) => (
|
---|
707 | def.type === 'ClassName'
|
---|
708 | || def.type === 'FunctionName'
|
---|
709 | || def.type === 'Variable'
|
---|
710 | ));
|
---|
711 | if (!defInScope || !defInScope.node) {
|
---|
712 | return null;
|
---|
713 | }
|
---|
714 | componentNode = defInScope.node.init || defInScope.node;
|
---|
715 |
|
---|
716 | // Traverse the node properties to the component declaration
|
---|
717 | for (i = 0, j = componentPath.length; i < j; i++) {
|
---|
718 | if (!componentNode.properties) {
|
---|
719 | continue; // eslint-disable-line no-continue
|
---|
720 | }
|
---|
721 | for (k = 0, l = componentNode.properties.length; k < l; k++) {
|
---|
722 | if (componentNode.properties[k].key && componentNode.properties[k].key.name === componentPath[i]) {
|
---|
723 | componentNode = componentNode.properties[k];
|
---|
724 | break;
|
---|
725 | }
|
---|
726 | }
|
---|
727 | if (!componentNode || !componentNode.value) {
|
---|
728 | return null;
|
---|
729 | }
|
---|
730 | componentNode = componentNode.value;
|
---|
731 | }
|
---|
732 |
|
---|
733 | // Return the component
|
---|
734 | return components.add(componentNode, 1);
|
---|
735 | },
|
---|
736 |
|
---|
737 | isParentComponentNotStatelessComponent(node) {
|
---|
738 | return !!(
|
---|
739 | node.parent
|
---|
740 | && node.parent.key
|
---|
741 | && node.parent.key.type === 'Identifier'
|
---|
742 | // custom component functions must start with a capital letter (returns false otherwise)
|
---|
743 | && node.parent.key.name.charAt(0) === node.parent.key.name.charAt(0).toLowerCase()
|
---|
744 | // react render function cannot have params
|
---|
745 | && !!(node.params || []).length
|
---|
746 | );
|
---|
747 | },
|
---|
748 |
|
---|
749 | /**
|
---|
750 | * Identify whether a node (CallExpression) is a call to a React hook
|
---|
751 | *
|
---|
752 | * @param {ASTNode} node The AST node being searched. (expects CallExpression)
|
---|
753 | * @param {('useCallback'|'useContext'|'useDebugValue'|'useEffect'|'useImperativeHandle'|'useLayoutEffect'|'useMemo'|'useReducer'|'useRef'|'useState')[]} [expectedHookNames] React hook names to which search is limited.
|
---|
754 | * @returns {Boolean} True if the node is a call to a React hook
|
---|
755 | */
|
---|
756 | isReactHookCall(node, expectedHookNames) {
|
---|
757 | if (node.type !== 'CallExpression') {
|
---|
758 | return false;
|
---|
759 | }
|
---|
760 |
|
---|
761 | const defaultReactImports = components.getDefaultReactImports();
|
---|
762 | const namedReactImports = components.getNamedReactImports();
|
---|
763 |
|
---|
764 | const defaultReactImportName = defaultReactImports
|
---|
765 | && defaultReactImports[0]
|
---|
766 | && defaultReactImports[0].local.name;
|
---|
767 | const reactHookImportSpecifiers = namedReactImports
|
---|
768 | && namedReactImports.filter((specifier) => USE_HOOK_PREFIX_REGEX.test(specifier.imported.name));
|
---|
769 | const reactHookImportNames = reactHookImportSpecifiers
|
---|
770 | && fromEntries(reactHookImportSpecifiers.map((specifier) => [specifier.local.name, specifier.imported.name]));
|
---|
771 |
|
---|
772 | const isPotentialReactHookCall = defaultReactImportName
|
---|
773 | && node.callee.type === 'MemberExpression'
|
---|
774 | && node.callee.object.type === 'Identifier'
|
---|
775 | && node.callee.object.name === defaultReactImportName
|
---|
776 | && node.callee.property.type === 'Identifier'
|
---|
777 | && node.callee.property.name.match(USE_HOOK_PREFIX_REGEX);
|
---|
778 |
|
---|
779 | const isPotentialHookCall = reactHookImportNames
|
---|
780 | && node.callee.type === 'Identifier'
|
---|
781 | && node.callee.name.match(USE_HOOK_PREFIX_REGEX);
|
---|
782 |
|
---|
783 | const scope = (isPotentialReactHookCall || isPotentialHookCall) && getScope(context, node);
|
---|
784 |
|
---|
785 | const reactResolvedDefs = isPotentialReactHookCall
|
---|
786 | && scope.references
|
---|
787 | && scope.references.find(
|
---|
788 | (reference) => reference.identifier.name === defaultReactImportName
|
---|
789 | ).resolved.defs;
|
---|
790 |
|
---|
791 | const isReactShadowed = isPotentialReactHookCall && reactResolvedDefs
|
---|
792 | && reactResolvedDefs.some((reactDef) => reactDef.type !== 'ImportBinding');
|
---|
793 |
|
---|
794 | const potentialHookReference = isPotentialHookCall
|
---|
795 | && scope.references
|
---|
796 | && scope.references.find(
|
---|
797 | (reference) => reactHookImportNames[reference.identifier.name]
|
---|
798 | );
|
---|
799 |
|
---|
800 | const hookResolvedDefs = potentialHookReference && potentialHookReference.resolved.defs;
|
---|
801 | const localHookName = (isPotentialReactHookCall && node.callee.property.name)
|
---|
802 | || (isPotentialHookCall && potentialHookReference && node.callee.name);
|
---|
803 | const isHookShadowed = isPotentialHookCall
|
---|
804 | && hookResolvedDefs
|
---|
805 | && hookResolvedDefs.some(
|
---|
806 | (hookDef) => hookDef.name.name === localHookName
|
---|
807 | && hookDef.type !== 'ImportBinding'
|
---|
808 | );
|
---|
809 |
|
---|
810 | const isHookCall = (isPotentialReactHookCall && !isReactShadowed)
|
---|
811 | || (isPotentialHookCall && localHookName && !isHookShadowed);
|
---|
812 |
|
---|
813 | if (!isHookCall) {
|
---|
814 | return false;
|
---|
815 | }
|
---|
816 |
|
---|
817 | if (!expectedHookNames) {
|
---|
818 | return true;
|
---|
819 | }
|
---|
820 |
|
---|
821 | return arrayIncludes(
|
---|
822 | expectedHookNames,
|
---|
823 | (reactHookImportNames && reactHookImportNames[localHookName]) || localHookName
|
---|
824 | );
|
---|
825 | },
|
---|
826 | };
|
---|
827 |
|
---|
828 | // Component detection instructions
|
---|
829 | const detectionInstructions = {
|
---|
830 | CallExpression(node) {
|
---|
831 | if (!utils.isPragmaComponentWrapper(node)) {
|
---|
832 | return;
|
---|
833 | }
|
---|
834 | if (node.arguments.length > 0 && astUtil.isFunctionLikeExpression(node.arguments[0])) {
|
---|
835 | components.add(node, 2);
|
---|
836 | }
|
---|
837 | },
|
---|
838 |
|
---|
839 | ClassExpression(node) {
|
---|
840 | if (!componentUtil.isES6Component(node, context)) {
|
---|
841 | return;
|
---|
842 | }
|
---|
843 | components.add(node, 2);
|
---|
844 | },
|
---|
845 |
|
---|
846 | ClassDeclaration(node) {
|
---|
847 | if (!componentUtil.isES6Component(node, context)) {
|
---|
848 | return;
|
---|
849 | }
|
---|
850 | components.add(node, 2);
|
---|
851 | },
|
---|
852 |
|
---|
853 | ObjectExpression(node) {
|
---|
854 | if (!componentUtil.isES5Component(node, context)) {
|
---|
855 | return;
|
---|
856 | }
|
---|
857 | components.add(node, 2);
|
---|
858 | },
|
---|
859 |
|
---|
860 | FunctionExpression(node) {
|
---|
861 | if (node.async && node.generator) {
|
---|
862 | components.add(node, 0);
|
---|
863 | return;
|
---|
864 | }
|
---|
865 |
|
---|
866 | const component = utils.getStatelessComponent(node);
|
---|
867 | if (!component) {
|
---|
868 | return;
|
---|
869 | }
|
---|
870 | components.add(component, 2);
|
---|
871 | },
|
---|
872 |
|
---|
873 | FunctionDeclaration(node) {
|
---|
874 | if (node.async && node.generator) {
|
---|
875 | components.add(node, 0);
|
---|
876 | return;
|
---|
877 | }
|
---|
878 |
|
---|
879 | node = utils.getStatelessComponent(node);
|
---|
880 | if (!node) {
|
---|
881 | return;
|
---|
882 | }
|
---|
883 | components.add(node, 2);
|
---|
884 | },
|
---|
885 |
|
---|
886 | ArrowFunctionExpression(node) {
|
---|
887 | const component = utils.getStatelessComponent(node);
|
---|
888 | if (!component) {
|
---|
889 | return;
|
---|
890 | }
|
---|
891 | components.add(component, 2);
|
---|
892 | },
|
---|
893 |
|
---|
894 | ThisExpression(node) {
|
---|
895 | const component = utils.getParentStatelessComponent(node);
|
---|
896 | if (!component || !/Function/.test(component.type) || !node.parent.property) {
|
---|
897 | return;
|
---|
898 | }
|
---|
899 | // Ban functions accessing a property on a ThisExpression
|
---|
900 | components.add(node, 0);
|
---|
901 | },
|
---|
902 | };
|
---|
903 |
|
---|
904 | // Detect React import specifiers
|
---|
905 | const reactImportInstructions = {
|
---|
906 | ImportDeclaration(node) {
|
---|
907 | const isReactImported = node.source.type === 'Literal' && node.source.value === 'react';
|
---|
908 | if (!isReactImported) {
|
---|
909 | return;
|
---|
910 | }
|
---|
911 |
|
---|
912 | node.specifiers.forEach((specifier) => {
|
---|
913 | if (specifier.type === 'ImportDefaultSpecifier') {
|
---|
914 | components.addDefaultReactImport(specifier);
|
---|
915 | }
|
---|
916 | if (specifier.type === 'ImportSpecifier') {
|
---|
917 | components.addNamedReactImport(specifier);
|
---|
918 | }
|
---|
919 | });
|
---|
920 | },
|
---|
921 | };
|
---|
922 |
|
---|
923 | const ruleInstructions = rule(context, components, utils);
|
---|
924 | const propTypesInstructions = propTypesUtil(context, components, utils);
|
---|
925 | const usedPropTypesInstructions = usedPropTypesUtil(context, components, utils);
|
---|
926 | const defaultPropsInstructions = defaultPropsUtil(context, components, utils);
|
---|
927 |
|
---|
928 | const mergedRule = mergeRules([
|
---|
929 | detectionInstructions,
|
---|
930 | propTypesInstructions,
|
---|
931 | usedPropTypesInstructions,
|
---|
932 | defaultPropsInstructions,
|
---|
933 | reactImportInstructions,
|
---|
934 | ruleInstructions,
|
---|
935 | ]);
|
---|
936 |
|
---|
937 | return mergedRule;
|
---|
938 | }
|
---|
939 |
|
---|
940 | module.exports = Object.assign(Components, {
|
---|
941 | detect(rule) {
|
---|
942 | return componentRule.bind(this, rule);
|
---|
943 | },
|
---|
944 | });
|
---|