diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php index e81ab524b..4f442c7b6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ExitAnalyzer.php @@ -4,11 +4,14 @@ namespace Psalm\Internal\Analyzer\Statements\Expression; use PhpParser; use Psalm\CodeLocation; use Psalm\Context; +use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\Call\ArgumentAnalyzer; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\DataFlow\TaintSink; +use Psalm\Issue\ImpureFunctionCall; +use Psalm\IssueBuffer; use Psalm\Storage\FunctionLikeParameter; use Psalm\Type; use Psalm\Type\Atomic\TInt; @@ -21,6 +24,8 @@ class ExitAnalyzer PhpParser\Node\Expr\Exit_ $stmt, Context $context ) : bool { + $expr_type = null; + if ($stmt->expr) { $context->inside_call = true; @@ -80,7 +85,32 @@ class ExitAnalyzer $context->inside_call = false; } - $statements_analyzer->node_data->setType($stmt, \Psalm\Type::getEmpty()); + if ($expr_type + && !$expr_type->isInt() + && !$context->collect_mutations + && !$context->collect_initializations + ) { + if ($context->mutation_free || $context->external_mutation_free) { + $function_name = $stmt->getAttribute('kind') === PhpParser\Node\Expr\Exit_::KIND_DIE ? 'die' : 'exit'; + + if (IssueBuffer::accepts( + new ImpureFunctionCall( + 'Cannot call ' . $function_name . ' with a non-integer argument from a mutation-free context', + new CodeLocation($statements_analyzer, $stmt) + ), + $statements_analyzer->getSuppressedIssues() + )) { + // fall through + } + } elseif ($statements_analyzer->getSource() instanceof FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations + ) { + $statements_analyzer->getSource()->inferred_has_mutation = true; + $statements_analyzer->getSource()->inferred_impure = true; + } + } + + $statements_analyzer->node_data->setType($stmt, Type::getEmpty()); return true; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php index 562990893..34ca7330a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/PrintAnalyzer.php @@ -4,11 +4,13 @@ namespace Psalm\Internal\Analyzer\Statements\Expression; use PhpParser; use Psalm\CodeLocation; use Psalm\Context; +use Psalm\Internal\Analyzer\FunctionLikeAnalyzer; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\DataFlow\TaintSink; use Psalm\Issue\ForbiddenCode; +use Psalm\Issue\ImpureFunctionCall; use Psalm\IssueBuffer; use Psalm\Storage\FunctionLikeParameter; use Psalm\Type; @@ -82,6 +84,25 @@ class PrintAnalyzer } } + if (!$context->collect_initializations && !$context->collect_mutations) { + if ($context->mutation_free || $context->external_mutation_free) { + if (IssueBuffer::accepts( + new ImpureFunctionCall( + 'Cannot call print from a mutation-free context', + new CodeLocation($statements_analyzer, $stmt) + ), + $statements_analyzer->getSuppressedIssues() + )) { + // fall through + } + } elseif ($statements_analyzer->getSource() instanceof FunctionLikeAnalyzer + && $statements_analyzer->getSource()->track_mutations + ) { + $statements_analyzer->getSource()->inferred_has_mutation = true; + $statements_analyzer->getSource()->inferred_impure = true; + } + } + $statements_analyzer->node_data->setType($stmt, Type::getInt(false, 1)); return true; diff --git a/tests/PureAnnotationTest.php b/tests/PureAnnotationTest.php index 8be3f9438..db8a439b2 100644 --- a/tests/PureAnnotationTest.php +++ b/tests/PureAnnotationTest.php @@ -174,6 +174,38 @@ class PureAnnotationTest extends TestCase return $ar[0] ?? 0; }', ], + 'exitFunctionWithNoArgumentIsPure' => [ + ' [ + ' [ + ' [ + ' [ ' 'ImpureFunctionCall', ], + 'printFunctionIsImpure' => [ + ' 'ImpureFunctionCall', + ], + 'exitFunctionWithNonIntegerArgumentIsImpure' => [ + ' 'ImpureFunctionCall', + ], + 'dieFunctionWithNonIntegerArgumentIsImpure' => [ + ' 'ImpureFunctionCall', + ], 'impureByRef' => [ '