Test Failed
Push — master ( 398493...d4ef72 )
by Michael
11:04
created

Parser::isNextLineUnIndentedCollection()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 9
nc 6
nop 0
1
<?php
2
3
/*
4
 * This file is part of the Symfony package.
5
 *
6
 * (c) Fabien Potencier <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Symfony\Component\Yaml;
13
14
use Symfony\Component\Yaml\Exception\ParseException;
15
16
/**
17
 * Parser parses YAML strings to convert them to PHP arrays.
18
 *
19
 * @author Fabien Potencier <[email protected]>
20
 */
21
class Parser
22
{
23
    const BLOCK_SCALAR_HEADER_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
24
    // BC - wrongly named
25
    const FOLDED_SCALAR_PATTERN = self::BLOCK_SCALAR_HEADER_PATTERN;
26
27
    private $offset = 0;
28
    private $totalNumberOfLines;
29
    private $lines = array();
30
    private $currentLineNb = -1;
31
    private $currentLine = '';
32
    private $refs = array();
33
    private $skippedLineNumbers = array();
34
    private $locallySkippedLineNumbers = array();
35
36
    /**
37
     * @param int      $offset             The offset of YAML document (used for line numbers in error messages)
38
     * @param int|null $totalNumberOfLines The overall number of lines being parsed
39
     * @param int[]    $skippedLineNumbers Number of comment lines that have been skipped by the parser
40
     */
41
    public function __construct($offset = 0, $totalNumberOfLines = null, array $skippedLineNumbers = array())
42
    {
43
        $this->offset = $offset;
44
        $this->totalNumberOfLines = $totalNumberOfLines;
45
        $this->skippedLineNumbers = $skippedLineNumbers;
46
    }
47
48
    /**
49
     * Parses a YAML string to a PHP value.
50
     *
51
     * @param string $value                  A YAML string
52
     * @param bool   $exceptionOnInvalidType True if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
53
     * @param bool   $objectSupport          True if object support is enabled, false otherwise
54
     * @param bool   $objectForMap           True if maps should return a stdClass instead of array()
55
     *
56
     * @return mixed A PHP value
57
     *
58
     * @throws ParseException If the YAML is not valid
59
     */
60
    public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
61
    {
62
        if (false === preg_match('//u', $value)) {
63
            throw new ParseException('The YAML value does not appear to be valid UTF-8.');
64
        }
65
66
        $this->refs = array();
67
68
        $mbEncoding = null;
69
        $e = null;
70
        $data = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
71
72
        if (2 /* MB_OVERLOAD_STRING */ & (int) ini_get('mbstring.func_overload')) {
73
            $mbEncoding = mb_internal_encoding();
74
            mb_internal_encoding('UTF-8');
75
        }
76
77
        try {
78
            $data = $this->doParse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
79
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
80
        } catch (\Throwable $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
81
        }
82
83
        if (null !== $mbEncoding) {
84
            mb_internal_encoding($mbEncoding);
85
        }
86
87
        $this->lines = array();
88
        $this->currentLine = '';
89
        $this->refs = array();
90
        $this->skippedLineNumbers = array();
91
        $this->locallySkippedLineNumbers = array();
92
93
        if (null !== $e) {
94
            throw $e;
95
        }
96
97
        return $data;
98
    }
99
100
    private function doParse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
101
    {
102
        $this->currentLineNb = -1;
103
        $this->currentLine = '';
104
        $value = $this->cleanup($value);
105
        $this->lines = explode("\n", $value);
106
        $this->locallySkippedLineNumbers = array();
107
108
        if (null === $this->totalNumberOfLines) {
109
            $this->totalNumberOfLines = count($this->lines);
110
        }
111
112
        $data = array();
113
        $context = null;
114
        $allowOverwrite = false;
115
116
        while ($this->moveToNextLine()) {
117
            if ($this->isCurrentLineEmpty()) {
118
                continue;
119
            }
120
121
            // tab?
122
            if ("\t" === $this->currentLine[0]) {
123
                throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
124
            }
125
126
            $isRef = $mergeNode = false;
127
            if (self::preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+))?$#u', rtrim($this->currentLine), $values)) {
128
                if ($context && 'mapping' == $context) {
129
                    throw new ParseException('You cannot define a sequence item when in a mapping', $this->getRealCurrentLineNb() + 1, $this->currentLine);
130
                }
131
                $context = 'sequence';
132
133
                if (isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
134
                    $isRef = $matches['ref'];
135
                    $values['value'] = $matches['value'];
136
                }
137
138
                // array
139
                if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
140
                    $data[] = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(null, true), $exceptionOnInvalidType, $objectSupport, $objectForMap);
141
                } else {
142
                    if (isset($values['leadspaces'])
143
                        && self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($values['value']), $matches)
144
                    ) {
145
                        // this is a compact notation element, add to next block and parse
146
                        $block = $values['value'];
147
                        if ($this->isNextLineIndented()) {
148
                            $block .= "\n".$this->getNextEmbedBlock($this->getCurrentLineIndentation() + strlen($values['leadspaces']) + 1);
149
                        }
150
151
                        $data[] = $this->parseBlock($this->getRealCurrentLineNb(), $block, $exceptionOnInvalidType, $objectSupport, $objectForMap);
152
                    } else {
153
                        $data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap, $context);
154
                    }
155
                }
156
                if ($isRef) {
157
                    $this->refs[$isRef] = end($data);
158
                }
159
            } elseif (
160
                self::preg_match('#^(?P<key>'.Inline::REGEX_QUOTED_STRING.'|[^ \'"\[\{].*?) *\:(\s+(?P<value>.+))?$#u', rtrim($this->currentLine), $values)
161
                && (false === strpos($values['key'], ' #') || in_array($values['key'][0], array('"', "'")))
162
            ) {
163
                if ($context && 'sequence' == $context) {
164
                    throw new ParseException('You cannot define a mapping item when in a sequence', $this->currentLineNb + 1, $this->currentLine);
165
                }
166
                $context = 'mapping';
167
168
                // force correct settings
169
                Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap, $this->refs);
170
                try {
171
                    $key = Inline::parseScalar($values['key']);
172
                } catch (ParseException $e) {
173
                    $e->setParsedLine($this->getRealCurrentLineNb() + 1);
174
                    $e->setSnippet($this->currentLine);
175
176
                    throw $e;
177
                }
178
179
                // Convert float keys to strings, to avoid being converted to integers by PHP
180
                if (is_float($key)) {
181
                    $key = (string) $key;
182
                }
183
184
                if ('<<' === $key && (!isset($values['value']) || !self::preg_match('#^&(?P<ref>[^ ]+)#u', $values['value'], $refMatches))) {
185
                    $mergeNode = true;
186
                    $allowOverwrite = true;
187
                    if (isset($values['value']) && 0 === strpos($values['value'], '*')) {
188
                        $refName = substr($values['value'], 1);
189
                        if (!array_key_exists($refName, $this->refs)) {
190
                            throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
191
                        }
192
193
                        $refValue = $this->refs[$refName];
194
195
                        if (!is_array($refValue)) {
196
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
197
                        }
198
199
                        $data += $refValue; // array union
200
                    } else {
201
                        if (isset($values['value']) && '' !== $values['value']) {
202
                            $value = $values['value'];
203
                        } else {
204
                            $value = $this->getNextEmbedBlock();
205
                        }
206
                        $parsed = $this->parseBlock($this->getRealCurrentLineNb() + 1, $value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
207
208
                        if (!is_array($parsed)) {
209
                            throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
210
                        }
211
212
                        if (isset($parsed[0])) {
213
                            // If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
214
                            // and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
215
                            // in the sequence override keys specified in later mapping nodes.
216
                            foreach ($parsed as $parsedItem) {
217
                                if (!is_array($parsedItem)) {
218
                                    throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
219
                                }
220
221
                                $data += $parsedItem; // array union
222
                            }
223
                        } else {
224
                            // If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
225
                            // current mapping, unless the key already exists in it.
226
                            $data += $parsed; // array union
227
                        }
228
                    }
229
                } elseif ('<<' !== $key && isset($values['value']) && self::preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
230
                    $isRef = $matches['ref'];
231
                    $values['value'] = $matches['value'];
232
                }
233
234
                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...
235
                    // Merge keys
236
                } elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#') || '<<' === $key) {
237
                    // hash
238
                    // if next line is less indented or equal, then it means that the current value is null
239
                    if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
240
                        // Spec: Keys MUST be unique; first one wins.
241
                        // But overwriting is allowed when a merge node is used in current block.
242
                        if ($allowOverwrite || !isset($data[$key])) {
243
                            $data[$key] = null;
244
                        }
245
                    } else {
246
                        $value = $this->parseBlock($this->getRealCurrentLineNb() + 1, $this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $value is correct as $this->parseBlock($this-...Support, $objectForMap) targeting Symfony\Component\Yaml\Parser::parseBlock() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

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