1515 */
1616final class Differ
1717{
18- /** @var int a safe number for indicating showing all contexts */
18+ /**
19+ * @var int a safe number for indicating showing all contexts
20+ */
1921 const CONTEXT_ALL = \PHP_INT_MAX >> 3 ;
2022
23+ /**
24+ * @var string used to indicate a line has no EOL
25+ *
26+ * Arbitrary chars from the 15-16th Unicode reserved areas
27+ * and hopefully, they won't appear in source texts
28+ */
29+ const LINE_NO_EOL = "\u{fcf28}\u{fc231}" ;
30+
2131 /**
2232 * @var array cached properties and their default values
2333 */
@@ -267,7 +277,7 @@ public function getGroupedOpcodes(): array
267277 }
268278
269279 /**
270- * A EOL-at-EOF-sensitve version of getGroupedOpcodes().
280+ * A EOL-at-EOF-sensitive version of getGroupedOpcodes().
271281 *
272282 * @return int[][][] array of the grouped opcodes for the generated diff (GNU version)
273283 */
@@ -279,44 +289,11 @@ public function getGroupedOpcodesGnu(): array
279289 return $ this ->groupedOpcodesGnu ;
280290 }
281291
282- $ old = \array_map (
283- function (string $ line ): string {
284- return "{$ line }\n" ;
285- },
286- $ this ->old
287- );
288-
289- $ new = \array_map (
290- function (string $ line ): string {
291- return "{$ line }\n" ;
292- },
293- $ this ->new
294- );
295-
296- // note that the old and the new are never empty at this point
297- // they have at least one element "\n" in the array because explode("\n", "") === [""]
298- $ oldLastIdx = \count ($ old ) - 1 ;
299- $ newLastIdx = \count ($ new ) - 1 ;
300- $ oldLast = &$ old [$ oldLastIdx ];
301- $ newLast = &$ new [$ newLastIdx ];
302-
303- if ($ oldLast === "\n" ) {
304- // remove the last plain "\n" line since we don't need it anymore
305- unset($ old [$ oldLastIdx ]);
306- } else {
307- // this means the original source has no EOL at EOF
308- // we have to remove the trailing \n from the last line
309- $ oldLast = \substr ($ oldLast , 0 , -1 );
310- }
311-
312- if ($ newLast === "\n" ) {
313- unset($ new [$ newLastIdx ]);
314- } else {
315- $ newLast = \substr ($ newLast , 0 , -1 );
316- }
317-
318292 return $ this ->groupedOpcodesGnu = $ this ->sequenceMatcher
319- ->setSequences ($ old , $ new )
293+ ->setSequences (
294+ $ this ->makeLinesGnuCompatible ($ this ->old ),
295+ $ this ->makeLinesGnuCompatible ($ this ->new )
296+ )
320297 ->getGroupedOpcodes ($ this ->options ['context ' ]);
321298 }
322299
@@ -403,4 +380,31 @@ private function getText(array $lines, int $start = 0, ?int $end = null): array
403380 // hence the length for array_slice() must be non-negative
404381 return \array_slice ($ lines , $ start , \max (0 , $ end - $ start ));
405382 }
383+
384+ /**
385+ * Make the lines to be prepared for GNU-style diff.
386+ *
387+ * This method checks whether $lines has no EOL at EOF and append a special
388+ * indicator to the last line.
389+ *
390+ * @param string[] $lines the lines
391+ */
392+ private function makeLinesGnuCompatible (array $ lines ): array
393+ {
394+ // note that the $lines should not be empty at this point
395+ // they have at least one element "" in the array because explode("\n", "") === [""]
396+ $ lastLineIdx = \count ($ lines ) - 1 ;
397+ $ lastLine = &$ lines [$ lastLineIdx ];
398+
399+ if ($ lastLine === '' ) {
400+ // remove the last plain "" line since we don't need it anymore
401+ unset($ lines [$ lastLineIdx ]);
402+ } else {
403+ // this means the original source has no EOL at EOF
404+ // we append a special indicator to that line so it no longer matches
405+ $ lastLine .= self ::LINE_NO_EOL ;
406+ }
407+
408+ return $ lines ;
409+ }
406410}
0 commit comments