diff --git a/drupal-dev/composer-plugin/src/Plugin.php b/drupal-dev/composer-plugin/src/Plugin.php index a97d152..6b5e1fd 100644 --- a/drupal-dev/composer-plugin/src/Plugin.php +++ b/drupal-dev/composer-plugin/src/Plugin.php @@ -29,18 +29,31 @@ public function activate(Composer $composer, IOInterface $io): void $this->composer = $composer; $this->io = $io; - // Seed installer-paths from core's composer.json into the root package - // extra so the fallback in getInstallPath() works before the merge - // plugin has had a chance to merge core's extra. - $rootExtra = $composer->getPackage()->getExtra(); - if (empty($rootExtra['installer-paths'])) { - $coreFile = getcwd() . '/composer.json'; - if (file_exists($coreFile)) { - $coreConfig = json_decode(file_get_contents($coreFile), true); - if (!empty($coreConfig['extra']['installer-paths'])) { - $rootExtra['installer-paths'] = $coreConfig['extra']['installer-paths']; - $composer->getPackage()->setExtra($rootExtra); - } + // Seed installer-paths and config.platform from core's composer.json + // into the root package so the fallback in getInstallPath() works + // before the merge plugin has had a chance to merge core's extra, and + // so PHPStan sees the correct platform PHP version. + // + // config.platform must also be written to the root composer file on + // disk because PHPStan reads it directly from that file (via the + // $COMPOSER env var) in a separate process — the in-memory Config + // merge is not visible to it. + $coreFile = getcwd() . '/composer.json'; + if (file_exists($coreFile)) { + $coreConfig = json_decode(file_get_contents($coreFile), true); + + $rootExtra = $composer->getPackage()->getExtra(); + if (empty($rootExtra['installer-paths']) && !empty($coreConfig['extra']['installer-paths'])) { + $rootExtra['installer-paths'] = $coreConfig['extra']['installer-paths']; + $composer->getPackage()->setExtra($rootExtra); + } + + if (!empty($coreConfig['config']['platform'])) { + $composer->getConfig()->merge( + ['config' => ['platform' => $coreConfig['config']['platform']]], + $coreFile + ); + $this->syncPlatformToRootFile($coreConfig['config']['platform']); } } @@ -120,6 +133,44 @@ private function registerInstaller(): void $this->composer->getInstallationManager()->addInstaller($installer); } + /** + * Write config.platform into the root composer file (e.g. composer.local.json) + * so that tools like PHPStan, which read that file directly in a separate + * process, pick up the correct platform PHP version. + * + * Only writes when the value differs from what is already on disk to avoid + * unnecessary file churn. + * + * @param array $platform + */ + private function syncPlatformToRootFile(array $platform): void + { + $envComposer = getenv('COMPOSER'); + $rootFileName = is_string($envComposer) && $envComposer !== '' ? basename($envComposer) : 'composer.json'; + $rootFile = getcwd() . '/' . $rootFileName; + + // Only act when we're actually running under an overlay root file that + // is separate from the core composer.json we just read. + if ($rootFile === getcwd() . '/composer.json' || !file_exists($rootFile)) { + return; + } + + $rootData = json_decode(file_get_contents($rootFile), true); + if (!is_array($rootData)) { + return; + } + + if (($rootData['config']['platform'] ?? null) === $platform) { + return; + } + + $rootData['config']['platform'] = $platform; + file_put_contents( + $rootFile, + json_encode($rootData, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n" + ); + } + public function deactivate(Composer $composer, IOInterface $io): void { } diff --git a/tests/test.bats b/tests/test.bats index 3d2d7e4..e3d4eda 100644 --- a/tests/test.bats +++ b/tests/test.bats @@ -359,3 +359,21 @@ PY [ "${core_sha}" = "${overlay_sha}" ] fi } + +@test "config-platform" { + set -eu -o pipefail + addon_setup + + # The plugin must sync config.platform from composer.json into composer.local.json + # so that tools like PHPStan, which read the root composer file directly, pick + # up the correct platform PHP version instead of falling back to the runtime. + core_platform=$(python3 -c "import json; print(json.load(open('${TESTDIR}/composer.json'))['config']['platform']['php'])") + overlay_platform=$(python3 -c "import json; print(json.load(open('${TESTDIR}/composer.local.json'))['config']['platform']['php'])") + [ "${core_platform}" = "${overlay_platform}" ] + + # PHPStan must report the platform version, not the runtime version. + # -vvv emits "PHP version for analysis: X.Y (from config.platform.php in composer.json)" + run ddev exec vendor/bin/phpstan -vvv analyze --configuration=core/phpstan.neon.dist -- core/lib/Drupal/Core/Entity/EntityInterface.php + assert_success + assert_output --partial "from config.platform.php" +}