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