Completed
Branch development (b1b115)
by Johannes
10:28
created

Parser::doParse()   F

Complexity

Conditions 109
Paths > 20000

Size

Total Lines 321

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 321
rs 0
c 0
b 0
f 0
cc 109
nc 103492
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
/*
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
use Symfony\Component\Yaml\Tag\TaggedValue;
16
17
/**
18
 * Parser parses YAML strings to convert them to PHP arrays.
19
 *
20
 * @author Fabien Potencier <[email protected]>
21
 *
22
 * @final since version 3.4
23
 */
24
class Parser
25
{
26
    const TAG_PATTERN = '(?P<tag>![\w!.\/:-]+)';
27
    const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
28
29
    private $filename;
30
    private $offset = 0;
31
    private $totalNumberOfLines;
32
    private $lines = array();
33
    private $currentLineNb = -1;
34
    private $currentLine = '';
35
    private $refs = array();
36
    private $skippedLineNumbers = array();
37
    private $locallySkippedLineNumbers = array();
38
39
    public function __construct()
40
    {
41
        if (func_num_args() > 0) {
42
            @trigger_error(sprintf('The constructor arguments $offset, $totalNumberOfLines, $skippedLineNumbers of %s are deprecated and will be removed in 4.0', self::class), E_USER_DEPRECATED);
43
44
            $this->offset = func_get_arg(0);
45
            if (func_num_args() > 1) {
46
                $this->totalNumberOfLines = func_get_arg(1);
47
            }
48
            if (func_num_args() > 2) {
49
                $this->skippedLineNumbers = func_get_arg(2);
50
            }
51
        }
52
    }
53
54
    /**
55
     * Parses a YAML file into a PHP value.
56
     *
57
     * @param string $filename The path to the YAML file to be parsed
58
     * @param int    $flags    A bit field of PARSE_* constants to customize the YAML parser behavior
59
     *
60
     * @return mixed The YAML converted to a PHP value
61
     *
62
     * @throws ParseException If the file could not be read or the YAML is not valid
63
     */
64
    public function parseFile($filename, $flags = 0)
65
    {
66
        if (!is_file($filename)) {
67
            throw new ParseException(sprintf('File "%s" does not exist.', $filename));
68
        }
69
70
        if (!is_readable($filename)) {
71
            throw new ParseException(sprintf('File "%s" cannot be read.', $filename));
72
        }
73
74
        $this->filename = $filename;
75
76
        try {
77
            return $this->parse(file_get_contents($filename), $flags);
78
        } finally {
79
            $this->filename = null;
80
        }
81
    }
82
83
    /**
84
     * Parses a YAML string to a PHP value.
85
     *
86
     * @param string $value A YAML string
87
     * @param int    $flags A bit field of PARSE_* constants to customize the YAML parser behavior
88
     *
89
     * @return mixed A PHP value
90
     *
91
     * @throws ParseException If the YAML is not valid
92
     */
93
    public function parse($value, $flags = 0)
94
    {
95
        if (is_bool($flags)) {
96
            @trigger_error('Passing a boolean flag to toggle exception handling is deprecated since Symfony 3.1 and will be removed in 4.0. Use the Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE flag instead.', E_USER_DEPRECATED);
97
98
            if ($flags) {
99
                $flags = Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE;
100
            } else {
101
                $flags = 0;
102
            }
103
        }
104
105
        if (func_num_args() >= 3) {
106
            @trigger_error('Passing a boolean flag to toggle object support is deprecated since Symfony 3.1 and will be removed in 4.0. Use the Yaml::PARSE_OBJECT flag instead.', E_USER_DEPRECATED);
107
108
            if (func_get_arg(2)) {
109
                $flags |= Yaml::PARSE_OBJECT;
110
            }
111
        }
112
113
        if (func_num_args() >= 4) {
114
            @trigger_error('Passing a boolean flag to toggle object for map support is deprecated since Symfony 3.1 and will be removed in 4.0. Use the Yaml::PARSE_OBJECT_FOR_MAP flag instead.', E_USER_DEPRECATED);
115
116
            if (func_get_arg(3)) {
117
                $flags |= Yaml::PARSE_OBJECT_FOR_MAP;
118
            }
119
        }
120
121
        if (Yaml::PARSE_KEYS_AS_STRINGS & $flags) {
122
            @trigger_error('Using the Yaml::PARSE_KEYS_AS_STRINGS flag is deprecated since Symfony 3.4 as it will be removed in 4.0. Quote your keys when they are evaluable instead.', E_USER_DEPRECATED);
123
        }
124
125
        if (false === preg_match('//u', $value)) {
126
            throw new ParseException('The YAML value does not appear to be valid UTF-8.', -1, null, $this->filename);
127
        }
128
129
        $this->refs = array();
130
131
        $mbEncoding = null;
132
        $e = null;
133
        $data = null;
134
135
        if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
136
            $mbEncoding = mb_internal_encoding();
137
            mb_internal_encoding('UTF-8');
138
        }
139
140
        try {
141
            $data = $this->doParse($value, $flags);
142
        } catch (\Exception $e) {
143
        } catch (\Throwable $e) {
144
        }
145
146
        if (null !== $mbEncoding) {
147
            mb_internal_encoding($mbEncoding);
148
        }
149
150
        $this->lines = array();
151
        $this->currentLine = '';
152
        $this->refs = array();
153
        $this->skippedLineNumbers = array();
154
        $this->locallySkippedLineNumbers = array();
155
156
        if (null !== $e) {
157
            throw $e;
158
        }
159
160
        return $data;
161
    }
162
163
    private function doParse($value, $flags)
164
    {
165
        $this->currentLineNb = -1;
166
        $this->currentLine = '';
167
        $value = $this->cleanup($value);
168
        $this->lines = explode("\n", $value);
169
        $this->locallySkippedLineNumbers = array();
170
171
        if (null === $this->totalNumberOfLines) {
172
            $this->totalNumberOfLines = count($this->lines);
173
        }
174
175
        if (!$this->moveToNextLine()) {
176
            return null;
177
        }
178
179
        $data = array();
180
        $context = null;
181
        $allowOverwrite = false;
182
183
        while ($this->isCurrentLineEmpty()) {
184
            if (!$this->moveToNextLine()) {
185
                return null;
186
            }
187
        }
188
189
        // Resolves the tag and returns if end of the document
190
        if (null !== ($tag = $this->getLineTag($this->currentLine, $flags, false)) && !$this->moveToNextLine()) {
191
            return new TaggedValue($tag, '');
192
        }
193
194
        do {
195
            if ($this->isCurrentLineEmpty()) {
196
                continue;
197
            }
198
199
            // tab?
200
            if ("\t" === $this->currentLine[0]) {
201
                throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
202
            }
203
204
            Inline::initialize($flags, $this->getRealCurrentLineNb(), $this->filename);
205
206
            $isRef = $mergeNode = false;
207
            if (self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) {
208
                if ($context && 'mapping' == $context) {
209
                    throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
210
                }
211
                $context = 'sequence';
212
213
                if (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
214
                    $isRef = $matches['ref'];
215
                    $values['value'] = $matches['value'];
216
                }
217
218
                if (isset($values['value'][1]) && '?' === $values['value'][0] && ' ' === $values['value'][1]) {
219
                    @trigger_error($this->getDeprecationMessage('Starting an unquoted string with a question mark followed by a space is deprecated since Symfony 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.'), E_USER_DEPRECATED);
220
                }
221
222
                // array
223
                if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
224
                    $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags);
225
                } elseif (null !== $subTag = $this->getLineTag(ltrim($values['value'], ' '), $flags)) {
226
                    $data[] = new TaggedValue(
227
                        $subTag,
228
                        $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags)
229
                    );
230
                } else {
231
                    if (isset($values['leadspaces'])
232
                        && self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->trimTag($values['value']), $matches)
233
                    ) {
234
                        // this is a compact notation element, add to next block and parse
235
                        $block = $values['value'];
236
                        if ($this->isNextLineIndented()) {
237
                            $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1);
238
                        }
239
240
                        $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $flags);
241
                    } else {
242
                        $data[] = $this->parseValue($values['value'], $flags, $context);
243
                    }
244
                }
245
                if ($isRef) {
246
                    $this->refs[$isRef] = end($data);
247
                }
248
            } elseif (
249
                self::preg_match('#^(?P<key>(?:![^\s]++\s++)?(?:'.Inline::REGEX_QUOTED_STRING.'|(?:!?!php/const:)?[^ \'"\[\{!].*?)) *\:(\s++(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
250
                && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))
251
            ) {
252
                if ($context && 'sequence' == $context) {
253
                    throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine, $this->filename);
254
                }
255
                $context = 'mapping';
256
257
                try {
258
                    $i = 0;
259
                    $evaluateKey = !(Yaml::PARSE_KEYS_AS_STRINGS & $flags);
260
261
                    // constants in key will be evaluated anyway
262
                    if (isset($values['key'][0]) && '!' === $values['key'][0] && Yaml::PARSE_CONSTANT & $flags) {
263
                        $evaluateKey = true;
264
                    }
265
266
                    $key = Inline::parseScalar($values['key'], 0, null, $i, $evaluateKey);
267
                } catch (ParseException $e) {
268
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
269
                    $e->setSnippet($this->currentLine);
270
271
                    throw $e;
272
                }
273
274
                if (!is_string($key) && !is_int($key)) {
275
                    $keyType = is_numeric($key) ? 'numeric key' : 'non-string key';
276
                    @trigger_error($this->getDeprecationMessage(sprintf('Implicit casting of %s to string is deprecated since Symfony 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0. Quote your evaluable mapping keys instead.', $keyType)), E_USER_DEPRECATED);
277
                }
278
279
                // Convert float keys to strings, to avoid being converted to integers by PHP
280
                if (is_float($key)) {
281
                    $key = (string) $key;
282
                }
283
284
                if ('<<' === $key && (!isset($values['value']) || !self::preg_match('#^&(?P<ref>[^ ]+)#u', $values['value'], $refMatches))) {
285
                    $mergeNode = true;
286
                    $allowOverwrite = true;
287
                    if (isset($values['value'][0]) && '*' === $values['value'][0]) {
288
                        $refName = substr(rtrim($values['value']), 1);
289
                        if (!array_key_exists($refName, $this->refs)) {
290
                            throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
291
                        }
292
293
                        $refValue = $this->refs[$refName];
294
295
                        if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $refValue instanceof \stdClass) {
296
                            $refValue = (array) $refValue;
297
                        }
298
299
                        if (!is_array($refValue)) {
300
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
301
                        }
302
303
                        $data += $refValue; // array union
304
                    } else {
305
                        if (isset($values['value']) && '' !== $values['value']) {
306
                            $value = $values['value'];
307
                        } else {
308
                            $value = $this->getNextEmbedBlock();
309
                        }
310
                        $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $flags);
311
312
                        if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsed instanceof \stdClass) {
313
                            $parsed = (array) $parsed;
314
                        }
315
316
                        if (!is_array($parsed)) {
317
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
318
                        }
319
320
                        if (isset($parsed[0])) {
321
                            // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
322
                            // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
323
                            // in the sequence override keys specified in later mapping nodes.
324
                            foreach ($parsed as $parsedItem) {
325
                                if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsedItem instanceof \stdClass) {
326
                                    $parsedItem = (array) $parsedItem;
327
                                }
328
329
                                if (!is_array($parsedItem)) {
330
                                    throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem, $this->filename);
331
                                }
332
333
                                $data += $parsedItem; // array union
334
                            }
335
                        } else {
336
                            // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
337
                            // current mapping, unless the key already exists in it.
338
                            $data += $parsed; // array union
339
                        }
340
                    }
341
                } elseif ('<<' !== $key && isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]++) *+(?P<value>.*)#u', $values['value'], $matches)) {
342
                    $isRef = $matches['ref'];
343
                    $values['value'] = $matches['value'];
344
                }
345
346
                $subTag = null;
347
                if ($mergeNode) {
348
                    // Merge keys
349
                } elseif (!isset($values['value']) || '' === $values['value'] || 0 === strpos($values['value'], '#') || (null !== $subTag = $this->getLineTag($values['value'], $flags)) || '<<' === $key) {
350
                    // hash
351
                    // if next line is less indented or equal, then it means that the current value is null
352
                    if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
353
                        // Spec: Keys MUST be unique; first one wins.
354
                        // But overwriting is allowed when a merge node is used in current block.
355
                        if ($allowOverwrite || !isset($data[$key])) {
356
                            if (null !== $subTag) {
357
                                $data[$key] = new TaggedValue($subTag, '');
358
                            } else {
359
                                $data[$key] = null;
360
                            }
361
                        } else {
362
                            @trigger_error($this->getDeprecationMessage(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since Symfony 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key)), E_USER_DEPRECATED);
363
                        }
364
                    } else {
365
                        $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $flags);
366
                        if ('<<' === $key) {
367
                            $this->refs[$refMatches['ref']] = $value;
368
369
                            if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $value instanceof \stdClass) {
370
                                $value = (array) $value;
371
                            }
372
373
                            $data += $value;
374
                        } elseif ($allowOverwrite || !isset($data[$key])) {
375
                            // Spec: Keys MUST be unique; first one wins.
376
                            // But overwriting is allowed when a merge node is used in current block.
377
                            if (null !== $subTag) {
378
                                $data[$key] = new TaggedValue($subTag, $value);
379
                            } else {
380
                                $data[$key] = $value;
381
                            }
382
                        } else {
383
                            @trigger_error($this->getDeprecationMessage(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since Symfony 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key)), E_USER_DEPRECATED);
384
                        }
385
                    }
386
                } else {
387
                    $value = $this->parseValue(rtrim($values['value']), $flags, $context);
388
                    // Spec: Keys MUST be unique; first one wins.
389
                    // But overwriting is allowed when a merge node is used in current block.
390
                    if ($allowOverwrite || !isset($data[$key])) {
391
                        $data[$key] = $value;
392
                    } else {
393
                        @trigger_error($this->getDeprecationMessage(sprintf('Duplicate key "%s" detected whilst parsing YAML. Silent handling of duplicate mapping keys in YAML is deprecated since Symfony 3.2 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.', $key)), E_USER_DEPRECATED);
394
                    }
395
                }
396
                if ($isRef) {
397
                    $this->refs[$isRef] = $data[$key];
398
                }
399
            } else {
400
                // multiple documents are not supported
401
                if ('---' === $this->currentLine) {
402
                    throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine, $this->filename);
403
                }
404
405
                if ($deprecatedUsage = (isset($this->currentLine[1]) && '?' === $this->currentLine[0] && ' ' === $this->currentLine[1])) {
406
                    @trigger_error($this->getDeprecationMessage('Starting an unquoted string with a question mark followed by a space is deprecated since Symfony 3.3 and will throw \Symfony\Component\Yaml\Exception\ParseException in 4.0.'), E_USER_DEPRECATED);
407
                }
408
409
                // 1-liner optionally followed by newline(s)
410
                if (is_string($value) && $this->lines[0] === trim($value)) {
411
                    try {
412
                        $value = Inline::parse($this->lines[0], $flags, $this->refs);
413
                    } catch (ParseException $e) {
414
                        $e->setParsedLine($this->getRealCurrentLineNb() + 1);
415
                        $e->setSnippet($this->currentLine);
416
417
                        throw $e;
418
                    }
419
420
                    return $value;
421
                }
422
423
                // try to parse the value as a multi-line string as a last resort
424
                if (0 === $this->currentLineNb) {
425
                    $previousLineWasNewline = false;
426
                    $previousLineWasTerminatedWithBackslash = false;
427
                    $value = '';
428
429
                    foreach ($this->lines as $line) {
430
                        // If the indentation is not consistent at offset 0, it is to be considered as a ParseError
431
                        if (0 === $this->offset && !$deprecatedUsage && isset($line[0]) && ' ' === $line[0]) {
432
                            throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
433
                        }
434
                        if ('' === trim($line)) {
435
                            $value .= "\n";
436
                        } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
437
                            $value .= ' ';
438
                        }
439
440
                        if ('' !== trim($line) && '\\' === substr($line, -1)) {
441
                            $value .= ltrim(substr($line, 0, -1));
442
                        } elseif ('' !== trim($line)) {
443
                            $value .= trim($line);
444
                        }
445
446
                        if ('' === trim($line)) {
447
                            $previousLineWasNewline = true;
448
                            $previousLineWasTerminatedWithBackslash = false;
449
                        } elseif ('\\' === substr($line, -1)) {
450
                            $previousLineWasNewline = false;
451
                            $previousLineWasTerminatedWithBackslash = true;
452
                        } else {
453
                            $previousLineWasNewline = false;
454
                            $previousLineWasTerminatedWithBackslash = false;
455
                        }
456
                    }
457
458
                    try {
459
                        return Inline::parse(trim($value));
460
                    } catch (ParseException $e) {
461
                        // fall-through to the ParseException thrown below
462
                    }
463
                }
464
465
                throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
466
            }
467
        } while ($this->moveToNextLine());
468
469
        if (null !== $tag) {
470
            $data = new TaggedValue($tag, $data);
471
        }
472
473
        if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && !is_object($data) && 'mapping' === $context) {
474
            $object = new \stdClass();
475
476
            foreach ($data as $key => $value) {
477
                $object->$key = $value;
478
            }
479
480
            $data = $object;
481
        }
482
483
        return empty($data) ? null : $data;
484
    }
485
486
    private function parseBlock($offset, $yaml, $flags)
487
    {
488
        $skippedLineNumbers = $this->skippedLineNumbers;
489
490
        foreach ($this->locallySkippedLineNumbers as $lineNumber) {
491
            if ($lineNumber < $offset) {
492
                continue;
493
            }
494
495
            $skippedLineNumbers[] = $lineNumber;
496
        }
497
498
        $parser = new self();
499
        $parser->offset = $offset;
500
        $parser->totalNumberOfLines = $this->totalNumberOfLines;
501
        $parser->skippedLineNumbers = $skippedLineNumbers;
502
        $parser->refs = &$this->refs;
503
504
        return $parser->doParse($yaml, $flags);
505
    }
506
507
    /**
508
     * Returns the current line number (takes the offset into account).
509
     *
510
     * @internal
511
     *
512
     * @return int The current line number
513
     */
514
    public function getRealCurrentLineNb()
515
    {
516
        $realCurrentLineNumber = $this->currentLineNb + $this->offset;
517
518
        foreach ($this->skippedLineNumbers as $skippedLineNumber) {
519
            if ($skippedLineNumber > $realCurrentLineNumber) {
520
                break;
521
            }
522
523
            ++$realCurrentLineNumber;
524
        }
525
526
        return $realCurrentLineNumber;
527
    }
528
529
    /**
530
     * Returns the current line indentation.
531
     *
532
     * @return int The current line indentation
533
     */
534
    private function getCurrentLineIndentation()
535
    {
536
        return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
537
    }
538
539
    /**
540
     * Returns the next embed block of YAML.
541
     *
542
     * @param int  $indentation The indent level at which the block is to be read, or null for default
543
     * @param bool $inSequence  True if the enclosing data structure is a sequence
544
     *
545
     * @return string A YAML string
546
     *
547
     * @throws ParseException When indentation problem are detected
548
     */
549
    private function getNextEmbedBlock($indentation = null, $inSequence = false)
550
    {
551
        $oldLineIndentation = $this->getCurrentLineIndentation();
552
        $blockScalarIndentations = array();
553
554
        if ($this->isBlockScalarHeader()) {
555
            $blockScalarIndentations[] = $oldLineIndentation;
556
        }
557
558
        if (!$this->moveToNextLine()) {
559
            return;
560
        }
561
562
        if (null === $indentation) {
563
            $newIndent = null;
564
            $movements = 0;
565
566
            do {
567
                $EOF = false;
568
569
                // empty and comment-like lines do not influence the indentation depth
570
                if ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
571
                    $EOF = !$this->moveToNextLine();
572
573
                    if (!$EOF) {
574
                        ++$movements;
575
                    }
576
                } else {
577
                    $newIndent = $this->getCurrentLineIndentation();
578
                }
579
            } while (!$EOF && null === $newIndent);
580
581
            for ($i = 0; $i < $movements; ++$i) {
582
                $this->moveToPreviousLine();
583
            }
584
585
            $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
586
587
            if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
588
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
589
            }
590
        } else {
591
            $newIndent = $indentation;
592
        }
593
594
        $data = array();
595
        if ($this->getCurrentLineIndentation() >= $newIndent) {
596
            $data[] = substr($this->currentLine, $newIndent);
597
        } elseif ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
598
            $data[] = $this->currentLine;
599
        } else {
600
            $this->moveToPreviousLine();
601
602
            return;
603
        }
604
605
        if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
606
            // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
607
            // and therefore no nested list or mapping
608
            $this->moveToPreviousLine();
609
610
            return;
611
        }
612
613
        $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
614
615
        if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) {
616
            $blockScalarIndentations[] = $this->getCurrentLineIndentation();
617
        }
618
619
        $previousLineIndentation = $this->getCurrentLineIndentation();
620
621
        while ($this->moveToNextLine()) {
622
            $indent = $this->getCurrentLineIndentation();
623
624
            // terminate all block scalars that are more indented than the current line
625
            if (!empty($blockScalarIndentations) && $indent < $previousLineIndentation && '' !== trim($this->currentLine)) {
626
                foreach ($blockScalarIndentations as $key => $blockScalarIndentation) {
627
                    if ($blockScalarIndentation >= $indent) {
628
                        unset($blockScalarIndentations[$key]);
629
                    }
630
                }
631
            }
632
633
            if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) {
634
                $blockScalarIndentations[] = $indent;
635
            }
636
637
            $previousLineIndentation = $indent;
638
639
            if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
640
                $this->moveToPreviousLine();
641
                break;
642
            }
643
644
            if ($this->isCurrentLineBlank()) {
645
                $data[] = substr($this->currentLine, $newIndent);
646
                continue;
647
            }
648
649
            if ($indent >= $newIndent) {
650
                $data[] = substr($this->currentLine, $newIndent);
651
            } elseif ($this->isCurrentLineComment()) {
652
                $data[] = $this->currentLine;
653
            } elseif (0 == $indent) {
654
                $this->moveToPreviousLine();
655
656
                break;
657
            } else {
658
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
659
            }
660
        }
661
662
        return implode("\n", $data);
663
    }
664
665
    /**
666
     * Moves the parser to the next line.
667
     *
668
     * @return bool
669
     */
670
    private function moveToNextLine()
671
    {
672
        if ($this->currentLineNb >= count($this->lines) - 1) {
673
            return false;
674
        }
675
676
        $this->currentLine = $this->lines[++$this->currentLineNb];
677
678
        return true;
679
    }
680
681
    /**
682
     * Moves the parser to the previous line.
683
     *
684
     * @return bool
685
     */
686
    private function moveToPreviousLine()
687
    {
688
        if ($this->currentLineNb < 1) {
689
            return false;
690
        }
691
692
        $this->currentLine = $this->lines[--$this->currentLineNb];
693
694
        return true;
695
    }
696
697
    /**
698
     * Parses a YAML value.
699
     *
700
     * @param string $value   A YAML value
701
     * @param int    $flags   A bit field of PARSE_* constants to customize the YAML parser behavior
702
     * @param string $context The parser context (either sequence or mapping)
703
     *
704
     * @return mixed A PHP value
705
     *
706
     * @throws ParseException When reference does not exist
707
     */
708
    private function parseValue($value, $flags, $context)
709
    {
710
        if (0 === strpos($value, '*')) {
711
            if (false !== $pos = strpos($value, '#')) {
712
                $value = substr($value, 1, $pos - 2);
713
            } else {
714
                $value = substr($value, 1);
715
            }
716
717
            if (!array_key_exists($value, $this->refs)) {
718
                throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine, $this->filename);
719
            }
720
721
            return $this->refs[$value];
722
        }
723
724
        if (self::preg_match('/^(?:'.self::TAG_PATTERN.' +)?'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
725
            $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
726
727
            $data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers));
728
729
            if ('' !== $matches['tag']) {
730
                if ('!!binary' === $matches['tag']) {
731
                    return Inline::evaluateBinaryScalar($data);
732
                } elseif ('tagged' === $matches['tag']) {
733
                    return new TaggedValue(substr($matches['tag'], 1), $data);
734
                } elseif ('!' !== $matches['tag']) {
735
                    @trigger_error($this->getDeprecationMessage(sprintf('Using the custom tag "%s" for the value "%s" is deprecated since Symfony 3.3. It will be replaced by an instance of %s in 4.0.', $matches['tag'], $data, TaggedValue::class)), E_USER_DEPRECATED);
736
                }
737
            }
738
739
            return $data;
740
        }
741
742
        try {
743
            $quotation = '' !== $value && ('"' === $value[0] || "'" === $value[0]) ? $value[0] : null;
744
745
            // do not take following lines into account when the current line is a quoted single line value
746
            if (null !== $quotation && self::preg_match('/^'.$quotation.'.*'.$quotation.'(\s*#.*)?$/', $value)) {
747
                return Inline::parse($value, $flags, $this->refs);
748
            }
749
750
            $lines = array();
751
752
            while ($this->moveToNextLine()) {
753
                // unquoted strings end before the first unindented line
754
                if (null === $quotation && 0 === $this->getCurrentLineIndentation()) {
755
                    $this->moveToPreviousLine();
756
757
                    break;
758
                }
759
760
                $lines[] = trim($this->currentLine);
761
762
                // quoted string values end with a line that is terminated with the quotation character
763
                if ('' !== $this->currentLine && substr($this->currentLine, -1) === $quotation) {
764
                    break;
765
                }
766
            }
767
768
            for ($i = 0, $linesCount = count($lines), $previousLineBlank = false; $i < $linesCount; ++$i) {
769
                if ('' === $lines[$i]) {
770
                    $value .= "\n";
771
                    $previousLineBlank = true;
772
                } elseif ($previousLineBlank) {
773
                    $value .= $lines[$i];
774
                    $previousLineBlank = false;
775
                } else {
776
                    $value .= ' '.$lines[$i];
777
                    $previousLineBlank = false;
778
                }
779
            }
780
781
            Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
782
783
            $parsedValue = Inline::parse($value, $flags, $this->refs);
784
785
            if ('mapping' === $context && is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
786
                throw new ParseException('A colon cannot be used in an unquoted mapping value.', $this->getRealCurrentLineNb() + 1, $value, $this->filename);
787
            }
788
789
            return $parsedValue;
790
        } catch (ParseException $e) {
791
            $e->setParsedLine($this->getRealCurrentLineNb() + 1);
792
            $e->setSnippet($this->currentLine);
793
794
            throw $e;
795
        }
796
    }
797
798
    /**
799
     * Parses a block scalar.
800
     *
801
     * @param string $style       The style indicator that was used to begin this block scalar (| or >)
802
     * @param string $chomping    The chomping indicator that was used to begin this block scalar (+ or -)
803
     * @param int    $indentation The indentation indicator that was used to begin this block scalar
804
     *
805
     * @return string The text value
806
     */
807
    private function parseBlockScalar($style, $chomping = '', $indentation = 0)
808
    {
809
        $notEOF = $this->moveToNextLine();
810
        if (!$notEOF) {
811
            return '';
812
        }
813
814
        $isCurrentLineBlank = $this->isCurrentLineBlank();
815
        $blockLines = array();
816
817
        // leading blank lines are consumed before determining indentation
818
        while ($notEOF && $isCurrentLineBlank) {
819
            // newline only if not EOF
820
            if ($notEOF = $this->moveToNextLine()) {
821
                $blockLines[] = '';
822
                $isCurrentLineBlank = $this->isCurrentLineBlank();
823
            }
824
        }
825
826
        // determine indentation if not specified
827
        if (0 === $indentation) {
828
            if (self::preg_match('/^ +/', $this->currentLine, $matches)) {
829
                $indentation = strlen($matches[0]);
830
            }
831
        }
832
833
        if ($indentation > 0) {
834
            $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
835
836
            while (
837
                $notEOF && (
838
                    $isCurrentLineBlank ||
839
                    self::preg_match($pattern, $this->currentLine, $matches)
840
                )
841
            ) {
842
                if ($isCurrentLineBlank && strlen($this->currentLine) > $indentation) {
843
                    $blockLines[] = substr($this->currentLine, $indentation);
844
                } elseif ($isCurrentLineBlank) {
845
                    $blockLines[] = '';
846
                } else {
847
                    $blockLines[] = $matches[1];
848
                }
849
850
                // newline only if not EOF
851
                if ($notEOF = $this->moveToNextLine()) {
852
                    $isCurrentLineBlank = $this->isCurrentLineBlank();
853
                }
854
            }
855
        } elseif ($notEOF) {
856
            $blockLines[] = '';
857
        }
858
859
        if ($notEOF) {
860
            $blockLines[] = '';
861
            $this->moveToPreviousLine();
862
        } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
863
            $blockLines[] = '';
864
        }
865
866
        // folded style
867
        if ('>' === $style) {
868
            $text = '';
869
            $previousLineIndented = false;
870
            $previousLineBlank = false;
871
872
            for ($i = 0, $blockLinesCount = count($blockLines); $i < $blockLinesCount; ++$i) {
873
                if ('' === $blockLines[$i]) {
874
                    $text .= "\n";
875
                    $previousLineIndented = false;
876
                    $previousLineBlank = true;
877
                } elseif (' ' === $blockLines[$i][0]) {
878
                    $text .= "\n".$blockLines[$i];
879
                    $previousLineIndented = true;
880
                    $previousLineBlank = false;
881
                } elseif ($previousLineIndented) {
882
                    $text .= "\n".$blockLines[$i];
883
                    $previousLineIndented = false;
884
                    $previousLineBlank = false;
885
                } elseif ($previousLineBlank || 0 === $i) {
886
                    $text .= $blockLines[$i];
887
                    $previousLineIndented = false;
888
                    $previousLineBlank = false;
889
                } else {
890
                    $text .= ' '.$blockLines[$i];
891
                    $previousLineIndented = false;
892
                    $previousLineBlank = false;
893
                }
894
            }
895
        } else {
896
            $text = implode("\n", $blockLines);
897
        }
898
899
        // deal with trailing newlines
900
        if ('' === $chomping) {
901
            $text = preg_replace('/\n+$/', "\n", $text);
902
        } elseif ('-' === $chomping) {
903
            $text = preg_replace('/\n+$/', '', $text);
904
        }
905
906
        return $text;
907
    }
908
909
    /**
910
     * Returns true if the next line is indented.
911
     *
912
     * @return bool Returns true if the next line is indented, false otherwise
913
     */
914
    private function isNextLineIndented()
915
    {
916
        $currentIndentation = $this->getCurrentLineIndentation();
917
        $movements = 0;
918
919
        do {
920
            $EOF = !$this->moveToNextLine();
921
922
            if (!$EOF) {
923
                ++$movements;
924
            }
925
        } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));
926
927
        if ($EOF) {
928
            return false;
929
        }
930
931
        $ret = $this->getCurrentLineIndentation() > $currentIndentation;
932
933
        for ($i = 0; $i < $movements; ++$i) {
934
            $this->moveToPreviousLine();
935
        }
936
937
        return $ret;
938
    }
939
940
    /**
941
     * Returns true if the current line is blank or if it is a comment line.
942
     *
943
     * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
944
     */
945
    private function isCurrentLineEmpty()
946
    {
947
        return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
948
    }
949
950
    /**
951
     * Returns true if the current line is blank.
952
     *
953
     * @return bool Returns true if the current line is blank, false otherwise
954
     */
955
    private function isCurrentLineBlank()
956
    {
957
        return '' == trim($this->currentLine, ' ');
958
    }
959
960
    /**
961
     * Returns true if the current line is a comment line.
962
     *
963
     * @return bool Returns true if the current line is a comment line, false otherwise
964
     */
965
    private function isCurrentLineComment()
966
    {
967
        //checking explicitly the first char of the trim is faster than loops or strpos
968
        $ltrimmedLine = ltrim($this->currentLine, ' ');
969
970
        return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0];
971
    }
972
973
    private function isCurrentLineLastLineInDocument()
974
    {
975
        return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1);
976
    }
977
978
    /**
979
     * Cleanups a YAML string to be parsed.
980
     *
981
     * @param string $value The input YAML string
982
     *
983
     * @return string A cleaned up YAML string
984
     */
985
    private function cleanup($value)
986
    {
987
        $value = str_replace(array("\r\n", "\r"), "\n", $value);
988
989
        // strip YAML header
990
        $count = 0;
991
        $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
992
        $this->offset += $count;
993
994
        // remove leading comments
995
        $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
996
        if (1 === $count) {
997
            // items have been removed, update the offset
998
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
999
            $value = $trimmedValue;
1000
        }
1001
1002
        // remove start of the document marker (---)
1003
        $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
1004
        if (1 === $count) {
1005
            // items have been removed, update the offset
1006
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
1007
            $value = $trimmedValue;
1008
1009
            // remove end of the document marker (...)
1010
            $value = preg_replace('#\.\.\.\s*$#', '', $value);
1011
        }
1012
1013
        return $value;
1014
    }
1015
1016
    /**
1017
     * Returns true if the next line starts unindented collection.
1018
     *
1019
     * @return bool Returns true if the next line starts unindented collection, false otherwise
1020
     */
1021
    private function isNextLineUnIndentedCollection()
1022
    {
1023
        $currentIndentation = $this->getCurrentLineIndentation();
1024
        $movements = 0;
1025
1026
        do {
1027
            $EOF = !$this->moveToNextLine();
1028
1029
            if (!$EOF) {
1030
                ++$movements;
1031
            }
1032
        } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));
1033
1034
        if ($EOF) {
1035
            return false;
1036
        }
1037
1038
        $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
1039
1040
        for ($i = 0; $i < $movements; ++$i) {
1041
            $this->moveToPreviousLine();
1042
        }
1043
1044
        return $ret;
1045
    }
1046
1047
    /**
1048
     * Returns true if the string is un-indented collection item.
1049
     *
1050
     * @return bool Returns true if the string is un-indented collection item, false otherwise
1051
     */
1052
    private function isStringUnIndentedCollectionItem()
1053
    {
1054
        return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- ');
1055
    }
1056
1057
    /**
1058
     * Tests whether or not the current line is the header of a block scalar.
1059
     *
1060
     * @return bool
1061
     */
1062
    private function isBlockScalarHeader()
1063
    {
1064
        return (bool) self::preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine);
1065
    }
1066
1067
    /**
1068
     * A local wrapper for `preg_match` which will throw a ParseException if there
1069
     * is an internal error in the PCRE engine.
1070
     *
1071
     * This avoids us needing to check for "false" every time PCRE is used
1072
     * in the YAML engine
1073
     *
1074
     * @throws ParseException on a PCRE internal error
1075
     *
1076
     * @see preg_last_error()
1077
     *
1078
     * @internal
1079
     */
1080
    public static function preg_match($pattern, $subject, &$matches = null, $flags = 0, $offset = 0)
1081
    {
1082
        if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) {
1083
            switch (preg_last_error()) {
1084
                case PREG_INTERNAL_ERROR:
1085
                    $error = 'Internal PCRE error.';
1086
                    break;
1087
                case PREG_BACKTRACK_LIMIT_ERROR:
1088
                    $error = 'pcre.backtrack_limit reached.';
1089
                    break;
1090
                case PREG_RECURSION_LIMIT_ERROR:
1091
                    $error = 'pcre.recursion_limit reached.';
1092
                    break;
1093
                case PREG_BAD_UTF8_ERROR:
1094
                    $error = 'Malformed UTF-8 data.';
1095
                    break;
1096
                case PREG_BAD_UTF8_OFFSET_ERROR:
1097
                    $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
1098
                    break;
1099
                default:
1100
                    $error = 'Error.';
1101
            }
1102
1103
            throw new ParseException($error);
1104
        }
1105
1106
        return $ret;
1107
    }
1108
1109
    /**
1110
     * Trim the tag on top of the value.
1111
     *
1112
     * Prevent values such as `!foo {quz: bar}` to be considered as
1113
     * a mapping block.
1114
     */
1115
    private function trimTag($value)
1116
    {
1117
        if ('!' === $value[0]) {
1118
            return ltrim(substr($value, 1, strcspn($value, " \r\n", 1)), ' ');
1119
        }
1120
1121
        return $value;
1122
    }
1123
1124
    private function getLineTag($value, $flags, $nextLineCheck = true)
1125
    {
1126
        if ('' === $value || '!' !== $value[0] || 1 !== self::preg_match('/^'.self::TAG_PATTERN.' *( +#.*)?$/', $value, $matches)) {
1127
            return;
1128
        }
1129
1130
        if ($nextLineCheck && !$this->isNextLineIndented()) {
1131
            return;
1132
        }
1133
1134
        $tag = substr($matches['tag'], 1);
1135
1136
        // Built-in tags
1137
        if ($tag && '!' === $tag[0]) {
1138
            throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag), $this->getRealCurrentLineNb() + 1, $value, $this->filename);
1139
        }
1140
1141
        if (Yaml::PARSE_CUSTOM_TAGS & $flags) {
1142
            return $tag;
1143
        }
1144
1145
        throw new ParseException(sprintf('Tags support is not enabled. You must use the flag `Yaml::PARSE_CUSTOM_TAGS` to use "%s".', $matches['tag']), $this->getRealCurrentLineNb() + 1, $value, $this->filename);
1146
    }
1147
1148
    private function getDeprecationMessage($message)
1149
    {
1150
        $message = rtrim($message, '.');
1151
1152
        if (null !== $this->filename) {
1153
            $message .= ' in '.$this->filename;
1154
        }
1155
1156
        $message .= ' on line '.($this->getRealCurrentLineNb() + 1);
1157
1158
        return $message.'.';
1159
    }
1160
}
1161