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