From 1c9fbeb9785d01cfb59d45eccb63f67134fa3de4 Mon Sep 17 00:00:00 2001 From: MaximAL Date: Wed, 19 Jul 2023 15:13:06 +0300 Subject: [PATCH] Preserve indentation when writing JSON files (#11390) Refs #11341 --- src/Composer/Json/JsonFile.php | 34 +++++++++++++++++++-- src/Composer/Json/JsonManipulator.php | 6 +--- tests/Composer/Test/Json/Fixtures/tabs.json | 3 ++ tests/Composer/Test/Json/JsonFileTest.php | 23 ++++++++++++++ 4 files changed, 58 insertions(+), 8 deletions(-) create mode 100644 tests/Composer/Test/Json/Fixtures/tabs.json diff --git a/src/Composer/Json/JsonFile.php b/src/Composer/Json/JsonFile.php index 3ad34a619..7f9814d61 100644 --- a/src/Composer/Json/JsonFile.php +++ b/src/Composer/Json/JsonFile.php @@ -42,12 +42,16 @@ class JsonFile public const COMPOSER_SCHEMA_PATH = __DIR__ . '/../../../res/composer-schema.json'; + public const INDENT_DEFAULT = ' '; + /** @var string */ private $path; /** @var ?HttpDownloader */ private $httpDownloader; /** @var ?IOInterface */ private $io; + /** @var string */ + private $indent = self::INDENT_DEFAULT; /** * Initializes json file reader/parser. @@ -117,6 +121,8 @@ class JsonFile throw new \RuntimeException('Could not read '.$this->path); } + $this->indent = self::detectIndenting($json); + return static::parseJson($json, $this->path); } @@ -131,7 +137,7 @@ class JsonFile public function write(array $hash, int $options = JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) { if ($this->path === 'php://memory') { - file_put_contents($this->path, static::encode($hash, $options)); + file_put_contents($this->path, static::encode($hash, $options, $this->indent)); return; } @@ -153,7 +159,7 @@ class JsonFile $retries = 3; while ($retries--) { try { - $this->filePutContentsIfModified($this->path, static::encode($hash, $options). ($options & JSON_PRETTY_PRINT ? "\n" : '')); + $this->filePutContentsIfModified($this->path, static::encode($hash, $options, $this->indent). ($options & JSON_PRETTY_PRINT ? "\n" : '')); break; } catch (\Exception $e) { if ($retries > 0) { @@ -262,15 +268,28 @@ class JsonFile * * @param mixed $data Data to encode into a formatted JSON string * @param int $options json_encode options (defaults to JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) + * @param string $indent Indentation string * @return string Encoded json */ - public static function encode($data, int $options = 448) + public static function encode($data, int $options = 448, string $indent = self::INDENT_DEFAULT): string { $json = json_encode($data, $options); + if (false === $json) { self::throwEncodeError(json_last_error()); } + if (($options & JSON_PRETTY_PRINT) > 0 && $indent !== self::INDENT_DEFAULT ) { + // Pretty printing and not using default indentation + return Preg::replaceCallback( + '#^ {4,}#m', + static function ($match) use ($indent): string { + return str_repeat($indent, (int)(strlen($match[0] ?? '') / 4)); + }, + $json + ); + } + return $json; } @@ -279,6 +298,7 @@ class JsonFile * * @param int $code return code of json_last_error function * @throws \RuntimeException + * @return never */ private static function throwEncodeError(int $code): void { @@ -356,4 +376,12 @@ class JsonFile $result->getDetails()); } } + + public static function detectIndenting(?string $json): string + { + if (Preg::isMatchStrictGroups('#^([ \t]+)"#m', $json ?? '', $match)) { + return $match[1]; + } + return self::INDENT_DEFAULT; + } } diff --git a/src/Composer/Json/JsonManipulator.php b/src/Composer/Json/JsonManipulator.php index d6caffeba..8d6759671 100644 --- a/src/Composer/Json/JsonManipulator.php +++ b/src/Composer/Json/JsonManipulator.php @@ -561,10 +561,6 @@ class JsonManipulator protected function detectIndenting(): void { - if (Preg::isMatchStrictGroups('{^([ \t]+)"}m', $this->contents, $match)) { - $this->indent = $match[1]; - } else { - $this->indent = ' '; - } + $this->indent = JsonFile::detectIndenting($this->contents); } } diff --git a/tests/Composer/Test/Json/Fixtures/tabs.json b/tests/Composer/Test/Json/Fixtures/tabs.json new file mode 100644 index 000000000..460b5331d --- /dev/null +++ b/tests/Composer/Test/Json/Fixtures/tabs.json @@ -0,0 +1,3 @@ +{ + "foo": "bar" +} diff --git a/tests/Composer/Test/Json/JsonFileTest.php b/tests/Composer/Test/Json/JsonFileTest.php index 6f0ad2e74..74b124ec3 100644 --- a/tests/Composer/Test/Json/JsonFileTest.php +++ b/tests/Composer/Test/Json/JsonFileTest.php @@ -364,6 +364,29 @@ class JsonFileTest extends TestCase $this->assertEquals($data, $doubleData); } + public function testPreserveIndentationAfterRead(): void + { + copy(__DIR__.'/Fixtures/tabs.json', __DIR__.'/Fixtures/tabs2.json'); + $jsonFile = new JsonFile(__DIR__.'/Fixtures/tabs2.json'); + $data = $jsonFile->read(); + $jsonFile->write(['foo' => 'baz']); + + self::assertSame("{\n\t\"foo\": \"baz\"\n}\n", file_get_contents(__DIR__.'/Fixtures/tabs2.json')); + + unlink(__DIR__.'/Fixtures/tabs2.json'); + } + + public function testOverwritesIndentationByDefault(): void + { + copy(__DIR__.'/Fixtures/tabs.json', __DIR__.'/Fixtures/tabs2.json'); + $jsonFile = new JsonFile(__DIR__.'/Fixtures/tabs2.json'); + $jsonFile->write(['foo' => 'baz']); + + self::assertSame("{\n \"foo\": \"baz\"\n}\n", file_get_contents(__DIR__.'/Fixtures/tabs2.json')); + + unlink(__DIR__.'/Fixtures/tabs2.json'); + } + private function expectParseException(string $text, string $json): void { try {