Passed
Push — main ( 24e51f...08d9d8 )
by mikhail
06:25
created

AddNamedArgumentsRector::getParameters()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 5

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 10
c 1
b 0
f 0
nc 5
nop 1
dl 0
loc 15
ccs 11
cts 11
cp 1
crap 5
rs 9.6111
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SavinMikhail\AddNamedArgumentsRector;
6
7
use InvalidArgumentException;
8
use PhpParser\Node;
9
use PhpParser\Node\Expr\FuncCall;
10
use PhpParser\Node\Expr\MethodCall;
11
use PhpParser\Node\Expr\New_;
12
use PhpParser\Node\Expr\StaticCall;
13
use PhpParser\Node\Identifier;
14
use PhpParser\Node\Name;
15
use PHPStan\Reflection\ClassMemberAccessAnswerer;
0 ignored issues
show
Bug introduced by
The type PHPStan\Reflection\ClassMemberAccessAnswerer was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
16
use PHPStan\Reflection\ClassReflection;
0 ignored issues
show
Bug introduced by
The type PHPStan\Reflection\ClassReflection was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
use PHPStan\Reflection\ExtendedParameterReflection;
0 ignored issues
show
Bug introduced by
The type PHPStan\Reflection\ExtendedParameterReflection was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
use PHPStan\Reflection\ReflectionProvider;
0 ignored issues
show
Bug introduced by
The type PHPStan\Reflection\ReflectionProvider was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
19
use PHPStan\ShouldNotHappenException;
0 ignored issues
show
Bug introduced by
The type PHPStan\ShouldNotHappenException was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
20
use Rector\Contract\Rector\ConfigurableRectorInterface;
21
use Rector\NodeTypeResolver\Node\AttributeKey;
22
use Rector\Rector\AbstractRector;
23
use Rector\ValueObject\PhpVersion;
24
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
25
use SavinMikhail\AddNamedArgumentsRector\Config\ConfigStrategy;
26
use SavinMikhail\AddNamedArgumentsRector\Config\DefaultStrategy;
0 ignored issues
show
Bug introduced by
The type SavinMikhail\AddNamedArg...\Config\DefaultStrategy was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
27
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
28
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
29
use Webmozart\Assert\Assert;
30
31
use function count;
32
33
/**
34
 * @see AddNamedArgumentsRectorTest
35
 */
36
final class AddNamedArgumentsRector extends AbstractRector implements MinPhpVersionInterface, ConfigurableRectorInterface
37
{
38
    private string $configStrategy = DefaultStrategy::class;
39
40 2
    public function __construct(
41
        private readonly ReflectionProvider $reflectionProvider,
42 2
    ) {}
43
44
    public function getRuleDefinition(): RuleDefinition
45
    {
46
        return new RuleDefinition('Convert all arguments to named arguments', codeSamples: [
47
            new CodeSample(
48
                badCode: '$user->setPassword("123456");',
49
                goodCode: '$user->changePassword(password: "123456");',
50
            ),
51
        ]);
52
    }
53
54 11
    public function getNodeTypes(): array
55
    {
56 11
        return [FuncCall::class, StaticCall::class, MethodCall::class, New_::class];
57
    }
58
59 11
    public function refactor(Node $node): ?Node
60
    {
61
        /** @var FuncCall|StaticCall|MethodCall|New_ $node */
62 11
        $parameters = $this->getParameters($node);
63 11
        $classReflection = $this->getClassReflection($node);
64
65 11
        if (!$this->configStrategy::shouldApply($node, $parameters, $classReflection)) {
66 7
            return null;
67
        }
68
69 6
        $hasChanged = $this->addNamesToArgs($node, $parameters);
70
71 6
        return $hasChanged ? $node : null;
0 ignored issues
show
introduced by
The condition $hasChanged is always true.
Loading history...
72
    }
73
74 11
    private function getClassReflection(FuncCall|StaticCall|MethodCall|New_ $node): ?ClassReflection
75
    {
76 11
        if ($node instanceof MethodCall) {
77 2
            $callerType = $this->nodeTypeResolver->getType($node->var);
78 2
            $classReflections = $callerType->getObjectClassReflections();
79
80 2
            return $classReflections[0] ?? null;
81
        }
82
83 10
        if ($node instanceof StaticCall && $node->class instanceof Name) {
84 1
            $className = $this->getName($node->class);
85
86 1
            return $this->reflectionProvider->hasClass($className)
87 1
                ? $this->reflectionProvider->getClass($className)
88 1
                : null;
89
        }
90
91 9
        if ($node instanceof New_ && $node->class instanceof Name) {
92 3
            $className = $this->getName($node->class);
93
94 3
            return $this->reflectionProvider->hasClass($className)
95 3
                ? $this->reflectionProvider->getClass($className)
96 3
                : null;
97
        }
98
99 6
        return null;
100
    }
101
102
    /**
103
     * @return ExtendedParameterReflection[]
104
     */
105 11
    private function getParameters(Node $node): array
106
    {
107 11
        $parameters = [];
108
109 11
        if ($node instanceof New_) {
110 3
            $parameters = $this->getConstructorArgs($node);
111 9
        } elseif ($node instanceof MethodCall) {
112 2
            $parameters = $this->getMethodArgs($node);
113 7
        } elseif ($node instanceof StaticCall) {
114 1
            $parameters = $this->getStaticMethodArgs($node);
115 6
        } elseif ($node instanceof FuncCall) {
116 6
            $parameters = $this->getFuncArgs($node);
117
        }
118
119 11
        return $parameters;
120
    }
121
122
    /**
123
     * @return ExtendedParameterReflection[]
124
     */
125 1
    private function getStaticMethodArgs(StaticCall $node): array
126
    {
127 1
        if (! $node->class instanceof Name) {
128
            return [];
129
        }
130
131 1
        $className = $this->getName($node->class);
132 1
        if (! $this->reflectionProvider->hasClass($className)) {
133
            return [];
134
        }
135
136 1
        $classReflection = $this->reflectionProvider->getClass($className);
137
138 1
        if ($node->name instanceof Identifier) {
139 1
            $methodName = $node->name->name;
140
        } elseif ($node->name instanceof Name) {
0 ignored issues
show
introduced by
$node->name is never a sub-type of PhpParser\Node\Name.
Loading history...
141
            $methodName = (string) $node->name;
142
        } else {
143
            return [];
144
        }
145
146 1
        if (! $classReflection->hasMethod($methodName)) {
147
            return [];
148
        }
149
150
        /** @var ClassMemberAccessAnswerer $scope */
151 1
        $scope = $node->getAttribute(AttributeKey::SCOPE);
152 1
        $reflection = $classReflection->getMethod($methodName, $scope);
153
154
        try {
155 1
            return $reflection
156 1
                ->getOnlyVariant()
157 1
                ->getParameters();
158
        } catch (ShouldNotHappenException) {
159
            // for example in interface argument being called "$className" and in child class it being called "$entityName",
160
            // we have no idea what will be resolved in a runtime, so just skip
161
            return [];
162
        }
163
    }
164
165
    /**
166
     * @return ExtendedParameterReflection[]
167
     */
168 2
    private function getMethodArgs(MethodCall $node): array
169
    {
170 2
        $callerType = $this->nodeTypeResolver->getType($node->var);
171
172 2
        $name = $node->name;
173 2
        if ($name instanceof Node\Expr) {
174
            return [];
175
        }
176 2
        $methodName = $name->name;
177 2
        if (! $callerType->hasMethod($methodName)->yes()) {
178
            return [];
179
        }
180
181
        /** @var ClassMemberAccessAnswerer $scope */
182 2
        $scope = $node->getAttribute(AttributeKey::SCOPE);
183 2
        $reflection = $callerType->getMethod($methodName, $scope);
184
185
        try {
186 2
            return $reflection
187 2
                ->getOnlyVariant()
188 2
                ->getParameters();
189
        } catch (ShouldNotHappenException) {
190
            // for example in interface argument being called "$className" and in child class it being called "$entityName",
191
            // we have no idea what will be resolved in a runtime, so just skip
192
            return [];
193
        }
194
    }
195
196 9
    private function resolveCalledName(Node $node): ?string
197
    {
198 9
        if ($node instanceof FuncCall && $node->name instanceof Name) {
199 6
            return (string) $node->name;
200
        }
201
202 3
        if ($node instanceof MethodCall && $node->name instanceof Identifier) {
203
            return (string) $node->name;
204
        }
205
206 3
        if ($node instanceof StaticCall && $node->name instanceof Identifier) {
207
            return (string) $node->name;
208
        }
209
210 3
        if ($node instanceof New_ && $node->class instanceof Name) {
211 3
            return (string) $node->class;
212
        }
213
214
        return null;
215
    }
216
217
    /**
218
     * @return ExtendedParameterReflection[]
219
     */
220 3
    private function getConstructorArgs(New_ $node): array
221
    {
222 3
        $calledName = $this->resolveCalledName($node);
223 3
        if ($calledName === null) {
224
            return [];
225
        }
226
227 3
        if (! $this->reflectionProvider->hasClass($calledName)) {
228
            return [];
229
        }
230 3
        $classReflection = $this->reflectionProvider->getClass($calledName);
231
232 3
        if (! $classReflection->hasConstructor()) {
233
            return [];
234
        }
235
236 3
        $reflection = $classReflection->getConstructor();
237
238
        try {
239 3
            return $reflection
240 3
                ->getOnlyVariant()
241 3
                ->getParameters();
242 1
        } catch (ShouldNotHappenException) {
243
            // for example in interface argument being called "$className" and in child class it being called "$entityName",
244
            // we have no idea what will be resolved in a runtime, so just skip
245 1
            return [];
246
        }
247
    }
248
249
    /**
250
     * @return ExtendedParameterReflection[]
251
     */
252 6
    private function getFuncArgs(FuncCall $node): array
253
    {
254 6
        $calledName = $this->resolveCalledName($node);
255 6
        if ($calledName === null) {
256
            return [];
257
        }
258
259 6
        $scope = $node->getAttribute(AttributeKey::SCOPE);
260
261 6
        if (! $this->reflectionProvider->hasFunction(new Name($calledName), $scope)) {
262 1
            return [];
263
        }
264 5
        $reflection = $this->reflectionProvider->getFunction(new Name($calledName), $scope);
265
266
        try {
267 5
            return $reflection
268 5
                ->getOnlyVariant()
269 5
                ->getParameters();
270
        } catch (ShouldNotHappenException) {
271
            // for example in interface argument being called "$className" and in child class it being called "$entityName",
272
            // we have no idea what will be resolved in a runtime, so just skip
273
            return [];
274
        }
275
    }
276
277
    /**
278
     * @param ExtendedParameterReflection[] $parameters
279
     */
280 6
    private function addNamesToArgs(
281
        FuncCall|StaticCall|MethodCall|New_ $node,
282
        array $parameters,
283
    ): bool {
284 6
        $argNames = [];
285 6
        foreach ($node->args as $index => $arg) {
286 6
            $argNames[$index] = new Identifier($parameters[$index]->getName());
287
        }
288
289 6
        foreach ($node->args as $index => $arg) {
290 6
            $arg->name = $argNames[$index];
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on PhpParser\Node\VariadicPlaceholder.
Loading history...
291
        }
292
293 6
        return true;
294
    }
295
296 2
    public function provideMinPhpVersion(): int
297
    {
298 2
        return PhpVersion::PHP_80;
299
    }
300
301 1
    public function configure(array $configuration): void
302
    {
303 1
        Assert::lessThan(count($configuration), 2, 'You can pass only 1 strategy');
304 1
        if ($configuration === []) {
305
            return;
306
        }
307 1
        $strategyClass = $configuration[0];
308
309 1
        if (!class_exists($strategyClass)) {
310
            throw new InvalidArgumentException("Class {$strategyClass} does not exist.");
311
        }
312
313 1
        $strategy = new $strategyClass();
314
315 1
        Assert::isInstanceOf($strategy, ConfigStrategy::class, 'Your strategy must implement ConfigStrategy interface');
316
317 1
        $this->configStrategy = $strategyClass;
318
    }
319
}
320