phpstan-trace inspect --json

May 26, 2026 · View on GitHub

Stable, versioned JSON contract for tooling (IDE plugins, AI agents, CI).

phpstan-trace inspect <file>:<line> [<var>] --json [--api-version=N]
  • --api-version=N pins the output schema. Omitted ⇒ latest. Unknown version ⇒ exit 1.
  • Exit code is 0 for any well-formed JSON response (success or miss). 1 is reserved for hard failures (bad args, phpstan not found, JSON parse failure). Consumers should not branch on exit code alone — inspect found and reason.

Versioning policy

  • Additive changes (new optional fields) ship under the current apiVersion.
  • Breaking changes (renamed/removed fields, changed types) ship under a new apiVersion. Previous version stays supported for at least one minor release.
  • All responses carry apiVersion: <int> as the first field so consumers can dispatch.

Current versions: 1 (latest).

Response shapes (v1)

Three mutually exclusive shapes, distinguished by found and (when found=false) reason.

1. Success — chain found

{
  "apiVersion": 1,
  "found": true,
  "file": "/abs/path/to/src/User.php",
  "line": 42,
  "path": "$user",
  "functionKey": "/abs/path/to/src/User.php::App\\User::profile",
  "chain": [
    { "line": 10, "origin": "param",     "type": "App\\User|null" },
    { "line": 15, "origin": "narrow",    "type": "App\\User", "reason": "$user !== null" },
    { "line": 16, "origin": "read",      "type": "App\\User", "via": ["NewModelQueryDynamicMethodReturnTypeExtension"] }
  ]
}
FieldTypeNotes
apiVersionintAlways 1 in v1.
foundtrueDiscriminator.
filestring (abs path)Resolved absolute path.
lineintQueried line.
pathstringNormalized variable path (e.g. $user, $this->foo, Foo::$bar).
functionKeystring{absFile}::{fqn}. __top__ for file scope.
chainlist<ChainEvent>Ordered events, truncated to line <= <line>. May be empty only if a logic bug — successful responses generally have ≥1 event.

ChainEvent

FieldTypeRequiredNotes
lineintyesSource line of the event.
originstring (enum below)yesWhat kind of event.
typestringyesPHPStan-inferred type at this point.
reasonstringnoHuman-readable predicate for narrow (e.g. $x !== null, is_string($x)), or compound-op kind for assign-op.
vialist<string>noThird-party PHPStan extensions (short class names) that shaped this type. Absent if none attributed.

origin is one of:

ValueMeaning
paramFunction / method / closure / arrow-fn parameter entry.
assign$x = …
assign-op$x += …, $x ??= …, etc.
assign-ref$x = &$other
array-write$x[] = …, $x['k'] = …
narrowType narrowed by if / ternary guard. Carries reason.
readBare read / property fetch / static prop fetch.

2. Miss — variable specified but no chain found

{
  "apiVersion": 1,
  "found": false,
  "file": "/abs/path/to/src/User.php",
  "line": 42,
  "path": "$user",
  "reason": "path_not_tracked",
  "message": "No events recorded for $user in this file. The variable may be array-dim only ($x[] = ...), dynamic, or defined after the queried line.",
  "availablePaths": ["$this", "$id"]
}
FieldTypeNotes
apiVersionint
foundfalse
filestring
lineint
pathstringThe path that was queried.
reasonstring (enum)Machine-readable miss code.
messagestringHuman-readable diagnostic. Do not parse.
availablePathslist<string>Variable paths the extension did track in this file. May be empty.

reason enum (when path is present):

ValueMeaning
extension_not_loadedphpstan emitted zero trace events. Likely the extension isn't registered.
file_path_mismatchphpstan returned chains for other files only — argument resolution issue.
path_not_trackedFile was scanned, but no events match the queried variable path.

3. Ambiguous — no <var> supplied, line has 0 or >1 candidates

{
  "apiVersion": 1,
  "found": false,
  "file": "/abs/path/to/src/User.php",
  "line": 42,
  "reason": "ambiguous",
  "message": "Multiple variables are tracked at this line; specify <var>.",
  "candidates": ["$user", "$id"]
}
FieldTypeNotes
apiVersionint
foundfalse
filestring
lineint
reasonstring (enum)ambiguous or no_var_at_line.
messagestringHuman-readable.
candidateslist<string>Variable paths tracked at this line. Empty when reason=no_var_at_line.

Note: this shape has no path field (the user didn't supply one). Consumers should branch on path presence or on the reason value.

Consumer guidance

type Response = Success | Miss | Ambiguous;

function dispatch(r: Response) {
  if (r.apiVersion !== 1) throw new Error(`Unsupported apiVersion ${r.apiVersion}`);
  if (r.found) return renderChain(r);
  if (r.reason === "ambiguous" || r.reason === "no_var_at_line") return promptPick(r.candidates);
  return renderMiss(r); // path_not_tracked, file_path_mismatch, extension_not_loaded
}

Forward compatibility

  • New optional fields may appear on existing shapes under the same apiVersion. Ignore unknown fields.
  • New origin values may appear in chain[].origin. Treat unknown origins as opaque events.
  • New reason values may appear in miss responses. Default to a generic "no chain" branch.
  • apiVersion will only increment on breaking changes.

Stability scope

What's covered by the version contract:

  • Top-level field names and types on each response shape.
  • chain[].origin enum values listed above.
  • reason enum values listed above.

What's not covered (may change without bumping apiVersion):

  • message wording.
  • functionKey exact format (treat as opaque identifier).
  • PHPStan-inferred type string formatting (mirrors PHPStan internals).
  • via short-name list contents (depends on installed extensions).