Completed
Pull Request — master (#462)
by Richard
15:24
created

Parser::isCurrentLineBlank()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
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 (false === 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 (self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($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']) && self::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
                        && self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($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) {
125
                    $this->refs[$isRef] = end($data);
126
                }
127
            } elseif (
128
                self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
129
                && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))
130
            ) {
131
                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...
132
                    throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine);
133
                }
134
                $context = 'mapping';
135
136
                // force correct settings
137
                Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
138
                try {
139
                    $key = Inline::parseScalar($values['key']);
140
                } catch (ParseException $e) {
141
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
142
                    $e->setSnippet($this->currentLine);
143
144
                    throw $e;
145
                }
146
147
                // Convert float keys to strings, to avoid being converted to integers by PHP
148
                if (is_float($key)) {
149
                    $key = (string) $key;
150
                }
151
152
                if ('<<' === $key) {
153
                    $mergeNode = true;
154
                    $allowOverwrite = true;
155
                    if (isset($values['value']) && 0 === strpos($values['value'], '*')) {
156
                        $refName = substr($values['value'], 1);
157
                        if (!array_key_exists($refName, $this->refs)) {
158
                            throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
159
                        }
160
161
                        $refValue = $this->refs[$refName];
162
163
                        if (!is_array($refValue)) {
164
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
165
                        }
166
167
                        $data += $refValue; // array union
168
                    } else {
169
                        if (isset($values['value']) && $values['value'] !== '') {
170
                            $value = $values['value'];
171
                        } else {
172
                            $value = $this->getNextEmbedBlock();
173
                        }
174
                        $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
175
176
                        if (!is_array($parsed)) {
177
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
178
                        }
179
180
                        if (isset($parsed[0])) {
181
                            // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
182
                            // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
183
                            // in the sequence override keys specified in later mapping nodes.
184
                            foreach ($parsed as $parsedItem) {
185
                                if (!is_array($parsedItem)) {
186
                                    throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
187
                                }
188
189
                                $data += $parsedItem; // array union
190
                            }
191
                        } else {
192
                            // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
193
                            // current mapping, unless the key already exists in it.
194
                            $data += $parsed; // array union
195
                        }
196
                    }
197 View Code Duplication
                } elseif (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
198
                    $isRef = $matches['ref'];
199
                    $values['value'] = $matches['value'];
200
                }
201
202
                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...
203
                    // Merge keys
204
                } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
205
                    // hash
206
                    // if next line is less indented or equal, then it means that the current value is null
207
                    if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
208
                        // Spec: Keys MUST be unique; first one wins.
209
                        // But overwriting is allowed when a merge node is used in current block.
210
                        if ($allowOverwrite || !isset($data[$key])) {
211
                            $data[$key] = null;
212
                        }
213
                    } else {
214
                        $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap);
215
                        // Spec: Keys MUST be unique; first one wins.
216
                        // But overwriting is allowed when a merge node is used in current block.
217
                        if ($allowOverwrite || !isset($data[$key])) {
218
                            $data[$key] = $value;
219
                        }
220
                    }
221
                } else {
222
                    $value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
223
                    // Spec: Keys MUST be unique; first one wins.
224
                    // But overwriting is allowed when a merge node is used in current block.
225
                    if ($allowOverwrite || !isset($data[$key])) {
226
                        $data[$key] = $value;
227
                    }
228
                }
229
                if ($isRef) {
230
                    $this->refs[$isRef] = $data[$key];
231
                }
232
            } else {
233
                // multiple documents are not supported
234
                if ('---' === $this->currentLine) {
235
                    throw new ParseException('Multiple documents are not supported.', $this->currentLineNb + 1, $this->currentLine);
236
                }
237
238
                // 1-liner optionally followed by newline(s)
239
                if (is_string($value) && $this->lines[0] === trim($value)) {
240
                    try {
241
                        $value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
242
                    } catch (ParseException $e) {
243
                        $e->setParsedLine($this->getRealCurrentLineNb() + 1);
244
                        $e->setSnippet($this->currentLine);
245
246
                        throw $e;
247
                    }
248
249
                    if (isset($mbEncoding)) {
250
                        mb_internal_encoding($mbEncoding);
251
                    }
252
253
                    return $value;
254
                }
255
256
                throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
257
            }
258
        }
259
260
        if (isset($mbEncoding)) {
261
            mb_internal_encoding($mbEncoding);
262
        }
263
264
        if ($objectForMap && !is_object($data) && 'mapping' === $context) {
265
            $object = new \stdClass();
266
267
            foreach ($data as $key => $value) {
268
                $object->$key = $value;
269
            }
270
271
            $data = $object;
272
        }
273
274
        return empty($data) ? null : $data;
275
    }
276
277
    private function parseBlock($offset, $yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap)
278
    {
279
        $skippedLineNumbers = $this->skippedLineNumbers;
280
281
        foreach ($this->locallySkippedLineNumbers as $lineNumber) {
282
            if ($lineNumber < $offset) {
283
                continue;
284
            }
285
286
            $skippedLineNumbers[] = $lineNumber;
287
        }
288
289
        $parser = new self($offset, $this->totalNumberOfLines, $skippedLineNumbers);
290
        $parser->refs = &$this->refs;
291
292
        return $parser->parse($yaml, $exceptionOnInvalidType, $objectSupport, $objectForMap);
293
    }
294
295
    /**
296
     * Returns the current line number (takes the offset into account).
297
     *
298
     * @return int The current line number
299
     */
300
    private function getRealCurrentLineNb()
301
    {
302
        $realCurrentLineNumber = $this->currentLineNb + $this->offset;
303
304
        foreach ($this->skippedLineNumbers as $skippedLineNumber) {
305
            if ($skippedLineNumber > $realCurrentLineNumber) {
306
                break;
307
            }
308
309
            ++$realCurrentLineNumber;
310
        }
311
312
        return $realCurrentLineNumber;
313
    }
314
315
    /**
316
     * Returns the current line indentation.
317
     *
318
     * @return int The current line indentation
319
     */
320
    private function getCurrentLineIndentation()
321
    {
322
        return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
323
    }
324
325
    /**
326
     * Returns the next embed block of YAML.
327
     *
328
     * @param int  $indentation The indent level at which the block is to be read, or null for default
329
     * @param bool $inSequence  True if the enclosing data structure is a sequence
330
     *
331
     * @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...
332
     *
333
     * @throws ParseException When indentation problem are detected
334
     */
335
    private function getNextEmbedBlock($indentation = null, $inSequence = false)
336
    {
337
        $oldLineIndentation = $this->getCurrentLineIndentation();
338
        $blockScalarIndentations = array();
339
340
        if ($this->isBlockScalarHeader()) {
341
            $blockScalarIndentations[] = $this->getCurrentLineIndentation();
342
        }
343
344
        if (!$this->moveToNextLine()) {
345
            return;
346
        }
347
348
        if (null === $indentation) {
349
            $newIndent = $this->getCurrentLineIndentation();
350
351
            $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
352
353
            if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
354
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
355
            }
356
        } else {
357
            $newIndent = $indentation;
358
        }
359
360
        $data = array();
361
        if ($this->getCurrentLineIndentation() >= $newIndent) {
362
            $data[] = substr($this->currentLine, $newIndent);
363
        } else {
364
            $this->moveToPreviousLine();
365
366
            return;
367
        }
368
369
        if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
370
            // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
371
            // and therefore no nested list or mapping
372
            $this->moveToPreviousLine();
373
374
            return;
375
        }
376
377
        $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
378
379
        if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) {
380
            $blockScalarIndentations[] = $this->getCurrentLineIndentation();
381
        }
382
383
        $previousLineIndentation = $this->getCurrentLineIndentation();
384
385
        while ($this->moveToNextLine()) {
386
            $indent = $this->getCurrentLineIndentation();
387
388
            // terminate all block scalars that are more indented than the current line
389
            if (!empty($blockScalarIndentations) && $indent < $previousLineIndentation && trim($this->currentLine) !== '') {
390
                foreach ($blockScalarIndentations as $key => $blockScalarIndentation) {
391
                    if ($blockScalarIndentation >= $this->getCurrentLineIndentation()) {
392
                        unset($blockScalarIndentations[$key]);
393
                    }
394
                }
395
            }
396
397
            if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) {
398
                $blockScalarIndentations[] = $this->getCurrentLineIndentation();
399
            }
400
401
            $previousLineIndentation = $indent;
402
403
            if ($isItUnindentedCollection && !$this->isCurrentLineEmpty() && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
404
                $this->moveToPreviousLine();
405
                break;
406
            }
407
408
            if ($this->isCurrentLineBlank()) {
409
                $data[] = substr($this->currentLine, $newIndent);
410
                continue;
411
            }
412
413
            // we ignore "comment" lines only when we are not inside a scalar block
414
            if (empty($blockScalarIndentations) && $this->isCurrentLineComment()) {
415
                // remember ignored comment lines (they are used later in nested
416
                // parser calls to determine real line numbers)
417
                //
418
                // CAUTION: beware to not populate the global property here as it
419
                // will otherwise influence the getRealCurrentLineNb() call here
420
                // for consecutive comment lines and subsequent embedded blocks
421
                $this->locallySkippedLineNumbers[] = $this->getRealCurrentLineNb();
422
423
                continue;
424
            }
425
426
            if ($indent >= $newIndent) {
427
                $data[] = substr($this->currentLine, $newIndent);
428
            } elseif (0 == $indent) {
429
                $this->moveToPreviousLine();
430
431
                break;
432
            } else {
433
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
434
            }
435
        }
436
437
        return implode("\n", $data);
438
    }
439
440
    /**
441
     * Moves the parser to the next line.
442
     *
443
     * @return bool
444
     */
445 View Code Duplication
    private function moveToNextLine()
446
    {
447
        if ($this->currentLineNb >= count($this->lines) - 1) {
448
            return false;
449
        }
450
451
        $this->currentLine = $this->lines[++$this->currentLineNb];
452
453
        return true;
454
    }
455
456
    /**
457
     * Moves the parser to the previous line.
458
     *
459
     * @return bool
460
     */
461 View Code Duplication
    private function moveToPreviousLine()
462
    {
463
        if ($this->currentLineNb < 1) {
464
            return false;
465
        }
466
467
        $this->currentLine = $this->lines[--$this->currentLineNb];
468
469
        return true;
470
    }
471
472
    /**
473
     * Parses a YAML value.
474
     *
475
     * @param string $value                  A YAML value
476
     * @param bool   $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise
477
     * @param bool   $objectSupport          True if object support is enabled, false otherwise
478
     * @param bool   $objectForMap           true if maps should return a stdClass instead of array()
479
     * @param string $context                The parser context (either sequence or mapping)
480
     *
481
     * @return mixed A PHP value
482
     *
483
     * @throws ParseException When reference does not exist
484
     */
485
    private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $context)
486
    {
487
        if (0 === strpos($value, '*')) {
488 View Code Duplication
            if (false !== $pos = strpos($value, '#')) {
489
                $value = substr($value, 1, $pos - 2);
490
            } else {
491
                $value = substr($value, 1);
492
            }
493
494
            if (!array_key_exists($value, $this->refs)) {
495
                throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLineNb + 1, $this->currentLine);
496
            }
497
498
            return $this->refs[$value];
499
        }
500
501
        if (self::preg_match('/^'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
502
            $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
503
504
            return $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers));
505
        }
506
507
        try {
508
            $parsedValue = Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
509
510
            if ('mapping' === $context && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
511
                @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...
512
513
                // to be thrown in 3.0
514
                // throw new ParseException('A colon cannot be used in an unquoted mapping value.');
515
            }
516
517
            return $parsedValue;
518
        } catch (ParseException $e) {
519
            $e->setParsedLine($this->getRealCurrentLineNb() + 1);
520
            $e->setSnippet($this->currentLine);
521
522
            throw $e;
523
        }
524
    }
525
526
    /**
527
     * Parses a block scalar.
528
     *
529
     * @param string $style       The style indicator that was used to begin this block scalar (| or >)
530
     * @param string $chomping    The chomping indicator that was used to begin this block scalar (+ or -)
531
     * @param int    $indentation The indentation indicator that was used to begin this block scalar
532
     *
533
     * @return string The text value
534
     */
535
    private function parseBlockScalar($style, $chomping = '', $indentation = 0)
536
    {
537
        $notEOF = $this->moveToNextLine();
538
        if (!$notEOF) {
539
            return '';
540
        }
541
542
        $isCurrentLineBlank = $this->isCurrentLineBlank();
543
        $blockLines = array();
544
545
        // leading blank lines are consumed before determining indentation
546
        while ($notEOF && $isCurrentLineBlank) {
547
            // newline only if not EOF
548
            if ($notEOF = $this->moveToNextLine()) {
549
                $blockLines[] = '';
550
                $isCurrentLineBlank = $this->isCurrentLineBlank();
551
            }
552
        }
553
554
        // determine indentation if not specified
555
        if (0 === $indentation) {
556
            if (self::preg_match('/^ +/', $this->currentLine, $matches)) {
557
                $indentation = strlen($matches[0]);
558
            }
559
        }
560
561
        if ($indentation > 0) {
562
            $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
563
564
            while (
565
                $notEOF && (
566
                    $isCurrentLineBlank ||
567
                    self::preg_match($pattern, $this->currentLine, $matches)
568
                )
569
            ) {
570
                if ($isCurrentLineBlank && strlen($this->currentLine) > $indentation) {
571
                    $blockLines[] = substr($this->currentLine, $indentation);
572
                } elseif ($isCurrentLineBlank) {
573
                    $blockLines[] = '';
574
                } else {
575
                    $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...
576
                }
577
578
                // newline only if not EOF
579
                if ($notEOF = $this->moveToNextLine()) {
580
                    $isCurrentLineBlank = $this->isCurrentLineBlank();
581
                }
582
            }
583
        } elseif ($notEOF) {
584
            $blockLines[] = '';
585
        }
586
587
        if ($notEOF) {
588
            $blockLines[] = '';
589
            $this->moveToPreviousLine();
590
        } elseif (!$notEOF && !$this->isCurrentLineLastLineInDocument()) {
591
            $blockLines[] = '';
592
        }
593
594
        // folded style
595
        if ('>' === $style) {
596
            $text = '';
597
            $previousLineIndented = false;
598
            $previousLineBlank = false;
599
600
            for ($i = 0, $blockLinesCount = count($blockLines); $i < $blockLinesCount; ++$i) {
601
                if ('' === $blockLines[$i]) {
602
                    $text .= "\n";
603
                    $previousLineIndented = false;
604
                    $previousLineBlank = true;
605
                } elseif (' ' === $blockLines[$i][0]) {
606
                    $text .= "\n".$blockLines[$i];
607
                    $previousLineIndented = true;
608
                    $previousLineBlank = false;
609 View Code Duplication
                } elseif ($previousLineIndented) {
610
                    $text .= "\n".$blockLines[$i];
611
                    $previousLineIndented = false;
612
                    $previousLineBlank = false;
613
                } elseif ($previousLineBlank || 0 === $i) {
614
                    $text .= $blockLines[$i];
615
                    $previousLineIndented = false;
616
                    $previousLineBlank = false;
617 View Code Duplication
                } else {
618
                    $text .= ' '.$blockLines[$i];
619
                    $previousLineIndented = false;
620
                    $previousLineBlank = false;
621
                }
622
            }
623
        } else {
624
            $text = implode("\n", $blockLines);
625
        }
626
627
        // deal with trailing newlines
628
        if ('' === $chomping) {
629
            $text = preg_replace('/\n+$/', "\n", $text);
630
        } elseif ('-' === $chomping) {
631
            $text = preg_replace('/\n+$/', '', $text);
632
        }
633
634
        return $text;
635
    }
636
637
    /**
638
     * Returns true if the next line is indented.
639
     *
640
     * @return bool Returns true if the next line is indented, false otherwise
641
     */
642
    private function isNextLineIndented()
643
    {
644
        $currentIndentation = $this->getCurrentLineIndentation();
645
        $EOF = !$this->moveToNextLine();
646
647
        while (!$EOF && $this->isCurrentLineEmpty()) {
648
            $EOF = !$this->moveToNextLine();
649
        }
650
651
        if ($EOF) {
652
            return false;
653
        }
654
655
        $ret = $this->getCurrentLineIndentation() > $currentIndentation;
656
657
        $this->moveToPreviousLine();
658
659
        return $ret;
660
    }
661
662
    /**
663
     * Returns true if the current line is blank or if it is a comment line.
664
     *
665
     * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
666
     */
667
    private function isCurrentLineEmpty()
668
    {
669
        return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
670
    }
671
672
    /**
673
     * Returns true if the current line is blank.
674
     *
675
     * @return bool Returns true if the current line is blank, false otherwise
676
     */
677
    private function isCurrentLineBlank()
678
    {
679
        return '' == trim($this->currentLine, ' ');
680
    }
681
682
    /**
683
     * Returns true if the current line is a comment line.
684
     *
685
     * @return bool Returns true if the current line is a comment line, false otherwise
686
     */
687
    private function isCurrentLineComment()
688
    {
689
        //checking explicitly the first char of the trim is faster than loops or strpos
690
        $ltrimmedLine = ltrim($this->currentLine, ' ');
691
692
        return '' !== $ltrimmedLine && $ltrimmedLine[0] === '#';
693
    }
694
695
    private function isCurrentLineLastLineInDocument()
696
    {
697
        return ($this->offset + $this->currentLineNb) >= ($this->totalNumberOfLines - 1);
698
    }
699
700
    /**
701
     * Cleanups a YAML string to be parsed.
702
     *
703
     * @param string $value The input YAML string
704
     *
705
     * @return string A cleaned up YAML string
706
     */
707
    private function cleanup($value)
708
    {
709
        $value = str_replace(array("\r\n", "\r"), "\n", $value);
710
711
        // strip YAML header
712
        $count = 0;
713
        $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
714
        $this->offset += $count;
715
716
        // remove leading comments
717
        $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
718
        if ($count == 1) {
719
            // items have been removed, update the offset
720
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
721
            $value = $trimmedValue;
722
        }
723
724
        // remove start of the document marker (---)
725
        $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
726
        if ($count == 1) {
727
            // items have been removed, update the offset
728
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
729
            $value = $trimmedValue;
730
731
            // remove end of the document marker (...)
732
            $value = preg_replace('#\.\.\.\s*$#', '', $value);
733
        }
734
735
        return $value;
736
    }
737
738
    /**
739
     * Returns true if the next line starts unindented collection.
740
     *
741
     * @return bool Returns true if the next line starts unindented collection, false otherwise
742
     */
743
    private function isNextLineUnIndentedCollection()
744
    {
745
        $currentIndentation = $this->getCurrentLineIndentation();
746
        $notEOF = $this->moveToNextLine();
747
748
        while ($notEOF && $this->isCurrentLineEmpty()) {
749
            $notEOF = $this->moveToNextLine();
750
        }
751
752
        if (false === $notEOF) {
753
            return false;
754
        }
755
756
        $ret = $this->getCurrentLineIndentation() === $currentIndentation && $this->isStringUnIndentedCollectionItem();
757
758
        $this->moveToPreviousLine();
759
760
        return $ret;
761
    }
762
763
    /**
764
     * Returns true if the string is un-indented collection item.
765
     *
766
     * @return bool Returns true if the string is un-indented collection item, false otherwise
767
     */
768
    private function isStringUnIndentedCollectionItem()
769
    {
770
        return '-' === rtrim($this->currentLine) || 0 === strpos($this->currentLine, '- ');
771
    }
772
773
    /**
774
     * Tests whether or not the current line is the header of a block scalar.
775
     *
776
     * @return bool
777
     */
778
    private function isBlockScalarHeader()
779
    {
780
        return (bool) self::preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine);
781
    }
782
783
    /**
784
     * A local wrapper for `preg_match` which will throw a ParseException if there
785
     * is an internal error in the PCRE engine.
786
     *
787
     * This avoids us needing to check for "false" every time PCRE is used
788
     * in the YAML engine
789
     *
790
     * @throws ParseException on a PCRE internal error
791
     *
792
     * @see preg_last_error()
793
     *
794
     * @internal
795
     */
796
    public static function preg_match($pattern, $subject, &$matches = null, $flags = 0, $offset = 0)
797
    {
798
        if (false === $ret = preg_match($pattern, $subject, $matches, $flags, $offset)) {
799
            switch (preg_last_error()) {
800
                case PREG_INTERNAL_ERROR:
801
                    $error = 'Internal PCRE error.';
802
                    break;
803
                case PREG_BACKTRACK_LIMIT_ERROR:
804
                    $error = 'pcre.backtrack_limit reached.';
805
                    break;
806
                case PREG_RECURSION_LIMIT_ERROR:
807
                    $error = 'pcre.recursion_limit reached.';
808
                    break;
809
                case PREG_BAD_UTF8_ERROR:
810
                    $error = 'Malformed UTF-8 data.';
811
                    break;
812
                case PREG_BAD_UTF8_OFFSET_ERROR:
813
                    $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
814
                    break;
815
                default:
816
                    $error = 'Error.';
817
            }
818
819
            throw new ParseException($error);
820
        }
821
822
        return $ret;
823
    }
824
}
825