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\ErrorType;
10: use PHPStan\Type\Generic\TemplateMixedType;
11: use PHPStan\Type\MixedType;
12: use PHPStan\Type\NeverType;
13: use PHPStan\Type\NullType;
14: use PHPStan\Type\ObjectWithoutClassType;
15: use PHPStan\Type\StaticType;
16: use PHPStan\Type\StrictMixedType;
17: use PHPStan\Type\Type;
18: use PHPStan\Type\TypeCombinator;
19: use PHPStan\Type\TypeTraverser;
20: use PHPStan\Type\TypeUtils;
21: use PHPStan\Type\UnionType;
22: use PHPStan\Type\VerbosityLevel;
23: use function count;
24: use function sprintf;
25: use function strpos;
26:
27: 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 $checkListType,
38: private bool $checkBenevolentUnionTypes,
39: )
40: {
41: }
42:
43: /** @api */
44: public function isThis(Expr $expression): bool
45: {
46: return $expression instanceof Expr\Variable && $expression->name === 'this';
47: }
48:
49: /** @api */
50: public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTypes): bool
51: {
52: $checkForUnion = $this->checkUnionTypes;
53:
54: if ($this->checkBenevolentUnionTypes) {
55: $traverse = static function (Type $type, callable $traverse) use (&$checkForUnion): Type {
56: if ($type instanceof BenevolentUnionType) {
57: $checkForUnion = true;
58: return new UnionType($type->getTypes());
59: }
60:
61: return $traverse($type);
62: };
63:
64: $acceptedType = TypeTraverser::map($acceptedType, $traverse);
65: }
66:
67: if (
68: $this->checkExplicitMixed
69: ) {
70: $traverse = static function (Type $type, callable $traverse): Type {
71: if ($type instanceof TemplateMixedType) {
72: return $type->toStrictMixedType();
73: }
74: if (
75: $type instanceof MixedType
76: && $type->isExplicitMixed()
77: ) {
78: return new StrictMixedType();
79: }
80:
81: return $traverse($type);
82: };
83: $acceptingType = TypeTraverser::map($acceptingType, $traverse);
84: $acceptedType = TypeTraverser::map($acceptedType, $traverse);
85: }
86:
87: if (
88: $this->checkImplicitMixed
89: ) {
90: $traverse = static function (Type $type, callable $traverse): Type {
91: if ($type instanceof TemplateMixedType) {
92: return $type->toStrictMixedType();
93: }
94: if (
95: $type instanceof MixedType
96: && !$type->isExplicitMixed()
97: ) {
98: return new StrictMixedType();
99: }
100:
101: return $traverse($type);
102: };
103: $acceptingType = TypeTraverser::map($acceptingType, $traverse);
104: $acceptedType = TypeTraverser::map($acceptedType, $traverse);
105: }
106:
107: if (
108: !$this->checkNullables
109: && !$acceptingType instanceof NullType
110: && !$acceptedType instanceof NullType
111: && !$acceptedType instanceof BenevolentUnionType
112: ) {
113: $acceptedType = TypeCombinator::removeNull($acceptedType);
114: }
115:
116: $accepts = $acceptingType->accepts($acceptedType, $strictTypes);
117: if ($accepts->yes()) {
118: return true;
119: }
120: if ($acceptingType instanceof UnionType) {
121: foreach ($acceptingType->getTypes() as $innerType) {
122: if (self::accepts($innerType, $acceptedType, $strictTypes)) {
123: return true;
124: }
125: }
126:
127: return false;
128: }
129:
130: if (
131: $acceptedType->isArray()->yes()
132: && $acceptingType->isArray()->yes()
133: && (
134: $acceptedType->isConstantArray()->no()
135: || !$acceptedType->isIterableAtLeastOnce()->no()
136: )
137: && $acceptingType->isConstantArray()->no()
138: ) {
139: return (
140: !$acceptingType->isIterableAtLeastOnce()->yes()
141: || $acceptedType->isIterableAtLeastOnce()->yes()
142: ) && (
143: !$this->checkListType
144: || !$acceptingType->isList()->yes()
145: || $acceptedType->isList()->yes()
146: ) && self::accepts(
147: $acceptingType->getIterableKeyType(),
148: $acceptedType->getIterableKeyType(),
149: $strictTypes,
150: ) && self::accepts(
151: $acceptingType->getIterableValueType(),
152: $acceptedType->getIterableValueType(),
153: $strictTypes,
154: );
155: }
156:
157: return $checkForUnion ? $accepts->yes() : !$accepts->no();
158: }
159:
160: /**
161: * @api
162: * @param callable(Type $type): bool $unionTypeCriteriaCallback
163: */
164: public function findTypeToCheck(
165: Scope $scope,
166: Expr $var,
167: string $unknownClassErrorPattern,
168: callable $unionTypeCriteriaCallback,
169: ): FoundTypeResult
170: {
171: if ($this->checkThisOnly && !$this->isThis($var)) {
172: return new FoundTypeResult(new ErrorType(), [], [], null);
173: }
174: $type = $scope->getType($var);
175: if (!$this->checkNullables && !$type->isNull()->yes()) {
176: $type = TypeCombinator::removeNull($type);
177: }
178:
179: if (
180: $this->checkExplicitMixed
181: && $type instanceof MixedType
182: && !$type instanceof TemplateMixedType
183: && $type->isExplicitMixed()
184: ) {
185: return new FoundTypeResult(new StrictMixedType(), [], [], null);
186: }
187:
188: if (
189: $this->checkImplicitMixed
190: && $type instanceof MixedType
191: && !$type instanceof TemplateMixedType
192: && !$type->isExplicitMixed()
193: ) {
194: return new FoundTypeResult(new StrictMixedType(), [], [], null);
195: }
196:
197: if ($type instanceof MixedType || $type instanceof NeverType) {
198: return new FoundTypeResult(new ErrorType(), [], [], null);
199: }
200: if ($type instanceof StaticType) {
201: $type = $type->getStaticObjectType();
202: }
203:
204: $errors = [];
205: $directClassNames = TypeUtils::getDirectClassNames($type);
206: $hasClassExistsClass = false;
207: foreach ($directClassNames as $referencedClass) {
208: if ($this->reflectionProvider->hasClass($referencedClass)) {
209: $classReflection = $this->reflectionProvider->getClass($referencedClass);
210: if (!$classReflection->isTrait()) {
211: continue;
212: }
213: }
214:
215: if ($scope->isInClassExists($referencedClass)) {
216: $hasClassExistsClass = true;
217: continue;
218: }
219:
220: $errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass))->line($var->getLine())->discoveringSymbolsTip()->build();
221: }
222:
223: if (count($errors) > 0 || $hasClassExistsClass) {
224: return new FoundTypeResult(new ErrorType(), [], $errors, null);
225: }
226:
227: if (!$this->checkUnionTypes && $type instanceof ObjectWithoutClassType) {
228: return new FoundTypeResult(new ErrorType(), [], [], null);
229: }
230:
231: if (
232: (
233: !$this->checkUnionTypes
234: && $type instanceof UnionType
235: && !$type instanceof BenevolentUnionType
236: ) || (
237: !$this->checkBenevolentUnionTypes
238: && $type instanceof BenevolentUnionType
239: )
240: ) {
241: $newTypes = [];
242:
243: foreach ($type->getTypes() as $innerType) {
244: if (!$unionTypeCriteriaCallback($innerType)) {
245: continue;
246: }
247:
248: $newTypes[] = $innerType;
249: }
250:
251: if (count($newTypes) > 0) {
252: return new FoundTypeResult(TypeCombinator::union(...$newTypes), $directClassNames, [], null);
253: }
254: }
255:
256: $tip = null;
257: if (strpos($type->describe(VerbosityLevel::typeOnly()), 'PhpParser\\Node\\Arg|PhpParser\\Node\\VariadicPlaceholder') !== false && !$unionTypeCriteriaCallback($type)) {
258: $tip = 'Use <fg=cyan>->getArgs()</> instead of <fg=cyan>->args</>.';
259: }
260:
261: return new FoundTypeResult($type, $directClassNames, [], $tip);
262: }
263:
264: }
265: