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