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\ObjectType; |
18: | use PHPStan\Type\StrictMixedType; |
19: | use PHPStan\Type\Type; |
20: | use PHPStan\Type\TypeCombinator; |
21: | use PHPStan\Type\TypeTraverser; |
22: | use PHPStan\Type\TypeUtils; |
23: | use PHPStan\Type\UnionType; |
24: | use function count; |
25: | use function sprintf; |
26: | |
27: | final class RuleLevelHelper |
28: | { |
29: | |
30: | public function __construct( |
31: | private ReflectionProvider $reflectionProvider, |
32: | private bool $checkNullables, |
33: | private bool $checkThisOnly, |
34: | private bool $checkUnionTypes, |
35: | private bool $checkExplicitMixed, |
36: | private bool $checkImplicitMixed, |
37: | private bool $checkBenevolentUnionTypes, |
38: | private bool $discoveringSymbolsTip, |
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: | $errorBuilder = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass)) |
227: | ->line($var->getStartLine()) |
228: | ->identifier('class.notFound'); |
229: | |
230: | if ($this->discoveringSymbolsTip) { |
231: | $errorBuilder->discoveringSymbolsTip(); |
232: | } |
233: | |
234: | $errors[] = $errorBuilder->build(); |
235: | } |
236: | } |
237: | |
238: | if (count($errors) > 0 || $hasClassExistsClass) { |
239: | return new FoundTypeResult(new ErrorType(), [], $errors, null); |
240: | } |
241: | |
242: | if (!$this->checkUnionTypes && $type->isObject()->yes() && count($type->getObjectClassNames()) === 0) { |
243: | return new FoundTypeResult(new ErrorType(), [], [], null); |
244: | } |
245: | |
246: | if ($type instanceof UnionType) { |
247: | $shouldFilterUnion = ( |
248: | !$this->checkUnionTypes |
249: | && !$type instanceof BenevolentUnionType |
250: | ) || ( |
251: | !$this->checkBenevolentUnionTypes |
252: | && $type instanceof BenevolentUnionType |
253: | ); |
254: | |
255: | $newTypes = []; |
256: | |
257: | foreach ($type->getTypes() as $innerType) { |
258: | if ($shouldFilterUnion && !$unionTypeCriteriaCallback($innerType)) { |
259: | continue; |
260: | } |
261: | |
262: | $newTypes[] = $this->findTypeToCheckImplementation( |
263: | $scope, |
264: | $var, |
265: | $innerType, |
266: | $unknownClassErrorPattern, |
267: | $unionTypeCriteriaCallback, |
268: | )->getType(); |
269: | } |
270: | |
271: | if (count($newTypes) > 0) { |
272: | $newUnion = TypeCombinator::union(...$newTypes); |
273: | if ( |
274: | !$this->checkBenevolentUnionTypes |
275: | && $type instanceof BenevolentUnionType |
276: | ) { |
277: | $newUnion = TypeUtils::toBenevolentUnion($newUnion); |
278: | } |
279: | |
280: | return new FoundTypeResult($newUnion, $directClassNames, [], null); |
281: | } |
282: | } |
283: | |
284: | if ($type instanceof IntersectionType) { |
285: | $newTypes = []; |
286: | |
287: | $changed = false; |
288: | foreach ($type->getTypes() as $innerType) { |
289: | if ($innerType instanceof TemplateMixedType) { |
290: | $changed = true; |
291: | $newTypes[] = $this->findTypeToCheckImplementation( |
292: | $scope, |
293: | $var, |
294: | $innerType->toStrictMixedType(), |
295: | $unknownClassErrorPattern, |
296: | $unionTypeCriteriaCallback, |
297: | )->getType(); |
298: | continue; |
299: | } |
300: | $newTypes[] = $innerType; |
301: | } |
302: | |
303: | if ($changed) { |
304: | return new FoundTypeResult(TypeCombinator::intersect(...$newTypes), $directClassNames, [], null); |
305: | } |
306: | } |
307: | |
308: | $tip = null; |
309: | if ( |
310: | $type instanceof UnionType |
311: | && count($type->getTypes()) === 2 |
312: | && $type->getTypes()[0] instanceof ObjectType |
313: | && $type->getTypes()[1] instanceof ObjectType |
314: | && $type->getTypes()[0]->getClassName() === 'PhpParser\\Node\\Arg' |
315: | && $type->getTypes()[1]->getClassName() === 'PhpParser\\Node\\VariadicPlaceholder' |
316: | && !$unionTypeCriteriaCallback($type) |
317: | ) { |
318: | $tip = 'Use <fg=cyan>->getArgs()</> instead of <fg=cyan>->args</>.'; |
319: | } |
320: | |
321: | return new FoundTypeResult($type, $directClassNames, [], $tip); |
322: | } |
323: | |
324: | } |
325: | |