PHPackages                             kayw-geek/phpstan-type-trace - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Debugging &amp; Profiling](/categories/debugging)
4. /
5. kayw-geek/phpstan-type-trace

ActivePhpstan-extension[Debugging &amp; Profiling](/categories/debugging)

kayw-geek/phpstan-type-trace
============================

Visualize the full type-inference chain of a value in PHPStan analysis output.

v0.2.1(2mo ago)20730↓16.7%MITPHPPHP ^8.2CI passing

Since May 25Pushed 1mo agoCompare

[ Source](https://github.com/kayw-geek/phpstan-type-trace)[ Packagist](https://packagist.org/packages/kayw-geek/phpstan-type-trace)[ RSS](/packages/kayw-geek-phpstan-type-trace/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (1)Dependencies (3)Versions (9)Used By (0)

phpstan-type-trace
==================

[](#phpstan-type-trace)

🌐 **[Live examples →](https://kayw-geek.github.io/phpstan-type-trace/)** — read 5 real chains in 10 seconds, no install.

When PHPStan tells you:

```
Parameter #1 $amount of method format() expects float, float|null given.

```

You know the final type at the call site. You don't know which assign, which param, or which missing narrow put the `null` there. You scroll up, guess, get it wrong, repeat.

This extension prints the full chain — every event that shaped the variable up to that line:

```
$amount · App\PriceCalculator::format [src/PriceCalculator.php] (up to L25)
  L16  param   float|null
  L20  narrow  Webmozart\Assert\Assert::notNull($amount)  =>  float    via AssertTypeSpecifyingExtension
  L25  read    float

```

One command, zero source edits, third-party extensions attributed.

[![Hero](docs/hero.png)](docs/hero.png)

Above: a longer chain from real larastan code — nine events including three `narrow` rows that show *why* the type tightened.

Install
-------

[](#install)

```
composer require --dev kayw-geek/phpstan-type-trace
```

Auto-registered via [phpstan-extension-installer](https://github.com/phpstan/extension-installer). Otherwise add to `phpstan.neon`:

```
includes:
    - vendor/kayw-geek/phpstan-type-trace/extension.neon
```

Usage
-----

[](#usage)

### CLI — inspect any line, no source edits

[](#cli--inspect-any-line-no-source-edits)

```
./vendor/bin/phpstan-trace inspect src/Foo.php:42 myVar
```

Variable name is optional — if only one variable has events at the target line, it's auto-picked. Otherwise the candidates are listed.

Pass `--json` for machine-readable output (handy for IDE plugins, agents, and CI). Schema is versioned and documented in [`docs/json-api.md`](docs/json-api.md); pin with `--api-version=N`.

### `traceType()` — drop in a marker, get the chain on your next phpstan run

[](#tracetype--drop-in-a-marker-get-the-chain-on-your-next-phpstan-run)

No extra command. Just call `traceType($var)` anywhere, then run `vendor/bin/phpstan analyse` like you always do — the chain shows up as a phpstan error at that line.

```
function compute(?float $discount = null): float
{
    $discount ??= 0.1;
    traceType($discount, 'after ??=');
    return 1 - $discount;
}
```

```
 ------ -----------------------------------------------------------
  Line   PriceCalculator.php
 ------ -----------------------------------------------------------
  5      Type chain for $discount in compute — after ??=
           L3   param      float|null
           L4   assign-op  float
 ------ -----------------------------------------------------------

```

`traceType()` is a runtime no-op (autoloaded from `src/runtime.php`), so leaving a stray call in production code does nothing — it only emits during static analysis.

Signature:

```
function traceType(mixed $value, ?string $reason = null): void
```

`$value` accepts a variable, property fetch (`$this->x`), or static property (`Foo::$bar`). For arbitrary expressions, only the snapshot type is printed. `$reason` is a string literal shown in the chain header.

What gets captured
------------------

[](#what-gets-captured)

SourceOrigin labelExample`via`Function/method params`param``function f(int $x)`Closure / arrow-fn params`param``fn(int $x) => ...`Variable assignment`assign``$x = 5;`✓Compound assignment`assign-op``$x += 1; $x ??= 'def';`✓Reference assignment`assign-ref``$x = &$other;`Array write`array-write``$x[] = 'y'; $x['k'] = $v;`Property fetch`read``$this->foo`✓Static property fetch`read``Foo::$bar`✓Variable read`read`bare `$x` usageIf / ternary narrowing`narrow``if (is_string($x))`, `$x ?? 'd'`, etc.✓`narrow` events carry a `reason` showing the predicate that justified the narrowing (`is_string($x)`, `$x instanceof Foo`, `$x !== null`, ...), anchored to the branch where the narrow takes effect. Same-line events are ordered by source position, so an inline ternary reads cause → effect: the cond-read first, then the narrow, then the then-branch read.

**`via` — third-party extension attribution.** When the inferred type was shaped by a third-party PHPStan extension, the extension's short class name is appended (`via NewModelQueryDynamicMethodReturnTypeExtension`, `via AssertTypeSpecifyingExtension`, ...). Three categories are attributed today: dynamic return type (assign / assign-op), type-specifying (narrow), and properties class reflection (read). Detection is by source-file location — official add-ons like `phpstan/phpstan-webmozart-assert` that ship under the `PHPStan\` namespace are still attributed; only classes shipped by `phpstan/phpstan` core are filtered out.

When the inferred type surprises you, `via` tells you which extension to blame (or thank) without grepping the vendor tree.

**Not attributed yet:** type-specifying calls used as bare statements (`Assert::notNull($x);` outside any `if` / ternary). PHPStan still narrows the scope but no `narrow` event is emitted. Wrap the call in an `if` if you need the attribution.

Limitations
-----------

[](#limitations)

- Loops report the post-fixpoint type, not per-iteration deltas.
- Multiple closures inside the same enclosing function share one bucket. Same-named vars across sibling closures may collide.
- Cannot follow values across function boundaries.
- Ref-aliases (`$alias = &$x; $alias[] = 'y';`) show only the snapshot at the call.

Use it in PhpStorm
------------------

[](#use-it-in-phpstorm)

[![JetBrains Plugin](https://camo.githubusercontent.com/a0d52ed4bcbd88c1f1d08ef20076e573413d3e0177beb8855e9c93ed3765a9af/68747470733a2f2f696d672e736869656c64732e696f2f6a6574627261696e732f706c7567696e2f762f33313936322d7068707374616e2d747970652d74726163653f6c6162656c3d50687053746f726d253230706c7567696e)](https://plugins.jetbrains.com/plugin/31962-phpstan-type-trace)

Install the companion plugin to read chains inside the editor — caret on a variable, run **Trace Type at Caret**, and the chain renders in a dedicated tool window with clickable line numbers, copyable types, and `via ` attribution pills.

The plugin shells out to this CLI, so the package above must be installed in your project's `vendor/`. Marketplace page: .

Use it with Claude Code
-----------------------

[](#use-it-with-claude-code)

When Claude Code (or any LLM agent) is chasing PHPStan errors, it usually guesses at types. With this extension installed as a [Claude Code plugin](https://docs.claude.com/claude-code), Claude invokes the trace automatically — fixes are grounded in real upstream type evidence, not pattern-matching.

```
/plugin marketplace add kayw-geek/phpstan-type-trace
/plugin install phpstan-type-trace@kayw-geek

```

Installed into `~/.claude/plugins/cache/`, auto-discovered across every project. Updates: `/plugin marketplace update kayw-geek` then reinstall.

**How it works**Two-phase PHPStan pipeline:

1. **Collectors** (one per event kind) record every relevant AST event with `(file, functionKey, path, line, pos, type, origin)`:
    - Param entry: `ParamInFunctionCollector`, `ParamInMethodCollector`, `ParamInClosureCollector`, `ParamInArrowFunctionCollector` — hooked on PHPStan's `In*Node` virtual nodes so scope is already inside the function when params are read.
    - Reads: `VarReadCollector`, `PropertyFetchCollector`, `StaticPropertyFetchCollector`.
    - Writes: `AssignCollector`, `AssignOpCollector` (covers all 13 compound-op subclasses), `AssignRefCollector`, `ArrayWriteCollector`.
    - Narrowing: `NarrowingCollector` (if-statements), `TernaryNarrowingCollector` (ternaries) — anchored to the branch where the narrow holds, with a reason predicate extracted from the guard.
    - Call sites: `TraceCallCollector`.
2. **`TraceReportRule`** runs once at the end on the virtual `CollectedDataNode`. For each `traceType()` call it joins the recorded events on `(functionKey, path)` filtered to lines `
