1: | <?php declare(strict_types = 1); |
2: | |
3: | namespace PHPStan\Rules; |
4: | |
5: | use PhpParser\Node\Expr; |
6: | use PHPStan\Analyser\Scope; |
7: | use PHPStan\Reflection\ReflectionProvider; |
8: | use PHPStan\Type\BenevolentUnionType; |
9: | use PHPStan\Type\CompoundType; |
10: | use PHPStan\Type\ErrorType; |
11: | use PHPStan\Type\Generic\TemplateMixedType; |
12: | use PHPStan\Type\MixedType; |
13: | use PHPStan\Type\NeverType; |
14: | use PHPStan\Type\NullType; |
15: | use PHPStan\Type\ObjectWithoutClassType; |
16: | use PHPStan\Type\StaticType; |
17: | use PHPStan\Type\StrictMixedType; |
18: | use PHPStan\Type\Type; |
19: | use PHPStan\Type\TypeCombinator; |
20: | use PHPStan\Type\TypeTraverser; |
21: | use PHPStan\Type\TypeUtils; |
22: | use PHPStan\Type\UnionType; |
23: | use PHPStan\Type\VerbosityLevel; |
24: | use function count; |
25: | use function sprintf; |
26: | use function strpos; |
27: | |
28: | class RuleLevelHelper |
29: | { |
30: | |
31: | public function __construct( |
32: | private ReflectionProvider $reflectionProvider, |
33: | private bool $checkNullables, |
34: | private bool $checkThisOnly, |
35: | private bool $checkUnionTypes, |
36: | private bool $checkExplicitMixed, |
37: | private bool $checkImplicitMixed, |
38: | ) |
39: | { |
40: | } |
41: | |
42: | |
43: | public function isThis(Expr $expression): bool |
44: | { |
45: | return $expression instanceof Expr\Variable && $expression->name === 'this'; |
46: | } |
47: | |
48: | |
49: | public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTypes): bool |
50: | { |
51: | if ( |
52: | $this->checkExplicitMixed |
53: | ) { |
54: | $traverse = static function (Type $type, callable $traverse): Type { |
55: | if ($type instanceof TemplateMixedType) { |
56: | return $type->toStrictMixedType(); |
57: | } |
58: | if ( |
59: | $type instanceof MixedType |
60: | && $type->isExplicitMixed() |
61: | ) { |
62: | return new StrictMixedType(); |
63: | } |
64: | |
65: | return $traverse($type); |
66: | }; |
67: | $acceptingType = TypeTraverser::map($acceptingType, $traverse); |
68: | $acceptedType = TypeTraverser::map($acceptedType, $traverse); |
69: | } |
70: | |
71: | if ( |
72: | $this->checkImplicitMixed |
73: | ) { |
74: | $traverse = static function (Type $type, callable $traverse): Type { |
75: | if ($type instanceof TemplateMixedType) { |
76: | return $type->toStrictMixedType(); |
77: | } |
78: | if ( |
79: | $type instanceof MixedType |
80: | && !$type->isExplicitMixed() |
81: | ) { |
82: | return new StrictMixedType(); |
83: | } |
84: | |
85: | return $traverse($type); |
86: | }; |
87: | $acceptingType = TypeTraverser::map($acceptingType, $traverse); |
88: | $acceptedType = TypeTraverser::map($acceptedType, $traverse); |
89: | } |
90: | |
91: | if ( |
92: | !$this->checkNullables |
93: | && !$acceptingType instanceof NullType |
94: | && !$acceptedType instanceof NullType |
95: | && !$acceptedType instanceof BenevolentUnionType |
96: | ) { |
97: | $acceptedType = TypeCombinator::removeNull($acceptedType); |
98: | } |
99: | |
100: | $accepts = $acceptingType->accepts($acceptedType, $strictTypes); |
101: | if (!$accepts->yes() && $acceptingType instanceof UnionType && !$acceptedType instanceof CompoundType) { |
102: | foreach ($acceptingType->getTypes() as $innerType) { |
103: | if (self::accepts($innerType, $acceptedType, $strictTypes)) { |
104: | return true; |
105: | } |
106: | } |
107: | |
108: | return false; |
109: | } |
110: | |
111: | if ( |
112: | $acceptedType->isArray()->yes() |
113: | && $acceptingType->isArray()->yes() |
114: | && !$acceptingType->isIterableAtLeastOnce()->yes() |
115: | && count(TypeUtils::getOldConstantArrays($acceptedType)) === 0 |
116: | && count(TypeUtils::getOldConstantArrays($acceptingType)) === 0 |
117: | ) { |
118: | return self::accepts( |
119: | $acceptingType->getIterableKeyType(), |
120: | $acceptedType->getIterableKeyType(), |
121: | $strictTypes, |
122: | ) && self::accepts( |
123: | $acceptingType->getIterableValueType(), |
124: | $acceptedType->getIterableValueType(), |
125: | $strictTypes, |
126: | ); |
127: | } |
128: | |
129: | return $this->checkUnionTypes ? $accepts->yes() : !$accepts->no(); |
130: | } |
131: | |
132: | |
133: | |
134: | |
135: | |
136: | public function findTypeToCheck( |
137: | Scope $scope, |
138: | Expr $var, |
139: | string $unknownClassErrorPattern, |
140: | callable $unionTypeCriteriaCallback, |
141: | ): FoundTypeResult |
142: | { |
143: | if ($this->checkThisOnly && !$this->isThis($var)) { |
144: | return new FoundTypeResult(new ErrorType(), [], [], null); |
145: | } |
146: | $type = $scope->getType($var); |
147: | if (!$this->checkNullables && !$type instanceof NullType) { |
148: | $type = TypeCombinator::removeNull($type); |
149: | } |
150: | |
151: | if ( |
152: | $this->checkExplicitMixed |
153: | && $type instanceof MixedType |
154: | && !$type instanceof TemplateMixedType |
155: | && $type->isExplicitMixed() |
156: | ) { |
157: | return new FoundTypeResult(new StrictMixedType(), [], [], null); |
158: | } |
159: | |
160: | if ( |
161: | $this->checkImplicitMixed |
162: | && $type instanceof MixedType |
163: | && !$type instanceof TemplateMixedType |
164: | && !$type->isExplicitMixed() |
165: | ) { |
166: | return new FoundTypeResult(new StrictMixedType(), [], [], null); |
167: | } |
168: | |
169: | if ($type instanceof MixedType || $type instanceof NeverType) { |
170: | return new FoundTypeResult(new ErrorType(), [], [], null); |
171: | } |
172: | if ($type instanceof StaticType) { |
173: | $type = $type->getStaticObjectType(); |
174: | } |
175: | |
176: | $errors = []; |
177: | $directClassNames = TypeUtils::getDirectClassNames($type); |
178: | $hasClassExistsClass = false; |
179: | foreach ($directClassNames as $referencedClass) { |
180: | if ($this->reflectionProvider->hasClass($referencedClass)) { |
181: | $classReflection = $this->reflectionProvider->getClass($referencedClass); |
182: | if (!$classReflection->isTrait()) { |
183: | continue; |
184: | } |
185: | } |
186: | |
187: | if ($scope->isInClassExists($referencedClass)) { |
188: | $hasClassExistsClass = true; |
189: | continue; |
190: | } |
191: | |
192: | $errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass))->line($var->getLine())->discoveringSymbolsTip()->build(); |
193: | } |
194: | |
195: | if (count($errors) > 0 || $hasClassExistsClass) { |
196: | return new FoundTypeResult(new ErrorType(), [], $errors, null); |
197: | } |
198: | |
199: | if (!$this->checkUnionTypes) { |
200: | if ($type instanceof ObjectWithoutClassType) { |
201: | return new FoundTypeResult(new ErrorType(), [], [], null); |
202: | } |
203: | if ($type instanceof UnionType) { |
204: | $newTypes = []; |
205: | foreach ($type->getTypes() as $innerType) { |
206: | if (!$unionTypeCriteriaCallback($innerType)) { |
207: | continue; |
208: | } |
209: | |
210: | $newTypes[] = $innerType; |
211: | } |
212: | |
213: | if (count($newTypes) > 0) { |
214: | return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null); |
215: | } |
216: | } |
217: | } |
218: | |
219: | $tip = null; |
220: | if (strpos($type->describe(VerbosityLevel::typeOnly()), 'PhpParser\\Node\\Arg|PhpParser\\Node\\VariadicPlaceholder') !== false && !$unionTypeCriteriaCallback($type)) { |
221: | $tip = 'Use <fg=cyan>->getArgs()</> instead of <fg=cyan>->args</>.'; |
222: | } |
223: | |
224: | return new FoundTypeResult($type, $directClassNames, [], $tip); |
225: | } |
226: | |
227: | } |
228: | |