Skip to content

Commit

Permalink
Add checkstyle reporting ability
Browse files Browse the repository at this point in the history
Often used or usable with myriad CI processes, checkstyle allows
various tools to automatically interpret lint output and do what you
wish with them.
  • Loading branch information
johnbacon committed Aug 2, 2023
1 parent b1d5c86 commit 4422bfa
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 18 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ vendor/
.php-cs-fixer.cache
.phpunit.result.cache
composer.lock
/.idea
2 changes: 1 addition & 1 deletion bin/tlint
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env php
<?php

const TLINT_VERSION = 'v9.0.0';
const TLINT_VERSION = 'v9.1.0';

foreach (
[
Expand Down
5 changes: 5 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ Want the output from a file as JSON? (Primarily used for integration with editor
tlint lint test.php --json
```

Want the output from a file as a [checkstyle XML report](https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/v3.22.0/doc/schemas/fix/checkstyle.xsd)? (Primarily used with CI tools like [reviewdog](https://github.com/reviewdog/reviewdog) and [cs2pr](https://github.com/staabm/annotate-pull-request-from-checkstyle))
```
tlint lint test.php --checkstyle
```

Want to only run a single linter?

```
Expand Down
49 changes: 49 additions & 0 deletions src/Commands/LintCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Tighten\TLint\Commands;

use DOMDocument;
use DOMElement;
use PhpParser\Error;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
Expand All @@ -17,6 +19,7 @@ class LintCommand extends BaseCommand
{
private const NO_LINTS_FOUND_OR_SUCCESS = 0;
private const LINTS_FOUND_OR_ERROR = 1;
private DOMElement $checkstyle;

protected function configure()
{
Expand All @@ -36,6 +39,9 @@ protected function configure()
new InputOption(
'json'
),
new InputOption(
'checkstyle'
),
new InputOption(
'only',
null,
Expand All @@ -55,6 +61,13 @@ protected function execute(InputInterface $input, OutputInterface $output)
return self::NO_LINTS_FOUND_OR_SUCCESS;
}

if ($input->getOption('checkstyle')) {
$dom = new DOMDocument('1.0', 'UTF-8');
$this->checkstyle = $dom->createElement('checkstyle');
$this->checkstyle->setAttribute('version', '3.7.2');
$dom->appendChild($this->checkstyle);
}

if (is_file($fileOrDirectory)) {
$finalResponseCode = $this->lintFile($input, $output, $fileOrDirectory);
} elseif (is_dir($fileOrDirectory)) {
Expand All @@ -79,6 +92,13 @@ protected function execute(InputInterface $input, OutputInterface $output)
return self::NO_LINTS_FOUND_OR_SUCCESS;
}

if ($input->getOption('checkstyle')) {
$dom->formatOutput = true;
$output->write($dom->saveXML());

return self::NO_LINTS_FOUND_OR_SUCCESS;
}

if ($finalResponseCode === self::NO_LINTS_FOUND_OR_SUCCESS) {
$output->writeLn('LGTM!');
}
Expand Down Expand Up @@ -134,6 +154,10 @@ private function lintFile(InputInterface $input, OutputInterface $output, $file)
return $this->outputLintsAsJson($output, $file, $lints);
}

if ($input->getOption('checkstyle')) {
return $this->outputLintsAsCheckstyle($output, $file, $lints);
}

return $this->outputLints($output, $file, $lints);
}

Expand All @@ -156,6 +180,31 @@ private function outputLintsAsJson(OutputInterface $output, $file, $lints)
return self::NO_LINTS_FOUND_OR_SUCCESS;
}

private function outputLintsAsCheckstyle(OutputInterface $output, $file, $lints)
{
if (! empty($lints)) {
foreach ($lints as $lint) {
$fileNode = $this->checkstyle->ownerDocument->createElement('file');
$fileNode->setAttribute('name', $file);
$this->checkstyle->appendChild($fileNode);

$title = explode(PHP_EOL, (string) $lint)[0];

$errorNode = $this->checkstyle->ownerDocument->createElement('error');
$errorNode->setAttribute('line', $lint->getNode()->getStartLine());
$errorNode->setAttribute('severity', 'error');
$errorNode->setAttribute('message', $title);
$errorNode->setAttribute('source', basename(str_replace('\\', '/', get_class($lint->getLinter()))));

$fileNode->appendChild($errorNode);
}

return self::LINTS_FOUND_OR_ERROR;
}

return self::NO_LINTS_FOUND_OR_SUCCESS;
}

private function outputLints(OutputInterface $output, $file, $lints)
{
if (! empty($lints)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,10 +289,10 @@ class Thing
$this->assertSame($file, $formatted);
}

/** @test */
public function catches_missing_line_between_visibility_changes_in_anon_class()
{
$file = <<<'file'
/** @test */
public function catches_missing_line_between_visibility_changes_in_anon_class()
{
$file = <<<'file'
<?php
namespace App;
Expand All @@ -312,7 +312,7 @@ public function getThing(): NodeVisitorAbstract
}
file;

$expected = <<<'file'
$expected = <<<'file'
<?php
namespace App;
Expand All @@ -333,8 +333,8 @@ public function getThing(): NodeVisitorAbstract
}
file;

$formatted = (new TFormat)->format(new OneLineBetweenClassVisibilityChanges($file));
$formatted = (new TFormat)->format(new OneLineBetweenClassVisibilityChanges($file));

$this->assertSame($expected, $formatted);
}
$this->assertSame($expected, $formatted);
}
}
88 changes: 88 additions & 0 deletions tests/Linting/CanOutputLintsAsCheckstyleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

namespace Tests\Linting;

use DomDocument;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Tighten\TLint\Commands\LintCommand;
use Tighten\TLint\Linters\OneLineBetweenClassVisibilityChanges;

class CanOutputLintsAsCheckstyleTest extends TestCase
{
/** @test */
public function can_use_checkstyle_flag_with_lints()
{
$application = new Application;
$command = new LintCommand;
$application->add($command);
$commandTester = new CommandTester($command);

$file = <<<'file'
<?php
class Test
{
public $test1;
private $test2;
}

file;

$filePath = tempnam(sys_get_temp_dir(), 'test');

file_put_contents($filePath, $file);

$commandTester->execute([
'command' => $command->getName(),
'file or directory' => $filePath,
'--checkstyle' => true,
]);

$output = $commandTester->getDisplay();
$xml = new DomDocument;
$xml->loadXML($output);
$error = $xml->getElementsByTagName('error')->item(0)->attributes;

$this->assertEquals('6', $error->getNamedItem('line')->nodeValue);
$this->assertEquals('error', $error->getNamedItem('severity')->nodeValue);
$this->assertEquals('! ' . OneLineBetweenClassVisibilityChanges::DESCRIPTION, $error->getNamedItem('message')->nodeValue);
$this->assertEquals('OneLineBetweenClassVisibilityChanges', $error->getNamedItem('source')->nodeValue);

$this->assertEquals(0, $commandTester->getStatusCode());
}

/** @test */
public function can_use_checkstyle_flag_without_lints()
{
$application = new Application;
$command = new LintCommand;
$application->add($command);
$commandTester = new CommandTester($command);

$file = <<<'file'
<?php
echo 'a';

file;

$filePath = tempnam(sys_get_temp_dir(), 'test');

file_put_contents($filePath, $file);

$commandTester->execute([
'command' => $command->getName(),
'file or directory' => $filePath,
'--checkstyle' => true,
]);

$output = $commandTester->getDisplay();

$xml = new DOMDocument;
$xml->loadXML($output);
$this->assertEquals(0, $xml->getElementsByTagName('file')->length);
$this->assertEquals(0, $commandTester->getStatusCode());
}
}
18 changes: 9 additions & 9 deletions tests/Linting/Linters/UseAuthHelperOverFacadeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,23 @@ public function catches_auth_facade_usage_in_code()
$this->assertEquals(4, $lints[0]->getNode()->getLine());
}

/** @test */
public function does_not_trigger_on_non_auth_call()
{
$file = <<<file
/** @test */
public function does_not_trigger_on_non_auth_call()
{
$file = <<<file
<?php
use Some\Other\AuthClass as Auth;
echo Auth::user()->name;
file;

$lints = (new TLint)->lint(
new UseAuthHelperOverFacade($file, '.php')
);
$lints = (new TLint)->lint(
new UseAuthHelperOverFacade($file, '.php')
);

$this->assertEmpty($lints);
}
$this->assertEmpty($lints);
}

/** @test */
public function does_not_trigger_on_non_facade_call()
Expand Down

0 comments on commit 4422bfa

Please sign in to comment.