@samitouri / QOS-React-1 / commits / ec61f187fe

DevTools: Fix nested HOC name extraction in extractHOCNames (#37215)

## 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** <img width="1920" height="690" alt="devtools-hoc-BEFORE-buggy" src="https://github.com/user-attachments/assets/14e7e632-da97-43fb-867b-9da3b9d7cb22" /> **After** <img width="1920" height="690" alt="devtools-hoc-AFTER-fixed" src="https://github.com/user-attachments/assets/4fe91b78-af11-4584-986b-b7aa9a03d0b6" /> Not sure if we need a new fixture can add one if required

BIKI DAS committed Aug 6, 2026 at 18:14 UTC ec61f187fe39b0aa8ec6b508f2553b2047dc30cc
2 files changed +73 -1
packages/react-devtools-shared/src/__tests__/extractHOCNames-test.js new
+72
@@ -0,0 +1,72 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import {extractHOCNames} from 'react-devtools-shared/src/backend/views/utils';
11 +
12 +describe('extractHOCNames', () => {
13 + it('should return an empty result for an empty display name', () => {
14 + expect(extractHOCNames('')).toEqual({
15 + baseComponentName: '',
16 + hocNames: [],
17 + });
18 + });
19 +
20 + it('should not extract anything from an unwrapped component', () => {
21 + expect(extractHOCNames('Button')).toEqual({
22 + baseComponentName: 'Button',
23 + hocNames: [],
24 + });
25 + });
26 +
27 + it('should extract a single wrapper', () => {
28 + expect(extractHOCNames('Memo(Button)')).toEqual({
29 + baseComponentName: 'Button',
30 + hocNames: ['Memo'],
31 + });
32 + });
33 +
34 + it('should extract every wrapper of a nested display name', () => {
35 + expect(extractHOCNames('Memo(ForwardRef(Button))')).toEqual({
36 + baseComponentName: 'Button',
37 + hocNames: ['Memo', 'ForwardRef'],
38 + });
39 + });
40 +
41 + it('should extract wrappers nested more than two levels deep', () => {
42 + expect(extractHOCNames('Memo(Forget(ForwardRef(Button)))')).toEqual({
43 + baseComponentName: 'Button',
44 + hocNames: ['Memo', 'Forget', 'ForwardRef'],
45 + });
46 + });
47 +
48 + it('should extract lowercase wrapper names verbatim', () => {
49 + expect(extractHOCNames('withRouter(Button)')).toEqual({
50 + baseComponentName: 'Button',
51 + hocNames: ['withRouter'],
52 + });
53 + });
54 +
55 + it('should extract a mix of lowercase and uppercase wrappers', () => {
56 + expect(extractHOCNames('connect(Memo(Button))')).toEqual({
57 + baseComponentName: 'Button',
58 + hocNames: ['connect', 'Memo'],
59 + });
60 + });
61 +
62 + it('should not extract from a display name that is not shaped like a wrapper', () => {
63 + expect(extractHOCNames('Foo (bar)')).toEqual({
64 + baseComponentName: 'Foo (bar)',
65 + hocNames: [],
66 + });
67 + expect(extractHOCNames('Memo(Button) extra')).toEqual({
68 + baseComponentName: 'Memo(Button) extra',
69 + hocNames: [],
70 + });
71 + });
72 +});
packages/react-devtools-shared/src/backend/views/utils.js
+1 -1
@@ -149,7 +149,7 @@ export function extractHOCNames(displayName: string): {
149 } {
150 if (!displayName) return {baseComponentName: '', hocNames: []};
151
152 - const hocRegex = /([A-Z][a-zA-Z0-9]*?)\((.*)\)/g;
152 + const hocRegex = /^([A-Za-z_$][A-Za-z0-9_$]*)\((.*)\)$/;
153 const hocNames: string[] = [];
154 let baseComponentName = displayName;
155 let match;