Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions PhpcsChanged/CheckstyleReporter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);

namespace PhpcsChanged;

use PhpcsChanged\Reporter;
use PhpcsChanged\PhpcsMessages;
use PhpcsChanged\LintMessage;

class CheckstyleReporter implements Reporter {
#[\Override]
public function getFormattedMessages(PhpcsMessages $messages, array $options): string { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
$files = array_unique(array_map(function(LintMessage $message): string {
return $message->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 = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
$output .= "<checkstyle version=\"phpcs-changed-2.11.8\">\n";
$output .= $outputByFile;
$output .= "</checkstyle>\n";

return $output;
}

private function getFormattedMessagesForFile(array $messages, string $file): string {
if (count($messages) === 0) {
return '';
}

$xmlOutputForFile = "\t<file name=\"{$file}\">\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<error line=\"%d\" column=\"%d\" severity=\"%s\" message=\"%s\" source=\"%s\"/>\n",
$line,
$column,
$severity,
$messageText,
$source
);
return $output;
}, '');
$xmlOutputForFile .= "\t</file>\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;
}
}
5 changes: 4 additions & 1 deletion PhpcsChanged/Cli.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -144,7 +145,7 @@ function printHelp(): void {
printTwoColumns([
'--standard <STANDARD>' => 'The phpcs standard to use.',
'--extensions <EXTENSIONS>' => 'A comma separated list of extensions to check.',
'--report <REPORTER>' => 'The phpcs reporter to use. One of "full" (default), "json", "xml", or "junit".',
'--report <REPORTER>' => '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 <PATTERNS>' => 'A comma separated list of patterns to ignore files and directories.',
'--warning-severity' => 'The phpcs warning severity to report. See phpcs documentation for usage.',
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions index.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
247 changes: 247 additions & 0 deletions tests/CheckstyleReporterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
<?php
declare(strict_types=1);

require_once dirname(__DIR__) . '/index.php';
require_once __DIR__ . '/helpers/helpers.php';

use PHPUnit\Framework\TestCase;
use PhpcsChanged\PhpcsMessages;
use PhpcsChanged\CheckstyleReporter;

final class CheckstyleReporterTest extends TestCase {
public function testSingleWarning() {
$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');
$expected = <<<EOF
<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="phpcs-changed-2.11.8">
<file name="fileA.php">
<error line="15" column="5" severity="warning" message="Found unused symbol Foo." source="ImportDetection.Imports.RequireImports.Import"/>
</file>
</checkstyle>

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
<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="phpcs-changed-2.11.8">
<file name="fileA.php">
<error line="15" column="5" severity="error" message="Found unused symbol Foo." source="ImportDetection.Imports.RequireImports.Import"/>
</file>
</checkstyle>

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
<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="phpcs-changed-2.11.8">
<file name="fileA.php">
<error line="133825" column="5" severity="warning" message="Found unused symbol Foo." source="ImportDetection.Imports.RequireImports.Import"/>
<error line="15" column="5" severity="warning" message="Found unused symbol Bar." source="ImportDetection.Imports.RequireImports.Import"/>
</file>
</checkstyle>

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
<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="phpcs-changed-2.11.8">
<file name="fileA.php">
<error line="12" column="2" severity="error" message="Found unused symbol Faa." source="ImportDetection.Imports.RequireImports.Something"/>
<error line="15" column="5" severity="error" message="Found unused symbol Foo." source="ImportDetection.Imports.RequireImports.Import"/>
<error line="18" column="8" severity="warning" message="Found unused symbol Bar." source="ImportDetection.Imports.RequireImports.Boom"/>
</file>
<file name="fileB.php">
<error line="30" column="5" severity="warning" message="Found unused symbol Hi." source="ImportDetection.Imports.RequireImports.Zoop"/>
</file>
</checkstyle>

EOF;
$reporter = new CheckstyleReporter();
$result = $reporter->getFormattedMessages($messages, ['s' => 1]);
$this->assertEquals($expected, $result);
}

public function testNoWarnings() {
$messages = PhpcsMessages::fromArrays([]);
$expected = <<<EOF
<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="phpcs-changed-2.11.8">
</checkstyle>

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
<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="phpcs-changed-2.11.8">
<file name="STDIN">
<error line="15" column="5" severity="warning" message="Found unused symbol Foo." source="ImportDetection.Imports.RequireImports.Import"/>
</file>
</checkstyle>

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 <xml> & "quotes".',
],
], 'fileA.php');
$expected = <<<EOF
<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="phpcs-changed-2.11.8">
<file name="fileA.php">
<error line="15" column="5" severity="error" message="Message with &lt;xml&gt; &amp; &quot;quotes&quot;." source="Test.Source&lt;&gt;&amp;&quot;"/>
</file>
</checkstyle>

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