1
0
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:
Matt Brown 2017-04-06 14:53:45 -04:00
parent d65221fbe4
commit 14bb967c7d
5 changed files with 173 additions and 25 deletions

View File

@ -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()) {

View File

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

View File

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

View File

@ -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()) {

View File

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