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: /** @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: $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: * @api
308: * @param callable(Type $type): bool $unionTypeCriteriaCallback
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: /** @param callable(Type $type): bool $unionTypeCriteriaCallback */
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: