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\StrictMixedType; |
18: | use PHPStan\Type\Type; |
19: | use PHPStan\Type\TypeCombinator; |
20: | use PHPStan\Type\TypeTraverser; |
21: | use PHPStan\Type\TypeUtils; |
22: | use PHPStan\Type\UnionType; |
23: | use PHPStan\Type\VerbosityLevel; |
24: | use function count; |
25: | use function sprintf; |
26: | use function str_contains; |
27: | |
28: | final class RuleLevelHelper |
29: | { |
30: | |
31: | public function __construct( |
32: | private ReflectionProvider $reflectionProvider, |
33: | private bool $checkNullables, |
34: | private bool $checkThisOnly, |
35: | private bool $checkUnionTypes, |
36: | private bool $checkExplicitMixed, |
37: | private bool $checkImplicitMixed, |
38: | private bool $checkBenevolentUnionTypes, |
39: | ) |
40: | { |
41: | } |
42: | |
43: | |
44: | public function isThis(Expr $expression): bool |
45: | { |
46: | return $expression instanceof Expr\Variable && $expression->name === 'this'; |
47: | } |
48: | |
49: | private function transformCommonType(Type $type): Type |
50: | { |
51: | if (!$this->checkExplicitMixed && !$this->checkImplicitMixed) { |
52: | return $type; |
53: | } |
54: | |
55: | return TypeTraverser::map($type, function (Type $type, callable $traverse) { |
56: | if ($type instanceof TemplateMixedType) { |
57: | if ($this->checkExplicitMixed) { |
58: | return $type->toStrictMixedType(); |
59: | } |
60: | } |
61: | if ( |
62: | $type instanceof MixedType |
63: | && ( |
64: | ($type->isExplicitMixed() && $this->checkExplicitMixed) |
65: | || (!$type->isExplicitMixed() && $this->checkImplicitMixed) |
66: | ) |
67: | ) { |
68: | return new StrictMixedType(); |
69: | } |
70: | |
71: | return $traverse($type); |
72: | }); |
73: | } |
74: | |
75: | |
76: | |
77: | |
78: | private function transformAcceptedType(Type $acceptingType, Type $acceptedType): array |
79: | { |
80: | $checkForUnion = $this->checkUnionTypes; |
81: | $acceptedType = TypeTraverser::map($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type { |
82: | if ($acceptedType instanceof CallableType) { |
83: | if ($acceptedType->isCommonCallable()) { |
84: | return $acceptedType; |
85: | } |
86: | |
87: | return new CallableType( |
88: | $acceptedType->getParameters(), |
89: | $traverse($this->transformCommonType($acceptedType->getReturnType())), |
90: | $acceptedType->isVariadic(), |
91: | $acceptedType->getTemplateTypeMap(), |
92: | $acceptedType->getResolvedTemplateTypeMap(), |
93: | $acceptedType->getTemplateTags(), |
94: | $acceptedType->isPure(), |
95: | ); |
96: | } |
97: | |
98: | if ($acceptedType instanceof ClosureType) { |
99: | if ($acceptedType->isCommonCallable()) { |
100: | return $acceptedType; |
101: | } |
102: | |
103: | return new ClosureType( |
104: | $acceptedType->getParameters(), |
105: | $traverse($this->transformCommonType($acceptedType->getReturnType())), |
106: | $acceptedType->isVariadic(), |
107: | $acceptedType->getTemplateTypeMap(), |
108: | $acceptedType->getResolvedTemplateTypeMap(), |
109: | $acceptedType->getCallSiteVarianceMap(), |
110: | $acceptedType->getTemplateTags(), |
111: | $acceptedType->getThrowPoints(), |
112: | $acceptedType->getImpurePoints(), |
113: | $acceptedType->getInvalidateExpressions(), |
114: | $acceptedType->getUsedVariables(), |
115: | $acceptedType->acceptsNamedArguments(), |
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: | |
142: | public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTypes): RuleLevelHelperAcceptsResult |
143: | { |
144: | [$acceptedType, $checkForUnion] = $this->transformAcceptedType($acceptingType, $acceptedType); |
145: | $acceptingType = $this->transformCommonType($acceptingType); |
146: | |
147: | $accepts = $acceptingType->accepts($acceptedType, $strictTypes); |
148: | |
149: | return new RuleLevelHelperAcceptsResult( |
150: | $checkForUnion ? $accepts->yes() : !$accepts->no(), |
151: | $accepts->reasons, |
152: | ); |
153: | } |
154: | |
155: | |
156: | |
157: | |
158: | |
159: | public function findTypeToCheck( |
160: | Scope $scope, |
161: | Expr $var, |
162: | string $unknownClassErrorPattern, |
163: | callable $unionTypeCriteriaCallback, |
164: | ): FoundTypeResult |
165: | { |
166: | if ($this->checkThisOnly && !$this->isThis($var)) { |
167: | return new FoundTypeResult(new ErrorType(), [], [], null); |
168: | } |
169: | $type = $scope->getType($var); |
170: | |
171: | return $this->findTypeToCheckImplementation($scope, $var, $type, $unknownClassErrorPattern, $unionTypeCriteriaCallback, true); |
172: | } |
173: | |
174: | |
175: | private function findTypeToCheckImplementation( |
176: | Scope $scope, |
177: | Expr $var, |
178: | Type $type, |
179: | string $unknownClassErrorPattern, |
180: | callable $unionTypeCriteriaCallback, |
181: | bool $isTopLevel = false, |
182: | ): FoundTypeResult |
183: | { |
184: | if (!$this->checkNullables && !$type->isNull()->yes()) { |
185: | $type = TypeCombinator::removeNull($type); |
186: | } |
187: | |
188: | if ( |
189: | ($this->checkExplicitMixed || $this->checkImplicitMixed) |
190: | && $type instanceof MixedType |
191: | && ($type->isExplicitMixed() ? $this->checkExplicitMixed : $this->checkImplicitMixed) |
192: | ) { |
193: | return new FoundTypeResult( |
194: | $type instanceof TemplateMixedType |
195: | ? $type->toStrictMixedType() |
196: | : new StrictMixedType(), |
197: | [], |
198: | [], |
199: | null, |
200: | ); |
201: | } |
202: | |
203: | if ($type instanceof MixedType || $type instanceof NeverType) { |
204: | return new FoundTypeResult(new ErrorType(), [], [], null); |
205: | } |
206: | |
207: | $errors = []; |
208: | $hasClassExistsClass = false; |
209: | $directClassNames = []; |
210: | |
211: | if ($isTopLevel) { |
212: | $directClassNames = $type->getObjectClassNames(); |
213: | foreach ($directClassNames as $referencedClass) { |
214: | if ($this->reflectionProvider->hasClass($referencedClass)) { |
215: | $classReflection = $this->reflectionProvider->getClass($referencedClass); |
216: | if (!$classReflection->isTrait()) { |
217: | continue; |
218: | } |
219: | } |
220: | |
221: | if ($scope->isInClassExists($referencedClass)) { |
222: | $hasClassExistsClass = true; |
223: | continue; |
224: | } |
225: | |
226: | $errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass)) |
227: | ->line($var->getStartLine()) |
228: | ->identifier('class.notFound') |
229: | ->discoveringSymbolsTip() |
230: | ->build(); |
231: | } |
232: | } |
233: | |
234: | if (count($errors) > 0 || $hasClassExistsClass) { |
235: | return new FoundTypeResult(new ErrorType(), [], $errors, null); |
236: | } |
237: | |
238: | if (!$this->checkUnionTypes && $type->isObject()->yes() && count($type->getObjectClassNames()) === 0) { |
239: | return new FoundTypeResult(new ErrorType(), [], [], null); |
240: | } |
241: | |
242: | if ($type instanceof UnionType) { |
243: | $shouldFilterUnion = ( |
244: | !$this->checkUnionTypes |
245: | && !$type instanceof BenevolentUnionType |
246: | ) || ( |
247: | !$this->checkBenevolentUnionTypes |
248: | && $type instanceof BenevolentUnionType |
249: | ); |
250: | |
251: | $newTypes = []; |
252: | |
253: | foreach ($type->getTypes() as $innerType) { |
254: | if ($shouldFilterUnion && !$unionTypeCriteriaCallback($innerType)) { |
255: | continue; |
256: | } |
257: | |
258: | $newTypes[] = $this->findTypeToCheckImplementation( |
259: | $scope, |
260: | $var, |
261: | $innerType, |
262: | $unknownClassErrorPattern, |
263: | $unionTypeCriteriaCallback, |
264: | )->getType(); |
265: | } |
266: | |
267: | if (count($newTypes) > 0) { |
268: | $newUnion = TypeCombinator::union(...$newTypes); |
269: | if ( |
270: | !$this->checkBenevolentUnionTypes |
271: | && $type instanceof BenevolentUnionType |
272: | ) { |
273: | $newUnion = TypeUtils::toBenevolentUnion($newUnion); |
274: | } |
275: | |
276: | return new FoundTypeResult($newUnion, $directClassNames, [], null); |
277: | } |
278: | } |
279: | |
280: | if ($type instanceof IntersectionType) { |
281: | $newTypes = []; |
282: | |
283: | $changed = false; |
284: | foreach ($type->getTypes() as $innerType) { |
285: | if ($innerType instanceof TemplateMixedType) { |
286: | $changed = true; |
287: | $newTypes[] = $this->findTypeToCheckImplementation( |
288: | $scope, |
289: | $var, |
290: | $innerType->toStrictMixedType(), |
291: | $unknownClassErrorPattern, |
292: | $unionTypeCriteriaCallback, |
293: | )->getType(); |
294: | continue; |
295: | } |
296: | $newTypes[] = $innerType; |
297: | } |
298: | |
299: | if ($changed) { |
300: | return new FoundTypeResult(TypeCombinator::intersect(...$newTypes), $directClassNames, [], null); |
301: | } |
302: | } |
303: | |
304: | $tip = null; |
305: | if (str_contains($type->describe(VerbosityLevel::typeOnly()), 'PhpParser\\Node\\Arg|PhpParser\\Node\\VariadicPlaceholder') && !$unionTypeCriteriaCallback($type)) { |
306: | $tip = 'Use <fg=cyan>->getArgs()</> instead of <fg=cyan>->args</>.'; |
307: | } |
308: | |
309: | return new FoundTypeResult($type, $directClassNames, [], $tip); |
310: | } |
311: | |
312: | } |
313: | |