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: | final 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: | $acceptedType->getInvalidateExpressions(), |
128: | $acceptedType->getUsedVariables(), |
129: | $acceptedType->acceptsNamedArguments(), |
130: | ); |
131: | } |
132: | |
133: | if ( |
134: | !$this->checkNullables |
135: | && !$acceptingType instanceof NullType |
136: | && !$acceptedType instanceof NullType |
137: | && !$acceptedType instanceof BenevolentUnionType |
138: | ) { |
139: | return $traverse(TypeCombinator::removeNull($acceptedType)); |
140: | } |
141: | |
142: | if ($this->checkBenevolentUnionTypes) { |
143: | if ($acceptedType instanceof BenevolentUnionType) { |
144: | $checkForUnion = true; |
145: | return $traverse(TypeUtils::toStrictUnion($acceptedType)); |
146: | } |
147: | } |
148: | |
149: | return $traverse($this->transformCommonType($acceptedType)); |
150: | }); |
151: | |
152: | return [$acceptedType, $checkForUnion]; |
153: | } |
154: | |
155: | public function acceptsWithReason(Type $acceptingType, Type $acceptedType, bool $strictTypes): RuleLevelHelperAcceptsResult |
156: | { |
157: | if ($this->newRuleLevelHelper) { |
158: | [$acceptedType, $checkForUnion] = $this->transformAcceptedType($acceptingType, $acceptedType); |
159: | $acceptingType = $this->transformCommonType($acceptingType); |
160: | |
161: | $accepts = $acceptingType->acceptsWithReason($acceptedType, $strictTypes); |
162: | |
163: | return new RuleLevelHelperAcceptsResult( |
164: | $checkForUnion ? $accepts->yes() : !$accepts->no(), |
165: | $accepts->reasons, |
166: | ); |
167: | } |
168: | |
169: | $checkForUnion = $this->checkUnionTypes; |
170: | |
171: | if ($this->checkBenevolentUnionTypes) { |
172: | $traverse = static function (Type $type, callable $traverse) use (&$checkForUnion): Type { |
173: | if ($type instanceof BenevolentUnionType) { |
174: | $checkForUnion = true; |
175: | return TypeUtils::toStrictUnion($type); |
176: | } |
177: | |
178: | return $traverse($type); |
179: | }; |
180: | |
181: | $acceptedType = TypeTraverser::map($acceptedType, $traverse); |
182: | } |
183: | |
184: | if ( |
185: | $this->checkExplicitMixed |
186: | ) { |
187: | $traverse = static function (Type $type, callable $traverse): Type { |
188: | if ($type instanceof TemplateMixedType) { |
189: | return $type->toStrictMixedType(); |
190: | } |
191: | if ( |
192: | $type instanceof MixedType |
193: | && $type->isExplicitMixed() |
194: | ) { |
195: | return new StrictMixedType(); |
196: | } |
197: | |
198: | return $traverse($type); |
199: | }; |
200: | $acceptingType = TypeTraverser::map($acceptingType, $traverse); |
201: | $acceptedType = TypeTraverser::map($acceptedType, $traverse); |
202: | } |
203: | |
204: | if ( |
205: | $this->checkImplicitMixed |
206: | ) { |
207: | $traverse = static function (Type $type, callable $traverse): Type { |
208: | if ($type instanceof TemplateMixedType) { |
209: | return $type->toStrictMixedType(); |
210: | } |
211: | if ( |
212: | $type instanceof MixedType |
213: | && !$type->isExplicitMixed() |
214: | ) { |
215: | return new StrictMixedType(); |
216: | } |
217: | |
218: | return $traverse($type); |
219: | }; |
220: | $acceptingType = TypeTraverser::map($acceptingType, $traverse); |
221: | $acceptedType = TypeTraverser::map($acceptedType, $traverse); |
222: | } |
223: | |
224: | if ( |
225: | !$this->checkNullables |
226: | && !$acceptingType instanceof NullType |
227: | && !$acceptedType instanceof NullType |
228: | && !$acceptedType instanceof BenevolentUnionType |
229: | ) { |
230: | $acceptedType = TypeCombinator::removeNull($acceptedType); |
231: | } |
232: | |
233: | $accepts = $acceptingType->acceptsWithReason($acceptedType, $strictTypes); |
234: | if ($accepts->yes()) { |
235: | return new RuleLevelHelperAcceptsResult(true, $accepts->reasons); |
236: | } |
237: | if ($acceptingType instanceof UnionType) { |
238: | $reasons = []; |
239: | foreach ($acceptingType->getTypes() as $innerType) { |
240: | $accepts = self::acceptsWithReason($innerType, $acceptedType, $strictTypes); |
241: | if ($accepts->result) { |
242: | return $accepts; |
243: | } |
244: | |
245: | $reasons = array_merge($reasons, $accepts->reasons); |
246: | } |
247: | |
248: | return new RuleLevelHelperAcceptsResult(false, $reasons); |
249: | } |
250: | |
251: | if ( |
252: | $acceptedType->isArray()->yes() |
253: | && $acceptingType->isArray()->yes() |
254: | && ( |
255: | $acceptedType->isConstantArray()->no() |
256: | || !$acceptedType->isIterableAtLeastOnce()->no() |
257: | ) |
258: | && $acceptingType->isConstantArray()->no() |
259: | ) { |
260: | if ($acceptingType->isIterableAtLeastOnce()->yes() && !$acceptedType->isIterableAtLeastOnce()->yes()) { |
261: | $verbosity = VerbosityLevel::getRecommendedLevelByType($acceptingType, $acceptedType); |
262: | return new RuleLevelHelperAcceptsResult(false, [ |
263: | sprintf( |
264: | '%s %s empty.', |
265: | $acceptedType->describe($verbosity), |
266: | $acceptedType->isIterableAtLeastOnce()->no() ? 'is' : 'might be', |
267: | ), |
268: | ]); |
269: | } |
270: | |
271: | if ( |
272: | $acceptingType->isList()->yes() |
273: | && !$acceptedType->isList()->yes() |
274: | ) { |
275: | $report = $checkForUnion || $acceptedType->isList()->no(); |
276: | |
277: | if ($report) { |
278: | $verbosity = VerbosityLevel::getRecommendedLevelByType($acceptingType, $acceptedType); |
279: | return new RuleLevelHelperAcceptsResult(false, [ |
280: | sprintf( |
281: | '%s %s a list.', |
282: | $acceptedType->describe($verbosity), |
283: | $acceptedType->isList()->no() ? 'is not' : 'might not be', |
284: | ), |
285: | ]); |
286: | } |
287: | } |
288: | |
289: | return self::acceptsWithReason( |
290: | $acceptingType->getIterableKeyType(), |
291: | $acceptedType->getIterableKeyType(), |
292: | $strictTypes, |
293: | )->and(self::acceptsWithReason( |
294: | $acceptingType->getIterableValueType(), |
295: | $acceptedType->getIterableValueType(), |
296: | $strictTypes, |
297: | )); |
298: | } |
299: | |
300: | return new RuleLevelHelperAcceptsResult( |
301: | $checkForUnion ? $accepts->yes() : !$accepts->no(), |
302: | $accepts->reasons, |
303: | ); |
304: | } |
305: | |
306: | |
307: | |
308: | |
309: | |
310: | public function findTypeToCheck( |
311: | Scope $scope, |
312: | Expr $var, |
313: | string $unknownClassErrorPattern, |
314: | callable $unionTypeCriteriaCallback, |
315: | ): FoundTypeResult |
316: | { |
317: | if ($this->checkThisOnly && !$this->isThis($var)) { |
318: | return new FoundTypeResult(new ErrorType(), [], [], null); |
319: | } |
320: | $type = $scope->getType($var); |
321: | |
322: | return $this->findTypeToCheckImplementation($scope, $var, $type, $unknownClassErrorPattern, $unionTypeCriteriaCallback, true); |
323: | } |
324: | |
325: | |
326: | private function findTypeToCheckImplementation( |
327: | Scope $scope, |
328: | Expr $var, |
329: | Type $type, |
330: | string $unknownClassErrorPattern, |
331: | callable $unionTypeCriteriaCallback, |
332: | bool $isTopLevel = false, |
333: | ): FoundTypeResult |
334: | { |
335: | if (!$this->checkNullables && !$type->isNull()->yes()) { |
336: | $type = TypeCombinator::removeNull($type); |
337: | } |
338: | |
339: | if ($this->newRuleLevelHelper) { |
340: | if ( |
341: | ($this->checkExplicitMixed || $this->checkImplicitMixed) |
342: | && $type instanceof MixedType |
343: | && ($type->isExplicitMixed() ? $this->checkExplicitMixed : $this->checkImplicitMixed) |
344: | ) { |
345: | return new FoundTypeResult( |
346: | $type instanceof TemplateMixedType |
347: | ? $type->toStrictMixedType() |
348: | : new StrictMixedType(), |
349: | [], |
350: | [], |
351: | null, |
352: | ); |
353: | } |
354: | } else { |
355: | if ( |
356: | $this->checkExplicitMixed |
357: | && $type instanceof MixedType |
358: | && !$type instanceof TemplateMixedType |
359: | && $type->isExplicitMixed() |
360: | ) { |
361: | return new FoundTypeResult(new StrictMixedType(), [], [], null); |
362: | } |
363: | |
364: | if ( |
365: | $this->checkImplicitMixed |
366: | && $type instanceof MixedType |
367: | && !$type instanceof TemplateMixedType |
368: | && !$type->isExplicitMixed() |
369: | ) { |
370: | return new FoundTypeResult(new StrictMixedType(), [], [], null); |
371: | } |
372: | } |
373: | |
374: | if ($type instanceof MixedType || $type instanceof NeverType) { |
375: | return new FoundTypeResult(new ErrorType(), [], [], null); |
376: | } |
377: | if (!$this->newRuleLevelHelper) { |
378: | if ($isTopLevel && $type instanceof StaticType) { |
379: | $type = $type->getStaticObjectType(); |
380: | } |
381: | } |
382: | |
383: | $errors = []; |
384: | $hasClassExistsClass = false; |
385: | $directClassNames = []; |
386: | |
387: | if ($isTopLevel) { |
388: | $directClassNames = $type->getObjectClassNames(); |
389: | foreach ($directClassNames as $referencedClass) { |
390: | if ($this->reflectionProvider->hasClass($referencedClass)) { |
391: | $classReflection = $this->reflectionProvider->getClass($referencedClass); |
392: | if (!$classReflection->isTrait()) { |
393: | continue; |
394: | } |
395: | } |
396: | |
397: | if ($scope->isInClassExists($referencedClass)) { |
398: | $hasClassExistsClass = true; |
399: | continue; |
400: | } |
401: | |
402: | $errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass)) |
403: | ->line($var->getStartLine()) |
404: | ->identifier('class.notFound') |
405: | ->discoveringSymbolsTip() |
406: | ->build(); |
407: | } |
408: | } |
409: | |
410: | if (count($errors) > 0 || $hasClassExistsClass) { |
411: | return new FoundTypeResult(new ErrorType(), [], $errors, null); |
412: | } |
413: | |
414: | if (!$this->checkUnionTypes && $type instanceof ObjectWithoutClassType) { |
415: | return new FoundTypeResult(new ErrorType(), [], [], null); |
416: | } |
417: | |
418: | if ($this->newRuleLevelHelper) { |
419: | if ($type instanceof UnionType) { |
420: | $shouldFilterUnion = ( |
421: | !$this->checkUnionTypes |
422: | && !$type instanceof BenevolentUnionType |
423: | ) || ( |
424: | !$this->checkBenevolentUnionTypes |
425: | && $type instanceof BenevolentUnionType |
426: | ); |
427: | |
428: | $newTypes = []; |
429: | |
430: | foreach ($type->getTypes() as $innerType) { |
431: | if ($shouldFilterUnion && !$unionTypeCriteriaCallback($innerType)) { |
432: | continue; |
433: | } |
434: | |
435: | $newTypes[] = $this->findTypeToCheckImplementation( |
436: | $scope, |
437: | $var, |
438: | $innerType, |
439: | $unknownClassErrorPattern, |
440: | $unionTypeCriteriaCallback, |
441: | )->getType(); |
442: | } |
443: | |
444: | if (count($newTypes) > 0) { |
445: | $newUnion = TypeCombinator::union(...$newTypes); |
446: | if ( |
447: | !$this->checkBenevolentUnionTypes |
448: | && $type instanceof BenevolentUnionType |
449: | ) { |
450: | $newUnion = TypeUtils::toBenevolentUnion($newUnion); |
451: | } |
452: | |
453: | return new FoundTypeResult($newUnion, $directClassNames, [], null); |
454: | } |
455: | } |
456: | |
457: | if ($type instanceof IntersectionType) { |
458: | $newTypes = []; |
459: | |
460: | foreach ($type->getTypes() as $innerType) { |
461: | $newTypes[] = $this->findTypeToCheckImplementation( |
462: | $scope, |
463: | $var, |
464: | $innerType, |
465: | $unknownClassErrorPattern, |
466: | $unionTypeCriteriaCallback, |
467: | )->getType(); |
468: | } |
469: | |
470: | return new FoundTypeResult(TypeCombinator::intersect(...$newTypes), $directClassNames, [], null); |
471: | } |
472: | } else { |
473: | if ( |
474: | ( |
475: | !$this->checkUnionTypes |
476: | && $type instanceof UnionType |
477: | && !$type instanceof BenevolentUnionType |
478: | ) || ( |
479: | !$this->checkBenevolentUnionTypes |
480: | && $type instanceof BenevolentUnionType |
481: | ) |
482: | ) { |
483: | $newTypes = []; |
484: | |
485: | foreach ($type->getTypes() as $innerType) { |
486: | if (!$unionTypeCriteriaCallback($innerType)) { |
487: | continue; |
488: | } |
489: | |
490: | $newTypes[] = $innerType; |
491: | } |
492: | |
493: | if (count($newTypes) > 0) { |
494: | return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null); |
495: | } |
496: | } |
497: | } |
498: | |
499: | $tip = null; |
500: | if (str_contains($type->describe(VerbosityLevel::typeOnly()), 'PhpParser\\Node\\Arg|PhpParser\\Node\\VariadicPlaceholder') && !$unionTypeCriteriaCallback($type)) { |
501: | $tip = 'Use <fg=cyan>->getArgs()</> instead of <fg=cyan>->args</>.'; |
502: | } |
503: | |
504: | return new FoundTypeResult($type, $directClassNames, [], $tip); |
505: | } |
506: | |
507: | } |
508: | |