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