Skip to content

Commit 16e08d3

Browse files
committed
[code-quality] add ScalarArgumentToExpectedParamTypeRector
1 parent 1947a57 commit 16e08d3

File tree

8 files changed

+294
-3
lines changed

8 files changed

+294
-3
lines changed

config/sets/phpunit-code-quality.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
use Rector\PHPUnit\CodeQuality\Rector\MethodCall\NarrowIdenticalWithConsecutiveRector;
3838
use Rector\PHPUnit\CodeQuality\Rector\MethodCall\NarrowSingleWillReturnCallbackRector;
3939
use Rector\PHPUnit\CodeQuality\Rector\MethodCall\RemoveExpectAnyFromMockRector;
40+
use Rector\PHPUnit\CodeQuality\Rector\MethodCall\ScalarArgumentToExpectedParamTypeRector;
4041
use Rector\PHPUnit\CodeQuality\Rector\MethodCall\SingleWithConsecutiveToWithRector;
4142
use Rector\PHPUnit\CodeQuality\Rector\MethodCall\StringCastAssertStringContainsStringRector;
4243
use Rector\PHPUnit\CodeQuality\Rector\MethodCall\UseSpecificWillMethodRector;
@@ -74,6 +75,7 @@
7475
StringCastAssertStringContainsStringRector::class,
7576
AddParamTypeFromDependsRector::class,
7677
AddReturnTypeToDependedRector::class,
78+
ScalarArgumentToExpectedParamTypeRector::class,
7779

7880
NarrowUnusedSetUpDefinedPropertyRector::class,
7981

rector.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
naming: true,
4040
earlyReturn: true,
4141
rectorPreset: true,
42-
phpunitCodeQuality: true
42+
phpunitCodeQuality: true,
43+
symfonyCodeQuality: true,
4344
)
4445
->withConfiguredRule(StringClassNameToClassConstantRector::class, [
4546
// keep unprefixed to protected from downgrade
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\MethodCall\ScalarArgumentToExpectedParamTypeRector\Fixture;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Rector\PHPUnit\Tests\CodeQuality\Rector\MethodCall\ScalarArgumentToExpectedParamTypeRector\Source\SomeClassWithSetter;
7+
8+
final class SimpleMethodCall extends TestCase
9+
{
10+
public function test()
11+
{
12+
$someClassWithSetter = new SomeClassWithSetter();
13+
$someClassWithSetter->setPhoneNumber(123456);
14+
}
15+
}
16+
17+
?>
18+
-----
19+
<?php
20+
21+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\MethodCall\ScalarArgumentToExpectedParamTypeRector\Fixture;
22+
23+
use PHPUnit\Framework\TestCase;
24+
use Rector\PHPUnit\Tests\CodeQuality\Rector\MethodCall\ScalarArgumentToExpectedParamTypeRector\Source\SomeClassWithSetter;
25+
26+
final class SimpleMethodCall extends TestCase
27+
{
28+
public function test()
29+
{
30+
$someClassWithSetter = new SomeClassWithSetter();
31+
$someClassWithSetter->setPhoneNumber('123456');
32+
}
33+
}
34+
35+
?>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\MethodCall\ScalarArgumentToExpectedParamTypeRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class ScalarArgumentToExpectedParamTypeRectorTest extends AbstractRectorTestCase
12+
{
13+
#[DataProvider('provideData')]
14+
public function test(string $filePath): void
15+
{
16+
$this->doTestFile($filePath);
17+
}
18+
19+
public static function provideData(): Iterator
20+
{
21+
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
22+
}
23+
24+
public function provideConfigFilePath(): string
25+
{
26+
return __DIR__ . '/config/configured_rule.php';
27+
}
28+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\Tests\CodeQuality\Rector\MethodCall\ScalarArgumentToExpectedParamTypeRector\Source;
6+
7+
final class SomeClassWithSetter
8+
{
9+
public function setPhoneNumber(string $phoneNumber)
10+
{
11+
12+
}
13+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use Rector\PHPUnit\CodeQuality\Rector\MethodCall\ScalarArgumentToExpectedParamTypeRector;
7+
8+
return static function (RectorConfig $rectorConfig): void {
9+
$rectorConfig->rule(ScalarArgumentToExpectedParamTypeRector::class);
10+
};
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\PHPUnit\CodeQuality\Rector\MethodCall;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Expr\MethodCall;
9+
use PhpParser\Node\Expr\StaticCall;
10+
use PhpParser\Node\Scalar\Int_;
11+
use PhpParser\Node\Scalar\String_;
12+
use PHPStan\Type\IntegerType;
13+
use PHPStan\Type\StringType;
14+
use Rector\PHPUnit\CodeQuality\Reflection\MethodParametersAndReturnTypesResolver;
15+
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
16+
use Rector\Rector\AbstractRector;
17+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
18+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
19+
20+
/**
21+
* @see \Rector\PHPUnit\Tests\CodeQuality\Rector\MethodCall\ScalarArgumentToExpectedParamTypeRector\ScalarArgumentToExpectedParamTypeRectorTest
22+
*/
23+
final class ScalarArgumentToExpectedParamTypeRector extends AbstractRector
24+
{
25+
public function __construct(
26+
private readonly TestsNodeAnalyzer $testsNodeAnalyzer,
27+
private readonly MethodParametersAndReturnTypesResolver $methodParametersAndReturnTypesResolver,
28+
) {
29+
}
30+
31+
public function getRuleDefinition(): RuleDefinition
32+
{
33+
return new RuleDefinition(
34+
'Correct expected type in setter of tests, if param type is strictly defined',
35+
[
36+
new CodeSample(
37+
<<<'CODE_SAMPLE'
38+
use PHPUnit\Framework\TestCase;
39+
40+
class SomeTest extends TestCase
41+
{
42+
public function test()
43+
{
44+
$someClass = new SomeClass();
45+
$someClass->setPhone(12345);
46+
}
47+
}
48+
49+
final class SomeClass
50+
{
51+
public function setPhone(string $phone)
52+
{
53+
}
54+
}
55+
CODE_SAMPLE
56+
,
57+
<<<'CODE_SAMPLE'
58+
use PHPUnit\Framework\TestCase;
59+
60+
class SomeTest extends TestCase
61+
{
62+
public function test()
63+
{
64+
$someClass = new SomeClass();
65+
$someClass->setPhone('12345');
66+
}
67+
}
68+
69+
final class SomeClass
70+
{
71+
public function setPhone(string $phone)
72+
{
73+
}
74+
}
75+
CODE_SAMPLE
76+
),
77+
]
78+
);
79+
}
80+
81+
/**
82+
* @return array<class-string<Node>>
83+
*/
84+
public function getNodeTypes(): array
85+
{
86+
return [MethodCall::class, StaticCall::class];
87+
}
88+
89+
/**
90+
* @param MethodCall|StaticCall $node
91+
*/
92+
public function refactor(Node $node): ?Node
93+
{
94+
if (! $this->testsNodeAnalyzer->isInTestClass($node)) {
95+
return null;
96+
}
97+
98+
if ($node->isFirstClassCallable()) {
99+
return null;
100+
}
101+
102+
if ($node->getArgs() === []) {
103+
return null;
104+
}
105+
106+
$hasChanged = false;
107+
108+
if (! $this->hasStringOrNumberArguments($node)) {
109+
return null;
110+
}
111+
112+
$callParameterTypes = $this->methodParametersAndReturnTypesResolver->resolveCallParameterTypes($node);
113+
114+
foreach ($node->getArgs() as $key => $arg) {
115+
if (! $arg->value instanceof Node\Scalar) {
116+
continue;
117+
}
118+
119+
$knownParameterType = $callParameterTypes[$key] ?? null;
120+
if (! $knownParameterType instanceof \PHPStan\Type\Type) {
121+
continue;
122+
}
123+
124+
if ($knownParameterType instanceof StringType) {
125+
if ($arg->value instanceof Int_) {
126+
$arg->value = new String_((string) $arg->value->value);
127+
128+
$hasChanged = true;
129+
}
130+
}
131+
132+
if ($knownParameterType instanceof IntegerType) {
133+
if ($arg->value instanceof String_) {
134+
$arg->value = new Int_((int) $arg->value->value);
135+
$hasChanged = true;
136+
}
137+
}
138+
}
139+
140+
if (! $hasChanged) {
141+
return null;
142+
}
143+
144+
return $node;
145+
}
146+
147+
private function hasStringOrNumberArguments(StaticCall|MethodCall $call): bool
148+
{
149+
foreach ($call->getArgs() as $arg) {
150+
if ($arg->value instanceof Int_) {
151+
return true;
152+
}
153+
154+
if ($arg->value instanceof String_) {
155+
return true;
156+
}
157+
}
158+
159+
return false;
160+
}
161+
}

rules/CodeQuality/Reflection/MethodParametersAndReturnTypesResolver.php

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
namespace Rector\PHPUnit\CodeQuality\Reflection;
66

7+
use PhpParser\Node\Expr\MethodCall;
8+
use PhpParser\Node\Expr\StaticCall;
9+
use PhpParser\Node\Identifier;
710
use PHPStan\Reflection\ClassReflection;
811
use PHPStan\Reflection\ExtendedMethodReflection;
912
use PHPStan\Reflection\ParametersAcceptorSelector;
@@ -13,10 +16,17 @@
1316
use PHPStan\Type\StaticType;
1417
use PHPStan\Type\Type;
1518
use Rector\Enum\ClassName;
19+
use Rector\NodeTypeResolver\NodeTypeResolver;
20+
use Rector\PHPStan\ScopeFetcher;
1621
use Rector\PHPUnit\CodeQuality\ValueObject\ParamTypesAndReturnType;
1722

18-
final class MethodParametersAndReturnTypesResolver
23+
final readonly class MethodParametersAndReturnTypesResolver
1924
{
25+
public function __construct(
26+
private NodeTypeResolver $nodeTypeResolver
27+
) {
28+
}
29+
2030
public function resolveFromReflection(
2131
IntersectionType $intersectionType,
2232
string $methodName,
@@ -51,10 +61,41 @@ public function resolveFromReflection(
5161
return null;
5262
}
5363

64+
/**
65+
* @return null|Type[]
66+
*/
67+
public function resolveCallParameterTypes(MethodCall|StaticCall $call): ?array
68+
{
69+
if (! $call->name instanceof Identifier) {
70+
return null;
71+
}
72+
73+
$methodName = $call->name->toString();
74+
75+
$callerType = $this->nodeTypeResolver->getType($call instanceof MethodCall ? $call->var : $call->class);
76+
if (! $callerType instanceof ObjectType) {
77+
return null;
78+
}
79+
80+
$classReflection = $callerType->getClassReflection();
81+
if (! $classReflection instanceof ClassReflection) {
82+
return null;
83+
}
84+
85+
if (! $classReflection->hasNativeMethod($methodName)) {
86+
return null;
87+
}
88+
89+
$scope = ScopeFetcher::fetch($call);
90+
$methodReflection = $classReflection->getMethod($methodName, $scope);
91+
92+
return $this->resolveParameterTypes($methodReflection, $classReflection);
93+
}
94+
5495
/**
5596
* @return Type[]
5697
*/
57-
private function resolveParameterTypes(
98+
public function resolveParameterTypes(
5899
ExtendedMethodReflection $extendedMethodReflection,
59100
ClassReflection $currentClassReflection
60101
): array {

0 commit comments

Comments
 (0)