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