fix: allow parsing empty context lines at the very end of unified diffs#690
fix: allow parsing empty context lines at the very end of unified diffs#690rtritto wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adjusts how parsePatch determines the hunk line “operation” for empty strings while iterating through a unified diff.
Changes:
- Simplifies operation detection by treating empty lines as context (
' ') unconditionally.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const operation = diffstr[i].length == 0 ? ' ' : diffstr[i][0]; | ||
| if (operation === '+' || operation === '-' || operation === ' ' || operation === '\\') { | ||
| hunk.lines.push(diffstr[i]); |
I can't repro this; if I use Do you have exact repro steps to cause an existing patch-generating tool outputting a patch that (It may be worth applying this change even if you are mistaken about yarn and pnpm, for consistency with non-trailing empty context lines and to handle the case where trimming is done by a text editor. I'll have a look and think about it. But would like to understand either way whether there really is an even stronger case for this based on yarn and pnpm emitting patches we currently consider malformed, as you suggest.) |
|
Hmm... doesn't this change actually have the effect of letting us imagine an empty context line AFTER the final newline of a patch? That is, it's not letting us parse a patch like this, which actually we can already parse fine (formatted here as a JS template string, for ease of seeing exactly what newlines are present)... but is actually letting us parse one like THIS: But... there literally isn't a line after I can see the argument for why this makes sense to do given that we already
This PR effectively modifies our handling of the scenario where a patch is malformed in BOTH of these ways at once (i.e. where there should've been a final Am I understanding correctly, do you think, @rtritto? I'd welcome your thoughts. |
|
@ExplodingCabbage thanks for reply, my use case (reproduction) is with --- a/dist/index.js
+++ b/dist/index.js
@@ -121,11 +121,16 @@ function auto(options) {
return [
catchAll(),
devServer(),
- ...node$1(options?.node).map((p) => {
- p[INSTANCE] = instance;
- return enablePluginIf((config) => noDeploymentTargetFound(p, config), p);
- }),
- ...netlifyGlue()
+ {
+ name: "ud:target:emit",
+ apply: "build",
+ config: {
+ order: "post",
+ handler() {
+ return { environments: { ssr: { build: { rolldownOptions: { input: { index: './src/server/entrypoint.ts' } } } } } }
+ }
+ }
+ }
];
}Output: file:///C:/Users/<USER>/AppData/Local/Yarn/Berry/cache/diff-npm-9.0.0-7def25b473-10c0.zip/node_modules/diff/libesm/patch/parse.js:478
throw new Error(`Hunk at line ${chunkHeaderIndex + 1} contained invalid line ${diffstr[i]}`);
^
Error: Hunk at line 3 contained invalid line
at parseHunk (file:///C:/Users/<USER>/AppData/Local/Yarn/Berry/cache/diff-npm-9.0.0-7def25b473-10c0.zip/node_modules/diff/libesm/patch/parse.js:478:23)
at parseIndex (file:///C:/Users/<USER>/AppData/Local/Yarn/Berry/cache/diff-npm-9.0.0-7def25b473-10c0.zip/node_modules/diff/libesm/patch/parse.js:190:34)
at parsePatch (file:///C:/Users/<USER>/AppData/Local/Yarn/Berry/cache/diff-npm-9.0.0-7def25b473-10c0.zip/node_modules/diff/libesm/patch/parse.js:509:9)
at applyPatch (file:///C:/Users/<USER>/AppData/Local/Yarn/Berry/cache/diff-npm-9.0.0-7def25b473-10c0.zip/node_modules/diff/libesm/patch/apply.js:30:19)
at applyPatchCore (file:///C:/<MY_PROJECT>/src/applyPatchCore.ts:30:25)
at file:///C:/<MY_PROJECT>/src/applyPatch.ts:6:11
at withPatchLifecycle (file:///C:/<MY_PROJECT>/src/utils.ts:39:9)
at applyPatch (file:///C:/<MY_PROJECT>/src/applyPatch.ts:5:9)
at file:///C:/<MY_PROJECT>/scripts/patcher.ts:3:7
at ModuleJob.run (node:internal/modules/esm/module_job:439:25)Workaround: change PS: tell me if you need some further help |
You are spot on – your understanding is exactly correct. The PR does indeed allow To give you some context on why I proposed this: it addresses a very frustrating and common real-world edge case when using ecosystem tools like When this slightly truncated patch is fed back into Standard GNU If you feel this level of leniency is too risky or outside the scope of WDYT? |
When parsing unified diff files, empty context lines (which should technically consist of a single space
" ") are often inadvertently truncated to completely empty strings ("") due to code editors trimming trailing whitespace or formatting tools removing extraneous spaces.Currently, the
parsePatch(specificallyparseHunk) function successfully forgives and maps these empty strings back to a space token (' '), except when the empty string happens to be the last line in thediffstrarray:If a patch file ends with a trailing newline,
uniDiff.split(/\n/)yields""as the last element of the array. If a hunk still expects one last context line at that exact end-of-file position,ipoints to this final""string. Because of thei != (diffstr.length - 1)check, it falls back todiffstr[i][0], which evaluates toundefined.Since
undefinedis not a valid operation (+,-,,\),parseHunkthrows an opaque error:Error: Hunk at line X contained invalid lineChanges Made:
Removed the boundary exception
&& i != (diffstr.length - 1)condition. Empty strings are now globally and safely interpreted as empty context lines regardless of their index in the diff string.Why this fixes real-world use cases:
This minor tweak prevents runtime exceptions when using
applyPatch()on valid patch files generated by package managers (likeyarn patchorpnpm patch) or saved via code editors, where a hunk requires an empty contextual line extending to the very end of the file.