Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preserve indentation when writing JSON files #11390

Merged
merged 3 commits into from
Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
34 changes: 31 additions & 3 deletions src/Composer/Json/JsonFile.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -117,6 +121,8 @@ public function read()
throw new \RuntimeException('Could not read '.$this->path);
}

$this->indent = self::detectIndenting($json);

return static::parseJson($json, $this->path);
}

Expand All @@ -131,7 +137,7 @@ public function read()
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;
}
Expand All @@ -153,7 +159,7 @@ public function write(array $hash, int $options = JSON_UNESCAPED_SLASHES | JSON_
$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) {
Expand Down Expand Up @@ -262,15 +268,28 @@ public static function validateJsonSchema(string $source, $data, int $schema, ?s
*
* @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));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the number of spaces is not a multiple of 4, we should probably keep the remaining spaces (as those would not be about indentation but about alignment)

Copy link
Contributor Author

@maximal maximal Mar 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mathematically speaking, the number of tabs must always be equal to the indent level, and then any number of spaces for alignment (aka SmartTabs approach), but I think here we can simplify the process as we are dealing with json_encode() result which can have either 4 or 0 spaces for indentation and have no alignment at all. (Also, JSON cannot have multiline strings and comments, so the regex-based replacing is somewhat reasonable.)

},
$json
);
}

return $json;
}

Expand All @@ -279,6 +298,7 @@ public static function encode($data, int $options = 448)
*
* @param int $code return code of json_last_error function
* @throws \RuntimeException
* @return never
*/
private static function throwEncodeError(int $code): void
{
Expand Down Expand Up @@ -346,4 +366,12 @@ protected static function validateSyntax(string $json, ?string $file = null): bo

throw new ParsingException('"'.$file.'" does not contain valid JSON'."\n".$result->getMessage(), $result->getDetails());
}

public static function detectIndenting(?string $json): string
{
if (Preg::isMatchStrictGroups('#^([ \t]+)"#m', $json ?? '', $match)) {
return $match[1];
}
return self::INDENT_DEFAULT;
}
}
6 changes: 1 addition & 5 deletions src/Composer/Json/JsonManipulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -561,10 +561,6 @@ public function format($data, int $depth = 0): string

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);
}
}
3 changes: 3 additions & 0 deletions tests/Composer/Test/Json/Fixtures/tabs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"foo": "bar"
}
23 changes: 23 additions & 0 deletions tests/Composer/Test/Json/JsonFileTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,29 @@ public function testDoubleEscapedUnicode(): void
$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 {
Expand Down