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

Add option to allow certain keys to preserve their order / not be normalised #996

Closed
wants to merge 23 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
63190b2
Allow certain keys to perserve their order
fredden Nov 9, 2022
e3220e3
Apply changes from php-cs-fixer
fredden Nov 9, 2022
3428966
Appease psalm static code analysis tool
fredden Nov 9, 2022
650edf0
Apply changes from php-cs-fixer
fredden Nov 10, 2022
bc653ee
Add mbstring extension as requirement
fredden Nov 10, 2022
8af9994
Increase test coverage
fredden Nov 11, 2022
a9db665
Merge remote-tracking branch 'upstream/main' into preserve-order
fredden Dec 6, 2022
892159e
Apply changes from php-cs-fixer
fredden Dec 6, 2022
1267419
Merge remote-tracking branch 'upstream/main' into preserve-order
fredden Jan 27, 2023
2c522f9
Fix code broken during merge
fredden Jan 27, 2023
3acbcdf
Merge remote-tracking branch 'upstream/main' into preserve-order
fredden Jan 31, 2023
04648b0
Merge remote-tracking branch 'upstream/main' into preserve-order
fredden Feb 2, 2023
c201715
Merge remote-tracking branch 'upstream/main' into preserve-order
fredden Feb 3, 2023
56c48bf
Merge remote-tracking branch 'upstream/main' into preserve-order
fredden Feb 7, 2023
f230604
Fix problems identified by psalm
fredden Feb 7, 2023
32c163e
Merge remote-tracking branch 'upstream/main' into preserve-order
fredden Feb 8, 2023
5dfd3c6
Merge remote-tracking branch 'upstream/main' into preserve-order
fredden Feb 10, 2023
5620033
Merge remote-tracking branch 'upstream/main' into preserve-order
fredden Feb 15, 2023
f510a64
Merge remote-tracking branch 'upstream/main' into preserve-order
fredden Feb 18, 2023
5ac6f5f
Merge remote-tracking branch 'upstream/main' into preserve-order
fredden Feb 21, 2023
1262a8e
Run php-cs-fixer with new ruleset
fredden Feb 21, 2023
1fc5830
Merge remote-tracking branch 'upstream/main' into preserve-order
fredden Feb 22, 2023
b8ecead
Merge remote-tracking branch 'upstream/main' into preserve-order
fredden Feb 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,23 @@ As an alternative to specifying the `--indent-size` and `--indent-style` options

:bulb: The configuration provided in composer extra always overrides the configuration provided via command line options.

#### Sorting of additional keys

By default, this plug-in will sort all keys unless their order is important according to the Composer specification. This means that `repositories.*` and the children of `scripts.*.*` will remain in the order initially specified, but all other keys will be sorted. If you would like additional keys to remain unsorted, you may specify their paths in the `preserve-order` configuration option as follows:

```json
{
"extra": {
"composer-normalize": {
"preserve-order": [
"extra.installer-paths",
"extra.patches.*"
]
}
}
}
```

### Continuous Integration

If you want to run this in continuous integration services, use the `--dry-run` option.
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"require": {
"php": "~8.0.0 || ~8.1.0 || ~8.2.0",
"ext-json": "*",
"ext-mbstring": "*",
"composer-plugin-api": "^2.0.0",
"ergebnis/json-normalizer": "~2.1.0",
"ergebnis/json-printer": "^3.3.0",
Expand Down
3 changes: 2 additions & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

84 changes: 84 additions & 0 deletions src/Command/NormalizeCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,86 @@ protected function execute(

$normalizer = new Normalizer\ChainNormalizer(
$this->normalizer,
new class($json) implements Normalizer\NormalizerInterface {
private const NEVER_SORT_PATHS = [
'scripts.*',
];

public function __construct(
private Normalizer\Json $originalJson,
) {
}

public function normalize(Normalizer\Json $json): Normalizer\Json
{
$normalizedDecoded = (array) $json->decoded();
$originalDecoded = (array) $this->originalJson->decoded();
$pathsToPreserve = self::NEVER_SORT_PATHS;

if (\count($originalDecoded) !== \count($normalizedDecoded)) {
// This is a workaround for a bug where $this->originalJson->decoded() returns an empty object.
$originalDecoded = (array) Normalizer\Json::fromEncoded($this->originalJson->encoded())->decoded();
}

if (isset($originalDecoded['extra'])) {
$extra = (array) $originalDecoded['extra'];

if (isset($extra['composer-normalize'])) {
$config = (array) $extra['composer-normalize'];

if (isset($config['preserve-order'])) {
$userPaths = (array) $config['preserve-order'];
$pathsToPreserve = \array_merge($pathsToPreserve, $userPaths);
}
}
}

foreach ($pathsToPreserve as $pathToPreserve) {
\assert(\is_string($pathToPreserve));
$normalizedDecoded = self::restoreSpecificKeyOrder($normalizedDecoded, $originalDecoded, $pathToPreserve);
}

return Normalizer\Json::fromEncoded(\json_encode($normalizedDecoded));
}

private static function restoreSpecificKeyOrder(array $normalized, array $original, string $path): array
{
if (\mb_strpos($path, '.') === false) {
// found a leaf
if (\array_key_exists($path, $normalized)) {
/** @var null|array|bool|float|int|object|string $original[$path] */
$normalized[$path] = $original[$path];
} elseif (\mb_strpos($path, '*') !== false) {
foreach (\array_keys($normalized) as $key) {
if (\fnmatch($path, (string) $key)) {
/** @var null|array|bool|float|int|object|string $original[$key] */
$normalized[$key] = $original[$key];
}
}
}
} else {
// found a branch
if (!\str_contains($path, '.')) {
$prefix = $path;
$suffix = '';
} else {
[$prefix, $suffix] = \explode('.', $path, 2);
}

if (\array_key_exists($prefix, $normalized)) {
$normalized[$prefix] = self::restoreSpecificKeyOrder((array) $normalized[$prefix], (array) $original[$prefix], $suffix);
} elseif (\mb_strpos($prefix, '*') !== false) {
foreach (\array_keys($normalized) as $key) {
if (\fnmatch($prefix, (string) $key)) {
$normalized[$key] = self::restoreSpecificKeyOrder((array) $normalized[$key], (array) $original[$key], $suffix);
}
}
}
}

return $normalized;
}
},
new class($this->printer, $format) implements Normalizer\NormalizerInterface {
public function __construct(
private Printer\PrinterInterface $printer,
Expand Down Expand Up @@ -400,6 +480,10 @@ private static function indentFromExtra(array $extra): ?Normalizer\Format\Indent
\array_keys($configuration),
);

if ($missingKeys === $requiredKeys && isset($configuration['preserve-order'])) {
return null;
}

if ([] !== $missingKeys) {
throw new \RuntimeException(\sprintf(
'Configuration in composer extra requires keys "%s" with corresponding values."',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

/**
* Copyright (c) 2018-2023 Andreas Möller
*
* For the full copyright and license information, please view
* the LICENSE.md file that was distributed with this source code.
*
* @see https://github.com/ergebnis/composer-normalize
*/

namespace Ergebnis\Composer\Normalize\Test\Integration\Command\NormalizeCommand\Extra\Valid\WithPreserveOrder;

use Ergebnis\Composer\Normalize\Test\Integration;
use Ergebnis\Composer\Normalize\Test\Util;
use Symfony\Component\Console;

/**
* @internal
*
* @covers \Ergebnis\Composer\Normalize\Command\NormalizeCommand
* @covers \Ergebnis\Composer\Normalize\NormalizePlugin
*
* @uses \Ergebnis\Composer\Normalize\Version
*/
final class Test extends Integration\Command\NormalizeCommand\AbstractTestCase
{
/**
* @dataProvider \Ergebnis\Composer\Normalize\Test\DataProvider\Command\NormalizeCommandProvider::commandInvocationIndentSizeAndIndentStyle
*/
public function testSucceeds(
Util\CommandInvocation $commandInvocation,
int $indentSize,
string $indentStyle,
): void {
$scenario = self::createScenario(
$commandInvocation,
__DIR__ . '/fixture',
);

$initialState = $scenario->initialState();

self::assertComposerJsonFileExists($initialState);
self::assertComposerLockFileNotExists($initialState);

$application = self::createApplicationWithNormalizeCommandAsProvidedByNormalizePlugin();

$input = new Console\Input\ArrayInput($scenario->consoleParameters());
$output = new Console\Output\BufferedOutput();

$exitCode = $application->run(
$input,
$output,
);

self::assertExitCodeSame(0, $exitCode);

$display = $output->fetch();

$expected = \sprintf(
'Successfully normalized %s.',
$scenario->composerJsonFileReference(),
);

self::assertStringContainsString($expected, $display);

$currentState = $scenario->currentState();

self::assertComposerJsonFileModified($initialState, $currentState);
self::assertComposerLockFileNotExists($currentState);

$decoded = (array) \json_decode($currentState->composerJsonFile()->contents(), true, 32, \JSON_THROW_ON_ERROR);
$require = (array) $decoded['require'];
self::assertSame(['ext-json', 'php'], \array_keys($require));
$extra = (array) $decoded['extra'];
self::assertSame(['composer-normalize', 'other'], \array_keys($extra));
$other = (array) $extra['other'];
self::assertSame(['keep-unsorted', 'sort-this'], \array_keys($other));
$keepUnsorted = (array) $other['keep-unsorted'];
self::assertSame(['one', 'two', 'three', 'four'], $keepUnsorted);

// FIXME: when ergebnis/json-normalizer has been upgraded to ^3.0, the following test can be uncommented / should work.
// @see https://github.com/ergebnis/composer-normalize/pull/956
// $sortThis = (array) $other['sort-this'];
// self::assertSame(['first', 'last'], $sortThis);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"type": "library",
"_comment": "This composer.json is valid according to a lax validation, a composer.lock is not present, and composer.json is not yet normalized.",
"keywords": ["foo", "bar"],
"license": "MIT",
"authors": [
{"name": "Andreas Möller", "email": "[email protected]"}
],
"extra": {
"other": {
"sort-this": [ "last", "first" ],
"keep-unsorted": [ "one", "two", "three", "four"]
},
"composer-normalize": {
"preserve-order": [
"this.path.does.*.not.*.exist.no-match-with-keys",
"require",
"extra.*.keep-unsorted"
]
}
},
"require": {
"ext-json": "*",
"php": "^5.6"
},
"config": {
"allow-plugins": {
"ergebnis/composer-normalize": true,
"ergebnis/*": false
}
},
"scripts": {
"auto-scripts": {
"cache:clear": "symfony-cmd",
"doctrine:migrations:migrate -v": "symfony-cmd",
"bazinga:js-translation:dump assets --merge-domains --format=json": "symfony-cmd",
"assets:install %PUBLIC_DIR%": "symfony-cmd"
}
}
}