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\ObjectWithoutClassType; |
18: | use PHPStan\Type\StaticType; |
19: | use PHPStan\Type\StrictMixedType; |
20: | use PHPStan\Type\Type; |
21: | use PHPStan\Type\TypeCombinator; |
22: | use PHPStan\Type\TypeTraverser; |
23: | use PHPStan\Type\TypeUtils; |
24: | use PHPStan\Type\UnionType; |
25: | use PHPStan\Type\VerbosityLevel; |
26: | use function array_merge; |
27: | use function count; |
28: | use function sprintf; |
29: | use function str_contains; |
30: | |
31: | class RuleLevelHelper |
32: | { |
33: | |
34: | public function __construct( |
35: | private ReflectionProvider $reflectionProvider, |
36: | private bool $checkNullables, |
37: | private bool $checkThisOnly, |
38: | private bool $checkUnionTypes, |
39: | private bool $checkExplicitMixed, |
40: | private bool $checkImplicitMixed, |
41: | private bool $newRuleLevelHelper, |
42: | private bool $checkBenevolentUnionTypes, |
43: | ) |
44: | { |
45: | } |
46: | |
47: | |
48: | public function isThis(Expr $expression): bool |
49: | { |
50: | return $expression instanceof Expr\Variable && $expression->name === 'this'; |
51: | } |
52: | |
53: | |
54: | public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTypes): bool |
55: | { |
56: | return $this->acceptsWithReason($acceptingType, $acceptedType, $strictTypes)->result; |
57: | } |
58: | |
59: | private function transformCommonType(Type $type): Type |
60: | { |
61: | if (!$this->checkExplicitMixed && !$this->checkImplicitMixed) { |
62: | return $type; |
63: | } |
64: | |
65: | return TypeTraverser::map($type, function (Type $type, callable $traverse) { |
66: | if ($type instanceof TemplateMixedType) { |
67: | if (!$this->newRuleLevelHelper) { |
68: | return $type->toStrictMixedType(); |
69: | } |
70: | |
71: | if ($this->checkExplicitMixed) { |
72: | return $type->toStrictMixedType(); |
73: | } |
74: | } |
75: | if ( |
76: | $type instanceof MixedType |
77: | && ( |
78: | ($type->isExplicitMixed() && $this->checkExplicitMixed) |
79: | || (!$type->isExplicitMixed() && $this->checkImplicitMixed) |
80: | ) |
81: | ) { |
82: | return new StrictMixedType(); |
83: | } |
84: | |
85: | return $traverse($type); |
86: | }); |
87: | } |
88: | |
89: | |
90: | |
91: | |
92: | private function transformAcceptedType(Type $acceptingType, Type $acceptedType): array |
93: | { |
94: | $checkForUnion = $this->checkUnionTypes; |
95: | $acceptedType = TypeTraverser::map($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type { |
96: | if ($acceptedType instanceof CallableType) { |
97: | if ($acceptedType->isCommonCallable()) { |
98: | return $acceptedType; |
99: | } |
100: | |
101: | return new CallableType( |
102: | $acceptedType->getParameters(), |
103: | $traverse($this->transformCommonType($acceptedType->getReturnType())), |
104: | $acceptedType->isVariadic(), |
105: | $acceptedType->getTemplateTypeMap(), |
106: | $acceptedType->getResolvedTemplateTypeMap(), |
107: | $acceptedType->getTemplateTags(), |
108: | $acceptedType->isPure(), |
109: | ); |
110: | } |
111: | |
112: | if ($acceptedType instanceof ClosureType) { |
113: | if ($acceptedType->isCommonCallable()) { |
114: | return $acceptedType; |
115: | } |
116: | |
117: | return new ClosureType( |
118: | $acceptedType->getParameters(), |
119: | $traverse($this->transformCommonType($acceptedType->getReturnType())), |
120: | $acceptedType->isVariadic(), |
121: | $acceptedType->getTemplateTypeMap(), |
122: | $acceptedType->getResolvedTemplateTypeMap(), |
123: | $acceptedType->getCallSiteVarianceMap(), |
124: | $acceptedType->getTemplateTags(), |
125: | $acceptedType->getThrowPoints(), |
126: | $acceptedType->getImpurePoints(), |
127: | ); |
128: | } |
129: | |
130: | if ( |
131: | !$this->checkNullables |
132: | && !$acceptingType instanceof NullType |
133: | && !$acceptedType instanceof NullType |
134: | && !$acceptedType instanceof BenevolentUnionType |
135: | ) { |
136: | return $traverse(TypeCombinator::removeNull($acceptedType)); |
137: | } |
138: | |
139: | if ($this->checkBenevolentUnionTypes) { |
140: | if ($acceptedType instanceof BenevolentUnionType) { |
141: | $checkForUnion = true; |
142: | return $traverse(TypeUtils::toStrictUnion($acceptedType)); |
143: | } |
144: | } |
145: | |
146: | return $traverse($this->transformCommonType($acceptedType)); |
147: | }); |
148: | |
149: | return [$acceptedType, $checkForUnion]; |
150: | } |
151: | |
152: | public function acceptsWithReason(Type $acceptingType, Type $acceptedType, bool $strictTypes): RuleLevelHelperAcceptsResult |
153: | { |
154: | if ($this->newRuleLevelHelper) { |
155: | [$acceptedType, $checkForUnion] = $this->transformAcceptedType($acceptingType, $acceptedType); |
156: | $acceptingType = $this->transformCommonType($acceptingType); |
157: | |
158: | $accepts = $acceptingType->acceptsWithReason($acceptedType, $strictTypes); |
159: | |
160: | return new RuleLevelHelperAcceptsResult( |
161: | $checkForUnion ? $accepts->yes() : !$accepts->no(), |
162: | $accepts->reasons, |
163: | ); |
164: | } |
165: | |
166: | $checkForUnion = $this->checkUnionTypes; |
167: | |
168: | if ($this->checkBenevolentUnionTypes) { |
169: | $traverse = static function (Type $type, callable $traverse) use (&$checkForUnion): Type { |
170: | if ($type instanceof BenevolentUnionType) { |
171: | $checkForUnion = true; |
172: | return TypeUtils::toStrictUnion($type); |
173: | } |
174: | |
175: | return $traverse($type); |
176: | }; |
177: | |
178: | $acceptedType = TypeTraverser::map($acceptedType, $traverse); |
179: | } |
180: | |
181: | if ( |
182: | $this->checkExplicitMixed |
183: | ) { |
184: | $traverse = static function (Type $type, callable $traverse): Type { |
185: | if ($type instanceof TemplateMixedType) { |
186: | return $type->toStrictMixedType(); |
187: | } |
188: | if ( |
189: | $type instanceof MixedType |
190: | && $type->isExplicitMixed() |
191: | ) { |
192: | return new StrictMixedType(); |
193: | } |
194: | |
195: | return $traverse($type); |
196: | }; |
197: | $acceptingType = TypeTraverser::map($acceptingType, $traverse); |
198: | $acceptedType = TypeTraverser::map($acceptedType, $traverse); |
199: | } |
200: | |
201: | if ( |
202: | $this->checkImplicitMixed |
203: | ) { |
204: | $traverse = static function (Type $type, callable $traverse): Type { |
205: | if ($type instanceof TemplateMixedType) { |
206: | return $type->toStrictMixedType(); |
207: | } |
208: | if ( |
209: | $type instanceof MixedType |
210: | && !$type->isExplicitMixed() |
211: | ) { |
212: | return new StrictMixedType(); |
213: | } |
214: | |
215: | return $traverse($type); |
216: | }; |
217: | $acceptingType = TypeTraverser::map($acceptingType, $traverse); |
218: | $acceptedType = TypeTraverser::map($acceptedType, $traverse); |
219: | } |
220: | |
221: | if ( |
222: | !$this->checkNullables |
223: | && !$acceptingType instanceof NullType |
224: | && !$acceptedType instanceof NullType |
225: | && !$acceptedType instanceof BenevolentUnionType |
226: | ) { |
227: | $acceptedType = TypeCombinator::removeNull($acceptedType); |
228: | } |
229: | |
230: | $accepts = $acceptingType->acceptsWithReason($acceptedType, $strictTypes); |
231: | if ($accepts->yes()) { |
232: | return new RuleLevelHelperAcceptsResult(true, $accepts->reasons); |
233: | } |
234: | if ($acceptingType instanceof UnionType) { |
235: | $reasons = []; |
236: | foreach ($acceptingType->getTypes() as $innerType) { |
237: | $accepts = self::acceptsWithReason($innerType, $acceptedType, $strictTypes); |
238: | if ($accepts->result) { |
239: | return $accepts; |
240: | } |
241: | |
242: | $reasons = array_merge($reasons, $accepts->reasons); |
243: | } |
244: | |
245: | return new RuleLevelHelperAcceptsResult(false, $reasons); |
246: | } |
247: | |
248: | if ( |
249: | $acceptedType->isArray()->yes() |
250: | && $acceptingType->isArray()->yes() |
251: | && ( |
252: | $acceptedType->isConstantArray()->no() |
253: | || !$acceptedType->isIterableAtLeastOnce()->no() |
254: | ) |
255: | && $acceptingType->isConstantArray()->no() |
256: | ) { |
257: | if ($acceptingType->isIterableAtLeastOnce()->yes() && !$acceptedType->isIterableAtLeastOnce()->yes()) { |
258: | $verbosity = VerbosityLevel::getRecommendedLevelByType($acceptingType, $acceptedType); |
259: | return new RuleLevelHelperAcceptsResult(false, [ |
260: | sprintf( |
261: | '%s %s empty.', |
262: | $acceptedType->describe($verbosity), |
263: | $acceptedType->isIterableAtLeastOnce()->no() ? 'is' : 'might be', |
264: | ), |
265: | ]); |
266: | } |
267: | |
268: | if ( |
269: | $acceptingType->isList()->yes() |
270: | && !$acceptedType->isList()->yes() |
271: | ) { |
272: | $report = $checkForUnion || $acceptedType->isList()->no(); |
273: | |
274: | if ($report) { |
275: | $verbosity = VerbosityLevel::getRecommendedLevelByType($acceptingType, $acceptedType); |
276: | return new RuleLevelHelperAcceptsResult(false, [ |
277: | sprintf( |
278: | '%s %s a list.', |
279: | $acceptedType->describe($verbosity), |
280: | $acceptedType->isList()->no() ? 'is not' : 'might not be', |
281: | ), |
282: | ]); |
283: | } |
284: | } |
285: | |
286: | return self::acceptsWithReason( |
287: | $acceptingType->getIterableKeyType(), |
288: | $acceptedType->getIterableKeyType(), |
289: | $strictTypes, |
290: | )->and(self::acceptsWithReason( |
291: | $acceptingType->getIterableValueType(), |
292: | $acceptedType->getIterableValueType(), |
293: | $strictTypes, |
294: | )); |
295: | } |
296: | |
297: | return new RuleLevelHelperAcceptsResult( |
298: | $checkForUnion ? $accepts->yes() : !$accepts->no(), |
299: | $accepts->reasons, |
300: | ); |
301: | } |
302: | |
303: | |
304: | |
305: | |
306: | |
307: | public function findTypeToCheck( |
308: | Scope $scope, |
309: | Expr $var, |
310: | string $unknownClassErrorPattern, |
311: | callable $unionTypeCriteriaCallback, |
312: | ): FoundTypeResult |
313: | { |
314: | if ($this->checkThisOnly && !$this->isThis($var)) { |
315: | return new FoundTypeResult(new ErrorType(), [], [], null); |
316: | } |
317: | $type = $scope->getType($var); |
318: | |
319: | return $this->findTypeToCheckImplementation($scope, $var, $type, $unknownClassErrorPattern, $unionTypeCriteriaCallback, true); |
320: | } |
321: | |
322: | |
323: | private function findTypeToCheckImplementation( |
324: | Scope $scope, |
325: | Expr $var, |
326: | Type $type, |
327: | string $unknownClassErrorPattern, |
328: | callable $unionTypeCriteriaCallback, |
329: | bool $isTopLevel = false, |
330: | ): FoundTypeResult |
331: | { |
332: | if (!$this->checkNullables && !$type->isNull()->yes()) { |
333: | $type = TypeCombinator::removeNull($type); |
334: | } |
335: | |
336: | if ($this->newRuleLevelHelper) { |
337: | if ( |
338: | ($this->checkExplicitMixed || $this->checkImplicitMixed) |
339: | && $type instanceof MixedType |
340: | && ($type->isExplicitMixed() ? $this->checkExplicitMixed : $this->checkImplicitMixed) |
341: | ) { |
342: | return new FoundTypeResult( |
343: | $type instanceof TemplateMixedType |
344: | ? $type->toStrictMixedType() |
345: | : new StrictMixedType(), |
346: | [], |
347: | [], |
348: | null, |
349: | ); |
350: | } |
351: | } else { |
352: | if ( |
353: | $this->checkExplicitMixed |
354: | && $type instanceof MixedType |
355: | && !$type instanceof TemplateMixedType |
356: | && $type->isExplicitMixed() |
357: | ) { |
358: | return new FoundTypeResult(new StrictMixedType(), [], [], null); |
359: | } |
360: | |
361: | if ( |
362: | $this->checkImplicitMixed |
363: | && $type instanceof MixedType |
364: | && !$type instanceof TemplateMixedType |
365: | && !$type->isExplicitMixed() |
366: | ) { |
367: | return new FoundTypeResult(new StrictMixedType(), [], [], null); |
368: | } |
369: | } |
370: | |
371: | if ($type instanceof MixedType || $type instanceof NeverType) { |
372: | return new FoundTypeResult(new ErrorType(), [], [], null); |
373: | } |
374: | if (!$this->newRuleLevelHelper) { |
375: | if ($isTopLevel && $type instanceof StaticType) { |
376: | $type = $type->getStaticObjectType(); |
377: | } |
378: | } |
379: | |
380: | $errors = []; |
381: | $hasClassExistsClass = false; |
382: | $directClassNames = []; |
383: | |
384: | if ($isTopLevel) { |
385: | $directClassNames = $type->getObjectClassNames(); |
386: | foreach ($directClassNames as $referencedClass) { |
387: | if ($this->reflectionProvider->hasClass($referencedClass)) { |
388: | $classReflection = $this->reflectionProvider->getClass($referencedClass); |
389: | if (!$classReflection->isTrait()) { |
390: | continue; |
391: | } |
392: | } |
393: | |
394: | if ($scope->isInClassExists($referencedClass)) { |
395: | $hasClassExistsClass = true; |
396: | continue; |
397: | } |
398: | |
399: | $errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass)) |
400: | ->line($var->getStartLine()) |
401: | ->identifier('class.notFound') |
402: | ->discoveringSymbolsTip() |
403: | ->build(); |
404: | } |
405: | } |
406: | |
407: | if (count($errors) > 0 || $hasClassExistsClass) { |
408: | return new FoundTypeResult(new ErrorType(), [], $errors, null); |
409: | } |
410: | |
411: | if (!$this->checkUnionTypes && $type instanceof ObjectWithoutClassType) { |
412: | return new FoundTypeResult(new ErrorType(), [], [], null); |
413: | } |
414: | |
415: | if ($this->newRuleLevelHelper) { |
416: | if ($type instanceof UnionType) { |
417: | $shouldFilterUnion = ( |
418: | !$this->checkUnionTypes |
419: | && !$type instanceof BenevolentUnionType |
420: | ) || ( |
421: | !$this->checkBenevolentUnionTypes |
422: | && $type instanceof BenevolentUnionType |
423: | ); |
424: | |
425: | $newTypes = []; |
426: | |
427: | foreach ($type->getTypes() as $innerType) { |
428: | if ($shouldFilterUnion && !$unionTypeCriteriaCallback($innerType)) { |
429: | continue; |
430: | } |
431: | |
432: | $newTypes[] = $this->findTypeToCheckImplementation( |
433: | $scope, |
434: | $var, |
435: | $innerType, |
436: | $unknownClassErrorPattern, |
437: | $unionTypeCriteriaCallback, |
438: | )->getType(); |
439: | } |
440: | |
441: | if (count($newTypes) > 0) { |
442: | return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null); |
443: | } |
444: | } |
445: | |
446: | if ($type instanceof IntersectionType) { |
447: | $newTypes = []; |
448: | |
449: | foreach ($type->getTypes() as $innerType) { |
450: | $newTypes[] = $this->findTypeToCheckImplementation( |
451: | $scope, |
452: | $var, |
453: | $innerType, |
454: | $unknownClassErrorPattern, |
455: | $unionTypeCriteriaCallback, |
456: | )->getType(); |
457: | } |
458: | |
459: | return new FoundTypeResult(TypeCombinator::intersect(...$newTypes), $directClassNames, [], null); |
460: | } |
461: | } else { |
462: | if ( |
463: | ( |
464: | !$this->checkUnionTypes |
465: | && $type instanceof UnionType |
466: | && !$type instanceof BenevolentUnionType |
467: | ) || ( |
468: | !$this->checkBenevolentUnionTypes |
469: | && $type instanceof BenevolentUnionType |
470: | ) |
471: | ) { |
472: | $newTypes = []; |
473: | |
474: | foreach ($type->getTypes() as $innerType) { |
475: | if (!$unionTypeCriteriaCallback($innerType)) { |
476: | continue; |
477: | } |
478: | |
479: | $newTypes[] = $innerType; |
480: | } |
481: | |
482: | if (count($newTypes) > 0) { |
483: | return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null); |
484: | } |
485: | } |
486: | } |
487: | |
488: | $tip = null; |
489: | if (str_contains($type->describe(VerbosityLevel::typeOnly()), 'PhpParser\\Node\\Arg|PhpParser\\Node\\VariadicPlaceholder') && !$unionTypeCriteriaCallback($type)) { |
490: | $tip = 'Use <fg=cyan>->getArgs()</> instead of <fg=cyan>->args</>.'; |
491: | } |
492: | |
493: | return new FoundTypeResult($type, $directClassNames, [], $tip); |
494: | } |
495: | |
496: | } |
497: | |