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
8 changes: 3 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,9 @@ jobs:
run: composer stan
- name: PHPMD (codesize)
run: composer md
# Dogfood: run phptramp on its own source. Report-only for now (--limit 99
# can never fail) so the chain output is visible in CI logs; Phase 6 lowers
# it to a real gate once the codebase is deliberately chain-clean.
- name: Dogfood (report-only)
run: bin/phptramp --folder src --limit 99
# Dogfood: phptramp gates itself via its own config (limit 3, warn-limit 2).
- name: Dogfood
run: composer tramp

# Line coverage says a line ran; it does not say a test would notice if the
# line were wrong. Infection changes the line and reports whether anything
Expand Down
48 changes: 39 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
> methods that never use them — "this parameter was passed through 4 classes / 5 methods
> before being used."

**Status: Phase 3.** Cross-file chain reporting works: `phptramp --folder src`
**Status: Phase 4.** 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. The classifier and whole-project index remain inspectable via `--dump-index`.
up. Diff-aware mode (`--changed-only`/`--git-base`/`--diff`) is 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.

## What it does
Expand All @@ -33,8 +34,28 @@ the "tramp data" smell: every method in the middle is coupled to a value it has
business knowing about. The fix is usually a parameter object, a context object, or
dependency injection at the terminal — this tool tells you *where*.

The flagship feature (Phase 4) is diff-aware CI mode: run it on a pull request and it
reports **"your edit made this chain longer"**, marking exactly which hops are yours.
The flagship feature is diff-aware CI mode: run it on a pull request and it reports
**"your edit made this chain longer"**, marking exactly which hops are yours.
`--changed-only` restricts findings to chains that intersect a diff — a hop matches iff
its declaration line or its forwarding call-site line was touched — and each matching
hop's location line grows a `*YOURS*` annotation:

```text
$ vendor/bin/phptramp --folder src --changed-only --git-base origin/main

FINDING $config: 3 pass-through hops across 4 classes
origin App\Http\Controller::handle($config) src/Http/Controller.php:21
hop 2 App\Service\ServiceA::process($config) src/Service/ServiceA.php:14 *YOURS*
hop 3 App\Service\ServiceB::run($config) src/Service/ServiceB.php:9
terminal App\Mail\Mailer::__construct($config) src/Mail/Mailer.php:12 (stored)

1 finding (limit: 3 hops).
```

`--git-base <ref>` (default `origin/main`) supplies the diff via
`git diff --unified=0 <ref>...HEAD` (three-dot, merge-base semantics); `--diff <path|->`
reads a unified diff from a file, or from stdin with `-`, instead — either always implies
`--changed-only`. See [docs/ci.md](docs/ci.md) for GitHub Actions and GitLab recipes.

## Installation

Expand All @@ -59,9 +80,13 @@ composer tramp
composer tramp -- --format json --warn-limit 2
```

phptramp itself does not ship a `composer.json` script of this name — `vendor/bin/phptramp`
(or `bin/phptramp` inside this repo) is the invocation; the snippet above is a convenience
you add to a *consuming* project.
This repository gates itself with exactly such a script: `composer tramp` runs
`bin/phptramp` against the paths and thresholds in its own `phptramp.dist.json`
(`paths: src`, `limit: 3`, `warn-limit: 2`), and CI fails the build on any finding at or
over the limit. That is the same `composer tramp` invocation a consuming project gets from
the snippet above — here the paths come from config, so no `--folder` is needed; a consumer
can configure `paths` the same way or pass `--folder src` inline. (`--no-config` bypasses
the config file entirely when you want to drive everything from flags.)

## CLI

Expand All @@ -75,8 +100,9 @@ phptramp [options]
--warn-limit <n> Warn (do not fail CI) on chains with >= n hops
--format <fmt> text|json|github|checkstyle|sarif|summary (default: text)
--explain Show why chains ended (call resolution trace)
--changed-only Reserved for diff-aware mode (Phase 4) — accepted, not yet wired up
--git-base <ref> Reserved for diff-aware mode (Phase 4) — accepted, not yet wired up
--changed-only Only report chains touching changed lines
--git-base <ref> Diff base for --changed-only (default: origin/main)
--diff <path|-> Read the diff from a file, or stdin with '-' (implies --changed-only)
--baseline <file> Reserved for baselining (Phase 5) — accepted, not yet wired up
--generate-baseline <f> Reserved for baselining (Phase 5) — accepted, not yet wired up
```
Expand Down Expand Up @@ -124,6 +150,10 @@ which always runs with `--format json`.
| `sarif` | `{"ruleId":"phptramp.trampData","level":"error","message":{"text":"$config: 3 pass-through hops across 4 classes (terminal: Demo\\Mailer::__construct [stored])"}}` |
| `summary` | `12 chains total; 5 at or over the limit (limit: 3 hops).` |

In a `--changed-only` run, each `json` chain entry additionally carries a `"changed":
true|false` field marking whether that hop intersects the diff; a normal full run omits
the field entirely rather than sending it as always-`false` noise.

## Suppression

Mark a specific false positive as intentional instead of raising `--limit` project-wide:
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@md",
"@test"
],
"tramp": "@php bin/phptramp",
"infection": "infection --threads=max",
"infection:diff": "infection --git-diff-filter=AM --git-diff-base=origin/main --threads=max --ignore-msi-with-no-mutations"
},
Expand Down
105 changes: 105 additions & 0 deletions docs/ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# CI cookbook

phptramp's flagship mode is diff-aware: on a pull/merge request it reports only
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.

## GitHub Actions — PR job

`--changed-only --git-base` shells out to `git diff --unified=0 <base>...HEAD`
(three-dot, merge-base semantics), so the checkout needs history and the base
ref present locally. `--format github` emits `::error`/`::warning`/`::notice`
annotations that GitHub renders inline on the PR diff.

```yaml
name: phptramp

on:
pull_request: ~

jobs:
phptramp:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
- run: composer update --no-interaction --no-progress
- name: Fetch the base branch
run: git fetch --no-tags origin "${GITHUB_BASE_REF}"
- name: phptramp (changed lines only)
run: >-
vendor/bin/phptramp --folder src
--changed-only --git-base "origin/${GITHUB_BASE_REF}"
--format github
```

## GitLab CI — merge request job

Same invocation; GitLab has no built-in consumer for `--format github`
annotations, so `text` (readable in the job log) or `json` (for a downstream
step) are the usual picks. `rules` scopes the job to merge request pipelines,
where `CI_MERGE_REQUEST_TARGET_BRANCH_NAME` is available. GitLab's default
clone is shallow (~20 commits), which leaves the merge-base with the target
branch unreachable and breaks `--git-base`'s three-dot diff — `GIT_DEPTH: 0`
forces a full-history clone, the same job GitHub Actions' `fetch-depth: 0`
does above.

```yaml
phptramp:
stage: test
image: php:8.4-cli
variables:
GIT_DEPTH: 0
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
before_script:
- git fetch --no-tags origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
- composer update --no-interaction --no-progress
script:
- >-
vendor/bin/phptramp --folder src
--changed-only --git-base "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
--format text
```

## Piping an arbitrary diff

Any unified diff works, not just `git diff <base>...HEAD` — a range, a
`.patch` file, output from another review tool. Pipe it to stdin with
`--diff -`:

```bash
git diff main...HEAD | vendor/bin/phptramp --folder src --changed-only --diff - --format json
```

`--diff <path>` reads from a file instead of stdin; either form implies
`--changed-only`, so it can be dropped when `--diff` is present.

## Full-scan gate: the pattern this repo uses

Not every project wants diff-only gating — a full-repo scan with a config-driven
threshold is simpler and catches tramp data introduced outside the diff window
(e.g. a rename that lengthens an existing chain without touching the forwarding
line itself). This repository gates itself that way: `composer tramp` runs
`bin/phptramp` against `phptramp.dist.json` (`paths: src`, `limit: 3`,
`warn-limit: 2`), and CI fails the build on any finding at or over the limit —
see the `static` job's `Dogfood` step in
[`.github/workflows/ci.yml`](../.github/workflows/ci.yml). A consuming project
gets the identical invocation from the README's `composer tramp` recipe; the
two approaches (diff-aware PR annotations, full-scan gate) are not mutually
exclusive and commonly run side by side.

## Baseline for legacy adoption

Turning phptramp on for an existing codebase full of pre-existing tramp data
means the full-scan gate above fails immediately. `--baseline`/
`--generate-baseline` — snapshot current findings once, then only fail on
*new* ones — is the answer, and ships in Phase 5. Until then, `--changed-only`
is the practical way to adopt phptramp on a legacy codebase: it never sees
pre-existing chains that a PR doesn't touch.
28 changes: 19 additions & 9 deletions docs/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -706,28 +706,38 @@ the detailed plan wins.
- [x] **3.5 SummaryReporter:** `--format=summary` — histogram of chain lengths, top 10
longest chains, most-forwarded parameters.

`--changed-only`/`--git-base` (Phase 4) and `--baseline`/`--generate-baseline` (Phase 5)
remain accepted-but-inert CLI flags — parsed into `Options` for forward compatibility,
not yet consumed by analysis. Not a Phase 3 deliverable; tracked in their own phases.
`--changed-only`/`--git-base`/`--diff` were, at this point, accepted-but-inert CLI
flags — parsed into `Options` for forward compatibility, not yet consumed by
analysis; Phase 4 below wires them up. `--baseline`/`--generate-baseline` remain
accepted-but-inert pending Phase 5.

---

## Phase 4 — Diff-aware mode (flagship)
## Phase 4 — Diff-aware mode (flagship) ✅ (issue #7)

**Deliverable:** `phptramp --changed-only --git-base origin/main` reports only chains
intersecting the diff and marks which hops are the user's.

- **4.1 DiffParser + ChangedLines:** parse unified diff from stdin (`--changed-only -`)
**Detailed step-by-step plan: [docs/plans/phase-4.md](plans/phase-4.md)** — written
against the shipped Phase 1–3 interfaces; where it refines the sketches below
(diff via `--diff <path|->` instead of a positional `-`, a `--no-config` escape
hatch, marking via immutable Finding rebuild), the detailed plan wins. Two items
are pulled forward into this phase at the maintainer's request, both shipped here
rather than in Phase 6: the repo's own `composer tramp` script + `phptramp.dist.json`,
and the CI dogfood step becoming a real gate (limit 3, warn-limit 2 — thresholds
measured on the actual tree).

- [x] **4.1 DiffParser + ChangedLines:** parse unified diff from stdin (`--changed-only -`)
or by executing `git diff --unified=0 <base>...HEAD` (note: three-dot, merge-base
semantics, matching CI expectations); produce `file -> set<line>` of *added/modified*
lines, realpath-normalized.
- **4.2 Intersection filter:** frozen rule 9 — hop matches if its declaration line or
- [x] **4.2 Intersection filter:** frozen rule 9 — hop matches if its declaration line or
its forwarding call-site line ∈ changed lines of its file. Chain reported iff ≥ 1 hop
matches.
- **4.3 Hop marking:** `Hop::isChanged` flows into every reporter; text renders
- [x] **4.3 Hop marking:** `Hop::isChanged` flows into every reporter; text renders
`hop 2 *YOURS*`, json gets `"changed": true`, github annotates changed hops as the
error location — the "your edit made this chain longer" experience.
- **4.4 CI cookbook page:** `docs/ci.md` with copy-paste GitHub Actions and GitLab
- [x] **4.4 CI cookbook page:** `docs/ci.md` with copy-paste GitHub Actions and GitLab
snippets for PR-only gating.

---
Expand Down Expand Up @@ -756,7 +766,7 @@ intersecting the diff and marks which hops are the user's.
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.
- **6.4 Release:** dogfood job gating (`--limit 3`, real), Packagist submission, tag
- **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.

Expand Down
Loading
Loading