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\CallableType; |
10: | use PHPStan\Type\ClosureType; |
11: | use PHPStan\Type\ErrorType; |
12: | use PHPStan\Type\Generic\TemplateMixedType; |
13: | use PHPStan\Type\IntersectionType; |
14: | use PHPStan\Type\MixedType; |
15: | use PHPStan\Type\NeverType; |
16: | use PHPStan\Type\NullType; |
17: | use PHPStan\Type\ObjectType; |
18: | use PHPStan\Type\StrictMixedType; |
19: | use PHPStan\Type\Type; |
20: | use PHPStan\Type\TypeCombinator; |
21: | use PHPStan\Type\TypeTraverser; |
22: | use PHPStan\Type\TypeUtils; |
23: | use PHPStan\Type\UnionType; |
24: | use function count; |
25: | use function sprintf; |
26: | |
27: | final class RuleLevelHelper |
28: | { |
29: | |
30: | public function __construct( |
31: | private ReflectionProvider $reflectionProvider, |
32: | private bool $checkNullables, |
33: | private bool $checkThisOnly, |
34: | private bool $checkUnionTypes, |
35: | private bool $checkExplicitMixed, |
36: | private bool $checkImplicitMixed, |
37: | private bool $checkBenevolentUnionTypes, |
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: | private function transformCommonType(Type $type): Type |
49: | { |
50: | if (!$this->checkExplicitMixed && !$this->checkImplicitMixed) { |
51: | return $type; |
52: | } |
53: | |
54: | return TypeTraverser::map($type, function (Type $type, callable $traverse) { |
55: | if ($type instanceof TemplateMixedType) { |
56: | if ($this->checkExplicitMixed) { |
57: | return $type->toStrictMixedType(); |
58: | } |
59: | } |
60: | if ( |
61: | $type instanceof MixedType |
62: | && ( |
63: | ($type->isExplicitMixed() && $this->checkExplicitMixed) |
64: | || (!$type->isExplicitMixed() && $this->checkImplicitMixed) |
65: | ) |
66: | ) { |
67: | return new StrictMixedType(); |
68: | } |
69: | |
70: | return $traverse($type); |
71: | }); |
72: | } |
73: | |
74: | |
75: | |
76: | |
77: | private function transformAcceptedType(Type $acceptingType, Type $acceptedType): array |
78: | { |
79: | $checkForUnion = $this->checkUnionTypes; |
80: | $acceptedType = TypeTraverser::map($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type { |
81: | if ($acceptedType instanceof CallableType) { |
82: | if ($acceptedType->isCommonCallable()) { |
83: | return $acceptedType; |
84: | } |
85: | |
86: | return new CallableType( |
87: | $acceptedType->getParameters(), |
88: | $traverse($this->transformCommonType($acceptedType->getReturnType())), |
89: | $acceptedType->isVariadic(), |
90: | $acceptedType->getTemplateTypeMap(), |
91: | $acceptedType->getResolvedTemplateTypeMap(), |
92: | $acceptedType->getTemplateTags(), |
93: | $acceptedType->isPure(), |
94: | ); |
95: | } |
96: | |
97: | if ($acceptedType instanceof ClosureType) { |
98: | if ($acceptedType->isCommonCallable()) { |
99: | return $acceptedType; |
100: | } |
101: | |
102: | return new ClosureType( |
103: | $acceptedType->getParameters(), |
104: | $traverse($this->transformCommonType($acceptedType->getReturnType())), |
105: | $acceptedType->isVariadic(), |
106: | $acceptedType->getTemplateTypeMap(), |
107: | $acceptedType->getResolvedTemplateTypeMap(), |
108: | $acceptedType->getCallSiteVarianceMap(), |
109: | $acceptedType->getTemplateTags(), |
110: | $acceptedType->getThrowPoints(), |
111: | $acceptedType->getImpurePoints(), |
112: | $acceptedType->getInvalidateExpressions(), |
113: | $acceptedType->getUsedVariables(), |
114: | $acceptedType->acceptsNamedArguments(), |
115: | ); |
116: | } |
117: | |
118: | if ( |
119: | !$this->checkNullables |
120: | && !$acceptingType instanceof NullType |
121: | && !$acceptedType instanceof NullType |
122: | && !$acceptedType instanceof BenevolentUnionType |
123: | ) { |
124: | return $traverse(TypeCombinator::removeNull($acceptedType)); |
125: | } |
126: | |
127: | if ($this->checkBenevolentUnionTypes) { |
128: | if ($acceptedType instanceof BenevolentUnionType) { |
129: | $checkForUnion = true; |
130: | return $traverse(TypeUtils::toStrictUnion($acceptedType)); |
131: | } |
132: | } |
133: | |
134: | return $traverse($this->transformCommonType($acceptedType)); |
135: | }); |
136: | |
137: | return [$acceptedType, $checkForUnion]; |
138: | } |
139: | |
140: | |
141: | public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTypes): RuleLevelHelperAcceptsResult |
142: | { |
143: | [$acceptedType, $checkForUnion] = $this->transformAcceptedType($acceptingType, $acceptedType); |
144: | $acceptingType = $this->transformCommonType($acceptingType); |
145: | |
146: | $accepts = $acceptingType->accepts($acceptedType, $strictTypes); |
147: | |
148: | return new RuleLevelHelperAcceptsResult( |
149: | $checkForUnion ? $accepts->yes() : !$accepts->no(), |
150: | $accepts->reasons, |
151: | ); |
152: | } |
153: | |
154: | |
155: | |
156: | |
157: | |
158: | public function findTypeToCheck( |
159: | Scope $scope, |
160: | Expr $var, |
161: | string $unknownClassErrorPattern, |
162: | callable $unionTypeCriteriaCallback, |
163: | ): FoundTypeResult |
164: | { |
165: | if ($this->checkThisOnly && !$this->isThis($var)) { |
166: | return new FoundTypeResult(new ErrorType(), [], [], null); |
167: | } |
168: | $type = $scope->getType($var); |
169: | |
170: | return $this->findTypeToCheckImplementation($scope, $var, $type, $unknownClassErrorPattern, $unionTypeCriteriaCallback, true); |
171: | } |
172: | |
173: | |
174: | private function findTypeToCheckImplementation( |
175: | Scope $scope, |
176: | Expr $var, |
177: | Type $type, |
178: | string $unknownClassErrorPattern, |
179: | callable $unionTypeCriteriaCallback, |
180: | bool $isTopLevel = false, |
181: | ): FoundTypeResult |
182: | { |
183: | if (!$this->checkNullables && !$type->isNull()->yes()) { |
184: | $type = TypeCombinator::removeNull($type); |
185: | } |
186: | |
187: | if ( |
188: | ($this->checkExplicitMixed || $this->checkImplicitMixed) |
189: | && $type instanceof MixedType |
190: | && ($type->isExplicitMixed() ? $this->checkExplicitMixed : $this->checkImplicitMixed) |
191: | ) { |
192: | return new FoundTypeResult( |
193: | $type instanceof TemplateMixedType |
194: | ? $type->toStrictMixedType() |
195: | : new StrictMixedType(), |
196: | [], |
197: | [], |
198: | null, |
199: | ); |
200: | } |
201: | |
202: | if ($type instanceof MixedType || $type instanceof NeverType) { |
203: | return new FoundTypeResult(new ErrorType(), [], [], null); |
204: | } |
205: | |
206: | $errors = []; |
207: | $hasClassExistsClass = false; |
208: | $directClassNames = []; |
209: | |
210: | if ($isTopLevel) { |
211: | $directClassNames = $type->getObjectClassNames(); |
212: | foreach ($directClassNames as $referencedClass) { |
213: | if ($this->reflectionProvider->hasClass($referencedClass)) { |
214: | $classReflection = $this->reflectionProvider->getClass($referencedClass); |
215: | if (!$classReflection->isTrait()) { |
216: | continue; |
217: | } |
218: | } |
219: | |
220: | if ($scope->isInClassExists($referencedClass)) { |
221: | $hasClassExistsClass = true; |
222: | continue; |
223: | } |
224: | |
225: | $errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass)) |
226: | ->line($var->getStartLine()) |
227: | ->identifier('class.notFound') |
228: | ->discoveringSymbolsTip() |
229: | ->build(); |
230: | } |
231: | } |
232: | |
233: | if (count($errors) > 0 || $hasClassExistsClass) { |
234: | return new FoundTypeResult(new ErrorType(), [], $errors, null); |
235: | } |
236: | |
237: | if (!$this->checkUnionTypes && $type->isObject()->yes() && count($type->getObjectClassNames()) === 0) { |
238: | return new FoundTypeResult(new ErrorType(), [], [], null); |
239: | } |
240: | |
241: | if ($type instanceof UnionType) { |
242: | $shouldFilterUnion = ( |
243: | !$this->checkUnionTypes |
244: | && !$type instanceof BenevolentUnionType |
245: | ) || ( |
246: | !$this->checkBenevolentUnionTypes |
247: | && $type instanceof BenevolentUnionType |
248: | ); |
249: | |
250: | $newTypes = []; |
251: | |
252: | foreach ($type->getTypes() as $innerType) { |
253: | if ($shouldFilterUnion && !$unionTypeCriteriaCallback($innerType)) { |
254: | continue; |
255: | } |
256: | |
257: | $newTypes[] = $this->findTypeToCheckImplementation( |
258: | $scope, |
259: | $var, |
260: | $innerType, |
261: | $unknownClassErrorPattern, |
262: | $unionTypeCriteriaCallback, |
263: | )->getType(); |
264: | } |
265: | |
266: | if (count($newTypes) > 0) { |
267: | $newUnion = TypeCombinator::union(...$newTypes); |
268: | if ( |
269: | !$this->checkBenevolentUnionTypes |
270: | && $type instanceof BenevolentUnionType |
271: | ) { |
272: | $newUnion = TypeUtils::toBenevolentUnion($newUnion); |
273: | } |
274: | |
275: | return new FoundTypeResult($newUnion, $directClassNames, [], null); |
276: | } |
277: | } |
278: | |
279: | if ($type instanceof IntersectionType) { |
280: | $newTypes = []; |
281: | |
282: | $changed = false; |
283: | foreach ($type->getTypes() as $innerType) { |
284: | if ($innerType instanceof TemplateMixedType) { |
285: | $changed = true; |
286: | $newTypes[] = $this->findTypeToCheckImplementation( |
287: | $scope, |
288: | $var, |
289: | $innerType->toStrictMixedType(), |
290: | $unknownClassErrorPattern, |
291: | $unionTypeCriteriaCallback, |
292: | )->getType(); |
293: | continue; |
294: | } |
295: | $newTypes[] = $innerType; |
296: | } |
297: | |
298: | if ($changed) { |
299: | return new FoundTypeResult(TypeCombinator::intersect(...$newTypes), $directClassNames, [], null); |
300: | } |
301: | } |
302: | |
303: | $tip = null; |
304: | if ( |
305: | $type instanceof UnionType |
306: | && count($type->getTypes()) === 2 |
307: | && $type->getTypes()[0] instanceof ObjectType |
308: | && $type->getTypes()[1] instanceof ObjectType |
309: | && $type->getTypes()[0]->getClassName() === 'PhpParser\\Node\\Arg' |
310: | && $type->getTypes()[1]->getClassName() === 'PhpParser\\Node\\VariadicPlaceholder' |
311: | && !$unionTypeCriteriaCallback($type) |
312: | ) { |
313: | $tip = 'Use <fg=cyan>->getArgs()</> instead of <fg=cyan>->args</>.'; |
314: | } |
315: | |
316: | return new FoundTypeResult($type, $directClassNames, [], $tip); |
317: | } |
318: | |
319: | } |
320: | |