1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * Copyright (c) 2018-2020 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\Command;
15

16
use Composer\Command;
17
use Composer\Console\Application;
18
use Composer\Factory;
19
use Composer\IO;
20
use Ergebnis\Composer\Normalize\Exception;
21
use Ergebnis\Json\Normalizer;
22
use Localheinz\Diff;
23
use Symfony\Component\Console;
24

25
/**
26
 * @internal
27
 */
28
final class NormalizeCommand extends Command\BaseCommand
29
{
30
    /**
31
     * @var array<string, string>
32
     */
33
    private static $indentStyles = [
34
        'space' => ' ',
35
        'tab' => "\t",
36
    ];
37

38
    private $factory;
39

40
    private $normalizer;
41

42
    private $formatter;
43

44
    private $differ;
45

46 1
    public function __construct(
47
        Factory $factory,
48
        Normalizer\NormalizerInterface $normalizer,
49
        Normalizer\Format\FormatterInterface $formatter,
50
        Diff\Differ $differ
51
    ) {
52 1
        parent::__construct('normalize');
53

54 1
        $this->factory = $factory;
55 1
        $this->normalizer = $normalizer;
56 1
        $this->formatter = $formatter;
57 1
        $this->differ = $differ;
58
    }
59

60 1
    protected function configure(): void
61
    {
62 1
        $this->setDescription('Normalizes composer.json according to its JSON schema (https://getcomposer.org/schema.json).');
63 1
        $this->setDefinition([
64 1
            new Console\Input\InputArgument(
65 1
                'file',
66 1
                Console\Input\InputArgument::OPTIONAL,
67 1
                'Path to composer.json file'
68
            ),
69 1
            new Console\Input\InputOption(
70 1
                'diff',
71 1
                null,
72 1
                Console\Input\InputOption::VALUE_NONE,
73 1
                'Show the results of normalizing'
74
            ),
75 1
            new Console\Input\InputOption(
76 1
                'dry-run',
77 1
                null,
78 1
                Console\Input\InputOption::VALUE_NONE,
79 1
                'Show the results of normalizing, but do not modify any files'
80
            ),
81 1
            new Console\Input\InputOption(
82 1
                'indent-size',
83 1
                null,
84 1
                Console\Input\InputOption::VALUE_REQUIRED,
85 1
                'Indent size (an integer greater than 0); should be used with the --indent-style option'
86
            ),
87 1
            new Console\Input\InputOption(
88 1
                'indent-style',
89 1
                null,
90 1
                Console\Input\InputOption::VALUE_REQUIRED,
91 1
                \sprintf(
92
                    'Indent style (one of "%s"); should be used with the --indent-size option',
93 1
                    \implode('", "', \array_keys(self::$indentStyles))
94
                )
95
            ),
96 1
            new Console\Input\InputOption(
97 1
                'no-check-lock',
98 1
                null,
99 1
                Console\Input\InputOption::VALUE_NONE,
100 1
                'Do not check if lock file is up to date'
101
            ),
102 1
            new Console\Input\InputOption(
103 1
                'no-update-lock',
104 1
                null,
105 1
                Console\Input\InputOption::VALUE_NONE,
106 1
                'Do not update lock file if it exists'
107
            ),
108
        ]);
109
    }
110

111 1
    protected function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
112
    {
113 1
        $io = $this->getIO();
114

115
        try {
116 1
            $indent = self::indentFrom($input);
117 1
        } catch (\RuntimeException $exception) {
118 1
            $io->writeError(\sprintf(
119
                '<error>%s</error>',
120 1
                $exception->getMessage()
121
            ));
122

123 1
            return 1;
124
        }
125

126 1
        $composerFile = $input->getArgument('file');
127

128 1
        if (null === $composerFile) {
129 1
            $composerFile = Factory::getComposerFile();
130
        }
131

132 1
        $composer = $this->factory->createComposer(
133 1
            $io,
134
            $composerFile
135
        );
136

137 1
        if (false === $input->getOption('dry-run') && !\is_writable($composerFile)) {
138 0
            $io->writeError(\sprintf(
139
                '<error>%s is not writable.</error>',
140 0
                $composerFile
141
            ));
142

143 0
            return 1;
144
        }
145

146 1
        $locker = $composer->getLocker();
147

148 1
        if (false === $input->getOption('no-check-lock') && $locker->isLocked() && !$locker->isFresh()) {
149 1
            $io->writeError('<error>The lock file is not up to date with the latest changes in composer.json, it is recommended that you run `composer update --lock`.</error>');
150

151 1
            return 1;
152
        }
153

154
        /** @var string $encoded */
155 1
        $encoded = \file_get_contents($composerFile);
156

157 1
        $json = Normalizer\Json::fromEncoded($encoded);
158

159
        try {
160 1
            $normalized = $this->normalizer->normalize($json);
161 1
        } catch (Normalizer\Exception\OriginalInvalidAccordingToSchemaException $exception) {
162 0
            $io->writeError('<error>Original composer.json does not match the expected JSON schema:</error>');
163

164 0
            self::showValidationErrors(
165
                $io,
166 0
                ...$exception->errors()
167
            );
168

169 0
            return 1;
170 1
        } catch (Normalizer\Exception\NormalizedInvalidAccordingToSchemaException $exception) {
171 0
            $io->writeError('<error>Normalized composer.json does not match the expected JSON schema:</error>');
172

173 0
            self::showValidationErrors(
174
                $io,
175 0
                ...$exception->errors()
176
            );
177

178 0
            return 1;
179 1
        } catch (\RuntimeException $exception) {
180 1
            $io->writeError(\sprintf(
181
                '<error>%s</error>',
182 1
                $exception->getMessage()
183
            ));
184

185 1
            return 1;
186
        }
187

188 1
        $format = $json->format();
189

190 1
        if (null !== $indent) {
191 1
            $format = $format->withIndent($indent);
192
        }
193

194 1
        $formatted = $this->formatter->format(
195 1
            $normalized,
196
            $format
197
        );
198

199 1
        if ($json->encoded() === $formatted->encoded()) {
200 1
            $io->write(\sprintf(
201
                '<info>%s is already normalized.</info>',
202 1
                $composerFile
203
            ));
204

205 1
            return 0;
206
        }
207

208 1
        if (true === $input->getOption('diff') || true === $input->getOption('dry-run')) {
209 1
            $io->writeError(\sprintf(
210
                '<error>%s is not normalized.</error>',
211 1
                $composerFile
212
            ));
213

214 1
            $diff = $this->differ->diff(
215 1
                $json->encoded(),
216 1
                $formatted->encoded()
217
            );
218

219 1
            $io->write([
220 1
                '',
221 1
                '<fg=yellow>---------- begin diff ----------</>',
222 1
                self::formatDiff($diff),
223 1
                '<fg=yellow>----------- end diff -----------</>',
224 1
                '',
225
            ]);
226
        }
227

228 1
        if (true === $input->getOption('dry-run')) {
229 1
            return 1;
230
        }
231

232 1
        \file_put_contents($composerFile, $formatted);
233

234 1
        $io->write(\sprintf(
235
            '<info>Successfully normalized %s.</info>',
236 1
            $composerFile
237
        ));
238

239 1
        if (true === $input->getOption('no-update-lock') || false === $locker->isLocked()) {
240 1
            return 0;
241
        }
242

243 1
        $io->write('<info>Updating lock file.</info>');
244

245 1
        $application = new Application();
246

247 1
        $application->setAutoExit(false);
248

249 1
        return self::updateLockerInWorkingDirectory(
250
            $application,
251
            $output,
252 1
            \dirname($composerFile)
253
        );
254
    }
255

256
    /**
257
     * @throws \RuntimeException
258
     */
259 1
    private static function indentFrom(Console\Input\InputInterface $input): ?Normalizer\Format\Indent
260
    {
261
        /** @var null|string $indentSize */
262 1
        $indentSize = $input->getOption('indent-size');
263

264
        /** @var null|string $indentStyle */
265 1
        $indentStyle = $input->getOption('indent-style');
266

267 1
        if (null === $indentSize && null === $indentStyle) {
268 1
            return null;
269
        }
270

271 1
        if (null === $indentSize) {
272 1
            throw new \RuntimeException('When using the indent-style option, an indent size needs to be specified using the indent-size option.');
273
        }
274

275 1
        if (null === $indentStyle) {
276 1
            throw new \RuntimeException(\sprintf(
277
                'When using the indent-size option, an indent style (one of "%s") needs to be specified using the indent-style option.',
278 1
                \implode('", "', \array_keys(self::$indentStyles))
279
            ));
280
        }
281

282 1
        if ((string) (int) $indentSize !== $indentSize || 1 > $indentSize) {
283 1
            throw new \RuntimeException(\sprintf(
284
                'Indent size needs to be an integer greater than 0, but "%s" is not.',
285 1
                $indentSize
286
            ));
287
        }
288

289
        try {
290 1
            $indent = Normalizer\Format\Indent::fromSizeAndStyle(
291 1
                (int) $indentSize,
292
                $indentStyle
293
            );
294 1
        } catch (Normalizer\Exception\InvalidIndentSizeException $exception) {
295 0
            throw new \RuntimeException(\sprintf(
296
                'Indent size needs to be an integer greater than %d, but "%s" is not.',
297 0
                $exception->minimumSize(),
298 0
                $exception->size()
299
            ));
300 1
        } catch (Normalizer\Exception\InvalidIndentStyleException $exception) {
301 1
            throw new \RuntimeException(\sprintf(
302
                'Indent style needs to be one of "%s", but "%s" is not.',
303 1
                \implode('", "', \array_keys(self::$indentStyles)),
304 1
                $indentStyle
305
            ));
306
        }
307

308 1
        return $indent;
309
    }
310

311 0
    private static function showValidationErrors(IO\IOInterface $io, string ...$errors): void
312
    {
313 0
        foreach ($errors as $error) {
314 0
            $io->writeError(\sprintf(
315
                '<error>- %s</error>',
316 0
                $error
317
            ));
318
        }
319

320 0
        $io->writeError('<warning>See https://getcomposer.org/doc/04-schema.md for details on the schema</warning>');
321
    }
322

323 1
    private static function formatDiff(string $diff): string
324
    {
325 1
        $lines = \explode(
326
            "\n",
327 1
            $diff
328
        );
329

330
        $formatted = \array_map(static function (string $line): string {
331 1
            $replaced = \preg_replace(
332
                [
333
                    '/^(\+.*)$/',
334
                    '/^(-.*)$/',
335
                ],
336
                [
337
                    '<fg=green>$1</>',
338
                    '<fg=red>$1</>',
339
                ],
340 1
                $line
341
            );
342

343 1
            if (!\is_string($replaced)) {
344 0
                throw Exception\ShouldNotHappen::create();
345
            }
346

347 1
            return $replaced;
348 1
        }, $lines);
349

350 1
        return \implode(
351
            "\n",
352 1
            $formatted
353
        );
354
    }
355

356
    /**
357
     * @see https://getcomposer.org/doc/03-cli.md#update
358
     *
359
     * @throws \Exception
360
     */
361 1
    private static function updateLockerInWorkingDirectory(
362
        Console\Application $application,
363
        Console\Output\OutputInterface $output,
364
        string $workingDirectory
365
    ): int {
366 1
        return $application->run(
367 1
            new Console\Input\ArrayInput([
368 1
                'command' => 'update',
369
                '--ignore-platform-reqs' => true,
370
                '--lock' => true,
371
                '--no-autoloader' => true,
372
                '--no-plugins' => true,
373
                '--no-scripts' => true,
374 1
                '--working-dir' => $workingDirectory,
375
            ]),
376
            $output
377
        );
378
    }
379
}

Read our documentation on viewing source code .

Loading