From a1acbfec071f939eb9af92a939b8b9a027197f4a Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Sat, 3 Dec 2016 19:11:30 -0500 Subject: [PATCH] Show code snippets when reporting errors This also introduces a new method of identifying specific code locations when creating issues --- examples/StringChecker.php | 22 +-- src/Psalm/Checker/ClassLikeChecker.php | 51 +++---- src/Psalm/Checker/CommentChecker.php | 47 +++++-- src/Psalm/Checker/FileChecker.php | 40 +++--- src/Psalm/Checker/FunctionChecker.php | 130 +++++++++--------- src/Psalm/Checker/FunctionLikeChecker.php | 74 +++++----- src/Psalm/Checker/MethodChecker.php | 61 ++++---- src/Psalm/Checker/NamespaceChecker.php | 2 + src/Psalm/Checker/SourceChecker.php | 38 ++++- .../Statements/Block/ForeachChecker.php | 10 +- .../Checker/Statements/Block/IfChecker.php | 19 +-- .../Statements/Block/SwitchChecker.php | 4 +- .../Checker/Statements/Block/TryChecker.php | 4 +- .../Checker/Statements/Block/WhileChecker.php | 4 +- .../Expression/AssignmentChecker.php | 63 +++------ .../Statements/Expression/CallChecker.php | 119 +++++++--------- .../Statements/Expression/FetchChecker.php | 85 ++++-------- .../Checker/Statements/ExpressionChecker.php | 79 +++++------ src/Psalm/Checker/StatementsChecker.php | 81 ++++++++--- src/Psalm/Checker/TraitChecker.php | 1 + src/Psalm/Checker/TypeChecker.php | 20 ++- src/Psalm/CodeLocation.php | 55 ++++++++ src/Psalm/FunctionDocblockComment.php | 3 + src/Psalm/FunctionLikeParameter.php | 7 +- src/Psalm/Issue/CodeIssue.php | 85 +++++++++--- src/Psalm/IssueBuffer.php | 24 ++-- src/Psalm/Plugin.php | 8 +- src/Psalm/StatementsSource.php | 18 ++- 28 files changed, 640 insertions(+), 514 deletions(-) create mode 100644 src/Psalm/CodeLocation.php diff --git a/examples/StringChecker.php b/examples/StringChecker.php index 671e66d6966..2f8b654edbb 100644 --- a/examples/StringChecker.php +++ b/examples/StringChecker.php @@ -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 $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 @@ public function checkExpression(PhpParser\Node\Expr $stmt, \Psalm\Context $conte 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 @@ public function checkExpression(PhpParser\Node\Expr $stmt, \Psalm\Context $conte if ($fq_class_name !== $stmt->value) { if (Checker\MethodChecker::checkMethodExists( $stmt->value, - $file_name, - $stmt->getLine(), - $suppressed_issues) + $code_location, + $suppressed_issues + ) ) { return false; } diff --git a/src/Psalm/Checker/ClassLikeChecker.php b/src/Psalm/Checker/ClassLikeChecker.php index a51bbbd448a..d7e8fc7711a 100644 --- a/src/Psalm/Checker/ClassLikeChecker.php +++ b/src/Psalm/Checker/ClassLikeChecker.php @@ -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 @@ public function __construct(PhpParser\Node\Stmt\ClassLike $class, StatementsSour $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 @@ public function check($check_methods = true, Context $class_context = null, $upd 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 @@ public function check($check_methods = true, Context $class_context = null, $upd 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 @@ public function check($check_methods = true, Context $class_context = null, $upd */ 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 @@ protected function visitTraitUse( 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 @@ protected function visitTraitUse( 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 @@ protected function visitPropertyDeclaration( 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 @@ protected function visitPropertyDeclaration( 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 @@ public static function classExtendsOrImplements($fq_class_name, $possible_parent /** * @param string $fq_class_name - * @param string $file_name - * @param int $line_number + * @param CodeLocation $code_location * @param array $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 @@ public static function checkFullyQualifiedClassLikeName( 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 @@ public static function checkFullyQualifiedClassLikeName( 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 @@ public static function checkFullyQualifiedClassLikeName( } } - FileChecker::addFileReferenceToClass(Config::getInstance()->getBaseDir() . $file_name, $fq_class_name); + FileChecker::addFileReferenceToClass(Config::getInstance()->getBaseDir() . $code_location->file_name, $fq_class_name); return true; } diff --git a/src/Psalm/Checker/CommentChecker.php b/src/Psalm/Checker/CommentChecker.php index 72a4c56f880..cb4509c27c1 100644 --- a/src/Psalm/Checker/CommentChecker.php +++ b/src/Psalm/Checker/CommentChecker.php @@ -78,23 +78,25 @@ public static function getTypeFromComment( } /** - * @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 @@ public static function extractDocblockInfo($comment) && !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 @@ public static function extractDocblockInfo($comment) $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 @@ protected static function splitDocLine($return_block) * 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 @@ public static function parseDocComment($docblock) // 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 @@ public static function parseDocComment($docblock) $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 @@ public static function parseDocComment($docblock) $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; } } diff --git a/src/Psalm/Checker/FileChecker.php b/src/Psalm/Checker/FileChecker.php index c7276e58bc2..8a7131f24f4 100644 --- a/src/Psalm/Checker/FileChecker.php +++ b/src/Psalm/Checker/FileChecker.php @@ -17,7 +17,7 @@ class FileChecker extends SourceChecker implements StatementsSource /** * @var string */ - protected $real_file_name; + protected $file_path; /** * @var array> @@ -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 @@ public function check( 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 @@ public function check( } 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 @@ public function check( 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 @@ protected function getStatements() { return $this->preloaded_statements ? $this->preloaded_statements - : self::getStatementsForFile($this->real_file_name); + : self::getStatementsForFile($this->file_path); } /** @@ -339,8 +339,10 @@ public static function getStatementsForFile($file_name) $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 @@ public static function getStatementsForFile($file_name) } 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 @@ public function getAliasedClassesFlipped($namespace_name = null) return $this->aliased_classes_flipped; } - /** - * @return string - */ - public function getRealFileName() - { - return $this->real_file_name; - } - /** * @param string $file_name * @return mixed diff --git a/src/Psalm/Checker/FunctionChecker.php b/src/Psalm/Checker/FunctionChecker.php index a1d06504b68..d8b8a71ceb4 100644 --- a/src/Psalm/Checker/FunctionChecker.php +++ b/src/Psalm/Checker/FunctionChecker.php @@ -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 @@ protected function registerFunction(PhpParser\Node\Stmt\Function_ $function, $fi 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 @@ protected function registerFunction(PhpParser\Node\Stmt\Function_ $function, $fi $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 ($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 ($docblock_info->variadic) { - self::$variadic_functions[$file_name][$function_id] = true; - } + if ($docblock_info) { + if ($docblock_info->deprecated) { + self::$deprecated_functions[$file_name][$function_id] = true; + } - $this->suppressed_issues = $docblock_info->suppress; + if ($docblock_info->variadic) { + self::$variadic_functions[$file_name][$function_id] = true; + } - if ($function->returnType) { - $return_type = Type::parseString( - is_string($function->returnType) - ? $function->returnType - : ClassLikeChecker::getFQCLNFromNameObject( - $function->returnType, - $this->namespace, - $this->getAliasedClasses() - ) - ); - } + $this->suppressed_issues = $docblock_info->suppress; - if ($config->use_docblock_types) { - if ($docblock_info->return_type) { - $return_type = - Type::parseString( - self::fixUpLocalType( - (string)$docblock_info->return_type, - null, + 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 @@ public static function getParamsFromCallMap($function_id) $arg_name, $by_reference, $arg_type ? Type::parseString($arg_type) : Type::getMixed(), + null, $optional, false, $arg_name === '...' @@ -362,16 +368,14 @@ public static function getReturnTypeFromCallMap($function_id) /** * @param string $function_id * @param array $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 @@ public static function getReturnTypeFromCallMapWithArgs( $array_return_type = self::getArrayReturnType( $call_map_key, $call_args, - $file_name, - $line_number, + $code_location, $suppressed_issues ); @@ -434,20 +437,18 @@ public static function getReturnTypeFromCallMapWithArgs( /** * @param string $call_map_key * @param array $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 @@ protected static function getArrayReturnType( /** * @param string $call_map_key * @param array $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 @@ protected static function getArrayMapReturnType( 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 ); diff --git a/src/Psalm/Checker/FunctionLikeChecker.php b/src/Psalm/Checker/FunctionLikeChecker.php index 3a91a635ac9..1fbab9cf389 100644 --- a/src/Psalm/Checker/FunctionLikeChecker.php +++ b/src/Psalm/Checker/FunctionLikeChecker.php @@ -3,6 +3,7 @@ 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 @@ public function __construct($function, StatementsSource $source) $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 @@ public function check(Context $context, Context $global_context = null) 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 @@ public function check(Context $context, Context $global_context = null) $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 @@ public function check(Context $context, Context $global_context = null) foreach ($this->function->getParams() as $param) { $function_params[] = self::getTranslatedParam( $param, - $this->fq_class_name, - $this->namespace, - $this->getAliasedClasses() + $this ); } } @@ -221,14 +220,17 @@ public function check(Context $context, Context $global_context = null) $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 @@ public function check(Context $context, Context $global_context = null) $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 @@ public function checkReturnTypes($update_docblock = false) 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 @@ public function checkReturnTypes($update_docblock = false) 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 @@ public function checkReturnTypes($update_docblock = false) 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 @@ public function checkReturnTypes($update_docblock = false) 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 @@ public function checkReturnTypes($update_docblock = false) } /** - * @param array $docblock_params + * @param array $docblock_params * @param array $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 @@ protected function improveParamsFromDocblock( 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 @@ protected function improveParamsFromDocblock( } } - 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 @@ protected function improveParamsFromDocblock( /** * @param PhpParser\Node\Param $param - * @param string $fq_class_name - * @param string $namespace - * @param array $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 @@ public static function getTranslatedParam( } 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 @@ public static function getTranslatedParam( $param->name, $param->byRef, $param_type ?: Type::getMixed(), + new CodeLocation($source, $param), $is_optional, $is_nullable, $param->variadic @@ -772,6 +767,7 @@ protected static function getReflectionParamArray(\ReflectionParameter $param) $param_name, (bool)$param->isPassedByReference(), $param_type, + null, $is_optional, $is_nullable ); diff --git a/src/Psalm/Checker/MethodChecker.php b/src/Psalm/Checker/MethodChecker.php index 4b01f70178b..ae9184626cd 100644 --- a/src/Psalm/Checker/MethodChecker.php +++ b/src/Psalm/Checker/MethodChecker.php @@ -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 @@ public static function extractReflectionMethodInfo(\ReflectionMethod $method) * 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 $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 @@ public static function checkMethodStatic($method_id, $file_name, $line_number, a 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 @@ protected function registerMethod(PhpParser\Node\Stmt\ClassMethod $method) { 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 @@ protected function registerMethod(PhpParser\Node\Stmt\ClassMethod $method) 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 @@ protected function registerMethod(PhpParser\Node\Stmt\ClassMethod $method) $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 @@ protected function registerMethod(PhpParser\Node\Stmt\ClassMethod $method) $docblock_info->params, $method_param_names, self::$method_params[$method_id], - $method->getLine() + new CodeLocation($this, $method, true) ); } } @@ -433,20 +428,19 @@ protected static function fixUpReturnType($return_type, $method_id) } /** - * @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 @@ public static function registerClassMethod($method_id) } /** - * @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 @@ public static function checkMethodNotDeprecated($method_id, $file_name, $line_nu 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 @@ public static function checkMethodNotDeprecated($method_id, $file_name, $line_nu * @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 @@ public static function checkMethodVisibility( $method_id, $calling_context, StatementsSource $source, - $line_number, + CodeLocation $code_location, array $suppressed_issues ) { self::registerClassMethod($method_id); @@ -551,7 +543,7 @@ public static function checkMethodVisibility( 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 @@ public static function checkMethodVisibility( 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 @@ public static function checkMethodVisibility( if (IssueBuffer::accepts( new InaccessibleMethod( 'Cannot access protected method ' . $method_id, - $source->getFileName(), - $line_number + $code_location ), $suppressed_issues )) { @@ -614,8 +604,7 @@ public static function checkMethodVisibility( new InaccessibleMethod( 'Cannot access protected method ' . MethodChecker::getCasedMethodId($method_id) . ' from context ' . $calling_context, - $source->getFileName(), - $line_number + $code_location ), $suppressed_issues )) { diff --git a/src/Psalm/Checker/NamespaceChecker.php b/src/Psalm/Checker/NamespaceChecker.php index 80cf5e65009..b0017f32fdc 100644 --- a/src/Psalm/Checker/NamespaceChecker.php +++ b/src/Psalm/Checker/NamespaceChecker.php @@ -35,7 +35,9 @@ public function __construct(Namespace_ $namespace, StatementsSource $source) $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(); } diff --git a/src/Psalm/Checker/SourceChecker.php b/src/Psalm/Checker/SourceChecker.php index 268e135ce6b..d615dd983cc 100644 --- a/src/Psalm/Checker/SourceChecker.php +++ b/src/Psalm/Checker/SourceChecker.php @@ -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 @@ public function getFileName() return $this->file_name; } + /** + * @return string + */ + public function getFilePath() + { + return $this->file_path; + } + /** * @return null|string */ @@ -206,13 +224,23 @@ public function getIncludeFileName() 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 @@ public function getCheckedFileName() return $this->include_file_name ?: $this->file_name; } + /** + * @return string + */ + public function getCheckedFilePath() + { + return $this->include_file_path ?: $this->file_path; + } + /** * @return bool */ diff --git a/src/Psalm/Checker/Statements/Block/ForeachChecker.php b/src/Psalm/Checker/Statements/Block/ForeachChecker.php index 359c4460d05..6412c179679 100644 --- a/src/Psalm/Checker/Statements/Block/ForeachChecker.php +++ b/src/Psalm/Checker/Statements/Block/ForeachChecker.php @@ -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 @@ public static function check( 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 @@ public static function check( 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 @@ public static function check( ) { if (ClassLikeChecker::checkFullyQualifiedClassLikeName( $return_type->value, - $statements_checker->getCheckedFileName(), - $stmt->getLine(), + new CodeLocation($statements_checker->getSource(), $stmt), $statements_checker->getSuppressedIssues() ) === false) { return false; diff --git a/src/Psalm/Checker/Statements/Block/IfChecker.php b/src/Psalm/Checker/Statements/Block/IfChecker.php index 4da75a5c7fb..85371e1694d 100644 --- a/src/Psalm/Checker/Statements/Block/IfChecker.php +++ b/src/Psalm/Checker/Statements/Block/IfChecker.php @@ -6,6 +6,7 @@ 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 @@ public static function check( 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 @@ public static function check( $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 @@ protected static function checkIfBlock( $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 @@ protected static function checkElseIfBlock( $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 @@ protected static function checkElseIfBlock( $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 @@ protected static function checkElseBlock( $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() ); diff --git a/src/Psalm/Checker/Statements/Block/SwitchChecker.php b/src/Psalm/Checker/Statements/Block/SwitchChecker.php index 2127522afc2..45d224d8890 100644 --- a/src/Psalm/Checker/Statements/Block/SwitchChecker.php +++ b/src/Psalm/Checker/Statements/Block/SwitchChecker.php @@ -5,6 +5,7 @@ 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 @@ public static function check( if (IssueBuffer::accepts( new InvalidContinue( 'Continue called when not in loop', - $statements_checker->getCheckedFileName(), - $case->getLine() + new CodeLocation($statements_checker->getSource(), $case) ) )) { return false; diff --git a/src/Psalm/Checker/Statements/Block/TryChecker.php b/src/Psalm/Checker/Statements/Block/TryChecker.php index f056f85e6ea..fd972263ca5 100644 --- a/src/Psalm/Checker/Statements/Block/TryChecker.php +++ b/src/Psalm/Checker/Statements/Block/TryChecker.php @@ -3,6 +3,7 @@ use PhpParser; use Psalm\Checker\ClassLikeChecker; +use Psalm\CodeLocation; use Psalm\Checker\ScopeChecker; use Psalm\Checker\StatementsChecker; use Psalm\Context; @@ -43,8 +44,7 @@ public static function check( if (ClassLikeChecker::checkFullyQualifiedClassLikeName( $fq_class_name, - $statements_checker->getCheckedFileName(), - $stmt->getLine(), + new CodeLocation($statements_checker->getSource(), $stmt), $statements_checker->getSuppressedIssues() ) === false) { return false; diff --git a/src/Psalm/Checker/Statements/Block/WhileChecker.php b/src/Psalm/Checker/Statements/Block/WhileChecker.php index 053f083d6d2..16eb2d51a24 100644 --- a/src/Psalm/Checker/Statements/Block/WhileChecker.php +++ b/src/Psalm/Checker/Statements/Block/WhileChecker.php @@ -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 @@ public static function check( $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() ); diff --git a/src/Psalm/Checker/Statements/Expression/AssignmentChecker.php b/src/Psalm/Checker/Statements/Expression/AssignmentChecker.php index 7f599fb2f7e..16b7160082c 100644 --- a/src/Psalm/Checker/Statements/Expression/AssignmentChecker.php +++ b/src/Psalm/Checker/Statements/Expression/AssignmentChecker.php @@ -9,6 +9,7 @@ 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 @@ public static function check( 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 @@ public static function checkAssignmentOperation( $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 @@ public static function checkPropertyAssignment( 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 @@ public static function checkPropertyAssignment( 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 @@ public static function checkPropertyAssignment( 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 @@ public static function checkPropertyAssignment( 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 @@ public static function checkPropertyAssignment( 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 @@ public static function checkPropertyAssignment( 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 @@ public static function checkPropertyAssignment( 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 @@ public static function checkPropertyAssignment( 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 @@ public static function checkPropertyAssignment( 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 @@ public static function checkPropertyAssignment( 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 @@ public static function checkPropertyAssignment( 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 @@ public static function checkPropertyAssignment( 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 @@ protected static function checkStaticPropertyAssignment( 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 @@ protected static function checkStaticPropertyAssignment( 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 @@ protected static function checkStaticPropertyAssignment( 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 @@ protected static function checkStaticPropertyAssignment( 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 @@ protected static function checkStaticPropertyAssignment( 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 @@ protected static function checkArrayAssignment( 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 @@ protected static function checkArrayAssignment( 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() )) { diff --git a/src/Psalm/Checker/Statements/Expression/CallChecker.php b/src/Psalm/Checker/Statements/Expression/CallChecker.php index b792c2180b3..367794bc7f2 100644 --- a/src/Psalm/Checker/Statements/Expression/CallChecker.php +++ b/src/Psalm/Checker/Statements/Expression/CallChecker.php @@ -10,6 +10,7 @@ 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 @@ public static function checkFunctionCall( 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 @@ public static function checkFunctionCall( $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 @@ public static function checkFunctionCall( $stmt->args, $method_id, $context, - $stmt->getLine() + $code_location ) === false) { return false; } @@ -129,8 +131,7 @@ public static function checkFunctionCall( $stmt->inferredType = FunctionChecker::getReturnTypeFromCallMapWithArgs( $method_id, $stmt->args, - $statements_checker->getCheckedFileName(), - $stmt->getLine(), + $code_location, $statements_checker->getSuppressedIssues() ); } else { @@ -185,8 +186,7 @@ public static function checkNew( 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 @@ public static function checkNew( $stmt->args, $method_id, $context, - $stmt->getLine() + new CodeLocation($statements_checker->getSource(), $stmt) ) === false) { return false; } @@ -305,8 +305,7 @@ public static function checkMethodCall( 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 @@ public static function checkMethodCall( 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 @@ public static function checkMethodCall( 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 @@ public static function checkMethodCall( 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 @@ public static function checkMethodCall( $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 @@ public static function checkMethodCall( $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 @@ public static function checkMethodCall( $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 @@ public static function checkMethodCall( 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 @@ public static function checkMethodCall( $stmt->args, $method_id, $context, - $stmt->getLine(), + new CodeLocation($statements_checker->getSource(), $stmt), $has_mock ) === false) { return false; @@ -551,8 +544,7 @@ public static function checkStaticCall( 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 @@ public static function checkStaticCall( $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 @@ public static function checkStaticCall( $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 @@ public static function checkStaticCall( $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 @@ public static function checkStaticCall( ) { 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 @@ public static function checkStaticCall( 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 @@ public static function checkStaticCall( $stmt->args, $method_id, $context, - $stmt->getLine(), + new CodeLocation($statements_checker->getSource(), $stmt), $has_mock ) === false) { return false; @@ -720,7 +708,7 @@ public static function checkStaticCall( * @param array $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 @@ protected static function checkFunctionArguments( array $args, $method_id, Context $context, - $line_number, + CodeLocation $code_location, $is_mock = false ) { $function_params = null; @@ -879,7 +867,7 @@ protected static function checkFunctionArguments( ), $cased_method_id, $argument_offset, - $arg->value->getLine() + new CodeLocation($statements_checker->getSource(), $arg->value) ) === false) { return false; } @@ -924,8 +912,7 @@ protected static function checkFunctionArguments( 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 @@ protected static function checkFunctionArguments( 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 @@ protected static function checkFunctionArguments( $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 @@ protected static function checkFunctionArguments( 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 @@ protected static function checkFunctionArguments( 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 @@ protected static function checkFunctionArguments( 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 @@ protected static function checkFunctionArguments( 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 @@ protected static function checkFunctionArguments( 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 @@ protected static function checkFunctionArguments( * @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 @@ protected static function checkFunctionArgumentType( Type\Union $param_type, $cased_method_id, $argument_offset, - $line_number + CodeLocation $code_location ) { if ($param_type->isMixed()) { return null; @@ -1086,8 +1065,7 @@ protected static function checkFunctionArgumentType( 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 @@ protected static function checkFunctionArgumentType( 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 @@ protected static function checkFunctionArgumentType( 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 @@ protected static function checkFunctionArgumentType( 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 @@ protected static function checkFunctionArgumentType( 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 @@ protected static function checkFunctionArgumentType( * @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 @@ protected static function checkFunctionExists( if (IssueBuffer::accepts( new UndefinedFunction( 'Function ' . $cased_function_id . ' does not exist', - $statements_checker->getCheckedFileName(), - $line_number + $code_location ), $statements_checker->getSuppressedIssues() )) { diff --git a/src/Psalm/Checker/Statements/Expression/FetchChecker.php b/src/Psalm/Checker/Statements/Expression/FetchChecker.php index 16c9cb3b8ca..0653cd1c478 100644 --- a/src/Psalm/Checker/Statements/Expression/FetchChecker.php +++ b/src/Psalm/Checker/Statements/Expression/FetchChecker.php @@ -9,6 +9,7 @@ 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 @@ public static function checkPropertyFetch( 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 @@ public static function checkPropertyFetch( 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 @@ public static function checkPropertyFetch( 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 @@ public static function checkPropertyFetch( 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 @@ public static function checkPropertyFetch( 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 @@ public static function checkPropertyFetch( 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 @@ public static function checkPropertyFetch( 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 @@ public static function checkPropertyFetch( 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 @@ public static function checkPropertyFetch( 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 @@ public static function checkPropertyFetch( 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 @@ public static function checkConstFetch( 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 @@ public static function checkClassConstFetch( 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 @@ public static function checkClassConstFetch( 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 @@ public static function checkStaticPropertyFetch( 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 @@ public static function checkStaticPropertyFetch( 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 @@ public static function checkStaticPropertyFetch( 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 @@ public static function checkArrayAccess( 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 @@ public static function checkArrayAccess( $assignment_key_type, $assignment_value_type, $var_id, - $stmt->getLine() + new CodeLocation($statements_checker->getSource(), $stmt) ); if ($refined_type === false) { @@ -888,8 +871,7 @@ public static function checkArrayAccess( 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 @@ public static function checkArrayAccess( 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 @@ public static function checkArrayAccess( 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 @@ public static function checkArrayAccess( 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 @@ public static function checkArrayAccess( 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 @@ public static function checkArrayAccess( 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 @@ public static function checkArrayAccess( * @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 @@ protected static function refineArrayType( 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 @@ protected static function refineArrayType( 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() )) { diff --git a/src/Psalm/Checker/Statements/ExpressionChecker.php b/src/Psalm/Checker/Statements/ExpressionChecker.php index aed2d3a42bd..962fd5aeaa6 100644 --- a/src/Psalm/Checker/Statements/ExpressionChecker.php +++ b/src/Psalm/Checker/Statements/ExpressionChecker.php @@ -12,6 +12,7 @@ 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 @@ public static function check( 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 @@ public static function check( // 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 @@ public static function check( 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 @@ public static function check( } } - 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 @@ public static function checkVariable( 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 @@ public static function checkVariable( 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 @@ public static function checkVariable( 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 @@ protected static function checkBinaryOp( $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 @@ protected static function checkBinaryOp( $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 @@ protected static function checkBinaryOp( // 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 @@ protected static function checkBinaryOp( $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 @@ protected static function checkBinaryOp( } /** - * @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 @@ protected static function checkClosureUses( 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 @@ protected static function checkClosureUses( 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 @@ protected static function checkClosureUses( 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 @@ protected static function checkTernary( $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 @@ protected static function checkTernary( $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 @@ protected static function checkTernary( '!empty', $stmt->cond->inferredType, '', - $statements_checker->getCheckedFileName(), - $stmt->getLine(), + new CodeLocation($statements_checker->getSource(), $stmt), $statements_checker->getSuppressedIssues() ); diff --git a/src/Psalm/Checker/StatementsChecker.php b/src/Psalm/Checker/StatementsChecker.php index 60566a51417..7b77823ba92 100644 --- a/src/Psalm/Checker/StatementsChecker.php +++ b/src/Psalm/Checker/StatementsChecker.php @@ -10,6 +10,7 @@ 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 @@ public function __construct(StatementsSource $source) { $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 @@ public function check(array $stmts, Context $context, Context $loop_context = nu } 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 @@ public function check(array $stmts, Context $context, Context $loop_context = nu 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 @@ public function check(array $stmts, Context $context, Context $loop_context = nu 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 @@ public function check(array $stmts, Context $context, Context $loop_context = nu 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 @@ public function check(array $stmts, Context $context, Context $loop_context = nu 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 @@ public function checkInclude(PhpParser\Node\Expr\Include_ $stmt, Context $contex 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 @@ public function getCheckedFileName() return $this->checked_file_name; } + /** + * @return string + */ + public function getCheckedFilePath() + { + return $this->checked_file_path; + } + /** * @return string */ @@ -873,6 +904,14 @@ public function getFileName() return $this->file_name; } + /** + * @return string + */ + public function getFilePath() + { + return $this->file_path; + } + /** * @return string|null */ diff --git a/src/Psalm/Checker/TraitChecker.php b/src/Psalm/Checker/TraitChecker.php index 5128dbd0770..eb3ff021dd1 100644 --- a/src/Psalm/Checker/TraitChecker.php +++ b/src/Psalm/Checker/TraitChecker.php @@ -27,6 +27,7 @@ public function __construct(PhpParser\Node\Stmt\ClassLike $class, StatementsSour $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; diff --git a/src/Psalm/Checker/TypeChecker.php b/src/Psalm/Checker/TypeChecker.php index 7cb4e7b8625..130693757da 100644 --- a/src/Psalm/Checker/TypeChecker.php +++ b/src/Psalm/Checker/TypeChecker.php @@ -3,6 +3,7 @@ use PhpParser; use Psalm\Checker\Statements\ExpressionChecker; +use Psalm\CodeLocation; use Psalm\Issue\FailedTypeResolution; use Psalm\IssueBuffer; use Psalm\Type; @@ -1049,16 +1050,14 @@ protected static function hasCallableCheck(PhpParser\Node\Expr\FuncCall $stmt) * * @param array $new_types * @param array $existing_types - * @param string $file_name - * @param int $line_number + * @param CodeLocation $code_location * @param array $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 @@ public static function reconcileKeyedTypes( (string) $new_type_part, $result_type, $key, - $file_name, - $line_number, + $code_location, $suppressed_issues ); @@ -1124,8 +1122,7 @@ public static function reconcileKeyedTypes( * @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 @@ public static function reconcileTypes( $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 @@ public static function reconcileTypes( $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; diff --git a/src/Psalm/CodeLocation.php b/src/Psalm/CodeLocation.php new file mode 100644 index 00000000000..e8aaa0de7bf --- /dev/null +++ b/src/Psalm/CodeLocation.php @@ -0,0 +1,55 @@ +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; + } +} diff --git a/src/Psalm/FunctionDocblockComment.php b/src/Psalm/FunctionDocblockComment.php index 5ae4f97b641..86115028b2d 100644 --- a/src/Psalm/FunctionDocblockComment.php +++ b/src/Psalm/FunctionDocblockComment.php @@ -30,4 +30,7 @@ class FunctionDocblockComment * @var array */ public $suppress = []; + + /** @var int */ + public $return_type_line_number; } diff --git a/src/Psalm/FunctionLikeParameter.php b/src/Psalm/FunctionLikeParameter.php index 6dbe1b8cf0a..80c3070de4f 100644 --- a/src/Psalm/FunctionLikeParameter.php +++ b/src/Psalm/FunctionLikeParameter.php @@ -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 @@ public function __construct( $name, $by_ref, Type\Union $type, + CodeLocation $code_location = null, $is_optional = true, $is_nullable = false, $is_variadic = false @@ -66,5 +68,6 @@ public function __construct( $this->is_optional = $is_optional; $this->is_nullable = $is_nullable; $this->is_variadic = $is_variadic; + $this->code_location = $code_location; } } diff --git a/src/Psalm/Issue/CodeIssue.php b/src/Psalm/Issue/CodeIssue.php index c6e511490af..c1a5598c834 100644 --- a/src/Psalm/Issue/CodeIssue.php +++ b/src/Psalm/Issue/CodeIssue.php @@ -1,19 +1,16 @@ line_number = $line_number; - $this->file_name = $file_name; + $this->code_location = $code_location; $this->message = $message; } @@ -37,7 +32,23 @@ public function __construct($message, $file_name, $line_number) */ 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 @@ public function getLineNumber() */ public function getFileName() { - return $this->file_name; + return $this->code_location->file_name; } /** @@ -53,6 +64,48 @@ public function getFileName() */ 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; } } diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index cdf04d83303..4bafe12fd95 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -54,15 +54,15 @@ public static function add(Issue\CodeIssue $e) $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; - - case Config::REPORT_SUPPRESS: - return false; + if ($reporting_level === 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 @@ public static function add(Issue\CodeIssue $e) } 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) { diff --git a/src/Psalm/Plugin.php b/src/Psalm/Plugin.php index 30bb656e59d..320def4d5df 100644 --- a/src/Psalm/Plugin.php +++ b/src/Psalm/Plugin.php @@ -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 @@ public function checkExpression(PhpParser\Node\Expr $stmt, Context $context, $fi * * @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; } diff --git a/src/Psalm/StatementsSource.php b/src/Psalm/StatementsSource.php index 84afdc2791d..f10b2c28aac 100644 --- a/src/Psalm/StatementsSource.php +++ b/src/Psalm/StatementsSource.php @@ -57,20 +57,36 @@ public function getParentClass(); */ 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(); + /** + * @return string + */ + public function getCheckedFilePath(); + /** * @param string|null $file_name + * @param string|null $file_path */ - public function setIncludeFileName($file_name); + public function setIncludeFileName($file_name, $file_path); /** * @return bool