From 1e3e6a85e4dcefd4efa8bfe7068208b5197da6ad Mon Sep 17 00:00:00 2001 From: orklah Date: Fri, 30 Jul 2021 21:44:51 +0200 Subject: [PATCH 1/3] introduce basic integer range --- .../Comparator/IntegerRangeComparator.php | 73 ++++++++++++++++++ .../Type/Comparator/ScalarTypeComparator.php | 12 ++- src/Psalm/Internal/Type/TypeParser.php | 47 +++++++++++ src/Psalm/Type/Atomic/TIntRange.php | 77 +++++++++++++++++++ tests/IntRangeTest.php | 72 +++++++++++++++++ 5 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php create mode 100644 src/Psalm/Type/Atomic/TIntRange.php create mode 100644 tests/IntRangeTest.php diff --git a/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php new file mode 100644 index 00000000000..8400b7fbdd0 --- /dev/null +++ b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php @@ -0,0 +1,73 @@ +min_bound === TIntRange::BOUND_MIN; + $is_input_max = $input_type_part->max_bound === TIntRange::BOUND_MAX; + $is_container_min = $container_type_part->min_bound === TIntRange::BOUND_MIN; + $is_container_max = $container_type_part->max_bound === TIntRange::BOUND_MAX; + + $is_input_min_in_container = ( + $is_container_min || + (!$is_input_min && $container_type_part->min_bound <= $input_type_part->min_bound) + ); + $is_input_max_in_container = ( + $is_container_max || + (!$is_input_max && $container_type_part->max_bound >= $input_type_part->max_bound) + ); + return $is_input_min_in_container && $is_input_max_in_container; + } +} diff --git a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php index 79012adad0f..39fcc8b1c2d 100644 --- a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php @@ -19,6 +19,7 @@ use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\THtmlEscapedString; use Psalm\Type\Atomic\TInt; +use Psalm\Type\Atomic\TIntRange; use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; @@ -383,14 +384,23 @@ public static function isContainedBy( return false; } + if ($input_type_part instanceof TIntRange && $container_type_part instanceof TIntRange) { + return IntegerRangeComparator::isContainedBy( + $input_type_part, + $container_type_part + ); + } + if ($input_type_part instanceof TInt && $container_type_part instanceof TPositiveInt) { if ($input_type_part instanceof TPositiveInt) { return true; } - if ($input_type_part instanceof TLiteralInt) { return $input_type_part->value > 0; } + if ($input_type_part instanceof TIntRange) { + return $input_type_part->isPositive(); + } if ($atomic_comparison_result) { $atomic_comparison_result->type_coerced = true; diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index a5d4bcace47..d505223ed3f 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -13,6 +13,7 @@ use Psalm\Type\Atomic\TClassStringMap; use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TGenericObject; +use Psalm\Type\Atomic\TIntRange; use Psalm\Type\Atomic\TIterable; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TList; @@ -466,6 +467,7 @@ function ($int) { * @param array $type_aliases * @return Atomic|Union * @throws TypeParseTreeException + * @psalm-suppress ComplexMethod to be refactored */ private static function getTypeFromGenericTree( ParseTree\GenericTree $parse_tree, @@ -777,6 +779,51 @@ private static function getTypeFromGenericTree( return new Atomic\TIntMaskOf($param_type); } + if ($generic_type_value === 'int') { + if (count($generic_params) !== 2) { + throw new TypeParseTreeException('int range must have 2 params'); + } + + $min_bound = Atomic\TIntRange::BOUND_MIN; + $max_bound = Atomic\TIntRange::BOUND_MAX; + + $param0_union_types = array_values($generic_params[0]->getAtomicTypes()); + $param1_union_types = array_values($generic_params[1]->getAtomicTypes()); + + if (count($param0_union_types) > 1 || count($param1_union_types) > 1) { + throw new TypeParseTreeException('Union types are not allowed in int range type'); + } + + if ($param0_union_types[0] instanceof TNamedObject && + $param0_union_types[0]->value === TIntRange::BOUND_MAX + ) { + throw new TypeParseTreeException("min bound for int range param can't be 'max'"); + } + if ($param1_union_types[0] instanceof TNamedObject && + $param1_union_types[0]->value === TIntRange::BOUND_MIN + ) { + throw new TypeParseTreeException("max bound for int range param can't be 'min'"); + } + + + if ($param0_union_types[0] instanceof TLiteralInt) { + $min_bound = $param0_union_types[0]->value; + } + if ($param1_union_types[0] instanceof TLiteralInt) { + $max_bound = $param1_union_types[0]->value; + } + + if ($min_bound === TIntRange::BOUND_MIN && $max_bound === TIntRange::BOUND_MAX) { + return new Atomic\TInt(); + } + + if ($min_bound === 0 && $max_bound === TIntRange::BOUND_MAX) { + return new Atomic\TPositiveInt(); + } + + return new Atomic\TIntRange($min_bound, $max_bound); + } + if (isset(TypeTokenizer::PSALM_RESERVED_WORDS[$generic_type_value]) && $generic_type_value !== 'self' && $generic_type_value !== 'static' diff --git a/src/Psalm/Type/Atomic/TIntRange.php b/src/Psalm/Type/Atomic/TIntRange.php new file mode 100644 index 00000000000..09206467dc4 --- /dev/null +++ b/src/Psalm/Type/Atomic/TIntRange.php @@ -0,0 +1,77 @@ +min_bound = $min_bound; + $this->max_bound = $max_bound; + } + + public function __toString(): string + { + return $this->getKey(); + } + + public function getKey(bool $include_extra = true): string + { + return 'int<' . $this->min_bound . ', ' . $this->max_bound . '>'; + } + + public function canBeFullyExpressedInPhp(int $php_major_version, int $php_minor_version): bool + { + return false; + } + + /** + * @param array $aliased_classes + */ + public function toPhpString( + ?string $namespace, + array $aliased_classes, + ?string $this_class, + int $php_major_version, + int $php_minor_version + ): ?string { + return $php_major_version >= 7 ? 'int' : null; + } + + /** + * @param array $aliased_classes + */ + public function toNamespacedString( + ?string $namespace, + array $aliased_classes, + ?string $this_class, + bool $use_phpdoc_format + ): string { + return $use_phpdoc_format ? 'int' : 'int<' . $this->min_bound . ', ' . $this->max_bound . '>'; + } + + public function isPositive(): bool + { + return $this->min_bound !== self::BOUND_MIN && $this->min_bound > 0; + } +} diff --git a/tests/IntRangeTest.php b/tests/IntRangeTest.php new file mode 100644 index 00000000000..10b1d5fa2ce --- /dev/null +++ b/tests/IntRangeTest.php @@ -0,0 +1,72 @@ +,error_levels?:string[]}> + */ + public function providerValidCodeParse(): iterable + { + return [ + 'intRangeContained' => [ + ' $a + * @return int<-1, max> + */ + function scope(int $a){ + return $a; + }', + ], + 'positiveIntRange' => [ + ' $a + * @return positive-int + */ + function scope(int $a){ + return $a; + }', + ], + 'intRangeToInt' => [ + ' $a + * @return int + */ + function scope(int $a){ + return $a; + }', + ], + ]; + } + + /** + * @return iterable + */ + public function providerInvalidCodeParse(): iterable + { + return [ + 'intRangeNotContained' => [ + ' $a + * @return int<-1, 11> + * @psalm-suppress InvalidReturnStatement + */ + function scope(int $a){ + return $a; + }', + 'error_message' => 'InvalidReturnType', + ], + ]; + } +} From 0252a65e0d7b37581c3bfb9f70c4a96fd6b7ec82 Mon Sep 17 00:00:00 2001 From: orklah Date: Fri, 30 Jul 2021 21:46:11 +0200 Subject: [PATCH 2/3] fix syntax --- src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php index 8400b7fbdd0..4b51f12b8c4 100644 --- a/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php @@ -53,7 +53,7 @@ class IntegerRangeComparator { public static function isContainedBy( TIntRange $input_type_part, - TIntRange $container_type_part, + TIntRange $container_type_part ) : bool { $is_input_min = $input_type_part->min_bound === TIntRange::BOUND_MIN; $is_input_max = $input_type_part->max_bound === TIntRange::BOUND_MAX; From ba9f7d09acab816364601c3093b0a430256eba3e Mon Sep 17 00:00:00 2001 From: orklah Date: Fri, 30 Jul 2021 22:22:38 +0200 Subject: [PATCH 3/3] fix CS --- .../Comparator/IntegerRangeComparator.php | 42 ------------------- tests/IntRangeTest.php | 4 -- 2 files changed, 46 deletions(-) diff --git a/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php index 4b51f12b8c4..583afcad9fd 100644 --- a/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php @@ -2,49 +2,7 @@ namespace Psalm\Internal\Type\Comparator; -use Psalm\Codebase; -use Psalm\Internal\Analyzer\ClassLikeAnalyzer; -use Psalm\Type; -use Psalm\Type\Atomic\Scalar; -use Psalm\Type\Atomic\TArray; -use Psalm\Type\Atomic\TArrayKey; -use Psalm\Type\Atomic\TBool; -use Psalm\Type\Atomic\TCallableString; -use Psalm\Type\Atomic\TClassString; -use Psalm\Type\Atomic\TDependentGetClass; -use Psalm\Type\Atomic\TDependentGetDebugType; -use Psalm\Type\Atomic\TDependentGetType; -use Psalm\Type\Atomic\TDependentListKey; -use Psalm\Type\Atomic\TFalse; -use Psalm\Type\Atomic\TFloat; -use Psalm\Type\Atomic\THtmlEscapedString; -use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIntRange; -use Psalm\Type\Atomic\TLiteralClassString; -use Psalm\Type\Atomic\TLiteralFloat; -use Psalm\Type\Atomic\TLiteralInt; -use Psalm\Type\Atomic\TLiteralString; -use Psalm\Type\Atomic\TLowercaseString; -use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\Atomic\TNonEmptyNonspecificLiteralString; -use Psalm\Type\Atomic\TNonEmptyString; -use Psalm\Type\Atomic\TNonFalsyString; -use Psalm\Type\Atomic\TNonspecificLiteralInt; -use Psalm\Type\Atomic\TNonspecificLiteralString; -use Psalm\Type\Atomic\TNumeric; -use Psalm\Type\Atomic\TNumericString; -use Psalm\Type\Atomic\TPositiveInt; -use Psalm\Type\Atomic\TScalar; -use Psalm\Type\Atomic\TSingleLetter; -use Psalm\Type\Atomic\TString; -use Psalm\Type\Atomic\TTemplateParam; -use Psalm\Type\Atomic\TTemplateParamClass; -use Psalm\Type\Atomic\TTraitString; -use Psalm\Type\Atomic\TTrue; - -use function array_values; -use function get_class; -use function strtolower; /** * @internal diff --git a/tests/IntRangeTest.php b/tests/IntRangeTest.php index 10b1d5fa2ce..dd96bac1769 100644 --- a/tests/IntRangeTest.php +++ b/tests/IntRangeTest.php @@ -1,10 +1,6 @@