|
9 | 9 | use PHPStan\Analyser\Scope; |
10 | 10 | use PHPStan\DependencyInjection\RegisteredRule; |
11 | 11 | use PHPStan\Internal\SprintfHelper; |
| 12 | +use PHPStan\Reflection\ParametersAcceptor; |
12 | 13 | use PHPStan\Reflection\ParametersAcceptorSelector; |
13 | 14 | use PHPStan\Reflection\ReflectionProvider; |
14 | 15 | use PHPStan\Rules\FunctionCallParametersCheck; |
15 | 16 | use PHPStan\Rules\Rule; |
| 17 | +use function count; |
16 | 18 |
|
17 | 19 | /** |
18 | 20 | * @implements Rule<Node\Expr\FuncCall> |
@@ -43,13 +45,15 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE |
43 | 45 | $function = $this->reflectionProvider->getFunction($node->name, $scope); |
44 | 46 | $functionName = SprintfHelper::escapeFormatString($function->getName()); |
45 | 47 |
|
| 48 | + $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs( |
| 49 | + $scope, |
| 50 | + $node->getArgs(), |
| 51 | + $function->getVariants(), |
| 52 | + $function->getNamedArgumentsVariants(), |
| 53 | + ); |
| 54 | + |
46 | 55 | return $this->check->check( |
47 | | - ParametersAcceptorSelector::selectFromArgs( |
48 | | - $scope, |
49 | | - $node->getArgs(), |
50 | | - $function->getVariants(), |
51 | | - $function->getNamedArgumentsVariants(), |
52 | | - ), |
| 56 | + self::getVariantForArgumentsCount($node->getArgs(), $function->getVariants(), $parametersAcceptor) ?? $parametersAcceptor, |
53 | 57 | $scope, |
54 | 58 | $function->isBuiltin(), |
55 | 59 | $node, |
@@ -77,4 +81,75 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE |
77 | 81 | ); |
78 | 82 | } |
79 | 83 |
|
| 84 | + /** |
| 85 | + * When no variant accepts the number of arguments, they are combined into |
| 86 | + * one whose parameters missing from some variant are optional, so mt_rand() |
| 87 | + * with the variants () and ($min, $max) would accept a single argument. |
| 88 | + * Such a count is checked against the variant closest to it instead. |
| 89 | + * |
| 90 | + * @param Node\Arg[] $args |
| 91 | + * @param list<ParametersAcceptor> $variants |
| 92 | + */ |
| 93 | + private static function getVariantForArgumentsCount(array $args, array $variants, ParametersAcceptor $selectedParametersAcceptor): ?ParametersAcceptor |
| 94 | + { |
| 95 | + if (count($variants) < 2) { |
| 96 | + return null; |
| 97 | + } |
| 98 | + |
| 99 | + foreach ($args as $arg) { |
| 100 | + if ($arg->unpack || $arg->name !== null) { |
| 101 | + return null; |
| 102 | + } |
| 103 | + } |
| 104 | + |
| 105 | + $argsCount = count($args); |
| 106 | + if (!self::acceptsArgumentsCount($selectedParametersAcceptor, $argsCount)) { |
| 107 | + return null; |
| 108 | + } |
| 109 | + |
| 110 | + $closestVariant = null; |
| 111 | + $closestDistance = null; |
| 112 | + foreach ($variants as $variant) { |
| 113 | + if (self::acceptsArgumentsCount($variant, $argsCount)) { |
| 114 | + return null; |
| 115 | + } |
| 116 | + |
| 117 | + [$minCount, $maxCount] = self::getArgumentsCountRange($variant); |
| 118 | + $isInsufficient = $argsCount < $minCount; |
| 119 | + $distance = $isInsufficient ? $minCount - $argsCount : $argsCount - (int) $maxCount; |
| 120 | + if ($closestDistance !== null && ($distance > $closestDistance || ($distance === $closestDistance && !$isInsufficient))) { |
| 121 | + continue; |
| 122 | + } |
| 123 | + |
| 124 | + $closestVariant = $variant; |
| 125 | + $closestDistance = $distance; |
| 126 | + } |
| 127 | + |
| 128 | + return $closestVariant; |
| 129 | + } |
| 130 | + |
| 131 | + private static function acceptsArgumentsCount(ParametersAcceptor $parametersAcceptor, int $argsCount): bool |
| 132 | + { |
| 133 | + [$minCount, $maxCount] = self::getArgumentsCountRange($parametersAcceptor); |
| 134 | + |
| 135 | + return $argsCount >= $minCount && ($maxCount === null || $argsCount <= $maxCount); |
| 136 | + } |
| 137 | + |
| 138 | + /** |
| 139 | + * @return array{int, int|null} |
| 140 | + */ |
| 141 | + private static function getArgumentsCountRange(ParametersAcceptor $parametersAcceptor): array |
| 142 | + { |
| 143 | + $minCount = 0; |
| 144 | + foreach ($parametersAcceptor->getParameters() as $parameter) { |
| 145 | + if ($parameter->isOptional()) { |
| 146 | + continue; |
| 147 | + } |
| 148 | + |
| 149 | + $minCount++; |
| 150 | + } |
| 151 | + |
| 152 | + return [$minCount, $parametersAcceptor->isVariadic() ? null : count($parametersAcceptor->getParameters())]; |
| 153 | + } |
| 154 | + |
80 | 155 | } |
0 commit comments