StrictUnifiedDiffOutputBuilder::writeHunk()   B
last analyzed

Complexity

Conditions 10
Paths 24

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 19
c 1
b 0
f 0
dl 0
loc 35
rs 7.6666
cc 10
nc 24
nop 8

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