Files
2026-08-01 23:14:50 +01:00

137 lines
4.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { lineInRanges, parseAddedLineRanges } from "./diff-ranges.mjs";
const OLD_A = "--- a/a.tsx";
const NEW_A = "+++ b/a.tsx";
describe("parseAddedLineRanges", () => {
it("reads a single-line hunk with no explicit count", () => {
const diff = [OLD_A, NEW_A, "@@ -4 +4 @@", "-old", "+new"].join("\n");
expect(parseAddedLineRanges(diff).get("a.tsx")).toEqual([[4, 4]]);
});
it("reads a multi-line hunk", () => {
const diff = [OLD_A, NEW_A, "@@ -10,0 +11,3 @@", "+x", "+y", "+z"].join("\n");
expect(parseAddedLineRanges(diff).get("a.tsx")).toEqual([[11, 13]]);
});
it("ignores deletion-only hunks, which add no attributable line", () => {
const diff = [OLD_A, NEW_A, "@@ -5,2 +4,0 @@", "-gone", "-also"].join("\n");
expect(parseAddedLineRanges(diff).get("a.tsx")).toEqual([]);
});
it("ignores deleted files", () => {
const diff = ["--- a/gone.tsx", "+++ /dev/null", "@@ -1,2 +0,0 @@", "-a", "-b"].join("\n");
expect(parseAddedLineRanges(diff).has("gone.tsx")).toBe(false);
});
it("keeps hunks separate across several files", () => {
const diff = [
"diff --git a/a.tsx b/a.tsx",
OLD_A,
NEW_A,
"@@ -1 +1 @@",
"+a",
"diff --git a/b.tsx b/b.tsx",
"--- a/b.tsx",
"+++ b/b.tsx",
"@@ -20,0 +21,2 @@",
"+b",
"+c",
].join("\n");
const ranges = parseAddedLineRanges(diff);
expect(ranges.get("a.tsx")).toEqual([[1, 1]]);
expect(ranges.get("b.tsx")).toEqual([[21, 22]]);
});
it("collects multiple hunks in one file", () => {
const diff = [OLD_A, NEW_A, "@@ -1 +1 @@", "+a", "@@ -50,0 +51,2 @@", "+b", "+c"].join("\n");
expect(parseAddedLineRanges(diff).get("a.tsx")).toEqual([
[1, 1],
[51, 52],
]);
});
it("does not mistake a '+++' inside added content for a file header", () => {
// A line of added source that begins with "+++" arrives as "++++...".
const diff = [OLD_A, NEW_A, "@@ -1,0 +1,1 @@", "++++ not a header"].join("\n");
const ranges = parseAddedLineRanges(diff);
expect([...ranges.keys()]).toEqual(["a.tsx"]);
expect(ranges.get("a.tsx")).toEqual([[1, 1]]);
});
it("returns nothing for an empty diff", () => {
expect(parseAddedLineRanges("").size).toBe(0);
});
it("ignores a pure rename, which emits no headers or hunks", () => {
// A plain `git mv` must cost nothing: attributing the moved file's existing
// lines would demand a full migration for a file nobody edited.
const diff = [
"diff --git a/old.tsx b/new.tsx",
"similarity index 100%",
"rename from old.tsx",
"rename to new.tsx",
].join("\n");
expect(parseAddedLineRanges(diff).size).toBe(0);
});
it("does not attribute a later hunk to the file before a headerless entry", () => {
// A binary entry emits no `+++`, so without resetting on `diff --git` the
// previous file would stay selected and absorb the next hunk.
const diff = [
"diff --git a/a.tsx b/a.tsx",
OLD_A,
NEW_A,
"@@ -1 +1 @@",
"+a",
"diff --git a/img.png b/img.png",
"Binary files a/img.png and b/img.png differ",
"@@ -99,0 +99,3 @@",
"+not really a hunk",
].join("\n");
const ranges = parseAddedLineRanges(diff);
expect(ranges.get("a.tsx")).toEqual([[1, 1]]);
expect(ranges.has("img.png")).toBe(false);
});
it("keeps a rename-plus-edit scoped to the edited lines only", () => {
const diff = [
"diff --git a/old.tsx b/new.tsx",
"similarity index 92%",
"rename from old.tsx",
"rename to new.tsx",
"--- a/old.tsx",
"+++ b/new.tsx",
"@@ -2,0 +3 @@",
"+const added = 1;",
].join("\n");
expect(parseAddedLineRanges(diff).get("new.tsx")).toEqual([[3, 3]]);
});
});
describe("lineInRanges", () => {
const ranges: Array<[number, number]> = [
[3, 5],
[10, 10],
];
it("matches inclusive bounds", () => {
expect(lineInRanges(ranges, 3)).toBe(true);
expect(lineInRanges(ranges, 5)).toBe(true);
expect(lineInRanges(ranges, 10)).toBe(true);
});
it("rejects lines outside every range", () => {
expect(lineInRanges(ranges, 2)).toBe(false);
expect(lineInRanges(ranges, 6)).toBe(false);
expect(lineInRanges(ranges, 11)).toBe(false);
});
it("treats a missing range list as no match", () => {
// Reached when a modified file produced no attributable added lines.
expect(lineInRanges(undefined as unknown as Array<[number, number]>, 1)).toBe(false);
});
});