Last active
May 6, 2026 13:21
-
-
Save paranoiq/d4a5c5396af2019cb367100e7b3dffbb to your computer and use it in GitHub Desktop.
PHPStan output tweaker - version, colors, better dividers
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?php | |
| // origin: https://gist.github.com/paranoiq/d4a5c5396af2019cb367100e7b3dffbb | |
| // Ensure output is treated as a TTY/ANSI capable stream | |
| // Usage: phpstan analyze --ansi | php phpstan-tweak.php | |
| $lw = "\e[97m"; | |
| $w = "\e[37m"; | |
| $lk = "\e[90m"; | |
| $k = "\e[30m"; | |
| $r = "\e[31m"; | |
| $lr = "\e[91m"; | |
| $g = "\e[32m"; | |
| $lg = "\e[92m"; | |
| $b = "\e[34m"; | |
| $lb = "\e[94m"; | |
| $c = "\e[36m"; | |
| $lc = "\e[96m"; | |
| $m = "\e[35m"; | |
| $lm = "\e[95m"; | |
| $y = "\e[33m"; | |
| $ly = "\e[93m"; | |
| $e = "\e[0m"; // end | |
| $width = static function (): int | |
| { | |
| static $col; | |
| if ($col !== null) { | |
| return $col; | |
| } | |
| $col = (int) @exec('tput cols'); | |
| if ($col !== 0) { | |
| return $col; | |
| } | |
| if (PHP_OS_FAMILY === 'Windows') { | |
| exec('mode CON', $output); | |
| [, $col] = explode(':', $output[4]); | |
| $col = (int) trim($col); | |
| } | |
| return $col = $col ?: 120; | |
| }; | |
| // PHPStan version | |
| $version = null; | |
| $lockFile = __DIR__ . '/../composer.lock'; | |
| if (file_exists($lockFile)) { | |
| $data = json_decode(file_get_contents($lockFile), true, 512, JSON_THROW_ON_ERROR); | |
| $packages = array_merge($data['packages'] ?? [], $data['packages-dev'] ?? []); | |
| foreach ($packages as $package) { | |
| if ($package['name'] === 'phpstan/phpstan') { | |
| $version = $package['version']; | |
| break; | |
| } | |
| } | |
| } | |
| echo " \e[32mPHPStan {$version}\e[39m on PHP " . PHP_VERSION . "\n\n"; | |
| while ($line = fgets(STDIN)) { | |
| // drop less useful tips | |
| $line = str_replace('Learn more: ', '', $line); | |
| $line = preg_replace('~https://phpstan.org/blog/remem[\w-]+~', '', $line); // may be split on two lines... | |
| if (preg_match("~s\e\[0m\s*$~", $line)) { // "...remembered-values" - drop next line TODO: not working | |
| continue; | |
| } | |
| if (str_contains($line, 'user-guide/discovering-symbols')) { | |
| continue; | |
| } | |
| // end highlighting of file name before trait context info | |
| $line = str_replace(' (in context of class', "{$e} (in context of class", $line); | |
| // PHP_INT_MAX written literally for some reason... | |
| $line = str_replace('9223372036854775807', 'max', $line); | |
| // highlight numeric literals (do not fuck up existing ansi codes) | |
| $line = preg_replace('~(?<![[; ])( -?\d++(?:\.\d+)? )(?![m;]|errors)~', "{$r}$0{$e}", $line); | |
| // remove original separators | |
| if (preg_match('~^ ------ -+\s+$~', $line)) { | |
| continue; | |
| } | |
| // add better | |
| if (preg_match("~\e\[32mLine\e\[39m~", $line)) { | |
| $line .= " ------ " . str_repeat('-', $width() - 9) . "\n"; | |
| } | |
| // highlight rule identifiers (e.g., ? class.notFound) | |
| $line = preg_replace('~?\s*([a-zA-Z0-9._-]+)~u', "{$lk}$0{$e}", $line); | |
| // highlight variables | |
| $line = preg_replace('~(\\$\w+)~u', "{$y}$0{$e}", $line); | |
| $line = preg_replace('~->(\w+)~u', "->{$y}$1{$e}", $line); | |
| // highlight keys in array shapes | |
| $line = preg_replace('~(?<!@phpstan-|remaining |Dumped )\b(\w+)(?=\??: )~u', "{$y}$0{$e}", $line); | |
| // original is ugly | |
| $line = str_replace('Dumped type:', "{$lm}Dumped type:", $line); | |
| // highlight non-highlighted annotations | |
| $line = preg_replace('~@[a-z-]+~', "{$lb}$0{$e}", $line); | |
| // highlight types | |
| $types = 'bool|true|false|null|scalar|empty-scalar|non-empty-scalar' | |
| . '|(?:non-)?(?:negative-|positive-|zero-)?int(?:<(?:\d+|min|max)>)?|float|number|numeric' | |
| . '|(?:non-empty-|non-falsy-|numeric-|class-|callable-|)string|array-key' | |
| . '|(?:non-empty-)?(?:array|list|iterable|key-of|value-of)(?:<[^>]>|\\[\\]|{[^}]})?' // todo: nested <>, collections | |
| . '|object|resource|callable|mixed|never|void|self|static|parent|\\*NEVER\\*|stdClass'; // todo: do not hightlight "static class ..." | |
| $codePattern = '/(::[a-zA-Z0-9_]+)|([a-zA-Z0-9_]+\(\))|([a-zA-Z0-9_\\\|]*[\\\|][a-zA-Z0-9_\\\|]+)|(\b(' . $types . ')\b)/'; | |
| $line = preg_replace_callback($codePattern, static function ($m) use ($b, $lm, $e): string { | |
| $str = $m[0]; | |
| $str = preg_replace('~[|&<>[\\]]~', "{$lm}$0\e[34m", $str); | |
| $str = str_replace('::', "{$e}::{$b}", $str); | |
| return "{$b}{$str}{$e}"; | |
| }, $line); | |
| // highlight PHP operators used in error messages | |
| $line = preg_replace('~&&|\|\||\||===?|!==?|\?->|\?\?=|\?\?|\?|->|<=|>=|<|>|\{|}~', "{$lm}$0{$e}", $line); | |
| // highlight string constants | |
| $line = preg_replace_callback("~'[^']*'~", static function ($m) use ($r, $e): string { | |
| $str = preg_replace('~\e\[[0-9;]*m~', '', $m[0]); // remove ansi formatting inside | |
| return "{$r}{$str}{$e}"; | |
| }, $line); | |
| echo $line; | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
in ide with light background:
