diff --git a/PhpcsChanged/CheckstyleReporter.php b/PhpcsChanged/CheckstyleReporter.php
new file mode 100644
index 0000000..00e345e
--- /dev/null
+++ b/PhpcsChanged/CheckstyleReporter.php
@@ -0,0 +1,75 @@
+getFile() ?? 'STDIN';
+ }, $messages->getMessages()));
+ if (count($files) === 0) {
+ $files = ['STDIN'];
+ }
+
+ $outputByFile = array_reduce($files, function(string $output, string $file) use ($messages): string {
+ $messagesForFile = array_values(array_filter($messages->getMessages(), static function(LintMessage $message) use ($file): bool {
+ return ($message->getFile() ?? 'STDIN') === $file;
+ }));
+ $output .= $this->getFormattedMessagesForFile($messagesForFile, $file);
+ return $output;
+ }, '');
+
+ $output = "\n";
+ $output .= "\n";
+ $output .= $outputByFile;
+ $output .= "\n";
+
+ return $output;
+ }
+
+ private function getFormattedMessagesForFile(array $messages, string $file): string {
+ if (count($messages) === 0) {
+ return '';
+ }
+
+ $xmlOutputForFile = "\t\n";
+ $xmlOutputForFile .= array_reduce($messages, function(string $output, LintMessage $message): string {
+ $line = $message->getLineNumber();
+ $column = $message->getColumn();
+ $source = $this->escapeXml($message->getSource());
+ $messageText = $this->escapeXml($message->getMessage());
+ $type = $message->getType();
+
+ // Map phpcs types to Checkstyle severity levels
+ $severity = $type === 'ERROR' ? 'error' : 'warning';
+
+ $output .= sprintf(
+ "\t\t\n",
+ $line,
+ $column,
+ $severity,
+ $messageText,
+ $source
+ );
+ return $output;
+ }, '');
+ $xmlOutputForFile .= "\t\n";
+
+ return $xmlOutputForFile;
+ }
+
+ private function escapeXml(string $string): string {
+ return htmlspecialchars($string, ENT_XML1 | ENT_QUOTES, 'UTF-8');
+ }
+
+ #[\Override]
+ public function getExitCode(PhpcsMessages $messages): int {
+ return (count($messages->getMessages()) > 0) ? 1 : 0;
+ }
+}
diff --git a/PhpcsChanged/Cli.php b/PhpcsChanged/Cli.php
index a677746..9ea650c 100644
--- a/PhpcsChanged/Cli.php
+++ b/PhpcsChanged/Cli.php
@@ -9,6 +9,7 @@
use PhpcsChanged\JsonReporter;
use PhpcsChanged\FullReporter;
use PhpcsChanged\JunitReporter;
+use PhpcsChanged\CheckstyleReporter;
use PhpcsChanged\PhpcsMessages;
use PhpcsChanged\ShellException;
use PhpcsChanged\ShellOperator;
@@ -144,7 +145,7 @@ function printHelp(): void {
printTwoColumns([
'--standard ' => 'The phpcs standard to use.',
'--extensions ' => 'A comma separated list of extensions to check.',
- '--report ' => 'The phpcs reporter to use. One of "full" (default), "json", "xml", or "junit".',
+ '--report ' => 'The phpcs reporter to use. One of "full" (default), "json", "xml", "junit", or "checkstyle".',
'-s' => 'Show sniff codes for each error when the reporter is "full".',
'--ignore ' => 'A comma separated list of patterns to ignore files and directories.',
'--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
return new XmlReporter($options, $shell);
case 'junit':
return new JunitReporter();
+ case 'checkstyle':
+ return new CheckstyleReporter();
}
printErrorAndExit("Unknown Reporter '{$reportType}'");
throw new \Exception("Unknown Reporter '{$reportType}'"); // Just in case we don't exit for some reason.
diff --git a/README.md b/README.md
index 15f5a1d..9534d86 100644
--- a/README.md
+++ b/README.md
@@ -100,7 +100,7 @@ More than one file can be specified after a version control option, including gl
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.
-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.
+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.
You can use `--standard` to specify a specific phpcs standard to run. This matches the phpcs option of the same name.
diff --git a/index.php b/index.php
index 48486b9..a3986e5 100644
--- a/index.php
+++ b/index.php
@@ -19,6 +19,7 @@
require_once __DIR__ . '/PhpcsChanged/FullReporter.php';
require_once __DIR__ . '/PhpcsChanged/XmlReporter.php';
require_once __DIR__ . '/PhpcsChanged/JunitReporter.php';
+require_once __DIR__ . '/PhpcsChanged/CheckstyleReporter.php';
require_once __DIR__ . '/PhpcsChanged/NoChangesException.php';
require_once __DIR__ . '/PhpcsChanged/ShellException.php';
require_once __DIR__ . '/PhpcsChanged/ShellOperator.php';
diff --git a/tests/CheckstyleReporterTest.php b/tests/CheckstyleReporterTest.php
new file mode 100644
index 0000000..95c9e80
--- /dev/null
+++ b/tests/CheckstyleReporterTest.php
@@ -0,0 +1,247 @@
+ 'WARNING',
+ 'severity' => 5,
+ 'fixable' => false,
+ 'column' => 5,
+ 'source' => 'ImportDetection.Imports.RequireImports.Import',
+ 'line' => 15,
+ 'message' => 'Found unused symbol Foo.',
+ ],
+ ], 'fileA.php');
+ $expected = <<
+
+
+
+
+
+
+EOF;
+ $reporter = new CheckstyleReporter();
+ $result = $reporter->getFormattedMessages($messages, []);
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testSingleError() {
+ $messages = PhpcsMessages::fromArrays([
+ [
+ 'type' => 'ERROR',
+ 'severity' => 5,
+ 'fixable' => false,
+ 'column' => 5,
+ 'source' => 'ImportDetection.Imports.RequireImports.Import',
+ 'line' => 15,
+ 'message' => 'Found unused symbol Foo.',
+ ],
+ ], 'fileA.php');
+ $expected = <<
+
+
+
+
+
+
+EOF;
+ $reporter = new CheckstyleReporter();
+ $result = $reporter->getFormattedMessages($messages, []);
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testMultipleWarningsWithLongLineNumber() {
+ $messages = PhpcsMessages::fromArrays([
+ [
+ 'type' => 'WARNING',
+ 'severity' => 5,
+ 'fixable' => false,
+ 'column' => 5,
+ 'source' => 'ImportDetection.Imports.RequireImports.Import',
+ 'line' => 133825,
+ 'message' => 'Found unused symbol Foo.',
+ ],
+ [
+ 'type' => 'WARNING',
+ 'severity' => 5,
+ 'fixable' => false,
+ 'column' => 5,
+ 'source' => 'ImportDetection.Imports.RequireImports.Import',
+ 'line' => 15,
+ 'message' => 'Found unused symbol Bar.',
+ ],
+ ], 'fileA.php');
+ $expected = <<
+
+
+
+
+
+
+
+EOF;
+ $reporter = new CheckstyleReporter();
+ $result = $reporter->getFormattedMessages($messages, []);
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testMultipleWarningsErrorsAndFiles() {
+ $messagesA = PhpcsMessages::fromArrays([
+ [
+ 'type' => 'ERROR',
+ 'severity' => 5,
+ 'fixable' => true,
+ 'column' => 2,
+ 'source' => 'ImportDetection.Imports.RequireImports.Something',
+ 'line' => 12,
+ 'message' => 'Found unused symbol Faa.',
+ ],
+ [
+ 'type' => 'ERROR',
+ 'severity' => 5,
+ 'fixable' => false,
+ 'column' => 5,
+ 'source' => 'ImportDetection.Imports.RequireImports.Import',
+ 'line' => 15,
+ 'message' => 'Found unused symbol Foo.',
+ ],
+ [
+ 'type' => 'WARNING',
+ 'severity' => 5,
+ 'fixable' => false,
+ 'column' => 8,
+ 'source' => 'ImportDetection.Imports.RequireImports.Boom',
+ 'line' => 18,
+ 'message' => 'Found unused symbol Bar.',
+ ],
+ ], 'fileA.php');
+ $messagesB = PhpcsMessages::fromArrays([
+ [
+ 'type' => 'WARNING',
+ 'severity' => 5,
+ 'fixable' => false,
+ 'column' => 5,
+ 'source' => 'ImportDetection.Imports.RequireImports.Zoop',
+ 'line' => 30,
+ 'message' => 'Found unused symbol Hi.',
+ ],
+ ], 'fileB.php');
+ $messages = PhpcsMessages::merge([$messagesA, $messagesB]);
+ $expected = <<
+
+
+
+
+
+
+
+
+
+
+
+EOF;
+ $reporter = new CheckstyleReporter();
+ $result = $reporter->getFormattedMessages($messages, ['s' => 1]);
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testNoWarnings() {
+ $messages = PhpcsMessages::fromArrays([]);
+ $expected = <<
+
+
+
+EOF;
+ $reporter = new CheckstyleReporter();
+ $result = $reporter->getFormattedMessages($messages, []);
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testSingleWarningWithNoFilename() {
+ $messages = PhpcsMessages::fromArrays([
+ [
+ 'type' => 'WARNING',
+ 'severity' => 5,
+ 'fixable' => false,
+ 'column' => 5,
+ 'source' => 'ImportDetection.Imports.RequireImports.Import',
+ 'line' => 15,
+ 'message' => 'Found unused symbol Foo.',
+ ],
+ ]);
+ $expected = <<
+
+
+
+
+
+
+EOF;
+ $reporter = new CheckstyleReporter();
+ $result = $reporter->getFormattedMessages($messages, []);
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testXmlEscaping() {
+ $messages = PhpcsMessages::fromArrays([
+ [
+ 'type' => 'ERROR',
+ 'severity' => 5,
+ 'fixable' => false,
+ 'column' => 5,
+ 'source' => 'Test.Source<>&"',
+ 'line' => 15,
+ 'message' => 'Message with & "quotes".',
+ ],
+ ], 'fileA.php');
+ $expected = <<
+
+
+
+
+
+
+EOF;
+ $reporter = new CheckstyleReporter();
+ $result = $reporter->getFormattedMessages($messages, []);
+ $this->assertEquals($expected, $result);
+ }
+
+ public function testGetExitCodeWithMessages() {
+ $messages = PhpcsMessages::fromArrays([
+ [
+ 'type' => 'WARNING',
+ 'severity' => 5,
+ 'fixable' => false,
+ 'column' => 5,
+ 'source' => 'ImportDetection.Imports.RequireImports.Import',
+ 'line' => 15,
+ 'message' => 'Found unused symbol Foo.',
+ ],
+ ], 'fileA.php');
+ $reporter = new CheckstyleReporter();
+ $this->assertEquals(1, $reporter->getExitCode($messages));
+ }
+
+ public function testGetExitCodeWithNoMessages() {
+ $messages = PhpcsMessages::fromArrays([], 'fileA.php');
+ $reporter = new CheckstyleReporter();
+ $this->assertEquals(0, $reporter->getExitCode($messages));
+ }
+}