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\Identifier;
13: use PhpParser\Node\Name;
14: use PHPStan\DependencyInjection\AutowiredService;
15: use PHPStan\DependencyInjection\Container;
16: use PHPStan\Node\Expr\AlwaysRememberedExpr;
17: use PHPStan\Node\Printer\ExprPrinter;
18: use PHPStan\Reflection\ReflectionProvider;
19: use PHPStan\TrinaryLogic;
20: use PHPStan\Turbo\ShadowedByTurboExtension;
21: use PHPStan\Type\ExtensionClassHelper;
22: use PHPStan\Type\FunctionTypeSpecifyingExtension;
23: use PHPStan\Type\MethodTypeSpecifyingExtension;
24: use PHPStan\Type\NullType;
25: use PHPStan\Type\StaticMethodTypeSpecifyingExtension;
26: use PHPStan\Type\StaticTypeFactory;
27: use PHPStan\Type\Type;
28: use PHPStan\Type\TypeCombinator;
29: use function array_merge;
30: use function is_array;
31:
32: #[AutowiredService(name: 'typeSpecifier', factory: '@typeSpecifierFactory::create')]
33: #[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/TypeSpecifier.cpp')]
34: final class TypeSpecifier
35: {
36:
37: private const CONTAINS_CALL_ATTRIBUTE_NAME = 'containsCall';
38:
39: /** @var MethodTypeSpecifyingExtension[][]|null */
40: private ?array $methodTypeSpecifyingExtensionsByClass = null;
41:
42: /** @var StaticMethodTypeSpecifyingExtension[][]|null */
43: private ?array $staticMethodTypeSpecifyingExtensionsByClass = null;
44:
45: /**
46: * @param FunctionTypeSpecifyingExtension[] $functionTypeSpecifyingExtensions
47: * @param MethodTypeSpecifyingExtension[] $methodTypeSpecifyingExtensions
48: * @param StaticMethodTypeSpecifyingExtension[] $staticMethodTypeSpecifyingExtensions
49: */
50: public function __construct(
51: private ExprPrinter $exprPrinter,
52: private ReflectionProvider $reflectionProvider,
53: private array $functionTypeSpecifyingExtensions,
54: private array $methodTypeSpecifyingExtensions,
55: private array $staticMethodTypeSpecifyingExtensions,
56: private bool $rememberPossiblyImpureFunctionValues,
57: private Container $container,
58: )
59: {
60: }
61:
62: /**
63: * @api
64: */
65: public function specifyTypesInCondition(
66: Scope $scope,
67: Expr $expr,
68: TypeSpecifierContext $context,
69: ): SpecifiedTypes
70: {
71: if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) {
72: return (new SpecifiedTypes([], []))->setRootExpr($expr);
73: }
74:
75: $exprHandler = ExprHandlerRegistry::resolve($expr, $this->container);
76: if ($exprHandler !== null) {
77: if ($scope instanceof MutatingScope) {
78: return $scope->specifyTypesOfNewWorldHandlerNode($expr, $context);
79: }
80: }
81:
82: return $this->specifyDefaultTypes($scope, $expr, $context);
83: }
84:
85: /**
86: * Fallback used by ExprHandler::specifyTypes implementations that have no
87: * Expr-specific narrowing: applies the default truthy/falsey narrowing, or
88: * returns empty SpecifiedTypes in a null context.
89: *
90: * @internal
91: */
92: public function specifyDefaultTypes(Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes
93: {
94: if (!$context->null()) {
95: return $this->handleDefaultTruthyOrFalseyContext($context, $expr, $scope);
96: }
97:
98: return (new SpecifiedTypes([], []))->setRootExpr($expr);
99: }
100:
101: /** @internal */
102: public function handleDefaultTruthyOrFalseyContext(TypeSpecifierContext $context, Expr $expr, Scope $scope): SpecifiedTypes
103: {
104: if ($context->null()) {
105: return (new SpecifiedTypes([], []))->setRootExpr($expr);
106: }
107: if (!$context->truthy()) {
108: $type = StaticTypeFactory::truthy();
109: return $this->create($expr, $type, TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr);
110: } elseif (!$context->falsey()) {
111: $type = StaticTypeFactory::falsey();
112: return $this->create($expr, $type, TypeSpecifierContext::createFalse(), $scope)->setRootExpr($expr);
113: }
114:
115: return (new SpecifiedTypes([], []))->setRootExpr($expr);
116: }
117:
118: /**
119: * @api
120: */
121: public function create(
122: Expr $expr,
123: Type $type,
124: TypeSpecifierContext $context,
125: Scope $scope,
126: ): SpecifiedTypes
127: {
128: if ($expr instanceof Instanceof_ || $expr instanceof Expr\List_) {
129: return (new SpecifiedTypes([], []))->setRootExpr($expr);
130: }
131:
132: $specifiedExprs = [];
133: if ($expr instanceof Expr\Assign) {
134: $specifiedExprs[] = $expr->var;
135: $specifiedExprs[] = $expr->expr;
136:
137: while ($expr->expr instanceof Expr\Assign) {
138: $specifiedExprs[] = $expr->expr->var;
139: $expr = $expr->expr;
140: }
141: } elseif ($expr instanceof Expr\AssignOp\Coalesce) {
142: $specifiedExprs[] = $expr->var;
143: } else {
144: $specifiedExprs[] = $expr;
145: }
146:
147: $types = null;
148:
149: foreach ($specifiedExprs as $specifiedExpr) {
150: $newTypes = $this->createForExpr($specifiedExpr, $type, $context, $scope);
151:
152: if ($types === null) {
153: $types = $newTypes;
154: } else {
155: $types = $types->unionWith($newTypes);
156: }
157: }
158:
159: return $types;
160: }
161:
162: private function createForExpr(
163: Expr $expr,
164: Type $type,
165: TypeSpecifierContext $context,
166: Scope $scope,
167: ): SpecifiedTypes
168: {
169: // the null-containment probe only feeds the nullsafe-shortcircuit unwrap
170: // and createNullsafeTypes() - both are no-ops for a bare variable, so the
171: // probe (and its type ask) is skipped for one
172: if (!$expr instanceof Expr\Variable) {
173: if ($context->true()) {
174: $containsNull = !$type->isNull()->no() && !$scope->getType($expr)->isNull()->no();
175: } elseif ($context->false()) {
176: $containsNull = !TypeCombinator::containsNull($type) && !$scope->getType($expr)->isNull()->no();
177: }
178: }
179:
180: $originalExpr = $expr;
181: if (isset($containsNull) && !$containsNull) {
182: $expr = NullsafeOperatorHelper::getNullsafeShortcircuitedExpr($expr);
183: }
184:
185: if (
186: !$context->null()
187: && $expr instanceof Expr\BinaryOp\Coalesce
188: ) {
189: if (
190: ($context->true() && $type->isSuperTypeOf($scope->getType($expr->right))->no())
191: || ($context->false() && $type->isSuperTypeOf($scope->getType($expr->right))->yes())
192: ) {
193: $expr = $expr->left;
194: }
195: }
196:
197: if (
198: $expr instanceof FuncCall
199: && $expr->name instanceof Name
200: && !$this->reflectionProvider->hasFunction($expr->name, $scope)
201: ) {
202: return new SpecifiedTypes([], []);
203: }
204:
205: if (!($expr instanceof AlwaysRememberedExpr) && $this->expressionContainsNonPureCall($expr, $scope)) {
206: if (isset($containsNull) && !$containsNull) {
207: return $this->createNullsafeTypes($originalExpr, $scope, $context, $type);
208: }
209:
210: return new SpecifiedTypes([], []);
211: }
212:
213: $sureTypes = [];
214: $sureNotTypes = [];
215: if ($context->false()) {
216: $exprString = $this->exprPrinter->printExpr($expr);
217: $sureNotTypes[$exprString] = [$expr, $type];
218:
219: if ($expr !== $originalExpr) {
220: $originalExprString = $this->exprPrinter->printExpr($originalExpr);
221: $sureNotTypes[$originalExprString] = [$originalExpr, $type];
222: }
223: } elseif ($context->true()) {
224: $exprString = $this->exprPrinter->printExpr($expr);
225: $sureTypes[$exprString] = [$expr, $type];
226:
227: if ($expr !== $originalExpr) {
228: $originalExprString = $this->exprPrinter->printExpr($originalExpr);
229: $sureTypes[$originalExprString] = [$originalExpr, $type];
230: }
231: }
232:
233: $types = new SpecifiedTypes($sureTypes, $sureNotTypes);
234: if (isset($containsNull) && !$containsNull) {
235: return $this->createNullsafeTypes($originalExpr, $scope, $context, $type)->unionWith($types);
236: }
237:
238: return $types;
239: }
240:
241: private function expressionContainsNonPureCall(Expr $expr, Scope $scope): bool
242: {
243: // The answer for an expression without any call in it cannot change between
244: // scopes, and most specified expressions (plain variables, property fetches,
245: // constant fetches) are of that shape, so it's remembered on the node itself.
246: if ($expr->getAttribute(self::CONTAINS_CALL_ATTRIBUTE_NAME) === false) {
247: return false;
248: }
249:
250: $containsCall = false;
251: $containsNonPureCall = $this->findNonPureCall($expr, $scope, $containsCall);
252: if (!$containsCall) {
253: $expr->setAttribute(self::CONTAINS_CALL_ATTRIBUTE_NAME, false);
254: }
255:
256: return $containsNonPureCall;
257: }
258:
259: /**
260: * Depth-first pre-order search for a call that isn't known to be pure, replacing a
261: * NodeFinder::findFirst() call - this runs for every expression being specified,
262: * so the traverser/visitor machinery overhead was significant.
263: *
264: * $containsCall is set when the sub-tree contains a call of any kind.
265: */
266: private function findNonPureCall(Node $node, Scope $scope, bool &$containsCall): bool
267: {
268: if ($node instanceof Expr\CallLike) {
269: $containsCall = true;
270:
271: if ($this->callIsNotPure($node, $scope)) {
272: return true;
273: }
274: }
275:
276: foreach ($node->getSubNodeNames() as $subNodeName) {
277: $subNode = $node->$subNodeName;
278: if ($subNode instanceof Node) {
279: if ($this->findNonPureCall($subNode, $scope, $containsCall)) {
280: return true;
281: }
282: } elseif (is_array($subNode)) {
283: foreach ($subNode as $subNodeItem) {
284: if (
285: $subNodeItem instanceof Node
286: && $this->findNonPureCall($subNodeItem, $scope, $containsCall)
287: ) {
288: return true;
289: }
290: }
291: }
292: }
293:
294: return false;
295: }
296:
297: private function callIsNotPure(Expr\CallLike $call, Scope $scope): bool
298: {
299: if ($call instanceof FuncCall) {
300: if ($call->name instanceof Name) {
301: if (!$this->reflectionProvider->hasFunction($call->name, $scope)) {
302: return false;
303: }
304:
305: return $this->isNotPure($this->reflectionProvider->getFunction($call->name, $scope)->hasSideEffects());
306: }
307:
308: $nameType = $scope->getType($call->name);
309: if ($nameType->isCallable()->yes()) {
310: $isPure = null;
311: foreach ($nameType->getCallableParametersAcceptors($scope) as $variant) {
312: $variantIsPure = $variant->isPure();
313: $isPure = $isPure === null ? $variantIsPure : $isPure->and($variantIsPure);
314: }
315: if ($isPure !== null) {
316: return $this->isNotPure($isPure->negate());
317: }
318: }
319:
320: return false;
321: }
322:
323: if ($call instanceof MethodCall) {
324: if (!$call->name instanceof Identifier) {
325: return true;
326: }
327:
328: $methodReflection = $scope->getMethodReflection($scope->getType($call->var), $call->name->name);
329: if ($methodReflection === null) {
330: return true;
331: }
332:
333: return $this->isNotPure($methodReflection->hasSideEffects());
334: }
335:
336: if ($call instanceof StaticCall) {
337: if (!$call->name instanceof Identifier) {
338: return true;
339: }
340:
341: if ($call->class instanceof Name) {
342: $calledOnType = $scope->resolveTypeByName($call->class);
343: } else {
344: $calledOnType = $scope->getType($call->class);
345: }
346:
347: $methodReflection = $scope->getMethodReflection($calledOnType, $call->name->name);
348: if ($methodReflection === null) {
349: return true;
350: }
351:
352: return $this->isNotPure($methodReflection->hasSideEffects());
353: }
354:
355: return false;
356: }
357:
358: private function isNotPure(TrinaryLogic $hasSideEffects): bool
359: {
360: if ($hasSideEffects->yes()) {
361: return true;
362: }
363:
364: return !$this->rememberPossiblyImpureFunctionValues && !$hasSideEffects->no();
365: }
366:
367: private function createNullsafeTypes(Expr $expr, Scope $scope, TypeSpecifierContext $context, ?Type $type): SpecifiedTypes
368: {
369: if ($expr instanceof Expr\NullsafePropertyFetch) {
370: if ($type !== null) {
371: $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), $type, $context, $scope);
372: } else {
373: $propertyFetchTypes = $this->create(new PropertyFetch($expr->var, $expr->name), new NullType(), TypeSpecifierContext::createFalse(), $scope);
374: }
375:
376: return $propertyFetchTypes->unionWith(
377: $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), $scope),
378: );
379: }
380:
381: if ($expr instanceof Expr\NullsafeMethodCall) {
382: if ($type !== null) {
383: $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), $type, $context, $scope);
384: } else {
385: $methodCallTypes = $this->create(new MethodCall($expr->var, $expr->name, $expr->args), new NullType(), TypeSpecifierContext::createFalse(), $scope);
386: }
387:
388: return $methodCallTypes->unionWith(
389: $this->create($expr->var, new NullType(), TypeSpecifierContext::createFalse(), $scope),
390: );
391: }
392:
393: if ($expr instanceof Expr\PropertyFetch) {
394: return $this->createNullsafeTypes($expr->var, $scope, $context, null);
395: }
396:
397: if ($expr instanceof Expr\MethodCall) {
398: return $this->createNullsafeTypes($expr->var, $scope, $context, null);
399: }
400:
401: if ($expr instanceof Expr\ArrayDimFetch) {
402: return $this->createNullsafeTypes($expr->var, $scope, $context, null);
403: }
404:
405: if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) {
406: return $this->createNullsafeTypes($expr->class, $scope, $context, null);
407: }
408:
409: if ($expr instanceof Expr\StaticCall && $expr->class instanceof Expr) {
410: return $this->createNullsafeTypes($expr->class, $scope, $context, null);
411: }
412:
413: return new SpecifiedTypes([], []);
414: }
415:
416: /**
417: * @return FunctionTypeSpecifyingExtension[]
418: *
419: * @internal
420: */
421: public function getFunctionTypeSpecifyingExtensions(): array
422: {
423: return $this->functionTypeSpecifyingExtensions;
424: }
425:
426: /**
427: * @return MethodTypeSpecifyingExtension[]
428: *
429: * @internal
430: */
431: public function getMethodTypeSpecifyingExtensionsForClass(string $className): array
432: {
433: if ($this->methodTypeSpecifyingExtensionsByClass === null) {
434: $byClass = [];
435: foreach ($this->methodTypeSpecifyingExtensions as $extension) {
436: $byClass[$extension->getClass()][] = $extension;
437: }
438:
439: $this->methodTypeSpecifyingExtensionsByClass = $byClass;
440: }
441: return $this->getTypeSpecifyingExtensionsForType($this->methodTypeSpecifyingExtensionsByClass, $className);
442: }
443:
444: /**
445: * @return StaticMethodTypeSpecifyingExtension[]
446: *
447: * @internal
448: */
449: public function getStaticMethodTypeSpecifyingExtensionsForClass(string $className): array
450: {
451: if ($this->staticMethodTypeSpecifyingExtensionsByClass === null) {
452: $byClass = [];
453: foreach ($this->staticMethodTypeSpecifyingExtensions as $extension) {
454: $byClass[$extension->getClass()][] = $extension;
455: }
456:
457: $this->staticMethodTypeSpecifyingExtensionsByClass = $byClass;
458: }
459: return $this->getTypeSpecifyingExtensionsForType($this->staticMethodTypeSpecifyingExtensionsByClass, $className);
460: }
461:
462: /**
463: * @param MethodTypeSpecifyingExtension[][]|StaticMethodTypeSpecifyingExtension[][] $extensions
464: * @return mixed[]
465: */
466: private function getTypeSpecifyingExtensionsForType(array $extensions, string $className): array
467: {
468: $extensionsForClass = [[]];
469: $extensionClassNames = ExtensionClassHelper::getExtensionClassNames($this->reflectionProvider, $className);
470: foreach ($extensionClassNames as $extensionClassName) {
471: if (!isset($extensions[$extensionClassName])) {
472: continue;
473: }
474:
475: $extensionsForClass[] = $extensions[$extensionClassName];
476: }
477:
478: return array_merge(...$extensionsForClass);
479: }
480:
481: }
482: