StrictUnifiedDiffOutputBuilder::writeHunk()   B
last analyzed

Complexity

Conditions 10
Paths 24

Size

Total Lines 36
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 19
nc 24
nop 8
dl 0
loc 36
rs 7.6666
c 0
b 0
f 0

How to fix   Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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