Repository navigation
feat(#11): per-file index cache, PhpStorm recipe, v0.1.0 docs - #12
Merged
Merged
Conversation
Cache-and-release phase against the shipped Phase 5 interfaces. The headline decision is evidence-based: cold indexing measured at ~19.6s for the 600k-LOC vendor tree (~1.6s per 50k LOC), so the per-file cache alone meets the warm-run IDE target and parallel indexing is deferred to the post-v0.1 roadmap with the numbers recorded — per the master plan's own 'performance last, when profiling has real data' rule. The Indexer refactor into per-file FileIndexer/FileIndex units is a pure refactor gated on byte-identical composer tramp output; the cache treats every failure as a silent miss so it can never change findings. Release follows CLAUDE.md's git-flow tag-triggered workflow literally, with Packagist submission marked as a maintainer-only action.
Split the single accumulating IndexingVisitor traversal into per-file units: FileIndexer parses and classifies one file with a fresh visitor and traverser, producing a serializable FileIndex; Indexer becomes a merge orchestrator that combines per-file results in input order (last FQMN/FQCN wins, suppression parts concatenated) and builds one SuppressionIndex from the concatenation. No behaviour change — the 374-test suite plus 5 new FileIndexerTest cases pass and composer tramp is byte-identical to the captured baseline (sha 07982cd5). IndexingVisitor::suppressions() and SuppressionCollector::suppressions() become dead code and are removed; SuppressionCollector::parts() exposes the raw three-part shape the Indexer now merges. The dead @see IndexingVisitor::suppressions() in SuppressionIndex.php is repointed to Indexer, and two accidental // phptramp-ignore markers in docblock documentation (which silently created stale line suppressions) are reworded out of the prose.
Adds FileIndexCache, a transparency-critical per-source-file cache for serialized FileIndex payloads. Entries are keyed by sha1(realpath), and every read validates format, Application::VERSION, source path, mtime, and size — any drift, corrupt payload, or unreadable entry degrades to a silent miss (null), never an exception. put() creates the directory lazily and ignores write failures (unwritable dir, full disk) so the cache can never block analysis. unserialize() is scoped to an explicit allowed_classes whitelist of the FileIndex value-object set; no arbitrary object can be materialized from a tampered entry.
Wires Task 2's FileIndexCache into the CLI as the default-on per-file index cache at `.phptramp.cache/` (overridable via the `cache` config key, disabled via `--no-cache`). No analysis semantics change: `composer tramp` self-gate stdout stays byte-identical (sha256 07982cd5...) across cold, warm, and `--no-cache` runs. - Options gains readonly `noCache` (default false) and `cacheDir` (default `.phptramp.cache`) at the end of the constructor so existing positional args stay stable. - `--no-cache` is a BoolFlags unit member (keeps ArgvParser under PHPMD's field-count ceiling); no `--cache` CLI value flag — the dir only comes from config or the default, per the brief. - ConfigLoader maps `cache` (string) to `cacheDir`; non-string throws ConfigException (same shape as baseline/format). Path is not resolved, matching `exclude`. - Indexer takes an optional `?FileIndexCache` (default null = uncached); every existing `new Indexer()` caller is unaffected. On a hit the parse is skipped; on a miss parse + put. mtime/size identity invalidates on edit, so the aggregate-ParseException contract is unchanged. - IndexerFactory (new, 1-method final class) builds the Indexer from Options; extracted out of Application because the on/off ternary tipped Application's class-complexity to PHPMD's 50 threshold. - FixtureTest runs with --no-cache so fixtures stay hermetic (they pin semantics, not cache behavior) and don't litter .phptramp.cache/ into committed fixture dirs. Manual sanity (bin/phptramp --folder vendor --limit 99 --format summary): cold 19.35s (4610 files parsed) -> warm 4.46s (4610 cache hits) Indexing drops to ~0 warm; the residual 4.46s is the unchanged chain-resolution + reporting pipeline. Cold vs warm stdout byte-identical.
array_merge rebuilds the whole accumulated method/class map on every iteration, so merging F files is O(total entries^2). Append each file's entries in place instead — O(total), same later-file-wins semantics (keys are FQMN/FQCN/file paths, never numeric, so no renumbering).
get() stat-ed the source file before checking whether a cache entry even existed, so every file on a cold run paid a stat it never used. Read the entry first and stat only once a payload is in hand; a file with no entry now skips the syscall entirely. Pure reorder — behaviour is identical on every path.
…e object
The suppression trio (methods, params, ignore lines) travelled together as
three loose parallel fields on FileIndex, an array{methods, params, lines}
shape redeclared in SuppressionCollector and IndexingVisitor, and three
accumulators in Indexer::index() — only ever to build one SuppressionIndex.
Bundle them into a SuppressionParts value object that owns the merge
(concatenate methods and params, union ignore lines by file) and the
SuppressionIndex construction. FileIndex drops from five fields to three,
the redeclared array shape collapses to a type, and Indexer folds a list
of parts in one pass instead of maintaining three accumulators.
FileIndex now nests a SuppressionParts, so the cache learns the new class
(ALLOWED_CLASSES) and bumps FORMAT 1->2; entries written under the old
format silently miss and re-parse.
The anchored /.phptramp.cache/ pattern missed cache dirs the tool writes under subdirectories — e.g. tests/fixtures/config-driven/.phptramp.cache/ left behind by a config-driven fixture run. Unanchor it so every cache dir is ignored wherever phptramp creates one.
IndexerFactory existed only to hold one ternary and keep it out of Application, but it never grew past a thin wrapper — unlike its sibling ReporterFactory, which genuinely dispatches over six report formats. That ternary needed its own home only because dumpIndex duplicated buildIndex's locate-then-index body, so the indexer was built at two sites. Make dumpIndex delegate to buildIndex; construction collapses to one site where the cache decision reads inline. PHPMD stays green because Application gains one branch while dumpIndex sheds the duplicated one, so the codesize ceiling the factory was routing around is never reached.
testGetReturnsNullWhenSourceSizeChanges restores the source mtime to the second the file already carries — a no-op touch. On PHP 8.2 a no-op touch does not flush PHP's stat cache, so get()'s stat reported the pre-write size and the entry looked current: a false pass on platforms that flush anyway (8.3/8.4) and a hard failure on 8.2. Flush the stat cache explicitly so the stat reflects the grown file, exactly as the fresh process a real re-run gets. Production is unaffected: indexFile only ever get()s a file before it put()s it, never re-statting its own write.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #11.
Phase 6: a default-on per-file index cache, the PhpStorm IDE recipe, docs polish, and the v0.1.0 release prep.
Index cache
FileIndexerparses and classifies one source file into a self-contained, serializableFileIndex, using a fresh visitor per call — the seam that makes a file independently cacheable.FileIndexCachepersists a serializedFileIndexkeyed by the source's real path, invalidated by mtime/size/app-version/payload-format. Transparency is total: every failure path is a silent miss, never an exception, and the cache never changes the merged result..phptramp.cache/, with--no-cacheand acacheconfig key to relocate it.Docs / IDE
docs/phpstorm.md) and a docs release pass.docs/plan.md.Quality pass (this branch's later commits)
array_mergein a loop (drops an O(n²) on the warm path).stat()until a cache entry is actually found.SuppressionPartsvalue object —FileIndexdrops from five fields to three, andIndexer::index()folds a list of parts in one pass instead of three parallel accumulators.IndexerFactory:dumpIndex()now delegates tobuildIndex(), so the cache decision reads inline at one site (PHPMD stays green)..phptramp.cache/at any depth, not just the repo root.Verification
composer checkgreen (cs + stan + md + test) — 403 tests.composer infection:diffgreen — MSI 94% (gate is 80), 100% mutation code coverage.