Repository: jsx-eslint/eslint-plugin-react
Pull request #3273 (2022-04-18 11:15:55)

[Refactor] improve performance by avoiding unnecessary component detect
Rules using `Component.detect()` are generally slow. Some rules uses `Component.detect(context, components, utils)` solely for the `utils` it provided. These `Component.detect`s can be avoided by extracting some `utils` functions into a module.

Benchmark with config `plugin:react/all`:
```diff
$ time npx eslint .
- 16.429
+ 13.692
```


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3f1f805..19627cba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,7 +24,9 @@ This change log adheres to standards from [Keep a CHANGELOG](https://keepachange
 * [Refactor] improve performance for detecting class components ([#3267][] @golopot)
 * [Refactor] [`no-deprecated`]: improve performance ([#3271][] @golopot)
 * [Refactor] [`no-did-mount-set-state`], [`no-did-update-set-state`], [`no-will-update-set-state`]: improve performance ([#3272][] @golopot)
+* [Refactor] improve performance by avoiding unnecessary `Components.detect` ([#3273][] @golopot)
 
+[#3273]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3273
 [#3272]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3272
 [#3271]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3271
 [#3267]: https://github.com/jsx-eslint/eslint-plugin-react/pull/3267
diff --git a/lib/rules/display-name.js b/lib/rules/display-name.js
index 528b8f2d..a99aabce 100644
--- a/lib/rules/display-name.js
+++ b/lib/rules/display-name.js
@@ -9,6 +9,7 @@ const values = require('object.values');
 
 const Components = require('../util/Components');
 const astUtil = require('../util/ast');
+const componentUtil = require('../util/componentUtil');
 const docsUrl = require('../util/docsUrl');
 const propsUtil = require('../util/props');
 const report = require('../util/report');
@@ -106,7 +107,7 @@ module.exports = {
         astUtil.isFunctionLikeExpression(node)
         && node.parent
         && (node.parent.type === 'VariableDeclarator' || node.parent.type === 'Property' || node.parent.method === true)
-        && (!node.parent.parent || !utils.isES5Component(node.parent.parent))
+        && (!node.parent.parent || !componentUtil.isES5Component(node.parent.parent, context))
       );
 
       if (
@@ -192,7 +193,7 @@ module.exports = {
       },
 
       ObjectExpression(node) {
-        if (!utils.isES5Component(node)) {
+        if (!componentUtil.isES5Component(node, context)) {
           return;
         }
         if (ignoreTranspilerName || !hasTranspilerName(node)) {
diff --git a/lib/rules/jsx-indent.js b/lib/rules/jsx-indent.js
index 88d177d3..ad14fa6a 100644
--- a/lib/rules/jsx-indent.js
+++ b/lib/rules/jsx-indent.js
@@ -36,7 +36,6 @@ const astUtil = require('../util/ast');
 const docsUrl = require('../util/docsUrl');
 const reportC = require('../util/report');
 const jsxUtil = require('../util/jsx');
-const isCreateElement = require('../util/isCreateElement');
 
 // ------------------------------------------------------------------------------
 // Rule Definition
@@ -428,7 +427,7 @@ module.exports = {
         }
         if (
           !fn
-          || !jsxUtil.isReturningJSX((n) => isCreateElement(n, context), node, context, true)
+          || !jsxUtil.isReturningJSX(node, context, true)
         ) {
           return;
         }
diff --git a/lib/rules/jsx-no-bind.js b/lib/rules/jsx-no-bind.js
index 709fc985..5e55aa88 100644
--- a/lib/rules/jsx-no-bind.js
+++ b/lib/rules/jsx-no-bind.js
@@ -8,7 +8,6 @@
 'use strict';
 
 const propName = require('jsx-ast-utils/propName');
-const Components = require('../util/Components');
 const docsUrl = require('../util/docsUrl');
 const jsxUtil = require('../util/jsx');
 const report = require('../util/report');
@@ -63,7 +62,7 @@ module.exports = {
     }],
   },
 
-  create: Components.detect((context) => {
+  create(context) {
     const configuration = context.options[0] || {};
 
     // Keep track of all the variable names pointing to a bind call,
@@ -198,5 +197,5 @@ module.exports = {
         }
       },
     };
-  }),
+  },
 };
diff --git a/lib/rules/no-access-state-in-setstate.js b/lib/rules/no-access-state-in-setstate.js
index e3e76a23..25773018 100644
--- a/lib/rules/no-access-state-in-setstate.js
+++ b/lib/rules/no-access-state-in-setstate.js
@@ -6,7 +6,7 @@
 'use strict';
 
 const docsUrl = require('../util/docsUrl');
-const Components = require('../util/Components');
+const componentUtil = require('../util/componentUtil');
 const report = require('../util/report');
 
 // ------------------------------------------------------------------------------
@@ -29,7 +29,7 @@ module.exports = {
     messages,
   },
 
-  create: Components.detect((context, components, utils) => {
+  create(context) {
     function isSetStateCall(node) {
       return node.type === 'CallExpression'
         && node.callee.property
@@ -48,7 +48,7 @@ module.exports = {
     }
 
     function isClassComponent() {
-      return !!(utils.getParentES6Component() || utils.getParentES5Component());
+      return !!(componentUtil.getParentES6Component(context) || componentUtil.getParentES5Component(context));
     }
 
     // The methods array contains all methods or functions that are using this.state
@@ -183,5 +183,5 @@ module.exports = {
         });
       },
     };
-  }),
+  },
 };
diff --git a/lib/rules/no-arrow-function-lifecycle.js b/lib/rules/no-arrow-function-lifecycle.js
index e2a69f3d..273eb1ca 100644
--- a/lib/rules/no-arrow-function-lifecycle.js
+++ b/lib/rules/no-arrow-function-lifecycle.js
@@ -9,6 +9,7 @@ const values = require('object.values');
 
 const Components = require('../util/Components');
 const astUtil = require('../util/ast');
+const componentUtil = require('../util/componentUtil');
 const docsUrl = require('../util/docsUrl');
 const lifecycleMethods = require('../util/lifecycleMethods');
 const report = require('../util/report');
@@ -44,7 +45,7 @@ module.exports = {
     fixable: 'code',
   },
 
-  create: Components.detect((context, components, utils) => {
+  create: Components.detect((context, components) => {
     /**
      * @param {Array} properties list of component properties
      */
@@ -57,7 +58,7 @@ module.exports = {
         const propertyName = astUtil.getPropertyName(node);
         const nodeType = node.value.type;
         const isLifecycleMethod = (
-          node.static && !utils.isES5Component(node)
+          node.static && !componentUtil.isES5Component(node, context)
             ? lifecycleMethods.static
             : lifecycleMethods.instance
         ).indexOf(propertyName) > -1;
diff --git a/lib/rules/no-deprecated.js b/lib/rules/no-deprecated.js
index e9c22346..b8d42272 100644
--- a/lib/rules/no-deprecated.js
+++ b/lib/rules/no-deprecated.js
@@ -8,9 +8,8 @@
 'use strict';
 
 const values = require('object.values');
-
-const Components = require('../util/Components');
 const astUtil = require('../util/ast');
+const componentUtil = require('../util/componentUtil');
 const docsUrl = require('../util/docsUrl');
 const pragmaUtil = require('../util/pragma');
 const testReactVersion = require('../util/version').testReactVersion;
@@ -104,7 +103,7 @@ module.exports = {
     schema: [],
   },
 
-  create: Components.detect((context, components, utils) => {
+  create(context) {
     const pragma = pragmaUtil.getFromContext(context);
     const deprecated = getDeprecated(pragma);
 
@@ -167,7 +166,10 @@ module.exports = {
      * @param {ASTNode} node The AST node being checked.
      */
     function checkLifeCycleMethods(node) {
-      if (utils.isES5Component(node) || utils.isES6Component(node)) {
+      if (
+        componentUtil.isES5Component(node, context)
+     || componentUtil.isES6Component(node, context)
+      ) {
         const methods = getLifeCycleMethods(node);
         methods.forEach((method) => checkDeprecation(node, method.name, method.node));
       }
@@ -221,5 +223,5 @@ module.exports = {
       ClassExpression: checkLifeCycleMethods,
       ObjectExpression: checkLifeCycleMethods,
     };
-  }),
+  },
 };
diff --git a/lib/rules/no-direct-mutation-state.js b/lib/rules/no-direct-mutation-state.js
index 6a30ed89..573ff1bc 100644
--- a/lib/rules/no-direct-mutation-state.js
+++ b/lib/rules/no-direct-mutation-state.js
@@ -7,6 +7,7 @@
 'use strict';
 
 const Components = require('../util/Components');
+const componentUtil = require('../util/componentUtil');
 const docsUrl = require('../util/docsUrl');
 const report = require('../util/report');
 
@@ -99,7 +100,7 @@ module.exports = {
           return;
         }
         const item = getOuterMemberExpression(node.left);
-        if (utils.isStateMemberExpression(item)) {
+        if (componentUtil.isStateMemberExpression(item)) {
           const mutations = (component && component.mutations) || [];
           mutations.push(node.left.object);
           components.set(node, {
@@ -115,7 +116,7 @@ module.exports = {
           return;
         }
         const item = getOuterMemberExpression(node.argument);
-        if (utils.isStateMemberExpression(item)) {
+        if (componentUtil.isStateMemberExpression(item)) {
           const mutations = (component && component.mutations) || [];
           mutations.push(item);
           components.set(node, {
diff --git a/lib/rules/no-redundant-should-component-update.js b/lib/rules/no-redundant-should-component-update.js
index 4298666f..b4a7c5d0 100644
--- a/lib/rules/no-redundant-should-component-update.js
+++ b/lib/rules/no-redundant-should-component-update.js
@@ -4,8 +4,8 @@
 
 'use strict';
 
-const Components = require('../util/Components');
 const astUtil = require('../util/ast');
+const componentUtil = require('../util/componentUtil');
 const docsUrl = require('../util/docsUrl');
 const report = require('../util/report');
 
@@ -31,7 +31,7 @@ module.exports = {
     schema: [],
   },
 
-  create: Components.detect((context, components, utils) => {
+  create(context) {
     /**
      * Checks for shouldComponentUpdate property
      * @param {ASTNode} node The AST node being checked.
@@ -65,7 +65,7 @@ module.exports = {
      * @param {ASTNode} node The AST node being checked.
      */
     function checkForViolation(node) {
-      if (utils.isPureComponent(node)) {
+      if (componentUtil.isPureComponent(node, context)) {
         const hasScu = hasShouldComponentUpdate(node);
         if (hasScu) {
           const className = getNodeName(node);
@@ -83,5 +83,5 @@ module.exports = {
       ClassDeclaration: checkForViolation,
       ClassExpression: checkForViolation,
     };
-  }),
+  },
 };
diff --git a/lib/rules/no-string-refs.js b/lib/rules/no-string-refs.js
index 88ef59b8..40ee1550 100644
--- a/lib/rules/no-string-refs.js
+++ b/lib/rules/no-string-refs.js
@@ -5,7 +5,7 @@
 
 'use strict';
 
-const Components = require('../util/Components');
+const componentUtil = require('../util/componentUtil');
 const docsUrl = require('../util/docsUrl');
 const report = require('../util/report');
 
@@ -40,7 +40,7 @@ module.exports = {
     }],
   },
 
-  create: Components.detect((context, components, utils) => {
+  create(context) {
     const detectTemplateLiterals = context.options[0] ? context.options[0].noTemplateLiterals : false;
     /**
      * Checks if we are using refs
@@ -49,7 +49,7 @@ module.exports = {
      */
     function isRefsUsage(node) {
       return !!(
-        (utils.getParentES6Component() || utils.getParentES5Component())
+        (componentUtil.getParentES6Component(context) || componentUtil.getParentES5Component(context))
         && node.object.type === 'ThisExpression'
         && node.property.name === 'refs'
       );
@@ -115,5 +115,5 @@ module.exports = {
         }
       },
     };
-  }),
+  },
 };
diff --git a/lib/rules/no-typos.js b/lib/rules/no-typos.js
index 76783729..be64da58 100644
--- a/lib/rules/no-typos.js
+++ b/lib/rules/no-typos.js
@@ -7,6 +7,7 @@
 const PROP_TYPES = Object.keys(require('prop-types'));
 const Components = require('../util/Components');
 const docsUrl = require('../util/docsUrl');
+const componentUtil = require('../util/componentUtil');
 const report = require('../util/report');
 const lifecycleMethods = require('../util/lifecycleMethods');
 
@@ -202,7 +203,7 @@ module.exports = {
       },
 
       'ClassProperty, PropertyDefinition'(node) {
-        if (!node.static || !utils.isES6Component(node.parent.parent)) {
+        if (!node.static || !componentUtil.isES6Component(node.parent.parent, context)) {
           return;
         }
 
@@ -223,7 +224,7 @@ module.exports = {
 
         if (
           relatedComponent
-            && (utils.isES6Component(relatedComponent.node) || (
+            && (componentUtil.isES6Component(relatedComponent.node, context) || (
               relatedComponent.node.type !== 'ClassDeclaration' && utils.isReturningJSX(relatedComponent.node)))
             && (node.parent && node.parent.type === 'AssignmentExpression' && node.parent.right)
         ) {
@@ -232,7 +233,7 @@ module.exports = {
       },
 
       MethodDefinition(node) {
-        if (!utils.isES6Component(node.parent.parent)) {
+        if (!componentUtil.isES6Component(node.parent.parent, context)) {
           return;
         }
 
@@ -240,7 +241,7 @@ module.exports = {
       },
 
       ObjectExpression(node) {
-        const component = utils.isES5Component(node) && components.get(node);
+        const component = componentUtil.isES5Component(node, context) && components.get(node);
 
         if (!component) {
           return;
diff --git a/lib/rules/no-unsafe.js b/lib/rules/no-unsafe.js
index fed98304..48fa0ec7 100644
--- a/lib/rules/no-unsafe.js
+++ b/lib/rules/no-unsafe.js
@@ -5,8 +5,8 @@
 
 'use strict';
 
-const Components = require('../util/Components');
 const astUtil = require('../util/ast');
+const componentUtil = require('../util/componentUtil');
 const docsUrl = require('../util/docsUrl');
 const testReactVersion = require('../util/version').testReactVersion;
 const report = require('../util/report');
@@ -44,7 +44,7 @@ module.exports = {
     ],
   },
 
-  create: Components.detect((context, components, utils) => {
+  create(context) {
     const config = context.options[0] || {};
     const checkAliases = config.checkAliases || false;
 
@@ -133,7 +133,7 @@ module.exports = {
      * @param {ASTNode} node The AST node being checked.
      */
     function checkLifeCycleMethods(node) {
-      if (utils.isES5Component(node) || utils.isES6Component(node)) {
+      if (componentUtil.isES5Component(node, context) || componentUtil.isES6Component(node, context)) {
         const methods = getLifeCycleMethods(node);
         methods.forEach((method) => checkUnsafe(node, method));
       }
@@ -144,5 +144,5 @@ module.exports = {
       ClassExpression: checkLifeCycleMethods,
       ObjectExpression: checkLifeCycleMethods,
     };
-  }),
+  },
 };
diff --git a/lib/rules/no-unused-class-component-methods.js b/lib/rules/no-unused-class-component-methods.js
index 3bcadd1d..9d15618b 100644
--- a/lib/rules/no-unused-class-component-methods.js
+++ b/lib/rules/no-unused-class-component-methods.js
@@ -5,8 +5,8 @@
 
 'use strict';
 
-const Components = require('../util/Components');
 const docsUrl = require('../util/docsUrl');
+const componentUtil = require('../util/componentUtil');
 const report = require('../util/report');
 
 // ------------------------------------------------------------------------------
@@ -115,7 +115,7 @@ module.exports = {
     ],
   },
 
-  create: Components.detect((context, components, utils) => {
+  create: ((context) => {
     let classInfo = null;
 
     // Takes an ObjectExpression node and adds all named Property nodes to the
@@ -171,13 +171,13 @@ module.exports = {
 
     return {
       ClassDeclaration(node) {
-        if (utils.isES6Component(node)) {
+        if (componentUtil.isES6Component(node, context)) {
           classInfo = getInitialClassInfo(node, true);
         }
       },
 
       ObjectExpression(node) {
-        if (utils.isES5Component(node)) {
+        if (componentUtil.isES5Component(node, context)) {
           classInfo = getInitialClassInfo(node, false);
         }
       },
diff --git a/lib/rules/no-unused-state.js b/lib/rules/no-unused-state.js
index ed4393f6..2c89f16b 100644
--- a/lib/rules/no-unused-state.js
+++ b/lib/rules/no-unused-state.js
@@ -9,9 +9,9 @@
 
 'use strict';
 
-const Components = require('../util/Components');
 const docsUrl = require('../util/docsUrl');
 const ast = require('../util/ast');
+const componentUtil = require('../util/componentUtil');
 const report = require('../util/report');
 
 // Descend through all wrapping TypeCastExpressions and return the expression
@@ -91,7 +91,7 @@ module.exports = {
     schema: [],
   },
 
-  create: Components.detect((context, components, utils) => {
+  create(context) {
     // Non-null when we are inside a React component ClassDeclaration and we have
     // not yet encountered any use of this.state which we have chosen not to
     // analyze. If we encounter any such usage (like this.state being spread as
@@ -233,7 +233,7 @@ module.exports = {
     }
 
     function handleES6ComponentEnter(node) {
-      if (utils.isES6Component(node)) {
+      if (componentUtil.isES6Component(node, context)) {
         classInfo = getInitialClassInfo();
       }
     }
@@ -270,7 +270,7 @@ module.exports = {
       'ClassExpression:exit': handleES6ComponentExit,
 
       ObjectExpression(node) {
-        if (utils.isES5Component(node)) {
+        if (componentUtil.isES5Component(node, context)) {
           classInfo = getInitialClassInfo();
         }
       },
@@ -280,7 +280,7 @@ module.exports = {
           return;
         }
 
-        if (utils.isES5Component(node)) {
+        if (componentUtil.isES5Component(node, context)) {
           reportUnusedFields();
           classInfo = null;
         }
@@ -424,7 +424,7 @@ module.exports = {
         }
 
         const parent = node.parent;
-        if (!utils.isES5Component(parent.parent)) {
+        if (!componentUtil.isES5Component(parent.parent, context)) {
           return;
         }
 
@@ -516,5 +516,5 @@ module.exports = {
         }
       },
     };
-  }),
+  },
 };
diff --git a/lib/rules/prefer-es6-class.js b/lib/rules/prefer-es6-class.js
index 01ac32ff..5ca020eb 100644
--- a/lib/rules/prefer-es6-class.js
+++ b/lib/rules/prefer-es6-class.js
@@ -5,7 +5,7 @@
 
 'use strict';
 
-const Components = require('../util/Components');
+const componentUtil = require('../util/componentUtil');
 const docsUrl = require('../util/docsUrl');
 const report = require('../util/report');
 
@@ -34,24 +34,24 @@ module.exports = {
     }],
   },
 
-  create: Components.detect((context, components, utils) => {
+  create(context) {
     const configuration = context.options[0] || 'always';
 
     return {
       ObjectExpression(node) {
-        if (utils.isES5Component(node) && configuration === 'always') {
+        if (componentUtil.isES5Component(node, context) && configuration === 'always') {
           report(context, messages.shouldUseES6Class, 'shouldUseES6Class', {
             node,
           });
         }
       },
       ClassDeclaration(node) {
-        if (utils.isES6Component(node) && configuration === 'never') {
+        if (componentUtil.isES6Component(node, context) && configuration === 'never') {
           report(context, messages.shouldUseCreateClass, 'shouldUseCreateClass', {
             node,
           });
         }
       },
     };
-  }),
+  },
 };
diff --git a/lib/rules/prefer-stateless-function.js b/lib/rules/prefer-stateless-function.js
index 3ade763a..7d39738e 100644
--- a/lib/rules/prefer-stateless-function.js
+++ b/lib/rules/prefer-stateless-function.js
@@ -10,6 +10,7 @@
 const Components = require('../util/Components');
 const testReactVersion = require('../util/version').testReactVersion;
 const astUtil = require('../util/ast');
+const componentUtil = require('../util/componentUtil');
 const docsUrl = require('../util/docsUrl');
 const report = require('../util/report');
 
@@ -272,7 +273,7 @@ module.exports = {
     }
 
     function visitClass(node) {
-      if (ignorePureComponents && utils.isPureComponent(node)) {
+      if (ignorePureComponents && componentUtil.isPureComponent(node, context)) {
         markSCUAsDeclared(node);
       }
 
@@ -370,7 +371,10 @@ module.exports = {
             || list[component].invalidReturn
             || list[component].hasChildContextTypes
             || list[component].useDecorators
-            || (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node))
+            || (
+              !componentUtil.isES5Component(list[component].node, context)
+              && !componentUtil.isES6Component(list[component].node, context)
+            )
           ) {
             return;
           }
diff --git a/lib/rules/require-optimization.js b/lib/rules/require-optimization.js
index 8f61d214..3a91c51d 100644
--- a/lib/rules/require-optimization.js
+++ b/lib/rules/require-optimization.js
@@ -6,6 +6,7 @@
 'use strict';
 
 const Components = require('../util/Components');
+const componentUtil = require('../util/componentUtil');
 const docsUrl = require('../util/docsUrl');
 const report = require('../util/report');
 
@@ -38,7 +39,7 @@ module.exports = {
     }],
   },
 
-  create: Components.detect((context, components, utils) => {
+  create: Components.detect((context, components) => {
     const configuration = context.options[0] || {};
     const allowDecorators = configuration.allowDecorators || [];
 
@@ -177,7 +178,11 @@ module.exports = {
       },
 
       ClassDeclaration(node) {
-        if (!(hasPureRenderDecorator(node) || hasCustomDecorator(node) || utils.isPureComponent(node))) {
+        if (!(
+          hasPureRenderDecorator(node)
+          || hasCustomDecorator(node)
+          || componentUtil.isPureComponent(node, context)
+        )) {
           return;
         }
         markSCUAsDeclared(node);
diff --git a/lib/rules/require-render-return.js b/lib/rules/require-render-return.js
index fa4afcbe..4148e0a9 100644
--- a/lib/rules/require-render-return.js
+++ b/lib/rules/require-render-return.js
@@ -7,6 +7,7 @@
 
 const Components = require('../util/Components');
 const astUtil = require('../util/ast');
+const componentUtil = require('../util/componentUtil');
 const docsUrl = require('../util/docsUrl');
 const report = require('../util/report');
 
@@ -32,7 +33,7 @@ module.exports = {
     schema: [],
   },
 
-  create: Components.detect((context, components, utils) => {
+  create: Components.detect((context, components) => {
     /**
      * Mark a return statement as present
      * @param {ASTNode} node The AST node being checked.
@@ -86,7 +87,10 @@ module.exports = {
           if (
             !findRenderMethod(list[component].node)
             || list[component].hasReturnStatement
-            || (!utils.isES5Component(list[component].node) && !utils.isES6Component(list[component].node))
+            || (
+              !componentUtil.isES5Component(list[component].node, context)
+              && !componentUtil.isES6Component(list[component].node, context)
+            )
           ) {
             return;
           }
diff --git a/lib/rules/state-in-constructor.js b/lib/rules/state-in-constructor.js
index a7c543d3..8f52fb0f 100644
--- a/lib/rules/state-in-constructor.js
+++ b/lib/rules/state-in-constructor.js
@@ -5,7 +5,8 @@
 
 'use strict';
 
-const Components = require('../util/Components');
+const astUtil = require('../util/ast');
+const componentUtil = require('../util/componentUtil');
 const docsUrl = require('../util/docsUrl');
 const report = require('../util/report');
 
@@ -34,7 +35,7 @@ module.exports = {
     }],
   },
 
-  create: Components.detect((context, components, utils) => {
+  create(context) {
     const option = context.options[0] || 'always';
     return {
       'ClassProperty, PropertyDefinition'(node) {
@@ -42,7 +43,7 @@ module.exports = {
           option === 'always'
           && !node.static
           && node.key.name === 'state'
-          && utils.getParentES6Component()
+          && componentUtil.getParentES6Component(context)
         ) {
           report(context, messages.stateInitConstructor, 'stateInitConstructor', {
             node,
@@ -52,9 +53,9 @@ module.exports = {
       AssignmentExpression(node) {
         if (
           option === 'never'
-          && utils.isStateMemberExpression(node.left)
-          && utils.inConstructor()
-          && utils.getParentES6Component()
+          && componentUtil.isStateMemberExpression(node.left)
+          && astUtil.inConstructor(context)
+          && componentUtil.getParentES6Component(context)
         ) {
           report(context, messages.stateInitClassProp, 'stateInitClassProp', {
             node,
@@ -62,5 +63,5 @@ module.exports = {
         }
       },
     };
-  }),
+  },
 };
diff --git a/lib/rules/static-property-placement.js b/lib/rules/static-property-placement.js
index 3fa15d97..3be4df5e 100644
--- a/lib/rules/static-property-placement.js
+++ b/lib/rules/static-property-placement.js
@@ -9,6 +9,7 @@ const fromEntries = require('object.fromentries');
 const Components = require('../util/Components');
 const docsUrl = require('../util/docsUrl');
 const astUtil = require('../util/ast');
+const componentUtil = require('../util/componentUtil');
 const propsUtil = require('../util/props');
 const report = require('../util/report');
 
@@ -141,7 +142,7 @@ module.exports = {
     // ----------------------------------------------------------------------
     return {
       'ClassProperty, PropertyDefinition'(node) {
-        if (!utils.getParentES6Component()) {
+        if (!componentUtil.getParentES6Component(context)) {
           return;
         }
 
@@ -160,7 +161,7 @@ module.exports = {
         const relatedComponent = utils.getRelatedComponent(node);
 
         // If the related component is not an ES6 component then skip this node
-        if (!relatedComponent || !utils.isES6Component(relatedComponent.node)) {
+        if (!relatedComponent || !componentUtil.isES6Component(relatedComponent.node, context)) {
           return;
         }
 
@@ -170,7 +171,7 @@ module.exports = {
 
       MethodDefinition(node) {
         // If the function is inside a class and is static getter then check if correctly positioned
-        if (utils.getParentES6Component() && node.static && node.kind === 'get') {
+        if (componentUtil.getParentES6Component(context) && node.static && node.kind === 'get') {
           // Report error if needed
           reportNodeIncorrectlyPositioned(node, STATIC_GETTER);
         }
diff --git a/lib/rules/void-dom-elements-no-children.js b/lib/rules/void-dom-elements-no-children.js
index 70f38b3f..df791aee 100644
--- a/lib/rules/void-dom-elements-no-children.js
+++ b/lib/rules/void-dom-elements-no-children.js
@@ -8,8 +8,8 @@
 
 const has = require('object.hasown/polyfill')();
 
-const Components = require('../util/Components');
 const docsUrl = require('../util/docsUrl');
+const isCreateElement = require('../util/isCreateElement');
 const report = require('../util/report');
 
 // ------------------------------------------------------------------------------
@@ -63,7 +63,7 @@ module.exports = {
     schema: [],
   },
 
-  create: Components.detect((context, components, utils) => ({
+  create: (context) => ({
     JSXElement(node) {
       const elementName = node.openingElement.name.name;
 
@@ -108,7 +108,7 @@ module.exports = {
         return;
       }
 
-      if (!utils.isCreateElement(node)) {
+      if (!isCreateElement(node, context)) {
         return;
       }
 
@@ -161,5 +161,5 @@ module.exports = {
         });
       }
     },
-  })),
+  }),
 };
diff --git a/lib/types.d.ts b/lib/types.d.ts
index 7e22f278..e13e2045 100644
--- a/lib/types.d.ts
+++ b/lib/types.d.ts
@@ -13,9 +13,7 @@ declare global {
   type JSXFragment = ASTNode;
   type JSXSpreadAttribute = ASTNode;
 
-  interface Context extends eslint.SourceCode {
-    getFirstTokens(node: estree.Node | ASTNode, options?: eslint.SourceCode.CursorWithCountOptions): eslint.AST.Token[];
-  }
+  type Context = eslint.Rule.RuleContext
 
   type TypeDeclarationBuilder = (annotation: ASTNode, parentName: string, seen: Set<typeof annotation>) => object;
 
diff --git a/lib/util/Components.js b/lib/util/Components.js
index 8db12a57..4b545fa7 100644
--- a/lib/util/Components.js
+++ b/lib/util/Components.js
@@ -5,7 +5,6 @@
 
 'use strict';
 
-const doctrine = require('doctrine');
 const arrayIncludes = require('array-includes');
 const fromEntries = require('object.fromentries');
 const values = require('object.values');
@@ -13,12 +12,12 @@ const values = require('object.values');
 const variableUtil = require('./variable');
 const pragmaUtil = require('./pragma');
 const astUtil = require('./ast');
+const componentUtil = require('./componentUtil');
 const propTypesUtil = require('./propTypes');
 const jsxUtil = require('./jsx');
 const usedPropTypesUtil = require('./usedPropTypes');
 const defaultPropsUtil = require('./defaultProps');
 const isFirstLetterCapitalized = require('./isFirstLetterCapitalized');
-const isCreateElement = require('./isCreateElement');
 const isDestructuredFromPragmaImport = require('./isDestructuredFromPragmaImport');
 
 function getId(node) {
@@ -249,7 +248,6 @@ function getWrapperFunctions(context, pragma) {
 }
 
 function componentRule(rule, context) {
-  const createClass = pragmaUtil.getCreateClassFromContext(context);
   const pragma = pragmaUtil.getFromContext(context);
   const sourceCode = context.getSourceCode();
   const components = new Components();
@@ -257,104 +255,6 @@ function componentRule(rule, context) {
 
   // Utilities for component detection
   const utils = {
-
-    /**
-     * Check if an ObjectExpression is a React ES5 component
-     *
-     * @param {ASTNode} node The AST node being checked.
-     * @returns {Boolean} True if the node is a React ES5 component, false if not
-     */
-    isES5Component(node) {
-      if (!node.parent || !node.parent.callee) {
-        return false;
-      }
-      const callee = node.parent.callee;
-      // React.createClass({})
-      if (callee.type === 'MemberExpression') {
-        return callee.object.name === pragma && callee.property.name === createClass;
-      }
-      // createClass({})
-      if (callee.type === 'Identifier') {
-        return callee.name === createClass;
-      }
-      return false;
-    },
-
-    /**
-     * Check if a class is a React ES6 component
-     *
-     * @param {ASTNode} node The AST node being checked.
-     * @returns {Boolean} True if the node is a React ES6 component, false if not
-     */
-    isES6Component(node) {
-      if (utils.isExplicitComponent(node)) {
-        return true;
-      }
-
-      if (!node.superClass) {
-        return false;
-      }
-      if (node.superClass.type === 'MemberExpression') {
-        return node.superClass.object.name === pragma
-          && /^(Pure)?Component$/.test(node.superClass.property.name);
-      }
-      if (node.superClass.type === 'Identifier') {
-        return /^(Pure)?Component$/.test(node.superClass.name);
-      }
-      return false;
-    },
-
-    /**
-     * Check if the node is explicitly declared as a descendant of a React Component
-     *
-     * @param {ASTNode} node The AST node being checked (can be a ReturnStatement or an ArrowFunctionExpression).
-     * @returns {Boolean} True if the node is explicitly declared as a descendant of a React Component, false if not
-     */
-    isExplicitComponent(node) {
-      let comment;
-      // Sometimes the passed node may not have been parsed yet by eslint, and this function call crashes.
-      // Can be removed when eslint sets "parent" property for all nodes on initial AST traversal: https://github.com/eslint/eslint-scope/issues/27
-      // eslint-disable-next-line no-warning-comments
-      // FIXME: Remove try/catch when https://github.com/eslint/eslint-scope/issues/27 is implemented.
-      try {
-        comment = sourceCode.getJSDocComment(node);
-      } catch (e) {
-        comment = null;
-      }
-
-      if (comment === null) {
-        return false;
-      }
-
-      let commentAst;
-      try {
-        commentAst = doctrine.parse(comment.value, {
-          unwrap: true,
-          tags: ['extends', 'augments'],
-        });
-      } catch (e) {
-        // handle a bug in the archived `doctrine`, see #2596
-        return false;
-      }
-
-      const relevantTags = commentAst.tags.filter((tag) => tag.name === 'React.Component' || tag.name === 'React.PureComponent');
-
-      return relevantTags.length > 0;
-    },
-
-    /**
-     * Checks to see if our component extends React.PureComponent
-     *
-     * @param {ASTNode} node The AST node being checked.
-     * @returns {Boolean} True if node extends React.PureComponent, false if not
-     */
-    isPureComponent(node) {
-      if (node.superClass) {
-        return new RegExp(`^(${pragma}\\.)?PureComponent$`).test(sourceCode.getText(node.superClass));
-      }
-      return false;
-    },
-
     /**
      * Check if variable is destructured from pragma import
      *
@@ -365,50 +265,16 @@ function componentRule(rule, context) {
       return isDestructuredFromPragmaImport(variable, context);
     },
 
-    /**
-     * Checks to see if node is called within createElement from pragma
-     *
-     * @param {ASTNode} node The AST node being checked.
-     * @returns {Boolean} True if createElement called from pragma
-     */
-    isCreateElement(node) {
-      return isCreateElement(node, context);
-    },
-
-    /**
-     * Check if we are in a class constructor
-     * @return {boolean} true if we are in a class constructor, false if not
-     */
-    inConstructor() {
-      let scope = context.getScope();
-      while (scope) {
-        if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') {
-          return true;
-        }
-        scope = scope.upper;
-      }
-      return false;
-    },
-
-    /**
-     * Determine if the node is MemberExpression of `this.state`
-     * @param {Object} node The node to process
-     * @returns {Boolean}
-     */
-    isStateMemberExpression(node) {
-      return node.type === 'MemberExpression' && node.object.type === 'ThisExpression' && node.property.name === 'state';
-    },
-
     isReturningJSX(ASTNode, strict) {
-      return jsxUtil.isReturningJSX(this.isCreateElement.bind(this), ASTNode, context, strict, true);
+      return jsxUtil.isReturningJSX(ASTNode, context, strict, true);
     },
 
     isReturningJSXOrNull(ASTNode, strict) {
-      return jsxUtil.isReturningJSX(this.isCreateElement.bind(this), ASTNode, context, strict);
+      return jsxUtil.isReturningJSX(ASTNode, context, strict);
     },
 
     isReturningOnlyNull(ASTNode) {
-      return jsxUtil.isReturningOnlyNull(this.isCreateElement.bind(this), ASTNode, context);
+      return jsxUtil.isReturningOnlyNull(ASTNode, context);
     },
 
     getPragmaComponentWrapper(node) {
@@ -532,46 +398,12 @@ function componentRule(rule, context) {
      */
     getParentComponent() {
       return (
-        utils.getParentES6Component()
-        || utils.getParentES5Component()
+        componentUtil.getParentES6Component(context)
+        || componentUtil.getParentES5Component(context)
         || utils.getParentStatelessComponent()
       );
     },
 
-    /**
-     * Get the parent ES5 component node from the current scope
-     *
-     * @returns {ASTNode} component node, null if we are not in a component
-     */
-    getParentES5Component() {
-      let scope = context.getScope();
-      while (scope) {
-        const node = scope.block && scope.block.parent && scope.block.parent.parent;
-        if (node && utils.isES5Component(node)) {
-          return node;
-        }
-        scope = scope.upper;
-      }
-      return null;
-    },
-
-    /**
-     * Get the parent ES6 component node from the current scope
-     *
-     * @returns {ASTNode} component node, null if we are not in a component
-     */
-    getParentES6Component() {
-      let scope = context.getScope();
-      while (scope && scope.type !== 'class') {
-        scope = scope.upper;
-      }
-      const node = scope && scope.block;
-      if (!node || !utils.isES6Component(node)) {
-        return null;
-      }
-      return node;
-    },
-
     /**
      * @param {ASTNode} node
      * @returns {boolean}
@@ -904,14 +736,14 @@ function componentRule(rule, context) {
     },
 
     ClassExpression(node) {
-      if (!utils.isES6Component(node)) {
+      if (!componentUtil.isES6Component(node, context)) {
         return;
       }
       components.add(node, 2);
     },
 
     ClassDeclaration(node) {
-      if (!utils.isES6Component(node)) {
+      if (!componentUtil.isES6Component(node, context)) {
         return;
       }
       components.add(node, 2);
@@ -926,7 +758,7 @@ function componentRule(rule, context) {
     },
 
     ObjectExpression(node) {
-      if (!utils.isES5Component(node)) {
+      if (!componentUtil.isES5Component(node, context)) {
         return;
       }
       components.add(node, 2);
diff --git a/lib/util/ast.js b/lib/util/ast.js
index 8001b416..d25cab46 100644
--- a/lib/util/ast.js
+++ b/lib/util/ast.js
@@ -241,6 +241,23 @@ function isClass(node) {
   return node.type === 'ClassDeclaration' || node.type === 'ClassExpression';
 }
 
+/**
+ * Check if we are in a class constructor
+ * @param {Context} context
+ * @return {boolean}
+ */
+function inConstructor(context) {
+  let scope = context.getScope();
+  while (scope) {
+    // @ts-ignore
+    if (scope.block && scope.block.parent && scope.block.parent.kind === 'constructor') {
+      return true;
+    }
+    scope = scope.upper;
+  }
+  return false;
+}
+
 /**
  * Removes quotes from around an identifier.
  * @param {string} string the identifier to strip
@@ -253,12 +270,12 @@ function stripQuotes(string) {
 /**
  * Retrieve the name of a key node
  * @param {Context} context The AST node with the key.
- * @param {ASTNode} node The AST node with the key.
+ * @param {any} node The AST node with the key.
  * @return {string | undefined} the name of the key
  */
 function getKeyValue(context, node) {
   if (node.type === 'ObjectTypeProperty') {
-    const tokens = context.getFirstTokens(node, 2);
+    const tokens = context.getSourceCode().getFirstTokens(node, 2);
     return (tokens[0].value === '+' || tokens[0].value === '-'
       ? tokens[1].value
       : stripQuotes(tokens[0].value)
@@ -397,6 +414,7 @@ module.exports = {
   isClass,
   isFunction,
   isFunctionLikeExpression,
+  inConstructor,
   isNodeFirstInLine,
   unwrapTSAsExpression,
   traverseReturns,
diff --git a/lib/util/componentUtil.js b/lib/util/componentUtil.js
new file mode 100644
index 00000000..35d54edc
--- /dev/null
+++ b/lib/util/componentUtil.js
@@ -0,0 +1,182 @@
+'use strict';
+
+const doctrine = require('doctrine');
+const pragmaUtil = require('./pragma');
+
+// eslint-disable-next-line valid-jsdoc
+/**
+ * @template {(_: object) => any} T
+ * @param {T} fn
+ * @returns {T}
+ */
+function memoize(fn) {
+  const cache = new WeakMap();
+  // @ts-ignore
+  return function memoizedFn(arg) {
+    const cachedValue = cache.get(arg);
+    if (cachedValue !== undefined) {
+      return cachedValue;
+    }
+    const v = fn(arg);
+    cache.set(arg, v);
+    return v;
+  };
+}
+
+const getPragma = memoize(pragmaUtil.getFromContext);
+const getCreateClass = memoize(pragmaUtil.getCreateClassFromContext);
+
+/**
+ * @param {ASTNode} node
+ * @param {Context} context
+ * @returns {boolean}
+ */
+function isES5Component(node, context) {
+  const pragma = getPragma(context);
+  const createClass = getCreateClass(context);
+
+  if (!node.parent || !node.parent.callee) {
+    return false;
+  }
+  const callee = node.parent.callee;
+  // React.createClass({})
+  if (callee.type === 'MemberExpression') {
+    return callee.object.name === pragma && callee.property.name === createClass;
+  }
+  // createClass({})
+  if (callee.type === 'Identifier') {
+    return callee.name === createClass;
+  }
+  return false;
+}
+
+/**
+ * Check if the node is explicitly declared as a descendant of a React Component
+ * @param {any} node
+ * @param {Context} context
+ * @returns {boolean}
+ */
+function isExplicitComponent(node, context) {
+  const sourceCode = context.getSourceCode();
+  let comment;
+  // Sometimes the passed node may not have been parsed yet by eslint, and this function call crashes.
+  // Can be removed when eslint sets "parent" property for all nodes on initial AST traversal: https://github.com/eslint/eslint-scope/issues/27
+  // eslint-disable-next-line no-warning-comments
+  // FIXME: Remove try/catch when https://github.com/eslint/eslint-scope/issues/27 is implemented.
+  try {
+    comment = sourceCode.getJSDocComment(node);
+  } catch (e) {
+    comment = null;
+  }
+
+  if (comment === null) {
+    return false;
+  }
+
+  let commentAst;
+  try {
+    commentAst = doctrine.parse(comment.value, {
+      unwrap: true,
+      tags: ['extends', 'augments'],
+    });
+  } catch (e) {
+    // handle a bug in the archived `doctrine`, see #2596
+    return false;
+  }
+
+  const relevantTags = commentAst.tags.filter((tag) => tag.name === 'React.Component' || tag.name === 'React.PureComponent');
+
+  return relevantTags.length > 0;
+}
+
+/**
+ * @param {ASTNode} node
+ * @param {Context} context
+ * @returns {boolean}
+ */
+function isES6Component(node, context) {
+  const pragma = getPragma(context);
+  if (isExplicitComponent(node, context)) {
+    return true;
+  }
+
+  if (!node.superClass) {
+    return false;
+  }
+  if (node.superClass.type === 'MemberExpression') {
+    return node.superClass.object.name === pragma
+          && /^(Pure)?Component$/.test(node.superClass.property.name);
+  }
+  if (node.superClass.type === 'Identifier') {
+    return /^(Pure)?Component$/.test(node.superClass.name);
+  }
+  return false;
+}
+
+/**
+ * Get the parent ES5 component node from the current scope
+ * @param {Context} context
+ * @returns {ASTNode|null}
+ */
+function getParentES5Component(context) {
+  let scope = context.getScope();
+  while (scope) {
+    // @ts-ignore
+    const node = scope.block && scope.block.parent && scope.block.parent.parent;
+    if (node && isES5Component(node, context)) {
+      return node;
+    }
+    scope = scope.upper;
+  }
+  return null;
+}
+
+/**
+ * Get the parent ES6 component node from the current scope
+ * @param {Context} context
+ * @returns {ASTNode | null}
+ */
+function getParentES6Component(context) {
+  let scope = context.getScope();
+  while (scope && scope.type !== 'class') {
+    scope = scope.upper;
+  }
+  const node = scope && scope.block;
+  if (!node || !isES6Component(node, context)) {
+    return null;
+  }
+  return node;
+}
+
+/**
+ * Checks if a component extends React.PureComponent
+ * @param {ASTNode} node
+ * @param {Context} context
+ * @returns {boolean}
+ */
+function isPureComponent(node, context) {
+  const pragma = getPragma(context);
+  const sourceCode = context.getSourceCode();
+  if (node.superClass) {
+    return new RegExp(`^(${pragma}\\.)?PureComponent$`).test(sourceCode.getText(node.superClass));
+  }
+  return false;
+}
+
+/**
+ * @param {ASTNode} node
+ * @returns {boolean}
+ */
+function isStateMemberExpression(node) {
+  return node.type === 'MemberExpression' && node.object.type === 'ThisExpression' && node.property.name === 'state';
+}
+
+module.exports = {
+  isES5Component,
+  isES6Component,
+  getParentES5Component,
+  getParentES6Component,
+  isExplicitComponent,
+  isPureComponent,
+  isStateMemberExpression,
+};
diff --git a/lib/util/defaultProps.js b/lib/util/defaultProps.js
index 65961cec..44b42454 100644
--- a/lib/util/defaultProps.js
+++ b/lib/util/defaultProps.js
@@ -6,6 +6,7 @@
 
 const fromEntries = require('object.fromentries');
 const astUtil = require('./ast');
+const componentUtil = require('./componentUtil');
 const propsUtil = require('./props');
 const variableUtil = require('./variable');
 const propWrapperUtil = require('./propWrapper');
@@ -171,7 +172,7 @@ module.exports = function defaultPropsInstructions(context, components, utils) {
       }
 
       // find component this propTypes/defaultProps belongs to
-      const component = components.get(utils.getParentES6Component());
+      const component = components.get(componentUtil.getParentES6Component(context));
       if (!component) {
         return;
       }
@@ -214,7 +215,7 @@ module.exports = function defaultPropsInstructions(context, components, utils) {
       }
 
       // find component this propTypes/defaultProps belongs to
-      const component = components.get(utils.getParentES6Component());
+      const component = components.get(componentUtil.getParentES6Component(context));
       if (!component) {
         return;
       }
@@ -240,7 +241,7 @@ module.exports = function defaultPropsInstructions(context, components, utils) {
     // });
     ObjectExpression(node) {
       // find component this propTypes/defaultProps belongs to
-      const component = utils.isES5Component(node) && components.get(node);
+      const component = componentUtil.isES5Component(node, context) && components.get(node);
       if (!component) {
         return;
       }
diff --git a/lib/util/jsx.js b/lib/util/jsx.js
index 46930f0b..55073bfe 100644
--- a/lib/util/jsx.js
+++ b/lib/util/jsx.js
@@ -7,6 +7,7 @@
 const elementType = require('jsx-ast-utils/elementType');
 
 const astUtil = require('./ast');
+const isCreateElement = require('./isCreateElement');
 const variableUtil = require('./variable');
 
 // See https://github.com/babel/babel/blob/ce420ba51c68591e057696ef43e028f41c6e04cd/packages/babel-types/src/validators/react/isCompatTag.js
@@ -85,15 +86,13 @@ function isWhiteSpaces(value) {
 /**
  * Check if the node is returning JSX or null
  *
- * @param {Function} isCreateElement Function to determine if a CallExpresion is
- *   a createElement one
  * @param {ASTNode} ASTnode The AST node being checked
  * @param {Context} context The context of `ASTNode`.
  * @param {Boolean} [strict] If true, in a ternary condition the node must return JSX in both cases
  * @param {Boolean} [ignoreNull] If true, null return values will be ignored
  * @returns {Boolean} True if the node is returning JSX or null, false if not
  */
-function isReturningJSX(isCreateElement, ASTnode, context, strict, ignoreNull) {
+function isReturningJSX(ASTnode, context, strict, ignoreNull) {
   const isJSXValue = (node) => {
     if (!node) {
       return false;
@@ -115,7 +114,7 @@ function isReturningJSX(isCreateElement, ASTnode, context, strict, ignoreNull) {
       case 'JSXFragment':
         return true;
       case 'CallExpression':
-        return isCreateElement(node);
+        return isCreateElement(node, context);
       case 'Literal':
         if (!ignoreNull && node.value === null) {
           return true;
@@ -144,13 +143,11 @@ function isReturningJSX(isCreateElement, ASTnode, context, strict, ignoreNull) {
 /**
  * Check if the node is returning only null values
  *
- * @param {Function} isCreateElement Function to determine if a CallExpresion is
- *   a createElement one
  * @param {ASTNode} ASTnode The AST node being checked
  * @param {Context} context The context of `ASTNode`.
  * @returns {Boolean} True if the node is returning only null values
  */
-function isReturningOnlyNull(isCreateElement, ASTnode, context) {
+function isReturningOnlyNull(ASTnode, context) {
   let found = false;
   let foundSomethingElse = false;
   astUtil.traverseReturns(ASTnode, context, (node) => {
diff --git a/lib/util/pragma.js b/lib/util/pragma.js
index 62c983c3..2bde47fb 100644
--- a/lib/util/pragma.js
+++ b/lib/util/pragma.js
@@ -9,6 +9,10 @@ const JSX_ANNOTATION_REGEX = /@jsx\s+([^\s]+)/;
 // Does not check for reserved keywords or unicode characters
 const JS_IDENTIFIER_REGEX = /^[_$a-zA-Z][_$a-zA-Z0-9]*$/;
 
+/**
+ * @param {Context} context
+ * @returns {string}
+ */
 function getCreateClassFromContext(context) {
   let pragma = 'createReactClass';
   // .eslintrc shared settings (https://eslint.org/docs/user-guide/configuring#adding-shared-settings)
@@ -21,6 +25,10 @@ function getCreateClassFromContext(context) {
   return pragma;
 }
 
+/**
+ * @param {Context} context
+ * @returns {string}
+ */
 function getFragmentFromContext(context) {
   let pragma = 'Fragment';
   // .eslintrc shared settings (https://eslint.org/docs/user-guide/configuring#adding-shared-settings)
@@ -33,6 +41,10 @@ function getFragmentFromContext(context) {
   return pragma;
 }
 
+/**
+ * @param {Context} context
+ * @returns {string}
+ */
 function getFromContext(context) {
   let pragma = 'React';
 
diff --git a/lib/util/usedPropTypes.js b/lib/util/usedPropTypes.js
index 3f384457..f20c22bb 100644
--- a/lib/util/usedPropTypes.js
+++ b/lib/util/usedPropTypes.js
@@ -5,6 +5,7 @@
 'use strict';
 
 const astUtil = require('./ast');
+const componentUtil = require('./componentUtil');
 const testReactVersion = require('./version').testReactVersion;
 const ast = require('./ast');
 
@@ -181,8 +182,12 @@ function isPropArgumentInSetStateUpdater(context, name) {
   return false;
 }
 
-function isInClassComponent(utils) {
-  return utils.getParentES6Component() || utils.getParentES5Component();
+/**
+ * @param {Context} context
+ * @returns {boolean}
+ */
+function isInClassComponent(context) {
+  return !!(componentUtil.getParentES6Component(context) || componentUtil.getParentES5Component(context));
 }
 
 /**
@@ -219,7 +224,7 @@ function hasSpreadOperator(context, node) {
 function isPropTypesUsageByMemberExpression(node, context, utils, checkAsyncSafeLifeCycles) {
   const unwrappedObjectNode = ast.unwrapTSAsExpression(node.object);
 
-  if (isInClassComponent(utils)) {
+  if (isInClassComponent(context)) {
     // this.props.*
     if (isThisDotProps(unwrappedObjectNode)) {
       return true;
@@ -227,7 +232,7 @@ function isPropTypesUsageByMemberExpression(node, context, utils, checkAsyncSafe
     // props.* or prevProps.* or nextProps.*
     if (
       isCommonVariableNameForProps(unwrappedObjectNode.name)
-      && (inLifeCycleMethod(context, checkAsyncSafeLifeCycles) || utils.inConstructor())
+      && (inLifeCycleMethod(context, checkAsyncSafeLifeCycles) || astUtil.inConstructor(context))
     ) {
       return true;
     }
@@ -465,7 +470,7 @@ module.exports = function usedPropTypesInstructions(context, components, utils)
       const unwrappedInitNode = ast.unwrapTSAsExpression(node.init);
 
       // let props = this.props
-      if (isThisDotProps(unwrappedInitNode) && isInClassComponent(utils) && node.id.type === 'Identifier') {
+      if (isThisDotProps(unwrappedInitNode) && isInClassComponent(context) && node.id.type === 'Identifier') {
         propVariables.set(node.id.name, []);
       }
 
@@ -501,7 +506,7 @@ module.exports = function usedPropTypesInstructions(context, components, utils)
       }
 
       // let {firstname} = this.props
-      if (isThisDotProps(unwrappedInitNode) && isInClassComponent(utils)) {
+      if (isThisDotProps(unwrappedInitNode) && isInClassComponent(context)) {
         markPropTypesAsUsed(node.id);
         return;
       }


Test patch:
diff --git a/tests/util/jsx.js b/tests/util/jsx.js
index 6aef83e0..c8009fc0 100644
--- a/tests/util/jsx.js
+++ b/tests/util/jsx.js
@@ -34,7 +34,7 @@ const mockContext = {
 describe('jsxUtil', () => {
   describe('isReturningJSX', () => {
     const assertValid = (codeStr) => assert(
-      isReturningJSX(() => false, parseCode(codeStr), mockContext)
+      isReturningJSX(parseCode(codeStr), mockContext)
     );
 
     it('Works when returning JSX', () => {