Completed
Push — master ( 230074...33d096 )
by Michael
09:38
created

Parser   D

Complexity

Total Complexity 180

Size/Duplication

Total Lines 830
Duplicated Lines 5.18 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
dl 43
loc 830
rs 4.4444
c 0
b 0
f 0
wmc 180
lcom 1
cbo 2

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
C parse() 0 39 7
F doParse() 8 200 59
A getRealCurrentLineNb() 0 14 3
A getCurrentLineIndentation() 0 4 1
D getNextEmbedBlock() 0 104 32
A moveToNextLine() 10 10 2
A moveToPreviousLine() 10 10 2
C parseValue() 5 40 14
A isNextLineIndented() 0 19 4
A isCurrentLineEmpty() 0 4 2
A isCurrentLineBlank() 0 4 1
A isCurrentLineComment() 0 7 2
A isCurrentLineLastLineInDocument() 0 4 1
B cleanup() 0 30 3
B isNextLineUnIndentedCollection() 0 19 5
A isStringUnIndentedCollectionItem() 0 4 2
A isBlockScalarHeader() 0 4 1
C preg_match() 0 28 7

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Parser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Parser, and based on these observations, apply Extract Interface, too.

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
68
        $this->refs = array();
69
70
        $mbEncoding = null;
71
        $e = null;
72
        $data = null;
73
74
        if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
75
            $mbEncoding = mb_internal_encoding();
76
            mb_internal_encoding('UTF-8');
77
        }
78
79
        try {
80
            $data = $this->doParse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
81
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
82
        } catch (\Throwable $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

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