1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-21 21:31:13 +01:00

Emit more InvalidOperand issues

This commit is contained in:
Matthew Brown 2016-12-29 00:14:06 -05:00
parent a91fc2d3dc
commit 18e7c65430
5 changed files with 114 additions and 48 deletions

View File

@ -14,6 +14,7 @@ use Psalm\Checker\TypeChecker;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Issue\ForbiddenCode;
use Psalm\Issue\ImplicitToStringCast;
use Psalm\Issue\InvalidArgument;
use Psalm\Issue\InvalidScalarArgument;
use Psalm\Issue\InvalidScope;
@ -1169,7 +1170,8 @@ class CallChecker
$param_type,
true,
$scalar_type_match_found,
$coerced_type
$coerced_type,
$to_string_cast
);
if ($coerced_type) {
@ -1185,13 +1187,26 @@ class CallChecker
}
}
if ($to_string_cast && $cased_method_id !== 'echo') {
if (IssueBuffer::accepts(
new ImplicitToStringCast(
'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id . ' expects ' .
$param_type . ', ' . $input_type . ' provided with a __toString method',
$code_location
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
}
if (!$type_match_found) {
if ($scalar_type_match_found) {
if ($cased_method_id !== 'echo') {
if (IssueBuffer::accepts(
new InvalidScalarArgument(
'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id . ' expects ' . $param_type .
', ' . $input_type . ' provided',
'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id . ' expects ' .
$param_type . ', ' . $input_type . ' provided',
$code_location
),
$statements_checker->getSuppressedIssues()

View File

@ -1043,53 +1043,73 @@ class ExpressionChecker
$config = Config::getInstance();
if ($left_type && $right_type) {
foreach ($left_type->types as $left_type_part) {
foreach ($right_type->types as $right_type_part) {
if ($left_type_part->isMixed() || $right_type_part->isMixed()) {
if ($left_type_part->isMixed()) {
if (IssueBuffer::accepts(
new MixedOperand(
'Left operand cannot be mixed',
new CodeLocation($statements_checker->getSource(), $left)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new MixedOperand(
'Right operand cannot be mixed',
new CodeLocation($statements_checker->getSource(), $right)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
}
$result_type = Type::getString();
$result_type = Type::getString();
return;
if ($left_type->isMixed() || $right_type->isMixed()) {
if ($left_type->isMixed()) {
if (IssueBuffer::accepts(
new MixedOperand(
'Left operand cannot be mixed',
new CodeLocation($statements_checker->getSource(), $left)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
if ($left_type_part->isString() && $right_type_part->isString()) {
$result_type = Type::getString();
continue;
} else {
if (IssueBuffer::accepts(
new MixedOperand(
'Right operand cannot be mixed',
new CodeLocation($statements_checker->getSource(), $right)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
}
if ($config->strict_binary_operands) {
if (IssueBuffer::accepts(
new InvalidOperand(
'Cannot concatenate a string and a non-string',
new CodeLocation($statements_checker->getSource(), $parent)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
}
return;
}
$result_type = Type::getString();
$left_type_match = TypeChecker::isContainedBy(
$left_type,
Type::getString(),
false,
$left_has_scalar_match,
$left_type_coerced,
$left_to_string_cast
);
$right_type_match = TypeChecker::isContainedBy(
$right_type,
Type::getString(),
false,
$right_has_scalar_match,
$right_type_coerced,
$right_to_string_cast
);
if (!$left_type_match && (!$left_has_scalar_match || $config->strict_binary_operands)) {
if (IssueBuffer::accepts(
new InvalidOperand(
'Cannot concatenate a string and a ' . $left_type,
new CodeLocation($statements_checker->getSource(), $left)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
}
if (!$right_type_match && (!$right_has_scalar_match || $config->strict_binary_operands)) {
if (IssueBuffer::accepts(
new InvalidOperand(
'Cannot concatenate a string and a ' . $right_type,
new CodeLocation($statements_checker->getSource(), $right)
),
$statements_checker->getSuppressedIssues()
)) {
// fall through
}
}
}

View File

@ -655,6 +655,7 @@ class TypeChecker
* @param bool $ignore_null
* @param bool &$has_scalar_match
* @param bool &$type_coerced whether or not there was type coercion involved
* @param bool &$to_string_cast
* @return bool
*/
public static function isContainedBy(
@ -662,7 +663,8 @@ class TypeChecker
Type\Union $container_type,
$ignore_null = false,
&$has_scalar_match = null,
&$type_coerced = null
&$type_coerced = null,
&$to_string_cast = null
) {
$has_scalar_match = true;
@ -758,6 +760,7 @@ class TypeChecker
// check whether the object has a __toString method
if (MethodChecker::methodExists($input_type_part->value . '::__toString')) {
$type_match_found = true;
$to_string_cast = true;
}
}

View File

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

View File

@ -15,12 +15,11 @@ class ToStringTest extends PHPUnit_Framework_TestCase
public static function setUpBeforeClass()
{
self::$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$config = new TestConfig();
}
public function setUp()
{
$config = new TestConfig();
FileChecker::clearCache();
}
@ -111,4 +110,27 @@ class ToStringTest extends PHPUnit_Framework_TestCase
$context = new Context('somefile.php');
$file_checker->check(true, true, $context);
}
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage ImplicitToStringCast
*/
public function testImplicitCast()
{
$stmts = self::$parser->parse('<?php
class A {
public function __toString() : string
{
return "hello";
}
}
function foo(string $b) : void {}
foo(new A());
');
$file_checker = new FileChecker('somefile.php', $stmts);
$context = new Context('somefile.php');
$file_checker->check(true, true, $context);
}
}