({
file_path,
edits,
}: {
file_path: string
edits: EditInput[]
})
| 579 | * Returns the normalized input if successful, or the original input if not |
| 580 | */ |
| 581 | export function normalizeFileEditInput({ |
| 582 | file_path, |
| 583 | edits, |
| 584 | }: { |
| 585 | file_path: string |
| 586 | edits: EditInput[] |
| 587 | }): { |
| 588 | file_path: string |
| 589 | edits: EditInput[] |
| 590 | } { |
| 591 | if (edits.length === 0) { |
| 592 | return { file_path, edits } |
| 593 | } |
| 594 | |
| 595 | // Markdown uses two trailing spaces as a hard line break — stripping would |
| 596 | // silently change semantics. Skip stripTrailingWhitespace for .md/.mdx. |
| 597 | const isMarkdown = /\.(md|mdx)$/i.test(file_path) |
| 598 | |
| 599 | try { |
| 600 | const fullPath = expandPath(file_path) |
| 601 | |
| 602 | // Use cached file read to avoid redundant I/O operations. |
| 603 | // If the file doesn't exist, readFileSyncCached throws ENOENT which the |
| 604 | // catch below handles by returning the original input (no TOCTOU pre-check). |
| 605 | const fileContent = readFileSyncCached(fullPath) |
| 606 | |
| 607 | return { |
| 608 | file_path, |
| 609 | edits: edits.map(({ old_string, new_string, replace_all }) => { |
| 610 | const normalizedNewString = isMarkdown |
| 611 | ? new_string |
| 612 | : stripTrailingWhitespace(new_string) |
| 613 | |
| 614 | // If exact string match works, keep it as is |
| 615 | if (fileContent.includes(old_string)) { |
| 616 | return { |
| 617 | old_string, |
| 618 | new_string: normalizedNewString, |
| 619 | replace_all, |
| 620 | } |
| 621 | } |
| 622 | |
| 623 | // Try de-sanitize string if exact match fails |
| 624 | const { result: desanitizedOldString, appliedReplacements } = |
| 625 | desanitizeMatchString(old_string) |
| 626 | |
| 627 | if (fileContent.includes(desanitizedOldString)) { |
| 628 | // Apply the same exact replacements to new_string |
| 629 | let desanitizedNewString = normalizedNewString |
| 630 | for (const { from, to } of appliedReplacements) { |
| 631 | desanitizedNewString = desanitizedNewString.replaceAll(from, to) |
| 632 | } |
| 633 | |
| 634 | return { |
| 635 | old_string: desanitizedOldString, |
| 636 | new_string: desanitizedNewString, |
| 637 | replace_all, |
| 638 | } |
no test coverage detected