Completed
Push — master ( 96df1a...267f86 )
by Naveen
09:52
created

Parser::parse()   F

Complexity

Conditions 75
Paths 3485

Size

Total Lines 257
Code Lines 158

Duplication

Lines 8
Ratio 3.11 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 8
loc 257
rs 2
cc 75
eloc 158
nc 3485
nop 4

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\Yaml;
13
14
use Symfony\Component\Yaml\Exception\ParseException;
15
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
25
    private $offset = 0;
26
    private $lines = array();
27
    private $currentLineNb = -1;
28
    private $currentLine = '';
29
    private $refs = array();
30
31
    /**
32
     * Constructor.
33
     *
34
     * @param int $offset The offset of YAML document (used for line numbers in error messages)
35
     */
36
    public function __construct($offset = 0)
37
    {
38
        $this->offset = $offset;
39
    }
40
41
    /**
42
     * Parses a YAML string to a PHP value.
43
     *
44
     * @param string $value                  A YAML string
45
     * @param bool   $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
46
     * @param bool   $objectSupport          true if object support is enabled, false otherwise
47
     * @param bool   $objectForMap           true if maps should return a stdClass instead of array()
48
     *
49
     * @return mixed A PHP value
50
     *
51
     * @throws ParseException If the YAML is not valid
52
     */
53
    public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
54
    {
55
        if (!preg_match('//u', $value)) {
56
            throw new ParseException('The YAML value does not appear to be valid UTF-8.');
57
        }
58
        $this->currentLineNb = -1;
59
        $this->currentLine = '';
60
        $value = $this->cleanup($value);
61
        $this->lines = explode("\n", $value);
62
63
        if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
64
            $mbEncoding = mb_internal_encoding();
65
            mb_internal_encoding('UTF-8');
66
        }
67
68
        $data = array();
69
        $context = null;
70
        $allowOverwrite = false;
71
        while ($this->moveToNextLine()) {
72
            if ($this->isCurrentLineEmpty()) {
73
                continue;
74
            }
75
76
            // tab?
77
            if ("\t" === $this->currentLine[0]) {
78
                throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
79
            }
80
81
            $isRef = $mergeNode = false;
82
            if (preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+?))?\s*$#u', $this->currentLine, $values)) {
83
                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...
84
                    throw new ParseException('You cannot define a sequence item when in a mapping');
85
                }
86
                $context = 'sequence';
87
88 View Code Duplication
                if (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
89
                    $isRef = $matches['ref'];
90
                    $values['value'] = $matches['value'];
91
                }
92
93
                // array
94
                if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
95
                    $c = $this->getRealCurrentLineNb() + 1;
96
                    $parser = new self($c);
97
                    $parser->refs = &$this->refs;
98
                    $data[] = $parser->parse($this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap);
99
                } else {
100
                    if (isset($values['leadspaces'])
101
                        && preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $values['value'], $matches)
102
                    ) {
103
                        // this is a compact notation element, add to next block and parse
104
                        $c = $this->getRealCurrentLineNb();
105
                        $parser = new self($c);
106
                        $parser->refs = &$this->refs;
107
108
                        $block = $values['value'];
109
                        if ($this->isNextLineIndented()) {
110
                            $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1);
111
                        }
112
113
                        $data[] = $parser->parse($block, $exceptionOnInvalidType, $objectSupport, $objectForMap);
114
                    } else {
115
                        $data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
116
                    }
117
                }
118
                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...
119
                    $this->refs[$isRef] = end($data);
120
                }
121
            } 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('"', "'")))) {
122
                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...
123
                    throw new ParseException('You cannot define a mapping item when in a sequence');
124
                }
125
                $context = 'mapping';
126
127
                // force correct settings
128
                Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
129
                try {
130
                    $key = Inline::parseScalar($values['key']);
131
                } catch (ParseException $e) {
132
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
133
                    $e->setSnippet($this->currentLine);
134
135
                    throw $e;
136
                }
137
138
                // Convert float keys to strings, to avoid being converted to integers by PHP
139
                if (is_float($key)) {
140
                    $key = (string) $key;
141
                }
142
143
                if ('<<' === $key) {
144
                    $mergeNode = true;
145
                    $allowOverwrite = true;
146
                    if (isset($values['value']) && 0 === strpos($values['value'], '*')) {
147
                        $refName = substr($values['value'], 1);
148
                        if (!array_key_exists($refName, $this->refs)) {
149
                            throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
150
                        }
151
152
                        $refValue = $this->refs[$refName];
153
154
                        if (!is_array($refValue)) {
155
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
156
                        }
157
158
                        foreach ($refValue as $key => $value) {
159
                            if (!isset($data[$key])) {
160
                                $data[$key] = $value;
161
                            }
162
                        }
163
                    } else {
164
                        if (isset($values['value']) && $values['value'] !== '') {
165
                            $value = $values['value'];
166
                        } else {
167
                            $value = $this->getNextEmbedBlock();
168
                        }
169
                        $c = $this->getRealCurrentLineNb() + 1;
170
                        $parser = new self($c);
171
                        $parser->refs = &$this->refs;
172
                        $parsed = $parser->parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
173
174
                        if (!is_array($parsed)) {
175
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
176
                        }
177
178
                        if (isset($parsed[0])) {
179
                            // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
180
                            // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
181
                            // in the sequence override keys specified in later mapping nodes.
182
                            foreach ($parsed as $parsedItem) {
183
                                if (!is_array($parsedItem)) {
184
                                    throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
185
                                }
186
187
                                foreach ($parsedItem as $key => $value) {
188
                                    if (!isset($data[$key])) {
189
                                        $data[$key] = $value;
190
                                    }
191
                                }
192
                            }
193
                        } else {
194
                            // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
195
                            // current mapping, unless the key already exists in it.
196
                            foreach ($parsed as $key => $value) {
197
                                if (!isset($data[$key])) {
198
                                    $data[$key] = $value;
199
                                }
200
                            }
201
                        }
202
                    }
203 View Code Duplication
                } elseif (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
204
                    $isRef = $matches['ref'];
205
                    $values['value'] = $matches['value'];
206
                }
207
208
                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...
209
                    // Merge keys
210
                } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
211
                    // hash
212
                    // if next line is less indented or equal, then it means that the current value is null
213
                    if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
214
                        // Spec: Keys MUST be unique; first one wins.
215
                        // But overwriting is allowed when a merge node is used in current block.
216
                        if ($allowOverwrite || !isset($data[$key])) {
217
                            $data[$key] = null;
218
                        }
219
                    } else {
220
                        $c = $this->getRealCurrentLineNb() + 1;
221
                        $parser = new self($c);
222
                        $parser->refs = &$this->refs;
223
                        $value = $parser->parse($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.');
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)) {
305
            $data = (object) $data;
306
        }
307
308
        return empty($data) ? null : $data;
309
    }
310
311
    /**
312
     * Returns the current line number (takes the offset into account).
313
     *
314
     * @return int The current line number
315
     */
316
    private function getRealCurrentLineNb()
317
    {
318
        return $this->currentLineNb + $this->offset;
319
    }
320
321
    /**
322
     * Returns the current line indentation.
323
     *
324
     * @return int The current line indentation
325
     */
326
    private function getCurrentLineIndentation()
327
    {
328
        return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
329
    }
330
331
    /**
332
     * Returns the next embed block of YAML.
333
     *
334
     * @param int  $indentation The indent level at which the block is to be read, or null for default
335
     * @param bool $inSequence  True if the enclosing data structure is a sequence
336
     *
337
     * @return string A YAML string
338
     *
339
     * @throws ParseException When indentation problem are detected
340
     */
341
    private function getNextEmbedBlock($indentation = null, $inSequence = false)
342
    {
343
        $oldLineIndentation = $this->getCurrentLineIndentation();
344
        $blockScalarIndentations = array();
345
346
        if ($this->isBlockScalarHeader()) {
347
            $blockScalarIndentations[] = $this->getCurrentLineIndentation();
348
        }
349
350
        if (!$this->moveToNextLine()) {
351
            return;
352
        }
353
354
        if (null === $indentation) {
355
            $newIndent = $this->getCurrentLineIndentation();
356
357
            $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem();
358
359
            if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
360
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
361
            }
362
        } else {
363
            $newIndent = $indentation;
364
        }
365
366
        $data = array();
367
        if ($this->getCurrentLineIndentation() >= $newIndent) {
368
            $data[] = substr($this->currentLine, $newIndent);
369
        } else {
370
            $this->moveToPreviousLine();
371
372
            return;
373
        }
374
375
        if ($inSequence && $oldLineIndentation === $newIndent && isset($data[0][0]) && '-' === $data[0][0]) {
376
            // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
377
            // and therefore no nested list or mapping
378
            $this->moveToPreviousLine();
379
380
            return;
381
        }
382
383
        $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem();
384
385
        if (empty($blockScalarIndentations) && $this->isBlockScalarHeader()) {
386
            $blockScalarIndentations[] = $this->getCurrentLineIndentation();
387
        }
388
389
        $previousLineIndentation = $this->getCurrentLineIndentation();
390
391
        while ($this->moveToNextLine()) {
392
            $indent = $this->getCurrentLineIndentation();
393
394
            // terminate all block scalars that are more indented than the current line
395
            if (!empty($blockScalarIndentations) && $indent < $previousLineIndentation && trim($this->currentLine) !== '') {
396
                foreach ($blockScalarIndentations as $key => $blockScalarIndentation) {
397
                    if ($blockScalarIndentation >= $this->getCurrentLineIndentation()) {
398
                        unset($blockScalarIndentations[$key]);
399
                    }
400
                }
401
            }
402
403
            if (empty($blockScalarIndentations) && !$this->isCurrentLineComment() && $this->isBlockScalarHeader()) {
404
                $blockScalarIndentations[] = $this->getCurrentLineIndentation();
405
            }
406
407
            $previousLineIndentation = $indent;
408
409
            if ($isItUnindentedCollection && !$this->isStringUnIndentedCollectionItem() && $newIndent === $indent) {
410
                $this->moveToPreviousLine();
411
                break;
412
            }
413
414
            if ($this->isCurrentLineBlank()) {
415
                $data[] = substr($this->currentLine, $newIndent);
416
                continue;
417
            }
418
419
            // we ignore "comment" lines only when we are not inside a scalar block
420
            if (empty($blockScalarIndentations) && $this->isCurrentLineComment()) {
421
                continue;
422
            }
423
424
            if ($indent >= $newIndent) {
425
                $data[] = substr($this->currentLine, $newIndent);
426
            } elseif (0 == $indent) {
427
                $this->moveToPreviousLine();
428
429
                break;
430
            } else {
431
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
432
            }
433
        }
434
435
        return implode("\n", $data);
436
    }
437
438
    /**
439
     * Moves the parser to the next line.
440
     *
441
     * @return bool
442
     */
443
    private function moveToNextLine()
444
    {
445
        if ($this->currentLineNb >= count($this->lines) - 1) {
446
            return false;
447
        }
448
449
        $this->currentLine = $this->lines[++$this->currentLineNb];
450
451
        return true;
452
    }
453
454
    /**
455
     * Moves the parser to the previous line.
456
     */
457
    private function moveToPreviousLine()
458
    {
459
        $this->currentLine = $this->lines[--$this->currentLineNb];
460
    }
461
462
    /**
463
     * Parses a YAML value.
464
     *
465
     * @param string $value                  A YAML value
466
     * @param bool   $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise
467
     * @param bool   $objectSupport          True if object support is enabled, false otherwise
468
     * @param bool   $objectForMap           true if maps should return a stdClass instead of array()
469
     * @param string $context                The parser context (either sequence or mapping)
470
     *
471
     * @return mixed A PHP value
472
     *
473
     * @throws ParseException When reference does not exist
474
     */
475
    private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $context)
476
    {
477
        if (0 === strpos($value, '*')) {
478 View Code Duplication
            if (false !== $pos = strpos($value, '#')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
479
                $value = substr($value, 1, $pos - 2);
480
            } else {
481
                $value = substr($value, 1);
482
            }
483
484
            if (!array_key_exists($value, $this->refs)) {
485
                throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLine);
486
            }
487
488
            return $this->refs[$value];
489
        }
490
491
        if (preg_match('/^'.self::BLOCK_SCALAR_HEADER_PATTERN.'$/', $value, $matches)) {
492
            $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
493
494
            return $this->parseBlockScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers));
495
        }
496
497
        try {
498
            $parsedValue = Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
499
500
            if ('mapping' === $context && '"' !== $value[0] && "'" !== $value[0] && '[' !== $value[0] && '{' !== $value[0] && '!' !== $value[0] && false !== strpos($parsedValue, ': ')) {
501
                throw new ParseException('A colon cannot be used in an unquoted mapping value.');
502
            }
503
504
            return $parsedValue;
505
        } catch (ParseException $e) {
506
            $e->setParsedLine($this->getRealCurrentLineNb() + 1);
507
            $e->setSnippet($this->currentLine);
508
509
            throw $e;
510
        }
511
    }
512
513
    /**
514
     * Parses a block scalar.
515
     *
516
     * @param string $style       The style indicator that was used to begin this block scalar (| or >)
517
     * @param string $chomping    The chomping indicator that was used to begin this block scalar (+ or -)
518
     * @param int    $indentation The indentation indicator that was used to begin this block scalar
519
     *
520
     * @return string The text value
521
     */
522
    private function parseBlockScalar($style, $chomping = '', $indentation = 0)
523
    {
524
        $notEOF = $this->moveToNextLine();
525
        if (!$notEOF) {
526
            return '';
527
        }
528
529
        $isCurrentLineBlank = $this->isCurrentLineBlank();
530
        $blockLines = array();
531
532
        // leading blank lines are consumed before determining indentation
533
        while ($notEOF && $isCurrentLineBlank) {
534
            // newline only if not EOF
535
            if ($notEOF = $this->moveToNextLine()) {
536
                $blockLines[] = '';
537
                $isCurrentLineBlank = $this->isCurrentLineBlank();
538
            }
539
        }
540
541
        // determine indentation if not specified
542
        if (0 === $indentation) {
543
            if (preg_match('/^ +/', $this->currentLine, $matches)) {
544
                $indentation = strlen($matches[0]);
545
            }
546
        }
547
548
        if ($indentation > 0) {
549
            $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
550
551
            while (
552
                $notEOF && (
553
                    $isCurrentLineBlank ||
554
                    preg_match($pattern, $this->currentLine, $matches)
555
                )
556
            ) {
557
                if ($isCurrentLineBlank && strlen($this->currentLine) > $indentation) {
558
                    $blockLines[] = substr($this->currentLine, $indentation);
559
                } elseif ($isCurrentLineBlank) {
560
                    $blockLines[] = '';
561
                } else {
562
                    $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...
563
                }
564
565
                // newline only if not EOF
566
                if ($notEOF = $this->moveToNextLine()) {
567
                    $isCurrentLineBlank = $this->isCurrentLineBlank();
568
                }
569
            }
570
        } elseif ($notEOF) {
571
            $blockLines[] = '';
572
        }
573
574
        if ($notEOF) {
575
            $blockLines[] = '';
576
            $this->moveToPreviousLine();
577
        }
578
579
        // folded style
580
        if ('>' === $style) {
581
            $text = '';
582
            $previousLineIndented = false;
583
            $previousLineBlank = false;
584
585
            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...
586
                if ('' === $blockLines[$i]) {
587
                    $text .= "\n";
588
                    $previousLineIndented = false;
589
                    $previousLineBlank = true;
590
                } elseif (' ' === $blockLines[$i][0]) {
591
                    $text .= "\n".$blockLines[$i];
592
                    $previousLineIndented = true;
593
                    $previousLineBlank = false;
594 View Code Duplication
                } elseif ($previousLineIndented) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
595
                    $text .= "\n".$blockLines[$i];
596
                    $previousLineIndented = false;
597
                    $previousLineBlank = false;
598
                } elseif ($previousLineBlank || 0 === $i) {
599
                    $text .= $blockLines[$i];
600
                    $previousLineIndented = false;
601
                    $previousLineBlank = false;
602 View Code Duplication
                } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
603
                    $text .= ' '.$blockLines[$i];
604
                    $previousLineIndented = false;
605
                    $previousLineBlank = false;
606
                }
607
            }
608
        } else {
609
            $text = implode("\n", $blockLines);
610
        }
611
612
        // deal with trailing newlines
613
        if ('' === $chomping) {
614
            $text = preg_replace('/\n+$/', "\n", $text);
615
        } elseif ('-' === $chomping) {
616
            $text = preg_replace('/\n+$/', '', $text);
617
        }
618
619
        return $text;
620
    }
621
622
    /**
623
     * Returns true if the next line is indented.
624
     *
625
     * @return bool Returns true if the next line is indented, false otherwise
626
     */
627
    private function isNextLineIndented()
628
    {
629
        $currentIndentation = $this->getCurrentLineIndentation();
630
        $EOF = !$this->moveToNextLine();
631
632
        while (!$EOF && $this->isCurrentLineEmpty()) {
633
            $EOF = !$this->moveToNextLine();
634
        }
635
636
        if ($EOF) {
637
            return false;
638
        }
639
640
        $ret = false;
641
        if ($this->getCurrentLineIndentation() > $currentIndentation) {
642
            $ret = true;
643
        }
644
645
        $this->moveToPreviousLine();
646
647
        return $ret;
648
    }
649
650
    /**
651
     * Returns true if the current line is blank or if it is a comment line.
652
     *
653
     * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
654
     */
655
    private function isCurrentLineEmpty()
656
    {
657
        return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
658
    }
659
660
    /**
661
     * Returns true if the current line is blank.
662
     *
663
     * @return bool Returns true if the current line is blank, false otherwise
664
     */
665
    private function isCurrentLineBlank()
666
    {
667
        return '' == trim($this->currentLine, ' ');
668
    }
669
670
    /**
671
     * Returns true if the current line is a comment line.
672
     *
673
     * @return bool Returns true if the current line is a comment line, false otherwise
674
     */
675
    private function isCurrentLineComment()
676
    {
677
        //checking explicitly the first char of the trim is faster than loops or strpos
678
        $ltrimmedLine = ltrim($this->currentLine, ' ');
679
680
        return '' !== $ltrimmedLine && $ltrimmedLine[0] === '#';
681
    }
682
683
    /**
684
     * Cleanups a YAML string to be parsed.
685
     *
686
     * @param string $value The input YAML string
687
     *
688
     * @return string A cleaned up YAML string
689
     */
690
    private function cleanup($value)
691
    {
692
        $value = str_replace(array("\r\n", "\r"), "\n", $value);
693
694
        // strip YAML header
695
        $count = 0;
696
        $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
697
        $this->offset += $count;
698
699
        // remove leading comments
700
        $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
701
        if ($count == 1) {
702
            // items have been removed, update the offset
703
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
704
            $value = $trimmedValue;
705
        }
706
707
        // remove start of the document marker (---)
708
        $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
709
        if ($count == 1) {
710
            // items have been removed, update the offset
711
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
712
            $value = $trimmedValue;
713
714
            // remove end of the document marker (...)
715
            $value = preg_replace('#\.\.\.\s*$#', '', $value);
716
        }
717
718
        return $value;
719
    }
720
721
    /**
722
     * Returns true if the next line starts unindented collection.
723
     *
724
     * @return bool Returns true if the next line starts unindented collection, false otherwise
725
     */
726
    private function isNextLineUnIndentedCollection()
727
    {
728
        $currentIndentation = $this->getCurrentLineIndentation();
729
        $notEOF = $this->moveToNextLine();
730
731
        while ($notEOF && $this->isCurrentLineEmpty()) {
732
            $notEOF = $this->moveToNextLine();
733
        }
734
735
        if (false === $notEOF) {
736
            return false;
737
        }
738
739
        $ret = false;
740
        if (
741
            $this->getCurrentLineIndentation() == $currentIndentation
742
            &&
743
            $this->isStringUnIndentedCollectionItem()
744
        ) {
745
            $ret = true;
746
        }
747
748
        $this->moveToPreviousLine();
749
750
        return $ret;
751
    }
752
753
    /**
754
     * Returns true if the string is un-indented collection item.
755
     *
756
     * @return bool Returns true if the string is un-indented collection item, false otherwise
757
     */
758
    private function isStringUnIndentedCollectionItem()
759
    {
760
        return 0 === strpos($this->currentLine, '- ');
761
    }
762
763
    /**
764
     * Tests whether or not the current line is the header of a block scalar.
765
     *
766
     * @return bool
767
     */
768
    private function isBlockScalarHeader()
769
    {
770
        return (bool) preg_match('~'.self::BLOCK_SCALAR_HEADER_PATTERN.'$~', $this->currentLine);
771
    }
772
}
773