From ec61f187fe39b0aa8ec6b508f2553b2047dc30cc Mon Sep 17 00:00:00 2001 From: BIKI DAS <72331432+Biki-das@users.noreply.github.com> Date: Thu, 6 Aug 2026 18:14:04 +0530 Subject: [PATCH] DevTools: Fix nested HOC name extraction in extractHOCNames (#37215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary There was a bug in the helper that unwraps component names like Forget(Memo(Button)) into a base component name plus its HOC wrappers. The regex was using the g flag, which means exec() remembers its position via lastIndex. Since each iteration replaces the current string with the shorter unwrapped inner string, lastIndex ends up pointing past the end of the new string. The next exec() returns null, so the loop stops after unwrapping only the outermost HOC. component named Forget(Memo(ForgetMemoCounter)) before fixes ✨Memo(ForgetMemoCounter) after fixes ✨🧠ForgetMemoCounter component named Forget(ForwardRef(ForgetForwardRefCounter)) before fixes ✨ForwardRef(ForgetForwardRefCounter) after fixes ✨ForgetForwardRefCounter ## How did you test this change? Tested the change locally in `devtool` and added tests for the same **Before** devtools-hoc-BEFORE-buggy **After** devtools-hoc-AFTER-fixed Not sure if we need a new fixture can add one if required --- .../src/__tests__/extractHOCNames-test.js | 72 +++++++++++++++++++ .../src/backend/views/utils.js | 2 +- 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 packages/react-devtools-shared/src/__tests__/extractHOCNames-test.js diff --git a/packages/react-devtools-shared/src/__tests__/extractHOCNames-test.js b/packages/react-devtools-shared/src/__tests__/extractHOCNames-test.js new file mode 100644 index 000000000000..810cccb2b688 --- /dev/null +++ b/packages/react-devtools-shared/src/__tests__/extractHOCNames-test.js @@ -0,0 +1,72 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import {extractHOCNames} from 'react-devtools-shared/src/backend/views/utils'; + +describe('extractHOCNames', () => { + it('should return an empty result for an empty display name', () => { + expect(extractHOCNames('')).toEqual({ + baseComponentName: '', + hocNames: [], + }); + }); + + it('should not extract anything from an unwrapped component', () => { + expect(extractHOCNames('Button')).toEqual({ + baseComponentName: 'Button', + hocNames: [], + }); + }); + + it('should extract a single wrapper', () => { + expect(extractHOCNames('Memo(Button)')).toEqual({ + baseComponentName: 'Button', + hocNames: ['Memo'], + }); + }); + + it('should extract every wrapper of a nested display name', () => { + expect(extractHOCNames('Memo(ForwardRef(Button))')).toEqual({ + baseComponentName: 'Button', + hocNames: ['Memo', 'ForwardRef'], + }); + }); + + it('should extract wrappers nested more than two levels deep', () => { + expect(extractHOCNames('Memo(Forget(ForwardRef(Button)))')).toEqual({ + baseComponentName: 'Button', + hocNames: ['Memo', 'Forget', 'ForwardRef'], + }); + }); + + it('should extract lowercase wrapper names verbatim', () => { + expect(extractHOCNames('withRouter(Button)')).toEqual({ + baseComponentName: 'Button', + hocNames: ['withRouter'], + }); + }); + + it('should extract a mix of lowercase and uppercase wrappers', () => { + expect(extractHOCNames('connect(Memo(Button))')).toEqual({ + baseComponentName: 'Button', + hocNames: ['connect', 'Memo'], + }); + }); + + it('should not extract from a display name that is not shaped like a wrapper', () => { + expect(extractHOCNames('Foo (bar)')).toEqual({ + baseComponentName: 'Foo (bar)', + hocNames: [], + }); + expect(extractHOCNames('Memo(Button) extra')).toEqual({ + baseComponentName: 'Memo(Button) extra', + hocNames: [], + }); + }); +}); diff --git a/packages/react-devtools-shared/src/backend/views/utils.js b/packages/react-devtools-shared/src/backend/views/utils.js index 55c5a2f14eb3..145ba87f31ed 100644 --- a/packages/react-devtools-shared/src/backend/views/utils.js +++ b/packages/react-devtools-shared/src/backend/views/utils.js @@ -149,7 +149,7 @@ export function extractHOCNames(displayName: string): { } { if (!displayName) return {baseComponentName: '', hocNames: []}; - const hocRegex = /([A-Z][a-zA-Z0-9]*?)\((.*)\)/g; + const hocRegex = /^([A-Za-z_$][A-Za-z0-9_$]*)\((.*)\)$/; const hocNames: string[] = []; let baseComponentName = displayName; let match;