bug #11168 [YAML] fix merge node (<<) (Tobion)

This PR was merged into the 2.5 branch.

Discussion
----------

[YAML] fix merge node (<<)

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | yes but according to spec
| Deprecations? | no
| Tests pass?   | yes
| Needs merge to | 2.5
| Fixed tickets | #11142 and #11154
| License       | MIT
| Doc PR        | —

First commit small refactoring.
Second fixes #11154 (causing a BC break for a less common feature)
Third fixes #11142

Commits
-------

dee1562 [Yaml] fix overwriting of keys after merged map
8c621ab [Yaml] fix priority of sequence merges according to spec
02614e0 [Yaml] refactoring of merges for performance
This commit is contained in:
Fabien Potencier 2014-06-20 09:17:53 +02:00
commit a50aca04e4
2 changed files with 70 additions and 34 deletions

View File

@ -66,6 +66,7 @@ class Parser
$data = array();
$context = null;
$allowOverwrite = false;
while ($this->moveToNextLine()) {
if ($this->isCurrentLineEmpty()) {
continue;
@ -76,7 +77,7 @@ class Parser
throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
}
$isRef = $isInPlace = $isProcessed = false;
$isRef = $mergeNode = false;
if (preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+?))?\s*$#u', $this->currentLine, $values)) {
if ($context && 'mapping' == $context) {
throw new ParseException('You cannot define a sequence item when in a mapping');
@ -132,10 +133,24 @@ class Parser
}
if ('<<' === $key) {
$mergeNode = true;
$allowOverwrite = true;
if (isset($values['value']) && 0 === strpos($values['value'], '*')) {
$isInPlace = substr($values['value'], 1);
if (!array_key_exists($isInPlace, $this->refs)) {
throw new ParseException(sprintf('Reference "%s" does not exist.', $isInPlace), $this->getRealCurrentLineNb() + 1, $this->currentLine);
$refName = substr($values['value'], 1);
if (!array_key_exists($refName, $this->refs)) {
throw new ParseException(sprintf('Reference "%s" does not exist.', $refName), $this->getRealCurrentLineNb() + 1, $this->currentLine);
}
$refValue = $this->refs[$refName];
if (!is_array($refValue)) {
throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
}
foreach ($refValue as $key => $value) {
if (!isset($data[$key])) {
$data[$key] = $value;
}
}
} else {
if (isset($values['value']) && $values['value'] !== '') {
@ -148,40 +163,49 @@ class Parser
$parser->refs =& $this->refs;
$parsed = $parser->parse($value, $exceptionOnInvalidType, $objectSupport);
$merged = array();
if (!is_array($parsed)) {
throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
} elseif (isset($parsed[0])) {
// Numeric array, merge individual elements
foreach (array_reverse($parsed) as $parsedItem) {
}
if (isset($parsed[0])) {
// If the value associated with the merge key is a sequence, then this sequence is expected to contain mapping nodes
// and each of these nodes is merged in turn according to its order in the sequence. Keys in mapping nodes earlier
// in the sequence override keys specified in later mapping nodes.
foreach ($parsed as $parsedItem) {
if (!is_array($parsedItem)) {
throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
}
$merged = array_merge($parsedItem, $merged);
foreach ($parsedItem as $key => $value) {
if (!isset($data[$key])) {
$data[$key] = $value;
}
}
}
} else {
// Associative array, merge
$merged = array_merge($merged, $parsed);
// If the value associated with the key is a single mapping node, each of its key/value pairs is inserted into the
// current mapping, unless the key already exists in it.
foreach ($parsed as $key => $value) {
if (!isset($data[$key])) {
$data[$key] = $value;
}
}
}
$isProcessed = $merged;
}
} elseif (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
$isRef = $matches['ref'];
$values['value'] = $matches['value'];
}
if ($isProcessed) {
if ($mergeNode) {
// Merge keys
$data = $isProcessed;
// hash
} elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
// hash
// if next line is less indented or equal, then it means that the current value is null
if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
if (!isset($data[$key])) {
// But overwriting is allowed when a merge node is used in current block.
if ($allowOverwrite || !isset($data[$key])) {
$data[$key] = null;
}
} else {
@ -190,23 +214,17 @@ class Parser
$parser->refs =& $this->refs;
$value = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport);
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
if (!isset($data[$key])) {
// But overwriting is allowed when a merge node is used in current block.
if ($allowOverwrite || !isset($data[$key])) {
$data[$key] = $value;
}
}
} else {
if ($isInPlace) {
$data = $this->refs[$isInPlace];
} else {
$value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport);;
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
if (!isset($data[$key])) {
$data[$key] = $value;
}
$value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport);
// Spec: Keys MUST be unique; first one wins.
// But overwriting is allowed when a merge node is used in current block.
if ($allowOverwrite || !isset($data[$key])) {
$data[$key] = $value;
}
}
} else {

View File

@ -10,9 +10,19 @@ yaml: |
a: Steve
b: Clark
c: Brian
bar: &bar
bar:
a: before
d: other
<<: *foo
b: new
x: Oren
c:
foo: bar
foo: ignore
bar: foo
duplicate:
foo: bar
foo: ignore
foo2: &foo2
a: Ballmer
ding: &dong [ fi, fei, fo, fam]
@ -24,4 +34,12 @@ yaml: |
head:
<<: [ *foo , *dong , *foo2 ]
php: |
array('foo' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian'), 'bar' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian', 'x' => 'Oren'), 'foo2' => array('a' => 'Ballmer'), 'ding' => array('fi', 'fei', 'fo', 'fam'), 'check' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian', 'fi', 'fei', 'fo', 'fam', 'isit' => 'tested'), 'head' => array('a' => 'Ballmer', 'b' => 'Clark', 'c' => 'Brian', 'fi', 'fei', 'fo', 'fam'))
array(
'foo' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian'),
'bar' => array('a' => 'before', 'd' => 'other', 'b' => 'new', 'c' => array('foo' => 'bar', 'bar' => 'foo'), 'x' => 'Oren'),
'duplicate' => array('foo' => 'bar'),
'foo2' => array('a' => 'Ballmer'),
'ding' => array('fi', 'fei', 'fo', 'fam'),
'check' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian', 'fi', 'fei', 'fo', 'fam', 'isit' => 'tested'),
'head' => array('a' => 'Steve', 'b' => 'Clark', 'c' => 'Brian', 'fi', 'fei', 'fo', 'fam')
)