Subversion-Projekte lars-tiefland.laravel_shop

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

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