Skip to content

Commit 1bb6756

Browse files
committed
fix handling of noNewlineAtEndOfFile
1 parent 683cc90 commit 1bb6756

File tree

7 files changed

+85
-56
lines changed

7 files changed

+85
-56
lines changed

.vscode/launch.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"version": "0.2.0",
3+
"configurations": [
4+
{
5+
"name": "Debug Jest Tests",
6+
"type": "node",
7+
"request": "launch",
8+
"runtimeArgs": [
9+
"--inspect-brk",
10+
"${workspaceRoot}/node_modules/.bin/jest",
11+
"--runInBand"
12+
],
13+
"console": "integratedTerminal",
14+
"internalConsoleOptions": "neverOpen",
15+
"port": 9229
16+
}
17+
]
18+
}

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@
1313
"prettier.trailingComma": "all",
1414
"editor.formatOnSave": true,
1515
"tslint.enable": true,
16-
"typescript.tsdk": "node_modules/typescript/lib"
16+
"typescript.tsdk": "node_modules/typescript/lib",
17+
"debug.node.autoAttach": "on"
1718
}

property-based-tests/executeTestCase.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { executeEffects } from "../src/patch/apply"
77
import { parsePatch } from "../src/patch/parse"
88

99
import { TestCase, Files } from "./testCases"
10+
import { appendFileSync, existsSync, writeFileSync } from "fs"
1011

1112
jest.mock("fs-extra", () => {
1213
let workingFiles: Files
@@ -67,13 +68,12 @@ export function executeTestCase(testCase: TestCase) {
6768
try {
6869
f()
6970
} catch (e) {
70-
console.error(
71-
"TEST CASE FAILED",
72-
JSON.stringify({
73-
testCase,
74-
workingFiles: fs.getWorkingFiles(),
75-
}),
76-
)
71+
const data = JSON.stringify(testCase) + "\n\n"
72+
if (!existsSync("generative-test-errors.log")) {
73+
writeFileSync("generative-test-errors.log", data)
74+
} else {
75+
appendFileSync("generative-test-errors.log", data)
76+
}
7777
throw e
7878
}
7979
}

property-based-tests/regressionTests.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,21 @@ describe("regression tests", () => {
155155
modifiedFiles: { "1dkfI.J": { contents: "b\nb\nc", mode: 420 } },
156156
})
157157
})
158+
159+
describe("16", () => {
160+
executeTestCase({
161+
cleanFiles: {
162+
"k/1dt4myqe.e1": {
163+
contents: "a\n\n\n\nbanana\n",
164+
mode: 420,
165+
},
166+
},
167+
modifiedFiles: {
168+
"k/1dt4myqe.e1": {
169+
contents: "b\n\n\n\nbanana\n",
170+
mode: 420,
171+
},
172+
},
173+
})
174+
})
158175
})

property-based-tests/testCases.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,12 @@ function mutateFiles(files: Files): Files {
122122
break
123123
}
124124
case "createFile": {
125-
mutatedFiles[makeFileName()] = makeFileContents()
125+
// TODO make sure there isn't a dir with that filename already
126+
let filename = makeFileName()
127+
while (Object.keys(mutatedFiles).some(k => k.startsWith(filename))) {
128+
filename = makeFileName()
129+
}
130+
mutatedFiles[filename] = makeFileContents()
126131
break
127132
}
128133
case "deleteLine": {

src/patch/apply.ts

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export type Effect =
2626
type: "replace"
2727
path: string
2828
lines: string[]
29-
noNewlineAtEndOfFile: boolean
3029
}
3130

3231
export const executeEffects = (effects: Effect[]) => {
@@ -47,10 +46,7 @@ export const executeEffects = (effects: Effect[]) => {
4746
)
4847
break
4948
case "replace":
50-
fs.writeFileSync(
51-
eff.path,
52-
eff.lines.join("\n") + (eff.noNewlineAtEndOfFile ? "" : "\n"),
53-
)
49+
fs.writeFileSync(eff.path, eff.lines.join("\n"))
5450
break
5551
}
5652
})
@@ -70,16 +66,33 @@ function assertLineEquality(onDisk: string, expected: string) {
7066
}
7167
}
7268

69+
/**
70+
* How does noNewLineAtEndOfFile work?
71+
*
72+
* if you remove the newline from a file that had one without editing other bits:
73+
*
74+
* it creates an insertion/removal pair where the insertion has \ No new line at end of file
75+
*
76+
* if you edit a file that didn't have a new line and don't add one:
77+
*
78+
* both insertion and deletion have \ No new line at end of file
79+
*
80+
* if you edit a file that didn't have a new line and add one:
81+
*
82+
* deletion has \ No new line at end of file
83+
* but not insertion
84+
*
85+
* if you edit a file that had a new line and leave it in:
86+
*
87+
* neither insetion nor deletion have the annoation
88+
*
89+
*/
90+
7391
function applyPatch({ parts, path }: FilePatch): Effect {
7492
// modifying the file in place
7593
const fileContents = fs.readFileSync(path).toString()
7694

7795
const fileLines: string[] = fileContents.split(/\n/)
78-
if (fileLines[fileLines.length - 1] === "") {
79-
fileLines.pop()
80-
}
81-
82-
let noNewlineAtEndOfFile = true
8396

8497
// when adding or removing lines from a file, gotta
8598
// make sure that the original lines in hunk headers match up
@@ -117,30 +130,19 @@ function applyPatch({ parts, path }: FilePatch): Effect {
117130
)
118131
contextIndex -= part.lines.length
119132

120-
if (contextIndex >= fileLines.length) {
121-
if (
122-
(hunkHeader.patched.length > 0 && part.noNewlineAtEndOfFile) ||
123-
(parts[i - 2] &&
124-
(parts[i - 2].type === "insertion" ||
125-
parts[i - 2].type === "context") &&
126-
!(parts[i - 2] as any).noNewlineAtEndOfFile)
127-
) {
128-
// delete the fact that there was no newline at the end of the file
129-
// by adding a newline to the end of the file
130-
noNewlineAtEndOfFile = false
131-
}
132-
}
133-
} else {
134-
if (contextIndex >= fileLines.length) {
135-
noNewlineAtEndOfFile = part.noNewlineAtEndOfFile
133+
if (part.noNewlineAtEndOfFile) {
134+
fileLines.push("")
136135
}
137136
}
138137
break
139138
case "insertion":
140139
fileLines.splice(contextIndex, 0, ...part.lines)
141140
contextIndex += part.lines.length
142-
if (contextIndex >= fileLines.length) {
143-
noNewlineAtEndOfFile = part.noNewlineAtEndOfFile
141+
if (part.noNewlineAtEndOfFile) {
142+
if (contextIndex !== fileLines.length - 1) {
143+
throw new Error("Invalid patch application state.")
144+
}
145+
fileLines.pop()
144146
}
145147
break
146148
}
@@ -149,7 +151,6 @@ function applyPatch({ parts, path }: FilePatch): Effect {
149151

150152
return {
151153
type: "replace",
152-
noNewlineAtEndOfFile,
153154
path,
154155
lines: fileLines,
155156
}

src/patch/parse.ts

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -31,31 +31,18 @@ export const parseHunkHeaderLine = (headerLine: string): HunkHeader => {
3131
}
3232
}
3333

34-
interface Insertion {
35-
type: "insertion"
34+
interface PatchMutationPart {
35+
type: "context" | "insertion" | "deletion"
3636
lines: string[]
3737
noNewlineAtEndOfFile: boolean
3838
}
39-
40-
interface Deletion {
41-
type: "deletion"
42-
lines: string[]
43-
noNewlineAtEndOfFile: boolean
44-
}
45-
46-
interface Context {
47-
type: "context"
48-
lines: string[]
49-
noNewlineAtEndOfFile: boolean
50-
}
51-
52-
type PatchMutationPart = Insertion | Deletion | Context
5339
export type PatchHunk = HunkHeader | PatchMutationPart
5440

5541
interface FileRename {
5642
type: "rename"
5743
fromPath: string
5844
toPath: string
45+
// TODO: check wheter renaming a file can have \ No newline at end of file
5946
}
6047

6148
export interface FilePatch {
@@ -201,7 +188,7 @@ class PatchParser {
201188
type: "insertion",
202189
lines: [],
203190
noNewlineAtEndOfFile: true,
204-
} as PatchMutationPart
191+
}
205192
} else {
206193
throw new Error(`unexpected patch file comment ${this.currentLine}`)
207194
}
@@ -246,7 +233,7 @@ class PatchParser {
246233
type: blockType,
247234
lines,
248235
noNewlineAtEndOfFile,
249-
} as PatchMutationPart
236+
}
250237
}
251238

252239
private currentLineIsPartOfHunk(): boolean {

0 commit comments

Comments
 (0)