mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Add JSON output format
This commit is contained in:
parent
6356f28a1f
commit
a5195b2571
25
bin/psalm
25
bin/psalm
@ -19,7 +19,13 @@ ini_set('memory_limit', '2048M');
|
||||
ini_set('xdebug.max_nesting_level', 512);
|
||||
|
||||
// get options from command line
|
||||
$options = getopt('f:m:hc:', ['help', 'debug', 'config:', 'monochrome', 'show-info:', 'diff', 'file:', 'self-check', 'update-docblocks']);
|
||||
$options = getopt(
|
||||
'f:m:hc:',
|
||||
[
|
||||
'help', 'debug', 'config:', 'monochrome', 'show-info:', 'diff',
|
||||
'file:', 'self-check', 'update-docblocks', 'output-format:',
|
||||
]
|
||||
);
|
||||
|
||||
if (array_key_exists('help', $options)) {
|
||||
$options['h'] = false;
|
||||
@ -47,6 +53,7 @@ Options:
|
||||
--diff File to check is a diff
|
||||
--self-check Psalm checks itself
|
||||
--update-docblocks Adds correct return types to the given file(s)
|
||||
--output-format=json Changes the output format
|
||||
|
||||
|
||||
HELP;
|
||||
@ -59,11 +66,12 @@ $debug = array_key_exists('debug', $options);
|
||||
|
||||
if (isset($options['f'])) {
|
||||
$input_paths = is_array($options['f']) ? $options['f'] : [$options['f']];
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
$input_paths = $argv ? $argv : null;
|
||||
}
|
||||
|
||||
$output_format = isset($options['output-format']) ? $options['output-format'] : ProjectChecker::TYPE_CONSOLE;
|
||||
|
||||
$paths_to_check = null;
|
||||
|
||||
if ($input_paths) {
|
||||
@ -120,21 +128,20 @@ if ($path_to_config) {
|
||||
ProjectChecker::setConfigXML($path_to_config);
|
||||
}
|
||||
|
||||
ProjectChecker::$use_color = $use_color;
|
||||
ProjectChecker::$show_info = $show_info;
|
||||
$project_checker = new ProjectChecker($use_color, $show_info, $output_format);
|
||||
|
||||
$time = microtime(true);
|
||||
|
||||
if (array_key_exists('self-check', $options)) {
|
||||
ProjectChecker::checkDir(dirname(__DIR__) . '/src', $debug);
|
||||
$project_checker->checkDir(dirname(__DIR__) . '/src', $debug);
|
||||
} elseif ($paths_to_check === null) {
|
||||
ProjectChecker::check($debug, $is_diff);
|
||||
$project_checker->check($debug, $is_diff);
|
||||
} elseif ($paths_to_check) {
|
||||
foreach ($paths_to_check as $path_to_check) {
|
||||
if (is_dir($path_to_check)) {
|
||||
ProjectChecker::checkDir($path_to_check, $debug, $update_docblocks);
|
||||
$project_checker->checkDir($path_to_check, $debug, $update_docblocks);
|
||||
} else {
|
||||
ProjectChecker::checkFile($path_to_check, $debug, $update_docblocks);
|
||||
$project_checker->checkFile($path_to_check, $debug, $update_docblocks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -855,7 +855,10 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
}
|
||||
}
|
||||
|
||||
FileChecker::addFileReferenceToClass(Config::getInstance()->getBaseDir() . $code_location->file_name, $fq_class_name);
|
||||
FileChecker::addFileReferenceToClass(
|
||||
Config::getInstance()->getBaseDir() . $code_location->file_name,
|
||||
$fq_class_name
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -118,10 +118,10 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
public function __construct($file_name, array $preloaded_statements = [])
|
||||
{
|
||||
$this->file_path = $file_name;
|
||||
$this->file_name = Config::getInstance()->shortenFileName($file_name);
|
||||
$this->file_name = Config::getInstance()->shortenFileName($this->file_path);
|
||||
|
||||
self::$file_checkers[$this->file_name] = $this;
|
||||
self::$file_checkers[$file_name] = $this;
|
||||
self::$file_checkers[$this->file_path] = $this;
|
||||
|
||||
if ($preloaded_statements) {
|
||||
$this->preloaded_statements = $preloaded_statements;
|
||||
@ -143,7 +143,7 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
$cache = true,
|
||||
$update_docblocks = false
|
||||
) {
|
||||
if ($cache && isset(self::$functions_checked[$this->file_name])) {
|
||||
if ($cache && isset(self::$functions_checked[$this->file_path])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -343,13 +343,14 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $file_name
|
||||
* @param string $file_path
|
||||
* @return array<int, \PhpParser\Node>
|
||||
*/
|
||||
public static function getStatementsForFile($file_name)
|
||||
public static function getStatementsForFile($file_path)
|
||||
{
|
||||
$stmts = [];
|
||||
|
||||
$project_checker = ProjectChecker::getInstance();
|
||||
$root_cache_directory = Config::getInstance()->getCacheDirectory();
|
||||
$parser_cache_directory = $root_cache_directory
|
||||
? $root_cache_directory . '/' . self::PARSER_CACHE_DIRECTORY
|
||||
@ -361,9 +362,9 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
|
||||
$version = 'parsercache3';
|
||||
|
||||
$file_contents = (string)file_get_contents($file_name);
|
||||
$file_contents = $project_checker->getFileContents($file_path);
|
||||
$file_content_hash = md5($version . $file_contents);
|
||||
$name_cache_key = self::getParserCacheKey($file_name);
|
||||
$name_cache_key = self::getParserCacheKey($file_path);
|
||||
|
||||
if (self::$file_content_hashes === null) {
|
||||
/** @var array<string, string> */
|
||||
@ -379,7 +380,7 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
if (isset(self::$file_content_hashes[$name_cache_key]) &&
|
||||
$file_content_hash === self::$file_content_hashes[$name_cache_key] &&
|
||||
is_readable($cache_location) &&
|
||||
filemtime($cache_location) > filemtime($file_name)
|
||||
filemtime($cache_location) > filemtime($file_path)
|
||||
) {
|
||||
/** @var array<int, \PhpParser\Node> */
|
||||
$stmts = unserialize((string)file_get_contents($cache_location));
|
||||
|
@ -349,7 +349,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
|
||||
$statements_checker->registerVariable(
|
||||
$function_param->name,
|
||||
$function_param->code_location->line_number
|
||||
$function_param->code_location->getLineNumber()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -14,21 +14,60 @@ class ProjectChecker
|
||||
*
|
||||
* @var Config|null
|
||||
*/
|
||||
protected static $config;
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* @var self
|
||||
*/
|
||||
public static $instance;
|
||||
|
||||
/**
|
||||
* Whether or not to use colors in error output
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public static $use_color = true;
|
||||
public $use_color;
|
||||
|
||||
/**
|
||||
* Whether or not to show informational messages
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public static $show_info = true;
|
||||
public $show_info;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $output_format;
|
||||
|
||||
/**
|
||||
* @var array<string, string>
|
||||
*/
|
||||
public $fake_files = [];
|
||||
|
||||
const TYPE_CONSOLE = 'console';
|
||||
const TYPE_JSON = 'json';
|
||||
|
||||
public function __construct($use_color = true, $show_info = true, $output_format = self::TYPE_CONSOLE)
|
||||
{
|
||||
$this->use_color = $use_color;
|
||||
$this->show_info = $show_info;
|
||||
|
||||
if (!in_array($output_format, [self::TYPE_CONSOLE, self::TYPE_JSON])) {
|
||||
throw new \UnexpectedValueException('Unrecognised output format ' . $output_format);
|
||||
}
|
||||
|
||||
$this->output_format = $output_format;
|
||||
self::$instance = $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return self
|
||||
*/
|
||||
public static function getInstance()
|
||||
{
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param boolean $debug
|
||||
@ -36,7 +75,7 @@ class ProjectChecker
|
||||
* @param boolean $update_docblocks
|
||||
* @return void
|
||||
*/
|
||||
public static function check($debug = false, $is_diff = false, $update_docblocks = false)
|
||||
public function check($debug = false, $is_diff = false, $update_docblocks = false)
|
||||
{
|
||||
$cwd = getcwd();
|
||||
|
||||
@ -46,8 +85,8 @@ class ProjectChecker
|
||||
throw new \InvalidArgumentException('Cannot work with empty cwd');
|
||||
}
|
||||
|
||||
if (!self::$config) {
|
||||
self::$config = self::getConfigForPath($cwd);
|
||||
if (!$this->config) {
|
||||
$this->config = self::getConfigForPath($cwd);
|
||||
}
|
||||
|
||||
$diff_files = null;
|
||||
@ -57,16 +96,16 @@ class ProjectChecker
|
||||
$deleted_files = FileChecker::getDeletedReferencedFiles();
|
||||
$diff_files = $deleted_files;
|
||||
|
||||
foreach (self::$config->getIncludeDirs() as $dir_name) {
|
||||
$diff_files = array_merge($diff_files, self::getDiffFilesInDir($dir_name, self::$config));
|
||||
foreach ($this->config->getIncludeDirs() as $dir_name) {
|
||||
$diff_files = array_merge($diff_files, self::getDiffFilesInDir($dir_name, $this->config));
|
||||
}
|
||||
}
|
||||
|
||||
$files_checked = [];
|
||||
|
||||
if ($diff_files === null || $deleted_files === null || count($diff_files) > 200) {
|
||||
foreach (self::$config->getIncludeDirs() as $dir_name) {
|
||||
self::checkDirWithConfig($dir_name, self::$config, $debug, $update_docblocks);
|
||||
foreach ($this->config->getIncludeDirs() as $dir_name) {
|
||||
$this->checkDirWithConfig($dir_name, $this->config, $debug, $update_docblocks);
|
||||
}
|
||||
} else {
|
||||
if ($debug) {
|
||||
@ -77,7 +116,7 @@ class ProjectChecker
|
||||
|
||||
// strip out deleted files
|
||||
$file_list = array_diff($file_list, $deleted_files);
|
||||
self::checkDiffFilesWithConfig(self::$config, $debug, $file_list);
|
||||
$this->checkDiffFilesWithConfig($this->config, $debug, $file_list);
|
||||
}
|
||||
|
||||
$removed_parser_files = FileChecker::deleteOldParserCaches(
|
||||
@ -89,7 +128,7 @@ class ProjectChecker
|
||||
}
|
||||
|
||||
if ($is_diff) {
|
||||
FileChecker::touchParserCaches(self::getAllFiles(self::$config), $start_checks);
|
||||
FileChecker::touchParserCaches($this->getAllFiles($this->config), $start_checks);
|
||||
}
|
||||
|
||||
IssueBuffer::finish(true, (int)$start_checks);
|
||||
@ -101,18 +140,18 @@ class ProjectChecker
|
||||
* @param boolean $update_docblocks
|
||||
* @return void
|
||||
*/
|
||||
public static function checkDir($dir_name, $debug = false, $update_docblocks = false)
|
||||
public function checkDir($dir_name, $debug = false, $update_docblocks = false)
|
||||
{
|
||||
if (!self::$config) {
|
||||
self::$config = self::getConfigForPath($dir_name);
|
||||
self::$config->hide_external_errors = self::$config->isInProjectDirs(
|
||||
self::$config->shortenFileName($dir_name)
|
||||
if (!$this->config) {
|
||||
$this->config = self::getConfigForPath($dir_name);
|
||||
$this->config->hide_external_errors = $this->config->isInProjectDirs(
|
||||
$this->config->shortenFileName($dir_name)
|
||||
);
|
||||
}
|
||||
|
||||
FileChecker::loadReferenceCache();
|
||||
|
||||
self::checkDirWithConfig($dir_name, self::$config, $debug, $update_docblocks);
|
||||
$this->checkDirWithConfig($dir_name, $this->config, $debug, $update_docblocks);
|
||||
|
||||
IssueBuffer::finish();
|
||||
}
|
||||
@ -124,7 +163,7 @@ class ProjectChecker
|
||||
* @param bool $update_docblocks
|
||||
* @return void
|
||||
*/
|
||||
protected static function checkDirWithConfig($dir_name, Config $config, $debug, $update_docblocks)
|
||||
protected function checkDirWithConfig($dir_name, Config $config, $debug, $update_docblocks)
|
||||
{
|
||||
$file_extensions = $config->getFileExtensions();
|
||||
$filetype_handlers = $config->getFiletypeHandlers();
|
||||
@ -162,7 +201,7 @@ class ProjectChecker
|
||||
* @param Config $config
|
||||
* @return array<int, string>
|
||||
*/
|
||||
protected static function getAllFiles(Config $config)
|
||||
protected function getAllFiles(Config $config)
|
||||
{
|
||||
$file_extensions = $config->getFileExtensions();
|
||||
$file_names = [];
|
||||
@ -227,7 +266,7 @@ class ProjectChecker
|
||||
* @param array<string> $file_list
|
||||
* @return void
|
||||
*/
|
||||
protected static function checkDiffFilesWithConfig(Config $config, $debug, array $file_list = [])
|
||||
protected function checkDiffFilesWithConfig(Config $config, $debug, array $file_list = [])
|
||||
{
|
||||
$file_extensions = $config->getFileExtensions();
|
||||
$filetype_handlers = $config->getFiletypeHandlers();
|
||||
@ -270,25 +309,25 @@ class ProjectChecker
|
||||
* @param bool $update_docblocks
|
||||
* @return void
|
||||
*/
|
||||
public static function checkFile($file_name, $debug = false, $update_docblocks = false)
|
||||
public function checkFile($file_name, $debug = false, $update_docblocks = false)
|
||||
{
|
||||
if ($debug) {
|
||||
echo 'Checking ' . $file_name . PHP_EOL;
|
||||
}
|
||||
|
||||
if (!self::$config) {
|
||||
self::$config = self::getConfigForPath($file_name);
|
||||
if (!$this->config) {
|
||||
$this->config = self::getConfigForPath($file_name);
|
||||
}
|
||||
|
||||
self::$config->hide_external_errors = self::$config->isInProjectDirs(
|
||||
self::$config->shortenFileName($file_name)
|
||||
$this->config->hide_external_errors = $this->config->isInProjectDirs(
|
||||
$this->config->shortenFileName($file_name)
|
||||
);
|
||||
|
||||
$file_name_parts = explode('.', $file_name);
|
||||
|
||||
$extension = array_pop($file_name_parts);
|
||||
|
||||
$filetype_handlers = self::$config->getFiletypeHandlers();
|
||||
$filetype_handlers = $this->config->getFiletypeHandlers();
|
||||
|
||||
FileChecker::loadReferenceCache();
|
||||
|
||||
@ -353,7 +392,7 @@ class ProjectChecker
|
||||
* @return void
|
||||
* @throws Exception\ConfigException If a config file is not found in the given location.
|
||||
*/
|
||||
public static function setConfigXML($path_to_config)
|
||||
public function setConfigXML($path_to_config)
|
||||
{
|
||||
if (!file_exists($path_to_config)) {
|
||||
throw new Exception\ConfigException('Config not found at location ' . $path_to_config);
|
||||
@ -361,13 +400,13 @@ class ProjectChecker
|
||||
|
||||
$dir_path = dirname($path_to_config) . '/';
|
||||
|
||||
self::$config = Config::loadFromXML($path_to_config);
|
||||
$this->config = Config::loadFromXML($path_to_config);
|
||||
|
||||
if (self::$config->autoloader) {
|
||||
require_once($dir_path . self::$config->autoloader);
|
||||
if ($this->config->autoloader) {
|
||||
require_once($dir_path . $this->config->autoloader);
|
||||
}
|
||||
|
||||
self::$config->collectPredefinedConstants();
|
||||
$this->config->collectPredefinedConstants();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -397,4 +436,27 @@ class ProjectChecker
|
||||
|
||||
return array_unique($all_files_to_check);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $file_path
|
||||
* @param string $file_contents
|
||||
* @return void
|
||||
*/
|
||||
public function registerFile($file_path, $file_contents)
|
||||
{
|
||||
$this->fake_files[$file_path] = $file_contents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $file_path
|
||||
* @return string
|
||||
*/
|
||||
public function getFileContents($file_path)
|
||||
{
|
||||
if (isset($this->fake_files[$file_path])) {
|
||||
return $this->fake_files[$file_path];
|
||||
}
|
||||
|
||||
return (string)file_get_contents($file_path);
|
||||
}
|
||||
}
|
||||
|
@ -3,12 +3,6 @@ namespace Psalm;
|
||||
|
||||
class CodeLocation
|
||||
{
|
||||
/** @var int */
|
||||
public $file_start;
|
||||
|
||||
/** @var int */
|
||||
public $file_end;
|
||||
|
||||
/** @var string */
|
||||
public $file_path;
|
||||
|
||||
@ -16,19 +10,40 @@ class CodeLocation
|
||||
public $file_name;
|
||||
|
||||
/** @var int */
|
||||
public $line_number;
|
||||
|
||||
/** @var bool */
|
||||
public $single_line;
|
||||
protected $line_number;
|
||||
|
||||
/** @var int */
|
||||
public $preview_start;
|
||||
protected $file_start;
|
||||
|
||||
/** @var int */
|
||||
protected $file_end;
|
||||
|
||||
/** @var bool */
|
||||
protected $single_line;
|
||||
|
||||
/** @var int */
|
||||
protected $preview_start;
|
||||
|
||||
/** @var int */
|
||||
protected $preview_end = -1;
|
||||
|
||||
/** @var int */
|
||||
protected $selection_start = -1;
|
||||
|
||||
/** @var int */
|
||||
protected $selection_end = -1;
|
||||
|
||||
/** @var string */
|
||||
protected $snippet = '';
|
||||
|
||||
/** @var int|null */
|
||||
public $comment_line_number;
|
||||
protected $docblock_line_number;
|
||||
|
||||
/** @var string|null */
|
||||
public $regex;
|
||||
protected $regex;
|
||||
|
||||
/** @var boolean */
|
||||
private $have_recalculated = false;
|
||||
|
||||
/**
|
||||
* @param StatementsSource $statements_source
|
||||
@ -36,8 +51,12 @@ class CodeLocation
|
||||
* @param boolean $single_line
|
||||
* @param string $regex A regular expression to select part of the snippet
|
||||
*/
|
||||
public function __construct(StatementsSource $statements_source, \PhpParser\Node $stmt, $single_line = false, $regex = null)
|
||||
{
|
||||
public function __construct(
|
||||
StatementsSource $statements_source,
|
||||
\PhpParser\Node $stmt,
|
||||
$single_line = false,
|
||||
$regex = null
|
||||
) {
|
||||
$this->file_start = (int)$stmt->getAttribute('startFilePos');
|
||||
$this->file_end = (int)$stmt->getAttribute('endFilePos');
|
||||
$this->file_path = $statements_source->getCheckedFilePath();
|
||||
@ -56,6 +75,115 @@ class CodeLocation
|
||||
*/
|
||||
public function setCommentLine($line)
|
||||
{
|
||||
$this->comment_line_number = $line;
|
||||
$this->docblock_line_number = $this->line_number;
|
||||
$this->line_number = $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-suppress MixedArrayAccess
|
||||
* @return void
|
||||
*/
|
||||
private function calculateRealLocation()
|
||||
{
|
||||
if ($this->have_recalculated) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->selection_start = $this->file_start;
|
||||
$this->selection_end = $this->file_end;
|
||||
|
||||
$project_checker = Checker\ProjectChecker::getInstance();
|
||||
|
||||
$file_contents = $project_checker->getFileContents($this->file_path);
|
||||
|
||||
$this->preview_end = (int)strpos(
|
||||
$file_contents,
|
||||
"\n",
|
||||
$this->single_line ? $this->selection_start : $this->selection_end
|
||||
);
|
||||
|
||||
if ($this->docblock_line_number && $this->preview_start < $this->selection_start) {
|
||||
$preview_lines = explode(
|
||||
"\n",
|
||||
substr(
|
||||
$file_contents,
|
||||
$this->preview_start,
|
||||
$this->selection_start - $this->preview_start - 1
|
||||
)
|
||||
);
|
||||
|
||||
$preview_offset = 0;
|
||||
|
||||
$i = 0;
|
||||
|
||||
$comment_line_offset = $this->line_number - $this->docblock_line_number;
|
||||
|
||||
for ($i = 0; $i < $comment_line_offset; $i++) {
|
||||
$preview_offset += strlen($preview_lines[$i]) + 1;
|
||||
}
|
||||
|
||||
$preview_offset += (int)strpos($preview_lines[$i], '@');
|
||||
|
||||
$this->selection_start = $preview_offset + $this->preview_start;
|
||||
$this->selection_end = (int)strpos($file_contents, "\n", $this->selection_start);
|
||||
} elseif ($this->regex) {
|
||||
$preview_snippet = substr(
|
||||
$file_contents,
|
||||
$this->selection_start,
|
||||
$this->selection_end - $this->selection_start
|
||||
);
|
||||
|
||||
if (preg_match($this->regex, $preview_snippet, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
$this->selection_start = $this->selection_start + (int)$matches[1][1];
|
||||
$this->selection_end = $this->selection_start + strlen((string)$matches[1][0]);
|
||||
}
|
||||
}
|
||||
|
||||
// reset preview start to beginning of line
|
||||
$this->preview_start = (int)strrpos(
|
||||
$file_contents,
|
||||
"\n",
|
||||
min($this->preview_start, $this->selection_start) - strlen($file_contents)
|
||||
) + 1;
|
||||
|
||||
$this->snippet = substr($file_contents, $this->preview_start, $this->preview_end - $this->preview_start);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getLineNumber()
|
||||
{
|
||||
return $this->line_number;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSnippet()
|
||||
{
|
||||
$this->calculateRealLocation();
|
||||
|
||||
return $this->snippet;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getSelectionBounds()
|
||||
{
|
||||
$this->calculateRealLocation();
|
||||
|
||||
return [$this->selection_start, $this->selection_end];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, int>
|
||||
*/
|
||||
public function getSnippetBounds()
|
||||
{
|
||||
$this->calculateRealLocation();
|
||||
|
||||
return [$this->preview_start, $this->preview_end];
|
||||
}
|
||||
}
|
||||
|
@ -32,15 +32,23 @@ abstract class CodeIssue
|
||||
*/
|
||||
public function getLineNumber()
|
||||
{
|
||||
return $this->code_location->line_number;
|
||||
return $this->code_location->getLineNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[]
|
||||
* @return CodeLocation
|
||||
*/
|
||||
public function getFileRange()
|
||||
public function getLocation()
|
||||
{
|
||||
return [$this->code_location->file_start, $this->code_location->file_end];
|
||||
return $this->code_location;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getShortLocation()
|
||||
{
|
||||
return $this->code_location->file_name . ':' . $this->code_location->getLineNumber();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -64,70 +72,6 @@ abstract class CodeIssue
|
||||
*/
|
||||
public function getMessage()
|
||||
{
|
||||
return $this->code_location->file_name . ':' . $this->code_location->line_number .' - ' . $this->message;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @psalm-suppress MixedArrayAccess
|
||||
*/
|
||||
public function getFileSnippet()
|
||||
{
|
||||
$selection_start = $this->code_location->file_start;
|
||||
$selection_end = $this->code_location->file_end;
|
||||
|
||||
$preview_start = $this->code_location->preview_start;
|
||||
|
||||
$file_contents = (string)file_get_contents($this->code_location->file_path);
|
||||
|
||||
$preview_end = (int)strpos(
|
||||
$file_contents,
|
||||
"\n",
|
||||
$this->code_location->single_line ? $selection_start : $selection_end
|
||||
);
|
||||
|
||||
if ($this->code_location->comment_line_number && $preview_start < $selection_start) {
|
||||
$preview_lines = explode(
|
||||
"\n",
|
||||
substr($file_contents, $preview_start, $selection_start - $preview_start - 1)
|
||||
);
|
||||
|
||||
$preview_offset = 0;
|
||||
|
||||
$i = 0;
|
||||
|
||||
$comment_line_offset = $this->code_location->comment_line_number - $this->code_location->line_number;
|
||||
|
||||
for ($i = 0; $i < $comment_line_offset; $i++) {
|
||||
$preview_offset += strlen($preview_lines[$i]) + 1;
|
||||
}
|
||||
|
||||
$preview_offset += (int)strpos($preview_lines[$i], '@');
|
||||
|
||||
$selection_start = $preview_offset + $preview_start;
|
||||
$selection_end = (int)strpos($file_contents, "\n", $selection_start);
|
||||
} elseif ($this->code_location->regex) {
|
||||
$preview_snippet = substr($file_contents, $selection_start, $selection_end - $selection_start);
|
||||
|
||||
if (preg_match($this->code_location->regex, $preview_snippet, $matches, PREG_OFFSET_CAPTURE)) {
|
||||
$selection_start = $selection_start + (int)$matches[1][1];
|
||||
$selection_end = $selection_start + strlen((string)$matches[1][0]);
|
||||
}
|
||||
}
|
||||
|
||||
// reset preview start to beginning of line
|
||||
$preview_start = (int)strrpos(
|
||||
$file_contents,
|
||||
"\n",
|
||||
min($preview_start, $selection_start) - strlen($file_contents)
|
||||
) + 1;
|
||||
|
||||
$code_line = substr($file_contents, $preview_start, $preview_end - $preview_start);
|
||||
|
||||
$code_line_error_start = $selection_start - $preview_start;
|
||||
$code_line_error_length = $selection_end - $selection_start + 1;
|
||||
return substr($code_line, 0, $code_line_error_start) .
|
||||
"\e[97;41m" . substr($code_line, $code_line_error_start, $code_line_error_length) .
|
||||
"\e[0m" . substr($code_line, $code_line_error_length + $code_line_error_start) . PHP_EOL;
|
||||
return $this->message;
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,11 @@ use Psalm\Checker\ProjectChecker;
|
||||
|
||||
class IssueBuffer
|
||||
{
|
||||
/**
|
||||
* @var array<int, array>
|
||||
*/
|
||||
protected static $issue_data = [];
|
||||
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
@ -46,11 +51,12 @@ class IssueBuffer
|
||||
public static function add(Issue\CodeIssue $e)
|
||||
{
|
||||
$config = Config::getInstance();
|
||||
$project_checker = ProjectChecker::getInstance();
|
||||
|
||||
$fqcn_parts = explode('\\', get_class($e));
|
||||
$issue_type = array_pop($fqcn_parts);
|
||||
|
||||
$error_message = $issue_type . ' - ' . $e->getMessage();
|
||||
$error_message = $issue_type . ' - ' . $e->getShortLocation() . ' - ' . $e->getMessage();
|
||||
|
||||
$reporting_level = $config->getReportingLevel($issue_type);
|
||||
|
||||
@ -59,8 +65,16 @@ class IssueBuffer
|
||||
}
|
||||
|
||||
if ($reporting_level === Config::REPORT_INFO) {
|
||||
if (ProjectChecker::$show_info && !self::alreadyEmitted($error_message)) {
|
||||
echo 'INFO: ' . $error_message . PHP_EOL;
|
||||
if ($project_checker->show_info && !self::alreadyEmitted($error_message)) {
|
||||
switch ($project_checker->output_format) {
|
||||
case ProjectChecker::TYPE_CONSOLE:
|
||||
echo 'INFO: ' . $error_message . PHP_EOL;
|
||||
break;
|
||||
|
||||
case ProjectChecker::TYPE_JSON:
|
||||
self::$issue_data[] = self::getIssueArray($e, Config::REPORT_INFO);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -70,10 +84,19 @@ class IssueBuffer
|
||||
}
|
||||
|
||||
if (!self::alreadyEmitted($error_message)) {
|
||||
echo (ProjectChecker::$use_color ? "\e[0;31m" : '') . 'ERROR: ' .
|
||||
(ProjectChecker::$use_color ? "\e[0m" : '') . $error_message . PHP_EOL;
|
||||
switch ($project_checker->output_format) {
|
||||
case ProjectChecker::TYPE_CONSOLE:
|
||||
echo ($project_checker->use_color ? "\e[0;31mERROR\e[0m" : 'ERROR') .
|
||||
': ' . $error_message . PHP_EOL;
|
||||
|
||||
echo $e->getFileSnippet() . PHP_EOL;
|
||||
echo self::getSnippet($e, $project_checker->use_color) . PHP_EOL . PHP_EOL;
|
||||
|
||||
break;
|
||||
|
||||
case ProjectChecker::TYPE_JSON:
|
||||
self::$issue_data[] = self::getIssueArray($e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($config->stop_on_first_error) {
|
||||
@ -85,6 +108,62 @@ class IssueBuffer
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Issue\CodeIssue $e
|
||||
* @param string $severity
|
||||
* @return array
|
||||
*/
|
||||
protected static function getIssueArray(Issue\CodeIssue $e, $severity = Config::REPORT_ERROR)
|
||||
{
|
||||
$location = $e->getLocation();
|
||||
$selection_bounds = $location->getSelectionBounds();
|
||||
|
||||
return [
|
||||
'type' => $severity,
|
||||
'line_number' => $location->getLineNumber(),
|
||||
'message' => $e->getMessage(),
|
||||
'file_name' => $location->file_name,
|
||||
'file_path' => $location->file_path,
|
||||
'snippet' => $location->getSnippet(),
|
||||
'from' => $selection_bounds[0],
|
||||
'to' => $selection_bounds[1],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array>
|
||||
*/
|
||||
public static function getIssueData()
|
||||
{
|
||||
return self::$issue_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Issue\CodeIssue $e
|
||||
* @param boolean $use_color
|
||||
* @return string
|
||||
*/
|
||||
protected static function getSnippet(Issue\CodeIssue $e, $use_color = true)
|
||||
{
|
||||
$location = $e->getLocation();
|
||||
|
||||
$snippet = $location->getSnippet();
|
||||
|
||||
if (!$use_color) {
|
||||
return $snippet;
|
||||
}
|
||||
|
||||
$snippet_bounds = $location->getSnippetBounds();
|
||||
$selection_bounds = $location->getSelectionBounds();
|
||||
|
||||
$selection_start = $selection_bounds[0] - $snippet_bounds[0];
|
||||
$selection_length = $selection_bounds[1] - $selection_bounds[0] + 1;
|
||||
|
||||
return substr($snippet, 0, $selection_start) .
|
||||
"\e[97;41m" . substr($snippet, $selection_start, $selection_length) .
|
||||
"\e[0m" . substr($snippet, $selection_length + $selection_start) . PHP_EOL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $is_full
|
||||
* @param int|null $start_time
|
||||
@ -95,6 +174,11 @@ class IssueBuffer
|
||||
Checker\FileChecker::updateReferenceCache();
|
||||
|
||||
if (count(self::$errors)) {
|
||||
$project_checker = ProjectChecker::getInstance();
|
||||
if ($project_checker->output_format === ProjectChecker::TYPE_JSON) {
|
||||
echo json_encode(self::$issue_data) . PHP_EOL;
|
||||
}
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
56
tests/JsonOutputTest.php
Normal file
56
tests/JsonOutputTest.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
namespace Psalm\Tests;
|
||||
|
||||
use PhpParser\ParserFactory;
|
||||
use PHPUnit_Framework_TestCase;
|
||||
use Psalm\Checker\FileChecker;
|
||||
use Psalm\Checker\ProjectChecker;
|
||||
use Psalm\Config;
|
||||
use Psalm\Context;
|
||||
use Psalm\IssueBuffer;
|
||||
|
||||
class JsonOutputTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
protected static $parser;
|
||||
|
||||
public static function setUpBeforeClass()
|
||||
{
|
||||
self::$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
|
||||
|
||||
$config = Config::getInstance();
|
||||
$config->throw_exception = false;
|
||||
$config->cache_directory = null;
|
||||
$config->stop_on_first_error = false;
|
||||
}
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
FileChecker::clearCache();
|
||||
}
|
||||
|
||||
public function testJsonOutput()
|
||||
{
|
||||
$file_contents = '<?php
|
||||
function foo(int $a) : string {
|
||||
return $a + 1;
|
||||
}';
|
||||
|
||||
$project_checker = new ProjectChecker(false, true, ProjectChecker::TYPE_JSON);
|
||||
$project_checker->registerFile(
|
||||
'somefile.php',
|
||||
$file_contents
|
||||
);
|
||||
|
||||
$file_checker = new FileChecker('somefile.php');
|
||||
$file_checker->check();
|
||||
$issue_data = IssueBuffer::getIssueData()[0];
|
||||
$this->assertSame('somefile.php', $issue_data['file_path']);
|
||||
$this->assertSame('error', $issue_data['type']);
|
||||
$this->assertSame("The given return type 'string' for foo is incorrect, got 'int'", $issue_data['message']);
|
||||
$this->assertSame(2, $issue_data['line_number']);
|
||||
$this->assertSame(
|
||||
'string',
|
||||
substr($file_contents, $issue_data['from'], $issue_data['to'] - $issue_data['from'])
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user