feat(php): consolidated PHP tooling (php, artisan, phpunit, phpstan, pest, paratest, ecs, pint)#1649
Conversation
📊 Automated PR Analysis
SummaryAdds eight new rtk subcommands for PHP tooling (php, artisan, phpunit, phpstan, pest, paratest, ecs, pint) with structured output parsers and compression. Consolidates work from three prior PRs by different authors into a single module, including shared test output filtering, Composer custom-bin-dir support, and registry normalization. Review Checklist
Linked issues: #1503, #1246, #874, #1110 Analyzed automatically by wshm · This is an automated analysis, not a human review. |
|
Hi, |
Can be added maybe as a follow-up, PR is already quite big, but 100% a good idea. |
|
Added f9e139a on top of this branch: swaps the Companion fix at the recovery layer: #1696 — same head-bias bug in |
|
Tested this branch on a production Symfony monorepo (27 packages, PHP 8.4/8.5, Pest 4.6.3 + PHPStan level 9), built from source and wired through the Claude Code Works well across the toolchain:
Coverage matches our PHP stack exactly and the filtered output stays faithful to each tool's native format. Would love to see this land — happy to help test further. Thanks for the thorough work here. |
|
Really looking forward to this landing as part of rtk. It's needed to get real benefits from rtk for those of us on PHP monoliths at the moment :) Pretty please? |
|
Hi @iliaal, Since you're aggregating contributions from others in this PR, please ensure you include the "Co-authored-by:" trailer in the commit message to properly credit them. Example: This helps maintain proper attribution for all contributors. Thanks! |
|
Tried this branch locally against current PHP tool versions (Pint 1.27.1, PHPStan 2.1.40, ECS 13.0.4) — great work, the savings are substantial. I hit two parser bugs where the expected schema no longer matches what current tooling emits. Both stem from test fixtures using an older schema, so the suite stays green. Details + fixes below. 1.
{"result":"fail","files":[{"path":"app/Foo.php","fixers":["concat_space"]}]}The fields are required with no aliases, so serde rejects it and #[serde(alias = "path")]
name: String,
#[serde(rename = "appliedFixers", alias = "fixers")]
applied_fixers: Vec<String>,2.
if phpstan.totals.file_errors == 0 && phpstan.totals.errors == 0 { return "phpstan: ok".to_string(); }
// and report phpstan.totals.file_errors in the summary lineBoth fixtures set |
|
Thanks for the precise report, @evaldnet — both confirmed and fixed on this branch (184ced3).
Both ship with regression tests on the current-version schemas — the old fixtures set |
|
@EliW No. Both old and current tool output parse; I checked each schema against the new code. pint: the fix adds serde aliases, not a rename. The struct accepts both the old keys ( phpstan: no new version dependency. Safety net under both: if the JSON ever fails to parse, rtk falls back to raw tool output. You lose the compression for that run, not the result. So anyone stuck on an older phpstan or pint won't get broken output; worst case is unfiltered passthrough. |
|
Rebuilt on 184ced3 and re-ran against current versions. Both fixes hold:
One separate thing I noticed while testing: the leading
Since |
|
Fixed on the branch (bd91048). One correction on the mechanism: the regex isn't the blocker. |
…pest, paratest, ecs, pint) Consolidates the PHP-tooling work from three upstream PRs plus a new Pint module, leaving phpt to its own PR (rtk-ai#1503). - rtk php / rtk artisan: syntax check (-l) and Laravel artisan wrapper. - rtk phpunit: structured-state parser, aggregate counts, bounded failure list. Uses runner::run_filtered. - rtk phpstan: typed serde::Deserialize parser for --error-format=json, groups errors by file, sorts by count desc. Utility commands (--version, list, clear-result-cache) pass through unchanged. - rtk pest / rtk paratest: shared test_output helper. - rtk ecs / rtk pint: code-style fixers; pint uses --format=json for structured per-file rule counts. Composer custom-bin-dir detection: composer_bin_dirs() reads COMPOSER_BIN_DIR and composer.json config.bin-dir, so tools/bin/phpunit classifies identically to vendor/bin/phpunit. registry.rs normalizes tool paths before matching. Sources: - rtk-ai#1246 (aaronflorey, self-closed): php, artisan, ecs, pest, paratest, test_output, utils, composer_bin_dirs, registry normalization. - rtk-ai#874 (Beninho, open): phpunit state-machine parser. - rtk-ai#1110 (LucianoVandi, open): phpstan typed parser. - New: pint_cmd.rs. Tests: discover::registry 253 pass; cmds::php 36 pass; cargo build --release 0 errors; cargo fmt --check clean.
|
Rebased onto current
The six php-tooling commits replayed with zero conflicts. Locally: Heads up: the workflow run on the new head is sitting in |
KuSh
left a comment
There was a problem hiding this comment.
Code Review — feat(php): consolidated PHP tooling
Review performed using Claude Code (claude-sonnet-4-6) by the RTK maintainers.
This is a solid and well-scoped contribution. The PHPUnit state machine, the PHPStan errors vs file_errors distinction, and the Pint dual-schema handling via serde aliases are all done correctly and are well-tested. The php_tool_command() abstraction for Composer-aware binary resolution is the right design.
That said, there are three blocking bugs and two maintainability issues to address before merge.
🔴 Blockers
1. filter_phpstan_text: "not found" is dangerously broad (inline comment on phpstan_cmd.rs:165)
2. phpstan_cmd.rs bypasses php_tool_command() (inline comment on phpstan_cmd.rs:48)
3. is_numbered_failure_heading() has false positives (inline comment on phpunit_cmd.rs:89)
🟡 Should-fix
4. compact_php_path() is Laravel-only (inline comment on phpstan_cmd.rs:192)
5. short_path() calls current_dir() in a per-file loop (inline comment on pint_cmd.rs:126)
What's working well
- PHPStan
errorsvsfile_errorsbug is correctly identified and tested (test_filter_phpstan_file_errors_not_hidden) — this was a real prior bug fixed here - PHPUnit state machine correctly handles success, multi-failure, errors, skipped, and truncation cases with 9 test cases
- Pint dual-schema serde aliases (
name/appliedFixersvspath/fixers) cleanly handle the Pint ≥1.14 rename php_tool_command()centralises Composer bin-dir resolution correctly — pity PHPStan doesn't use it (see inline comment)- Token savings are verified against a real 380-line
phpstan_raw.jsonfixture, not synthetic data - No
unwrap()in production paths, fallback pattern respected throughout
short_path() called current_dir() on every file (up to MAX_FILES_SHOWN per invocation). Hoist the cwd prefix out of the loop and strip it inline.
…ewrite The six Composer-tool rules had inconsistent patterns, and their rewrite_prefixes only worked because classify normalizes the command while rewrite stripped literal prefixes off the raw text. A form the pattern accepted but the prefix list missed (e.g. ./bin/phpunit) would classify yet silently fail to rewrite. rewrite_segment_inner now normalizes the leading invocation for these tools (php wrapper, ./, vendor/bin, composer bin-dir) the same way classify does, so the prefix list collapses to the residual canonical forms: bin/<tool> + bare name for phpunit/phpstan, bare name for pest/paratest/ecs/pint. Patterns standardized to ^(?:php\s+)?(?:\./)?(?:(?:vendor/)?bin/)?<tool> for phpunit/phpstan and ^(?:\./)?(?:vendor/bin/)?<tool> for the rest. Adds a form-coverage test asserting every accepted spelling maps to one rewrite.
PHP-CS-Fixer omits the fixers key in dry-run/diff modes (it is optional
in the JSON reporter), so a valid report like {"files":[{"name":"x.php"}]}
failed the whole parse and fell back to raw output. Mark the field
#[serde(default)] so a missing key deserializes to an empty vec.
filter_phpunit_output matched on raw bytes, so `--colors=always` output
(colorized "OK ("/"FAILURES!"/"Tests:" lines) defeated every anchor and
counts were lost. Strip ANSI first, mirroring the other PHP filters.
Also report PHPUnit errors (thrown exceptions) distinctly from failures
(assertion mismatches) instead of lumping both under "failures".
…format `phpstan -c phpstan.neon analyse src/` put a global option before the subcommand, so the first-arg-only check missed it and the run fell back to unfiltered passthrough. Scan all args for analyse/analyze. Replace the has_custom_format bool with a 3-state ErrorFormat enum so an explicit `--error-format=json` is preserved without appending a second `--error-format json`. Strip ANSI in the text fallback for `--ansi`.
Pest has no root `pest.php` file — its bootstrap lives at tests/Pest.php and its canonical marker is the vendor/bin/pest binary. The root-level check never matched real Pest projects and false-positived on unrelated utility files, misrouting PHPUnit-only projects to the Pest filter.
Matching only "by Sebastian Bergmann" misrouted any LICENSE, composer metadata, or git log mentioning the author to the phpunit filter. Require the input to start with "PHPUnit " as well.
composer_bin_dirs() re-read composer.json and the env on every call; the rewrite hot path queries it several times per command segment via normalize_php_tool_command (both classify_command and rewrite_segment_inner). Resolution is constant for a process, so cache it in a OnceLock.
KuSh
left a comment
There was a problem hiding this comment.
Could you add tests for paratest? That’s what’s causing the CI failure. See .claude/rules/cli-testing.md for the full testing guide. After that, LGTM!
paratest_cmd.rs lacked the mandatory #[cfg(test)] module, failing the check-test-presence CI gate. Add tests exercising the ParaTest-specific path in filter_test_runner_output: banner + "Random Seed:" + dot progress stripped, result summary and failure details preserved.
|
Added in Verified locally: |
# Conflicts: # src/main.rs
.claude/rules/cli-testing.md marks token-savings verification as a 🔴 requirement for filters. Add a ≥60% savings assertion over a realistic paratest run (banner + config + parallel-worker progress → one-line summary), alongside the existing behavioral tests.
|
LGTM, thanks for all the work! |
Summary
Consolidates the open and closed PHP-tooling work into a single PR against
develop, omitting.phpt(which has its own PR #1503).This PR adds eight
rtksubcommands for PHP test runners and code tooling:php,artisan,phpunit,phpstan,pest,paratest,ecs,pint. Output compression in the 60% to 95% range per tool, structured parsers where possible (PHPUnit state machine, PHPStan typed JSON, Pint per-file rule counts).Credits to original authors
php,artisan,ecs,pest,paratest,test_output,utils,composer_bin_dirs, registry normalization for Composer custom-bin-dir layouts.phpunitstate-machine parser. I ported it into the consolidated module, switched it torunner::run_filtered, and stripped emoji from output.phpstantyped serde::Deserialize parser for--error-format=json. Groups errors by file, sorts by count descending; passes utility commands (--version,list,clear-result-cache) through unchanged. I stripped emoji from the success output.pint(Laravel Pint code-style fixer) using--format=jsonfor structured per-file rule counts.If the maintainers prefer this consolidation, close the three separate PRs in favor of this one. If they prefer the originals, close this PR and let the per-tool PRs continue independently. Either is fine with me.
Composer custom-bin-dir support
composer_bin_dirs()readsCOMPOSER_BIN_DIRandcomposer.json'sconfig.bin-dir, sotools/bin/phpunitclassifies identically tovendor/bin/phpunit.registry.rsnormalizes tool paths before matching, so a single rule covers the standard Composer layouts.