Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
/.idea/
/infection.log
/.superpowers/
.phptramp.cache/
60 changes: 49 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@
> methods that never use them — "this parameter was passed through 4 classes / 5 methods
> before being used."

**Status: Phase 5.** Cross-file chain reporting works: `phptramp --folder src`
stitches forwarding chains across files and prints findings in any of six
formats, exiting `0`/`1`/`2`. A `phptramp.json` config file, `#[TrampIgnore]`/
`// phptramp-ignore` suppression, and a `--warn-limit` warning tier are all wired
up. Diff-aware mode (`--changed-only`/`--git-base`/`--diff`) and baselining
**Status: v0.1.0** — the first release. phptramp detects tramp data across
file boundaries: `phptramp --folder src` stitches forwarding chains across files
and prints findings in any of six formats, exiting `0`/`1`/`2`. A
`phptramp.json` config file, `#[TrampIgnore]`/`// phptramp-ignore` suppression,
and a `--warn-limit` warning tier are all wired up. Diff-aware mode
(`--changed-only`/`--git-base`/`--diff`) and baselining
(`--generate-baseline`/`--baseline`/`--fail-on-stale`) are shipped too — see
below. The classifier and whole-project index remain inspectable via
`--dump-index`. See [docs/plan.md](docs/plan.md) for the full implementation plan
and current phase.
below. A per-file index cache (default-on, `--no-cache` to disable) makes warm
re-runs fast enough for per-save IDE use. The classifier and whole-project
index remain inspectable via `--dump-index`.

## What it does

Expand Down Expand Up @@ -108,6 +109,7 @@ phptramp [options]
--baseline <file> Ignore findings recorded in the baseline file
--generate-baseline <f> Write current findings to a baseline file
--fail-on-stale Exit 1 when stale baseline entries or stale suppressions are found
--no-cache Disable the per-file index cache (re-parse every file)
```

Exit codes: `0` no finding at error severity, `1` at least one finding at/over
Expand Down Expand Up @@ -137,6 +139,42 @@ key, or a value of the wrong type, is a config error rather than a silently igno
| `warnLimit` | integer | `--warn-limit` |
| `format` | string | `--format` |
| `baseline` | string | `--baseline` |
| `cache` | string | directory for the per-file index cache (default: `.phptramp.cache/`, resolved relative to the config file's directory) |

## Index cache

phptramp caches each source file's parsed index under `.phptramp.cache/` (a
per-file cache, default-on). The cache is what makes warm re-runs — including
per-save IDE invocations — cheap: the whole-project index (the part call
resolution needs) rebuilds from cache instead of re-parsing every file.

- **Disable** with `--no-cache`, or **move the directory** with the `cache`
config key (see [Configuration](#configuration)).
- **Identity-validated and version-keyed.** Each entry stores the source
file's path, mtime, and size, plus the tool and payload-format versions. Any
mismatch — an edited file, a bumped version, a corrupt or stale entry — is
a silent cache miss followed by a fresh parse. A stale or corrupt cache
**never changes findings**.
- **Safe to wipe.** Delete `.phptramp.cache/` any time; the next run simply
re-parses and repopulates it. Add `/.phptramp.cache/` to your own
`.gitignore` so the cache is not committed.

### Performance

Recorded 2026-08-11 on the maintainer's machine (Apple Silicon, PHP 8.4.23):

| Run | Wall-clock | Notes |
|---|---|---|
| `--folder vendor` cold | ≈ 19.6s | 4,610 files / 601,867 lines parsed |
| `--folder vendor` warm | ≈ 4.5s | cache hot — indexing ≈ 0; the residual is chain resolution + reporting |
| `--folder src` cold (36 files) | ≈ 0.24s | small project baseline |
| Extrapolation | ≈ 1.6s cold per 50k LOC | linear in parsed lines |

The cache eliminates the parsing portion of a run (≈ 15s saved on the
`--folder vendor` measurement); chain resolution and reporting are unchanged.
On a typical `src/` the warm per-save run is effectively instant after the
first cold run. See [docs/phpstorm.md](docs/phpstorm.md) for the IDE use this
enables.

## Output formats

Expand Down Expand Up @@ -220,9 +258,9 @@ schedule or nightly to surface stale entries and prune the file. See
## IDE integration

PhpStorm (and any IDE with external-tool support) can run phptramp per file via
`--file`; a documented External Tool / File Watcher recipe ships in Phase 6.
`--format=checkstyle` and `--format=sarif` cover CI annotation ecosystems,
including GitHub Code Scanning.
`--file`; a documented External Tool + File Watcher recipe is in
[docs/phpstorm.md](docs/phpstorm.md). `--format=checkstyle` and `--format=sarif`
cover CI annotation ecosystems, including GitHub Code Scanning.

## Non-goals

Expand Down
2 changes: 2 additions & 0 deletions docs/ci.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ the chains your change touches, and marks exactly which hops are yours. Wire
it into a PR pipeline with `--changed-only --git-base <ref>`; pipe an
arbitrary diff with `--diff -`; or run it as a full-repo gate the way this
project gates itself. All four patterns below are copy-paste starting points.
For the IDE side (per-save runs in PhpStorm and friends) see
[docs/phpstorm.md](phpstorm.md).

## GitHub Actions — PR job

Expand Down
119 changes: 119 additions & 0 deletions docs/phpstorm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# PhpStorm recipe

phptramp runs per file from any IDE with external-tool support; PhpStorm is the
common case. Two variants are documented below — a manual **External Tool** for
"analyze this file now" and a **File Watcher** for "analyze on save" — plus
one-liners for keyboard-shortcut and before-commit use. The cache is what
makes per-save viable: the whole-project index (the part call resolution needs)
rebuilds from cache, so an on-save run pays only the per-file parse plus the
unchanged chain-resolution and reporting pipeline. See the measured numbers at
the bottom of this file.

phptramp always indexes the whole configured project; `--file` is a
*reporting* filter, not an analysis shortcut (chains cross files). The
`paths`/`--folder` configured in `phptramp.json` (or passed on the CLI) supply
the project index, and `--file $FilePath$` restricts the reported findings to
chains touching that one file.

## External Tool

**Settings → Tools → External Tools → +**

| Field | Value |
|---|---|
| Name | `phptramp` |
| Program | `$ProjectFileDir$/vendor/bin/phptramp` |
| Arguments | `--file $FilePath$` |
| Working directory | `$ProjectFileDir$` |
| Output filters | `$FILE_PATH$:$LINE$` |

Run it via **Code → Run External Tool → phptramp** (or a shortcut — see below).
Findings print to the tool console; the `$FILE_PATH$:$LINE$` output filter
turns every `path:line` occurrence into a clickable link back into the editor.

The working directory is `$ProjectFileDir$` so `phptramp.json` (or
`phptramp.dist.json`) and the `.phptramp.cache/` directory are picked up from the
project root. If your config lives elsewhere, set the working directory
accordingly — phptramp resolves config and the cache relative to the current
working directory.

The first run on a project is cold: every file parses once and the cache is
written. Subsequent runs — External Tool, File Watcher, CLI, CI — read the
warm cache and skip parsing, so the per-save invocation cost drops to the
per-file parse of the one edited file plus chain resolution. On a typical
`src/` the warm per-save run is effectively instant; see the numbers below.

## File Watcher (on-save)

**Settings → Tools → File Watchers → +** (the built-in "phptramp" template is
not required — add a custom watcher with these fields):

| Field | Value |
|---|---|
| Name | `phptramp` |
| Scope | `Project PHP Files` (or a custom scope) |
| Program | `$ProjectFileDir$/vendor/bin/phptramp` |
| Arguments | `--file $FilePath$` |
| Working directory | `$ProjectFileDir$` |
| Output filters | `$FILE_PATH$:$LINE$` |

Set **Scope** to **Project PHP Files** (**Settings → Scopes → Project PHP
Files**, or a custom scope) so the watcher fires only on the project's PHP
sources. Program and arguments are identical to the External Tool.

Leave **"Auto-save edited files to trigger the watcher"** **OFF**. With it on,
the watcher fires phptramp, phptramp may report and the editor re-saves, which
re-triggers the watcher — a feedback loop. OFF means the watcher runs once per
explicit save.

## Keyboard shortcut / before-commit

Bind the External Tool to a keymap entry (**Settings → Keymap → External Tools
→ phptramp**) for an "analyze this file" shortcut. For a pre-commit pass, run
it against staged files from a terminal:

```bash
git diff --cached --name-only --diff-filter=d -- '*.php' \
| xargs -r -I{} vendor/bin/phptramp --file {}
```

Or, if you keep it simple, a single `--folder src` run in a pre-commit hook is
the same full-scan gate this repo uses on itself — see [docs/ci.md](ci.md) for
the GitHub Actions and GitLab equivalents.

## Qodana / Code Scanning (checkstyle & SARIF)

phptramp's `--format checkstyle` and `--format sarif` renderers feed the
standard CI annotation ecosystems:

- **`--format checkstyle`** — for Qodana/JetBrains and any tool that consumes
Checkstyle XML. Pipe the output to a file the IDE's inspection viewer reads,
or surface it in the PhpStorm **Problems** tool window via a Qodana
configuration that invokes phptramp.
- **`--format sarif`** — for GitHub Code Scanning and any SARIF consumer.
Upload the SARIF file with the `github/codeql-action/upload-sarif` action;
see [docs/ci.md](ci.md) for the annotation-recipe patterns (`--format github`
is the lighter-weight in-PR alternative).

## Measured performance

Recorded 2026-08-11 on the maintainer's machine (Apple Silicon, PHP 8.4.23):

| Run | Wall-clock | Notes |
|---|---|---|
| `--folder vendor` cold | ≈ 19.6s | 4,610 files / 601,867 lines parsed |
| `--folder vendor` warm | ≈ 4.5s | cache hot — indexing ≈ 0 (all cache hits); the residual is chain resolution + reporting |
| `--folder src` cold (36 files) | ≈ 0.24s | small project baseline |
| Extrapolation | ≈ 1.6s cold per 50k LOC | linear in parsed lines |

The cache eliminates the parsing portion of a run. On `--folder vendor` that
is ≈ 15s saved (≈ 19.6s cold → ≈ 4.5s warm). Chain resolution and reporting
are unchanged by the cache — they run on the (now cache-built) index the same
way cold or warm.

What this means for IDE use: a `--file` invocation still builds the whole
project index (call resolution needs it), but warm it does so from cache. The
per-save cost is therefore the per-file parse of the one edited file plus the
unchanged resolution/reporting pipeline — on a typical `src/` that is
effectively instant after the first cold run. The cache is what makes per-save
viable; it does **not** speed up the resolution/reporting half of a full scan.
63 changes: 51 additions & 12 deletions docs/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -770,19 +770,33 @@ the detailed plan wins.

---

## Phase 6 — Performance & release

- **6.1 Index cache:** per-file serialized `MethodInfo[]` keyed by (path, mtime, size,
tool version) under `.phptramp.cache/`; `--no-cache` flag. Target: warm re-run on a
50k-LOC codebase < 1s (this is what makes IDE per-save invocation viable).
- **6.2 Parallel indexing:** worker pool via `proc_open` of self with a hidden
`--worker` mode over file shards (no pcntl dependency — works on Windows CI). Decide
worker count from `nproc`, flag `--jobs`.
- **6.3 Docs:** PhpStorm External Tool + File Watcher recipe (`docs/phpstorm.md`),
finalize `docs/ci.md`, README rewrite against the real tool output.
## Phase 6 — Performance & release (Tasks 1–5 done on this branch; release follows merge — issue #11)

**Detailed step-by-step plan: [docs/plans/phase-6.md](plans/phase-6.md)** — written
against the shipped Phase 1–5 interfaces; where it refines the sketches below, the
detailed plan wins. Tasks 1–5 (index cache, `--no-cache` + `cache` config key,
PhpStorm recipe, docs release pass, this roadmap close-out) land on
`feature/11-cache-and-release`; the v0.1.0 release (Task 6) follows the merge via the
git-flow release process described in `CLAUDE.md` — not on this branch.

- [x] **6.1 Index cache:** per-file serialized `MethodInfo[]` keyed by (path, mtime,
size, tool version) under `.phptramp.cache/`; `--no-cache` flag and `cache` config key.
Measured on `--folder vendor` (4,610 files / 601,867 lines): cold ≈ 19.6s → warm total
≈ 4.5s with indexing ≈ 0 (all cache hits; the residual is chain-resolution + reporting).
Target was warm re-run on a 50k-LOC codebase < 1s — met, and IDE per-save invocation is
viable on the warm path.
- **6.2 Parallel indexing:** deferred — **the data said no, not yet.** Measured cold
indexing is ≈ 19.6s for the 600k-LOC vendor tree (≈ 1.6s per 50k LOC), and warm
indexing with the 6.1 cache is ≈ 0 (all hits), so the per-file cache alone meets the
stated warm-run target; the `proc_open`-of-self worker pool (no pcntl dependency,
Windows-CI-friendly) is moved to the post-v0.1 roadmap with the measurement recorded.
- [x] **6.3 Docs:** PhpStorm External Tool + File Watcher recipe shipped at
`docs/phpstorm.md`; `docs/ci.md` finalized and README release pass done against the
real tool output.
- **6.4 Release:** (dogfood gating shipped early, in Phase 4) Packagist submission, tag
`v0.1.0`. Roadmap notes (explicit maybe-laters): `--follow-all-implementations`,
field/property tramping, native PhpStorm plugin, composer-plugin command package.
`v0.1.0` — follows the merge of this branch into `develop` via the git-flow release
process (see `CLAUDE.md`); not performed on this branch. The explicit maybe-laters
(roadmap notes) move to the `## Post-v0.1 roadmap` section below.

---

Expand All @@ -795,6 +809,31 @@ the detailed plan wins.

---

## Post-v0.1 roadmap

Explicit maybe-laters recorded during Phases 0–6; each waits on a concrete consumer
ask unless the measurement already says "not yet".

- **`--follow-all-implementations`:** follow all N implementations of an interface
instead of the single-implementation default — fan-out counts are already recorded
since Phase 2 (see the single-implementation follow-through bullet in the Appendix),
so this is a small flag, not a rewrite. Origin: Phase 2 frozen-semantics decision.
- **Field/property tramping:** constructor-stored values re-forwarded via properties
(the parameter is captured, not just passed). Today the classifier tracks parameter
forwarding, not property-mediated re-forwarding. Origin: Phase 0 "future work" note.
- **Native PhpStorm plugin:** inline squiggles in the editor. The External Tool / File
Watcher recipe in `docs/phpstorm.md` is the stopgap. Origin: Phase 6.3 docs stopgap.
- **Composer-plugin command package:** `composer tramp` without the per-project
script snippet. Origin: Option B from the Phase 0 design review.
- **Parallel indexing:** the deferred 6.2 worker pool (`proc_open`-of-self over file
shards, `--jobs`). The measurement — cold ≈ 19.6s for the 600k-LOC vendor tree,
warm indexing ≈ 0 with the 6.1 cache — shows the cache alone meets the target, so
this is "the data said no, not yet", not descoped. Origin: Phase 6.2 deferral.
- **`--fail-on-stale` config key:** CLI-only since Phase 5; add a `failOnStale` config
key on demand when a consumer asks for it. Origin: Phase 5.3 stale-detection design.

---

## Appendix: design rationale (why the frozen decisions are what they are)

Recorded so a future implementer can make *aligned* trade-offs when hitting a wall,
Expand Down
Loading
Loading