Completed
Push — master ( ac6391...9b00f4 )
by Michael
12:56
created

Parser::isBlockScalarHeader()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 1
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\Yaml;
13
14
use Symfony\Component\Yaml\Exception\ParseException;
15
16
/**
17
 * Parser parses YAML strings to convert them to PHP arrays.
18
 *
19
 * @author Fabien Potencier <[email protected]>
20
 */
21
class Parser
22
{
23
    const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
24
    // BC - wrongly named
25
    const FOLDED_SCALAR_PATTERN = self::BLOCK_SCALAR_HEADER_PATTERN;
26
27
    private $offset = 0;
28
    private $totalNumberOfLines;
29
    private $lines = array();
30
    private $currentLineNb = -1;
31
    private $currentLine = '';
32
    private $refs = array();
33
    private $skippedLineNumbers = array();
34
    private $locallySkippedLineNumbers = array();
35
36
    /**
37
     * Constructor.
38
     *
39
     * @param int      $offset             The offset of YAML document (used for line numbers in error messages)
40
     * @param int|null $totalNumberOfLines The overall number of lines being parsed
41
     * @param int[]    $skippedLineNumbers Number of comment lines that have been skipped by the parser
42
     */
43
    public function __construct($offset = 0, $totalNumberOfLines = null, array $skippedLineNumbers = array())
44
    {
45
        $this->offset = $offset;
46
        $this->totalNumberOfLines = $totalNumberOfLines;
47
        $this->skippedLineNumbers = $skippedLineNumbers;
48
    }
49
50
    /**
51
     * Parses a YAML string to a PHP value.
52
     *
53
     * @param string $value                  A YAML string
54
     * @param bool   $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
55
     * @param bool   $objectSupport          true if object support is enabled, false otherwise
56
     * @param bool   $objectForMap           true if maps should return a stdClass instead of array()
57
     *
58
     * @return mixed A PHP value
59
     *
60
     * @throws ParseException If the YAML is not valid
61
     */
62
    public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
63
    {
64
        if (!preg_match('//u', $value)) {
65
            throw new ParseException('The YAML value does not appear to be valid UTF-8.');
66
        }
67
        $this->currentLineNb = -1;
68
        $this->currentLine = '';
69
        $value = $this->cleanup($value);
70
        $this->lines = explode("\n", $value);
71
72
        if (null === $this->totalNumberOfLines) {
73
            $this->totalNumberOfLines = count($this->lines);
74
        }
75
76
        if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
77
            $mbEncoding = mb_internal_encoding();
78
            mb_internal_encoding('UTF-8');
79
        }
80
81
        $data = array();
82
        $context = null;
83
        $allowOverwrite = false;
84
        while ($this->moveToNextLine()) {
85
            if ($this->isCurrentLineEmpty()) {
86
                continue;
87
            }
88
89
            // tab?
90
            if ("\t" === $this->currentLine[0]) {
91
                throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
92
            }
93
94
            $isRef = $mergeNode = false;
95
            if (preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+?))?\s*$#u', $this->currentLine, $values)) {
96
                if ($context && 'mapping' == $context) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $context of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
97
                    throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine);
98
                }
99
                $context = 'sequence';
100
101 View Code Duplication
                if (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
102
                    $isRef = $matches['ref'];
103
                    $values['value'] = $matches['value'];
104
                }
105
106
                // array
107
                if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
108
                    $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap);
109
                } else {
110
                    if (isset($values['leadspaces'])
111
                        && preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $values['value'], $matches)
112
                    ) {
113
                        // this is a compact notation element, add to next block and parse
114
                        $block = $values['value'];
115
                        if ($this->isNextLineIndented()) {
116
                            $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1);
117
                        }
118
119
                        $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $exceptionOnInvalidType, $objectSupport, $objectForMap);
120
                    } else {
121
                        $data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
122
                    }
123
                }
124
                if ($isRef) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $isRef of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
125
                    $this->refs[$isRef] = end($data);
126
                }
127
            } elseif (preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->currentLine, $values) && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))) {
128
                if ($context && 'sequence' == $context) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $context of type null|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
129
                    throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine);
130
                }
131
                $context = 'mapping';
132
133
                // force correct settings
134
                Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
135
                try {
136
                    $key = Inline::parseScalar($values['key']);
137
                } catch (ParseException $e) {
138
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
139
                    $e->setSnippet($this->currentLine);
140
141
                    throw $e;
142
                }
143
144
                // Convert float keys to strings, to avoid being converted to integers by PHP
145
                if (is_float($key)) {
146
                    $key = (string) $key;
147
                }
148
149
                if ('<<' === $key) {
150
                    $mergeNode = true;
151
                    $allowOverwrite = true;
152
                    if (isset($values['value']) && 0 === strpos($values['value'], '*')) {
153
                        $refName = substr($values['value'], 1);
154
                        if (!array_key_exists($refName, $this->refs)) {
155
                            throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
156
                        }
157
158
                        $refValue = $this->refs[$refName];
159
160
                        if (!is_array($refValue)) {
161
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
162
                        }
163
164
                        foreach ($refValue as $key => $value) {
165
                            if (!isset($data[$key])) {
166
                                $data[$key] = $value;
167
                            }
168
                        }
169
                    } else {
170
                        if (isset($values['value']) && $values['value'] !== '') {
171
                            $value = $values['value'];
172
                        } else {
173
                            $value = $this->getNextEmbedBlock();
174
                        }
175
                        $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
176
177
                        if (!is_array($parsed)) {
178
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
179
                        }
180
181
                        if (isset($parsed[0])) {
182
                            // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
183
                            // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
184
                            // in the sequence override keys specified in later mapping nodes.
185
                            foreach ($parsed as $parsedItem) {
186
                                if (!is_array($parsedItem)) {
187
                                    throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
188
                                }
189
190
                                foreach ($parsedItem as $key => $value) {
191
                                    if (!isset($data[$key])) {
192
                                        $data[$key] = $value;
193
                                    }
194
                                }
195
                            }
196
                        } else {
197
                            // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
198
                            // current mapping, unless the key already exists in it.
199
                            foreach ($parsed as $key => $value) {
200
                                if (!isset($data[$key])) {
201
                                    $data[$key] = $value;
202
                                }
203
                            }
204
                        }
205
                    }
206 View Code Duplication
                } elseif (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
207
                    $isRef = $matches['ref'];
208
                    $values['value'] = $matches['value'];
209
                }
210
211
                if ($mergeNode) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
212
                    // Merge keys
213
                } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
214
                    // hash
215
                    // if next line is less indented or equal, then it means that the current value is null
216
                    if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
217
                        // Spec: Keys MUST be unique; first one wins.
218
                        // But overwriting is allowed when a merge node is used in current block.
219
                        if ($allowOverwrite || !isset($data[$key])) {
220
                            $data[$key] = null;
221
                        }
222
                    } else {
223
                        $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap);
224
                        // Spec: Keys MUST be unique; first one wins.
225
                        // But overwriting is allowed when a merge node is used in current block.
226
                        if ($allowOverwrite || !isset($data[$key])) {
227
                            $data[$key] = $value;
228
                        }
229
                    }
230
                } else {
231
                    $value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
232
                    // Spec: Keys MUST be unique; first one wins.
233
                    // But overwriting is allowed when a merge node is used in current block.
234
                    if ($allowOverwrite || !isset($data[$key])) {
235
                        $data[$key] = $value;
236
                    }
237
                }
238
                if ($isRef) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $isRef of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
239
                    $this->refs[$isRef] = $data[$key];
240
                }
241
            } else {
242
                // multiple documents are not supported
243
                if ('---' === $this->currentLine) {
244
                    throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine);
245
                }
246
247
                // 1-liner optionally followed by newline(s)
248
                if (is_string($value) && $this->lines[0] === trim($value)) {
249
                    try {
250
                        $value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
251
                    } catch (ParseException $e) {
252
                        $e->setParsedLine($this->getRealCurrentLineNb() + 1);
253
                        $e->setSnippet($this->currentLine);
254
255
                        throw $e;
256
                    }
257
258
                    if (is_array($value)) {
259
                        $first = reset($value);
260
                        if (is_string($first) && 0 === strpos($first, '*')) {
261
                            $data = array();
262
                            foreach ($value as $alias) {
263
                                $data[] = $this->refs[substr($alias, 1)];
264
                            }
265
                            $value = $data;
266
                        }
267
                    }
268
269
                    if (isset($mbEncoding)) {
270
                        mb_internal_encoding($mbEncoding);
271
                    }
272
273
                    return $value;
274
                }
275
276
                switch (preg_last_error()) {
277
                    case PREG_INTERNAL_ERROR:
278
                        $error = 'Internal PCRE error.';
279
                        break;
280
                    case PREG_BACKTRACK_LIMIT_ERROR:
281
                        $error = 'pcre.backtrack_limit reached.';
282
                        break;
283
                    case PREG_RECURSION_LIMIT_ERROR:
284
                        $error = 'pcre.recursion_limit reached.';
285
                        break;
286
                    case PREG_BAD_UTF8_ERROR:
287
                        $error = 'Malformed UTF-8 data.';
288
                        break;
289
                    case PREG_BAD_UTF8_OFFSET_ERROR:
290
                        $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
291
                        break;
292
                    default:
293
                        $error = 'Unable to parse.';
294
                }
295
296
                throw new ParseException($error, $this->getRealCurrentLineNb() + 1, $this->currentLine);
297
            }
298
        }
299
300
        if (isset($mbEncoding)) {
301
            mb_internal_encoding($mbEncoding);
302
        }
303
304
        if ($objectForMap && !is_object($data) && 'mapping' === $context) {
305
            $object = new \stdClass();
306
307
            foreach ($data as $key => $value) {
308
                $object->$key = $value;
309
            }
310
311
            $data = $object;
312
        }
313
314
        return empty($data) ? null : $data;
315
    }
316
317
    private function parseBlock($offset, $yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap)
318
    {
319
        $skippedLineNumbers = $this->skippedLineNumbers;
320
321
        foreach ($this->locallySkippedLineNumbers as $lineNumber) {
322
            if ($lineNumber < $offset) {
323
                continue;
324
            }
325
326
            $skippedLineNumbers[] = $lineNumber;
327
        }
328
329
        $parser = new self($offset, $this->totalNumberOfLines, $skippedLineNumbers);
330
        $parser->refs = &$this->refs;
331
332
        return $parser->parse($yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap);
333
    }
334
335
    /**
336
     * Returns the current line number (takes the offset into account).
337
     *
338
     * @return int The current line number
339
     */
340
    private function getRealCurrentLineNb()
341
    {
342
        $realCurrentLineNumber = $this->currentLineNb + $this->offset;
343
344
        foreach ($this->skippedLineNumbers as $skippedLineNumber) {
345
            if ($skippedLineNumber > $realCurrentLineNumber) {
346
                break;
347
            }
348
349
            ++$realCurrentLineNumber;
350
        }
351
352
        return $realCurrentLineNumber;
353
    }
354
355
    /**
356
     * Returns the current line indentation.
357
     *
358
     * @return int The current line indentation
359
     */
360
    private function getCurrentLineIndentation()
361
    {
362
        return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
363
    }
364
365
    /**
366
     * Returns the next embed block of YAML.
367
     *
368
     * @param int  $indentation The indent level at which the block is to be read, or null for default
369
     * @param bool $inSequence  True if the enclosing data structure is a sequence
370
     *
371
     * @return string A YAML string
0 ignored issues
show
Documentation introduced by
Should the return type not be null|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
372
     *
373
     * @throws ParseException When indentation problem are detected
374
     */
375
    private function getNextEmbedBlock($indentation = null, $inSequence = false)
376
    {
377
        $oldLineIndentation = $this->getCurrentLineIndentation();
378
        $blockScalarIndentations = array();
379
380
        if ($this->isBlockScalarHeader()) {
381
            $blockScalarIndentations[] = $this->getCurrentLineIndentation();
382
        }
383
384
        if (!$this->moveToNextLine()) {
385
            return;
386
        }
387
388
        if (null === $indentation) {
389
            $newIndent = $this->getCurrentLineIndentation();
390
391
            $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
392
393
            if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
394
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
395
            }
396
        } else {
397
            $newIndent = $indentation;
398
        }
399
400
        $data = array();
401
        if ($this->getCurrentLineIndentation() >= $newIndent) {
402
            $data[] = substr($this->currentLine, $newIndent);
403
        } else {
404
            $this->moveToPreviousLine();
405
406
            return;
407
        }
408
409
        if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
410
            // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
411
            // and therefore no nested list or mapping
412
            $this->moveToPreviousLine();
413
414
            return;
415
        }
416
417
        $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
418
419
        if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) {
420
            $blockScalarIndentations[] = $this->getCurrentLineIndentation();
421
        }
422
423
        $previousLineIndentation = $this->getCurrentLineIndentation();
424
425
        while ($this->moveToNextLine()) {
426
            $indent = $this->getCurrentLineIndentation();
427
428
            // terminate all block scalars that are more indented than the current line
429
            if (!empty($blockScalarIndentations) && $indent < $previousLineIndentation && trim($this->currentLine) !== '') {
430
                foreach ($blockScalarIndentations as $key => $blockScalarIndentation) {
431
                    if ($blockScalarIndentation >= $this->getCurrentLineIndentation()) {
432
                        unset($blockScalarIndentations[$key]);
433
                    }
434
                }
435
            }
436
437
            if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) {
438
                $blockScalarIndentations[] = $this->getCurrentLineIndentation();
439
            }
440
441
            $previousLineIndentation = $indent;
442
443
            if ($isItUnindentedCollection && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
444
                $this->moveToPreviousLine();
445
                break;
446
            }
447
448
            if ($this->isCurrentLineBlank()) {
449
                $data[] = substr($this->currentLine, $newIndent);
450
                continue;
451
            }
452
453
            // we ignore "comment" lines only when we are not inside a scalar block
454
            if (empty($blockScalarIndentations) && $this->isCurrentLineComment()) {
455
                // remember ignored comment lines (they are used later in nested
456
                // parser calls to determine real line numbers)
457
                //
458
                // CAUTION: beware to not populate the global property here as it
459
                // will otherwise influence the getRealCurrentLineNb() call here
460
                // for consecutive comment lines and subsequent embedded blocks
461
                $this->locallySkippedLineNumbers[] = $this->getRealCurrentLineNb();
462
463
                continue;
464
            }
465
466
            if ($indent >= $newIndent) {
467
                $data[] = substr($this->currentLine, $newIndent);
468
            } elseif (0 == $indent) {
469
                $this->moveToPreviousLine();
470
471
                break;
472
            } else {
473
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
474
            }
475
        }
476
477
        return implode("\n", $data);
478
    }
479
480
    /**
481
     * Moves the parser to the next line.
482
     *
483
     * @return bool
484
     */
485 View Code Duplication
    private function moveToNextLine()
486
    {
487
        if ($this->currentLineNb >= count($this->lines) - 1) {
488
            return false;
489
        }
490
491
        $this->currentLine = $this->lines[++$this->currentLineNb];
492
493
        return true;
494
    }
495
496
    /**
497
     * Moves the parser to the previous line.
498
     *
499
     * @return bool
500
     */
501 View Code Duplication
    private function moveToPreviousLine()
502
    {
503
        if ($this->currentLineNb < 1) {
504
            return false;
505
        }
506
507
        $this->currentLine = $this->lines[--$this->currentLineNb];
508
509
        return true;
510
    }
511
512
    /**
513
     * Parses a YAML value.
514
     *
515
     * @param string $value                  A YAML value
516
     * @param bool   $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise
517
     * @param bool   $objectSupport          True if object support is enabled, false otherwise
518
     * @param bool   $objectForMap           true if maps should return a stdClass instead of array()
519
     * @param string $context                The parser context (either sequence or mapping)
520
     *
521
     * @return mixed A PHP value
522
     *
523
     * @throws ParseException When reference does not exist
524
     */
525
    private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $context)
526
    {
527
        if (0 === strpos($value, '*')) {
528 View Code Duplication
            if (false !== $pos = strpos($value, '#')) {
529
                $value = substr($value, 1, $pos - 2);
530
            } else {
531
                $value = substr($value, 1);
532
            }
533
534
            if (!array_key_exists($value, $this->refs)) {
535
                throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine);
536
            }
537
538
            return $this->refs[$value];
539
        }
540
541
        if (preg_match('/^'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
542
            $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
543
544
            return $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers));
545
        }
546
547
        try {
548
            $parsedValue = Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
549
550
            if ('mapping' === $context && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
551
                @trigger_error(sprintf('Using a colon in the unquoted mapping value "%s" in line %d is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.', $value, $this->getRealCurrentLineNb() + 1), E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
552
553
                // to be thrown in 3.0
554
                // throw new ParseException('A colon cannot be used in an unquoted mapping value.');
555
            }
556
557
            return $parsedValue;
558
        } catch (ParseException $e) {
559
            $e->setParsedLine($this->getRealCurrentLineNb() + 1);
560
            $e->setSnippet($this->currentLine);
561
562
            throw $e;
563
        }
564
    }
565
566
    /**
567
     * Parses a block scalar.
568
     *
569
     * @param string $style       The style indicator that was used to begin this block scalar (| or >)
570
     * @param string $chomping    The chomping indicator that was used to begin this block scalar (+ or -)
571
     * @param int    $indentation The indentation indicator that was used to begin this block scalar
572
     *
573
     * @return string The text value
574
     */
575
    private function parseBlockScalar($style, $chomping = '', $indentation = 0)
576
    {
577
        $notEOF = $this->moveToNextLine();
578
        if (!$notEOF) {
579
            return '';
580
        }
581
582
        $isCurrentLineBlank = $this->isCurrentLineBlank();
583
        $blockLines = array();
584
585
        // leading blank lines are consumed before determining indentation
586
        while ($notEOF && $isCurrentLineBlank) {
587
            // newline only if not EOF
588
            if ($notEOF = $this->moveToNextLine()) {
589
                $blockLines[] = '';
590
                $isCurrentLineBlank = $this->isCurrentLineBlank();
591
            }
592
        }
593
594
        // determine indentation if not specified
595
        if (0 === $indentation) {
596
            if (preg_match('/^ +/', $this->currentLine, $matches)) {
597
                $indentation = strlen($matches[0]);
598
            }
599
        }
600
601
        if ($indentation > 0) {
602
            $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
603
604
            while (
605
                $notEOF && (
606
                    $isCurrentLineBlank ||
607
                    preg_match($pattern, $this->currentLine, $matches)
608
                )
609
            ) {
610
                if ($isCurrentLineBlank && strlen($this->currentLine) > $indentation) {
611
                    $blockLines[] = substr($this->currentLine, $indentation);
612
                } elseif ($isCurrentLineBlank) {
613
                    $blockLines[] = '';
614
                } else {
615
                    $blockLines[] = $matches[1];
0 ignored issues
show
Bug introduced by
The variable $matches does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
616
                }
617
618
                // newline only if not EOF
619
                if ($notEOF = $this->moveToNextLine()) {
620
                    $isCurrentLineBlank = $this->isCurrentLineBlank();
621
                }
622
            }
623
        } elseif ($notEOF) {
624
            $blockLines[] = '';
625
        }
626
627
        if ($notEOF) {
628
            $blockLines[] = '';
629
            $this->moveToPreviousLine();
630
        } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
631
            $blockLines[] = '';
632
        }
633
634
        // folded style
635
        if ('>' === $style) {
636
            $text = '';
637
            $previousLineIndented = false;
638
            $previousLineBlank = false;
639
640
            for ($i = 0; $i < count($blockLines); ++$i) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
641
                if ('' === $blockLines[$i]) {
642
                    $text .= "\n";
643
                    $previousLineIndented = false;
644
                    $previousLineBlank = true;
645
                } elseif (' ' === $blockLines[$i][0]) {
646
                    $text .= "\n".$blockLines[$i];
647
                    $previousLineIndented = true;
648
                    $previousLineBlank = false;
649 View Code Duplication
                } elseif ($previousLineIndented) {
650
                    $text .= "\n".$blockLines[$i];
651
                    $previousLineIndented = false;
652
                    $previousLineBlank = false;
653
                } elseif ($previousLineBlank || 0 === $i) {
654
                    $text .= $blockLines[$i];
655
                    $previousLineIndented = false;
656
                    $previousLineBlank = false;
657 View Code Duplication
                } else {
658
                    $text .= ' '.$blockLines[$i];
659
                    $previousLineIndented = false;
660
                    $previousLineBlank = false;
661
                }
662
            }
663
        } else {
664
            $text = implode("\n", $blockLines);
665
        }
666
667
        // deal with trailing newlines
668
        if ('' === $chomping) {
669
            $text = preg_replace('/\n+$/', "\n", $text);
670
        } elseif ('-' === $chomping) {
671
            $text = preg_replace('/\n+$/', '', $text);
672
        }
673
674
        return $text;
675
    }
676
677
    /**
678
     * Returns true if the next line is indented.
679
     *
680
     * @return bool Returns true if the next line is indented, false otherwise
681
     */
682
    private function isNextLineIndented()
683
    {
684
        $currentIndentation = $this->getCurrentLineIndentation();
685
        $EOF = !$this->moveToNextLine();
686
687
        while (!$EOF && $this->isCurrentLineEmpty()) {
688
            $EOF = !$this->moveToNextLine();
689
        }
690
691
        if ($EOF) {
692
            return false;
693
        }
694
695
        $ret = false;
696
        if ($this->getCurrentLineIndentation() > $currentIndentation) {
697
            $ret = true;
698
        }
699
700
        $this->moveToPreviousLine();
701
702
        return $ret;
703
    }
704
705
    /**
706
     * Returns true if the current line is blank or if it is a comment line.
707
     *
708
     * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
709
     */
710
    private function isCurrentLineEmpty()
711
    {
712
        return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
713
    }
714
715
    /**
716
     * Returns true if the current line is blank.
717
     *
718
     * @return bool Returns true if the current line is blank, false otherwise
719
     */
720
    private function isCurrentLineBlank()
721
    {
722
        return '' == trim($this->currentLine, ' ');
723
    }
724
725
    /**
726
     * Returns true if the current line is a comment line.
727
     *
728
     * @return bool Returns true if the current line is a comment line, false otherwise
729
     */
730
    private function isCurrentLineComment()
731
    {
732
        //checking explicitly the first char of the trim is faster than loops or strpos
733
        $ltrimmedLine = ltrim($this->currentLine, ' ');
734
735
        return '' !== $ltrimmedLine && $ltrimmedLine[0] === '#';
736
    }
737
738
    private function isCurrentLineLastLineInDocument()
739
    {
740
        return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1);
741
    }
742
743
    /**
744
     * Cleanups a YAML string to be parsed.
745
     *
746
     * @param string $value The input YAML string
747
     *
748
     * @return string A cleaned up YAML string
749
     */
750
    private function cleanup($value)
751
    {
752
        $value = str_replace(array("\r\n", "\r"), "\n", $value);
753
754
        // strip YAML header
755
        $count = 0;
756
        $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
757
        $this->offset += $count;
758
759
        // remove leading comments
760
        $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
761
        if ($count == 1) {
762
            // items have been removed, update the offset
763
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
764
            $value = $trimmedValue;
765
        }
766
767
        // remove start of the document marker (---)
768
        $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
769
        if ($count == 1) {
770
            // items have been removed, update the offset
771
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
772
            $value = $trimmedValue;
773
774
            // remove end of the document marker (...)
775
            $value = preg_replace('#\.\.\.\s*$#', '', $value);
776
        }
777
778
        return $value;
779
    }
780
781
    /**
782
     * Returns true if the next line starts unindented collection.
783
     *
784
     * @return bool Returns true if the next line starts unindented collection, false otherwise
785
     */
786
    private function isNextLineUnIndentedCollection()
787
    {
788
        $currentIndentation = $this->getCurrentLineIndentation();
789
        $notEOF = $this->moveToNextLine();
790
791
        while ($notEOF && $this->isCurrentLineEmpty()) {
792
            $notEOF = $this->moveToNextLine();
793
        }
794
795
        if (false === $notEOF) {
796
            return false;
797
        }
798
799
        $ret = false;
800
        if (
801
            $this->getCurrentLineIndentation() == $currentIndentation
802
            &&
803
            $this->isStringUnIndentedCollectionItem()
804
        ) {
805
            $ret = true;
806
        }
807
808
        $this->moveToPreviousLine();
809
810
        return $ret;
811
    }
812
813
    /**
814
     * Returns true if the string is un-indented collection item.
815
     *
816
     * @return bool Returns true if the string is un-indented collection item, false otherwise
817
     */
818
    private function isStringUnIndentedCollectionItem()
819
    {
820
        return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- ');
821
    }
822
823
    /**
824
     * Tests whether or not the current line is the header of a block scalar.
825
     *
826
     * @return bool
827
     */
828
    private function isBlockScalarHeader()
829
    {
830
        return (bool) preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine);
831
    }
832
}
833