Skip to content

Commit 95a318a

Browse files
authored
Merge 892159e into d160e24
2 parents d160e24 + 892159e commit 95a318a

File tree

6 files changed

+226
-1
lines changed

6 files changed

+226
-1
lines changed

README.md

+17
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,23 @@ As an alternative to specifying the `--indent-size` and `--indent-style` options
148148

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

151+
#### Sorting of additional keys
152+
153+
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:
154+
155+
```json
156+
{
157+
"extra": {
158+
"composer-normalize": {
159+
"preserve-order": [
160+
"extra.installer-paths",
161+
"extra.patches.*"
162+
]
163+
}
164+
}
165+
}
166+
```
167+
151168
### Continuous Integration
152169

153170
If you want to run this in continuous integration services, use the `--dry-run` option.

composer.json

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
},
2323
"require": {
2424
"php": "^8.0",
25+
"ext-json": "*",
26+
"ext-mbstring": "*",
2527
"composer-plugin-api": "^2.0.0",
2628
"ergebnis/json-normalizer": "~2.1.0",
2729
"ergebnis/json-printer": "^3.3.0",

composer.lock

+3-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Command/NormalizeCommand.php

+75
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
*/
3030
final class NormalizeCommand extends Command\BaseCommand
3131
{
32+
private const NEVER_SORT_PATHS = [
33+
'scripts.*',
34+
];
35+
3236
public function __construct(
3337
private Factory $factory,
3438
private Normalizer\NormalizerInterface $normalizer,
@@ -215,6 +219,8 @@ protected function execute(
215219
return 1;
216220
}
217221

222+
$normalized = self::restoreSortOrderForSpecificKeys($normalized, $json);
223+
218224
$format = Normalizer\Format\Format::fromJson($json);
219225

220226
if (null !== $indent) {
@@ -373,6 +379,10 @@ private static function indentFromExtra(array $extra): ?Normalizer\Format\Indent
373379
\array_keys($configuration),
374380
);
375381

382+
if ($missingKeys === $requiredKeys && isset($configuration['preserve-order'])) {
383+
return null;
384+
}
385+
376386
if ([] !== $missingKeys) {
377387
throw new \RuntimeException(\sprintf(
378388
'Configuration in composer extra requires keys "%s" with corresponding values."',
@@ -501,4 +511,69 @@ private static function updateLockerInWorkingDirectory(
501511
$output,
502512
);
503513
}
514+
515+
private static function restoreSortOrderForSpecificKeys(Normalizer\Json $normalized, Normalizer\Json $original): Normalizer\Json
516+
{
517+
$normalizedDecoded = (array) $normalized->decoded();
518+
$originalDecoded = (array) $original->decoded();
519+
$pathsToPreserve = self::NEVER_SORT_PATHS;
520+
521+
if (\count($originalDecoded) !== \count($normalizedDecoded)) {
522+
// This is a workaround for a bug where $json->decoded() returns an empty object.
523+
$originalDecoded = (array) Normalizer\Json::fromEncoded($original->encoded())->decoded();
524+
}
525+
526+
if (isset($originalDecoded['extra'])) {
527+
$extra = (array) $originalDecoded['extra'];
528+
529+
if (isset($extra['composer-normalize'])) {
530+
$config = (array) $extra['composer-normalize'];
531+
532+
if (isset($config['preserve-order'])) {
533+
$userPaths = (array) $config['preserve-order'];
534+
$pathsToPreserve = \array_merge($pathsToPreserve, $userPaths);
535+
}
536+
}
537+
}
538+
539+
foreach ($pathsToPreserve as $pathToPreserve) {
540+
\assert(\is_string($pathToPreserve));
541+
$normalizedDecoded = self::restoreSpecificKeyOrder($normalizedDecoded, $originalDecoded, $pathToPreserve);
542+
}
543+
544+
return Normalizer\Json::fromEncoded(\json_encode($normalizedDecoded));
545+
}
546+
547+
private static function restoreSpecificKeyOrder(array $normalized, array $original, string $path): array
548+
{
549+
if (\mb_strpos($path, '.') === false) {
550+
// found a leaf
551+
if (\array_key_exists($path, $normalized)) {
552+
/** @var null|array|bool|float|int|object|string $original[$path] */
553+
$normalized[$path] = $original[$path];
554+
} elseif (\mb_strpos($path, '*') !== false) {
555+
foreach (\array_keys($normalized) as $key) {
556+
if (\fnmatch($path, (string) $key)) {
557+
/** @var null|array|bool|float|int|object|string $original[$key] */
558+
$normalized[$key] = $original[$key];
559+
}
560+
}
561+
}
562+
} else {
563+
// found a branch
564+
[$prefix, $suffix] = \explode('.', $path, 2);
565+
566+
if (\array_key_exists($prefix, $normalized)) {
567+
$normalized[$prefix] = self::restoreSpecificKeyOrder((array) $normalized[$prefix], (array) $original[$prefix], $suffix);
568+
} elseif (\mb_strpos($prefix, '*') !== false) {
569+
foreach (\array_keys($normalized) as $key) {
570+
if (\fnmatch($prefix, (string) $key)) {
571+
$normalized[$key] = self::restoreSpecificKeyOrder((array) $normalized[$key], (array) $original[$key], $suffix);
572+
}
573+
}
574+
}
575+
}
576+
577+
return $normalized;
578+
}
504579
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Copyright (c) 2018-2022 Andreas Möller
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE.md file that was distributed with this source code.
10+
*
11+
* @see https://github.com/ergebnis/composer-normalize
12+
*/
13+
14+
namespace Ergebnis\Composer\Normalize\Test\Integration\Command\NormalizeCommand\Extra\Valid\WithPreserveOrder;
15+
16+
use Ergebnis\Composer\Normalize\Test\Integration;
17+
use Ergebnis\Composer\Normalize\Test\Util;
18+
use Symfony\Component\Console;
19+
20+
/**
21+
* @internal
22+
*
23+
* @covers \Ergebnis\Composer\Normalize\Command\NormalizeCommand
24+
* @covers \Ergebnis\Composer\Normalize\NormalizePlugin
25+
*
26+
* @uses \Ergebnis\Composer\Normalize\Version
27+
*/
28+
final class Test extends Integration\Command\NormalizeCommand\AbstractTestCase
29+
{
30+
/**
31+
* @dataProvider \Ergebnis\Composer\Normalize\Test\DataProvider\Command\NormalizeCommandProvider::commandInvocationIndentSizeAndIndentStyle
32+
*/
33+
public function testSucceeds(
34+
Util\CommandInvocation $commandInvocation,
35+
int $indentSize,
36+
string $indentStyle,
37+
): void {
38+
$scenario = self::createScenario(
39+
$commandInvocation,
40+
__DIR__ . '/fixture',
41+
);
42+
43+
$initialState = $scenario->initialState();
44+
45+
self::assertComposerJsonFileExists($initialState);
46+
self::assertComposerLockFileNotExists($initialState);
47+
48+
$application = self::createApplicationWithNormalizeCommandAsProvidedByNormalizePlugin();
49+
50+
$input = new Console\Input\ArrayInput($scenario->consoleParameters());
51+
$output = new Console\Output\BufferedOutput();
52+
53+
$exitCode = $application->run(
54+
$input,
55+
$output,
56+
);
57+
58+
self::assertExitCodeSame(0, $exitCode);
59+
60+
$display = $output->fetch();
61+
62+
$expected = \sprintf(
63+
'Successfully normalized %s.',
64+
$scenario->composerJsonFileReference(),
65+
);
66+
67+
self::assertStringContainsString($expected, $display);
68+
69+
$currentState = $scenario->currentState();
70+
71+
self::assertComposerJsonFileModified($initialState, $currentState);
72+
self::assertComposerLockFileNotExists($currentState);
73+
74+
$decoded = (array) \json_decode($currentState->composerJsonFile()->contents(), true, 32, \JSON_THROW_ON_ERROR);
75+
$require = (array) $decoded['require'];
76+
self::assertSame(['ext-json', 'php'], \array_keys($require));
77+
$extra = (array) $decoded['extra'];
78+
self::assertSame(['composer-normalize', 'other'], \array_keys($extra));
79+
$other = (array) $extra['other'];
80+
self::assertSame(['keep-unsorted', 'sort-this'], \array_keys($other));
81+
$keepUnsorted = (array) $other['keep-unsorted'];
82+
self::assertSame(['one', 'two', 'three', 'four'], $keepUnsorted);
83+
84+
// FIXME: when ergebnis/json-normalizer has been upgraded to ^3.0, the following test can be uncommented / should work.
85+
// @see https://github.com/ergebnis/composer-normalize/pull/956
86+
// $sortThis = (array) $other['sort-this'];
87+
// self::assertSame(['first', 'last'], $sortThis);
88+
}
89+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"type": "library",
3+
"_comment": "This composer.json is valid according to a lax validation, a composer.lock is not present, and composer.json is not yet normalized.",
4+
"keywords": ["foo", "bar"],
5+
"license": "MIT",
6+
"authors": [
7+
{"name": "Andreas Möller", "email": "[email protected]"}
8+
],
9+
"extra": {
10+
"other": {
11+
"sort-this": [ "last", "first" ],
12+
"keep-unsorted": [ "one", "two", "three", "four"]
13+
},
14+
"composer-normalize": {
15+
"preserve-order": [
16+
"this.path.does.*.not.*.exist.no-match-with-keys",
17+
"require",
18+
"extra.*.keep-unsorted"
19+
]
20+
}
21+
},
22+
"require": {
23+
"ext-json": "*",
24+
"php": "^5.6"
25+
},
26+
"config": {
27+
"allow-plugins": {
28+
"ergebnis/composer-normalize": true,
29+
"ergebnis/*": false
30+
}
31+
},
32+
"scripts": {
33+
"auto-scripts": {
34+
"cache:clear": "symfony-cmd",
35+
"doctrine:migrations:migrate -v": "symfony-cmd",
36+
"bazinga:js-translation:dump assets --merge-domains --format=json": "symfony-cmd",
37+
"assets:install %PUBLIC_DIR%": "symfony-cmd"
38+
}
39+
}
40+
}

0 commit comments

Comments
 (0)