diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index cbdc174bc..b625cbf1a 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -771,8 +771,12 @@ EOT foreach ($bits as $bit) { $currentValue = $currentValue[$bit] ?? null; } - if (is_array($currentValue)) { - $value = array_merge($currentValue, $value); + if (is_array($currentValue) && is_array($value)) { + if (array_is_list($currentValue) && array_is_list($value)) { + $value = array_merge($currentValue, $value); + } else { + $value = $value + $currentValue; + } } } } diff --git a/src/Composer/Json/JsonManipulator.php b/src/Composer/Json/JsonManipulator.php index 8d6759671..809cea57a 100644 --- a/src/Composer/Json/JsonManipulator.php +++ b/src/Composer/Json/JsonManipulator.php @@ -416,6 +416,9 @@ class JsonManipulator if ($subName !== null) { $curVal = json_decode($children, true); unset($curVal[$name][$subName]); + if ($curVal[$name] === []) { + $curVal[$name] = new \ArrayObject(); + } $this->addSubNode($mainNode, $name, $curVal[$name]); } @@ -427,7 +430,7 @@ class JsonManipulator if ($subName !== null) { $curVal = json_decode($matches['content'], true); unset($curVal[$name][$subName]); - $childrenClean = $this->format($curVal); + $childrenClean = $this->format($curVal, 0, true); } return $matches['start'] . $childrenClean . $matches['end']; @@ -534,12 +537,19 @@ class JsonManipulator /** * @param mixed $data */ - public function format($data, int $depth = 0): string + public function format($data, int $depth = 0, bool $wasObject = false): string { - if (is_array($data)) { - reset($data); + if ($data instanceof \stdClass || $data instanceof \ArrayObject) { + $data = (array) $data; + $wasObject = true; + } - if (is_numeric(key($data))) { + if (is_array($data)) { + if (\count($data) === 0) { + return $wasObject ? '{' . $this->newline . str_repeat($this->indent, $depth + 1) . '}' : '[]'; + } + + if (array_is_list($data)) { foreach ($data as $key => $val) { $data[$key] = $this->format($val, $depth + 1); } @@ -550,7 +560,7 @@ class JsonManipulator $out = '{' . $this->newline; $elems = []; foreach ($data as $key => $val) { - $elems[] = str_repeat($this->indent, $depth + 2) . JsonFile::encode($key). ': '.$this->format($val, $depth + 1); + $elems[] = str_repeat($this->indent, $depth + 2) . JsonFile::encode((string) $key). ': '.$this->format($val, $depth + 1); } return $out . implode(','.$this->newline, $elems) . $this->newline . str_repeat($this->indent, $depth + 1) . '}'; diff --git a/tests/Composer/Test/Command/ConfigCommandTest.php b/tests/Composer/Test/Command/ConfigCommandTest.php index 68653c622..27ee92b4a 100644 --- a/tests/Composer/Test/Command/ConfigCommandTest.php +++ b/tests/Composer/Test/Command/ConfigCommandTest.php @@ -82,6 +82,26 @@ class ConfigCommandTest extends TestCase ['setting-key' => 'preferred-install.foo/*', '--unset' => true], ['config' => ['preferred-install' => []]], ]; + yield 'set extra with merge' => [ + [], + ['setting-key' => 'extra.patches.foo/bar', 'setting-value' => ['{"123":"value"}'], '--json' => true, '--merge' => true], + ['extra' => ['patches' => ['foo/bar' => [123 => 'value']]]], + ]; + yield 'combine extra with merge' => [ + ['extra' => ['patches' => ['foo/bar' => [5 => 'oldvalue']]]], + ['setting-key' => 'extra.patches.foo/bar', 'setting-value' => ['{"123":"value"}'], '--json' => true, '--merge' => true], + ['extra' => ['patches' => ['foo/bar' => [123 => 'value', 5 => 'oldvalue']]]], + ]; + yield 'combine extra with list' => [ + ['extra' => ['patches' => ['foo/bar' => ['oldvalue']]]], + ['setting-key' => 'extra.patches.foo/bar', 'setting-value' => ['{"123":"value"}'], '--json' => true, '--merge' => true], + ['extra' => ['patches' => ['foo/bar' => [123 => 'value', 0 => 'oldvalue']]]], + ]; + yield 'overwrite extra with merge' => [ + ['extra' => ['patches' => ['foo/bar' => [123 => 'oldvalue']]]], + ['setting-key' => 'extra.patches.foo/bar', 'setting-value' => ['{"123":"value"}'], '--json' => true, '--merge' => true], + ['extra' => ['patches' => ['foo/bar' => [123 => 'value']]]], + ]; } /** diff --git a/tests/Composer/Test/Json/JsonManipulatorTest.php b/tests/Composer/Test/Json/JsonManipulatorTest.php index 8ee8b5862..1d27069bd 100644 --- a/tests/Composer/Test/Json/JsonManipulatorTest.php +++ b/tests/Composer/Test/Json/JsonManipulatorTest.php @@ -1721,6 +1721,38 @@ class JsonManipulatorTest extends TestCase ', $manipulator->getContents()); } + public function testRemoveSubNodePreservesObjectTypeWhenEmpty(): void + { + $manipulator = new JsonManipulator('{ + "test": {"0": "foo"} +}'); + + $this->assertTrue($manipulator->removeSubNode('test', '0')); + $this->assertEquals('{ + "test": { + } +} +', $manipulator->getContents()); + } + + public function testRemoveSubNodePreservesObjectTypeWhenEmpty2(): void + { + $manipulator = new JsonManipulator('{ + "config": { + "preferred-install": {"foo/*": "source"} + } +}'); + + $this->assertTrue($manipulator->removeConfigSetting('preferred-install.foo/*')); + $this->assertEquals('{ + "config": { + "preferred-install": { + } + } +} +', $manipulator->getContents()); + } + public function testAddSubNodeInRequire(): void { $manipulator = new JsonManipulator('{