ソースを参照

fix(core): enforce patch EOF anchors (#38039)

Aiden Cline 3 週間 前
コミット
3ea8895299

+ 12 - 4
packages/core/src/patch.ts

@@ -157,6 +157,10 @@ function parseUpdate(lines: ReadonlyArray<string>, start: number, end: number) {
       else if (line.startsWith("+")) newLines.push(line.slice(1))
       index++
     }
+    if (lines[index]?.trim() === "*** End of File") {
+      endOfFile = true
+      index++
+    }
     chunks.push({ oldLines, newLines, changeContext, endOfFile: endOfFile || undefined })
   }
   return { chunks, next: index }
@@ -192,11 +196,15 @@ function computeReplacements(lines: ReadonlyArray<string>, path: string, chunks:
 
 function seek(lines: ReadonlyArray<string>, pattern: ReadonlyArray<string>, start: number, eof = false) {
   if (pattern.length === 0) return -1
-  for (const compare of [exact, rstrip, trim, normalized]) {
-    if (eof) {
-      const offset = lines.length - pattern.length
-      if (offset >= start && matches(lines, pattern, offset, compare)) return offset
+  if (eof) {
+    const offset = lines.length - pattern.length
+    if (offset < start) return -1
+    for (const compare of [exact, rstrip, trim, normalized]) {
+      if (matches(lines, pattern, offset, compare)) return offset
     }
+    return -1
+  }
+  for (const compare of [exact, rstrip, trim, normalized]) {
     for (let offset = start; offset <= lines.length - pattern.length; offset++) {
       if (matches(lines, pattern, offset, compare)) return offset
     }

+ 25 - 0
packages/core/test/patch.test.ts

@@ -113,6 +113,21 @@ describe("Patch", () => {
     ])
   })
 
+  test("preserves the end-of-file marker", () => {
+    expect(
+      parse(
+        "*** Begin Patch\n*** Update File: file.txt\n@@\n+quux\n*** End of File\n\n*** End Patch",
+      ),
+    ).toEqual([
+      {
+        type: "update",
+        path: "file.txt",
+        movePath: undefined,
+        chunks: [{ oldLines: [], newLines: ["quux"], changeContext: undefined, endOfFile: true }],
+      },
+    ])
+  })
+
   test("derives fuzzy line updates while preserving BOM", () => {
     const update = Patch.derive("update.txt", [{ oldLines: ["  old   "], newLines: ["new"] }], "\uFEFFold\n")
     expect(update).toEqual({ content: "new\n", bom: true })
@@ -174,6 +189,16 @@ describe("Patch", () => {
     ).toBe("marker\nmiddle\nmarker changed\nend\n")
   })
 
+  test("does not fall back to a non-EOF match", () => {
+    expect(() =>
+      Patch.derive(
+        "update.txt",
+        [{ oldLines: ["marker", "end"], newLines: ["changed", "end"], endOfFile: true }],
+        "marker\nend\nmiddle\n",
+      ),
+    ).toThrow("Failed to find expected lines")
+  })
+
   test("matches V1 lenient parsing of malformed hunk bodies", () => {
     expect(parse("*** Begin Patch\n*** Add File: add.txt\nmissing plus\n*** End Patch")).toEqual([
       { type: "add", path: "add.txt", contents: "" },

+ 21 - 3
packages/core/test/tool-patch.test.ts

@@ -392,14 +392,32 @@ describe("PatchTool", () => {
     withTempTool((directory, registry) =>
       Effect.gen(function* () {
         const target = path.join(directory, "tail.txt")
-        yield* Effect.promise(() => fs.writeFile(target, "alpha\nlast\n"))
+        yield* Effect.promise(() => fs.writeFile(target, "first\nsecond"))
         yield* executeTool(
           registry,
           call(
-            "*** Begin Patch\n*** Update File: tail.txt\n@@\n-last\n+end\n*** End of File\n*** End Patch",
+            "*** Begin Patch\n*** Update File: tail.txt\n@@\n first\n-second\n+second updated\n*** End of File\n*** End Patch",
           ),
         )
-        expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("alpha\nend\n")
+        expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("first\nsecond updated\n")
+      }),
+    ),
+  )
+
+  it.live("applies an end-of-file chunk to the final duplicate", () =>
+    withTempTool((directory, registry) =>
+      Effect.gen(function* () {
+        const target = path.join(directory, "duplicates.txt")
+        yield* Effect.promise(() => fs.writeFile(target, "marker\nend\nmiddle\nmarker\nend\n"))
+        yield* executeTool(
+          registry,
+          call(
+            "*** Begin Patch\n*** Update File: duplicates.txt\n@@\n-marker\n-end\n+marker changed\n+end\n*** End of File\n*** End Patch",
+          ),
+        )
+        expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe(
+          "marker\nend\nmiddle\nmarker changed\nend\n",
+        )
       }),
     ),
   )