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

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

Pred finalna verzija

  • Property mode set to 100644
File size: 29.4 KB
RevLine 
[d565449]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.
[0c6b92a]73 * @param {number} confidence Confidence in the component detection (0=banned, 1=maybe, 2=yes)
[d565449]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 *
[0c6b92a]186 * @returns {number} Components list length
[d565449]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 /**
[0c6b92a]306 * @param {ASTNode} node
[d565449]307 * @param {boolean=} strict
308 * @returns {boolean}
309 */
[0c6b92a]310 isReturningJSX(node, strict) {
311 return jsxUtil.isReturningJSX(context, node, strict, true);
[d565449]312 },
313
[0c6b92a]314 isReturningJSXOrNull(node, strict) {
315 return jsxUtil.isReturningJSX(context, node, strict);
[d565449]316 },
317
[0c6b92a]318 isReturningOnlyNull(node) {
319 return jsxUtil.isReturningOnlyNull(node, context);
[d565449]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) {
[0c6b92a]410 if (!astUtil.isCallExpression(node)) {
[d565449]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 } }
[0c6b92a]570 if (
571 node.parent
572 && node.parent.key
573 && node.parent.key.type === 'MemberExpression'
574 && !utils.isReturningJSX(node)
575 && !utils.isReturningOnlyNull(node)
576 ) {
[d565449]577 return undefined;
578 }
579
580 if (
581 node.parent.type === 'Property' && (
582 (node.parent.method && !node.parent.computed) // case: { f() { return ... } }
583 || (!node.id && !node.parent.computed) // case: { f: () => ... }
584 )
585 ) {
[0c6b92a]586 if (
587 isFirstLetterCapitalized(node.parent.key.name)
588 && utils.isReturningJSX(node)
589 ) {
[d565449]590 return node;
591 }
592 return undefined;
593 }
594
595 // Case like `React.memo(() => <></>)` or `React.forwardRef(...)`
596 const pragmaComponentWrapper = utils.getPragmaComponentWrapper(node);
597 if (pragmaComponentWrapper && utils.isReturningJSXOrNull(node)) {
598 return pragmaComponentWrapper;
599 }
600
601 if (!(utils.isInAllowedPositionForComponent(node) && utils.isReturningJSXOrNull(node))) {
602 return undefined;
603 }
604
605 if (utils.isParentComponentNotStatelessComponent(node)) {
606 return undefined;
607 }
608
609 if (node.id) {
610 return isFirstLetterCapitalized(node.id.name) ? node : undefined;
611 }
612
613 if (
614 isPropertyAssignment
615 && !isModuleExportsAssignment
616 && !isFirstLetterCapitalized(parent.left.property.name)
617 ) {
618 return undefined;
619 }
620
621 if (parent.type === 'Property' && utils.isReturningOnlyNull(node)) {
622 return undefined;
623 }
624
625 return node;
626 }
627
628 return undefined;
629 },
630
631 /**
632 * Get the parent stateless component node from the current scope
633 *
634 * @param {ASTNode} node The AST node being checked
635 * @returns {ASTNode} component node, null if we are not in a component
636 */
637 getParentStatelessComponent(node) {
638 let scope = getScope(context, node);
639 while (scope) {
640 const statelessComponent = utils.getStatelessComponent(scope.block);
641 if (statelessComponent) {
642 return statelessComponent;
643 }
644 scope = scope.upper;
645 }
646 return null;
647 },
648
649 /**
650 * Get the related component from a node
651 *
652 * @param {ASTNode} node The AST node being checked (must be a MemberExpression).
[0c6b92a]653 * @returns {ASTNode | null} component node, null if we cannot find the component
[d565449]654 */
655 getRelatedComponent(node) {
656 let i;
657 let j;
658 let k;
659 let l;
660 let componentNode;
661 // Get the component path
662 const componentPath = [];
663 let nodeTemp = node;
664 while (nodeTemp) {
665 if (nodeTemp.property && nodeTemp.property.type === 'Identifier') {
666 componentPath.push(nodeTemp.property.name);
667 }
668 if (nodeTemp.object && nodeTemp.object.type === 'Identifier') {
669 componentPath.push(nodeTemp.object.name);
670 }
671 nodeTemp = nodeTemp.object;
672 }
673 componentPath.reverse();
674 const componentName = componentPath.slice(0, componentPath.length - 1).join('.');
675
676 // Find the variable in the current scope
677 const variableName = componentPath.shift();
678 if (!variableName) {
679 return null;
680 }
681 const variableInScope = variableUtil.getVariableFromContext(context, node, variableName);
682 if (!variableInScope) {
683 return null;
684 }
685
686 // Try to find the component using variable references
687 variableInScope.references.some((ref) => {
688 let refId = ref.identifier;
689 if (refId.parent && refId.parent.type === 'MemberExpression') {
690 refId = refId.parent;
691 }
692 if (getText(context, refId) !== componentName) {
693 return false;
694 }
695 if (refId.type === 'MemberExpression') {
696 componentNode = refId.parent.right;
697 } else if (
698 refId.parent
699 && refId.parent.type === 'VariableDeclarator'
700 && refId.parent.init
701 && refId.parent.init.type !== 'Identifier'
702 ) {
703 componentNode = refId.parent.init;
704 }
705 return true;
706 });
707
708 if (componentNode) {
709 // Return the component
710 return components.add(componentNode, 1);
711 }
712
713 // Try to find the component using variable declarations
714 const defs = variableInScope.defs;
715 const defInScope = defs.find((def) => (
716 def.type === 'ClassName'
717 || def.type === 'FunctionName'
718 || def.type === 'Variable'
719 ));
720 if (!defInScope || !defInScope.node) {
721 return null;
722 }
723 componentNode = defInScope.node.init || defInScope.node;
724
725 // Traverse the node properties to the component declaration
726 for (i = 0, j = componentPath.length; i < j; i++) {
727 if (!componentNode.properties) {
728 continue; // eslint-disable-line no-continue
729 }
730 for (k = 0, l = componentNode.properties.length; k < l; k++) {
731 if (componentNode.properties[k].key && componentNode.properties[k].key.name === componentPath[i]) {
732 componentNode = componentNode.properties[k];
733 break;
734 }
735 }
736 if (!componentNode || !componentNode.value) {
737 return null;
738 }
739 componentNode = componentNode.value;
740 }
741
742 // Return the component
743 return components.add(componentNode, 1);
744 },
745
746 isParentComponentNotStatelessComponent(node) {
747 return !!(
748 node.parent
749 && node.parent.key
750 && node.parent.key.type === 'Identifier'
751 // custom component functions must start with a capital letter (returns false otherwise)
752 && node.parent.key.name.charAt(0) === node.parent.key.name.charAt(0).toLowerCase()
753 // react render function cannot have params
754 && !!(node.params || []).length
755 );
756 },
757
758 /**
759 * Identify whether a node (CallExpression) is a call to a React hook
760 *
761 * @param {ASTNode} node The AST node being searched. (expects CallExpression)
762 * @param {('useCallback'|'useContext'|'useDebugValue'|'useEffect'|'useImperativeHandle'|'useLayoutEffect'|'useMemo'|'useReducer'|'useRef'|'useState')[]} [expectedHookNames] React hook names to which search is limited.
[0c6b92a]763 * @returns {boolean} True if the node is a call to a React hook
[d565449]764 */
765 isReactHookCall(node, expectedHookNames) {
[0c6b92a]766 if (!astUtil.isCallExpression(node)) {
[d565449]767 return false;
768 }
769
770 const defaultReactImports = components.getDefaultReactImports();
771 const namedReactImports = components.getNamedReactImports();
772
773 const defaultReactImportName = defaultReactImports
774 && defaultReactImports[0]
775 && defaultReactImports[0].local.name;
776 const reactHookImportSpecifiers = namedReactImports
777 && namedReactImports.filter((specifier) => USE_HOOK_PREFIX_REGEX.test(specifier.imported.name));
778 const reactHookImportNames = reactHookImportSpecifiers
779 && fromEntries(reactHookImportSpecifiers.map((specifier) => [specifier.local.name, specifier.imported.name]));
780
781 const isPotentialReactHookCall = defaultReactImportName
782 && node.callee.type === 'MemberExpression'
783 && node.callee.object.type === 'Identifier'
784 && node.callee.object.name === defaultReactImportName
785 && node.callee.property.type === 'Identifier'
786 && node.callee.property.name.match(USE_HOOK_PREFIX_REGEX);
787
788 const isPotentialHookCall = reactHookImportNames
789 && node.callee.type === 'Identifier'
790 && node.callee.name.match(USE_HOOK_PREFIX_REGEX);
791
792 const scope = (isPotentialReactHookCall || isPotentialHookCall) && getScope(context, node);
793
794 const reactResolvedDefs = isPotentialReactHookCall
795 && scope.references
796 && scope.references.find(
797 (reference) => reference.identifier.name === defaultReactImportName
798 ).resolved.defs;
799
800 const isReactShadowed = isPotentialReactHookCall && reactResolvedDefs
801 && reactResolvedDefs.some((reactDef) => reactDef.type !== 'ImportBinding');
802
803 const potentialHookReference = isPotentialHookCall
804 && scope.references
805 && scope.references.find(
806 (reference) => reactHookImportNames[reference.identifier.name]
807 );
808
809 const hookResolvedDefs = potentialHookReference && potentialHookReference.resolved.defs;
[0c6b92a]810 const localHookName = (
811 isPotentialReactHookCall
812 && node.callee.property.name
813 ) || (
814 isPotentialHookCall
815 && potentialHookReference
816 && node.callee.name
817 );
[d565449]818 const isHookShadowed = isPotentialHookCall
819 && hookResolvedDefs
820 && hookResolvedDefs.some(
821 (hookDef) => hookDef.name.name === localHookName
822 && hookDef.type !== 'ImportBinding'
823 );
824
825 const isHookCall = (isPotentialReactHookCall && !isReactShadowed)
826 || (isPotentialHookCall && localHookName && !isHookShadowed);
827
828 if (!isHookCall) {
829 return false;
830 }
831
832 if (!expectedHookNames) {
833 return true;
834 }
835
836 return arrayIncludes(
837 expectedHookNames,
838 (reactHookImportNames && reactHookImportNames[localHookName]) || localHookName
839 );
840 },
841 };
842
843 // Component detection instructions
844 const detectionInstructions = {
845 CallExpression(node) {
846 if (!utils.isPragmaComponentWrapper(node)) {
847 return;
848 }
849 if (node.arguments.length > 0 && astUtil.isFunctionLikeExpression(node.arguments[0])) {
850 components.add(node, 2);
851 }
852 },
853
854 ClassExpression(node) {
855 if (!componentUtil.isES6Component(node, context)) {
856 return;
857 }
858 components.add(node, 2);
859 },
860
861 ClassDeclaration(node) {
862 if (!componentUtil.isES6Component(node, context)) {
863 return;
864 }
865 components.add(node, 2);
866 },
867
868 ObjectExpression(node) {
869 if (!componentUtil.isES5Component(node, context)) {
870 return;
871 }
872 components.add(node, 2);
873 },
874
875 FunctionExpression(node) {
876 if (node.async && node.generator) {
877 components.add(node, 0);
878 return;
879 }
880
881 const component = utils.getStatelessComponent(node);
882 if (!component) {
883 return;
884 }
885 components.add(component, 2);
886 },
887
888 FunctionDeclaration(node) {
889 if (node.async && node.generator) {
890 components.add(node, 0);
891 return;
892 }
893
[0c6b92a]894 const cNode = utils.getStatelessComponent(node);
895 if (!cNode) {
[d565449]896 return;
897 }
[0c6b92a]898 components.add(cNode, 2);
[d565449]899 },
900
901 ArrowFunctionExpression(node) {
902 const component = utils.getStatelessComponent(node);
903 if (!component) {
904 return;
905 }
906 components.add(component, 2);
907 },
908
909 ThisExpression(node) {
910 const component = utils.getParentStatelessComponent(node);
911 if (!component || !/Function/.test(component.type) || !node.parent.property) {
912 return;
913 }
914 // Ban functions accessing a property on a ThisExpression
915 components.add(node, 0);
916 },
917 };
918
919 // Detect React import specifiers
920 const reactImportInstructions = {
921 ImportDeclaration(node) {
922 const isReactImported = node.source.type === 'Literal' && node.source.value === 'react';
923 if (!isReactImported) {
924 return;
925 }
926
927 node.specifiers.forEach((specifier) => {
928 if (specifier.type === 'ImportDefaultSpecifier') {
929 components.addDefaultReactImport(specifier);
930 }
931 if (specifier.type === 'ImportSpecifier') {
932 components.addNamedReactImport(specifier);
933 }
934 });
935 },
936 };
937
938 const ruleInstructions = rule(context, components, utils);
939 const propTypesInstructions = propTypesUtil(context, components, utils);
940 const usedPropTypesInstructions = usedPropTypesUtil(context, components, utils);
941 const defaultPropsInstructions = defaultPropsUtil(context, components, utils);
942
943 const mergedRule = mergeRules([
944 detectionInstructions,
945 propTypesInstructions,
946 usedPropTypesInstructions,
947 defaultPropsInstructions,
948 reactImportInstructions,
949 ruleInstructions,
950 ]);
951
952 return mergedRule;
953}
954
955module.exports = Object.assign(Components, {
956 detect(rule) {
957 return componentRule.bind(this, rule);
958 },
959});
Note: See TracBrowser for help on using the repository browser.