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

Show code snippets when reporting errors

This also introduces a new method of identifying specific code locations when creating issues
This commit is contained in:
Matthew Brown 2016-12-03 19:11:30 -05:00
parent 1d603b11d0
commit a1acbfec07
28 changed files with 642 additions and 516 deletions

View File

@ -15,12 +15,16 @@ class StringChecker extends \Psalm\Plugin
* checks an expression
* @param PhpParser\Node\Expr $stmt
* @param Context $context
* @param string $file_name
* @param CodeLocation $file_name
* @param array<string> $suppressed_issues
* @return null|false
*/
public function checkExpression(PhpParser\Node\Expr $stmt, \Psalm\Context $context, $file_name, array $suppressed_issues)
{
public function checkExpression(
PhpParser\Node\Expr $stmt,
Context $context,
CodeLocation $code_location,
array $suppressed_issues
) {
if ($stmt instanceof \PhpParser\Node\Scalar\String_) {
$class_or_class_method = '/^\\\?Psalm(\\\[A-Z][A-Za-z0-9]+)+(::[A-Za-z0-9]+)?$/';
@ -29,9 +33,9 @@ class StringChecker extends \Psalm\Plugin
if (Checker\ClassChecker::checkFullyQualifiedClassLikeName(
$fq_class_name,
$file_name,
$stmt->getLine(),
$suppressed_issues) === false
$code_location,
$suppressed_issues
) === false
) {
return false;
}
@ -39,9 +43,9 @@ class StringChecker extends \Psalm\Plugin
if ($fq_class_name !== $stmt->value) {
if (Checker\MethodChecker::checkMethodExists(
$stmt->value,
$file_name,
$stmt->getLine(),
$suppressed_issues)
$code_location,
$suppressed_issues
)
) {
return false;
}

View File

@ -2,6 +2,7 @@
namespace Psalm\Checker;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Config;
use Psalm\Context;
use Psalm\Exception\DocblockParseException;
@ -202,7 +203,9 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
$this->aliased_constants = $source->getAliasedConstants();
$this->aliased_functions = $source->getAliasedFunctions();
$this->file_name = $source->getFileName();
$this->file_path = $source->getFilePath();
$this->include_file_name = $source->getIncludeFileName();
$this->include_file_path = $source->getIncludeFilePath();
$this->fq_class_name = $fq_class_name;
$this->suppressed_issues = $source->getSuppressedIssues();
@ -289,8 +292,7 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
foreach ($parent_interfaces as $interface_name) {
if (self::checkFullyQualifiedClassLikeName(
$interface_name,
$this->file_name,
$this->class->getLine(),
new CodeLocation($this, $this->class, true),
$this->getSuppressedIssues()
) === false) {
return false;
@ -392,8 +394,7 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
if (IssueBuffer::accepts(
new UnimplementedInterfaceMethod(
'Method ' . $method_name . ' is not defined on class ' . $this->fq_class_name,
$this->file_name,
$this->class->getLine()
new CodeLocation($this, $this->class)
),
$this->suppressed_issues
)) {
@ -434,10 +435,17 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
*/
protected function registerParentClassProperties($parent_class)
{
if (!$this->class instanceof PhpParser\Node\Stmt\Class_) {
throw new \UnexpectedValueException('Cannot register parent class where none exists');
}
if (!$this->class->extends) {
throw new \UnexpectedValueException('Cannot register parent class where none exists');
}
if (self::checkFullyQualifiedClassLikeName(
$parent_class,
$this->file_name,
$this->class->getLine(),
new CodeLocation($this, $this->class->extends, true),
$this->getSuppressedIssues()
) === false
) {
@ -535,7 +543,10 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
if (!TraitChecker::traitExists($trait_name)) {
if (IssueBuffer::accepts(
new UndefinedTrait('Trait ' . $trait_name . ' does not exist', $this->file_name, $trait->getLine()),
new UndefinedTrait(
'Trait ' . $trait_name . ' does not exist',
new CodeLocation($this, $trait)
),
$this->suppressed_issues
)) {
return false;
@ -547,8 +558,7 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
if (IssueBuffer::accepts(
new UndefinedTrait(
'Trait ' . $trait_name . ' has wrong casing',
$this->file_name,
$trait->getLine()
new CodeLocation($this, $trait)
),
$this->suppressed_issues
)) {
@ -596,13 +606,11 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
if ($comment && $config->use_docblock_types) {
try {
$type_in_comment = CommentChecker::getTypeFromComment((string) $comment, null, $this);
}
catch (DocblockParseException $e) {
} catch (DocblockParseException $e) {
if (IssueBuffer::accepts(
new InvalidDocblock(
(string)$e->getMessage(),
$this->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($this, $this->class, true)
)
)) {
return false;
@ -613,8 +621,7 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
new MissingPropertyType(
'Property ' . $this->fq_class_name . '::$' . $stmt->props[0]->name . ' does not have a ' .
'declared type',
$this->file_name,
$stmt->getLine()
new CodeLocation($this, $stmt)
),
$this->suppressed_issues
)) {
@ -757,15 +764,13 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
/**
* @param string $fq_class_name
* @param string $file_name
* @param int $line_number
* @param CodeLocation $code_location
* @param array<string> $suppressed_issues
* @return bool|null
*/
public static function checkFullyQualifiedClassLikeName(
$fq_class_name,
$file_name,
$line_number,
CodeLocation $code_location,
array $suppressed_issues
) {
if (empty($fq_class_name)) {
@ -781,8 +786,7 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
if (IssueBuffer::accepts(
new UndefinedClass(
'Class or interface ' . $fq_class_name . ' does not exist',
$file_name,
$line_number
$code_location
),
$suppressed_issues
)) {
@ -798,8 +802,7 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
if (IssueBuffer::accepts(
new InvalidClass(
'Class or interface ' . $fq_class_name . ' has wrong casing',
$file_name,
$line_number
$code_location
),
$suppressed_issues
)) {
@ -807,7 +810,7 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
}
}
FileChecker::addFileReferenceToClass(Config::getInstance()->getBaseDir() . $file_name, $fq_class_name);
FileChecker::addFileReferenceToClass(Config::getInstance()->getBaseDir() . $code_location->file_name, $fq_class_name);
return true;
}

View File

@ -78,23 +78,25 @@ class CommentChecker
}
/**
* @param string $comment
* @param string $comment
* @param int $line_number
* @return FunctionDocblockComment
* @throws DocblockParseException If there was a problem parsing the docblock.
* @psalm-suppress MixedArrayAccess
*/
public static function extractDocblockInfo($comment)
public static function extractDocblockInfo($comment, $line_number)
{
$comments = self::parseDocComment($comment);
$comments = self::parseDocComment($comment, $line_number);
$info = new FunctionDocblockComment();
if (isset($comments['specials']['return']) || isset($comments['specials']['psalm-return'])) {
$return_block = trim(
isset($comments['specials']['psalm-return'])
? (string)$comments['specials']['psalm-return'][0]
: (string)$comments['specials']['return'][0]
);
/** @var array */
$return_specials = isset($comments['specials']['psalm-return'])
? $comments['specials']['psalm-return']
: $comments['specials']['return'];
$return_block = trim((string)reset($return_specials));
try {
$line_parts = self::splitDocLine($return_block);
@ -107,11 +109,12 @@ class CommentChecker
&& !strpos($line_parts[0], '::')
) {
$info->return_type = $line_parts[0];
$info->return_type_line_number = array_keys($return_specials)[0];
}
}
if (isset($comments['specials']['param'])) {
foreach ($comments['specials']['param'] as $param) {
foreach ($comments['specials']['param'] as $line_number => $param) {
try {
$line_parts = self::splitDocLine((string)$param);
} catch (DocblockParseException $e) {
@ -128,7 +131,11 @@ class CommentChecker
$line_parts[1] = substr($line_parts[1], 1);
}
$info->params[] = ['name' => substr($line_parts[1], 1), 'type' => $line_parts[0]];
$info->params[] = [
'name' => substr($line_parts[1], 1),
'type' => $line_parts[0],
'line_number' => $line_number
];
}
}
}
@ -202,9 +209,10 @@ class CommentChecker
* https://github.com/facebook/libphutil/blob/master/src/parser/docblock/PhutilDocblockParser.php
*
* @param string $docblock
* @param int $line_number
* @return array Array of the main comment and specials
*/
public static function parseDocComment($docblock)
public static function parseDocComment($docblock, $line_number = null)
{
// Strip off comments.
$docblock = trim($docblock);
@ -214,6 +222,13 @@ class CommentChecker
// Normalize multi-line @specials.
$lines = explode("\n", $docblock);
$line_map = [];
if ($line_number) {
$line_number++;
}
$last = false;
foreach ($lines as $k => $line) {
if (preg_match('/^\s?@\w/i', $line)) {
@ -224,6 +239,10 @@ class CommentChecker
$lines[$last] = rtrim($lines[$last]).' '.trim($line);
unset($lines[$k]);
}
if ($line_number) {
$line_map[$line] = $line_number++;
}
}
$docblock = implode("\n", $lines);
@ -235,14 +254,14 @@ class CommentChecker
$have_specials = preg_match_all('/^\s?@([\w\-:]+)\s*([^\n]*)/m', $docblock, $matches, PREG_SET_ORDER);
if ($have_specials) {
$docblock = preg_replace('/^\s?@([\w\-:]+)\s*([^\n]*)/m', '', $docblock);
foreach ($matches as $match) {
foreach ($matches as $m => $match) {
list($_, $type, $data) = $match;
if (empty($special[$type])) {
$special[$type] = array();
$special[$type] = [];
}
$special[$type][] = $data;
$special[$type][$line_map && isset($line_map[$_]) ? $line_map[$_] : $m] = $data;
}
}

View File

@ -17,7 +17,7 @@ class FileChecker extends SourceChecker implements StatementsSource
/**
* @var string
*/
protected $real_file_name;
protected $file_path;
/**
* @var array<string, array<string, string>>
@ -117,7 +117,7 @@ class FileChecker extends SourceChecker implements StatementsSource
*/
public function __construct($file_name, array $preloaded_statements = [])
{
$this->real_file_name = $file_name;
$this->file_path = $file_name;
$this->file_name = Config::getInstance()->shortenFileName($file_name);
self::$file_checkers[$this->file_name] = $this;
@ -147,11 +147,11 @@ class FileChecker extends SourceChecker implements StatementsSource
return null;
}
if ($cache && $check_classes && !$check_functions && isset(self::$classes_checked[$this->real_file_name])) {
if ($cache && $check_classes && !$check_functions && isset(self::$classes_checked[$this->file_path])) {
return null;
}
if ($cache && !$check_classes && !$check_functions && isset(self::$files_checked[$this->real_file_name])) {
if ($cache && !$check_classes && !$check_functions && isset(self::$files_checked[$this->file_path])) {
return null;
}
@ -249,19 +249,19 @@ class FileChecker extends SourceChecker implements StatementsSource
}
if ($check_functions) {
self::$functions_checked[$this->real_file_name] = true;
self::$functions_checked[$this->file_path] = true;
}
if ($check_classes) {
self::$classes_checked[$this->real_file_name] = true;
self::$classes_checked[$this->file_path] = true;
}
self::$files_checked[$this->real_file_name] = true;
self::$files_checked[$this->file_path] = true;
if ($update_docblocks && isset(self::$docblock_return_types[$this->file_name])) {
$line_upset = 0;
$file_lines = explode(PHP_EOL, (string)file_get_contents($this->real_file_name));
$file_lines = explode(PHP_EOL, (string)file_get_contents($this->file_path));
$file_docblock_updates = self::$docblock_return_types[$this->file_name];
@ -269,7 +269,7 @@ class FileChecker extends SourceChecker implements StatementsSource
self::updateDocblock($file_lines, $line_number, $line_upset, $type[0], $type[1], $type[2]);
}
file_put_contents($this->real_file_name, implode(PHP_EOL, $file_lines));
file_put_contents($this->file_path, implode(PHP_EOL, $file_lines));
echo 'Added/updated ' . count($file_docblock_updates) . ' docblocks in ' . $this->file_name . PHP_EOL;
}
@ -321,7 +321,7 @@ class FileChecker extends SourceChecker implements StatementsSource
{
return $this->preloaded_statements
? $this->preloaded_statements
: self::getStatementsForFile($this->real_file_name);
: self::getStatementsForFile($this->file_path);
}
/**
@ -339,8 +339,10 @@ class FileChecker extends SourceChecker implements StatementsSource
$cache_location = null;
$name_cache_key = null;
$version = 'parsercache2';
$file_contents = (string)file_get_contents($file_name);
$file_content_hash = md5($file_contents);
$file_content_hash = md5($version . $file_contents);
$name_cache_key = self::getParserCacheKey($file_name);
if (self::$file_content_hashes === null) {
@ -365,7 +367,13 @@ class FileChecker extends SourceChecker implements StatementsSource
}
if (!$stmts) {
$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
$lexer = new PhpParser\Lexer([
'usedAttributes' => [
'comments', 'startLine', 'startFilePos', 'endFilePos'
]
]);
$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7, $lexer);
$stmts = $parser->parse($file_contents);
}
@ -481,14 +489,6 @@ class FileChecker extends SourceChecker implements StatementsSource
return $this->aliased_classes_flipped;
}
/**
* @return string
*/
public function getRealFileName()
{
return $this->real_file_name;
}
/**
* @param string $file_name
* @return mixed

View File

@ -2,6 +2,7 @@
namespace Psalm\Checker;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Config;
use Psalm\EffectsAnalyser;
use Psalm\Exception\DocblockParseException;
@ -194,9 +195,7 @@ class FunctionChecker extends FunctionLikeChecker
foreach ($function->getParams() as $param) {
$param_array = self::getTranslatedParam(
$param,
$this->fq_class_name,
$this->namespace,
$this->getAliasedClasses()
$this
);
self::$file_function_params[$file_name][$function_id][] = $param_array;
@ -210,63 +209,69 @@ class FunctionChecker extends FunctionLikeChecker
$this->suppressed_issues = [];
try {
$docblock_info = CommentChecker::extractDocblockInfo((string)$function->getDocComment());
} catch (DocblockParseException $e) {
if (IssueBuffer::accepts(
new InvalidDocblock(
'Invalid type passed in docblock for ' . $this->getMethodId(),
$this->getCheckedFileName(),
$function->getLine()
)
)) {
return false;
}
}
$doc_comment = $function->getDocComment();
if ($docblock_info) {
if ($docblock_info->deprecated) {
self::$deprecated_functions[$file_name][$function_id] = true;
}
if ($docblock_info->variadic) {
self::$variadic_functions[$file_name][$function_id] = true;
}
$this->suppressed_issues = $docblock_info->suppress;
if ($function->returnType) {
$return_type = Type::parseString(
is_string($function->returnType)
? $function->returnType
: ClassLikeChecker::getFQCLNFromNameObject(
$function->returnType,
$this->namespace,
$this->getAliasedClasses()
)
if ($doc_comment) {
try {
$docblock_info = CommentChecker::extractDocblockInfo(
(string)$doc_comment,
$doc_comment->getLine()
);
} catch (DocblockParseException $e) {
if (IssueBuffer::accepts(
new InvalidDocblock(
'Invalid type passed in docblock for ' . $this->getMethodId(),
new CodeLocation($this, $function, true)
)
)) {
return false;
}
}
if ($config->use_docblock_types) {
if ($docblock_info->return_type) {
$return_type =
Type::parseString(
self::fixUpLocalType(
(string)$docblock_info->return_type,
null,
if ($docblock_info) {
if ($docblock_info->deprecated) {
self::$deprecated_functions[$file_name][$function_id] = true;
}
if ($docblock_info->variadic) {
self::$variadic_functions[$file_name][$function_id] = true;
}
$this->suppressed_issues = $docblock_info->suppress;
if ($function->returnType) {
$return_type = Type::parseString(
is_string($function->returnType)
? $function->returnType
: ClassLikeChecker::getFQCLNFromNameObject(
$function->returnType,
$this->namespace,
$this->getAliasedClasses()
)
);
);
}
if ($docblock_info->params) {
$this->improveParamsFromDocblock(
$docblock_info->params,
$function_param_names,
self::$file_function_params[$file_name][$function_id],
$function->getLine()
);
if ($config->use_docblock_types) {
if ($docblock_info->return_type) {
$return_type =
Type::parseString(
self::fixUpLocalType(
(string)$docblock_info->return_type,
null,
$this->namespace,
$this->getAliasedClasses()
)
);
}
if ($docblock_info->params) {
$this->improveParamsFromDocblock(
$docblock_info->params,
$function_param_names,
self::$file_function_params[$file_name][$function_id],
new CodeLocation($this, $function, false)
);
}
}
}
}
@ -326,6 +331,7 @@ class FunctionChecker extends FunctionLikeChecker
$arg_name,
$by_reference,
$arg_type ? Type::parseString($arg_type) : Type::getMixed(),
null,
$optional,
false,
$arg_name === '...'
@ -362,16 +368,14 @@ class FunctionChecker extends FunctionLikeChecker
/**
* @param string $function_id
* @param array<PhpParser\Node\Arg> $call_args
* @param string $file_name
* @param int $line_number
* @param CodeLocation $code_location
* @param array $suppressed_issues
* @return Type\Union
*/
public static function getReturnTypeFromCallMapWithArgs(
$function_id,
array $call_args,
$file_name,
$line_number,
CodeLocation $code_location,
array $suppressed_issues
) {
$call_map_key = strtolower($function_id);
@ -409,8 +413,7 @@ class FunctionChecker extends FunctionLikeChecker
$array_return_type = self::getArrayReturnType(
$call_map_key,
$call_args,
$file_name,
$line_number,
$code_location,
$suppressed_issues
);
@ -434,20 +437,18 @@ class FunctionChecker extends FunctionLikeChecker
/**
* @param string $call_map_key
* @param array<PhpParser\Node\Arg> $call_args
* @param string $file_name
* @param int $line_number
* @param CodeLocation $code_location
* @param array $suppressed_issues
* @return Type\Union|null
*/
protected static function getArrayReturnType(
$call_map_key,
$call_args,
$file_name,
$line_number,
CodeLocation $code_location,
array $suppressed_issues
) {
if ($call_map_key === 'array_map' || $call_map_key === 'array_filter') {
return self::getArrayMapReturnType($call_map_key, $call_args, $file_name, $line_number, $suppressed_issues);
return self::getArrayMapReturnType($call_map_key, $call_args, $code_location, $suppressed_issues);
}
$first_arg = isset($call_args[0]->value) ? $call_args[0]->value : null;
@ -552,16 +553,14 @@ class FunctionChecker extends FunctionLikeChecker
/**
* @param string $call_map_key
* @param array<PhpParser\Node\Arg> $call_args
* @param string $file_name
* @param int $line_number
* @param CodeLocation $code_location
* @param array $suppressed_issues
* @return Type\Union
*/
protected static function getArrayMapReturnType(
$call_map_key,
$call_args,
$file_name,
$line_number,
CodeLocation $code_location,
array $suppressed_issues
) {
$function_index = $call_map_key === 'array_map' ? 0 : 1;
@ -591,8 +590,7 @@ class FunctionChecker extends FunctionLikeChecker
IssueBuffer::accepts(
new InvalidReturnType(
'No return type could be found in the closure passed to ' . $call_map_key,
$file_name,
$line_number
$code_location
),
$suppressed_issues
);

View File

@ -3,6 +3,7 @@ namespace Psalm\Checker;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Stmt\ClassMethod;
use Psalm\CodeLocation;
use PhpParser\Node\Stmt\Function_;
use PhpParser;
use Psalm\Checker\Statements\ExpressionChecker;
@ -86,7 +87,9 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
$this->class_name = $source->getClassName();
$this->class_extends = $source->getParentClass();
$this->file_name = $source->getFileName();
$this->file_path = $source->getFilePath();
$this->include_file_name = $source->getIncludeFileName();
$this->include_file_path = $source->getIncludeFilePath();
$this->fq_class_name = $source->getFQCLN();
$this->source = $source;
$this->suppressed_issues = $source->getSuppressedIssues();
@ -162,8 +165,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
new MethodSignatureMismatch(
'Method ' . $cased_method_id .' has fewer arguments than parent method ' .
$parent_method_id,
$this->getCheckedFileName(),
$this->function->getLine()
new CodeLocation($this, $this->function)
)
)) {
return false;
@ -185,8 +187,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
$function_params[$i]->signature_type . '\', expecting \'' .
$implemented_param->signature_type . '\' as defined by ' .
$parent_method_id,
$this->getCheckedFileName(),
$this->function->getLine()
new CodeLocation($this, $this->function)
)
)) {
return false;
@ -206,9 +207,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
foreach ($this->function->getParams() as $param) {
$function_params[] = self::getTranslatedParam(
$param,
$this->fq_class_name,
$this->namespace,
$this->getAliasedClasses()
$this
);
}
}
@ -221,14 +220,17 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
$this->getMethodId()
);
if (!$function_param->code_location) {
throw new \UnexpectedValueException('We should know where this code is');
}
foreach ($param_type->types as $atomic_type) {
if ($atomic_type->isObjectType()
&& !$atomic_type->isObject()
&& $this->function instanceof PhpParser\Node
&& ClassLikeChecker::checkFullyQualifiedClassLikeName(
$atomic_type->value,
$this->file_name,
$this->function->getLine(),
$function_param->code_location,
$this->suppressed_issues
) === false
) {
@ -238,7 +240,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
$context->vars_in_scope['$' . $function_param->name] = $param_type;
$statements_checker->registerVariable($function_param->name, $function_param->line);
$statements_checker->registerVariable($function_param->name, $function_param->code_location->line_number);
}
$statements_checker->check($function_stmts, $context, null, $global_context);
@ -430,8 +432,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
if (IssueBuffer::accepts(
new MissingReturnType(
'Method ' . $cased_method_id . ' does not have a return type',
$this->file_name,
$this->function->getLine()
new CodeLocation($this, $this->function, true)
),
$this->suppressed_issues
)) {
@ -495,8 +496,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
new InvalidReturnType(
'No return type was found for method ' . $cased_method_id .
' but return type \'' . $declared_return_type . '\' was expected',
$this->getCheckedFileName(),
$this->function->getLine()
new CodeLocation($this, $this->function, true)
)
)) {
return false;
@ -515,8 +515,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
new MixedInferredReturnType(
'Could not verify return type \'' . $declared_return_type . '\' for ' .
$cased_method_id,
$this->getCheckedFileName(),
$this->function->getLine()
new CodeLocation($this, $this->function, true)
),
$this->getSuppressedIssues()
)) {
@ -549,8 +548,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
new InvalidReturnType(
'The given return type \'' . $declared_return_type . '\' for ' . $cased_method_id .
' is incorrect, got \'' . $inferred_return_type . '\'',
$this->getCheckedFileName(),
$this->function->getLine()
new CodeLocation($this, $this->function, true)
),
$this->getSuppressedIssues()
)) {
@ -563,30 +561,31 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
}
/**
* @param array<array{type:string,name:string}> $docblock_params
* @param array<array{type:string,name:string,line_number:int}> $docblock_params
* @param array<string,Type\Union> $function_param_names
* @param array<\Psalm\FunctionLikeParameter> &$function_signature
* @param int $method_line_number
* @param CodeLocation $code_location
* @return false|null
*/
protected function improveParamsFromDocblock(
array $docblock_params,
array $function_param_names,
array &$function_signature,
$method_line_number
CodeLocation $code_location
) {
$docblock_param_vars = [];
foreach ($docblock_params as $docblock_param) {
$param_name = $docblock_param['name'];
$line_number = $docblock_param['line_number'];
if (!array_key_exists($param_name, $function_param_names)) {
$code_location->setCommentLine($line_number);
if (IssueBuffer::accepts(
new InvalidDocblock(
'Parameter $' . $param_name .' does not appear in the argument list for ' .
$this->getMethodId(),
$this->getCheckedFileName(),
$method_line_number
$code_location
)
)) {
return false;
@ -608,12 +607,12 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
if ($function_param_names[$param_name] && !$function_param_names[$param_name]->isMixed()) {
if (!$new_param_type->isIn($function_param_names[$param_name])) {
$code_location->setCommentLine($line_number);
if (IssueBuffer::accepts(
new InvalidDocblock(
'Parameter $' . $param_name .' has wrong type \'' . $new_param_type . '\', should be \'' .
$function_param_names[$param_name] . '\'',
$this->getCheckedFileName(),
$method_line_number
$code_location
)
)) {
return false;
@ -638,14 +637,13 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
}
}
foreach ($function_param_names as $param_name => $_) {
if (!isset($docblock_param_vars[$param_name])) {
foreach ($function_signature as &$function_signature_param) {
if (!isset($docblock_param_vars[$function_signature_param->name]) && $function_signature_param->code_location) {
if (IssueBuffer::accepts(
new InvalidDocblock(
'Parameter $' . $param_name .' does not appear in the docbock for ' .
'Parameter $' . $function_signature_param->name .' does not appear in the docbock for ' .
$this->getMethodId(),
$this->getCheckedFileName(),
$method_line_number
$function_signature_param->code_location
)
)) {
return false;
@ -660,16 +658,12 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
/**
* @param PhpParser\Node\Param $param
* @param string $fq_class_name
* @param string $namespace
* @param array<string> $aliased_classes
* @param StatementsSource $source
* @return FunctionLikeParameter
*/
public static function getTranslatedParam(
PhpParser\Node\Param $param,
$fq_class_name,
$namespace,
array $aliased_classes
StatementsSource $source
) {
$param_type = null;
@ -684,12 +678,12 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
} elseif ($param->type instanceof PhpParser\Node\Name\FullyQualified) {
$param_type_string = implode('\\', $param->type->parts);
} elseif ($param->type->parts === ['self']) {
$param_type_string = $fq_class_name;
$param_type_string = $source->getFQCLN();
} else {
$param_type_string = ClassLikeChecker::getFQCLNFromString(
implode('\\', $param->type->parts),
$namespace,
$aliased_classes
$source->getNamespace(),
$source->getAliasedClasses()
);
}
@ -720,6 +714,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
$param->name,
$param->byRef,
$param_type ?: Type::getMixed(),
new CodeLocation($source, $param),
$is_optional,
$is_nullable,
$param->variadic
@ -772,6 +767,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
$param_name,
(bool)$param->isPassedByReference(),
$param_type,
null,
$is_optional,
$is_nullable
);

View File

@ -2,6 +2,7 @@
namespace Psalm\Checker;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Config;
use Psalm\Exception\DocblockParseException;
use Psalm\Issue\DeprecatedMethod;
@ -227,12 +228,11 @@ class MethodChecker extends FunctionLikeChecker
* Determines whether a given method is static or not
*
* @param string $method_id
* @param string $file_name
* @param int $line_number
* @param CodeLocation $code_location
* @param array<int, string> $suppressed_issues
* @return bool
*/
public static function checkMethodStatic($method_id, $file_name, $line_number, array $suppressed_issues)
public static function checkMethodStatic($method_id, CodeLocation $code_location, array $suppressed_issues)
{
self::registerClassMethod($method_id);
@ -243,8 +243,7 @@ class MethodChecker extends FunctionLikeChecker
if (IssueBuffer::accepts(
new InvalidStaticInvocation(
'Method ' . MethodChecker::getCasedMethodId($method_id) . ' is not static',
$file_name,
$line_number
$code_location
),
$suppressed_issues
)) {
@ -263,8 +262,7 @@ class MethodChecker extends FunctionLikeChecker
{
if (strtolower($method->name) === strtolower((string)$this->class_name)) {
$method_id = $this->fq_class_name . '::__construct';
}
else {
} else {
$method_id = $this->fq_class_name . '::' . strtolower($method->name);
}
@ -299,9 +297,7 @@ class MethodChecker extends FunctionLikeChecker
foreach ($method->getParams() as $param) {
$param_array = $this->getTranslatedParam(
$param,
$this->fq_class_name,
$this->namespace,
$this->getAliasedClasses()
$this
);
self::$method_params[$method_id][] = $param_array;
@ -331,13 +327,12 @@ class MethodChecker extends FunctionLikeChecker
$docblock_info = null;
try {
$docblock_info = CommentChecker::extractDocblockInfo((string)$doc_comment);
$docblock_info = CommentChecker::extractDocblockInfo((string)$doc_comment, $doc_comment->getLine());
} catch (DocblockParseException $e) {
if (IssueBuffer::accepts(
new InvalidDocblock(
'Invalid type passed in docblock for ' . $cased_method_id,
$this->getCheckedFileName(),
$method->getLine()
new CodeLocation($this, $method)
)
)) {
return false;
@ -373,7 +368,7 @@ class MethodChecker extends FunctionLikeChecker
$docblock_info->params,
$method_param_names,
self::$method_params[$method_id],
$method->getLine()
new CodeLocation($this, $method, true)
);
}
}
@ -433,20 +428,19 @@ class MethodChecker extends FunctionLikeChecker
}
/**
* @param string $method_id
* @param string $file_name
* @param int $line_number
* @param array $suppressed_issues
* @param string $method_id
* @param CodeLocation $code_location
* @param array $suppressed_issues
* @return bool|null
*/
public static function checkMethodExists($method_id, $file_name, $line_number, array $suppressed_issues)
public static function checkMethodExists($method_id, CodeLocation $code_location, array $suppressed_issues)
{
if (self::methodExists($method_id)) {
return true;
}
if (IssueBuffer::accepts(
new UndefinedMethod('Method ' . $method_id . ' does not exist', $file_name, $line_number),
new UndefinedMethod('Method ' . $method_id . ' does not exist', $code_location),
$suppressed_issues
)) {
return false;
@ -501,13 +495,12 @@ class MethodChecker extends FunctionLikeChecker
}
/**
* @param string $method_id
* @param string $file_name
* @param int $line_number
* @param array $suppressed_issues
* @param string $method_id
* @param CodeLocation $code_location
* @param array $suppressed_issues
* @return false|null
*/
public static function checkMethodNotDeprecated($method_id, $file_name, $line_number, array $suppressed_issues)
public static function checkMethodNotDeprecated($method_id, CodeLocation $code_location, array $suppressed_issues)
{
self::registerClassMethod($method_id);
@ -515,8 +508,7 @@ class MethodChecker extends FunctionLikeChecker
if (IssueBuffer::accepts(
new DeprecatedMethod(
'The method ' . MethodChecker::getCasedMethodId($method_id) . ' has been marked as deprecated',
$file_name,
$line_number
$code_location
),
$suppressed_issues
)) {
@ -531,7 +523,7 @@ class MethodChecker extends FunctionLikeChecker
* @param string $method_id
* @param string|null $calling_context
* @param StatementsSource $source
* @param int $line_number
* @param CodeLocation $code_location
* @param array $suppressed_issues
* @return false|null
*/
@ -539,7 +531,7 @@ class MethodChecker extends FunctionLikeChecker
$method_id,
$calling_context,
StatementsSource $source,
$line_number,
CodeLocation $code_location,
array $suppressed_issues
) {
self::registerClassMethod($method_id);
@ -551,7 +543,7 @@ class MethodChecker extends FunctionLikeChecker
if (!isset(self::$method_visibility[$declared_method_id])) {
if (IssueBuffer::accepts(
new InaccessibleMethod('Cannot access method ' . $method_id, $source->getFileName(), $line_number),
new InaccessibleMethod('Cannot access method ' . $method_id, $code_location),
$suppressed_issues
)) {
return false;
@ -572,8 +564,7 @@ class MethodChecker extends FunctionLikeChecker
new InaccessibleMethod(
'Cannot access private method ' . MethodChecker::getCasedMethodId($method_id) .
' from context ' . $calling_context,
$source->getFileName(),
$line_number
$code_location
),
$suppressed_issues
)) {
@ -592,8 +583,7 @@ class MethodChecker extends FunctionLikeChecker
if (IssueBuffer::accepts(
new InaccessibleMethod(
'Cannot access protected method ' . $method_id,
$source->getFileName(),
$line_number
$code_location
),
$suppressed_issues
)) {
@ -614,8 +604,7 @@ class MethodChecker extends FunctionLikeChecker
new InaccessibleMethod(
'Cannot access protected method ' . MethodChecker::getCasedMethodId($method_id) .
' from context ' . $calling_context,
$source->getFileName(),
$line_number
$code_location
),
$suppressed_issues
)) {

View File

@ -35,7 +35,9 @@ class NamespaceChecker extends SourceChecker implements StatementsSource
$this->namespace = $namespace;
$this->namespace_name = $this->namespace->name ? implode('\\', $this->namespace->name->parts) : '';
$this->file_name = $source->getFileName();
$this->file_path = $source->getFilePath();
$this->include_file_name = $source->getIncludeFileName();
$this->include_file_path = $source->getIncludeFilePath();
$this->suppressed_issues = $source->getSuppressedIssues();
}

View File

@ -34,11 +34,21 @@ abstract class SourceChecker implements StatementsSource
*/
protected $file_name;
/**
* @var string
*/
protected $file_path;
/**
* @var string|null
*/
protected $include_file_name;
/**
* @var string|null
*/
protected $include_file_path;
/**
* @var array
*/
@ -198,6 +208,14 @@ abstract class SourceChecker implements StatementsSource
return $this->file_name;
}
/**
* @return string
*/
public function getFilePath()
{
return $this->file_path;
}
/**
* @return null|string
*/
@ -206,13 +224,23 @@ abstract class SourceChecker implements StatementsSource
return $this->include_file_name;
}
/**
* @return null|string
*/
public function getIncludeFilePath()
{
return $this->include_file_path;
}
/**
* @param string|null $file_name
* @param string|null $file_path
* @return void
*/
public function setIncludeFileName($file_name)
public function setIncludeFileName($file_name, $file_path)
{
$this->include_file_name = $file_name;
$this->include_file_path = $file_path;
}
/**
@ -223,6 +251,14 @@ abstract class SourceChecker implements StatementsSource
return $this->include_file_name ?: $this->file_name;
}
/**
* @return string
*/
public function getCheckedFilePath()
{
return $this->include_file_path ?: $this->file_path;
}
/**
* @return bool
*/

View File

@ -2,6 +2,7 @@
namespace Psalm\Checker\Statements\Block;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\IssueBuffer;
use Psalm\Checker\ClassChecker;
@ -101,8 +102,7 @@ class ForeachChecker
if (IssueBuffer::accepts(
new NullReference(
'Cannot iterate over ' . $return_type->value,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -120,8 +120,7 @@ class ForeachChecker
if (IssueBuffer::accepts(
new InvalidIterator(
'Cannot iterate over ' . $return_type->value,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -159,8 +158,7 @@ class ForeachChecker
) {
if (ClassLikeChecker::checkFullyQualifiedClassLikeName(
$return_type->value,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
) === false) {
return false;

View File

@ -6,6 +6,7 @@ use Psalm\Checker\ScopeChecker;
use Psalm\Checker\Statements\ExpressionChecker;
use Psalm\Checker\StatementsChecker;
use Psalm\Checker\TypeChecker;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\IfScope;
use Psalm\Type;
@ -111,8 +112,7 @@ class IfChecker
TypeChecker::reconcileKeyedTypes(
$reconcilable_if_types,
$if_context->vars_in_scope,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
);
@ -139,8 +139,7 @@ class IfChecker
$else_vars_reconciled = TypeChecker::reconcileKeyedTypes(
$if_scope->negated_types,
$temp_else_context->vars_in_scope,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
);
@ -284,8 +283,7 @@ class IfChecker
$outer_context_vars_reconciled = TypeChecker::reconcileKeyedTypes(
$if_scope->negated_types,
$outer_context->vars_in_scope,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
);
@ -353,8 +351,7 @@ class IfChecker
$elseif_vars_reconciled = TypeChecker::reconcileKeyedTypes(
$if_scope->negated_types,
$elseif_context->vars_in_scope,
$statements_checker->getCheckedFileName(),
$elseif->getLine(),
new CodeLocation($statements_checker->getSource(), $elseif),
$statements_checker->getSuppressedIssues()
);
@ -414,8 +411,7 @@ class IfChecker
$elseif_vars_reconciled = TypeChecker::reconcileKeyedTypes(
$reconcilable_elseif_types,
$elseif_context->vars_in_scope,
$statements_checker->getCheckedFileName(),
$elseif->getLine(),
new CodeLocation($statements_checker->getSource(), $elseif),
$statements_checker->getSuppressedIssues()
);
@ -569,8 +565,7 @@ class IfChecker
$else_vars_reconciled = TypeChecker::reconcileKeyedTypes(
$if_scope->negated_types,
$else_context->vars_in_scope,
$statements_checker->getCheckedFileName(),
$else->getLine(),
new CodeLocation($statements_checker->getSource(), $else),
$statements_checker->getSuppressedIssues()
);

View File

@ -5,6 +5,7 @@ use PhpParser;
use Psalm\Checker\ScopeChecker;
use Psalm\Checker\StatementsChecker;
use Psalm\Checker\Statements\ExpressionChecker;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Issue\InvalidContinue;
use Psalm\IssueBuffer;
@ -133,8 +134,7 @@ class SwitchChecker
if (IssueBuffer::accepts(
new InvalidContinue(
'Continue called when not in loop',
$statements_checker->getCheckedFileName(),
$case->getLine()
new CodeLocation($statements_checker->getSource(), $case)
)
)) {
return false;

View File

@ -3,6 +3,7 @@ namespace Psalm\Checker\Statements\Block;
use PhpParser;
use Psalm\Checker\ClassLikeChecker;
use Psalm\CodeLocation;
use Psalm\Checker\ScopeChecker;
use Psalm\Checker\StatementsChecker;
use Psalm\Context;
@ -43,8 +44,7 @@ class TryChecker
if (ClassLikeChecker::checkFullyQualifiedClassLikeName(
$fq_class_name,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
) === false) {
return false;

View File

@ -2,6 +2,7 @@
namespace Psalm\Checker\Statements\Block;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Checker\Statements\ExpressionChecker;
use Psalm\Checker\StatementsChecker;
@ -43,8 +44,7 @@ class WhileChecker
$while_vars_in_scope_reconciled = TypeChecker::reconcileKeyedTypes(
$while_types,
$while_context->vars_in_scope,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
);

View File

@ -9,6 +9,7 @@ use Psalm\Checker\InterfaceChecker;
use Psalm\Checker\MethodChecker;
use Psalm\Checker\StatementsChecker;
use Psalm\Checker\Statements\ExpressionChecker;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Issue\FailedTypeResolution;
use Psalm\Issue\InvalidArrayAssignment;
@ -158,8 +159,7 @@ class AssignmentChecker
if (IssueBuffer::accepts(
new FailedTypeResolution(
'Cannot assign ' . $var_id . ' to type void',
$statements_checker->getCheckedFileName(),
$assign_var->getLine()
new CodeLocation($statements_checker->getSource(), $assign_var)
),
$statements_checker->getSuppressedIssues()
)) {
@ -200,7 +200,7 @@ class AssignmentChecker
$expr_type = isset($stmt->expr->inferredType) ? $stmt->expr->inferredType : null;
if ($stmt instanceof PhpParser\Node\Expr\AssignOp\Plus) {
ExpressionChecker::checkPlusOp($statements_checker, $stmt->getLine(), $var_type, $expr_type, $result_type);
ExpressionChecker::checkPlusOp($var_type, $expr_type, $result_type);
if ($result_type && $var_id) {
$context->vars_in_scope[$var_id] = $result_type;
@ -259,8 +259,7 @@ class AssignmentChecker
if (IssueBuffer::accepts(
new InvalidScope(
'Cannot use $this when not inside class',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -279,8 +278,7 @@ class AssignmentChecker
if (IssueBuffer::accepts(
new MixedPropertyAssignment(
$var_id . ' with mixed type cannot be assigned to',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -294,8 +292,7 @@ class AssignmentChecker
if (IssueBuffer::accepts(
new NullPropertyAssignment(
$var_id . ' with null type cannot be assigned to',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -309,8 +306,7 @@ class AssignmentChecker
if (IssueBuffer::accepts(
new NullPropertyAssignment(
$var_id . ' with possibly null type \'' . $lhs_type . '\' cannot be assigned to',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -336,8 +332,7 @@ class AssignmentChecker
if (IssueBuffer::accepts(
new InvalidPropertyAssignment(
$var_id . ' with possible non-object type \'' . $lhs_type_part . '\' cannot be assigned to',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -382,8 +377,7 @@ class AssignmentChecker
if (IssueBuffer::accepts(
new NoInterfaceProperties(
'Interfaces cannot have properties',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -396,8 +390,7 @@ class AssignmentChecker
if (IssueBuffer::accepts(
new UndefinedClass(
'Cannot set properties of undefined class ' . $lhs_type_part->value,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -417,8 +410,7 @@ class AssignmentChecker
if (IssueBuffer::accepts(
new UndefinedThisPropertyAssignment(
'Instance property ' . $lhs_type_part->value . '::$' . $prop_name . ' is not defined',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -428,8 +420,7 @@ class AssignmentChecker
if (IssueBuffer::accepts(
new UndefinedPropertyAssignment(
'Instance property ' . $lhs_type_part->value . '::$' . $prop_name . ' is not defined',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -447,8 +438,7 @@ class AssignmentChecker
new MissingPropertyType(
'Property ' . $lhs_type_part->value . '::$' . $stmt->name . ' does not have a declared ' .
'type',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -482,8 +472,7 @@ class AssignmentChecker
if (IssueBuffer::accepts(
new MissingPropertyDeclaration(
'Missing property declaration for ' . $var_id,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -507,8 +496,7 @@ class AssignmentChecker
new InvalidPropertyAssignment(
$var_id . ' with declared type \'' . $class_property_type . '\' cannot be assigned type \'' .
$assignment_type . '\'',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -580,8 +568,7 @@ class AssignmentChecker
if (IssueBuffer::accepts(
new InvisibleProperty(
'Static property ' . $var_id . ' is not visible in this context',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -592,8 +579,7 @@ class AssignmentChecker
if (IssueBuffer::accepts(
new UndefinedThisPropertyAssignment(
'Static property ' . $var_id . ' is not defined',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -603,8 +589,7 @@ class AssignmentChecker
if (IssueBuffer::accepts(
new UndefinedPropertyAssignment(
'Static property ' . $var_id . ' is not defined',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -624,8 +609,7 @@ class AssignmentChecker
if (IssueBuffer::accepts(
new MissingPropertyType(
'Property ' . $fq_class_name . '::$' . $prop_name . ' does not have a declared type',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -652,8 +636,7 @@ class AssignmentChecker
new InvalidPropertyAssignment(
$var_id . ' with declared type \'' . $class_property_type . '\' cannot be assigned type \'' .
$assignment_type . '\'',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -754,8 +737,7 @@ class AssignmentChecker
if (IssueBuffer::accepts(
new MixedStringOffsetAssignment(
'Cannot assign a mixed variable to a string offset for ' . $var_id,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -768,8 +750,7 @@ class AssignmentChecker
if (IssueBuffer::accepts(
new InvalidArrayAssignment(
'Cannot assign string offset for ' . $var_id . ' of type ' . $value_type,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {

View File

@ -10,6 +10,7 @@ use Psalm\Checker\MethodChecker;
use Psalm\Checker\StatementsChecker;
use Psalm\Checker\Statements\ExpressionChecker;
use Psalm\Checker\TraitChecker;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Issue\ForbiddenCode;
use Psalm\Issue\InvalidArgument;
@ -65,8 +66,7 @@ class CallChecker
if (IssueBuffer::accepts(
new ForbiddenCode(
'Unsafe ' . implode('', $method->parts),
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -109,8 +109,10 @@ class CallChecker
$in_call_map = FunctionChecker::inCallMap($method_id);
$code_location = new CodeLocation($statements_checker->getSource(), $stmt);
if (!$in_call_map &&
self::checkFunctionExists($statements_checker, $method_id, $context, $stmt->getLine()) === false
self::checkFunctionExists($statements_checker, $method_id, $context, $code_location) === false
) {
return false;
}
@ -120,7 +122,7 @@ class CallChecker
$stmt->args,
$method_id,
$context,
$stmt->getLine()
$code_location
) === false) {
return false;
}
@ -129,8 +131,7 @@ class CallChecker
$stmt->inferredType = FunctionChecker::getReturnTypeFromCallMapWithArgs(
$method_id,
$stmt->args,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
$code_location,
$statements_checker->getSuppressedIssues()
);
} else {
@ -185,8 +186,7 @@ class CallChecker
if (ClassLikeChecker::checkFullyQualifiedClassLikeName(
$fq_class_name,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt->class),
$statements_checker->getSuppressedIssues()
) === false) {
return false;
@ -226,7 +226,7 @@ class CallChecker
$stmt->args,
$method_id,
$context,
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
) === false) {
return false;
}
@ -305,8 +305,7 @@ class CallChecker
if (IssueBuffer::accepts(
new InvalidScope(
'Use of $this in non-class context',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -377,8 +376,7 @@ class CallChecker
if (IssueBuffer::accepts(
new NullReference(
'Cannot call method ' . $stmt->name . ' on possibly null variable ' . $var_id,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -394,8 +392,7 @@ class CallChecker
if (IssueBuffer::accepts(
new InvalidArgument(
'Cannot call method ' . $stmt->name . ' on ' . $class_type . ' variable ' . $var_id,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -408,8 +405,7 @@ class CallChecker
if (IssueBuffer::accepts(
new MixedMethodCall(
'Cannot call method ' . $stmt->name . ' on a mixed variable ' . $var_id,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -432,8 +428,7 @@ class CallChecker
$does_class_exist = ClassLikeChecker::checkFullyQualifiedClassLikeName(
$fq_class_name,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
);
@ -446,8 +441,7 @@ class CallChecker
$does_method_exist = MethodChecker::checkMethodExists(
$cased_method_id,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
);
@ -462,7 +456,7 @@ class CallChecker
$method_id,
$context->self,
$statements_checker->getSource(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
) === false) {
return false;
@ -470,8 +464,7 @@ class CallChecker
if (MethodChecker::checkMethodNotDeprecated(
$method_id,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
) === false) {
return false;
@ -507,7 +500,7 @@ class CallChecker
$stmt->args,
$method_id,
$context,
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$has_mock
) === false) {
return false;
@ -551,8 +544,7 @@ class CallChecker
if (IssueBuffer::accepts(
new ParentNotFound(
'Cannot call method on parent as this class does not extend another',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -585,8 +577,7 @@ class CallChecker
$does_class_exist = ClassLikeChecker::checkFullyQualifiedClassLikeName(
$fq_class_name,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
);
@ -637,8 +628,7 @@ class CallChecker
$does_method_exist = MethodChecker::checkMethodExists(
$cased_method_id,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
);
@ -650,7 +640,7 @@ class CallChecker
$method_id,
$context->self,
$statements_checker->getSource(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
) === false) {
return false;
@ -663,8 +653,7 @@ class CallChecker
) {
if (MethodChecker::checkMethodStatic(
$method_id,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
) === false) {
return false;
@ -673,8 +662,7 @@ class CallChecker
if (MethodChecker::checkMethodNotDeprecated(
$method_id,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
) === false) {
return false;
@ -705,7 +693,7 @@ class CallChecker
$stmt->args,
$method_id,
$context,
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$has_mock
) === false) {
return false;
@ -720,7 +708,7 @@ class CallChecker
* @param array<int, PhpParser\Node\Arg> $args
* @param string|null $method_id
* @param Context $context
* @param int $line_number
* @param CodeLocation $code_location
* @param boolean $is_mock
* @return false|null
*/
@ -729,7 +717,7 @@ class CallChecker
array $args,
$method_id,
Context $context,
$line_number,
CodeLocation $code_location,
$is_mock = false
) {
$function_params = null;
@ -879,7 +867,7 @@ class CallChecker
),
$cased_method_id,
$argument_offset,
$arg->value->getLine()
new CodeLocation($statements_checker->getSource(), $arg->value)
) === false) {
return false;
}
@ -924,8 +912,7 @@ class CallChecker
if (IssueBuffer::accepts(
new TooManyArguments(
'Too many arguments in closure for ' . ($cased_method_id ?: $method_id),
$statements_checker->getCheckedFileName(),
$closure_arg->getLine()
new CodeLocation($statements_checker->getSource(), $closure_arg)
),
$statements_checker->getSuppressedIssues()
)) {
@ -935,8 +922,7 @@ class CallChecker
if (IssueBuffer::accepts(
new TooFewArguments(
'You must supply a param in the closure for ' . ($cased_method_id ?: $method_id),
$statements_checker->getCheckedFileName(),
$closure_arg->getLine()
new CodeLocation($statements_checker->getSource(), $closure_arg)
),
$statements_checker->getSuppressedIssues()
)) {
@ -954,9 +940,7 @@ class CallChecker
$translated_param = FunctionLikeChecker::getTranslatedParam(
$closure_param,
$statements_checker->getFQCLN(),
$statements_checker->getNamespace(),
$statements_checker->getAliasedClasses()
$statements_checker->getSource()
);
$param_type = $translated_param->type;
@ -978,8 +962,7 @@ class CallChecker
new TypeCoercion(
'First parameter of closure passed to function ' . $cased_method_id . ' expects ' .
$param_type . ', parent type ' . $input_type . ' provided',
$statements_checker->getCheckedFileName(),
$closure_param->getLine()
new CodeLocation($statements_checker->getSource(), $closure_param)
),
$statements_checker->getSuppressedIssues()
)) {
@ -993,8 +976,7 @@ class CallChecker
new InvalidScalarArgument(
'First parameter of closure passed to function ' . $cased_method_id . ' expects ' .
$param_type . ', ' . $input_type . ' provided',
$statements_checker->getCheckedFileName(),
$closure_param->getLine()
new CodeLocation($statements_checker->getSource(), $closure_param)
),
$statements_checker->getSuppressedIssues()
)) {
@ -1004,8 +986,7 @@ class CallChecker
new InvalidArgument(
'First parameter of closure passed to function ' . $cased_method_id . ' expects ' .
$param_type . ', ' . $input_type . ' provided',
$statements_checker->getCheckedFileName(),
$closure_param->getLine()
new CodeLocation($statements_checker->getSource(), $closure_param)
),
$statements_checker->getSuppressedIssues()
)) {
@ -1024,8 +1005,7 @@ class CallChecker
if (IssueBuffer::accepts(
new TooManyArguments(
'Too many arguments for method ' . ($cased_method_id ?: $method_id),
$statements_checker->getCheckedFileName(),
$line_number
$code_location
),
$statements_checker->getSuppressedIssues()
)) {
@ -1043,8 +1023,7 @@ class CallChecker
if (IssueBuffer::accepts(
new TooFewArguments(
'Too few arguments for method ' . $cased_method_id,
$statements_checker->getCheckedFileName(),
$line_number
$code_location
),
$statements_checker->getSuppressedIssues()
)) {
@ -1066,7 +1045,7 @@ class CallChecker
* @param Type\Union $param_type
* @param string $cased_method_id
* @param int $argument_offset
* @param int $line_number
* @param CodeLocation $code_location
* @return null|false
*/
protected static function checkFunctionArgumentType(
@ -1075,7 +1054,7 @@ class CallChecker
Type\Union $param_type,
$cased_method_id,
$argument_offset,
$line_number
CodeLocation $code_location
) {
if ($param_type->isMixed()) {
return null;
@ -1086,8 +1065,7 @@ class CallChecker
new MixedArgument(
'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id . ' cannot be mixed, expecting ' .
$param_type,
$statements_checker->getCheckedFileName(),
$line_number
$code_location
),
$statements_checker->getSuppressedIssues()
)) {
@ -1102,8 +1080,7 @@ class CallChecker
new NullReference(
'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id . ' cannot be null, possibly ' .
'null value provided',
$statements_checker->getCheckedFileName(),
$line_number
$code_location
),
$statements_checker->getSuppressedIssues()
)) {
@ -1123,8 +1100,7 @@ class CallChecker
new TypeCoercion(
'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id . ' expects ' . $param_type .
', parent type ' . $input_type . ' provided',
$statements_checker->getCheckedFileName(),
$line_number
$code_location
),
$statements_checker->getSuppressedIssues()
)) {
@ -1138,8 +1114,7 @@ class CallChecker
new InvalidScalarArgument(
'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id . ' expects ' . $param_type .
', ' . $input_type . ' provided',
$statements_checker->getCheckedFileName(),
$line_number
$code_location
),
$statements_checker->getSuppressedIssues()
)) {
@ -1149,8 +1124,7 @@ class CallChecker
new InvalidArgument(
'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id . ' expects ' . $param_type .
', ' . $input_type . ' provided',
$statements_checker->getCheckedFileName(),
$line_number
$code_location
),
$statements_checker->getSuppressedIssues()
)) {
@ -1165,14 +1139,14 @@ class CallChecker
* @param StatementsChecker $statements_checker
* @param string $function_id
* @param Context $context
* @param int $line_number
* @param CodeLocation $code_location
* @return bool
*/
protected static function checkFunctionExists(
StatementsChecker $statements_checker,
$function_id,
Context $context,
$line_number
CodeLocation $code_location
) {
$cased_function_id = $function_id;
$function_id = strtolower($function_id);
@ -1181,8 +1155,7 @@ class CallChecker
if (IssueBuffer::accepts(
new UndefinedFunction(
'Function ' . $cased_function_id . ' does not exist',
$statements_checker->getCheckedFileName(),
$line_number
$code_location
),
$statements_checker->getSuppressedIssues()
)) {

View File

@ -9,6 +9,7 @@ use Psalm\Checker\MethodChecker;
use Psalm\Checker\StatementsChecker;
use Psalm\Checker\Statements\ExpressionChecker;
use Psalm\Checker\TraitChecker;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Issue\InvalidArrayAccess;
use Psalm\Issue\InvalidArrayAssignment;
@ -96,8 +97,7 @@ class FetchChecker
if (IssueBuffer::accepts(
new NullPropertyFetch(
'Cannot get property on null variable ' . $stmt_var_id,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -111,8 +111,7 @@ class FetchChecker
if (IssueBuffer::accepts(
new MixedPropertyFetch(
'Cannot fetch property on empty var ' . $stmt_var_id,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -126,8 +125,7 @@ class FetchChecker
if (IssueBuffer::accepts(
new MixedPropertyFetch(
'Cannot fetch property on mixed var ' . $stmt_var_id,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -141,8 +139,7 @@ class FetchChecker
if (IssueBuffer::accepts(
new NullPropertyFetch(
'Cannot get property on possibly null variable ' . $stmt_var_id,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -172,8 +169,7 @@ class FetchChecker
if (IssueBuffer::accepts(
new InvalidPropertyFetch(
'Cannot fetch property on non-object ' . $stmt_var_id . ' of type ' . $lhs_type_part,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -203,8 +199,7 @@ class FetchChecker
if (IssueBuffer::accepts(
new NoInterfaceProperties(
'Interfaces cannot have properties',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -217,8 +212,7 @@ class FetchChecker
if (IssueBuffer::accepts(
new UndefinedClass(
'Cannot get properties of undefined class ' . $lhs_type_part->value,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -258,8 +252,7 @@ class FetchChecker
if (IssueBuffer::accepts(
new UndefinedThisPropertyFetch(
'Instance property ' . $lhs_type_part->value .'::$' . $stmt->name . ' is not defined',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -269,8 +262,7 @@ class FetchChecker
if (IssueBuffer::accepts(
new UndefinedPropertyFetch(
'Instance property ' . $lhs_type_part->value .'::$' . $stmt->name . ' is not defined',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -287,8 +279,7 @@ class FetchChecker
if (IssueBuffer::accepts(
new MissingPropertyType(
'Property ' . $lhs_type_part->value . '::$' . $stmt->name . ' does not have a declared type',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -350,8 +341,7 @@ class FetchChecker
if (IssueBuffer::accepts(
new UndefinedConstant(
'Const ' . $const_name . ' is not defined',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -389,8 +379,7 @@ class FetchChecker
if (ClassLikeChecker::checkFullyQualifiedClassLikeName(
$fq_class_name,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
) === false) {
return false;
@ -410,8 +399,7 @@ class FetchChecker
if (IssueBuffer::accepts(
new UndefinedConstant(
'Const ' . $const_id . ' is not defined',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -463,8 +451,7 @@ class FetchChecker
if (IssueBuffer::accepts(
new ParentNotFound(
'Cannot check property fetch on parent as this class does not extend another',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -495,8 +482,7 @@ class FetchChecker
if (ClassLikeChecker::checkFullyQualifiedClassLikeName(
$fq_class_name,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
) === false) {
return false;
@ -556,16 +542,14 @@ class FetchChecker
IssueBuffer::add(
new InvisibleProperty(
'Static property ' . $var_id . ' is not visible in this context',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
)
);
} else {
IssueBuffer::add(
new UndefinedPropertyFetch(
'Static property ' . $var_id . ' does not exist',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
)
);
}
@ -683,8 +667,7 @@ class FetchChecker
if (IssueBuffer::accepts(
new InvalidArrayAssignment(
'Cannot assign value on variable ' . $var_id . ' of scalar type ' . $type->value,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -701,7 +684,7 @@ class FetchChecker
$assignment_key_type,
$assignment_value_type,
$var_id,
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
);
if ($refined_type === false) {
@ -888,8 +871,7 @@ class FetchChecker
new InvalidArrayAccess(
'Cannot access value on array variable ' . $var_id . ' using int offset - ' .
'expecting ' . $expected_keys_string,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -909,8 +891,7 @@ class FetchChecker
if (IssueBuffer::accepts(
new NullArrayAccess(
'Cannot access array value on possibly null variable ' . $array_var_id . ' of type ' . $var_type,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -926,8 +907,7 @@ class FetchChecker
if (IssueBuffer::accepts(
new MixedArrayAccess(
'Cannot access array value on mixed variable ' . $array_var_id,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -940,8 +920,7 @@ class FetchChecker
if (IssueBuffer::accepts(
new InvalidArrayAccess(
'Cannot access array value on non-array variable ' . $array_var_id . ' of type ' . $var_type,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -975,8 +954,7 @@ class FetchChecker
new MixedArrayOffset(
'Cannot access value on variable ' . $var_id . ' using mixed offset - expecting ' .
$key_type,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -987,8 +965,7 @@ class FetchChecker
new InvalidArrayAccess(
'Cannot access value on variable ' . $var_id . ' using ' . $at . ' offset - ' .
'expecting ' . $key_type,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -1008,7 +985,7 @@ class FetchChecker
* @param Type\Union $assignment_key_type
* @param Type\Union $assignment_value_type
* @param string|null $var_id
* @param int $line_number
* @param CodeLocation $code_location
* @return Type\Atomic|null|false
*/
protected static function refineArrayType(
@ -1017,14 +994,13 @@ class FetchChecker
Type\Union $assignment_key_type,
Type\Union $assignment_value_type,
$var_id,
$line_number
CodeLocation $code_location
) {
if ($type->value === 'null') {
if (IssueBuffer::accepts(
new NullReference(
'Cannot assign value on possibly null array' . ($var_id ? ' ' . $var_id : ''),
$statements_checker->getCheckedFileName(),
$line_number
$code_location
),
$statements_checker->getSuppressedIssues()
)) {
@ -1043,8 +1019,7 @@ class FetchChecker
new InvalidArrayAssignment(
'Cannot assign value on variable' . ($var_id ? ' ' . $var_id : '') . ' of type ' . $type->value . ' that does not ' .
'implement ArrayAccess',
$statements_checker->getCheckedFileName(),
$line_number
$code_location
),
$statements_checker->getSuppressedIssues()
)) {

View File

@ -12,6 +12,7 @@ use Psalm\Checker\Statements\Expression\CallChecker;
use Psalm\Checker\Statements\Expression\FetchChecker;
use Psalm\Checker\StatementsChecker;
use Psalm\Checker\TypeChecker;
use Psalm\CodeLocation;
use Psalm\Config;
use Psalm\Context;
use Psalm\Issue\ForbiddenCode;
@ -309,8 +310,7 @@ class ExpressionChecker
if (ClassLikeChecker::checkFullyQualifiedClassLikeName(
$fq_class_name,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt->class),
$statements_checker->getSuppressedIssues()
) === false) {
return false;
@ -354,7 +354,10 @@ class ExpressionChecker
// do nothing
} elseif ($stmt instanceof PhpParser\Node\Expr\ShellExec) {
if (IssueBuffer::accepts(
new ForbiddenCode('Use of shell_exec', $statements_checker->getCheckedFileName(), $stmt->getLine()),
new ForbiddenCode(
'Use of shell_exec',
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
return false;
@ -371,8 +374,7 @@ class ExpressionChecker
if (IssueBuffer::accepts(
new UnrecognizedExpression(
'Psalm does not understand ' . get_class($stmt),
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -380,14 +382,20 @@ class ExpressionChecker
}
}
foreach (Config::getInstance()->getPlugins() as $plugin) {
if ($plugin->checkExpression(
$stmt,
$context,
$statements_checker->getCheckedFileName(),
$statements_checker->getSuppressedIssues()
) === false) {
return false;
$plugins = Config::getInstance()->getPlugins();
if ($plugins) {
$code_location = new CodeLocation($statements_checker->getSource(), $stmt);
foreach ($plugins as $plugin) {
if ($plugin->checkExpression(
$stmt,
$context,
$code_location,
$statements_checker->getSuppressedIssues()
) === false) {
return false;
}
}
}
@ -416,8 +424,7 @@ class ExpressionChecker
if (IssueBuffer::accepts(
new InvalidStaticVariable(
'Invalid reference to $this in a static context',
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -475,8 +482,7 @@ class ExpressionChecker
IssueBuffer::add(
new UndefinedVariable(
'Cannot find referenced variable ' . $var_name,
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
)
);
@ -489,8 +495,7 @@ class ExpressionChecker
new PossiblyUndefinedVariable(
'Possibly undefined variable ' . $var_name .', first seen on line ' .
$statements_checker->getFirstAppearance($var_name),
$statements_checker->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($statements_checker->getSource(), $stmt)
),
$statements_checker->getSuppressedIssues()
)) {
@ -647,8 +652,7 @@ class ExpressionChecker
$op_vars_in_scope = TypeChecker::reconcileKeyedTypes(
$left_type_assertions,
$context->vars_in_scope,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
);
@ -695,8 +699,7 @@ class ExpressionChecker
$op_vars_in_scope = TypeChecker::reconcileKeyedTypes(
$negated_type_assertions,
$context->vars_in_scope,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
);
@ -745,10 +748,8 @@ class ExpressionChecker
// let's do some fun type assignment
if (isset($stmt->left->inferredType) && isset($stmt->right->inferredType)) {
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Plus) {
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Plus || $stmt instanceof PhpParser\Node\Expr\BinaryOp\Minus) {
self::checkPlusOp(
$statements_checker,
$stmt->getLine(),
$stmt->left->inferredType,
$stmt->right->inferredType,
$result_type
@ -758,9 +759,7 @@ class ExpressionChecker
$stmt->inferredType = $result_type;
}
} elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Mul
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Minus
) {
} elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Mul) {
if ($stmt->left->inferredType->isInt() && $stmt->right->inferredType->isInt()) {
$stmt->inferredType = Type::getInt();
} elseif ($stmt->left->inferredType->hasNumericType() &&
@ -798,16 +797,12 @@ class ExpressionChecker
}
/**
* @param StatementsChecker $statements_checker
* @param int $line_number
* @param Type\Union|null $left_type
* @param Type\Union|null $right_type
* @param Type\Union|null &$result_type
* @return void
*/
public static function checkPlusOp(
StatementsChecker $statements_checker,
$line_number,
Type\Union $left_type = null,
Type\Union $right_type = null,
Type\Union &$result_type = null
@ -1021,8 +1016,7 @@ class ExpressionChecker
IssueBuffer::add(
new UndefinedVariable(
'Cannot find referenced variable $' . $use->var,
$statements_checker->getCheckedFileName(),
$use->getLine()
new CodeLocation($statements_checker->getSource(), $use)
)
);
@ -1035,8 +1029,7 @@ class ExpressionChecker
new PossiblyUndefinedVariable(
'Possibly undefined variable $' . $use->var . ', first seen on line ' .
$statements_checker->getFirstAppearance('$' . $use->var),
$statements_checker->getCheckedFileName(),
$use->getLine()
new CodeLocation($statements_checker->getSource(), $use)
),
$statements_checker->getSuppressedIssues()
)) {
@ -1050,8 +1043,7 @@ class ExpressionChecker
IssueBuffer::add(
new UndefinedVariable(
'Cannot find referenced variable $' . $use->var,
$statements_checker->getCheckedFileName(),
$use->getLine()
new CodeLocation($statements_checker->getSource(), $use)
)
);
@ -1172,8 +1164,7 @@ class ExpressionChecker
$t_if_vars_in_scope_reconciled = TypeChecker::reconcileKeyedTypes(
$reconcilable_if_types,
$t_if_context->vars_in_scope,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
);
@ -1197,8 +1188,7 @@ class ExpressionChecker
$t_else_vars_in_scope_reconciled = TypeChecker::reconcileKeyedTypes(
$negated_if_types,
$t_else_context->vars_in_scope,
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
);
@ -1225,8 +1215,7 @@ class ExpressionChecker
'!empty',
$stmt->cond->inferredType,
'',
$statements_checker->getCheckedFileName(),
$stmt->getLine(),
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
);

View File

@ -10,6 +10,7 @@ use Psalm\Checker\Statements\Block\TryChecker;
use Psalm\Checker\Statements\Block\WhileChecker;
use Psalm\Checker\Statements\ExpressionChecker;
use Psalm\Checker\Statements\Expression\AssignmentChecker;
use Psalm\CodeLocation;
use Psalm\Config;
use Psalm\Context;
use Psalm\Issue\ContinueOutsideLoop;
@ -72,16 +73,31 @@ class StatementsChecker
*/
protected $file_name;
/**
* @var string
*/
protected $file_path;
/**
* @var string
*/
protected $checked_file_name;
/**
* @var string
*/
protected $checked_file_path;
/**
* @var string|null
*/
protected $include_file_name;
/**
* @var string|null
*/
protected $include_file_path;
/**
* @var bool
*/
@ -116,7 +132,9 @@ class StatementsChecker
{
$this->source = $source;
$this->file_name = $this->source->getFileName();
$this->file_path = $this->source->getFilePath();
$this->checked_file_name = $this->source->getCheckedFileName();
$this->checked_file_path = $this->source->getCheckedFilePath();
$this->namespace = $this->source->getNamespace();
$this->is_static = $this->source->isStatic();
$this->fq_class_name = $this->source->getFQCLN();
@ -157,16 +175,23 @@ class StatementsChecker
}
foreach ($stmts as $stmt) {
foreach (Config::getInstance()->getPlugins() as $plugin) {
if ($plugin->checkStatement(
$stmt,
$context,
$this->checked_file_name,
$this->getSuppressedIssues()
) === false) {
return false;
$plugins = Config::getInstance()->getPlugins();
if ($plugins) {
$code_location = new CodeLocation($this->source, $stmt);
foreach ($plugins as $plugin) {
if ($plugin->checkStatement(
$stmt,
$context,
$code_location,
$this->getSuppressedIssues()
) === false) {
return false;
}
}
}
if ($has_returned && !($stmt instanceof PhpParser\Node\Stmt\Nop) &&
!($stmt instanceof PhpParser\Node\Stmt\InlineHTML)) {
@ -223,8 +248,7 @@ class StatementsChecker
if (IssueBuffer::accepts(
new ContinueOutsideLoop(
'Continue call outside loop context',
$this->checked_file_name,
$stmt->getLine()
new CodeLocation($this->source, $stmt)
),
$this->suppressed_issues
)) {
@ -291,15 +315,13 @@ class StatementsChecker
if (IssueBuffer::accepts(
new InvalidGlobal(
'Cannot use global scope here',
$this->checked_file_name,
$stmt->getLine()
new CodeLocation($this->source, $stmt)
),
$this->suppressed_issues
)) {
return false;
}
}
else {
} else {
foreach ($stmt->vars as $var) {
if ($var instanceof PhpParser\Node\Expr\Variable) {
if (is_string($var->name)) {
@ -361,8 +383,7 @@ class StatementsChecker
if (IssueBuffer::accepts(
new InvalidNamespace(
'Cannot redeclare namespace',
$this->checked_file_name,
$stmt->getLine()
new CodeLocation($this->source, $stmt)
),
$this->suppressed_issues
)) {
@ -378,8 +399,7 @@ class StatementsChecker
if (IssueBuffer::accepts(
new UnrecognizedStatement(
'Psalm does not understand ' . get_class($stmt),
$this->getCheckedFileName(),
$stmt->getLine()
new CodeLocation($this->source, $stmt)
),
$this->getSuppressedIssues()
)) {
@ -713,11 +733,14 @@ class StatementsChecker
if (file_exists($path_to_file)) {
$include_stmts = FileChecker::getStatementsForFile($path_to_file);
$old_include_file_name = $this->include_file_name;
$this->include_file_name = Config::getInstance()->shortenFileName($path_to_file);
$this->source->setIncludeFileName($this->include_file_name);
$old_include_file_path = $this->include_file_path;
$this->include_file_path = $path_to_file;
$this->include_file_name = Config::getInstance()->shortenFileName($this->include_file_path);
$this->source->setIncludeFileName($this->include_file_name, $this->include_file_path);
$this->check($include_stmts, $context);
$this->include_file_name = $old_include_file_name;
$this->source->setIncludeFileName($old_include_file_name);
$this->include_file_path = $old_include_file_path;
$this->source->setIncludeFileName($old_include_file_name, $old_include_file_path);
return null;
}
}
@ -865,6 +888,14 @@ class StatementsChecker
return $this->checked_file_name;
}
/**
* @return string
*/
public function getCheckedFilePath()
{
return $this->checked_file_path;
}
/**
* @return string
*/
@ -873,6 +904,14 @@ class StatementsChecker
return $this->file_name;
}
/**
* @return string
*/
public function getFilePath()
{
return $this->file_path;
}
/**
* @return string|null
*/

View File

@ -27,6 +27,7 @@ class TraitChecker extends ClassLikeChecker
$this->namespace = $source->getNamespace();
$this->aliased_classes = $source->getAliasedClasses();
$this->file_name = $source->getFileName();
$this->file_path = $source->getFilePath();
$this->fq_class_name = $fq_class_name;
$this->parent_class = null;

View File

@ -3,6 +3,7 @@ namespace Psalm\Checker;
use PhpParser;
use Psalm\Checker\Statements\ExpressionChecker;
use Psalm\CodeLocation;
use Psalm\Issue\FailedTypeResolution;
use Psalm\IssueBuffer;
use Psalm\Type;
@ -1049,16 +1050,14 @@ class TypeChecker
*
* @param array<string,string> $new_types
* @param array<string,Type\Union> $existing_types
* @param string $file_name
* @param int $line_number
* @param CodeLocation $code_location
* @param array<string> $suppressed_issues
* @return array|false
*/
public static function reconcileKeyedTypes(
array $new_types,
array $existing_types,
$file_name,
$line_number,
CodeLocation $code_location,
array $suppressed_issues = []
) {
$keys = array_merge(array_keys($new_types), array_keys($existing_types));
@ -1087,8 +1086,7 @@ class TypeChecker
(string) $new_type_part,
$result_type,
$key,
$file_name,
$line_number,
$code_location,
$suppressed_issues
);
@ -1124,8 +1122,7 @@ class TypeChecker
* @param string $new_var_type
* @param Type\Union $existing_var_type
* @param string $key
* @param string $file_name
* @param int $line_number
* @param CodeLocation $code_location
* @param array $suppressed_issues
* @return Type\Union|null|false
*/
@ -1133,8 +1130,7 @@ class TypeChecker
$new_var_type,
Type\Union $existing_var_type = null,
$key = null,
$file_name = null,
$line_number = null,
CodeLocation $code_location = null,
array $suppressed_issues = []
) {
$result_var_types = null;
@ -1191,9 +1187,9 @@ class TypeChecker
$existing_var_type->removeType($negated_type);
if (empty($existing_var_type->types)) {
if ($key && $file_name && $line_number) {
if ($key && $code_location) {
if (IssueBuffer::accepts(
new FailedTypeResolution('Cannot resolve types for ' . $key, $file_name, $line_number),
new FailedTypeResolution('Cannot resolve types for ' . $key, $code_location),
$suppressed_issues
)) {
return false;

View File

@ -0,0 +1,55 @@
<?php
namespace Psalm;
class CodeLocation
{
/** @var int */
public $file_start;
/** @var int */
public $file_end;
/** @var string */
public $file_path;
/** @var string */
public $file_name;
/** @var int */
public $line_number;
/** @var bool */
public $single_line;
/** @var int */
public $preview_start;
/** @var int|null */
public $comment_line_number;
/**
* @param StatementsSource $statements_source
* @param \PhpParser\Node $stmt
* @param boolean $single_line
*/
public function __construct(StatementsSource $statements_source, \PhpParser\Node $stmt, $single_line = false)
{
$this->file_start = (int)$stmt->getAttribute('startFilePos');
$this->file_end = (int)$stmt->getAttribute('endFilePos');
$this->file_path = $statements_source->getCheckedFilePath();
$this->file_name = $statements_source->getCheckedFileName();
$this->single_line = $single_line;
$doc_comment = $stmt->getDocComment();
$this->preview_start = $doc_comment ? $doc_comment->getFilePos() : $this->file_start;
$this->line_number = $doc_comment ? $doc_comment->getLine() : $stmt->getLine();
}
/**
* @param int $line
* @return void
*/
public function setCommentLine($line) {
$this->comment_line_number = $line;
}
}

View File

@ -30,4 +30,7 @@ class FunctionDocblockComment
* @var array<string>
*/
public $suppress = [];
/** @var int */
public $return_type_line_number;
}

View File

@ -34,9 +34,9 @@ class FunctionLikeParameter
public $is_nullable;
/**
* @var int
* @var CodeLocation|null
*/
public $line;
public $code_location;
/**
* @var bool
@ -47,6 +47,7 @@ class FunctionLikeParameter
* @param string $name
* @param boolean $by_ref
* @param Type\Union $type
* @param CodeLocation $code_location
* @param boolean $is_optional
* @param boolean $is_nullable
* @param boolean $is_variadic
@ -55,6 +56,7 @@ class FunctionLikeParameter
$name,
$by_ref,
Type\Union $type,
CodeLocation $code_location = null,
$is_optional = true,
$is_nullable = false,
$is_variadic = false
@ -66,5 +68,6 @@ class FunctionLikeParameter
$this->is_optional = $is_optional;
$this->is_nullable = $is_nullable;
$this->is_variadic = $is_variadic;
$this->code_location = $code_location;
}
}

View File

@ -1,19 +1,16 @@
<?php
namespace Psalm\Issue;
use Psalm\CodeLocation;
abstract class CodeIssue
{
const CODE_EXCEPTION = 1;
/**
* @var string
* @var CodeLocation
*/
protected $file_name;
/**
* @var int
*/
protected $line_number;
protected $code_location;
/**
* @var string
@ -21,14 +18,12 @@ abstract class CodeIssue
protected $message;
/**
* @param string $message
* @param string $file_name
* @param int $line_number
* @param string $message
* @param CodeLocation $code_location
*/
public function __construct($message, $file_name, $line_number)
public function __construct($message, CodeLocation $code_location)
{
$this->line_number = $line_number;
$this->file_name = $file_name;
$this->code_location = $code_location;
$this->message = $message;
}
@ -37,7 +32,23 @@ abstract class CodeIssue
*/
public function getLineNumber()
{
return $this->line_number;
return $this->code_location->line_number;
}
/**
* @return int[]
*/
public function getFileRange()
{
return [$this->code_location->file_start, $this->code_location->file_end];
}
/**
* @return string
*/
public function getFilePath()
{
return $this->code_location->file_path;
}
/**
@ -45,7 +56,7 @@ abstract class CodeIssue
*/
public function getFileName()
{
return $this->file_name;
return $this->code_location->file_name;
}
/**
@ -53,6 +64,48 @@ abstract class CodeIssue
*/
public function getMessage()
{
return $this->file_name . ':' . $this->line_number .' - ' . $this->message;
return $this->code_location->file_name . ':' . $this->code_location->line_number .' - ' . $this->message;
}
/**
* @return string
*/
public function getFileSnippet()
{
$file_start = $this->code_location->file_start;
$file_end = $this->code_location->file_end;
$preview_start = $this->code_location->preview_start;
$file_contents = (string)file_get_contents($this->code_location->file_path);
if ($this->code_location->comment_line_number && $preview_start < $file_start) {
$preview_lines = explode("\n", substr($file_contents, $preview_start, $file_start - $preview_start - 1));
$preview_offset = 0;
$i = 0;
while ($i < $this->code_location->comment_line_number - $this->code_location->line_number) {
$preview_offset += strlen($preview_lines[$i++]) + 1;
}
$preview_offset += (int)strpos($preview_lines[$i], '@');
$file_start = $preview_offset + $preview_start;
}
$line_beginning = (int)strrpos(
$file_contents,
"\n",
min($preview_start, $file_start) - strlen($file_contents)
) + 1;
$line_end = (int)strpos($file_contents, "\n", $this->code_location->single_line ? $file_start : $file_end);
$code_line = substr($file_contents, $line_beginning, $line_end - $line_beginning);
$code_line_error_start = $file_start - $line_beginning;
$code_line_error_length = $file_end - $file_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;
}
}

View File

@ -54,15 +54,15 @@ class IssueBuffer
$reporting_level = $config->getReportingLevel($issue_type);
switch ($reporting_level) {
case Config::REPORT_INFO:
if (ProjectChecker::$show_info && !self::alreadyEmitted($error_message)) {
echo 'INFO: ' . $error_message . PHP_EOL;
}
return false;
if ($reporting_level === Config::REPORT_SUPPRESS) {
return false;
}
case Config::REPORT_SUPPRESS:
return false;
if ($reporting_level === Config::REPORT_INFO) {
if (ProjectChecker::$show_info && !self::alreadyEmitted($error_message)) {
echo 'INFO: ' . $error_message . PHP_EOL;
}
return false;
}
if ($config->throw_exception) {
@ -70,8 +70,10 @@ class IssueBuffer
}
if (!self::alreadyEmitted($error_message)) {
echo (ProjectChecker::$use_color ? "\033[0;31m" : '') . 'ERROR: ' .
(ProjectChecker::$use_color ? "\033[0m" : '') . $error_message . PHP_EOL;
echo (ProjectChecker::$use_color ? "\e[0;31m" : '') . 'ERROR: ' .
(ProjectChecker::$use_color ? "\e[0m" : '') . $error_message . PHP_EOL;
echo $e->getFileSnippet() . PHP_EOL;
}
if ($config->stop_on_first_error) {

View File

@ -10,12 +10,12 @@ abstract class Plugin
*
* @param PhpParser\Node\Expr $stmt
* @param Context $context
* @param string $file_name
* @param CodeLocation $code_location
* @param array $suppressed_issues
* @return null|false
* @psalm-suppress InvalidReturnType
*/
public function checkExpression(PhpParser\Node\Expr $stmt, Context $context, $file_name, array $suppressed_issues)
public function checkExpression(PhpParser\Node\Expr $stmt, Context $context, CodeLocation $code_location, array $suppressed_issues)
{
return null;
}
@ -25,12 +25,12 @@ abstract class Plugin
*
* @param PhpParser\Node $stmt
* @param Context $context
* @param string $file_name
* @param CodeLocation $code_location
* @param array $suppressed_issues
* @return null|false
* @psalm-suppress InvalidReturnType
*/
public function checkStatement(PhpParser\Node $stmt, Context $context, $file_name, array $suppressed_issues)
public function checkStatement(PhpParser\Node $stmt, Context $context, CodeLocation $code_location, array $suppressed_issues)
{
return null;
}

View File

@ -57,20 +57,36 @@ interface StatementsSource
*/
public function getFileName();
/**
* @return string
*/
public function getFilePath();
/**
* @return string|null
*/
public function getIncludeFileName();
/**
* @return string|null
*/
public function getIncludeFilePath();
/**
* @return string
*/
public function getCheckedFileName();
/**
* @param string|null $file_name
* @return string
*/
public function setIncludeFileName($file_name);
public function getCheckedFilePath();
/**
* @param string|null $file_name
* @param string|null $file_path
*/
public function setIncludeFileName($file_name, $file_path);
/**
* @return bool