Skip to content

Commit aeb8a67

Browse files
authored
[Php85] Add ArrayKeyExistsNullToEmptyStringRector (#7183)
* Add ArrayKeyExistsNullToEmptyStringRector to fix deprecated null key
1 parent a7f887d commit aeb8a67

10 files changed

Lines changed: 370 additions & 167 deletions

File tree

config/set/php85.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Rector\Php85\Rector\ArrayDimFetch\ArrayFirstLastRector;
1111
use Rector\Php85\Rector\ClassMethod\NullDebugInfoReturnRector;
1212
use Rector\Php85\Rector\Const_\DeprecatedAnnotationToDeprecatedAttributeRector;
13+
use Rector\Php85\Rector\FuncCall\ArrayKeyExistsNullToEmptyStringRector;
1314
use Rector\Php85\Rector\FuncCall\RemoveFinfoBufferContextArgRector;
1415
use Rector\Php85\Rector\Switch_\ColonAfterSwitchCaseRector;
1516
use Rector\Removing\Rector\FuncCall\RemoveFuncCallArgRector;
@@ -32,6 +33,7 @@
3233
NullDebugInfoReturnRector::class,
3334
DeprecatedAnnotationToDeprecatedAttributeRector::class,
3435
ColonAfterSwitchCaseRector::class,
36+
ArrayKeyExistsNullToEmptyStringRector::class,
3537
]
3638
);
3739

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Tests\Php85\Rector\FuncCall\ArrayKeyExistsNullToEmptyStringRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class ArrayKeyExistsNullToEmptyStringRectorTest 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: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
array_key_exists(null, $array);
4+
5+
?>
6+
-----
7+
<?php
8+
9+
array_key_exists('', $array);
10+
11+
?>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
array_key_exists($k, $array);
4+
5+
?>
6+
-----
7+
<?php
8+
9+
array_key_exists((string) $k, $array);
10+
11+
?>
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+
use Rector\Config\RectorConfig;
6+
use Rector\Php85\Rector\FuncCall\ArrayKeyExistsNullToEmptyStringRector;
7+
use Rector\ValueObject\PhpVersion;
8+
9+
return static function (RectorConfig $rectorConfig): void {
10+
$rectorConfig->rule(ArrayKeyExistsNullToEmptyStringRector::class);
11+
12+
$rectorConfig->phpVersion(PhpVersion::PHP_85);
13+
};
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Php81\NodeManipulator;
6+
7+
use PhpParser\Node\Arg;
8+
use PhpParser\Node\Expr;
9+
use PhpParser\Node\Expr\Cast\String_ as CastString_;
10+
use PhpParser\Node\Expr\FuncCall;
11+
use PhpParser\Node\Expr\MethodCall;
12+
use PhpParser\Node\Expr\Ternary;
13+
use PhpParser\Node\Scalar\InterpolatedString;
14+
use PhpParser\Node\Scalar\String_;
15+
use PHPStan\Analyser\Scope;
16+
use PHPStan\Reflection\Native\ExtendedNativeParameterReflection;
17+
use PHPStan\Reflection\ParametersAcceptor;
18+
use PHPStan\Type\ErrorType;
19+
use PHPStan\Type\MixedType;
20+
use PHPStan\Type\NullType;
21+
use PHPStan\Type\Type;
22+
use PHPStan\Type\UnionType;
23+
use Rector\NodeAnalyzer\PropertyFetchAnalyzer;
24+
use Rector\NodeTypeResolver\NodeTypeResolver;
25+
use Rector\PhpParser\Node\Value\ValueResolver;
26+
27+
final readonly class NullToStrictStringConverter
28+
{
29+
public function __construct(
30+
private ValueResolver $valueResolver,
31+
private NodeTypeResolver $nodeTypeResolver,
32+
private PropertyFetchAnalyzer $propertyFetchAnalyzer,
33+
) {
34+
}
35+
36+
/**
37+
* @param Arg[] $args
38+
*/
39+
public function convertIfNull(
40+
FuncCall $funcCall,
41+
array $args,
42+
int $position,
43+
bool $isTrait,
44+
Scope $scope,
45+
ParametersAcceptor $parametersAcceptor
46+
): ?FuncCall {
47+
if (! isset($args[$position])) {
48+
return null;
49+
}
50+
51+
$argValue = $args[$position]->value;
52+
if ($this->valueResolver->isNull($argValue)) {
53+
$args[$position]->value = new String_('');
54+
$funcCall->args = $args;
55+
return $funcCall;
56+
}
57+
58+
if ($this->shouldSkipValue($argValue, $scope, $isTrait)) {
59+
return null;
60+
}
61+
62+
$parameter = $parametersAcceptor->getParameters()[$position] ?? null;
63+
if ($parameter instanceof ExtendedNativeParameterReflection && $parameter->getType() instanceof UnionType) {
64+
$parameterType = $parameter->getType();
65+
if (! $this->isValidUnionType($parameterType)) {
66+
return null;
67+
}
68+
}
69+
70+
if ($argValue instanceof Ternary && ! $this->shouldSkipValue($argValue->else, $scope, $isTrait)) {
71+
if ($this->valueResolver->isNull($argValue->else)) {
72+
$argValue->else = new String_('');
73+
} else {
74+
$argValue->else = new CastString_($argValue->else);
75+
}
76+
77+
$args[$position]->value = $argValue;
78+
$funcCall->args = $args;
79+
return $funcCall;
80+
}
81+
82+
$args[$position]->value = new CastString_($argValue);
83+
$funcCall->args = $args;
84+
return $funcCall;
85+
}
86+
87+
private function shouldSkipValue(Expr $expr, Scope $scope, bool $isTrait): bool
88+
{
89+
$type = $this->nodeTypeResolver->getType($expr);
90+
if ($type->isString()->yes()) {
91+
return true;
92+
}
93+
94+
$nativeType = $this->nodeTypeResolver->getNativeType($expr);
95+
if ($nativeType->isString()->yes()) {
96+
return true;
97+
}
98+
99+
if ($this->shouldSkipType($type)) {
100+
return true;
101+
}
102+
103+
if ($expr instanceof InterpolatedString) {
104+
return true;
105+
}
106+
107+
if ($this->isAnErrorType($expr, $nativeType, $scope)) {
108+
return true;
109+
}
110+
111+
return $this->shouldSkipTrait($expr, $type, $isTrait);
112+
}
113+
114+
private function isValidUnionType(Type $type): bool
115+
{
116+
if (! $type instanceof UnionType) {
117+
return false;
118+
}
119+
120+
foreach ($type->getTypes() as $childType) {
121+
if ($childType->isString()->yes()) {
122+
continue;
123+
}
124+
125+
if ($childType->isInteger()->yes()) {
126+
continue;
127+
}
128+
129+
if ($childType->isNull()->yes()) {
130+
continue;
131+
}
132+
133+
return false;
134+
}
135+
136+
return true;
137+
}
138+
139+
private function shouldSkipType(Type $type): bool
140+
{
141+
return ! $type instanceof MixedType
142+
&& ! $type->isNull()
143+
->yes()
144+
&& ! $this->isValidUnionType($type);
145+
}
146+
147+
private function shouldSkipTrait(Expr $expr, Type $type, bool $isTrait): bool
148+
{
149+
if (! $type instanceof MixedType) {
150+
return false;
151+
}
152+
153+
if (! $isTrait) {
154+
return false;
155+
}
156+
157+
if ($type->isExplicitMixed()) {
158+
return false;
159+
}
160+
161+
if (! $expr instanceof MethodCall) {
162+
return $this->propertyFetchAnalyzer->isLocalPropertyFetch($expr);
163+
}
164+
165+
return true;
166+
}
167+
168+
private function isAnErrorType(Expr $expr, Type $type, Scope $scope): bool
169+
{
170+
if ($type instanceof ErrorType) {
171+
return true;
172+
}
173+
174+
$parentScope = $scope->getParentScope();
175+
if ($parentScope instanceof Scope) {
176+
return $parentScope->getType($expr) instanceof ErrorType;
177+
}
178+
179+
return $type instanceof MixedType
180+
&& ! $type->isExplicitMixed()
181+
&& $type->getSubtractedType() instanceof NullType;
182+
}
183+
}

0 commit comments

Comments
 (0)