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 (!$this->checkNullables && !$type->isNull()->yes()) {
194: $type = TypeCombinator::removeNull($type);
195: }
196:
197: if (
198: ($this->checkExplicitMixed || $this->checkImplicitMixed)
199: && $type instanceof MixedType
200: && ($type->isExplicitMixed() ? $this->checkExplicitMixed : $this->checkImplicitMixed)
201: ) {
202: return new FoundTypeResult(
203: $type instanceof TemplateMixedType
204: ? $type->toStrictMixedType()
205: : new StrictMixedType(),
206: [],
207: [],
208: null,
209: );
210: }
211:
212: if ($type instanceof MixedType || $type instanceof NeverType) {
213: return new FoundTypeResult(new ErrorType(), [], [], null);
214: }
215:
216: $errors = [];
217: $hasClassExistsClass = false;
218: $directClassNames = [];
219:
220: if ($isTopLevel) {
221: $directClassNames = $type->getObjectClassNames();
222: foreach ($directClassNames as $referencedClass) {
223: if ($this->reflectionProvider->hasClass($referencedClass)) {
224: $classReflection = $this->reflectionProvider->getClass($referencedClass);
225: if (!$classReflection->isTrait()) {
226: continue;
227: }
228: }
229:
230: if ($scope->isInClassExists($referencedClass)) {
231: $hasClassExistsClass = true;
232: continue;
233: }
234:
235: $errorBuilder = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass))
236: ->line($var->getStartLine())
237: ->identifier('class.notFound');
238:
239: if ($this->discoveringSymbolsTip) {
240: $errorBuilder->discoveringSymbolsTip();
241: }
242:
243: $errors[] = $errorBuilder->build();
244: }
245: }
246:
247: if (count($errors) > 0 || $hasClassExistsClass) {
248: return new FoundTypeResult(new ErrorType(), [], $errors, null);
249: }
250:
251: if (!$this->checkUnionTypes && $type->isObject()->yes() && count($type->getObjectClassNames()) === 0) {
252: return new FoundTypeResult(new ErrorType(), [], [], null);
253: }
254:
255: if ($type instanceof UnionType) {
256: $shouldFilterUnion = (
257: !$this->checkUnionTypes
258: && !$type instanceof BenevolentUnionType
259: ) || (
260: !$this->checkBenevolentUnionTypes
261: && $type instanceof BenevolentUnionType
262: );
263:
264: $newTypes = [];
265:
266: foreach ($type->getTypes() as $innerType) {
267: if ($shouldFilterUnion && !$unionTypeCriteriaCallback($innerType)) {
268: continue;
269: }
270:
271: $newTypes[] = $this->findTypeToCheckImplementation(
272: $scope,
273: $var,
274: $innerType,
275: $unknownClassErrorPattern,
276: $unionTypeCriteriaCallback,
277: )->getType();
278: }
279:
280: if (count($newTypes) > 0) {
281: $newUnion = TypeCombinator::union(...$newTypes);
282: if (
283: !$this->checkBenevolentUnionTypes
284: && $type instanceof BenevolentUnionType
285: ) {
286: $newUnion = TypeUtils::toBenevolentUnion($newUnion);
287: }
288:
289: return new FoundTypeResult($newUnion, $directClassNames, [], null);
290: }
291: }
292:
293: if ($type instanceof IntersectionType) {
294: $newTypes = [];
295:
296: $changed = false;
297: foreach ($type->getTypes() as $innerType) {
298: if ($innerType instanceof TemplateMixedType) {
299: $changed = true;
300: $newTypes[] = $this->findTypeToCheckImplementation(
301: $scope,
302: $var,
303: $innerType->toStrictMixedType(),
304: $unknownClassErrorPattern,
305: $unionTypeCriteriaCallback,
306: )->getType();
307: continue;
308: }
309: $newTypes[] = $innerType;
310: }
311:
312: if ($changed) {
313: return new FoundTypeResult(TypeCombinator::intersect(...$newTypes), $directClassNames, [], null);
314: }
315: }
316:
317: $tip = null;
318: if (
319: $type instanceof UnionType
320: && count($type->getTypes()) === 2
321: && $type->isObject()->yes()
322: && $type->getTypes()[0]->getObjectClassNames() === ['PhpParser\\Node\\Arg']
323: && $type->getTypes()[1]->getObjectClassNames() === ['PhpParser\\Node\\VariadicPlaceholder']
324: && !$unionTypeCriteriaCallback($type)
325: ) {
326: $tip = 'Use <fg=cyan>->getArgs()</> instead of <fg=cyan>->args</>.';
327: }
328:
329: return new FoundTypeResult($type, $directClassNames, [], $tip);
330: }
331:
332: }
333: