1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use PhpParser\Node;
6: use PhpParser\Node\Expr;
7: use PhpParser\Node\Expr\FuncCall;
8: use PhpParser\Node\Expr\Instanceof_;
9: use PhpParser\Node\Expr\MethodCall;
10: use PhpParser\Node\Expr\PropertyFetch;
11: use PhpParser\Node\Expr\StaticCall;
12: use PhpParser\Node\Name;
13: use PHPStan\DependencyInjection\AutowiredService;
14: use PHPStan\DependencyInjection\Container;
15: use PHPStan\Node\Printer\ExprPrinter;
16: use PHPStan\Reflection\ReflectionProvider;
17: use PHPStan\Type\ExtensionClassHelper;
18: use PHPStan\Type\FunctionTypeSpecifyingExtension;
19: use PHPStan\Type\MethodTypeSpecifyingExtension;
20: use PHPStan\Type\NullType;
21: use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
22: use PHPStan\Type\StaticTypeFactory;
23: use PHPStan\Type\Type;
24: use PHPStan\Type\TypeCombinator;
25: use function array_merge;
26:
27: #[AutowiredService(name: 'typeSpecifier', factory: '@typeSpecifierFactory::create')]
28: final class TypeSpecifier
29: {
30:
31: /** @var MethodTypeSpecifyingExtension[][]|null */
32: private ?array $methodTypeSpecifyingExtensionsByClass = null;
33:
34: /** @var StaticMethodTypeSpecifyingExtension[][]|null */
35: private ?array $staticMethodTypeSpecifyingExtensionsByClass = null;
36:
37: /**
38: * @param FunctionTypeSpecifyingExtension[] $functionTypeSpecifyingExtensions
39: * @param MethodTypeSpecifyingExtension[] $methodTypeSpecifyingExtensions
40: * @param StaticMethodTypeSpecifyingExtension[] $staticMethodTypeSpecifyingExtensions
41: */
42: public function __construct(
43: private ExprPrinter $exprPrinter,
44: private ReflectionProvider $reflectionProvider,
45: private array $functionTypeSpecifyingExtensions,
46: private array $methodTypeSpecifyingExtensions,
47: private array $staticMethodTypeSpecifyingExtensions,
48: private bool $rememberPossiblyImpureFunctionValues,
49: private Container $container,
50: )
51: {
52: }
53:
54: /**
55: * @api
56: */
57: public function specifyTypesInCondition(
58: Scope $scope,
59: Expr $expr,
60: TypeSpecifierContext $context,
61: ): SpecifiedTypes
62: {
63: if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) {
64: return (new SpecifiedTypes([], []))->setRootExpr($expr);
65: }
66:
67: $exprHandler = ExprHandlerRegistry::resolve($expr, $this->container);
68: if ($exprHandler !== null) {
69: if ($scope instanceof MutatingScope) {
70: return $scope->specifyTypesOfNewWorldHandlerNode($expr, $context);
71: }
72: }
73:
74: return $this->specifyDefaultTypes($scope, $expr, $context);
75: }
76:
77: /**
78: * Fallback used by ExprHandler::specifyTypes implementations that have no
79: * Expr-specific narrowing: applies the default truthy/falsey narrowing, or
80: * returns empty SpecifiedTypes in a null context.
81: *
82: * @internal
83: */
84: public function specifyDefaultTypes(Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes
85: {
86: if (!$context->null()) {
87: return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
88: }
89:
90: return (new SpecifiedTypes([], []))->setRootExpr($expr);
91: }
92:
93: /** @internal */
94: public function handleDefaultTruthyOrFalseyContext(TypeSpecifierContext $context, Expr $expr, Scope $scope): SpecifiedTypes
95: {
96: if ($context->null()) {
97: return (new SpecifiedTypes([], []))->setRootExpr($expr);
98: }
99: if (!$context->truthy()) {
100: $type = StaticTypeFactory::truthy();
101: return $this->create($expr, $type, TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr);
102: } elseif (!$context->falsey()) {
103: $type = StaticTypeFactory::falsey();
104: return $this->create($expr, $type, TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr);
105: }
106:
107: return (new SpecifiedTypes([], []))->setRootExpr($expr);
108: }
109:
110: /**
111: * @api
112: */
113: public function create(
114: Expr $expr,
115: Type $type,
116: TypeSpecifierContext $context,
117: Scope $scope,
118: ): SpecifiedTypes
119: {
120: if ($expr instanceof Instanceof_ || $expr instanceof Expr\List_) {
121: return (new SpecifiedTypes([], []))->setRootExpr($expr);
122: }
123:
124: $specifiedExprs = [];
125: if ($expr instanceof Expr\Assign) {
126: $specifiedExprs[] = $expr->var;
127: $specifiedExprs[] = $expr->expr;
128:
129: while ($expr->expr instanceof Expr\Assign) {
130: $specifiedExprs[] = $expr->expr->var;
131: $expr = $expr->expr;
132: }
133: } elseif ($expr instanceof Expr\AssignOp\Coalesce) {
134: $specifiedExprs[] = $expr->var;
135: } else {
136: $specifiedExprs[] = $expr;
137: }
138:
139: $types = null;
140:
141: foreach ($specifiedExprs as $specifiedExpr) {
142: $newTypes = $this->createForExpr($specifiedExpr, $type, $context, $scope);
143:
144: if ($types === null) {
145: $types = $newTypes;
146: } else {
147: $types = $types->unionWith($newTypes);
148: }
149: }
150:
151: return $types;
152: }
153:
154: private function createForExpr(
155: Expr $expr,
156: Type $type,
157: TypeSpecifierContext $context,
158: Scope $scope,
159: ): SpecifiedTypes
160: {
161: // the null-containment probe only feeds the nullsafe-shortcircuit unwrap
162: // and createNullsafeTypes() - both are no-ops for a bare variable, so the
163: // probe (and its type ask) is skipped for one
164: if (!$expr instanceof Expr\Variable) {
165: if ($context->true()) {
166: $containsNull = !$type->isNull()->no() && !$scope->getType($expr)->isNull()->no();
167: } elseif ($context->false()) {
168: $containsNull = !TypeCombinator::containsNull($type) && !$scope->getType($expr)->isNull()->no();
169: }
170: }
171:
172: $originalExpr = $expr;
173: if (isset($containsNull) && !$containsNull) {
174: $expr = NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($expr);
175: }
176:
177: if (
178: !$context->null()
179: && $expr instanceof Expr\BinaryOp\Coalesce
180: ) {
181: if (
182: ($context->true() && $type->isSuperTypeOf($scope->getType($expr->right))->no())
183: || ($context->false() && $type->isSuperTypeOf($scope->getType($expr->right))->yes())
184: ) {
185: $expr = $expr->left;
186: }
187: }
188:
189: if (
190: $expr instanceof FuncCall
191: && $expr->name instanceof Name
192: ) {
193: $has = $this->reflectionProvider->hasFunction($expr->name, $scope);
194: if (!$has) {
195: // backwards compatibility with previous behaviour
196: return new SpecifiedTypes([], []);
197: }
198:
199: $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope);
200: $hasSideEffects = $functionReflection->hasSideEffects();
201: if ($hasSideEffects->yes()) {
202: return new SpecifiedTypes([], []);
203: }
204:
205: if (!$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no()) {
206: return new SpecifiedTypes([], []);
207: }
208: }
209:
210: if (
211: $expr instanceof FuncCall
212: && !$expr->name instanceof Name
213: ) {
214: $nameType = $scope->getType($expr->name);
215: if ($nameType->isCallable()->yes()) {
216: $isPure = null;
217: foreach ($nameType->getCallableParametersAcceptors($scope) as $variant) {
218: $variantIsPure = $variant->isPure();
219: $isPure = $isPure === null ? $variantIsPure : $isPure->and($variantIsPure);
220: }
221:
222: if ($isPure !== null) {
223: if ($isPure->no()) {
224: return new SpecifiedTypes([], []);
225: }
226:
227: if (!$this->rememberPossiblyImpureFunctionValues && !$isPure->yes()) {
228: return new SpecifiedTypes([], []);
229: }
230: }
231: }
232: }
233:
234: if (
235: $expr instanceof MethodCall
236: && $expr->name instanceof Node\Identifier
237: ) {
238: $methodName = $expr->name->toString();
239: $calledOnType = $scope->getType($expr->var);
240: $methodReflection = $scope->getMethodReflection($calledOnType, $methodName);
241: if (
242: $methodReflection === null
243: || $methodReflection->hasSideEffects()->yes()
244: || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
245: ) {
246: if (isset($containsNull) && !$containsNull) {
247: return $this->createNullsafeTypes($originalExpr, $scope, $context, $type);
248: }
249:
250: return new SpecifiedTypes([], []);
251: }
252: }
253:
254: if (
255: $expr instanceof StaticCall
256: && $expr->name instanceof Node\Identifier
257: ) {
258: $methodName = $expr->name->toString();
259: if ($expr->class instanceof Name) {
260: $calledOnType = $scope->resolveTypeByName($expr->class);
261: } else {
262: $calledOnType = $scope->getType($expr->class);
263: }
264:
265: $methodReflection = $scope->getMethodReflection($calledOnType, $methodName);
266: if (
267: $methodReflection === null
268: || $methodReflection->hasSideEffects()->yes()
269: || (!$this->rememberPossiblyImpureFunctionValues && !$methodReflection->hasSideEffects()->no())
270: ) {
271: if (isset($containsNull) && !$containsNull) {
272: return $this->createNullsafeTypes($originalExpr, $scope, $context, $type);
273: }
274:
275: return new SpecifiedTypes([], []);
276: }
277: }
278:
279: $sureTypes = [];
280: $sureNotTypes = [];
281: if ($context->false()) {
282: $exprString = $this->exprPrinter->printExpr($expr);
283: $sureNotTypes[$exprString] = [$expr, $type];
284:
285: if ($expr !== $originalExpr) {
286: $originalExprString = $this->exprPrinter->printExpr($originalExpr);
287: $sureNotTypes[$originalExprString] = [$originalExpr, $type];
288: }
289: } elseif ($context->true()) {
290: $exprString = $this->exprPrinter->printExpr($expr);
291: $sureTypes[$exprString] = [$expr, $type];
292:
293: if ($expr !== $originalExpr) {
294: $originalExprString = $this->exprPrinter->printExpr($originalExpr);
295: $sureTypes[$originalExprString] = [$originalExpr, $type];
296: }
297: }
298:
299: $types = new SpecifiedTypes($sureTypes, $sureNotTypes);
300: if (isset($containsNull) && !$containsNull) {
301: return $this->createNullsafeTypes($originalExpr, $scope, $context, $type)->unionWith($types);
302: }
303:
304: return $types;
305: }
306:
307: private function createNullsafeTypes(Expr $expr, Scope $scope, TypeSpecifierContext $context, ?Type $type): SpecifiedTypes
308: {
309: if ($expr instanceof Expr\NullsafePropertyFetch) {
310: if ($type !== null) {
311: $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), $type, $context, $scope);
312: } else {
313: $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), new NullType(), TypeSpecifierContext::createFalse(), $scope);
314: }
315:
316: return $propertyFetchTypes->unionWith(
317: $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), $scope),
318: );
319: }
320:
321: if ($expr instanceof Expr\NullsafeMethodCall) {
322: if ($type !== null) {
323: $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), $type, $context, $scope);
324: } else {
325: $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), new NullType(), TypeSpecifierContext::createFalse(), $scope);
326: }
327:
328: return $methodCallTypes->unionWith(
329: $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), $scope),
330: );
331: }
332:
333: if ($expr instanceof Expr\PropertyFetch) {
334: return $this->createNullsafeTypes($expr->var, $scope, $context, null);
335: }
336:
337: if ($expr instanceof Expr\MethodCall) {
338: return $this->createNullsafeTypes($expr->var, $scope, $context, null);
339: }
340:
341: if ($expr instanceof Expr\ArrayDimFetch) {
342: return $this->createNullsafeTypes($expr->var, $scope, $context, null);
343: }
344:
345: if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) {
346: return $this->createNullsafeTypes($expr->class, $scope, $context, null);
347: }
348:
349: if ($expr instanceof Expr\StaticCall && $expr->class instanceof Expr) {
350: return $this->createNullsafeTypes($expr->class, $scope, $context, null);
351: }
352:
353: return new SpecifiedTypes([], []);
354: }
355:
356: /**
357: * @return FunctionTypeSpecifyingExtension[]
358: *
359: * @internal
360: */
361: public function getFunctionTypeSpecifyingExtensions(): array
362: {
363: return $this->functionTypeSpecifyingExtensions;
364: }
365:
366: /**
367: * @return MethodTypeSpecifyingExtension[]
368: *
369: * @internal
370: */
371: public function getMethodTypeSpecifyingExtensionsForClass(string $className): array
372: {
373: if ($this->methodTypeSpecifyingExtensionsByClass === null) {
374: $byClass = [];
375: foreach ($this->methodTypeSpecifyingExtensions as $extension) {
376: $byClass[$extension->getClass()][] = $extension;
377: }
378:
379: $this->methodTypeSpecifyingExtensionsByClass = $byClass;
380: }
381: return $this->getTypeSpecifyingExtensionsForType($this->methodTypeSpecifyingExtensionsByClass, $className);
382: }
383:
384: /**
385: * @return StaticMethodTypeSpecifyingExtension[]
386: *
387: * @internal
388: */
389: public function getStaticMethodTypeSpecifyingExtensionsForClass(string $className): array
390: {
391: if ($this->staticMethodTypeSpecifyingExtensionsByClass === null) {
392: $byClass = [];
393: foreach ($this->staticMethodTypeSpecifyingExtensions as $extension) {
394: $byClass[$extension->getClass()][] = $extension;
395: }
396:
397: $this->staticMethodTypeSpecifyingExtensionsByClass = $byClass;
398: }
399: return $this->getTypeSpecifyingExtensionsForType($this->staticMethodTypeSpecifyingExtensionsByClass, $className);
400: }
401:
402: /**
403: * @param MethodTypeSpecifyingExtension[][]|StaticMethodTypeSpecifyingExtension[][] $extensions
404: * @return mixed[]
405: */
406: private function getTypeSpecifyingExtensionsForType(array $extensions, string $className): array
407: {
408: $extensionsForClass = [[]];
409: $extensionClassNames = ExtensionClassHelper::getExtensionClassNames($this->reflectionProvider, $className);
410: foreach ($extensionClassNames as $extensionClassName) {
411: if (!isset($extensions[$extensionClassName])) {
412: continue;
413: }
414:
415: $extensionsForClass[] = $extensions[$extensionClassName];
416: }
417:
418: return array_merge(...$extensionsForClass);
419: }
420:
421: }
422: