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\MixedType; |
14: | use PHPStan\Type\NeverType; |
15: | use PHPStan\Type\NullType; |
16: | use PHPStan\Type\ObjectWithoutClassType; |
17: | use PHPStan\Type\StaticType; |
18: | use PHPStan\Type\StrictMixedType; |
19: | use PHPStan\Type\Type; |
20: | use PHPStan\Type\TypeCombinator; |
21: | use PHPStan\Type\TypeTraverser; |
22: | use PHPStan\Type\UnionType; |
23: | use PHPStan\Type\VerbosityLevel; |
24: | use function array_merge; |
25: | use function count; |
26: | use function sprintf; |
27: | use function strpos; |
28: | |
29: | class RuleLevelHelper |
30: | { |
31: | |
32: | public function __construct( |
33: | private ReflectionProvider $reflectionProvider, |
34: | private bool $checkNullables, |
35: | private bool $checkThisOnly, |
36: | private bool $checkUnionTypes, |
37: | private bool $checkExplicitMixed, |
38: | private bool $checkImplicitMixed, |
39: | private bool $newRuleLevelHelper, |
40: | private bool $checkBenevolentUnionTypes, |
41: | ) |
42: | { |
43: | } |
44: | |
45: | |
46: | public function isThis(Expr $expression): bool |
47: | { |
48: | return $expression instanceof Expr\Variable && $expression->name === 'this'; |
49: | } |
50: | |
51: | |
52: | public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTypes): bool |
53: | { |
54: | return $this->acceptsWithReason($acceptingType, $acceptedType, $strictTypes)->result; |
55: | } |
56: | |
57: | private function transformCommonType(Type $type): Type |
58: | { |
59: | if (!$this->checkExplicitMixed && !$this->checkImplicitMixed) { |
60: | return $type; |
61: | } |
62: | |
63: | return TypeTraverser::map($type, function (Type $type, callable $traverse) { |
64: | if ($type instanceof TemplateMixedType) { |
65: | return $type->toStrictMixedType(); |
66: | } |
67: | if ( |
68: | $type instanceof MixedType |
69: | && ( |
70: | ($type->isExplicitMixed() && $this->checkExplicitMixed) |
71: | || (!$type->isExplicitMixed() && $this->checkImplicitMixed) |
72: | ) |
73: | ) { |
74: | return new StrictMixedType(); |
75: | } |
76: | |
77: | return $traverse($type); |
78: | }); |
79: | } |
80: | |
81: | |
82: | |
83: | |
84: | private function transformAcceptedType(Type $acceptingType, Type $acceptedType): array |
85: | { |
86: | $checkForUnion = $this->checkUnionTypes; |
87: | $acceptedType = TypeTraverser::map($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type { |
88: | if ($acceptedType instanceof CallableType) { |
89: | if ($acceptedType->isCommonCallable()) { |
90: | return new CallableType(null, null, $acceptedType->isVariadic()); |
91: | } |
92: | |
93: | return new CallableType( |
94: | $acceptedType->getParameters(), |
95: | $traverse($this->transformCommonType($acceptedType->getReturnType())), |
96: | $acceptedType->isVariadic(), |
97: | ); |
98: | } |
99: | |
100: | if ($acceptedType instanceof ClosureType) { |
101: | return new ClosureType( |
102: | $acceptedType->getParameters(), |
103: | $traverse($this->transformCommonType($acceptedType->getReturnType())), |
104: | $acceptedType->isVariadic(), |
105: | $acceptedType->getTemplateTypeMap(), |
106: | $acceptedType->getResolvedTemplateTypeMap(), |
107: | ); |
108: | } |
109: | |
110: | if ( |
111: | !$this->checkNullables |
112: | && !$acceptingType instanceof NullType |
113: | && !$acceptedType instanceof NullType |
114: | && !$acceptedType instanceof BenevolentUnionType |
115: | ) { |
116: | return $traverse(TypeCombinator::removeNull($acceptedType)); |
117: | } |
118: | |
119: | if ($this->checkBenevolentUnionTypes) { |
120: | if ($acceptedType instanceof BenevolentUnionType) { |
121: | $checkForUnion = true; |
122: | return $traverse(new UnionType($acceptedType->getTypes())); |
123: | } |
124: | } |
125: | |
126: | return $traverse($this->transformCommonType($acceptedType)); |
127: | }); |
128: | |
129: | return [$acceptedType, $checkForUnion]; |
130: | } |
131: | |
132: | public function acceptsWithReason(Type $acceptingType, Type $acceptedType, bool $strictTypes): RuleLevelHelperAcceptsResult |
133: | { |
134: | if ($this->newRuleLevelHelper) { |
135: | [$acceptedType, $checkForUnion] = $this->transformAcceptedType($acceptingType, $acceptedType); |
136: | $acceptingType = $this->transformCommonType($acceptingType); |
137: | |
138: | $accepts = $acceptingType->acceptsWithReason($acceptedType, $strictTypes); |
139: | |
140: | return new RuleLevelHelperAcceptsResult( |
141: | $checkForUnion ? $accepts->yes() : !$accepts->no(), |
142: | $accepts->reasons, |
143: | ); |
144: | } |
145: | |
146: | $checkForUnion = $this->checkUnionTypes; |
147: | |
148: | if ($this->checkBenevolentUnionTypes) { |
149: | $traverse = static function (Type $type, callable $traverse) use (&$checkForUnion): Type { |
150: | if ($type instanceof BenevolentUnionType) { |
151: | $checkForUnion = true; |
152: | return new UnionType($type->getTypes()); |
153: | } |
154: | |
155: | return $traverse($type); |
156: | }; |
157: | |
158: | $acceptedType = TypeTraverser::map($acceptedType, $traverse); |
159: | } |
160: | |
161: | if ( |
162: | $this->checkExplicitMixed |
163: | ) { |
164: | $traverse = static function (Type $type, callable $traverse): Type { |
165: | if ($type instanceof TemplateMixedType) { |
166: | return $type->toStrictMixedType(); |
167: | } |
168: | if ( |
169: | $type instanceof MixedType |
170: | && $type->isExplicitMixed() |
171: | ) { |
172: | return new StrictMixedType(); |
173: | } |
174: | |
175: | return $traverse($type); |
176: | }; |
177: | $acceptingType = TypeTraverser::map($acceptingType, $traverse); |
178: | $acceptedType = TypeTraverser::map($acceptedType, $traverse); |
179: | } |
180: | |
181: | if ( |
182: | $this->checkImplicitMixed |
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->checkNullables |
203: | && !$acceptingType instanceof NullType |
204: | && !$acceptedType instanceof NullType |
205: | && !$acceptedType instanceof BenevolentUnionType |
206: | ) { |
207: | $acceptedType = TypeCombinator::removeNull($acceptedType); |
208: | } |
209: | |
210: | $accepts = $acceptingType->acceptsWithReason($acceptedType, $strictTypes); |
211: | if ($accepts->yes()) { |
212: | return new RuleLevelHelperAcceptsResult(true, $accepts->reasons); |
213: | } |
214: | if ($acceptingType instanceof UnionType) { |
215: | $reasons = []; |
216: | foreach ($acceptingType->getTypes() as $innerType) { |
217: | $accepts = self::acceptsWithReason($innerType, $acceptedType, $strictTypes); |
218: | if ($accepts->result) { |
219: | return $accepts; |
220: | } |
221: | |
222: | $reasons = array_merge($reasons, $accepts->reasons); |
223: | } |
224: | |
225: | return new RuleLevelHelperAcceptsResult(false, $reasons); |
226: | } |
227: | |
228: | if ( |
229: | $acceptedType->isArray()->yes() |
230: | && $acceptingType->isArray()->yes() |
231: | && ( |
232: | $acceptedType->isConstantArray()->no() |
233: | || !$acceptedType->isIterableAtLeastOnce()->no() |
234: | ) |
235: | && $acceptingType->isConstantArray()->no() |
236: | ) { |
237: | if ($acceptingType->isIterableAtLeastOnce()->yes() && !$acceptedType->isIterableAtLeastOnce()->yes()) { |
238: | $verbosity = VerbosityLevel::getRecommendedLevelByType($acceptingType, $acceptedType); |
239: | return new RuleLevelHelperAcceptsResult(false, [ |
240: | sprintf( |
241: | '%s %s empty.', |
242: | $acceptedType->describe($verbosity), |
243: | $acceptedType->isIterableAtLeastOnce()->no() ? 'is' : 'might be', |
244: | ), |
245: | ]); |
246: | } |
247: | |
248: | if ( |
249: | $acceptingType->isList()->yes() |
250: | && !$acceptedType->isList()->yes() |
251: | ) { |
252: | $report = $checkForUnion || $acceptedType->isList()->no(); |
253: | |
254: | if ($report) { |
255: | $verbosity = VerbosityLevel::getRecommendedLevelByType($acceptingType, $acceptedType); |
256: | return new RuleLevelHelperAcceptsResult(false, [ |
257: | sprintf( |
258: | '%s %s a list.', |
259: | $acceptedType->describe($verbosity), |
260: | $acceptedType->isList()->no() ? 'is not' : 'might not be', |
261: | ), |
262: | ]); |
263: | } |
264: | } |
265: | |
266: | return self::acceptsWithReason( |
267: | $acceptingType->getIterableKeyType(), |
268: | $acceptedType->getIterableKeyType(), |
269: | $strictTypes, |
270: | )->and(self::acceptsWithReason( |
271: | $acceptingType->getIterableValueType(), |
272: | $acceptedType->getIterableValueType(), |
273: | $strictTypes, |
274: | )); |
275: | } |
276: | |
277: | return new RuleLevelHelperAcceptsResult( |
278: | $checkForUnion ? $accepts->yes() : !$accepts->no(), |
279: | $accepts->reasons, |
280: | ); |
281: | } |
282: | |
283: | |
284: | |
285: | |
286: | |
287: | public function findTypeToCheck( |
288: | Scope $scope, |
289: | Expr $var, |
290: | string $unknownClassErrorPattern, |
291: | callable $unionTypeCriteriaCallback, |
292: | ): FoundTypeResult |
293: | { |
294: | if ($this->checkThisOnly && !$this->isThis($var)) { |
295: | return new FoundTypeResult(new ErrorType(), [], [], null); |
296: | } |
297: | $type = $scope->getType($var); |
298: | if (!$this->checkNullables && !$type->isNull()->yes()) { |
299: | $type = TypeCombinator::removeNull($type); |
300: | } |
301: | |
302: | if ( |
303: | $this->checkExplicitMixed |
304: | && $type instanceof MixedType |
305: | && !$type instanceof TemplateMixedType |
306: | && $type->isExplicitMixed() |
307: | ) { |
308: | return new FoundTypeResult(new StrictMixedType(), [], [], null); |
309: | } |
310: | |
311: | if ( |
312: | $this->checkImplicitMixed |
313: | && $type instanceof MixedType |
314: | && !$type instanceof TemplateMixedType |
315: | && !$type->isExplicitMixed() |
316: | ) { |
317: | return new FoundTypeResult(new StrictMixedType(), [], [], null); |
318: | } |
319: | |
320: | if ($type instanceof MixedType || $type instanceof NeverType) { |
321: | return new FoundTypeResult(new ErrorType(), [], [], null); |
322: | } |
323: | if ($type instanceof StaticType) { |
324: | $type = $type->getStaticObjectType(); |
325: | } |
326: | |
327: | $errors = []; |
328: | $directClassNames = $type->getObjectClassNames(); |
329: | $hasClassExistsClass = false; |
330: | foreach ($directClassNames as $referencedClass) { |
331: | if ($this->reflectionProvider->hasClass($referencedClass)) { |
332: | $classReflection = $this->reflectionProvider->getClass($referencedClass); |
333: | if (!$classReflection->isTrait()) { |
334: | continue; |
335: | } |
336: | } |
337: | |
338: | if ($scope->isInClassExists($referencedClass)) { |
339: | $hasClassExistsClass = true; |
340: | continue; |
341: | } |
342: | |
343: | $errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass))->line($var->getLine())->discoveringSymbolsTip()->build(); |
344: | } |
345: | |
346: | if (count($errors) > 0 || $hasClassExistsClass) { |
347: | return new FoundTypeResult(new ErrorType(), [], $errors, null); |
348: | } |
349: | |
350: | if (!$this->checkUnionTypes && $type instanceof ObjectWithoutClassType) { |
351: | return new FoundTypeResult(new ErrorType(), [], [], null); |
352: | } |
353: | |
354: | if ( |
355: | ( |
356: | !$this->checkUnionTypes |
357: | && $type instanceof UnionType |
358: | && !$type instanceof BenevolentUnionType |
359: | ) || ( |
360: | !$this->checkBenevolentUnionTypes |
361: | && $type instanceof BenevolentUnionType |
362: | ) |
363: | ) { |
364: | $newTypes = []; |
365: | |
366: | foreach ($type->getTypes() as $innerType) { |
367: | if (!$unionTypeCriteriaCallback($innerType)) { |
368: | continue; |
369: | } |
370: | |
371: | $newTypes[] = $innerType; |
372: | } |
373: | |
374: | if (count($newTypes) > 0) { |
375: | return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null); |
376: | } |
377: | } |
378: | |
379: | $tip = null; |
380: | if (strpos($type->describe(VerbosityLevel::typeOnly()), 'PhpParser\\Node\\Arg|PhpParser\\Node\\VariadicPlaceholder') !== false && !$unionTypeCriteriaCallback($type)) { |
381: | $tip = 'Use <fg=cyan>->getArgs()</> instead of <fg=cyan>->args</>.'; |
382: | } |
383: | |
384: | return new FoundTypeResult($type, $directClassNames, [], $tip); |
385: | } |
386: | |
387: | } |
388: | |