Skip to content

Instantly share code, notes, and snippets.

@paranoiq
Last active May 6, 2026 13:21
Show Gist options
  • Select an option

  • Save paranoiq/d4a5c5396af2019cb367100e7b3dffbb to your computer and use it in GitHub Desktop.

Select an option

Save paranoiq/d4a5c5396af2019cb367100e7b3dffbb to your computer and use it in GitHub Desktop.
PHPStan output tweaker - version, colors, better dividers
<?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;
}
@paranoiq

paranoiq commented Apr 5, 2026

Copy link
Copy Markdown
Author

in ide with light background:
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment