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\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 function count;
24: use function sprintf;
25:
26: final class RuleLevelHelper
27: {
28:
29: public function __construct(
30: private ReflectionProvider $reflectionProvider,
31: private bool $checkNullables,
32: private bool $checkThisOnly,
33: private bool $checkUnionTypes,
34: private bool $checkExplicitMixed,
35: private bool $checkImplicitMixed,
36: private bool $checkBenevolentUnionTypes,
37: private bool $discoveringSymbolsTip,
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: $errorBuilder = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass))
226: ->line($var->getStartLine())
227: ->identifier('class.notFound');
228:
229: if ($this->discoveringSymbolsTip) {
230: $errorBuilder->discoveringSymbolsTip();
231: }
232:
233: $errors[] = $errorBuilder->build();
234: }
235: }
236:
237: if (count($errors) > 0 || $hasClassExistsClass) {
238: return new FoundTypeResult(new ErrorType(), [], $errors, null);
239: }
240:
241: if (!$this->checkUnionTypes && $type->isObject()->yes() && count($type->getObjectClassNames()) === 0) {
242: return new FoundTypeResult(new ErrorType(), [], [], null);
243: }
244:
245: if ($type instanceof UnionType) {
246: $shouldFilterUnion = (
247: !$this->checkUnionTypes
248: && !$type instanceof BenevolentUnionType
249: ) || (
250: !$this->checkBenevolentUnionTypes
251: && $type instanceof BenevolentUnionType
252: );
253:
254: $newTypes = [];
255:
256: foreach ($type->getTypes() as $innerType) {
257: if ($shouldFilterUnion && !$unionTypeCriteriaCallback($innerType)) {
258: continue;
259: }
260:
261: $newTypes[] = $this->findTypeToCheckImplementation(
262: $scope,
263: $var,
264: $innerType,
265: $unknownClassErrorPattern,
266: $unionTypeCriteriaCallback,
267: )->getType();
268: }
269:
270: if (count($newTypes) > 0) {
271: $newUnion = TypeCombinator::union(...$newTypes);
272: if (
273: !$this->checkBenevolentUnionTypes
274: && $type instanceof BenevolentUnionType
275: ) {
276: $newUnion = TypeUtils::toBenevolentUnion($newUnion);
277: }
278:
279: return new FoundTypeResult($newUnion, $directClassNames, [], null);
280: }
281: }
282:
283: if ($type instanceof IntersectionType) {
284: $newTypes = [];
285:
286: $changed = false;
287: foreach ($type->getTypes() as $innerType) {
288: if ($innerType instanceof TemplateMixedType) {
289: $changed = true;
290: $newTypes[] = $this->findTypeToCheckImplementation(
291: $scope,
292: $var,
293: $innerType->toStrictMixedType(),
294: $unknownClassErrorPattern,
295: $unionTypeCriteriaCallback,
296: )->getType();
297: continue;
298: }
299: $newTypes[] = $innerType;
300: }
301:
302: if ($changed) {
303: return new FoundTypeResult(TypeCombinator::intersect(...$newTypes), $directClassNames, [], null);
304: }
305: }
306:
307: $tip = null;
308: if (
309: $type instanceof UnionType
310: && count($type->getTypes()) === 2
311: && $type->isObject()->yes()
312: && $type->getTypes()[0]->getObjectClassNames() === ['PhpParser\\Node\\Arg']
313: && $type->getTypes()[1]->getObjectClassNames() === ['PhpParser\\Node\\VariadicPlaceholder']
314: && !$unionTypeCriteriaCallback($type)
315: ) {
316: $tip = 'Use <fg=cyan>->getArgs()</> instead of <fg=cyan>->args</>.';
317: }
318:
319: return new FoundTypeResult($type, $directClassNames, [], $tip);
320: }
321:
322: }
323: