1
0
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:
Matthew Brown 2016-11-05 17:53:30 -04:00
parent c5fb513318
commit c5591adf10
13 changed files with 155 additions and 25 deletions

View File

@ -13,6 +13,7 @@
<MixedMethodCall errorLevel="suppress" />
<MixedPropertyFetch errorLevel="suppress" />
<MixedPropertyAssignment errorLevel="suppress" />
<MixedInferredReturnType errorLevel="suppress" />
<InvalidDocblock errorLevel="info" />
<DeprecatedMethod errorLevel="info" />

View File

@ -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 = [];

View File

@ -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 = [];

View File

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

View File

@ -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 = [];

View File

@ -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,

View File

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

View File

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

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Issue;
class InvalidGlobal extends CodeError
{
}

View File

@ -0,0 +1,6 @@
<?php
namespace Psalm\Issue;
class MixedInferredReturnType extends CodeError
{
}

View File

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

View File

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

View File

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