source: imaps-frontend/node_modules/eslint-plugin-react/lib/util/Components.js@ d565449

main
Last change on this file since d565449 was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 29.2 KB
Line 
1/**
2 * @fileoverview Utility class and functions for React components detection
3 * @author Yannick Croissant
4 */
5
6'use strict';
7
8const arrayIncludes = require('array-includes');
9const fromEntries = require('object.fromentries');
10const values = require('object.values');
11const iterFrom = require('es-iterator-helpers/Iterator.from');
12const map = require('es-iterator-helpers/Iterator.prototype.map');
13
14const variableUtil = require('./variable');
15const pragmaUtil = require('./pragma');
16const astUtil = require('./ast');
17const componentUtil = require('./componentUtil');
18const propTypesUtil = require('./propTypes');
19const jsxUtil = require('./jsx');
20const usedPropTypesUtil = require('./usedPropTypes');
21const defaultPropsUtil = require('./defaultProps');
22const isFirstLetterCapitalized = require('./isFirstLetterCapitalized');
23const isDestructuredFromPragmaImport = require('./isDestructuredFromPragmaImport');
24const eslintUtil = require('./eslint');
25
26const getScope = eslintUtil.getScope;
27const getText = eslintUtil.getText;
28
29function getId(node) {
30 return node ? `${node.range[0]}:${node.range[1]}` : '';
31}
32
33function 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
46function 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
55const USE_HOOK_PREFIX_REGEX = /^use[A-Z]/;
56
57const Lists = new WeakMap();
58const ReactImports = new WeakMap();
59
60/**
61 * Components
62 */
63class 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
240function 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 */
262function 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
287function 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
940module.exports = Object.assign(Components, {
941 detect(rule) {
942 return componentRule.bind(this, rule);
943 },
944});
Note: See TracBrowser for help on using the repository browser.