A PHPStan output formatter. Feed it the JSON produced by
phpstan analyse --error-format=json and it emits the same findings in a
shape humans, pull requests, and GitHub Actions can actually read.
phpstan-report is not a static analyser. It does not parse PHP, does
not need a Composer-installed PHPStan, and does not know what your
phpstan.neon says. It only reshapes phpstan's own JSON output, so it works
with whatever PHPStan version your project is already using.
PHPStan has an excellent human-readable output and an excellent JSON output,
but they don't overlap. The human output scrolls off your CI log and drops
structure; the JSON output is shaped like a storage format, with errors
nested under a files map keyed by absolute path. You can't paste it into
a PR comment, you can't stream it into GitHub annotations, and the baseline
format PHPStan itself uses (phpstan-baseline.neon) is equally unfriendly
to PR review tooling.
phpstan-report does four things on top of the raw JSON:
- Normalizes it to a flat
errors[]list withfile,line,message,level, andidentifier. - Rolls up per-file counts and a top-N message histogram.
- Diffs against a previous run (also just a JSON file) and reports
new vs resolved findings, matched by
(file, message)so line shifts don't create phantom deltas. - Renders into four output formats:
human(default, colorized),markdown(a GitHub table),json(a slim, report-shaped projection), andgithub(GitHub Actions workflow commands for inline annotations).
- Four output formats:
human,markdown,json,github. --top Nto cap the top-error histogram (default 10).--baseline file.jsonfor delta-reporting against a previous run.--fail-on-newexits 1 when the current run has more errors than the baseline, even if the baseline also had errors — useful as a CI gate on strict-mode bumps.--pattern-leveldrops findings belowerror,warning, orinfousing message-pattern classification (message regex + phpstan's ownignorableflag).- Exit codes:
0clean,1errors or regression,2bad args or malformed input. - Zero runtime dependencies (PHP 8.2 stdlib +
json_decode). - PHPStan-version independent: consumes the stable
totals/files/messagesshape only.
docker build -t phpstan-report .
# Run against a committed fixture
docker run --rm -v $(pwd)/tests/fixtures:/work phpstan-report /work/mixed.jsonThe image is a multi-stage Alpine build: PHPUnit and Composer live in the
builder stage only, so the runtime image ships PHP 8.2-cli, the src/ tree,
and the bin script. Final size is under 100 MB.
git clone https://github.com/sen-ltd/phpstan-report.git
cd phpstan-report
composer install
./bin/phpstan-report tests/fixtures/mixed.jsonThe typical flow is a two-stage PHPStan run:
# Generate raw phpstan JSON output
./vendor/bin/phpstan analyse src --error-format=json > phpstan.json
# Human report
phpstan-report phpstan.json
# GitHub Actions annotations (inline red squiggles in the diff view)
phpstan-report phpstan.json --format github
# Markdown table ready to paste as a PR comment
phpstan-report phpstan.json --format markdown
# Regression gate against a committed baseline
phpstan-report phpstan.json --baseline phpstan-baseline.json --fail-on-newOr pipe it directly:
./vendor/bin/phpstan analyse src --error-format=json | phpstan-report -- name: phpstan
run: |
./vendor/bin/phpstan analyse src --error-format=json > phpstan.json || true
docker run --rm -v "$PWD":/work sen-ltd/phpstan-report \
/work/phpstan.json --format github
docker run --rm -v "$PWD":/work sen-ltd/phpstan-report \
/work/phpstan.json \
--baseline /work/phpstan-baseline.json \
--fail-on-newThe first invocation emits workflow commands so PHPStan findings show up as inline annotations on the PR diff. The second one runs the regression gate: exits 1 if the branch introduced more findings than the committed baseline.
== phpstan-report ==
summary: 3 errors, 2 warnings, 2 info (7 across 3 files)
per file:
3 src/Service/UserService.php
2 src/Controller/AuthController.php
2 src/Util/Helpers.php
top errors (cap 10):
INFO 2x Unused use statement Psr\Log\LoggerInterface.
first seen: src/Util/Helpers.php:6
ERR 1x Call to an undefined method App\Service\UserService::fetchCurrentUser().
first seen: src/Service/UserService.php:42
...
## phpstan-report
**3 errors**, **2 warnings**, **2 info** — 7 findings across 3 files.
| File | Line | Level | Message |
| --- | ---: | --- | --- |
| `src/Service/UserService.php` | 42 | ERROR | Call to an undefined method ... |
| `src/Service/UserService.php` | 58 | ERROR | Parameter $user has invalid type ... |::error file=src/Service/UserService.php,line=42::Call to an undefined method ...
::error file=src/Service/UserService.php,line=58::Parameter $user has invalid type ...
::notice file=src/Util/Helpers.php,line=6::Unused use statement ...
{
"summary": { "errors": 3, "warnings": 2, "info": 2, "files": 3 },
"perFile": { "src/Service/UserService.php": 3, "..." : 2 },
"top": [ { "message": "...", "count": 2 } ],
"errors": [ ... ],
"diff": null
}PHPStan's JSON output does not include a severity per message — everything
is just "an error." phpstan-report adds a classification step on top:
- Fatal patterns (regex on the message text) →
error. These are the findings that almost always indicate real brokenness: undefined methods / functions / variables, invalid parameter types, undefined class references, bad return types. - Non-fatal +
ignorable: true→info. - Everything else →
warning.
The classifier is deliberately simple so you can override it in your own
pipeline by editing src/Parser.php — no config file, no plugin system.
The baseline is just another phpstan JSON file (or a previously saved
phpstan-report --format json output). Errors are matched across runs by
(file, message) — line number is ignored on purpose, because the whole
reason .neon baselines cause friction in PR review is that refactors
shift line numbers even when nothing was actually fixed.
phpstan-report phpstan.json --baseline phpstan-baseline.jsonOutput includes a baseline diff block:
baseline diff:
+3 new
-0 resolved
With --fail-on-new, the process exits 1 whenever the current run has
more errors than the baseline.
composer install
./vendor/bin/phpunitOr in Docker without touching host PHP:
docker run --rm --entrypoint /app/vendor/bin/phpunit phpstan-report \
--no-coverage -c /app/phpunit.xml56 tests cover the parser (normalization, classification, per-file / top-N rollups, filtering), baseline comparison (new / resolved / shared / line-shift immunity), each of the four formatters, and every CLI exit-code branch.
What this tool does NOT do:
- Run PHPStan for you. You pipe its JSON output in.
- Emit SARIF. Planned, but not in the initial cut.
- Categorize or ignore errors by identifier. The PHPStan identifier is passed through in the JSON output for downstream tools.
- Replace
phpstan-baseline.neon. This is a lightweight alternative aimed specifically at PR review and CI gating.
MIT. See LICENSE.