From 3ef8aba0833e8f089abb9ed685280098e97fb24f Mon Sep 17 00:00:00 2001 From: Duncan Beevers Date: Mon, 13 Dec 2021 04:57:42 -0800 Subject: [PATCH] [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. --- lib/util/Components.js | 89 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/lib/util/Components.js b/lib/util/Components.js index c0621b645c..909bdbd34e 100644 --- a/lib/util/Components.js +++ b/lib/util/Components.js @@ -46,6 +46,8 @@ function mergeUsedPropTypes(propsList, newPropsList) { return propsList.concat(propsToAdd); } +const USE_HOOK_PREFIX_REGEX = /^use/i; + const Lists = new WeakMap(); const ReactImports = new WeakMap(); @@ -787,6 +789,93 @@ function componentRule(rule, context) { && !!(node.params || []).length ); }, + + /** + * Identify whether a node (CallExpression) is a call to a React hook + * + * @param {ASTNode} node The AST node being searched. (expects CallExpression) + * @param {('useCallback'|'useContext'|'useDebugValue'|'useEffect'|'useImperativeHandle'|'useLayoutEffect'|'useMemo'|'useReducer'|'useRef'|'useState')[]} [expectedHookNames] React hook names to which search is limited. + * @returns {Boolean} True if the node is a call to a React hook + */ + isReactHookCall(node, expectedHookNames) { + if (node.type !== 'CallExpression') { + return false; + } + + const defaultReactImports = components.getDefaultReactImports(); + const namedReactImports = components.getNamedReactImports(); + + const defaultReactImportSpecifier = defaultReactImports + ? defaultReactImports[0] + : undefined; + + const defaultReactImportName = defaultReactImportSpecifier + ? defaultReactImportSpecifier.local.name + : undefined; + + const reactHookImportSpecifiers = namedReactImports + ? namedReactImports.filter((specifier) => specifier.imported.name.match(USE_HOOK_PREFIX_REGEX)) + : undefined; + const reactHookImportNames = reactHookImportSpecifiers + ? reactHookImportSpecifiers.reduce( + (acc, specifier) => { + acc[specifier.local.name] = specifier.imported.name; + return acc; + }, + {} + ) + : undefined; + + const isPotentialReactHookCall = !!( + defaultReactImportName + && node.callee.type === 'MemberExpression' + && node.callee.object.type === 'Identifier' + && node.callee.object.name === defaultReactImportName + && node.callee.property.type === 'Identifier' + && node.callee.property.name.match(USE_HOOK_PREFIX_REGEX) + ); + + const isPotentialHookCall = !!( + reactHookImportNames + && node.callee.type === 'Identifier' + && node.callee.name.match(USE_HOOK_PREFIX_REGEX) + ); + + const scope = isPotentialReactHookCall || isPotentialHookCall + ? context.getScope() + : undefined; + + const reactResolvedDefs = isPotentialReactHookCall + && scope.references + && scope.references.find( + (reference) => reference.identifier.name === defaultReactImportName + ).resolved.defs; + const potentialHookReference = isPotentialHookCall + && scope.references + && scope.references.find( + (reference) => reactHookImportNames[reference.identifier.name] + ); + const hookResolvedDefs = potentialHookReference && potentialHookReference.resolved.defs; + + const hookName = (isPotentialReactHookCall && node.callee.property.name) + || (isPotentialHookCall && potentialHookReference && node.callee.name); + const normalizedHookName = (reactHookImportNames && reactHookImportNames[hookName]) || hookName; + + const isReactShadowed = isPotentialReactHookCall && reactResolvedDefs + && reactResolvedDefs.some((reactDef) => reactDef.type !== 'ImportBinding'); + + const isHookShadowed = isPotentialHookCall + && hookResolvedDefs + && hookResolvedDefs.some( + (hookDef) => hookDef.name.name === hookName + && hookDef.type !== 'ImportBinding' + ); + + const isHookCall = (isPotentialReactHookCall && !isReactShadowed) + || (isPotentialHookCall && hookName && !isHookShadowed); + return !!(isHookCall + && (!expectedHookNames || arrayIncludes(expectedHookNames, normalizedHookName))); + }, }; // Component detection instructions