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