StrictUnifiedDiffOutputBuilder::writeDiffHunks()   F
last analyzed

Complexity

Conditions 18
Paths 210

Size

Total Lines 140
Code Lines 73

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 18
eloc 73
nc 210
nop 2
dl 0
loc 140
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\ConfigurationException;
14
use SebastianBergmann\Diff\Differ;
15
16
/**
17
 * Strict Unified diff output builder.
18
 *
19
 * Generates (strict) Unified diff's (unidiffs) with hunks.
20
 */
21
final class StrictUnifiedDiffOutputBuilder implements DiffOutputBuilderInterface
22
{
23
    private static $default = [
24
        'collapseRanges'      => true, // ranges of length one are rendered with the trailing `,1`
25
        'commonLineThreshold' => 6,    // number of same lines before ending a new hunk and creating a new one (if needed)
26
        'contextLines'        => 3,    // like `diff:  -u, -U NUM, --unified[=NUM]`, for patch/git apply compatibility best to keep at least @ 3
27
        'fromFile'            => null,
28
        'fromFileDate'        => null,
29
        'toFile'              => null,
30
        'toFileDate'          => null,
31
    ];
32
    /**
33
     * @var bool
34
     */
35
    private $changed;
36
37
    /**
38
     * @var bool
39
     */
40
    private $collapseRanges;
41
42
    /**
43
     * @var int >= 0
44
     */
45
    private $commonLineThreshold;
46
47
    /**
48
     * @var string
49
     */
50
    private $header;
51
52
    /**
53
     * @var int >= 0
54
     */
55
    private $contextLines;
56
57
    public function __construct(array $options = [])
58
    {
59
        $options = \array_merge(self::$default, $options);
60
61
        if (!\is_bool($options['collapseRanges'])) {
62
            throw new ConfigurationException('collapseRanges', 'a bool', $options['collapseRanges']);
63
        }
64
65
        if (!\is_int($options['contextLines']) || $options['contextLines'] < 0) {
66
            throw new ConfigurationException('contextLines', 'an int >= 0', $options['contextLines']);
67
        }
68
69
        if (!\is_int($options['commonLineThreshold']) || $options['commonLineThreshold'] <= 0) {
70
            throw new ConfigurationException('commonLineThreshold', 'an int > 0', $options['commonLineThreshold']);
71
        }
72
73
        foreach (['fromFile', 'toFile'] as $option) {
74
            if (!\is_string($options[$option])) {
75
                throw new ConfigurationException($option, 'a string', $options[$option]);
76
            }
77
        }
78
79
        foreach (['fromFileDate', 'toFileDate'] as $option) {
80
            if (null !== $options[$option] && !\is_string($options[$option])) {
81
                throw new ConfigurationException($option, 'a string or <null>', $options[$option]);
82
            }
83
        }
84
85
        $this->header = \sprintf(
86
            "--- %s%s\n+++ %s%s\n",
87
            $options['fromFile'],
88
            null === $options['fromFileDate'] ? '' : "\t" . $options['fromFileDate'],
89
            $options['toFile'],
90
            null === $options['toFileDate'] ? '' : "\t" . $options['toFileDate']
91
        );
92
93
        $this->collapseRanges      = $options['collapseRanges'];
94
        $this->commonLineThreshold = $options['commonLineThreshold'];
95
        $this->contextLines        = $options['contextLines'];
96
    }
97
98
    public function getDiff(array $diff): string
99
    {
100
        if (0 === \count($diff)) {
101
            return '';
102
        }
103
104
        $this->changed = false;
105
106
        $buffer = \fopen('php://memory', 'r+b');
107
        \fwrite($buffer, $this->header);
108
109
        $this->writeDiffHunks($buffer, $diff);
110
111
        if (!$this->changed) {
112
            \fclose($buffer);
113
114
            return '';
115
        }
116
117
        $diff = \stream_get_contents($buffer, -1, 0);
118
119
        \fclose($buffer);
120
121
        // If the last char is not a linebreak: add it.
122
        // This might happen when both the `from` and `to` do not have a trailing linebreak
123
        $last = \substr($diff, -1);
124
125
        return "\n" !== $last && "\r" !== $last
126
            ? $diff . "\n"
127
            : $diff
128
        ;
129
    }
130
131
    private function writeDiffHunks($output, array $diff): void
132
    {
133
        // detect "No newline at end of file" and insert into `$diff` if needed
134
135
        $upperLimit = \count($diff);
136
137
        if (0 === $diff[$upperLimit - 1][1]) {
138
            $lc = \substr($diff[$upperLimit - 1][0], -1);
139
140
            if ("\n" !== $lc) {
141
                \array_splice($diff, $upperLimit, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]);
142
            }
143
        } else {
144
            // search back for the last `+` and `-` line,
145
            // check if has trailing linebreak, else add under it warning under it
146
            $toFind = [1 => true, 2 => true];
147
148
            for ($i = $upperLimit - 1; $i >= 0; --$i) {
149
                if (isset($toFind[$diff[$i][1]])) {
150
                    unset($toFind[$diff[$i][1]]);
151
                    $lc = \substr($diff[$i][0], -1);
152
153
                    if ("\n" !== $lc) {
154
                        \array_splice($diff, $i + 1, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]);
155
                    }
156
157
                    if (!\count($toFind)) {
158
                        break;
159
                    }
160
                }
161
            }
162
        }
163
164
        // write hunks to output buffer
165
166
        $cutOff      = \max($this->commonLineThreshold, $this->contextLines);
167
        $hunkCapture = false;
168
        $sameCount   = $toRange = $fromRange = 0;
169
        $toStart     = $fromStart = 1;
170
171
        foreach ($diff as $i => $entry) {
172
            if (0 === $entry[1]) { // same
173
                if (false === $hunkCapture) {
174
                    ++$fromStart;
175
                    ++$toStart;
176
177
                    continue;
178
                }
179
180
                ++$sameCount;
181
                ++$toRange;
182
                ++$fromRange;
183
184
                if ($sameCount === $cutOff) {
185
                    $contextStartOffset = ($hunkCapture - $this->contextLines) < 0
186
                        ? $hunkCapture
187
                        : $this->contextLines
188
                    ;
189
190
                    // note: $contextEndOffset = $this->contextLines;
191
                    //
192
                    // because we never go beyond the end of the diff.
193
                    // with the cutoff/contextlines here the follow is never true;
194
                    //
195
                    // if ($i - $cutOff + $this->contextLines + 1 > \count($diff)) {
196
                    //    $contextEndOffset = count($diff) - 1;
197
                    // }
198
                    //
199
                    // ; that would be true for a trailing incomplete hunk case which is dealt with after this loop
200
201
                    $this->writeHunk(
202
                        $diff,
203
                        $hunkCapture - $contextStartOffset,
204
                        $i - $cutOff + $this->contextLines + 1,
205
                        $fromStart - $contextStartOffset,
206
                        $fromRange - $cutOff + $contextStartOffset + $this->contextLines,
207
                        $toStart - $contextStartOffset,
208
                        $toRange - $cutOff + $contextStartOffset + $this->contextLines,
209
                        $output
210
                    );
211
212
                    $fromStart += $fromRange;
213
                    $toStart += $toRange;
214
215
                    $hunkCapture = false;
216
                    $sameCount   = $toRange = $fromRange = 0;
217
                }
218
219
                continue;
220
            }
221
222
            $sameCount = 0;
223
224
            if ($entry[1] === Differ::NO_LINE_END_EOF_WARNING) {
225
                continue;
226
            }
227
228
            $this->changed = true;
229
230
            if (false === $hunkCapture) {
231
                $hunkCapture = $i;
232
            }
233
234
            if (Differ::ADDED === $entry[1]) { // added
235
                ++$toRange;
236
            }
237
238
            if (Differ::REMOVED === $entry[1]) { // removed
239
                ++$fromRange;
240
            }
241
        }
242
243
        if (false === $hunkCapture) {
244
            return;
245
        }
246
247
        // we end here when cutoff (commonLineThreshold) was not reached, but we where capturing a hunk,
248
        // do not render hunk till end automatically because the number of context lines might be less than the commonLineThreshold
249
250
        $contextStartOffset = $hunkCapture - $this->contextLines < 0
251
            ? $hunkCapture
252
            : $this->contextLines
253
        ;
254
255
        // prevent trying to write out more common lines than there are in the diff _and_
256
        // do not write more than configured through the context lines
257
        $contextEndOffset = \min($sameCount, $this->contextLines);
258
259
        $fromRange -= $sameCount;
260
        $toRange -= $sameCount;
261
262
        $this->writeHunk(
263
            $diff,
264
            $hunkCapture - $contextStartOffset,
265
            $i - $sameCount + $contextEndOffset + 1,
266
            $fromStart - $contextStartOffset,
267
            $fromRange + $contextStartOffset + $contextEndOffset,
268
            $toStart - $contextStartOffset,
269
            $toRange + $contextStartOffset + $contextEndOffset,
270
            $output
271
        );
272
    }
273
274
    private function writeHunk(
275
        array $diff,
276
        int $diffStartIndex,
277
        int $diffEndIndex,
278
        int $fromStart,
279
        int $fromRange,
280
        int $toStart,
281
        int $toRange,
282
        $output
283
    ): void {
284
        \fwrite($output, '@@ -' . $fromStart);
285
286
        if (!$this->collapseRanges || 1 !== $fromRange) {
287
            \fwrite($output, ',' . $fromRange);
288
        }
289
290
        \fwrite($output, ' +' . $toStart);
291
292
        if (!$this->collapseRanges || 1 !== $toRange) {
293
            \fwrite($output, ',' . $toRange);
294
        }
295
296
        \fwrite($output, " @@\n");
297
298
        for ($i = $diffStartIndex; $i < $diffEndIndex; ++$i) {
299
            if ($diff[$i][1] === Differ::ADDED) {
300
                $this->changed = true;
301
                \fwrite($output, '+' . $diff[$i][0]);
302
            } elseif ($diff[$i][1] === Differ::REMOVED) {
303
                $this->changed = true;
304
                \fwrite($output, '-' . $diff[$i][0]);
305
            } elseif ($diff[$i][1] === Differ::OLD) {
306
                \fwrite($output, ' ' . $diff[$i][0]);
307
            } elseif ($diff[$i][1] === Differ::NO_LINE_END_EOF_WARNING) {
308
                $this->changed = true;
309
                \fwrite($output, $diff[$i][0]);
310
            }
311
            //} elseif ($diff[$i][1] === Differ::DIFF_LINE_END_WARNING) { // custom comment inserted by PHPUnit/diff package
312
                //  skip
313
            //} else {
314
                //  unknown/invalid
315
            //}
316
        }
317
    }
318
}
319