Skip to content

Commit a0fce63

Browse files
committed
Add Checkstyle XML reporter format
1 parent 2900b8b commit a0fce63

5 files changed

Lines changed: 328 additions & 2 deletions

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace PhpcsChanged;
5+
6+
use PhpcsChanged\Reporter;
7+
use PhpcsChanged\PhpcsMessages;
8+
use PhpcsChanged\LintMessage;
9+
10+
class CheckstyleReporter implements Reporter {
11+
#[\Override]
12+
public function getFormattedMessages(PhpcsMessages $messages, array $options): string { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
13+
$files = array_unique(array_map(function(LintMessage $message): string {
14+
return $message->getFile() ?? 'STDIN';
15+
}, $messages->getMessages()));
16+
if (count($files) === 0) {
17+
$files = ['STDIN'];
18+
}
19+
20+
$outputByFile = array_reduce($files, function(string $output, string $file) use ($messages): string {
21+
$messagesForFile = array_values(array_filter($messages->getMessages(), static function(LintMessage $message) use ($file): bool {
22+
return ($message->getFile() ?? 'STDIN') === $file;
23+
}));
24+
$output .= $this->getFormattedMessagesForFile($messagesForFile, $file);
25+
return $output;
26+
}, '');
27+
28+
$output = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
29+
$output .= "<checkstyle version=\"phpcs-changed-2.11.8\">\n";
30+
$output .= $outputByFile;
31+
$output .= "</checkstyle>\n";
32+
33+
return $output;
34+
}
35+
36+
private function getFormattedMessagesForFile(array $messages, string $file): string {
37+
if (count($messages) === 0) {
38+
return '';
39+
}
40+
41+
$xmlOutputForFile = "\t<file name=\"{$file}\">\n";
42+
$xmlOutputForFile .= array_reduce($messages, function(string $output, LintMessage $message): string {
43+
$line = $message->getLineNumber();
44+
$column = $message->getColumn();
45+
$source = $this->escapeXml($message->getSource());
46+
$messageText = $this->escapeXml($message->getMessage());
47+
$type = $message->getType();
48+
49+
// Map phpcs types to Checkstyle severity levels
50+
$severity = $type === 'ERROR' ? 'error' : 'warning';
51+
52+
$output .= sprintf(
53+
"\t\t<error line=\"%d\" column=\"%d\" severity=\"%s\" message=\"%s\" source=\"%s\"/>\n",
54+
$line,
55+
$column,
56+
$severity,
57+
$messageText,
58+
$source
59+
);
60+
return $output;
61+
}, '');
62+
$xmlOutputForFile .= "\t</file>\n";
63+
64+
return $xmlOutputForFile;
65+
}
66+
67+
private function escapeXml(string $string): string {
68+
return htmlspecialchars($string, ENT_XML1 | ENT_QUOTES, 'UTF-8');
69+
}
70+
71+
#[\Override]
72+
public function getExitCode(PhpcsMessages $messages): int {
73+
return (count($messages->getMessages()) > 0) ? 1 : 0;
74+
}
75+
}

PhpcsChanged/Cli.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PhpcsChanged\JsonReporter;
1010
use PhpcsChanged\FullReporter;
1111
use PhpcsChanged\JunitReporter;
12+
use PhpcsChanged\CheckstyleReporter;
1213
use PhpcsChanged\PhpcsMessages;
1314
use PhpcsChanged\ShellException;
1415
use PhpcsChanged\ShellOperator;
@@ -144,7 +145,7 @@ function printHelp(): void {
144145
printTwoColumns([
145146
'--standard <STANDARD>' => 'The phpcs standard to use.',
146147
'--extensions <EXTENSIONS>' => 'A comma separated list of extensions to check.',
147-
'--report <REPORTER>' => 'The phpcs reporter to use. One of "full" (default), "json", "xml", or "junit".',
148+
'--report <REPORTER>' => 'The phpcs reporter to use. One of "full" (default), "json", "xml", "junit", or "checkstyle".',
148149
'-s' => 'Show sniff codes for each error when the reporter is "full".',
149150
'--ignore <PATTERNS>' => 'A comma separated list of patterns to ignore files and directories.',
150151
'--warning-severity' => 'The phpcs warning severity to report. See phpcs documentation for usage.',
@@ -194,6 +195,8 @@ function getReporter(string $reportType, CliOptions $options, ShellOperator $she
194195
return new XmlReporter($options, $shell);
195196
case 'junit':
196197
return new JunitReporter();
198+
case 'checkstyle':
199+
return new CheckstyleReporter();
197200
}
198201
printErrorAndExit("Unknown Reporter '{$reportType}'");
199202
throw new \Exception("Unknown Reporter '{$reportType}'"); // Just in case we don't exit for some reason.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ More than one file can be specified after a version control option, including gl
100100

101101
You can use `--ignore` to ignore any directory, file, or paths matching provided pattern(s). For example.: `--ignore=bin/*,vendor/*` would ignore any files in bin directory, as well as in vendor.
102102

103-
You can use `--report` to customize the output type. `full` (the default) is human-readable, `json` prints a JSON object as shown above, and 'xml' can be used by IDEs. These match the phpcs reporters of the same names. `junit` can also be used for [JUnit XML](https://github.com/testmoapp/junitxml) which can be helpful for test runners.
103+
You can use `--report` to customize the output type. `full` (the default) is human-readable, `json` prints a JSON object as shown above, and 'xml' can be used by IDEs. These match the phpcs reporters of the same names. `junit` can also be used for [JUnit XML](https://github.com/testmoapp/junitxml) which can be helpful for test runners. `checkstyle` will output Checkstyle format.
104104

105105
You can use `--standard` to specify a specific phpcs standard to run. This matches the phpcs option of the same name.
106106

index.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
require_once __DIR__ . '/PhpcsChanged/FullReporter.php';
2020
require_once __DIR__ . '/PhpcsChanged/XmlReporter.php';
2121
require_once __DIR__ . '/PhpcsChanged/JunitReporter.php';
22+
require_once __DIR__ . '/PhpcsChanged/CheckstyleReporter.php';
2223
require_once __DIR__ . '/PhpcsChanged/NoChangesException.php';
2324
require_once __DIR__ . '/PhpcsChanged/ShellException.php';
2425
require_once __DIR__ . '/PhpcsChanged/ShellOperator.php';

tests/CheckstyleReporterTest.php

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
require_once dirname(__DIR__) . '/index.php';
5+
require_once __DIR__ . '/helpers/helpers.php';
6+
7+
use PHPUnit\Framework\TestCase;
8+
use PhpcsChanged\PhpcsMessages;
9+
use PhpcsChanged\CheckstyleReporter;
10+
11+
final class CheckstyleReporterTest extends TestCase {
12+
public function testSingleWarning() {
13+
$messages = PhpcsMessages::fromArrays([
14+
[
15+
'type' => 'WARNING',
16+
'severity' => 5,
17+
'fixable' => false,
18+
'column' => 5,
19+
'source' => 'ImportDetection.Imports.RequireImports.Import',
20+
'line' => 15,
21+
'message' => 'Found unused symbol Foo.',
22+
],
23+
], 'fileA.php');
24+
$expected = <<<EOF
25+
<?xml version="1.0" encoding="UTF-8"?>
26+
<checkstyle version="phpcs-changed-2.11.8">
27+
<file name="fileA.php">
28+
<error line="15" column="5" severity="warning" message="Found unused symbol Foo." source="ImportDetection.Imports.RequireImports.Import"/>
29+
</file>
30+
</checkstyle>
31+
32+
EOF;
33+
$reporter = new CheckstyleReporter();
34+
$result = $reporter->getFormattedMessages($messages, []);
35+
$this->assertEquals($expected, $result);
36+
}
37+
38+
public function testSingleError() {
39+
$messages = PhpcsMessages::fromArrays([
40+
[
41+
'type' => 'ERROR',
42+
'severity' => 5,
43+
'fixable' => false,
44+
'column' => 5,
45+
'source' => 'ImportDetection.Imports.RequireImports.Import',
46+
'line' => 15,
47+
'message' => 'Found unused symbol Foo.',
48+
],
49+
], 'fileA.php');
50+
$expected = <<<EOF
51+
<?xml version="1.0" encoding="UTF-8"?>
52+
<checkstyle version="phpcs-changed-2.11.8">
53+
<file name="fileA.php">
54+
<error line="15" column="5" severity="error" message="Found unused symbol Foo." source="ImportDetection.Imports.RequireImports.Import"/>
55+
</file>
56+
</checkstyle>
57+
58+
EOF;
59+
$reporter = new CheckstyleReporter();
60+
$result = $reporter->getFormattedMessages($messages, []);
61+
$this->assertEquals($expected, $result);
62+
}
63+
64+
public function testMultipleWarningsWithLongLineNumber() {
65+
$messages = PhpcsMessages::fromArrays([
66+
[
67+
'type' => 'WARNING',
68+
'severity' => 5,
69+
'fixable' => false,
70+
'column' => 5,
71+
'source' => 'ImportDetection.Imports.RequireImports.Import',
72+
'line' => 133825,
73+
'message' => 'Found unused symbol Foo.',
74+
],
75+
[
76+
'type' => 'WARNING',
77+
'severity' => 5,
78+
'fixable' => false,
79+
'column' => 5,
80+
'source' => 'ImportDetection.Imports.RequireImports.Import',
81+
'line' => 15,
82+
'message' => 'Found unused symbol Bar.',
83+
],
84+
], 'fileA.php');
85+
$expected = <<<EOF
86+
<?xml version="1.0" encoding="UTF-8"?>
87+
<checkstyle version="phpcs-changed-2.11.8">
88+
<file name="fileA.php">
89+
<error line="133825" column="5" severity="warning" message="Found unused symbol Foo." source="ImportDetection.Imports.RequireImports.Import"/>
90+
<error line="15" column="5" severity="warning" message="Found unused symbol Bar." source="ImportDetection.Imports.RequireImports.Import"/>
91+
</file>
92+
</checkstyle>
93+
94+
EOF;
95+
$reporter = new CheckstyleReporter();
96+
$result = $reporter->getFormattedMessages($messages, []);
97+
$this->assertEquals($expected, $result);
98+
}
99+
100+
public function testMultipleWarningsErrorsAndFiles() {
101+
$messagesA = PhpcsMessages::fromArrays([
102+
[
103+
'type' => 'ERROR',
104+
'severity' => 5,
105+
'fixable' => true,
106+
'column' => 2,
107+
'source' => 'ImportDetection.Imports.RequireImports.Something',
108+
'line' => 12,
109+
'message' => 'Found unused symbol Faa.',
110+
],
111+
[
112+
'type' => 'ERROR',
113+
'severity' => 5,
114+
'fixable' => false,
115+
'column' => 5,
116+
'source' => 'ImportDetection.Imports.RequireImports.Import',
117+
'line' => 15,
118+
'message' => 'Found unused symbol Foo.',
119+
],
120+
[
121+
'type' => 'WARNING',
122+
'severity' => 5,
123+
'fixable' => false,
124+
'column' => 8,
125+
'source' => 'ImportDetection.Imports.RequireImports.Boom',
126+
'line' => 18,
127+
'message' => 'Found unused symbol Bar.',
128+
],
129+
], 'fileA.php');
130+
$messagesB = PhpcsMessages::fromArrays([
131+
[
132+
'type' => 'WARNING',
133+
'severity' => 5,
134+
'fixable' => false,
135+
'column' => 5,
136+
'source' => 'ImportDetection.Imports.RequireImports.Zoop',
137+
'line' => 30,
138+
'message' => 'Found unused symbol Hi.',
139+
],
140+
], 'fileB.php');
141+
$messages = PhpcsMessages::merge([$messagesA, $messagesB]);
142+
$expected = <<<EOF
143+
<?xml version="1.0" encoding="UTF-8"?>
144+
<checkstyle version="phpcs-changed-2.11.8">
145+
<file name="fileA.php">
146+
<error line="12" column="2" severity="error" message="Found unused symbol Faa." source="ImportDetection.Imports.RequireImports.Something"/>
147+
<error line="15" column="5" severity="error" message="Found unused symbol Foo." source="ImportDetection.Imports.RequireImports.Import"/>
148+
<error line="18" column="8" severity="warning" message="Found unused symbol Bar." source="ImportDetection.Imports.RequireImports.Boom"/>
149+
</file>
150+
<file name="fileB.php">
151+
<error line="30" column="5" severity="warning" message="Found unused symbol Hi." source="ImportDetection.Imports.RequireImports.Zoop"/>
152+
</file>
153+
</checkstyle>
154+
155+
EOF;
156+
$reporter = new CheckstyleReporter();
157+
$result = $reporter->getFormattedMessages($messages, ['s' => 1]);
158+
$this->assertEquals($expected, $result);
159+
}
160+
161+
public function testNoWarnings() {
162+
$messages = PhpcsMessages::fromArrays([]);
163+
$expected = <<<EOF
164+
<?xml version="1.0" encoding="UTF-8"?>
165+
<checkstyle version="phpcs-changed-2.11.8">
166+
</checkstyle>
167+
168+
EOF;
169+
$reporter = new CheckstyleReporter();
170+
$result = $reporter->getFormattedMessages($messages, []);
171+
$this->assertEquals($expected, $result);
172+
}
173+
174+
public function testSingleWarningWithNoFilename() {
175+
$messages = PhpcsMessages::fromArrays([
176+
[
177+
'type' => 'WARNING',
178+
'severity' => 5,
179+
'fixable' => false,
180+
'column' => 5,
181+
'source' => 'ImportDetection.Imports.RequireImports.Import',
182+
'line' => 15,
183+
'message' => 'Found unused symbol Foo.',
184+
],
185+
]);
186+
$expected = <<<EOF
187+
<?xml version="1.0" encoding="UTF-8"?>
188+
<checkstyle version="phpcs-changed-2.11.8">
189+
<file name="STDIN">
190+
<error line="15" column="5" severity="warning" message="Found unused symbol Foo." source="ImportDetection.Imports.RequireImports.Import"/>
191+
</file>
192+
</checkstyle>
193+
194+
EOF;
195+
$reporter = new CheckstyleReporter();
196+
$result = $reporter->getFormattedMessages($messages, []);
197+
$this->assertEquals($expected, $result);
198+
}
199+
200+
public function testXmlEscaping() {
201+
$messages = PhpcsMessages::fromArrays([
202+
[
203+
'type' => 'ERROR',
204+
'severity' => 5,
205+
'fixable' => false,
206+
'column' => 5,
207+
'source' => 'Test.Source<>&"',
208+
'line' => 15,
209+
'message' => 'Message with <xml> & "quotes".',
210+
],
211+
], 'fileA.php');
212+
$expected = <<<EOF
213+
<?xml version="1.0" encoding="UTF-8"?>
214+
<checkstyle version="phpcs-changed-2.11.8">
215+
<file name="fileA.php">
216+
<error line="15" column="5" severity="error" message="Message with &lt;xml&gt; &amp; &quot;quotes&quot;." source="Test.Source&lt;&gt;&amp;&quot;"/>
217+
</file>
218+
</checkstyle>
219+
220+
EOF;
221+
$reporter = new CheckstyleReporter();
222+
$result = $reporter->getFormattedMessages($messages, []);
223+
$this->assertEquals($expected, $result);
224+
}
225+
226+
public function testGetExitCodeWithMessages() {
227+
$messages = PhpcsMessages::fromArrays([
228+
[
229+
'type' => 'WARNING',
230+
'severity' => 5,
231+
'fixable' => false,
232+
'column' => 5,
233+
'source' => 'ImportDetection.Imports.RequireImports.Import',
234+
'line' => 15,
235+
'message' => 'Found unused symbol Foo.',
236+
],
237+
], 'fileA.php');
238+
$reporter = new CheckstyleReporter();
239+
$this->assertEquals(1, $reporter->getExitCode($messages));
240+
}
241+
242+
public function testGetExitCodeWithNoMessages() {
243+
$messages = PhpcsMessages::fromArrays([], 'fileA.php');
244+
$reporter = new CheckstyleReporter();
245+
$this->assertEquals(0, $reporter->getExitCode($messages));
246+
}
247+
}

0 commit comments

Comments
 (0)