Skip to content

Phase 2: chain stitching → usable tool - #4

Merged
larspohlmann merged 11 commits into
developfrom
feature/3-chain-stitching
Aug 11, 2026
Merged

larspohlmann merged 11 commits into
developfrom
feature/3-chain-stitching

Conversation

@larspohlmann

Copy link
Copy Markdown
Owner

Implements Phase 2 (docs/plans/phase-2.md): cross-file tramp-data chain stitching. phptramp --folder src --limit 3 now stitches forwarding chains across files, prints text findings, and exits 0/1/2.

Closes #3

What landed (one commit per task, strict fixture-first TDD)

  • ClassKind on ClassInfo — the resolver can tell a concrete class from an interface/abstract type.
  • ClassHierarchy — trait flattening, parent-chain method lookup, and implementationsOf across the whole index (with cycle guards). Adds an additive MethodIndex::allClasses().
  • CallResolver (+ ReceiverTyper, Resolution/ResolvedTarget/ExternalTarget/TruncatedResolution) — resolves a ForwardSite per the frozen rules 1–4.
  • storedOnly on ParamInfo — classifies $this->x = $p stores (and promoted properties) so the chain terminal reads stored.
  • ChainBuilder — walks the forward graph from origins (and entry-less cycles) to terminals, one Finding per root-to-terminal path.
  • TextReporter, default pipeline, exit codes — aligned text output; non-text formats exit 2 until Phase 3.
  • --explain — per-edge resolution traces.
  • End-to-end fixtures + harness (expected-findings.json) and a report-only dogfood CI step.

Notable decisions (flagged in commit bodies)

  • The phase-2 sketch said static calls put the class in CalleeRef->name; Phase 1 actually stores the method there and the class in receiverHint, and leaves same-namespace function names unqualified — the resolver follows real Phase 1 output.
  • MethodIndex::allClasses() was added (not in the sketch) because implementationsOf must enumerate method-less marker interfaces/DTOs.
  • The reporter uses a uniform two-space column separator (the plan's byte example incidentally showed three before the terminal note); output is pinned byte-for-byte.

Verification

  • composer check green (154 tests, 431 assertions).
  • Diff-scoped Infection 93% MSI (gate is 80) — every non-equivalent mutant killed; the rest are equivalent/defensive.
  • Dogfooded on its own src/ end-to-end.

Performance (memoizing resolve(), an inverted implementations index) is intentionally deferred to Phase 6.

…okup

Adds MethodIndex::allClasses() (additive) so ClassHierarchy can enumerate
method-less marker interfaces and DTOs when computing implementationsOf.
Resolves a ForwardSite to ResolvedTarget/ExternalTarget/TruncatedResolution
per the frozen rules. ReceiverTyper owns hint->FQCN. Note: Phase 1 stores the
method name in CalleeRef->name and the class in receiverHint for static calls,
and leaves same-namespace function names unqualified, so function resolution
tries the namespaced candidate before the global one.
ForwardCollector now tracks whether every use of a parameter is a
$this->prop = $p store; ParamInfo gains storedOnly (also true for promoted
constructor properties), which ChainBuilder maps to the 'stored' terminal.
Walks the forward graph from origins (and entry-less cycles) to terminals,
emitting one Finding per root-to-terminal path. The walk, path accumulation
(PartialChain), and terminal descriptor (Terminal) are separate small classes,
with a per-run ChainTraversal, to keep each PHPMD-clean and honor the
few-parameters rule as the plan anticipates.
Default invocation now runs FileLocator -> Indexer -> resolver -> ChainBuilder,
filters hops >= limit, renders aligned text, and returns 1/0. Non-text formats
exit 2 until Phase 3. TextReporter uses a uniform two-space column separator (the
plan's byte example incidentally shows three spaces before the terminal note).
With --explain each finding is followed by its per-edge resolution trace under
an 'explain:' header; without the flag the output is byte-for-byte unchanged.
FixtureTest gains a findings harness (expected-findings.json, chain flattened
to FQMNs). New fixtures pin one frozen-semantics rule each end-to-end:
use-breaks-chain, interface-single-impl, interface-multi-impl (truncation +
note), variadic-decorator, recursion, static-function-mix; classifier-smoke
gains its expected-findings.json.
Adds a report-only dogfood step (bin/phptramp --folder src --limit 99) to the
static CI job, updates README status to Phase 2 and the example to the real hop
labels, and checks off Phase 2 in docs/plan.md and docs/plans/phase-2.md.
Raises diff-scoped MSI from 84% to 94% by pinning behaviours the suite left
unverified: real resolution-trace strings, static::/parent:: and function-caller
resolution, static calls to unknown classes, the union truncation reason,
proper-origin detection when the origin is declared last, used-terminals that
also forward, the cyclic-hierarchy guard, and storedOnly on the closure/arrow/
by-ref/store-and-forward paths. The remaining escaped mutants are equivalent
(bijective key reorderings, in-file sort-index fall-through, class/fqmn yielding
the same namespace) or defensive-unreachable branches.
Applies /simplify cleanup (behaviour-preserving):
- Hoist the thrice-duplicated 'find parameter by name' loop onto
  MethodInfo::paramNamed(); CallResolver, ReceiverTyper and ChainTraversal
  now delegate to it.
- Make Resolution behavioural (describe()) so ChainTraversal::traceTarget()
  and its dead defensive 'unknown' branch disappear and a future fourth
  Resolution type becomes a compile error rather than a silent fallback.
- Drop the unused $hops parameter from TextReporter::label() and extract a
  shared labelledLine() so note rows and hop rows format through one path.

Deferred to Phase 6 (performance): memoizing CallResolver::resolve() (each
forward is resolved in both incomingTargets() and walk()) and an inverted
implementations index in ClassHierarchy.
@larspohlmann
larspohlmann merged commit 6c60774 into develop Aug 11, 2026
5 checks passed
@larspohlmann
larspohlmann deleted the feature/3-chain-stitching branch August 11, 2026 08:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Phase 2 — Chain stitching: cross-file findings, text output, exit codes

1 participant