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($this->transformCommonType($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($this->transformCommonType($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: );
126: }
127:
128: if (
129: !$this->checkNullables
130: && !$acceptingType instanceof NullType
131: && !$acceptedType instanceof NullType
132: && !$acceptedType instanceof BenevolentUnionType
133: ) {
134: return $traverse(TypeCombinator::removeNull($acceptedType));
135: }
136:
137: if ($this->checkBenevolentUnionTypes) {
138: if ($acceptedType instanceof BenevolentUnionType) {
139: $checkForUnion = true;
140: return $traverse(TypeUtils::toStrictUnion($acceptedType));
141: }
142: }
143:
144: return $traverse($this->transformCommonType($acceptedType));
145: });
146:
147: return [$acceptedType, $checkForUnion];
148: }
149:
150: /** @api */
151: public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTypes): RuleLevelHelperAcceptsResult
152: {
153: [$acceptedType, $checkForUnion] = $this->transformAcceptedType($acceptingType, $acceptedType);
154: $acceptingType = $this->transformCommonType($acceptingType);
155:
156: $accepts = $acceptingType->accepts($acceptedType, $strictTypes);
157:
158: return new RuleLevelHelperAcceptsResult(
159: $checkForUnion ? $accepts->yes() : !$accepts->no(),
160: $accepts->reasons,
161: );
162: }
163:
164: /**
165: * @api
166: * @param callable(Type $type): bool $unionTypeCriteriaCallback
167: */
168: public function findTypeToCheck(
169: Scope $scope,
170: Expr $var,
171: string $unknownClassErrorPattern,
172: callable $unionTypeCriteriaCallback,
173: ): FoundTypeResult
174: {
175: if ($this->checkThisOnly && !$this->isThis($var)) {
176: return new FoundTypeResult(new ErrorType(), [], [], null);
177: }
178: $type = $scope->getType($var);
179:
180: return $this->findTypeToCheckImplementation($scope, $var, $type, $unknownClassErrorPattern, $unionTypeCriteriaCallback, true);
181: }
182:
183: /** @param callable(Type $type): bool $unionTypeCriteriaCallback */
184: private function findTypeToCheckImplementation(
185: Scope $scope,
186: Expr $var,
187: Type $type,
188: string $unknownClassErrorPattern,
189: callable $unionTypeCriteriaCallback,
190: bool $isTopLevel = false,
191: ): FoundTypeResult
192: {
193: if (
194: !$this->checkNullables
195: && !$type->isNull()->yes()
196: && !$unionTypeCriteriaCallback(new NullType())
197: ) {
198: $type = TypeCombinator::removeNull($type);
199: }
200:
201: if (
202: ($this->checkExplicitMixed || $this->checkImplicitMixed)
203: && $type instanceof MixedType
204: && ($type->isExplicitMixed() ? $this->checkExplicitMixed : $this->checkImplicitMixed)
205: ) {
206: return new FoundTypeResult(
207: $type instanceof TemplateMixedType
208: ? $type->toStrictMixedType()
209: : new StrictMixedType(),
210: [],
211: [],
212: null,
213: );
214: }
215:
216: if ($type instanceof MixedType || $type instanceof NeverType) {
217: return new FoundTypeResult(new ErrorType(), [], [], null);
218: }
219:
220: $errors = [];
221: $hasClassExistsClass = false;
222: $directClassNames = [];
223:
224: if ($isTopLevel) {
225: $directClassNames = $type->getObjectClassNames();
226: foreach ($directClassNames as $referencedClass) {
227: if ($this->reflectionProvider->hasClass($referencedClass)) {
228: $classReflection = $this->reflectionProvider->getClass($referencedClass);
229: if (!$classReflection->isTrait()) {
230: continue;
231: }
232: }
233:
234: if ($scope->isInClassExists($referencedClass)) {
235: $hasClassExistsClass = true;
236: continue;
237: }
238:
239: $errorBuilder = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass))
240: ->line($var->getStartLine())
241: ->identifier('class.notFound');
242:
243: if ($this->discoveringSymbolsTip) {
244: $errorBuilder->discoveringSymbolsTip();
245: }
246:
247: $errors[] = $errorBuilder->build();
248: }
249: }
250:
251: if (count($errors) > 0 || $hasClassExistsClass) {
252: return new FoundTypeResult(new ErrorType(), [], $errors, null);
253: }
254:
255: if (!$this->checkUnionTypes && $type->isObject()->yes() && count($type->getObjectClassNames()) === 0) {
256: return new FoundTypeResult(new ErrorType(), [], [], null);
257: }
258:
259: if ($type instanceof UnionType) {
260: $shouldFilterUnion = (
261: !$this->checkUnionTypes
262: && !$type instanceof BenevolentUnionType
263: ) || (
264: !$this->checkBenevolentUnionTypes
265: && $type instanceof BenevolentUnionType
266: );
267:
268: $newTypes = [];
269:
270: foreach ($type->getTypes() as $innerType) {
271: if ($shouldFilterUnion && !$unionTypeCriteriaCallback($innerType)) {
272: continue;
273: }
274:
275: $newTypes[] = $this->findTypeToCheckImplementation(
276: $scope,
277: $var,
278: $innerType,
279: $unknownClassErrorPattern,
280: $unionTypeCriteriaCallback,
281: )->getType();
282: }
283:
284: if (count($newTypes) > 0) {
285: $newUnion = TypeCombinator::union(...$newTypes);
286: if (
287: !$this->checkBenevolentUnionTypes
288: && $type instanceof BenevolentUnionType
289: ) {
290: $newUnion = TypeUtils::toBenevolentUnion($newUnion);
291: }
292:
293: return new FoundTypeResult($newUnion, $directClassNames, [], null);
294: }
295: }
296:
297: if ($type instanceof IntersectionType) {
298: $newTypes = [];
299:
300: $changed = false;
301: foreach ($type->getTypes() as $innerType) {
302: if ($innerType instanceof TemplateMixedType) {
303: $changed = true;
304: $newTypes[] = $this->findTypeToCheckImplementation(
305: $scope,
306: $var,
307: $innerType->toStrictMixedType(),
308: $unknownClassErrorPattern,
309: $unionTypeCriteriaCallback,
310: )->getType();
311: continue;
312: }
313: $newTypes[] = $innerType;
314: }
315:
316: if ($changed) {
317: return new FoundTypeResult(TypeCombinator::intersect(...$newTypes), $directClassNames, [], null);
318: }
319: }
320:
321: $tip = null;
322: if (
323: $type instanceof UnionType
324: && count($type->getTypes()) === 2
325: && $type->isObject()->yes()
326: && $type->getTypes()[0]->getObjectClassNames() === ['PhpParser\\Node\\Arg']
327: && $type->getTypes()[1]->getObjectClassNames() === ['PhpParser\\Node\\VariadicPlaceholder']
328: && !$unionTypeCriteriaCallback($type)
329: ) {
330: $tip = 'Use <fg=cyan>->getArgs()</> instead of <fg=cyan>->args</>.';
331: }
332:
333: return new FoundTypeResult($type, $directClassNames, [], $tip);
334: }
335:
336: }
337: