mirror of
https://github.com/danog/psalm.git
synced 2024-12-15 10:57:08 +01:00
832a190dd4
* Add initial enum preparation * Support cases method * Ignore bad use error cc @weirdan * Fix type
343 lines
12 KiB
PHP
343 lines
12 KiB
PHP
<?php
|
|
namespace Psalm\Internal\Analyzer\Statements\Expression;
|
|
|
|
use PhpParser;
|
|
use Psalm\Internal\Algebra\FormulaGenerator;
|
|
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
|
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
|
use Psalm\Issue\UnhandledMatchCondition;
|
|
use Psalm\Context;
|
|
use Psalm\Node\Expr\BinaryOp\VirtualIdentical;
|
|
use Psalm\Node\Expr\VirtualArray;
|
|
use Psalm\Node\Expr\VirtualArrayItem;
|
|
use Psalm\Node\Expr\VirtualConstFetch;
|
|
use Psalm\Node\Expr\VirtualFuncCall;
|
|
use Psalm\Node\Expr\VirtualNew;
|
|
use Psalm\Node\Expr\VirtualTernary;
|
|
use Psalm\Node\Expr\VirtualThrow;
|
|
use Psalm\Node\Expr\VirtualVariable;
|
|
use Psalm\Node\Name\VirtualFullyQualified;
|
|
use Psalm\Node\VirtualArg;
|
|
use Psalm\Type;
|
|
|
|
use function substr;
|
|
use function array_reverse;
|
|
use function array_shift;
|
|
use function in_array;
|
|
use function count;
|
|
use function array_map;
|
|
|
|
class MatchAnalyzer
|
|
{
|
|
public static function analyze(
|
|
StatementsAnalyzer $statements_analyzer,
|
|
PhpParser\Node\Expr\Match_ $stmt,
|
|
Context $context
|
|
) : bool {
|
|
$was_inside_call = $context->inside_call;
|
|
|
|
$context->inside_call = true;
|
|
|
|
$was_inside_conditional = $context->inside_conditional;
|
|
|
|
$context->inside_conditional = true;
|
|
|
|
if (ExpressionAnalyzer::analyze($statements_analyzer, $stmt->cond, $context) === false) {
|
|
$context->inside_conditional = $was_inside_conditional;
|
|
|
|
return false;
|
|
}
|
|
|
|
$context->inside_conditional = $was_inside_conditional;
|
|
|
|
$switch_var_id = ExpressionIdentifier::getArrayVarId(
|
|
$stmt->cond,
|
|
null,
|
|
$statements_analyzer
|
|
);
|
|
|
|
$match_condition = $stmt->cond;
|
|
|
|
if (!$switch_var_id) {
|
|
if ($stmt->cond instanceof PhpParser\Node\Expr\FuncCall
|
|
&& $stmt->cond->name instanceof PhpParser\Node\Name
|
|
&& ($stmt->cond->name->parts === ['get_class']
|
|
|| $stmt->cond->name->parts === ['gettype']
|
|
|| $stmt->cond->name->parts === ['get_debug_type'])
|
|
&& $stmt->cond->args
|
|
) {
|
|
$first_arg = $stmt->cond->args[0];
|
|
|
|
if (!$first_arg->value instanceof PhpParser\Node\Expr\Variable) {
|
|
$switch_var_id = '$__tmp_switch__' . (int) $first_arg->value->getAttribute('startFilePos');
|
|
|
|
$condition_type = $statements_analyzer->node_data->getType($first_arg->value) ?: Type::getMixed();
|
|
|
|
$context->vars_in_scope[$switch_var_id] = $condition_type;
|
|
|
|
$match_condition = new VirtualFuncCall(
|
|
$stmt->cond->name,
|
|
[
|
|
new VirtualArg(
|
|
new VirtualVariable(
|
|
substr($switch_var_id, 1),
|
|
$first_arg->value->getAttributes()
|
|
),
|
|
false,
|
|
false,
|
|
$first_arg->getAttributes()
|
|
)
|
|
],
|
|
$stmt->cond->getAttributes()
|
|
);
|
|
}
|
|
} elseif ($stmt->cond instanceof PhpParser\Node\Expr\FuncCall
|
|
|| $stmt->cond instanceof PhpParser\Node\Expr\MethodCall
|
|
|| $stmt->cond instanceof PhpParser\Node\Expr\StaticCall
|
|
) {
|
|
$switch_var_id = '$__tmp_switch__' . (int) $stmt->cond->getAttribute('startFilePos');
|
|
|
|
$condition_type = $statements_analyzer->node_data->getType($stmt->cond) ?: Type::getMixed();
|
|
|
|
$context->vars_in_scope[$switch_var_id] = $condition_type;
|
|
|
|
$match_condition = new VirtualVariable(
|
|
substr($switch_var_id, 1),
|
|
$stmt->cond->getAttributes()
|
|
);
|
|
}
|
|
}
|
|
|
|
$arms = $stmt->arms;
|
|
|
|
foreach ($arms as $i => $arm) {
|
|
// move default to the end
|
|
if ($arm->conds === null) {
|
|
unset($arms[$i]);
|
|
$arms[] = $arm;
|
|
}
|
|
}
|
|
|
|
$arms = array_reverse($arms);
|
|
|
|
$last_arm = array_shift($arms);
|
|
|
|
if (!$last_arm) {
|
|
if (\Psalm\IssueBuffer::accepts(
|
|
new UnhandledMatchCondition(
|
|
'This match expression does not match anything',
|
|
new \Psalm\CodeLocation($statements_analyzer->getSource(), $match_condition)
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
// continue
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
$old_node_data = $statements_analyzer->node_data;
|
|
|
|
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
|
|
|
|
if (!$last_arm->conds) {
|
|
$ternary = $last_arm->body;
|
|
} else {
|
|
$ternary = new VirtualTernary(
|
|
self::convertCondsToConditional($last_arm->conds, $match_condition, $last_arm->getAttributes()),
|
|
$last_arm->body,
|
|
new VirtualThrow(
|
|
new VirtualNew(
|
|
new VirtualFullyQualified(
|
|
'UnhandledMatchError',
|
|
$stmt->getAttributes()
|
|
),
|
|
[],
|
|
$stmt->getAttributes()
|
|
)
|
|
),
|
|
$stmt->getAttributes()
|
|
);
|
|
}
|
|
|
|
foreach ($arms as $arm) {
|
|
if (!$arm->conds) {
|
|
continue;
|
|
}
|
|
|
|
$ternary = new VirtualTernary(
|
|
self::convertCondsToConditional($arm->conds, $match_condition, $arm->getAttributes()),
|
|
$arm->body,
|
|
$ternary,
|
|
$arm->getAttributes()
|
|
);
|
|
}
|
|
|
|
$suppressed_issues = $statements_analyzer->getSuppressedIssues();
|
|
|
|
if (!in_array('RedundantCondition', $suppressed_issues, true)) {
|
|
$statements_analyzer->addSuppressedIssues(['RedundantCondition']);
|
|
}
|
|
|
|
if (!in_array('RedundantConditionGivenDocblockType', $suppressed_issues, true)) {
|
|
$statements_analyzer->addSuppressedIssues(['RedundantConditionGivenDocblockType']);
|
|
}
|
|
|
|
if (ExpressionAnalyzer::analyze($statements_analyzer, $ternary, $context) === false) {
|
|
return false;
|
|
}
|
|
|
|
if (!in_array('RedundantCondition', $suppressed_issues, true)) {
|
|
$statements_analyzer->removeSuppressedIssues(['RedundantCondition']);
|
|
}
|
|
|
|
if (!in_array('RedundantConditionGivenDocblockType', $suppressed_issues, true)) {
|
|
$statements_analyzer->removeSuppressedIssues(['RedundantConditionGivenDocblockType']);
|
|
}
|
|
|
|
if ($switch_var_id && $last_arm->conds) {
|
|
$codebase = $statements_analyzer->getCodebase();
|
|
|
|
$all_conds = $last_arm->conds;
|
|
|
|
foreach ($arms as $arm) {
|
|
if (!$arm->conds) {
|
|
throw new \UnexpectedValueException('bad');
|
|
}
|
|
|
|
$all_conds = \array_merge($arm->conds, $all_conds);
|
|
}
|
|
|
|
$all_match_condition = self::convertCondsToConditional(
|
|
\array_values($all_conds),
|
|
$match_condition,
|
|
$match_condition->getAttributes()
|
|
);
|
|
|
|
ExpressionAnalyzer::analyze($statements_analyzer, $all_match_condition, $context);
|
|
|
|
$clauses = FormulaGenerator::getFormula(
|
|
\spl_object_id($all_match_condition),
|
|
\spl_object_id($all_match_condition),
|
|
$all_match_condition,
|
|
$context->self,
|
|
$statements_analyzer,
|
|
$codebase,
|
|
false,
|
|
false
|
|
);
|
|
|
|
$reconcilable_types = \Psalm\Internal\Algebra::getTruthsFromFormula(
|
|
\Psalm\Internal\Algebra::negateFormula($clauses)
|
|
);
|
|
|
|
// if the if has an || in the conditional, we cannot easily reason about it
|
|
if ($reconcilable_types) {
|
|
$changed_var_ids = [];
|
|
|
|
$vars_in_scope_reconciled = \Psalm\Type\Reconciler::reconcileKeyedTypes(
|
|
$reconcilable_types,
|
|
[],
|
|
$context->vars_in_scope,
|
|
$changed_var_ids,
|
|
[],
|
|
$statements_analyzer,
|
|
[],
|
|
$context->inside_loop,
|
|
null
|
|
);
|
|
|
|
if (isset($vars_in_scope_reconciled[$switch_var_id])) {
|
|
$array_literal_types = \array_filter(
|
|
$vars_in_scope_reconciled[$switch_var_id]->getAtomicTypes(),
|
|
function ($type) {
|
|
return $type instanceof Type\Atomic\TLiteralInt
|
|
|| $type instanceof Type\Atomic\TLiteralString
|
|
|| $type instanceof Type\Atomic\TLiteralFloat
|
|
|| $type instanceof Type\Atomic\TEnumCase;
|
|
}
|
|
);
|
|
|
|
if ($array_literal_types) {
|
|
if (\Psalm\IssueBuffer::accepts(
|
|
new UnhandledMatchCondition(
|
|
'This match expression is not exhaustive - consider values '
|
|
. $vars_in_scope_reconciled[$switch_var_id]->getId(),
|
|
new \Psalm\CodeLocation($statements_analyzer->getSource(), $match_condition)
|
|
),
|
|
$statements_analyzer->getSuppressedIssues()
|
|
)) {
|
|
// continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$stmt_expr_type = $statements_analyzer->node_data->getType($ternary);
|
|
|
|
$old_node_data->setType($stmt, $stmt_expr_type ?: Type::getMixed());
|
|
|
|
$statements_analyzer->node_data = $old_node_data;
|
|
|
|
$context->inside_call = $was_inside_call;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @param non-empty-list<PhpParser\Node\Expr> $conds
|
|
*/
|
|
private static function convertCondsToConditional(
|
|
array $conds,
|
|
PhpParser\Node\Expr $match_condition,
|
|
array $attributes
|
|
) : PhpParser\Node\Expr {
|
|
if (count($conds) === 1) {
|
|
return new VirtualIdentical(
|
|
$match_condition,
|
|
$conds[0],
|
|
$attributes
|
|
);
|
|
}
|
|
|
|
$array_items = array_map(
|
|
function ($cond): PhpParser\Node\Expr\ArrayItem {
|
|
return new VirtualArrayItem($cond, null, false, $cond->getAttributes());
|
|
},
|
|
$conds
|
|
);
|
|
|
|
return new VirtualFuncCall(
|
|
new VirtualFullyQualified(['in_array']),
|
|
[
|
|
new VirtualArg(
|
|
$match_condition,
|
|
false,
|
|
false,
|
|
$attributes
|
|
),
|
|
new VirtualArg(
|
|
new VirtualArray(
|
|
$array_items,
|
|
$attributes
|
|
),
|
|
false,
|
|
false,
|
|
$attributes
|
|
),
|
|
new VirtualArg(
|
|
new VirtualConstFetch(
|
|
new VirtualFullyQualified(['true']),
|
|
$attributes
|
|
),
|
|
false,
|
|
false,
|
|
$attributes
|
|
),
|
|
],
|
|
$attributes
|
|
);
|
|
}
|
|
}
|