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