Skip to content

Commit a1598d2

Browse files
committed
fix: prevent nested renames
1 parent 1f8f0ae commit a1598d2

File tree

2 files changed

+31
-19
lines changed

2 files changed

+31
-19
lines changed

src/utils/search-and-replace.ts

+28-17
Original file line numberDiff line numberDiff line change
@@ -16,30 +16,33 @@ export async function searchAndReplace(
1616

1717
async function processFile(filePath: string): Promise<void> {
1818
try {
19-
const content = await readFile(filePath, 'utf8')
20-
let newContent = content
19+
let newContent = await readFile(filePath, 'utf8')
20+
let changesMade = false
2121

2222
for (const [i, fromString] of fromStrings.entries()) {
2323
const regex = new RegExp(fromString, 'g')
24-
newContent = newContent.replace(regex, toStrings[i])
24+
if (regex.test(newContent)) {
25+
newContent = newContent.replace(regex, toStrings[i])
26+
changesMade = true
27+
}
2528
}
2629

27-
if (content !== newContent) {
30+
if (changesMade) {
2831
if (!isDryRun) {
2932
await writeFile(filePath, newContent, 'utf8')
3033
}
3134
if (isVerbose) {
3235
console.log(`${isDryRun ? '[Dry Run] ' : ''}File modified: ${filePath}`)
33-
}
34-
for (const [index, fromStr] of fromStrings.entries()) {
35-
const count = (newContent.match(new RegExp(toStrings[index], 'g')) || []).length
36-
if (count > 0 && isVerbose) {
37-
console.log(` Replaced "${fromStr}" with "${toStrings[index]}" ${count} time(s)`)
36+
for (const [index, fromStr] of fromStrings.entries()) {
37+
const count = (newContent.match(new RegExp(toStrings[index], 'g')) || []).length
38+
if (count > 0) {
39+
console.log(` Replaced "${fromStr}" with "${toStrings[index]}" ${count} time(s)`)
40+
}
3841
}
3942
}
4043
}
4144
} catch (error) {
42-
console.error(`Error processing file ${filePath}:`, error)
45+
console.error(`Error processing file: ${filePath}`, error)
4346
}
4447
}
4548

@@ -79,6 +82,7 @@ export async function searchAndReplace(
7982
async function renamePaths(directoryPath: string): Promise<void> {
8083
try {
8184
const entries = await readdir(directoryPath, { withFileTypes: true })
85+
const renameQueue: { oldPath: string; newPath: string }[] = []
8286

8387
for (const entry of entries) {
8488
if (EXCLUDED_DIRECTORIES.has(entry.name)) {
@@ -96,16 +100,23 @@ export async function searchAndReplace(
96100
}
97101

98102
if (oldPath !== newPath) {
99-
if (!isDryRun) {
100-
await rename(oldPath, newPath)
101-
}
102-
if (isVerbose) {
103-
console.log(`${isDryRun ? '[Dry Run] ' : ''}Renamed: ${oldPath} -> ${newPath}`)
104-
}
103+
renameQueue.push({ oldPath, newPath })
105104
}
106105

107106
if (entry.isDirectory()) {
108-
await renamePaths(entry.isDirectory() ? newPath : oldPath)
107+
await renamePaths(oldPath) // Process subdirectories first
108+
}
109+
}
110+
111+
// Sort by descending path length to rename deepest paths first
112+
renameQueue.sort((a, b) => b.oldPath.length - a.oldPath.length)
113+
114+
for (const { oldPath, newPath } of renameQueue) {
115+
if (!isDryRun) {
116+
await rename(oldPath, newPath)
117+
}
118+
if (isVerbose) {
119+
console.log(`${isDryRun ? '[Dry Run] ' : ''}Renamed: ${oldPath} -> ${newPath}`)
109120
}
110121
}
111122
} catch (error) {

test/search-and-replace.test.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,9 @@ describe('searchAndReplace', () => {
9191
await searchAndReplace(tempDir, ['Hello', 'Old'], ['Hi', 'New'], false, true)
9292

9393
// Check that log shows correct counts for replacements
94-
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Replaced "Hello" with "Hi" 1 time(s)'))
95-
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('Replaced "Old" with "New" 1 time(s)'))
94+
expect(consoleLogSpy).toHaveBeenNthCalledWith(2, ' Replaced "Hello" with "Hi" 1 time(s)')
95+
expect(consoleLogSpy).toHaveBeenNthCalledWith(4, ' Replaced "Old" with "New" 1 time(s)')
96+
expect(consoleLogSpy).toHaveBeenNthCalledWith(6, ' Replaced "Hello" with "Hi" 1 time(s)')
9697

9798
consoleLogSpy.mockRestore()
9899
})

0 commit comments

Comments
 (0)