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