Parser::doParse()   F
last analyzed

Complexity

Conditions 64
Paths 1976

Size

Total Lines 203
Code Lines 116

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 116
dl 0
loc 203
rs 0
c 0
b 0
f 0
cc 64
nc 1976
nop 4

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
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\Yaml;
13
14
use Symfony\Component\Yaml\Exception\ParseException;
15
16
/**
17
 * Parser parses YAML strings to convert them to PHP arrays.
18
 *
19
 * @author Fabien Potencier <[email protected]>
20
 */
21
class Parser
22
{
23
    const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
24
    // BC - wrongly named
25
    const FOLDED_SCALAR_PATTERN = self::BLOCK_SCALAR_HEADER_PATTERN;
26
27
    private $offset = 0;
28
    private $totalNumberOfLines;
29
    private $lines = array();
30
    private $currentLineNb = -1;
31
    private $currentLine = '';
32
    private $refs = array();
33
    private $skippedLineNumbers = array();
34
    private $locallySkippedLineNumbers = array();
35
36
    /**
37
     * @param int      $offset             The offset of YAML document (used for line numbers in error messages)
38
     * @param int|null $totalNumberOfLines The overall number of lines being parsed
39
     * @param int[]    $skippedLineNumbers Number of comment lines that have been skipped by the parser
40
     */
41
    public function __construct($offset = 0, $totalNumberOfLines = null, array $skippedLineNumbers = array())
42
    {
43
        $this->offset = $offset;
44
        $this->totalNumberOfLines = $totalNumberOfLines;
45
        $this->skippedLineNumbers = $skippedLineNumbers;
46
    }
47
48
    /**
49
     * Parses a YAML string to a PHP value.
50
     *
51
     * @param string $value                  A YAML string
52
     * @param bool   $exceptionOnInvalidType True if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
53
     * @param bool   $objectSupport          True if object support is enabled, false otherwise
54
     * @param bool   $objectForMap           True if maps should return a stdClass instead of array()
55
     *
56
     * @return mixed A PHP value
57
     *
58
     * @throws ParseException If the YAML is not valid
59
     */
60
    public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
61
    {
62
        if (false === preg_match('//u', $value)) {
63
            throw new ParseException('The YAML value does not appear to be valid UTF-8.');
64
        }
65
66
        $this->refs = array();
67
68
        $mbEncoding = null;
69
        $e = null;
70
        $data = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
71
72
        if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
73
            $mbEncoding = mb_internal_encoding();
74
            mb_internal_encoding('UTF-8');
75
        }
76
77
        try {
78
            $data = $this->doParse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
79
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
80
        } catch (\Throwable $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
81
        }
82
83
        if (null !== $mbEncoding) {
84
            mb_internal_encoding($mbEncoding);
0 ignored issues
show
Bug introduced by
It seems like $mbEncoding can also be of type true; however, parameter $encoding of mb_internal_encoding() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

84
            mb_internal_encoding(/** @scrutinizer ignore-type */ $mbEncoding);
Loading history...
85
        }
86
87
        $this->lines = array();
88
        $this->currentLine = '';
89
        $this->refs = array();
90
        $this->skippedLineNumbers = array();
91
        $this->locallySkippedLineNumbers = array();
92
93
        if (null !== $e) {
94
            throw $e;
95
        }
96
97
        return $data;
98
    }
99
100
    private function doParse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
101
    {
102
        $this->currentLineNb = -1;
103
        $this->currentLine = '';
104
        $value = $this->cleanup($value);
105
        $this->lines = explode("\n", $value);
106
        $this->locallySkippedLineNumbers = array();
107
108
        if (null === $this->totalNumberOfLines) {
109
            $this->totalNumberOfLines = \count($this->lines);
110
        }
111
112
        $data = array();
113
        $context = null;
114
        $allowOverwrite = false;
115
116
        while ($this->moveToNextLine()) {
117
            if ($this->isCurrentLineEmpty()) {
118
                continue;
119
            }
120
121
            // tab?
122
            if ("\t" === $this->currentLine[0]) {
123
                throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
124
            }
125
126
            $isRef = $mergeNode = false;
127
            if (self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) {
128
                if ($context && 'mapping' == $context) {
129
                    throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine);
130
                }
131
                $context = 'sequence';
132
133
                if (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
134
                    $isRef = $matches['ref'];
135
                    $values['value'] = $matches['value'];
136
                }
137
138
                // array
139
                if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
140
                    $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap);
141
                } else {
142
                    if (isset($values['leadspaces'])
143
                        && self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($values['value']), $matches)
144
                    ) {
145
                        // this is a compact notation element, add to next block and parse
146
                        $block = $values['value'];
147
                        if ($this->isNextLineIndented()) {
148
                            $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + \strlen($values['leadspaces']) + 1);
149
                        }
150
151
                        $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $exceptionOnInvalidType, $objectSupport, $objectForMap);
152
                    } else {
153
                        $data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
154
                    }
155
                }
156
                if ($isRef) {
157
                    $this->refs[$isRef] = end($data);
158
                }
159
            } elseif (
160
                self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
161
                && (false === strpos($values['key'], ' #') || \in_array($values['key'][0], array('"', "'")))
162
            ) {
163
                if ($context && 'sequence' == $context) {
164
                    throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine);
165
                }
166
                $context = 'mapping';
167
168
                // force correct settings
169
                Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
170
                try {
171
                    $key = Inline::parseScalar($values['key']);
172
                } catch (ParseException $e) {
173
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
174
                    $e->setSnippet($this->currentLine);
175
176
                    throw $e;
177
                }
178
179
                // Convert float keys to strings, to avoid being converted to integers by PHP
180
                if (\is_float($key)) {
181
                    $key = (string) $key;
182
                }
183
184
                if ('<<' === $key && (!isset($values['value']) || !self::preg_match('#^&(?P<ref>[^ ]+)#u', $values['value'], $refMatches))) {
185
                    $mergeNode = true;
186
                    $allowOverwrite = true;
187
                    if (isset($values['value']) && 0 === strpos($values['value'], '*')) {
188
                        $refName = substr($values['value'], 1);
189
                        if (!array_key_exists($refName, $this->refs)) {
190
                            throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
191
                        }
192
193
                        $refValue = $this->refs[$refName];
194
195
                        if (!\is_array($refValue)) {
196
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
197
                        }
198
199
                        $data += $refValue; // array union
200
                    } else {
201
                        if (isset($values['value']) && '' !== $values['value']) {
202
                            $value = $values['value'];
203
                        } else {
204
                            $value = $this->getNextEmbedBlock();
205
                        }
206
                        $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
207
208
                        if (!\is_array($parsed)) {
209
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
210
                        }
211
212
                        if (isset($parsed[0])) {
213
                            // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
214
                            // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
215
                            // in the sequence override keys specified in later mapping nodes.
216
                            foreach ($parsed as $parsedItem) {
217
                                if (!\is_array($parsedItem)) {
218
                                    throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
219
                                }
220
221
                                $data += $parsedItem; // array union
222
                            }
223
                        } else {
224
                            // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
225
                            // current mapping, unless the key already exists in it.
226
                            $data += $parsed; // array union
227
                        }
228
                    }
229
                } elseif ('<<' !== $key && isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
230
                    $isRef = $matches['ref'];
231
                    $values['value'] = $matches['value'];
232
                }
233
234
                if ($mergeNode) {
235
                    // Merge keys
236
                } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#') || '<<' === $key) {
237
                    // hash
238
                    // if next line is less indented or equal, then it means that the current value is null
239
                    if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
240
                        // Spec: Keys MUST be unique; first one wins.
241
                        // But overwriting is allowed when a merge node is used in current block.
242
                        if ($allowOverwrite || !isset($data[$key])) {
243
                            $data[$key] = null;
244
                        }
245
                    } else {
246
                        $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap);
247
248
                        if ('<<' === $key) {
249
                            $this->refs[$refMatches['ref']] = $value;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $refMatches does not seem to be defined for all execution paths leading up to this point.
Loading history...
250
                            $data += $value;
251
                        } elseif ($allowOverwrite || !isset($data[$key])) {
252
                            // Spec: Keys MUST be unique; first one wins.
253
                            // But overwriting is allowed when a merge node is used in current block.
254
                            $data[$key] = $value;
255
                        }
256
                    }
257
                } else {
258
                    $value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
259
                    // Spec: Keys MUST be unique; first one wins.
260
                    // But overwriting is allowed when a merge node is used in current block.
261
                    if ($allowOverwrite || !isset($data[$key])) {
262
                        $data[$key] = $value;
263
                    }
264
                }
265
                if ($isRef) {
266
                    $this->refs[$isRef] = $data[$key];
267
                }
268
            } else {
269
                // multiple documents are not supported
270
                if ('---' === $this->currentLine) {
271
                    throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine);
272
                }
273
274
                // 1-liner optionally followed by newline(s)
275
                if (\is_string($value) && $this->lines[0] === trim($value)) {
276
                    try {
277
                        $value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
278
                    } catch (ParseException $e) {
279
                        $e->setParsedLine($this->getRealCurrentLineNb() + 1);
280
                        $e->setSnippet($this->currentLine);
281
282
                        throw $e;
283
                    }
284
285
                    return $value;
286
                }
287
288
                throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
289
            }
290
        }
291
292
        if ($objectForMap && !\is_object($data) && 'mapping' === $context) {
293
            $object = new \stdClass();
294
295
            foreach ($data as $key => $value) {
0 ignored issues
show
introduced by
$value is overwriting one of the parameters of this function.
Loading history...
296
                $object->$key = $value;
297
            }
298
299
            $data = $object;
300
        }
301
302
        return empty($data) ? null : $data;
303
    }
304
305
    private function parseBlock($offset, $yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap)
306
    {
307
        $skippedLineNumbers = $this->skippedLineNumbers;
308
309
        foreach ($this->locallySkippedLineNumbers as $lineNumber) {
310
            if ($lineNumber < $offset) {
311
                continue;
312
            }
313
314
            $skippedLineNumbers[] = $lineNumber;
315
        }
316
317
        $parser = new self($offset, $this->totalNumberOfLines, $skippedLineNumbers);
318
        $parser->refs = &$this->refs;
319
320
        return $parser->doParse($yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap);
321
    }
322
323
    /**
324
     * Returns the current line number (takes the offset into account).
325
     *
326
     * @return int The current line number
327
     */
328
    private function getRealCurrentLineNb()
329
    {
330
        $realCurrentLineNumber = $this->currentLineNb + $this->offset;
331
332
        foreach ($this->skippedLineNumbers as $skippedLineNumber) {
333
            if ($skippedLineNumber > $realCurrentLineNumber) {
334
                break;
335
            }
336
337
            ++$realCurrentLineNumber;
338
        }
339
340
        return $realCurrentLineNumber;
341
    }
342
343
    /**
344
     * Returns the current line indentation.
345
     *
346
     * @return int The current line indentation
347
     */
348
    private function getCurrentLineIndentation()
349
    {
350
        return \strlen($this->currentLine) - \strlen(ltrim($this->currentLine, ' '));
351
    }
352
353
    /**
354
     * Returns the next embed block of YAML.
355
     *
356
     * @param int  $indentation The indent level at which the block is to be read, or null for default
357
     * @param bool $inSequence  True if the enclosing data structure is a sequence
358
     *
359
     * @return string A YAML string
360
     *
361
     * @throws ParseException When indentation problem are detected
362
     */
363
    private function getNextEmbedBlock($indentation = null, $inSequence = false)
364
    {
365
        $oldLineIndentation = $this->getCurrentLineIndentation();
366
        $blockScalarIndentations = array();
367
368
        if ($this->isBlockScalarHeader()) {
369
            $blockScalarIndentations[] = $this->getCurrentLineIndentation();
370
        }
371
372
        if (!$this->moveToNextLine()) {
373
            return;
374
        }
375
376
        if (null === $indentation) {
377
            $newIndent = $this->getCurrentLineIndentation();
378
379
            $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
380
381
            if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
382
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
383
            }
384
        } else {
385
            $newIndent = $indentation;
386
        }
387
388
        $data = array();
389
        if ($this->getCurrentLineIndentation() >= $newIndent) {
390
            $data[] = substr($this->currentLine, $newIndent);
391
        } else {
392
            $this->moveToPreviousLine();
393
394
            return;
395
        }
396
397
        if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
398
            // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
399
            // and therefore no nested list or mapping
400
            $this->moveToPreviousLine();
401
402
            return;
403
        }
404
405
        $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
406
407
        if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) {
408
            $blockScalarIndentations[] = $this->getCurrentLineIndentation();
409
        }
410
411
        $previousLineIndentation = $this->getCurrentLineIndentation();
412
413
        while ($this->moveToNextLine()) {
414
            $indent = $this->getCurrentLineIndentation();
415
416
            // terminate all block scalars that are more indented than the current line
417
            if (!empty($blockScalarIndentations) && $indent < $previousLineIndentation && '' !== trim($this->currentLine)) {
418
                foreach ($blockScalarIndentations as $key => $blockScalarIndentation) {
419
                    if ($blockScalarIndentation >= $this->getCurrentLineIndentation()) {
420
                        unset($blockScalarIndentations[$key]);
421
                    }
422
                }
423
            }
424
425
            if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) {
426
                $blockScalarIndentations[] = $this->getCurrentLineIndentation();
427
            }
428
429
            $previousLineIndentation = $indent;
430
431
            if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
432
                $this->moveToPreviousLine();
433
                break;
434
            }
435
436
            if ($this->isCurrentLineBlank()) {
437
                $data[] = substr($this->currentLine, $newIndent);
438
                continue;
439
            }
440
441
            // we ignore "comment" lines only when we are not inside a scalar block
442
            if (empty($blockScalarIndentations) && $this->isCurrentLineComment()) {
443
                // remember ignored comment lines (they are used later in nested
444
                // parser calls to determine real line numbers)
445
                //
446
                // CAUTION: beware to not populate the global property here as it
447
                // will otherwise influence the getRealCurrentLineNb() call here
448
                // for consecutive comment lines and subsequent embedded blocks
449
                $this->locallySkippedLineNumbers[] = $this->getRealCurrentLineNb();
450
451
                continue;
452
            }
453
454
            if ($indent >= $newIndent) {
455
                $data[] = substr($this->currentLine, $newIndent);
456
            } elseif (0 == $indent) {
457
                $this->moveToPreviousLine();
458
459
                break;
460
            } else {
461
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
462
            }
463
        }
464
465
        return implode("\n", $data);
466
    }
467
468
    /**
469
     * Moves the parser to the next line.
470
     *
471
     * @return bool
472
     */
473
    private function moveToNextLine()
474
    {
475
        if ($this->currentLineNb >= \count($this->lines) - 1) {
476
            return false;
477
        }
478
479
        $this->currentLine = $this->lines[++$this->currentLineNb];
480
481
        return true;
482
    }
483
484
    /**
485
     * Moves the parser to the previous line.
486
     *
487
     * @return bool
488
     */
489
    private function moveToPreviousLine()
490
    {
491
        if ($this->currentLineNb < 1) {
492
            return false;
493
        }
494
495
        $this->currentLine = $this->lines[--$this->currentLineNb];
496
497
        return true;
498
    }
499
500
    /**
501
     * Parses a YAML value.
502
     *
503
     * @param string $value                  A YAML value
504
     * @param bool   $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise
505
     * @param bool   $objectSupport          True if object support is enabled, false otherwise
506
     * @param bool   $objectForMap           True if maps should return a stdClass instead of array()
507
     * @param string $context                The parser context (either sequence or mapping)
508
     *
509
     * @return mixed A PHP value
510
     *
511
     * @throws ParseException When reference does not exist
512
     */
513
    private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $context)
514
    {
515
        if (0 === strpos($value, '*')) {
516
            if (false !== $pos = strpos($value, '#')) {
517
                $value = substr($value, 1, $pos - 2);
518
            } else {
519
                $value = substr($value, 1);
520
            }
521
522
            if (!array_key_exists($value, $this->refs)) {
523
                throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine);
524
            }
525
526
            return $this->refs[$value];
527
        }
528
529
        if (self::preg_match('/^'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
530
            $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
531
532
            return $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers));
0 ignored issues
show
Bug introduced by
It seems like $modifiers can also be of type string; however, parameter $num of abs() does only seem to accept double|integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

532
            return $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs(/** @scrutinizer ignore-type */ $modifiers));
Loading history...
533
        }
534
535
        try {
536
            $parsedValue = Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
537
538
            if ('mapping' === $context && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
0 ignored issues
show
Bug introduced by
It seems like $parsedValue can also be of type array and array; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

538
            if ('mapping' === $context && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos(/** @scrutinizer ignore-type */ $parsedValue, ': ')) {
Loading history...
539
                @trigger_error(sprintf('Using a colon in the unquoted mapping value "%s" in line %d is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.', $value, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
540
541
                // to be thrown in 3.0
542
                // throw new ParseException('A colon cannot be used in an unquoted mapping value.');
543
            }
544
545
            return $parsedValue;
546
        } catch (ParseException $e) {
547
            $e->setParsedLine($this->getRealCurrentLineNb() + 1);
548
            $e->setSnippet($this->currentLine);
549
550
            throw $e;
551
        }
552
    }
553
554
    /**
555
     * Parses a block scalar.
556
     *
557
     * @param string $style       The style indicator that was used to begin this block scalar (| or >)
558
     * @param string $chomping    The chomping indicator that was used to begin this block scalar (+ or -)
559
     * @param int    $indentation The indentation indicator that was used to begin this block scalar
560
     *
561
     * @return string The text value
562
     */
563
    private function parseBlockScalar($style, $chomping = '', $indentation = 0)
564
    {
565
        $notEOF = $this->moveToNextLine();
566
        if (!$notEOF) {
567
            return '';
568
        }
569
570
        $isCurrentLineBlank = $this->isCurrentLineBlank();
571
        $blockLines = array();
572
573
        // leading blank lines are consumed before determining indentation
574
        while ($notEOF && $isCurrentLineBlank) {
575
            // newline only if not EOF
576
            if ($notEOF = $this->moveToNextLine()) {
577
                $blockLines[] = '';
578
                $isCurrentLineBlank = $this->isCurrentLineBlank();
579
            }
580
        }
581
582
        // determine indentation if not specified
583
        if (0 === $indentation) {
584
            if (self::preg_match('/^ +/', $this->currentLine, $matches)) {
585
                $indentation = \strlen($matches[0]);
586
            }
587
        }
588
589
        if ($indentation > 0) {
590
            $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
591
592
            while (
593
                $notEOF && (
594
                    $isCurrentLineBlank ||
595
                    self::preg_match($pattern, $this->currentLine, $matches)
596
                )
597
            ) {
598
                if ($isCurrentLineBlank && \strlen($this->currentLine) > $indentation) {
599
                    $blockLines[] = substr($this->currentLine, $indentation);
600
                } elseif ($isCurrentLineBlank) {
601
                    $blockLines[] = '';
602
                } else {
603
                    $blockLines[] = $matches[1];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $matches does not seem to be defined for all execution paths leading up to this point.
Loading history...
604
                }
605
606
                // newline only if not EOF
607
                if ($notEOF = $this->moveToNextLine()) {
608
                    $isCurrentLineBlank = $this->isCurrentLineBlank();
609
                }
610
            }
611
        } elseif ($notEOF) {
612
            $blockLines[] = '';
613
        }
614
615
        if ($notEOF) {
616
            $blockLines[] = '';
617
            $this->moveToPreviousLine();
618
        } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
619
            $blockLines[] = '';
620
        }
621
622
        // folded style
623
        if ('>' === $style) {
624
            $text = '';
625
            $previousLineIndented = false;
626
            $previousLineBlank = false;
627
628
            for ($i = 0, $blockLinesCount = \count($blockLines); $i < $blockLinesCount; ++$i) {
629
                if ('' === $blockLines[$i]) {
630
                    $text .= "\n";
631
                    $previousLineIndented = false;
632
                    $previousLineBlank = true;
633
                } elseif (' ' === $blockLines[$i][0]) {
634
                    $text .= "\n".$blockLines[$i];
635
                    $previousLineIndented = true;
636
                    $previousLineBlank = false;
637
                } elseif ($previousLineIndented) {
638
                    $text .= "\n".$blockLines[$i];
639
                    $previousLineIndented = false;
640
                    $previousLineBlank = false;
641
                } elseif ($previousLineBlank || 0 === $i) {
642
                    $text .= $blockLines[$i];
643
                    $previousLineIndented = false;
644
                    $previousLineBlank = false;
645
                } else {
646
                    $text .= ' '.$blockLines[$i];
647
                    $previousLineIndented = false;
648
                    $previousLineBlank = false;
649
                }
650
            }
651
        } else {
652
            $text = implode("\n", $blockLines);
653
        }
654
655
        // deal with trailing newlines
656
        if ('' === $chomping) {
657
            $text = preg_replace('/\n+$/', "\n", $text);
658
        } elseif ('-' === $chomping) {
659
            $text = preg_replace('/\n+$/', '', $text);
660
        }
661
662
        return $text;
663
    }
664
665
    /**
666
     * Returns true if the next line is indented.
667
     *
668
     * @return bool Returns true if the next line is indented, false otherwise
669
     */
670
    private function isNextLineIndented()
671
    {
672
        $currentIndentation = $this->getCurrentLineIndentation();
673
        $EOF = !$this->moveToNextLine();
674
675
        while (!$EOF && $this->isCurrentLineEmpty()) {
676
            $EOF = !$this->moveToNextLine();
677
        }
678
679
        if ($EOF) {
680
            return false;
681
        }
682
683
        $ret = $this->getCurrentLineIndentation() > $currentIndentation;
684
685
        $this->moveToPreviousLine();
686
687
        return $ret;
688
    }
689
690
    /**
691
     * Returns true if the current line is blank or if it is a comment line.
692
     *
693
     * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
694
     */
695
    private function isCurrentLineEmpty()
696
    {
697
        return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
698
    }
699
700
    /**
701
     * Returns true if the current line is blank.
702
     *
703
     * @return bool Returns true if the current line is blank, false otherwise
704
     */
705
    private function isCurrentLineBlank()
706
    {
707
        return '' == trim($this->currentLine, ' ');
708
    }
709
710
    /**
711
     * Returns true if the current line is a comment line.
712
     *
713
     * @return bool Returns true if the current line is a comment line, false otherwise
714
     */
715
    private function isCurrentLineComment()
716
    {
717
        //checking explicitly the first char of the trim is faster than loops or strpos
718
        $ltrimmedLine = ltrim($this->currentLine, ' ');
719
720
        return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0];
721
    }
722
723
    private function isCurrentLineLastLineInDocument()
724
    {
725
        return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1);
726
    }
727
728
    /**
729
     * Cleanups a YAML string to be parsed.
730
     *
731
     * @param string $value The input YAML string
732
     *
733
     * @return string A cleaned up YAML string
734
     */
735
    private function cleanup($value)
736
    {
737
        $value = str_replace(array("\r\n", "\r"), "\n", $value);
738
739
        // strip YAML header
740
        $count = 0;
741
        $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
742
        $this->offset += $count;
743
744
        // remove leading comments
745
        $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
746
        if (1 == $count) {
747
            // items have been removed, update the offset
748
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
749
            $value = $trimmedValue;
750
        }
751
752
        // remove start of the document marker (---)
753
        $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
754
        if (1 == $count) {
755
            // items have been removed, update the offset
756
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
757
            $value = $trimmedValue;
758
759
            // remove end of the document marker (...)
760
            $value = preg_replace('#\.\.\.\s*$#', '', $value);
761
        }
762
763
        return $value;
764
    }
765
766
    /**
767
     * Returns true if the next line starts unindented collection.
768
     *
769
     * @return bool Returns true if the next line starts unindented collection, false otherwise
770
     */
771
    private function isNextLineUnIndentedCollection()
772
    {
773
        $currentIndentation = $this->getCurrentLineIndentation();
774
        $notEOF = $this->moveToNextLine();
775
776
        while ($notEOF && $this->isCurrentLineEmpty()) {
777
            $notEOF = $this->moveToNextLine();
778
        }
779
780
        if (false === $notEOF) {
781
            return false;
782
        }
783
784
        $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
785
786
        $this->moveToPreviousLine();
787
788
        return $ret;
789
    }
790
791
    /**
792
     * Returns true if the string is un-indented collection item.
793
     *
794
     * @return bool Returns true if the string is un-indented collection item, false otherwise
795
     */
796
    private function isStringUnIndentedCollectionItem()
797
    {
798
        return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- ');
799
    }
800
801
    /**
802
     * Tests whether or not the current line is the header of a block scalar.
803
     *
804
     * @return bool
805
     */
806
    private function isBlockScalarHeader()
807
    {
808
        return (bool) self::preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine);
809
    }
810
811
    /**
812
     * A local wrapper for `preg_match` which will throw a ParseException if there
813
     * is an internal error in the PCRE engine.
814
     *
815
     * This avoids us needing to check for "false" every time PCRE is used
816
     * in the YAML engine
817
     *
818
     * @throws ParseException on a PCRE internal error
819
     *
820
     * @see preg_last_error()
821
     *
822
     * @internal
823
     */
824
    public static function preg_match($pattern, $subject, &$matches = null, $flags = 0, $offset = 0)
825
    {
826
        if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) {
827
            switch (preg_last_error()) {
828
                case PREG_INTERNAL_ERROR:
829
                    $error = 'Internal PCRE error.';
830
                    break;
831
                case PREG_BACKTRACK_LIMIT_ERROR:
832
                    $error = 'pcre.backtrack_limit reached.';
833
                    break;
834
                case PREG_RECURSION_LIMIT_ERROR:
835
                    $error = 'pcre.recursion_limit reached.';
836
                    break;
837
                case PREG_BAD_UTF8_ERROR:
838
                    $error = 'Malformed UTF-8 data.';
839
                    break;
840
                case PREG_BAD_UTF8_OFFSET_ERROR:
841
                    $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
842
                    break;
843
                default:
844
                    $error = 'Error.';
845
            }
846
847
            throw new ParseException($error);
848
        }
849
850
        return $ret;
851
    }
852
}
853