GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( cc39b3...2b121f )
by Anton
04:25 queued 01:05
created

Parser::getCurrentLineIndentation()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 3
c 2
b 0
f 0
nc 2
nop 0
dl 0
loc 7
rs 10
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
23
 */
24
class Parser
25
{
26
    public const TAG_PATTERN = '(?P<tag>![\w!.\/:-]+)';
27
    public const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
28
    public const REFERENCE_PATTERN = '#^&(?P<ref>[^ ]++) *+(?P<value>.*)#u';
29
30
    private ?string $filename = null;
31
    private int $offset = 0;
32
    private int $numberOfParsedLines = 0;
33
    private ?int $totalNumberOfLines = null;
34
    private array $lines = [];
35
    private int $currentLineNb = -1;
36
    private string $currentLine = '';
37
    private array $refs = [];
38
    private array $skippedLineNumbers = [];
39
    private array $locallySkippedLineNumbers = [];
40
    private array $refsBeingParsed = [];
41
42
    /**
43
     * Parses a YAML file into a PHP value.
44
     *
45
     * @param string $filename The path to the YAML file to be parsed
46
     * @param int    $flags    A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior
47
     *
48
     * @throws ParseException If the file could not be read or the YAML is not valid
49
     */
50
    public function parseFile(string $filename, int $flags = 0): mixed
51
    {
52
        if (!is_file($filename)) {
53
            throw new ParseException(sprintf('File "%s" does not exist.', $filename));
54
        }
55
56
        if (!is_readable($filename)) {
57
            throw new ParseException(sprintf('File "%s" cannot be read.', $filename));
58
        }
59
60
        $this->filename = $filename;
61
62
        try {
63
            return $this->parse(file_get_contents($filename), $flags);
64
        } finally {
65
            $this->filename = null;
66
        }
67
    }
68
69
    /**
70
     * Parses a YAML string to a PHP value.
71
     *
72
     * @param string $value A YAML string
73
     * @param int    $flags A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior
74
     *
75
     * @throws ParseException If the YAML is not valid
76
     */
77
    public function parse(string $value, int $flags = 0): mixed
78
    {
79
        if (false === preg_match('//u', $value)) {
80
            throw new ParseException('The YAML value does not appear to be valid UTF-8.', -1, null, $this->filename);
81
        }
82
83
        $this->refs = [];
84
85
        try {
86
            $data = $this->doParse($value, $flags);
87
        } finally {
88
            $this->refsBeingParsed = [];
89
            $this->offset = 0;
90
            $this->lines = [];
91
            $this->currentLine = '';
92
            $this->numberOfParsedLines = 0;
93
            $this->refs = [];
94
            $this->skippedLineNumbers = [];
95
            $this->locallySkippedLineNumbers = [];
96
            $this->totalNumberOfLines = null;
97
        }
98
99
        return $data;
100
    }
101
102
    private function doParse(string $value, int $flags): mixed
103
    {
104
        $this->currentLineNb = -1;
105
        $this->currentLine = '';
106
        $value = $this->cleanup($value);
107
        $this->lines = explode("\n", $value);
108
        $this->numberOfParsedLines = \count($this->lines);
109
        $this->locallySkippedLineNumbers = [];
110
        $this->totalNumberOfLines ??= $this->numberOfParsedLines;
111
112
        if (!$this->moveToNextLine()) {
113
            return null;
114
        }
115
116
        $data = [];
117
        $context = null;
118
        $allowOverwrite = false;
119
120
        while ($this->isCurrentLineEmpty()) {
121
            if (!$this->moveToNextLine()) {
122
                return null;
123
            }
124
        }
125
126
        // Resolves the tag and returns if end of the document
127
        if (null !== ($tag = $this->getLineTag($this->currentLine, $flags, false)) && !$this->moveToNextLine()) {
128
            return new TaggedValue($tag, '');
129
        }
130
131
        do {
132
            if ($this->isCurrentLineEmpty()) {
133
                continue;
134
            }
135
136
            // tab?
137
            if ("\t" === $this->currentLine[0]) {
138
                throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
139
            }
140
141
            Inline::initialize($flags, $this->getRealCurrentLineNb(), $this->filename);
142
143
            $isRef = $mergeNode = false;
144
            if ('-' === $this->currentLine[0] && self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) {
145
                if ($context && 'mapping' == $context) {
146
                    throw new ParseException('You cannot define a sequence item when in a mapping.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
147
                }
148
                $context = 'sequence';
149
150
                if (isset($values['value']) && '&' === $values['value'][0] && self::preg_match(self::REFERENCE_PATTERN, $values['value'], $matches)) {
151
                    $isRef = $matches['ref'];
152
                    $this->refsBeingParsed[] = $isRef;
153
                    $values['value'] = $matches['value'];
154
                }
155
156
                if (isset($values['value'][1]) && '?' === $values['value'][0] && ' ' === $values['value'][1]) {
157
                    throw new ParseException('Complex mappings are not supported.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
158
                }
159
160
                // array
161
                if (isset($values['value']) && str_starts_with(ltrim($values['value'], ' '), '-')) {
162
                    // Inline first child
163
                    $currentLineNumber = $this->getRealCurrentLineNb();
164
165
                    $sequenceIndentation = \strlen($values['leadspaces']) + 1;
166
                    $sequenceYaml = substr($this->currentLine, $sequenceIndentation);
167
                    $sequenceYaml .= "\n".$this->getNextEmbedBlock($sequenceIndentation, true);
168
169
                    $data[] = $this->parseBlock($currentLineNumber, rtrim($sequenceYaml), $flags);
170
                } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || str_starts_with(ltrim($values['value'], ' '), '#')) {
171
                    $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true) ?? '', $flags);
172
                } elseif (null !== $subTag = $this->getLineTag(ltrim($values['value'], ' '), $flags)) {
173
                    $data[] = new TaggedValue(
174
                        $subTag,
175
                        $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $flags)
176
                    );
177
                } else {
178
                    if (
179
                        isset($values['leadspaces'])
180
                        && (
181
                            '!' === $values['value'][0]
182
                            || self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->trimTag($values['value']), $matches)
183
                        )
184
                    ) {
185
                        $block = $values['value'];
186
                        if ($this->isNextLineIndented() || isset($matches['value']) && '>-' === $matches['value']) {
187
                            $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + \strlen($values['leadspaces']) + 1);
188
                        }
189
190
                        $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $flags);
191
                    } else {
192
                        $data[] = $this->parseValue($values['value'], $flags, $context);
193
                    }
194
                }
195
                if ($isRef) {
196
                    $this->refs[$isRef] = end($data);
197
                    array_pop($this->refsBeingParsed);
198
                }
199
            } elseif (
200
                self::preg_match('#^(?P<key>(?:![^\s]++\s++)?(?:'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{!].*?)) *\:(( |\t)++(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
201
                && (!str_contains($values['key'], ' #') || \in_array($values['key'][0], ['"', "'"]))
202
            ) {
203
                if ($context && 'sequence' == $context) {
204
                    throw new ParseException('You cannot define a mapping item when in a sequence.', $this->currentLineNb + 1, $this->currentLine, $this->filename);
205
                }
206
                $context = 'mapping';
207
208
                try {
209
                    $key = Inline::parseScalar($values['key']);
210
                } catch (ParseException $e) {
211
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
212
                    $e->setSnippet($this->currentLine);
213
214
                    throw $e;
215
                }
216
217
                if (!\is_string($key) && !\is_int($key)) {
218
                    throw new ParseException((is_numeric($key) ? 'Numeric' : 'Non-string').' keys are not supported. Quote your evaluable mapping keys instead.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
219
                }
220
221
                // Convert float keys to strings, to avoid being converted to integers by PHP
222
                if (\is_float($key)) {
223
                    $key = (string) $key;
224
                }
225
226
                if ('<<' === $key && (!isset($values['value']) || '&' !== $values['value'][0] || !self::preg_match('#^&(?P<ref>[^ ]+)#u', $values['value'], $refMatches))) {
227
                    $mergeNode = true;
228
                    $allowOverwrite = true;
229
                    if (isset($values['value'][0]) && '*' === $values['value'][0]) {
230
                        $refName = substr(rtrim($values['value']), 1);
231
                        if (!\array_key_exists($refName, $this->refs)) {
232
                            if (false !== $pos = array_search($refName, $this->refsBeingParsed, true)) {
233
                                throw new ParseException(sprintf('Circular reference [%s] detected for reference "%s".', implode(', ', array_merge(\array_slice($this->refsBeingParsed, $pos), [$refName])), $refName), $this->currentLineNb + 1, $this->currentLine, $this->filename);
0 ignored issues
show
Bug introduced by
It seems like $pos can also be of type string; however, parameter $offset of array_slice() does only seem to accept 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

233
                                throw new ParseException(sprintf('Circular reference [%s] detected for reference "%s".', implode(', ', array_merge(\array_slice($this->refsBeingParsed, /** @scrutinizer ignore-type */ $pos), [$refName])), $refName), $this->currentLineNb + 1, $this->currentLine, $this->filename);
Loading history...
234
                            }
235
236
                            throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
237
                        }
238
239
                        $refValue = $this->refs[$refName];
240
241
                        if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $refValue instanceof \stdClass) {
242
                            $refValue = (array) $refValue;
243
                        }
244
245
                        if (!\is_array($refValue)) {
246
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
247
                        }
248
249
                        $data += $refValue; // array union
250
                    } else {
251
                        if (isset($values['value']) && '' !== $values['value']) {
252
                            $value = $values['value'];
253
                        } else {
254
                            $value = $this->getNextEmbedBlock();
255
                        }
256
                        $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $flags);
257
258
                        if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsed instanceof \stdClass) {
259
                            $parsed = (array) $parsed;
260
                        }
261
262
                        if (!\is_array($parsed)) {
263
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
264
                        }
265
266
                        if (isset($parsed[0])) {
267
                            // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
268
                            // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
269
                            // in the sequence override keys specified in later mapping nodes.
270
                            foreach ($parsed as $parsedItem) {
271
                                if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $parsedItem instanceof \stdClass) {
272
                                    $parsedItem = (array) $parsedItem;
273
                                }
274
275
                                if (!\is_array($parsedItem)) {
276
                                    throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem, $this->filename);
277
                                }
278
279
                                $data += $parsedItem; // array union
280
                            }
281
                        } else {
282
                            // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
283
                            // current mapping, unless the key already exists in it.
284
                            $data += $parsed; // array union
285
                        }
286
                    }
287
                } elseif ('<<' !== $key && isset($values['value']) && '&' === $values['value'][0] && self::preg_match(self::REFERENCE_PATTERN, $values['value'], $matches)) {
288
                    $isRef = $matches['ref'];
289
                    $this->refsBeingParsed[] = $isRef;
290
                    $values['value'] = $matches['value'];
291
                }
292
293
                $subTag = null;
294
                if ($mergeNode) {
295
                    // Merge keys
296
                } elseif (!isset($values['value']) || '' === $values['value'] || str_starts_with($values['value'], '#') || (null !== $subTag = $this->getLineTag($values['value'], $flags)) || '<<' === $key) {
297
                    // hash
298
                    // if next line is less indented or equal, then it means that the current value is null
299
                    if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
300
                        // Spec: Keys MUST be unique; first one wins.
301
                        // But overwriting is allowed when a merge node is used in current block.
302
                        if ($allowOverwrite || !isset($data[$key])) {
303
                            if (null !== $subTag) {
304
                                $data[$key] = new TaggedValue($subTag, '');
0 ignored issues
show
Bug introduced by
$subTag of type void is incompatible with the type string expected by parameter $tag of Symfony\Component\Yaml\T...gedValue::__construct(). ( Ignorable by Annotation )

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

304
                                $data[$key] = new TaggedValue(/** @scrutinizer ignore-type */ $subTag, '');
Loading history...
305
                            } else {
306
                                $data[$key] = null;
307
                            }
308
                        } else {
309
                            throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), $this->getRealCurrentLineNb() + 1, $this->currentLine);
310
                        }
311
                    } else {
312
                        // remember the parsed line number here in case we need it to provide some contexts in error messages below
313
                        $realCurrentLineNbKey = $this->getRealCurrentLineNb();
314
                        $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $flags);
315
                        if ('<<' === $key) {
316
                            $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...
317
318
                            if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && $value instanceof \stdClass) {
319
                                $value = (array) $value;
320
                            }
321
322
                            $data += $value;
323
                        } elseif ($allowOverwrite || !isset($data[$key])) {
324
                            // Spec: Keys MUST be unique; first one wins.
325
                            // But overwriting is allowed when a merge node is used in current block.
326
                            if (null !== $subTag) {
327
                                $data[$key] = new TaggedValue($subTag, $value);
328
                            } else {
329
                                $data[$key] = $value;
330
                            }
331
                        } else {
332
                            throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), $realCurrentLineNbKey + 1, $this->currentLine);
333
                        }
334
                    }
335
                } else {
336
                    $value = $this->parseValue(rtrim($values['value']), $flags, $context);
337
                    // Spec: Keys MUST be unique; first one wins.
338
                    // But overwriting is allowed when a merge node is used in current block.
339
                    if ($allowOverwrite || !isset($data[$key])) {
340
                        $data[$key] = $value;
341
                    } else {
342
                        throw new ParseException(sprintf('Duplicate key "%s" detected.', $key), $this->getRealCurrentLineNb() + 1, $this->currentLine);
343
                    }
344
                }
345
                if ($isRef) {
346
                    $this->refs[$isRef] = $data[$key];
347
                    array_pop($this->refsBeingParsed);
348
                }
349
            } elseif ('"' === $this->currentLine[0] || "'" === $this->currentLine[0]) {
350
                if (null !== $context) {
351
                    throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
352
                }
353
354
                try {
355
                    return Inline::parse($this->lexInlineQuotedString(), $flags, $this->refs);
356
                } catch (ParseException $e) {
357
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
358
                    $e->setSnippet($this->currentLine);
359
360
                    throw $e;
361
                }
362
            } elseif ('{' === $this->currentLine[0]) {
363
                if (null !== $context) {
364
                    throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
365
                }
366
367
                try {
368
                    $parsedMapping = Inline::parse($this->lexInlineMapping(), $flags, $this->refs);
369
370
                    while ($this->moveToNextLine()) {
371
                        if (!$this->isCurrentLineEmpty()) {
372
                            throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
373
                        }
374
                    }
375
376
                    return $parsedMapping;
377
                } catch (ParseException $e) {
378
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
379
                    $e->setSnippet($this->currentLine);
380
381
                    throw $e;
382
                }
383
            } elseif ('[' === $this->currentLine[0]) {
384
                if (null !== $context) {
385
                    throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
386
                }
387
388
                try {
389
                    $parsedSequence = Inline::parse($this->lexInlineSequence(), $flags, $this->refs);
390
391
                    while ($this->moveToNextLine()) {
392
                        if (!$this->isCurrentLineEmpty()) {
393
                            throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
394
                        }
395
                    }
396
397
                    return $parsedSequence;
398
                } catch (ParseException $e) {
399
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
400
                    $e->setSnippet($this->currentLine);
401
402
                    throw $e;
403
                }
404
            } else {
405
                // multiple documents are not supported
406
                if ('---' === $this->currentLine) {
407
                    throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine, $this->filename);
408
                }
409
410
                if (isset($this->currentLine[1]) && '?' === $this->currentLine[0] && ' ' === $this->currentLine[1]) {
411
                    throw new ParseException('Complex mappings are not supported.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
412
                }
413
414
                // 1-liner optionally followed by newline(s)
415
                if (\is_string($value) && $this->lines[0] === trim($value)) {
416
                    try {
417
                        $value = Inline::parse($this->lines[0], $flags, $this->refs);
418
                    } catch (ParseException $e) {
419
                        $e->setParsedLine($this->getRealCurrentLineNb() + 1);
420
                        $e->setSnippet($this->currentLine);
421
422
                        throw $e;
423
                    }
424
425
                    return $value;
426
                }
427
428
                // try to parse the value as a multi-line string as a last resort
429
                if (0 === $this->currentLineNb) {
430
                    $previousLineWasNewline = false;
431
                    $previousLineWasTerminatedWithBackslash = false;
432
                    $value = '';
433
434
                    foreach ($this->lines as $line) {
435
                        $trimmedLine = trim($line);
436
                        if ('#' === ($trimmedLine[0] ?? '')) {
437
                            continue;
438
                        }
439
                        // If the indentation is not consistent at offset 0, it is to be considered as a ParseError
440
                        if (0 === $this->offset && isset($line[0]) && ' ' === $line[0]) {
441
                            throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
442
                        }
443
444
                        if (str_contains($line, ': ')) {
445
                            throw new ParseException('Mapping values are not allowed in multi-line blocks.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
446
                        }
447
448
                        if ('' === $trimmedLine) {
449
                            $value .= "\n";
450
                        } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
451
                            $value .= ' ';
452
                        }
453
454
                        if ('' !== $trimmedLine && str_ends_with($line, '\\')) {
455
                            $value .= ltrim(substr($line, 0, -1));
456
                        } elseif ('' !== $trimmedLine) {
457
                            $value .= $trimmedLine;
458
                        }
459
460
                        if ('' === $trimmedLine) {
461
                            $previousLineWasNewline = true;
462
                            $previousLineWasTerminatedWithBackslash = false;
463
                        } elseif (str_ends_with($line, '\\')) {
464
                            $previousLineWasNewline = false;
465
                            $previousLineWasTerminatedWithBackslash = true;
466
                        } else {
467
                            $previousLineWasNewline = false;
468
                            $previousLineWasTerminatedWithBackslash = false;
469
                        }
470
                    }
471
472
                    try {
473
                        return Inline::parse(trim($value));
474
                    } catch (ParseException) {
475
                        // fall-through to the ParseException thrown below
476
                    }
477
                }
478
479
                throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
480
            }
481
        } while ($this->moveToNextLine());
482
483
        if (null !== $tag) {
484
            $data = new TaggedValue($tag, $data);
485
        }
486
487
        if (Yaml::PARSE_OBJECT_FOR_MAP & $flags && 'mapping' === $context && !\is_object($data)) {
488
            $object = new \stdClass();
489
490
            foreach ($data as $key => $value) {
491
                $object->$key = $value;
492
            }
493
494
            $data = $object;
495
        }
496
497
        return $data ?: null;
498
    }
499
500
    private function parseBlock(int $offset, string $yaml, int $flags): mixed
501
    {
502
        $skippedLineNumbers = $this->skippedLineNumbers;
503
504
        foreach ($this->locallySkippedLineNumbers as $lineNumber) {
505
            if ($lineNumber < $offset) {
506
                continue;
507
            }
508
509
            $skippedLineNumbers[] = $lineNumber;
510
        }
511
512
        $parser = new self();
513
        $parser->offset = $offset;
514
        $parser->totalNumberOfLines = $this->totalNumberOfLines;
515
        $parser->skippedLineNumbers = $skippedLineNumbers;
516
        $parser->refs = &$this->refs;
517
        $parser->refsBeingParsed = $this->refsBeingParsed;
518
519
        return $parser->doParse($yaml, $flags);
520
    }
521
522
    /**
523
     * Returns the current line number (takes the offset into account).
524
     *
525
     * @internal
526
     */
527
    public function getRealCurrentLineNb(): int
528
    {
529
        $realCurrentLineNumber = $this->currentLineNb + $this->offset;
530
531
        foreach ($this->skippedLineNumbers as $skippedLineNumber) {
532
            if ($skippedLineNumber > $realCurrentLineNumber) {
533
                break;
534
            }
535
536
            ++$realCurrentLineNumber;
537
        }
538
539
        return $realCurrentLineNumber;
540
    }
541
542
    private function getCurrentLineIndentation(): int
543
    {
544
        if (' ' !== ($this->currentLine[0] ?? '')) {
545
            return 0;
546
        }
547
548
        return \strlen($this->currentLine) - \strlen(ltrim($this->currentLine, ' '));
549
    }
550
551
    /**
552
     * Returns the next embed block of YAML.
553
     *
554
     * @param int|null $indentation The indent level at which the block is to be read, or null for default
555
     * @param bool     $inSequence  True if the enclosing data structure is a sequence
556
     *
557
     * @throws ParseException When indentation problem are detected
558
     */
559
    private function getNextEmbedBlock(?int $indentation = null, bool $inSequence = false): string
560
    {
561
        $oldLineIndentation = $this->getCurrentLineIndentation();
562
563
        if (!$this->moveToNextLine()) {
564
            return '';
565
        }
566
567
        if (null === $indentation) {
568
            $newIndent = null;
569
            $movements = 0;
570
571
            do {
572
                $EOF = false;
573
574
                // empty and comment-like lines do not influence the indentation depth
575
                if ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
576
                    $EOF = !$this->moveToNextLine();
577
578
                    if (!$EOF) {
579
                        ++$movements;
580
                    }
581
                } else {
582
                    $newIndent = $this->getCurrentLineIndentation();
583
                }
584
            } while (!$EOF && null === $newIndent);
585
586
            for ($i = 0; $i < $movements; ++$i) {
587
                $this->moveToPreviousLine();
588
            }
589
590
            $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
591
592
            if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
593
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
594
            }
595
        } else {
596
            $newIndent = $indentation;
597
        }
598
599
        $data = [];
600
601
        if ($this->getCurrentLineIndentation() >= $newIndent) {
602
            $data[] = substr($this->currentLine, $newIndent ?? 0);
603
        } elseif ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()) {
604
            $data[] = $this->currentLine;
605
        } else {
606
            $this->moveToPreviousLine();
607
608
            return '';
609
        }
610
611
        if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
612
            // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
613
            // and therefore no nested list or mapping
614
            $this->moveToPreviousLine();
615
616
            return '';
617
        }
618
619
        $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
620
        $isItComment = $this->isCurrentLineComment();
621
622
        while ($this->moveToNextLine()) {
623
            if ($isItComment && !$isItUnindentedCollection) {
624
                $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
625
                $isItComment = $this->isCurrentLineComment();
626
            }
627
628
            $indent = $this->getCurrentLineIndentation();
629
630
            if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
631
                $this->moveToPreviousLine();
632
                break;
633
            }
634
635
            if ($this->isCurrentLineBlank()) {
636
                $data[] = substr($this->currentLine, $newIndent ?? 0);
637
                continue;
638
            }
639
640
            if ($indent >= $newIndent) {
641
                $data[] = substr($this->currentLine, $newIndent ?? 0);
642
            } elseif ($this->isCurrentLineComment()) {
643
                $data[] = $this->currentLine;
644
            } elseif (0 == $indent) {
645
                $this->moveToPreviousLine();
646
647
                break;
648
            } else {
649
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
650
            }
651
        }
652
653
        return implode("\n", $data);
654
    }
655
656
    private function hasMoreLines(): bool
657
    {
658
        return (\count($this->lines) - 1) > $this->currentLineNb;
659
    }
660
661
    /**
662
     * Moves the parser to the next line.
663
     */
664
    private function moveToNextLine(): bool
665
    {
666
        if ($this->currentLineNb >= $this->numberOfParsedLines - 1) {
667
            return false;
668
        }
669
670
        $this->currentLine = $this->lines[++$this->currentLineNb];
671
672
        return true;
673
    }
674
675
    /**
676
     * Moves the parser to the previous line.
677
     */
678
    private function moveToPreviousLine(): bool
679
    {
680
        if ($this->currentLineNb < 1) {
681
            return false;
682
        }
683
684
        $this->currentLine = $this->lines[--$this->currentLineNb];
685
686
        return true;
687
    }
688
689
    /**
690
     * Parses a YAML value.
691
     *
692
     * @param string $value   A YAML value
693
     * @param int    $flags   A bit field of Yaml::PARSE_* constants to customize the YAML parser behavior
694
     * @param string $context The parser context (either sequence or mapping)
695
     *
696
     * @throws ParseException When reference does not exist
697
     */
698
    private function parseValue(string $value, int $flags, string $context): mixed
699
    {
700
        if (str_starts_with($value, '*')) {
701
            if (false !== $pos = strpos($value, '#')) {
702
                $value = substr($value, 1, $pos - 2);
703
            } else {
704
                $value = substr($value, 1);
705
            }
706
707
            if (!\array_key_exists($value, $this->refs)) {
708
                if (false !== $pos = array_search($value, $this->refsBeingParsed, true)) {
709
                    throw new ParseException(sprintf('Circular reference [%s] detected for reference "%s".', implode(', ', array_merge(\array_slice($this->refsBeingParsed, $pos), [$value])), $value), $this->currentLineNb + 1, $this->currentLine, $this->filename);
0 ignored issues
show
Bug introduced by
It seems like $pos can also be of type string; however, parameter $offset of array_slice() does only seem to accept 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

709
                    throw new ParseException(sprintf('Circular reference [%s] detected for reference "%s".', implode(', ', array_merge(\array_slice($this->refsBeingParsed, /** @scrutinizer ignore-type */ $pos), [$value])), $value), $this->currentLineNb + 1, $this->currentLine, $this->filename);
Loading history...
710
                }
711
712
                throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine, $this->filename);
713
            }
714
715
            return $this->refs[$value];
716
        }
717
718
        if (\in_array($value[0], ['!', '|', '>'], true) && self::preg_match('/^(?:'.self::TAG_PATTERN.' +)?'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
719
            $modifiers = $matches['modifiers'] ?? '';
720
721
            $data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), abs((int) $modifiers));
0 ignored issues
show
Bug introduced by
It seems like abs((int)$modifiers) can also be of type double; however, parameter $indentation of Symfony\Component\Yaml\Parser::parseBlockScalar() does only seem to accept 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

721
            $data = $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), /** @scrutinizer ignore-type */ abs((int) $modifiers));
Loading history...
722
723
            if ('' !== $matches['tag'] && '!' !== $matches['tag']) {
724
                if ('!!binary' === $matches['tag']) {
725
                    return Inline::evaluateBinaryScalar($data);
726
                }
727
728
                return new TaggedValue(substr($matches['tag'], 1), $data);
729
            }
730
731
            return $data;
732
        }
733
734
        try {
735
            if ('' !== $value && '{' === $value[0]) {
736
                $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value));
737
738
                return Inline::parse($this->lexInlineMapping($cursor), $flags, $this->refs);
739
            } elseif ('' !== $value && '[' === $value[0]) {
740
                $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value));
741
742
                return Inline::parse($this->lexInlineSequence($cursor), $flags, $this->refs);
743
            }
744
745
            switch ($value[0] ?? '') {
746
                case '"':
747
                case "'":
748
                    $cursor = \strlen(rtrim($this->currentLine)) - \strlen(rtrim($value));
749
                    $parsedValue = Inline::parse($this->lexInlineQuotedString($cursor), $flags, $this->refs);
750
751
                    if (isset($this->currentLine[$cursor]) && preg_replace('/\s*(#.*)?$/A', '', substr($this->currentLine, $cursor))) {
752
                        throw new ParseException(sprintf('Unexpected characters near "%s".', substr($this->currentLine, $cursor)));
753
                    }
754
755
                    return $parsedValue;
756
                default:
757
                    $lines = [];
758
759
                    while ($this->moveToNextLine()) {
760
                        // unquoted strings end before the first unindented line
761
                        if (0 === $this->getCurrentLineIndentation()) {
762
                            $this->moveToPreviousLine();
763
764
                            break;
765
                        }
766
767
                        $lines[] = trim($this->currentLine);
768
                    }
769
770
                    for ($i = 0, $linesCount = \count($lines), $previousLineBlank = false; $i < $linesCount; ++$i) {
771
                        if ('' === $lines[$i]) {
772
                            $value .= "\n";
773
                            $previousLineBlank = true;
774
                        } elseif ($previousLineBlank) {
775
                            $value .= $lines[$i];
776
                            $previousLineBlank = false;
777
                        } else {
778
                            $value .= ' '.$lines[$i];
779
                            $previousLineBlank = false;
780
                        }
781
                    }
782
783
                    Inline::$parsedLineNumber = $this->getRealCurrentLineNb();
784
785
                    $parsedValue = Inline::parse($value, $flags, $this->refs);
786
787
                    if ('mapping' === $context && \is_string($parsedValue) && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && str_contains($parsedValue, ': ')) {
788
                        throw new ParseException('A colon cannot be used in an unquoted mapping value.', $this->getRealCurrentLineNb() + 1, $value, $this->filename);
789
                    }
790
791
                    return $parsedValue;
792
            }
793
        } catch (ParseException $e) {
794
            $e->setParsedLine($this->getRealCurrentLineNb() + 1);
795
            $e->setSnippet($this->currentLine);
796
797
            throw $e;
798
        }
799
    }
800
801
    /**
802
     * Parses a block scalar.
803
     *
804
     * @param string $style       The style indicator that was used to begin this block scalar (| or >)
805
     * @param string $chomping    The chomping indicator that was used to begin this block scalar (+ or -)
806
     * @param int    $indentation The indentation indicator that was used to begin this block scalar
807
     */
808
    private function parseBlockScalar(string $style, string $chomping = '', int $indentation = 0): string
809
    {
810
        $notEOF = $this->moveToNextLine();
811
        if (!$notEOF) {
812
            return '';
813
        }
814
815
        $isCurrentLineBlank = $this->isCurrentLineBlank();
816
        $blockLines = [];
817
818
        // leading blank lines are consumed before determining indentation
819
        while ($notEOF && $isCurrentLineBlank) {
820
            // newline only if not EOF
821
            if ($notEOF = $this->moveToNextLine()) {
822
                $blockLines[] = '';
823
                $isCurrentLineBlank = $this->isCurrentLineBlank();
824
            }
825
        }
826
827
        // determine indentation if not specified
828
        if (0 === $indentation) {
829
            $currentLineLength = \strlen($this->currentLine);
830
831
            for ($i = 0; $i < $currentLineLength && ' ' === $this->currentLine[$i]; ++$i) {
832
                ++$indentation;
833
            }
834
        }
835
836
        if ($indentation > 0) {
837
            $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
838
839
            while (
840
                $notEOF && (
841
                    $isCurrentLineBlank
842
                    || self::preg_match($pattern, $this->currentLine, $matches)
843
                )
844
            ) {
845
                if ($isCurrentLineBlank && \strlen($this->currentLine) > $indentation) {
846
                    $blockLines[] = substr($this->currentLine, $indentation);
847
                } elseif ($isCurrentLineBlank) {
848
                    $blockLines[] = '';
849
                } else {
850
                    $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...
851
                }
852
853
                // newline only if not EOF
854
                if ($notEOF = $this->moveToNextLine()) {
855
                    $isCurrentLineBlank = $this->isCurrentLineBlank();
856
                }
857
            }
858
        } elseif ($notEOF) {
859
            $blockLines[] = '';
860
        }
861
862
        if ($notEOF) {
863
            $blockLines[] = '';
864
            $this->moveToPreviousLine();
865
        } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
866
            $blockLines[] = '';
867
        }
868
869
        // folded style
870
        if ('>' === $style) {
871
            $text = '';
872
            $previousLineIndented = false;
873
            $previousLineBlank = false;
874
875
            for ($i = 0, $blockLinesCount = \count($blockLines); $i < $blockLinesCount; ++$i) {
876
                if ('' === $blockLines[$i]) {
877
                    $text .= "\n";
878
                    $previousLineIndented = false;
879
                    $previousLineBlank = true;
880
                } elseif (' ' === $blockLines[$i][0]) {
881
                    $text .= "\n".$blockLines[$i];
882
                    $previousLineIndented = true;
883
                    $previousLineBlank = false;
884
                } elseif ($previousLineIndented) {
885
                    $text .= "\n".$blockLines[$i];
886
                    $previousLineIndented = false;
887
                    $previousLineBlank = false;
888
                } elseif ($previousLineBlank || 0 === $i) {
889
                    $text .= $blockLines[$i];
890
                    $previousLineIndented = false;
891
                    $previousLineBlank = false;
892
                } else {
893
                    $text .= ' '.$blockLines[$i];
894
                    $previousLineIndented = false;
895
                    $previousLineBlank = false;
896
                }
897
            }
898
        } else {
899
            $text = implode("\n", $blockLines);
900
        }
901
902
        // deal with trailing newlines
903
        if ('' === $chomping) {
904
            $text = preg_replace('/\n+$/', "\n", $text);
905
        } elseif ('-' === $chomping) {
906
            $text = preg_replace('/\n+$/', '', $text);
907
        }
908
909
        return $text;
910
    }
911
912
    /**
913
     * Returns true if the next line is indented.
914
     */
915
    private function isNextLineIndented(): bool
916
    {
917
        $currentIndentation = $this->getCurrentLineIndentation();
918
        $movements = 0;
919
920
        do {
921
            $EOF = !$this->moveToNextLine();
922
923
            if (!$EOF) {
924
                ++$movements;
925
            }
926
        } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));
927
928
        if ($EOF) {
929
            for ($i = 0; $i < $movements; ++$i) {
930
                $this->moveToPreviousLine();
931
            }
932
933
            return false;
934
        }
935
936
        $ret = $this->getCurrentLineIndentation() > $currentIndentation;
937
938
        for ($i = 0; $i < $movements; ++$i) {
939
            $this->moveToPreviousLine();
940
        }
941
942
        return $ret;
943
    }
944
945
    private function isCurrentLineEmpty(): bool
946
    {
947
        return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
948
    }
949
950
    private function isCurrentLineBlank(): bool
951
    {
952
        return '' === $this->currentLine || '' === trim($this->currentLine, ' ');
953
    }
954
955
    private function isCurrentLineComment(): bool
956
    {
957
        // checking explicitly the first char of the trim is faster than loops or strpos
958
        $ltrimmedLine = '' !== $this->currentLine && ' ' === $this->currentLine[0] ? ltrim($this->currentLine, ' ') : $this->currentLine;
959
960
        return '' !== $ltrimmedLine && '#' === $ltrimmedLine[0];
961
    }
962
963
    private function isCurrentLineLastLineInDocument(): bool
964
    {
965
        return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1);
966
    }
967
968
    private function cleanup(string $value): string
969
    {
970
        $value = str_replace(["\r\n", "\r"], "\n", $value);
971
972
        // strip YAML header
973
        $count = 0;
974
        $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
975
        $this->offset += $count;
976
977
        // remove leading comments
978
        $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
979
        if (1 === $count) {
980
            // items have been removed, update the offset
981
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
982
            $value = $trimmedValue;
983
        }
984
985
        // remove start of the document marker (---)
986
        $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
987
        if (1 === $count) {
988
            // items have been removed, update the offset
989
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
990
            $value = $trimmedValue;
991
992
            // remove end of the document marker (...)
993
            $value = preg_replace('#\.\.\.\s*$#', '', $value);
994
        }
995
996
        return $value;
997
    }
998
999
    private function isNextLineUnIndentedCollection(): bool
1000
    {
1001
        $currentIndentation = $this->getCurrentLineIndentation();
1002
        $movements = 0;
1003
1004
        do {
1005
            $EOF = !$this->moveToNextLine();
1006
1007
            if (!$EOF) {
1008
                ++$movements;
1009
            }
1010
        } while (!$EOF && ($this->isCurrentLineEmpty() || $this->isCurrentLineComment()));
1011
1012
        if ($EOF) {
1013
            return false;
1014
        }
1015
1016
        $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
1017
1018
        for ($i = 0; $i < $movements; ++$i) {
1019
            $this->moveToPreviousLine();
1020
        }
1021
1022
        return $ret;
1023
    }
1024
1025
    private function isStringUnIndentedCollectionItem(): bool
1026
    {
1027
        return '-' === rtrim($this->currentLine) || str_starts_with($this->currentLine, '- ');
1028
    }
1029
1030
    /**
1031
     * A local wrapper for "preg_match" which will throw a ParseException if there
1032
     * is an internal error in the PCRE engine.
1033
     *
1034
     * This avoids us needing to check for "false" every time PCRE is used
1035
     * in the YAML engine
1036
     *
1037
     * @throws ParseException on a PCRE internal error
1038
     *
1039
     * @internal
1040
     */
1041
    public static function preg_match(string $pattern, string $subject, ?array &$matches = null, int $flags = 0, int $offset = 0): int
1042
    {
1043
        if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) {
1044
            throw new ParseException(preg_last_error_msg());
1045
        }
1046
1047
        return $ret;
1048
    }
1049
1050
    /**
1051
     * Trim the tag on top of the value.
1052
     *
1053
     * Prevent values such as "!foo {quz: bar}" to be considered as
1054
     * a mapping block.
1055
     */
1056
    private function trimTag(string $value): string
1057
    {
1058
        if ('!' === $value[0]) {
1059
            return ltrim(substr($value, 1, strcspn($value, " \r\n", 1)), ' ');
1060
        }
1061
1062
        return $value;
1063
    }
1064
1065
    private function getLineTag(string $value, int $flags, bool $nextLineCheck = true): ?string
1066
    {
1067
        if ('' === $value || '!' !== $value[0] || 1 !== self::preg_match('/^'.self::TAG_PATTERN.' *( +#.*)?$/', $value, $matches)) {
1068
            return null;
1069
        }
1070
1071
        if ($nextLineCheck && !$this->isNextLineIndented()) {
1072
            return null;
1073
        }
1074
1075
        $tag = substr($matches['tag'], 1);
1076
1077
        // Built-in tags
1078
        if ($tag && '!' === $tag[0]) {
1079
            throw new ParseException(sprintf('The built-in tag "!%s" is not implemented.', $tag), $this->getRealCurrentLineNb() + 1, $value, $this->filename);
1080
        }
1081
1082
        if (Yaml::PARSE_CUSTOM_TAGS & $flags) {
1083
            return $tag;
1084
        }
1085
1086
        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);
1087
    }
1088
1089
    private function lexInlineQuotedString(int &$cursor = 0): string
1090
    {
1091
        $quotation = $this->currentLine[$cursor];
1092
        $value = $quotation;
1093
        ++$cursor;
1094
1095
        $previousLineWasNewline = true;
1096
        $previousLineWasTerminatedWithBackslash = false;
1097
        $lineNumber = 0;
1098
1099
        do {
1100
            if (++$lineNumber > 1) {
1101
                $cursor += strspn($this->currentLine, ' ', $cursor);
1102
            }
1103
1104
            if ($this->isCurrentLineBlank()) {
1105
                $value .= "\n";
1106
            } elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
1107
                $value .= ' ';
1108
            }
1109
1110
            for (; \strlen($this->currentLine) > $cursor; ++$cursor) {
1111
                switch ($this->currentLine[$cursor]) {
1112
                    case '\\':
1113
                        if ("'" === $quotation) {
1114
                            $value .= '\\';
1115
                        } elseif (isset($this->currentLine[++$cursor])) {
1116
                            $value .= '\\'.$this->currentLine[$cursor];
1117
                        }
1118
1119
                        break;
1120
                    case $quotation:
1121
                        ++$cursor;
1122
1123
                        if ("'" === $quotation && isset($this->currentLine[$cursor]) && "'" === $this->currentLine[$cursor]) {
1124
                            $value .= "''";
1125
                            break;
1126
                        }
1127
1128
                        return $value.$quotation;
1129
                    default:
1130
                        $value .= $this->currentLine[$cursor];
1131
                }
1132
            }
1133
1134
            if ($this->isCurrentLineBlank()) {
1135
                $previousLineWasNewline = true;
1136
                $previousLineWasTerminatedWithBackslash = false;
1137
            } elseif ('\\' === $this->currentLine[-1]) {
1138
                $previousLineWasNewline = false;
1139
                $previousLineWasTerminatedWithBackslash = true;
1140
            } else {
1141
                $previousLineWasNewline = false;
1142
                $previousLineWasTerminatedWithBackslash = false;
1143
            }
1144
1145
            if ($this->hasMoreLines()) {
1146
                $cursor = 0;
1147
            }
1148
        } while ($this->moveToNextLine());
1149
1150
        throw new ParseException('Malformed inline YAML string.');
1151
    }
1152
1153
    private function lexUnquotedString(int &$cursor): string
1154
    {
1155
        $offset = $cursor;
1156
        $cursor += strcspn($this->currentLine, '[]{},: ', $cursor);
1157
1158
        if ($cursor === $offset) {
1159
            throw new ParseException('Malformed unquoted YAML string.');
1160
        }
1161
1162
        return substr($this->currentLine, $offset, $cursor - $offset);
1163
    }
1164
1165
    private function lexInlineMapping(int &$cursor = 0): string
1166
    {
1167
        return $this->lexInlineStructure($cursor, '}');
1168
    }
1169
1170
    private function lexInlineSequence(int &$cursor = 0): string
1171
    {
1172
        return $this->lexInlineStructure($cursor, ']');
1173
    }
1174
1175
    private function lexInlineStructure(int &$cursor, string $closingTag): string
1176
    {
1177
        $value = $this->currentLine[$cursor];
1178
        ++$cursor;
1179
1180
        do {
1181
            $this->consumeWhitespaces($cursor);
1182
1183
            while (isset($this->currentLine[$cursor])) {
1184
                switch ($this->currentLine[$cursor]) {
1185
                    case '"':
1186
                    case "'":
1187
                        $value .= $this->lexInlineQuotedString($cursor);
1188
                        break;
1189
                    case ':':
1190
                    case ',':
1191
                        $value .= $this->currentLine[$cursor];
1192
                        ++$cursor;
1193
                        break;
1194
                    case '{':
1195
                        $value .= $this->lexInlineMapping($cursor);
1196
                        break;
1197
                    case '[':
1198
                        $value .= $this->lexInlineSequence($cursor);
1199
                        break;
1200
                    case $closingTag:
1201
                        $value .= $this->currentLine[$cursor];
1202
                        ++$cursor;
1203
1204
                        return $value;
1205
                    case '#':
1206
                        break 2;
1207
                    default:
1208
                        $value .= $this->lexUnquotedString($cursor);
1209
                }
1210
1211
                if ($this->consumeWhitespaces($cursor)) {
1212
                    $value .= ' ';
1213
                }
1214
            }
1215
1216
            if ($this->hasMoreLines()) {
1217
                $cursor = 0;
1218
            }
1219
        } while ($this->moveToNextLine());
1220
1221
        throw new ParseException('Malformed inline YAML string.');
1222
    }
1223
1224
    private function consumeWhitespaces(int &$cursor): bool
1225
    {
1226
        $whitespacesConsumed = 0;
1227
1228
        do {
1229
            $whitespaceOnlyTokenLength = strspn($this->currentLine, ' ', $cursor);
1230
            $whitespacesConsumed += $whitespaceOnlyTokenLength;
1231
            $cursor += $whitespaceOnlyTokenLength;
1232
1233
            if (isset($this->currentLine[$cursor])) {
1234
                return 0 < $whitespacesConsumed;
1235
            }
1236
1237
            if ($this->hasMoreLines()) {
1238
                $cursor = 0;
1239
            }
1240
        } while ($this->moveToNextLine());
1241
1242
        return 0 < $whitespacesConsumed;
1243
    }
1244
}
1245