fix parsing inline YAML spanning multiple lines
This commit is contained in:
parent
789448b65c
commit
85a5c31e05
@ -4,6 +4,7 @@ CHANGELOG
|
||||
4.4.0
|
||||
-----
|
||||
|
||||
* Added support for parsing the inline notation spanning multiple lines.
|
||||
* Added support to dump `null` as `~` by using the `Yaml::DUMP_NULL_AS_TILDE` flag.
|
||||
* deprecated accepting STDIN implicitly when using the `lint:yaml` command, use `lint:yaml -` (append a dash) instead to make it explicit.
|
||||
|
||||
|
@ -274,7 +274,7 @@ class Inline
|
||||
$output = self::parseQuotedScalar($scalar, $i);
|
||||
|
||||
if (null !== $delimiters) {
|
||||
$tmp = ltrim(substr($scalar, $i), ' ');
|
||||
$tmp = ltrim(substr($scalar, $i), " \n");
|
||||
if ('' === $tmp) {
|
||||
throw new ParseException(sprintf('Unexpected end of line, expected one of "%s".', implode('', $delimiters)), self::$parsedLineNumber + 1, $scalar, self::$parsedFilename);
|
||||
}
|
||||
@ -419,6 +419,7 @@ class Inline
|
||||
switch ($mapping[$i]) {
|
||||
case ' ':
|
||||
case ',':
|
||||
case "\n":
|
||||
++$i;
|
||||
continue 2;
|
||||
case '}':
|
||||
@ -450,7 +451,7 @@ class Inline
|
||||
}
|
||||
}
|
||||
|
||||
if (!$isKeyQuoted && (!isset($mapping[$i + 1]) || !\in_array($mapping[$i + 1], [' ', ',', '[', ']', '{', '}'], true))) {
|
||||
if (!$isKeyQuoted && (!isset($mapping[$i + 1]) || !\in_array($mapping[$i + 1], [' ', ',', '[', ']', '{', '}', "\n"], true))) {
|
||||
throw new ParseException('Colons must be followed by a space or an indication character (i.e. " ", ",", "[", "]", "{", "}").', self::$parsedLineNumber + 1, $mapping);
|
||||
}
|
||||
|
||||
@ -459,7 +460,7 @@ class Inline
|
||||
}
|
||||
|
||||
while ($i < $len) {
|
||||
if (':' === $mapping[$i] || ' ' === $mapping[$i]) {
|
||||
if (':' === $mapping[$i] || ' ' === $mapping[$i] || "\n" === $mapping[$i]) {
|
||||
++$i;
|
||||
|
||||
continue;
|
||||
@ -508,7 +509,7 @@ class Inline
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$value = self::parseScalar($mapping, $flags, [',', '}'], $i, null === $tag, $references);
|
||||
$value = self::parseScalar($mapping, $flags, [',', '}', "\n"], $i, null === $tag, $references);
|
||||
// Spec: Keys MUST be unique; first one wins.
|
||||
// Parser cannot abort this mapping earlier, since lines
|
||||
// are processed sequentially.
|
||||
|
@ -353,6 +353,61 @@ class Parser
|
||||
$this->refs[$isRef] = $data[$key];
|
||||
array_pop($this->refsBeingParsed);
|
||||
}
|
||||
} elseif ('"' === $this->currentLine[0] || "'" === $this->currentLine[0]) {
|
||||
if (null !== $context) {
|
||||
throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
|
||||
}
|
||||
|
||||
try {
|
||||
return Inline::parse($this->parseQuotedString($this->currentLine), $flags, $this->refs);
|
||||
} catch (ParseException $e) {
|
||||
$e->setParsedLine($this->getRealCurrentLineNb() + 1);
|
||||
$e->setSnippet($this->currentLine);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
} elseif ('{' === $this->currentLine[0]) {
|
||||
if (null !== $context) {
|
||||
throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
|
||||
}
|
||||
|
||||
try {
|
||||
$parsedMapping = Inline::parse($this->lexInlineMapping($this->currentLine), $flags, $this->refs);
|
||||
|
||||
while ($this->moveToNextLine()) {
|
||||
if (!$this->isCurrentLineEmpty()) {
|
||||
throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
|
||||
}
|
||||
}
|
||||
|
||||
return $parsedMapping;
|
||||
} catch (ParseException $e) {
|
||||
$e->setParsedLine($this->getRealCurrentLineNb() + 1);
|
||||
$e->setSnippet($this->currentLine);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
} elseif ('[' === $this->currentLine[0]) {
|
||||
if (null !== $context) {
|
||||
throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
|
||||
}
|
||||
|
||||
try {
|
||||
$parsedSequence = Inline::parse($this->lexInlineSequence($this->currentLine), $flags, $this->refs);
|
||||
|
||||
while ($this->moveToNextLine()) {
|
||||
if (!$this->isCurrentLineEmpty()) {
|
||||
throw new ParseException('Unable to parse.', $this->getRealCurrentLineNb() + 1, $this->currentLine, $this->filename);
|
||||
}
|
||||
}
|
||||
|
||||
return $parsedSequence;
|
||||
} catch (ParseException $e) {
|
||||
$e->setParsedLine($this->getRealCurrentLineNb() + 1);
|
||||
$e->setSnippet($this->currentLine);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
} else {
|
||||
// multiple documents are not supported
|
||||
if ('---' === $this->currentLine) {
|
||||
@ -678,6 +733,12 @@ class Parser
|
||||
}
|
||||
|
||||
try {
|
||||
if ('' !== $value && '{' === $value[0]) {
|
||||
return Inline::parse($this->lexInlineMapping($value), $flags, $this->refs);
|
||||
} elseif ('' !== $value && '[' === $value[0]) {
|
||||
return Inline::parse($this->lexInlineSequence($value), $flags, $this->refs);
|
||||
}
|
||||
|
||||
$quotation = '' !== $value && ('"' === $value[0] || "'" === $value[0]) ? $value[0] : null;
|
||||
|
||||
// do not take following lines into account when the current line is a quoted single line value
|
||||
@ -1072,4 +1133,122 @@ class Parser
|
||||
|
||||
throw new ParseException(sprintf('Tags support is not enabled. You must use the flag "Yaml::PARSE_CUSTOM_TAGS" to use "%s".', $matches['tag']), $this->getRealCurrentLineNb() + 1, $value, $this->filename);
|
||||
}
|
||||
|
||||
private function parseQuotedString($yaml)
|
||||
{
|
||||
if ('' === $yaml || ('"' !== $yaml[0] && "'" !== $yaml[0])) {
|
||||
throw new \InvalidArgumentException(sprintf('"%s" is not a quoted string.', $yaml));
|
||||
}
|
||||
|
||||
$lines = [$yaml];
|
||||
|
||||
while ($this->moveToNextLine()) {
|
||||
$lines[] = $this->currentLine;
|
||||
|
||||
if (!$this->isCurrentLineEmpty() && $yaml[0] === $this->currentLine[-1]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$value = '';
|
||||
|
||||
for ($i = 0, $linesCount = \count($lines), $previousLineWasNewline = false, $previousLineWasTerminatedWithBackslash = false; $i < $linesCount; ++$i) {
|
||||
if ('' === trim($lines[$i])) {
|
||||
$value .= "\n";
|
||||
} elseif (!$previousLineWasNewline && !$previousLineWasTerminatedWithBackslash) {
|
||||
$value .= ' ';
|
||||
}
|
||||
|
||||
if ('' !== trim($lines[$i]) && '\\' === substr($lines[$i], -1)) {
|
||||
$value .= ltrim(substr($lines[$i], 0, -1));
|
||||
} elseif ('' !== trim($lines[$i])) {
|
||||
$value .= trim($lines[$i]);
|
||||
}
|
||||
|
||||
if ('' === trim($lines[$i])) {
|
||||
$previousLineWasNewline = true;
|
||||
$previousLineWasTerminatedWithBackslash = false;
|
||||
} elseif ('\\' === substr($lines[$i], -1)) {
|
||||
$previousLineWasNewline = false;
|
||||
$previousLineWasTerminatedWithBackslash = true;
|
||||
} else {
|
||||
$previousLineWasNewline = false;
|
||||
$previousLineWasTerminatedWithBackslash = false;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
|
||||
for ($i = 1; isset($yaml[$i]) && $quotation !== $yaml[$i]; ++$i) {
|
||||
}
|
||||
|
||||
// quoted single line string
|
||||
if (isset($yaml[$i]) && $quotation === $yaml[$i]) {
|
||||
return $yaml;
|
||||
}
|
||||
|
||||
$lines = [$yaml];
|
||||
|
||||
while ($this->moveToNextLine()) {
|
||||
for ($i = 1; isset($this->currentLine[$i]) && $quotation !== $this->currentLine[$i]; ++$i) {
|
||||
}
|
||||
|
||||
$lines[] = trim($this->currentLine);
|
||||
|
||||
if (isset($this->currentLine[$i]) && $quotation === $this->currentLine[$i]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function lexInlineMapping(string $yaml): string
|
||||
{
|
||||
if ('' === $yaml || '{' !== $yaml[0]) {
|
||||
throw new \InvalidArgumentException(sprintf('"%s" is not a sequence.', $yaml));
|
||||
}
|
||||
|
||||
for ($i = 1; isset($yaml[$i]) && '}' !== $yaml[$i]; ++$i) {
|
||||
}
|
||||
|
||||
if (isset($yaml[$i]) && '}' === $yaml[$i]) {
|
||||
return $yaml;
|
||||
}
|
||||
|
||||
$lines = [$yaml];
|
||||
|
||||
while ($this->moveToNextLine()) {
|
||||
$lines[] = $this->currentLine;
|
||||
}
|
||||
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
private function lexInlineSequence($yaml)
|
||||
{
|
||||
if ('' === $yaml || '[' !== $yaml[0]) {
|
||||
throw new \InvalidArgumentException(sprintf('"%s" is not a sequence.', $yaml));
|
||||
}
|
||||
|
||||
for ($i = 1; isset($yaml[$i]) && ']' !== $yaml[$i]; ++$i) {
|
||||
}
|
||||
|
||||
if (isset($yaml[$i]) && ']' === $yaml[$i]) {
|
||||
return $yaml;
|
||||
}
|
||||
|
||||
$value = $yaml;
|
||||
|
||||
while ($this->moveToNextLine()) {
|
||||
for ($i = 1; isset($this->currentLine[$i]) && ']' !== $this->currentLine[$i]; ++$i) {
|
||||
}
|
||||
|
||||
$value .= trim($this->currentLine);
|
||||
|
||||
if (isset($this->currentLine[$i]) && ']' === $this->currentLine[$i]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
@ -711,7 +711,7 @@ class InlineTest extends TestCase
|
||||
public function testUnfinishedInlineMap()
|
||||
{
|
||||
$this->expectException('Symfony\Component\Yaml\Exception\ParseException');
|
||||
$this->expectExceptionMessage('Unexpected end of line, expected one of ",}" at line 1 (near "{abc: \'def\'").');
|
||||
$this->expectExceptionMessage("Unexpected end of line, expected one of \",}\n\" at line 1 (near \"{abc: 'def'\").");
|
||||
Inline::parse("{abc: 'def'");
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@
|
||||
namespace Symfony\Component\Yaml\Tests;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use Symfony\Component\Yaml\Parser;
|
||||
use Symfony\Component\Yaml\Tag\TaggedValue;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
@ -1614,6 +1615,206 @@ EOF;
|
||||
return $tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider inlineNotationSpanningMultipleLinesProvider
|
||||
*/
|
||||
public function testInlineNotationSpanningMultipleLines($expected, string $yaml)
|
||||
{
|
||||
$this->assertEquals($expected, $this->parser->parse($yaml));
|
||||
}
|
||||
|
||||
public function inlineNotationSpanningMultipleLinesProvider(): array
|
||||
{
|
||||
return [
|
||||
'mapping' => [
|
||||
['foo' => 'bar', 'bar' => 'baz'],
|
||||
<<<YAML
|
||||
{
|
||||
'foo': 'bar',
|
||||
'bar': 'baz'
|
||||
}
|
||||
YAML
|
||||
,
|
||||
],
|
||||
'sequence' => [
|
||||
['foo', 'bar'],
|
||||
<<<YAML
|
||||
[
|
||||
'foo',
|
||||
'bar'
|
||||
]
|
||||
YAML
|
||||
,
|
||||
],
|
||||
'sequence nested in mapping' => [
|
||||
['foo' => ['bar', 'foobar'], 'bar' => ['baz']],
|
||||
<<<YAML
|
||||
{
|
||||
'foo': ['bar', 'foobar'],
|
||||
'bar': ['baz']
|
||||
}
|
||||
YAML
|
||||
,
|
||||
],
|
||||
'sequence spanning multiple lines nested in mapping' => [
|
||||
[
|
||||
'foobar' => [
|
||||
'foo',
|
||||
'bar',
|
||||
'baz',
|
||||
],
|
||||
],
|
||||
<<<YAML
|
||||
foobar: [foo,
|
||||
bar,
|
||||
baz
|
||||
]
|
||||
YAML
|
||||
,
|
||||
],
|
||||
'nested sequence nested in mapping starting on the same line' => [
|
||||
[
|
||||
'foo' => [
|
||||
'foobar',
|
||||
[
|
||||
'bar',
|
||||
'baz',
|
||||
],
|
||||
],
|
||||
],
|
||||
<<<YAML
|
||||
foo: [foobar, [
|
||||
bar,
|
||||
baz
|
||||
]]
|
||||
YAML
|
||||
,
|
||||
],
|
||||
'nested sequence nested in mapping starting on the following line' => [
|
||||
[
|
||||
'foo' => [
|
||||
'foobar',
|
||||
[
|
||||
'bar',
|
||||
'baz',
|
||||
],
|
||||
],
|
||||
],
|
||||
<<<YAML
|
||||
foo: [foobar,
|
||||
[
|
||||
bar,
|
||||
baz
|
||||
]]
|
||||
YAML
|
||||
,
|
||||
],
|
||||
'mapping nested in sequence' => [
|
||||
['foo', ['bar' => 'baz']],
|
||||
<<<YAML
|
||||
[
|
||||
'foo',
|
||||
{
|
||||
'bar': 'baz'
|
||||
}
|
||||
]
|
||||
YAML
|
||||
,
|
||||
],
|
||||
'mapping spanning multiple lines nested in sequence' => [
|
||||
[
|
||||
[
|
||||
'foo' => 'bar',
|
||||
'bar' => 'baz',
|
||||
],
|
||||
],
|
||||
<<<YAML
|
||||
- {
|
||||
foo: bar,
|
||||
bar: baz
|
||||
}
|
||||
YAML
|
||||
,
|
||||
],
|
||||
'nested mapping nested in sequence starting on the same line' => [
|
||||
[
|
||||
[
|
||||
'foo' => [
|
||||
'bar' => 'foobar',
|
||||
],
|
||||
'bar' => 'baz',
|
||||
],
|
||||
],
|
||||
<<<YAML
|
||||
- { foo: {
|
||||
bar: foobar
|
||||
},
|
||||
bar: baz
|
||||
}
|
||||
YAML
|
||||
,
|
||||
],
|
||||
'nested mapping nested in sequence starting on the following line' => [
|
||||
[
|
||||
[
|
||||
'foo' => [
|
||||
'bar' => 'foobar',
|
||||
],
|
||||
'bar' => 'baz',
|
||||
],
|
||||
],
|
||||
<<<YAML
|
||||
- { foo:
|
||||
{
|
||||
bar: foobar
|
||||
},
|
||||
bar: baz
|
||||
}
|
||||
YAML
|
||||
,
|
||||
],
|
||||
'single quoted multi-line string' => [
|
||||
"foo\nbar",
|
||||
<<<YAML
|
||||
'foo
|
||||
|
||||
bar'
|
||||
YAML
|
||||
,
|
||||
],
|
||||
'double quoted multi-line string' => [
|
||||
"foo\nbar",
|
||||
<<<YAML
|
||||
'foo
|
||||
|
||||
bar'
|
||||
YAML
|
||||
,
|
||||
],
|
||||
'single-quoted multi-line mapping value' => [
|
||||
['foo' => "bar\nbaz"],
|
||||
<<<YAML
|
||||
foo: 'bar
|
||||
|
||||
baz'
|
||||
YAML
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testRootLevelInlineMappingFollowedByMoreContentIsInvalid()
|
||||
{
|
||||
$this->expectException(ParseException::class);
|
||||
$this->expectExceptionMessage('Unable to parse at line 2 (near "foobar").');
|
||||
|
||||
$yaml = <<<YAML
|
||||
{ foo: bar }
|
||||
foobar
|
||||
YAML;
|
||||
|
||||
$this->parser->parse($yaml);
|
||||
}
|
||||
|
||||
public function testTaggedInlineMapping()
|
||||
{
|
||||
$this->assertEquals(new TaggedValue('foo', ['foo' => 'bar']), $this->parser->parse('!foo {foo: bar}', Yaml::PARSE_CUSTOM_TAGS));
|
||||
@ -1749,7 +1950,7 @@ YAML;
|
||||
public function testParsingIniThrowsException()
|
||||
{
|
||||
$this->expectException('Symfony\Component\Yaml\Exception\ParseException');
|
||||
$this->expectExceptionMessage('Unable to parse at line 1 (near "[parameters]").');
|
||||
$this->expectExceptionMessage('Unable to parse at line 2 (near " foo = bar").');
|
||||
$ini = <<<INI
|
||||
[parameters]
|
||||
foo = bar
|
||||
|
Reference in New Issue
Block a user