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:
commit
90e1662964
@ -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->tags['variablesfrom'][0]</code>
|
||||
@ -117,7 +117,7 @@
|
||||
<code>$gettype_expr->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->children[0]</code>
|
||||
<code>$parse_tree->children[0]</code>
|
||||
<code>$parse_tree->condition->children[0]</code>
|
||||
<code>array_keys($offset_template_data)[0]</code>
|
||||
<code>array_keys($template_type_map[$array_param_name])[0]</code>
|
||||
|
@ -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] : [];
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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) {
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
*/
|
||||
|
@ -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',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -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];
|
||||
}
|
||||
}'
|
||||
|
@ -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 */
|
||||
|
Loading…
x
Reference in New Issue
Block a user