Skip to content

Commit 3ef8aba

Browse files
committed
[Tests] Component util isReactHookCall
Rename Components test suite filename to match sibling lib/util/Components filename. Extend Components testComponentsDetect function to accept custom instructions, and to accumulate the results of processing those instructions. Add utility to check whether a CallExpression is a React hook call.
1 parent d56fdb8 commit 3ef8aba

File tree

1 file changed

+89
-0
lines changed

1 file changed

+89
-0
lines changed

lib/util/Components.js

+89
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ function mergeUsedPropTypes(propsList, newPropsList) {
4646
return propsList.concat(propsToAdd);
4747
}
4848

49+
const USE_HOOK_PREFIX_REGEX = /^use/i;
50+
4951
const Lists = new WeakMap();
5052
const ReactImports = new WeakMap();
5153

@@ -787,6 +789,93 @@ function componentRule(rule, context) {
787789
&& !!(node.params || []).length
788790
);
789791
},
792+
793+
/**
794+
* Identify whether a node (CallExpression) is a call to a React hook
795+
*
796+
* @param {ASTNode} node The AST node being searched. (expects CallExpression)
797+
* @param {('useCallback'|'useContext'|'useDebugValue'|'useEffect'|'useImperativeHandle'|'useLayoutEffect'|'useMemo'|'useReducer'|'useRef'|'useState')[]} [expectedHookNames] React hook names to which search is limited.
798+
* @returns {Boolean} True if the node is a call to a React hook
799+
*/
800+
isReactHookCall(node, expectedHookNames) {
801+
if (node.type !== 'CallExpression') {
802+
return false;
803+
}
804+
805+
const defaultReactImports = components.getDefaultReactImports();
806+
const namedReactImports = components.getNamedReactImports();
807+
808+
const defaultReactImportSpecifier = defaultReactImports
809+
? defaultReactImports[0]
810+
: undefined;
811+
812+
const defaultReactImportName = defaultReactImportSpecifier
813+
? defaultReactImportSpecifier.local.name
814+
: undefined;
815+
816+
const reactHookImportSpecifiers = namedReactImports
817+
? namedReactImports.filter((specifier) => specifier.imported.name.match(USE_HOOK_PREFIX_REGEX))
818+
: undefined;
819+
const reactHookImportNames = reactHookImportSpecifiers
820+
? reactHookImportSpecifiers.reduce(
821+
(acc, specifier) => {
822+
acc[specifier.local.name] = specifier.imported.name;
823+
return acc;
824+
},
825+
{}
826+
)
827+
: undefined;
828+
829+
const isPotentialReactHookCall = !!(
830+
defaultReactImportName
831+
&& node.callee.type === 'MemberExpression'
832+
&& node.callee.object.type === 'Identifier'
833+
&& node.callee.object.name === defaultReactImportName
834+
&& node.callee.property.type === 'Identifier'
835+
&& node.callee.property.name.match(USE_HOOK_PREFIX_REGEX)
836+
);
837+
838+
const isPotentialHookCall = !!(
839+
reactHookImportNames
840+
&& node.callee.type === 'Identifier'
841+
&& node.callee.name.match(USE_HOOK_PREFIX_REGEX)
842+
);
843+
844+
const scope = isPotentialReactHookCall || isPotentialHookCall
845+
? context.getScope()
846+
: undefined;
847+
848+
const reactResolvedDefs = isPotentialReactHookCall
849+
&& scope.references
850+
&& scope.references.find(
851+
(reference) => reference.identifier.name === defaultReactImportName
852+
).resolved.defs;
853+
const potentialHookReference = isPotentialHookCall
854+
&& scope.references
855+
&& scope.references.find(
856+
(reference) => reactHookImportNames[reference.identifier.name]
857+
);
858+
const hookResolvedDefs = potentialHookReference && potentialHookReference.resolved.defs;
859+
860+
const hookName = (isPotentialReactHookCall && node.callee.property.name)
861+
|| (isPotentialHookCall && potentialHookReference && node.callee.name);
862+
const normalizedHookName = (reactHookImportNames && reactHookImportNames[hookName]) || hookName;
863+
864+
const isReactShadowed = isPotentialReactHookCall && reactResolvedDefs
865+
&& reactResolvedDefs.some((reactDef) => reactDef.type !== 'ImportBinding');
866+
867+
const isHookShadowed = isPotentialHookCall
868+
&& hookResolvedDefs
869+
&& hookResolvedDefs.some(
870+
(hookDef) => hookDef.name.name === hookName
871+
&& hookDef.type !== 'ImportBinding'
872+
);
873+
874+
const isHookCall = (isPotentialReactHookCall && !isReactShadowed)
875+
|| (isPotentialHookCall && hookName && !isHookShadowed);
876+
return !!(isHookCall
877+
&& (!expectedHookNames || arrayIncludes(expectedHookNames, normalizedHookName)));
878+
},
790879
};
791880

792881
// Component detection instructions

0 commit comments

Comments
 (0)