|
4 | 4 |
|
5 | 5 | namespace Rector\CodingStyle\Rector\FunctionLike; |
6 | 6 |
|
| 7 | +use PhpParser\Comment\Doc; |
7 | 8 | use PhpParser\Node; |
8 | 9 | use PhpParser\Node\Arg; |
9 | 10 | use PhpParser\Node\Expr; |
|
23 | 24 | use PhpParser\Node\VariadicPlaceholder; |
24 | 25 | use PhpParser\NodeVisitor; |
25 | 26 | use PHPStan\Analyser\Scope; |
| 27 | +use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode; |
26 | 28 | use PHPStan\Reflection\Annotations\AnnotationMethodReflection; |
| 29 | +use PHPStan\Reflection\ExtendedFunctionVariant; |
| 30 | +use PHPStan\Reflection\Native\NativeFunctionReflection; |
| 31 | +use PHPStan\Reflection\ParametersAcceptorSelector; |
27 | 32 | use PHPStan\Reflection\ResolvedFunctionVariantWithOriginal; |
28 | 33 | use PHPStan\Type\CallableType; |
29 | 34 | use PHPStan\Type\ObjectType; |
| 35 | +use PHPStan\Type\UnionType; |
| 36 | +use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory; |
30 | 37 | use Rector\NodeTypeResolver\PHPStan\ParametersAcceptorSelectorVariantsWrapper; |
31 | 38 | use Rector\PhpParser\AstResolver; |
32 | 39 | use Rector\PhpParser\Node\BetterNodeFinder; |
@@ -56,7 +63,8 @@ final class FunctionLikeToFirstClassCallableRector extends AbstractRector implem |
56 | 63 | public function __construct( |
57 | 64 | private readonly AstResolver $astResolver, |
58 | 65 | private readonly ReflectionResolver $reflectionResolver, |
59 | | - private readonly BetterNodeFinder $betterNodeFinder |
| 66 | + private readonly BetterNodeFinder $betterNodeFinder, |
| 67 | + private readonly PhpDocInfoFactory $phpDocInfoFactory |
60 | 68 | ) { |
61 | 69 | } |
62 | 70 |
|
@@ -127,69 +135,80 @@ public function refactor(Node $node): null|CallLike |
127 | 135 | ); |
128 | 136 |
|
129 | 137 | if ($reflection instanceof ResolvedFunctionVariantWithOriginal) { |
130 | | - return null; |
| 138 | + $reflection = ParametersAcceptorSelector::combineAcceptors( |
| 139 | + $methodReflection->getVariants() |
| 140 | + ); |
| 141 | + |
| 142 | + if (! $reflection instanceof ExtendedFunctionVariant) { |
| 143 | + return null; |
| 144 | + } |
131 | 145 | } |
132 | 146 |
|
133 | 147 | $classMethodOrFunction = $this->astResolver->resolveClassMethodOrFunctionFromCall($node); |
| 148 | + if (! $classMethodOrFunction instanceof FunctionLike) { |
| 149 | + return null; |
| 150 | + } |
| 151 | + |
134 | 152 | foreach ($reflection->getParameters() as $index => $parameterReflection) { |
135 | 153 | if ($index !== $key) { |
136 | 154 | continue; |
137 | 155 | } |
138 | 156 |
|
139 | 157 | if ($parameterReflection->getType() instanceof CallableType |
140 | | - && count($parameterReflection->getType()->getParameters()) > 1 |
| 158 | + && |
| 159 | + count($parameterReflection->getType()->getParameters()) !== 1 |
| 160 | + && ! $methodReflection instanceof NativeFunctionReflection |
| 161 | + && $this->hasDocCommentForCallable($classMethodOrFunction, $index) |
141 | 162 | ) { |
142 | 163 | $args[$key]->value->setAttribute(self::HAS_CALLBACK_SIGNATURE_MULTI_PARAMS, true); |
143 | 164 | return null; |
144 | 165 | } |
145 | 166 |
|
146 | | - if ($classMethodOrFunction instanceof FunctionLike) { |
147 | | - $parameterName = $parameterReflection->getName(); |
| 167 | + $parameterName = $parameterReflection->getName(); |
148 | 168 |
|
149 | | - $isInvokable = (bool) $this->betterNodeFinder->findFirstInFunctionLikeScoped( |
150 | | - $classMethodOrFunction, |
151 | | - fn (Node $node): bool => $node instanceof FuncCall |
152 | | - && $node->name instanceof Variable |
153 | | - && $this->isName($node->name, $parameterName) |
154 | | - && count($node->args) > 1 |
155 | | - ); |
156 | | - |
157 | | - if ($isInvokable) { |
158 | | - $args[$key]->value->setAttribute(self::HAS_CALLBACK_SIGNATURE_MULTI_PARAMS, true); |
159 | | - return null; |
160 | | - } |
| 169 | + $isInvokable = (bool) $this->betterNodeFinder->findFirstInFunctionLikeScoped( |
| 170 | + $classMethodOrFunction, |
| 171 | + fn (Node $node): bool => $node instanceof FuncCall |
| 172 | + && $node->name instanceof Variable |
| 173 | + && $this->isName($node->name, $parameterName) |
| 174 | + && count($node->args) > 1 |
| 175 | + ); |
161 | 176 |
|
162 | | - $isClosureBindTo = (bool) $this->betterNodeFinder->findFirstInFunctionLikeScoped( |
163 | | - $classMethodOrFunction, |
164 | | - function (Node $node) use ($parameterName): bool { |
165 | | - if (! $node instanceof MethodCall) { |
166 | | - return false; |
167 | | - } |
| 177 | + if ($isInvokable) { |
| 178 | + $args[$key]->value->setAttribute(self::HAS_CALLBACK_SIGNATURE_MULTI_PARAMS, true); |
| 179 | + return null; |
| 180 | + } |
168 | 181 |
|
169 | | - if (! $node->name instanceof Identifier) { |
170 | | - return false; |
171 | | - } |
| 182 | + $isClosureBindTo = (bool) $this->betterNodeFinder->findFirstInFunctionLikeScoped( |
| 183 | + $classMethodOrFunction, |
| 184 | + function (Node $node) use ($parameterName): bool { |
| 185 | + if (! $node instanceof MethodCall) { |
| 186 | + return false; |
| 187 | + } |
172 | 188 |
|
173 | | - if (! $this->isName($node->name, 'bindTo')) { |
174 | | - return false; |
175 | | - } |
| 189 | + if (! $node->name instanceof Identifier) { |
| 190 | + return false; |
| 191 | + } |
176 | 192 |
|
177 | | - if (! $node->var instanceof Variable) { |
178 | | - return false; |
179 | | - } |
| 193 | + if (! $this->isName($node->name, 'bindTo')) { |
| 194 | + return false; |
| 195 | + } |
180 | 196 |
|
181 | | - if (! $this->isObjectType($node->var, new ObjectType('Closure'))) { |
182 | | - return false; |
183 | | - } |
| 197 | + if (! $node->var instanceof Variable) { |
| 198 | + return false; |
| 199 | + } |
184 | 200 |
|
185 | | - return $this->isName($node->var, $parameterName); |
| 201 | + if (! $this->isObjectType($node->var, new ObjectType('Closure'))) { |
| 202 | + return false; |
186 | 203 | } |
187 | | - ); |
188 | 204 |
|
189 | | - if ($isClosureBindTo) { |
190 | | - $args[$key]->value->setAttribute(self::HAS_CALLBACK_SIGNATURE_MULTI_PARAMS, true); |
191 | | - return null; |
| 205 | + return $this->isName($node->var, $parameterName); |
192 | 206 | } |
| 207 | + ); |
| 208 | + |
| 209 | + if ($isClosureBindTo) { |
| 210 | + $args[$key]->value->setAttribute(self::HAS_CALLBACK_SIGNATURE_MULTI_PARAMS, true); |
| 211 | + return null; |
193 | 212 | } |
194 | 213 | } |
195 | 214 | } |
@@ -218,6 +237,37 @@ public function provideMinPhpVersion(): int |
218 | 237 | return PhpVersionFeature::FIRST_CLASS_CALLABLE_SYNTAX; |
219 | 238 | } |
220 | 239 |
|
| 240 | + private function hasDocCommentForCallable(FunctionLike $functionLike, int $index): bool |
| 241 | + { |
| 242 | + $docComment = $functionLike->getDocComment(); |
| 243 | + if (! $docComment instanceof Doc) { |
| 244 | + return false; |
| 245 | + } |
| 246 | + |
| 247 | + $phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($functionLike); |
| 248 | + $params = $functionLike->getParams(); |
| 249 | + |
| 250 | + $paramName = null; |
| 251 | + foreach ($params as $key => $param) { |
| 252 | + if ($key === $index) { |
| 253 | + $paramName = (string) $this->getName($param); |
| 254 | + break; |
| 255 | + } |
| 256 | + } |
| 257 | + |
| 258 | + if ($paramName === null) { |
| 259 | + return false; |
| 260 | + } |
| 261 | + |
| 262 | + $paramTagValueNode = $phpDocInfo->getParamTagValueByName($paramName); |
| 263 | + if ($paramTagValueNode instanceof ParamTagValueNode) { |
| 264 | + $type = $phpDocInfo->getParamType($paramName); |
| 265 | + return ($type instanceof CallableType && count($type->getParameters()) !== 1) || $type instanceof UnionType; |
| 266 | + } |
| 267 | + |
| 268 | + return false; |
| 269 | + } |
| 270 | + |
221 | 271 | private function shouldSkip( |
222 | 272 | ArrowFunction|Closure $node, |
223 | 273 | FuncCall|MethodCall|StaticCall $callLike, |
|
0 commit comments