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