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)); + } +}