1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-22 05:41:20 +01:00

Add XML as possible output format + add report generation (#206)

* Add XML as possible output format + add report generation

* Add missing xml root node

* Change XML generator (previous one don't escape '<' and '>')

* Change option (only one option) + unit test
This commit is contained in:
MacFJA 2017-09-08 17:18:48 +02:00 committed by Matthew Brown
parent e89a2929d5
commit c4ce8bede9
6 changed files with 1532 additions and 1233 deletions

View File

@ -14,7 +14,8 @@ $options = getopt(
[ [
'help', 'debug', 'config:', 'monochrome', 'show-info:', 'diff', 'help', 'debug', 'config:', 'monochrome', 'show-info:', 'diff',
'file:', 'self-check', 'update-docblocks', 'output-format:', 'file:', 'self-check', 'update-docblocks', 'output-format:',
'find-dead-code', 'init', 'find-references-to:', 'root:', 'threads:' 'find-dead-code', 'init', 'find-references-to:', 'root:', 'threads:',
'report:'
] ]
); );
@ -88,6 +89,10 @@ Options:
--threads=INT --threads=INT
If greater than one, Psalm will run analysis on multiple threads, speeding things up. If greater than one, Psalm will run analysis on multiple threads, speeding things up.
--report=PATH
The path where to output report file. The output format is base on the file extension.
(Currently supported format: ".json", ".xml", ".txt")
HELP; HELP;
exit; exit;
@ -313,7 +318,8 @@ $project_checker = new ProjectChecker(
$debug, $debug,
$update_docblocks, $update_docblocks,
$find_dead_code || $find_references_to !== null, $find_dead_code || $find_references_to !== null,
$find_references_to $find_references_to,
isset($options['report'])?$options['report']:null
); );
// initialise custom config, if passed // initialise custom config, if passed

View File

@ -12,7 +12,8 @@
"require": { "require": {
"php": "^5.6 || ^7.0", "php": "^5.6 || ^7.0",
"nikic/PHP-Parser": "^3.0.4", "nikic/PHP-Parser": "^3.0.4",
"composer/composer": "^1.3" "composer/composer": "^1.3",
"openlss/lib-array2xml": "^0.5.1"
}, },
"bin": ["bin/psalm"], "bin": ["bin/psalm"],
"autoload": { "autoload": {

2457
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -229,6 +229,11 @@ class ProjectChecker
*/ */
public $infer_types_from_usage = false; public $infer_types_from_usage = false;
/**
* @var array<string,string>
*/
public $reports = [];
/** /**
* Whether to log functions just at the file level or globally (for stubs) * Whether to log functions just at the file level or globally (for stubs)
* *
@ -239,16 +244,20 @@ class ProjectChecker
const TYPE_CONSOLE = 'console'; const TYPE_CONSOLE = 'console';
const TYPE_JSON = 'json'; const TYPE_JSON = 'json';
const TYPE_EMACS = 'emacs'; const TYPE_EMACS = 'emacs';
const TYPE_XML = 'xml';
/** /**
* @param FileProvider $file_provider
* @param CacheProvider $cache_provider
* @param bool $use_color * @param bool $use_color
* @param bool $show_info * @param bool $show_info
* @param bool $debug_output
* @param string $output_format * @param string $output_format
* @param int $threads * @param int $threads
* @param bool $debug_output
* @param bool $update_docblocks * @param bool $update_docblocks
* @param bool $collect_references * @param bool $collect_references
* @param string $find_references_to * @param string $find_references_to
* @param string $reports
*/ */
public function __construct( public function __construct(
FileProvider $file_provider, FileProvider $file_provider,
@ -260,7 +269,8 @@ class ProjectChecker
$debug_output = false, $debug_output = false,
$update_docblocks = false, $update_docblocks = false,
$collect_references = false, $collect_references = false,
$find_references_to = null $find_references_to = null,
$reports = null
) { ) {
$this->file_provider = $file_provider; $this->file_provider = $file_provider;
$this->cache_provider = $cache_provider; $this->cache_provider = $cache_provider;
@ -272,10 +282,31 @@ class ProjectChecker
$this->collect_references = $collect_references; $this->collect_references = $collect_references;
$this->find_references_to = $find_references_to; $this->find_references_to = $find_references_to;
if (!in_array($output_format, [self::TYPE_CONSOLE, self::TYPE_JSON, self::TYPE_EMACS], true)) { if (!in_array($output_format, [self::TYPE_CONSOLE, self::TYPE_JSON, self::TYPE_EMACS, self::TYPE_XML], true)) {
throw new \UnexpectedValueException('Unrecognised output format ' . $output_format); throw new \UnexpectedValueException('Unrecognised output format ' . $output_format);
} }
if ($reports) {
/**
* @var array<string,string>
*/
$mapping = [
'.xml' => self::TYPE_XML,
'.json' => self::TYPE_JSON,
'.txt' => self::TYPE_EMACS,
'.emacs' => self::TYPE_EMACS,
];
foreach ($mapping as $extension => $type) {
if (substr($reports, -strlen($extension)) === $extension) {
$this->reports[$type] = $reports;
break;
}
}
if (empty($this->reports)) {
throw new \UnexpectedValueException('Unrecognised report format ' . $reports);
}
}
$this->output_format = $output_format; $this->output_format = $output_format;
self::$instance = $this; self::$instance = $this;

View File

@ -1,6 +1,7 @@
<?php <?php
namespace Psalm; namespace Psalm;
use LSS\Array2XML;
use Psalm\Checker\ProjectChecker; use Psalm\Checker\ProjectChecker;
use Psalm\Issue\CodeIssue; use Psalm\Issue\CodeIssue;
@ -201,24 +202,18 @@ class IssueBuffer
$project_checker = ProjectChecker::getInstance(); $project_checker = ProjectChecker::getInstance();
if (self::$issues_data) { if (self::$issues_data) {
if ($project_checker->output_format === ProjectChecker::TYPE_JSON) {
echo json_encode(self::$issues_data) . PHP_EOL;
} elseif ($project_checker->output_format === ProjectChecker::TYPE_EMACS) {
foreach (self::$issues_data as $issue_data) { foreach (self::$issues_data as $issue_data) {
if ($issue_data['severity'] === Config::REPORT_ERROR) { if ($issue_data['severity'] === Config::REPORT_ERROR) {
$has_error = true; $has_error = true;
} }
echo self::getEmacsOutput($issue_data) . PHP_EOL;
}
} else {
foreach (self::$issues_data as $issue_data) {
if ($issue_data['severity'] === Config::REPORT_ERROR) {
$has_error = true;
} }
echo self::getConsoleOutput($issue_data, $project_checker->use_color) . PHP_EOL . PHP_EOL; echo self::getOutput($project_checker->output_format, $project_checker->use_color);
} foreach ($project_checker->reports as $format => $path) {
file_put_contents(
$path,
self::getOutput($format, $project_checker->use_color)
);
} }
} }
@ -236,6 +231,37 @@ class IssueBuffer
} }
} }
/**
* @param string $format
* @param bool $useColor
*
* @return string
*/
public static function getOutput($format, $useColor)
{
if ($format === ProjectChecker::TYPE_JSON) {
return json_encode(self::$issues_data) . PHP_EOL;
} elseif ($format === ProjectChecker::TYPE_XML) {
$xml = Array2XML::createXML('report', ['item' => self::$issues_data]);
return $xml->saveXML();
} elseif ($format === ProjectChecker::TYPE_EMACS) {
$output = '';
foreach (self::$issues_data as $issue_data) {
$output .= self::getEmacsOutput($issue_data) . PHP_EOL;
}
return $output;
}
$output = '';
foreach (self::$issues_data as $issue_data) {
$output .= self::getConsoleOutput($issue_data, $useColor) . PHP_EOL . PHP_EOL;
}
return $output;
}
/** /**
* @param string $message * @param string $message
* *

186
tests/ReportOutputTest.php Normal file
View File

@ -0,0 +1,186 @@
<?php
namespace Psalm\Tests;
use LSS\XML2Array;
use Psalm\Checker\FileChecker;
use Psalm\Checker\ProjectChecker;
use Psalm\IssueBuffer;
class ReportOutputTest extends TestCase
{
/**
* @return void
*/
public function setUp()
{
// `TestCase::setUp()` creates its own ProjectChecker and Config instance, but we don't want to do that in this
// case, so don't run a `parent::setUp()` call here.
FileChecker::clearCache();
$this->file_provider = new Provider\FakeFileProvider();
$this->project_checker = new \Psalm\Checker\ProjectChecker(
$this->file_provider,
new Provider\FakeCacheProvider(),
false
);
$this->project_checker->reports['json'] = __DIR__ . '/test-report.json';
$config = new TestConfig();
$config->throw_exception = false;
$config->stop_on_first_error = false;
$this->project_checker->setConfig($config);
}
/**
* @return void
*/
public function testReportFormatValid()
{
// No exception
foreach (['.xml', '.txt', '.json', '.emacs'] as $extension) {
$checker = new \Psalm\Checker\ProjectChecker(
$this->file_provider,
new Provider\FakeCacheProvider(),
false,
true,
\Psalm\Checker\ProjectChecker::TYPE_CONSOLE,
1,
false,
false,
false,
null,
'/tmp/report' . $extension
);
}
}
/**
* @expectedException \UnexpectedValueException
*
* @return void
*/
public function testReportFormatException()
{
$checker = new \Psalm\Checker\ProjectChecker(
$this->file_provider,
new Provider\FakeCacheProvider(),
false,
true,
\Psalm\Checker\ProjectChecker::TYPE_CONSOLE,
1,
false,
false,
false,
null,
'/tmp/report.log'
);
}
/**
* @return void
*/
public function testJsonOutputForGetPsalmDotOrg()
{
$file_contents = '<?php
function psalmCanVerify(int $your_code) : ?string {
return $as_you . "type";
}
// and it supports PHP 5.4 - 7.1
echo CHANGE_ME;
if (rand(0, 100) > 10) {
$a = 5;
} else {
//$a = 2;
}
echo $a;';
$this->addFile(
'somefile.php',
$file_contents
);
$file_checker = new FileChecker('somefile.php', $this->project_checker);
$file_checker->visitAndAnalyzeMethods();
$issue_data = [
[
'severity' => 'error',
'line_number' => 7,
'type' => 'UndefinedConstant',
'message' => 'Const CHANGE_ME is not defined',
'file_name' => 'somefile.php',
'file_path' => 'somefile.php',
'snippet' => 'echo CHANGE_ME;',
'from' => 126,
'to' => 135,
'snippet_from' => 121,
'snippet_to' => 136,
'column' => 6,
],
[
'severity' => 'error',
'line_number' => 15,
'type' => 'PossiblyUndefinedVariable',
'message' => 'Possibly undefined variable $a, first seen on line 10',
'file_name' => 'somefile.php',
'file_path' => 'somefile.php',
'snippet' => 'echo $a',
'from' => 202,
'to' => 204,
'snippet_from' => 197,
'snippet_to' => 204,
'column' => 6,
],
[
'severity' => 'error',
'line_number' => 3,
'type' => 'UndefinedVariable',
'message' => 'Cannot find referenced variable $as_you',
'file_name' => 'somefile.php',
'file_path' => 'somefile.php',
'snippet' => ' return $as_you . "type";',
'from' => 67,
'to' => 74,
'snippet_from' => 58,
'snippet_to' => 84,
'column' => 10,
],
[
'severity' => 'error',
'line_number' => 2,
'type' => 'MixedInferredReturnType',
'message' => 'Could not verify return type \'string|null\' for psalmCanVerify',
'file_name' => 'somefile.php',
'file_path' => 'somefile.php',
'snippet' => 'function psalmCanVerify(int $your_code) : ?string {
return $as_you . "type";
}',
'from' => 48,
'to' => 55,
'snippet_from' => 6,
'snippet_to' => 86,
'column' => 43,
],
];
$emacs = 'somefile.php:7:6:error - Const CHANGE_ME is not defined
somefile.php:15:6:error - Possibly undefined variable $a, first seen on line 10
somefile.php:3:10:error - Cannot find referenced variable $as_you
somefile.php:2:43:error - Could not verify return type \'string|null\' for psalmCanVerify
';
$this->assertSame(
$issue_data,
json_decode(IssueBuffer::getOutput(ProjectChecker::TYPE_JSON, false), true)
);
$this->assertSame(
$emacs,
IssueBuffer::getOutput(ProjectChecker::TYPE_EMACS, false)
);
// FIXME: The XML parser only return strings, all int value are casted, so the assertSame failed
//$this->assertSame(
// ['report' => ['item' => $issue_data]],
// XML2Array::createArray(IssueBuffer::getOutput(ProjectChecker::TYPE_XML, false), LIBXML_NOCDATA)
//);
}
}