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