refactor(typescript-edit-benchmark): restructured line pairing to use diff-based matching

- Replaced line-by-line comparison with diff-based pairing to correctly handle insertions and deletions that shift line indices.
- Added logic to preserve trailing newline semantics from the actual content.
- Added test case verifying that indent-only changes are normalized even when earlier insertions shift line positions.
This commit is contained in:
can1357
2026-05-27 13:34:19 +02:00
parent abac192890
commit 2ef4eb7931
2 changed files with 80 additions and 15 deletions
@@ -244,27 +244,48 @@ function normalizeBlankLines(text: string): string {
}
function restoreWhitespaceOnlyLineDiffs(expected: string, actual: string): string {
const expectedLines = expected.split("\n");
const actualLines = actual.split("\n");
const max = Math.max(expectedLines.length, actualLines.length);
const out = new Array<string>(max);
const changes = diffLines(expected, actual);
const out: string[] = [];
let pendingRemoved: string[] = [];
let pendingAdded: string[] = [];
for (let i = 0; i < max; i++) {
const expectedLine = expectedLines[i];
const actualLine = actualLines[i];
if (expectedLine === undefined || actualLine === undefined) {
out[i] = actualLine ?? "";
const flush = () => {
const pairs = Math.min(pendingRemoved.length, pendingAdded.length);
for (let i = 0; i < pairs; i++) {
const removedLine = pendingRemoved[i]!;
const addedLine = pendingAdded[i]!;
out.push(
removedLine !== addedLine && equalsIgnoringWhitespace(removedLine, addedLine) ? removedLine : addedLine,
);
}
// Unmatched added lines (insertions beyond the removal window) stay as-is.
for (let i = pairs; i < pendingAdded.length; i++) {
out.push(pendingAdded[i]!);
}
// Unmatched removed lines have no counterpart in actual — drop them.
pendingRemoved = [];
pendingAdded = [];
};
for (const change of changes) {
const lines = splitLines(change.value);
if (change.removed) {
pendingRemoved.push(...lines);
continue;
}
if (expectedLine !== actualLine && equalsIgnoringWhitespace(expectedLine, actualLine)) {
out[i] = expectedLine;
} else {
out[i] = actualLine;
if (change.added) {
pendingAdded.push(...lines);
continue;
}
flush();
out.push(...lines);
}
flush();
return out.join("\n");
// Preserve trailing newline semantics: rejoin with "\n" and add a trailing
// newline iff actual originally ended with one.
const joined = out.join("\n");
return actual.endsWith("\n") ? `${joined}\n` : joined;
}
function equalsIgnoringWhitespace(a: string, b: string): boolean {
@@ -120,4 +120,48 @@ describe("verifyExpectedFiles", () => {
await cleanup();
}
});
it("normalizes indent-only diffs even when earlier insertions shift line indices", async () => {
const { expectedDir, actualDir, cleanup } = await createTempDirs();
try {
// Force prettier to bail (intentional syntax error: unbalanced brace) so the
// verifier falls back to the whitespace-restore pass on raw content.
const expected = [
"function broken(",
" // missing close paren on purpose",
" return {",
" a: 1,",
" b: 2,",
" };",
"}",
"",
].join("\n");
const actual = [
"function broken(",
" // missing close paren on purpose",
" const inserted = true;",
" return {",
" a: 1,",
" b: 2,",
" };",
"}",
"",
].join("\n");
await Bun.write(path.join(expectedDir, "index.ts"), expected);
await Bun.write(path.join(actualDir, "index.ts"), actual);
const result = await verifyExpectedFiles(expectedDir, actualDir);
// The only real change should be the inserted const; the body's indent
// drift must not be reported as added/removed lines.
expect(result.success).toBe(false);
const diff = result.diff ?? "";
const changeLines = diff.split("\n").filter(line => line.startsWith("+") || line.startsWith("-"));
expect(changeLines.some(line => line.includes("const inserted"))).toBe(true);
expect(changeLines.some(line => line.includes("a:"))).toBe(false);
expect(changeLines.some(line => line.includes("b:"))).toBe(false);
} finally {
await cleanup();
}
});
});