From a62df761f5c17bf4734b40b3aae9f57e6d10b9b8 Mon Sep 17 00:00:00 2001 From: Mazen Touati Date: Tue, 30 Sep 2025 15:05:42 +0200 Subject: [PATCH 1/2] chore: ignore test results --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 06df57b..e71faaa 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ composer.lock .php_cs .php_cs.cache .phpunit.result.cache +.phpunit.cache/test-results build coverage From 07d8f8c137da4835d3db74792cd2078fa8bc09fb Mon Sep 17 00:00:00 2001 From: Mazen Touati Date: Tue, 30 Sep 2025 15:20:02 +0200 Subject: [PATCH 2/2] feat: add `json` formatter --- src/Application/Formatters/FormatResolver.php | 1 + src/Application/Formatters/JsonFormatter.php | 45 +++++ src/Domain/Violations/Violation.php | 18 +- tests/Unit/Formatters/FormatResolverTest.php | 5 + tests/Unit/Formatters/JsonFormatterTest.php | 188 ++++++++++++++++++ .../stubs/json-empty-violations.json | 9 + .../stubs/json-mixed-violations.json | 34 ++++ .../Formatters/stubs/json-single-error.json | 18 ++ .../stubs/json-violation-without-line.json | 18 ++ 9 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 src/Application/Formatters/JsonFormatter.php create mode 100644 tests/Unit/Formatters/JsonFormatterTest.php create mode 100644 tests/Unit/Formatters/stubs/json-empty-violations.json create mode 100644 tests/Unit/Formatters/stubs/json-mixed-violations.json create mode 100644 tests/Unit/Formatters/stubs/json-single-error.json create mode 100644 tests/Unit/Formatters/stubs/json-violation-without-line.json diff --git a/src/Application/Formatters/FormatResolver.php b/src/Application/Formatters/FormatResolver.php index f2b0513..57d5947 100644 --- a/src/Application/Formatters/FormatResolver.php +++ b/src/Application/Formatters/FormatResolver.php @@ -12,6 +12,7 @@ class FormatResolver /** @var array> */ public const FORMATTERS = [ 'github' => GithubFormatter::class, + 'json' => JsonFormatter::class, 'table' => TableFormatter::class, ]; diff --git a/src/Application/Formatters/JsonFormatter.php b/src/Application/Formatters/JsonFormatter.php new file mode 100644 index 0000000..db6344e --- /dev/null +++ b/src/Application/Formatters/JsonFormatter.php @@ -0,0 +1,45 @@ +output = $output; + } + + public function format(ViolationsCollection $violations): void + { + $report = [ + 'package' => 'phecks', + 'violations' => $violations->toArray(), + 'summary' => $this->buildSummary($violations), + 'total' => $violations->count(), + ]; + + $this->output->writeln(json_encode($report, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT)); + } + + /** + * @return array{errors: int, warnings: int} + */ + private function buildSummary(ViolationsCollection $violations): array + { + $groupedBySeverity = $violations->groupBy->getSeverity(); + + return [ + 'errors' => $groupedBySeverity->get(ViolationSeverity::ERROR)?->count() ?? 0, + 'warnings' => $groupedBySeverity->get(ViolationSeverity::WARNING)?->count() ?? 0, + ]; + } +} diff --git a/src/Domain/Violations/Violation.php b/src/Domain/Violations/Violation.php index 1baf5e4..811f146 100644 --- a/src/Domain/Violations/Violation.php +++ b/src/Domain/Violations/Violation.php @@ -2,9 +2,10 @@ namespace Juampi92\Phecks\Domain\Violations; +use Illuminate\Contracts\Support\Arrayable; use Juampi92\Phecks\Domain\DTOs\FileMatch; -class Violation +class Violation implements Arrayable { private string $identifier; @@ -85,4 +86,19 @@ public function setSeverity(string $severity): self return $this; } + + /** + * @return array{identifier: string, file: string, line: int|null, message: string, url: string|null, severity: string} + */ + public function toArray(): array + { + return [ + 'identifier' => $this->getIdentifier(), + 'file' => $this->getTarget(), + 'line' => $this->getLine(), + 'message' => $this->getMessage(), + 'url' => $this->getUrl(), + 'severity' => $this->getSeverity(), + ]; + } } diff --git a/tests/Unit/Formatters/FormatResolverTest.php b/tests/Unit/Formatters/FormatResolverTest.php index 3af35ab..1971123 100644 --- a/tests/Unit/Formatters/FormatResolverTest.php +++ b/tests/Unit/Formatters/FormatResolverTest.php @@ -4,6 +4,7 @@ use Juampi92\Phecks\Application\Formatters\FormatResolver; use Juampi92\Phecks\Application\Formatters\GithubFormatter; +use Juampi92\Phecks\Application\Formatters\JsonFormatter; use Juampi92\Phecks\Application\Formatters\TableFormatter; use Juampi92\Phecks\Tests\Unit\TestCase; use Mockery; @@ -39,6 +40,10 @@ public static function formatterDataProvider(): array 'formatter' => 'github', 'expected' => GithubFormatter::class, ], + 'json' => [ + 'formatter' => 'json', + 'expected' => JsonFormatter::class, + ], ]; } diff --git a/tests/Unit/Formatters/JsonFormatterTest.php b/tests/Unit/Formatters/JsonFormatterTest.php new file mode 100644 index 0000000..c63f595 --- /dev/null +++ b/tests/Unit/Formatters/JsonFormatterTest.php @@ -0,0 +1,188 @@ +loadStub('json-empty-violations.json'); + + // Act + + $formatter->format($violations); + + // Assert + + $actualReport = json_decode($output->fetch(), true); + + $this->assertEquals($expectedReport, $actualReport); + } + + public function test_can_render_single_error_violation(): void + { + // Arrange + + $formatter = new JsonFormatter( + new ArrayInput([]), + $output = new BufferedOutput(), + ); + + $violations = new ViolationsCollection([ + new Violation( + 'MyError', + new FileMatch('./app/FileOne.php', 15), + 'Testing error', + 'https://www.foo.bar' + ), + ]); + + $expectedReport = $this->loadStub('json-single-error.json'); + + // Act + + $formatter->format($violations); + + // Assert + + $actualReport = json_decode($output->fetch(), true); + + $this->assertEquals($expectedReport, $actualReport); + } + + public function test_can_render_multiple_violations_with_mixed_severities(): void + { + // Arrange + + $formatter = new JsonFormatter( + new ArrayInput([]), + $output = new BufferedOutput(), + ); + + $violations = new ViolationsCollection([ + new Violation( + 'FirstError', + new FileMatch('./app/FileOne.php', 15), + 'First error message', + 'https://docs.example.com' + ), + new Violation( + 'FirstWarning', + new FileMatch('./app/FileTwo.php', 30), + 'First warning message', + null, + ViolationSeverity::WARNING + ), + new Violation( + 'SecondError', + new FileMatch('./app/FileThree.php', 45), + 'Second error message', + null, + ViolationSeverity::ERROR + ), + ]); + + $expectedReport = $this->loadStub('json-mixed-violations.json'); + + // Act + + $formatter->format($violations); + + // Assert + + $actualReport = json_decode($output->fetch(), true); + + $this->assertEquals($expectedReport, $actualReport); + } + + public function test_can_render_violation_without_line_number(): void + { + // Arrange + + $formatter = new JsonFormatter( + new ArrayInput([]), + $output = new BufferedOutput(), + ); + + $violations = new ViolationsCollection([ + new Violation( + 'NoLineError', + new FileMatch('./app/FileOne.php'), + 'Error without line number', + null + ), + ]); + + $expectedReport = $this->loadStub('json-violation-without-line.json'); + + // Act + + $formatter->format($violations); + + // Assert + + $actualReport = json_decode($output->fetch(), true); + + $this->assertEquals($expectedReport, $actualReport); + } + + public function test_output_is_valid_json(): void + { + // Arrange + + $formatter = new JsonFormatter( + new ArrayInput([]), + $output = new BufferedOutput(), + ); + + $violations = new ViolationsCollection([ + new Violation( + 'TestError', + new FileMatch('./app/Test.php', 10), + 'Test message', + null + ), + ]); + + // Act + + $formatter->format($violations); + + $jsonOutput = $output->fetch(); + + // Assert + + $this->assertNotFalse(json_decode($jsonOutput)); + + $this->assertEquals(JSON_ERROR_NONE, json_last_error()); + } + + /* + * Helpers. + */ + + private function loadStub(string $filename): array + { + $content = file_get_contents(__DIR__ . '/stubs/' . $filename); + + return json_decode($content, true); + } +} diff --git a/tests/Unit/Formatters/stubs/json-empty-violations.json b/tests/Unit/Formatters/stubs/json-empty-violations.json new file mode 100644 index 0000000..c06366b --- /dev/null +++ b/tests/Unit/Formatters/stubs/json-empty-violations.json @@ -0,0 +1,9 @@ +{ + "package": "phecks", + "violations": [], + "summary": { + "errors": 0, + "warnings": 0 + }, + "total": 0 +} \ No newline at end of file diff --git a/tests/Unit/Formatters/stubs/json-mixed-violations.json b/tests/Unit/Formatters/stubs/json-mixed-violations.json new file mode 100644 index 0000000..5753222 --- /dev/null +++ b/tests/Unit/Formatters/stubs/json-mixed-violations.json @@ -0,0 +1,34 @@ +{ + "package": "phecks", + "violations": [ + { + "identifier": "FirstError", + "file": "./app/FileOne.php", + "line": 15, + "message": "First error message", + "url": "https://docs.example.com", + "severity": "error" + }, + { + "identifier": "FirstWarning", + "file": "./app/FileTwo.php", + "line": 30, + "message": "First warning message", + "url": null, + "severity": "warning" + }, + { + "identifier": "SecondError", + "file": "./app/FileThree.php", + "line": 45, + "message": "Second error message", + "url": null, + "severity": "error" + } + ], + "summary": { + "errors": 2, + "warnings": 1 + }, + "total": 3 +} \ No newline at end of file diff --git a/tests/Unit/Formatters/stubs/json-single-error.json b/tests/Unit/Formatters/stubs/json-single-error.json new file mode 100644 index 0000000..682c642 --- /dev/null +++ b/tests/Unit/Formatters/stubs/json-single-error.json @@ -0,0 +1,18 @@ +{ + "package": "phecks", + "violations": [ + { + "identifier": "MyError", + "file": "./app/FileOne.php", + "line": 15, + "message": "Testing error", + "url": "https://www.foo.bar", + "severity": "error" + } + ], + "summary": { + "errors": 1, + "warnings": 0 + }, + "total": 1 +} \ No newline at end of file diff --git a/tests/Unit/Formatters/stubs/json-violation-without-line.json b/tests/Unit/Formatters/stubs/json-violation-without-line.json new file mode 100644 index 0000000..699a336 --- /dev/null +++ b/tests/Unit/Formatters/stubs/json-violation-without-line.json @@ -0,0 +1,18 @@ +{ + "package": "phecks", + "violations": [ + { + "identifier": "NoLineError", + "file": "./app/FileOne.php", + "line": null, + "message": "Error without line number", + "url": null, + "severity": "error" + } + ], + "summary": { + "errors": 1, + "warnings": 0 + }, + "total": 1 +} \ No newline at end of file