mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Add TypeDoesNotContainType issue and fix those issues in Psalm code
This commit is contained in:
parent
73b1ab1411
commit
562f71b21f
@ -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
14
composer.lock
generated
@ -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": [
|
||||
|
@ -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)) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
6
src/Psalm/Issue/TypeDoesNotContainType.php
Normal file
6
src/Psalm/Issue/TypeDoesNotContainType.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class TypeDoesNotContainType extends CodeError
|
||||
{
|
||||
}
|
@ -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) {
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user