1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-09 14:38:37 +01:00
psalm/src/Psalm/Internal/Analyzer/Statements/Expression/MatchAnalyzer.php

365 lines
12 KiB
PHP
Raw Normal View History

<?php
namespace Psalm\Internal\Analyzer\Statements\Expression;
use PhpParser;
2021-12-03 20:11:20 +01:00
use Psalm\CodeLocation;
2021-06-08 04:55:21 +02:00
use Psalm\Context;
2021-12-03 20:11:20 +01:00
use Psalm\Internal\Algebra;
2020-11-03 22:15:44 +01:00
use Psalm\Internal\Algebra\FormulaGenerator;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
2020-09-01 04:59:47 +02:00
use Psalm\Issue\UnhandledMatchCondition;
2021-12-03 20:11:20 +01:00
use Psalm\IssueBuffer;
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 Psalm\Type\Atomic;
2021-12-13 04:45:57 +01:00
use Psalm\Type\Atomic\TEnumCase;
use Psalm\Type\Atomic\TLiteralFloat;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
2021-12-03 20:11:20 +01:00
use Psalm\Type\Reconciler;
2021-12-03 21:40:18 +01:00
use UnexpectedValueException;
2020-09-22 07:10:46 +02:00
2021-12-03 21:07:25 +01:00
use function array_filter;
2021-06-08 04:55:21 +02:00
use function array_map;
2021-12-03 21:07:25 +01:00
use function array_merge;
2020-08-31 00:29:28 +02:00
use function array_reverse;
use function array_shift;
use function count;
2021-06-08 04:55:21 +02:00
use function in_array;
2021-12-03 21:07:25 +01:00
use function spl_object_id;
2021-06-08 04:55:21 +02:00
use function substr;
2022-01-03 07:55:32 +01:00
/**
* @internal
*/
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::getExtendedVarId(
$stmt->cond,
null,
2022-12-18 17:15:15 +01:00
$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']
2023-02-22 23:55:27 +01:00
|| $stmt->cond->name->parts === ['count']
|| $stmt->cond->name->parts === ['sizeof'])
2021-10-09 23:37:04 +02:00
&& $stmt->cond->getArgs()
) {
2021-10-09 23:37:04 +02:00
$first_arg = $stmt->cond->getArgs()[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),
2022-12-18 17:15:15 +01:00
$first_arg->value->getAttributes(),
),
false,
false,
2022-12-18 17:15:15 +01:00
$first_arg->getAttributes(),
),
],
2022-12-18 17:15:15 +01:00
$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),
2022-12-18 17:15:15 +01:00
$stmt->cond->getAttributes(),
);
}
}
$arms = $stmt->arms;
$flattened_arms = [];
$last_arm = null;
foreach ($arms as $arm) {
if ($arm->conds === null) {
$last_arm = $arm;
continue;
}
foreach ($arm->conds as $cond) {
$flattened_arms[] = new PhpParser\Node\MatchArm(
[$cond],
$arm->body,
$arm->getAttributes(),
);
}
}
$arms = $flattened_arms;
$arms = array_reverse($arms);
$last_arm ??= array_shift($arms);
if (!$last_arm) {
2021-12-03 20:11:20 +01:00
IssueBuffer::maybeAdd(
new UnhandledMatchCondition(
'This match expression does not match anything',
2022-12-18 17:15:15 +01:00
new CodeLocation($statements_analyzer->getSource(), $match_condition),
),
2022-12-18 17:15:15 +01:00
$statements_analyzer->getSuppressedIssues(),
);
return false;
}
$old_node_data = $statements_analyzer->node_data;
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
2020-08-30 22:30:43 +02:00
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',
2022-12-18 17:15:15 +01:00
$stmt->getAttributes(),
),
[],
2022-12-18 17:15:15 +01:00
$stmt->getAttributes(),
),
),
2022-12-18 17:15:15 +01:00
$stmt->getAttributes(),
);
}
foreach ($arms as $arm) {
if (!$arm->conds) {
continue;
}
$ternary = new VirtualTernary(
self::convertCondsToConditional($arm->conds, $match_condition, $arm->getAttributes()),
$arm->body,
$ternary,
2022-12-18 17:15:15 +01:00
$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']);
}
2020-09-01 05:03:36 +02:00
if ($switch_var_id && $last_arm->conds) {
2020-09-01 04:59:47 +02:00
$codebase = $statements_analyzer->getCodebase();
2020-09-01 05:03:36 +02:00
$all_conds = $last_arm->conds;
2020-09-01 04:59:47 +02:00
foreach ($arms as $arm) {
2020-09-01 05:03:36 +02:00
if (!$arm->conds) {
2021-12-03 21:40:18 +01:00
throw new UnexpectedValueException('bad');
2020-09-01 05:03:36 +02:00
}
2021-12-03 21:07:25 +01:00
$all_conds = array_merge($arm->conds, $all_conds);
2020-09-01 04:59:47 +02:00
}
$all_match_condition = self::convertCondsToConditional(
2021-11-26 13:48:38 +01:00
$all_conds,
2020-09-01 04:59:47 +02:00
$match_condition,
2022-12-18 17:15:15 +01:00
$match_condition->getAttributes(),
2020-09-01 04:59:47 +02:00
);
ExpressionAnalyzer::analyze($statements_analyzer, $all_match_condition, $context);
2020-11-03 22:15:44 +01:00
$clauses = FormulaGenerator::getFormula(
2021-12-03 21:07:25 +01:00
spl_object_id($all_match_condition),
spl_object_id($all_match_condition),
2020-09-01 04:59:47 +02:00
$all_match_condition,
$context->self,
$statements_analyzer,
$codebase,
false,
2022-12-18 17:15:15 +01:00
false,
2020-09-01 04:59:47 +02:00
);
2021-12-03 20:11:20 +01:00
$reconcilable_types = Algebra::getTruthsFromFormula(
2022-12-18 17:15:15 +01:00
Algebra::negateFormula($clauses),
2020-09-01 04:59:47 +02:00
);
// if the if has an || in the conditional, we cannot easily reason about it
if ($reconcilable_types) {
$changed_var_ids = [];
[$vars_in_scope_reconciled, $_] = Reconciler::reconcileKeyedTypes(
2020-09-01 04:59:47 +02:00
$reconcilable_types,
[],
$context->vars_in_scope,
$context->references_in_scope,
2020-09-01 04:59:47 +02:00
$changed_var_ids,
[],
$statements_analyzer,
[],
$context->inside_loop,
2022-12-18 17:15:15 +01:00
null,
2020-09-01 04:59:47 +02:00
);
if (isset($vars_in_scope_reconciled[$switch_var_id])) {
2021-12-03 21:07:25 +01:00
$array_literal_types = array_filter(
$vars_in_scope_reconciled[$switch_var_id]->getAtomicTypes(),
static fn(Atomic $type): bool => $type instanceof TLiteralInt
2022-01-05 23:45:11 +01:00
|| $type instanceof TLiteralString
|| $type instanceof TLiteralFloat
2022-12-18 17:15:15 +01:00
|| $type instanceof TEnumCase,
);
if ($array_literal_types) {
2021-12-03 20:11:20 +01:00
IssueBuffer::maybeAdd(
2020-09-01 04:59:47 +02:00
new UnhandledMatchCondition(
'This match expression is not exhaustive - consider values '
. $vars_in_scope_reconciled[$switch_var_id]->getId(),
2022-12-18 17:15:15 +01:00
new CodeLocation($statements_analyzer->getSource(), $match_condition),
2020-09-01 04:59:47 +02:00
),
2022-12-18 17:15:15 +01:00
$statements_analyzer->getSuppressedIssues(),
);
2020-09-01 04:59:47 +02:00
}
}
}
}
2020-08-30 22:30:43 +02:00
$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],
2022-12-18 17:15:15 +01:00
$attributes,
);
}
$array_items = array_map(
static fn(PhpParser\Node\Expr $cond): PhpParser\Node\Expr\ArrayItem =>
2022-01-05 23:45:11 +01:00
new VirtualArrayItem($cond, null, false, $cond->getAttributes()),
2022-12-18 17:15:15 +01:00
$conds,
);
return new VirtualFuncCall(
new VirtualFullyQualified(['in_array']),
[
new VirtualArg(
$match_condition,
false,
false,
2022-12-18 17:15:15 +01:00
$attributes,
),
new VirtualArg(
new VirtualArray(
$array_items,
2022-12-18 17:15:15 +01:00
$attributes,
),
false,
false,
2022-12-18 17:15:15 +01:00
$attributes,
),
new VirtualArg(
new VirtualConstFetch(
new VirtualFullyQualified(['true']),
2022-12-18 17:15:15 +01:00
$attributes,
),
false,
false,
2022-12-18 17:15:15 +01:00
$attributes,
),
],
2022-12-18 17:15:15 +01:00
$attributes,
);
}
}