Skip to content

Commit 52cb427

Browse files
authored
[Php81] Skip callable param union on FunctionLikeToFirstClassCallableRector (#7589)
* [Php81] Skip callable param union on FunctionLikeToFirstClassCallableRector * fix
1 parent 2826d31 commit 52cb427

File tree

3 files changed

+125
-40
lines changed

3 files changed

+125
-40
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace Rector\Tests\CodingStyle\Rector\FunctionLike\FunctionLikeToFirstClassCallableRector\Fixture;
4+
5+
use Rector\Tests\CodingStyle\Rector\FunctionLike\FunctionLikeToFirstClassCallableRector\Source\SomeCacheInterface;
6+
7+
class SkipCallableParamUnion {
8+
public function __construct(
9+
private readonly SomeCacheInterface $cache,
10+
) {
11+
}
12+
13+
public function get(): int {
14+
return $this->cache->get('bar', fn() => time());
15+
}
16+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Rector\Tests\CodingStyle\Rector\FunctionLike\FunctionLikeToFirstClassCallableRector\Source;
4+
5+
interface SomeCacheInterface {
6+
/**
7+
* @template T
8+
*
9+
* @param string $key
10+
* @param (callable(CacheItemInterface,bool):T)|(callable(ItemInterface,bool):T)|CallbackInterface<T> $callback
11+
* @param float|null $beta
12+
* @param array &$metadata
13+
*
14+
* @return T
15+
*
16+
* @throws InvalidArgumentException When $key is not valid or when $beta is negative
17+
*/
18+
public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed;
19+
}

rules/CodingStyle/Rector/FunctionLike/FunctionLikeToFirstClassCallableRector.php

Lines changed: 90 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Rector\CodingStyle\Rector\FunctionLike;
66

7+
use PhpParser\Comment\Doc;
78
use PhpParser\Node;
89
use PhpParser\Node\Arg;
910
use PhpParser\Node\Expr;
@@ -23,10 +24,16 @@
2324
use PhpParser\Node\VariadicPlaceholder;
2425
use PhpParser\NodeVisitor;
2526
use PHPStan\Analyser\Scope;
27+
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
2628
use PHPStan\Reflection\Annotations\AnnotationMethodReflection;
29+
use PHPStan\Reflection\ExtendedFunctionVariant;
30+
use PHPStan\Reflection\Native\NativeFunctionReflection;
31+
use PHPStan\Reflection\ParametersAcceptorSelector;
2732
use PHPStan\Reflection\ResolvedFunctionVariantWithOriginal;
2833
use PHPStan\Type\CallableType;
2934
use PHPStan\Type\ObjectType;
35+
use PHPStan\Type\UnionType;
36+
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
3037
use Rector\NodeTypeResolver\PHPStan\ParametersAcceptorSelectorVariantsWrapper;
3138
use Rector\PhpParser\AstResolver;
3239
use Rector\PhpParser\Node\BetterNodeFinder;
@@ -56,7 +63,8 @@ final class FunctionLikeToFirstClassCallableRector extends AbstractRector implem
5663
public function __construct(
5764
private readonly AstResolver $astResolver,
5865
private readonly ReflectionResolver $reflectionResolver,
59-
private readonly BetterNodeFinder $betterNodeFinder
66+
private readonly BetterNodeFinder $betterNodeFinder,
67+
private readonly PhpDocInfoFactory $phpDocInfoFactory
6068
) {
6169
}
6270

@@ -127,69 +135,80 @@ public function refactor(Node $node): null|CallLike
127135
);
128136

129137
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+
}
131145
}
132146

133147
$classMethodOrFunction = $this->astResolver->resolveClassMethodOrFunctionFromCall($node);
148+
if (! $classMethodOrFunction instanceof FunctionLike) {
149+
return null;
150+
}
151+
134152
foreach ($reflection->getParameters() as $index => $parameterReflection) {
135153
if ($index !== $key) {
136154
continue;
137155
}
138156

139157
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)
141162
) {
142163
$args[$key]->value->setAttribute(self::HAS_CALLBACK_SIGNATURE_MULTI_PARAMS, true);
143164
return null;
144165
}
145166

146-
if ($classMethodOrFunction instanceof FunctionLike) {
147-
$parameterName = $parameterReflection->getName();
167+
$parameterName = $parameterReflection->getName();
148168

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+
);
161176

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+
}
168181

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+
}
172188

173-
if (! $this->isName($node->name, 'bindTo')) {
174-
return false;
175-
}
189+
if (! $node->name instanceof Identifier) {
190+
return false;
191+
}
176192

177-
if (! $node->var instanceof Variable) {
178-
return false;
179-
}
193+
if (! $this->isName($node->name, 'bindTo')) {
194+
return false;
195+
}
180196

181-
if (! $this->isObjectType($node->var, new ObjectType('Closure'))) {
182-
return false;
183-
}
197+
if (! $node->var instanceof Variable) {
198+
return false;
199+
}
184200

185-
return $this->isName($node->var, $parameterName);
201+
if (! $this->isObjectType($node->var, new ObjectType('Closure'))) {
202+
return false;
186203
}
187-
);
188204

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);
192206
}
207+
);
208+
209+
if ($isClosureBindTo) {
210+
$args[$key]->value->setAttribute(self::HAS_CALLBACK_SIGNATURE_MULTI_PARAMS, true);
211+
return null;
193212
}
194213
}
195214
}
@@ -218,6 +237,37 @@ public function provideMinPhpVersion(): int
218237
return PhpVersionFeature::FIRST_CLASS_CALLABLE_SYNTAX;
219238
}
220239

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+
221271
private function shouldSkip(
222272
ArrowFunction|Closure $node,
223273
FuncCall|MethodCall|StaticCall $callLike,

0 commit comments

Comments
 (0)