Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
59.58% covered (warning)
59.58%
143 / 240
41.67% covered (danger)
41.67%
5 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
InstalledJson
59.58% covered (warning)
59.58%
143 / 240
41.67% covered (danger)
41.67%
5 / 12
353.59
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 copyInstalledJson
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 getJsonFile
62.50% covered (warning)
62.50%
10 / 16
0.00% covered (danger)
0.00%
0 / 1
3.47
 updatePackagePaths
34.78% covered (danger)
34.78%
8 / 23
0.00% covered (danger)
0.00%
0 / 1
15.99
 pathExistsInPackage
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 removeMissingAutoloadKeyPaths
31.58% covered (danger)
31.58%
18 / 57
0.00% covered (danger)
0.00%
0 / 1
148.12
 removeMovedPackagesAutoloadKeyFromVendorDirInstalledJson
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 removeMovedPackagesAutoloadKeyFromTargetDirInstalledJson
52.94% covered (warning)
52.94%
9 / 17
0.00% covered (danger)
0.00%
0 / 1
9.75
 updateNamespaces
70.00% covered (warning)
70.00%
28 / 40
0.00% covered (danger)
0.00%
0 / 1
21.08
 cleanTargetDirInstalledJson
84.85% covered (warning)
84.85%
28 / 33
0.00% covered (danger)
0.00%
0 / 1
6.13
 cleanupVendorInstalledJson
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
2
 reindexPackagesList
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * Changes "install-path" to point to vendor-prefixed target directory.
4 *
5 * * create new vendor-prefixed/composer/installed.json file with copied packages
6 * * when delete is enabled, update package paths in the original vendor/composer/installed.json
7 * * when delete is enabled, remove dead entries in the original vendor/composer/installed.json
8 * * update psr-0 autoload keys to have matching classmap entries
9 *
10 * @see vendor/composer/installed.json
11 *
12 * TODO: when delete_vendor_files is used, the original directory still exists so the paths are not updated.
13 *
14 * @package brianhenryie/strauss
15 */
16
17namespace BrianHenryIE\Strauss\Pipeline\Cleanup;
18
19use BrianHenryIE\Strauss\Composer\DependenciesCollection;
20use BrianHenryIE\Strauss\Config\CleanupConfigInterface;
21use BrianHenryIE\Strauss\Helpers\Flysystem\FileSystem;
22use BrianHenryIE\Strauss\Types\DiscoveredSymbols;
23use Composer\Json\JsonFile;
24use Composer\Json\JsonValidationException;
25use Exception;
26use League\Flysystem\FilesystemException;
27use Psr\Log\LoggerAwareTrait;
28use Psr\Log\LoggerInterface;
29use Seld\JsonLint\ParsingException;
30
31/**
32 * @phpstan-type InstalledJsonPackageSourceArray array{type:string, url:string, reference:string}
33 * @phpstan-type InstalledJsonPackageDistArray array{type:string, url:string, reference:string, shasum:string}
34 * @phpstan-type InstalledJsonPackageAutoloadPsr0Array array<string,string|array<string>>
35 * @phpstan-type InstalledJsonPackageAutoloadPsr4Array array<string,string|array<string>>
36 * @phpstan-type InstalledJsonPackageAutoloadClassmapArray string[]
37 * @phpstan-type InstalledJsonPackageAutoloadFilesArray string[]
38 * @phpstan-type InstalledJsonPackageAutoloadArray array{"psr-4"?:InstalledJsonPackageAutoloadPsr4Array, classmap?:InstalledJsonPackageAutoloadClassmapArray, files?:InstalledJsonPackageAutoloadFilesArray, "psr-0"?:InstalledJsonPackageAutoloadPsr0Array}
39 * @phpstan-type InstalledJsonPackageAuthorArray array{name:string,email:string}
40 * @phpstan-type InstalledJsonPackageSupportArray array{issues:string, source:string}
41 *
42 * @phpstan-type InstalledJsonPackageArray array{name:string, version:string, version_normalized:string, source:InstalledJsonPackageSourceArray, dist:InstalledJsonPackageDistArray, require:array<string,string>, require-dev:array<string,string>, time:string, type:string, installation-source:string, autoload?:InstalledJsonPackageAutoloadArray, notification-url:string, license:array<string>, authors:array<InstalledJsonPackageAuthorArray>, description:string, homepage:string, keywords:array<string>, support:InstalledJsonPackageSupportArray, install-path:string}
43 *
44 * @phpstan-type InstalledJsonArray array{packages:array<InstalledJsonPackageArray>, dev?:bool, dev-package-names:array<string>}
45 */
46class InstalledJson
47{
48    use LoggerAwareTrait;
49
50    protected CleanupConfigInterface $config;
51
52    protected FileSystem $filesystem;
53
54    public function __construct(
55        CleanupConfigInterface $config,
56        FileSystem $filesystem,
57        LoggerInterface $logger
58    ) {
59        $this->config = $config;
60        $this->filesystem = $filesystem;
61
62        $this->setLogger($logger);
63    }
64
65    /**
66     * @throws FilesystemException
67     */
68    public function copyInstalledJson(): void
69    {
70        $this->logger->debug('InstalledJson::copyInstalledJson()');
71
72        $source = $this->config->getAbsoluteVendorDirectory() . '/composer/installed.json';
73        $target = $this->config->getAbsoluteTargetDirectory() . '/composer/installed.json';
74
75        $this->logger->info('Copying {sourcePath} to {targetPath}', [
76            'sourcePath' => $source,
77            'targetPath' => $target
78        ]);
79
80        $this->filesystem->copy(
81            $source,
82            $target
83        );
84
85        $this->logger->info('Copied {sourcePath} to {targetPath}', [
86            'sourcePath' => $source,
87            'targetPath' => $target
88        ]);
89
90        $this->logger->debug($this->filesystem->read($this->config->getAbsoluteTargetDirectory() . '/composer/installed.json'));
91    }
92
93    /**
94     * @throws JsonValidationException
95     * @throws ParsingException
96     * @throws Exception
97     */
98    protected function getJsonFile(string $vendorDir): JsonFile
99    {
100        $installedJsonFile = new JsonFile(
101            sprintf(
102                '%s/composer/installed.json',
103                $this->filesystem->makeAbsolute($vendorDir)
104            )
105        );
106        if (!$installedJsonFile->exists()) {
107            if (!$this->config->isDryRun()) {
108                $this->logger->error(
109                    'Expected {installedJsonFilePath} does not exist.',
110                    [ 'installedJsonFilePath' => $installedJsonFile->getPath() ]
111                );
112            }
113            throw new Exception('Expected installed.json does not exist: ' . $installedJsonFile->getPath());
114        }
115
116        $installedJsonFile->validateSchema(JsonFile::LAX_SCHEMA);
117
118        $this->logger->info('Loaded file: {installedJsonFilePath}', ['installedJsonFilePath' => $installedJsonFile->getPath()]);
119
120        return $installedJsonFile;
121    }
122
123    /**
124     * @param InstalledJsonArray $installedJsonArray
125     * @param DependenciesCollection $flatDependencyTree
126     * @param string[] $excludedPackageNames
127     * @return InstalledJsonArray
128     */
129    protected function updatePackagePaths(
130        array $installedJsonArray,
131        DependenciesCollection $flatDependencyTree,
132        string $path,
133        DiscoveredSymbols $discoveredSymbols,
134        array $excludedPackageNames = []
135    ): array {
136
137        foreach ($installedJsonArray['packages'] as $key => $package) {
138            if (in_array($package['name'], $excludedPackageNames, true)) {
139                unset($installedJsonArray['packages'][$key]);
140                continue;
141            }
142
143            // Skip packages that were never copied in the first place.
144            if (!in_array($package['name'], array_keys($flatDependencyTree->toArray()))) {
145                $this->logger->debug('Skipping package: ' . $package['name']);
146                continue;
147            }
148            $this->logger->info('Checking package: ' . $package['name']);
149
150            // `composer/` is here because the install-path is relative to the `vendor/composer` directory.
151            $packageDir = $path . '/composer/' . $package['install-path'];
152            if (!$this->filesystem->directoryExists($packageDir)) {
153                $this->logger->debug('Package directory does not exist at : ' . $packageDir);
154
155                $newInstallPath = $this->filesystem->normalizePath($path . '/composer/' . $package['install-path']);
156
157                if (!$this->filesystem->directoryExists($newInstallPath)) {
158                    unset($installedJsonArray['packages'][$key]);
159                    $this->logger->info('Package directory does not exist: ' . $newInstallPath);
160                    continue;
161                }
162
163                $newRelativePath = $this->filesystem->getRelativePath(
164                    $path . '/composer/',
165                    $newInstallPath
166                );
167
168                $installedJsonArray['packages'][$key]['install-path'] = $newRelativePath;
169            } else {
170                $this->logger->debug('Original package directory exists at : ' . $packageDir);
171            }
172        }
173        return $installedJsonArray;
174    }
175
176    /**
177     * @param InstalledJsonPackageArray $packageArray
178     * @throws FilesystemException
179     */
180    protected function pathExistsInPackage(string $vendorDir, array $packageArray, string $relativePath): bool
181    {
182        return $this->filesystem->exists(
183            $vendorDir . '/composer/' . $packageArray['install-path'] . '/' . $relativePath
184        );
185    }
186
187    /**
188     * Remove autoload key entries from `installed.json` whose file or directory does not exist after deleting.
189     *
190     * @param InstalledJsonArray $installedJsonArray
191     * @return InstalledJsonArray
192     * @throws FilesystemException
193     */
194    protected function removeMissingAutoloadKeyPaths(array $installedJsonArray, string $vendorDir, string $installedJsonPath, DiscoveredSymbols $discoveredSymbols): array
195    {
196        foreach ($installedJsonArray['packages'] as $packageIndex => $packageArray) {
197            if (!isset($packageArray['autoload'])) {
198                $this->logger->info(
199                    'Package {packageName} has no autoload key in {installedJsonPath}',
200                    ['packageName' => $packageArray['name'],'installedJsonPath'=>$installedJsonPath]
201                );
202                continue;
203            }
204            // delete_vendor_files
205            $path = $vendorDir . '/composer/' . $packageArray['install-path'];
206            $pathExists = $this->filesystem->directoryExists($path);
207            // delete_vendor_packages
208            if (!$pathExists) {
209                $this->logger->info(
210                    'Removing package autoload key from {installedJsonPath}: {packageName}',
211                    ['packageName' => $packageArray['name'],'installedJsonPath'=>$installedJsonPath]
212                );
213                $installedJsonArray['packages'][$packageIndex]['autoload'] = [];
214            }
215            foreach ($installedJsonArray['packages'][$packageIndex]['autoload'] ?? [] as $type => $autoload) {
216                switch ($type) {
217                    case 'files':
218                    case 'classmap':
219                        // Ensure we filter the current autoload bucket and keep only existing paths
220                        $filtered = array_filter(
221                            (array) $autoload,
222                            function ($relativePath) use ($vendorDir, $packageArray): bool {
223                                return is_string($relativePath) && $this->pathExistsInPackage($vendorDir, $packageArray, $relativePath);
224                            }
225                        );
226                        // Reindex to produce a clean list of strings
227                        $installedJsonArray['packages'][$packageIndex]['autoload'][$type] = array_values($filtered);
228                        break;
229                    case 'psr-0':
230                        // Intentionally fall through.
231                    case 'psr-4':
232                        foreach ($autoload as $namespace => $paths) {
233                            switch (true) {
234                                case is_array($paths):
235                                    // e.g. [ 'psr-4' => [ 'BrianHenryIE\Project' => ['src','lib] ] ]
236                                    $validPaths = [];
237                                    foreach ($paths as $path) {
238                                        if ($this->pathExistsInPackage($vendorDir, $packageArray, $path)) {
239                                            $validPaths[] = $path;
240                                        } else {
241                                            $this->logger->debug('Removing non-existent path from autoload: ' . $path);
242                                        }
243                                    }
244                                    if (!empty($validPaths)) {
245                                        $installedJsonArray['packages'][$packageIndex]['autoload'][$type][$namespace] = $validPaths;
246                                    } else {
247                                        $this->logger->debug('Removing autoload key: ' . $type);
248                                        unset($installedJsonArray['packages'][$packageIndex]['autoload'][$type][$namespace]);
249                                    }
250                                    break;
251                                case is_string($paths):
252                                    // e.g. [ 'psr-4' => [ 'BrianHenryIE\Project' => 'src' ] ]
253                                    if (!$this->pathExistsInPackage($vendorDir, $packageArray, $paths)) {
254                                        $this->logger->debug('Removing autoload key: ' . $type . ' for ' . $paths);
255                                        unset($installedJsonArray['packages'][$packageIndex]['autoload'][$type][$namespace]);
256                                    }
257                                    break;
258                                default:
259                                    $this->logger->warning('Unexpectedly got neither a string nor array for autoload key in installed.json: ' . $type . ' ' . json_encode($paths));
260                                    break;
261                            }
262                        }
263                        break;
264                    case 'exclude-from-classmap':
265                        break;
266                    default:
267                        $this->logger->warning(
268                            'Unexpected autoload type in {installedJsonPath}: {type}',
269                            ['installedJsonPath'=>$installedJsonPath,'type'=>$type]
270                        );
271                        break;
272                }
273            }
274        }
275        /** @var InstalledJsonArray $installedJsonArray */
276        $installedJsonArray = $installedJsonArray;
277        return $installedJsonArray;
278    }
279
280    /**
281     * Remove the autoload key for packages from `installed.json` whose target directory does not exist after deleting.
282     *
283     * E.g. after the file is copied to the target directory, this will remove dev dependencies and unmodified dependencies from the second installed.json
284     *
285     * @param InstalledJsonArray $installedJsonArray
286     * @param DependenciesCollection $flatDependencyTree
287     * @return InstalledJsonArray
288     */
289    protected function removeMovedPackagesAutoloadKeyFromVendorDirInstalledJson(array $installedJsonArray, DependenciesCollection $flatDependencyTree, string $installedJsonPath): array
290    {
291        /**
292         * @var int $key
293         * @var InstalledJsonPackageArray $packageArray
294         */
295        foreach ($installedJsonArray['packages'] as $key => $packageArray) {
296            $packageName = $packageArray['name'];
297            $package = $flatDependencyTree[$packageName] ?? null;
298            if (!$package) {
299                // Probably a dev dependency that we aren't tracking.
300                continue;
301            }
302
303            if ($package->didDelete()) {
304                $this->logger->info(
305                    'Removing deleted package autoload key from {installedJsonPath}: {packageName}',
306                    ['installedJsonPath' => $installedJsonPath, 'packageName' => $packageName]
307                );
308                $installedJsonArray['packages'][$key]['autoload'] = [];
309            }
310        }
311        return $installedJsonArray;
312    }
313
314    /**
315     * Remove the autoload key for packages from `vendor-prefixed/composer/installed.json` whose target directory does not exist in `vendor-prefixed`.
316     *
317     * E.g. after the file is copied to the target directory, this will remove dev dependencies and unmodified dependencies from the second installed.json
318     *
319     * @param InstalledJsonArray $installedJsonArray
320     * @param DependenciesCollection $flatDependencyTree
321     * @return InstalledJsonArray
322     */
323    protected function removeMovedPackagesAutoloadKeyFromTargetDirInstalledJson(array $installedJsonArray, DependenciesCollection $flatDependencyTree, string $installedJsonPath): array
324    {
325        /**
326         * @var int $key
327         * @var InstalledJsonPackageArray $packageArray
328         */
329        foreach ($installedJsonArray['packages'] as $key => $packageArray) {
330            $packageName = $packageArray['name'];
331
332            $remove = false;
333
334            if (!in_array($packageName, array_keys($flatDependencyTree->toArray()))) {
335                // If it's not a package we were ever considering copying, then we can remove it.
336                $remove = true;
337            } else {
338                $package = $flatDependencyTree[$packageName] ?? null;
339                if (!$package) {
340                    // Probably a dev dependency.
341                    continue;
342                }
343                if (!$package->didCopy()) {
344                    // If it was marked not to copy, then we know it's not in the vendor-prefixed directory, and we can remove it.
345                    $remove = true;
346                }
347            }
348
349            if ($remove) {
350                $this->logger->info(
351                    'Removing deleted package autoload key from {installedJsonPath}: {packageName}',
352                    ['installedJsonPath' => $installedJsonPath, 'packageName' => $packageName]
353                );
354                $installedJsonArray['packages'][$key]['autoload'] = [];
355            }
356        }
357        return $installedJsonArray;
358    }
359
360    /**
361     * @param InstalledJsonArray $installedJsonArray
362     * @return InstalledJsonArray
363     */
364    protected function updateNamespaces(array $installedJsonArray, DiscoveredSymbols $discoveredSymbols): array
365    {
366        $this->logger->debug('InstalledJson::updateNamespaces()');
367
368        $discoveredNamespaces = $discoveredSymbols->getNamespaces()->toArray();
369
370        foreach ($installedJsonArray['packages'] as $key => $package) {
371            if (!isset($package['autoload'])) {
372                // woocommerce/action-scheduler
373                $this->logger->info('Package has no autoload key: ' . $package['name'] . ' ' . $package['type']);
374                continue;
375            }
376
377            $autoload_key = $package['autoload'];
378            // TODO: This was added before good PSR-0 support, we just added PSR-0 to the classmap and it worked ok.
379            if (!isset($autoload_key['classmap'])) {
380                $autoload_key['classmap'] = [];
381            }
382            foreach ($autoload_key as $type => $autoload) {
383                switch ($type) {
384                    case 'psr-0':
385                        // Intentionally fall through
386                    case 'psr-4':
387                        /**
388                         * e.g.
389                         * * {"psr-4":{"Psr\\Log\\":"Psr\/Log\/"}}
390                         * * {"psr-4":{"":"src\/"}}
391                         * * {"psr-4":{"Symfony\\Polyfill\\Mbstring\\":""}}
392                         * * {"psr-4":{"Another\\Package\\":["src","includes"]}}
393                         * * {"psr-0":{"PayPal":"lib\/"}}
394                         */
395                        foreach ($autoload_key[$type] as $originalNamespace => $packageRelativeDirectory) {
396                            // Replace $originalNamespace with updated namespace
397
398                            // Just for dev â€“ find a package like this and write a test for it.
399                            // pear/pear-core-minimal
400                            if (empty($originalNamespace)) {
401                                // In the case of `nesbot/carbon`, it uses an empty namespace but the classes are in the `Carbon`
402                                // namespace, so using `override_autoload` should be a good solution if this proves to be an issue.
403                                // The package directory will be updated, so for whatever reason the original empty namespace
404                                // works, maybe the updated namespace will work too.
405                                $this->logger->warning('Empty namespace found in autoload. Behaviour is not fully documented: ' . $package['name']);
406                                continue;
407                            }
408
409                            $trimmedOriginalNamespace = trim($originalNamespace, '\\');
410
411                            $this->logger->info('Checking '.$type.' namespace: ' . $trimmedOriginalNamespace);
412
413                            if (isset($discoveredNamespaces[$trimmedOriginalNamespace])) {
414                                $namespaceSymbol = $discoveredNamespaces[$trimmedOriginalNamespace];
415                            } else {
416                                $this->logger->debug('Namespace not found in list of changes: ' . $trimmedOriginalNamespace);
417                                continue;
418                            }
419
420                            if ($trimmedOriginalNamespace === trim($namespaceSymbol->getLocalReplacement(), '\\')) {
421                                $this->logger->debug('Namespace is unchanged: ' . $trimmedOriginalNamespace);
422                                continue;
423                            }
424
425                            // Update the namespace if it has changed.
426                            $this->logger->info('Updating namespace: ' . $trimmedOriginalNamespace . ' => ' . $namespaceSymbol->getLocalReplacement());
427                            $newKey = str_replace($trimmedOriginalNamespace, $namespaceSymbol->getLocalReplacement(), $originalNamespace);
428                            // PSR-0 underscore convention (PEAR-style): packages where class names use underscores
429                            // as directory separators with no PHP namespace declarations have no source files on
430                            // the namespace symbol. Their installed.json key must use underscores to match.
431                            // TODO: if this fails, filter the source files' namespaces to ensure they are all global.
432                            if ($type === 'psr-0' && empty($namespaceSymbol->getSourceFiles())) {
433                                $newKey = str_replace('\\', '_', $newKey);
434                            }
435                            /** @phpstan-ignore offsetAccess.notFound */
436                            $autoload_key[$type][$newKey] = $autoload_key[$type][$originalNamespace];
437                            unset($autoload_key[$type][$originalNamespace]);
438                        }
439                        break;
440                    default:
441                        /**
442                         * `files`, `classmap`, `exclude-from-classmap`
443                         * These don't contain namespaces in the autoload key.
444                         * * {"classmap":["src\/"]}
445                         * * {"files":["src\/functions.php"]}
446                         * * {"exclude-from-classmap":["\/Tests\/"]}
447                         *
448                         * Custom autoloader types might.
449                         */
450                        if (!in_array($type, ['files', 'classmap', 'exclude-from-classmap'])) {
451                            $this->logger->warning('Unexpected autoloader type: {type} in {packageName}.', [
452                                'type' => $type, 'packageName' => $package['name']
453                            ]);
454                        }
455                        break;
456                }
457            }
458            $installedJsonArray['packages'][$key]['autoload'] = array_filter($autoload_key);
459        }
460
461        $this->logger->debug('Finished InstalledJson::updateNamespaces()');
462
463        return $installedJsonArray;
464    }
465
466    /**
467     * TODO: This runs twice but should only run once.
468     *
469     * @throws Exception
470     * @throws FilesystemException
471     */
472    public function cleanTargetDirInstalledJson(DependenciesCollection $flatDependencyTree, DiscoveredSymbols $discoveredSymbols): void
473    {
474        $this->logger->debug('InstalledJson::cleanTargetDirInstalledJson()');
475
476        $targetDir = $this->config->getAbsoluteTargetDirectory();
477        try {
478            $installedJsonFile = $this->getJsonFile($targetDir);
479        } catch (Exception $e) {
480            if ($this->config->isDryRun()) {
481                $installedJsonFile = $this->getJsonFile($this->config->getAbsoluteVendorDirectory());
482            } else {
483                throw $e;
484            }
485        }
486
487        /**
488         * @var InstalledJsonArray $installedJsonArray
489         */
490        $installedJsonArray = $installedJsonFile->read();
491
492//        $this->logger->debug(
493//            '{installedJsonFilePath} before: {installedJsonArray}',
494//            ['installedJsonFilePath' => $installedJsonFile->getPath(), 'installedJsonArray' => json_encode($installedJsonArray)]
495//        );
496
497        $installedJsonArray = $this->updatePackagePaths(
498            $installedJsonArray,
499            $flatDependencyTree,
500            $this->config->getAbsoluteTargetDirectory(),
501            $discoveredSymbols,
502            $this->config->getExcludePackagesFromCopy()
503        );
504
505        $installedJsonArray = $this->removeMissingAutoloadKeyPaths($installedJsonArray, $this->config->getAbsoluteTargetDirectory(), $installedJsonFile->getPath(), $discoveredSymbols);
506
507        $installedJsonArray = $this->removeMovedPackagesAutoloadKeyFromTargetDirInstalledJson(
508            $installedJsonArray,
509            $flatDependencyTree,
510            $installedJsonFile->getPath()
511        );
512
513        $installedJsonArray = $this->updateNamespaces($installedJsonArray, $discoveredSymbols);
514
515        foreach ($installedJsonArray['packages'] as $index => $package) {
516            if (!in_array($package['name'], array_keys($flatDependencyTree->toArray()))) {
517                unset($installedJsonArray['packages'][$index]);
518            }
519        }
520
521        $installedJsonArray = $this->reindexPackagesList($installedJsonArray);
522        $installedJsonArray['dev'] = false;
523        $installedJsonArray['dev-package-names'] = [];
524
525//        $this->logger->debug('Installed.json after: ' . json_encode($installedJsonArray));
526
527        $this->logger->info('Writing installed.json to ' . $targetDir);
528
529        if (!$this->config->isDryRun()) {
530            $installedJsonFile->write($installedJsonArray);
531        }
532
533        $this->logger->info('Installed.json written to ' . $targetDir);
534
535        $this->logger->debug('Finished InstalledJson::cleanTargetDirInstalledJson()');
536    }
537
538    /**
539     * Composer creates a file `vendor/composer/installed.json` which is used when running `composer dump-autoload`.
540     * When `delete-vendor-packages` or `delete-vendor-files` is true, files and directories which have been deleted
541     * must also be removed from `installed.json` or Composer will throw an error.
542     *
543     * @throws Exception
544     * @throws FilesystemException
545     */
546    public function cleanupVendorInstalledJson(DependenciesCollection $flatDependencyTree, DiscoveredSymbols $discoveredSymbols): void
547    {
548        $this->logger->debug('InstalledJson::cleanupVendorInstalledJson()');
549
550        $vendorDir = $this->config->getAbsoluteVendorDirectory();
551
552        $vendorInstalledJsonFile = $this->getJsonFile($vendorDir);
553
554        $this->logger->info('Cleaning up {installedJsonPath}', ['installedJsonPath' => $vendorInstalledJsonFile->getPath()]);
555
556        /**
557         * @var InstalledJsonArray $installedJsonArray
558         */
559        $installedJsonArray = $vendorInstalledJsonFile->read();
560
561        $installedJsonArray = $this->removeMissingAutoloadKeyPaths($installedJsonArray, $this->config->getAbsoluteVendorDirectory(), $vendorInstalledJsonFile->getPath(), $discoveredSymbols);
562
563        $installedJsonArray = $this->removeMovedPackagesAutoloadKeyFromVendorDirInstalledJson($installedJsonArray, $flatDependencyTree, $vendorInstalledJsonFile->getPath());
564
565        $installedJsonArray = $this->updatePackagePaths(
566            $installedJsonArray,
567            $flatDependencyTree,
568            $this->config->getAbsoluteVendorDirectory(),
569            $discoveredSymbols
570        );
571
572        // Only relevant when source = target.
573        $installedJsonArray = $this->updateNamespaces($installedJsonArray, $discoveredSymbols);
574
575        $installedJsonArray = $this->reindexPackagesList($installedJsonArray);
576
577        if (!$this->config->isDryRun()) {
578            $vendorInstalledJsonFile->write($installedJsonArray);
579        }
580
581        $this->logger->debug('Finished InstalledJson::cleanupVendorInstalledJson()');
582    }
583
584    /**
585     * @param InstalledJsonArray $installedJsonArray
586     * @return InstalledJsonArray
587     */
588    private function reindexPackagesList(array $installedJsonArray): array
589    {
590        $installedJsonArray['packages'] = array_values($installedJsonArray['packages']);
591
592        return $installedJsonArray;
593    }
594}