* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Symfony\Bridge\PhpUnit; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Configuration; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Deprecation; /** * Catch deprecation notices and print a summary report at the end of the test suite. * * @author Nicolas Grekas */ class DeprecationErrorHandler { /** * @deprecated since Symfony 4.3, use max[self]=0 instead */ const MODE_WEAK_VENDORS = 'weak_vendors'; const MODE_DISABLED = 'disabled'; const MODE_WEAK = 'max[total]=999999&verbose=0'; const MODE_STRICT = 'max[total]=0'; private $mode; private $configuration; private $deprecations = [ 'unsilencedCount' => 0, 'remaining selfCount' => 0, 'legacyCount' => 0, 'otherCount' => 0, 'remaining directCount' => 0, 'remaining indirectCount' => 0, 'unsilenced' => [], 'remaining self' => [], 'legacy' => [], 'other' => [], 'remaining direct' => [], 'remaining indirect' => [], ]; private static $isRegistered = false; private static $utilPrefix; /** * Registers and configures the deprecation handler. * * The mode is a query string with options: * - "disabled" to disable the deprecation handler * - "verbose" to enable/disable displaying the deprecation report * - "max" to configure the number of deprecations to allow before exiting with a non-zero * status code; it's an array with keys "total", "self", "direct" and "indirect" * * The default mode is "max[total]=0&verbose=1". * * The mode can alternatively be "/some-regexp/" to stop the test suite whenever * a deprecation message matches the given regular expression. * * @param int|string|false $mode The reporting mode, defaults to not allowing any deprecations */ public static function register($mode = 0) { if (self::$isRegistered) { return; } self::$utilPrefix = class_exists('PHPUnit_Util_ErrorHandler') ? 'PHPUnit_Util_' : 'PHPUnit\Util\\'; $handler = new self(); $oldErrorHandler = set_error_handler([$handler, 'handleError']); if (null !== $oldErrorHandler) { restore_error_handler(); if ([self::$utilPrefix.'ErrorHandler', 'handleError'] === $oldErrorHandler) { restore_error_handler(); self::register($mode); } } else { $handler->mode = $mode; self::$isRegistered = true; register_shutdown_function([$handler, 'shutdown']); } } public static function collectDeprecations($outputFile) { $deprecations = []; $previousErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$deprecations, &$previousErrorHandler) { if (E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) { if ($previousErrorHandler) { return $previousErrorHandler($type, $msg, $file, $line, $context); } static $autoload = true; $ErrorHandler = class_exists('PHPUnit_Util_ErrorHandler', $autoload) ? 'PHPUnit_Util_ErrorHandler' : 'PHPUnit\Util\ErrorHandler'; $autoload = false; return $ErrorHandler::handleError($type, $msg, $file, $line, $context); } $deprecations[] = [error_reporting(), $msg, $file]; }); register_shutdown_function(function () use ($outputFile, &$deprecations) { file_put_contents($outputFile, serialize($deprecations)); }); } /** * @internal */ public function handleError($type, $msg, $file, $line, $context = []) { if ((E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) || !$this->getConfiguration()->isEnabled()) { $ErrorHandler = self::$utilPrefix.'ErrorHandler'; return $ErrorHandler::handleError($type, $msg, $file, $line, $context); } $deprecation = new Deprecation($msg, debug_backtrace(), $file); $group = 'other'; if ($deprecation->originatesFromAnObject()) { $class = $deprecation->originatingClass(); $method = $deprecation->originatingMethod(); $msg = $deprecation->getMessage(); if (0 !== error_reporting()) { $group = 'unsilenced'; } elseif ($deprecation->isLegacy(self::$utilPrefix)) { $group = 'legacy'; } elseif (!$deprecation->isSelf()) { $group = $deprecation->isIndirect() ? 'remaining indirect' : 'remaining direct'; } else { $group = 'remaining self'; } if ($this->getConfiguration()->shouldDisplayStackTrace($msg)) { echo "\n".ucfirst($group).' '.$deprecation->toString(); exit(1); } if ('legacy' !== $group) { $ref = &$this->deprecations[$group][$msg]['count']; ++$ref; $ref = &$this->deprecations[$group][$msg][$class.'::'.$method]; ++$ref; } } else { $ref = &$this->deprecations[$group][$msg]['count']; ++$ref; } ++$this->deprecations[$group.'Count']; } /** * @internal */ public function shutdown() { $configuration = $this->getConfiguration(); if ($configuration->isInRegexMode()) { return; } $currErrorHandler = set_error_handler('var_dump'); restore_error_handler(); if ($currErrorHandler !== [$this, 'handleError']) { echo "\n", self::colorize('THE ERROR HANDLER HAS CHANGED!', true), "\n"; } $groups = ['unsilenced', 'remaining self', 'remaining direct', 'remaining indirect', 'legacy', 'other']; $this->displayDeprecations($groups, $configuration); // store failing status $isFailing = !$configuration->tolerates($this->deprecations); // reset deprecations array foreach ($this->deprecations as $group => $arrayOrInt) { $this->deprecations[$group] = \is_int($arrayOrInt) ? 0 : []; } register_shutdown_function(function () use ($isFailing, $groups, $configuration) { foreach ($this->deprecations as $group => $arrayOrInt) { if (0 < (\is_int($arrayOrInt) ? $arrayOrInt : \count($arrayOrInt))) { echo "Shutdown-time deprecations:\n"; break; } } $this->displayDeprecations($groups, $configuration); if ($isFailing || !$configuration->tolerates($this->deprecations)) { exit(1); } }); } private function getConfiguration() { if (null !== $this->configuration) { return $this->configuration; } if (false === $mode = $this->mode) { $mode = getenv('SYMFONY_DEPRECATIONS_HELPER'); } if ('strict' === $mode) { return $this->configuration = Configuration::inStrictMode(); } if (self::MODE_DISABLED === $mode) { return $this->configuration = Configuration::inDisabledMode(); } if ('weak' === $mode) { return $this->configuration = Configuration::inWeakMode(); } if (self::MODE_WEAK_VENDORS === $mode) { ++$this->deprecations['remaining directCount']; $msg = sprintf('Setting SYMFONY_DEPRECATIONS_HELPER to "%s" is deprecated in favor of "max[self]=0"', $mode); $ref = &$this->deprecations['remaining direct'][$msg]['count']; ++$ref; $mode = 'max[self]=0'; } if (isset($mode[0]) && '/' === $mode[0]) { return $this->configuration = Configuration::fromRegex($mode); } if (preg_match('/^[1-9][0-9]*$/', (string) $mode)) { return $this->configuration = Configuration::fromNumber($mode); } if (!$mode) { return $this->configuration = Configuration::fromNumber(0); } return $this->configuration = Configuration::fromUrlEncodedString((string) $mode); } /** * @param string $str * @param bool $red * * @return string */ private static function colorize($str, $red) { if (!self::hasColorSupport()) { return $str; } $color = $red ? '41;37' : '43;30'; return "\x1B[{$color}m{$str}\x1B[0m"; } /** * @param string[] $groups * @param Configuration $configuration */ private function displayDeprecations($groups, $configuration) { $cmp = function ($a, $b) { return $b['count'] - $a['count']; }; foreach ($groups as $group) { if ($this->deprecations[$group.'Count']) { echo "\n", self::colorize( sprintf('%s deprecation notices (%d)', ucfirst($group), $this->deprecations[$group.'Count']), 'legacy' !== $group && 'remaining indirect' !== $group ), "\n"; if (!$configuration->verboseOutput()) { continue; } uasort($this->deprecations[$group], $cmp); foreach ($this->deprecations[$group] as $msg => $notices) { echo "\n ", $notices['count'], 'x: ', $msg, "\n"; arsort($notices); foreach ($notices as $method => $count) { if ('count' !== $method) { echo ' ', $count, 'x in ', preg_replace('/(.*)\\\\(.*?::.*?)$/', '$2 from $1', $method), "\n"; } } } } } if (!empty($notices)) { echo "\n"; } } /** * Returns true if STDOUT is defined and supports colorization. * * Reference: Composer\XdebugHandler\Process::supportsColor * https://github.com/composer/xdebug-handler * * @return bool */ private static function hasColorSupport() { if (!\defined('STDOUT')) { return false; } if ('Hyper' === getenv('TERM_PROGRAM')) { return true; } if (\DIRECTORY_SEPARATOR === '\\') { return (\function_exists('sapi_windows_vt100_support') && sapi_windows_vt100_support(STDOUT)) || false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI') || 'xterm' === getenv('TERM'); } if (\function_exists('stream_isatty')) { return stream_isatty(STDOUT); } if (\function_exists('posix_isatty')) { return posix_isatty(STDOUT); } $stat = fstat(STDOUT); // Check if formatted mode is S_IFCHR return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; } }