Skip to content

Commit 71e15f1

Browse files
Add GitLab output format support to CLI.
Introduced the "gitlab" format for CLI output, alongside existing "text" and "json" formats. This includes a new `GitlabPrinter` implementation, updates to the `PrinterFactory` and output option handling, as well as corresponding unit and E2E tests. The change enhances integration options, particularly for GitLab CI.
1 parent 47a98b9 commit 71e15f1

File tree

10 files changed

+182
-6
lines changed

10 files changed

+182
-6
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@
5050
"autoload-dev": {
5151
"psr-4": {
5252
"Arkitect\\Tests\\": "tests/"
53-
}
53+
},
54+
"classmap": ["tests/E2E/_fixtures/"]
5455
},
5556
"config": {
5657
"bin-dir": "bin",

src/CLI/Command/Check.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ protected function configure(): void
9494
self::FORMAT_PARAM,
9595
'f',
9696
InputOption::VALUE_OPTIONAL,
97-
'Output format: json or text (default)',
97+
'Output format: text (default), json, gitlab',
9898
'text'
9999
);
100100
}
@@ -112,7 +112,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
112112
$skipBaseline = (bool) $input->getOption(self::SKIP_BASELINE_PARAM);
113113
$ignoreBaselineLinenumbers = (bool) $input->getOption(self::IGNORE_BASELINE_LINENUMBERS_PARAM);
114114
$format = $input->getOption(self::FORMAT_PARAM);
115-
$onlyErrors = Printer::FORMAT_JSON === $format;
115+
$onlyErrors = Printer::FORMAT_JSON === $format || Printer::FORMAT_GITLAB === $format;
116116

117117
if (true !== $skipBaseline && !$useBaseline && file_exists(self::DEFAULT_BASELINE_FILENAME)) {
118118
$useBaseline = self::DEFAULT_BASELINE_FILENAME;

src/CLI/Printer/GitlabPrinter.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Arkitect\CLI\Printer;
5+
6+
use Arkitect\Rules\Violation;
7+
8+
class GitlabPrinter implements Printer
9+
{
10+
public function print(array $violationsCollection): string
11+
{
12+
$details = [];
13+
$errorClassGrouped = [];
14+
15+
/**
16+
* @var string $key
17+
* @var Violation[] $violationsByFqcn
18+
*/
19+
foreach ($violationsCollection as $class => $violationsByFqcn) {
20+
foreach ($violationsByFqcn as $key => $violation) {
21+
/** @var class-string $fqcn */
22+
$fqcn = $violation->getFqcn();
23+
24+
$errorClassGrouped[$class][$key]['description'] = $violation->getError();
25+
$errorClassGrouped[$class][$key]['check_name'] = $class.'.'.$this->toKebabCase($violation->getError());
26+
$errorClassGrouped[$class][$key]['fingerprint'] = hash('sha256', $errorClassGrouped[$class][$key]['check_name']);
27+
$errorClassGrouped[$class][$key]['severity'] = 'major'; // Todo enable severity on violation
28+
$errorClassGrouped[$class][$key]['location']['path'] = $this->getPathFromFqcn($fqcn);
29+
30+
if (null !== $violation->getLine()) {
31+
$errorClassGrouped[$class][$key]['lines']['begin'] = $violation->getLine();
32+
} else {
33+
$errorClassGrouped[$class][$key]['lines']['begin'] = 1;
34+
}
35+
}
36+
}
37+
38+
return json_encode(array_merge($details, ...array_values($errorClassGrouped)));
39+
}
40+
41+
/**
42+
* @param class-string $fqcn
43+
*/
44+
private function getPathFromFqcn(string $fqcn): string
45+
{
46+
return (new \ReflectionClass($fqcn))->getFileName();
47+
}
48+
49+
private function toKebabCase(string $string): string
50+
{
51+
$string = preg_replace('/[^a-zA-Z0-9]+/', ' ', $string);
52+
$string = preg_replace('/\s+/', ' ', $string);
53+
$string = strtolower(trim($string));
54+
$string = str_replace(' ', '-', $string);
55+
56+
return $string;
57+
}
58+
}

src/CLI/Printer/Printer.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ interface Printer
99

1010
public const FORMAT_JSON = 'json';
1111

12+
public const FORMAT_GITLAB = 'gitlab';
13+
1214
public function print(array $violationsCollection): string;
1315
}

src/CLI/Printer/PrinterFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ final class PrinterFactory
88
public function create(string $format): Printer
99
{
1010
switch ($format) {
11+
case 'gitlab':
12+
return new GitlabPrinter();
1113
case 'json':
1214
return new JsonPrinter();
1315
default:

tests/E2E/Cli/CheckCommandTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,32 @@ public function test_json_format_output_no_errors(): void
195195
$this->assertCount(0, $json);
196196
}
197197

198+
public function test_gitlab_format_output(): void
199+
{
200+
$configFilePath = __DIR__.'/../_fixtures/configMvcForYieldBug.php';
201+
202+
$cmdTester = $this->runCheck($configFilePath, null, null, false, false, false, 'gitlab');
203+
204+
$this->assertCheckHasErrors($cmdTester);
205+
206+
$display = $cmdTester->getDisplay();
207+
208+
$this->assertJson($display);
209+
210+
$decodedResult = json_decode($display, true);
211+
212+
$this->assertIsString($display, 'Result should be a string');
213+
$this->assertJson($display, 'Result should be a valid JSON string');
214+
$this->assertCount(1, $decodedResult, 'Result should contain two violations');
215+
216+
$this->assertSame('should have a name that matches *Controller because all controllers should be end name with Controller', $decodedResult[0]['description']);
217+
$this->assertSame('App\Controller\Foo.should-have-a-name-that-matches-controller-because-all-controllers-should-be-end-name-with-controller', $decodedResult[0]['check_name']);
218+
$this->assertSame(hash('sha256', 'App\Controller\Foo.should-have-a-name-that-matches-controller-because-all-controllers-should-be-end-name-with-controller'), $decodedResult[0]['fingerprint']);
219+
$this->assertSame('major', $decodedResult[0]['severity']);
220+
$this->assertSame(getcwd().'/tests/E2E/_fixtures/mvc/Controller/Foo.php', $decodedResult[0]['location']['path']);
221+
$this->assertSame(1, $decodedResult[0]['lines']['begin']);
222+
}
223+
198224
protected function runCheck(
199225
$configFilePath = null,
200226
?bool $stopOnFailure = null,
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Arkitect\Tests\Unit\CLI\Printer;
5+
6+
use Arkitect\CLI\Printer\GitlabPrinter;
7+
use Arkitect\Rules\Violation;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class GitlabPrinterTest extends TestCase
11+
{
12+
/**
13+
* Test the `print` method returns a valid JSON string
14+
* with violations details when violations exist.
15+
*/
16+
public function test_print_with_violations(): void
17+
{
18+
$violation1 = $this->createMock(Violation::class);
19+
$violation1->method('getFqcn')->willReturn('Some\\ExampleClass');
20+
$violation1->method('getError')->willReturn('Some error message');
21+
$violation1->method('getLine')->willReturn(42);
22+
23+
$violation2 = $this->createMock(Violation::class);
24+
$violation2->method('getFqcn')->willReturn('Another\\ExampleClass');
25+
$violation2->method('getError')->willReturn('Another error message');
26+
$violation2->method('getLine')->willReturn(null);
27+
28+
$violationsCollection = [
29+
'RuleA' => [$violation1],
30+
'RuleB' => [$violation2],
31+
];
32+
33+
$printer = new GitlabPrinter();
34+
35+
$result = $printer->print($violationsCollection);
36+
$decodedResult = json_decode($result, true);
37+
38+
$this->assertIsString($result, 'Result should be a string');
39+
$this->assertJson($result, 'Result should be a valid JSON string');
40+
$this->assertCount(2, $decodedResult, 'Result should contain two violations');
41+
42+
$this->assertSame('Some error message', $decodedResult[0]['description']);
43+
$this->assertSame('RuleA.some-error-message', $decodedResult[0]['check_name']);
44+
$this->assertSame(hash('sha256', 'RuleA.some-error-message'), $decodedResult[0]['fingerprint']);
45+
$this->assertSame('major', $decodedResult[0]['severity']);
46+
$this->assertSame(__DIR__.'/GitlabPrinterTest.php', $decodedResult[0]['location']['path']);
47+
$this->assertSame(42, $decodedResult[0]['lines']['begin']);
48+
49+
$this->assertSame('Another error message', $decodedResult[1]['description']);
50+
$this->assertSame('RuleB.another-error-message', $decodedResult[1]['check_name']);
51+
$this->assertSame(hash('sha256', 'RuleB.another-error-message'), $decodedResult[1]['fingerprint']);
52+
$this->assertSame('major', $decodedResult[1]['severity']);
53+
$this->assertSame(__DIR__.'/GitlabPrinterTest.php', $decodedResult[1]['location']['path']);
54+
$this->assertSame(1, $decodedResult[1]['lines']['begin']);
55+
}
56+
57+
/**
58+
* Test the `print` method returns an empty JSON array
59+
* when no violations are provided.
60+
*/
61+
public function test_print_with_no_violations(): void
62+
{
63+
$violationsCollection = [];
64+
65+
$printer = new GitlabPrinter();
66+
67+
$result = $printer->print($violationsCollection);
68+
69+
$this->assertIsString($result, 'Result should be a string');
70+
$this->assertJson($result, 'Result should be a valid JSON string');
71+
72+
$decodedResult = json_decode($result, true);
73+
$this->assertEmpty($decodedResult, 'Result should be an empty array');
74+
}
75+
}
76+
77+
namespace Some;
78+
79+
class ExampleClass
80+
{
81+
}
82+
83+
namespace Another;
84+
85+
class ExampleClass
86+
{
87+
}

tests/Unit/Printer/JsonPrinterTest.php renamed to tests/Unit/CLI/Printer/JsonPrinterTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22
declare(strict_types=1);
33

4-
namespace Arkitect\Tests\Unit\Printer;
4+
namespace Arkitect\Tests\Unit\CLI\Printer;
55

66
use Arkitect\CLI\Printer\JsonPrinter;
77
use Arkitect\Rules\Violation;

tests/Unit/Printer/PrinterFactoryTest.php renamed to tests/Unit/CLI/Printer/PrinterFactoryTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22
declare(strict_types=1);
33

4-
namespace Arkitect\Tests\Unit\Printer;
4+
namespace Arkitect\Tests\Unit\CLI\Printer;
55

66
use Arkitect\CLI\Printer\JsonPrinter;
77
use Arkitect\CLI\Printer\PrinterFactory;

tests/Unit/Printer/TextPrinterTest.php renamed to tests/Unit/CLI/Printer/TextPrinterTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22
declare(strict_types=1);
33

4-
namespace Arkitect\Tests\Unit\Printer;
4+
namespace Arkitect\Tests\Unit\CLI\Printer;
55

66
use Arkitect\CLI\Printer\TextPrinter;
77
use Arkitect\Rules\Violation;

0 commit comments

Comments
 (0)