DeprecationErrorHandler.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Symfony\Bridge\PhpUnit;
  11. use PHPUnit\Framework\TestResult;
  12. use PHPUnit\Util\ErrorHandler;
  13. use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Configuration;
  14. use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Deprecation;
  15. use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\DeprecationGroup;
  16. use Symfony\Component\ErrorHandler\DebugClassLoader;
  17. /**
  18. * Catch deprecation notices and print a summary report at the end of the test suite.
  19. *
  20. * @author Nicolas Grekas <p@tchwork.com>
  21. */
  22. class DeprecationErrorHandler
  23. {
  24. const MODE_DISABLED = 'disabled';
  25. const MODE_WEAK = 'max[total]=999999&verbose=0';
  26. const MODE_STRICT = 'max[total]=0';
  27. private $mode;
  28. private $configuration;
  29. /**
  30. * @var DeprecationGroup[]
  31. */
  32. private $deprecationGroups = [];
  33. private static $isRegistered = false;
  34. private static $isAtLeastPhpUnit83;
  35. public function __construct()
  36. {
  37. $this->resetDeprecationGroups();
  38. }
  39. /**
  40. * Registers and configures the deprecation handler.
  41. *
  42. * The mode is a query string with options:
  43. * - "disabled" to enable/disable the deprecation handler
  44. * - "verbose" to enable/disable displaying the deprecation report
  45. * - "quiet" to disable displaying the deprecation report only for some groups (i.e. quiet[]=other)
  46. * - "max" to configure the number of deprecations to allow before exiting with a non-zero
  47. * status code; it's an array with keys "total", "self", "direct" and "indirect"
  48. *
  49. * The default mode is "max[total]=0&verbose=1".
  50. *
  51. * The mode can alternatively be "/some-regexp/" to stop the test suite whenever
  52. * a deprecation message matches the given regular expression.
  53. *
  54. * @param int|string|false $mode The reporting mode, defaults to not allowing any deprecations
  55. */
  56. public static function register($mode = 0)
  57. {
  58. if (self::$isRegistered) {
  59. return;
  60. }
  61. $handler = new self();
  62. $oldErrorHandler = set_error_handler([$handler, 'handleError']);
  63. if (null !== $oldErrorHandler) {
  64. restore_error_handler();
  65. if ($oldErrorHandler instanceof ErrorHandler || [ErrorHandler::class, 'handleError'] === $oldErrorHandler) {
  66. restore_error_handler();
  67. self::register($mode);
  68. }
  69. } else {
  70. $handler->mode = $mode;
  71. self::$isRegistered = true;
  72. register_shutdown_function([$handler, 'shutdown']);
  73. }
  74. }
  75. public static function collectDeprecations($outputFile)
  76. {
  77. $deprecations = [];
  78. $previousErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$deprecations, &$previousErrorHandler) {
  79. if (\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type && (\E_WARNING !== $type || false === strpos($msg, '" targeting switch is equivalent to "break'))) {
  80. if ($previousErrorHandler) {
  81. return $previousErrorHandler($type, $msg, $file, $line, $context);
  82. }
  83. return \call_user_func(self::getPhpUnitErrorHandler(), $type, $msg, $file, $line, $context);
  84. }
  85. $filesStack = [];
  86. foreach (debug_backtrace() as $frame) {
  87. if (!isset($frame['file']) || \in_array($frame['function'], ['require', 'require_once', 'include', 'include_once'], true)) {
  88. continue;
  89. }
  90. $filesStack[] = $frame['file'];
  91. }
  92. $deprecations[] = [error_reporting() & $type, $msg, $file, $filesStack];
  93. return null;
  94. });
  95. register_shutdown_function(function () use ($outputFile, &$deprecations) {
  96. file_put_contents($outputFile, serialize($deprecations));
  97. });
  98. }
  99. /**
  100. * @internal
  101. */
  102. public function handleError($type, $msg, $file, $line, $context = [])
  103. {
  104. if ((\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type && (\E_WARNING !== $type || false === strpos($msg, '" targeting switch is equivalent to "break'))) || !$this->getConfiguration()->isEnabled()) {
  105. return \call_user_func(self::getPhpUnitErrorHandler(), $type, $msg, $file, $line, $context);
  106. }
  107. $trace = debug_backtrace();
  108. if (isset($trace[1]['function'], $trace[1]['args'][0]) && ('trigger_error' === $trace[1]['function'] || 'user_error' === $trace[1]['function'])) {
  109. $msg = $trace[1]['args'][0];
  110. }
  111. $deprecation = new Deprecation($msg, $trace, $file);
  112. if ($deprecation->isMuted()) {
  113. return null;
  114. }
  115. if ($this->getConfiguration()->isBaselineDeprecation($deprecation)) {
  116. return null;
  117. }
  118. $msg = $deprecation->getMessage();
  119. if (error_reporting() & $type) {
  120. $group = 'unsilenced';
  121. } elseif ($deprecation->isLegacy()) {
  122. $group = 'legacy';
  123. } else {
  124. $group = [
  125. Deprecation::TYPE_SELF => 'self',
  126. Deprecation::TYPE_DIRECT => 'direct',
  127. Deprecation::TYPE_INDIRECT => 'indirect',
  128. Deprecation::TYPE_UNDETERMINED => 'other',
  129. ][$deprecation->getType()];
  130. }
  131. if ($this->getConfiguration()->shouldDisplayStackTrace($msg)) {
  132. echo "\n".ucfirst($group).' '.$deprecation->toString();
  133. exit(1);
  134. }
  135. if ('legacy' === $group) {
  136. $this->deprecationGroups[$group]->addNotice();
  137. } else if ($deprecation->originatesFromAnObject()) {
  138. $class = $deprecation->originatingClass();
  139. $method = $deprecation->originatingMethod();
  140. $this->deprecationGroups[$group]->addNoticeFromObject($msg, $class, $method);
  141. } else {
  142. $this->deprecationGroups[$group]->addNoticeFromProceduralCode($msg);
  143. }
  144. return null;
  145. }
  146. /**
  147. * @internal
  148. */
  149. public function shutdown()
  150. {
  151. $configuration = $this->getConfiguration();
  152. if ($configuration->isInRegexMode()) {
  153. return;
  154. }
  155. if (class_exists(DebugClassLoader::class, false)) {
  156. DebugClassLoader::checkClasses();
  157. }
  158. $currErrorHandler = set_error_handler('var_dump');
  159. restore_error_handler();
  160. if ($currErrorHandler !== [$this, 'handleError']) {
  161. echo "\n", self::colorize('THE ERROR HANDLER HAS CHANGED!', true), "\n";
  162. }
  163. $groups = array_keys($this->deprecationGroups);
  164. // store failing status
  165. $isFailing = !$configuration->tolerates($this->deprecationGroups);
  166. $this->displayDeprecations($groups, $configuration, $isFailing);
  167. $this->resetDeprecationGroups();
  168. register_shutdown_function(function () use ($isFailing, $groups, $configuration) {
  169. foreach ($this->deprecationGroups as $group) {
  170. if ($group->count() > 0) {
  171. echo "Shutdown-time deprecations:\n";
  172. break;
  173. }
  174. }
  175. $isFailingAtShutdown = !$configuration->tolerates($this->deprecationGroups);
  176. $this->displayDeprecations($groups, $configuration, $isFailingAtShutdown);
  177. if ($configuration->isGeneratingBaseline()) {
  178. $configuration->writeBaseline();
  179. }
  180. if ($isFailing || $isFailingAtShutdown) {
  181. exit(1);
  182. }
  183. });
  184. }
  185. private function resetDeprecationGroups()
  186. {
  187. $this->deprecationGroups = [
  188. 'unsilenced' => new DeprecationGroup(),
  189. 'self' => new DeprecationGroup(),
  190. 'direct' => new DeprecationGroup(),
  191. 'indirect' => new DeprecationGroup(),
  192. 'legacy' => new DeprecationGroup(),
  193. 'other' => new DeprecationGroup(),
  194. ];
  195. }
  196. private function getConfiguration()
  197. {
  198. if (null !== $this->configuration) {
  199. return $this->configuration;
  200. }
  201. if (false === $mode = $this->mode) {
  202. if (isset($_SERVER['SYMFONY_DEPRECATIONS_HELPER'])) {
  203. $mode = $_SERVER['SYMFONY_DEPRECATIONS_HELPER'];
  204. } elseif (isset($_ENV['SYMFONY_DEPRECATIONS_HELPER'])) {
  205. $mode = $_ENV['SYMFONY_DEPRECATIONS_HELPER'];
  206. } else {
  207. $mode = getenv('SYMFONY_DEPRECATIONS_HELPER');
  208. }
  209. }
  210. if ('strict' === $mode) {
  211. return $this->configuration = Configuration::inStrictMode();
  212. }
  213. if (self::MODE_DISABLED === $mode) {
  214. return $this->configuration = Configuration::inDisabledMode();
  215. }
  216. if ('weak' === $mode) {
  217. return $this->configuration = Configuration::inWeakMode();
  218. }
  219. if (isset($mode[0]) && '/' === $mode[0]) {
  220. return $this->configuration = Configuration::fromRegex($mode);
  221. }
  222. if (preg_match('/^[1-9][0-9]*$/', (string) $mode)) {
  223. return $this->configuration = Configuration::fromNumber($mode);
  224. }
  225. if (!$mode) {
  226. return $this->configuration = Configuration::fromNumber(0);
  227. }
  228. return $this->configuration = Configuration::fromUrlEncodedString((string) $mode);
  229. }
  230. /**
  231. * @param string $str
  232. * @param bool $red
  233. *
  234. * @return string
  235. */
  236. private static function colorize($str, $red)
  237. {
  238. if (!self::hasColorSupport()) {
  239. return $str;
  240. }
  241. $color = $red ? '41;37' : '43;30';
  242. return "\x1B[{$color}m{$str}\x1B[0m";
  243. }
  244. /**
  245. * @param string[] $groups
  246. * @param Configuration $configuration
  247. * @param bool $isFailing
  248. */
  249. private function displayDeprecations($groups, $configuration, $isFailing)
  250. {
  251. $cmp = function ($a, $b) {
  252. return $b->count() - $a->count();
  253. };
  254. foreach ($groups as $group) {
  255. if ($this->deprecationGroups[$group]->count()) {
  256. echo "\n", self::colorize(
  257. sprintf(
  258. '%s deprecation notices (%d)',
  259. \in_array($group, ['direct', 'indirect', 'self'], true) ? "Remaining $group" : ucfirst($group),
  260. $this->deprecationGroups[$group]->count()
  261. ),
  262. 'legacy' !== $group && 'indirect' !== $group
  263. ), "\n";
  264. if ('legacy' !== $group && !$configuration->verboseOutput($group) && !$isFailing) {
  265. continue;
  266. }
  267. $notices = $this->deprecationGroups[$group]->notices();
  268. uasort($notices, $cmp);
  269. foreach ($notices as $msg => $notice) {
  270. echo "\n ", $notice->count(), 'x: ', $msg, "\n";
  271. $countsByCaller = $notice->getCountsByCaller();
  272. arsort($countsByCaller);
  273. foreach ($countsByCaller as $method => $count) {
  274. if ('count' !== $method) {
  275. echo ' ', $count, 'x in ', preg_replace('/(.*)\\\\(.*?::.*?)$/', '$2 from $1', $method), "\n";
  276. }
  277. }
  278. }
  279. }
  280. }
  281. if (!empty($notices)) {
  282. echo "\n";
  283. }
  284. }
  285. private static function getPhpUnitErrorHandler()
  286. {
  287. if (!isset(self::$isAtLeastPhpUnit83)) {
  288. self::$isAtLeastPhpUnit83 = class_exists(ErrorHandler::class) && method_exists(ErrorHandler::class, '__invoke');
  289. }
  290. if (!self::$isAtLeastPhpUnit83) {
  291. return 'PHPUnit\Util\ErrorHandler::handleError';
  292. }
  293. foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) {
  294. if (isset($frame['object']) && $frame['object'] instanceof TestResult) {
  295. return new ErrorHandler(
  296. $frame['object']->getConvertDeprecationsToExceptions(),
  297. $frame['object']->getConvertErrorsToExceptions(),
  298. $frame['object']->getConvertNoticesToExceptions(),
  299. $frame['object']->getConvertWarningsToExceptions()
  300. );
  301. }
  302. }
  303. return function () { return false; };
  304. }
  305. /**
  306. * Returns true if STDOUT is defined and supports colorization.
  307. *
  308. * Reference: Composer\XdebugHandler\Process::supportsColor
  309. * https://github.com/composer/xdebug-handler
  310. *
  311. * @return bool
  312. */
  313. private static function hasColorSupport()
  314. {
  315. if (!\defined('STDOUT')) {
  316. return false;
  317. }
  318. // Follow https://no-color.org/
  319. if (isset($_SERVER['NO_COLOR']) || false !== getenv('NO_COLOR')) {
  320. return false;
  321. }
  322. if ('Hyper' === getenv('TERM_PROGRAM')) {
  323. return true;
  324. }
  325. if (\DIRECTORY_SEPARATOR === '\\') {
  326. return (\function_exists('sapi_windows_vt100_support')
  327. && sapi_windows_vt100_support(\STDOUT))
  328. || false !== getenv('ANSICON')
  329. || 'ON' === getenv('ConEmuANSI')
  330. || 'xterm' === getenv('TERM');
  331. }
  332. if (\function_exists('stream_isatty')) {
  333. return stream_isatty(\STDOUT);
  334. }
  335. if (\function_exists('posix_isatty')) {
  336. return posix_isatty(\STDOUT);
  337. }
  338. $stat = fstat(\STDOUT);
  339. // Check if formatted mode is S_IFCHR
  340. return $stat ? 0020000 === ($stat['mode'] & 0170000) : false;
  341. }
  342. }