mirror of
https://github.com/danog/psalm.git
synced 2025-01-21 21:31:13 +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:
parent
e89a2929d5
commit
c4ce8bede9
10
bin/psalm
10
bin/psalm
@ -14,7 +14,8 @@ $options = getopt(
|
||||
[
|
||||
'help', 'debug', 'config:', 'monochrome', 'show-info:', 'diff',
|
||||
'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
|
||||
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;
|
||||
|
||||
exit;
|
||||
@ -313,7 +318,8 @@ $project_checker = new ProjectChecker(
|
||||
$debug,
|
||||
$update_docblocks,
|
||||
$find_dead_code || $find_references_to !== null,
|
||||
$find_references_to
|
||||
$find_references_to,
|
||||
isset($options['report'])?$options['report']:null
|
||||
);
|
||||
|
||||
// initialise custom config, if passed
|
||||
|
@ -12,7 +12,8 @@
|
||||
"require": {
|
||||
"php": "^5.6 || ^7.0",
|
||||
"nikic/PHP-Parser": "^3.0.4",
|
||||
"composer/composer": "^1.3"
|
||||
"composer/composer": "^1.3",
|
||||
"openlss/lib-array2xml": "^0.5.1"
|
||||
},
|
||||
"bin": ["bin/psalm"],
|
||||
"autoload": {
|
||||
|
2457
composer.lock
generated
2457
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@ -229,6 +229,11 @@ class ProjectChecker
|
||||
*/
|
||||
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)
|
||||
*
|
||||
@ -239,16 +244,20 @@ class ProjectChecker
|
||||
const TYPE_CONSOLE = 'console';
|
||||
const TYPE_JSON = 'json';
|
||||
const TYPE_EMACS = 'emacs';
|
||||
const TYPE_XML = 'xml';
|
||||
|
||||
/**
|
||||
* @param bool $use_color
|
||||
* @param bool $show_info
|
||||
* @param bool $debug_output
|
||||
* @param string $output_format
|
||||
* @param int $threads
|
||||
* @param bool $update_docblocks
|
||||
* @param bool $collect_references
|
||||
* @param string $find_references_to
|
||||
* @param FileProvider $file_provider
|
||||
* @param CacheProvider $cache_provider
|
||||
* @param bool $use_color
|
||||
* @param bool $show_info
|
||||
* @param string $output_format
|
||||
* @param int $threads
|
||||
* @param bool $debug_output
|
||||
* @param bool $update_docblocks
|
||||
* @param bool $collect_references
|
||||
* @param string $find_references_to
|
||||
* @param string $reports
|
||||
*/
|
||||
public function __construct(
|
||||
FileProvider $file_provider,
|
||||
@ -260,7 +269,8 @@ class ProjectChecker
|
||||
$debug_output = false,
|
||||
$update_docblocks = false,
|
||||
$collect_references = false,
|
||||
$find_references_to = null
|
||||
$find_references_to = null,
|
||||
$reports = null
|
||||
) {
|
||||
$this->file_provider = $file_provider;
|
||||
$this->cache_provider = $cache_provider;
|
||||
@ -272,10 +282,31 @@ class ProjectChecker
|
||||
$this->collect_references = $collect_references;
|
||||
$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);
|
||||
}
|
||||
|
||||
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;
|
||||
self::$instance = $this;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
<?php
|
||||
namespace Psalm;
|
||||
|
||||
use LSS\Array2XML;
|
||||
use Psalm\Checker\ProjectChecker;
|
||||
use Psalm\Issue\CodeIssue;
|
||||
|
||||
@ -201,24 +202,18 @@ class IssueBuffer
|
||||
$project_checker = ProjectChecker::getInstance();
|
||||
|
||||
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) {
|
||||
if ($issue_data['severity'] === Config::REPORT_ERROR) {
|
||||
$has_error = true;
|
||||
}
|
||||
|
||||
echo self::getEmacsOutput($issue_data) . PHP_EOL;
|
||||
foreach (self::$issues_data as $issue_data) {
|
||||
if ($issue_data['severity'] === Config::REPORT_ERROR) {
|
||||
$has_error = true;
|
||||
}
|
||||
} 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
|
||||
*
|
||||
|
186
tests/ReportOutputTest.php
Normal file
186
tests/ReportOutputTest.php
Normal 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)
|
||||
//);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user