mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Fix whole bunches of things
This commit is contained in:
parent
3b9b4a8a6f
commit
61aeea6375
@ -9,257 +9,10 @@ use Psalm\FileSource;
|
||||
use Psalm\Issue\ParadoxicalCondition;
|
||||
use Psalm\Issue\RedundantCondition;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type\Algebra;
|
||||
|
||||
class AlgebraChecker
|
||||
{
|
||||
/** @var array<string, array<int, string>> */
|
||||
private static $broken_paths = [];
|
||||
|
||||
/**
|
||||
* @param PhpParser\Node\Expr $conditional
|
||||
* @param string|null $this_class_name
|
||||
* @param FileSource $source
|
||||
*
|
||||
* @return array<int, Clause>
|
||||
*/
|
||||
public static function getFormula(
|
||||
PhpParser\Node\Expr $conditional,
|
||||
$this_class_name,
|
||||
FileSource $source
|
||||
) {
|
||||
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd ||
|
||||
$conditional instanceof PhpParser\Node\Expr\BinaryOp\LogicalAnd
|
||||
) {
|
||||
$left_assertions = self::getFormula(
|
||||
$conditional->left,
|
||||
$this_class_name,
|
||||
$source
|
||||
);
|
||||
|
||||
$right_assertions = self::getFormula(
|
||||
$conditional->right,
|
||||
$this_class_name,
|
||||
$source
|
||||
);
|
||||
|
||||
return array_merge(
|
||||
$left_assertions,
|
||||
$right_assertions
|
||||
);
|
||||
}
|
||||
|
||||
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr ||
|
||||
$conditional instanceof PhpParser\Node\Expr\BinaryOp\LogicalOr
|
||||
) {
|
||||
// at the moment we only support formulae in CNF
|
||||
|
||||
$left_clauses = self::getFormula(
|
||||
$conditional->left,
|
||||
$this_class_name,
|
||||
$source
|
||||
);
|
||||
|
||||
$right_clauses = self::getFormula(
|
||||
$conditional->right,
|
||||
$this_class_name,
|
||||
$source
|
||||
);
|
||||
|
||||
return self::combineOredClauses($left_clauses, $right_clauses);
|
||||
}
|
||||
|
||||
$assertions = AssertionFinder::getAssertions(
|
||||
$conditional,
|
||||
$this_class_name,
|
||||
$source
|
||||
);
|
||||
|
||||
if ($assertions) {
|
||||
$clauses = [];
|
||||
|
||||
foreach ($assertions as $var => $type) {
|
||||
if ($type === 'isset' || $type === '!empty') {
|
||||
$key_parts = self::breakUpPathIntoParts($var);
|
||||
|
||||
$base_key = array_shift($key_parts);
|
||||
|
||||
if ($type === 'isset') {
|
||||
$clauses[] = new Clause([$base_key => ['^isset']]);
|
||||
} else {
|
||||
$clauses[] = new Clause([$base_key => ['^!empty']]);
|
||||
}
|
||||
|
||||
while ($key_parts) {
|
||||
$divider = array_shift($key_parts);
|
||||
|
||||
if ($divider === '[') {
|
||||
$array_key = array_shift($key_parts);
|
||||
array_shift($key_parts);
|
||||
|
||||
$new_base_key = $base_key . '[' . $array_key . ']';
|
||||
|
||||
$base_key = $new_base_key;
|
||||
} elseif ($divider === '->') {
|
||||
$property_name = array_shift($key_parts);
|
||||
$new_base_key = $base_key . '->' . $property_name;
|
||||
|
||||
$base_key = $new_base_key;
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Unexpected divider ' . $divider);
|
||||
}
|
||||
|
||||
if ($type === 'isset') {
|
||||
$clauses[] = new Clause([$base_key => ['^isset']]);
|
||||
} else {
|
||||
$clauses[] = new Clause([$base_key => ['^!empty']]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$clauses[] = new Clause([$var => [$type]]);
|
||||
}
|
||||
}
|
||||
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
return [new Clause([], true)];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function breakUpPathIntoParts($path)
|
||||
{
|
||||
if (isset(self::$broken_paths[$path])) {
|
||||
return self::$broken_paths[$path];
|
||||
}
|
||||
|
||||
$chars = str_split($path);
|
||||
|
||||
$string_char = null;
|
||||
$escape_char = false;
|
||||
|
||||
$parts = [''];
|
||||
$parts_offset = 0;
|
||||
|
||||
for ($i = 0, $char_count = count($chars); $i < $char_count; ++$i) {
|
||||
$char = $chars[$i];
|
||||
|
||||
if ($string_char) {
|
||||
if ($char === $string_char && !$escape_char) {
|
||||
$string_char = null;
|
||||
}
|
||||
|
||||
if ($char === '\\') {
|
||||
$escape_char = !$escape_char;
|
||||
}
|
||||
|
||||
$parts[$parts_offset] .= $char;
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($char) {
|
||||
case '[':
|
||||
case ']':
|
||||
$parts_offset++;
|
||||
$parts[$parts_offset] = $char;
|
||||
$parts_offset++;
|
||||
continue;
|
||||
|
||||
case '\'':
|
||||
case '"':
|
||||
if (!isset($parts[$parts_offset])) {
|
||||
$parts[$parts_offset] = '';
|
||||
}
|
||||
$parts[$parts_offset] .= $char;
|
||||
$string_char = $char;
|
||||
|
||||
continue;
|
||||
|
||||
case '-':
|
||||
if ($i < $char_count - 1 && $chars[$i + 1] === '>') {
|
||||
++$i;
|
||||
|
||||
$parts_offset++;
|
||||
$parts[$parts_offset] = '->';
|
||||
$parts_offset++;
|
||||
continue;
|
||||
}
|
||||
// fall through
|
||||
|
||||
default:
|
||||
if (!isset($parts[$parts_offset])) {
|
||||
$parts[$parts_offset] = '';
|
||||
}
|
||||
$parts[$parts_offset] .= $char;
|
||||
}
|
||||
}
|
||||
|
||||
self::$broken_paths[$path] = $parts;
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Negates a set of clauses
|
||||
* negateClauses([$a || $b]) => !$a && !$b
|
||||
* negateClauses([$a, $b]) => !$a || !$b
|
||||
* negateClauses([$a, $b || $c]) =>
|
||||
* (!$a || !$b) &&
|
||||
* (!$a || !$c)
|
||||
* negateClauses([$a, $b || $c, $d || $e || $f]) =>
|
||||
* (!$a || !$b || !$d) &&
|
||||
* (!$a || !$b || !$e) &&
|
||||
* (!$a || !$b || !$f) &&
|
||||
* (!$a || !$c || !$d) &&
|
||||
* (!$a || !$c || !$e) &&
|
||||
* (!$a || !$c || !$f)
|
||||
*
|
||||
* @param array<int, Clause> $clauses
|
||||
*
|
||||
* @return array<int, Clause>
|
||||
*/
|
||||
public static function negateFormula(array $clauses)
|
||||
{
|
||||
foreach ($clauses as $clause) {
|
||||
self::calculateNegation($clause);
|
||||
}
|
||||
|
||||
return self::groupImpossibilities($clauses);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Clause $clause
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function calculateNegation(Clause $clause)
|
||||
{
|
||||
if ($clause->impossibilities !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$impossibilities = [];
|
||||
|
||||
foreach ($clause->possibilities as $var_id => $possiblity) {
|
||||
$impossibility = [];
|
||||
|
||||
foreach ($possiblity as $type) {
|
||||
if ($type[0] !== '^' && (!isset($type[1]) || $type[1] !== '^')) {
|
||||
$impossibility[] = TypeChecker::negateType($type);
|
||||
}
|
||||
}
|
||||
|
||||
if ($impossibility) {
|
||||
$impossibilities[$var_id] = $impossibility;
|
||||
}
|
||||
}
|
||||
|
||||
$clause->impossibilities = $impossibilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* This looks to see if there are any clauses in one formula that contradict
|
||||
* clauses in another formula, or clauses that duplicate previous clauses
|
||||
@ -283,7 +36,7 @@ class AlgebraChecker
|
||||
PhpParser\Node $stmt,
|
||||
array $new_assigned_var_ids
|
||||
) {
|
||||
$negated_formula2 = self::negateFormula($formula2);
|
||||
$negated_formula2 = Algebra::negateFormula($formula2);
|
||||
|
||||
// remove impossible types
|
||||
foreach ($negated_formula2 as $clause_a) {
|
||||
@ -332,7 +85,8 @@ class AlgebraChecker
|
||||
if ($clause_a_contains_b_possibilities) {
|
||||
if (IssueBuffer::accepts(
|
||||
new ParadoxicalCondition(
|
||||
'Encountered a paradox when evaluating the conditional',
|
||||
'Encountered a paradox when evaluating the conditionals ('
|
||||
. $clause_a . ') and (' . $clause_b . ')',
|
||||
new CodeLocation($statements_checker, $stmt)
|
||||
),
|
||||
$statements_checker->getSuppressedIssues()
|
||||
@ -345,308 +99,4 @@ class AlgebraChecker
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a very simple simplification heuristic
|
||||
* for CNF formulae.
|
||||
*
|
||||
* It simplifies formulae:
|
||||
* ($a) && ($a || $b) => $a
|
||||
* (!$a) && (!$b) && ($a || $b || $c) => $c
|
||||
*
|
||||
* @param array<int, Clause> $clauses
|
||||
*
|
||||
* @return array<int, Clause>
|
||||
*/
|
||||
public static function simplifyCNF(array $clauses)
|
||||
{
|
||||
$cloned_clauses = [];
|
||||
|
||||
// avoid strict duplicates
|
||||
foreach ($clauses as $clause) {
|
||||
$cloned_clauses[$clause->getHash()] = clone $clause;
|
||||
}
|
||||
|
||||
// remove impossible types
|
||||
foreach ($cloned_clauses as $clause_a) {
|
||||
if (count($clause_a->possibilities) !== 1 || count(array_values($clause_a->possibilities)[0]) !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$clause_a->reconcilable || $clause_a->wedge) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$clause_var = array_keys($clause_a->possibilities)[0];
|
||||
$only_type = array_pop(array_values($clause_a->possibilities)[0]);
|
||||
$negated_clause_type = TypeChecker::negateType($only_type);
|
||||
|
||||
foreach ($cloned_clauses as $clause_b) {
|
||||
if ($clause_a === $clause_b || !$clause_b->reconcilable || $clause_b->wedge) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($clause_b->possibilities[$clause_var]) &&
|
||||
in_array($negated_clause_type, $clause_b->possibilities[$clause_var], true)
|
||||
) {
|
||||
$clause_b->possibilities[$clause_var] = array_filter(
|
||||
$clause_b->possibilities[$clause_var],
|
||||
/**
|
||||
* @param string $possible_type
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function ($possible_type) use ($negated_clause_type) {
|
||||
return $possible_type !== $negated_clause_type;
|
||||
}
|
||||
);
|
||||
|
||||
if (count($clause_b->possibilities[$clause_var]) === 0) {
|
||||
unset($clause_b->possibilities[$clause_var]);
|
||||
$clause_b->impossibilities = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$deduped_clauses = [];
|
||||
|
||||
// avoid strict duplicates
|
||||
foreach ($cloned_clauses as $clause) {
|
||||
$deduped_clauses[$clause->getHash()] = clone $clause;
|
||||
}
|
||||
|
||||
$deduped_clauses = array_filter(
|
||||
$deduped_clauses,
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
function (Clause $clause) {
|
||||
return (bool)count($clause->possibilities);
|
||||
}
|
||||
);
|
||||
|
||||
$simplified_clauses = [];
|
||||
|
||||
foreach ($deduped_clauses as $clause_a) {
|
||||
$is_redundant = false;
|
||||
|
||||
foreach ($deduped_clauses as $clause_b) {
|
||||
if ($clause_a === $clause_b || !$clause_b->reconcilable || $clause_b->wedge) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($clause_a->contains($clause_b)) {
|
||||
$is_redundant = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$is_redundant) {
|
||||
$simplified_clauses[] = $clause_a;
|
||||
}
|
||||
}
|
||||
|
||||
return $simplified_clauses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for clauses with only one possible value
|
||||
*
|
||||
* @param array<int, Clause> $clauses
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function getTruthsFromFormula(array $clauses)
|
||||
{
|
||||
$truths = [];
|
||||
|
||||
if (empty($clauses)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($clauses as $clause) {
|
||||
if (!$clause->reconcilable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($clause->possibilities as $var => $possible_types) {
|
||||
// if there's only one possible type, return it
|
||||
if (count($clause->possibilities) === 1 && count($possible_types) === 1) {
|
||||
if (isset($truths[$var])) {
|
||||
$truths[$var] .= '&' . array_pop($possible_types);
|
||||
} else {
|
||||
$truths[$var] = array_pop($possible_types);
|
||||
}
|
||||
} elseif (count($clause->possibilities) === 1) {
|
||||
$with_brackets = 0;
|
||||
|
||||
// if there's only one active clause, return all the non-negation clause members ORed together
|
||||
$things_that_can_be_said = array_filter(
|
||||
$possible_types,
|
||||
/**
|
||||
* @param string $possible_type
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @psalm-suppress MixedOperand
|
||||
*/
|
||||
function ($possible_type) use (&$with_brackets) {
|
||||
$with_brackets += (int) (strpos($possible_type, '(') > 0);
|
||||
return $possible_type[0] !== '!';
|
||||
}
|
||||
);
|
||||
|
||||
if ($things_that_can_be_said && count($things_that_can_be_said) === count($possible_types)) {
|
||||
$things_that_can_be_said = array_unique($things_that_can_be_said);
|
||||
|
||||
if ($with_brackets > 1) {
|
||||
$bracket_groups = [];
|
||||
|
||||
$removed = 0;
|
||||
|
||||
foreach ($things_that_can_be_said as $i => $t) {
|
||||
if (preg_match('/^\^(int|string|float)\(/', $t, $matches)) {
|
||||
$options = substr($t, strlen((string) $matches[0]), -1);
|
||||
|
||||
if (!isset($bracket_groups[(string) $matches[1]])) {
|
||||
$bracket_groups[(string) $matches[1]] = $options;
|
||||
} else {
|
||||
$bracket_groups[(string) $matches[1]] .= ',' . $options;
|
||||
}
|
||||
|
||||
array_splice($things_that_can_be_said, $i - $removed, 1);
|
||||
$removed++;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($bracket_groups as $type => $options) {
|
||||
$things_that_can_be_said[] = '^' . $type . '(' . $options . ')';
|
||||
}
|
||||
}
|
||||
|
||||
$truths[$var] = implode('|', $things_that_can_be_said);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $truths;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, Clause> $clauses
|
||||
*
|
||||
* @return array<int, Clause>
|
||||
*/
|
||||
protected static function groupImpossibilities(array $clauses)
|
||||
{
|
||||
$clause = array_pop($clauses);
|
||||
|
||||
$new_clauses = [];
|
||||
|
||||
if (!empty($clauses)) {
|
||||
$grouped_clauses = self::groupImpossibilities($clauses);
|
||||
|
||||
if (count($grouped_clauses) > 800) {
|
||||
// too many impossibilities
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($grouped_clauses as $grouped_clause) {
|
||||
if ($clause->impossibilities === null) {
|
||||
throw new \UnexpectedValueException('$clause->impossibilities should not be null');
|
||||
}
|
||||
|
||||
foreach ($clause->impossibilities as $var => $impossible_types) {
|
||||
foreach ($impossible_types as $impossible_type) {
|
||||
$new_clause_possibilities = $grouped_clause->possibilities;
|
||||
|
||||
if (isset($grouped_clause->possibilities[$var])
|
||||
&& !in_array($impossible_type, $new_clause_possibilities[$var])
|
||||
) {
|
||||
$new_clause_possibilities[$var][] = $impossible_type;
|
||||
} else {
|
||||
$new_clause_possibilities[$var] = [$impossible_type];
|
||||
}
|
||||
|
||||
$new_clause = new Clause($new_clause_possibilities);
|
||||
|
||||
//$new_clause->reconcilable = $clause->reconcilable;
|
||||
|
||||
$new_clauses[] = $new_clause;
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($clause && !$clause->wedge) {
|
||||
if ($clause->impossibilities === null) {
|
||||
throw new \UnexpectedValueException('$clause->impossibilities should not be null');
|
||||
}
|
||||
|
||||
foreach ($clause->impossibilities as $var => $impossible_types) {
|
||||
foreach ($impossible_types as $impossible_type) {
|
||||
$new_clause = new Clause([$var => [$impossible_type]]);
|
||||
//$new_clause->reconcilable = $clause->reconcilable;
|
||||
$new_clauses[] = $new_clause;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $new_clauses;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, Clause> $left_clauses
|
||||
* @param array<int, Clause> $right_clauses
|
||||
*
|
||||
* @return array<int, Clause>
|
||||
*/
|
||||
public static function combineOredClauses(array $left_clauses, array $right_clauses)
|
||||
{
|
||||
$clauses = [];
|
||||
|
||||
$all_wedges = true;
|
||||
|
||||
foreach ($left_clauses as $left_clause) {
|
||||
foreach ($right_clauses as $right_clause) {
|
||||
$possibilities = [];
|
||||
|
||||
$can_reconcile = true;
|
||||
|
||||
$all_wedges = $all_wedges && $left_clause->wedge && $right_clause->wedge;
|
||||
|
||||
if ($left_clause->wedge ||
|
||||
$right_clause->wedge ||
|
||||
!$left_clause->reconcilable ||
|
||||
!$right_clause->reconcilable
|
||||
) {
|
||||
$can_reconcile = false;
|
||||
}
|
||||
|
||||
foreach ($left_clause->possibilities as $var => $possible_types) {
|
||||
if (isset($possibilities[$var])) {
|
||||
$possibilities[$var] = array_merge($possibilities[$var], $possible_types);
|
||||
} else {
|
||||
$possibilities[$var] = $possible_types;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($right_clause->possibilities as $var => $possible_types) {
|
||||
if (isset($possibilities[$var])) {
|
||||
$possibilities[$var] = array_merge($possibilities[$var], $possible_types);
|
||||
} else {
|
||||
$possibilities[$var] = $possible_types;
|
||||
}
|
||||
}
|
||||
|
||||
$clauses[] = new Clause($possibilities, false, $can_reconcile);
|
||||
}
|
||||
}
|
||||
|
||||
if ($all_wedges) {
|
||||
return [new Clause([], true)];
|
||||
}
|
||||
|
||||
return $clauses;
|
||||
}
|
||||
}
|
||||
|
@ -73,7 +73,7 @@ class DoChecker
|
||||
}
|
||||
}
|
||||
|
||||
$while_clauses = AlgebraChecker::getFormula(
|
||||
$while_clauses = \Psalm\Type\Algebra::getFormula(
|
||||
$stmt->cond,
|
||||
$context->self,
|
||||
$statements_checker
|
||||
@ -103,7 +103,7 @@ class DoChecker
|
||||
$while_clauses = [new Clause([], true)];
|
||||
}
|
||||
|
||||
$reconcilable_while_types = AlgebraChecker::getTruthsFromFormula($while_clauses);
|
||||
$reconcilable_while_types = \Psalm\Type\Algebra::getTruthsFromFormula($while_clauses);
|
||||
|
||||
if ($reconcilable_while_types) {
|
||||
$changed_var_ids = [];
|
||||
|
@ -15,6 +15,7 @@ use Psalm\IssueBuffer;
|
||||
use Psalm\Scope\IfScope;
|
||||
use Psalm\Scope\LoopScope;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Algebra;
|
||||
use Psalm\Type\Reconciler;
|
||||
|
||||
class IfChecker
|
||||
@ -100,8 +101,6 @@ class IfChecker
|
||||
$if_context->branch_point = $if_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
|
||||
}
|
||||
|
||||
$if_context->parent_context = $context;
|
||||
|
||||
// we need to clone the current context so our ongoing updates to $context don't mess with elseif/else blocks
|
||||
$original_context = clone $context;
|
||||
|
||||
@ -190,7 +189,7 @@ class IfChecker
|
||||
}
|
||||
}
|
||||
|
||||
$if_clauses = AlgebraChecker::getFormula(
|
||||
$if_clauses = Algebra::getFormula(
|
||||
$stmt->cond,
|
||||
$context->self,
|
||||
$statements_checker
|
||||
@ -229,23 +228,23 @@ class IfChecker
|
||||
|
||||
// if we have assignments in the if, we may have duplicate clauses
|
||||
if ($cond_assigned_var_ids) {
|
||||
$if_clauses = AlgebraChecker::simplifyCNF($if_clauses);
|
||||
$if_clauses = Algebra::simplifyCNF($if_clauses);
|
||||
}
|
||||
|
||||
$if_context->clauses = AlgebraChecker::simplifyCNF(array_merge($context->clauses, $if_clauses));
|
||||
$if_context->clauses = Algebra::simplifyCNF(array_merge($context->clauses, $if_clauses));
|
||||
|
||||
// define this before we alter local claues after reconciliation
|
||||
$if_scope->reasonable_clauses = $if_context->clauses;
|
||||
|
||||
$if_scope->negated_clauses = AlgebraChecker::simplifyCNF(AlgebraChecker::negateFormula($if_clauses));
|
||||
$if_scope->negated_clauses = Algebra::negateFormula($if_clauses);
|
||||
|
||||
$if_scope->negated_types = AlgebraChecker::getTruthsFromFormula(
|
||||
AlgebraChecker::simplifyCNF(
|
||||
$if_scope->negated_types = Algebra::getTruthsFromFormula(
|
||||
Algebra::simplifyCNF(
|
||||
array_merge($context->clauses, $if_scope->negated_clauses)
|
||||
)
|
||||
);
|
||||
|
||||
$reconcilable_if_types = AlgebraChecker::getTruthsFromFormula($if_context->clauses);
|
||||
$reconcilable_if_types = Algebra::getTruthsFromFormula($if_context->clauses);
|
||||
|
||||
// if the if has an || in the conditional, we cannot easily reason about it
|
||||
if ($reconcilable_if_types) {
|
||||
@ -528,6 +527,10 @@ class IfChecker
|
||||
$has_break_statement = $final_actions === [ScopeChecker::ACTION_BREAK];
|
||||
$has_continue_statement = $final_actions === [ScopeChecker::ACTION_CONTINUE];
|
||||
|
||||
if (!$has_ending_statements) {
|
||||
$if_context->parent_context = $outer_context;
|
||||
}
|
||||
|
||||
$if_scope->final_actions = $final_actions;
|
||||
|
||||
$project_checker = $statements_checker->getFileChecker()->project_checker;
|
||||
@ -664,7 +667,7 @@ class IfChecker
|
||||
$mic_drop = true;
|
||||
}
|
||||
|
||||
$outer_context->clauses = AlgebraChecker::simplifyCNF(
|
||||
$outer_context->clauses = Algebra::simplifyCNF(
|
||||
array_merge($outer_context->clauses, $if_scope->negated_clauses)
|
||||
);
|
||||
}
|
||||
@ -754,9 +757,9 @@ class IfChecker
|
||||
|
||||
$entry_clauses = array_merge($original_context->clauses, $if_scope->negated_clauses);
|
||||
|
||||
if ($if_scope->negated_types) {
|
||||
$changed_var_ids = [];
|
||||
$changed_var_ids = [];
|
||||
|
||||
if ($if_scope->negated_types) {
|
||||
$elseif_vars_reconciled = Reconciler::reconcileKeyedTypes(
|
||||
$if_scope->negated_types,
|
||||
$elseif_context->vars_in_scope,
|
||||
@ -824,7 +827,7 @@ class IfChecker
|
||||
}
|
||||
}
|
||||
|
||||
$elseif_clauses = AlgebraChecker::getFormula(
|
||||
$elseif_clauses = Algebra::getFormula(
|
||||
$elseif->cond,
|
||||
$statements_checker->getFQCLN(),
|
||||
$statements_checker
|
||||
@ -879,15 +882,15 @@ class IfChecker
|
||||
$new_assigned_var_ids
|
||||
);
|
||||
|
||||
$elseif_context->clauses = AlgebraChecker::simplifyCNF(
|
||||
$elseif_context->clauses = Algebra::simplifyCNF(
|
||||
array_merge(
|
||||
$entry_clauses,
|
||||
$elseif_clauses
|
||||
)
|
||||
);
|
||||
|
||||
$reconcilable_elseif_types = AlgebraChecker::getTruthsFromFormula($elseif_context->clauses);
|
||||
$negated_elseif_types = AlgebraChecker::getTruthsFromFormula(AlgebraChecker::negateFormula($elseif_clauses));
|
||||
$reconcilable_elseif_types = Algebra::getTruthsFromFormula($elseif_context->clauses);
|
||||
$negated_elseif_types = Algebra::getTruthsFromFormula(Algebra::negateFormula($elseif_clauses));
|
||||
|
||||
$all_negated_vars = array_unique(
|
||||
array_merge(
|
||||
@ -907,7 +910,7 @@ class IfChecker
|
||||
}
|
||||
}
|
||||
|
||||
$changed_var_ids = [];
|
||||
$changed_var_ids = $changed_var_ids ?: [];
|
||||
|
||||
// if the elseif has an || in the conditional, we cannot easily reason about it
|
||||
if ($reconcilable_elseif_types) {
|
||||
@ -1051,7 +1054,7 @@ class IfChecker
|
||||
}
|
||||
|
||||
if ($if_scope->reasonable_clauses && $elseif_clauses) {
|
||||
$if_scope->reasonable_clauses = AlgebraChecker::combineOredClauses(
|
||||
$if_scope->reasonable_clauses = Algebra::combineOredClauses(
|
||||
$if_scope->reasonable_clauses,
|
||||
$elseif_clauses
|
||||
);
|
||||
@ -1149,7 +1152,7 @@ class IfChecker
|
||||
|
||||
$if_scope->negated_clauses = array_merge(
|
||||
$if_scope->negated_clauses,
|
||||
AlgebraChecker::negateFormula($elseif_clauses)
|
||||
Algebra::negateFormula($elseif_clauses)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1174,14 +1177,14 @@ class IfChecker
|
||||
|
||||
$original_context = clone $else_context;
|
||||
|
||||
$else_context->clauses = AlgebraChecker::simplifyCNF(
|
||||
$else_context->clauses = Algebra::simplifyCNF(
|
||||
array_merge(
|
||||
$else_context->clauses,
|
||||
$if_scope->negated_clauses
|
||||
)
|
||||
);
|
||||
|
||||
$else_types = AlgebraChecker::getTruthsFromFormula($else_context->clauses);
|
||||
$else_types = Algebra::getTruthsFromFormula($else_context->clauses);
|
||||
|
||||
if ($else_types) {
|
||||
$changed_var_ids = [];
|
||||
@ -1320,7 +1323,7 @@ class IfChecker
|
||||
}
|
||||
}
|
||||
} elseif ($if_scope->reasonable_clauses) {
|
||||
$outer_context->clauses = AlgebraChecker::simplifyCNF(
|
||||
$outer_context->clauses = Algebra::simplifyCNF(
|
||||
array_merge(
|
||||
$if_scope->reasonable_clauses,
|
||||
$original_context->clauses
|
||||
|
@ -2,7 +2,6 @@
|
||||
namespace Psalm\Checker\Statements\Block;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Checker\AlgebraChecker;
|
||||
use Psalm\Checker\ScopeChecker;
|
||||
use Psalm\Checker\Statements\ExpressionChecker;
|
||||
use Psalm\Checker\StatementsChecker;
|
||||
@ -12,6 +11,7 @@ use Psalm\Context;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Scope\LoopScope;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Algebra;
|
||||
use Psalm\Type\Reconciler;
|
||||
|
||||
class LoopChecker
|
||||
@ -58,7 +58,7 @@ class LoopChecker
|
||||
foreach ($pre_conditions as $pre_condition) {
|
||||
$pre_condition_clauses = array_merge(
|
||||
$pre_condition_clauses,
|
||||
AlgebraChecker::getFormula(
|
||||
Algebra::getFormula(
|
||||
$pre_condition,
|
||||
$loop_scope->loop_context->self,
|
||||
$statements_checker
|
||||
@ -358,8 +358,8 @@ class LoopChecker
|
||||
if ($pre_conditions && $pre_condition_clauses && !ScopeChecker::doesEverBreak($stmts)) {
|
||||
// if the loop contains an assertion and there are no break statements, we can negate that assertion
|
||||
// and apply it to the current context
|
||||
$negated_pre_condition_types = AlgebraChecker::getTruthsFromFormula(
|
||||
AlgebraChecker::negateFormula($pre_condition_clauses)
|
||||
$negated_pre_condition_types = Algebra::getTruthsFromFormula(
|
||||
Algebra::negateFormula($pre_condition_clauses)
|
||||
);
|
||||
|
||||
if ($negated_pre_condition_types) {
|
||||
@ -477,11 +477,11 @@ class LoopChecker
|
||||
|
||||
$asserted_var_ids = Context::getNewOrUpdatedVarIds($outer_context, $loop_context);
|
||||
|
||||
$loop_context->clauses = AlgebraChecker::simplifyCNF(
|
||||
$loop_context->clauses = Algebra::simplifyCNF(
|
||||
array_merge($outer_context->clauses, $pre_condition_clauses)
|
||||
);
|
||||
|
||||
$reconcilable_while_types = AlgebraChecker::getTruthsFromFormula($loop_context->clauses);
|
||||
$reconcilable_while_types = Algebra::getTruthsFromFormula($loop_context->clauses);
|
||||
|
||||
// if the while has an or as the main component, we cannot safely reason about it
|
||||
if ($pre_condition instanceof PhpParser\Node\Expr\BinaryOp &&
|
||||
|
@ -12,6 +12,7 @@ use Psalm\Issue\ContinueOutsideLoop;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Scope\LoopScope;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Algebra;
|
||||
use Psalm\Type\Reconciler;
|
||||
|
||||
class SwitchChecker
|
||||
@ -133,7 +134,7 @@ class SwitchChecker
|
||||
|
||||
$fake_equality = new PhpParser\Node\Expr\BinaryOp\Equal($switch_condition, $case->cond);
|
||||
|
||||
$case_clauses = AlgebraChecker::getFormula(
|
||||
$case_clauses = Algebra::getFormula(
|
||||
$fake_equality,
|
||||
$context->self,
|
||||
$statements_checker
|
||||
@ -142,9 +143,9 @@ class SwitchChecker
|
||||
// this will see whether any of the clauses in set A conflict with the clauses in set B
|
||||
AlgebraChecker::checkForParadox($context->clauses, $case_clauses, $statements_checker, $stmt->cond, []);
|
||||
|
||||
$case_context->clauses = AlgebraChecker::simplifyCNF(array_merge($context->clauses, $case_clauses));
|
||||
$case_context->clauses = Algebra::simplifyCNF(array_merge($context->clauses, $case_clauses));
|
||||
|
||||
$reconcilable_if_types = AlgebraChecker::getTruthsFromFormula($case_context->clauses);
|
||||
$reconcilable_if_types = Algebra::getTruthsFromFormula($case_context->clauses);
|
||||
|
||||
// if the if has an || in the conditional, we cannot easily reason about it
|
||||
if ($reconcilable_if_types) {
|
||||
|
@ -59,11 +59,13 @@ class AssertionFinder
|
||||
return $if_types;
|
||||
}
|
||||
|
||||
if ($var_name = ExpressionChecker::getArrayVarId(
|
||||
$var_name = ExpressionChecker::getArrayVarId(
|
||||
$conditional,
|
||||
$this_class_name,
|
||||
$source
|
||||
)) {
|
||||
);
|
||||
|
||||
if ($var_name) {
|
||||
$if_types[$var_name] = '!falsy';
|
||||
|
||||
return $if_types;
|
||||
@ -90,7 +92,7 @@ class AssertionFinder
|
||||
$source
|
||||
);
|
||||
|
||||
return TypeChecker::negateTypes($if_types_to_negate);
|
||||
return \Psalm\Type\Algebra::negateTypes($if_types_to_negate);
|
||||
}
|
||||
|
||||
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical ||
|
||||
@ -253,7 +255,7 @@ class AssertionFinder
|
||||
$notif_types = self::getAssertions($base_conditional, $this_class_name, $source);
|
||||
|
||||
if (count($notif_types) === 1) {
|
||||
$if_types = TypeChecker::negateTypes($notif_types);
|
||||
$if_types = \Psalm\Type\Algebra::negateTypes($notif_types);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -523,8 +523,8 @@ class AssignmentChecker
|
||||
} elseif ($stmt instanceof PhpParser\Node\Expr\AssignOp\Div
|
||||
&& $var_type
|
||||
&& $expr_type
|
||||
&& $var_type->hasNumericType()
|
||||
&& $expr_type->hasNumericType()
|
||||
&& $var_type->hasDefinitelyNumericType()
|
||||
&& $expr_type->hasDefinitelyNumericType()
|
||||
&& $array_var_id
|
||||
) {
|
||||
$context->vars_in_scope[$array_var_id] = Type::combineUnionTypes(Type::getFloat(), Type::getInt());
|
||||
|
@ -2,7 +2,6 @@
|
||||
namespace Psalm\Checker\Statements\Expression;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Checker\AlgebraChecker;
|
||||
use Psalm\Checker\FunctionLikeChecker;
|
||||
use Psalm\Checker\Statements\Expression\Assignment\ArrayAssignmentChecker;
|
||||
use Psalm\Checker\Statements\ExpressionChecker;
|
||||
@ -22,6 +21,7 @@ use Psalm\Issue\PossiblyNullOperand;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\StatementsSource;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Algebra;
|
||||
use Psalm\Type\Atomic\ObjectLike;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Atomic\TFalse;
|
||||
@ -55,7 +55,7 @@ class BinaryOpChecker
|
||||
} elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd ||
|
||||
$stmt instanceof PhpParser\Node\Expr\BinaryOp\LogicalAnd
|
||||
) {
|
||||
$if_clauses = AlgebraChecker::getFormula(
|
||||
$if_clauses = Algebra::getFormula(
|
||||
$stmt->left,
|
||||
$statements_checker->getFQCLN(),
|
||||
$statements_checker
|
||||
@ -92,9 +92,9 @@ class BinaryOpChecker
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
|
||||
$simplified_clauses = AlgebraChecker::simplifyCNF(array_merge($context->clauses, $if_clauses));
|
||||
$simplified_clauses = Algebra::simplifyCNF(array_merge($context->clauses, $if_clauses));
|
||||
|
||||
$left_type_assertions = AlgebraChecker::getTruthsFromFormula($simplified_clauses);
|
||||
$left_type_assertions = Algebra::getTruthsFromFormula($simplified_clauses);
|
||||
|
||||
$changed_var_ids = [];
|
||||
|
||||
@ -173,20 +173,20 @@ class BinaryOpChecker
|
||||
|
||||
$new_referenced_var_ids = array_diff_key($new_referenced_var_ids, $new_assigned_var_ids);
|
||||
|
||||
$left_clauses = AlgebraChecker::getFormula(
|
||||
$left_clauses = Algebra::getFormula(
|
||||
$stmt->left,
|
||||
$statements_checker->getFQCLN(),
|
||||
$statements_checker
|
||||
);
|
||||
|
||||
$rhs_clauses = AlgebraChecker::simplifyCNF(
|
||||
$rhs_clauses = Algebra::simplifyCNF(
|
||||
array_merge(
|
||||
$context->clauses,
|
||||
AlgebraChecker::negateFormula($left_clauses)
|
||||
Algebra::negateFormula($left_clauses)
|
||||
)
|
||||
);
|
||||
|
||||
$negated_type_assertions = AlgebraChecker::getTruthsFromFormula($rhs_clauses);
|
||||
$negated_type_assertions = Algebra::getTruthsFromFormula($rhs_clauses);
|
||||
|
||||
$changed_var_ids = [];
|
||||
|
||||
@ -271,19 +271,19 @@ class BinaryOpChecker
|
||||
} elseif ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Coalesce) {
|
||||
$t_if_context = clone $context;
|
||||
|
||||
$if_clauses = AlgebraChecker::getFormula(
|
||||
$if_clauses = Algebra::getFormula(
|
||||
$stmt,
|
||||
$statements_checker->getFQCLN(),
|
||||
$statements_checker
|
||||
);
|
||||
|
||||
$ternary_clauses = AlgebraChecker::simplifyCNF(array_merge($context->clauses, $if_clauses));
|
||||
$ternary_clauses = Algebra::simplifyCNF(array_merge($context->clauses, $if_clauses));
|
||||
|
||||
$negated_clauses = AlgebraChecker::negateFormula($if_clauses);
|
||||
$negated_clauses = Algebra::negateFormula($if_clauses);
|
||||
|
||||
$negated_if_types = AlgebraChecker::getTruthsFromFormula($negated_clauses);
|
||||
$negated_if_types = Algebra::getTruthsFromFormula($negated_clauses);
|
||||
|
||||
$reconcilable_if_types = AlgebraChecker::getTruthsFromFormula($ternary_clauses);
|
||||
$reconcilable_if_types = Algebra::getTruthsFromFormula($ternary_clauses);
|
||||
|
||||
$changed_var_ids = [];
|
||||
|
||||
@ -539,7 +539,7 @@ class BinaryOpChecker
|
||||
&& $left_type
|
||||
&& $right_type
|
||||
&& ($left_type->isMixedNotFromIsset() || $right_type->isMixedNotFromIsset())
|
||||
&& ($left_type->hasNumericType() || $right_type->hasNumericType())
|
||||
&& ($left_type->hasDefinitelyNumericType() || $right_type->hasDefinitelyNumericType())
|
||||
) {
|
||||
$source_checker = $statements_source->getSource();
|
||||
if ($source_checker instanceof FunctionLikeChecker
|
||||
@ -659,6 +659,8 @@ class BinaryOpChecker
|
||||
$candidate_result_type = self::analyzeNonDivOperands(
|
||||
$statements_source,
|
||||
$codebase,
|
||||
$config,
|
||||
$context,
|
||||
$left,
|
||||
$right,
|
||||
$parent,
|
||||
@ -735,6 +737,7 @@ class BinaryOpChecker
|
||||
/**
|
||||
* @param StatementsSource|null $statements_source
|
||||
* @param \Psalm\Codebase|null $codebase
|
||||
* @param Context|null $context
|
||||
* @param string[] &$invalid_left_messages
|
||||
* @param string[] &$invalid_right_messages
|
||||
* @param bool &$has_valid_left_operand
|
||||
@ -745,6 +748,8 @@ class BinaryOpChecker
|
||||
public static function analyzeNonDivOperands(
|
||||
$statements_source,
|
||||
$codebase,
|
||||
Config $config,
|
||||
$context,
|
||||
PhpParser\Node\Expr $left,
|
||||
PhpParser\Node\Expr $right,
|
||||
PhpParser\Node $parent,
|
||||
|
@ -2,7 +2,6 @@
|
||||
namespace Psalm\Checker\Statements\Expression\Call;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Checker\AlgebraChecker;
|
||||
use Psalm\Checker\FunctionChecker;
|
||||
use Psalm\Checker\FunctionLikeChecker;
|
||||
use Psalm\Checker\ProjectChecker;
|
||||
@ -24,6 +23,7 @@ use Psalm\Type\Atomic\TMixed;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Atomic\TNull;
|
||||
use Psalm\Type\Atomic\TString;
|
||||
use Psalm\Type\Algebra;
|
||||
use Psalm\Type\Reconciler;
|
||||
|
||||
class FunctionCallChecker extends \Psalm\Checker\Statements\Expression\CallChecker
|
||||
@ -349,15 +349,15 @@ class FunctionCallChecker extends \Psalm\Checker\Statements\Expression\CallCheck
|
||||
$function->parts === ['assert'] &&
|
||||
isset($stmt->args[0])
|
||||
) {
|
||||
$assert_clauses = AlgebraChecker::getFormula(
|
||||
$assert_clauses = \Psalm\Type\Algebra::getFormula(
|
||||
$stmt->args[0]->value,
|
||||
$statements_checker->getFQCLN(),
|
||||
$statements_checker
|
||||
);
|
||||
|
||||
$simplified_clauses = AlgebraChecker::simplifyCNF(array_merge($context->clauses, $assert_clauses));
|
||||
$simplified_clauses = Algebra::simplifyCNF(array_merge($context->clauses, $assert_clauses));
|
||||
|
||||
$assert_type_assertions = AlgebraChecker::getTruthsFromFormula($simplified_clauses);
|
||||
$assert_type_assertions = Algebra::getTruthsFromFormula($simplified_clauses);
|
||||
|
||||
$changed_vars = [];
|
||||
|
||||
|
@ -2,12 +2,12 @@
|
||||
namespace Psalm\Checker\Statements\Expression;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Checker\AlgebraChecker;
|
||||
use Psalm\Checker\Statements\ExpressionChecker;
|
||||
use Psalm\Checker\StatementsChecker;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Algebra;
|
||||
use Psalm\Type\Reconciler;
|
||||
|
||||
class TernaryChecker
|
||||
@ -39,23 +39,23 @@ class TernaryChecker
|
||||
|
||||
$t_if_context = clone $context;
|
||||
|
||||
$if_clauses = AlgebraChecker::getFormula(
|
||||
$if_clauses = \Psalm\Type\Algebra::getFormula(
|
||||
$stmt->cond,
|
||||
$statements_checker->getFQCLN(),
|
||||
$statements_checker
|
||||
);
|
||||
|
||||
$ternary_clauses = AlgebraChecker::simplifyCNF(array_merge($context->clauses, $if_clauses));
|
||||
$ternary_clauses = Algebra::simplifyCNF(array_merge($context->clauses, $if_clauses));
|
||||
|
||||
$negated_clauses = AlgebraChecker::negateFormula($if_clauses);
|
||||
$negated_clauses = Algebra::negateFormula($if_clauses);
|
||||
|
||||
$negated_if_types = AlgebraChecker::getTruthsFromFormula(
|
||||
AlgebraChecker::simplifyCNF(
|
||||
$negated_if_types = Algebra::getTruthsFromFormula(
|
||||
Algebra::simplifyCNF(
|
||||
array_merge($context->clauses, $negated_clauses)
|
||||
)
|
||||
);
|
||||
|
||||
$reconcilable_if_types = AlgebraChecker::getTruthsFromFormula($ternary_clauses);
|
||||
$reconcilable_if_types = Algebra::getTruthsFromFormula($ternary_clauses);
|
||||
|
||||
$changed_var_ids = [];
|
||||
|
||||
|
@ -971,46 +971,6 @@ class TypeChecker
|
||||
return $result_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $types
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function negateTypes(array $types)
|
||||
{
|
||||
return array_map(
|
||||
/**
|
||||
* @param string $type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function ($type) {
|
||||
return self::negateType($type);
|
||||
},
|
||||
$types
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function negateType($type)
|
||||
{
|
||||
if ($type === 'mixed') {
|
||||
return $type;
|
||||
}
|
||||
|
||||
$type_parts = explode('&', (string)$type);
|
||||
|
||||
foreach ($type_parts as &$type_part) {
|
||||
$type_part = $type_part[0] === '!' ? substr($type_part, 1) : '!' . $type_part;
|
||||
}
|
||||
|
||||
return implode('&', $type_parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
|
@ -95,4 +95,45 @@ class Clause
|
||||
return md5($possibility_string) .
|
||||
($this->wedge || !$this->reconcilable ? spl_object_hash($this) : '');
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
return implode(
|
||||
' || ',
|
||||
array_map(
|
||||
/**
|
||||
* @param string $var_id
|
||||
* @param string[] $values
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function ($var_id, $values) {
|
||||
return implode(
|
||||
' || ' ,
|
||||
array_map(
|
||||
/**
|
||||
* @param string $value
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function ($value) use ($var_id) {
|
||||
if ($value === 'falsy') {
|
||||
return '!' . $var_id;
|
||||
}
|
||||
|
||||
if ($value === '!falsy') {
|
||||
return $var_id;
|
||||
}
|
||||
|
||||
return $var_id . '==' . $value;
|
||||
},
|
||||
$values
|
||||
)
|
||||
);
|
||||
},
|
||||
array_keys($this->possibilities),
|
||||
array_values($this->possibilities)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -443,7 +443,7 @@ class Context
|
||||
$clauses_to_keep = [];
|
||||
|
||||
foreach ($clauses as $clause) {
|
||||
\Psalm\Checker\AlgebraChecker::calculateNegation($clause);
|
||||
\Psalm\Type\Algebra::calculateNegation($clause);
|
||||
|
||||
$quoted_remove_var_id = preg_quote($remove_var_id);
|
||||
|
||||
@ -468,7 +468,7 @@ class Context
|
||||
foreach ($clause->possibilities[$remove_var_id] as $type) {
|
||||
// empty and !empty are not definitive for arrays and scalar types
|
||||
if (($type === '!falsy' || $type === 'falsy') &&
|
||||
($new_type->hasArray() || $new_type->hasNumericType())
|
||||
($new_type->hasArray() || $new_type->hasPossiblyNumericType())
|
||||
) {
|
||||
$type_changed = true;
|
||||
break;
|
||||
|
535
src/Psalm/Type/Algebra.php
Normal file
535
src/Psalm/Type/Algebra.php
Normal file
@ -0,0 +1,535 @@
|
||||
<?php
|
||||
namespace Psalm\Type;
|
||||
|
||||
use PhpParser;
|
||||
use Psalm\Checker\Statements\Expression\AssertionFinder;
|
||||
use Psalm\Clause;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\FileSource;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Type\Algebra;
|
||||
|
||||
class Algebra
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $types
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function negateTypes(array $types)
|
||||
{
|
||||
return array_map(
|
||||
/**
|
||||
* @param string $type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
function ($type) {
|
||||
return self::negateType($type);
|
||||
},
|
||||
$types
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private static function negateType($type)
|
||||
{
|
||||
if ($type === 'mixed') {
|
||||
return $type;
|
||||
}
|
||||
|
||||
$type_parts = explode('&', (string)$type);
|
||||
|
||||
foreach ($type_parts as &$type_part) {
|
||||
$type_part = $type_part[0] === '!' ? substr($type_part, 1) : '!' . $type_part;
|
||||
}
|
||||
|
||||
return implode('&', $type_parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PhpParser\Node\Expr $conditional
|
||||
* @param string|null $this_class_name
|
||||
* @param FileSource $source
|
||||
*
|
||||
* @return array<int, Clause>
|
||||
*/
|
||||
public static function getFormula(
|
||||
PhpParser\Node\Expr $conditional,
|
||||
$this_class_name,
|
||||
FileSource $source
|
||||
) {
|
||||
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd ||
|
||||
$conditional instanceof PhpParser\Node\Expr\BinaryOp\LogicalAnd
|
||||
) {
|
||||
$left_assertions = self::getFormula(
|
||||
$conditional->left,
|
||||
$this_class_name,
|
||||
$source
|
||||
);
|
||||
|
||||
$right_assertions = self::getFormula(
|
||||
$conditional->right,
|
||||
$this_class_name,
|
||||
$source
|
||||
);
|
||||
|
||||
return array_merge(
|
||||
$left_assertions,
|
||||
$right_assertions
|
||||
);
|
||||
}
|
||||
|
||||
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr ||
|
||||
$conditional instanceof PhpParser\Node\Expr\BinaryOp\LogicalOr
|
||||
) {
|
||||
// at the moment we only support formulae in CNF
|
||||
|
||||
$left_clauses = self::getFormula(
|
||||
$conditional->left,
|
||||
$this_class_name,
|
||||
$source
|
||||
);
|
||||
|
||||
$right_clauses = self::getFormula(
|
||||
$conditional->right,
|
||||
$this_class_name,
|
||||
$source
|
||||
);
|
||||
|
||||
return self::combineOredClauses($left_clauses, $right_clauses);
|
||||
}
|
||||
|
||||
$assertions = AssertionFinder::getAssertions(
|
||||
$conditional,
|
||||
$this_class_name,
|
||||
$source
|
||||
);
|
||||
|
||||
if ($assertions) {
|
||||
$clauses = [];
|
||||
|
||||
foreach ($assertions as $var => $type) {
|
||||
if ($type === 'isset' || $type === '!empty') {
|
||||
$key_parts = Reconciler::breakUpPathIntoParts($var);
|
||||
|
||||
$base_key = array_shift($key_parts);
|
||||
|
||||
if ($type === 'isset') {
|
||||
$clauses[] = new Clause([$base_key => ['^isset']]);
|
||||
} else {
|
||||
$clauses[] = new Clause([$base_key => ['^!empty']]);
|
||||
}
|
||||
|
||||
while ($key_parts) {
|
||||
$divider = array_shift($key_parts);
|
||||
|
||||
if ($divider === '[') {
|
||||
$array_key = array_shift($key_parts);
|
||||
array_shift($key_parts);
|
||||
|
||||
$new_base_key = $base_key . '[' . $array_key . ']';
|
||||
|
||||
$base_key = $new_base_key;
|
||||
} elseif ($divider === '->') {
|
||||
$property_name = array_shift($key_parts);
|
||||
$new_base_key = $base_key . '->' . $property_name;
|
||||
|
||||
$base_key = $new_base_key;
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Unexpected divider ' . $divider);
|
||||
}
|
||||
|
||||
if ($type === 'isset') {
|
||||
$clauses[] = new Clause([$base_key => ['^isset']]);
|
||||
} else {
|
||||
$clauses[] = new Clause([$base_key => ['^!empty']]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$clauses[] = new Clause([$var => [$type]]);
|
||||
}
|
||||
}
|
||||
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
return [new Clause([], true)];
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a very simple simplification heuristic
|
||||
* for CNF formulae.
|
||||
*
|
||||
* It simplifies formulae:
|
||||
* ($a) && ($a || $b) => $a
|
||||
* (!$a) && (!$b) && ($a || $b || $c) => $c
|
||||
*
|
||||
* @param array<int, Clause> $clauses
|
||||
*
|
||||
* @return array<int, Clause>
|
||||
*/
|
||||
public static function simplifyCNF(array $clauses)
|
||||
{
|
||||
$cloned_clauses = [];
|
||||
|
||||
// avoid strict duplicates
|
||||
foreach ($clauses as $clause) {
|
||||
$unique_clause = clone $clause;
|
||||
foreach ($unique_clause->possibilities as $var_id => $possibilities) {
|
||||
if (count($possibilities)) {
|
||||
$unique_clause->possibilities[$var_id] = array_unique($possibilities);
|
||||
}
|
||||
}
|
||||
$cloned_clauses[$clause->getHash()] = $unique_clause;
|
||||
}
|
||||
|
||||
// remove impossible types
|
||||
foreach ($cloned_clauses as $clause_a) {
|
||||
if (count($clause_a->possibilities) !== 1 || count(array_values($clause_a->possibilities)[0]) !== 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$clause_a->reconcilable || $clause_a->wedge) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$clause_var = array_keys($clause_a->possibilities)[0];
|
||||
$only_type = array_pop(array_values($clause_a->possibilities)[0]);
|
||||
$negated_clause_type = self::negateType($only_type);
|
||||
|
||||
foreach ($cloned_clauses as $clause_b) {
|
||||
if ($clause_a === $clause_b || !$clause_b->reconcilable || $clause_b->wedge) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($clause_b->possibilities[$clause_var]) &&
|
||||
in_array($negated_clause_type, $clause_b->possibilities[$clause_var], true)
|
||||
) {
|
||||
$clause_b->possibilities[$clause_var] = array_filter(
|
||||
$clause_b->possibilities[$clause_var],
|
||||
/**
|
||||
* @param string $possible_type
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function ($possible_type) use ($negated_clause_type) {
|
||||
return $possible_type !== $negated_clause_type;
|
||||
}
|
||||
);
|
||||
|
||||
if (count($clause_b->possibilities[$clause_var]) === 0) {
|
||||
unset($clause_b->possibilities[$clause_var]);
|
||||
$clause_b->impossibilities = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$deduped_clauses = [];
|
||||
|
||||
// avoid strict duplicates
|
||||
foreach ($cloned_clauses as $clause) {
|
||||
$deduped_clauses[$clause->getHash()] = clone $clause;
|
||||
}
|
||||
|
||||
$deduped_clauses = array_filter(
|
||||
$deduped_clauses,
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
function (Clause $clause) {
|
||||
return (bool)count($clause->possibilities);
|
||||
}
|
||||
);
|
||||
|
||||
$simplified_clauses = [];
|
||||
|
||||
foreach ($deduped_clauses as $clause_a) {
|
||||
$is_redundant = false;
|
||||
|
||||
foreach ($deduped_clauses as $clause_b) {
|
||||
if ($clause_a === $clause_b || !$clause_b->reconcilable || $clause_b->wedge) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($clause_a->contains($clause_b)) {
|
||||
$is_redundant = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$is_redundant) {
|
||||
$simplified_clauses[] = $clause_a;
|
||||
}
|
||||
}
|
||||
|
||||
return $simplified_clauses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for clauses with only one possible value
|
||||
*
|
||||
* @param array<int, Clause> $clauses
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function getTruthsFromFormula(array $clauses)
|
||||
{
|
||||
$truths = [];
|
||||
|
||||
if (empty($clauses)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($clauses as $clause) {
|
||||
if (!$clause->reconcilable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($clause->possibilities as $var => $possible_types) {
|
||||
// if there's only one possible type, return it
|
||||
if (count($clause->possibilities) === 1 && count($possible_types) === 1) {
|
||||
if (isset($truths[$var])) {
|
||||
$truths[$var] .= '&' . array_pop($possible_types);
|
||||
} else {
|
||||
$truths[$var] = array_pop($possible_types);
|
||||
}
|
||||
} elseif (count($clause->possibilities) === 1) {
|
||||
$with_brackets = 0;
|
||||
|
||||
// if there's only one active clause, return all the non-negation clause members ORed together
|
||||
$things_that_can_be_said = array_filter(
|
||||
$possible_types,
|
||||
/**
|
||||
* @param string $possible_type
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @psalm-suppress MixedOperand
|
||||
*/
|
||||
function ($possible_type) use (&$with_brackets) {
|
||||
$with_brackets += (int) (strpos($possible_type, '(') > 0);
|
||||
return $possible_type[0] !== '!';
|
||||
}
|
||||
);
|
||||
|
||||
if ($things_that_can_be_said && count($things_that_can_be_said) === count($possible_types)) {
|
||||
$things_that_can_be_said = array_unique($things_that_can_be_said);
|
||||
|
||||
if ($with_brackets > 1) {
|
||||
$bracket_groups = [];
|
||||
|
||||
$removed = 0;
|
||||
|
||||
foreach ($things_that_can_be_said as $i => $t) {
|
||||
if (preg_match('/^\^(int|string|float)\(/', $t, $matches)) {
|
||||
$options = substr($t, strlen((string) $matches[0]), -1);
|
||||
|
||||
if (!isset($bracket_groups[(string) $matches[1]])) {
|
||||
$bracket_groups[(string) $matches[1]] = $options;
|
||||
} else {
|
||||
$bracket_groups[(string) $matches[1]] .= ',' . $options;
|
||||
}
|
||||
|
||||
array_splice($things_that_can_be_said, $i - $removed, 1);
|
||||
$removed++;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($bracket_groups as $type => $options) {
|
||||
$things_that_can_be_said[] = '^' . $type . '(' . $options . ')';
|
||||
}
|
||||
}
|
||||
|
||||
$truths[$var] = implode('|', $things_that_can_be_said);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $truths;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, Clause> $clauses
|
||||
*
|
||||
* @return array<int, Clause>
|
||||
*/
|
||||
private static function groupImpossibilities(array $clauses)
|
||||
{
|
||||
if (count($clauses) > 5000) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$clause = array_shift($clauses);
|
||||
|
||||
$new_clauses = [];
|
||||
|
||||
if ($clauses) {
|
||||
$grouped_clauses = self::groupImpossibilities($clauses);
|
||||
|
||||
if (count($grouped_clauses) > 5000) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($grouped_clauses as $grouped_clause) {
|
||||
if ($clause->impossibilities === null) {
|
||||
throw new \UnexpectedValueException('$clause->impossibilities should not be null');
|
||||
}
|
||||
|
||||
foreach ($clause->impossibilities as $var => $impossible_types) {
|
||||
foreach ($impossible_types as $impossible_type) {
|
||||
$new_clause_possibilities = $grouped_clause->possibilities;
|
||||
|
||||
if (isset($grouped_clause->possibilities[$var])) {
|
||||
$new_clause_possibilities[$var][] = $impossible_type;
|
||||
} else {
|
||||
$new_clause_possibilities[$var] = [$impossible_type];
|
||||
}
|
||||
|
||||
$new_clause = new Clause($new_clause_possibilities);
|
||||
|
||||
//$new_clause->reconcilable = $clause->reconcilable;
|
||||
|
||||
$new_clauses[] = $new_clause;
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($clause && !$clause->wedge) {
|
||||
if ($clause->impossibilities === null) {
|
||||
throw new \UnexpectedValueException('$clause->impossibilities should not be null');
|
||||
}
|
||||
|
||||
foreach ($clause->impossibilities as $var => $impossible_types) {
|
||||
foreach ($impossible_types as $impossible_type) {
|
||||
$new_clause = new Clause([$var => [$impossible_type]]);
|
||||
//$new_clause->reconcilable = $clause->reconcilable;
|
||||
$new_clauses[] = $new_clause;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $new_clauses;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, Clause> $left_clauses
|
||||
* @param array<int, Clause> $right_clauses
|
||||
*
|
||||
* @return array<int, Clause>
|
||||
*/
|
||||
public static function combineOredClauses(array $left_clauses, array $right_clauses)
|
||||
{
|
||||
$clauses = [];
|
||||
|
||||
$all_wedges = true;
|
||||
|
||||
foreach ($left_clauses as $left_clause) {
|
||||
foreach ($right_clauses as $right_clause) {
|
||||
$possibilities = [];
|
||||
|
||||
$can_reconcile = true;
|
||||
|
||||
$all_wedges = $all_wedges && $left_clause->wedge && $right_clause->wedge;
|
||||
|
||||
if ($left_clause->wedge ||
|
||||
$right_clause->wedge ||
|
||||
!$left_clause->reconcilable ||
|
||||
!$right_clause->reconcilable
|
||||
) {
|
||||
$can_reconcile = false;
|
||||
}
|
||||
|
||||
foreach ($left_clause->possibilities as $var => $possible_types) {
|
||||
if (isset($possibilities[$var])) {
|
||||
$possibilities[$var] = array_merge($possibilities[$var], $possible_types);
|
||||
} else {
|
||||
$possibilities[$var] = $possible_types;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($right_clause->possibilities as $var => $possible_types) {
|
||||
if (isset($possibilities[$var])) {
|
||||
$possibilities[$var] = array_merge($possibilities[$var], $possible_types);
|
||||
} else {
|
||||
$possibilities[$var] = $possible_types;
|
||||
}
|
||||
}
|
||||
|
||||
$clauses[] = new Clause($possibilities, false, $can_reconcile);
|
||||
}
|
||||
}
|
||||
|
||||
if ($all_wedges) {
|
||||
return [new Clause([], true)];
|
||||
}
|
||||
|
||||
return $clauses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Negates a set of clauses
|
||||
* negateClauses([$a || $b]) => !$a && !$b
|
||||
* negateClauses([$a, $b]) => !$a || !$b
|
||||
* negateClauses([$a, $b || $c]) =>
|
||||
* (!$a || !$b) &&
|
||||
* (!$a || !$c)
|
||||
* negateClauses([$a, $b || $c, $d || $e || $f]) =>
|
||||
* (!$a || !$b || !$d) &&
|
||||
* (!$a || !$b || !$e) &&
|
||||
* (!$a || !$b || !$f) &&
|
||||
* (!$a || !$c || !$d) &&
|
||||
* (!$a || !$c || !$e) &&
|
||||
* (!$a || !$c || !$f)
|
||||
*
|
||||
* @param array<int, Clause> $clauses
|
||||
*
|
||||
* @return array<int, Clause>
|
||||
*/
|
||||
public static function negateFormula(array $clauses)
|
||||
{
|
||||
foreach ($clauses as $clause) {
|
||||
self::calculateNegation($clause);
|
||||
}
|
||||
|
||||
$negated = self::simplifyCNF(self::groupImpossibilities($clauses));
|
||||
return $negated;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Clause $clause
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function calculateNegation(Clause $clause)
|
||||
{
|
||||
if ($clause->impossibilities !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$impossibilities = [];
|
||||
|
||||
foreach ($clause->possibilities as $var_id => $possiblity) {
|
||||
$impossibility = [];
|
||||
|
||||
foreach ($possiblity as $type) {
|
||||
if ($type[0] !== '^' && (!isset($type[1]) || $type[1] !== '^')) {
|
||||
$impossibility[] = self::negateType($type);
|
||||
}
|
||||
}
|
||||
|
||||
if ($impossibility) {
|
||||
$impossibilities[$var_id] = $impossibility;
|
||||
}
|
||||
}
|
||||
|
||||
$clause->impossibilities = $impossibilities;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -34,6 +34,9 @@ use Psalm\Type\Atomic\TTrue;
|
||||
|
||||
class Reconciler
|
||||
{
|
||||
/** @var array<string, array<int, string>> */
|
||||
private static $broken_paths = [];
|
||||
|
||||
/**
|
||||
* Takes two arrays and consolidates them, removing null values from existing types where applicable
|
||||
*
|
||||
@ -135,7 +138,7 @@ class Reconciler
|
||||
$changed_var_ids[] = $key;
|
||||
|
||||
if (substr($key, -1) === ']') {
|
||||
$key_parts = AlgebraChecker::breakUpPathIntoParts($key);
|
||||
$key_parts = self::breakUpPathIntoParts($key);
|
||||
self::adjustObjectLikeType(
|
||||
$key_parts,
|
||||
$existing_types,
|
||||
@ -345,11 +348,13 @@ class Reconciler
|
||||
return Type::getNull();
|
||||
}
|
||||
|
||||
$existing_var_atomic_types = $existing_var_type->getTypes();
|
||||
|
||||
if ($new_var_type === '!object' && !$existing_var_type->isMixed()) {
|
||||
$non_object_types = [];
|
||||
$did_remove_type = false;
|
||||
|
||||
foreach ($existing_var_type->getTypes() as $type) {
|
||||
foreach ($existing_var_atomic_types as $type) {
|
||||
if (!$type->isObjectType()) {
|
||||
$non_object_types[] = $type;
|
||||
} else {
|
||||
@ -384,7 +389,7 @@ class Reconciler
|
||||
$non_scalar_types = [];
|
||||
$did_remove_type = false;
|
||||
|
||||
foreach ($existing_var_type->getTypes() as $type) {
|
||||
foreach ($existing_var_atomic_types as $type) {
|
||||
if (!($type instanceof Scalar)) {
|
||||
$non_scalar_types[] = $type;
|
||||
} else {
|
||||
@ -419,7 +424,7 @@ class Reconciler
|
||||
$non_bool_types = [];
|
||||
$did_remove_type = false;
|
||||
|
||||
foreach ($existing_var_type->getTypes() as $type) {
|
||||
foreach ($existing_var_atomic_types as $type) {
|
||||
if (!$type instanceof TBool) {
|
||||
$non_bool_types[] = $type;
|
||||
} else {
|
||||
@ -454,7 +459,7 @@ class Reconciler
|
||||
$non_numeric_types = [];
|
||||
$did_remove_type = $existing_var_type->hasString();
|
||||
|
||||
foreach ($existing_var_type->getTypes() as $type) {
|
||||
foreach ($existing_var_atomic_types as $type) {
|
||||
if (!$type->isNumericType()) {
|
||||
$non_numeric_types[] = $type;
|
||||
} else {
|
||||
@ -488,8 +493,7 @@ class Reconciler
|
||||
if (($new_var_type === '!falsy' || $new_var_type === '!empty')
|
||||
&& !$existing_var_type->isMixed()
|
||||
) {
|
||||
$did_remove_type = $existing_var_type->hasString()
|
||||
|| $existing_var_type->hasNumericType()
|
||||
$did_remove_type = $existing_var_type->hasDefinitelyNumericType()
|
||||
|| $existing_var_type->isEmpty()
|
||||
|| $existing_var_type->hasType('bool')
|
||||
|| $existing_var_type->possibly_undefined;
|
||||
@ -510,6 +514,28 @@ class Reconciler
|
||||
$existing_var_type->addType(new TTrue);
|
||||
}
|
||||
|
||||
if ($existing_var_type->hasType('string')) {
|
||||
if ($existing_var_atomic_types['string'] instanceof Type\Atomic\TLiteralString) {
|
||||
$non_empty_values = [];
|
||||
|
||||
foreach ($existing_var_atomic_types['string']->values as $string_value => $_) {
|
||||
if ($string_value) {
|
||||
$non_empty_values[$string_value] = true;
|
||||
} else {
|
||||
$did_remove_type = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$non_empty_values) {
|
||||
$existing_var_type->removeType('string');
|
||||
} else {
|
||||
$existing_var_type->addType(new Type\Atomic\TLiteralString($non_empty_values));
|
||||
}
|
||||
} else {
|
||||
$did_remove_type = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ($existing_var_type->hasType('array')) {
|
||||
$did_remove_type = true;
|
||||
|
||||
@ -576,8 +602,6 @@ class Reconciler
|
||||
|
||||
$negated_type = substr($new_var_type, 1);
|
||||
|
||||
$existing_var_atomic_types = $existing_var_type->getTypes();
|
||||
|
||||
if (isset($existing_var_atomic_types['int'])
|
||||
&& $existing_var_type->from_calculation
|
||||
&& ($negated_type === 'int' || $negated_type === 'float')
|
||||
@ -700,14 +724,15 @@ class Reconciler
|
||||
$is_strict_equality = true;
|
||||
}
|
||||
|
||||
$existing_var_atomic_types = $existing_var_type->getTypes();
|
||||
|
||||
if ($new_var_type === 'falsy' || $new_var_type === 'empty') {
|
||||
if ($existing_var_type->isMixed()) {
|
||||
return $existing_var_type;
|
||||
}
|
||||
|
||||
$did_remove_type = $existing_var_type->hasString()
|
||||
|| $existing_var_type->hasScalar()
|
||||
|| $existing_var_type->hasNumericType();
|
||||
$did_remove_type = $existing_var_type->hasScalar()
|
||||
|| $existing_var_type->hasDefinitelyNumericType();
|
||||
|
||||
if ($existing_var_type->hasType('bool')) {
|
||||
$did_remove_type = true;
|
||||
@ -720,8 +745,31 @@ class Reconciler
|
||||
$existing_var_type->removeType('true');
|
||||
}
|
||||
|
||||
if ($existing_var_type->hasType('array')
|
||||
&& $existing_var_type->getTypes()['array']->getId() !== 'array<empty, empty>'
|
||||
if ($existing_var_type->hasType('string')) {
|
||||
if ($existing_var_atomic_types['string'] instanceof Type\Atomic\TLiteralString) {
|
||||
$empty_values = [];
|
||||
|
||||
foreach ($existing_var_atomic_types['string']->values as $string_value => $_) {
|
||||
if (!$string_value) {
|
||||
$empty_values[$string_value] = true;
|
||||
} else {
|
||||
$did_remove_type = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$empty_values) {
|
||||
$existing_var_type->removeType('string');
|
||||
} else {
|
||||
$existing_var_type->addType(new Type\Atomic\TLiteralString($empty_values));
|
||||
}
|
||||
} else {
|
||||
$did_remove_type = true;
|
||||
$existing_var_type->addType(new Type\Atomic\TLiteralString(['' => true, 0 => true]));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($existing_var_atomic_types['array'])
|
||||
&& $existing_var_atomic_types['array']->getId() !== 'array<empty, empty>'
|
||||
) {
|
||||
$did_remove_type = true;
|
||||
$existing_var_type->addType(new TArray(
|
||||
@ -732,7 +780,7 @@ class Reconciler
|
||||
));
|
||||
}
|
||||
|
||||
foreach ($existing_var_type->getTypes() as $type_key => $type) {
|
||||
foreach ($existing_var_atomic_types as $type_key => $type) {
|
||||
if ($type instanceof TNamedObject
|
||||
|| $type instanceof TResource
|
||||
|| $type instanceof TCallable
|
||||
@ -770,7 +818,7 @@ class Reconciler
|
||||
$object_types = [];
|
||||
$did_remove_type = false;
|
||||
|
||||
foreach ($existing_var_type->getTypes() as $type) {
|
||||
foreach ($existing_var_atomic_types as $type) {
|
||||
if ($type->isObjectType()) {
|
||||
$object_types[] = $type;
|
||||
} else {
|
||||
@ -851,7 +899,7 @@ class Reconciler
|
||||
$scalar_types = [];
|
||||
$did_remove_type = false;
|
||||
|
||||
foreach ($existing_var_type->getTypes() as $type) {
|
||||
foreach ($existing_var_atomic_types as $type) {
|
||||
if ($type instanceof Scalar) {
|
||||
$scalar_types[] = $type;
|
||||
} else {
|
||||
@ -886,7 +934,7 @@ class Reconciler
|
||||
$bool_types = [];
|
||||
$did_remove_type = false;
|
||||
|
||||
foreach ($existing_var_type->getTypes() as $type) {
|
||||
foreach ($existing_var_atomic_types as $type) {
|
||||
if ($type instanceof TBool) {
|
||||
$bool_types[] = $type;
|
||||
} else {
|
||||
@ -917,8 +965,6 @@ class Reconciler
|
||||
return Type::getMixed();
|
||||
}
|
||||
|
||||
$existing_var_atomic_types = $existing_var_type->getTypes();
|
||||
|
||||
if (isset($existing_var_atomic_types['int'])
|
||||
&& $existing_var_type->from_calculation
|
||||
&& ($new_var_type === 'int' || $new_var_type === 'float')
|
||||
@ -1370,6 +1416,83 @@ class Reconciler
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public static function breakUpPathIntoParts($path)
|
||||
{
|
||||
if (isset(self::$broken_paths[$path])) {
|
||||
return self::$broken_paths[$path];
|
||||
}
|
||||
|
||||
$chars = str_split($path);
|
||||
|
||||
$string_char = null;
|
||||
$escape_char = false;
|
||||
|
||||
$parts = [''];
|
||||
$parts_offset = 0;
|
||||
|
||||
for ($i = 0, $char_count = count($chars); $i < $char_count; ++$i) {
|
||||
$char = $chars[$i];
|
||||
|
||||
if ($string_char) {
|
||||
if ($char === $string_char && !$escape_char) {
|
||||
$string_char = null;
|
||||
}
|
||||
|
||||
if ($char === '\\') {
|
||||
$escape_char = !$escape_char;
|
||||
}
|
||||
|
||||
$parts[$parts_offset] .= $char;
|
||||
continue;
|
||||
}
|
||||
|
||||
switch ($char) {
|
||||
case '[':
|
||||
case ']':
|
||||
$parts_offset++;
|
||||
$parts[$parts_offset] = $char;
|
||||
$parts_offset++;
|
||||
continue;
|
||||
|
||||
case '\'':
|
||||
case '"':
|
||||
if (!isset($parts[$parts_offset])) {
|
||||
$parts[$parts_offset] = '';
|
||||
}
|
||||
$parts[$parts_offset] .= $char;
|
||||
$string_char = $char;
|
||||
|
||||
continue;
|
||||
|
||||
case '-':
|
||||
if ($i < $char_count - 1 && $chars[$i + 1] === '>') {
|
||||
++$i;
|
||||
|
||||
$parts_offset++;
|
||||
$parts[$parts_offset] = '->';
|
||||
$parts_offset++;
|
||||
continue;
|
||||
}
|
||||
// fall through
|
||||
|
||||
default:
|
||||
if (!isset($parts[$parts_offset])) {
|
||||
$parts[$parts_offset] = '';
|
||||
}
|
||||
$parts[$parts_offset] .= $char;
|
||||
}
|
||||
}
|
||||
|
||||
self::$broken_paths[$path] = $parts;
|
||||
|
||||
return $parts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type for a given (non-existent key) based on the passed keys
|
||||
*
|
||||
@ -1381,7 +1504,7 @@ class Reconciler
|
||||
*/
|
||||
private static function getValueForKey(ProjectChecker $project_checker, $key, array &$existing_keys)
|
||||
{
|
||||
$key_parts = AlgebraChecker::breakUpPathIntoParts($key);
|
||||
$key_parts = self::breakUpPathIntoParts($key);
|
||||
|
||||
if (count($key_parts) === 1) {
|
||||
return isset($existing_keys[$key_parts[0]]) ? clone $existing_keys[$key_parts[0]] : null;
|
||||
|
@ -407,7 +407,17 @@ class Union
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasNumericType()
|
||||
public function hasDefinitelyNumericType()
|
||||
{
|
||||
return isset($this->types['int'])
|
||||
|| isset($this->types['float'])
|
||||
|| isset($this->types['numeric-string']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasPossiblyNumericType()
|
||||
{
|
||||
return isset($this->types['int'])
|
||||
|| isset($this->types['float'])
|
||||
|
@ -786,7 +786,7 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P
|
||||
continue;
|
||||
}
|
||||
|
||||
$if_clauses = \Psalm\Checker\AlgebraChecker::getFormula(
|
||||
$if_clauses = \Psalm\Type\Algebra::getFormula(
|
||||
$function_stmt->cond,
|
||||
$this->fq_classlike_names
|
||||
? $this->fq_classlike_names[count($this->fq_classlike_names) - 1]
|
||||
@ -794,9 +794,9 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P
|
||||
$this->file_scanner
|
||||
);
|
||||
|
||||
$negated_formula = \Psalm\Checker\AlgebraChecker::negateFormula($if_clauses);
|
||||
$negated_formula = \Psalm\Type\Algebra::negateFormula($if_clauses);
|
||||
|
||||
$rules = \Psalm\Checker\AlgebraChecker::getTruthsFromFormula($negated_formula);
|
||||
$rules = \Psalm\Type\Algebra::getTruthsFromFormula($negated_formula);
|
||||
|
||||
foreach ($rules as $var_id => $rule) {
|
||||
if (strpos($rule, '|') !== false) {
|
||||
|
@ -541,7 +541,7 @@ class RedundantConditionTest extends TestCase
|
||||
}',
|
||||
'error_message' => 'TypeDoesNotContainType',
|
||||
],
|
||||
'SKIPPED-twoVarLogicNotNestedWithElseifNegatedInIf' => [
|
||||
'twoVarLogicNotNestedWithElseifNegatedInIf' => [
|
||||
'<?php
|
||||
function foo(?string $a, ?string $b): ?string {
|
||||
if ($a) {
|
||||
|
@ -126,7 +126,25 @@ class TypeAlgebraTest extends TestCase
|
||||
return $a;
|
||||
}',
|
||||
],
|
||||
'threeVarLogicNotNestedWithNoRedefinitions' => [
|
||||
'threeVarLogicNotNestedWithNoRedefinitionsWithClasses' => [
|
||||
'<?php
|
||||
function foo(?stdClass $a, ?stdClass $b, ?stdClass $c): stdClass {
|
||||
if ($a) {
|
||||
// do nothing
|
||||
} elseif ($b) {
|
||||
// do nothing here
|
||||
} elseif ($c) {
|
||||
// do nothing here
|
||||
} else {
|
||||
return new stdClass;
|
||||
}
|
||||
|
||||
if (!$a && !$b) return $c;
|
||||
if (!$a) return $b;
|
||||
return $a;
|
||||
}',
|
||||
],
|
||||
'threeVarLogicNotNestedWithNoRedefinitionsWithStrings' => [
|
||||
'<?php
|
||||
function foo(?string $a, ?string $b, ?string $c): string {
|
||||
if ($a) {
|
||||
@ -547,8 +565,7 @@ class TypeAlgebraTest extends TestCase
|
||||
function takesA(A $a): void {}
|
||||
|
||||
function foo(?A $a, ?A $b, ?A $c): void {
|
||||
if (!$a || ($b && $c)
|
||||
) {
|
||||
if (!$a || ($b && $c)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -722,6 +739,24 @@ class TypeAlgebraTest extends TestCase
|
||||
return null;
|
||||
}',
|
||||
],
|
||||
'instanceofNoRedundant' => [
|
||||
'<?php
|
||||
function logic(Bar $a, ?Bar $b) : void {
|
||||
if ((!$a instanceof Foo || !$b instanceof Foo)
|
||||
&& (!$a instanceof Foo || !$b instanceof Bar)
|
||||
&& (!$a instanceof Bar || !$b instanceof Foo)
|
||||
&& (!$a instanceof Bar || !$b instanceof Bar)
|
||||
) {
|
||||
|
||||
} else {
|
||||
if ($b instanceof Foo) {}
|
||||
}
|
||||
}
|
||||
|
||||
class Foo {}
|
||||
class Bar extends Foo {}
|
||||
class Bat extends Foo {}',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -836,7 +871,7 @@ class TypeAlgebraTest extends TestCase
|
||||
if (!$a) return $b;
|
||||
return $a;
|
||||
}',
|
||||
'error_message' => 'NullableReturnStatement',
|
||||
'error_message' => 'RedundantCondition',
|
||||
],
|
||||
'repeatedIfStatements' => [
|
||||
'<?php
|
||||
|
@ -1,13 +1,13 @@
|
||||
<?php
|
||||
namespace Psalm\Tests;
|
||||
|
||||
use Psalm\Checker\AlgebraChecker;
|
||||
use Psalm\Checker\FileChecker;
|
||||
use Psalm\Checker\StatementsChecker;
|
||||
use Psalm\Checker\TypeChecker;
|
||||
use Psalm\Clause;
|
||||
use Psalm\Context;
|
||||
use Psalm\Type;
|
||||
use Psalm\Type\Algebra;
|
||||
use Psalm\Type\Reconciler;
|
||||
|
||||
class TypeReconciliationTest extends TestCase
|
||||
@ -91,7 +91,7 @@ class TypeReconciliationTest extends TestCase
|
||||
new Clause(['$a' => ['!falsy']]),
|
||||
];
|
||||
|
||||
$negated_formula = AlgebraChecker::negateFormula($formula);
|
||||
$negated_formula = Algebra::negateFormula($formula);
|
||||
|
||||
$this->assertSame(1, count($negated_formula));
|
||||
$this->assertSame(['$a' => ['falsy']], $negated_formula[0]->possibilities);
|
||||
@ -100,7 +100,7 @@ class TypeReconciliationTest extends TestCase
|
||||
new Clause(['$a' => ['!falsy'], '$b' => ['!falsy']]),
|
||||
];
|
||||
|
||||
$negated_formula = AlgebraChecker::negateFormula($formula);
|
||||
$negated_formula = Algebra::negateFormula($formula);
|
||||
|
||||
$this->assertSame(2, count($negated_formula));
|
||||
$this->assertSame(['$a' => ['falsy']], $negated_formula[0]->possibilities);
|
||||
@ -111,7 +111,7 @@ class TypeReconciliationTest extends TestCase
|
||||
new Clause(['$b' => ['!falsy']]),
|
||||
];
|
||||
|
||||
$negated_formula = AlgebraChecker::negateFormula($formula);
|
||||
$negated_formula = Algebra::negateFormula($formula);
|
||||
|
||||
$this->assertSame(1, count($negated_formula));
|
||||
$this->assertSame(['$a' => ['falsy'], '$b' => ['falsy']], $negated_formula[0]->possibilities);
|
||||
@ -120,7 +120,7 @@ class TypeReconciliationTest extends TestCase
|
||||
new Clause(['$a' => ['int', 'string'], '$b' => ['!falsy']]),
|
||||
];
|
||||
|
||||
$negated_formula = AlgebraChecker::negateFormula($formula);
|
||||
$negated_formula = Algebra::negateFormula($formula);
|
||||
|
||||
$this->assertSame(3, count($negated_formula));
|
||||
$this->assertSame(['$a' => ['!int']], $negated_formula[0]->possibilities);
|
||||
@ -174,7 +174,7 @@ class TypeReconciliationTest extends TestCase
|
||||
new Clause(['$a' => ['falsy'], '$b' => ['falsy']]),
|
||||
];
|
||||
|
||||
$simplified_formula = AlgebraChecker::simplifyCNF($formula);
|
||||
$simplified_formula = Algebra::simplifyCNF($formula);
|
||||
|
||||
$this->assertSame(2, count($simplified_formula));
|
||||
$this->assertSame(['$a' => ['!falsy']], $simplified_formula[0]->possibilities);
|
||||
|
Loading…
Reference in New Issue
Block a user