UnifiedDiffOutputBuilder::getDiff()   B
last analyzed

Complexity

Conditions 7
Paths 36

Size

Total Lines 26
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 13
c 1
b 0
f 0
dl 0
loc 26
rs 8.8333
cc 7
nc 36
nop 1
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\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($header = "--- Original\n+++ New\n", $addLineNumbers = false)
46
    {
47
        $this->header         = $header;
48
        $this->addLineNumbers = $addLineNumbers;
49
    }
50
51
    public function getDiff(array $diff)
52
    {
53
        $buffer = \fopen('php://memory', 'r+b');
54
55
        if ('' !== $this->header) {
56
            \fwrite($buffer, $this->header);
57
            if ("\n" !== \substr($this->header, -1, 1)) {
58
                \fwrite($buffer, "\n");
59
            }
60
        }
61
62
        if (0 !== \count($diff)) {
63
            $this->writeDiffHunks($buffer, $diff);
64
        }
65
66
        $diff = \stream_get_contents($buffer, -1, 0);
67
68
        \fclose($buffer);
69
70
        // If the diff is non-empty and a linebreak: add it.
71
        // This might happen when both the `from` and `to` do not have a trailing linebreak
72
        $last = \substr($diff, -1);
73
74
        return 0 !== \strlen($diff) && "\n" !== $last && "\r" !== $last
75
            ? $diff . "\n"
76
            : $diff
77
        ;
78
    }
79
80
    private function writeDiffHunks($output, array $diff)
81
    {
82
        // detect "No newline at end of file" and insert into `$diff` if needed
83
84
        $upperLimit = \count($diff);
85
86
        if (0 === $diff[$upperLimit - 1][1]) {
87
            $lc = \substr($diff[$upperLimit - 1][0], -1);
88
            if ("\n" !== $lc) {
89
                \array_splice($diff, $upperLimit, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]);
90
            }
91
        } else {
92
            // search back for the last `+` and `-` line,
93
            // check if has trailing linebreak, else add under it warning under it
94
            $toFind = [1 => true, 2 => true];
95
            for ($i = $upperLimit - 1; $i >= 0; --$i) {
96
                if (isset($toFind[$diff[$i][1]])) {
97
                    unset($toFind[$diff[$i][1]]);
98
                    $lc = \substr($diff[$i][0], -1);
99
                    if ("\n" !== $lc) {
100
                        \array_splice($diff, $i + 1, 0, [["\n\\ No newline at end of file\n", Differ::NO_LINE_END_EOF_WARNING]]);
101
                    }
102
103
                    if (!\count($toFind)) {
104
                        break;
105
                    }
106
                }
107
            }
108
        }
109
110
        // write hunks to output buffer
111
112
        $cutOff      = \max($this->commonLineThreshold, $this->contextLines);
113
        $hunkCapture = false;
114
        $sameCount   = $toRange   = $fromRange = 0;
115
        $toStart     = $fromStart = 1;
116
117
        foreach ($diff as $i => $entry) {
118
            if (0 === $entry[1]) { // same
119
                if (false === $hunkCapture) {
120
                    ++$fromStart;
121
                    ++$toStart;
122
123
                    continue;
124
                }
125
126
                ++$sameCount;
127
                ++$toRange;
128
                ++$fromRange;
129
130
                if ($sameCount === $cutOff) {
131
                    $contextStartOffset = ($hunkCapture - $this->contextLines) < 0
132
                        ? $hunkCapture
133
                        : $this->contextLines
134
                    ;
135
136
                    // note: $contextEndOffset = $this->contextLines;
137
                    //
138
                    // because we never go beyond the end of the diff.
139
                    // with the cutoff/contextlines here the follow is never true;
140
                    //
141
                    // if ($i - $cutOff + $this->contextLines + 1 > \count($diff)) {
142
                    //    $contextEndOffset = count($diff) - 1;
143
                    // }
144
                    //
145
                    // ; that would be true for a trailing incomplete hunk case which is dealt with after this loop
146
147
                    $this->writeHunk(
148
                        $diff,
149
                        $hunkCapture - $contextStartOffset,
150
                        $i - $cutOff + $this->contextLines + 1,
151
                        $fromStart - $contextStartOffset,
152
                        $fromRange - $cutOff + $contextStartOffset + $this->contextLines,
153
                        $toStart - $contextStartOffset,
154
                        $toRange - $cutOff + $contextStartOffset + $this->contextLines,
155
                        $output
156
                    );
157
158
                    $fromStart += $fromRange;
159
                    $toStart += $toRange;
160
161
                    $hunkCapture = false;
162
                    $sameCount   = $toRange = $fromRange = 0;
163
                }
164
165
                continue;
166
            }
167
168
            $sameCount = 0;
169
170
            if ($entry[1] === Differ::NO_LINE_END_EOF_WARNING) {
171
                continue;
172
            }
173
174
            if (false === $hunkCapture) {
175
                $hunkCapture = $i;
176
            }
177
178
            if (Differ::ADDED === $entry[1]) {
179
                ++$toRange;
180
            }
181
182
            if (Differ::REMOVED === $entry[1]) {
183
                ++$fromRange;
184
            }
185
        }
186
187
        if (false === $hunkCapture) {
188
            return;
189
        }
190
191
        // we end here when cutoff (commonLineThreshold) was not reached, but we where capturing a hunk,
192
        // do not render hunk till end automatically because the number of context lines might be less than the commonLineThreshold
193
194
        $contextStartOffset = $hunkCapture - $this->contextLines < 0
195
            ? $hunkCapture
196
            : $this->contextLines
197
        ;
198
199
        // prevent trying to write out more common lines than there are in the diff _and_
200
        // do not write more than configured through the context lines
201
        $contextEndOffset = \min($sameCount, $this->contextLines);
202
203
        $fromRange -= $sameCount;
204
        $toRange -= $sameCount;
205
206
        $this->writeHunk(
207
            $diff,
208
            $hunkCapture - $contextStartOffset,
209
            $i - $sameCount + $contextEndOffset + 1,
210
            $fromStart - $contextStartOffset,
211
            $fromRange + $contextStartOffset + $contextEndOffset,
212
            $toStart - $contextStartOffset,
213
            $toRange + $contextStartOffset + $contextEndOffset,
214
            $output
215
        );
216
    }
217
218
    private function writeHunk(
219
        array $diff,
220
        $diffStartIndex,
221
        $diffEndIndex,
222
        $fromStart,
223
        $fromRange,
224
        $toStart,
225
        $toRange,
226
        $output
227
    ) {
228
        if ($this->addLineNumbers) {
229
            \fwrite($output, '@@ -' . $fromStart);
230
231
            if (!$this->collapseRanges || 1 !== $fromRange) {
232
                \fwrite($output, ',' . $fromRange);
233
            }
234
235
            \fwrite($output, ' +' . $toStart);
236
            if (!$this->collapseRanges || 1 !== $toRange) {
237
                \fwrite($output, ',' . $toRange);
238
            }
239
240
            \fwrite($output, " @@\n");
241
        } else {
242
            \fwrite($output, "@@ @@\n");
243
        }
244
245
        for ($i = $diffStartIndex; $i < $diffEndIndex; ++$i) {
246
            if ($diff[$i][1] === Differ::ADDED) {
247
                \fwrite($output, '+' . $diff[$i][0]);
248
            } elseif ($diff[$i][1] === Differ::REMOVED) {
249
                \fwrite($output, '-' . $diff[$i][0]);
250
            } elseif ($diff[$i][1] === Differ::OLD) {
251
                \fwrite($output, ' ' . $diff[$i][0]);
252
            } elseif ($diff[$i][1] === Differ::NO_LINE_END_EOF_WARNING) {
253
                \fwrite($output, "\n"); // $diff[$i][0]
254
            } else { /* Not changed (old) Differ::OLD or Warning Differ::DIFF_LINE_END_WARNING */
255
                \fwrite($output, ' ' . $diff[$i][0]);
256
            }
257
        }
258
    }
259
}
260