Passed
Push — master ( 11eaad...3f8f84 )
by Allan
02:05
created

src/Composer/Commands/ValidateCommand.php (1 issue)

Labels
Severity
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 Composer\Repository\WritableRepositoryInterface as PackageRepository;
9
10
use Symfony\Component\Console\Input\InputInterface;
11
use Symfony\Component\Console\Output\OutputInterface;
12
use Symfony\Component\Console\Input\InputOption;
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\Utils\PathUtils;
19
20
/**
21
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
22
 */
23
class ValidateCommand extends \Composer\Command\BaseCommand
24
{
25
    protected function configure()
26
    {
27
        parent::configure();
28
29
        $this->setName('patch:validate');
30
31
        $this->setDescription('Validate that all patches have proper target configuration');
32
33
        $this->addOption(
34
            '--from-source',
35
            null,
36
            InputOption::VALUE_NONE,
37
            'Use latest information from package configurations in vendor folder'
38
        );
39
40
        $this->addOption(
41
            '--local',
42
            null,
43
            InputOption::VALUE_NONE,
44
            'Only validate patches that are owned by the ROOT package'
45
        );
46
    }
47
    
48
    protected function execute(InputInterface $input, OutputInterface $output)
49
    {
50
        $output->writeln('<info>Scanning packages for orphan patches</info>');
51
52
        $composer = $this->getComposer();
53
        
54
        $localOnly = $input->getOption('local');
55
        
56
        $patchListAnalyser = new \Vaimo\ComposerPatches\Patch\DefinitionList\Analyser();
57
        
58
        $pluginConfig = array(
59
            Config::PATCHER_SOURCES => $this->createSourcesEnablerConfig($localOnly)
60
        );
61
62
        $configFactory = new \Vaimo\ComposerPatches\Factories\ConfigFactory($composer, array(
63
            Config::PATCHER_FROM_SOURCE => (bool)$input->getOption('from-source')
64
        ));
65
        
66
        $repository = $composer->getRepositoryManager()->getLocalRepository();
67
68
        $pluginConfig = $configFactory->create(array($pluginConfig));
69
        
70
        $patchesLoader = $this->createPatchesLoader($pluginConfig);
71
72
        $patches = $patchesLoader->loadFromPackagesRepository($repository);
73
        
74
        $patchPaths = $patchListAnalyser->extractValue($patches, array(Patch::PATH, Patch::SOURCE));
75
        
76
        $patchDefines = array_combine(
77
            $patchPaths,
78
            $patchListAnalyser->extractDictionary($patches, array(Patch::OWNER, Patch::URL))
79
        );
80
        
81
        $patchStatuses = array_filter(
82
            array_combine(
83
                $patchPaths,
84
                $patchListAnalyser->extractValue($patches, array(Patch::STATUS_LABEL))
85
            ) ?: array()
86
        );
87
        
88
        $matches = $this->resolveValidationTargets($repository, $pluginConfig);
89
90
        $installPaths = $this->collectInstallPaths($matches);
91
        
92
        $fileMatches = $this->collectPatchFilesFromPackages($matches, $pluginConfig);
93
        
94
        $groups = $this->collectOrphans($fileMatches, $patchDefines, $installPaths, $patchStatuses);
0 ignored issues
show
It seems like $patchDefines can also be of type false; however, parameter $patches of Vaimo\ComposerPatches\Co...mmand::collectOrphans() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

94
        $groups = $this->collectOrphans($fileMatches, /** @scrutinizer ignore-type */ $patchDefines, $installPaths, $patchStatuses);
Loading history...
95
        
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 \Vaimo\ComposerPatches\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();
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\RootPackage
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();
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.+\.patch/i', rtrim($filter, '/'));
187
188
            $searchResult = $fileSystemUtils->collectPathsRecursively($installPath, $filter);
189
190
            $fileMatchGroups[] = array_fill_keys($searchResult, array(
191
                Patch::OWNER => $packageName,
192
                Patch::URL => ''
193
            ));
194
        }
195
196
        return array_reduce($fileMatchGroups, 'array_replace', array());
197
    }
198
    
199
    private function resolveValidationTargets(PackageRepository $repository, Config $pluginConfig)
200
    {
201
        $composer = $this->getComposer();
202
203
        $packageResolver = new \Vaimo\ComposerPatches\Composer\Plugin\PackageResolver(
204
            array($composer->getPackage())
205
        );
206
        
207
        $srcResolverFactory = new \Vaimo\ComposerPatches\Factories\SourcesResolverFactory($composer);
208
        $packageListUtils = new \Vaimo\ComposerPatches\Utils\PackageListUtils();
209
210
        $srcResolver = $srcResolverFactory->create($pluginConfig);
211
        
212
        $sources = $srcResolver->resolvePackages($repository);
213
214
        $repositoryUtils = new \Vaimo\ComposerPatches\Utils\RepositoryUtils();
215
216
        $pluginPackage = $packageResolver->resolveForNamespace($repository, __NAMESPACE__);
217
218
        $pluginName = $pluginPackage->getName();
219
220
        $pluginUsers = array_merge(
221
            $repositoryUtils->filterByDependency($repository, $pluginName),
222
            array($composer->getPackage())
223
        );
224
225
        return array_intersect_key(
226
            $packageListUtils->listToNameDictionary($sources),
227
            $packageListUtils->listToNameDictionary($pluginUsers)
228
        );
229
    }
230
    
231
    private function createPatchesLoader(\Vaimo\ComposerPatches\Config $pluginConfig)
232
    {
233
        $composer = $this->getComposer();
234
        
235
        $loaderFactory = new \Vaimo\ComposerPatches\Factories\PatchesLoaderFactory($composer);
236
237
        $loaderComponentsPool = $this->createLoaderPool(array(
238
            'constraints' => false,
239
            'platform' => false,
240
            'targets-resolver' => false,
241
            'local-exclude' => false,
242
            'root-patch' => false,
243
            'global-exclude' => false
244
        ));
245
246
        return $loaderFactory->create($loaderComponentsPool, $pluginConfig, true);
247
    }
248
249
    private function createLoaderPool(array $componentUpdates = array())
250
    {
251
        $composer = $this->getComposer();
252
        $appIO = $this->getIO();
253
254
        $componentPool = new ComponentPool($composer, $appIO, true);
255
256
        foreach ($componentUpdates as $componentName => $replacement) {
257
            $componentPool->registerComponent($componentName, $replacement);
258
        }
259
260
        return $componentPool;
261
    }
262
    
263
    private function collectOrphans(array $files, array $patches, array $paths, array $statuses)
264
    {
265
        $orphanFiles = array_diff_key($files, $patches);
266
        $orphanConfig = array_diff_key($patches, $files);
267
268
        /**
269
         * Make sure that downloaded patches are not perceived as missing files
270
         */
271
        $orphanConfig = array_diff_key(
272
            $orphanConfig,
273
            array_flip(
274
                array_filter(array_keys($orphanConfig), 'file_exists')
275
            )
276
        );
277
278
        $groups = array_fill_keys(array_keys($paths), array());
279
        
280
        foreach ($orphanFiles as $path => $config) {
281
            $ownerName = $config[Patch::OWNER];
282
            $installPath = $paths[$ownerName];
283
            
284
            $groups[$ownerName][] = array(
285
                'issue' => 'NO CONFIG',
286
                'path' => $config[Patch::URL] ?: PathUtils::reducePathLeft($path, $installPath)
287
            );
288
        }
289
        
290
        foreach ($orphanConfig as $path => $config) {
291
            $ownerName = $config[Patch::OWNER];
292
            $installPath = $paths[$ownerName];
293
            
294
            $groups[$ownerName][] = array(
295
                'issue' => isset($statuses[$path]) && $statuses[$path]
296
                    ? $statuses[$path]
297
                    : 'NO FILE',
298
                'path' => $config[Patch::URL] ?: PathUtils::reducePathLeft($path, $installPath)
299
            );
300
        }
301
302
        return array_filter($groups);
303
    }
304
305
    private function outputOrphans(OutputInterface $output, array $groups)
306
    {
307
        $lines = array();
308
        
309
        foreach ($groups as $packageName => $items) {
310
            $lines[] = sprintf('  - <info>%s</info>', $packageName);
311
312
            foreach ($items as $item) {
313
                $lines[] = sprintf(
314
                    '    ~ %s [<fg=red>%s</>]',
315
                    $item['path'],
316
                    $item['issue']
317
                );
318
            }
319
        }
320
        
321
        foreach ($lines as $line) {
322
            $output->writeln($line);
323
        }
324
    }
325
}
326