Skip to content

Commit 9b1638f

Browse files
committed
code review
1 parent d46d0aa commit 9b1638f

File tree

5 files changed

+149
-30
lines changed

5 files changed

+149
-30
lines changed

codemods/v14-async-functions/scripts/codemod.ts

Lines changed: 116 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
*/
322341
function 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+
*/
539573
function 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+
*/
674724
function 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+
*/
852917
function 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

11861267
function 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+
*/
12151309
function 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
}

codemods/v14-update-deps/README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ This codemod updates your `package.json` file to prepare for React Native Testin
55
- ✅ Removing `@types/react-test-renderer` (no longer needed)
66
- ✅ Removing `react-test-renderer` (replaced by universal-test-renderer)
77
- ✅ Moving `@testing-library/react-native` from `dependencies` to `devDependencies` if present
8-
- ✅ Adding `@testing-library/react-native@^14.0.0-alpha` to `devDependencies` if not present
9-
- ✅ Updating `@testing-library/react-native` to `^14.0.0-alpha` (latest alpha version)
8+
- ✅ Adding `@testing-library/react-native@^14.0.0-alpha.5` to `devDependencies` if not present
9+
- ✅ Updating `@testing-library/react-native` to `^14.0.0-alpha.5`
1010
- ✅ Adding `universal-test-renderer@0.10.1` to `devDependencies` (always added)
1111

1212
## What it does
@@ -17,9 +17,9 @@ This codemod automatically updates your `package.json` dependencies to match the
1717

1818
2. **Moves RNTL to devDependencies**: If `@testing-library/react-native` is in `dependencies`, it's moved to `devDependencies`
1919

20-
3. **Ensures RNTL is present**: If `@testing-library/react-native` is not present, it's added to `devDependencies` with version `^14.0.0-alpha`
20+
3. **Ensures RNTL is present**: If `@testing-library/react-native` is not present, it's added to `devDependencies` with version `^14.0.0-alpha.5`
2121

22-
4. **Updates RNTL version**: Updates `@testing-library/react-native` to `^14.0.0-alpha` to get the latest alpha version
22+
4. **Updates RNTL version**: Updates `@testing-library/react-native` to `^14.0.0-alpha.5`
2323

2424
5. **Adds universal-test-renderer**: Always adds `universal-test-renderer@0.10.1` to `devDependencies`
2525

@@ -56,7 +56,7 @@ npx codemod@latest run @testing-library/react-native-v14-update-deps --target ./
5656
```json
5757
{
5858
"devDependencies": {
59-
"@testing-library/react-native": "^14.0.0-alpha",
59+
"@testing-library/react-native": "^14.0.0-alpha.5",
6060
"universal-test-renderer": "0.10.1"
6161
}
6262
}
@@ -79,7 +79,7 @@ npx codemod@latest run @testing-library/react-native-v14-update-deps --target ./
7979
```json
8080
{
8181
"devDependencies": {
82-
"@testing-library/react-native": "^14.0.0-alpha",
82+
"@testing-library/react-native": "^14.0.0-alpha.5",
8383
"universal-test-renderer": "0.10.1"
8484
}
8585
}
@@ -103,7 +103,7 @@ npx codemod@latest run @testing-library/react-native-v14-update-deps --target ./
103103
{
104104
"devDependencies": {
105105
"some-other-package": "^1.0.0",
106-
"@testing-library/react-native": "^14.0.0-alpha",
106+
"@testing-library/react-native": "^14.0.0-alpha.5",
107107
"universal-test-renderer": "0.10.1"
108108
}
109109
}
@@ -121,7 +121,7 @@ npx codemod@latest run @testing-library/react-native-v14-update-deps --target ./
121121
pnpm install
122122
```
123123

124-
2. **Version resolution**: The codemod sets `@testing-library/react-native` to `^14.0.0-alpha`, which will resolve to the latest alpha version available. You can manually update this to a specific version if needed.
124+
2. **Version resolution**: The codemod sets `@testing-library/react-native` to `^14.0.0-alpha.5`. You can manually update this to a different version if needed.
125125

126126
3. **Always adds packages**: The codemod always ensures both `@testing-library/react-native` and `universal-test-renderer` are present in `devDependencies`, even if they weren't there before.
127127

@@ -138,7 +138,7 @@ npm test
138138

139139
1. **Package manager**: The codemod updates `package.json` but doesn't run the package manager install command. You need to run `npm install` / `yarn install` / `pnpm install` manually after the codemod completes.
140140

141-
2. **Version pinning**: If you have a specific alpha version pinned, the codemod will update it to the range `^14.0.0-alpha`. You may want to review and adjust the version after running the codemod.
141+
2. **Version pinning**: If you have a specific alpha version pinned, the codemod will update it to `^14.0.0-alpha.5`. You may want to review and adjust the version after running the codemod.
142142

143143
3. **Workspace projects**: For monorepos with multiple `package.json` files, the codemod will process each one individually.
144144

codemods/v14-update-deps/scripts/codemod.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,9 +55,11 @@ export default async function transform(
5555

5656
return null;
5757
} catch (error) {
58-
const errorMessage = error instanceof Error ? error.message : String(error);
59-
console.error(`Error processing ${filename}:`, errorMessage);
60-
return null;
58+
// Re-throw error to let the codemod platform handle it
59+
// This provides better error reporting than silently returning null
60+
throw new Error(
61+
`Error processing ${filename}: ${error instanceof Error ? error.message : String(error)}`,
62+
);
6163
}
6264
}
6365

codemods/v14-update-deps/scripts/test.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22

33
/**
44
* Test script for the package.json update codemod
5+
* Note: This test script uses console.log for test output, which is acceptable for test scripts
56
*/
67

8+
/* eslint-disable no-console */
79
import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
810
import { join, dirname } from 'path';
911
import { fileURLToPath } from 'url';
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"compilerOptions": {
3+
"module": "NodeNext",
4+
"moduleResolution": "NodeNext",
5+
"types": ["@codemod.com/jssg-types"],
6+
"allowImportingTsExtensions": true,
7+
"noEmit": true,
8+
"verbatimModuleSyntax": true,
9+
"erasableSyntaxOnly": true,
10+
"strict": true,
11+
"strictNullChecks": true,
12+
"noImplicitReturns": true,
13+
"noFallthroughCasesInSwitch": true,
14+
"noUncheckedIndexedAccess": true
15+
},
16+
"exclude": ["tests"]
17+
}

0 commit comments

Comments
 (0)