@@ -107,6 +107,127 @@ def is_inside_string(code: str, pos: int) -> bool:
107107 return in_string
108108
109109
110+ class JsxRenderCallTransformer :
111+ """Transforms render(<ComponentName>...) calls in React test code.
112+
113+ React components are invoked via JSX in tests, not as direct function calls.
114+ Tests use render(<Component />) from @testing-library/react. This transformer
115+ wraps those render() calls with codeflash instrumentation.
116+
117+ Examples:
118+ - render(<Comp />) -> codeflash.capturePerf('Comp', '1', () => render(<Comp />))
119+ - render(<Comp>...</Comp>, opts) -> codeflash.capturePerf('Comp', '1', () => render(<Comp>...</Comp>, opts))
120+ """
121+
122+ def __init__ (self , function_to_optimize : FunctionToOptimize , capture_func : str ) -> None :
123+ self .function_to_optimize = function_to_optimize
124+ self .func_name = function_to_optimize .function_name
125+ self .qualified_name = function_to_optimize .qualified_name
126+ self .capture_func = capture_func
127+ self .invocation_counter = 0
128+ # Match render( followed by JSX containing the component name
129+ # Captures: (whitespace)(await )?render(
130+ self ._render_pattern = re .compile (
131+ rf"(\s*)(await\s+)?render\s*\(\s*<\s*{ re .escape (self .func_name )} [\s>/]"
132+ )
133+
134+ def transform (self , code : str ) -> str :
135+ """Transform all render(<Component>) calls in the code."""
136+ result : list [str ] = []
137+ pos = 0
138+
139+ while pos < len (code ):
140+ match = self ._render_pattern .search (code , pos )
141+ if not match :
142+ result .append (code [pos :])
143+ break
144+
145+ # Skip if inside a string literal
146+ if is_inside_string (code , match .start ()):
147+ result .append (code [pos : match .end ()])
148+ pos = match .end ()
149+ continue
150+
151+ # Skip if already wrapped with codeflash
152+ lookback = code [max (0 , match .start () - 60 ) : match .start ()]
153+ if f"codeflash.{ self .capture_func } (" in lookback :
154+ result .append (code [pos : match .end ()])
155+ pos = match .end ()
156+ continue
157+
158+ # Add everything before the match
159+ result .append (code [pos : match .start ()])
160+
161+ leading_ws = match .group (1 )
162+ prefix = match .group (2 ) or "" # "await " or ""
163+
164+ # Find the render( opening paren
165+ render_call_text = code [match .start ():]
166+ render_paren_offset = render_call_text .index ("(" )
167+ open_paren_pos = match .start () + render_paren_offset
168+
169+ # Find the matching closing paren of render(...)
170+ close_pos = self ._find_matching_paren (code , open_paren_pos )
171+ if close_pos == - 1 :
172+ # Can't find matching paren, skip
173+ result .append (code [match .start () : match .end ()])
174+ pos = match .end ()
175+ continue
176+
177+ # Extract the full render(...) arguments
178+ render_args = code [open_paren_pos + 1 : close_pos - 1 ]
179+
180+ # Check for trailing semicolon
181+ end_pos = close_pos
182+ while end_pos < len (code ) and code [end_pos ] in " \t " :
183+ end_pos += 1
184+ has_semicolon = end_pos < len (code ) and code [end_pos ] == ";"
185+ if has_semicolon :
186+ end_pos += 1
187+
188+ self .invocation_counter += 1
189+ line_id = str (self .invocation_counter )
190+ semicolon = ";" if has_semicolon else ""
191+
192+ # Wrap render(...) in a lambda: codeflash.capturePerf('name', 'id', () => render(...))
193+ transformed = (
194+ f"{ leading_ws } { prefix } codeflash.{ self .capture_func } ('{ self .qualified_name } ', "
195+ f"'{ line_id } ', () => render({ render_args } )){ semicolon } "
196+ )
197+ result .append (transformed )
198+ pos = end_pos
199+
200+ return "" .join (result )
201+
202+ def _find_matching_paren (self , code : str , open_paren_pos : int ) -> int :
203+ """Find the position after the closing paren for the given opening paren."""
204+ if open_paren_pos >= len (code ) or code [open_paren_pos ] != "(" :
205+ return - 1
206+
207+ depth = 1
208+ pos = open_paren_pos + 1
209+ in_string = False
210+ string_char = None
211+
212+ while pos < len (code ) and depth > 0 :
213+ char = code [pos ]
214+ if char in "\" '`" and (pos == 0 or code [pos - 1 ] != "\\ " ):
215+ if not in_string :
216+ in_string = True
217+ string_char = char
218+ elif char == string_char :
219+ in_string = False
220+ string_char = None
221+ elif not in_string :
222+ if char == "(" :
223+ depth += 1
224+ elif char == ")" :
225+ depth -= 1
226+ pos += 1
227+
228+ return pos if depth == 0 else - 1
229+
230+
110231class StandaloneCallTransformer :
111232 """Transforms standalone func(...) calls in JavaScript test code.
112233
@@ -467,6 +588,31 @@ def transform_standalone_calls(
467588 return result , transformer .invocation_counter
468589
469590
591+ def transform_jsx_render_calls (
592+ code : str , function_to_optimize : FunctionToOptimize , capture_func : str , start_counter : int = 0
593+ ) -> tuple [str , int ]:
594+ """Transform render(<Component>...) calls in React test code.
595+
596+ This handles React components that are invoked via JSX rather than direct function
597+ calls. When a component is used as <ComponentName> in a render() call, this wraps
598+ the entire render() with codeflash instrumentation.
599+
600+ Args:
601+ code: The test code to transform.
602+ function_to_optimize: The React component being tested.
603+ capture_func: The capture function to use ('capture' or 'capturePerf').
604+ start_counter: Starting value for the invocation counter.
605+
606+ Returns:
607+ Tuple of (transformed code, final counter value).
608+
609+ """
610+ transformer = JsxRenderCallTransformer (function_to_optimize = function_to_optimize , capture_func = capture_func )
611+ transformer .invocation_counter = start_counter
612+ result = transformer .transform (code )
613+ return result , transformer .invocation_counter
614+
615+
470616class ExpectCallTransformer :
471617 """Transforms expect(func(...)).assertion() calls in JavaScript test code.
472618
@@ -1038,6 +1184,21 @@ def inject_profiling_into_existing_js_test(
10381184 return True , instrumented_code
10391185
10401186
1187+ def _is_jsx_component_usage (code : str , func_name : str ) -> bool :
1188+ """Check if a function is used as a JSX component in render() calls.
1189+
1190+ Returns True if the code contains patterns like render(<FuncName ...) or
1191+ <FuncName> or <FuncName />, indicating the function is a React component
1192+ rendered via JSX rather than called directly.
1193+ """
1194+ # Check for JSX usage: <ComponentName or <ComponentName> or <ComponentName />
1195+ jsx_pattern = rf"<\s*{ re .escape (func_name )} [\s>/]"
1196+ if not re .search (jsx_pattern , code ):
1197+ return False
1198+ # Also verify there's a render() call (from @testing-library/react or similar)
1199+ return bool (re .search (r"\brender\s*\(" , code ))
1200+
1201+
10411202def _is_function_used_in_test (code : str , func_name : str ) -> bool :
10421203 """Check if a function is imported or used in the test code.
10431204
@@ -1122,6 +1283,8 @@ def _instrument_js_test_code(
11221283 # Choose capture function based on mode
11231284 capture_func = "capturePerf" if mode == TestingMode .PERFORMANCE else "capture"
11241285
1286+ # Save code state before transforms to detect if any calls were instrumented
1287+ code_before_transforms = code
11251288 # Transform React render calls: render(React.createElement(Component, ...))
11261289 # Do this first so expect/standalone transforms don't interfere with render patterns
11271290 code , render_counter = transform_render_calls (
@@ -1140,10 +1303,21 @@ def _instrument_js_test_code(
11401303
11411304 # Transform standalone calls (not inside expect wrappers)
11421305 # Continue counter from expect transformer to ensure unique IDs
1143- code , _final_counter = transform_standalone_calls (
1306+ code , final_counter = transform_standalone_calls (
11441307 code = code , function_to_optimize = function_to_optimize , capture_func = capture_func , start_counter = expect_counter
11451308 )
11461309
1310+ # If no direct function calls were instrumented, check for JSX usage (React components).
1311+ # React components are invoked via JSX (<Component />) in render() calls, not as
1312+ # direct function calls. Detect this pattern and wrap render() calls instead.
1313+ if code == code_before_transforms and _is_jsx_component_usage (code_before_transforms , function_to_optimize .function_name ):
1314+ code , _jsx_counter = transform_jsx_render_calls (
1315+ code = code ,
1316+ function_to_optimize = function_to_optimize ,
1317+ capture_func = capture_func ,
1318+ start_counter = final_counter ,
1319+ )
1320+
11471321 return code
11481322
11491323
0 commit comments