Skip to content

Commit bca109c

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

File tree

8 files changed

+290
-3
lines changed

8 files changed

+290
-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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
}
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: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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;
11+
use PhpParser\Node\Scalar\Int_;
12+
use PhpParser\Node\Scalar\String_;
13+
use PHPStan\Type\IntegerType;
14+
use PHPStan\Type\StringType;
15+
use PHPStan\Type\Type;
16+
use Rector\PHPUnit\CodeQuality\Reflection\MethodParametersAndReturnTypesResolver;
17+
use Rector\PHPUnit\NodeAnalyzer\TestsNodeAnalyzer;
18+
use Rector\Rector\AbstractRector;
19+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
20+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
21+
22+
/**
23+
* @see \Rector\PHPUnit\Tests\CodeQuality\Rector\MethodCall\ScalarArgumentToExpectedParamTypeRector\ScalarArgumentToExpectedParamTypeRectorTest
24+
*/
25+
final class ScalarArgumentToExpectedParamTypeRector extends AbstractRector
26+
{
27+
public function __construct(
28+
private readonly TestsNodeAnalyzer $testsNodeAnalyzer,
29+
private readonly MethodParametersAndReturnTypesResolver $methodParametersAndReturnTypesResolver,
30+
) {
31+
}
32+
33+
public function getRuleDefinition(): RuleDefinition
34+
{
35+
return new RuleDefinition(
36+
'Correct expected type in setter of tests, if param type is strictly defined',
37+
[
38+
new CodeSample(
39+
<<<'CODE_SAMPLE'
40+
use PHPUnit\Framework\TestCase;
41+
42+
class SomeTest extends TestCase
43+
{
44+
public function test()
45+
{
46+
$someClass = new SomeClass();
47+
$someClass->setPhone(12345);
48+
}
49+
}
50+
51+
final class SomeClass
52+
{
53+
public function setPhone(string $phone)
54+
{
55+
}
56+
}
57+
CODE_SAMPLE
58+
,
59+
<<<'CODE_SAMPLE'
60+
use PHPUnit\Framework\TestCase;
61+
62+
class SomeTest extends TestCase
63+
{
64+
public function test()
65+
{
66+
$someClass = new SomeClass();
67+
$someClass->setPhone('12345');
68+
}
69+
}
70+
71+
final class SomeClass
72+
{
73+
public function setPhone(string $phone)
74+
{
75+
}
76+
}
77+
CODE_SAMPLE
78+
),
79+
]
80+
);
81+
}
82+
83+
/**
84+
* @return array<class-string<Node>>
85+
*/
86+
public function getNodeTypes(): array
87+
{
88+
return [MethodCall::class, StaticCall::class];
89+
}
90+
91+
/**
92+
* @param MethodCall|StaticCall $node
93+
*/
94+
public function refactor(Node $node): ?Node
95+
{
96+
if (! $this->testsNodeAnalyzer->isInTestClass($node)) {
97+
return null;
98+
}
99+
100+
if ($node->isFirstClassCallable()) {
101+
return null;
102+
}
103+
104+
if ($node->getArgs() === []) {
105+
return null;
106+
}
107+
108+
$hasChanged = false;
109+
110+
if (! $this->hasStringOrNumberArguments($node)) {
111+
return null;
112+
}
113+
114+
$callParameterTypes = $this->methodParametersAndReturnTypesResolver->resolveCallParameterTypes($node);
115+
116+
foreach ($node->getArgs() as $key => $arg) {
117+
if (! $arg->value instanceof Scalar) {
118+
continue;
119+
}
120+
121+
$knownParameterType = $callParameterTypes[$key] ?? null;
122+
if (! $knownParameterType instanceof Type) {
123+
continue;
124+
}
125+
126+
if ($knownParameterType instanceof StringType && $arg->value instanceof Int_) {
127+
$arg->value = new String_((string) $arg->value->value);
128+
$hasChanged = true;
129+
}
130+
131+
if ($knownParameterType instanceof IntegerType && $arg->value instanceof String_) {
132+
$arg->value = new Int_((int) $arg->value->value);
133+
$hasChanged = true;
134+
}
135+
}
136+
137+
if (! $hasChanged) {
138+
return null;
139+
}
140+
141+
return $node;
142+
}
143+
144+
private function hasStringOrNumberArguments(StaticCall|MethodCall $call): bool
145+
{
146+
foreach ($call->getArgs() as $arg) {
147+
if ($arg->value instanceof Int_) {
148+
return true;
149+
}
150+
151+
if ($arg->value instanceof String_) {
152+
return true;
153+
}
154+
}
155+
156+
return false;
157+
}
158+
}

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+
$extendedMethodReflection = $classReflection->getMethod($methodName, $scope);
91+
92+
return $this->resolveParameterTypes($extendedMethodReflection, $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)