diff --git a/conf/config.neon b/conf/config.neon index b7e91bd3d9..54f4ff3186 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -1053,6 +1053,10 @@ services: arguments: reportMagicProperties: %reportMagicProperties% checkDynamicProperties: %checkDynamicProperties% + - + class: PHPStan\Type\Php\BcMathNumberOperatorTypeSpecifyingExtension + tags: + - phpstan.broker.operatorTypeSpecifyingExtension - class: PHPStan\Rules\Properties\UninitializedPropertyRule diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 6f55bdda15..5215a30606 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -400,4 +400,9 @@ public function substrReturnFalseInsteadOfEmptyString(): bool return $this->versionId < 80000; } + public function supportsBcMathNumberOperatorOverloading(): bool + { + return $this->versionId >= 80400; + } + } diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index 9fef24a587..fb935ac630 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -872,6 +872,11 @@ public function getModType(Expr $left, Expr $right, callable $getTypeCallback): return $this->getNeverType($leftType, $rightType); } + $extensionSpecified = $this->callOperatorTypeSpecifyingExtensions(new BinaryOp\Mod($left, $right), $leftType, $rightType); + if ($extensionSpecified !== null) { + return $extensionSpecified; + } + if ($leftType->toNumber() instanceof ErrorType || $rightType->toNumber() instanceof ErrorType) { return new ErrorType(); } @@ -1234,16 +1239,16 @@ public function getPowType(Expr $left, Expr $right, callable $getTypeCallback): $leftType = $getTypeCallback($left); $rightType = $getTypeCallback($right); - $exponentiatedTyped = $leftType->exponentiate($rightType); - if (!$exponentiatedTyped instanceof ErrorType) { - return $exponentiatedTyped; - } - $extensionSpecified = $this->callOperatorTypeSpecifyingExtensions(new BinaryOp\Pow($left, $right), $leftType, $rightType); if ($extensionSpecified !== null) { return $extensionSpecified; } + $exponentiatedTyped = $leftType->exponentiate($rightType); + if (!$exponentiatedTyped instanceof ErrorType) { + return $exponentiatedTyped; + } + return new ErrorType(); } diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 0ad78520f2..5a05575059 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -31,6 +31,8 @@ use PHPStan\Reflection\Type\UnresolvedPropertyPrototypeReflection; use PHPStan\ShouldNotHappenException; use PHPStan\TrinaryLogic; +use PHPStan\Type\Accessory\AccessoryNonEmptyStringType; +use PHPStan\Type\Accessory\AccessoryNumericStringType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; use PHPStan\Type\Constant\ConstantStringType; @@ -593,6 +595,14 @@ public function toFloat(): Type public function toString(): Type { + if ($this->isInstanceOf('BcMath\Number')->yes()) { + return new IntersectionType([ + new StringType(), + new AccessoryNumericStringType(), + new AccessoryNonEmptyStringType(), + ]); + } + $classReflection = $this->getClassReflection(); if ($classReflection === null) { return new ErrorType(); @@ -678,7 +688,10 @@ public function toCoercedArgumentType(bool $strictTypes): Type public function toBoolean(): BooleanType { - if ($this->isInstanceOf('SimpleXMLElement')->yes()) { + if ( + $this->isInstanceOf('SimpleXMLElement')->yes() + || $this->isInstanceOf('BcMath\Number')->yes() + ) { return new BooleanType(); } diff --git a/src/Type/Php/BcMathNumberOperatorTypeSpecifyingExtension.php b/src/Type/Php/BcMathNumberOperatorTypeSpecifyingExtension.php new file mode 100644 index 0000000000..3e0d793052 --- /dev/null +++ b/src/Type/Php/BcMathNumberOperatorTypeSpecifyingExtension.php @@ -0,0 +1,53 @@ +phpVersion->supportsBcMathNumberOperatorOverloading() || $leftSide instanceof NeverType || $rightSide instanceof NeverType) { + return false; + } + + $bcMathNumberType = new ObjectType('BcMath\Number'); + + return in_array($operatorSigil, ['-', '+', '*', '/', '**', '%'], true) + && ( + $bcMathNumberType->isSuperTypeOf($leftSide)->yes() + || $bcMathNumberType->isSuperTypeOf($rightSide)->yes() + ); + } + + public function specifyType(string $operatorSigil, Type $leftSide, Type $rightSide): Type + { + $bcMathNumberType = new ObjectType('BcMath\Number'); + $otherSide = $bcMathNumberType->isSuperTypeOf($leftSide)->yes() + ? $rightSide + : $leftSide; + + if ( + $otherSide->isInteger()->yes() + || $otherSide->isNumericString()->yes() + || $bcMathNumberType->isSuperTypeOf($otherSide)->yes() + ) { + return $bcMathNumberType; + } + + return new ErrorType(); + } + +} diff --git a/tests/PHPStan/Analyser/nsrt/bcmath-number.php b/tests/PHPStan/Analyser/nsrt/bcmath-number.php new file mode 100644 index 0000000000..2bdd9611b4 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bcmath-number.php @@ -0,0 +1,408 @@ += 8.4 + +declare(strict_types = 1); + +namespace BcMathNumber; + +use BcMath\Number; +use function PHPStan\Testing\assertType; + +class Foo +{ + public function bcVsBc(Number $a, Number $b): void + { + assertType('BcMath\Number', $a + $b); + assertType('BcMath\Number', $a - $b); + assertType('BcMath\Number', $a * $b); + assertType('BcMath\Number', $a / $b); + assertType('BcMath\Number', $a % $b); + assertType('non-falsy-string', $a . $b); + assertType('BcMath\Number', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsInt(Number $a, int $b): void + { + assertType('BcMath\Number', $a + $b); + assertType('BcMath\Number', $a - $b); + assertType('BcMath\Number', $a * $b); + assertType('BcMath\Number', $a / $b); + assertType('BcMath\Number', $a % $b); + assertType('non-falsy-string', $a . $b); + assertType('BcMath\Number', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsFloat(Number $a, float $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('non-falsy-string', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + /** @param numeric-string $b */ + public function bcVsNumericString(Number $a, string $b): void + { + assertType('BcMath\Number', $a + $b); + assertType('BcMath\Number', $a - $b); + assertType('BcMath\Number', $a * $b); + assertType('BcMath\Number', $a / $b); + assertType('BcMath\Number', $a % $b); + assertType('non-falsy-string', $a . $b); + assertType('BcMath\Number', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsNonNumericString(Number $a, string $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('non-empty-string', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsBool(Number $a, bool $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('non-empty-string&numeric-string', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsNull(Number $a): void + { + $b = null; + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('0', $a * $b); // BUG: This throws type error, but getMulType assumes that since null (mostly) behaves like zero, it will be zero. + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('non-empty-string&numeric-string', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('false', $a && $b); + assertType('bool', $a || $b); + assertType('false', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsArray(Number $a, array $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('*ERROR*', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsObject(Number $a, object $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('*ERROR*', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('true', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('true', $a or $b); + } + + /** @param resource $b */ + public function bcVsResource(Number $a, $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('non-empty-string', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('true', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('true', $a or $b); + } + + public function bcVsCallable(Number $a, callable $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('*ERROR*', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('true', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('true', $a or $b); + } + + public function bcVsIterable(Number $a, iterable $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('*ERROR*', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + + public function bcVsStringable(Number $a, \Stringable $b): void + { + assertType('*ERROR*', $a + $b); + assertType('*ERROR*', $a - $b); + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*ERROR*', $a % $b); + assertType('non-empty-string', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*ERROR*', $a << $b); + assertType('*ERROR*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('int<-1, 1>', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*ERROR*', $a & $b); + assertType('*ERROR*', $a ^ $b); + assertType('*ERROR*', $a | $b); + assertType('bool', $a && $b); + assertType('true', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('true', $a or $b); + } + + public function bcVsNever(Number $a): void + { + for ($b = 1; $b < count([]); $b++) { + assertType('*NEVER*', $a + $b); + assertType('*ERROR*', $a - $b); // Inconsistency: getPlusType handles never types right at the beginning, getMinusType doesn't. + assertType('*ERROR*', $a * $b); + assertType('*ERROR*', $a / $b); + assertType('*NEVER*', $a % $b); + assertType('non-empty-string&numeric-string', $a . $b); + assertType('*ERROR*', $a ** $b); + assertType('*NEVER*', $a << $b); + assertType('*NEVER*', $a >> $b); + assertType('bool', $a < $b); + assertType('bool', $a <= $b); + assertType('bool', $a > $b); + assertType('bool', $a >= $b); + assertType('*NEVER*', $a <=> $b); + assertType('bool', $a == $b); + assertType('bool', $a != $b); + assertType('*NEVER*', $a & $b); + assertType('*NEVER*', $a ^ $b); + assertType('*NEVER*', $a | $b); + assertType('bool', $a && $b); + assertType('bool', $a || $b); + assertType('bool', $a and $b); + assertType('bool', $a xor $b); + assertType('bool', $a or $b); + } + } +}