mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Warn when doing === comparisons on incompatible types
This commit is contained in:
parent
d65221fbe4
commit
14bb967c7d
@ -72,9 +72,7 @@ class WhileChecker
|
||||
|
||||
$asserted_while_vars = array_keys(AlgebraChecker::getTruthsFromFormula($while_clauses));
|
||||
|
||||
if ($statements_checker->analyzeLoop($stmt->stmts, $asserted_while_vars, $while_context, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
$statements_checker->analyzeLoop($stmt->stmts, $asserted_while_vars, $while_context, $context);
|
||||
|
||||
foreach ($context->vars_in_scope as $var => $type) {
|
||||
if ($type->isMixed()) {
|
||||
|
@ -7,7 +7,6 @@ use Psalm\Checker\Statements\ExpressionChecker;
|
||||
use Psalm\Checker\TypeChecker;
|
||||
use Psalm\Clause;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Issue\FailedTypeResolution;
|
||||
use Psalm\Issue\TypeDoesNotContainType;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\StatementsSource;
|
||||
@ -95,6 +94,7 @@ class AssertionFinder
|
||||
$typed_value_position = self::hasTypedValueComparison($conditional);
|
||||
|
||||
$var_name = null;
|
||||
$var_type = null;
|
||||
|
||||
if ($null_position !== null) {
|
||||
if ($null_position === self::ASSIGNMENT_TO_RIGHT) {
|
||||
@ -103,14 +103,16 @@ class AssertionFinder
|
||||
$this_class_name,
|
||||
$source
|
||||
);
|
||||
|
||||
$var_type = isset($conditional->left->inferredType) ? $conditional->left->inferredType : null;
|
||||
} elseif ($null_position === self::ASSIGNMENT_TO_LEFT) {
|
||||
$var_name = ExpressionChecker::getArrayVarId(
|
||||
$conditional->right,
|
||||
$this_class_name,
|
||||
$source
|
||||
);
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Bad null variable position');
|
||||
|
||||
$var_type = isset($conditional->right->inferredType) ? $conditional->right->inferredType : null;
|
||||
}
|
||||
|
||||
if ($var_name) {
|
||||
@ -119,6 +121,28 @@ class AssertionFinder
|
||||
} else {
|
||||
$if_types[$var_name] = 'empty';
|
||||
}
|
||||
} elseif ($var_type && $conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
|
||||
$null_type = Type::getFalse();
|
||||
|
||||
if (!TypeChecker::isContainedBy(
|
||||
$var_type,
|
||||
$null_type,
|
||||
$source->getFileChecker()
|
||||
) && !TypeChecker::isContainedBy(
|
||||
$null_type,
|
||||
$var_type,
|
||||
$source->getFileChecker()
|
||||
)) {
|
||||
if (IssueBuffer::accepts(
|
||||
new TypeDoesNotContainType(
|
||||
$var_type . ' does not contain ' . $null_type,
|
||||
new CodeLocation($source, $conditional)
|
||||
),
|
||||
$source->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $if_types;
|
||||
@ -140,6 +164,8 @@ class AssertionFinder
|
||||
$this_class_name,
|
||||
$source
|
||||
);
|
||||
|
||||
$var_type = isset($conditional->left->inferredType) ? $conditional->left->inferredType : null;
|
||||
}
|
||||
} elseif ($false_position === self::ASSIGNMENT_TO_LEFT) {
|
||||
if ($conditional->right instanceof PhpParser\Node\Expr\FuncCall) {
|
||||
@ -156,9 +182,9 @@ class AssertionFinder
|
||||
$this_class_name,
|
||||
$source
|
||||
);
|
||||
|
||||
$var_type = isset($conditional->right->inferredType) ? $conditional->right->inferredType : null;
|
||||
}
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Bad null variable position');
|
||||
}
|
||||
|
||||
if ($var_name) {
|
||||
@ -167,6 +193,28 @@ class AssertionFinder
|
||||
} else {
|
||||
$if_types[$var_name] = 'empty';
|
||||
}
|
||||
} elseif ($var_type && $conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
|
||||
$false_type = Type::getFalse();
|
||||
|
||||
if (!TypeChecker::isContainedBy(
|
||||
$var_type,
|
||||
$false_type,
|
||||
$source->getFileChecker()
|
||||
) && !TypeChecker::isContainedBy(
|
||||
$false_type,
|
||||
$var_type,
|
||||
$source->getFileChecker()
|
||||
)) {
|
||||
if (IssueBuffer::accepts(
|
||||
new TypeDoesNotContainType(
|
||||
$var_type . ' does not contain ' . $false_type,
|
||||
new CodeLocation($source, $conditional)
|
||||
),
|
||||
$source->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $if_types;
|
||||
@ -206,6 +254,7 @@ class AssertionFinder
|
||||
|
||||
if ($typed_value_position) {
|
||||
$var_type = null;
|
||||
$other_type = null;
|
||||
|
||||
if ($typed_value_position === self::ASSIGNMENT_TO_RIGHT) {
|
||||
/** @var PhpParser\Node\Expr $conditional->right */
|
||||
@ -215,7 +264,8 @@ class AssertionFinder
|
||||
$source
|
||||
);
|
||||
|
||||
$var_type = '^' . $conditional->right->inferredType;
|
||||
$var_type = $conditional->right->inferredType;
|
||||
$other_type = $conditional->left->inferredType;
|
||||
} elseif ($typed_value_position === self::ASSIGNMENT_TO_LEFT) {
|
||||
/** @var PhpParser\Node\Expr $conditional->left */
|
||||
$var_name = ExpressionChecker::getArrayVarId(
|
||||
@ -224,16 +274,64 @@ class AssertionFinder
|
||||
$source
|
||||
);
|
||||
|
||||
$var_type = '^' . $conditional->left->inferredType;
|
||||
$var_type = $conditional->left->inferredType;
|
||||
$other_type = $conditional->right->inferredType;
|
||||
}
|
||||
|
||||
if ($var_name && $var_type) {
|
||||
$if_types[$var_name] = $var_type;
|
||||
if ($var_type) {
|
||||
if ($var_name) {
|
||||
$if_types[$var_name] = '^' . $var_type;
|
||||
} elseif ($other_type && $conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
|
||||
if (!TypeChecker::isContainedBy(
|
||||
$var_type,
|
||||
$other_type,
|
||||
$source->getFileChecker()
|
||||
) && !TypeChecker::isContainedBy(
|
||||
$other_type,
|
||||
$var_type,
|
||||
$source->getFileChecker()
|
||||
)) {
|
||||
if (IssueBuffer::accepts(
|
||||
new TypeDoesNotContainType(
|
||||
$var_type . ' does not contain ' . $other_type,
|
||||
new CodeLocation($source, $conditional)
|
||||
),
|
||||
$source->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $if_types;
|
||||
}
|
||||
|
||||
$var_type = isset($conditional->left->inferredType) ? $conditional->left->inferredType : null;
|
||||
$other_type = isset($conditional->right->inferredType) ? $conditional->right->inferredType : null;
|
||||
|
||||
if ($var_type && $other_type && $conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
|
||||
if (!TypeChecker::isContainedBy(
|
||||
$var_type,
|
||||
$other_type,
|
||||
$source->getFileChecker()
|
||||
) && !TypeChecker::isContainedBy(
|
||||
$other_type,
|
||||
$var_type,
|
||||
$source->getFileChecker()
|
||||
)) {
|
||||
if (IssueBuffer::accepts(
|
||||
new TypeDoesNotContainType(
|
||||
$var_type . ' does not contain ' . $other_type,
|
||||
new CodeLocation($source, $conditional)
|
||||
),
|
||||
$source->getSuppressedIssues()
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
@ -886,7 +886,7 @@ class CallChecker
|
||||
* @param FunctionLikeChecker $source
|
||||
* @param string $method_name
|
||||
* @param Context $context
|
||||
* @return false|null
|
||||
* @return void
|
||||
*/
|
||||
public static function collectSpecialInformation(
|
||||
FunctionLikeChecker $source,
|
||||
@ -909,9 +909,7 @@ class CallChecker
|
||||
|
||||
$method_id = $fq_class_name . '::' . strtolower($method_name);
|
||||
|
||||
if ($file_checker->project_checker->getMethodMutations($method_id, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
$file_checker->project_checker->getMethodMutations($method_id, $context);
|
||||
} elseif ($context->collect_initializations &&
|
||||
$context->self &&
|
||||
(
|
||||
@ -949,9 +947,7 @@ class CallChecker
|
||||
}
|
||||
}
|
||||
|
||||
if ($class_checker->getMethodMutations(strtolower($method_name), $context) === false) {
|
||||
return false;
|
||||
}
|
||||
$class_checker->getMethodMutations(strtolower($method_name), $context);
|
||||
|
||||
foreach ($local_vars_in_scope as $var => $type) {
|
||||
$context->vars_in_scope[$var] = $type;
|
||||
@ -1016,9 +1012,7 @@ class CallChecker
|
||||
if ($context->collect_mutations) {
|
||||
$method_id = $fq_class_name . '::' . strtolower($stmt->name);
|
||||
|
||||
if ($file_checker->project_checker->getMethodMutations($method_id, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
$file_checker->project_checker->getMethodMutations($method_id, $context);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -545,9 +545,7 @@ class StatementsChecker extends SourceChecker implements StatementsSource
|
||||
{
|
||||
$do_context = clone $context;
|
||||
|
||||
if ($this->analyzeLoop($stmt->stmts, [], $do_context, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
$this->analyzeLoop($stmt->stmts, [], $do_context, $context);
|
||||
|
||||
foreach ($context->vars_in_scope as $var => $type) {
|
||||
if ($type->isMixed()) {
|
||||
|
@ -405,6 +405,66 @@ class TypeReconciliationTest extends PHPUnit_Framework_TestCase
|
||||
$file_checker->visitAndAnalyzeMethods($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Psalm\Exception\CodeException
|
||||
* @expectedExceptionMessage TypeDoesNotContainType
|
||||
* @return void
|
||||
*/
|
||||
public function testFunctionValueIsNotType()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
if (json_last_error() === "5") { }
|
||||
');
|
||||
|
||||
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
|
||||
$file_checker->visitAndAnalyzeMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Psalm\Exception\CodeException
|
||||
* @expectedExceptionMessage TypeDoesNotContainType
|
||||
* @return void
|
||||
*/
|
||||
public function testStringIsNotInt()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
if (5 === "5") { }
|
||||
');
|
||||
|
||||
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
|
||||
$file_checker->visitAndAnalyzeMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Psalm\Exception\CodeException
|
||||
* @expectedExceptionMessage TypeDoesNotContainType
|
||||
* @return void
|
||||
*/
|
||||
public function testStringIsNotNull()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
if (5 === null) { }
|
||||
');
|
||||
|
||||
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
|
||||
$file_checker->visitAndAnalyzeMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Psalm\Exception\CodeException
|
||||
* @expectedExceptionMessage TypeDoesNotContainType
|
||||
* @return void
|
||||
*/
|
||||
public function testStringIsNotFalse()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
if (5 === false) { }
|
||||
');
|
||||
|
||||
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
|
||||
$file_checker->visitAndAnalyzeMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* @expectedException \Psalm\Exception\CodeException
|
||||
* @expectedExceptionMessage FailedTypeResolution
|
||||
|
Loading…
Reference in New Issue
Block a user