Reports/Ctrf: add CTRF reporter#1420
Conversation
Adds a new `ctrf` report which emits results in the Common Test Report Format (https://ctrf.io/), an open JSON standard for test results. Each sniff violation maps to a CTRF test entry: ERROR violations have status "failed" and WARNING violations have status "other". Files with no violations emit a single "passed" test so consumers see the full set of files that were processed. PHPCS-native metadata (sniff source, severity, fixability) is preserved via CTRF's rawStatus, tags, and extra fields. The output validates against the CTRF schema and works in both sequential and parallel scanning modes. Use `--report=ctrf` for stdout, or `--report-ctrf=/path` to write to a file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Use `2>/dev/null` instead of `2>&1` so PHPCS's "Time: Xms; Memory: YMB" stderr line doesn't pollute the JSON output captured for validation. - Add `--no-colors` to match the convention of the existing E2E tests and guarantee no ANSI codes leak into the JSON. - Quote variable expansions (was triggering bashunit warnings on certain paths). - Suppress ShellCheck SC2016 on `php -r '...'` blocks where the single quotes are intentional (PHP, not bash, is the consumer). - Add explicit assertions to test_phpcs_ctrf_report_to_file so bashunit no longer flags it as risky (test had no direct assert call). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
remark-lint's `no-undefined-references` rule (with `fail-on-warnings: true` in CI) flagged `## [Unreleased]` as a reference to an undefined link definition. The brackets were a Keep-a-Changelog convention not followed elsewhere in this file. Drop them to keep the heading as plain text. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Fix Coverage: 7.2 (Linux) failure: assertMatchesRegularExpression() was added in PHPUnit 9.1, but assertRegExp() was removed in PHPUnit 10. PHPCS supports both. Use the same runtime feature-detection pattern the codebase already uses elsewhere (StatusWriterTestHelper.php, Generators/HTMLTest.php) via a small `assertRegex()` helper. - Add testNonUtf8EncodingIsTranscoded covering the iconv fallback path for messages from non-UTF-8 source files. - Add a comment in generate() explaining why `passed` is counted from the cached partials while `failed`/`other` come from the parameters (the asymmetry is deliberate: prefer authoritative sources where PHPCS provides them; fall back to scanning only when it doesn't). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@Luc45 I appreciate what you are trying to do, but AI usage is not acceptable for this repo. |
I've manually reviewed and verified every single line written here, and tested it exhaustively against real, large codebases, both serial mode and parallel, and I'm very confident in this implementation. Apart from the tools that I used to write the code, is there any concern with the implementation? |
|
I can also push this as a separate packagist library if this is a hard rule. But honestly, I think AI is just one more tool in the stack - it depends on how conscious you are while using it. |
|
I've published it as a standalone package for now https://github.com/Luc45/phpcs-ctrf-reporter Let me know if you'd like to integrate it into PHPCS Core and I'll gladly do it. Feel free to close this PR or move forward with merging it, your call. Thanks for all your work, Juliette! |
|
I'll be closing this PR now, pending your decision. |
|
@Luc45 No-AI use is a hard rule, which is not open for discussion. There are too much ethical problems, legal risks and threats to the longevity of OSS, that accepting any AI-generated code would be an existential threat to any OSS codebase and I see it as my duty as custodian of this project to protect the users of this package against that. Happy to see that the report will be available to users via the package you published, but due to the use of AI, I cannot accept it in PHPCS itself. Thank you for contributing to the larger ecosystem by publishing it separately. |
Description
Adds a new
ctrfreport which emits results in the Common Test Report Format (CTRF), an open JSON standard for test results. The format is consumed by a growing ecosystem of CI/CD integrations (GitHub Actions summaries, dashboards, etc.), letting PHPCS lint runs feed into the same reporting pipelines as test runs.JUnit already serves a similar purpose, but JUnit XML is older, has looser semantics, and isn't as well-supported by modern CI surfaces. CTRF is a tighter JSON schema that's validatable end-to-end.
Minimal example
Given this fixture (
example.php):Running
bin/phpcs --no-colors --basepath=. --standard=PSR12 --report=ctrf example.phpproduces (verbatim):{ "reportFormat": "CTRF", "specVersion": "1.0.0", "reportId": "c236c666-f286-457f-82da-b5074eaec7ec", "timestamp": "2026-04-28T13:26:26Z", "generatedBy": "PHP_CodeSniffer 4.0.2", "results": { "tool": { "name": "PHP_CodeSniffer", "version": "4.0.2" }, "summary": { "tests": 1, "passed": 0, "failed": 1, "skipped": 0, "pending": 0, "other": 0, "suites": 1, "start": 1777382786761, "stop": 1777382786761, "duration": 0, "extra": { "fixable": 1 } }, "tests": [ { "name": "Squiz.Functions.MultiLineFunctionDeclaration.BraceOnSameLine at example.php (7:34)", "status": "failed", "duration": 0, "start": 1777382786761, "stop": 1777382786761, "message": "Opening brace should be on a new line", "filePath": "example.php", "line": 7, "suite": [ "example.php" ], "rawStatus": "ERROR", "type": "lint", "tags": [ "ERROR", "fixable" ], "extra": { "column": 34, "severity": 5, "fixable": true, "source": "Squiz.Functions.MultiLineFunctionDeclaration.BraceOnSameLine" } } ] } }How violations map to CTRF
Each sniff violation produces one CTRF test entry (mirroring the JUnit reporter's per-violation pattern):
status: "failed"status: "other"(CTRF's catch-all for non-pass/non-fail outcomes; the ERROR/WARNING distinction is preserved inrawStatus,tags, andextra.severity)status: "passed"test entry, so the report contains the full set of files that were processedPHPCS-native metadata (sniff source, severity, fixability, column) is preserved via CTRF's
rawStatus,tags, andextraextension fields. The output passesnpx --package=ctrf-cli@0.0.5 -- ctrf-cli validate <file>.Use
--report=ctrffor stdout, or--report-ctrf=/path/to/file.jsonto write to a file. Composes with other reporters (--report=ctrf,summary).Manual testing
Beyond the unit and end-to-end tests added in this PR, I validated the reporter against a real-world codebase: WooCommerce's
plugins/woocommerce/src/Internal/directory (443 PHP files, ~10MB of source) using the PSR12 standard. The same scan was run with--report=jsonand--report=ctrf, and in both serial and parallel (--parallel=8) modes.ctrf-cli validate+validate-strict)Equivalence checks performed:
ctrf-cli validateANDctrf-cli validate-strict(the strict variant enforcesadditionalProperties: false, so no extra fields anywhere). Verified on both serial and parallel outputs.summary.testsequals the actualtestsarray length in both runs (136,749).(status, name, line, rawStatus, source)); identical per-file violation counts. The fork-merge pattern preserves every entry without drops or duplicates.failed+ 2,611other= 136,749 tests ✓Edge cases also verified:
json_encode).JSON_INVALID_UTF8_SUBSTITUTEto prevent silent test drops; covered bytestInvalidUtf8DoesNotDropTest.--report=ctrf,summary).--report-ctrf=/path).Suggested changelog entry
Related issues/external references
CTRF spec: https://ctrf.io/
Types of changes
PR checklist