UnifiedDiffOutputBuilder.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. <?php declare(strict_types=1);
  2. /*
  3. * This file is part of sebastian/diff.
  4. *
  5. * (c) Sebastian Bergmann <sebastian@phpunit.de>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace SebastianBergmann\Diff\Output;
  11. use SebastianBergmann\Diff\Differ;
  12. /**
  13. * Builds a diff string representation in unified diff format in chunks.
  14. */
  15. final class UnifiedDiffOutputBuilder extends AbstractChunkOutputBuilder
  16. {
  17. /**
  18. * @var bool
  19. */
  20. private $collapseRanges = true;
  21. /**
  22. * @var int >= 0
  23. */
  24. private $commonLineThreshold = 6;
  25. /**
  26. * @var int >= 0
  27. */
  28. private $contextLines = 3;
  29. /**
  30. * @var string
  31. */
  32. private $header;
  33. /**
  34. * @var bool
  35. */
  36. private $addLineNumbers;
  37. public function __construct(string $header = "--- Original\n+++ New\n", bool $addLineNumbers = false)
  38. {
  39. $this->header = $header;
  40. $this->addLineNumbers = $addLineNumbers;
  41. }
  42. public function getDiff(array $diff): string
  43. {
  44. $buffer = \fopen('php://memory', 'r+b');
  45. if ('' !== $this->header) {
  46. \fwrite($buffer, $this->header);
  47. if ("\n" !== \substr($this->header, -1, 1)) {
  48. \fwrite($buffer, "\n");
  49. }
  50. }
  51. if (0 !== \count($diff)) {
  52. $this->writeDiffHunks($buffer, $diff);
  53. }
  54. $diff = \stream_get_contents($buffer, -1, 0);
  55. \fclose($buffer);
  56. // If the last char is not a linebreak: add it.
  57. // This might happen when both the `from` and `to` do not have a trailing linebreak
  58. $last = \substr($diff, -1);
  59. return "\n" !== $last && "\r" !== $last
  60. ? $diff . "\n"
  61. : $diff
  62. ;
  63. }
  64. private function writeDiffHunks($output, array $diff): void
  65. {
  66. // detect "No newline at end of file" and insert into `$diff` if needed
  67. $upperLimit = \count($diff);
  68. if (0 === $diff[$upperLimit - 1][1]) {
  69. $lc = \substr($diff[$upperLimit - 1][0], -1);
  70. if ("\n" !== $lc) {
  71. \array_splice($diff, $upperLimit, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]);
  72. }
  73. } else {
  74. // search back for the last `+` and `-` line,
  75. // check if has trailing linebreak, else add under it warning under it
  76. $toFind = [1 => true, 2 => true];
  77. for ($i = $upperLimit - 1; $i >= 0; --$i) {
  78. if (isset($toFind[$diff[$i][1]])) {
  79. unset($toFind[$diff[$i][1]]);
  80. $lc = \substr($diff[$i][0], -1);
  81. if ("\n" !== $lc) {
  82. \array_splice($diff, $i + 1, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]);
  83. }
  84. if (!\count($toFind)) {
  85. break;
  86. }
  87. }
  88. }
  89. }
  90. // write hunks to output buffer
  91. $cutOff = \max($this->commonLineThreshold, $this->contextLines);
  92. $hunkCapture = false;
  93. $sameCount = $toRange = $fromRange = 0;
  94. $toStart = $fromStart = 1;
  95. foreach ($diff as $i => $entry) {
  96. if (0 === $entry[1]) { // same
  97. if (false === $hunkCapture) {
  98. ++$fromStart;
  99. ++$toStart;
  100. continue;
  101. }
  102. ++$sameCount;
  103. ++$toRange;
  104. ++$fromRange;
  105. if ($sameCount === $cutOff) {
  106. $contextStartOffset = ($hunkCapture - $this->contextLines) < 0
  107. ? $hunkCapture
  108. : $this->contextLines
  109. ;
  110. // note: $contextEndOffset = $this->contextLines;
  111. //
  112. // because we never go beyond the end of the diff.
  113. // with the cutoff/contextlines here the follow is never true;
  114. //
  115. // if ($i - $cutOff + $this->contextLines + 1 > \count($diff)) {
  116. // $contextEndOffset = count($diff) - 1;
  117. // }
  118. //
  119. // ; that would be true for a trailing incomplete hunk case which is dealt with after this loop
  120. $this->writeHunk(
  121. $diff,
  122. $hunkCapture - $contextStartOffset,
  123. $i - $cutOff + $this->contextLines + 1,
  124. $fromStart - $contextStartOffset,
  125. $fromRange - $cutOff + $contextStartOffset + $this->contextLines,
  126. $toStart - $contextStartOffset,
  127. $toRange - $cutOff + $contextStartOffset + $this->contextLines,
  128. $output
  129. );
  130. $fromStart += $fromRange;
  131. $toStart += $toRange;
  132. $hunkCapture = false;
  133. $sameCount = $toRange = $fromRange = 0;
  134. }
  135. continue;
  136. }
  137. $sameCount = 0;
  138. if ($entry[1] === Differ::NO_LINE_END_EOF_WARNING) {
  139. continue;
  140. }
  141. if (false === $hunkCapture) {
  142. $hunkCapture = $i;
  143. }
  144. if (Differ::ADDED === $entry[1]) {
  145. ++$toRange;
  146. }
  147. if (Differ::REMOVED === $entry[1]) {
  148. ++$fromRange;
  149. }
  150. }
  151. if (false === $hunkCapture) {
  152. return;
  153. }
  154. // we end here when cutoff (commonLineThreshold) was not reached, but we where capturing a hunk,
  155. // do not render hunk till end automatically because the number of context lines might be less than the commonLineThreshold
  156. $contextStartOffset = $hunkCapture - $this->contextLines < 0
  157. ? $hunkCapture
  158. : $this->contextLines
  159. ;
  160. // prevent trying to write out more common lines than there are in the diff _and_
  161. // do not write more than configured through the context lines
  162. $contextEndOffset = \min($sameCount, $this->contextLines);
  163. $fromRange -= $sameCount;
  164. $toRange -= $sameCount;
  165. $this->writeHunk(
  166. $diff,
  167. $hunkCapture - $contextStartOffset,
  168. $i - $sameCount + $contextEndOffset + 1,
  169. $fromStart - $contextStartOffset,
  170. $fromRange + $contextStartOffset + $contextEndOffset,
  171. $toStart - $contextStartOffset,
  172. $toRange + $contextStartOffset + $contextEndOffset,
  173. $output
  174. );
  175. }
  176. private function writeHunk(
  177. array $diff,
  178. int $diffStartIndex,
  179. int $diffEndIndex,
  180. int $fromStart,
  181. int $fromRange,
  182. int $toStart,
  183. int $toRange,
  184. $output
  185. ): void {
  186. if ($this->addLineNumbers) {
  187. \fwrite($output, '@@ -' . $fromStart);
  188. if (!$this->collapseRanges || 1 !== $fromRange) {
  189. \fwrite($output, ',' . $fromRange);
  190. }
  191. \fwrite($output, ' +' . $toStart);
  192. if (!$this->collapseRanges || 1 !== $toRange) {
  193. \fwrite($output, ',' . $toRange);
  194. }
  195. \fwrite($output, " @@\n");
  196. } else {
  197. \fwrite($output, "@@ @@\n");
  198. }
  199. for ($i = $diffStartIndex; $i < $diffEndIndex; ++$i) {
  200. if ($diff[$i][1] === Differ::ADDED) {
  201. \fwrite($output, '+' . $diff[$i][0]);
  202. } elseif ($diff[$i][1] === Differ::REMOVED) {
  203. \fwrite($output, '-' . $diff[$i][0]);
  204. } elseif ($diff[$i][1] === Differ::OLD) {
  205. \fwrite($output, ' ' . $diff[$i][0]);
  206. } elseif ($diff[$i][1] === Differ::NO_LINE_END_EOF_WARNING) {
  207. \fwrite($output, "\n"); // $diff[$i][0]
  208. } else { /* Not changed (old) Differ::OLD or Warning Differ::DIFF_LINE_END_WARNING */
  209. \fwrite($output, ' ' . $diff[$i][0]);
  210. }
  211. }
  212. }
  213. }