1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-10 06:58:41 +01:00
psalm/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php

531 lines
22 KiB
PHP

<?php
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\BinaryOp\AndAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\BinaryOp\CoalesceAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\BinaryOp\ConcatAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\BinaryOp\NonComparisonOpAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\BinaryOp\OrAnalyzer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\Codebase\VariableUseGraph;
use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Internal\MethodIdentifier;
use Psalm\Issue\DocblockTypeContradiction;
use Psalm\Issue\ImpureMethodCall;
use Psalm\Issue\InvalidOperand;
use Psalm\Issue\RedundantCondition;
use Psalm\Issue\RedundantConditionGivenDocblockType;
use Psalm\Issue\TypeDoesNotContainType;
use Psalm\IssueBuffer;
use Psalm\Plugin\EventHandler\Event\AddRemoveTaintsEvent;
use Psalm\Type;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Union;
use UnexpectedValueException;
use function in_array;
use function strlen;
/**
* @internal
*/
class BinaryOpAnalyzer
{
public static function analyze(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr\BinaryOp $stmt,
Context $context,
int $nesting = 0,
bool $from_stmt = false
): bool {
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Concat && $nesting > 100) {
$statements_analyzer->node_data->setType($stmt, Type::getString());
// ignore deeply-nested string concatenation
return true;
}
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd ||
$stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalAnd
) {
$was_inside_general_use = $context->inside_general_use;
$context->inside_general_use = true;
$expr_result = AndAnalyzer::analyze(
$statements_analyzer,
$stmt,
$context,
$from_stmt
);
$context->inside_general_use = $was_inside_general_use;
$statements_analyzer->node_data->setType($stmt, Type::getBool());
return $expr_result;
}
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr ||
$stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalOr
) {
$was_inside_general_use = $context->inside_general_use;
$context->inside_general_use = true;
$expr_result = OrAnalyzer::analyze(
$statements_analyzer,
$stmt,
$context,
$from_stmt
);
$context->inside_general_use = $was_inside_general_use;
$statements_analyzer->node_data->setType($stmt, Type::getBool());
return $expr_result;
}
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Coalesce) {
$expr_result = CoalesceAnalyzer::analyze(
$statements_analyzer,
$stmt,
$context
);
self::addDataFlow(
$statements_analyzer,
$stmt,
$stmt->left,
$stmt->right,
'coalesce'
);
return $expr_result;
}
if ($stmt->left instanceof PhpParser\Node\Expr\BinaryOp) {
if (self::analyze($statements_analyzer, $stmt->left, $context, $nesting + 1) === false) {
return false;
}
} else {
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->left, $context) === false) {
return false;
}
}
if ($stmt->right instanceof PhpParser\Node\Expr\BinaryOp) {
if (self::analyze($statements_analyzer, $stmt->right, $context, $nesting + 1) === false) {
return false;
}
} else {
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->right, $context) === false) {
return false;
}
}
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Concat) {
$stmt_type = Type::getString();
ConcatAnalyzer::analyze(
$statements_analyzer,
$stmt->left,
$stmt->right,
$context,
$result_type
);
if ($result_type) {
$stmt_type = $result_type;
}
if ($statements_analyzer->data_flow_graph
&& ($statements_analyzer->data_flow_graph instanceof VariableUseGraph
|| !in_array('TaintedInput', $statements_analyzer->getSuppressedIssues()))
) {
$stmt_left_type = $statements_analyzer->node_data->getType($stmt->left);
$stmt_right_type = $statements_analyzer->node_data->getType($stmt->right);
$var_location = new CodeLocation($statements_analyzer, $stmt);
$new_parent_node = DataFlowNode::getForAssignment('concat', $var_location);
$statements_analyzer->data_flow_graph->addNode($new_parent_node);
$stmt_type->parent_nodes = [
$new_parent_node->id => $new_parent_node
];
$codebase = $statements_analyzer->getCodebase();
$event = new AddRemoveTaintsEvent($stmt, $context, $statements_analyzer, $codebase);
$added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event);
$removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event);
if ($stmt_left_type && $stmt_left_type->parent_nodes) {
foreach ($stmt_left_type->parent_nodes as $parent_node) {
$statements_analyzer->data_flow_graph->addPath(
$parent_node,
$new_parent_node,
'concat',
$added_taints,
$removed_taints
);
}
}
if ($stmt_right_type && $stmt_right_type->parent_nodes) {
foreach ($stmt_right_type->parent_nodes as $parent_node) {
$statements_analyzer->data_flow_graph->addPath(
$parent_node,
$new_parent_node,
'concat',
$added_taints,
$removed_taints
);
}
}
}
$statements_analyzer->node_data->setType($stmt, $stmt_type);
return true;
}
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Spaceship) {
$statements_analyzer->node_data->setType(
$stmt,
new Union(
[
new TLiteralInt(-1),
new TLiteralInt(0),
new TLiteralInt(1)
]
)
);
self::addDataFlow(
$statements_analyzer,
$stmt,
$stmt->left,
$stmt->right,
'<=>'
);
return true;
}
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Equal
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\NotEqual
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Identical
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Greater
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\GreaterOrEqual
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Smaller
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\SmallerOrEqual
) {
$statements_analyzer->node_data->setType($stmt, Type::getBool());
$stmt_left_type = $statements_analyzer->node_data->getType($stmt->left);
$stmt_right_type = $statements_analyzer->node_data->getType($stmt->right);
if (($stmt instanceof PhpParser\Node\Expr\BinaryOp\Greater
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\GreaterOrEqual
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Smaller
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\SmallerOrEqual)
&& $statements_analyzer->getCodebase()->config->strict_binary_operands
&& $stmt_left_type
&& $stmt_right_type
&& (($stmt_left_type->isSingle() && $stmt_left_type->hasBool())
|| ($stmt_right_type->isSingle() && $stmt_right_type->hasBool()))
) {
IssueBuffer::maybeAdd(
new InvalidOperand(
'Cannot compare ' . $stmt_left_type->getId() . ' to ' . $stmt_right_type->getId(),
new CodeLocation($statements_analyzer, $stmt)
),
$statements_analyzer->getSuppressedIssues()
);
}
if (($stmt instanceof PhpParser\Node\Expr\BinaryOp\Equal
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\NotEqual
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Identical
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical)
&& $stmt->left instanceof PhpParser\Node\Expr\FuncCall
&& $stmt->left->name instanceof PhpParser\Node\Name
&& $stmt->left->name->parts === ['substr']
&& isset($stmt->left->getArgs()[1])
&& $stmt_right_type
&& $stmt_right_type->hasLiteralString()
) {
$from_type = $statements_analyzer->node_data->getType($stmt->left->getArgs()[1]->value);
$length_type = isset($stmt->left->getArgs()[2])
? ($statements_analyzer->node_data->getType($stmt->left->getArgs()[2]->value) ?? Type::getMixed())
: null;
$string_length = null;
if ($from_type && $from_type->isSingleIntLiteral() && $length_type === null) {
$string_length = -$from_type->getSingleIntLiteral()->value;
} elseif ($length_type && $length_type->isSingleIntLiteral()) {
$string_length = $length_type->getSingleIntLiteral()->value;
}
if ($string_length > 0) {
foreach ($stmt_right_type->getAtomicTypes() as $atomic_right_type) {
if ($atomic_right_type instanceof TLiteralString) {
if (strlen($atomic_right_type->value) !== $string_length) {
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Equal
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\Identical
) {
if ($atomic_right_type->from_docblock) {
IssueBuffer::maybeAdd(
new DocblockTypeContradiction(
$atomic_right_type . ' string length is not ' . $string_length,
new CodeLocation($statements_analyzer, $stmt),
"strlen($atomic_right_type) !== $string_length"
),
$statements_analyzer->getSuppressedIssues()
);
} else {
IssueBuffer::maybeAdd(
new TypeDoesNotContainType(
$atomic_right_type . ' string length is not ' . $string_length,
new CodeLocation($statements_analyzer, $stmt),
"strlen($atomic_right_type) !== $string_length"
),
$statements_analyzer->getSuppressedIssues()
);
}
} else {
if ($atomic_right_type->from_docblock) {
IssueBuffer::maybeAdd(
new RedundantConditionGivenDocblockType(
$atomic_right_type . ' string length is never ' . $string_length,
new CodeLocation($statements_analyzer, $stmt),
"strlen($atomic_right_type) !== $string_length"
),
$statements_analyzer->getSuppressedIssues()
);
} else {
IssueBuffer::maybeAdd(
new RedundantCondition(
$atomic_right_type . ' string length is never ' . $string_length,
new CodeLocation($statements_analyzer, $stmt),
"strlen($atomic_right_type) !== $string_length"
),
$statements_analyzer->getSuppressedIssues()
);
}
}
}
}
}
}
}
$codebase = $statements_analyzer->getCodebase();
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Equal
&& $stmt_left_type
&& $stmt_right_type
&& ($context->mutation_free || $codebase->alter_code)
) {
self::checkForImpureEqualityComparison(
$statements_analyzer,
$stmt,
$stmt_left_type,
$stmt_right_type
);
}
self::addDataFlow(
$statements_analyzer,
$stmt,
$stmt->left,
$stmt->right,
'comparison'
);
return true;
}
NonComparisonOpAnalyzer::analyze(
$statements_analyzer,
$stmt,
$context
);
return true;
}
public static function addDataFlow(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr $stmt,
PhpParser\Node\Expr $left,
PhpParser\Node\Expr $right,
string $type = 'binaryop'
): void {
if ($stmt->getLine() === -1) {
throw new UnexpectedValueException('bad');
}
$result_type = $statements_analyzer->node_data->getType($stmt);
if (!$result_type) {
return;
}
if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph
&& $stmt instanceof PhpParser\Node\Expr\BinaryOp
&& !$stmt instanceof PhpParser\Node\Expr\BinaryOp\Concat
&& !$stmt instanceof PhpParser\Node\Expr\BinaryOp\Coalesce
&& (!$stmt instanceof PhpParser\Node\Expr\BinaryOp\Plus || !$result_type->hasArray())
) {
//among BinaryOp, only Concat and Coalesce can pass tainted value to the result. Also Plus on arrays only
return;
}
if ($statements_analyzer->data_flow_graph) {
$stmt_left_type = $statements_analyzer->node_data->getType($left);
$stmt_right_type = $statements_analyzer->node_data->getType($right);
$var_location = new CodeLocation($statements_analyzer, $stmt);
$new_parent_node = DataFlowNode::getForAssignment($type, $var_location);
$statements_analyzer->data_flow_graph->addNode($new_parent_node);
$result_type->parent_nodes = [
$new_parent_node->id => $new_parent_node
];
if ($stmt_left_type && $stmt_left_type->parent_nodes) {
foreach ($stmt_left_type->parent_nodes as $parent_node) {
$statements_analyzer->data_flow_graph->addPath($parent_node, $new_parent_node, $type);
}
}
if ($stmt_right_type && $stmt_right_type->parent_nodes) {
foreach ($stmt_right_type->parent_nodes as $parent_node) {
$statements_analyzer->data_flow_graph->addPath($parent_node, $new_parent_node, $type);
}
}
if ($stmt instanceof PhpParser\Node\Expr\AssignOp
&& $statements_analyzer->data_flow_graph instanceof VariableUseGraph
) {
$root_expr = $left;
while ($root_expr instanceof PhpParser\Node\Expr\ArrayDimFetch) {
$root_expr = $root_expr->var;
}
if ($left instanceof PhpParser\Node\Expr\PropertyFetch) {
$statements_analyzer->data_flow_graph->addPath(
$new_parent_node,
new DataFlowNode('variable-use', 'variable use', null),
'used-by-instance-property'
);
} if ($left instanceof PhpParser\Node\Expr\StaticPropertyFetch) {
$statements_analyzer->data_flow_graph->addPath(
$new_parent_node,
new DataFlowNode('variable-use', 'variable use', null),
'use-in-static-property'
);
} elseif (!$left instanceof PhpParser\Node\Expr\Variable) {
$statements_analyzer->data_flow_graph->addPath(
$new_parent_node,
new DataFlowNode('variable-use', 'variable use', null),
'variable-use'
);
}
}
}
}
private static function checkForImpureEqualityComparison(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Expr\BinaryOp\Equal $stmt,
Union $stmt_left_type,
Union $stmt_right_type
): void {
$codebase = $statements_analyzer->getCodebase();
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 MethodIdentifier(
$atomic_type->value,
'__tostring'
)
);
} catch (UnexpectedValueException $e) {
continue;
}
if (!$storage->mutation_free) {
if ($statements_analyzer->getSource()
instanceof FunctionLikeAnalyzer
&& $statements_analyzer->getSource()->track_mutations
) {
$statements_analyzer->getSource()->inferred_has_mutation = true;
$statements_analyzer->getSource()->inferred_impure = true;
} else {
IssueBuffer::maybeAdd(
new ImpureMethodCall(
'Cannot call a possibly-mutating method '
. $atomic_type->value . '::__toString from a pure context',
new CodeLocation($statements_analyzer, $stmt)
),
$statements_analyzer->getSuppressedIssues()
);
}
}
}
}
} 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 MethodIdentifier(
$atomic_type->value,
'__tostring'
)
);
} catch (UnexpectedValueException $e) {
continue;
}
if (!$storage->mutation_free) {
if ($statements_analyzer->getSource() instanceof FunctionLikeAnalyzer
&& $statements_analyzer->getSource()->track_mutations
) {
$statements_analyzer->getSource()->inferred_has_mutation = true;
$statements_analyzer->getSource()->inferred_impure = true;
} else {
IssueBuffer::maybeAdd(
new ImpureMethodCall(
'Cannot call a possibly-mutating method '
. $atomic_type->value . '::__toString from a pure context',
new CodeLocation($statements_analyzer, $stmt)
),
$statements_analyzer->getSuppressedIssues()
);
}
}
}
}
}
}
}