-
-
Notifications
You must be signed in to change notification settings - Fork 336
/
ParentClassMethodTypeOverrideGuard.php
160 lines (130 loc) · 5.83 KB
/
ParentClassMethodTypeOverrideGuard.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
<?php
declare(strict_types=1);
namespace Rector\VendorLocker;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariantWithPhpDocs;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use Rector\Core\PhpParser\AstResolver;
use Rector\Core\Reflection\ReflectionResolver;
use Rector\Core\ValueObject\MethodName;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\NodeTypeResolver\TypeComparator\TypeComparator;
use Rector\StaticTypeMapper\StaticTypeMapper;
use Rector\TypeDeclaration\TypeInferer\ParamTypeInferer;
use Symplify\SmartFileSystem\Normalizer\PathNormalizer;
final class ParentClassMethodTypeOverrideGuard
{
public function __construct(
private readonly NodeNameResolver $nodeNameResolver,
private readonly PathNormalizer $pathNormalizer,
private readonly AstResolver $astResolver,
private readonly ParamTypeInferer $paramTypeInferer,
private readonly ReflectionResolver $reflectionResolver,
private readonly TypeComparator $typeComparator,
private readonly StaticTypeMapper $staticTypeMapper
) {
}
public function isReturnTypeChangeAllowed(ClassMethod $classMethod): bool
{
// __construct cannot declare a return type
// so the return type change is not allowed
if ($this->nodeNameResolver->isName($classMethod, MethodName::CONSTRUCT)) {
return false;
}
// make sure return type is not protected by parent contract
$parentClassMethodReflection = $this->getParentClassMethod($classMethod);
// nothing to check
if (! $parentClassMethodReflection instanceof MethodReflection) {
return true;
}
$parametersAcceptor = ParametersAcceptorSelector::selectSingle($parentClassMethodReflection->getVariants());
if ($parametersAcceptor instanceof FunctionVariantWithPhpDocs && ! $parametersAcceptor->getNativeReturnType() instanceof MixedType) {
return false;
}
$classReflection = $parentClassMethodReflection->getDeclaringClass();
$fileName = $classReflection->getFileName();
// probably internal
if ($fileName === null) {
return false;
}
/*
* Below verify that both current file name and parent file name is not in the /vendor/, if yes, then allowed.
* This can happen when rector run into /vendor/ directory while child and parent both are there.
*
* @see https://3v4l.org/Rc0RF#v8.0.13
*
* - both in /vendor/ -> allowed
* - one of them in /vendor/ -> not allowed
* - both not in /vendor/ -> allowed
*/
/** @var ClassReflection $currentClassReflection */
$currentClassReflection = $this->reflectionResolver->resolveClassReflection($classMethod);
/** @var string $currentFileName */
$currentFileName = $currentClassReflection->getFileName();
// child (current)
$normalizedCurrentFileName = $this->pathNormalizer->normalizePath($currentFileName);
$isCurrentInVendor = str_contains($normalizedCurrentFileName, '/vendor/');
// parent
$normalizedFileName = $this->pathNormalizer->normalizePath($fileName);
$isParentInVendor = str_contains($normalizedFileName, '/vendor/');
return ($isCurrentInVendor && $isParentInVendor) || (! $isCurrentInVendor && ! $isParentInVendor);
}
public function hasParentClassMethod(ClassMethod $classMethod): bool
{
return $this->getParentClassMethod($classMethod) instanceof MethodReflection;
}
public function hasParentClassMethodDifferentType(ClassMethod $classMethod, int $position, Type $currentType): bool
{
if ($classMethod->isPrivate()) {
return false;
}
$methodReflection = $this->getParentClassMethod($classMethod);
if (! $methodReflection instanceof MethodReflection) {
return false;
}
$classMethod = $this->astResolver->resolveClassMethodFromMethodReflection($methodReflection);
if (! $classMethod instanceof ClassMethod) {
return false;
}
if ($classMethod->isPrivate()) {
return false;
}
if (! isset($classMethod->params[$position])) {
return false;
}
$inferedType = $this->paramTypeInferer->inferParam($classMethod->params[$position]);
return $inferedType::class !== $currentType::class;
}
public function getParentClassMethod(ClassMethod $classMethod): ?MethodReflection
{
$classReflection = $this->reflectionResolver->resolveClassReflection($classMethod);
if (! $classReflection instanceof ClassReflection) {
return null;
}
/** @var string $methodName */
$methodName = $this->nodeNameResolver->getName($classMethod);
$parentClassReflections = array_merge($classReflection->getParents(), $classReflection->getInterfaces());
foreach ($parentClassReflections as $parentClassReflection) {
if (! $parentClassReflection->hasNativeMethod($methodName)) {
continue;
}
return $parentClassReflection->getNativeMethod($methodName);
}
return null;
}
public function shouldSkipReturnTypeChange(ClassMethod $classMethod, Type $parentType): bool
{
if ($classMethod->returnType === null) {
return false;
}
$currentReturnType = $this->staticTypeMapper->mapPhpParserNodePHPStanType($classMethod->returnType);
if ($this->typeComparator->isSubtype($currentReturnType, $parentType)) {
return true;
}
return $this->typeComparator->areTypesEqual($currentReturnType, $parentType);
}
}