Passed
Push — main ( f53fdb...24e51f )
by mikhail
13:18
created

AddNamedArgumentsRector::addNamesToArgs()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

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