Issues (18)

src/Composer/Commands/ValidateCommand.php (5 issues)

1
<?php
2
/**
3
 * Copyright © Vaimo Group. All rights reserved.
4
 * See LICENSE_VAIMO.txt for license details.
5
 */
6
namespace Vaimo\ComposerPatches\Composer\Commands;
7
8
use Symfony\Component\Console\Input\InputInterface;
9
use Symfony\Component\Console\Output\OutputInterface;
10
use Symfony\Component\Console\Input\InputOption;
11
12
use Composer\Repository\WritableRepositoryInterface as PackageRepository;
13
14
use Vaimo\ComposerPatches\Composer\ConfigKeys;
15
use Vaimo\ComposerPatches\Config;
16
use Vaimo\ComposerPatches\Patch\Definition as Patch;
17
use Vaimo\ComposerPatches\Patch\DefinitionList\Loader\ComponentPool;
18
use Vaimo\ComposerPatches\Composer\Context as ComposerContext;
19
use Vaimo\ComposerPatches\Utils\PathUtils;
20
21
/**
22
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
23
 */
24
class ValidateCommand extends \Composer\Command\BaseCommand
25
{
26
    protected function configure()
27
    {
28
        parent::configure();
29
30
        $this->setName('patch:validate');
31
32
        $this->setDescription('Validate that all patches have proper target configuration');
33
34
        $this->addOption(
35
            '--from-source',
36
            null,
37
            InputOption::VALUE_NONE,
38
            'Use latest information from package configurations in vendor folder'
39
        );
40
41
        $this->addOption(
42
            '--local',
43
            null,
44
            InputOption::VALUE_NONE,
45
            'Only validate patches that are owned by the ROOT package'
46
        );
47
    }
48
49
    protected function execute(InputInterface $input, OutputInterface $output)
50
    {
51
        $output->writeln('<info>Scanning packages for orphan patches</info>');
52
53
        $composer = $this->getComposer();
0 ignored issues
show
Deprecated Code introduced by
The function Composer\Command\BaseCommand::getComposer() has been deprecated: since Composer 2.3.0 use requireComposer or tryComposer depending on whether you have $required set to true or false ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

53
        $composer = /** @scrutinizer ignore-deprecated */ $this->getComposer();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
54
55
        $localOnly = $input->getOption('local');
56
57
        $patchListAnalyser = new \Vaimo\ComposerPatches\Patch\DefinitionList\Analyser();
58
59
        $pluginConfig = array(
60
            Config::PATCHER_SOURCES => $this->createSourcesEnablerConfig($localOnly)
61
        );
62
63
        $contextFactory = new \Vaimo\ComposerPatches\Factories\ComposerContextFactory($composer);
64
        $composerContext = $contextFactory->create();
65
66
        $configFactory = new \Vaimo\ComposerPatches\Factories\ConfigFactory($composerContext, array(
67
            Config::PATCHER_FROM_SOURCE => (bool)$input->getOption('from-source')
68
        ));
69
70
        $repository = $composer->getRepositoryManager()->getLocalRepository();
71
72
        $pluginConfig = $configFactory->create(array($pluginConfig));
73
74
        $patchesLoader = $this->createPatchesLoader($composerContext, $pluginConfig);
75
76
        $patches = $patchesLoader->loadFromPackagesRepository($repository);
77
78
        $patchPaths = $patchListAnalyser->extractValue($patches, array(Patch::PATH, Patch::SOURCE));
79
80
        $patchDefines = array_combine(
81
            $patchPaths,
82
            $patchListAnalyser->extractDictionary($patches, array(Patch::OWNER, Patch::URL))
83
        );
84
85
        $patchStatuses = array_filter(
86
            array_combine(
87
                $patchPaths,
88
                $patchListAnalyser->extractValue($patches, array(Patch::STATUS_LABEL))
89
            ) ?: array()
90
        );
91
92
        $matches = $this->resolveValidationTargets($repository, $pluginConfig);
93
        $installPaths = $this->collectInstallPaths($matches);
94
        $fileMatches = $this->collectPatchFilesFromPackages($matches, $pluginConfig);
95
        $groups = $this->collectOrphans($fileMatches, $patchDefines, $installPaths, $patchStatuses);
96
        $this->outputOrphans($output, $groups);
97
98
        $output->writeln(
99
            $groups ? '<error>Orphans found!</error>' : '<info>Validation completed successfully</info>'
100
        );
101
102
        return (int)(bool)$groups;
103
    }
104
105
    private function createSourcesEnablerConfig($localOnly)
106
    {
107
        $configDefaults = new Config\Defaults();
108
109
        $defaultValues = $configDefaults->getPatcherConfig();
110
111
        if (isset($defaultValues[Config::PATCHER_SOURCES]) && is_array($defaultValues[Config::PATCHER_SOURCES])) {
112
            $sourceKeys = array_keys((array)$defaultValues[Config::PATCHER_SOURCES]);
113
114
            return $localOnly
115
                ? array_replace(array_fill_keys($sourceKeys, false), array('project' => true))
116
                : array_fill_keys($sourceKeys, true);
117
        }
118
119
        return array();
120
    }
121
122
    private function collectInstallPaths(array $matches)
123
    {
124
        $composer = $this->getComposer();
0 ignored issues
show
Deprecated Code introduced by
The function Composer\Command\BaseCommand::getComposer() has been deprecated: since Composer 2.3.0 use requireComposer or tryComposer depending on whether you have $required set to true or false ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

124
        $composer = /** @scrutinizer ignore-deprecated */ $this->getComposer();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
125
        $projectRoot = getcwd();
126
127
        $installationManager = $composer->getInstallationManager();
128
129
        $installPaths = array();
130
        foreach ($matches as $packageName => $package) {
131
            $installPaths[$packageName] = $package instanceof \Composer\Package\RootPackageInterface
132
                ? $projectRoot
133
                : $installationManager->getInstallPath($package);
134
        }
135
136
        return $installPaths;
137
    }
138
139
    private function collectPatchFilesFromPackages(array $matches, Config $pluginConfig)
140
    {
141
        $composer = $this->getComposer();
0 ignored issues
show
Deprecated Code introduced by
The function Composer\Command\BaseCommand::getComposer() has been deprecated: since Composer 2.3.0 use requireComposer or tryComposer depending on whether you have $required set to true or false ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

141
        $composer = /** @scrutinizer ignore-deprecated */ $this->getComposer();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
142
        $composerConfig = $composer->getConfig();
143
144
        $configReaderFactory = new \Vaimo\ComposerPatches\Factories\PatcherConfigReaderFactory($composer);
145
        $dataUtils = new \Vaimo\ComposerPatches\Utils\DataUtils();
146
        $filterUtils = new \Vaimo\ComposerPatches\Utils\FilterUtils();
147
        $fileSystemUtils = new \Vaimo\ComposerPatches\Utils\FileSystemUtils();
148
149
        $projectRoot = getcwd();
150
151
        $vendorRoot = $composerConfig->get(ConfigKeys::VENDOR_DIR);
152
        $vendorPath = ltrim(
153
            substr($vendorRoot, strlen($projectRoot)),
154
            DIRECTORY_SEPARATOR
155
        );
156
157
        $defaultIgnores = array($vendorPath, '.hg', '.git', '.idea');
158
159
        $patcherConfigReader = $configReaderFactory->create($pluginConfig);
160
161
        $installPaths = $this->collectInstallPaths($matches);
162
163
        $fileMatchGroups = array();
164
165
        foreach ($matches as $packageName => $package) {
166
            $patcherConfig = $patcherConfigReader->readFromPackage($package);
167
168
            $ignores = $dataUtils->getValueByPath(
169
                $patcherConfig,
170
                array(Config::PATCHER_CONFIG_ROOT, Config::PATCHES_IGNORE),
171
                array()
172
            );
173
174
            $installPath = $installPaths[$packageName];
175
176
            $skippedPaths = $dataUtils->prefixArrayValues(
177
                array_merge($defaultIgnores, $ignores),
178
                $installPath . DIRECTORY_SEPARATOR
179
            );
180
181
            $filter = $filterUtils->composeRegex(
182
                $filterUtils->invertRules($skippedPaths),
183
                '/'
184
            );
185
186
            $filter = sprintf('%s/i', rtrim($filter, '/') . Config::PATCH_FILE_REGEX_MATCHER);
187
            $searchResult = $fileSystemUtils->collectFilePathsRecursively($installPath, $filter);
188
189
            $fileMatchGroups[] = array_fill_keys($searchResult, array(
190
                Patch::OWNER => $packageName,
191
                Patch::URL => ''
192
            ));
193
        }
194
195
        return array_reduce($fileMatchGroups, 'array_replace', array());
196
    }
197
198
    private function resolveValidationTargets(PackageRepository $repository, Config $pluginConfig)
199
    {
200
        $composer = $this->getComposer();
0 ignored issues
show
Deprecated Code introduced by
The function Composer\Command\BaseCommand::getComposer() has been deprecated: since Composer 2.3.0 use requireComposer or tryComposer depending on whether you have $required set to true or false ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

200
        $composer = /** @scrutinizer ignore-deprecated */ $this->getComposer();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
201
202
        $packageResolver = new \Vaimo\ComposerPatches\Composer\Plugin\PackageResolver(
203
            array($composer->getPackage())
204
        );
205
206
        $srcResolverFactory = new \Vaimo\ComposerPatches\Factories\SourcesResolverFactory($composer);
207
        $packageListUtils = new \Vaimo\ComposerPatches\Utils\PackageListUtils();
208
209
        $srcResolver = $srcResolverFactory->create($pluginConfig);
210
211
        $sources = $srcResolver->resolvePackages($repository);
212
213
        $repositoryUtils = new \Vaimo\ComposerPatches\Utils\RepositoryUtils();
214
215
        $pluginPackage = $packageResolver->resolveForNamespace(
216
            $repository->getCanonicalPackages(),
217
            __NAMESPACE__
218
        );
219
220
        $pluginName = $pluginPackage->getName();
221
222
        $pluginUsers = array_merge(
223
            $repositoryUtils->filterByDependency($repository, $pluginName),
224
            array($composer->getPackage())
225
        );
226
227
        return array_intersect_key(
228
            $packageListUtils->listToNameDictionary($sources),
229
            $packageListUtils->listToNameDictionary($pluginUsers)
230
        );
231
    }
232
233
    private function createPatchesLoader(ComposerContext $composerContext, Config $pluginConfig)
234
    {
235
        $composer = $this->getComposer();
0 ignored issues
show
Deprecated Code introduced by
The function Composer\Command\BaseCommand::getComposer() has been deprecated: since Composer 2.3.0 use requireComposer or tryComposer depending on whether you have $required set to true or false ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

235
        $composer = /** @scrutinizer ignore-deprecated */ $this->getComposer();

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
236
237
        $loaderFactory = new \Vaimo\ComposerPatches\Factories\PatchesLoaderFactory($composer);
238
239
        $componentOverrides = array(
240
            'constraints' => false,
241
            'platform' => false,
242
            'targets-resolver' => false,
243
            'local-exclude' => false,
244
            'root-patch' => false,
245
            'global-exclude' => false
246
        );
247
248
        $loaderComponentsPool = $this->createLoaderPool($composerContext, $componentOverrides);
249
250
        return $loaderFactory->create($loaderComponentsPool, $pluginConfig, true);
251
    }
252
253
    private function createLoaderPool(ComposerContext $composerContext, array $componentUpdates = array())
254
    {
255
        $appIO = $this->getIO();
256
257
        $componentPool = new ComponentPool($composerContext, $appIO, true);
258
259
        foreach ($componentUpdates as $componentName => $replacement) {
260
            $componentPool->registerComponent($componentName, $replacement);
261
        }
262
263
        return $componentPool;
264
    }
265
266
    private function collectOrphans(array $files, array $patches, array $paths, array $statuses)
267
    {
268
        $orphanFiles = array_diff_key($files, $patches);
269
        $orphanConfig = array_diff_key($patches, $files);
270
271
        /**
272
         * Make sure that downloaded patches are not perceived as missing files
273
         */
274
        $orphanConfig = array_diff_key(
275
            $orphanConfig,
276
            array_flip(
277
                array_filter(array_keys($orphanConfig), 'file_exists')
278
            )
279
        );
280
281
        $groups = array_fill_keys(array_keys($paths), array());
282
283
        $results = array(
284
            $this->findWithMissingConfig($orphanFiles, $paths),
285
            $this->findWithMissingFile($orphanConfig, $paths, $statuses)
286
        );
287
288
        foreach ($results as $result) {
289
            foreach ($result as $ownerName => $items) {
290
                $groups[$ownerName] = array_merge($groups[$ownerName], $items);
291
            }
292
        }
293
294
        return array_filter($groups);
295
    }
296
297
    private function findWithMissingConfig($orphanFiles, $paths)
298
    {
299
        $pathFlags = array_fill_keys(array_keys($paths), true);
300
        $groups = array();
301
302
        foreach ($orphanFiles as $path => $config) {
303
            $ownerName = $config[Patch::OWNER];
304
            $installPath = $paths[$ownerName];
305
306
            if (!isset($pathFlags[$ownerName])) {
307
                continue;
308
            }
309
310
            $groups[$ownerName][] = array(
311
                'issue' => 'NO CONFIG',
312
                'path' => $config[Patch::URL] ?: PathUtils::reducePathLeft($path, $installPath)
313
            );
314
        }
315
316
        return $groups;
317
    }
318
319
    private function findWithMissingFile($orphanConfig, $paths, $statuses)
320
    {
321
        $pathFlags = array_fill_keys(array_keys($paths), true);
322
        $groups = array();
323
324
        foreach ($orphanConfig as $path => $config) {
325
            $ownerName = $config[Patch::OWNER];
326
            $installPath = $paths[$ownerName];
327
328
            if (!isset($pathFlags[$ownerName])) {
329
                continue;
330
            }
331
332
            $groups[$ownerName][] = array(
333
                'issue' => isset($statuses[$path]) && $statuses[$path]
334
                    ? $statuses[$path]
335
                    : 'NO FILE',
336
                'path' => $config[Patch::URL] ?: PathUtils::reducePathLeft($path, $installPath)
337
            );
338
        }
339
340
        return $groups;
341
    }
342
343
    private function outputOrphans(OutputInterface $output, array $groups)
344
    {
345
        $lines = array();
346
347
        foreach ($groups as $packageName => $items) {
348
            $lines[] = sprintf('  - <info>%s</info>', $packageName);
349
350
            foreach ($items as $item) {
351
                $lines[] = sprintf(
352
                    '    ~ %s [<fg=red>%s</>]',
353
                    $item['path'],
354
                    $item['issue']
355
                );
356
            }
357
        }
358
359
        foreach ($lines as $line) {
360
            $output->writeln($line);
361
        }
362
    }
363
}
364