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\StrictMixedType;
18: use PHPStan\Type\Type;
19: use PHPStan\Type\TypeCombinator;
20: use PHPStan\Type\TypeTraverser;
21: use PHPStan\Type\TypeUtils;
22: use PHPStan\Type\UnionType;
23: use PHPStan\Type\VerbosityLevel;
24: use function count;
25: use function sprintf;
26: use function str_contains;
27:
28: final class RuleLevelHelper
29: {
30:
31: public function __construct(
32: private ReflectionProvider $reflectionProvider,
33: private bool $checkNullables,
34: private bool $checkThisOnly,
35: private bool $checkUnionTypes,
36: private bool $checkExplicitMixed,
37: private bool $checkImplicitMixed,
38: private bool $checkBenevolentUnionTypes,
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: $errors[] = RuleErrorBuilder::message(sprintf($unknownClassErrorPattern, $referencedClass))
227: ->line($var->getStartLine())
228: ->identifier('class.notFound')
229: ->discoveringSymbolsTip()
230: ->build();
231: }
232: }
233:
234: if (count($errors) > 0 || $hasClassExistsClass) {
235: return new FoundTypeResult(new ErrorType(), [], $errors, null);
236: }
237:
238: if (!$this->checkUnionTypes && $type->isObject()->yes() && count($type->getObjectClassNames()) === 0) {
239: return new FoundTypeResult(new ErrorType(), [], [], null);
240: }
241:
242: if ($type instanceof UnionType) {
243: $shouldFilterUnion = (
244: !$this->checkUnionTypes
245: && !$type instanceof BenevolentUnionType
246: ) || (
247: !$this->checkBenevolentUnionTypes
248: && $type instanceof BenevolentUnionType
249: );
250:
251: $newTypes = [];
252:
253: foreach ($type->getTypes() as $innerType) {
254: if ($shouldFilterUnion && !$unionTypeCriteriaCallback($innerType)) {
255: continue;
256: }
257:
258: $newTypes[] = $this->findTypeToCheckImplementation(
259: $scope,
260: $var,
261: $innerType,
262: $unknownClassErrorPattern,
263: $unionTypeCriteriaCallback,
264: )->getType();
265: }
266:
267: if (count($newTypes) > 0) {
268: $newUnion = TypeCombinator::union(...$newTypes);
269: if (
270: !$this->checkBenevolentUnionTypes
271: && $type instanceof BenevolentUnionType
272: ) {
273: $newUnion = TypeUtils::toBenevolentUnion($newUnion);
274: }
275:
276: return new FoundTypeResult($newUnion, $directClassNames, [], null);
277: }
278: }
279:
280: if ($type instanceof IntersectionType) {
281: $newTypes = [];
282:
283: $changed = false;
284: foreach ($type->getTypes() as $innerType) {
285: if ($innerType instanceof TemplateMixedType) {
286: $changed = true;
287: $newTypes[] = $this->findTypeToCheckImplementation(
288: $scope,
289: $var,
290: $innerType->toStrictMixedType(),
291: $unknownClassErrorPattern,
292: $unionTypeCriteriaCallback,
293: )->getType();
294: continue;
295: }
296: $newTypes[] = $innerType;
297: }
298:
299: if ($changed) {
300: return new FoundTypeResult(TypeCombinator::intersect(...$newTypes), $directClassNames, [], null);
301: }
302: }
303:
304: $tip = null;
305: if (str_contains($type->describe(VerbosityLevel::typeOnly()), 'PhpParser\\Node\\Arg|PhpParser\\Node\\VariadicPlaceholder') && !$unionTypeCriteriaCallback($type)) {
306: $tip = 'Use <fg=cyan>->getArgs()</> instead of <fg=cyan>->args</>.';
307: }
308:
309: return new FoundTypeResult($type, $directClassNames, [], $tip);
310: }
311:
312: }
313: