Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
59.92% covered (warning)
59.92%
145 / 242
36.36% covered (danger)
36.36%
4 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
InstalledJson
59.92% covered (warning)
59.92%
145 / 242
36.36% covered (danger)
36.36%
4 / 11
346.52
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
72.09% covered (warning)
72.09%
31 / 43
0.00% covered (danger)
0.00%
0 / 1
21.56
 cleanTargetDirInstalledJson
86.11% covered (warning)
86.11%
31 / 36
0.00% covered (danger)
0.00%
0 / 1
6.10
 cleanupVendorInstalledJson
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
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\ComposerPackage;
20use BrianHenryIE\Strauss\Config\CleanupConfigInterface;
21use BrianHenryIE\Strauss\Helpers\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 array<string,ComposerPackage> $flatDependencyTree
126     * @param string[] $excludedPackageNames
127     * @return InstalledJsonArray
128     */
129    protected function updatePackagePaths(array $installedJsonArray, array $flatDependencyTree, string $path, array $excludedPackageNames = []): array
130    {
131
132        foreach ($installedJsonArray['packages'] as $key => $package) {
133            if (in_array($package['name'], $excludedPackageNames, true)) {
134                unset($installedJsonArray['packages'][$key]);
135                continue;
136            }
137
138            // Skip packages that were never copied in the first place.
139            if (!in_array($package['name'], array_keys($flatDependencyTree))) {
140                $this->logger->debug('Skipping package: ' . $package['name']);
141                continue;
142            }
143            $this->logger->info('Checking package: ' . $package['name']);
144
145            // `composer/` is here because the install-path is relative to the `vendor/composer` directory.
146            $packageDir = $path . '/composer/' . $package['install-path'];
147            if (!$this->filesystem->directoryExists($packageDir)) {
148                $this->logger->debug('Package directory does not exist at : ' . $packageDir);
149
150                $newInstallPath = $this->filesystem->normalizePath($path . '/composer/' . $package['install-path']);
151
152                if (!$this->filesystem->directoryExists($newInstallPath)) {
153                    unset($installedJsonArray['packages'][$key]);
154                    $this->logger->info('Package directory does not exist: ' . $newInstallPath);
155                    continue;
156                }
157
158                $newRelativePath = $this->filesystem->getRelativePath(
159                    $path . '/composer/',
160                    $newInstallPath
161                );
162
163                $installedJsonArray['packages'][$key]['install-path'] = $newRelativePath;
164            } else {
165                $this->logger->debug('Original package directory exists at : ' . $packageDir);
166            }
167        }
168        return $installedJsonArray;
169    }
170
171    /**
172     * @param InstalledJsonPackageArray $packageArray
173     * @throws FilesystemException
174     */
175    protected function pathExistsInPackage(string $vendorDir, array $packageArray, string $relativePath): bool
176    {
177        return $this->filesystem->exists(
178            $vendorDir . '/composer/' . $packageArray['install-path'] . '/' . $relativePath
179        );
180    }
181
182    /**
183     * Remove autoload key entries from `installed.json` whose file or directory does not exist after deleting.
184     *
185     * @param InstalledJsonArray $installedJsonArray
186     * @return InstalledJsonArray
187     * @throws FilesystemException
188     */
189    protected function removeMissingAutoloadKeyPaths(array $installedJsonArray, string $vendorDir, string $installedJsonPath): array
190    {
191        foreach ($installedJsonArray['packages'] as $packageIndex => $packageArray) {
192            if (!isset($packageArray['autoload'])) {
193                $this->logger->info(
194                    'Package {packageName} has no autoload key in {installedJsonPath}',
195                    ['packageName' => $packageArray['name'],'installedJsonPath'=>$installedJsonPath]
196                );
197                continue;
198            }
199            // delete_vendor_files
200            $path = $vendorDir . '/composer/' . $packageArray['install-path'];
201            $pathExists = $this->filesystem->directoryExists($path);
202            // delete_vendor_packages
203            if (!$pathExists) {
204                $this->logger->info(
205                    'Removing package autoload key from {installedJsonPath}: {packageName}',
206                    ['packageName' => $packageArray['name'],'installedJsonPath'=>$installedJsonPath]
207                );
208                $installedJsonArray['packages'][$packageIndex]['autoload'] = [];
209            }
210            foreach ($installedJsonArray['packages'][$packageIndex]['autoload'] ?? [] as $type => $autoload) {
211                switch ($type) {
212                    case 'files':
213                    case 'classmap':
214                        // Ensure we filter the current autoload bucket and keep only existing paths
215                        $filtered = array_filter(
216                            (array) $autoload,
217                            function ($relativePath) use ($vendorDir, $packageArray): bool {
218                                return is_string($relativePath) && $this->pathExistsInPackage($vendorDir, $packageArray, $relativePath);
219                            }
220                        );
221                        // Reindex to produce a clean list of strings
222                        $installedJsonArray['packages'][$packageIndex]['autoload'][$type] = array_values($filtered);
223                        break;
224                    case 'psr-0':
225                    case 'psr-4':
226                        foreach ($autoload as $namespace => $paths) {
227                            switch (true) {
228                                case is_array($paths):
229                                    // e.g. [ 'psr-4' => [ 'BrianHenryIE\Project' => ['src','lib] ] ]
230                                    $validPaths = [];
231                                    foreach ($paths as $path) {
232                                        if ($this->pathExistsInPackage($vendorDir, $packageArray, $path)) {
233                                            $validPaths[] = $path;
234                                        } else {
235                                            $this->logger->debug('Removing non-existent path from autoload: ' . $path);
236                                        }
237                                    }
238                                    if (!empty($validPaths)) {
239                                        $installedJsonArray['packages'][$packageIndex]['autoload'][$type][$namespace] = $validPaths;
240                                    } else {
241                                        $this->logger->debug('Removing autoload key: ' . $type);
242                                        unset($installedJsonArray['packages'][$packageIndex]['autoload'][$type][$namespace]);
243                                    }
244                                    break;
245                                case is_string($paths):
246                                    // e.g. [ 'psr-4' => [ 'BrianHenryIE\Project' => 'src' ] ]
247                                    if (!$this->pathExistsInPackage($vendorDir, $packageArray, $paths)) {
248                                        $this->logger->debug('Removing autoload key: ' . $type . ' for ' . $paths);
249                                        unset($installedJsonArray['packages'][$packageIndex]['autoload'][$type][$namespace]);
250                                    }
251                                    break;
252                                default:
253                                    $this->logger->warning('Unexpectedly got neither a string nor array for autoload key in installed.json: ' . $type . ' ' . json_encode($paths));
254                                    break;
255                            }
256                        }
257                        break;
258                    case 'exclude-from-classmap':
259                        break;
260                    default:
261                        $this->logger->warning(
262                            'Unexpected autoload type in {installedJsonPath}: {type}',
263                            ['installedJsonPath'=>$installedJsonPath,'type'=>$type]
264                        );
265                        break;
266                }
267            }
268        }
269        /** @var InstalledJsonArray $installedJsonArray */
270        $installedJsonArray = $installedJsonArray;
271        return $installedJsonArray;
272    }
273
274    /**
275     * Remove the autoload key for packages from `installed.json` whose target directory does not exist after deleting.
276     *
277     * E.g. after the file is copied to the target directory, this will remove dev dependencies and unmodified dependencies from the second installed.json
278     *
279     * @param InstalledJsonArray $installedJsonArray
280     * @param array<string,ComposerPackage> $flatDependencyTree
281     * @return InstalledJsonArray
282     */
283    protected function removeMovedPackagesAutoloadKeyFromVendorDirInstalledJson(array $installedJsonArray, array $flatDependencyTree, string $installedJsonPath): array
284    {
285        /**
286         * @var int $key
287         * @var InstalledJsonPackageArray $packageArray
288         */
289        foreach ($installedJsonArray['packages'] as $key => $packageArray) {
290            $packageName = $packageArray['name'];
291            $package = $flatDependencyTree[$packageName] ?? null;
292            if (!$package) {
293                // Probably a dev dependency that we aren't tracking.
294                continue;
295            }
296
297            if ($package->didDelete()) {
298                $this->logger->info(
299                    'Removing deleted package autoload key from {installedJsonPath}: {packageName}',
300                    ['installedJsonPath' => $installedJsonPath, 'packageName' => $packageName]
301                );
302                $installedJsonArray['packages'][$key]['autoload'] = [];
303            }
304        }
305        return $installedJsonArray;
306    }
307
308    /**
309     * Remove the autoload key for packages from `vendor-prefixed/composer/installed.json` whose target directory does not exist in `vendor-prefixed`.
310     *
311     * E.g. after the file is copied to the target directory, this will remove dev dependencies and unmodified dependencies from the second installed.json
312     *
313     * @param InstalledJsonArray $installedJsonArray
314     * @param array<string,ComposerPackage> $flatDependencyTree
315     * @return InstalledJsonArray
316     */
317    protected function removeMovedPackagesAutoloadKeyFromTargetDirInstalledJson(array $installedJsonArray, array $flatDependencyTree, string $installedJsonPath): array
318    {
319        /**
320         * @var int $key
321         * @var InstalledJsonPackageArray $packageArray
322         */
323        foreach ($installedJsonArray['packages'] as $key => $packageArray) {
324            $packageName = $packageArray['name'];
325
326            $remove = false;
327
328            if (!in_array($packageName, array_keys($flatDependencyTree))) {
329                // If it's not a package we were ever considering copying, then we can remove it.
330                $remove = true;
331            } else {
332                $package = $flatDependencyTree[$packageName] ?? null;
333                if (!$package) {
334                    // Probably a dev dependency.
335                    continue;
336                }
337                if (!$package->didCopy()) {
338                    // If it was marked not to copy, then we know it's not in the vendor-prefixed directory, and we can remove it.
339                    $remove = true;
340                }
341            }
342
343            if ($remove) {
344                $this->logger->info(
345                    'Removing deleted package autoload key from {installedJsonPath}: {packageName}',
346                    ['installedJsonPath' => $installedJsonPath, 'packageName' => $packageName]
347                );
348                $installedJsonArray['packages'][$key]['autoload'] = [];
349            }
350        }
351        return $installedJsonArray;
352    }
353
354    /**
355     * @param InstalledJsonArray $installedJsonArray
356     * @return InstalledJsonArray
357     */
358    protected function updateNamespaces(array $installedJsonArray, DiscoveredSymbols $discoveredSymbols): array
359    {
360        $this->logger->debug('InstalledJson::updateNamespaces()');
361
362        $discoveredNamespaces = $discoveredSymbols->getNamespaces();
363
364        foreach ($installedJsonArray['packages'] as $key => $package) {
365            if (!isset($package['autoload'])) {
366                // woocommerce/action-scheduler
367                $this->logger->info('Package has no autoload key: ' . $package['name'] . ' ' . $package['type']);
368                continue;
369            }
370
371            $autoload_key = $package['autoload'];
372            if (!isset($autoload_key['classmap'])) {
373                $autoload_key['classmap'] = [];
374            }
375            foreach ($autoload_key as $type => $autoload) {
376                switch ($type) {
377                    case 'psr-0':
378                        /** @var string $relativePath */
379                        foreach (array_values((array) $autoload_key[$type]) as $relativePath) {
380                            $packageRelativePath = $package['install-path'];
381                            if (1 === preg_match('#.*'.preg_quote($this->config->getAbsoluteTargetDirectory(), '#').'/(.*)#', $packageRelativePath, $matches)) {
382                                $packageRelativePath = $matches[1];
383                            }
384                            // Convert psr-0 autoloading to classmap autoloading
385                            if ($this->filesystem->directoryExists($this->config->getAbsoluteTargetDirectory() . '/composer/' . $packageRelativePath . $relativePath)) {
386                                $autoload_key['classmap'][] = $relativePath;
387                            }
388                        }
389                        // Intentionally fall through
390                        // Although the PSR-0 implementation here is a bit of a hack.
391                    case 'psr-4':
392                        /**
393                         * e.g.
394                         * * {"psr-4":{"Psr\\Log\\":"Psr\/Log\/"}}
395                         * * {"psr-4":{"":"src\/"}}
396                         * * {"psr-4":{"Symfony\\Polyfill\\Mbstring\\":""}}
397                         * * {"psr-4":{"Another\\Package\\":["src","includes"]}}
398                         * * {"psr-0":{"PayPal":"lib\/"}}
399                         */
400                        foreach ($autoload_key[$type] ?? [] as $originalNamespace => $packageRelativeDirectory) {
401                            // Replace $originalNamespace with updated namespace
402
403                            // Just for dev â€“ find a package like this and write a test for it.
404                            if (empty($originalNamespace)) {
405                                // In the case of `nesbot/carbon`, it uses an empty namespace but the classes are in the `Carbon`
406                                // namespace, so using `override_autoload` should be a good solution if this proves to be an issue.
407                                // The package directory will be updated, so for whatever reason the original empty namespace
408                                // works, maybe the updated namespace will work too.
409                                $this->logger->warning('Empty namespace found in autoload. Behaviour is not fully documented: ' . $package['name']);
410                                continue;
411                            }
412
413                            $trimmedOriginalNamespace = trim($originalNamespace, '\\');
414
415                            $this->logger->info('Checking '.$type.' namespace: ' . $trimmedOriginalNamespace);
416
417                            if (isset($discoveredNamespaces[$trimmedOriginalNamespace])) {
418                                $namespaceSymbol = $discoveredNamespaces[$trimmedOriginalNamespace];
419                            } else {
420                                $this->logger->debug('Namespace not found in list of changes: ' . $trimmedOriginalNamespace);
421                                continue;
422                            }
423
424                            if ($trimmedOriginalNamespace === trim($namespaceSymbol->getReplacement(), '\\')) {
425                                $this->logger->debug('Namespace is unchanged: ' . $trimmedOriginalNamespace);
426                                continue;
427                            }
428
429                            // Update the namespace if it has changed.
430                            $this->logger->info('Updating namespace: ' . $trimmedOriginalNamespace . ' => ' . $namespaceSymbol->getReplacement());
431                            /** @phpstan-ignore offsetAccess.notFound */
432                            $autoload_key[$type][str_replace($trimmedOriginalNamespace, $namespaceSymbol->getReplacement(), $originalNamespace)] = $autoload_key[$type][$originalNamespace];
433                            unset($autoload_key[$type][$originalNamespace]);
434                        }
435                        break;
436                    default:
437                        /**
438                         * `files`, `classmap`, `exclude-from-classmap`
439                         * These don't contain namespaces in the autoload key.
440                         * * {"classmap":["src\/"]}
441                         * * {"files":["src\/functions.php"]}
442                         * * {"exclude-from-classmap":["\/Tests\/"]}
443                         *
444                         * Custom autoloader types might.
445                         */
446                        if (!in_array($type, ['files', 'classmap', 'exclude-from-classmap'])) {
447                            $this->logger->warning('Unexpected autoloader type: {type} in {packageName}.', [
448                                'type' => $type, 'packageName' => $package['name']
449                            ]);
450                        }
451                        break;
452                }
453            }
454            $installedJsonArray['packages'][$key]['autoload'] = array_filter($autoload_key);
455        }
456
457        $this->logger->debug('Finished InstalledJson::updateNamespaces()');
458
459        return $installedJsonArray;
460    }
461
462    /**
463     * @param array<string,ComposerPackage> $flatDependencyTree
464     * @param DiscoveredSymbols $discoveredSymbols
465     * @throws Exception
466     * @throws FilesystemException
467     */
468    public function cleanTargetDirInstalledJson(array $flatDependencyTree, DiscoveredSymbols $discoveredSymbols): void
469    {
470        $this->logger->debug('InstalledJson::cleanTargetDirInstalledJson()');
471
472        $targetDir = $this->config->getAbsoluteTargetDirectory();
473        try {
474            $installedJsonFile = $this->getJsonFile($targetDir);
475        } catch (Exception $e) {
476            if ($this->config->isDryRun()) {
477                $installedJsonFile = $this->getJsonFile($this->config->getAbsoluteVendorDirectory());
478            } else {
479                throw $e;
480            }
481        }
482
483        /**
484         * @var InstalledJsonArray $installedJsonArray
485         */
486        $installedJsonArray = $installedJsonFile->read();
487
488        $this->logger->debug(
489            '{installedJsonFilePath} before: {installedJsonArray}',
490            ['installedJsonFilePath' => $installedJsonFile->getPath(), 'installedJsonArray' => json_encode($installedJsonArray)]
491        );
492
493        $installedJsonArray = $this->updatePackagePaths(
494            $installedJsonArray,
495            $flatDependencyTree,
496            $this->config->getAbsoluteTargetDirectory(),
497            $this->config->getExcludePackagesFromCopy()
498        );
499
500        $installedJsonArray = $this->removeMissingAutoloadKeyPaths($installedJsonArray, $this->config->getAbsoluteTargetDirectory(), $installedJsonFile->getPath());
501
502        $installedJsonArray = $this->removeMovedPackagesAutoloadKeyFromTargetDirInstalledJson(
503            $installedJsonArray,
504            $flatDependencyTree,
505            $installedJsonFile->getPath()
506        );
507
508        $installedJsonArray = $this->updateNamespaces($installedJsonArray, $discoveredSymbols);
509
510        foreach ($installedJsonArray['packages'] as $index => $package) {
511            if (!in_array($package['name'], array_keys($flatDependencyTree))) {
512                unset($installedJsonArray['packages'][$index]);
513            }
514        }
515
516        $installedJsonArray['dev'] = false;
517        $installedJsonArray['dev-package-names'] = [];
518
519        $this->logger->debug('Installed.json after: ' . json_encode($installedJsonArray));
520
521        $this->logger->info('Writing installed.json to ' . $targetDir);
522
523        if (!$this->config->isDryRun()) {
524            $installedJsonFile->write($installedJsonArray);
525        }
526
527        $this->logger->info('Installed.json written to ' . $targetDir);
528
529        $this->logger->debug('Finished InstalledJson::cleanTargetDirInstalledJson()');
530    }
531
532    /**
533     * Composer creates a file `vendor/composer/installed.json` which is used when running `composer dump-autoload`.
534     * When `delete-vendor-packages` or `delete-vendor-files` is true, files and directories which have been deleted
535     * must also be removed from `installed.json` or Composer will throw an error.
536     *
537     * @param array<string,ComposerPackage> $flatDependencyTree
538     * @throws Exception
539     * @throws FilesystemException
540     */
541    public function cleanupVendorInstalledJson(array $flatDependencyTree, DiscoveredSymbols $discoveredSymbols): void
542    {
543        $this->logger->debug('InstalledJson::cleanupVendorInstalledJson()');
544
545        $vendorDir = $this->config->getAbsoluteVendorDirectory();
546
547        $vendorInstalledJsonFile = $this->getJsonFile($vendorDir);
548
549        $this->logger->info('Cleaning up {installedJsonPath}', ['installedJsonPath' => $vendorInstalledJsonFile->getPath()]);
550
551        /**
552         * @var InstalledJsonArray $installedJsonArray
553         */
554        $installedJsonArray = $vendorInstalledJsonFile->read();
555
556        $installedJsonArray = $this->removeMissingAutoloadKeyPaths($installedJsonArray, $this->config->getAbsoluteVendorDirectory(), $vendorInstalledJsonFile->getPath());
557
558        $installedJsonArray = $this->removeMovedPackagesAutoloadKeyFromVendorDirInstalledJson($installedJsonArray, $flatDependencyTree, $vendorInstalledJsonFile->getPath());
559
560        $installedJsonArray = $this->updatePackagePaths(
561            $installedJsonArray,
562            $flatDependencyTree,
563            $this->config->getAbsoluteVendorDirectory()
564        );
565
566        // Only relevant when source = target.
567        $installedJsonArray = $this->updateNamespaces($installedJsonArray, $discoveredSymbols);
568
569        if (!$this->config->isDryRun()) {
570            $vendorInstalledJsonFile->write($installedJsonArray);
571        }
572
573        $this->logger->debug('Finished InstalledJson::cleanupVendorInstalledJson()');
574    }
575}