From cd50c21a8161896693a7ca5c182dacb545c84a52 Mon Sep 17 00:00:00 2001 From: Julien Falque Date: Wed, 24 Jul 2024 11:36:23 +0200 Subject: [PATCH 1/4] Improve `abs()` return type --- conf/config.neon | 5 + .../AbsFunctionDynamicReturnTypeExtension.php | 115 ++++++++++++++ tests/PHPStan/Analyser/nsrt/abs.php | 148 ++++++++++++++++++ 3 files changed, 268 insertions(+) create mode 100644 src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php create mode 100644 tests/PHPStan/Analyser/nsrt/abs.php diff --git a/conf/config.neon b/conf/config.neon index 60fb7e44f1..4c41181c4d 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1108,6 +1108,11 @@ services: - class: PHPStan\Type\BitwiseFlagHelper + - + class: PHPStan\Type\Php\AbsFunctionDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\ArgumentBasedFunctionReturnTypeExtension tags: diff --git a/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php b/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..7c7bf154ed --- /dev/null +++ b/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php @@ -0,0 +1,115 @@ +getName() === 'abs'; + } + + public function getTypeFromFunctionCall( + FunctionReflection $functionReflection, + FuncCall $functionCall, + Scope $scope, + ): ?Type + { + $args = $functionCall->getArgs(); + + if (!isset($args[0])) { + return null; + } + + $type = $scope->getType($args[0]->value); + + if ($type instanceof UnionType) { + $ranges = []; + + foreach ($type->getTypes() as $unionType) { + if ( + !$unionType instanceof ConstantIntegerType + && !$unionType instanceof IntegerRangeType + && !$unionType instanceof ConstantFloatType + ) { + return null; + } + + $absRange = $this->absType($unionType); + + foreach ($ranges as $index => $range) { + if (!($range instanceof IntegerRangeType)) { + continue; + } + + $unionRange = $range->tryUnion($absRange); + + if ($unionRange !== null) { + $ranges[$index] = $unionRange; + + continue 2; + } + } + + $ranges[] = $absRange; + } + + if (count($ranges) === 1) { + return $ranges[0]; + } + + return new UnionType($ranges); + } + + if ( + $type instanceof ConstantIntegerType + || $type instanceof IntegerRangeType + || $type instanceof ConstantFloatType + ) { + return $this->absType($type); + } + + return null; + } + + private function absType(ConstantIntegerType|IntegerRangeType|ConstantFloatType $type): Type + { + if ($type instanceof ConstantIntegerType) { + return new ConstantIntegerType(abs($type->getValue())); + } + + if ($type instanceof ConstantFloatType) { + return new ConstantFloatType(abs($type->getValue())); + } + + $min = $type->getMin(); + $max = $type->getMax(); + + if ($min !== null && $min >= 0) { + return IntegerRangeType::fromInterval($min, $max); + } + + if ($max === null || $max >= 0) { + $inversedMin = $min !== null ? $min * -1 : null; + + return IntegerRangeType::fromInterval(0, $inversedMin !== null && $max !== null ? max($inversedMin, $max) : null); + } + + return IntegerRangeType::fromInterval($max * -1, $min !== null ? $min * -1 : null); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/abs.php b/tests/PHPStan/Analyser/nsrt/abs.php new file mode 100644 index 0000000000..49760b73c0 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/abs.php @@ -0,0 +1,148 @@ +', abs($int)); + + /** @var positive-int $int */ + assertType('int<1, max>', abs($int)); + + /** @var negative-int $int */ + assertType('int<1, max>', abs($int)); + + /** @var non-negative-int $int */ + assertType('int<0, max>', abs($int)); + + /** @var non-positive-int $int */ + assertType('int<0, max>', abs($int)); + + /** @var int<0, max> $int */ + assertType('int<0, max>', abs($int)); + + /** @var int<0, 123> $int */ + assertType('int<0, 123>', abs($int)); + + /** @var int<-123, 0> $int */ + assertType('int<0, 123>', abs($int)); + + /** @var int<1, max> $int */ + assertType('int<1, max>', abs($int)); + + /** @var int<123, max> $int */ + assertType('int<123, max>', abs($int)); + + /** @var int<123, 456> $int */ + assertType('int<123, 456>', abs($int)); + + /** @var int $int */ + assertType('int<0, max>', abs($int)); + + /** @var int $int */ + assertType('int<1, max>', abs($int)); + + /** @var int $int */ + assertType('int<123, max>', abs($int)); + + /** @var int<-456, -123> $int */ + assertType('int<123, 456>', abs($int)); + + /** @var int<-123, 123> $int */ + assertType('int<0, 123>', abs($int)); + + /** @var int $int */ + assertType('int<0, max>', abs($int)); + } + + public function multipleIntegerRanges(int $int): void + { + /** @var non-zero-int $int */ + assertType('int<1, max>', abs($int)); + + /** @var int|int<1, max> $int */ + assertType('int<1, max>', abs($int)); + + /** @var int<-20, -10>|int<5, 25> $int */ + assertType('int<5, 25>', abs($int)); + + /** @var int<-20, -5>|int<10, 25> $int */ + assertType('int<5, 25>', abs($int)); + + /** @var int<-25, -10>|int<5, 20> $int */ + assertType('int<5, 25>', abs($int)); + + /** @var int<-20, -10>|int<20, 30> $int */ + assertType('int<10, 30>', abs($int)); + } + + public function constantInteger(int $int): void + { + /** @var 0 $int */ + assertType('0', abs($int)); + + /** @var 1 $int */ + assertType('1', abs($int)); + + /** @var -1 $int */ + assertType('1', abs($int)); + } + + public function mixedIntegerUnion(int $int): void + { + /** @var 123|int<456, max> $int */ + assertType('123|int<456, max>', abs($int)); + + /** @var int|-123 $int */ + assertType('123|int<456, max>', abs($int)); + + /** @var -123|int<124, 125> $int */ + assertType('int<123, 125>', abs($int)); + + /** @var int<124, 125>|-123 $int */ + assertType('int<123, 125>', abs($int)); + } + + public function constantFloat(float $float): void + { + /** @var 0.0 $float */ + assertType('0.0', abs($float)); + + /** @var 1.0 $float */ + assertType('1.0', abs($float)); + + /** @var -1.0 $float */ + assertType('1.0', abs($float)); + } + + public function mixedUnion(float $float): void + { + /** @var 1.0|int<2, 3> $float */ + assertType('1.0|int<2, 3>', abs($float)); + + /** @var -1.0|int<-3, -2> $float */ + assertType('1.0|int<2, 3>', abs($float)); + + /** @var 2.0|int<1, 3> $float */ + assertType('2.0|int<1, 3>', abs($float)); + + /** @var -2.0|int<-3, -1> $float */ + assertType('2.0|int<1, 3>', abs($float)); + } + + public function invalidType(mixed $nonInt): void + { + /** @var string $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var string|positive-int $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + } + +} From 6b99e0cb96f59c814ce4619c101b49b2075d0790 Mon Sep 17 00:00:00 2001 From: Julien Falque Date: Wed, 24 Jul 2024 14:46:22 +0200 Subject: [PATCH 2/4] fixup! Improve `abs()` return type --- .../AbsFunctionDynamicReturnTypeExtension.php | 44 ++++++++++--------- tests/PHPStan/Analyser/nsrt/abs.php | 36 +++++++++++---- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php b/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php index 7c7bf154ed..f5b2a32dc5 100644 --- a/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php @@ -38,49 +38,53 @@ public function getTypeFromFunctionCall( $type = $scope->getType($args[0]->value); if ($type instanceof UnionType) { - $ranges = []; + $absUnionTypes = []; foreach ($type->getTypes() as $unionType) { - if ( - !$unionType instanceof ConstantIntegerType - && !$unionType instanceof IntegerRangeType - && !$unionType instanceof ConstantFloatType - ) { + $absUnionType = $this->tryAbsType($unionType); + + if ($absUnionType === null) { return null; } - $absRange = $this->absType($unionType); - - foreach ($ranges as $index => $range) { - if (!($range instanceof IntegerRangeType)) { + foreach ($absUnionTypes as $index => $otherAbsUnionType) { + if (!($otherAbsUnionType instanceof IntegerRangeType)) { continue; } - $unionRange = $range->tryUnion($absRange); + $unionRange = $otherAbsUnionType->tryUnion($absUnionType); if ($unionRange !== null) { - $ranges[$index] = $unionRange; + $absUnionTypes[$index] = $unionRange; continue 2; } } - $ranges[] = $absRange; + $absUnionTypes[] = $absUnionType; } - if (count($ranges) === 1) { - return $ranges[0]; + if (count($absUnionTypes) === 1) { + return $absUnionTypes[0]; } - return new UnionType($ranges); + return new UnionType($absUnionTypes); } + return $this->tryAbsType($type); + } + + private function tryAbsType(Type $type): ?Type + { + $numberType = $type->toNumber(); + if ( - $type instanceof ConstantIntegerType - || $type instanceof IntegerRangeType - || $type instanceof ConstantFloatType + $numberType instanceof IntegerRangeType + || $numberType instanceof ConstantIntegerType + || $numberType instanceof ConstantFloatType + ) { - return $this->absType($type); + return $this->absType($numberType); } return null; diff --git a/tests/PHPStan/Analyser/nsrt/abs.php b/tests/PHPStan/Analyser/nsrt/abs.php index 49760b73c0..12a3acdf3c 100644 --- a/tests/PHPStan/Analyser/nsrt/abs.php +++ b/tests/PHPStan/Analyser/nsrt/abs.php @@ -121,19 +121,37 @@ public function constantFloat(float $float): void assertType('1.0', abs($float)); } - public function mixedUnion(float $float): void + public function string(string $string): void { - /** @var 1.0|int<2, 3> $float */ - assertType('1.0|int<2, 3>', abs($float)); + /** @var string $string */ + assertType('float|int<0, max>', abs($string)); - /** @var -1.0|int<-3, -2> $float */ - assertType('1.0|int<2, 3>', abs($float)); + /** @var numeric-string $string */ + assertType('float|int<0, max>', abs($string)); - /** @var 2.0|int<1, 3> $float */ - assertType('2.0|int<1, 3>', abs($float)); + /** @var '-1' $string */ + assertType('1', abs($string)); - /** @var -2.0|int<-3, -1> $float */ - assertType('2.0|int<1, 3>', abs($float)); + /** @var '-1'|'-2.0'|'3.0'|'4' $string */ + assertType('1|2.0|3.0|4', abs($string)); + } + + public function mixedUnion(mixed $value): void + { + /** @var 1.0|int<2, 3> $value */ + assertType('1.0|int<2, 3>', abs($value)); + + /** @var -1.0|int<-3, -2> $value */ + assertType('1.0|int<2, 3>', abs($value)); + + /** @var 2.0|int<1, 3> $value */ + assertType('2.0|int<1, 3>', abs($value)); + + /** @var -2.0|int<-3, -1> $value */ + assertType('2.0|int<1, 3>', abs($value)); + + /** @var -1.0|int<2, 3>|numeric-string $value */ + assertType('float|int<0, max>', abs($value)); } public function invalidType(mixed $nonInt): void From 076ead681125adc0e271759d1f5c578708a19529 Mon Sep 17 00:00:00 2001 From: Julien Falque Date: Thu, 25 Jul 2024 16:54:09 +0200 Subject: [PATCH 3/4] fixup! Improve `abs()` return type --- src/Type/Accessory/AccessoryArrayListType.php | 5 ++ .../Accessory/AccessoryLiteralStringType.php | 5 ++ .../Accessory/AccessoryNonEmptyStringType.php | 5 ++ .../Accessory/AccessoryNonFalsyStringType.php | 5 ++ .../Accessory/AccessoryNumericStringType.php | 5 ++ src/Type/Accessory/HasOffsetType.php | 5 ++ src/Type/Accessory/HasOffsetValueType.php | 5 ++ src/Type/Accessory/NonEmptyArrayType.php | 5 ++ src/Type/Accessory/OversizedArrayType.php | 5 ++ src/Type/ArrayType.php | 5 ++ src/Type/BooleanType.php | 5 ++ src/Type/CallableType.php | 5 ++ src/Type/ClosureType.php | 5 ++ src/Type/Constant/ConstantBooleanType.php | 5 ++ src/Type/Constant/ConstantFloatType.php | 6 ++ src/Type/Constant/ConstantIntegerType.php | 6 ++ src/Type/Constant/ConstantStringType.php | 5 ++ src/Type/FloatType.php | 5 ++ src/Type/IntegerRangeType.php | 15 ++++ src/Type/IntegerType.php | 5 ++ src/Type/IntersectionType.php | 7 ++ src/Type/IterableType.php | 5 ++ src/Type/MixedType.php | 5 ++ src/Type/NeverType.php | 5 ++ src/Type/NonexistentParentClassType.php | 5 ++ src/Type/NullType.php | 5 ++ src/Type/ObjectType.php | 5 ++ .../AbsFunctionDynamicReturnTypeExtension.php | 88 ++----------------- src/Type/ResourceType.php | 5 ++ src/Type/StaticType.php | 5 ++ src/Type/StrictMixedType.php | 5 ++ src/Type/StringType.php | 5 ++ src/Type/Traits/LateResolvableTypeTrait.php | 5 ++ src/Type/Traits/ObjectTypeTrait.php | 5 ++ src/Type/Type.php | 2 + src/Type/UnionType.php | 7 ++ src/Type/VoidType.php | 5 ++ tests/PHPStan/Analyser/nsrt/abs.php | 21 +++++ .../Rules/Api/ApiClassImplementsRuleTest.php | 8 +- .../data/class-implements-out-of-phpstan.php | 5 ++ 40 files changed, 229 insertions(+), 86 deletions(-) diff --git a/src/Type/Accessory/AccessoryArrayListType.php b/src/Type/Accessory/AccessoryArrayListType.php index 6f76467aac..874b177716 100644 --- a/src/Type/Accessory/AccessoryArrayListType.php +++ b/src/Type/Accessory/AccessoryArrayListType.php @@ -408,6 +408,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return TypeCombinator::union( diff --git a/src/Type/Accessory/AccessoryLiteralStringType.php b/src/Type/Accessory/AccessoryLiteralStringType.php index baf139b2dd..eac0986542 100644 --- a/src/Type/Accessory/AccessoryLiteralStringType.php +++ b/src/Type/Accessory/AccessoryLiteralStringType.php @@ -171,6 +171,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new IntegerType(); diff --git a/src/Type/Accessory/AccessoryNonEmptyStringType.php b/src/Type/Accessory/AccessoryNonEmptyStringType.php index 6ac04d9922..8e98af9d78 100644 --- a/src/Type/Accessory/AccessoryNonEmptyStringType.php +++ b/src/Type/Accessory/AccessoryNonEmptyStringType.php @@ -177,6 +177,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new IntegerType(); diff --git a/src/Type/Accessory/AccessoryNonFalsyStringType.php b/src/Type/Accessory/AccessoryNonFalsyStringType.php index c261682b6a..5aff19f63b 100644 --- a/src/Type/Accessory/AccessoryNonFalsyStringType.php +++ b/src/Type/Accessory/AccessoryNonFalsyStringType.php @@ -173,6 +173,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new IntegerType(); diff --git a/src/Type/Accessory/AccessoryNumericStringType.php b/src/Type/Accessory/AccessoryNumericStringType.php index e2ebaa514a..292d00f7a0 100644 --- a/src/Type/Accessory/AccessoryNumericStringType.php +++ b/src/Type/Accessory/AccessoryNumericStringType.php @@ -179,6 +179,11 @@ public function toNumber(): Type ]); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toInteger(): Type { return new IntegerType(); diff --git a/src/Type/Accessory/HasOffsetType.php b/src/Type/Accessory/HasOffsetType.php index 91a844d20c..cdb258d613 100644 --- a/src/Type/Accessory/HasOffsetType.php +++ b/src/Type/Accessory/HasOffsetType.php @@ -328,6 +328,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/HasOffsetValueType.php b/src/Type/Accessory/HasOffsetValueType.php index 8f07251305..a87f7879ab 100644 --- a/src/Type/Accessory/HasOffsetValueType.php +++ b/src/Type/Accessory/HasOffsetValueType.php @@ -374,6 +374,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ErrorType(); diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index 353dfe36e0..8ef7ecbb88 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -389,6 +389,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ConstantIntegerType(1); diff --git a/src/Type/Accessory/OversizedArrayType.php b/src/Type/Accessory/OversizedArrayType.php index dc309f1a0e..c6c9c5b6d7 100644 --- a/src/Type/Accessory/OversizedArrayType.php +++ b/src/Type/Accessory/OversizedArrayType.php @@ -385,6 +385,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ConstantIntegerType(1); diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index 54dd4b36dd..c58bcf951a 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -588,6 +588,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index aa8665f522..72be662c95 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -56,6 +56,11 @@ public function toNumber(): Type return $this->toInteger(); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toString(): Type { return TypeCombinator::union( diff --git a/src/Type/CallableType.php b/src/Type/CallableType.php index 1f8b5981a9..e51cf45631 100644 --- a/src/Type/CallableType.php +++ b/src/Type/CallableType.php @@ -299,6 +299,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); diff --git a/src/Type/ClosureType.php b/src/Type/ClosureType.php index e7c48df9bb..93b9989102 100644 --- a/src/Type/ClosureType.php +++ b/src/Type/ClosureType.php @@ -422,6 +422,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ErrorType(); diff --git a/src/Type/Constant/ConstantBooleanType.php b/src/Type/Constant/ConstantBooleanType.php index 88b7822f6a..db5328d0d2 100644 --- a/src/Type/Constant/ConstantBooleanType.php +++ b/src/Type/Constant/ConstantBooleanType.php @@ -82,6 +82,11 @@ public function toNumber(): Type return new ConstantIntegerType((int) $this->value); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toString(): Type { return new ConstantStringType((string) $this->value); diff --git a/src/Type/Constant/ConstantFloatType.php b/src/Type/Constant/ConstantFloatType.php index 3468ab5de8..3aeb1e2e18 100644 --- a/src/Type/Constant/ConstantFloatType.php +++ b/src/Type/Constant/ConstantFloatType.php @@ -12,6 +12,7 @@ use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function abs; use function ini_get; use function ini_set; use function is_finite; @@ -76,6 +77,11 @@ public function toInteger(): Type return new ConstantIntegerType((int) $this->value); } + public function toAbsoluteNumber(): Type + { + return new self(abs($this->value)); + } + public function toArrayKey(): Type { return new ConstantIntegerType((int) $this->value); diff --git a/src/Type/Constant/ConstantIntegerType.php b/src/Type/Constant/ConstantIntegerType.php index ae3d0511a8..9226acacc2 100644 --- a/src/Type/Constant/ConstantIntegerType.php +++ b/src/Type/Constant/ConstantIntegerType.php @@ -15,6 +15,7 @@ use PHPStan\Type\Traits\ConstantScalarTypeTrait; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; +use function abs; use function sprintf; /** @api */ @@ -76,6 +77,11 @@ public function toFloat(): Type return new ConstantFloatType($this->value); } + public function toAbsoluteNumber(): Type + { + return new self(abs($this->value)); + } + public function toString(): Type { return new ConstantStringType((string) $this->value); diff --git a/src/Type/Constant/ConstantStringType.php b/src/Type/Constant/ConstantStringType.php index 95ea40c601..fbdbaf2f97 100644 --- a/src/Type/Constant/ConstantStringType.php +++ b/src/Type/Constant/ConstantStringType.php @@ -292,6 +292,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toInteger(): Type { return new ConstantIntegerType((int) $this->value); diff --git a/src/Type/FloatType.php b/src/Type/FloatType.php index eaf79b1794..2111d4d912 100644 --- a/src/Type/FloatType.php +++ b/src/Type/FloatType.php @@ -110,6 +110,11 @@ public function toNumber(): Type return $this; } + public function toAbsoluteNumber(): Type + { + return $this; + } + public function toFloat(): Type { return $this; diff --git a/src/Type/IntegerRangeType.php b/src/Type/IntegerRangeType.php index b1134d9c20..b7962f71ae 100644 --- a/src/Type/IntegerRangeType.php +++ b/src/Type/IntegerRangeType.php @@ -447,6 +447,21 @@ public function toBoolean(): BooleanType return new ConstantBooleanType(false); } + public function toAbsoluteNumber(): Type + { + if ($this->min !== null && $this->min >= 0) { + return $this; + } + + if ($this->max === null || $this->max >= 0) { + $inversedMin = $this->min !== null ? $this->min * -1 : null; + + return self::fromInterval(0, $inversedMin !== null && $this->max !== null ? max($inversedMin, $this->max) : null); + } + + return self::fromInterval($this->max * -1, $this->min !== null ? $this->min * -1 : null); + } + public function toString(): Type { $isZero = (new ConstantIntegerType(0))->isSuperTypeOf($this); diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index 0c57a8087a..f91a646c9d 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -62,6 +62,11 @@ public function toNumber(): Type return $this; } + public function toAbsoluteNumber(): Type + { + return IntegerRangeType::createAllGreaterThanOrEqualTo(0); + } + public function toFloat(): Type { return new FloatType(); diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index f04fe05ef7..9b4fbf22ba 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -914,6 +914,13 @@ public function toNumber(): Type return $type; } + public function toAbsoluteNumber(): Type + { + $type = $this->intersectTypes(static fn (Type $type): Type => $type->toAbsoluteNumber()); + + return $type; + } + public function toString(): Type { $type = $this->intersectTypes(static fn (Type $type): Type => $type->toString()); diff --git a/src/Type/IterableType.php b/src/Type/IterableType.php index 25c1ff3039..f36cab2e7f 100644 --- a/src/Type/IterableType.php +++ b/src/Type/IterableType.php @@ -217,6 +217,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); diff --git a/src/Type/MixedType.php b/src/Type/MixedType.php index 78b80cd897..402d192a7d 100644 --- a/src/Type/MixedType.php +++ b/src/Type/MixedType.php @@ -468,6 +468,11 @@ public function toNumber(): Type ]); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toInteger(): Type { return new IntegerType(); diff --git a/src/Type/NeverType.php b/src/Type/NeverType.php index b6e378a330..b619768b6d 100644 --- a/src/Type/NeverType.php +++ b/src/Type/NeverType.php @@ -351,6 +351,11 @@ public function toNumber(): Type return $this; } + public function toAbsoluteNumber(): Type + { + return $this; + } + public function toString(): Type { return $this; diff --git a/src/Type/NonexistentParentClassType.php b/src/Type/NonexistentParentClassType.php index b13f5d5105..bbf9dceec8 100644 --- a/src/Type/NonexistentParentClassType.php +++ b/src/Type/NonexistentParentClassType.php @@ -127,6 +127,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); diff --git a/src/Type/NullType.php b/src/Type/NullType.php index 469134dad3..e72cf25be2 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -144,6 +144,11 @@ public function toNumber(): Type return new ConstantIntegerType(0); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toString(): Type { return new ConstantStringType(''); diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 6a7b22e829..45a04563c9 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -563,6 +563,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return $this->toNumber()->toAbsoluteNumber(); + } + public function toInteger(): Type { if ($this->isInstanceOf('SimpleXMLElement')->yes()) { diff --git a/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php b/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php index f5b2a32dc5..d47d7f39eb 100644 --- a/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php +++ b/src/Type/Php/AbsFunctionDynamicReturnTypeExtension.php @@ -5,15 +5,9 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\FunctionReflection; -use PHPStan\Type\Constant\ConstantFloatType; -use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicFunctionReturnTypeExtension; -use PHPStan\Type\IntegerRangeType; +use PHPStan\Type\ErrorType; use PHPStan\Type\Type; -use PHPStan\Type\UnionType; -use function abs; -use function count; -use function max; class AbsFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension { @@ -35,85 +29,15 @@ public function getTypeFromFunctionCall( return null; } - $type = $scope->getType($args[0]->value); + $inputType = $scope->getType($args[0]->value); - if ($type instanceof UnionType) { - $absUnionTypes = []; + $outputType = $inputType->toAbsoluteNumber(); - foreach ($type->getTypes() as $unionType) { - $absUnionType = $this->tryAbsType($unionType); - - if ($absUnionType === null) { - return null; - } - - foreach ($absUnionTypes as $index => $otherAbsUnionType) { - if (!($otherAbsUnionType instanceof IntegerRangeType)) { - continue; - } - - $unionRange = $otherAbsUnionType->tryUnion($absUnionType); - - if ($unionRange !== null) { - $absUnionTypes[$index] = $unionRange; - - continue 2; - } - } - - $absUnionTypes[] = $absUnionType; - } - - if (count($absUnionTypes) === 1) { - return $absUnionTypes[0]; - } - - return new UnionType($absUnionTypes); - } - - return $this->tryAbsType($type); - } - - private function tryAbsType(Type $type): ?Type - { - $numberType = $type->toNumber(); - - if ( - $numberType instanceof IntegerRangeType - || $numberType instanceof ConstantIntegerType - || $numberType instanceof ConstantFloatType - - ) { - return $this->absType($numberType); - } - - return null; - } - - private function absType(ConstantIntegerType|IntegerRangeType|ConstantFloatType $type): Type - { - if ($type instanceof ConstantIntegerType) { - return new ConstantIntegerType(abs($type->getValue())); - } - - if ($type instanceof ConstantFloatType) { - return new ConstantFloatType(abs($type->getValue())); - } - - $min = $type->getMin(); - $max = $type->getMax(); - - if ($min !== null && $min >= 0) { - return IntegerRangeType::fromInterval($min, $max); - } - - if ($max === null || $max >= 0) { - $inversedMin = $min !== null ? $min * -1 : null; - - return IntegerRangeType::fromInterval(0, $inversedMin !== null && $max !== null ? max($inversedMin, $max) : null); + if ($outputType instanceof ErrorType) { + return null; } - return IntegerRangeType::fromInterval($max * -1, $min !== null ? $min * -1 : null); + return $outputType; } } diff --git a/src/Type/ResourceType.php b/src/Type/ResourceType.php index afdcc5d432..4327f30bde 100644 --- a/src/Type/ResourceType.php +++ b/src/Type/ResourceType.php @@ -55,6 +55,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new StringType(); diff --git a/src/Type/StaticType.php b/src/Type/StaticType.php index df29b9eda2..766cede665 100644 --- a/src/Type/StaticType.php +++ b/src/Type/StaticType.php @@ -600,6 +600,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return $this->getStaticObjectType()->toString(); diff --git a/src/Type/StrictMixedType.php b/src/Type/StrictMixedType.php index 8bdd24093d..cc114d5c34 100644 --- a/src/Type/StrictMixedType.php +++ b/src/Type/StrictMixedType.php @@ -365,6 +365,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new ErrorType(); diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 2090d52c85..3fe7dfca68 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -143,6 +143,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toInteger(): Type { return new IntegerType(); diff --git a/src/Type/Traits/LateResolvableTypeTrait.php b/src/Type/Traits/LateResolvableTypeTrait.php index df4d0c286b..a749a6fae0 100644 --- a/src/Type/Traits/LateResolvableTypeTrait.php +++ b/src/Type/Traits/LateResolvableTypeTrait.php @@ -327,6 +327,11 @@ public function toNumber(): Type return $this->resolve()->toNumber(); } + public function toAbsoluteNumber(): Type + { + return $this->resolve()->toAbsoluteNumber(); + } + public function toInteger(): Type { return $this->resolve()->toInteger(); diff --git a/src/Type/Traits/ObjectTypeTrait.php b/src/Type/Traits/ObjectTypeTrait.php index 8105cd0555..0c37e439c7 100644 --- a/src/Type/Traits/ObjectTypeTrait.php +++ b/src/Type/Traits/ObjectTypeTrait.php @@ -233,6 +233,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); diff --git a/src/Type/Type.php b/src/Type/Type.php index 8b56ea16af..3ac0a41c24 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -313,6 +313,8 @@ public function inferTemplateTypes(Type $receivedType): TemplateTypeMap; */ public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array; + public function toAbsoluteNumber(): Type; + /** * Traverses inner types * diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index b754092e81..241fb0aa69 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -878,6 +878,13 @@ public function toNumber(): Type return $type; } + public function toAbsoluteNumber(): Type + { + $type = $this->unionTypes(static fn (Type $type): Type => $type->toAbsoluteNumber()); + + return $type; + } + public function toString(): Type { $type = $this->unionTypes(static fn (Type $type): Type => $type->toString()); diff --git a/src/Type/VoidType.php b/src/Type/VoidType.php index d0c9273d40..8cbd1076ba 100644 --- a/src/Type/VoidType.php +++ b/src/Type/VoidType.php @@ -97,6 +97,11 @@ public function toNumber(): Type return new ErrorType(); } + public function toAbsoluteNumber(): Type + { + return new ErrorType(); + } + public function toString(): Type { return new ErrorType(); diff --git a/tests/PHPStan/Analyser/nsrt/abs.php b/tests/PHPStan/Analyser/nsrt/abs.php index 12a3acdf3c..69c7a08a0c 100644 --- a/tests/PHPStan/Analyser/nsrt/abs.php +++ b/tests/PHPStan/Analyser/nsrt/abs.php @@ -154,6 +154,12 @@ public function mixedUnion(mixed $value): void assertType('float|int<0, max>', abs($value)); } + public function intersection(mixed $value): void + { + /** @var int&int<-10, 10> $value */ + assertType('int<0, 10>', abs($value)); + } + public function invalidType(mixed $nonInt): void { /** @var string $nonInt */ @@ -161,6 +167,21 @@ public function invalidType(mixed $nonInt): void /** @var string|positive-int $nonInt */ assertType('float|int<0, max>', abs($nonInt)); + + /** @var 'foo' $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var array $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var non-empty-list $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var object $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); + + /** @var \DateTime $nonInt */ + assertType('float|int<0, max>', abs($nonInt)); } } diff --git a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php index 9b95f57e25..a6a68a9b5e 100644 --- a/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php +++ b/tests/PHPStan/Rules/Api/ApiClassImplementsRuleTest.php @@ -42,22 +42,22 @@ public function testRuleOutOfPhpStan(): void ], [ 'Implementing PHPStan\Reflection\ReflectionProvider is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 333, + 338, $tip, ], [ 'Implementing PHPStan\Analyser\Scope is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 338, + 343, $tip, ], [ 'Implementing PHPStan\Reflection\FunctionReflection is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 343, + 348, $tip, ], [ 'Implementing PHPStan\Reflection\ExtendedMethodReflection is not covered by backward compatibility promise. The interface might change in a minor PHPStan version.', - 347, + 352, $tip, ], ]); diff --git a/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php b/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php index b7b9d69943..c7b42d7e6e 100644 --- a/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php +++ b/tests/PHPStan/Rules/Api/data/class-implements-out-of-phpstan.php @@ -217,6 +217,11 @@ public function toNumber(): \PHPStan\Type\Type // TODO: Implement toNumber() method. } + public function toAbsoluteNumber(): \PHPStan\Type\Type + { + // TODO: Implement toAbsoluteNumber() method. + } + public function toInteger(): \PHPStan\Type\Type { // TODO: Implement toInteger() method. From 3abfc169ce42a723ccdc15257ed6412e82e951f8 Mon Sep 17 00:00:00 2001 From: Julien Falque Date: Fri, 26 Jul 2024 10:27:36 +0200 Subject: [PATCH 4/4] fixup! Improve `abs()` return type --- tests/PHPStan/Analyser/nsrt/abs.php | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/abs.php b/tests/PHPStan/Analyser/nsrt/abs.php index 69c7a08a0c..eb644eb4bd 100644 --- a/tests/PHPStan/Analyser/nsrt/abs.php +++ b/tests/PHPStan/Analyser/nsrt/abs.php @@ -92,6 +92,10 @@ public function constantInteger(int $int): void /** @var -1 $int */ assertType('1', abs($int)); + + assertType('123', abs(123)); + + assertType('123', abs(-123)); } public function mixedIntegerUnion(int $int): void @@ -119,6 +123,10 @@ public function constantFloat(float $float): void /** @var -1.0 $float */ assertType('1.0', abs($float)); + + assertType('123.4', abs(123.4)); + + assertType('123.4', abs(-123.4)); } public function string(string $string): void @@ -134,6 +142,19 @@ public function string(string $string): void /** @var '-1'|'-2.0'|'3.0'|'4' $string */ assertType('1|2.0|3.0|4', abs($string)); + + /** @var literal-string $string */ + assertType('float|int<0, max>', abs($string)); + + assertType('123', abs('123')); + + assertType('123', abs('-123')); + + assertType('123.0', abs('123.0')); + + assertType('123.0', abs('-123.0')); + + assertType('float|int<0, max>', abs('foo')); } public function mixedUnion(mixed $value): void @@ -182,6 +203,13 @@ public function invalidType(mixed $nonInt): void /** @var \DateTime $nonInt */ assertType('float|int<0, max>', abs($nonInt)); + + /** @var null $nonInt */ + assertType('0', abs($nonInt)); + + assertType('float|int<0, max>', abs('foo')); + + assertType('0', abs(null)); } }