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),
+ ]));
+ }
+
+}