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: | ); |
126: | } |
127: | |
128: | if ( |
129: | !$this->checkNullables |
130: | && !$acceptingType instanceof NullType |
131: | && !$acceptedType instanceof NullType |
132: | && !$acceptedType instanceof BenevolentUnionType |
133: | ) { |
134: | return $traverse(TypeCombinator::removeNull($acceptedType)); |
135: | } |
136: | |
137: | if ($this->checkBenevolentUnionTypes) { |
138: | if ($acceptedType instanceof BenevolentUnionType) { |
139: | $checkForUnion = true; |
140: | return $traverse(TypeUtils::toStrictUnion($acceptedType)); |
141: | } |
142: | } |
143: | |
144: | return $traverse($this->transformCommonType($acceptedType)); |
145: | }); |
146: | |
147: | return [$acceptedType, $checkForUnion]; |
148: | } |
149: | |
150: | |
151: | public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTypes): RuleLevelHelperAcceptsResult |
152: | { |
153: | [$acceptedType, $checkForUnion] = $this->transformAcceptedType($acceptingType, $acceptedType); |
154: | $acceptingType = $this->transformCommonType($acceptingType); |
155: | |
156: | $accepts = $acceptingType->accepts($acceptedType, $strictTypes); |
157: | |
158: | return new RuleLevelHelperAcceptsResult( |
159: | $checkForUnion ? $accepts->yes() : !$accepts->no(), |
160: | $accepts->reasons, |
161: | ); |
162: | } |
163: | |
164: | |
165: | |
166: | |
167: | |
168: | public function findTypeToCheck( |
169: | Scope $scope, |
170: | Expr $var, |
171: | string $unknownClassErrorPattern, |
172: | callable $unionTypeCriteriaCallback, |
173: | ): FoundTypeResult |
174: | { |
175: | if ($this->checkThisOnly && !$this->isThis($var)) { |
176: | return new FoundTypeResult(new ErrorType(), [], [], null); |
177: | } |
178: | $type = $scope->getType($var); |
179: | |
180: | return $this->findTypeToCheckImplementation($scope, $var, $type, $unknownClassErrorPattern, $unionTypeCriteriaCallback, true); |
181: | } |
182: | |
183: | |
184: | private function findTypeToCheckImplementation( |
185: | Scope $scope, |
186: | Expr $var, |
187: | Type $type, |
188: | string $unknownClassErrorPattern, |
189: | callable $unionTypeCriteriaCallback, |
190: | bool $isTopLevel = false, |
191: | ): FoundTypeResult |
192: | { |
193: | if (!$this->checkNullables && !$type->isNull()->yes()) { |
194: | $type = TypeCombinator::removeNull($type); |
195: | } |
196: | |
197: | if ( |
198: | ($this->checkExplicitMixed || $this->checkImplicitMixed) |
199: | && $type instanceof MixedType |
200: | && ($type->isExplicitMixed() ? $this->checkExplicitMixed : $this->checkImplicitMixed) |
201: | ) { |
202: | return new FoundTypeResult( |
203: | $type instanceof TemplateMixedType |
204: | ? $type->toStrictMixedType() |
205: | : new StrictMixedType(), |
206: | [], |
207: | [], |
208: | null, |
209: | ); |
210: | } |
211: | |
212: | if ($type instanceof MixedType || $type instanceof NeverType) { |
213: | return new FoundTypeResult(new ErrorType(), [], [], null); |
214: | } |
215: | |
216: | $errors = []; |
217: | $hasClassExistsClass = false; |
218: | $directClassNames = []; |
219: | |
220: | if ($isTopLevel) { |
221: | $directClassNames = $type->getObjectClassNames(); |
222: | foreach ($directClassNames as $referencedClass) { |
223: | if ($this->reflectionProvider->hasClass($referencedClass)) { |
224: | $classReflection = $this->reflectionProvider->getClass($referencedClass); |
225: | if (!$classReflection->isTrait()) { |
226: | continue; |
227: | } |
228: | } |
229: | |
230: | if ($scope->isInClassExists($referencedClass)) { |
231: | $hasClassExistsClass = true; |
232: | continue; |
233: | } |
234: | |
235: | $errorBuilder = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass)) |
236: | ->line($var->getStartLine()) |
237: | ->identifier('class.notFound'); |
238: | |
239: | if ($this->discoveringSymbolsTip) { |
240: | $errorBuilder->discoveringSymbolsTip(); |
241: | } |
242: | |
243: | $errors[] = $errorBuilder->build(); |
244: | } |
245: | } |
246: | |
247: | if (count($errors) > 0 || $hasClassExistsClass) { |
248: | return new FoundTypeResult(new ErrorType(), [], $errors, null); |
249: | } |
250: | |
251: | if (!$this->checkUnionTypes && $type->isObject()->yes() && count($type->getObjectClassNames()) === 0) { |
252: | return new FoundTypeResult(new ErrorType(), [], [], null); |
253: | } |
254: | |
255: | if ($type instanceof UnionType) { |
256: | $shouldFilterUnion = ( |
257: | !$this->checkUnionTypes |
258: | && !$type instanceof BenevolentUnionType |
259: | ) || ( |
260: | !$this->checkBenevolentUnionTypes |
261: | && $type instanceof BenevolentUnionType |
262: | ); |
263: | |
264: | $newTypes = []; |
265: | |
266: | foreach ($type->getTypes() as $innerType) { |
267: | if ($shouldFilterUnion && !$unionTypeCriteriaCallback($innerType)) { |
268: | continue; |
269: | } |
270: | |
271: | $newTypes[] = $this->findTypeToCheckImplementation( |
272: | $scope, |
273: | $var, |
274: | $innerType, |
275: | $unknownClassErrorPattern, |
276: | $unionTypeCriteriaCallback, |
277: | )->getType(); |
278: | } |
279: | |
280: | if (count($newTypes) > 0) { |
281: | $newUnion = TypeCombinator::union(...$newTypes); |
282: | if ( |
283: | !$this->checkBenevolentUnionTypes |
284: | && $type instanceof BenevolentUnionType |
285: | ) { |
286: | $newUnion = TypeUtils::toBenevolentUnion($newUnion); |
287: | } |
288: | |
289: | return new FoundTypeResult($newUnion, $directClassNames, [], null); |
290: | } |
291: | } |
292: | |
293: | if ($type instanceof IntersectionType) { |
294: | $newTypes = []; |
295: | |
296: | $changed = false; |
297: | foreach ($type->getTypes() as $innerType) { |
298: | if ($innerType instanceof TemplateMixedType) { |
299: | $changed = true; |
300: | $newTypes[] = $this->findTypeToCheckImplementation( |
301: | $scope, |
302: | $var, |
303: | $innerType->toStrictMixedType(), |
304: | $unknownClassErrorPattern, |
305: | $unionTypeCriteriaCallback, |
306: | )->getType(); |
307: | continue; |
308: | } |
309: | $newTypes[] = $innerType; |
310: | } |
311: | |
312: | if ($changed) { |
313: | return new FoundTypeResult(TypeCombinator::intersect(...$newTypes), $directClassNames, [], null); |
314: | } |
315: | } |
316: | |
317: | $tip = null; |
318: | if ( |
319: | $type instanceof UnionType |
320: | && count($type->getTypes()) === 2 |
321: | && $type->isObject()->yes() |
322: | && $type->getTypes()[0]->getObjectClassNames() === ['PhpParser\\Node\\Arg'] |
323: | && $type->getTypes()[1]->getObjectClassNames() === ['PhpParser\\Node\\VariadicPlaceholder'] |
324: | && !$unionTypeCriteriaCallback($type) |
325: | ) { |
326: | $tip = 'Use <fg=cyan>->getArgs()</> instead of <fg=cyan>->args</>.'; |
327: | } |
328: | |
329: | return new FoundTypeResult($type, $directClassNames, [], $tip); |
330: | } |
331: | |
332: | } |
333: | |