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

Add TypeDoesNotContainType issue and fix those issues in Psalm code

This commit is contained in:
Matthew Brown 2016-12-11 13:48:11 -05:00
parent 73b1ab1411
commit 562f71b21f
13 changed files with 207 additions and 178 deletions

View File

@ -11,7 +11,7 @@
],
"require": {
"php": ">=5.5",
"nikic/PHP-Parser": ">=3.0.1"
"nikic/PHP-Parser": ">=3.0.2"
},
"bin": ["bin/psalm"],
"autoload": {

14
composer.lock generated
View File

@ -4,21 +4,21 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "11441a18f0cfe84a623e41d06913af49",
"content-hash": "1922cc6635c63bfa5cf5efac8b57e44a",
"hash": "388130e1e8fbae6b9998981593ede881",
"content-hash": "dc7c43d263dbc2c21eb204be11c4b80e",
"packages": [
{
"name": "nikic/php-parser",
"version": "v3.0.1",
"version": "v3.0.2",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "aa6aec90e11a7a4e7d44129c7cb5422ffd15939e"
"reference": "adf44419c0fc014a0f191db6f89d3e55d4211744"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/aa6aec90e11a7a4e7d44129c7cb5422ffd15939e",
"reference": "aa6aec90e11a7a4e7d44129c7cb5422ffd15939e",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/adf44419c0fc014a0f191db6f89d3e55d4211744",
"reference": "adf44419c0fc014a0f191db6f89d3e55d4211744",
"shasum": ""
},
"require": {
@ -56,7 +56,7 @@
"parser",
"php"
],
"time": "2016-12-01 12:37:30"
"time": "2016-12-06 11:30:35"
}
],
"packages-dev": [

View File

@ -229,6 +229,7 @@ class CommentChecker
$line_number++;
}
/** @var int|false */
$last = false;
foreach ($lines as $k => $line) {
if (preg_match('/^\s?@\w/i', $line)) {

View File

@ -7,6 +7,7 @@ use PhpParser\Node\Stmt\Function_;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Checker\Statements\ExpressionChecker;
use Psalm\Checker\TypeChecker;
use Psalm\Context;
use Psalm\EffectsAnalyser;
use Psalm\Exception\DocblockParseException;
@ -1013,134 +1014,6 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
return implode('', $return_type_tokens);
}
/**
* Does the input param type match the given param type
*
* @param Type\Union $input_type
* @param Type\Union $param_type
* @param bool &$has_scalar_match
* @param bool &$type_coerced whether or not there was type coercion involved
* @return bool
*/
public static function doesParamMatch(
Type\Union $input_type,
Type\Union $param_type,
&$has_scalar_match = null,
&$type_coerced = null
) {
$has_scalar_match = true;
if ($param_type->isMixed()) {
return true;
}
$type_match_found = false;
$has_type_mismatch = false;
foreach ($input_type->types as $input_type_part) {
if ($input_type_part->isNull()) {
continue;
}
$type_match_found = false;
$scalar_type_match_found = false;
foreach ($param_type->types as $param_type_part) {
if ($param_type_part->isNull()) {
continue;
}
if ($input_type_part->value === $param_type_part->value ||
ClassChecker::classExtendsOrImplements($input_type_part->value, $param_type_part->value) ||
ExpressionChecker::isMock($input_type_part->value)
) {
$type_match_found = true;
break;
}
if ($input_type_part->value === 'false' && $param_type_part->value === 'bool') {
$type_match_found = true;
}
if ($input_type_part->value === 'int' && $param_type_part->value === 'float') {
$type_match_found = true;
}
if ($input_type_part->value === 'Closure' && $param_type_part->value === 'callable') {
$type_match_found = true;
}
if ($param_type_part->isNumeric() && $input_type_part->isNumericType()) {
$type_match_found = true;
}
if ($param_type_part->isGenericArray() && $input_type_part->isObjectLike()) {
$type_match_found = true;
}
if ($param_type_part->isIterable() &&
(
$input_type_part->isArray() ||
ClassChecker::classExtendsOrImplements($input_type_part->value, 'Traversable')
)
) {
$type_match_found = true;
}
if ($param_type_part->isScalar() && $input_type_part->isScalarType()) {
$type_match_found = true;
}
if ($param_type_part->isString() && $input_type_part->isObjectType()) {
// check whether the object has a __toString method
if (MethodChecker::methodExists($input_type_part->value . '::__toString')) {
$type_match_found = true;
}
}
if ($param_type_part->isCallable() &&
($input_type_part->value === 'string' || $input_type_part->value === 'array')
) {
// @todo add value checks if possible here
$type_match_found = true;
}
if ($input_type_part->isNumeric()) {
if ($param_type_part->isNumericType()) {
$scalar_type_match_found = true;
}
}
if ($input_type_part->isScalarType() || $input_type_part->isScalar()) {
if ($param_type_part->isScalarType()) {
$scalar_type_match_found = true;
}
} elseif ($param_type_part->isObject() &&
!$input_type_part->isArray() &&
!$input_type_part->isResource()
) {
$type_match_found = true;
}
if (ClassChecker::classExtendsOrImplements($param_type_part->value, $input_type_part->value)) {
$type_coerced = true;
$type_match_found = true;
break;
}
}
if (!$type_match_found) {
if (!$scalar_type_match_found) {
$has_scalar_match = false;
}
return false;
}
}
return true;
}
/**
* @param string $method_id
* @param array<PhpParser\Node\Arg> $args
@ -1217,7 +1090,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
continue;
}
if (FunctionLikeChecker::doesParamMatch($arg->value->inferredType, $param_type)) {
if (TypeChecker::isContainedBy($arg->value->inferredType, $param_type)) {
continue;
}

View File

@ -8,9 +8,9 @@ class ScopeChecker
/**
* Do all code paths in this list of statements exit the block (return/throw)
*
* @param array<PhpParser\Node\Stmt> $stmts
* @param bool $check_continue - also looks for a continue
* @param bool $check_break
* @param array<PhpParser\Node\Stmt|PhpParser\Node\Expr> $stmts
* @param bool $check_continue - also looks for a continue
* @param bool $check_break
* @return bool
*/
public static function doesLeaveBlock(array $stmts, $check_continue = true, $check_break = true)

View File

@ -175,10 +175,6 @@ class ForeachChecker
$statements_checker->registerVariable('$' . $stmt->keyVar->name, $stmt->getLine());
}
if ($value_type && $value_type instanceof Type\Atomic) {
$value_type = new Type\Union([$value_type]);
}
AssignmentChecker::check(
$statements_checker,
$stmt->valueVar,

View File

@ -272,7 +272,7 @@ class IfChecker
// if we have a check like if (!isset($a)) { $a = true; } we want to make sure $a is always set
foreach ($if_scope->new_vars as $var_id => $type) {
if (isset($if_scope->negated_types[$var_id]) && $if_scope->negated_types[$var_id] === '!null') {
if (isset($if_scope->negated_types[$var_id]) && $if_scope->negated_types[$var_id] === '!isset') {
$if_scope->forced_new_vars[$var_id] = Type::getMixed();
}
}

View File

@ -10,6 +10,7 @@ use Psalm\Checker\MethodChecker;
use Psalm\Checker\StatementsChecker;
use Psalm\Checker\Statements\ExpressionChecker;
use Psalm\Checker\TraitChecker;
use Psalm\Checker\TypeChecker;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Issue\ForbiddenCode;
@ -1013,9 +1014,10 @@ class CallChecker
continue;
}
$type_match_found = FunctionLikeChecker::doesParamMatch(
$type_match_found = TypeChecker::isContainedBy(
$input_type,
$closure_param_type,
false,
$scalar_type_match_found,
$coerced_type
);
@ -1150,9 +1152,10 @@ class CallChecker
}
}
$type_match_found = FunctionLikeChecker::doesParamMatch(
$type_match_found = TypeChecker::isContainedBy(
$input_type,
$param_type,
true,
$scalar_type_match_found,
$coerced_type
);

View File

@ -5,6 +5,7 @@ use PhpParser;
use Psalm\Checker\Statements\ExpressionChecker;
use Psalm\CodeLocation;
use Psalm\Issue\FailedTypeResolution;
use Psalm\Issue\TypeDoesNotContainType;
use Psalm\IssueBuffer;
use Psalm\Type;
@ -348,7 +349,7 @@ class TypeChecker
}
if ($var_name) {
if ($conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
if ($conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical) {
$if_types[$var_name] = 'false';
} else {
// we do this because == null gives us a weaker idea than === null
@ -386,7 +387,7 @@ class TypeChecker
);
if ($var_name) {
$if_types[$var_name] = 'null';
$if_types[$var_name] = 'isset';
}
}
}
@ -598,7 +599,7 @@ class TypeChecker
}
if ($var_name) {
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical) {
$if_types[$var_name] = '!false';
} else {
$if_types[$var_name] = '!empty';
@ -654,7 +655,7 @@ class TypeChecker
);
if ($var_name) {
$if_types[$var_name] = '!null';
$if_types[$var_name] = '!isset';
}
}
}
@ -1162,10 +1163,6 @@ class TypeChecker
return $existing_var_type;
}
if ($new_var_type === 'null') {
return Type::getNull();
}
if ($new_var_type[0] === '!') {
if ($new_var_type === '!object' && !$existing_var_type->isMixed()) {
$non_object_types = [];
@ -1181,7 +1178,7 @@ class TypeChecker
}
}
if (in_array($new_var_type, ['!empty', '!null'])) {
if (in_array($new_var_type, ['!empty', '!null', '!isset'])) {
$existing_var_type->removeType('null');
if ($new_var_type === '!empty') {
@ -1246,7 +1243,159 @@ class TypeChecker
}
}
return Type::parseString($new_var_type);
if ($new_var_type === 'isset') {
return Type::getNull();
}
$new_type = Type::parseString($new_var_type);
if ($existing_var_type->isMixed()) {
return $new_type;
}
if (!TypeChecker::isContainedBy($new_type, $existing_var_type) && $code_location) {
if (IssueBuffer::accepts(
new TypeDoesNotContainType(
'Cannot resolve types for ' . $key . ' - ' . $existing_var_type . ' does not contain ' . $new_type,
$code_location
),
$suppressed_issues
)) {
// fall through
}
}
return $new_type;
}
/**
* Does the input param type match the given param type
*
* @param Type\Union $input_type
* @param Type\Union $container_type
* @param bool $ignore_null
* @param bool &$has_scalar_match
* @param bool &$type_coerced whether or not there was type coercion involved
* @return bool
*/
public static function isContainedBy(
Type\Union $input_type,
Type\Union $container_type,
$ignore_null = false,
&$has_scalar_match = null,
&$type_coerced = null
) {
$has_scalar_match = true;
if ($container_type->isMixed()) {
return true;
}
$type_match_found = false;
$has_type_mismatch = false;
foreach ($input_type->types as $input_type_part) {
if ($input_type_part->isNull() && $ignore_null) {
continue;
}
$type_match_found = false;
$scalar_type_match_found = false;
foreach ($container_type->types as $container_type_part) {
if ($container_type_part->isNull() && $ignore_null) {
continue;
}
if ($input_type_part->value === $container_type_part->value ||
ClassChecker::classExtendsOrImplements($input_type_part->value, $container_type_part->value) ||
ExpressionChecker::isMock($input_type_part->value)
) {
$type_match_found = true;
break;
}
if ($input_type_part->value === 'false' && $container_type_part->value === 'bool') {
$type_match_found = true;
}
if ($input_type_part->value === 'int' && $container_type_part->value === 'float') {
$type_match_found = true;
}
if ($input_type_part->value === 'Closure' && $container_type_part->value === 'callable') {
$type_match_found = true;
}
if ($container_type_part->isNumeric() && $input_type_part->isNumericType()) {
$type_match_found = true;
}
if ($container_type_part->isGenericArray() && $input_type_part->isObjectLike()) {
$type_match_found = true;
}
if ($container_type_part->isIterable() &&
(
$input_type_part->isArray() ||
ClassChecker::classExtendsOrImplements($input_type_part->value, 'Traversable')
)
) {
$type_match_found = true;
}
if ($container_type_part->isScalar() && $input_type_part->isScalarType()) {
$type_match_found = true;
}
if ($container_type_part->isString() && $input_type_part->isObjectType()) {
// check whether the object has a __toString method
if (MethodChecker::methodExists($input_type_part->value . '::__toString')) {
$type_match_found = true;
}
}
if ($container_type_part->isCallable() &&
($input_type_part->value === 'string' || $input_type_part->value === 'array')
) {
// @todo add value checks if possible here
$type_match_found = true;
}
if ($input_type_part->isNumeric()) {
if ($container_type_part->isNumericType()) {
$scalar_type_match_found = true;
}
}
if ($input_type_part->isScalarType() || $input_type_part->isScalar()) {
if ($container_type_part->isScalarType()) {
$scalar_type_match_found = true;
}
} elseif ($container_type_part->isObject() &&
!$input_type_part->isArray() &&
!$input_type_part->isResource()
) {
$type_match_found = true;
}
if (ClassChecker::classExtendsOrImplements($container_type_part->value, $input_type_part->value)) {
$type_coerced = true;
$type_match_found = true;
break;
}
}
if (!$type_match_found) {
if (!$scalar_type_match_found) {
$has_scalar_match = false;
}
return false;
}
}
return true;
}
/**
@ -1273,6 +1422,7 @@ class TypeChecker
$new_base_key = $base_key . '->' . $key_parts[$i];
if (!isset($existing_keys[$new_base_key])) {
/** @var null|Type\Union */
$new_base_type = null;
foreach ($existing_keys[$base_key]->types as $existing_key_type_part) {

View File

@ -12,9 +12,9 @@ class EffectsAnalyser
/**
* Gets the return types from a list of statements
*
* @param array<int,PhpParser\Node\Stmt> $stmts
* @param array<int,Type\Atomic> $yield_types
* @param bool $collapse_types
* @param array<int,PhpParser\Node\Stmt|PhpParser\Node\Expr> $stmts
* @param array<int,Type\Atomic> $yield_types
* @param bool $collapse_types
* @return array<int,Type\Atomic> a list of return types
*/
public static function getReturnTypes(array $stmts, array &$yield_types, $collapse_types = false)
@ -92,7 +92,7 @@ class EffectsAnalyser
/** @var Type\Union */
$key_type = null;
/** @var Type\Union */
/** @var Type\Union|null */
$value_type = null;
foreach ($yield_types as $type) {

View File

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

View File

@ -64,7 +64,7 @@ class PropertyTypeTest extends PHPUnit_Framework_TestCase
public $foo;
}
$a = null;
$a = rand(0, 10) ? new A() : (rand(0, 10) ? new B() : null);
$b = null;
if ($a instanceof A || $a instanceof B) {
@ -90,7 +90,7 @@ class PropertyTypeTest extends PHPUnit_Framework_TestCase
public $foo;
}
$a = null;
$a = rand(0, 10) ? new A() : new B();
$b = null;
if (rand(0, 10) === 4) {

View File

@ -88,16 +88,6 @@ class TypeReconciliationTest extends PHPUnit_Framework_TestCase
(string) TypeChecker::reconcileTypes('null', Type::parseString('MyObject|null'))
);
$this->assertEquals(
'null',
(string) TypeChecker::reconcileTypes('null', Type::parseString('MyObject'))
);
$this->assertEquals(
'null',
(string) TypeChecker::reconcileTypes('null', Type::parseString('MyObject|false'))
);
$this->assertEquals(
'null',
(string) TypeChecker::reconcileTypes('null', Type::parseString('mixed'))
@ -161,12 +151,22 @@ class TypeReconciliationTest extends PHPUnit_Framework_TestCase
);
}
public function testAllMixed()
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage TypeDoesNotContainType
*/
public function testMakeNonNullableNull()
{
$this->assertEquals(
'mixed',
(string) TypeChecker::reconcileTypes('mixed', Type::parseString('mixed'))
);
$stmts = self::$parser->parse('<?php
class A { }
$a = new A();
if ($a === null) {
}
');
$file_checker = new FileChecker('somefile.php', $stmts);
$context = new Context('somefile.php');
$file_checker->check(true, true, $context);
}
public function testNotInstanceOf()