|
46 | 46 | use PHPStan\Type\ClosureType; |
47 | 47 | use PHPStan\Type\Constant\ConstantArrayType; |
48 | 48 | use PHPStan\Type\Constant\ConstantArrayTypeBuilder; |
| 49 | +use PHPStan\Type\Constant\ConstantBooleanType; |
49 | 50 | use PHPStan\Type\ErrorType; |
50 | 51 | use PHPStan\Type\GeneralizePrecision; |
51 | 52 | use PHPStan\Type\IntegerRangeType; |
|
70 | 71 | use function in_array; |
71 | 72 | use function sprintf; |
72 | 73 | use function str_starts_with; |
| 74 | +use function substr; |
73 | 75 |
|
74 | 76 | /** |
75 | 77 | * @implements ExprHandler<FuncCall> |
@@ -817,7 +819,107 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type |
817 | 819 | } |
818 | 820 | } |
819 | 821 |
|
820 | | - return VoidToNullTypeTransformer::transform($parametersAcceptor->getReturnType(), $expr); |
| 822 | + $returnType = $parametersAcceptor->getReturnType(); |
| 823 | + $returnType = $this->narrowReturnTypeByAssertions($returnType, $functionReflection, $normalizedNode ?? $expr, $scope, $parametersAcceptor); |
| 824 | + |
| 825 | + return VoidToNullTypeTransformer::transform($returnType, $expr); |
| 826 | + } |
| 827 | + |
| 828 | + private function narrowReturnTypeByAssertions( |
| 829 | + Type $returnType, |
| 830 | + FunctionReflection $functionReflection, |
| 831 | + FuncCall $call, |
| 832 | + MutatingScope $scope, |
| 833 | + ParametersAcceptor $parametersAcceptor, |
| 834 | + ): Type |
| 835 | + { |
| 836 | + if (!$returnType->isBoolean()->yes() || $returnType->isTrue()->yes() || $returnType->isFalse()->yes()) { |
| 837 | + return $returnType; |
| 838 | + } |
| 839 | + |
| 840 | + $assertions = $functionReflection->getAsserts(); |
| 841 | + $assertsIfFalse = $assertions->getAssertsIfFalse(); |
| 842 | + $assertsIfTrue = $assertions->getAssertsIfTrue(); |
| 843 | + |
| 844 | + if (count($assertsIfFalse) === 0 && count($assertsIfTrue) === 0) { |
| 845 | + return $returnType; |
| 846 | + } |
| 847 | + |
| 848 | + $argTypes = $this->buildArgTypesForAssertions($call, $scope, $parametersAcceptor); |
| 849 | + |
| 850 | + foreach ($assertsIfFalse as $assert) { |
| 851 | + $param = $assert->getParameter(); |
| 852 | + if ($param->describe() !== $param->getParameterName()) { |
| 853 | + continue; |
| 854 | + } |
| 855 | + |
| 856 | + $paramName = substr($param->getParameterName(), 1); |
| 857 | + if (!isset($argTypes[$paramName])) { |
| 858 | + continue; |
| 859 | + } |
| 860 | + |
| 861 | + $actualType = $argTypes[$paramName]; |
| 862 | + $assertedType = $assert->getType(); |
| 863 | + |
| 864 | + if ($assert->isNegated()) { |
| 865 | + if ($assertedType->isSuperTypeOf($actualType)->yes()) { |
| 866 | + return new ConstantBooleanType(true); |
| 867 | + } |
| 868 | + } else { |
| 869 | + if ($assertedType->isSuperTypeOf($actualType)->no()) { |
| 870 | + return new ConstantBooleanType(true); |
| 871 | + } |
| 872 | + } |
| 873 | + } |
| 874 | + |
| 875 | + foreach ($assertsIfTrue as $assert) { |
| 876 | + $param = $assert->getParameter(); |
| 877 | + if ($param->describe() !== $param->getParameterName()) { |
| 878 | + continue; |
| 879 | + } |
| 880 | + |
| 881 | + $paramName = substr($param->getParameterName(), 1); |
| 882 | + if (!isset($argTypes[$paramName])) { |
| 883 | + continue; |
| 884 | + } |
| 885 | + |
| 886 | + $actualType = $argTypes[$paramName]; |
| 887 | + $assertedType = $assert->getType(); |
| 888 | + |
| 889 | + if ($assert->isNegated()) { |
| 890 | + if ($assertedType->isSuperTypeOf($actualType)->yes()) { |
| 891 | + return new ConstantBooleanType(false); |
| 892 | + } |
| 893 | + } else { |
| 894 | + if ($assertedType->isSuperTypeOf($actualType)->no()) { |
| 895 | + return new ConstantBooleanType(false); |
| 896 | + } |
| 897 | + } |
| 898 | + } |
| 899 | + |
| 900 | + return $returnType; |
| 901 | + } |
| 902 | + |
| 903 | + /** |
| 904 | + * @return array<string, Type> |
| 905 | + */ |
| 906 | + private function buildArgTypesForAssertions(FuncCall $call, MutatingScope $scope, ParametersAcceptor $parametersAcceptor): array |
| 907 | + { |
| 908 | + $argTypes = []; |
| 909 | + $parameters = $parametersAcceptor->getParameters(); |
| 910 | + foreach ($call->getArgs() as $i => $arg) { |
| 911 | + $name = null; |
| 912 | + if ($arg->name !== null) { |
| 913 | + $name = $arg->name->toString(); |
| 914 | + } elseif (isset($parameters[$i])) { |
| 915 | + $name = $parameters[$i]->getName(); |
| 916 | + } |
| 917 | + if ($name !== null) { |
| 918 | + $argTypes[$name] = $scope->getType($arg->value); |
| 919 | + } |
| 920 | + } |
| 921 | + |
| 922 | + return $argTypes; |
821 | 923 | } |
822 | 924 |
|
823 | 925 | private function getDynamicFunctionReturnType(MutatingScope $scope, FuncCall $normalizedNode, FunctionReflection $functionReflection): ?Type |
|
0 commit comments