mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Add support for global in functions and mixed inferred return errors
This commit is contained in:
parent
c5fb513318
commit
c5591adf10
@ -13,6 +13,7 @@
|
||||
<MixedMethodCall errorLevel="suppress" />
|
||||
<MixedPropertyFetch errorLevel="suppress" />
|
||||
<MixedPropertyAssignment errorLevel="suppress" />
|
||||
<MixedInferredReturnType errorLevel="suppress" />
|
||||
|
||||
<InvalidDocblock errorLevel="info" />
|
||||
<DeprecatedMethod errorLevel="info" />
|
||||
|
@ -14,21 +14,21 @@ class ClassChecker extends ClassLikeChecker
|
||||
/**
|
||||
* A lookup table of existing classes
|
||||
*
|
||||
* @var array
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
protected static $existing_classes = [];
|
||||
|
||||
/**
|
||||
* A lookup table of existing classes, all lowercased
|
||||
*
|
||||
* @var array
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
protected static $existing_classes_ci = [];
|
||||
|
||||
/**
|
||||
* A lookup table used for caching the results of classExtends calls
|
||||
*
|
||||
* @var array
|
||||
* @var array<string, array<string, bool>>
|
||||
*/
|
||||
protected static $class_extends = [];
|
||||
|
||||
|
@ -806,9 +806,9 @@ abstract class ClassLikeChecker implements StatementsSource
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $class
|
||||
* @param string $namespace
|
||||
* @param array $imported_namespaces
|
||||
* @param string $class
|
||||
* @param string $namespace
|
||||
* @param array<string, string> $imported_namespaces
|
||||
* @return string
|
||||
*/
|
||||
public static function getAbsoluteClassFromString($class, $namespace, array $imported_namespaces)
|
||||
@ -1311,7 +1311,7 @@ abstract class ClassLikeChecker implements StatementsSource
|
||||
/**
|
||||
* Gets the method/function call map
|
||||
*
|
||||
* @return array<string,array<string,string>>
|
||||
* @return array<string, array<string, string>>
|
||||
*/
|
||||
protected static function getPropertyMap()
|
||||
{
|
||||
@ -1319,6 +1319,7 @@ abstract class ClassLikeChecker implements StatementsSource
|
||||
return self::$property_map;
|
||||
}
|
||||
|
||||
/** @var array<string, array<string, string>> */
|
||||
$property_map = require_once(__DIR__.'/../PropertyMap.php');
|
||||
|
||||
self::$property_map = [];
|
||||
|
@ -225,7 +225,7 @@ class FileChecker implements StatementsSource
|
||||
$this->declared_classes = array_merge($namespace_checker->getDeclaredClasses());
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Function_ && $check_functions) {
|
||||
$function_context = new Context($this->short_file_name, $file_context->self);
|
||||
$function_checkers[$stmt->name]->check($function_context);
|
||||
$function_checkers[$stmt->name]->check($function_context, $file_context);
|
||||
|
||||
if (!$config->excludeIssueInFile('InvalidReturnType', $this->short_file_name)) {
|
||||
$function_checkers[$stmt->name]->checkReturnTypes();
|
||||
@ -332,13 +332,14 @@ class FileChecker implements StatementsSource
|
||||
$cache_location = $cache_directory . '/' . $key;
|
||||
|
||||
if (is_readable($cache_location)) {
|
||||
/** @var array<int, \PhpParser\Node> */
|
||||
$stmts = unserialize((string) file_get_contents($cache_location));
|
||||
$from_cache = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$stmts && $contents) {
|
||||
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
|
||||
$parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7);
|
||||
|
||||
$stmts = $parser->parse($contents);
|
||||
}
|
||||
|
@ -645,7 +645,7 @@ class FunctionChecker extends FunctionLikeChecker
|
||||
/**
|
||||
* Gets the method/function call map
|
||||
*
|
||||
* @return array<array<string,string>>
|
||||
* @return array<array<string, string>>
|
||||
*/
|
||||
protected static function getCallMap()
|
||||
{
|
||||
@ -653,6 +653,7 @@ class FunctionChecker extends FunctionLikeChecker
|
||||
return self::$call_map;
|
||||
}
|
||||
|
||||
/** @var array<array<string, string>> */
|
||||
$call_map = require_once(__DIR__.'/../CallMap.php');
|
||||
|
||||
self::$call_map = [];
|
||||
|
@ -12,6 +12,7 @@ use Psalm\FunctionLikeParameter;
|
||||
use Psalm\Issue\InvalidDocblock;
|
||||
use Psalm\Issue\InvalidReturnType;
|
||||
use Psalm\Issue\MethodSignatureMismatch;
|
||||
use Psalm\Issue\MixedInferredReturnType;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\StatementsSource;
|
||||
use Psalm\Type;
|
||||
@ -106,10 +107,11 @@ abstract class FunctionLikeChecker implements StatementsSource
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Context $context
|
||||
* @param Context $context
|
||||
* @param Context|null $global_context
|
||||
* @return false|null
|
||||
*/
|
||||
public function check(Context $context)
|
||||
public function check(Context $context, Context $global_context = null)
|
||||
{
|
||||
if ($function_stmts = $this->function->getStmts()) {
|
||||
$statements_checker = new StatementsChecker($this);
|
||||
@ -242,7 +244,7 @@ abstract class FunctionLikeChecker implements StatementsSource
|
||||
$statements_checker->registerVariable($function_param->name, $function_param->line);
|
||||
}
|
||||
|
||||
$statements_checker->check($function_stmts, $context);
|
||||
$statements_checker->check($function_stmts, $context, null, $global_context);
|
||||
|
||||
if (isset($this->return_vars_in_scope[''])) {
|
||||
$context->vars_in_scope = TypeChecker::combineKeyedTypes(
|
||||
@ -514,11 +516,27 @@ abstract class FunctionLikeChecker implements StatementsSource
|
||||
$inferred_return_type = $inferred_yield_type;
|
||||
}
|
||||
|
||||
if ($inferred_return_type && !$inferred_return_type->isMixed() && !$declared_return_type->isMixed()) {
|
||||
if ($inferred_return_type && !$declared_return_type->isMixed()) {
|
||||
if ($inferred_return_type->isNull() && $declared_return_type->isVoid()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($inferred_return_type->isMixed()) {
|
||||
if (IssueBuffer::accepts(
|
||||
new MixedInferredReturnType(
|
||||
'Could not verify return type \'' . $declared_return_type . '\' for ' .
|
||||
MethodChecker::getCasedMethodId($method_id),
|
||||
$this->getCheckedFileName(),
|
||||
$this->function->getLine()
|
||||
),
|
||||
$this->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!TypeChecker::hasIdenticalTypes(
|
||||
$declared_return_type,
|
||||
$inferred_return_type,
|
||||
|
@ -95,10 +95,12 @@ class ExpressionChecker
|
||||
if (self::check($statements_checker, $stmt->expr, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
$stmt->inferredType = $stmt->expr->inferredType;
|
||||
} elseif ($stmt instanceof PhpParser\Node\Expr\UnaryPlus) {
|
||||
if (self::check($statements_checker, $stmt->expr, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
$stmt->inferredType = $stmt->expr->inferredType;
|
||||
} elseif ($stmt instanceof PhpParser\Node\Expr\Isset_) {
|
||||
foreach ($stmt->vars as $isset_var) {
|
||||
if ($isset_var instanceof PhpParser\Node\Expr\PropertyFetch &&
|
||||
@ -111,6 +113,7 @@ class ExpressionChecker
|
||||
$context->vars_possibly_in_scope[$var_id] = true;
|
||||
}
|
||||
}
|
||||
$stmt->inferredType = Type::getBool();
|
||||
} elseif ($stmt instanceof PhpParser\Node\Expr\ClassConstFetch) {
|
||||
if (FetchChecker::checkClassConstFetch($statements_checker, $stmt, $context) === false) {
|
||||
return false;
|
||||
@ -1171,6 +1174,7 @@ class ExpressionChecker
|
||||
PhpParser\Node\Expr\BooleanNot $stmt,
|
||||
Context $context
|
||||
) {
|
||||
$stmt->inferredType = Type::getBool();
|
||||
return self::check($statements_checker, $stmt->expr, $context);
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ use Psalm\Checker\Statements\Expression\AssignmentChecker;
|
||||
use Psalm\Config;
|
||||
use Psalm\Context;
|
||||
use Psalm\Issue\ContinueOutsideLoop;
|
||||
use Psalm\Issue\InvalidGlobal;
|
||||
use Psalm\Issue\InvalidNamespace;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\StatementsSource;
|
||||
@ -119,9 +120,10 @@ class StatementsChecker
|
||||
* @param array<PhpParser\Node> $stmts
|
||||
* @param Context $context
|
||||
* @param Context|null $loop_context
|
||||
* @param Context|null $global_context
|
||||
* @return null|false
|
||||
*/
|
||||
public function check(array $stmts, Context $context, Context $loop_context = null)
|
||||
public function check(array $stmts, Context $context, Context $loop_context = null, Context $global_context = null)
|
||||
{
|
||||
$has_returned = false;
|
||||
|
||||
@ -155,8 +157,8 @@ class StatementsChecker
|
||||
}
|
||||
|
||||
/*
|
||||
if (isset($context->vars_in_scope['$first_arg'])) {
|
||||
var_dump($stmt->getLine() . ' ' . $context->vars_in_scope['$first_arg']);
|
||||
if (isset($context->vars_in_scope['$pos'])) {
|
||||
var_dump($stmt->getLine() . ' ' . $context->vars_in_scope['$pos']);
|
||||
}
|
||||
*/
|
||||
|
||||
@ -230,13 +232,32 @@ class StatementsChecker
|
||||
$this->aliased_classes[strtolower($use->alias)] = implode('\\', $use->name->parts);
|
||||
}
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Global_) {
|
||||
foreach ($stmt->vars as $var) {
|
||||
if ($var instanceof PhpParser\Node\Expr\Variable) {
|
||||
if (is_string($var->name)) {
|
||||
$context->vars_in_scope['$' . $var->name] = Type::getMixed();
|
||||
$context->vars_possibly_in_scope['$' . $var->name] = true;
|
||||
} else {
|
||||
ExpressionChecker::check($this, $var, $context);
|
||||
if (!$global_context) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidGlobal(
|
||||
'Cannot use global scope here',
|
||||
$this->checked_file_name,
|
||||
$stmt->getLine()
|
||||
),
|
||||
$this->suppressed_issues
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
foreach ($stmt->vars as $var) {
|
||||
if ($var instanceof PhpParser\Node\Expr\Variable) {
|
||||
if (is_string($var->name)) {
|
||||
$var_id = '$' . $var->name;
|
||||
|
||||
$context->vars_in_scope[$var_id] = isset($global_context->vars_in_scope[$var_id])
|
||||
? clone $global_context->vars_in_scope[$var_id]
|
||||
: Type::getMixed();
|
||||
|
||||
$context->vars_possibly_in_scope[$var_id] = true;
|
||||
} else {
|
||||
ExpressionChecker::check($this, $var, $context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -729,7 +750,11 @@ class StatementsChecker
|
||||
$const_name = implode('', $stmt->name->parts);
|
||||
|
||||
if (defined($const_name)) {
|
||||
return constant($const_name);
|
||||
$constant_value = constant($const_name);
|
||||
|
||||
if (is_string($constant_value)) {
|
||||
return $constant_value;
|
||||
}
|
||||
}
|
||||
} elseif ($stmt instanceof PhpParser\Node\Scalar\MagicConst\Dir) {
|
||||
return dirname($file_name);
|
||||
|
6
src/Psalm/Issue/InvalidGlobal.php
Normal file
6
src/Psalm/Issue/InvalidGlobal.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class InvalidGlobal extends CodeError
|
||||
{
|
||||
}
|
6
src/Psalm/Issue/MixedInferredReturnType.php
Normal file
6
src/Psalm/Issue/MixedInferredReturnType.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class MixedInferredReturnType extends CodeError
|
||||
{
|
||||
}
|
@ -376,4 +376,24 @@ class ArrayAssignmentTest extends PHPUnit_Framework_TestCase
|
||||
$context = new Context('somefile.php');
|
||||
$file_checker->check(true, true, $context);
|
||||
}
|
||||
|
||||
public function testArrayKey()
|
||||
{
|
||||
$file_checker = new \Psalm\Checker\FileChecker(
|
||||
'somefile.php',
|
||||
self::$parser->parse('<?php
|
||||
$a = ["foo", "bar"];
|
||||
$b = $a[0];
|
||||
|
||||
$c = ["a" => "foo", "b"=> "bar"];
|
||||
$d = "a";
|
||||
$e = $a[$d];
|
||||
')
|
||||
);
|
||||
|
||||
$context = new Context('somefile.php');
|
||||
$file_checker->check(true, true, $context);
|
||||
$this->assertEquals('string', (string)$context->vars_in_scope['$b']);
|
||||
$this->assertEquals('string', (string)$context->vars_in_scope['$e']);
|
||||
}
|
||||
}
|
||||
|
@ -391,4 +391,21 @@ class ReturnTypeTest extends PHPUnit_Framework_TestCase
|
||||
|
||||
$this->assertEquals('array<int,B>', (string) $context->vars_in_scope['$bees']);
|
||||
}
|
||||
|
||||
public function testIssetReturnType()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
/**
|
||||
* @param mixed $foo
|
||||
* @return bool
|
||||
*/
|
||||
function a($foo = null) {
|
||||
return isset($foo);
|
||||
}
|
||||
');
|
||||
|
||||
$file_checker = new FileChecker('somefile.php', $stmts);
|
||||
$context = new Context('somefile.php');
|
||||
$file_checker->check(true, true, $context);
|
||||
}
|
||||
}
|
||||
|
@ -445,4 +445,34 @@ class ScopeTest extends PHPUnit_Framework_TestCase
|
||||
$file_checker = new FileChecker('somefile.php', $stmts);
|
||||
$file_checker->check();
|
||||
}
|
||||
|
||||
public function testGlobalReturn()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
$foo = "foo";
|
||||
|
||||
function a() : string {
|
||||
global $foo;
|
||||
|
||||
return $foo;
|
||||
}
|
||||
');
|
||||
|
||||
$file_checker = new FileChecker('somefile.php', $stmts);
|
||||
$file_checker->check();
|
||||
}
|
||||
|
||||
public function testStatic()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
function a() : string {
|
||||
static $foo = "foo";
|
||||
|
||||
return $foo;
|
||||
}
|
||||
');
|
||||
|
||||
$file_checker = new FileChecker('somefile.php', $stmts);
|
||||
$file_checker->check();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user