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: /** @api */
48: public function isThis(Expr $expression): bool
49: {
50: return $expression instanceof Expr\Variable && $expression->name === 'this';
51: }
52:
53: /** @api */
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: * @return array{Type, bool}
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: * @api
305: * @param callable(Type $type): bool $unionTypeCriteriaCallback
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: /** @param callable(Type $type): bool $unionTypeCriteriaCallback */
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: