UnifiedDiffOutputBuilder::writeDiffHunks()   F
last analyzed

Complexity

Conditions 18
Paths 210

Size

Total Lines 138
Code Lines 72

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 18
eloc 72
nc 210
nop 2
dl 0
loc 138
rs 3.9083
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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