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:
parent
a91fc2d3dc
commit
18e7c65430
@ -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()
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
6
src/Psalm/Issue/ImplicitToStringCast.php
Normal file
6
src/Psalm/Issue/ImplicitToStringCast.php
Normal file
@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class ImplicitToStringCast extends CodeError
|
||||
{
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user