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:
parent
e89a2929d5
commit
c4ce8bede9
10
bin/psalm
10
bin/psalm
@ -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
|
||||||
|
@ -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
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;
|
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;
|
||||||
|
|
||||||
|
@ -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
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