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