StrictUnifiedDiffOutputBuilder::writeDiffHunks()   F
last analyzed

Complexity

Conditions 18
Paths 210

Size

Total Lines 137
Code Lines 73

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 73
c 1
b 0
f 0
dl 0
loc 137
rs 3.9083
cc 18
nc 210
nop 2

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
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