diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php index a65c194f6..15b4199f5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php @@ -12,6 +12,7 @@ use Psalm\Config; use Psalm\Context; use Psalm\Issue\FalseOperand; use Psalm\Issue\ImplicitToStringCast; +use Psalm\Issue\ImpureMethodCall; use Psalm\Issue\InvalidOperand; use Psalm\Issue\MixedOperand; use Psalm\Issue\NullOperand; @@ -925,6 +926,70 @@ class BinaryOpAnalyzer $statements_analyzer->node_data->setType($stmt, Type::getBool()); } + if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Equal) { + if ($stmt_left_type->hasString() && $stmt_right_type->hasObjectType()) { + foreach ($stmt_right_type->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof TNamedObject) { + try { + $storage = $codebase->methods->getStorage( + new \Psalm\Internal\MethodIdentifier( + $atomic_type->value, + '__tostring' + ) + ); + } catch (\UnexpectedValueException $e) { + continue; + } + + if ($context->mutation_free + && !$storage->mutation_free + ) { + if (IssueBuffer::accepts( + new ImpureMethodCall( + 'Cannot call an mutation-free method ' + . $atomic_type->value . '::__toString from a pure context', + new CodeLocation($source, $stmt) + ), + $statements_analyzer->getSuppressedIssues() + )) { + // fall through + } + } + } + } + } elseif ($stmt_right_type->hasString() && $stmt_left_type->hasObjectType()) { + foreach ($stmt_left_type->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof TNamedObject) { + try { + $storage = $codebase->methods->getStorage( + new \Psalm\Internal\MethodIdentifier( + $atomic_type->value, + '__tostring' + ) + ); + } catch (\UnexpectedValueException $e) { + continue; + } + + if ($context->mutation_free + && !$storage->mutation_free + ) { + if (IssueBuffer::accepts( + new ImpureMethodCall( + 'Cannot call an mutation-free method ' + . $atomic_type->value . '::__toString from a pure context', + new CodeLocation($statements_analyzer, $stmt) + ), + $statements_analyzer->getSuppressedIssues() + )) { + // fall through + } + } + } + } + } + } + if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Spaceship) { $statements_analyzer->node_data->setType($stmt, Type::getInt()); } diff --git a/tests/PureAnnotationTest.php b/tests/PureAnnotationTest.php index 87056fc12..03539dead 100644 --- a/tests/PureAnnotationTest.php +++ b/tests/PureAnnotationTest.php @@ -182,6 +182,23 @@ class PureAnnotationTest extends TestCase return $ar[0] ?? 0; }', ], + 'allowPureToString' => [ + ' 'ImpureStaticProperty', ], + 'preventImpureToString' => [ + ' 'ImpureMethodCall' + ], ]; } }