{"id":50926471,"url":"https://github.com/kayw-geek/phpstan-type-trace","last_synced_at":"2026-06-16T23:32:12.301Z","repository":{"id":359473651,"uuid":"1246229181","full_name":"kayw-geek/phpstan-type-trace","owner":"kayw-geek","description":"See the full type-inference chain of any value in PHPStan, not just a single snapshot.","archived":false,"fork":false,"pushed_at":"2026-05-22T05:36:55.000Z","size":628,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T11:44:31.250Z","etag":null,"topics":["agent","claude-code","debug-tool","php","phpstan","phpstan-extension","static-analysis"],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kayw-geek.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-22T02:05:48.000Z","updated_at":"2026-05-22T10:56:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/kayw-geek/phpstan-type-trace","commit_stats":null,"previous_names":["kayw-geek/phpstan-type-trace"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/kayw-geek/phpstan-type-trace","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kayw-geek%2Fphpstan-type-trace","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kayw-geek%2Fphpstan-type-trace/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kayw-geek%2Fphpstan-type-trace/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kayw-geek%2Fphpstan-type-trace/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kayw-geek","download_url":"https://codeload.github.com/kayw-geek/phpstan-type-trace/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kayw-geek%2Fphpstan-type-trace/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34428196,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-16T02:00:06.860Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent","claude-code","debug-tool","php","phpstan","phpstan-extension","static-analysis"],"created_at":"2026-06-16T23:32:11.144Z","updated_at":"2026-06-16T23:32:12.285Z","avatar_url":"https://github.com/kayw-geek.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# phpstan-type-trace\n\n🌐 **[Live examples →](https://kayw-geek.github.io/phpstan-type-trace/)** — read 5 real chains in 10 seconds, no install.\n\nWhen PHPStan tells you:\n\n    Parameter #1 $amount of method format() expects float, float|null given.\n\nYou 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.\n\nThis extension prints the full chain — every event that shaped the variable up to that line:\n\n```\n$amount · App\\PriceCalculator::format [src/PriceCalculator.php] (up to L25)\n  L16  param   float|null\n  L20  narrow  Webmozart\\Assert\\Assert::notNull($amount)  =\u003e  float    via AssertTypeSpecifyingExtension\n  L25  read    float\n```\n\nOne command, zero source edits, third-party extensions attributed.\n\n![Hero](docs/hero.png)\n\nAbove: a longer chain from real larastan code — nine events including three `narrow` rows that show *why* the type tightened.\n\n## Install\n\n```bash\ncomposer require --dev kayw-geek/phpstan-type-trace\n```\n\nAuto-registered via [phpstan-extension-installer](https://github.com/phpstan/extension-installer). Otherwise add to `phpstan.neon`:\n\n```neon\nincludes:\n    - vendor/kayw-geek/phpstan-type-trace/extension.neon\n```\n\n## Usage\n\n### CLI — inspect any line, no source edits\n\n```bash\n./vendor/bin/phpstan-trace inspect src/Foo.php:42 myVar\n```\n\nVariable name is optional — if only one variable has events at the target line, it's auto-picked. Otherwise the candidates are listed.\n\nPass `--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`.\n\n### `traceType()` — drop in a marker, get the chain on your next phpstan run\n\nNo 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.\n\n```php\nfunction compute(?float $discount = null): float\n{\n    $discount ??= 0.1;\n    traceType($discount, 'after ??=');\n    return 1 - $discount;\n}\n```\n\n```\n ------ -----------------------------------------------------------\n  Line   PriceCalculator.php\n ------ -----------------------------------------------------------\n  5      Type chain for $discount in compute — after ??=\n           L3   param      float|null\n           L4   assign-op  float\n ------ -----------------------------------------------------------\n```\n\n`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.\n\nSignature:\n\n```php\nfunction traceType(mixed $value, ?string $reason = null): void\n```\n\n`$value` accepts a variable, property fetch (`$this-\u003ex`), or static property (`Foo::$bar`). For arbitrary expressions, only the snapshot type is printed. `$reason` is a string literal shown in the chain header.\n\n## What gets captured\n\n| Source                    | Origin label  | Example                                  | `via` |\n| ------------------------- | ------------- | ---------------------------------------- | :---: |\n| Function/method params    | `param`       | `function f(int $x)`                     |       |\n| Closure / arrow-fn params | `param`       | `fn(int $x) =\u003e ...`                      |       |\n| Variable assignment       | `assign`      | `$x = 5;`                                | ✓     |\n| Compound assignment       | `assign-op`   | `$x += 1; $x ??= 'def';`                 | ✓     |\n| Reference assignment      | `assign-ref`  | `$x = \u0026$other;`                          |       |\n| Array write               | `array-write` | `$x[] = 'y'; $x['k'] = $v;`              |       |\n| Property fetch            | `read`        | `$this-\u003efoo`                             | ✓     |\n| Static property fetch     | `read`        | `Foo::$bar`                              | ✓     |\n| Variable read             | `read`        | bare `$x` usage                          |       |\n| If / ternary narrowing    | `narrow`      | `if (is_string($x))`, `$x ?? 'd'`, etc.  | ✓     |\n\n`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.\n\n**`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.\n\nWhen the inferred type surprises you, `via` tells you which extension to blame (or thank) without grepping the vendor tree.\n\n**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.\n\n## Limitations\n\n- Loops report the post-fixpoint type, not per-iteration deltas.\n- Multiple closures inside the same enclosing function share one bucket. Same-named vars across sibling closures may collide.\n- Cannot follow values across function boundaries.\n- Ref-aliases (`$alias = \u0026$x; $alias[] = 'y';`) show only the snapshot at the call.\n\n## Use it in PhpStorm\n\n[![JetBrains Plugin](https://img.shields.io/jetbrains/plugin/v/31962-phpstan-type-trace?label=PhpStorm%20plugin)](https://plugins.jetbrains.com/plugin/31962-phpstan-type-trace)\n\nInstall 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 \u003cExtension\u003e` attribution pills.\n\nThe plugin shells out to this CLI, so the package above must be installed in your project's `vendor/`. Marketplace page: \u003chttps://plugins.jetbrains.com/plugin/31962-phpstan-type-trace\u003e.\n\n## Use it with Claude Code\n\nWhen 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.\n\n```\n/plugin marketplace add kayw-geek/phpstan-type-trace\n/plugin install phpstan-type-trace@kayw-geek\n```\n\nInstalled into `~/.claude/plugins/cache/`, auto-discovered across every project. Updates: `/plugin marketplace update kayw-geek` then reinstall.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cstrong\u003eHow it works\u003c/strong\u003e\u003c/summary\u003e\n\nTwo-phase PHPStan pipeline:\n\n1. **Collectors** (one per event kind) record every relevant AST event with `(file, functionKey, path, line, pos, type, origin)`:\n   - 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.\n   - Reads: `VarReadCollector`, `PropertyFetchCollector`, `StaticPropertyFetchCollector`.\n   - Writes: `AssignCollector`, `AssignOpCollector` (covers all 13 compound-op subclasses), `AssignRefCollector`, `ArrayWriteCollector`.\n   - Narrowing: `NarrowingCollector` (if-statements), `TernaryNarrowingCollector` (ternaries) — anchored to the branch where the narrow holds, with a reason predicate extracted from the guard.\n   - Call sites: `TraceCallCollector`.\n2. **`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 `\u003c=` the call line, sorts by `line → source position → rank` (so inline ternaries read cond-read → narrow → then-read, not narrow first), collapses repeated reads of the same type *except* right after a narrow (since the narrow is evidence and the read is the usage — they convey different things), and emits the delta chain as a PHPStan error.\n\nThe CLI runs the same pipeline with a dump env var set, captures every chain as a JSON sentinel error, then filters to the `(file, line, variable)` you asked about.\n\n\u003c/details\u003e\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkayw-geek%2Fphpstan-type-trace","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkayw-geek%2Fphpstan-type-trace","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkayw-geek%2Fphpstan-type-trace/lists"}