@@ -157,7 +157,7 @@ export default async function transform(
157157 }
158158
159159 if (
160- ! isFunctionAlreadyAsync ( containingFunction , rootNode ) &&
160+ ! isFunctionAlreadyAsync ( containingFunction ) &&
161161 ! functionsToMakeAsync . has ( containingFunction . id ( ) )
162162 ) {
163163 functionsToMakeAsync . set ( containingFunction . id ( ) , containingFunction ) ;
@@ -179,7 +179,7 @@ export default async function transform(
179179 }
180180
181181 if (
182- ! isFunctionAlreadyAsync ( containingFunction , rootNode ) &&
182+ ! isFunctionAlreadyAsync ( containingFunction ) &&
183183 ! functionsToMakeAsync . has ( containingFunction . id ( ) )
184184 ) {
185185 functionsToMakeAsync . set ( containingFunction . id ( ) , containingFunction ) ;
@@ -207,7 +207,15 @@ export default async function transform(
207207 return rootNode . commitEdits ( edits ) ;
208208}
209209
210- function parseCustomRenderFunctionsFromOptions ( options : any ) : Set < string > {
210+ interface CodemodOptions {
211+ params ?: {
212+ customRenderFunctions ?: string | number | boolean ;
213+ } ;
214+ }
215+
216+ function parseCustomRenderFunctionsFromOptions (
217+ options ?: CodemodOptions ,
218+ ) : Set < string > {
211219 const customRenderFunctionsParam = options ?. params ?. customRenderFunctions
212220 ? String ( options . params . customRenderFunctions )
213221 : '' ;
@@ -319,6 +327,17 @@ function extractImportedFunctionNames(
319327 return { importedFunctions, specifiersToRemove } ;
320328}
321329
330+ /**
331+ * Removes duplicate import specifiers from import statements.
332+ * Handles complex comma placement scenarios when removing specifiers:
333+ * - Trailing commas after the specifier
334+ * - Leading commas before the specifier
335+ * - Edge cases with single vs multiple specifiers
336+ *
337+ * @param specifiersToRemove - Array of specifiers to remove with their import statements
338+ * @param rootNode - The root AST node (used to get full text for comma detection)
339+ * @param edits - Array to collect edit operations
340+ */
322341function removeDuplicateImportSpecifiers (
323342 specifiersToRemove : Array < { specifier : SgNode < TSX > ; importStmt : SgNode < TSX > } > ,
324343 rootNode : SgNode < TSX > ,
@@ -493,7 +512,8 @@ function findFireEventMethodCalls(
493512 }
494513 }
495514 } catch {
496- // Skip if field() is not available
515+ // Skip nodes where field() is not available or AST structure doesn't match expectations.
516+ // This is expected for malformed or edge-case AST structures and should be silently ignored.
497517 }
498518 }
499519 }
@@ -528,14 +548,28 @@ function findScreenMethodCalls(rootNode: SgNode<TSX>): SgNode<TSX>[] {
528548 }
529549 }
530550 } catch {
531- // Skip if field() is not available
551+ // Skip nodes where field() is not available or AST structure doesn't match expectations.
552+ // This is expected for malformed or edge-case AST structures and should be silently ignored.
532553 }
533554 }
534555 }
535556
536557 return functionCalls ;
537558}
538559
560+ /**
561+ * Tracks variables assigned from render() calls to identify renderer result objects.
562+ * This helps identify calls like `renderer.rerender()` or `renderer.unmount()` that need to be made async.
563+ *
564+ * Handles various assignment patterns:
565+ * - Direct assignment: `const renderer = render(...)`
566+ * - Destructured assignment: `const { rerender } = render(...)`
567+ * - Assignment expressions: `renderer = render(...)`
568+ *
569+ * @param rootNode - The root AST node to search within
570+ * @param importedFunctions - Set of imported function names (must include 'render')
571+ * @returns Set of variable names that represent renderer results
572+ */
539573function trackVariablesAssignedFromRender (
540574 rootNode : SgNode < TSX > ,
541575 importedFunctions : Set < string > ,
@@ -646,7 +680,8 @@ function findRendererMethodCalls(
646680 }
647681 }
648682 } catch {
649- // Skip if field() is not available
683+ // Skip nodes where field() is not available or AST structure doesn't match expectations.
684+ // This is expected for malformed or edge-case AST structures and should be silently ignored.
650685 }
651686 }
652687 }
@@ -671,6 +706,21 @@ function findRendererMethodCalls(
671706 return functionCalls ;
672707}
673708
709+ /**
710+ * Tracks variables assigned from renderHook() calls to identify hook result objects.
711+ * Similar to trackVariablesAssignedFromRender but handles renderHook-specific patterns.
712+ *
713+ * Handles various assignment patterns:
714+ * - Direct assignment: `const result = renderHook(...)`
715+ * - Destructured assignment: `const { rerender, unmount } = renderHook(...)`
716+ * - Renamed destructuring: `const { rerender: rerenderHook } = renderHook(...)`
717+ *
718+ * @param rootNode - The root AST node to search within
719+ * @param importedFunctions - Set of imported function names (must include 'renderHook')
720+ * @returns Object containing:
721+ * - renderHookVariables: Set of all variable names representing hook results
722+ * - renderHookMethodVariables: Set of renamed method variables (e.g., rerenderHook)
723+ */
674724function trackVariablesAssignedFromRenderHook (
675725 rootNode : SgNode < TSX > ,
676726 importedFunctions : Set < string > ,
@@ -824,7 +874,8 @@ function findRenderHookMethodCalls(
824874 }
825875 }
826876 } catch {
827- // Skip if field() is not available
877+ // Skip nodes where field() is not available or AST structure doesn't match expectations.
878+ // This is expected for malformed or edge-case AST structures and should be silently ignored.
828879 }
829880 }
830881 }
@@ -849,6 +900,20 @@ function findRenderHookMethodCalls(
849900 return functionCalls ;
850901}
851902
903+ /**
904+ * Automatically detects custom render functions by analyzing the code structure.
905+ * A custom render function is identified as:
906+ * - A function/const that starts with 'render' (e.g., renderWithProviders, renderWithTheme)
907+ * - Called from within test functions
908+ * - Contains calls to the base render() function from RNTL
909+ * - Defined at the top level (not nested inside other functions)
910+ *
911+ * This helps identify custom render wrappers that should be transformed.
912+ *
913+ * @param rootNode - The root AST node to search within
914+ * @param importedFunctions - Set of imported function names (must include 'render')
915+ * @returns Set of custom render function names that were auto-detected
916+ */
852917function findAutoDetectedCustomRenderFunctions (
853918 rootNode : SgNode < TSX > ,
854919 importedFunctions : Set < string > ,
@@ -1116,7 +1181,8 @@ function findRNTLFunctionCallsInNode(
11161181 }
11171182 }
11181183 } catch {
1119- // Skip if field() is not available
1184+ // Skip nodes where field() is not available or AST structure doesn't match expectations.
1185+ // This is expected for malformed or edge-case AST structures and should be silently ignored.
11201186 }
11211187 }
11221188 }
@@ -1151,7 +1217,7 @@ function transformRNTLCallsInsideCustomRender(
11511217 }
11521218
11531219 if ( needsAsync && ! customRenderFunctionsToMakeAsync . has ( funcNode . id ( ) ) ) {
1154- const isAsync = isFunctionAlreadyAsync ( funcNode , rootNode ) ;
1220+ const isAsync = isFunctionAlreadyAsync ( funcNode ) ;
11551221 if ( ! isAsync ) {
11561222 customRenderFunctionsToMakeAsync . set ( funcNode . id ( ) , funcNode ) ;
11571223 }
@@ -1172,15 +1238,30 @@ function addAwaitBeforeCall(functionCall: SgNode<TSX>, edits: Edit[]): void {
11721238 } ) ;
11731239}
11741240
1175- function isFunctionAlreadyAsync ( func : SgNode < TSX > , rootNode : SgNode < TSX > ) : boolean {
1241+ /**
1242+ * Checks if a function is already marked as async using AST-based detection.
1243+ * This is more reliable than string matching and handles edge cases better.
1244+ */
1245+ function isFunctionAlreadyAsync ( func : SgNode < TSX > ) : boolean {
11761246 if ( func . is ( 'arrow_function' ) ) {
1247+ // For arrow functions, check if 'async' is a direct child
11771248 const children = func . children ( ) ;
11781249 return children . some ( ( child ) => child . text ( ) === 'async' ) ;
1179- } else {
1180- const funcStart = func . range ( ) . start . index ;
1181- const textBefore = rootNode . text ( ) . substring ( Math . max ( 0 , funcStart - 10 ) , funcStart ) ;
1182- return textBefore . trim ( ) . endsWith ( 'async' ) ;
1250+ } else if ( func . is ( 'function_declaration' ) || func . is ( 'function_expression' ) ) {
1251+ // For function declarations/expressions, check for async modifier
1252+ // The async keyword appears before the 'function' keyword
1253+ const children = func . children ( ) ;
1254+ const functionKeywordIndex = children . findIndex ( ( child ) => child . text ( ) === 'function' ) ;
1255+ if ( functionKeywordIndex > 0 ) {
1256+ // Check if any child before 'function' is 'async'
1257+ return children
1258+ . slice ( 0 , functionKeywordIndex )
1259+ . some ( ( child ) => child . text ( ) === 'async' ) ;
1260+ }
1261+ // Also check if the first child is 'async'
1262+ return children . length > 0 && children [ 0 ] . text ( ) === 'async' ;
11831263 }
1264+ return false ;
11841265}
11851266
11861267function addAsyncKeywordToFunction ( func : SgNode < TSX > , edits : Edit [ ] ) : void {
@@ -1212,6 +1293,19 @@ function addAsyncKeywordToFunction(func: SgNode<TSX>, edits: Edit[]): void {
12121293 }
12131294}
12141295
1296+ /**
1297+ * Finds the containing test function (test, it, beforeEach, etc.) for a given node.
1298+ * Traverses up the AST tree to find the nearest test function that contains the node.
1299+ *
1300+ * Handles various test patterns:
1301+ * - Direct test functions: test(), it()
1302+ * - Test modifiers: test.skip(), it.only()
1303+ * - Test.each patterns: test.each(), it.each()
1304+ * - Hooks: beforeEach(), afterEach(), beforeAll(), afterAll()
1305+ *
1306+ * @param node - The AST node to find the containing test function for
1307+ * @returns The containing test function node, or null if not found
1308+ */
12151309function findContainingTestFunction ( node : SgNode < TSX > ) : SgNode < TSX > | null {
12161310 let current : SgNode < TSX > | null = node ;
12171311
@@ -1244,7 +1338,8 @@ function findContainingTestFunction(node: SgNode<TSX>): SgNode<TSX> | null {
12441338 }
12451339 }
12461340 } catch {
1247- // Skip if field() is not available
1341+ // Skip nodes where field() is not available or AST structure doesn't match expectations.
1342+ // This is expected for malformed or edge-case AST structures and should be silently ignored.
12481343 }
12491344 }
12501345 if ( funcNode . is ( 'call_expression' ) ) {
@@ -1262,7 +1357,8 @@ function findContainingTestFunction(node: SgNode<TSX>): SgNode<TSX> | null {
12621357 }
12631358 }
12641359 } catch {
1265- // Skip if field() is not available
1360+ // Skip nodes where field() is not available or AST structure doesn't match expectations.
1361+ // This is expected for malformed or edge-case AST structures and should be silently ignored.
12661362 }
12671363 }
12681364 }
@@ -1287,7 +1383,8 @@ function findContainingTestFunction(node: SgNode<TSX>): SgNode<TSX> | null {
12871383 }
12881384 }
12891385 } catch {
1290- // Skip if field() is not available
1386+ // Skip nodes where field() is not available or AST structure doesn't match expectations.
1387+ // This is expected for malformed or edge-case AST structures and should be silently ignored.
12911388 }
12921389 }
12931390 if ( funcNode . is ( 'call_expression' ) ) {
@@ -1305,7 +1402,8 @@ function findContainingTestFunction(node: SgNode<TSX>): SgNode<TSX> | null {
13051402 }
13061403 }
13071404 } catch {
1308- // Skip if field() is not available
1405+ // Skip nodes where field() is not available or AST structure doesn't match expectations.
1406+ // This is expected for malformed or edge-case AST structures and should be silently ignored.
13091407 }
13101408 }
13111409 }
0 commit comments