Parser::__construct()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 1
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 FOLDED_SCALAR_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 View Code Duplication
        if (function_exists('mb_internal_encoding') && ((int) ini_get('mbstring.func_overload')) & 2) {
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...
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);
116
                    }
117
                }
118
            } 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('"', "'")))) {
119
                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...
120
                    throw new ParseException('You cannot define a mapping item when in a sequence');
121
                }
122
                $context = 'mapping';
123
124
                // force correct settings
125
                Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
126
                try {
127
                    $key = Inline::parseScalar($values['key']);
128
                } catch (ParseException $e) {
129
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
130
                    $e->setSnippet($this->currentLine);
131
132
                    throw $e;
133
                }
134
135
                if ('<<' === $key) {
136
                    $mergeNode = true;
137
                    $allowOverwrite = true;
138
                    if (isset($values['value']) && 0 === strpos($values['value'], '*')) {
139
                        $refName = substr($values['value'], 1);
140
                        if (!array_key_exists($refName, $this->refs)) {
141
                            throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
142
                        }
143
144
                        $refValue = $this->refs[$refName];
145
146
                        if (!is_array($refValue)) {
147
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
148
                        }
149
150
                        foreach ($refValue as $key => $value) {
151
                            if (!isset($data[$key])) {
152
                                $data[$key] = $value;
153
                            }
154
                        }
155
                    } else {
156
                        if (isset($values['value']) && $values['value'] !== '') {
157
                            $value = $values['value'];
158
                        } else {
159
                            $value = $this->getNextEmbedBlock();
160
                        }
161
                        $c = $this->getRealCurrentLineNb() + 1;
162
                        $parser = new self($c);
163
                        $parser->refs = &$this->refs;
164
                        $parsed = $parser->parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
165
166
                        if (!is_array($parsed)) {
167
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
168
                        }
169
170
                        if (isset($parsed[0])) {
171
                            // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
172
                            // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
173
                            // in the sequence override keys specified in later mapping nodes.
174
                            foreach ($parsed as $parsedItem) {
175
                                if (!is_array($parsedItem)) {
176
                                    throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
177
                                }
178
179
                                foreach ($parsedItem as $key => $value) {
180
                                    if (!isset($data[$key])) {
181
                                        $data[$key] = $value;
182
                                    }
183
                                }
184
                            }
185
                        } else {
186
                            // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
187
                            // current mapping, unless the key already exists in it.
188
                            foreach ($parsed as $key => $value) {
189
                                if (!isset($data[$key])) {
190
                                    $data[$key] = $value;
191
                                }
192
                            }
193
                        }
194
                    }
195 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...
196
                    $isRef = $matches['ref'];
197
                    $values['value'] = $matches['value'];
198
                }
199
200
                if ($mergeNode) {
201
                    // Merge keys
202
                } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
203
                    // hash
204
                    // if next line is less indented or equal, then it means that the current value is null
205
                    if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
206
                        // Spec: Keys MUST be unique; first one wins.
207
                        // But overwriting is allowed when a merge node is used in current block.
208
                        if ($allowOverwrite || !isset($data[$key])) {
209
                            $data[$key] = null;
210
                        }
211
                    } else {
212
                        $c = $this->getRealCurrentLineNb() + 1;
213
                        $parser = new self($c);
214
                        $parser->refs = &$this->refs;
215
                        $value = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap);
216
                        // Spec: Keys MUST be unique; first one wins.
217
                        // But overwriting is allowed when a merge node is used in current block.
218
                        if ($allowOverwrite || !isset($data[$key])) {
219
                            $data[$key] = $value;
220
                        }
221
                    }
222
                } else {
223
                    $value = $this->parseValue($values['value'], $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
                // multiple documents are not supported
232
                if ('---' === $this->currentLine) {
233
                    throw new ParseException('Multiple documents are not supported.');
234
                }
235
236
                // 1-liner optionally followed by newline(s)
237
                if ($this->lines[0] === trim($value)) {
238
                    try {
239
                        $value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
240
                    } catch (ParseException $e) {
241
                        $e->setParsedLine($this->getRealCurrentLineNb() + 1);
242
                        $e->setSnippet($this->currentLine);
243
244
                        throw $e;
245
                    }
246
247
                    if (is_array($value)) {
248
                        $first = reset($value);
249
                        if (is_string($first) && 0 === strpos($first, '*')) {
250
                            $data = array();
251
                            foreach ($value as $alias) {
252
                                $data[] = $this->refs[substr($alias, 1)];
253
                            }
254
                            $value = $data;
255
                        }
256
                    }
257
258
                    if (isset($mbEncoding)) {
259
                        mb_internal_encoding($mbEncoding);
260
                    }
261
262
                    return $value;
263
                }
264
265
                switch (preg_last_error()) {
266
                    case PREG_INTERNAL_ERROR:
267
                        $error = 'Internal PCRE error.';
268
                        break;
269
                    case PREG_BACKTRACK_LIMIT_ERROR:
270
                        $error = 'pcre.backtrack_limit reached.';
271
                        break;
272
                    case PREG_RECURSION_LIMIT_ERROR:
273
                        $error = 'pcre.recursion_limit reached.';
274
                        break;
275
                    case PREG_BAD_UTF8_ERROR:
276
                        $error = 'Malformed UTF-8 data.';
277
                        break;
278
                    case PREG_BAD_UTF8_OFFSET_ERROR:
279
                        $error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
280
                        break;
281
                    default:
282
                        $error = 'Unable to parse.';
283
                }
284
285
                throw new ParseException($error, $this->getRealCurrentLineNb() + 1, $this->currentLine);
286
            }
287
288
            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...
289
                $this->refs[$isRef] = end($data);
290
            }
291
        }
292
293
        if (isset($mbEncoding)) {
294
            mb_internal_encoding($mbEncoding);
295
        }
296
297
        return empty($data) ? null : $data;
298
    }
299
300
    /**
301
     * Returns the current line number (takes the offset into account).
302
     *
303
     * @return int The current line number
304
     */
305
    private function getRealCurrentLineNb()
306
    {
307
        return $this->currentLineNb + $this->offset;
308
    }
309
310
    /**
311
     * Returns the current line indentation.
312
     *
313
     * @return int The current line indentation
314
     */
315
    private function getCurrentLineIndentation()
316
    {
317
        return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
318
    }
319
320
    /**
321
     * Returns the next embed block of YAML.
322
     *
323
     * @param int  $indentation The indent level at which the block is to be read, or null for default
324
     * @param bool $inSequence  True if the enclosing data structure is a sequence
325
     *
326
     * @return string A YAML string
327
     *
328
     * @throws ParseException When indentation problem are detected
329
     */
330
    private function getNextEmbedBlock($indentation = null, $inSequence = false)
331
    {
332
        $oldLineIndentation = $this->getCurrentLineIndentation();
333
334
        if (!$this->moveToNextLine()) {
335
            return;
336
        }
337
338
        if (null === $indentation) {
339
            $newIndent = $this->getCurrentLineIndentation();
340
341
            $unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem($this->currentLine);
0 ignored issues
show
Unused Code introduced by
The call to Parser::isStringUnIndentedCollectionItem() has too many arguments starting with $this->currentLine.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
342
343
            if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
344
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
345
            }
346
        } else {
347
            $newIndent = $indentation;
348
        }
349
350
        $data = array();
351
        if ($this->getCurrentLineIndentation() >= $newIndent) {
352
            $data[] = substr($this->currentLine, $newIndent);
353
        } else {
354
            $this->moveToPreviousLine();
355
356
            return;
357
        }
358
359
        if ($inSequence && $oldLineIndentation === $newIndent && '-' === $data[0][0]) {
360
            // the previous line contained a dash but no item content, this line is a sequence item with the same indentation
361
            // and therefore no nested list or mapping
362
            $this->moveToPreviousLine();
363
364
            return;
365
        }
366
367
        $isItUnindentedCollection = $this->isStringUnIndentedCollectionItem($this->currentLine);
0 ignored issues
show
Unused Code introduced by
The call to Parser::isStringUnIndentedCollectionItem() has too many arguments starting with $this->currentLine.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
368
369
        // Comments must not be removed inside a string block (ie. after a line ending with "|")
370
        $removeCommentsPattern = '~'.self::FOLDED_SCALAR_PATTERN.'$~';
371
        $removeComments = !preg_match($removeCommentsPattern, $this->currentLine);
372
373
        while ($this->moveToNextLine()) {
374
            $indent = $this->getCurrentLineIndentation();
375
376
            if ($indent === $newIndent) {
377
                $removeComments = !preg_match($removeCommentsPattern, $this->currentLine);
378
            }
379
380
            if ($isItUnindentedCollection && !$this->isStringUnIndentedCollectionItem($this->currentLine) && $newIndent === $indent) {
0 ignored issues
show
Unused Code introduced by
The call to Parser::isStringUnIndentedCollectionItem() has too many arguments starting with $this->currentLine.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
381
                $this->moveToPreviousLine();
382
                break;
383
            }
384
385
            if ($this->isCurrentLineBlank()) {
386
                $data[] = substr($this->currentLine, $newIndent);
387
                continue;
388
            }
389
390
            if ($removeComments && $this->isCurrentLineComment()) {
391
                continue;
392
            }
393
394
            if ($indent >= $newIndent) {
395
                $data[] = substr($this->currentLine, $newIndent);
396
            } elseif (0 == $indent) {
397
                $this->moveToPreviousLine();
398
399
                break;
400
            } else {
401
                throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
402
            }
403
        }
404
405
        return implode("\n", $data);
406
    }
407
408
    /**
409
     * Moves the parser to the next line.
410
     *
411
     * @return bool
412
     */
413
    private function moveToNextLine()
414
    {
415
        if ($this->currentLineNb >= count($this->lines) - 1) {
416
            return false;
417
        }
418
419
        $this->currentLine = $this->lines[++$this->currentLineNb];
420
421
        return true;
422
    }
423
424
    /**
425
     * Moves the parser to the previous line.
426
     */
427
    private function moveToPreviousLine()
428
    {
429
        $this->currentLine = $this->lines[--$this->currentLineNb];
430
    }
431
432
    /**
433
     * Parses a YAML value.
434
     *
435
     * @param string $value                  A YAML value
436
     * @param bool   $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise
437
     * @param bool   $objectSupport          True if object support is enabled, false otherwise
438
     * @param bool   $objectForMap           true if maps should return a stdClass instead of array()
439
     *
440
     * @return mixed A PHP value
441
     *
442
     * @throws ParseException When reference does not exist
443
     */
444
    private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap)
445
    {
446
        if (0 === strpos($value, '*')) {
447 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...
448
                $value = substr($value, 1, $pos - 2);
449
            } else {
450
                $value = substr($value, 1);
451
            }
452
453
            if (!array_key_exists($value, $this->refs)) {
454
                throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLine);
455
            }
456
457
            return $this->refs[$value];
458
        }
459
460
        if (preg_match('/^'.self::FOLDED_SCALAR_PATTERN.'$/', $value, $matches)) {
461
            $modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
462
463
            return $this->parseFoldedScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), (int) abs($modifiers));
464
        }
465
466
        try {
467
            return Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
468
        } catch (ParseException $e) {
469
            $e->setParsedLine($this->getRealCurrentLineNb() + 1);
470
            $e->setSnippet($this->currentLine);
471
472
            throw $e;
473
        }
474
    }
475
476
    /**
477
     * Parses a folded scalar.
478
     *
479
     * @param string $separator   The separator that was used to begin this folded scalar (| or >)
480
     * @param string $indicator   The indicator that was used to begin this folded scalar (+ or -)
481
     * @param int    $indentation The indentation that was used to begin this folded scalar
482
     *
483
     * @return string The text value
484
     */
485
    private function parseFoldedScalar($separator, $indicator = '', $indentation = 0)
486
    {
487
        $notEOF = $this->moveToNextLine();
488
        if (!$notEOF) {
489
            return '';
490
        }
491
492
        $isCurrentLineBlank = $this->isCurrentLineBlank();
493
        $text = '';
494
495
        // leading blank lines are consumed before determining indentation
496
        while ($notEOF && $isCurrentLineBlank) {
497
            // newline only if not EOF
498
            if ($notEOF = $this->moveToNextLine()) {
499
                $text .= "\n";
500
                $isCurrentLineBlank = $this->isCurrentLineBlank();
501
            }
502
        }
503
504
        // determine indentation if not specified
505
        if (0 === $indentation) {
506
            if (preg_match('/^ +/', $this->currentLine, $matches)) {
507
                $indentation = strlen($matches[0]);
508
            }
509
        }
510
511
        if ($indentation > 0) {
512
            $pattern = sprintf('/^ {%d}(.*)$/', $indentation);
513
514
            while (
515
                $notEOF && (
516
                    $isCurrentLineBlank ||
517
                    preg_match($pattern, $this->currentLine, $matches)
518
                )
519
            ) {
520
                if ($isCurrentLineBlank) {
521
                    $text .= substr($this->currentLine, $indentation);
522
                } else {
523
                    $text .= $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...
524
                }
525
526
                // newline only if not EOF
527
                if ($notEOF = $this->moveToNextLine()) {
528
                    $text .= "\n";
529
                    $isCurrentLineBlank = $this->isCurrentLineBlank();
530
                }
531
            }
532
        } elseif ($notEOF) {
533
            $text .= "\n";
534
        }
535
536
        if ($notEOF) {
537
            $this->moveToPreviousLine();
538
        }
539
540
        // replace all non-trailing single newlines with spaces in folded blocks
541
        if ('>' === $separator) {
542
            preg_match('/(\n*)$/', $text, $matches);
543
            $text = preg_replace('/(?<!\n)\n(?!\n)/', ' ', rtrim($text, "\n"));
544
            $text .= $matches[1];
545
        }
546
547
        // deal with trailing newlines as indicated
548
        if ('' === $indicator) {
549
            $text = preg_replace('/\n+$/s', "\n", $text);
550
        } elseif ('-' === $indicator) {
551
            $text = preg_replace('/\n+$/s', '', $text);
552
        }
553
554
        return $text;
555
    }
556
557
    /**
558
     * Returns true if the next line is indented.
559
     *
560
     * @return bool Returns true if the next line is indented, false otherwise
561
     */
562
    private function isNextLineIndented()
563
    {
564
        $currentIndentation = $this->getCurrentLineIndentation();
565
        $EOF = !$this->moveToNextLine();
566
567
        while (!$EOF && $this->isCurrentLineEmpty()) {
568
            $EOF = !$this->moveToNextLine();
569
        }
570
571
        if ($EOF) {
572
            return false;
573
        }
574
575
        $ret = false;
576
        if ($this->getCurrentLineIndentation() > $currentIndentation) {
577
            $ret = true;
578
        }
579
580
        $this->moveToPreviousLine();
581
582
        return $ret;
583
    }
584
585
    /**
586
     * Returns true if the current line is blank or if it is a comment line.
587
     *
588
     * @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
589
     */
590
    private function isCurrentLineEmpty()
591
    {
592
        return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
593
    }
594
595
    /**
596
     * Returns true if the current line is blank.
597
     *
598
     * @return bool Returns true if the current line is blank, false otherwise
599
     */
600
    private function isCurrentLineBlank()
601
    {
602
        return '' == trim($this->currentLine, ' ');
603
    }
604
605
    /**
606
     * Returns true if the current line is a comment line.
607
     *
608
     * @return bool Returns true if the current line is a comment line, false otherwise
609
     */
610
    private function isCurrentLineComment()
611
    {
612
        //checking explicitly the first char of the trim is faster than loops or strpos
613
        $ltrimmedLine = ltrim($this->currentLine, ' ');
614
615
        return $ltrimmedLine[0] === '#';
616
    }
617
618
    /**
619
     * Cleanups a YAML string to be parsed.
620
     *
621
     * @param string $value The input YAML string
622
     *
623
     * @return string A cleaned up YAML string
624
     */
625
    private function cleanup($value)
626
    {
627
        $value = str_replace(array("\r\n", "\r"), "\n", $value);
628
629
        // strip YAML header
630
        $count = 0;
631
        $value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#u', '', $value, -1, $count);
632
        $this->offset += $count;
633
634
        // remove leading comments
635
        $trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
636
        if ($count == 1) {
637
            // items have been removed, update the offset
638
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
639
            $value = $trimmedValue;
640
        }
641
642
        // remove start of the document marker (---)
643
        $trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
644
        if ($count == 1) {
645
            // items have been removed, update the offset
646
            $this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
647
            $value = $trimmedValue;
648
649
            // remove end of the document marker (...)
650
            $value = preg_replace('#\.\.\.\s*$#s', '', $value);
651
        }
652
653
        return $value;
654
    }
655
656
    /**
657
     * Returns true if the next line starts unindented collection.
658
     *
659
     * @return bool Returns true if the next line starts unindented collection, false otherwise
660
     */
661
    private function isNextLineUnIndentedCollection()
662
    {
663
        $currentIndentation = $this->getCurrentLineIndentation();
664
        $notEOF = $this->moveToNextLine();
665
666
        while ($notEOF && $this->isCurrentLineEmpty()) {
667
            $notEOF = $this->moveToNextLine();
668
        }
669
670
        if (false === $notEOF) {
671
            return false;
672
        }
673
674
        $ret = false;
675
        if (
676
            $this->getCurrentLineIndentation() == $currentIndentation
677
            &&
678
            $this->isStringUnIndentedCollectionItem($this->currentLine)
0 ignored issues
show
Unused Code introduced by
The call to Parser::isStringUnIndentedCollectionItem() has too many arguments starting with $this->currentLine.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
679
        ) {
680
            $ret = true;
681
        }
682
683
        $this->moveToPreviousLine();
684
685
        return $ret;
686
    }
687
688
    /**
689
     * Returns true if the string is un-indented collection item.
690
     *
691
     * @return bool Returns true if the string is un-indented collection item, false otherwise
692
     */
693
    private function isStringUnIndentedCollectionItem()
694
    {
695
        return (0 === strpos($this->currentLine, '- '));
696
    }
697
}
698