1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-21 21:31:13 +01:00

Merge pull request #6241 from orklah/range5

Range arithmetics and assertions
This commit is contained in:
orklah 2021-09-20 07:15:43 +02:00 committed by GitHub
commit 90e1662964
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1921 additions and 181 deletions

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="dev-master@5e8219b613d9f97cd7eeba16e1fea75cc8a808e6">
<files psalm-version="dev-master@4602e4ae22563289b2f1810dd9129cd0a2c76123">
<file src="examples/TemplateChecker.php">
<PossiblyUndefinedIntArrayOffset occurrences="2">
<code>$comment_block-&gt;tags['variablesfrom'][0]</code>
@ -117,7 +117,7 @@
<code>$gettype_expr-&gt;args[0]</code>
</PossiblyUndefinedIntArrayOffset>
</file>
<file src="src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/NonDivArithmeticOpAnalyzer.php">
<file src="src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php">
<PossiblyUndefinedIntArrayOffset occurrences="2">
<code>$invalid_left_messages[0]</code>
<code>$invalid_right_messages[0]</code>
@ -308,10 +308,9 @@
</PossiblyUndefinedIntArrayOffset>
</file>
<file src="src/Psalm/Internal/Type/TypeParser.php">
<PossiblyUndefinedIntArrayOffset occurrences="10">
<PossiblyUndefinedIntArrayOffset occurrences="9">
<code>$intersection_types[0]</code>
<code>$parse_tree-&gt;children[0]</code>
<code>$parse_tree-&gt;children[0]</code>
<code>$parse_tree-&gt;condition-&gt;children[0]</code>
<code>array_keys($offset_template_data)[0]</code>
<code>array_keys($template_type_map[$array_param_name])[0]</code>

View File

@ -10,6 +10,9 @@ use PhpParser\Node\Expr\BinaryOp\NotEqual;
use PhpParser\Node\Expr\BinaryOp\NotIdentical;
use PhpParser\Node\Expr\BinaryOp\Smaller;
use PhpParser\Node\Expr\BinaryOp\SmallerOrEqual;
use PhpParser\Node\Expr\UnaryMinus;
use PhpParser\Node\Expr\UnaryPlus;
use PhpParser\Node\Scalar\LNumber;
use Psalm\CodeLocation;
use Psalm\Codebase;
use Psalm\FileSource;
@ -174,7 +177,6 @@ class AssertionFinder
) {
return self::getGreaterAssertions(
$conditional,
$codebase,
$source,
$this_class_name
);
@ -1561,47 +1563,132 @@ class AssertionFinder
* @param PhpParser\Node\Expr\BinaryOp\Greater|PhpParser\Node\Expr\BinaryOp\GreaterOrEqual $conditional
* @return false|int
*/
protected static function hasPositiveNumberCheck(
protected static function hasSuperiorNumberCheck(
FileSource $source,
PhpParser\Node\Expr\BinaryOp $conditional,
?int &$min_count
?int &$literal_value_comparison,
bool &$isset_assert
) {
if ($conditional->right instanceof PhpParser\Node\Scalar\LNumber
&& $conditional->right->value >= (
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater
? 0
: 1
)
$right_assignment = false;
$value_right = null;
if ($source instanceof StatementsAnalyzer
&& ($type = $source->node_data->getType($conditional->right))
&& $type->isSingleIntLiteral()
) {
$min_count = $conditional->right->value +
$right_assignment = true;
$value_right = $type->getSingleIntLiteral()->value;
} elseif ($conditional->right instanceof LNumber) {
$right_assignment = true;
$value_right = $conditional->right->value;
} elseif ($conditional->right instanceof UnaryMinus && $conditional->right->expr instanceof LNumber) {
$right_assignment = true;
$value_right = -$conditional->right->expr->value;
} elseif ($conditional->right instanceof UnaryPlus && $conditional->right->expr instanceof LNumber) {
$right_assignment = true;
$value_right = $conditional->right->expr->value;
}
if ($right_assignment === true && $value_right !== null) {
$isset_assert = $value_right === 0 && $conditional instanceof Greater;
$literal_value_comparison = $value_right +
($conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater ? 1 : 0);
return self::ASSIGNMENT_TO_RIGHT;
}
$left_assignment = false;
$value_left = null;
if ($source instanceof StatementsAnalyzer
&& ($type = $source->node_data->getType($conditional->left))
&& $type->isSingleIntLiteral()
) {
$left_assignment = true;
$value_left = $type->getSingleIntLiteral()->value;
} elseif ($conditional->left instanceof LNumber) {
$left_assignment = true;
$value_left = $conditional->left->value;
} elseif ($conditional->left instanceof UnaryMinus && $conditional->left->expr instanceof LNumber) {
$left_assignment = true;
$value_left = -$conditional->left->expr->value;
} elseif ($conditional->left instanceof UnaryPlus && $conditional->left->expr instanceof LNumber) {
$left_assignment = true;
$value_left = $conditional->left->expr->value;
}
if ($left_assignment === true && $value_left !== null) {
$isset_assert = $value_left === 0 && $conditional instanceof Greater;
$literal_value_comparison = $value_left +
($conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater ? -1 : 0);
return self::ASSIGNMENT_TO_LEFT;
}
return false;
}
/**
* @param PhpParser\Node\Expr\BinaryOp\Greater|PhpParser\Node\Expr\BinaryOp\GreaterOrEqual $conditional
* @param PhpParser\Node\Expr\BinaryOp\Smaller|PhpParser\Node\Expr\BinaryOp\SmallerOrEqual $conditional
* @return false|int
*/
protected static function hasZeroCheck(
protected static function hasInferiorNumberCheck(
FileSource $source,
PhpParser\Node\Expr\BinaryOp $conditional,
?int &$zero_count
?int &$literal_value_comparison,
bool &$isset_assert
) {
if ($conditional->right instanceof PhpParser\Node\Scalar\LNumber
&& $conditional->right->value >= (
$conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater
? -1
: 0
)
$right_assignment = false;
$value_right = null;
if ($source instanceof StatementsAnalyzer
&& ($type = $source->node_data->getType($conditional->right))
&& $type->isSingleIntLiteral()
) {
$zero_count = $conditional->right->value +
($conditional instanceof PhpParser\Node\Expr\BinaryOp\Greater ? 1 : 0);
$right_assignment = true;
$value_right = $type->getSingleIntLiteral()->value;
} elseif ($conditional->right instanceof LNumber) {
$right_assignment = true;
$value_right = $conditional->right->value;
} elseif ($conditional->right instanceof UnaryMinus && $conditional->right->expr instanceof LNumber) {
$right_assignment = true;
$value_right = -$conditional->right->expr->value;
} elseif ($conditional->right instanceof UnaryPlus && $conditional->right->expr instanceof LNumber) {
$right_assignment = true;
$value_right = $conditional->right->expr->value;
}
if ($right_assignment === true && $value_right !== null) {
$isset_assert = $value_right === 0 && $conditional instanceof Smaller;
$literal_value_comparison = $value_right +
($conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller ? -1 : 0);
return self::ASSIGNMENT_TO_RIGHT;
}
$left_assignment = false;
$value_left = null;
if ($source instanceof StatementsAnalyzer
&& ($type = $source->node_data->getType($conditional->left))
&& $type->isSingleIntLiteral()
) {
$left_assignment = true;
$value_left = $type->getSingleIntLiteral()->value;
} elseif ($conditional->left instanceof LNumber) {
$left_assignment = true;
$value_left = $conditional->left->value;
} elseif ($conditional->left instanceof UnaryMinus && $conditional->left->expr instanceof LNumber) {
$left_assignment = true;
$value_left = -$conditional->left->expr->value;
} elseif ($conditional->left instanceof UnaryPlus && $conditional->left->expr instanceof LNumber) {
$left_assignment = true;
$value_left = $conditional->left->expr->value;
}
if ($left_assignment === true && $value_left !== null) {
$isset_assert = $value_left === 0 && $conditional instanceof Smaller;
$literal_value_comparison = $value_left +
($conditional instanceof PhpParser\Node\Expr\BinaryOp\Smaller ? 1 : 0);
return self::ASSIGNMENT_TO_LEFT;
}
return false;
}
@ -3605,7 +3692,6 @@ class AssertionFinder
*/
private static function getGreaterAssertions(
PhpParser\Node\Expr $conditional,
?Codebase $codebase,
FileSource $source,
?string $this_class_name
): array {
@ -3613,12 +3699,16 @@ class AssertionFinder
$min_count = null;
$count_equality_position = self::hasNonEmptyCountEqualityCheck($conditional, $min_count);
$min_comparison = null;
$positive_number_position = self::hasPositiveNumberCheck($conditional, $min_comparison);
$zero_comparison = null;
$zero_position = self::hasZeroCheck($conditional, $zero_comparison);
$max_count = null;
$count_inequality_position = self::hasLessThanCountEqualityCheck($conditional, $max_count);
$isset_assert = false;
$superior_value_comparison = null;
$superior_value_position = self::hasSuperiorNumberCheck(
$source,
$conditional,
$superior_value_comparison,
$isset_assert
);
if ($count_equality_position) {
if ($count_equality_position === self::ASSIGNMENT_TO_RIGHT) {
@ -3674,116 +3764,39 @@ class AssertionFinder
return $if_types ? [$if_types] : [];
}
if ($positive_number_position) {
if ($positive_number_position === self::ASSIGNMENT_TO_RIGHT) {
if ($superior_value_position) {
if ($superior_value_position === self::ASSIGNMENT_TO_RIGHT) {
$var_name = ExpressionIdentifier::getArrayVarId(
$conditional->left,
$this_class_name,
$source
);
$value_node = $conditional->left;
} else {
$var_name = ExpressionIdentifier::getArrayVarId(
$conditional->right,
$this_class_name,
$source
);
$value_node = $conditional->right;
}
if ($codebase
&& $source instanceof StatementsAnalyzer
&& ($var_type = $source->node_data->getType($value_node))
&& $var_type->isSingle()
&& $var_type->hasBool()
&& $min_comparison > 1
) {
if ($var_type->from_docblock) {
if (IssueBuffer::accepts(
new DocblockTypeContradiction(
$var_type . ' cannot be greater than ' . $min_comparison,
new CodeLocation($source, $conditional),
null
),
$source->getSuppressedIssues()
)) {
// fall through
if ($var_name !== null) {
if ($superior_value_position === self::ASSIGNMENT_TO_RIGHT) {
if ($superior_value_comparison === 0) {
$if_types[$var_name] = [['=positive-numeric', '=int(0)']];
} elseif ($superior_value_comparison === 1) {
$if_types[$var_name] = [['positive-numeric']];
} else {
$if_types[$var_name] = [['>' . $superior_value_comparison]];
}
} else {
if (IssueBuffer::accepts(
new TypeDoesNotContainType(
$var_type . ' cannot be greater than ' . $min_comparison,
new CodeLocation($source, $conditional),
null
),
$source->getSuppressedIssues()
)) {
// fall through
}
$if_types[$var_name] = [['<' . $superior_value_comparison]];
}
}
if ($var_name) {
$if_types[$var_name] = [[($min_comparison === 1 ? '' : '=') . 'positive-numeric']];
}
return $if_types ? [$if_types] : [];
}
if ($zero_position) {
if ($zero_position === self::ASSIGNMENT_TO_RIGHT) {
$var_name = ExpressionIdentifier::getArrayVarId(
$conditional->left,
$this_class_name,
$source
);
$value_node = $conditional->left;
} else {
$var_name = ExpressionIdentifier::getArrayVarId(
$conditional->right,
$this_class_name,
$source
);
$value_node = $conditional->right;
}
if ($codebase
&& $source instanceof StatementsAnalyzer
&& ($var_type = $source->node_data->getType($value_node))
&& $var_type->isSingle()
&& $var_type->hasBool()
&& $zero_comparison > 1
) {
if ($var_type->from_docblock) {
if (IssueBuffer::accepts(
new DocblockTypeContradiction(
$var_type . ' cannot be greater than ' . $zero_comparison,
new CodeLocation($source, $conditional),
null
),
$source->getSuppressedIssues()
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new TypeDoesNotContainType(
$var_type . ' cannot be greater than ' . $zero_comparison,
new CodeLocation($source, $conditional),
null
),
$source->getSuppressedIssues()
)) {
// fall through
}
if ($isset_assert) {
$if_types[$var_name][] = ['=isset'];
}
}
if ($var_name) {
$if_types[$var_name] = [[($zero_comparison === 1 ? '' : '=') . 'positive-numeric', '=int(0)']];
}
return $if_types ? [$if_types] : [];
}
@ -3802,10 +3815,16 @@ class AssertionFinder
$if_types = [];
$min_count = null;
$count_equality_position = self::hasNonEmptyCountEqualityCheck($conditional, $min_count);
$typed_value_position = self::hasTypedValueComparison($conditional, $source);
$max_count = null;
$count_inequality_position = self::hasLessThanCountEqualityCheck($conditional, $max_count);
$isset_assert = false;
$inferior_value_comparison = null;
$inferior_value_position = self::hasInferiorNumberCheck(
$source,
$conditional,
$inferior_value_comparison,
$isset_assert
);
if ($count_equality_position) {
if ($count_equality_position === self::ASSIGNMENT_TO_LEFT) {
@ -3857,37 +3876,38 @@ class AssertionFinder
return $if_types ? [$if_types] : [];
}
if ($typed_value_position) {
if ($typed_value_position === self::ASSIGNMENT_TO_RIGHT) {
if ($inferior_value_position) {
if ($inferior_value_position === self::ASSIGNMENT_TO_RIGHT) {
$var_name = ExpressionIdentifier::getArrayVarId(
$conditional->left,
$this_class_name,
$source
);
$expr = $conditional->right;
} elseif ($typed_value_position === self::ASSIGNMENT_TO_LEFT) {
} else {
$var_name = ExpressionIdentifier::getArrayVarId(
$conditional->right,
$this_class_name,
$source
);
$expr = $conditional->left;
} else {
throw new \UnexpectedValueException('$typed_value_position value');
}
$expr_type = $source instanceof StatementsAnalyzer
? $source->node_data->getType($expr)
: null;
if ($var_name
&& $expr_type
&& $expr_type->isSingleIntLiteral()
&& ($expr_type->getSingleIntLiteral()->value === 0)
) {
$if_types[$var_name] = [['=isset']];
if ($var_name !== null) {
if ($inferior_value_position === self::ASSIGNMENT_TO_RIGHT) {
$if_types[$var_name] = [['<' . $inferior_value_comparison]];
} else {
if ($inferior_value_comparison === 0) {
$if_types[$var_name] = [['=positive-numeric', '=int(0)']];
} elseif ($inferior_value_comparison === 1) {
$if_types[$var_name] = [['positive-numeric']];
} else {
$if_types[$var_name] = [['>' . $inferior_value_comparison]];
}
}
if ($isset_assert) {
$if_types[$var_name][] = ['=isset'];
}
}
return $if_types ? [$if_types] : [];

View File

@ -19,10 +19,12 @@ use Psalm\Issue\StringIncrement;
use Psalm\IssueBuffer;
use Psalm\StatementsSource;
use Psalm\Type;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TIntRange;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TList;
use Psalm\Type\Atomic\TLiteralInt;
@ -37,13 +39,15 @@ use function array_diff_key;
use function array_values;
use function is_int;
use function is_numeric;
use function max;
use function min;
use function preg_match;
use function strtolower;
/**
* @internal
*/
class NonDivArithmeticOpAnalyzer
class ArithmeticOpAnalyzer
{
public static function analyze(
?StatementsSource $statements_source,
@ -181,7 +185,7 @@ class NonDivArithmeticOpAnalyzer
foreach ($left_type->getAtomicTypes() as $left_type_part) {
foreach ($right_type->getAtomicTypes() as $right_type_part) {
$candidate_result_type = self::analyzeNonDivOperands(
$candidate_result_type = self::analyzeOperands(
$statements_source,
$codebase,
$config,
@ -288,7 +292,7 @@ class NonDivArithmeticOpAnalyzer
* @param string[] &$invalid_left_messages
* @param string[] &$invalid_right_messages
*/
private static function analyzeNonDivOperands(
private static function analyzeOperands(
?StatementsSource $statements_source,
?\Psalm\Codebase $codebase,
Config $config,
@ -665,6 +669,23 @@ class NonDivArithmeticOpAnalyzer
return null;
}
if ($left_type_part instanceof Type\Atomic\TIntRange && $right_type_part instanceof Type\Atomic\TIntRange) {
self::analyzeOperandsBetweenIntRange($parent, $result_type, $left_type_part, $right_type_part);
return null;
}
if (($left_type_part instanceof Type\Atomic\TIntRange && $right_type_part instanceof TInt) ||
($left_type_part instanceof TInt && $right_type_part instanceof Type\Atomic\TIntRange)
) {
self::analyzeOperandsBetweenIntRangeAndInt(
$parent,
$result_type,
$left_type_part,
$right_type_part
);
return null;
}
if ($left_type_part instanceof TInt && $right_type_part instanceof TInt) {
if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Div) {
$result_type = new Type\Union([new Type\Atomic\TInt(), new Type\Atomic\TFloat()]);
@ -702,17 +723,24 @@ class NonDivArithmeticOpAnalyzer
}
if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Mod) {
if ($always_positive) {
if ($right_type_part instanceof TLiteralInt && $right_type_part->value === 1) {
$result_type = Type::getInt(true, 0);
if ($right_type_part instanceof TLiteralInt) {
$literal_value_max = $right_type_part->value - 1;
if ($always_positive) {
$result_type = new Type\Union([new Type\Atomic\TIntRange(0, $literal_value_max)]);
} else {
$result_type = new Type\Union(
[new Type\Atomic\TIntRange(-$literal_value_max, $literal_value_max)]
);
}
} else {
if ($always_positive) {
$result_type = new Type\Union([
new Type\Atomic\TPositiveInt(),
new TLiteralInt(0)
]);
} else {
$result_type = Type::getInt();
}
} else {
$result_type = Type::getInt();
}
} elseif (!$result_type) {
$result_type = $always_positive ? Type::getPositiveInt(true) : Type::getInt(true);
@ -835,6 +863,10 @@ class NonDivArithmeticOpAnalyzer
} elseif ($operation instanceof PhpParser\Node\Expr\BinaryOp\Minus) {
$result = $operand1 - $operand2;
} elseif ($operation instanceof PhpParser\Node\Expr\BinaryOp\Mod) {
if ($operand2 === 0) {
return Type::getEmpty();
}
$result = $operand1 % $operand2;
} elseif ($operation instanceof PhpParser\Node\Expr\BinaryOp\Mul) {
$result = $operand1 * $operand2;
@ -867,4 +899,465 @@ class NonDivArithmeticOpAnalyzer
return $calculated_type;
}
private static function analyzeOperandsBetweenIntRange(
PhpParser\Node $parent,
?Type\Union &$result_type,
TIntRange $left_type_part,
TIntRange $right_type_part
): void {
if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Div) {
//can't assume an int range will stay int after division
if (!$result_type) {
$result_type = new Type\Union([new Type\Atomic\TInt(), new Type\Atomic\TFloat()]);
} else {
$result_type = Type::combineUnionTypes(
new Type\Union([new Type\Atomic\TInt(), new Type\Atomic\TFloat()]),
$result_type
);
}
return;
}
if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Mod) {
self::analyzeModBetweenIntRange($result_type, $left_type_part, $right_type_part);
return;
}
if ($parent instanceof PhpParser\Node\Expr\BinaryOp\BitwiseAnd ||
$parent instanceof PhpParser\Node\Expr\BinaryOp\BitwiseOr ||
$parent instanceof PhpParser\Node\Expr\BinaryOp\BitwiseXor
) {
//really complex to calculate
if (!$result_type) {
$result_type = Type::getInt();
} else {
$result_type = Type::combineUnionTypes(
Type::getInt(),
$result_type
);
}
return;
}
if ($parent instanceof PhpParser\Node\Expr\BinaryOp\ShiftLeft ||
$parent instanceof PhpParser\Node\Expr\BinaryOp\ShiftRight
) {
//really complex to calculate
if (!$result_type) {
$result_type = new Type\Union([new Type\Atomic\TInt()]);
} else {
$result_type = Type::combineUnionTypes(
new Type\Union([new Type\Atomic\TInt()]),
$result_type
);
}
return;
}
if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Mul) {
self::analyzeMulBetweenIntRange($parent, $result_type, $left_type_part, $right_type_part);
return;
}
if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Pow) {
self::analyzePowBetweenIntRange($result_type, $left_type_part, $right_type_part);
return;
}
if ($parent instanceof PhpParser\Node\Expr\BinaryOp\Minus) {
//for Minus, we have to assume the min is the min from first range minus the max from the second
$min_operand1 = $left_type_part->min_bound;
$min_operand2 = $right_type_part->max_bound;
//and the max is the max from first range minus the min from the second
$max_operand1 = $left_type_part->max_bound;
$max_operand2 = $right_type_part->min_bound;
} else {
$min_operand1 = $left_type_part->min_bound;
$min_operand2 = $right_type_part->min_bound;
$max_operand1 = $left_type_part->max_bound;
$max_operand2 = $right_type_part->max_bound;
}
$calculated_min_type = null;
if ($min_operand1 !== null && $min_operand2 !== null) {
// when there are two valid numbers, make any operation
$calculated_min_type = self::arithmeticOperation(
$parent,
$min_operand1,
$min_operand2,
false
);
}
$calculated_max_type = null;
if ($max_operand1 !== null && $max_operand2 !== null) {
// when there are two valid numbers, make any operation
$calculated_max_type = self::arithmeticOperation(
$parent,
$max_operand1,
$max_operand2,
false
);
}
$min_value = $calculated_min_type !== null ? $calculated_min_type->getSingleIntLiteral()->value : null;
$max_value = $calculated_max_type !== null ? $calculated_max_type->getSingleIntLiteral()->value : null;
$new_result_type = new Type\Union([new Type\Atomic\TIntRange($min_value, $max_value)]);
if (!$result_type) {
$result_type = $new_result_type;
} else {
$result_type = Type::combineUnionTypes($new_result_type, $result_type);
}
}
/**
* @param TIntRange|TInt $left_type_part
* @param TIntRange|TInt $right_type_part
*/
private static function analyzeOperandsBetweenIntRangeAndInt(
PhpParser\Node $parent,
?Type\Union &$result_type,
Atomic $left_type_part,
Atomic $right_type_part
): void {
if (!$left_type_part instanceof Type\Atomic\TIntRange) {
$left_type_part = TIntRange::convertToIntRange($left_type_part);
}
if (!$right_type_part instanceof Type\Atomic\TIntRange) {
$right_type_part = TIntRange::convertToIntRange($right_type_part);
}
self::analyzeOperandsBetweenIntRange($parent, $result_type, $left_type_part, $right_type_part);
}
private static function analyzeMulBetweenIntRange(
PhpParser\Node\Expr\BinaryOp\Mul $parent,
?Type\Union &$result_type,
TIntRange $left_type_part,
TIntRange $right_type_part
): void {
//Mul is a special case because of double negatives. We can only infer when we know both signs strictly
if ($right_type_part->min_bound !== null
&& $right_type_part->max_bound !== null
&& $left_type_part->min_bound !== null
&& $left_type_part->max_bound !== null
) {
//everything is known, we can do calculations
//[ x_1 , x_2 ] ⋆ [ y_1 , y_2 ] =
// [
// min(x_1 ⋆ y_1 , x_1 ⋆ y_2 , x_2 ⋆ y_1 , x_2 ⋆ y_2),
// max(x_1 ⋆ y_1 , x_1 ⋆ y_2 , x_2 ⋆ y_1 , x_2 ⋆ y_2)
// ]
$x_1 = $right_type_part->min_bound;
$x_2 = $right_type_part->max_bound;
$y_1 = $left_type_part->min_bound;
$y_2 = $left_type_part->max_bound;
$min_value = min($x_1 * $y_1, $x_1 * $y_2, $x_2 * $y_1, $x_2 * $y_2);
$max_value = max($x_1 * $y_1, $x_1 * $y_2, $x_2 * $y_1, $x_2 * $y_2);
$new_result_type = new Type\Union([new TIntRange($min_value, $max_value)]);
} elseif ($right_type_part->isPositiveOrZero() && $left_type_part->isPositiveOrZero()) {
// both operands are positive, result will be only positive
$min_operand1 = $left_type_part->min_bound;
$min_operand2 = $right_type_part->min_bound;
$max_operand1 = $left_type_part->max_bound;
$max_operand2 = $right_type_part->max_bound;
$calculated_min_type = null;
if ($min_operand1 !== null && $min_operand2 !== null) {
// when there are two valid numbers, make any operation
$calculated_min_type = self::arithmeticOperation(
$parent,
$min_operand1,
$min_operand2,
false
);
}
$calculated_max_type = null;
if ($max_operand1 !== null && $max_operand2 !== null) {
// when there are two valid numbers, make any operation
$calculated_max_type = self::arithmeticOperation(
$parent,
$max_operand1,
$max_operand2,
false
);
}
$min_value = $calculated_min_type !== null ? $calculated_min_type->getSingleIntLiteral()->value : null;
$max_value = $calculated_max_type !== null ? $calculated_max_type->getSingleIntLiteral()->value : null;
$new_result_type = new Type\Union([new Type\Atomic\TIntRange($min_value, $max_value)]);
} elseif ($right_type_part->isPositiveOrZero() && $left_type_part->isNegativeOrZero()) {
// one operand is negative, result will be negative and we have to check min vs max
$min_operand1 = $left_type_part->max_bound;
$min_operand2 = $right_type_part->min_bound;
$max_operand1 = $left_type_part->min_bound;
$max_operand2 = $right_type_part->max_bound;
$calculated_min_type = null;
if ($min_operand1 !== null && $min_operand2 !== null) {
// when there are two valid numbers, make any operation
$calculated_min_type = self::arithmeticOperation(
$parent,
$min_operand1,
$min_operand2,
false
);
}
$calculated_max_type = null;
if ($max_operand1 !== null && $max_operand2 !== null) {
// when there are two valid numbers, make any operation
$calculated_max_type = self::arithmeticOperation(
$parent,
$max_operand1,
$max_operand2,
false
);
}
$min_value = $calculated_min_type !== null ? $calculated_min_type->getSingleIntLiteral()->value : null;
$max_value = $calculated_max_type !== null ? $calculated_max_type->getSingleIntLiteral()->value : null;
if ($min_value > $max_value) {
[$min_value, $max_value] = [$max_value, $min_value];
}
$new_result_type = new Type\Union([new Type\Atomic\TIntRange($min_value, $max_value)]);
} elseif ($right_type_part->isNegativeOrZero() && $left_type_part->isPositiveOrZero()) {
// one operand is negative, result will be negative and we have to check min vs max
$min_operand1 = $left_type_part->min_bound;
$min_operand2 = $right_type_part->max_bound;
$max_operand1 = $left_type_part->max_bound;
$max_operand2 = $right_type_part->min_bound;
$calculated_min_type = null;
if ($min_operand1 !== null && $min_operand2 !== null) {
// when there are two valid numbers, make any operation
$calculated_min_type = self::arithmeticOperation(
$parent,
$min_operand1,
$min_operand2,
false
);
}
$calculated_max_type = null;
if ($max_operand1 !== null && $max_operand2 !== null) {
// when there are two valid numbers, make any operation
$calculated_max_type = self::arithmeticOperation(
$parent,
$max_operand1,
$max_operand2,
false
);
}
$min_value = $calculated_min_type !== null ? $calculated_min_type->getSingleIntLiteral()->value : null;
$max_value = $calculated_max_type !== null ? $calculated_max_type->getSingleIntLiteral()->value : null;
if ($min_value > $max_value) {
[$min_value, $max_value] = [$max_value, $min_value];
}
$new_result_type = new Type\Union([new Type\Atomic\TIntRange($min_value, $max_value)]);
} elseif ($right_type_part->isNegativeOrZero() && $left_type_part->isNegativeOrZero()) {
// both operand are negative, result will be positive
$min_operand1 = $left_type_part->max_bound;
$min_operand2 = $right_type_part->max_bound;
$max_operand1 = $left_type_part->min_bound;
$max_operand2 = $right_type_part->min_bound;
$calculated_min_type = null;
if ($min_operand1 !== null && $min_operand2 !== null) {
// when there are two valid numbers, make any operation
$calculated_min_type = self::arithmeticOperation(
$parent,
$min_operand1,
$min_operand2,
false
);
}
$calculated_max_type = null;
if ($max_operand1 !== null && $max_operand2 !== null) {
// when there are two valid numbers, make any operation
$calculated_max_type = self::arithmeticOperation(
$parent,
$max_operand1,
$max_operand2,
false
);
}
$min_value = $calculated_min_type !== null ? $calculated_min_type->getSingleIntLiteral()->value : null;
$max_value = $calculated_max_type !== null ? $calculated_max_type->getSingleIntLiteral()->value : null;
$new_result_type = new Type\Union([new Type\Atomic\TIntRange($min_value, $max_value)]);
} else {
$new_result_type = Type::getInt(true);
}
if (!$result_type) {
$result_type = $new_result_type;
} else {
$result_type = Type::combineUnionTypes($new_result_type, $result_type);
}
}
private static function analyzePowBetweenIntRange(
?Type\Union &$result_type,
TIntRange $left_type_part,
TIntRange $right_type_part
): void {
//If Pow first operand is negative, the result could be positive or negative, else it will be positive
//If Pow second operand is negative, the result will be float, if it's 0, it will be 1/-1, else positive
if ($left_type_part->isPositive()) {
if ($right_type_part->isPositive()) {
$new_result_type = new Type\Union([new TIntRange(1, null)]);
} elseif ($right_type_part->isNegative()) {
$new_result_type = Type::getFloat();
} elseif ($right_type_part->min_bound === 0 && $right_type_part->max_bound === 0) {
$new_result_type = Type::getInt(true, 1);
} else {
//$right_type_part may be a mix of positive, negative and 0
$new_result_type = new Type\Union([new TInt(), new TFloat()]);
}
} elseif ($left_type_part->isNegative()) {
if ($right_type_part->isPositive()) {
if ($right_type_part->min_bound === $right_type_part->max_bound) {
if ($right_type_part->max_bound % 2 === 0) {
$new_result_type = new Type\Union([new TIntRange(1, null)]);
} else {
$new_result_type = new Type\Union([new TIntRange(null, -1)]);
}
} else {
$new_result_type = Type::getInt(true);
}
} elseif ($right_type_part->isNegative()) {
$new_result_type = Type::getFloat();
} elseif ($right_type_part->min_bound === 0 && $right_type_part->max_bound === 0) {
$new_result_type = Type::getInt(true, -1);
} else {
//$right_type_part may be a mix of positive, negative and 0
$new_result_type = new Type\Union([new TInt(), new TFloat()]);
}
} elseif ($left_type_part->min_bound === 0 && $left_type_part->max_bound === 0) {
if ($right_type_part->isPositive()) {
$new_result_type = Type::getInt(true, 0);
} elseif ($right_type_part->min_bound === 0 && $right_type_part->max_bound === 0) {
$new_result_type = Type::getInt(true, 1);
} else {
//technically could be a float(INF)...
$new_result_type = Type::getEmpty();
}
} else {
//$left_type_part may be a mix of positive, negative and 0
if ($right_type_part->isPositive()) {
if ($right_type_part->min_bound === $right_type_part->max_bound
&& $right_type_part->max_bound % 2 === 0
) {
$new_result_type = new Type\Union([new TIntRange(1, null)]);
} else {
$new_result_type = Type::getInt(true);
}
} elseif ($right_type_part->isNegative()) {
$new_result_type = Type::getFloat();
} elseif ($right_type_part->min_bound === 0 && $right_type_part->max_bound === 0) {
$new_result_type = Type::getInt(true, 1);
} else {
//$left_type_part may be a mix of positive, negative and 0
$new_result_type = new Type\Union([new TInt(), new TFloat()]);
}
}
if (!$result_type) {
$result_type = $new_result_type;
} else {
$result_type = Type::combineUnionTypes($new_result_type, $result_type);
}
}
private static function analyzeModBetweenIntRange(
?Type\Union &$result_type,
TIntRange $left_type_part,
TIntRange $right_type_part
): void {
//result of Mod is not directly dependant on the bounds of the range
if ($right_type_part->min_bound !== null && $right_type_part->min_bound === $right_type_part->max_bound) {
//if the second operand is a literal, we can be pretty detailed
if ($right_type_part->max_bound === 0) {
$new_result_type = Type::getEmpty();
} else {
if ($left_type_part->isPositiveOrZero()) {
if ($right_type_part->isPositive()) {
$max = $right_type_part->min_bound - 1;
$new_result_type = new Type\Union([new TIntRange(0, $max)]);
} else {
$max = $right_type_part->min_bound + 1;
$new_result_type = new Type\Union([new TIntRange($max, 0)]);
}
} elseif ($left_type_part->isNegativeOrZero()) {
if ($right_type_part->isPositive()) {
$max = $right_type_part->min_bound - 1;
$new_result_type = new Type\Union([new TIntRange(-$max, 0)]);
} else {
$max = $right_type_part->min_bound + 1;
$new_result_type = new Type\Union([new TIntRange(-$max, 0)]);
}
} else {
if ($right_type_part->isPositive()) {
$max = $right_type_part->min_bound - 1;
} else {
$max = -$right_type_part->min_bound - 1;
}
$new_result_type = new Type\Union([new TIntRange(-$max, $max)]);
}
}
} elseif ($right_type_part->isPositive()) {
if ($left_type_part->isPositiveOrZero()) {
if ($right_type_part->max_bound !== null) {
//we now that the result will be a range between 0 and $right->max - 1
$new_result_type = new Type\Union(
[new TIntRange(0, $right_type_part->max_bound - 1)]
);
} else {
$new_result_type = new Type\Union([new TIntRange(0, null)]);
}
} elseif ($left_type_part->isNegativeOrZero()) {
$new_result_type = new Type\Union([new TIntRange(null, 0)]);
} else {
$new_result_type = Type::getInt(true);
}
} elseif ($right_type_part->isNegative()) {
if ($left_type_part->isPositiveOrZero()) {
$new_result_type = new Type\Union([new TIntRange(null, 0)]);
} elseif ($left_type_part->isNegativeOrZero()) {
$new_result_type = new Type\Union([new TIntRange(null, 0)]);
} else {
$new_result_type = Type::getInt(true);
}
} else {
$new_result_type = Type::getInt(true);
}
if (!$result_type) {
$result_type = $new_result_type;
} else {
$result_type = Type::combineUnionTypes(
$new_result_type,
$result_type
);
}
}
}

View File

@ -56,7 +56,7 @@ class NonComparisonOpAnalyzer
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\ShiftLeft
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\ShiftRight
) {
NonDivArithmeticOpAnalyzer::analyze(
ArithmeticOpAnalyzer::analyze(
$statements_analyzer,
$statements_analyzer->node_data,
$stmt->left,
@ -116,7 +116,7 @@ class NonComparisonOpAnalyzer
}
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\Div) {
NonDivArithmeticOpAnalyzer::analyze(
ArithmeticOpAnalyzer::analyze(
$statements_analyzer,
$statements_analyzer->node_data,
$stmt->left,
@ -144,7 +144,7 @@ class NonComparisonOpAnalyzer
}
if ($stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseOr) {
NonDivArithmeticOpAnalyzer::analyze(
ArithmeticOpAnalyzer::analyze(
$statements_analyzer,
$statements_analyzer->node_data,
$stmt->left,

View File

@ -54,7 +54,7 @@ class IncDecExpressionAnalyzer
$fake_right_expr = new VirtualLNumber(1, $stmt->getAttributes());
$statements_analyzer->node_data->setType($fake_right_expr, Type::getInt());
BinaryOp\NonDivArithmeticOpAnalyzer::analyze(
BinaryOp\ArithmeticOpAnalyzer::analyze(
$statements_analyzer,
$statements_analyzer->node_data,
$stmt->var,

View File

@ -3,7 +3,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression;
use PhpParser;
use Psalm\Internal\Analyzer\ClassLikeAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\BinaryOp\NonDivArithmeticOpAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\BinaryOp\ArithmeticOpAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Type\TypeCombiner;
use Psalm\StatementsSource;
@ -164,7 +164,7 @@ class SimpleTypeInferer
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseOr
|| $stmt instanceof PhpParser\Node\Expr\BinaryOp\BitwiseAnd
) {
NonDivArithmeticOpAnalyzer::analyze(
ArithmeticOpAnalyzer::analyze(
$file_source instanceof StatementsSource ? $file_source : null,
$nodes,
$stmt->left,

View File

@ -12,6 +12,7 @@ use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Type;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TIntRange;
use Psalm\Type\Atomic\TString;
class UnaryPlusMinusAnalyzer
@ -47,6 +48,37 @@ class UnaryPlusMinusAnalyzer
$type_part->value = -$type_part->value;
}
if ($type_part instanceof Type\Atomic\TIntRange
&& $stmt instanceof PhpParser\Node\Expr\UnaryMinus
) {
//we'll have to inverse min and max bound and negate any literal
$old_min_bound = $type_part->min_bound;
$old_max_bound = $type_part->max_bound;
if ($old_min_bound === null) {
//min bound is null, max bound will be null
$type_part->max_bound = null;
} elseif ($old_min_bound === 0) {
$type_part->max_bound = 0;
} else {
$type_part->max_bound = -$old_min_bound;
}
if ($old_max_bound === null) {
//max bound is null, min bound will be null
$type_part->min_bound = null;
} elseif ($old_max_bound === 0) {
$type_part->min_bound = 0;
} else {
$type_part->min_bound = -$old_max_bound;
}
}
if ($type_part instanceof Type\Atomic\TPositiveInt
&& $stmt instanceof PhpParser\Node\Expr\UnaryMinus
) {
$type_part = new TIntRange(null, -1);
}
$acceptable_types[] = $type_part;
} elseif ($type_part instanceof TString) {
$acceptable_types[] = new TInt;

View File

@ -17,6 +17,7 @@ use Psalm\Type;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TClassString;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TIntRange;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Atomic\TString;
@ -123,7 +124,8 @@ class AssertionReconciler extends \Psalm\Type\Reconciler
$negated,
$code_location,
$suppressed_issues,
$failed_reconciliation
$failed_reconciliation,
$inside_loop
);
}
@ -836,6 +838,34 @@ class AssertionReconciler extends \Psalm\Type\Reconciler
}
}
//These partial match wouldn't have been handled by AtomicTypeComparator
$new_range = null;
if ($new_type_part instanceof Atomic\TIntRange && $existing_type_part instanceof Atomic\TPositiveInt) {
$new_range = TIntRange::intersectIntRanges(
TIntRange::convertToIntRange($existing_type_part),
$new_type_part
);
} elseif ($existing_type_part instanceof Atomic\TIntRange
&& $new_type_part instanceof Atomic\TPositiveInt
) {
$new_range = TIntRange::intersectIntRanges(
$existing_type_part,
TIntRange::convertToIntRange($new_type_part)
);
} elseif ($new_type_part instanceof Atomic\TIntRange
&& $existing_type_part instanceof Atomic\TIntRange
) {
$new_range = TIntRange::intersectIntRanges(
$existing_type_part,
$new_type_part
);
}
if ($new_range !== null) {
$has_local_match = true;
$matching_atomic_types[] = $new_range;
}
if ($atomic_comparison_results->type_coerced) {
continue;
}

View File

@ -2,13 +2,23 @@
namespace Psalm\Internal\Type\Comparator;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TIntRange;
use Psalm\Type\Atomic\TPositiveInt;
use Psalm\Type\Union;
use function count;
use function get_class;
/**
* @internal
*/
class IntegerRangeComparator
{
/**
* This method is used to check if an integer range can be contained in another
*/
public static function isContainedBy(
TIntRange $input_type_part,
TIntRange $container_type_part
@ -28,4 +38,142 @@ class IntegerRangeComparator
);
return $is_input_min_in_container && $is_input_max_in_container;
}
/**
* This method is used to check if an integer range can be contained by multiple int types
* Worst case scenario, the input is `int<-50,max>` and container is `-50|int<-49,50>|positive-int|57`
*/
public static function isContainedByUnion(
TIntRange $input_type_part,
Union $container_type
) : bool {
$container_atomic_types = $container_type->getAtomicTypes();
$reduced_range = clone $input_type_part;
if (isset($container_atomic_types['int'])) {
if (get_class($container_atomic_types['int']) === TInt::class) {
return true;
} elseif (get_class($container_atomic_types['int']) === TPositiveInt::class) {
if ($input_type_part->isPositive()) {
return true;
} else {
//every positive integer is satisfied by the positive-int int container so we reduce the range
$reduced_range->max_bound = 0;
unset($container_atomic_types['int']);
}
} else {
throw new \UnexpectedValueException('Should not happen: unknown int key');
}
}
$new_nb_atomics = count($container_atomic_types);
//loop until we get to a stable situation. Either we can't remove atomics or we have a definite result
do {
$nb_atomics = $new_nb_atomics;
$result_reduction = self::reduceRangeIncrementally($container_atomic_types, $reduced_range);
$new_nb_atomics = count($container_atomic_types);
} while ($result_reduction === null && $nb_atomics !== $new_nb_atomics);
if ($result_reduction === null && $nb_atomics === 0) {
//the range could not be reduced enough and there is no more atomics, it's not contained
return false;
}
if ($result_reduction === null) {
//inconclusive result, we can't remove atomics anymore.
//container: `int<1, 5>`, input: `int<0, 6>`
//container: `5`, input: `int<4, 6>`
//we assume there's no combinations that makes the input contained
return false;
}
return $result_reduction;
}
/**
* This method receives an array of atomics from the container and a range.
* The goal is to use values in atomics in order to reduce the range.
* Once the range is empty, it means that every value in range was covered by some atomics combination
* @param array<string, Atomic> $container_atomic_types
*/
private static function reduceRangeIncrementally(array &$container_atomic_types, TIntRange $reduced_range): ?bool
{
foreach ($container_atomic_types as $key => $container_atomic_type) {
if ($container_atomic_type instanceof TIntRange) {
if (self::isContainedBy($reduced_range, $container_atomic_type)) {
if ($container_atomic_type->max_bound === null && $container_atomic_type->min_bound === null) {
//this container range covers any integer
return true;
}
if ($container_atomic_type->max_bound === null) {
//this container range is int<X, max>
//X-1 becomes the max of our reduced range if it was higher
$reduced_range->max_bound = TIntRange::getNewLowestBound(
$container_atomic_type->min_bound - 1,
$reduced_range->max_bound ?? $container_atomic_type->min_bound - 1
);
unset($container_atomic_types[$key]); //we don't need this one anymore
continue;
}
if ($container_atomic_type->min_bound === null) {
//this container range is int<min, X>
//X+1 becomes the min of our reduced range if it was lower
$reduced_range->min_bound = TIntRange::getNewHighestBound(
$container_atomic_type->max_bound + 1,
$reduced_range->min_bound ?? $container_atomic_type->max_bound + 1
);
unset($container_atomic_types[$key]); //we don't need this one anymore
continue;
}
//if the container range has no 'null' bound, it's more complex
//in this case, we can only reduce if the container include one bound of our reduced range
if ($reduced_range->min_bound !== null
&& $container_atomic_type->contains($reduced_range->min_bound)
) {
//this container range is int<X, Y> and contains the min of our reduced range.
//the min from our reduced range becomes Y + 1
$reduced_range->min_bound = $container_atomic_type->max_bound + 1;
unset($container_atomic_types[$key]); //we don't need this one anymore
} elseif ($reduced_range->max_bound !== null
&& $container_atomic_type->contains($reduced_range->max_bound)) {
//this container range is int<X, Y> and contains the max of our reduced range.
//the max from our reduced range becomes X - 1
$reduced_range->max_bound = $container_atomic_type->min_bound - 1;
unset($container_atomic_types[$key]); //we don't need this one anymore
}
//there is probably a case here where we could unset containers when they're not at all in our range
} else {
//the range in input is wider than container, we return false
return false;
}
} elseif ($container_atomic_type instanceof Atomic\TLiteralInt) {
if (!$reduced_range->contains($container_atomic_type->value)) {
unset($container_atomic_types[$key]); //we don't need this one anymore
} elseif ($reduced_range->min_bound === $container_atomic_type->value) {
$reduced_range->min_bound++;
unset($container_atomic_types[$key]); //we don't need this one anymore
} elseif ($reduced_range->max_bound === $container_atomic_type->value) {
$reduced_range->max_bound--;
unset($container_atomic_types[$key]); //we don't need this one anymore
}
}
}
//there is probably a case here if we're left only with TLiteralInt where we could return false if there's less
//of them than numbers in the reduced range
//there is also a case where if there's not TLiteralInt anymore and we're left with TIntRange that don't contain
//bounds from our reduced range where we could return false
//if our reduced range has its min bound superior to its max bound, it means the container covers it all.
if ($reduced_range->min_bound !== null &&
$reduced_range->max_bound !== null &&
$reduced_range->min_bound > $reduced_range->max_bound
) {
return true;
}
//if we didn't return true or false before then the result is inconclusive for this round
return null;
}
}

View File

@ -410,6 +410,50 @@ class ScalarTypeComparator
return false;
}
if ($input_type_part instanceof TInt && $container_type_part instanceof TIntRange) {
if ($input_type_part instanceof TPositiveInt) {
if ($container_type_part->min_bound > 1) {
//any positive int can't be pushed inside a range with a min > 1
if ($atomic_comparison_result) {
$atomic_comparison_result->type_coerced = true;
$atomic_comparison_result->type_coerced_from_scalar = true;
}
return false;
}
if ($container_type_part->max_bound !== null) {
//any positive int can't be pushed inside a range where the max bound isn't max without coercion
if ($atomic_comparison_result) {
$atomic_comparison_result->type_coerced = true;
$atomic_comparison_result->type_coerced_from_scalar = true;
}
return false;
}
return true;
}
if ($input_type_part instanceof TLiteralInt) {
$min_bound = $container_type_part->min_bound;
$max_bound = $container_type_part->max_bound;
return
($min_bound === null || $min_bound <= $input_type_part->value) &&
($max_bound === null || $max_bound >= $input_type_part->value);
}
//any int can't be pushed inside a range without coercion (unless the range is from min to max)
if ($container_type_part->min_bound !== null || $container_type_part->max_bound !== null) {
if ($atomic_comparison_result) {
$atomic_comparison_result->type_coerced = true;
$atomic_comparison_result->type_coerced_from_scalar = true;
}
}
return false;
}
if (get_class($input_type_part) === TFloat::class && $container_type_part instanceof TLiteralFloat) {
if ($atomic_comparison_result) {
$atomic_comparison_result->type_coerced = true;

View File

@ -8,6 +8,7 @@ use Psalm\Type;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TArrayKey;
use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TIntRange;
use Psalm\Type\Atomic\TMixed;
use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TNumeric;
@ -108,6 +109,15 @@ class UnionTypeComparator
continue;
}
if ($input_type_part instanceof Atomic\TIntRange && $container_type->hasInt()) {
if (IntegerRangeComparator::isContainedByUnion(
$input_type_part,
$container_type
)) {
continue;
}
}
foreach ($container_type->getAtomicTypes() as $container_type_part) {
if ($ignore_null
&& $container_type_part instanceof TNull
@ -394,6 +404,32 @@ class UnionTypeComparator
foreach ($type1->getAtomicTypes() as $type1_part) {
foreach ($type2->getAtomicTypes() as $type2_part) {
//special cases for TIntRange because it can contain a part of the other type.
//For exemple int<0,1> and positive-int can be identical but none contain the other
if (($type1_part instanceof Atomic\TIntRange && $type2_part instanceof Atomic\TPositiveInt)) {
$intersection_range = TIntRange::intersectIntRanges(
TIntRange::convertToIntRange($type2_part),
$type1_part
);
return $intersection_range !== null;
}
if ($type2_part instanceof Atomic\TIntRange && $type1_part instanceof Atomic\TPositiveInt) {
$intersection_range = TIntRange::intersectIntRanges(
TIntRange::convertToIntRange($type1_part),
$type2_part
);
return $intersection_range !== null;
}
if ($type1_part instanceof Atomic\TIntRange && $type2_part instanceof Atomic\TIntRange) {
$intersection_range = TIntRange::intersectIntRanges(
$type1_part,
$type2_part
);
return $intersection_range !== null;
}
$either_contains = AtomicTypeComparator::canBeIdentical(
$codebase,
$type1_part,

View File

@ -48,7 +48,8 @@ class NegatedAssertionReconciler extends Reconciler
bool $negated,
?CodeLocation $code_location,
array $suppressed_issues,
int &$failed_reconciliation
int &$failed_reconciliation,
bool $inside_loop
): Type\Union {
$is_equality = $is_strict_equality || $is_loose_equality;
@ -211,7 +212,8 @@ class NegatedAssertionReconciler extends Reconciler
$suppressed_issues,
$failed_reconciliation,
$is_equality,
$is_strict_equality
$is_strict_equality,
$inside_loop
);
if ($simple_negated_type) {

View File

@ -40,6 +40,8 @@ use function array_filter;
use function count;
use function explode;
use function get_class;
use function max;
use function min;
use function strpos;
use function substr;
@ -105,6 +107,22 @@ class SimpleAssertionReconciler extends \Psalm\Type\Reconciler
);
}
if ($assertion[0] === '>') {
return self::reconcileSuperiorTo(
$existing_var_type,
substr($assertion, 1),
$inside_loop
);
}
if ($assertion[0] === '<') {
return self::reconcileInferiorTo(
$existing_var_type,
substr($assertion, 1),
$inside_loop
);
}
if ($assertion === 'falsy' || $assertion === 'empty') {
return self::reconcileFalsyOrEmpty(
$assertion,
@ -622,6 +640,14 @@ class SimpleAssertionReconciler extends \Psalm\Type\Reconciler
}
} elseif ($atomic_type instanceof Type\Atomic\TPositiveInt) {
$positive_types[] = $atomic_type;
} elseif ($atomic_type instanceof Type\Atomic\TIntRange) {
if (!$atomic_type->isPositive()) {
$did_remove_type = true;
}
$positive_types[] = new Type\Atomic\TIntRange(
$atomic_type->min_bound === null ? 1 : max(1, $atomic_type->min_bound),
$atomic_type->max_bound === null ? null : max(1, $atomic_type->max_bound)
);
} elseif (get_class($atomic_type) === TInt::class) {
$positive_types[] = new Type\Atomic\TPositiveInt();
$did_remove_type = true;
@ -1558,6 +1584,103 @@ class SimpleAssertionReconciler extends \Psalm\Type\Reconciler
return $existing_var_type;
}
private static function reconcileSuperiorTo(
Union $existing_var_type,
string $assertion,
bool $inside_loop
) : Union {
$assertion_value = (int)$assertion;
foreach ($existing_var_type->getAtomicTypes() as $atomic_type) {
if ($inside_loop) {
continue;
}
if ($atomic_type instanceof Atomic\TIntRange) {
$existing_var_type->removeType($atomic_type->getKey());
if ($atomic_type->min_bound === null) {
$atomic_type->min_bound = $assertion_value;
} else {
$atomic_type->min_bound = Atomic\TIntRange::getNewHighestBound(
$assertion_value,
$atomic_type->min_bound
);
}
$existing_var_type->addType($atomic_type);
} elseif ($atomic_type instanceof Atomic\TLiteralInt) {
$new_range = new Atomic\TIntRange($assertion_value, null);
if (!$new_range->contains($atomic_type->value)) {
$existing_var_type->removeType($atomic_type->getKey());
} /*elseif ($inside_loop) {
//when inside a loop, allow the range to extends the type
$existing_var_type->removeType($atomic_type->getKey());
if ($atomic_type->value < $assertion_value) {
$existing_var_type->addType(new Atomic\TIntRange($atomic_type->value, $assertion_value));
} else {
$existing_var_type->addType(new Atomic\TIntRange($assertion_value, $atomic_type->value));
}
}*/
} elseif ($atomic_type instanceof Atomic\TPositiveInt) {
if ($assertion_value <= 0) {
//emit an issue here in the future about incompatible type
}
$existing_var_type->removeType($atomic_type->getKey());
$existing_var_type->addType(new Atomic\TIntRange($assertion_value, null));
} elseif ($atomic_type instanceof TInt) {
$existing_var_type->removeType($atomic_type->getKey());
$existing_var_type->addType(new Atomic\TIntRange($assertion_value, null));
}
}
return $existing_var_type;
}
private static function reconcileInferiorTo(
Union $existing_var_type,
string $assertion,
bool $inside_loop
) : Union {
$assertion_value = (int)$assertion;
foreach ($existing_var_type->getAtomicTypes() as $atomic_type) {
if ($inside_loop) {
continue;
}
if ($atomic_type instanceof Atomic\TIntRange) {
$existing_var_type->removeType($atomic_type->getKey());
if ($atomic_type->max_bound === null) {
$atomic_type->max_bound = $assertion_value;
} else {
$atomic_type->max_bound = min($atomic_type->max_bound, $assertion_value);
}
$existing_var_type->addType($atomic_type);
} elseif ($atomic_type instanceof Atomic\TLiteralInt) {
$new_range = new Atomic\TIntRange(null, $assertion_value);
if (!$new_range->contains($atomic_type->value)) {
$existing_var_type->removeType($atomic_type->getKey());
}/* elseif ($inside_loop) {
//when inside a loop, allow the range to extends the type
$existing_var_type->removeType($atomic_type->getKey());
if ($atomic_type->value < $assertion_value) {
$existing_var_type->addType(new Atomic\TIntRange($atomic_type->value, $assertion_value));
} else {
$existing_var_type->addType(new Atomic\TIntRange($assertion_value, $atomic_type->value));
}
}*/
} elseif ($atomic_type instanceof Atomic\TPositiveInt) {
if ($assertion_value <= 0) {
//emit an issue here in the future about incompatible type
}
$existing_var_type->removeType($atomic_type->getKey());
$existing_var_type->addType(new Atomic\TIntRange(1, $assertion_value));
} elseif ($atomic_type instanceof TInt) {
$existing_var_type->removeType($atomic_type->getKey());
$existing_var_type->addType(new Atomic\TIntRange(null, $assertion_value));
}
}
return $existing_var_type;
}
/**
* @param string[] $suppressed_issues
* @param 0|1|2 $failed_reconciliation
@ -2227,6 +2350,7 @@ class SimpleAssertionReconciler extends \Psalm\Type\Reconciler
if ($existing_var_type->hasInt()) {
$existing_int_types = $existing_var_type->getLiteralInts();
$existing_range_types = $existing_var_type->getRangeInts();
if ($existing_int_types) {
foreach ($existing_int_types as $int_key => $literal_type) {
@ -2235,6 +2359,14 @@ class SimpleAssertionReconciler extends \Psalm\Type\Reconciler
$did_remove_type = true;
}
}
} elseif ($existing_range_types) {
foreach ($existing_range_types as $int_key => $literal_type) {
if ($literal_type->contains(0)) {
$existing_var_type->removeType($int_key);
$existing_var_type->addType(new Type\Atomic\TLiteralInt(0));
$did_remove_type = true;
}
}
} else {
$did_remove_type = true;
$existing_var_type->removeType('int');

View File

@ -25,8 +25,10 @@ use Psalm\Type\Atomic\TString;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Atomic\TTrue;
use Psalm\Type\Reconciler;
use Psalm\Type\Union;
use function get_class;
use function max;
use function substr;
class SimpleNegatedAssertionReconciler extends Reconciler
@ -46,7 +48,8 @@ class SimpleNegatedAssertionReconciler extends Reconciler
array $suppressed_issues = [],
int &$failed_reconciliation = 0,
bool $is_equality = false,
bool $is_strict_equality = false
bool $is_strict_equality = false,
bool $inside_loop = false
) : ?Type\Union {
if ($assertion === 'object' && !$existing_var_type->hasMixed()) {
return self::reconcileObject(
@ -231,6 +234,22 @@ class SimpleNegatedAssertionReconciler extends Reconciler
return $existing_var_type;
}
if ($assertion[0] === '>') {
return self::reconcileSuperiorTo(
$existing_var_type,
substr($assertion, 1),
$inside_loop
);
}
if ($assertion[0] === '<') {
return self::reconcileInferiorTo(
$existing_var_type,
substr($assertion, 1),
$inside_loop
);
}
return null;
}
@ -1651,4 +1670,97 @@ class SimpleNegatedAssertionReconciler extends Reconciler
}
}
}
private static function reconcileSuperiorTo(Union $existing_var_type, string $assertion, bool $inside_loop): Union
{
$assertion_value = (int)$assertion - 1;
foreach ($existing_var_type->getAtomicTypes() as $atomic_type) {
if ($inside_loop) {
continue;
}
if ($atomic_type instanceof Atomic\TIntRange) {
$existing_var_type->removeType($atomic_type->getKey());
if ($atomic_type->max_bound === null) {
$atomic_type->max_bound = $assertion_value;
} else {
$atomic_type->max_bound = Atomic\TIntRange::getNewLowestBound(
$assertion_value,
$atomic_type->max_bound
);
}
$existing_var_type->addType($atomic_type);
} elseif ($atomic_type instanceof Atomic\TLiteralInt) {
$new_range = new Atomic\TIntRange(null, $assertion_value);
if (!$new_range->contains($atomic_type->value)) {
//emit an issue here in the future about incompatible type
$existing_var_type->removeType($atomic_type->getKey());
} /*elseif ($inside_loop) {
//when inside a loop, allow the range to extends the type
$existing_var_type->removeType($atomic_type->getKey());
if ($atomic_type->value < $assertion_value) {
$existing_var_type->addType(new Atomic\TIntRange($atomic_type->value, $assertion_value));
} else {
$existing_var_type->addType(new Atomic\TIntRange($assertion_value, $atomic_type->value));
}
}*/
} elseif ($atomic_type instanceof Atomic\TPositiveInt) {
if ($assertion_value > 0) {
//emit an issue here in the future about incompatible type
}
$existing_var_type->removeType($atomic_type->getKey());
$existing_var_type->addType(new Atomic\TIntRange(null, $assertion_value));
} elseif ($atomic_type instanceof TInt) {
$existing_var_type->removeType($atomic_type->getKey());
$existing_var_type->addType(new Atomic\TIntRange(null, $assertion_value));
}
}
return $existing_var_type;
}
private static function reconcileInferiorTo(Union $existing_var_type, string $assertion, bool $inside_loop): Union
{
$assertion_value = (int)$assertion + 1;
foreach ($existing_var_type->getAtomicTypes() as $atomic_type) {
if ($inside_loop) {
continue;
}
if ($atomic_type instanceof Atomic\TIntRange) {
$existing_var_type->removeType($atomic_type->getKey());
if ($atomic_type->min_bound === null) {
$atomic_type->min_bound = $assertion_value;
} else {
$atomic_type->min_bound = max($atomic_type->min_bound, $assertion_value);
}
$existing_var_type->addType($atomic_type);
} elseif ($atomic_type instanceof Atomic\TLiteralInt) {
$new_range = new Atomic\TIntRange($assertion_value, null);
if (!$new_range->contains($atomic_type->value)) {
//emit an issue here in the future about incompatible type
$existing_var_type->removeType($atomic_type->getKey());
}/* elseif ($inside_loop) {
//when inside a loop, allow the range to extends the type
$existing_var_type->removeType($atomic_type->getKey());
if ($atomic_type->value < $assertion_value) {
$existing_var_type->addType(new Atomic\TIntRange($atomic_type->value, $assertion_value));
} else {
$existing_var_type->addType(new Atomic\TIntRange($assertion_value, $atomic_type->value));
}
}*/
} elseif ($atomic_type instanceof Atomic\TPositiveInt) {
if ($assertion_value > 0) {
//emit an issue here in the future about incompatible type
}
$existing_var_type->removeType($atomic_type->getKey());
$existing_var_type->addType(new Atomic\TIntRange($assertion_value, 1));
} elseif ($atomic_type instanceof TInt) {
$existing_var_type->removeType($atomic_type->getKey());
$existing_var_type->addType(new Atomic\TIntRange($assertion_value, null));
}
}
return $existing_var_type;
}
}

View File

@ -21,6 +21,7 @@ use Psalm\Type\Atomic\TFalse;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TIntRange;
use Psalm\Type\Atomic\TIterable;
use Psalm\Type\Atomic\TKeyedArray;
use Psalm\Type\Atomic\TList;
@ -1199,6 +1200,24 @@ class TypeCombiner
}
);
if (isset($combination->value_types['int'])) {
$current_int_type = $combination->value_types['int'];
if ($current_int_type instanceof TIntRange) {
foreach ($combination->ints as $int) {
if (!$current_int_type->contains($int->value)) {
$current_int_type->min_bound = TIntRange::getNewLowestBound(
$current_int_type->min_bound,
$int->value
);
$current_int_type->max_bound = TIntRange::getNewHighestBound(
$current_int_type->max_bound,
$int->value
);
}
}
}
}
$combination->ints = null;
if (!isset($combination->value_types['int'])) {
@ -1236,10 +1255,35 @@ class TypeCombiner
$combination->value_types['int'] = $type;
} elseif (isset($combination->value_types['int'])
&& get_class($combination->value_types['int'])
!== get_class($type)
!== get_class($type)
) {
$combination->value_types['int'] = new TInt();
}
} elseif ($type instanceof TIntRange) {
if ($combination->ints) {
foreach ($combination->ints as $int) {
if (!$type->contains($int->value)) {
$type->min_bound = TIntRange::getNewLowestBound($type->min_bound, $int->value);
$type->max_bound = TIntRange::getNewHighestBound($type->max_bound, $int->value);
}
}
$combination->value_types['int'] = $type;
} elseif (!isset($combination->value_types['int'])) {
$combination->value_types['int'] = $type;
} else {
$old_type = $combination->value_types['int'];
if ($old_type instanceof TIntRange) {
$type->min_bound = TIntRange::getNewLowestBound($old_type->min_bound, $type->min_bound);
$type->max_bound = TIntRange::getNewHighestBound($old_type->max_bound, $type->max_bound);
} elseif ($old_type instanceof TPositiveInt) {
$type->min_bound = TIntRange::getNewLowestBound($type->min_bound, 0);
$type->max_bound = null;
} else {
$type = new TInt();
}
$combination->value_types['int'] = $type;
}
} else {
$combination->value_types['int'] = $type;
}

View File

@ -1,6 +1,9 @@
<?php
namespace Psalm\Type\Atomic;
use function max;
use function min;
/**
* Denotes an interval of integers between two bounds
*/
@ -70,4 +73,81 @@ class TIntRange extends TInt
{
return $this->min_bound !== null && $this->min_bound > 0;
}
public function isNegative(): bool
{
return $this->max_bound !== null && $this->max_bound < 0;
}
public function isPositiveOrZero(): bool
{
return $this->min_bound !== null && $this->min_bound >= 0;
}
public function isNegativeOrZero(): bool
{
return $this->max_bound !== null && $this->max_bound <= 0;
}
public function contains(int $i): bool
{
return
($this->min_bound === null && $this->max_bound === null) ||
($this->min_bound === null && $this->max_bound >= $i) ||
($this->max_bound === null && $this->min_bound <= $i) ||
($this->min_bound <= $i && $this->max_bound >= $i);
}
public static function getNewLowestBound(?int $bound1, ?int $bound2): ?int
{
if ($bound1 === null || $bound2 === null) {
return null;
}
return min($bound1, $bound2);
}
public static function getNewHighestBound(?int $bound1, ?int $bound2): ?int
{
if ($bound1 === null || $bound2 === null) {
return null;
}
return max($bound1, $bound2);
}
/**
* convert any int to its equivalent in int range
*/
public static function convertToIntRange(TInt $int_atomic): TIntRange
{
if ($int_atomic instanceof TPositiveInt) {
return new TIntRange(1, null);
}
if ($int_atomic instanceof TLiteralInt) {
return new TIntRange($int_atomic->value, $int_atomic->value);
}
return new TIntRange(null, null);
}
public static function intersectIntRanges(TIntRange $int_range1, TIntRange $int_range2): ?TIntRange
{
if ($int_range1->min_bound === null || $int_range2->min_bound === null) {
$new_min_bound = $int_range1->min_bound ?? $int_range2->min_bound;
} else {
$new_min_bound = self::getNewHighestBound($int_range1->min_bound, $int_range2->min_bound);
}
if ($int_range1->max_bound === null || $int_range2->max_bound === null) {
$new_max_bound = $int_range1->max_bound ?? $int_range2->max_bound;
} else {
$new_max_bound = self::getNewLowestBound($int_range1->max_bound, $int_range2->max_bound);
}
if ($new_min_bound !== null && $new_max_bound !== null && $new_min_bound > $new_max_bound) {
return null;
}
return new self($new_min_bound, $new_max_bound);
}
}

View File

@ -801,7 +801,10 @@ class Union implements TypeNode
public function hasInt(): bool
{
return isset($this->types['int']) || isset($this->types['array-key']) || $this->literal_int_types;
return isset($this->types['int']) || isset($this->types['array-key']) || $this->literal_int_types
|| array_filter($this->types, function (Atomic $type) {
return $type instanceof Type\Atomic\TIntRange;
});
}
public function hasPositiveInt(): bool
@ -1047,9 +1050,9 @@ class Union implements TypeNode
continue;
}
/*if ($atomic_type instanceof Type\Atomic\TIntRange && !$atomic_type->contains(0)) {
if ($atomic_type instanceof Type\Atomic\TIntRange && !$atomic_type->contains(0)) {
continue;
}*/
}
if ($atomic_type instanceof Type\Atomic\TPositiveInt) {
continue;
@ -1593,6 +1596,21 @@ class Union implements TypeNode
return $this->literal_int_types;
}
/**
* @return array<string, Type\Atomic\TIntRange>
*/
public function getRangeInts(): array
{
$ranges = [];
foreach ($this->getAtomicTypes() as $atomic) {
if ($atomic instanceof Type\Atomic\TIntRange) {
$ranges[$atomic->getKey()] = $atomic;
}
}
return $ranges;
}
/**
* @return array<string, TLiteralFloat>
*/

View File

@ -42,6 +42,532 @@ class IntRangeTest extends TestCase
return $a;
}',
],
'intReduced' => [
'<?php
function getInt(): int{return 0;}
$a = $b = $c = getInt();
assert($a >= 500);
assert($a < 5000);
assert($b >= -5000);
assert($b < -501);
assert(-60 > $c);
assert(-500 < $c);',
'assertions' => [
'$a===' => 'int<500, 4999>',
'$b===' => 'int<-5000, -502>',
'$c===' => 'int<-499, -61>',
]
],
'complexAssertions' => [
'<?php
function getInt(): int{return 0;}
$a = getInt();
assert($a >= 495 + 5);
$b = 5000;
assert($a < $b);
',
'assertions' => [
'$a===' => 'int<500, 4999>',
]
],
'negatedAssertions' => [
'<?php
function getInt(): int{return 0;}
$a = $b = $c = $d = $e = $f = $g = $h = $i = $j = $k = $l = $m = $n = $o = $p = getInt();
//>
if($a > 10){
die();
}
if($b > -10){
die();
}
//<
if($c < 500){
die();
}
if($d < -500){
die();
}
//>=
if($e >= 10){
die();
}
if($f >= -10){
die();
}
//<=
if($g <= 500){
die();
}
if($h <= -500){
die();
}
//>
if(10 > $i){
die();
}
if(-10 > $j){
die();
}
//<
if(500 < $k){
die();
}
if(-500 < $l){
die();
}
//>=
if(10 >= $m){
die();
}
if(-10 >= $n){
die();
}
//<=
if(500 <= $o){
die();
}
if(-500 <= $p){
die();
}
//inverse
',
'assertions' => [
'$a===' => 'int<min, 10>',
'$b===' => 'int<min, -10>',
'$c===' => 'int<500, max>',
'$d===' => 'int<-500, max>',
'$e===' => 'int<min, 9>',
'$f===' => 'int<min, -11>',
'$g===' => 'int<501, max>',
'$h===' => 'int<-499, max>',
'$i===' => 'int<10, max>',
'$j===' => 'int<-10, max>',
'$k===' => 'int<min, 500>',
'$l===' => 'int<min, -500>',
'$m===' => 'int<11, max>',
'$n===' => 'int<-9, max>',
'$o===' => 'int<min, 499>',
'$p===' => 'int<min, -501>',
]
],
'intOperations' => [
'<?php
function getInt(): int{return 0;}
$a = getInt();
assert($a >= 500);
assert($a < 5000);
$b = $a % 10;
$c = $a ** 2;
$d = $a - 5;
$e = $a * 1;',
'assertions' => [
'$b===' => 'int<0, 9>',
'$c===' => 'int<1, max>',
'$d===' => 'int<495, 4994>',
'$e===' => 'int<500, 4999>'
]
],
'mod' => [
'<?php
function getInt(): int{return 0;}
$a = $b = $c = $d = getInt();
assert($a >= 20);//positive range
assert($b <= -20);//negative range
/** @var int<0, 0> $c */; // 0 range
assert($d >= -100);// mixed range
assert($d <= 100);// mixed range
/** @var int<5, 5> $e */; // 5 range
$f = $a % $e;
$g = $b % $e;
$h = $d % $e;
$i = -3 % $a;
$j = -3 % $b;
$k = -3 % $c;
$l = -3 % $d;
$m = 3 % $a;
$n = 3 % $b;
$o = 3 % $c;
$p = 3 % $d;
$q = $a % 0;
$r = $a % 3;
$s = $a % -3;
$t = $b % 0;
$u = $b % 3;
$v = $b % -3;
$w = $c % 0;
$x = $c % 3;
$y = $c % -3;
$z = $d % 0;
$aa = $d % 3;
$ab = $d % -3;
',
'assertions' => [
'$f===' => 'int<0, 4>',
'$g===' => 'int<-4, 0>',
'$h===' => 'int<-4, 4>',
'$i===' => 'int<min, 0>',
'$j===' => 'int<min, 0>',
'$k===' => 'empty',
'$l===' => 'int',
'$m===' => 'int<0, max>',
'$n===' => 'int<min, 0>',
'$o===' => 'empty',
'$p===' => 'int',
'$q===' => 'empty',
'$r===' => 'int<0, 2>',
'$s===' => 'int<-2, 0>',
'$t===' => 'empty',
'$u===' => 'int<-2, 0>',
'$v===' => 'int<2, 0>',
'$w===' => 'empty',
'$x===' => 'int<0, 2>',
'$y===' => 'int<-2, 0>',
'$z===' => 'empty',
'$aa===' => 'int<-2, 2>',
'$ab===' => 'int<-2, 2>',
]
],
'pow' => [
'<?php
function getInt(): int{return 0;}
$a = $b = $c = $d = getInt();
assert($a >= 2);//positive range
assert($b <= -2);//negative range
/** @var int<0, 0> $c */; // 0 range
assert($d >= -100);// mixed range
assert($d <= 100);// mixed range
$e = 0 ** $a;
$f = 0 ** $b;
$g = 0 ** $c;
$h = 0 ** $d;
$i = (-2) ** $a;
$j = (-2) ** $b;
$k = (-2) ** $c;
$l = (-2) ** $d;
$m = 2 ** $a;
$n = 2 ** $b;
$o = 2 ** $c;
$p = 2 ** $d;
$q = $a ** 0;
$r = $a ** 2;
$s = $a ** -2;
$t = $b ** 0;
$u = $b ** 2;
$v = $b ** -2;
$w = $c ** 0;
$x = $c ** 2;
$y = $c ** -2;
$z = $d ** 0;
$aa = $d ** 2;
$ab = $d ** -2;
',
'assertions' => [
'$e===' => '0',
'$f===' => 'empty',
'$g===' => '1',
'$h===' => 'empty',
'$i===' => 'int',
'$j===' => 'float',
'$k===' => '-1',
'$l===' => 'float|int',
'$m===' => 'int<1, max>',
'$n===' => 'float',
'$o===' => '1',
'$p===' => 'float|int',
'$q===' => '1',
'$r===' => 'int<1, max>',
'$s===' => 'float',
'$t===' => '-1',
'$u===' => 'int<1, max>',
'$v===' => 'float',
'$w===' => '1',
'$x===' => '0',
'$y===' => 'empty',
'$z===' => '1',
'$aa===' => 'int<1, max>',
'$ab===' => 'float',
]
],
'multiplications' => [
'<?php
function getInt(): int{return 0;}
$a = $b = $c = $d = $e = $f = $g = $h = $i = $j = $k = $l = $m = $n = $o = $p = getInt();
assert($b <= -2);
assert($c <= 2);
assert($d >= -2);
assert($e >= 2);
assert($f >= -2);
assert($f <= 2);
$g = $a * $b;
$h = $a * $c;
$i = $a * $d;
$j = $a * $e;
$k = $a * $f;
$l = $b * $b;
$m = $b * $c;
$n = $b * $d;
$o = $b * $e;
$p = $b * $f;
$q = $c * $c;
$r = $c * $d;
$s = $c * $e;
$t = $c * $f;
$u = $d * $d;
$v = $d * $e;
$w = $d * $f;
$x = $e * $e;
$y = $d * $f;
$z = $f * $f;
',
'assertions' => [
'$g===' => 'int',
'$h===' => 'int',
'$i===' => 'int',
'$j===' => 'int',
'$k===' => 'int',
'$l===' => 'int<4, max>',
'$m===' => 'int',
'$n===' => 'int',
'$o===' => 'int<min, -4>',
'$p===' => 'int',
'$q===' => 'int',
'$r===' => 'int',
'$s===' => 'int',
'$t===' => 'int',
'$u===' => 'int',
'$v===' => 'int',
'$w===' => 'int',
'$x===' => 'int<4, max>',
'$y===' => 'int',
'$z===' => 'int<-4, 4>',
]
],
'SKIPPED-intLoopPositive' => [
'<?php
//skipped, int range in loops not supported yet
for($i = 0; $i < 10; $i++){
}',
'assertions' => [
'$i===' => 'int<0, 9>'
]
],
'SKIPPED-intLoopNegative' => [
'<?php
//skipped, int range in loops not supported yet
for($i = 10; $i > 1; $i--){
}',
'assertions' => [
'$i===' => 'int<2, 10>'
]
],
'integrateExistingArrayPositive' => [
'<?php
/** @return int<5, max> */
function getInt()
{
return 7;
}
$_arr = ["a", "b", "c"];
$a = getInt();
$_arr[$a] = 12;',
'assertions' => [
'$_arr===' => 'non-empty-array<int<0, max>, "a"|"b"|"c"|12>'
]
],
'integrateExistingArrayNegative' => [
'<?php
/** @return int<min, -1> */
function getInt()
{
return -2;
}
$_arr = ["a", "b", "c"];
$a = getInt();
$_arr[$a] = 12;',
'assertions' => [
'$_arr===' => 'non-empty-array<int<min, 2>, "a"|"b"|"c"|12>'
]
],
'SKIPPED-statementsInLoopAffectsEverything' => [
'<?php
//skipped, int range in loops not supported yet
$remainder = 1;
for ($i = 0; $i < 5; $i++) {
if ($remainder) {
$remainder--;
}
}',
'assertions' => [
'$remainder===' => 'int<min, 1>'
]
],
'SKIPPED-IntRangeRestrictWhenUntouched' => [
'<?php
//skipped, int range in loops not supported yet
foreach ([1, 2, 3] as $i) {
if ($i > 1) {
takesInt($i);
}
}
/** @psalm-param int<2, 3> $i */
function takesInt(int $i): void{
return;
}',
],
'SKIPPED-wrongLoopAssertion' => [
'<?php
//skipped, int range in loops not supported yet
function a(): array {
$type_tokens = getArray();
for ($i = 0, $l = rand(0,100); $i < $l; ++$i) {
/** @psalm-trace $i */;
if ($i > 0 && rand(0,1)) {
continue;
}
/** @psalm-trace $i */;
$type_tokens[$i] = "";
/** @psalm-trace $type_tokens */;
if($i > 1){
$type_tokens[$i - 2];
}
}
return [];
}
/** @return array<int, string> */
function getArray(): array{
return [];
}'
],
'IntRangeContainedInMultipleInt' => [
'<?php
$_arr = [];
foreach ([0, 1] as $i) {
$_arr[$i] = 1;
}
/** @var int<0,1> $j */
$j = 0;
echo $_arr[$j];'
],
'modulo' => [
'<?php
function getInt(): int{return 0;}
$a = getInt();
$b = $a % 10;
assert($a > 0);
$c = $a % 10;
$d = $a % $a;',
'assertions' => [
'$b===' => 'int<-9, 9>',
'$c===' => 'int<0, 9>',
'$d===' => '0|positive-int'
],
],
'minus' => [
'<?php
function getInt(): int{return 0;}
$a = $b = $d = $e = getInt();
assert($a > 5);
assert($a <= 10);
assert($b > -10);
assert($b <= 100);
$c = $a - $b;
$f = $a - $d;
assert($e > 0);
$g = $a - $e;
',
'assertions' => [
'$c===' => 'int<-94, 19>',
'$f===' => 'int<min, max>',
'$g===' => 'int<min, 9>',
],
],
'bits' => [
'<?php
function getInt(): int{return 0;}
$a = $b = $c = getInt();
assert($a > 5);
assert($b <= 6);
$d = $a ^ $b;
$e = $a & $b;
$f = $a | $b;
$g = $a << $b;
$h = $a >> $b;
',
'assertions' => [
'$d===' => 'int',
'$e===' => 'int',
'$f===' => 'int',
'$g===' => 'int',
'$h===' => 'int',
],
],
'UnaryMinus' => [
'<?php
function getInt(): int{return 0;}
$a = $c = $e = getInt();
assert($a > 5);
$b = -$a;
assert($c > 0);
$d = -$c;
assert($e > 5);
assert($e < 10);
$f = -$e;
',
'assertions' => [
'$b===' => 'int<min, -6>',
'$d===' => 'int<min, -1>',
'$f===' => 'int<-9, -6>',
],
],
'intersections' => [
'<?php
function getInt(): int{return 0;}
$a = getInt();
/** @var int<0, 10> $a */
$b = -$a;
$c = null;
if($b === $a){
//$b and $a should intersect at 0, so $c should be 0
$c = $b;
}
',
'assertions' => [
'$c===' => 'int<0, 0>|null',
],
],
];
}

View File

@ -358,7 +358,7 @@ class WhileTest extends \Psalm\Tests\TestCase
function foo() : void {
$pointers = ["hi"];
while (rand(0, 1) && 0 < ($parent = 0)) {
while (rand(0, 1) && -1 < ($parent = 0)) {
print $pointers[$parent];
}
}'

View File

@ -2675,6 +2675,48 @@ class ConditionalTest extends \Psalm\Tests\TestCase
'$_a===' => '"N"|"Y"',
]
],
'nullErasureWithSmallerAndGreater' => [
'<?php
function getIntOrNull(): ?int{return null;}
$a = getIntOrNull();
if ($a < 0) {
echo $a + 3;
}
if ($a <= 0) {
/** @psalm-suppress PossiblyNullOperand */
echo $a + 3;
}
if ($a > 0) {
echo $a + 3;
}
if ($a >= 0) {
/** @tmp-psalm-suppress PossiblyNullOperand this should be suppressed but assertions remove null for now */
echo $a + 3;
}
if (0 < $a) {
echo $a + 3;
}
if (0 <= $a) {
/** @tmp-psalm-suppress PossiblyNullOperand this should be suppressed but assertions remove null for now */
echo $a + 3;
}
if (0 > $a) {
echo $a + 3;
}
if (0 >= $a) {
/** @psalm-suppress PossiblyNullOperand */
echo $a + 3;
}
',
],
];
}
@ -2734,24 +2776,6 @@ class ConditionalTest extends \Psalm\Tests\TestCase
}',
'error_message' => 'TypeDoesNotContainType',
],
'dontEraseNullAfterLessThanCheck' => [
'<?php
$a = mt_rand(0, 1) ? mt_rand(-10, 10): null;
if ($a < -1) {
echo $a + 3;
}',
'error_message' => 'PossiblyNullOperand',
],
'dontEraseNullAfterGreaterThanCheck' => [
'<?php
$a = mt_rand(0, 1) ? mt_rand(-10, 10): null;
if (0 > $a) {
echo $a + 3;
}',
'error_message' => 'PossiblyNullOperand',
],
'nonRedundantConditionGivenDocblockType' => [
'<?php
/** @param array[] $arr */