1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Add JSON output format

This commit is contained in:
Matthew Brown 2016-12-07 22:38:57 -05:00
parent 6356f28a1f
commit a5195b2571
9 changed files with 427 additions and 142 deletions

View File

@ -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);
}
}
}

View File

@ -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;
}

View File

@ -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));

View File

@ -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()
);
}

View File

@ -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);
}
}

View File

@ -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];
}
}

View File

@ -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;
}
}

View File

@ -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
View 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'])
);
}
}