|
14 | 14 | use Doctrine\ORM\Query\ParserResult;
|
15 | 15 | use Doctrine\ORM\Query\SqlWalker;
|
16 | 16 | use PHPStan\ShouldNotHappenException;
|
| 17 | +use PHPStan\Type\Accessory\AccessoryNumericStringType; |
17 | 18 | use PHPStan\Type\BooleanType;
|
18 | 19 | use PHPStan\Type\Constant\ConstantBooleanType;
|
19 | 20 | use PHPStan\Type\Constant\ConstantFloatType;
|
@@ -575,6 +576,79 @@ public function walkFunction($function): string
|
575 | 576 | }
|
576 | 577 | }
|
577 | 578 |
|
| 579 | + private function createFloat(bool $nullable): Type |
| 580 | + { |
| 581 | + $float = new FloatType(); |
| 582 | + return $nullable ? TypeCombinator::addNull($float) : $float; |
| 583 | + } |
| 584 | + |
| 585 | + private function createFloatOrInt(bool $nullable): Type // @phpstan-ignore-line unused, but kept to ease conflict resolution with origin (#506) |
| 586 | + { |
| 587 | + $union = TypeCombinator::union( |
| 588 | + new FloatType(), |
| 589 | + new IntegerType() |
| 590 | + ); |
| 591 | + return $nullable ? TypeCombinator::addNull($union) : $union; |
| 592 | + } |
| 593 | + |
| 594 | + private function createInteger(bool $nullable): Type |
| 595 | + { |
| 596 | + $integer = new IntegerType(); |
| 597 | + return $nullable ? TypeCombinator::addNull($integer) : $integer; |
| 598 | + } |
| 599 | + |
| 600 | + private function createNonNegativeInteger(bool $nullable): Type // @phpstan-ignore-line unused, but kept to ease conflict resolution with origin (#506) |
| 601 | + { |
| 602 | + $integer = IntegerRangeType::fromInterval(0, null); |
| 603 | + return $nullable ? TypeCombinator::addNull($integer) : $integer; |
| 604 | + } |
| 605 | + |
| 606 | + private function createNumericString(bool $nullable): Type |
| 607 | + { |
| 608 | + $numericString = TypeCombinator::intersect( |
| 609 | + new StringType(), |
| 610 | + new AccessoryNumericStringType() |
| 611 | + ); |
| 612 | + |
| 613 | + return $nullable ? TypeCombinator::addNull($numericString) : $numericString; |
| 614 | + } |
| 615 | + |
| 616 | + private function createString(bool $nullable): Type |
| 617 | + { |
| 618 | + $string = new StringType(); |
| 619 | + return $nullable ? TypeCombinator::addNull($string) : $string; |
| 620 | + } |
| 621 | + |
| 622 | + /** |
| 623 | + * E.g. to ensure SUM(1) is inferred as int, not 1 |
| 624 | + */ |
| 625 | + private function generalizeConstantType(Type $type, bool $makeNullable): Type |
| 626 | + { |
| 627 | + $containsNull = $this->canBeNull($type); |
| 628 | + $typeNoNull = TypeCombinator::removeNull($type); |
| 629 | + |
| 630 | + if (!$typeNoNull->isConstantScalarValue()->yes()) { |
| 631 | + $result = $type; |
| 632 | + |
| 633 | + } elseif ($typeNoNull->isInteger()->yes()) { |
| 634 | + $result = $this->createInteger($containsNull); |
| 635 | + |
| 636 | + } elseif ($typeNoNull->isFloat()->yes()) { |
| 637 | + $result = $this->createFloat($containsNull); |
| 638 | + |
| 639 | + } elseif ($typeNoNull->isNumericString()->yes()) { |
| 640 | + $result = $this->createNumericString($containsNull); |
| 641 | + |
| 642 | + } elseif ($typeNoNull->isString()->yes()) { |
| 643 | + $result = $this->createString($containsNull); |
| 644 | + |
| 645 | + } else { |
| 646 | + $result = $type; |
| 647 | + } |
| 648 | + |
| 649 | + return $makeNullable ? TypeCombinator::addNull($result) : $result; |
| 650 | + } |
| 651 | + |
578 | 652 | /**
|
579 | 653 | * @param AST\OrderByClause $orderByClause
|
580 | 654 | */
|
@@ -642,7 +716,10 @@ public function walkCoalesceExpression($coalesceExpression): string
|
642 | 716 | $type = $this->unmarshalType($expression->dispatch($this));
|
643 | 717 | $allTypesContainNull = $allTypesContainNull && TypeCombinator::containsNull($type);
|
644 | 718 |
|
645 |
| - $expressionTypes[] = $type; |
| 719 | + // Some drivers manipulate the types, lets avoid false positives by generalizing constant types |
| 720 | + // e.g. sqlsrv: "COALESCE returns the data type of value with the highest precedence" |
| 721 | + // e.g. mysql: COALESCE(1, 'foo') === '1' (undocumented? https://gist.github.com/jrunning/4535434) |
| 722 | + $expressionTypes[] = $this->generalizeConstantType($type, false); |
646 | 723 | }
|
647 | 724 |
|
648 | 725 | $type = TypeCombinator::union(...$expressionTypes);
|
@@ -1400,6 +1477,11 @@ private function resolveDatabaseInternalType(string $typeName, ?string $enumType
|
1400 | 1477 | return $type;
|
1401 | 1478 | }
|
1402 | 1479 |
|
| 1480 | + private function canBeNull(Type $type): bool |
| 1481 | + { |
| 1482 | + return !$type->isSuperTypeOf(new NullType())->no(); |
| 1483 | + } |
| 1484 | + |
1403 | 1485 | private function toNumericOrNull(Type $type): Type
|
1404 | 1486 | {
|
1405 | 1487 | return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type {
|
|
0 commit comments