diff --git a/src/Command/AnalyseCommand.php b/src/Command/AnalyseCommand.php index bd073559b80..efd67ab2ef6 100644 --- a/src/Command/AnalyseCommand.php +++ b/src/Command/AnalyseCommand.php @@ -28,6 +28,7 @@ use PHPStan\Internal\ComposerHelper; use PHPStan\Internal\DirectoryCreator; use PHPStan\Internal\DirectoryCreatorException; +use PHPStan\Process\PcovHelper; use PHPStan\ShouldNotHappenException; use PHPStan\Turbo\TurboExtensionEnabler; use Symfony\Component\Console\Command\Command; @@ -86,6 +87,13 @@ final class AnalyseCommand extends Command */ private const RESULT_CACHE_CI_NOTIFICATION_ELAPSED_LIMIT = 60.0; + /** + * The pcov notification is only shown when the analysis took longer than this + * many seconds. Shorter runs do not waste enough time on pcov's call hook to + * be worth nagging about. + */ + private const PCOV_NOTIFICATION_ELAPSED_LIMIT = 10.0; + /** * @param string[] $composerAutoloaderProjectPaths */ @@ -676,6 +684,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $this->reportMissingResultCacheInCi($errorOutput, $analysisResult, $onlyFiles); + $this->reportPcovOverhead($errorOutput); $this->runDiagnoseExtensions($container, $inceptionResult->getErrorOutput(), $analysisResult->getProcessedFiles()); @@ -711,6 +720,23 @@ private function reportMissingResultCacheInCi(Output $errorOutput, AnalysisResul $errorOutput->writeLineFormatted(''); } + private function reportPcovOverhead(Output $errorOutput): void + { + if (!PcovHelper::isActive() || PcovHelper::isAllowed()) { + return; + } + + if (microtime(true) - $this->analysisStartTime < self::PCOV_NOTIFICATION_ELAPSED_LIMIT) { + return; + } + + $errorOutput->writeLineFormatted('Tip: The pcov extension is active, which makes this run slower than it needs to be.'); + $errorOutput->writeLineFormatted('pcov hooks into every function call in the process, even though PHPStan never collects code coverage.'); + $errorOutput->writeLineFormatted(sprintf('PHPStan disables it in its worker processes - run PHPStan with "php -d %s" to disable it in the main process too.', PcovHelper::DISABLED_INI_SETTING)); + $errorOutput->writeLineFormatted(sprintf('Set %s=1 if you need pcov to stay enabled.', PcovHelper::ALLOW_ENV_VARIABLE)); + $errorOutput->writeLineFormatted(''); + } + private function createStreamOutput(): StreamOutput { $resource = fopen('php://memory', 'w', false); diff --git a/src/Diagnose/PHPStanDiagnoseExtension.php b/src/Diagnose/PHPStanDiagnoseExtension.php index 3cc1e817551..9ebc1aa69f1 100644 --- a/src/Diagnose/PHPStanDiagnoseExtension.php +++ b/src/Diagnose/PHPStanDiagnoseExtension.php @@ -12,6 +12,7 @@ use PHPStan\Internal\ComposerHelper; use PHPStan\Php\ComposerPhpVersionFactory; use PHPStan\Php\PhpVersion; +use PHPStan\Process\PcovHelper; use ReflectionClass; use function array_count_values; use function array_key_exists; @@ -68,6 +69,13 @@ public function print(Output $output, array $processedFiles): void $phpRuntimeVersion->getVersionString(), )); + if (PcovHelper::isLoaded()) { + $output->writeLineFormatted(sprintf( + 'pcov extension: %s', + $this->describePcovStatus(), + )); + } + if ( $this->phpVersion->getSource() === PhpVersion::SOURCE_CONFIG && is_array($this->configPhpVersion) @@ -234,6 +242,26 @@ public function print(Output $output, array $processedFiles): void $output->writeLineFormatted(''); } + private function describePcovStatus(): string + { + $version = PcovHelper::getVersion() ?? 'unknown version'; + + if (PcovHelper::isAllowed()) { + return sprintf( + '%s, %s - kept everywhere because %s=1', + $version, + PcovHelper::isActive() ? 'active' : 'not active (pcov.enabled=0)', + PcovHelper::ALLOW_ENV_VARIABLE, + ); + } + + if (!PcovHelper::isActive()) { + return sprintf('%s, not active in this process (pcov.enabled=0), disabled in worker processes', $version); + } + + return sprintf('%s, active - it slows down every function call, disabled in worker processes', $version); + } + /** * @param list $processedFiles * @return array> diff --git a/src/Process/PcovHelper.php b/src/Process/PcovHelper.php new file mode 100644 index 00000000000..f260444766b --- /dev/null +++ b/src/Process/PcovHelper.php @@ -0,0 +1,65 @@ +assertTrue(PcovHelper::isLoaded()); + return; + } + + $this->assertFalse(PcovHelper::isLoaded()); + $this->assertFalse(PcovHelper::isActive()); + $this->assertFalse(PcovHelper::shouldDisableInSubProcesses()); + } + + public function testShouldNotDisableInSubProcessesWhenAllowed(): void + { + $originalValue = getenv(PcovHelper::ALLOW_ENV_VARIABLE); + putenv(sprintf('%s=1', PcovHelper::ALLOW_ENV_VARIABLE)); + + try { + $this->assertTrue(PcovHelper::isAllowed()); + $this->assertFalse(PcovHelper::shouldDisableInSubProcesses()); + } finally { + self::restoreAllowEnvVariable($originalValue); + } + } + + public function testShouldDisableInSubProcessesEvenWhenNotActiveInThisProcess(): void + { + if (!PcovHelper::isLoaded()) { + $this->markTestSkipped('pcov is not loaded in this process.'); + } + + $originalValue = getenv(PcovHelper::ALLOW_ENV_VARIABLE); + putenv(PcovHelper::ALLOW_ENV_VARIABLE); + + try { + $this->assertFalse(PcovHelper::isAllowed()); + // sub-processes read the php.ini, not this process' command line + $this->assertTrue(PcovHelper::shouldDisableInSubProcesses()); + $this->assertNotNull(PcovHelper::getVersion()); + } finally { + self::restoreAllowEnvVariable($originalValue); + } + } + + private static function restoreAllowEnvVariable(string|false $originalValue): void + { + putenv($originalValue === false + ? PcovHelper::ALLOW_ENV_VARIABLE + : sprintf('%s=%s', PcovHelper::ALLOW_ENV_VARIABLE, $originalValue)); + } + +} diff --git a/tests/PHPStan/Process/ProcessHelperTest.php b/tests/PHPStan/Process/ProcessHelperTest.php new file mode 100644 index 00000000000..f84960cc424 --- /dev/null +++ b/tests/PHPStan/Process/ProcessHelperTest.php @@ -0,0 +1,83 @@ +markTestSkipped('pcov is not loaded in this process.'); + } + + $this->assertStringContainsString( + sprintf('-d %s', PcovHelper::DISABLED_INI_SETTING), + self::getWorkerCommand(), + ); + } + + public function testWorkerCommandKeepsPcovWhenAllowed(): void + { + if (!PcovHelper::isLoaded()) { + $this->markTestSkipped('pcov is not loaded in this process.'); + } + + $originalValue = getenv(PcovHelper::ALLOW_ENV_VARIABLE); + putenv(sprintf('%s=1', PcovHelper::ALLOW_ENV_VARIABLE)); + + try { + $this->assertStringNotContainsString('pcov', self::getWorkerCommand()); + } finally { + putenv($originalValue === false + ? PcovHelper::ALLOW_ENV_VARIABLE + : sprintf('%s=%s', PcovHelper::ALLOW_ENV_VARIABLE, $originalValue)); + } + } + + public function testWorkerCommandDoesNotMentionPcovWhenItIsNotLoaded(): void + { + if (PcovHelper::isLoaded()) { + $this->markTestSkipped('pcov is loaded in this process.'); + } + + $this->assertStringNotContainsString('pcov', self::getWorkerCommand()); + } + + private static function getWorkerCommand(): string + { + return ProcessHelper::getWorkerCommand( + 'bin/phpstan', + 'worker', + null, + [], + self::createInput(), + ); + } + + private static function createInput(): InputInterface + { + return new ArrayInput(['paths' => ['src']], new InputDefinition([ + new InputArgument('paths', InputArgument::IS_ARRAY), + new InputOption(AnalyseCommand::OPTION_LEVEL, mode: InputOption::VALUE_REQUIRED), + new InputOption('autoload-file', mode: InputOption::VALUE_REQUIRED), + new InputOption('memory-limit', mode: InputOption::VALUE_REQUIRED), + new InputOption('xdebug', mode: InputOption::VALUE_NONE), + new InputOption('verbose', mode: InputOption::VALUE_NONE), + ])); + } + +}