diff --git a/src/Internal/Silencer.php b/src/Internal/Silencer.php new file mode 100644 index 00000000000..715e8fcd5f7 --- /dev/null +++ b/src/Internal/Silencer.php @@ -0,0 +1,41 @@ + true); + + try { + return $callback(); + } finally { + restore_error_handler(); + } + } + +} diff --git a/src/Type/Php/OpenSslCipherMethodsProvider.php b/src/Type/Php/OpenSslCipherMethodsProvider.php index 056d9df9080..ccd6ab9401a 100644 --- a/src/Type/Php/OpenSslCipherMethodsProvider.php +++ b/src/Type/Php/OpenSslCipherMethodsProvider.php @@ -5,6 +5,7 @@ use PHPStan\Analyser\DependencyTracker; use PHPStan\Analyser\ResultCache\ResultCacheValueExtension; use PHPStan\DependencyInjection\AutowiredService; +use PHPStan\Internal\Silencer; use function array_filter; use function array_map; use function array_values; @@ -54,10 +55,14 @@ private function getSupportedCipherMethods(): array // openssl_get_cipher_methods() reports algorithms that are not actually // supported on PHP 8.0-8.4 due to https://github.com/php/php-src/issues/19994 // Filter by actually testing each algorithm with openssl_cipher_iv_length(). - $methods = array_values(array_filter( + // + // Probing an unsupported algorithm warns, and @ is not enough on its own: a user error + // handler that does not consult error_reporting() is still called for a suppressed + // diagnostic, and whatever installs one is out of our hands. See Silencer. + $methods = Silencer::call(static fn (): array => array_values(array_filter( openssl_get_cipher_methods(true), - static fn (string $algorithm): bool => @openssl_cipher_iv_length($algorithm) !== false, - )); + static fn (string $algorithm): bool => openssl_cipher_iv_length($algorithm) !== false, + ))); } $this->supportedCipherMethods = array_map('strtolower', $methods); diff --git a/tests/PHPStan/Type/Php/OpenSslCipherMethodsProviderTest.php b/tests/PHPStan/Type/Php/OpenSslCipherMethodsProviderTest.php index 94b9f17093c..f06573acecc 100644 --- a/tests/PHPStan/Type/Php/OpenSslCipherMethodsProviderTest.php +++ b/tests/PHPStan/Type/Php/OpenSslCipherMethodsProviderTest.php @@ -6,6 +6,8 @@ use PHPStan\Analyser\ScopeFactory; use PHPStan\Analyser\ValueDependencyCollector; use PHPStan\Testing\PHPStanTestCase; +use function restore_error_handler; +use function set_error_handler; class OpenSslCipherMethodsProviderTest extends PHPStanTestCase { @@ -44,6 +46,36 @@ public function testIsSupportedCipherMethodTracksTheCipher(): void ], $dependencies['dependents']['/project/src/Analysed.php']['analysis']); } + /** + * Reading the ciphers out of the runtime means probing each one, and on PHP 8.0-8.4 + * openssl_get_cipher_methods() reports algorithms openssl_cipher_iv_length() rejects with a + * warning (php/php-src#19994) - 40 of 248 on PHP 8.4.23. `@` does not settle that: a user error + * handler that does not consult error_reporting() is still called for a suppressed diagnostic. + * See phpstan/phpstan#15176. + * + * Vacuous on a PHP where nothing is rejected, which is why the count is not asserted - only that + * whatever the probe does stays inside it. + */ + public function testProbingTheRuntimeLeaksNoWarningThroughAnUnsuppressedHandler(): void + { + $leaked = []; + set_error_handler(static function (int $errno, string $errstr) use (&$leaked): bool { + // deliberately does not check error_reporting(), so the @ operator does not hide anything + $leaked[] = $errstr; + + return true; + }); + + try { + $value = (new OpenSslCipherMethodsProvider())->getValue('aes-128-cbc'); + } finally { + restore_error_handler(); + } + + $this->assertSame([], $leaked, 'Probing the runtime for supported ciphers must not emit warnings.'); + $this->assertContains($value, ['supported', 'unsupported']); + } + /** * @param list $supportedCipherMethods */