Skip to content

Commit 2e5f684

Browse files
Support multiple output formats for violation reporting (#470)
* Support multiple output formats for violation reporting Added support for violation reporting in both text and JSON formats by introducing a `Printer` interface and implementing `TextPrinter` and `JsonPrinter` classes. Refactored relevant methods to utilize the new `Printer` system and updated CLI options to allow format selection. Included comprehensive unit tests for the new functionality. * Rename loop variables for clarity and consistency Updated variable names in JsonPrinter to improve readability and better reflect their purpose. This change enhances the maintainability of the code without modifying functionality.
1 parent d6a5467 commit 2e5f684

File tree

14 files changed

+399
-52
lines changed

14 files changed

+399
-52
lines changed

src/CLI/Command/Check.php

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Arkitect\CLI\Command;
66

77
use Arkitect\CLI\Config;
8+
use Arkitect\CLI\Printer\Printer;
89
use Arkitect\CLI\Progress\DebugProgress;
910
use Arkitect\CLI\Progress\ProgressBarProgress;
1011
use Arkitect\CLI\Runner;
@@ -26,6 +27,7 @@ class Check extends Command
2627
private const USE_BASELINE_PARAM = 'use-baseline';
2728
private const SKIP_BASELINE_PARAM = 'skip-baseline';
2829
private const IGNORE_BASELINE_LINENUMBERS_PARAM = 'ignore-baseline-linenumbers';
30+
private const FORMAT_PARAM = 'format';
2931

3032
private const GENERATE_BASELINE_PARAM = 'generate-baseline';
3133
private const DEFAULT_RULES_FILENAME = 'phparkitect.php';
@@ -87,6 +89,13 @@ protected function configure(): void
8789
'i',
8890
InputOption::VALUE_NONE,
8991
'Ignore line numbers when checking the baseline'
92+
)
93+
->addOption(
94+
self::FORMAT_PARAM,
95+
'f',
96+
InputOption::VALUE_OPTIONAL,
97+
'Output format: json or text (default)',
98+
'text'
9099
);
91100
}
92101

@@ -102,6 +111,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int
102111
$useBaseline = (string) $input->getOption(self::USE_BASELINE_PARAM);
103112
$skipBaseline = (bool) $input->getOption(self::SKIP_BASELINE_PARAM);
104113
$ignoreBaselineLinenumbers = (bool) $input->getOption(self::IGNORE_BASELINE_LINENUMBERS_PARAM);
114+
$format = $input->getOption(self::FORMAT_PARAM);
115+
$onlyErrors = Printer::FORMAT_JSON === $format;
105116

106117
if (true !== $skipBaseline && !$useBaseline && file_exists(self::DEFAULT_BASELINE_FILENAME)) {
107118
$useBaseline = self::DEFAULT_BASELINE_FILENAME;
@@ -112,7 +123,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int
112123

113124
return self::ERROR_CODE;
114125
}
115-
$output->writeln('<info>Baseline found: '.$useBaseline.'</info>');
126+
127+
if (!$onlyErrors) {
128+
$output->writeln('<info>Baseline found: '.$useBaseline.'</info>');
129+
}
116130

117131
$generateBaseline = $input->getOption(self::GENERATE_BASELINE_PARAM);
118132

@@ -122,18 +136,22 @@ protected function execute(InputInterface $input, OutputInterface $output): int
122136

123137
$progress = $verbose ? new DebugProgress($output) : new ProgressBarProgress($output);
124138

125-
$this->printHeadingLine($output);
139+
if (!$onlyErrors) {
140+
$this->printHeadingLine($output);
141+
}
126142

127143
$rulesFilename = $this->getConfigFilename($input);
128-
$output->writeln(sprintf("Config file: %s\n", $rulesFilename));
144+
if (!$onlyErrors) {
145+
$output->writeln(sprintf("Config file: %s\n", $rulesFilename));
146+
}
129147

130148
$config = new Config();
131149

132150
$this->readRules($config, $rulesFilename);
133151

134152
$runner = new Runner($stopOnFailure);
135153
try {
136-
$runner->run($config, $progress, $targetPhpVersion);
154+
$runner->run($config, $progress, $targetPhpVersion, $onlyErrors);
137155
} catch (FailOnFirstViolationException $e) {
138156
}
139157
$violations = $runner->getViolations();
@@ -146,7 +164,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
146164
$this->saveBaseline($generateBaseline, $violations);
147165

148166
$output->writeln('<info>Baseline file \''.$generateBaseline.'\'created!</info>');
149-
$this->printExecutionTime($output, $startTime);
167+
if (!$onlyErrors) {
168+
$this->printExecutionTime($output, $startTime);
169+
}
150170

151171
return self::SUCCESS_CODE;
152172
}
@@ -158,15 +178,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int
158178
}
159179

160180
if ($violations->count() > 0) {
161-
$this->printViolations($violations, $output);
162-
$this->printExecutionTime($output, $startTime);
181+
$this->printViolations($violations, $output, $format, $onlyErrors);
182+
if (!$onlyErrors) {
183+
$this->printExecutionTime($output, $startTime);
184+
}
163185

164186
return self::ERROR_CODE;
165187
}
166188

167189
$parsedErrors = $runner->getParsingErrors();
168190
if ($parsedErrors->count() > 0) {
169-
$this->printParsedErrors($parsedErrors, $output);
191+
$this->printParsedErrors($parsedErrors, $output, $onlyErrors);
170192
$this->printExecutionTime($output, $startTime);
171193

172194
return self::ERROR_CODE;
@@ -178,8 +200,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int
178200
return self::ERROR_CODE;
179201
}
180202

181-
$this->printNoViolationsDetectedMessage($output);
182-
$this->printExecutionTime($output, $startTime);
203+
$this->printNoViolationsDetectedMessage($output, $onlyErrors);
204+
205+
if (!$onlyErrors) {
206+
$this->printExecutionTime($output, $startTime);
207+
}
183208

184209
return self::SUCCESS_CODE;
185210
}
@@ -236,21 +261,29 @@ private function getConfigFilename(InputInterface $input): string
236261
return $filename;
237262
}
238263

239-
private function printViolations(Violations $violations, OutputInterface $output): void
264+
private function printViolations(Violations $violations, OutputInterface $output, string $format, bool $onlyErrors = false): void
240265
{
241-
$output->writeln('<error>ERRORS!</error>');
242-
$output->writeln(sprintf('%s', $violations->toString()));
243-
$output->writeln(sprintf('<error>%s VIOLATIONS DETECTED!</error>', \count($violations)));
266+
if (!$onlyErrors) {
267+
$output->writeln('<error>ERRORS!</error>');
268+
}
269+
$output->writeln(sprintf('%s', $violations->toString($format)));
270+
if (!$onlyErrors) {
271+
$output->writeln(sprintf('<error>%s VIOLATIONS DETECTED!</error>', \count($violations)));
272+
}
244273
}
245274

246-
private function printParsedErrors(ParsingErrors $parsingErrors, OutputInterface $output): void
275+
private function printParsedErrors(ParsingErrors $parsingErrors, OutputInterface $output, bool $onlyErrors = false): void
247276
{
248-
$output->writeln('<error>ERROR ON PARSING THESE FILES:</error>');
277+
if (!$onlyErrors) {
278+
$output->writeln('<error>ERROR ON PARSING THESE FILES:</error>');
279+
}
249280
$output->writeln(sprintf('%s', $parsingErrors->toString()));
250281
}
251282

252-
private function printNoViolationsDetectedMessage(OutputInterface $output): void
283+
private function printNoViolationsDetectedMessage(OutputInterface $output, bool $onlyErrors = false): void
253284
{
254-
$output->writeln('<info>NO VIOLATIONS DETECTED!</info>');
285+
if (!$onlyErrors) {
286+
$output->writeln('<info>NO VIOLATIONS DETECTED!</info>');
287+
}
255288
}
256289
}

src/CLI/Printer/JsonPrinter.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Arkitect\CLI\Printer;
5+
6+
use Arkitect\Rules\Violation;
7+
8+
class JsonPrinter implements Printer
9+
{
10+
public function print(array $violationsCollection): string
11+
{
12+
$totalViolations = 0;
13+
$details = [];
14+
15+
/**
16+
* @var string $key
17+
* @var Violation[] $violationsByFqcn
18+
*/
19+
foreach ($violationsCollection as $class => $violationsByFqcn) {
20+
$violationForThisFqcn = \count($violationsByFqcn);
21+
$totalViolations += $violationForThisFqcn;
22+
23+
$details[$class] = [];
24+
25+
foreach ($violationsByFqcn as $key => $violation) {
26+
$details[$class][$key]['error'] = $violation->getError();
27+
28+
if (null !== $violation->getLine()) {
29+
$details[$class][$key]['line'] = $violation->getLine();
30+
}
31+
}
32+
}
33+
34+
$errors = [
35+
'totalViolations' => $totalViolations,
36+
'details' => $details,
37+
];
38+
39+
return json_encode($errors);
40+
}
41+
}

src/CLI/Printer/Printer.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Arkitect\CLI\Printer;
5+
6+
interface Printer
7+
{
8+
public const FORMAT_TEXT = 'text';
9+
10+
public const FORMAT_JSON = 'json';
11+
12+
public function print(array $violationsCollection): string;
13+
}

src/CLI/Printer/PrinterFactory.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Arkitect\CLI\Printer;
5+
6+
final class PrinterFactory
7+
{
8+
public function create(string $format): Printer
9+
{
10+
switch ($format) {
11+
case 'json':
12+
return new JsonPrinter();
13+
default:
14+
return new TextPrinter();
15+
}
16+
}
17+
}

src/CLI/Printer/TextPrinter.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Arkitect\CLI\Printer;
5+
6+
use Arkitect\Rules\Violation;
7+
8+
class TextPrinter implements Printer
9+
{
10+
public function print(array $violationsCollection): string
11+
{
12+
$errors = '';
13+
14+
/**
15+
* @var string $key
16+
* @var Violation[] $violationsByFqcn
17+
*/
18+
foreach ($violationsCollection as $key => $violationsByFqcn) {
19+
$violationForThisFqcn = \count($violationsByFqcn);
20+
$errors .= "\n$key has {$violationForThisFqcn} violations";
21+
22+
foreach ($violationsByFqcn as $violation) {
23+
$errors .= "\n ".$violation->getError();
24+
25+
if (null !== $violation->getLine()) {
26+
$errors .= ' (on line '.$violation->getLine().')';
27+
}
28+
}
29+
$errors .= "\n";
30+
}
31+
32+
return $errors;
33+
}
34+
}

src/CLI/Runner.php

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,22 @@ public function __construct(bool $stopOnFailure = false)
2828
$this->parsingErrors = new ParsingErrors();
2929
}
3030

31-
public function run(Config $config, Progress $progress, TargetPhpVersion $targetPhpVersion): void
31+
public function run(Config $config, Progress $progress, TargetPhpVersion $targetPhpVersion, bool $onlyErrors): void
3232
{
3333
/** @var FileParser $fileParser */
3434
$fileParser = FileParserFactory::createFileParser($targetPhpVersion, $config->isParseCustomAnnotationsEnabled());
3535

3636
/** @var ClassSetRules $classSetRule */
3737
foreach ($config->getClassSetRules() as $classSetRule) {
38-
$progress->startFileSetAnalysis($classSetRule->getClassSet());
38+
if (!$onlyErrors) {
39+
$progress->startFileSetAnalysis($classSetRule->getClassSet());
40+
}
3941

40-
$this->check($classSetRule, $progress, $fileParser, $this->violations, $this->parsingErrors);
42+
$this->check($classSetRule, $progress, $fileParser, $this->violations, $this->parsingErrors, $onlyErrors);
4143

42-
$progress->endFileSetAnalysis($classSetRule->getClassSet());
44+
if (!$onlyErrors) {
45+
$progress->endFileSetAnalysis($classSetRule->getClassSet());
46+
}
4347
}
4448
}
4549

@@ -48,11 +52,14 @@ public function check(
4852
Progress $progress,
4953
Parser $fileParser,
5054
Violations $violations,
51-
ParsingErrors $parsingErrors
55+
ParsingErrors $parsingErrors,
56+
bool $onlyErrors = false
5257
): void {
5358
/** @var SplFileInfo $file */
5459
foreach ($classSetRule->getClassSet() as $file) {
55-
$progress->startParsingFile($file->getRelativePathname());
60+
if (!$onlyErrors) {
61+
$progress->startParsingFile($file->getRelativePathname());
62+
}
5663

5764
$fileParser->parse($file->getContents(), $file->getRelativePathname());
5865
$parsedErrors = $fileParser->getParsingErrors();
@@ -67,8 +74,9 @@ public function check(
6774
$rule->check($classDescription, $violations);
6875
}
6976
}
70-
71-
$progress->endParsingFile($file->getRelativePathname());
77+
if (!$onlyErrors) {
78+
$progress->endParsingFile($file->getRelativePathname());
79+
}
7280
}
7381
}
7482

src/PHPUnit/ArchRuleCheckerConstraintAdapter.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Arkitect\Analyzer\FileParserFactory;
99
use Arkitect\ClassSet;
1010
use Arkitect\ClassSetRules;
11+
use Arkitect\CLI\Printer\Printer;
1112
use Arkitect\CLI\Progress\VoidProgress;
1213
use Arkitect\CLI\Runner;
1314
use Arkitect\CLI\TargetPhpVersion;
@@ -78,6 +79,6 @@ protected function failureDescription($other): string
7879
return "\n".$this->parsingErrors->toString();
7980
}
8081

81-
return "\n".$this->violations->toString();
82+
return "\n".$this->violations->toString(Printer::FORMAT_TEXT);
8283
}
8384
}

src/Rules/Violations.php

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Arkitect\Rules;
66

7+
use Arkitect\CLI\Printer\PrinterFactory;
78
use Arkitect\Exceptions\FailOnFirstViolationException;
89
use Arkitect\Exceptions\IndexNotFoundException;
910

@@ -79,30 +80,11 @@ public function groupedByFqcn(): array
7980
}, []);
8081
}
8182

82-
public function toString(): string
83+
public function toString(string $format): string
8384
{
84-
$errors = '';
85-
$violationsCollection = $this->groupedByFqcn();
86-
87-
/**
88-
* @var string $key
89-
* @var Violation[] $violationsByFqcn
90-
*/
91-
foreach ($violationsCollection as $key => $violationsByFqcn) {
92-
$violationForThisFqcn = \count($violationsByFqcn);
93-
$errors .= "\n$key has {$violationForThisFqcn} violations";
94-
95-
foreach ($violationsByFqcn as $violation) {
96-
$errors .= "\n ".$violation->getError();
97-
98-
if (null !== $violation->getLine()) {
99-
$errors .= ' (on line '.$violation->getLine().')';
100-
}
101-
}
102-
$errors .= "\n";
103-
}
85+
$printer = (new PrinterFactory())->create($format);
10486

105-
return $errors;
87+
return $printer->print($this->groupedByFqcn());
10688
}
10789

10890
public function toArray(): array

0 commit comments

Comments
 (0)