Skip to content

sen-ltd/phpstan-report

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

phpstan-report

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.

Why

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:

  1. Normalizes it to a flat errors[] list with file, line, message, level, and identifier.
  2. Rolls up per-file counts and a top-N message histogram.
  3. 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.
  4. Renders into four output formats: human (default, colorized), markdown (a GitHub table), json (a slim, report-shaped projection), and github (GitHub Actions workflow commands for inline annotations).

Features

  • Four output formats: human, markdown, json, github.
  • --top N to cap the top-error histogram (default 10).
  • --baseline file.json for delta-reporting against a previous run.
  • --fail-on-new exits 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-level drops findings below error, warning, or info using message-pattern classification (message regex + phpstan's own ignorable flag).
  • Exit codes: 0 clean, 1 errors or regression, 2 bad args or malformed input.
  • Zero runtime dependencies (PHP 8.2 stdlib + json_decode).
  • PHPStan-version independent: consumes the stable totals / files / messages shape only.

Install (Docker — recommended)

docker build -t phpstan-report .
# Run against a committed fixture
docker run --rm -v $(pwd)/tests/fixtures:/work phpstan-report /work/mixed.json

The 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.

Install (local)

git clone https://github.com/sen-ltd/phpstan-report.git
cd phpstan-report
composer install
./bin/phpstan-report tests/fixtures/mixed.json

Usage

The 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-new

Or pipe it directly:

./vendor/bin/phpstan analyse src --error-format=json | phpstan-report -

Example GitHub Actions step

- 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-new

The 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.

Output formats

human (default)

== 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
  ...

markdown

## 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 ... |

github

::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 ...

json

{
  "summary": { "errors": 3, "warnings": 2, "info": 2, "files": 3 },
  "perFile": { "src/Service/UserService.php": 3, "..." : 2 },
  "top": [ { "message": "...", "count": 2 } ],
  "errors": [ ... ],
  "diff": null
}

How classification works

PHPStan's JSON output does not include a severity per message — everything is just "an error." phpstan-report adds a classification step on top:

  1. 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.
  2. Non-fatal + ignorable: trueinfo.
  3. Everything elsewarning.

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.

Baseline comparison

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.json

Output 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.

Tests

composer install
./vendor/bin/phpunit

Or in Docker without touching host PHP:

docker run --rm --entrypoint /app/vendor/bin/phpunit phpstan-report \
    --no-coverage -c /app/phpunit.xml

56 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.

Scope

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.

License

MIT. See LICENSE.

Links

About

A PHP 8.2 CLI that eats `phpstan analyse --error-format=json` and emits readable output for the four places phpstan findings actually get consumed: a human terminal, a GitHub PR comment, a GitHub Actions annotation stream, and downstream tooling. Zero runtime dependencies, PHP stdlib only, multi-stage Alpine Docker image at ~51 MB.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors