ergebnis / composer-normalize
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
     * @param Console\Input\InputInterface $input
258
     *
259
     * @throws \RuntimeException
260
     *
261
     * @return null|Normalizer\Format\Indent
262
     */
263 1
    private static function indentFrom(Console\Input\InputInterface $input): ?Normalizer\Format\Indent
264
    {
265
        /** @var null|string $indentSize */
266 1
        $indentSize = $input->getOption('indent-size');
267

268
        /** @var null|string $indentStyle */
269 1
        $indentStyle = $input->getOption('indent-style');
270

271 1
        if (null === $indentSize && null === $indentStyle) {
272 1
            return null;
273
        }
274

275 1
        if (null === $indentSize) {
276 1
            throw new \RuntimeException('When using the indent-style option, an indent size needs to be specified using the indent-size option.');
277
        }
278

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

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

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

312 1
        return $indent;
313
    }
314

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

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

327 1
    private static function formatDiff(string $diff): string
328
    {
329 1
        $lines = \explode(
330
            "\n",
331 1
            $diff
332
        );
333

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

347 1
            if (!\is_string($replaced)) {
348 0
                throw Exception\ShouldNotHappen::create();
349
            }
350

351 1
            return $replaced;
352 1
        }, $lines);
353

354 1
        return \implode(
355
            "\n",
356 1
            $formatted
357
        );
358
    }
359

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

Read our documentation on viewing source code .

Loading