Passed
Push — main ( 6ae173...96ec27 )
by mikhail
02:54
created

AddNamedArgumentsRector   B

Complexity

Total Complexity 48

Size/Duplication

Total Lines 244
Duplicated Lines 0 %

Test Coverage

Coverage 77.12%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 104
c 3
b 0
f 0
dl 0
loc 244
ccs 91
cts 118
cp 0.7712
rs 8.5599
wmc 48

13 Methods

Rating   Name   Duplication   Size   Complexity  
A getStaticMethodArgs() 0 31 6
A getMethodArgs() 0 19 3
A refactor() 0 12 3
A getRuleDefinition() 0 6 1
A getNodeTypes() 0 3 1
A __construct() 0 3 1
B resolveCalledName() 0 19 9
A getConstructorArgs() 0 20 4
A getParameters() 0 15 5
A provideMinPhpVersion() 0 3 1
A getFuncArgs() 0 17 3
A configure() 0 17 3
B addNamesToArgs() 0 32 8

How to fix   Complexity   

Complex Class

Complex classes like AddNamedArgumentsRector often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AddNamedArgumentsRector, and based on these observations, apply Extract Interface, too.

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