| 1: | <?php declare(strict_types = 1); |
| 2: | |
| 3: | namespace PHPStan\Analyser; |
| 4: | |
| 5: | use Closure; |
| 6: | use PhpParser\Comment\Doc; |
| 7: | use PhpParser\Node; |
| 8: | use PhpParser\Node\AttributeGroup; |
| 9: | use PhpParser\Node\Expr; |
| 10: | use PhpParser\Node\Expr\ArrayDimFetch; |
| 11: | use PhpParser\Node\Expr\Assign; |
| 12: | use PhpParser\Node\Expr\AssignRef; |
| 13: | use PhpParser\Node\Expr\CallLike; |
| 14: | use PhpParser\Node\Expr\FuncCall; |
| 15: | use PhpParser\Node\Expr\List_; |
| 16: | use PhpParser\Node\Expr\MethodCall; |
| 17: | use PhpParser\Node\Expr\New_; |
| 18: | use PhpParser\Node\Expr\PropertyFetch; |
| 19: | use PhpParser\Node\Expr\StaticCall; |
| 20: | use PhpParser\Node\Expr\StaticPropertyFetch; |
| 21: | use PhpParser\Node\Expr\Variable; |
| 22: | use PhpParser\Node\Identifier; |
| 23: | use PhpParser\Node\Name; |
| 24: | use PhpParser\Node\Stmt\Class_; |
| 25: | use PhpParser\Node\Stmt\Echo_; |
| 26: | use PhpParser\Node\Stmt\Foreach_; |
| 27: | use PhpParser\Node\Stmt\Goto_; |
| 28: | use PhpParser\Node\Stmt\If_; |
| 29: | use PhpParser\Node\Stmt\Return_; |
| 30: | use PhpParser\Node\Stmt\Static_; |
| 31: | use PhpParser\Node\Stmt\Switch_; |
| 32: | use PhpParser\NodeFinder; |
| 33: | use PHPStan\Analyser\ExprHandler\AssignHandler; |
| 34: | use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver; |
| 35: | use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper; |
| 36: | use PHPStan\Analyser\ExprHandler\Helper\VirtualExprResultHelper; |
| 37: | use PHPStan\DependencyInjection\AutowiredExtensions; |
| 38: | use PHPStan\DependencyInjection\AutowiredParameter; |
| 39: | use PHPStan\DependencyInjection\AutowiredService; |
| 40: | use PHPStan\DependencyInjection\Container; |
| 41: | use PHPStan\DependencyInjection\ExtensionsCollection; |
| 42: | use PHPStan\Node\ClosureReturnStatementsNode; |
| 43: | use PHPStan\Node\ExecutionEndNode; |
| 44: | use PHPStan\Node\Expr\NativeTypeExpr; |
| 45: | use PHPStan\Node\Expr\TypeExpr; |
| 46: | use PHPStan\Node\FunctionCallableNode; |
| 47: | use PHPStan\Node\FunctionCallExpressionNode; |
| 48: | use PHPStan\Node\InArrowFunctionNode; |
| 49: | use PHPStan\Node\InClosureNode; |
| 50: | use PHPStan\Node\InstantiationCallableNode; |
| 51: | use PHPStan\Node\InvalidateExprNode; |
| 52: | use PHPStan\Node\MethodCallableNode; |
| 53: | use PHPStan\Node\MethodCallExpressionNode; |
| 54: | use PHPStan\Node\PropertyAssignNode; |
| 55: | use PHPStan\Node\PropertyHookStatementNode; |
| 56: | use PHPStan\Node\ReturnStatement; |
| 57: | use PHPStan\Node\StaticMethodCallableNode; |
| 58: | use PHPStan\Node\StaticMethodCallExpressionNode; |
| 59: | use PHPStan\Node\UnreachableStatementNode; |
| 60: | use PHPStan\Node\VarTagChangedExpressionTypeNode; |
| 61: | use PHPStan\Parser\ArrowFunctionArgVisitor; |
| 62: | use PHPStan\Parser\ClosureArgVisitor; |
| 63: | use PHPStan\Parser\GotoLabelVisitor; |
| 64: | use PHPStan\Parser\ImmediatelyInvokedClosureVisitor; |
| 65: | use PHPStan\PhpDoc\Tag\VarTag; |
| 66: | use PHPStan\Reflection\Callables\SimpleImpurePoint; |
| 67: | use PHPStan\Reflection\Callables\SimpleThrowPoint; |
| 68: | use PHPStan\Reflection\ExtendedMethodReflection; |
| 69: | use PHPStan\Reflection\ExtendedParameterReflection; |
| 70: | use PHPStan\Reflection\FunctionReflection; |
| 71: | use PHPStan\Reflection\MethodReflection; |
| 72: | use PHPStan\Reflection\Native\NativeMethodReflection; |
| 73: | use PHPStan\Reflection\Native\NativeParameterReflection; |
| 74: | use PHPStan\Reflection\ParameterReflection; |
| 75: | use PHPStan\Reflection\ParametersAcceptor; |
| 76: | use PHPStan\Reflection\ParametersAcceptorSelector; |
| 77: | use PHPStan\Reflection\Php\PhpMethodReflection; |
| 78: | use PHPStan\Reflection\ReflectionProvider; |
| 79: | use PHPStan\Rules\Properties\ReadWritePropertiesExtension; |
| 80: | use PHPStan\ShouldNotHappenException; |
| 81: | use PHPStan\TrinaryLogic; |
| 82: | use PHPStan\Type\ClosureType; |
| 83: | use PHPStan\Type\ErrorType; |
| 84: | use PHPStan\Type\FileTypeMapper; |
| 85: | use PHPStan\Type\FunctionParameterClosureThisExtension; |
| 86: | use PHPStan\Type\FunctionParameterClosureTypeExtension; |
| 87: | use PHPStan\Type\FunctionParameterOutTypeExtension; |
| 88: | use PHPStan\Type\MethodParameterClosureThisExtension; |
| 89: | use PHPStan\Type\MethodParameterClosureTypeExtension; |
| 90: | use PHPStan\Type\MethodParameterOutTypeExtension; |
| 91: | use PHPStan\Type\MixedType; |
| 92: | use PHPStan\Type\NullType; |
| 93: | use PHPStan\Type\ObjectWithoutClassType; |
| 94: | use PHPStan\Type\ResourceType; |
| 95: | use PHPStan\Type\StaticMethodParameterClosureThisExtension; |
| 96: | use PHPStan\Type\StaticMethodParameterClosureTypeExtension; |
| 97: | use PHPStan\Type\StaticMethodParameterOutTypeExtension; |
| 98: | use PHPStan\Type\ThisType; |
| 99: | use PHPStan\Type\Type; |
| 100: | use PHPStan\Type\TypeCombinator; |
| 101: | use PHPStan\Type\TypeUtils; |
| 102: | use PHPStan\Type\UnionType; |
| 103: | use function array_fill_keys; |
| 104: | use function array_filter; |
| 105: | use function array_key_exists; |
| 106: | use function array_keys; |
| 107: | use function array_last; |
| 108: | use function array_map; |
| 109: | use function array_merge; |
| 110: | use function array_pop; |
| 111: | use function array_slice; |
| 112: | use function array_values; |
| 113: | use function count; |
| 114: | use function get_class; |
| 115: | use function getenv; |
| 116: | use function in_array; |
| 117: | use function is_array; |
| 118: | use function is_int; |
| 119: | use function is_string; |
| 120: | use function max; |
| 121: | use function spl_object_id; |
| 122: | use function sprintf; |
| 123: | use function usort; |
| 124: | |
| 125: | #[AutowiredService] |
| 126: | class NodeScopeResolver |
| 127: | { |
| 128: | |
| 129: | public const LOOP_SCOPE_ITERATIONS = 3; |
| 130: | public const GENERALIZE_AFTER_ITERATION = 1; |
| 131: | |
| 132: | |
| 133: | private array $analysedFiles = []; |
| 134: | |
| 135: | |
| 136: | |
| 137: | |
| 138: | |
| 139: | protected bool $returnStoredExpressionResults = false; |
| 140: | |
| 141: | |
| 142: | |
| 143: | |
| 144: | |
| 145: | |
| 146: | |
| 147: | private bool $consumeStoredExpressionResults = false; |
| 148: | |
| 149: | private ?NonNullabilityHelper $nonNullabilityHelper = null; |
| 150: | |
| 151: | |
| 152: | |
| 153: | |
| 154: | |
| 155: | |
| 156: | |
| 157: | |
| 158: | |
| 159: | |
| 160: | private array $nodeGatherers = []; |
| 161: | |
| 162: | |
| 163: | public static bool $guardNewWorld = false; |
| 164: | |
| 165: | |
| 166: | |
| 167: | |
| 168: | |
| 169: | |
| 170: | |
| 171: | |
| 172: | |
| 173: | public static array $guardRealExprIds = []; |
| 174: | |
| 175: | |
| 176: | |
| 177: | |
| 178: | |
| 179: | |
| 180: | |
| 181: | |
| 182: | public static array $guardProcessedExprIds = []; |
| 183: | |
| 184: | |
| 185: | |
| 186: | |
| 187: | |
| 188: | |
| 189: | |
| 190: | |
| 191: | |
| 192: | |
| 193: | |
| 194: | |
| 195: | |
| 196: | |
| 197: | public function __construct( |
| 198: | private readonly Container $container, |
| 199: | private readonly ReflectionProvider $reflectionProvider, |
| 200: | #[AutowiredExtensions(of: FunctionParameterOutTypeExtension::class)] |
| 201: | private readonly ExtensionsCollection $functionParameterOutTypeExtensions, |
| 202: | #[AutowiredExtensions(of: MethodParameterOutTypeExtension::class)] |
| 203: | private readonly ExtensionsCollection $methodParameterOutTypeExtensions, |
| 204: | #[AutowiredExtensions(of: StaticMethodParameterOutTypeExtension::class)] |
| 205: | private readonly ExtensionsCollection $staticMethodParameterOutTypeExtensions, |
| 206: | private readonly FileTypeMapper $fileTypeMapper, |
| 207: | #[AutowiredExtensions(of: ReadWritePropertiesExtension::class)] |
| 208: | private readonly ExtensionsCollection $readWritePropertiesExtensions, |
| 209: | #[AutowiredExtensions(of: FunctionParameterClosureThisExtension::class)] |
| 210: | private readonly ExtensionsCollection $functionParameterClosureThisExtensions, |
| 211: | #[AutowiredExtensions(of: MethodParameterClosureThisExtension::class)] |
| 212: | private readonly ExtensionsCollection $methodParameterClosureThisExtensions, |
| 213: | #[AutowiredExtensions(of: StaticMethodParameterClosureThisExtension::class)] |
| 214: | private readonly ExtensionsCollection $staticMethodParameterClosureThisExtensions, |
| 215: | #[AutowiredExtensions(of: FunctionParameterClosureTypeExtension::class)] |
| 216: | private readonly ExtensionsCollection $functionParameterClosureTypeExtensions, |
| 217: | #[AutowiredExtensions(of: MethodParameterClosureTypeExtension::class)] |
| 218: | private readonly ExtensionsCollection $methodParameterClosureTypeExtensions, |
| 219: | #[AutowiredExtensions(of: StaticMethodParameterClosureTypeExtension::class)] |
| 220: | private readonly ExtensionsCollection $staticMethodParameterClosureTypeExtensions, |
| 221: | #[AutowiredExtensions(of: PerFileAnalysisResettable::class)] |
| 222: | private readonly ExtensionsCollection $perFileAnalysisResettables, |
| 223: | #[AutowiredParameter] |
| 224: | private readonly bool $polluteScopeWithLoopInitialAssignments, |
| 225: | #[AutowiredParameter] |
| 226: | private readonly bool $polluteScopeWithAlwaysIterableForeach, |
| 227: | #[AutowiredParameter(ref: '%exceptions.implicitThrows%')] |
| 228: | private readonly bool $implicitThrows, |
| 229: | #[AutowiredParameter] |
| 230: | private readonly bool $treatPhpDocTypesAsCertain, |
| 231: | private readonly ExpressionResultFactory $expressionResultFactory, |
| 232: | ) |
| 233: | { |
| 234: | self::$guardNewWorld = getenv('PHPSTAN_GUARD_NW') === '1'; |
| 235: | } |
| 236: | |
| 237: | |
| 238: | |
| 239: | |
| 240: | |
| 241: | public function setAnalysedFiles(array $files): void |
| 242: | { |
| 243: | $this->analysedFiles = array_fill_keys($files, true); |
| 244: | } |
| 245: | |
| 246: | |
| 247: | |
| 248: | |
| 249: | |
| 250: | |
| 251: | |
| 252: | |
| 253: | |
| 254: | |
| 255: | |
| 256: | |
| 257: | private function getNonNullabilityHelper(): NonNullabilityHelper |
| 258: | { |
| 259: | return $this->nonNullabilityHelper ??= $this->container->getByType(NonNullabilityHelper::class); |
| 260: | } |
| 261: | |
| 262: | public function resetPerFileAnalysisState(): void |
| 263: | { |
| 264: | foreach ($this->perFileAnalysisResettables->getAll() as $resettableService) { |
| 265: | $resettableService->resetFileAnalysisState(); |
| 266: | } |
| 267: | } |
| 268: | |
| 269: | |
| 270: | |
| 271: | |
| 272: | |
| 273: | |
| 274: | public function processNodes( |
| 275: | array $nodes, |
| 276: | MutatingScope $scope, |
| 277: | callable $nodeCallback, |
| 278: | ): void |
| 279: | { |
| 280: | $scope = $scope->toWalkScope(); |
| 281: | if (self::$guardNewWorld) { |
| 282: | self::$guardRealExprIds = []; |
| 283: | self::$guardProcessedExprIds = []; |
| 284: | foreach ((new NodeFinder())->findInstanceOf($nodes, Expr::class) as $realExpr) { |
| 285: | self::$guardRealExprIds[spl_object_id($realExpr)] = true; |
| 286: | } |
| 287: | } |
| 288: | |
| 289: | $expressionResultStorage = new ExpressionResultStorage(); |
| 290: | $scope->pushExpressionResultStorage($expressionResultStorage); |
| 291: | |
| 292: | |
| 293: | $gatherers = $this->nodeGatherers; |
| 294: | $this->nodeGatherers = []; |
| 295: | try { |
| 296: | $this->processNodesWithStorage($nodes, $scope, $expressionResultStorage, $nodeCallback); |
| 297: | } finally { |
| 298: | $this->nodeGatherers = $gatherers; |
| 299: | $scope->popExpressionResultStorage(); |
| 300: | } |
| 301: | } |
| 302: | |
| 303: | |
| 304: | |
| 305: | |
| 306: | |
| 307: | private function processNodesWithStorage( |
| 308: | array $nodes, |
| 309: | MutatingScope $scope, |
| 310: | ExpressionResultStorage $expressionResultStorage, |
| 311: | callable $nodeCallback, |
| 312: | ): void |
| 313: | { |
| 314: | $alreadyTerminated = false; |
| 315: | $exitPoints = []; |
| 316: | |
| 317: | $stmts = []; |
| 318: | $stmtToNodeIndex = []; |
| 319: | foreach ($nodes as $i => $node) { |
| 320: | if (!($node instanceof Node\Stmt)) { |
| 321: | continue; |
| 322: | } |
| 323: | |
| 324: | $stmtToNodeIndex[count($stmts)] = $i; |
| 325: | $stmts[] = $node; |
| 326: | } |
| 327: | |
| 328: | $dummyParent = new Node\Stmt\Nop(); |
| 329: | foreach ($stmts as $si => $node) { |
| 330: | if ($alreadyTerminated && !($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\Label)) { |
| 331: | continue; |
| 332: | } |
| 333: | |
| 334: | $nestedLabelNames = $node->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE); |
| 335: | if ($nestedLabelNames !== null) { |
| 336: | $scope = $this->resolveBackwardGotoScope( |
| 337: | $dummyParent, |
| 338: | [$node], |
| 339: | $scope, |
| 340: | $expressionResultStorage, |
| 341: | StatementContext::createDeep(), |
| 342: | static fn (string $name): bool => isset($nestedLabelNames[$name]), |
| 343: | false, |
| 344: | ); |
| 345: | } |
| 346: | |
| 347: | $statementResult = $this->processStmtNode($node, $scope, $expressionResultStorage, $nodeCallback, StatementContext::createTopLevel()); |
| 348: | $scope = $statementResult->getScope(); |
| 349: | |
| 350: | if ($node instanceof Node\Stmt\Label) { |
| 351: | $labelName = $node->name->toString(); |
| 352: | |
| 353: | [$scope, $alreadyTerminated, $exitPoints] = $this->mergeForwardGotoExitPoints( |
| 354: | $labelName, |
| 355: | $scope, |
| 356: | $alreadyTerminated, |
| 357: | $exitPoints, |
| 358: | ); |
| 359: | |
| 360: | if ($alreadyTerminated) { |
| 361: | continue; |
| 362: | } |
| 363: | |
| 364: | if ($node->getAttribute(GotoLabelVisitor::HAS_BACKWARD_GOTO_ATTRIBUTE) === true) { |
| 365: | $scope = $this->resolveBackwardGotoScope( |
| 366: | $dummyParent, |
| 367: | array_slice($stmts, $si + 1), |
| 368: | $scope, |
| 369: | $expressionResultStorage, |
| 370: | StatementContext::createDeep(), |
| 371: | static fn (string $name): bool => $name === $labelName, |
| 372: | true, |
| 373: | ); |
| 374: | } |
| 375: | } |
| 376: | |
| 377: | $exitPoints = array_merge($exitPoints, $statementResult->getExitPoints()); |
| 378: | |
| 379: | if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) { |
| 380: | continue; |
| 381: | } |
| 382: | |
| 383: | $alreadyTerminated = true; |
| 384: | $nextStmts = $this->getNextUnreachableStatements(array_slice($nodes, $stmtToNodeIndex[$si] + 1), true); |
| 385: | $this->processUnreachableStatement($nextStmts, $scope, $expressionResultStorage, $nodeCallback); |
| 386: | } |
| 387: | } |
| 388: | |
| 389: | |
| 390: | public function findSettledExpressionResult(ExpressionResultStorage $storage, Expr $expr): ?ExpressionResult |
| 391: | { |
| 392: | return $storage->findExpressionResult($expr); |
| 393: | } |
| 394: | |
| 395: | |
| 396: | protected function createEagerExpressionResult(MutatingScope $scope, Expr $expr, Type $type, Type $nativeType): ExpressionResult |
| 397: | { |
| 398: | return $this->expressionResultFactory->create( |
| 399: | $scope, |
| 400: | beforeScope: $scope, |
| 401: | expr: $expr, |
| 402: | hasYield: false, |
| 403: | isAlwaysTerminating: false, |
| 404: | throwPoints: [], |
| 405: | impurePoints: [], |
| 406: | typeCallback: null, |
| 407: | specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), |
| 408: | type: $type, |
| 409: | nativeType: $nativeType, |
| 410: | ); |
| 411: | } |
| 412: | |
| 413: | public function storeExpressionResult(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $expressionResult): void |
| 414: | { |
| 415: | if (self::$guardNewWorld) { |
| 416: | self::$guardProcessedExprIds[spl_object_id($expr)] = true; |
| 417: | } |
| 418: | |
| 419: | $storage->storeExpressionResult($expr, $expressionResult); |
| 420: | } |
| 421: | |
| 422: | |
| 423: | |
| 424: | |
| 425: | |
| 426: | |
| 427: | |
| 428: | |
| 429: | |
| 430: | |
| 431: | public function narrowScopeWithCondition(MutatingScope $scope, Expr $expr, TypeSpecifierContext $context): MutatingScope |
| 432: | { |
| 433: | $specifiedTypes = $scope->specifyTypesOfNewWorldHandlerNode($expr, $context); |
| 434: | |
| 435: | return $scope->applySpecifiedTypes($specifiedTypes); |
| 436: | } |
| 437: | |
| 438: | |
| 439: | |
| 440: | |
| 441: | |
| 442: | private function resolveBackwardGotoScope( |
| 443: | Node $parentNode, |
| 444: | array $bodyStmts, |
| 445: | MutatingScope $scope, |
| 446: | ExpressionResultStorage $storage, |
| 447: | StatementContext $context, |
| 448: | Closure $gotoNameMatcher, |
| 449: | bool $mergeBodyScopeEachIteration, |
| 450: | ): MutatingScope |
| 451: | { |
| 452: | $bodyScope = $scope; |
| 453: | $count = 0; |
| 454: | $prevEntryScope = null; |
| 455: | do { |
| 456: | $prevScope = $bodyScope; |
| 457: | if ($mergeBodyScopeEachIteration) { |
| 458: | $bodyScope = $bodyScope->mergeWith($scope); |
| 459: | } |
| 460: | if ($prevEntryScope !== null && $bodyScope->equals($prevEntryScope)) { |
| 461: | |
| 462: | |
| 463: | $bodyScope = $prevScope; |
| 464: | break; |
| 465: | } |
| 466: | $prevEntryScope = $bodyScope; |
| 467: | $tempStorage = $storage->duplicate(); |
| 468: | $bodyScopeResult = $this->processStmtNodesInternal( |
| 469: | $parentNode, |
| 470: | $bodyStmts, |
| 471: | $bodyScope, |
| 472: | $tempStorage, |
| 473: | new NoopNodeCallback(), |
| 474: | $context, |
| 475: | ); |
| 476: | |
| 477: | $gotoScope = null; |
| 478: | foreach ($bodyScopeResult->getExitPoints() as $ep) { |
| 479: | $epStmt = $ep->getStatement(); |
| 480: | if (!($epStmt instanceof Goto_) || !$gotoNameMatcher($epStmt->name->toString())) { |
| 481: | continue; |
| 482: | } |
| 483: | |
| 484: | $gotoScope = $gotoScope === null ? $ep->getScope() : $gotoScope->mergeWith($ep->getScope()); |
| 485: | } |
| 486: | |
| 487: | if ($gotoScope !== null) { |
| 488: | $bodyScope = $scope->mergeWith($gotoScope); |
| 489: | } |
| 490: | |
| 491: | if ($bodyScope->equals($prevScope)) { |
| 492: | break; |
| 493: | } |
| 494: | |
| 495: | if ($count >= self::GENERALIZE_AFTER_ITERATION) { |
| 496: | $bodyScope = $prevScope->generalizeWith($bodyScope); |
| 497: | } |
| 498: | $count++; |
| 499: | } while ($count < self::LOOP_SCOPE_ITERATIONS); |
| 500: | |
| 501: | return $bodyScope; |
| 502: | } |
| 503: | |
| 504: | |
| 505: | |
| 506: | |
| 507: | |
| 508: | private function mergeForwardGotoExitPoints( |
| 509: | string $labelName, |
| 510: | MutatingScope $scope, |
| 511: | bool $alreadyTerminated, |
| 512: | array $exitPoints, |
| 513: | ): array |
| 514: | { |
| 515: | $newExitPoints = []; |
| 516: | foreach ($exitPoints as $exitPoint) { |
| 517: | $exitStmt = $exitPoint->getStatement(); |
| 518: | if ($exitStmt instanceof Goto_ && $exitStmt->name->toString() === $labelName) { |
| 519: | if ($alreadyTerminated) { |
| 520: | $scope = $exitPoint->getScope(); |
| 521: | $alreadyTerminated = false; |
| 522: | } else { |
| 523: | $scope = $scope->mergeWith($exitPoint->getScope()); |
| 524: | } |
| 525: | } else { |
| 526: | $newExitPoints[] = $exitPoint; |
| 527: | } |
| 528: | } |
| 529: | |
| 530: | return [$scope, $alreadyTerminated, $newExitPoints]; |
| 531: | } |
| 532: | |
| 533: | |
| 534: | |
| 535: | |
| 536: | |
| 537: | private function processUnreachableStatement(array $nextStmts, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback): void |
| 538: | { |
| 539: | if ($nextStmts === []) { |
| 540: | return; |
| 541: | } |
| 542: | |
| 543: | $unreachableStatement = null; |
| 544: | $nextStatements = []; |
| 545: | |
| 546: | foreach ($nextStmts as $key => $nextStmt) { |
| 547: | if ($key === 0) { |
| 548: | $unreachableStatement = $nextStmt; |
| 549: | continue; |
| 550: | } |
| 551: | |
| 552: | $nextStatements[] = $nextStmt; |
| 553: | } |
| 554: | |
| 555: | if (!$unreachableStatement instanceof Node\Stmt) { |
| 556: | return; |
| 557: | } |
| 558: | |
| 559: | $this->callNodeCallback($nodeCallback, new UnreachableStatementNode($unreachableStatement, $nextStatements), $scope, $storage); |
| 560: | } |
| 561: | |
| 562: | |
| 563: | |
| 564: | |
| 565: | |
| 566: | |
| 567: | public function processStmtNodes( |
| 568: | Node $parentNode, |
| 569: | array $stmts, |
| 570: | MutatingScope $scope, |
| 571: | callable $nodeCallback, |
| 572: | StatementContext $context, |
| 573: | ): StatementResult |
| 574: | { |
| 575: | |
| 576: | |
| 577: | |
| 578: | |
| 579: | $scope = $scope->toWalkScope(); |
| 580: | $storage = new ExpressionResultStorage(); |
| 581: | $scope->pushExpressionResultStorage($storage); |
| 582: | |
| 583: | |
| 584: | |
| 585: | $gatherers = $this->nodeGatherers; |
| 586: | $this->nodeGatherers = []; |
| 587: | try { |
| 588: | return $this->processStmtNodesInternal( |
| 589: | $parentNode, |
| 590: | $stmts, |
| 591: | $scope, |
| 592: | $storage, |
| 593: | $nodeCallback, |
| 594: | $context, |
| 595: | )->toPublic(); |
| 596: | } finally { |
| 597: | $this->nodeGatherers = $gatherers; |
| 598: | $scope->popExpressionResultStorage(); |
| 599: | } |
| 600: | } |
| 601: | |
| 602: | |
| 603: | |
| 604: | |
| 605: | |
| 606: | public function processStmtNodesInternal( |
| 607: | Node $parentNode, |
| 608: | array $stmts, |
| 609: | MutatingScope $scope, |
| 610: | ExpressionResultStorage $storage, |
| 611: | callable $nodeCallback, |
| 612: | StatementContext $context, |
| 613: | ): InternalStatementResult |
| 614: | { |
| 615: | |
| 616: | |
| 617: | |
| 618: | |
| 619: | |
| 620: | |
| 621: | $pushStorage = $scope->getCurrentExpressionResultStorage() !== $storage; |
| 622: | if ($pushStorage) { |
| 623: | $scope->pushExpressionResultStorage($storage); |
| 624: | } |
| 625: | try { |
| 626: | return $this->doProcessStmtNodes($parentNode, $stmts, $scope, $storage, $nodeCallback, $context); |
| 627: | } finally { |
| 628: | if ($pushStorage) { |
| 629: | $scope->popExpressionResultStorage(); |
| 630: | } |
| 631: | } |
| 632: | } |
| 633: | |
| 634: | |
| 635: | |
| 636: | |
| 637: | |
| 638: | private function doProcessStmtNodes( |
| 639: | Node $parentNode, |
| 640: | array $stmts, |
| 641: | MutatingScope $scope, |
| 642: | ExpressionResultStorage $storage, |
| 643: | callable $nodeCallback, |
| 644: | StatementContext $context, |
| 645: | ): InternalStatementResult |
| 646: | { |
| 647: | $exitPoints = []; |
| 648: | $throwPoints = []; |
| 649: | $impurePoints = []; |
| 650: | $alreadyTerminated = false; |
| 651: | $hasYield = false; |
| 652: | $stmtCount = count($stmts); |
| 653: | $shouldCheckLastStatement = $parentNode instanceof Node\Stmt\Function_ |
| 654: | || $parentNode instanceof Node\Stmt\ClassMethod |
| 655: | || $parentNode instanceof PropertyHookStatementNode |
| 656: | || $parentNode instanceof Expr\Closure; |
| 657: | |
| 658: | foreach ($stmts as $i => $stmt) { |
| 659: | if ($alreadyTerminated && !($stmt instanceof Node\Stmt\Function_ || $stmt instanceof Node\Stmt\ClassLike || $stmt instanceof Node\Stmt\Label)) { |
| 660: | continue; |
| 661: | } |
| 662: | |
| 663: | $isLast = $i === $stmtCount - 1; |
| 664: | |
| 665: | $nestedLabelNames = $stmt->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE); |
| 666: | if ($nestedLabelNames !== null && $context->isTopLevel()) { |
| 667: | $scope = $this->resolveBackwardGotoScope( |
| 668: | $parentNode, |
| 669: | [$stmt], |
| 670: | $scope, |
| 671: | $storage, |
| 672: | $context->enterDeep(), |
| 673: | static fn (string $name): bool => isset($nestedLabelNames[$name]), |
| 674: | false, |
| 675: | ); |
| 676: | } |
| 677: | |
| 678: | $statementResult = $this->processStmtNode( |
| 679: | $stmt, |
| 680: | $scope, |
| 681: | $storage, |
| 682: | $nodeCallback, |
| 683: | $context, |
| 684: | ); |
| 685: | $scope = $statementResult->getScope(); |
| 686: | $hasYield = $hasYield || $statementResult->hasYield(); |
| 687: | |
| 688: | if ($stmt instanceof Node\Stmt\Label) { |
| 689: | $labelName = $stmt->name->toString(); |
| 690: | |
| 691: | [$scope, $alreadyTerminated, $exitPoints] = $this->mergeForwardGotoExitPoints( |
| 692: | $labelName, |
| 693: | $scope, |
| 694: | $alreadyTerminated, |
| 695: | $exitPoints, |
| 696: | ); |
| 697: | |
| 698: | if ($alreadyTerminated) { |
| 699: | continue; |
| 700: | } |
| 701: | |
| 702: | if ($stmt->getAttribute(GotoLabelVisitor::HAS_BACKWARD_GOTO_ATTRIBUTE) === true && $context->isTopLevel()) { |
| 703: | $scope = $this->resolveBackwardGotoScope( |
| 704: | $parentNode, |
| 705: | array_slice($stmts, $i + 1), |
| 706: | $scope, |
| 707: | $storage, |
| 708: | $context->enterDeep(), |
| 709: | static fn (string $name): bool => $name === $labelName, |
| 710: | true, |
| 711: | ); |
| 712: | } |
| 713: | } |
| 714: | |
| 715: | if ($shouldCheckLastStatement && $isLast) { |
| 716: | $endStatements = $statementResult->getEndStatements(); |
| 717: | if (count($endStatements) > 0) { |
| 718: | foreach ($endStatements as $endStatement) { |
| 719: | $endStatementResult = $endStatement->getResult(); |
| 720: | $this->callNodeCallback($nodeCallback, new ExecutionEndNode( |
| 721: | $endStatement->getStatement(), |
| 722: | (new InternalStatementResult( |
| 723: | $endStatementResult->getScope(), |
| 724: | $hasYield, |
| 725: | $endStatementResult->isAlwaysTerminating(), |
| 726: | $endStatementResult->getExitPoints(), |
| 727: | $endStatementResult->getThrowPoints(), |
| 728: | $endStatementResult->getImpurePoints(), |
| 729: | ))->toPublic(), |
| 730: | $parentNode->getReturnType() !== null, |
| 731: | $this->readEndStatementExprResult($endStatement->getStatement(), $storage), |
| 732: | ), $endStatementResult->getScope(), $storage); |
| 733: | } |
| 734: | } else { |
| 735: | $this->callNodeCallback($nodeCallback, new ExecutionEndNode( |
| 736: | $stmt, |
| 737: | (new InternalStatementResult( |
| 738: | $scope, |
| 739: | $hasYield, |
| 740: | $statementResult->isAlwaysTerminating(), |
| 741: | $statementResult->getExitPoints(), |
| 742: | $statementResult->getThrowPoints(), |
| 743: | $statementResult->getImpurePoints(), |
| 744: | ))->toPublic(), |
| 745: | $parentNode->getReturnType() !== null, |
| 746: | $this->readEndStatementExprResult($stmt, $storage), |
| 747: | ), $scope, $storage); |
| 748: | } |
| 749: | } |
| 750: | |
| 751: | $exitPoints = array_merge($exitPoints, $statementResult->getExitPoints()); |
| 752: | $throwPoints = array_merge($throwPoints, $statementResult->getThrowPoints()); |
| 753: | $impurePoints = array_merge($impurePoints, $statementResult->getImpurePoints()); |
| 754: | |
| 755: | if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) { |
| 756: | continue; |
| 757: | } |
| 758: | |
| 759: | $alreadyTerminated = true; |
| 760: | $nextStmts = $this->getNextUnreachableStatements(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_); |
| 761: | $this->processUnreachableStatement($nextStmts, $scope, $storage, $nodeCallback); |
| 762: | } |
| 763: | |
| 764: | $statementResult = new InternalStatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints, $impurePoints); |
| 765: | if ($stmtCount === 0 && $shouldCheckLastStatement) { |
| 766: | $returnTypeNode = $parentNode->getReturnType(); |
| 767: | if ($parentNode instanceof Expr\Closure) { |
| 768: | $parentNode = new Node\Stmt\Expression($parentNode, $parentNode->getAttributes()); |
| 769: | } |
| 770: | |
| 771: | |
| 772: | $this->callNodeCallback($nodeCallback, new ExecutionEndNode( |
| 773: | $parentNode, |
| 774: | $statementResult->toPublic(), |
| 775: | $returnTypeNode !== null, |
| 776: | ), $scope, $storage); |
| 777: | } |
| 778: | |
| 779: | return $statementResult; |
| 780: | } |
| 781: | |
| 782: | |
| 783: | |
| 784: | |
| 785: | public function processStmtNode( |
| 786: | Node\Stmt $stmt, |
| 787: | MutatingScope $scope, |
| 788: | ExpressionResultStorage $storage, |
| 789: | callable $nodeCallback, |
| 790: | StatementContext $context, |
| 791: | ): InternalStatementResult |
| 792: | { |
| 793: | $overridingThrowPoints = null; |
| 794: | if ( |
| 795: | !$stmt instanceof Static_ |
| 796: | && !$stmt instanceof Node\Stmt\Global_ |
| 797: | && !$stmt instanceof Node\Stmt\Property |
| 798: | && !$stmt instanceof Node\Stmt\ClassConst |
| 799: | && !$stmt instanceof Node\Stmt\Const_ |
| 800: | && !$stmt instanceof Node\Stmt\ClassLike |
| 801: | && !$stmt instanceof Node\Stmt\Function_ |
| 802: | && !$stmt instanceof Node\Stmt\ClassMethod |
| 803: | ) { |
| 804: | if (!$stmt instanceof Foreach_) { |
| 805: | $scope = $this->processStmtVarAnnotation($scope, $storage, $stmt, null, $nodeCallback); |
| 806: | } |
| 807: | $overridingThrowPoints = $this->getOverridingThrowPoints($stmt, $scope); |
| 808: | } |
| 809: | |
| 810: | if ($stmt instanceof Node\Stmt\ClassMethod) { |
| 811: | |
| 812: | |
| 813: | if (!$scope->isInClass()) { |
| 814: | throw new ShouldNotHappenException(); |
| 815: | } |
| 816: | if ( |
| 817: | $scope->isInTrait() |
| 818: | && $scope->getClassReflection()->hasNativeMethod($stmt->name->toString()) |
| 819: | ) { |
| 820: | $methodReflection = $scope->getClassReflection()->getNativeMethod($stmt->name->toString()); |
| 821: | if ($methodReflection instanceof NativeMethodReflection) { |
| 822: | return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []); |
| 823: | } |
| 824: | if ($methodReflection instanceof PhpMethodReflection) { |
| 825: | $declaringTrait = $methodReflection->getDeclaringTrait(); |
| 826: | if ($declaringTrait === null || $declaringTrait->getName() !== $scope->getTraitReflection()->getName()) { |
| 827: | return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []); |
| 828: | } |
| 829: | } |
| 830: | } |
| 831: | } |
| 832: | |
| 833: | |
| 834: | |
| 835: | |
| 836: | |
| 837: | |
| 838: | $deferredStmtCallback = $stmt instanceof Return_ || $stmt instanceof Node\Stmt\Expression || $stmt instanceof Echo_ |
| 839: | || $stmt instanceof If_ || $stmt instanceof Switch_ || $stmt instanceof Foreach_ |
| 840: | || $stmt instanceof Node\Stmt\Unset_ || $stmt instanceof Node\Stmt\ClassConst |
| 841: | || $stmt instanceof Node\Stmt\Const_ || $stmt instanceof Node\Stmt\While_ |
| 842: | || $stmt instanceof Node\Stmt\Do_; |
| 843: | if (!$deferredStmtCallback) { |
| 844: | $this->callNodeCallback($nodeCallback, $stmt, $scope, $storage); |
| 845: | } |
| 846: | |
| 847: | $stmtHandler = StmtHandlerRegistry::resolve($stmt, $this->container); |
| 848: | if ($stmtHandler !== null) { |
| 849: | $stmtResult = $stmtHandler->processStmt($this, $stmt, $scope, $storage, $nodeCallback, $context); |
| 850: | if ($overridingThrowPoints !== null) { |
| 851: | return new InternalStatementResult( |
| 852: | $stmtResult->getScope(), |
| 853: | hasYield: $stmtResult->hasYield(), |
| 854: | isAlwaysTerminating: $stmtResult->isAlwaysTerminating(), |
| 855: | exitPoints: $stmtResult->getExitPoints(), |
| 856: | throwPoints: $overridingThrowPoints, |
| 857: | impurePoints: $stmtResult->getImpurePoints(), |
| 858: | endStatements: $stmtResult->getEndStatements(), |
| 859: | ); |
| 860: | } |
| 861: | |
| 862: | return $stmtResult; |
| 863: | } |
| 864: | |
| 865: | |
| 866: | return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: $overridingThrowPoints ?? [], impurePoints: []); |
| 867: | } |
| 868: | |
| 869: | |
| 870: | |
| 871: | |
| 872: | private function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $scope): ?array |
| 873: | { |
| 874: | foreach ($statement->getComments() as $comment) { |
| 875: | if (!$comment instanceof Doc) { |
| 876: | continue; |
| 877: | } |
| 878: | |
| 879: | $function = $scope->getFunction(); |
| 880: | $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( |
| 881: | $scope->getFile(), |
| 882: | $scope->isInClass() ? $scope->getClassReflection()->getName() : null, |
| 883: | $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, |
| 884: | $function !== null ? $function->getName() : null, |
| 885: | $comment->getText(), |
| 886: | ); |
| 887: | |
| 888: | $throwsTag = $resolvedPhpDoc->getThrowsTag(); |
| 889: | if ($throwsTag !== null) { |
| 890: | $throwsType = $throwsTag->getType(); |
| 891: | if ($throwsType->isVoid()->yes()) { |
| 892: | return []; |
| 893: | } |
| 894: | |
| 895: | return [InternalThrowPoint::createExplicit($scope, $throwsType, $statement, false)]; |
| 896: | } |
| 897: | } |
| 898: | |
| 899: | return null; |
| 900: | } |
| 901: | |
| 902: | public function isAnalysedFile(string $fileName): bool |
| 903: | { |
| 904: | return isset($this->analysedFiles[$fileName]); |
| 905: | } |
| 906: | |
| 907: | public function shouldPolluteScopeWithLoopInitialAssignments(): bool |
| 908: | { |
| 909: | return $this->polluteScopeWithLoopInitialAssignments; |
| 910: | } |
| 911: | |
| 912: | public function shouldPolluteScopeWithAlwaysIterableForeach(): bool |
| 913: | { |
| 914: | return $this->polluteScopeWithAlwaysIterableForeach; |
| 915: | } |
| 916: | |
| 917: | public function shouldTreatPhpDocTypesAsCertain(): bool |
| 918: | { |
| 919: | return $this->treatPhpDocTypesAsCertain; |
| 920: | } |
| 921: | |
| 922: | |
| 923: | public function getReadWritePropertiesExtensions(): ExtensionsCollection |
| 924: | { |
| 925: | return $this->readWritePropertiesExtensions; |
| 926: | } |
| 927: | |
| 928: | public function lookForSetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope |
| 929: | { |
| 930: | return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr)); |
| 931: | } |
| 932: | |
| 933: | public function lookForUnsetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope |
| 934: | { |
| 935: | return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->unsetAllowedUndefinedExpression($expr)); |
| 936: | } |
| 937: | |
| 938: | |
| 939: | |
| 940: | |
| 941: | private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope |
| 942: | { |
| 943: | if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) { |
| 944: | $scope = $callback($scope, $expr); |
| 945: | } |
| 946: | |
| 947: | if ($expr instanceof ArrayDimFetch) { |
| 948: | $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback); |
| 949: | } elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch || $expr instanceof Expr\NullsafeMethodCall) { |
| 950: | $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback); |
| 951: | } elseif ($expr instanceof StaticPropertyFetch && $expr->class instanceof Expr) { |
| 952: | $scope = $this->lookForExpressionCallback($scope, $expr->class, $callback); |
| 953: | } elseif ($expr instanceof List_) { |
| 954: | foreach ($expr->items as $item) { |
| 955: | if ($item === null) { |
| 956: | continue; |
| 957: | } |
| 958: | |
| 959: | $scope = $this->lookForExpressionCallback($scope, $item->value, $callback); |
| 960: | } |
| 961: | } |
| 962: | |
| 963: | return $scope; |
| 964: | } |
| 965: | |
| 966: | |
| 967: | |
| 968: | |
| 969: | |
| 970: | |
| 971: | |
| 972: | |
| 973: | |
| 974: | |
| 975: | |
| 976: | |
| 977: | |
| 978: | |
| 979: | |
| 980: | |
| 981: | public function processExprNodeConsumingStored(Node\Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult |
| 982: | { |
| 983: | $previous = $this->consumeStoredExpressionResults; |
| 984: | $this->consumeStoredExpressionResults = true; |
| 985: | try { |
| 986: | return $this->processExprNode($stmt, $expr, $scope, $storage, $nodeCallback, $context); |
| 987: | } finally { |
| 988: | $this->consumeStoredExpressionResults = $previous; |
| 989: | } |
| 990: | } |
| 991: | |
| 992: | public function processExprOnDemand(Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage): ExpressionResult |
| 993: | { |
| 994: | |
| 995: | |
| 996: | |
| 997: | |
| 998: | if ( |
| 999: | ExprHandlerRegistry::resolve($expr, $this->container) === null |
| 1000: | && !($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) |
| 1001: | ) { |
| 1002: | return $this->createEagerExpressionResult($scope, $expr, new MixedType(), new MixedType()); |
| 1003: | } |
| 1004: | |
| 1005: | |
| 1006: | |
| 1007: | |
| 1008: | |
| 1009: | |
| 1010: | $previous = $this->returnStoredExpressionResults; |
| 1011: | $this->returnStoredExpressionResults = true; |
| 1012: | $scope->pushExpressionResultStorage($storage); |
| 1013: | try { |
| 1014: | return $this->processExprNode( |
| 1015: | new Node\Stmt\Expression($expr), |
| 1016: | $expr, |
| 1017: | $scope, |
| 1018: | $storage, |
| 1019: | new NoopNodeCallback(), |
| 1020: | ExpressionContext::createTopLevel(), |
| 1021: | ); |
| 1022: | } finally { |
| 1023: | $scope->popExpressionResultStorage(); |
| 1024: | $this->returnStoredExpressionResults = $previous; |
| 1025: | } |
| 1026: | } |
| 1027: | |
| 1028: | |
| 1029: | |
| 1030: | |
| 1031: | |
| 1032: | |
| 1033: | |
| 1034: | |
| 1035: | public function processIndependentPassExpr(Expr $expr, MutatingScope $scope): ExpressionResult |
| 1036: | { |
| 1037: | return $this->processExprOnDemand($expr, $scope, new ExpressionResultStorage()); |
| 1038: | } |
| 1039: | |
| 1040: | |
| 1041: | |
| 1042: | |
| 1043: | |
| 1044: | |
| 1045: | |
| 1046: | |
| 1047: | public function readStoredResult(Expr $expr, ExpressionResultStorage $storage): ExpressionResult |
| 1048: | { |
| 1049: | $result = $storage->findExpressionResult($expr); |
| 1050: | if ($result === null) { |
| 1051: | throw new ShouldNotHappenException(sprintf( |
| 1052: | '%s on line %d has no stored ExpressionResult - it was not processed by processExprNode().', |
| 1053: | get_class($expr), |
| 1054: | $expr->getStartLine(), |
| 1055: | )); |
| 1056: | } |
| 1057: | |
| 1058: | return $result; |
| 1059: | } |
| 1060: | |
| 1061: | |
| 1062: | |
| 1063: | |
| 1064: | |
| 1065: | |
| 1066: | |
| 1067: | |
| 1068: | private function readEndStatementExprResult(Node\Stmt $stmt, ExpressionResultStorage $storage): ?ExpressionResult |
| 1069: | { |
| 1070: | if (!$stmt instanceof Node\Stmt\Expression) { |
| 1071: | return null; |
| 1072: | } |
| 1073: | |
| 1074: | return $storage->findExpressionResult($stmt->expr); |
| 1075: | } |
| 1076: | |
| 1077: | |
| 1078: | |
| 1079: | |
| 1080: | |
| 1081: | |
| 1082: | |
| 1083: | |
| 1084: | |
| 1085: | private function readArgResult(array $argResults, Expr $argValue): ExpressionResult |
| 1086: | { |
| 1087: | $result = $argResults[spl_object_id($argValue)] ?? null; |
| 1088: | if ($result === null) { |
| 1089: | throw new ShouldNotHappenException(sprintf( |
| 1090: | '%s on line %d has no captured ExpressionResult - it was not processed as an argument by processArgs().', |
| 1091: | get_class($argValue), |
| 1092: | $argValue->getStartLine(), |
| 1093: | )); |
| 1094: | } |
| 1095: | |
| 1096: | return $result; |
| 1097: | } |
| 1098: | |
| 1099: | |
| 1100: | |
| 1101: | |
| 1102: | |
| 1103: | |
| 1104: | |
| 1105: | |
| 1106: | public function readTypeOfMaybeStored(Expr $expr, MutatingScope $scope): Type |
| 1107: | { |
| 1108: | $storage = $scope->getCurrentExpressionResultStorage(); |
| 1109: | $result = $storage !== null ? $storage->findExpressionResult($expr) : null; |
| 1110: | if ($result !== null) { |
| 1111: | return $result->getTypeOnScope($scope, $scope->nativeTypesPromoted); |
| 1112: | } |
| 1113: | |
| 1114: | return $this->readScopeStateOrSyntheticType($expr, $scope); |
| 1115: | } |
| 1116: | |
| 1117: | |
| 1118: | |
| 1119: | |
| 1120: | |
| 1121: | |
| 1122: | |
| 1123: | |
| 1124: | |
| 1125: | |
| 1126: | |
| 1127: | public function findScopeStateType(Expr $expr, MutatingScope $scope): ?Type |
| 1128: | { |
| 1129: | if ($expr instanceof Expr\Variable && is_string($expr->name)) { |
| 1130: | if ($scope->hasVariableType($expr->name)->no()) { |
| 1131: | return new ErrorType(); |
| 1132: | } |
| 1133: | |
| 1134: | return $scope->getVariableType($expr->name); |
| 1135: | } |
| 1136: | |
| 1137: | |
| 1138: | |
| 1139: | |
| 1140: | if ($expr instanceof Node\Scalar\String_ || $expr instanceof Node\Scalar\Int_ || $expr instanceof Node\Scalar\Float_) { |
| 1141: | return $scope->getStateType($expr); |
| 1142: | } |
| 1143: | |
| 1144: | |
| 1145: | |
| 1146: | |
| 1147: | if ( |
| 1148: | !$expr instanceof Expr\Closure |
| 1149: | && !$expr instanceof Expr\ArrowFunction |
| 1150: | && $scope->hasExpressionType($expr)->yes() |
| 1151: | ) { |
| 1152: | return TypeUtils::resolveLateResolvableTypes($scope->getTrackedExpressionType($expr)); |
| 1153: | } |
| 1154: | |
| 1155: | return null; |
| 1156: | } |
| 1157: | |
| 1158: | |
| 1159: | |
| 1160: | |
| 1161: | |
| 1162: | |
| 1163: | |
| 1164: | public function readScopeStateOrSyntheticType(Expr $expr, MutatingScope $scope): Type |
| 1165: | { |
| 1166: | return $this->findScopeStateType($expr, $scope) ?? $this->processSyntheticOnDemand($expr, $scope)->getTypeOnScope($scope, $scope->nativeTypesPromoted); |
| 1167: | } |
| 1168: | |
| 1169: | |
| 1170: | |
| 1171: | |
| 1172: | |
| 1173: | |
| 1174: | |
| 1175: | public function requireScopeStateType(Expr $expr, MutatingScope $scope): Type |
| 1176: | { |
| 1177: | $type = $this->findScopeStateType($expr, $scope); |
| 1178: | if ($type === null) { |
| 1179: | throw new ShouldNotHappenException(sprintf( |
| 1180: | '%s on line %d is not tracked on the scope it was pinned as tracked on.', |
| 1181: | get_class($expr), |
| 1182: | $expr->getStartLine(), |
| 1183: | )); |
| 1184: | } |
| 1185: | |
| 1186: | return $type; |
| 1187: | } |
| 1188: | |
| 1189: | |
| 1190: | |
| 1191: | |
| 1192: | |
| 1193: | |
| 1194: | |
| 1195: | |
| 1196: | private function guardAgainstUnprocessedRealNode(Expr $expr, string $caller): void |
| 1197: | { |
| 1198: | if ( |
| 1199: | !self::$guardNewWorld |
| 1200: | || !isset(self::$guardRealExprIds[spl_object_id($expr)]) |
| 1201: | || isset(self::$guardProcessedExprIds[spl_object_id($expr)]) |
| 1202: | ) { |
| 1203: | return; |
| 1204: | } |
| 1205: | |
| 1206: | throw new ShouldNotHappenException(sprintf( |
| 1207: | '%s() asked about non-synthetic %s on line %d before it was processed by processExprNode() - it should consume the node\'s ExpressionResult instead.', |
| 1208: | $caller, |
| 1209: | get_class($expr), |
| 1210: | $expr->getStartLine(), |
| 1211: | )); |
| 1212: | } |
| 1213: | |
| 1214: | |
| 1215: | |
| 1216: | |
| 1217: | |
| 1218: | |
| 1219: | |
| 1220: | |
| 1221: | public function processSyntheticOnDemand(Expr $expr, MutatingScope $scope): ExpressionResult |
| 1222: | { |
| 1223: | $this->guardAgainstUnprocessedRealNode($expr, __FUNCTION__); |
| 1224: | $current = $scope->getCurrentExpressionResultStorage() ?? new ExpressionResultStorage(); |
| 1225: | |
| 1226: | return $this->processExprOnDemand($expr, $scope, $current->duplicate()); |
| 1227: | } |
| 1228: | |
| 1229: | |
| 1230: | |
| 1231: | |
| 1232: | public function processExprNode( |
| 1233: | Node\Stmt $stmt, |
| 1234: | Expr $expr, |
| 1235: | MutatingScope $scope, |
| 1236: | ExpressionResultStorage $storage, |
| 1237: | callable $nodeCallback, |
| 1238: | ExpressionContext $context, |
| 1239: | ): ExpressionResult |
| 1240: | { |
| 1241: | if ($this->returnStoredExpressionResults || $this->consumeStoredExpressionResults) { |
| 1242: | $storedResult = $storage->findExpressionResult($expr); |
| 1243: | |
| 1244: | |
| 1245: | |
| 1246: | |
| 1247: | |
| 1248: | |
| 1249: | |
| 1250: | if ($storedResult !== null && ($this->consumeStoredExpressionResults || $storedResult->askScopeVariableStateMatches($scope, $scope->nativeTypesPromoted))) { |
| 1251: | |
| 1252: | |
| 1253: | |
| 1254: | if ($storedResult->getBeforeScope() === $scope) { |
| 1255: | return $storedResult; |
| 1256: | } |
| 1257: | |
| 1258: | $reanchored = $storedResult->atAskPosition($scope); |
| 1259: | if ($this->consumeStoredExpressionResults) { |
| 1260: | |
| 1261: | |
| 1262: | |
| 1263: | |
| 1264: | |
| 1265: | $this->storeExpressionResult($storage, $expr, $reanchored); |
| 1266: | } |
| 1267: | |
| 1268: | return $reanchored; |
| 1269: | } |
| 1270: | } |
| 1271: | |
| 1272: | return $this->processExprNodeInternal($stmt, $expr, $scope, $storage, $nodeCallback, $context); |
| 1273: | } |
| 1274: | |
| 1275: | |
| 1276: | |
| 1277: | |
| 1278: | private function processExprNodeInternal( |
| 1279: | Node\Stmt $stmt, |
| 1280: | Expr $expr, |
| 1281: | MutatingScope $scope, |
| 1282: | ExpressionResultStorage $storage, |
| 1283: | callable $nodeCallback, |
| 1284: | ExpressionContext $context, |
| 1285: | ): ExpressionResult |
| 1286: | { |
| 1287: | if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { |
| 1288: | if ($expr instanceof FuncCall) { |
| 1289: | $newExpr = new FunctionCallableNode($expr->name, $expr); |
| 1290: | } elseif ($expr instanceof MethodCall) { |
| 1291: | $newExpr = new MethodCallableNode($expr->var, $expr->name, $expr); |
| 1292: | } elseif ($expr instanceof StaticCall) { |
| 1293: | $newExpr = new StaticMethodCallableNode($expr->class, $expr->name, $expr); |
| 1294: | } elseif ($expr instanceof New_ && !$expr->class instanceof Class_) { |
| 1295: | $newExpr = new InstantiationCallableNode($expr->class, $expr); |
| 1296: | } else { |
| 1297: | throw new ShouldNotHappenException(); |
| 1298: | } |
| 1299: | |
| 1300: | $newExprResult = $this->processExprNode($stmt, $newExpr, $scope, $storage, $nodeCallback, $context); |
| 1301: | $expressionResult = $this->expressionResultFactory->create( |
| 1302: | $newExprResult->getScope(), |
| 1303: | beforeScope: $scope, |
| 1304: | expr: $expr, |
| 1305: | hasYield: $newExprResult->hasYield(), |
| 1306: | isAlwaysTerminating: $newExprResult->isAlwaysTerminating(), |
| 1307: | throwPoints: $newExprResult->getThrowPoints(), |
| 1308: | impurePoints: $newExprResult->getImpurePoints(), |
| 1309: | |
| 1310: | |
| 1311: | typeCallback: static fn (bool $nativeTypesPromoted): Type => ($nativeTypesPromoted ? $newExprResult->getNativeType() : $newExprResult->getType()), |
| 1312: | specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), |
| 1313: | ); |
| 1314: | $this->storeExpressionResult($storage, $expr, $expressionResult); |
| 1315: | return $expressionResult; |
| 1316: | } |
| 1317: | |
| 1318: | $exprHandler = ExprHandlerRegistry::resolve($expr, $this->container); |
| 1319: | if ($exprHandler !== null) { |
| 1320: | $expressionResult = $exprHandler->processExpr($this, $stmt, $expr, $scope, $storage, $nodeCallback, $context); |
| 1321: | |
| 1322: | |
| 1323: | |
| 1324: | $expressionResult = $this->getNonNullabilityHelper()->applyPendingEnsure($expr, $expressionResult); |
| 1325: | $this->storeExpressionResult($storage, $expr, $expressionResult); |
| 1326: | |
| 1327: | |
| 1328: | |
| 1329: | |
| 1330: | |
| 1331: | |
| 1332: | |
| 1333: | $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $storage, $context); |
| 1334: | |
| 1335: | |
| 1336: | |
| 1337: | if ($expr instanceof FuncCall) { |
| 1338: | $this->callNodeCallbackWithExpression($nodeCallback, new FunctionCallExpressionNode($expr, $expressionResult, $expressionResult->getArgsResult()), $scope, $storage, $context); |
| 1339: | } elseif ($expr instanceof MethodCall) { |
| 1340: | $this->callNodeCallbackWithExpression($nodeCallback, new MethodCallExpressionNode($expr, $expressionResult, $expressionResult->getArgsResult()), $scope, $storage, $context); |
| 1341: | } elseif ($expr instanceof StaticCall) { |
| 1342: | $this->callNodeCallbackWithExpression($nodeCallback, new StaticMethodCallExpressionNode($expr, $expressionResult, $expressionResult->getArgsResult()), $scope, $storage, $context); |
| 1343: | } |
| 1344: | return $expressionResult; |
| 1345: | } |
| 1346: | |
| 1347: | throw new ShouldNotHappenException(sprintf('Unhandled expr: %s', get_class($expr))); |
| 1348: | } |
| 1349: | |
| 1350: | |
| 1351: | |
| 1352: | |
| 1353: | public function getAssignedVariables(Expr $expr): array |
| 1354: | { |
| 1355: | if ($expr instanceof Expr\Variable) { |
| 1356: | if (is_string($expr->name)) { |
| 1357: | return [$expr->name]; |
| 1358: | } |
| 1359: | |
| 1360: | return []; |
| 1361: | } |
| 1362: | |
| 1363: | if ($expr instanceof Expr\List_) { |
| 1364: | $names = []; |
| 1365: | foreach ($expr->items as $item) { |
| 1366: | if ($item === null) { |
| 1367: | continue; |
| 1368: | } |
| 1369: | |
| 1370: | $names = array_merge($names, $this->getAssignedVariables($item->value)); |
| 1371: | } |
| 1372: | |
| 1373: | return $names; |
| 1374: | } |
| 1375: | |
| 1376: | if ($expr instanceof ArrayDimFetch) { |
| 1377: | return $this->getAssignedVariables($expr->var); |
| 1378: | } |
| 1379: | |
| 1380: | return []; |
| 1381: | } |
| 1382: | |
| 1383: | private const REPLAYABLE_BODY_ATTRIBUTE = 'convergenceReplayableBody'; |
| 1384: | |
| 1385: | |
| 1386: | |
| 1387: | |
| 1388: | |
| 1389: | |
| 1390: | |
| 1391: | |
| 1392: | |
| 1393: | |
| 1394: | |
| 1395: | public function isReplayableConvergenceBody(Node $loopNode, array $bodyStmts): bool |
| 1396: | { |
| 1397: | $cached = $loopNode->getAttribute(self::REPLAYABLE_BODY_ATTRIBUTE); |
| 1398: | if ($cached !== null) { |
| 1399: | return $cached; |
| 1400: | } |
| 1401: | |
| 1402: | $replayable = true; |
| 1403: | foreach ($bodyStmts as $bodyStmt) { |
| 1404: | if ($this->hasContextSensitiveConstruct($bodyStmt)) { |
| 1405: | $replayable = false; |
| 1406: | break; |
| 1407: | } |
| 1408: | } |
| 1409: | $loopNode->setAttribute(self::REPLAYABLE_BODY_ATTRIBUTE, $replayable); |
| 1410: | |
| 1411: | return $replayable; |
| 1412: | } |
| 1413: | |
| 1414: | private function hasContextSensitiveConstruct(Node $node): bool |
| 1415: | { |
| 1416: | if ($node instanceof Expr\Closure) { |
| 1417: | return false; |
| 1418: | } |
| 1419: | if ( |
| 1420: | $node instanceof Node\Stmt\While_ |
| 1421: | || $node instanceof Node\Stmt\Do_ |
| 1422: | || $node instanceof Node\Stmt\For_ |
| 1423: | || $node instanceof Foreach_ |
| 1424: | || $node instanceof Node\Stmt\Label |
| 1425: | || $node instanceof Node\Stmt\ClassLike |
| 1426: | ) { |
| 1427: | return true; |
| 1428: | } |
| 1429: | |
| 1430: | foreach ($node->getSubNodeNames() as $subNodeName) { |
| 1431: | $subNode = $node->$subNodeName; |
| 1432: | if ($subNode instanceof Node) { |
| 1433: | if ($this->hasContextSensitiveConstruct($subNode)) { |
| 1434: | return true; |
| 1435: | } |
| 1436: | } elseif (is_array($subNode)) { |
| 1437: | foreach ($subNode as $item) { |
| 1438: | if ($item instanceof Node && $this->hasContextSensitiveConstruct($item)) { |
| 1439: | return true; |
| 1440: | } |
| 1441: | } |
| 1442: | } |
| 1443: | } |
| 1444: | |
| 1445: | return false; |
| 1446: | } |
| 1447: | |
| 1448: | |
| 1449: | |
| 1450: | |
| 1451: | |
| 1452: | |
| 1453: | |
| 1454: | |
| 1455: | |
| 1456: | |
| 1457: | |
| 1458: | |
| 1459: | |
| 1460: | |
| 1461: | |
| 1462: | |
| 1463: | public function pushNodeGatherer(callable $gatherer): void |
| 1464: | { |
| 1465: | $this->nodeGatherers[] = $gatherer; |
| 1466: | } |
| 1467: | |
| 1468: | public function popNodeGatherer(): void |
| 1469: | { |
| 1470: | array_pop($this->nodeGatherers); |
| 1471: | } |
| 1472: | |
| 1473: | |
| 1474: | |
| 1475: | |
| 1476: | public function replayRecording(RecordingNodeCallback $recording, callable $nodeCallback, ExpressionResultStorage $storage, MutatingScope $scope): void |
| 1477: | { |
| 1478: | $scope->pushExpressionResultStorage($storage); |
| 1479: | try { |
| 1480: | foreach ($recording->getPairs() as [$node, $pairScope]) { |
| 1481: | if (!$pairScope instanceof MutatingScope) { |
| 1482: | throw new ShouldNotHappenException(); |
| 1483: | } |
| 1484: | |
| 1485: | |
| 1486: | foreach ($this->nodeGatherers as $gatherer) { |
| 1487: | $gatherer($node, $pairScope); |
| 1488: | } |
| 1489: | $nodeCallback($node, $pairScope->toNodeCallbackScope()); |
| 1490: | } |
| 1491: | } finally { |
| 1492: | $scope->popExpressionResultStorage(); |
| 1493: | } |
| 1494: | } |
| 1495: | |
| 1496: | |
| 1497: | |
| 1498: | |
| 1499: | public function callNodeCallbackWithExpression( |
| 1500: | callable $nodeCallback, |
| 1501: | Node $expr, |
| 1502: | MutatingScope $scope, |
| 1503: | ExpressionResultStorage $storage, |
| 1504: | ExpressionContext $context, |
| 1505: | ): void |
| 1506: | { |
| 1507: | if ($context->isDeep()) { |
| 1508: | $scope = $scope->exitFirstLevelStatements(); |
| 1509: | } |
| 1510: | $this->callNodeCallback($nodeCallback, $expr, $scope, $storage); |
| 1511: | } |
| 1512: | |
| 1513: | |
| 1514: | |
| 1515: | |
| 1516: | public function callNodeCallback( |
| 1517: | callable $nodeCallback, |
| 1518: | Node $node, |
| 1519: | MutatingScope $scope, |
| 1520: | ExpressionResultStorage $storage, |
| 1521: | ): void |
| 1522: | { |
| 1523: | |
| 1524: | |
| 1525: | |
| 1526: | |
| 1527: | |
| 1528: | |
| 1529: | foreach ($this->nodeGatherers as $gatherer) { |
| 1530: | $gatherer($node, $scope); |
| 1531: | } |
| 1532: | |
| 1533: | if ($nodeCallback instanceof NoopNodeCallback) { |
| 1534: | return; |
| 1535: | } |
| 1536: | |
| 1537: | if ($nodeCallback instanceof RecordingNodeCallback) { |
| 1538: | |
| 1539: | |
| 1540: | $nodeCallback($node, $scope); |
| 1541: | return; |
| 1542: | } |
| 1543: | |
| 1544: | |
| 1545: | |
| 1546: | |
| 1547: | $nodeCallback($node, $scope->toNodeCallbackScope()); |
| 1548: | } |
| 1549: | |
| 1550: | |
| 1551: | |
| 1552: | |
| 1553: | public function processClosureNode( |
| 1554: | Node\Stmt $stmt, |
| 1555: | Expr\Closure $expr, |
| 1556: | MutatingScope $scope, |
| 1557: | ExpressionResultStorage $storage, |
| 1558: | callable $nodeCallback, |
| 1559: | ExpressionContext $context, |
| 1560: | ?Type $passedToType, |
| 1561: | ?Type $nativePassedToType = null, |
| 1562: | ): ProcessClosureResult |
| 1563: | { |
| 1564: | return $this->processClosureNodeInternal($stmt, $expr, $scope, $storage, $nodeCallback, $context, $passedToType, $nativePassedToType); |
| 1565: | } |
| 1566: | |
| 1567: | |
| 1568: | |
| 1569: | |
| 1570: | private function processClosureNodeInternal( |
| 1571: | Node\Stmt $stmt, |
| 1572: | Expr\Closure $expr, |
| 1573: | MutatingScope $scope, |
| 1574: | ExpressionResultStorage $storage, |
| 1575: | callable $nodeCallback, |
| 1576: | ExpressionContext $context, |
| 1577: | ?Type $passedToType, |
| 1578: | ?Type $nativePassedToType = null, |
| 1579: | ): ProcessClosureResult |
| 1580: | { |
| 1581: | foreach ($expr->params as $param) { |
| 1582: | $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); |
| 1583: | } |
| 1584: | |
| 1585: | $byRefUses = []; |
| 1586: | |
| 1587: | $closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME); |
| 1588: | $callableParameters = $this->createCallableParameters($scope, $expr, $closureCallArgs, $passedToType); |
| 1589: | $nativeCallableParameters = $this->createNativeCallableParameters($scope, $expr, $closureCallArgs, $nativePassedToType); |
| 1590: | |
| 1591: | $useScope = $scope; |
| 1592: | foreach ($expr->uses as $use) { |
| 1593: | if ($use->byRef) { |
| 1594: | $byRefUses[] = $use; |
| 1595: | $useScope = $useScope->enterExpressionAssign($use->var); |
| 1596: | |
| 1597: | $inAssignRightSideVariableName = $context->getInAssignRightSideVariableName(); |
| 1598: | $inAssignRightSideExpr = $context->getInAssignRightSideExpr(); |
| 1599: | if ( |
| 1600: | $inAssignRightSideVariableName === $use->var->name |
| 1601: | && $inAssignRightSideExpr !== null |
| 1602: | ) { |
| 1603: | |
| 1604: | |
| 1605: | |
| 1606: | $inAssignRightSideType = $context->getInAssignRightSideType() ?? $this->resolveCallableTypeForScope($inAssignRightSideExpr, $scope); |
| 1607: | if ($inAssignRightSideType instanceof ClosureType) { |
| 1608: | $variableType = $inAssignRightSideType; |
| 1609: | } else { |
| 1610: | $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName); |
| 1611: | if ($alreadyHasVariableType->no()) { |
| 1612: | $variableType = TypeCombinator::union(new NullType(), $inAssignRightSideType); |
| 1613: | } else { |
| 1614: | $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType); |
| 1615: | } |
| 1616: | } |
| 1617: | $inAssignRightSideNativeType = $context->getInAssignRightSideNativeType() ?? $this->resolveCallableTypeForScope($inAssignRightSideExpr, $scope->doNotTreatPhpDocTypesAsCertain()); |
| 1618: | if ($inAssignRightSideNativeType instanceof ClosureType) { |
| 1619: | $variableNativeType = $inAssignRightSideNativeType; |
| 1620: | } else { |
| 1621: | $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName); |
| 1622: | if ($alreadyHasVariableType->no()) { |
| 1623: | $variableNativeType = TypeCombinator::union(new NullType(), $inAssignRightSideNativeType); |
| 1624: | } else { |
| 1625: | $variableNativeType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideNativeType); |
| 1626: | } |
| 1627: | } |
| 1628: | $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType, $variableNativeType, TrinaryLogic::createYes()); |
| 1629: | } |
| 1630: | } |
| 1631: | $this->processExprNode($stmt, $use->var, $useScope, $storage, $nodeCallback, $context); |
| 1632: | if (!$use->byRef) { |
| 1633: | continue; |
| 1634: | } |
| 1635: | |
| 1636: | $useScope = $useScope->exitExpressionAssign($use->var); |
| 1637: | } |
| 1638: | |
| 1639: | if ($expr->returnType !== null) { |
| 1640: | $this->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage); |
| 1641: | } |
| 1642: | |
| 1643: | $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters, $nativeCallableParameters); |
| 1644: | $closureScope = $closureScope->processClosureScope($scope, null, $byRefUses); |
| 1645: | $closureType = $closureScope->getAnonymousFunctionReflection(); |
| 1646: | if (!$closureType instanceof ClosureType) { |
| 1647: | throw new ShouldNotHappenException(); |
| 1648: | } |
| 1649: | |
| 1650: | $this->callNodeCallback($nodeCallback, new InClosureNode($closureType, $expr), $closureScope, $storage); |
| 1651: | |
| 1652: | $executionEnds = []; |
| 1653: | $gatheredReturnStatements = []; |
| 1654: | $gatheredReturnStatementsWithScope = []; |
| 1655: | $gatheredYieldStatements = []; |
| 1656: | $gatheredYieldStatementsWithScope = []; |
| 1657: | $closureImpurePoints = []; |
| 1658: | $invalidateExpressions = []; |
| 1659: | $closureStmtsGatherer = static function (Node $node, Scope $scope) use (&$executionEnds, &$gatheredReturnStatements, &$gatheredReturnStatementsWithScope, &$gatheredYieldStatements, &$gatheredYieldStatementsWithScope, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void { |
| 1660: | if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) { |
| 1661: | return; |
| 1662: | } |
| 1663: | if ($node instanceof PropertyAssignNode) { |
| 1664: | $closureImpurePoints[] = new ImpurePoint( |
| 1665: | $scope, |
| 1666: | $node, |
| 1667: | 'propertyAssign', |
| 1668: | 'property assignment', |
| 1669: | true, |
| 1670: | ); |
| 1671: | $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch()); |
| 1672: | return; |
| 1673: | } |
| 1674: | if ($node instanceof ExecutionEndNode) { |
| 1675: | $executionEnds[] = $node; |
| 1676: | return; |
| 1677: | } |
| 1678: | if ($node instanceof InvalidateExprNode) { |
| 1679: | $invalidateExpressions[] = $node; |
| 1680: | return; |
| 1681: | } |
| 1682: | if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) { |
| 1683: | $gatheredYieldStatements[] = $node; |
| 1684: | $gatheredYieldStatementsWithScope[] = [$node, $scope]; |
| 1685: | } |
| 1686: | if (!$node instanceof Return_) { |
| 1687: | return; |
| 1688: | } |
| 1689: | |
| 1690: | $gatheredReturnStatements[] = new ReturnStatement($scope, $node); |
| 1691: | $gatheredReturnStatementsWithScope[] = [$node, $scope]; |
| 1692: | }; |
| 1693: | |
| 1694: | if (count($byRefUses) === 0) { |
| 1695: | $this->pushNodeGatherer($closureStmtsGatherer); |
| 1696: | try { |
| 1697: | $statementResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $nodeCallback, StatementContext::createTopLevel()); |
| 1698: | } finally { |
| 1699: | $this->popNodeGatherer(); |
| 1700: | } |
| 1701: | $publicStatementResult = $statementResult->toPublic(); |
| 1702: | $closureReturnStatementsNodeScope = $this->refineClosureNodeScope($closureScope, $scope, $expr, $gatheredReturnStatementsWithScope, $gatheredYieldStatementsWithScope, $executionEnds, $statementResult->getThrowPoints(), array_merge($closureImpurePoints, $statementResult->getImpurePoints()), $invalidateExpressions, $storage); |
| 1703: | $this->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode( |
| 1704: | $expr, |
| 1705: | $gatheredReturnStatements, |
| 1706: | $gatheredYieldStatements, |
| 1707: | $publicStatementResult, |
| 1708: | $executionEnds, |
| 1709: | array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints), |
| 1710: | ), $closureReturnStatementsNodeScope, $storage); |
| 1711: | |
| 1712: | return new ProcessClosureResult( |
| 1713: | $scope, |
| 1714: | $statementResult->getThrowPoints(), |
| 1715: | $statementResult->getImpurePoints(), |
| 1716: | $invalidateExpressions, |
| 1717: | $gatheredReturnStatementsWithScope, |
| 1718: | $gatheredYieldStatementsWithScope, |
| 1719: | $executionEnds, |
| 1720: | array_merge($closureImpurePoints, $statementResult->getImpurePoints()), |
| 1721: | ); |
| 1722: | } |
| 1723: | |
| 1724: | $originalStorage = $storage; |
| 1725: | |
| 1726: | $count = 0; |
| 1727: | $closureResultScope = null; |
| 1728: | $replayBodyRecording = null; |
| 1729: | $replayPassStorage = null; |
| 1730: | $replayPassResult = null; |
| 1731: | $replayEntryScope = null; |
| 1732: | $bodyIsReplayable = $this->isReplayableConvergenceBody($expr, $expr->stmts); |
| 1733: | do { |
| 1734: | $prevScope = $closureScope; |
| 1735: | |
| 1736: | $storage = $originalStorage->duplicate(); |
| 1737: | $bodyRecording = $bodyIsReplayable ? new RecordingNodeCallback() : new NoopNodeCallback(); |
| 1738: | |
| 1739: | |
| 1740: | |
| 1741: | |
| 1742: | $intermediaryClosureScopeResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $bodyRecording, StatementContext::createDeep()); |
| 1743: | |
| 1744: | |
| 1745: | if ($bodyRecording instanceof RecordingNodeCallback) { |
| 1746: | $replayBodyRecording = $bodyRecording; |
| 1747: | $replayPassStorage = $storage; |
| 1748: | $replayPassResult = $intermediaryClosureScopeResult; |
| 1749: | $replayEntryScope = $prevScope; |
| 1750: | } |
| 1751: | $intermediaryClosureScope = $intermediaryClosureScopeResult->getScope(); |
| 1752: | foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) { |
| 1753: | $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope()); |
| 1754: | } |
| 1755: | |
| 1756: | if ($expr->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) === true) { |
| 1757: | $closureResultScope = $intermediaryClosureScope; |
| 1758: | break; |
| 1759: | } |
| 1760: | |
| 1761: | $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters, $nativeCallableParameters); |
| 1762: | $closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses); |
| 1763: | |
| 1764: | if ($closureScope->equals($prevScope)) { |
| 1765: | break; |
| 1766: | } |
| 1767: | if ($count >= self::GENERALIZE_AFTER_ITERATION) { |
| 1768: | $closureScope = $prevScope->generalizeWith($closureScope); |
| 1769: | } |
| 1770: | $count++; |
| 1771: | } while ($count < self::LOOP_SCOPE_ITERATIONS); |
| 1772: | |
| 1773: | if ($closureResultScope === null) { |
| 1774: | $closureResultScope = $closureScope; |
| 1775: | } |
| 1776: | |
| 1777: | $storage = $originalStorage; |
| 1778: | $this->pushNodeGatherer($closureStmtsGatherer); |
| 1779: | try { |
| 1780: | if ( |
| 1781: | $replayBodyRecording !== null && $replayPassStorage !== null |
| 1782: | && $replayPassResult !== null && $replayEntryScope !== null |
| 1783: | && $closureScope->equals($replayEntryScope) |
| 1784: | ) { |
| 1785: | |
| 1786: | |
| 1787: | |
| 1788: | |
| 1789: | |
| 1790: | |
| 1791: | $closureScope = $replayEntryScope; |
| 1792: | $originalStorage->mergeResults($replayPassStorage); |
| 1793: | $this->replayRecording($replayBodyRecording, $nodeCallback, $originalStorage, $closureScope); |
| 1794: | $statementResult = $replayPassResult; |
| 1795: | } else { |
| 1796: | $statementResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $nodeCallback, StatementContext::createTopLevel()); |
| 1797: | } |
| 1798: | } finally { |
| 1799: | $this->popNodeGatherer(); |
| 1800: | } |
| 1801: | $publicStatementResult = $statementResult->toPublic(); |
| 1802: | $closureReturnStatementsNodeScope = $this->refineClosureNodeScope($closureScope, $scope, $expr, $gatheredReturnStatementsWithScope, $gatheredYieldStatementsWithScope, $executionEnds, $statementResult->getThrowPoints(), array_merge($closureImpurePoints, $statementResult->getImpurePoints()), $invalidateExpressions, $storage); |
| 1803: | $this->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode( |
| 1804: | $expr, |
| 1805: | $gatheredReturnStatements, |
| 1806: | $gatheredYieldStatements, |
| 1807: | $publicStatementResult, |
| 1808: | $executionEnds, |
| 1809: | array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints), |
| 1810: | ), $closureReturnStatementsNodeScope, $storage); |
| 1811: | |
| 1812: | return new ProcessClosureResult( |
| 1813: | $scope, |
| 1814: | $statementResult->getThrowPoints(), |
| 1815: | $statementResult->getImpurePoints(), |
| 1816: | $invalidateExpressions, |
| 1817: | $gatheredReturnStatementsWithScope, |
| 1818: | $gatheredYieldStatementsWithScope, |
| 1819: | $executionEnds, |
| 1820: | array_merge($closureImpurePoints, $statementResult->getImpurePoints()), |
| 1821: | $closureResultScope, |
| 1822: | $byRefUses, |
| 1823: | ); |
| 1824: | } |
| 1825: | |
| 1826: | |
| 1827: | |
| 1828: | |
| 1829: | |
| 1830: | |
| 1831: | |
| 1832: | |
| 1833: | |
| 1834: | |
| 1835: | |
| 1836: | |
| 1837: | |
| 1838: | |
| 1839: | |
| 1840: | |
| 1841: | private function refineClosureNodeScope( |
| 1842: | MutatingScope $closureScope, |
| 1843: | MutatingScope $scope, |
| 1844: | Expr\Closure $expr, |
| 1845: | array $gatheredReturnStatementsWithScope, |
| 1846: | array $gatheredYieldStatementsWithScope, |
| 1847: | array $executionEnds, |
| 1848: | array $throwPoints, |
| 1849: | array $impurePoints, |
| 1850: | array $invalidateExpressions, |
| 1851: | ExpressionResultStorage $storage, |
| 1852: | ): MutatingScope |
| 1853: | { |
| 1854: | $refinedClosureType = $this->container->getByType(ClosureTypeResolver::class)->buildClosureTypeForClosure( |
| 1855: | $scope, |
| 1856: | $expr, |
| 1857: | $gatheredReturnStatementsWithScope, |
| 1858: | $gatheredYieldStatementsWithScope, |
| 1859: | $executionEnds, |
| 1860: | $throwPoints, |
| 1861: | $impurePoints, |
| 1862: | $invalidateExpressions, |
| 1863: | false, |
| 1864: | $storage, |
| 1865: | ); |
| 1866: | |
| 1867: | return $closureScope->withAnonymousFunctionReflection($refinedClosureType); |
| 1868: | } |
| 1869: | |
| 1870: | |
| 1871: | |
| 1872: | |
| 1873: | |
| 1874: | public function processImmediatelyCalledCallable(MutatingScope $scope, array $invalidatedExpressions, array $uses): MutatingScope |
| 1875: | { |
| 1876: | if ($scope->isInClass()) { |
| 1877: | $uses[] = 'this'; |
| 1878: | } |
| 1879: | |
| 1880: | $finder = new NodeFinder(); |
| 1881: | foreach ($invalidatedExpressions as $invalidateExpression) { |
| 1882: | $result = $finder->findFirst([$invalidateExpression->getExpr()], static fn ($node) => $node instanceof Variable && in_array($node->name, $uses, true)); |
| 1883: | if ($result === null) { |
| 1884: | continue; |
| 1885: | } |
| 1886: | |
| 1887: | $requireMoreCharacters = $invalidateExpression->getExpr() instanceof Variable; |
| 1888: | $scope = $scope->invalidateExpression($invalidateExpression->getExpr(), $requireMoreCharacters); |
| 1889: | } |
| 1890: | |
| 1891: | return $scope; |
| 1892: | } |
| 1893: | |
| 1894: | |
| 1895: | |
| 1896: | |
| 1897: | public function processArrowFunctionNode( |
| 1898: | Node\Stmt $stmt, |
| 1899: | Expr\ArrowFunction $expr, |
| 1900: | MutatingScope $scope, |
| 1901: | ExpressionResultStorage $storage, |
| 1902: | callable $nodeCallback, |
| 1903: | ?Type $passedToType, |
| 1904: | ?Type $nativePassedToType = null, |
| 1905: | ): ProcessArrowFunctionResult |
| 1906: | { |
| 1907: | foreach ($expr->params as $param) { |
| 1908: | $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback); |
| 1909: | } |
| 1910: | if ($expr->returnType !== null) { |
| 1911: | $this->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage); |
| 1912: | } |
| 1913: | |
| 1914: | $arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME); |
| 1915: | $callableParameters = $this->createCallableParameters($scope, $expr, $arrowFunctionCallArgs, $passedToType); |
| 1916: | $nativeCallableParameters = $this->createNativeCallableParameters($scope, $expr, $arrowFunctionCallArgs, $nativePassedToType); |
| 1917: | $arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters, $nativeCallableParameters); |
| 1918: | if ($arrowFunctionScope->getAnonymousFunctionReflection() === null) { |
| 1919: | throw new ShouldNotHappenException(); |
| 1920: | } |
| 1921: | |
| 1922: | |
| 1923: | |
| 1924: | |
| 1925: | |
| 1926: | $arrowFunctionImpurePoints = []; |
| 1927: | $invalidateExpressions = []; |
| 1928: | $arrowFunctionStmtsGatherer = static function (Node $node, Scope $innerScope) use ($arrowFunctionScope, &$arrowFunctionImpurePoints, &$invalidateExpressions): void { |
| 1929: | if ($innerScope->getAnonymousFunctionReflection() !== $arrowFunctionScope->getAnonymousFunctionReflection()) { |
| 1930: | return; |
| 1931: | } |
| 1932: | |
| 1933: | if ($node instanceof InvalidateExprNode) { |
| 1934: | $invalidateExpressions[] = $node; |
| 1935: | return; |
| 1936: | } |
| 1937: | |
| 1938: | if (!$node instanceof PropertyAssignNode) { |
| 1939: | return; |
| 1940: | } |
| 1941: | |
| 1942: | $arrowFunctionImpurePoints[] = new ImpurePoint( |
| 1943: | $innerScope, |
| 1944: | $node, |
| 1945: | 'propertyAssign', |
| 1946: | 'property assignment', |
| 1947: | true, |
| 1948: | ); |
| 1949: | $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch()); |
| 1950: | }; |
| 1951: | |
| 1952: | $this->pushNodeGatherer($arrowFunctionStmtsGatherer); |
| 1953: | try { |
| 1954: | $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $nodeCallback, ExpressionContext::createTopLevel()); |
| 1955: | } finally { |
| 1956: | $this->popNodeGatherer(); |
| 1957: | } |
| 1958: | |
| 1959: | $closureTypeThrowPoints = array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->toPublic(), $exprResult->getThrowPoints()); |
| 1960: | $closureTypeImpurePoints = array_merge($arrowFunctionImpurePoints, $exprResult->getImpurePoints()); |
| 1961: | |
| 1962: | |
| 1963: | |
| 1964: | |
| 1965: | |
| 1966: | |
| 1967: | $refinedArrowFunctionType = $this->container->getByType(ClosureTypeResolver::class)->buildClosureTypeForArrowFunction( |
| 1968: | $scope, |
| 1969: | $expr, |
| 1970: | $arrowFunctionScope, |
| 1971: | $closureTypeThrowPoints, |
| 1972: | $closureTypeImpurePoints, |
| 1973: | $invalidateExpressions, |
| 1974: | false, |
| 1975: | $storage, |
| 1976: | ); |
| 1977: | $refinedArrowFunctionScope = $arrowFunctionScope->withAnonymousFunctionReflection($refinedArrowFunctionType); |
| 1978: | $this->callNodeCallback($nodeCallback, new InArrowFunctionNode($refinedArrowFunctionType, $expr), $refinedArrowFunctionScope, $storage); |
| 1979: | |
| 1980: | return new ProcessArrowFunctionResult( |
| 1981: | $this->expressionResultFactory->create( |
| 1982: | $scope, |
| 1983: | beforeScope: $scope, |
| 1984: | expr: $expr, |
| 1985: | hasYield: false, |
| 1986: | isAlwaysTerminating: $exprResult->isAlwaysTerminating(), |
| 1987: | throwPoints: $exprResult->getThrowPoints(), |
| 1988: | impurePoints: $exprResult->getImpurePoints(), |
| 1989: | typeCallback: static fn () => new MixedType(), |
| 1990: | specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), |
| 1991: | ), |
| 1992: | $arrowFunctionScope, |
| 1993: | $closureTypeThrowPoints, |
| 1994: | $closureTypeImpurePoints, |
| 1995: | $invalidateExpressions, |
| 1996: | ); |
| 1997: | } |
| 1998: | |
| 1999: | |
| 2000: | |
| 2001: | |
| 2002: | |
| 2003: | public function createCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array |
| 2004: | { |
| 2005: | return $this->doCreateCallableParameters($scope, $closureExpr, $args, $passedToType, fn (MutatingScope $s, Expr $e): Type => $this->resolveCallableTypeForScope($e, $s)); |
| 2006: | } |
| 2007: | |
| 2008: | |
| 2009: | |
| 2010: | |
| 2011: | |
| 2012: | public function createNativeCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $nativePassedToType): ?array |
| 2013: | { |
| 2014: | return $this->doCreateCallableParameters($scope, $closureExpr, $args, $nativePassedToType, fn (MutatingScope $s, Expr $e): Type => $this->resolveCallableTypeForScope($e, $s->doNotTreatPhpDocTypesAsCertain())); |
| 2015: | } |
| 2016: | |
| 2017: | |
| 2018: | |
| 2019: | |
| 2020: | |
| 2021: | |
| 2022: | |
| 2023: | |
| 2024: | |
| 2025: | |
| 2026: | private function resolveCallableTypeForScope(Expr $expr, MutatingScope $scope): Type |
| 2027: | { |
| 2028: | if ($expr instanceof Expr\Closure || $expr instanceof Expr\ArrowFunction) { |
| 2029: | return $this->container->getByType(ClosureTypeResolver::class)->getClosureType($scope, $expr, false, $scope->getCurrentExpressionResultStorage()); |
| 2030: | } |
| 2031: | |
| 2032: | return $this->readTypeOfMaybeStored($expr, $scope); |
| 2033: | } |
| 2034: | |
| 2035: | |
| 2036: | |
| 2037: | |
| 2038: | |
| 2039: | |
| 2040: | private function doCreateCallableParameters(MutatingScope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType, Closure $typeGetter): ?array |
| 2041: | { |
| 2042: | $callableParameters = null; |
| 2043: | if ($args !== null) { |
| 2044: | $closureType = $typeGetter($scope, $closureExpr); |
| 2045: | |
| 2046: | if ($closureType->isCallable()->no()) { |
| 2047: | return null; |
| 2048: | } |
| 2049: | |
| 2050: | $acceptors = $closureType->getCallableParametersAcceptors($scope); |
| 2051: | if (count($acceptors) === 1) { |
| 2052: | $callableParameters = $acceptors[0]->getParameters(); |
| 2053: | |
| 2054: | foreach ($callableParameters as $index => $callableParameter) { |
| 2055: | if (!isset($args[$index])) { |
| 2056: | continue; |
| 2057: | } |
| 2058: | |
| 2059: | if ($callableParameter->isVariadic()) { |
| 2060: | $argTypes = []; |
| 2061: | $argNumber = count($args); |
| 2062: | for ($j = $index; $j < $argNumber; $j++) { |
| 2063: | $argTypes[] = $typeGetter($scope, $args[$j]->value); |
| 2064: | } |
| 2065: | $type = TypeCombinator::union(...$argTypes); |
| 2066: | } else { |
| 2067: | $type = $typeGetter($scope, $args[$index]->value); |
| 2068: | } |
| 2069: | $callableParameters[$index] = new NativeParameterReflection( |
| 2070: | $callableParameter->getName(), |
| 2071: | $callableParameter->isOptional(), |
| 2072: | $type, |
| 2073: | $callableParameter->passedByReference(), |
| 2074: | $callableParameter->isVariadic(), |
| 2075: | $callableParameter->getDefaultValue(), |
| 2076: | ); |
| 2077: | } |
| 2078: | } |
| 2079: | } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) { |
| 2080: | if ($passedToType instanceof UnionType) { |
| 2081: | $passedToType = $passedToType->filterTypes(static fn (Type $innerType) => $innerType->isCallable()->yes()); |
| 2082: | |
| 2083: | if ($passedToType->isCallable()->no()) { |
| 2084: | return null; |
| 2085: | } |
| 2086: | } |
| 2087: | |
| 2088: | $acceptors = $passedToType->getCallableParametersAcceptors($scope); |
| 2089: | foreach ($acceptors as $acceptor) { |
| 2090: | $acceptorParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection( |
| 2091: | $callableParameter->getName(), |
| 2092: | $callableParameter->isOptional(), |
| 2093: | $callableParameter->getType(), |
| 2094: | $callableParameter->passedByReference(), |
| 2095: | $callableParameter->isVariadic(), |
| 2096: | $callableParameter->getDefaultValue(), |
| 2097: | ), $acceptor->getParameters()); |
| 2098: | |
| 2099: | if ($callableParameters === null) { |
| 2100: | $callableParameters = $acceptorParameters; |
| 2101: | continue; |
| 2102: | } |
| 2103: | |
| 2104: | $newParameters = []; |
| 2105: | $parameterCount = max(count($callableParameters), count($acceptorParameters)); |
| 2106: | for ($i = 0; $i < $parameterCount; $i++) { |
| 2107: | if (!array_key_exists($i, $acceptorParameters)) { |
| 2108: | $newParameters[] = $callableParameters[$i]->toOptional(); |
| 2109: | continue; |
| 2110: | } |
| 2111: | |
| 2112: | if (!array_key_exists($i, $callableParameters)) { |
| 2113: | $newParameters[] = $acceptorParameters[$i]->toOptional(); |
| 2114: | continue; |
| 2115: | } |
| 2116: | |
| 2117: | $newParameters[] = $callableParameters[$i]->union($acceptorParameters[$i]); |
| 2118: | } |
| 2119: | |
| 2120: | $callableParameters = $newParameters; |
| 2121: | } |
| 2122: | } |
| 2123: | |
| 2124: | return $callableParameters; |
| 2125: | } |
| 2126: | |
| 2127: | |
| 2128: | |
| 2129: | |
| 2130: | public function processParamNode( |
| 2131: | Node\Stmt $stmt, |
| 2132: | Node\Param $param, |
| 2133: | MutatingScope $scope, |
| 2134: | ExpressionResultStorage $storage, |
| 2135: | callable $nodeCallback, |
| 2136: | ): void |
| 2137: | { |
| 2138: | $this->processAttributeGroups($stmt, $param->attrGroups, $scope, $storage, $nodeCallback); |
| 2139: | $this->callNodeCallback($nodeCallback, $param, $scope, $storage); |
| 2140: | if ($param->type !== null) { |
| 2141: | $this->callNodeCallback($nodeCallback, $param->type, $scope, $storage); |
| 2142: | } |
| 2143: | if ($param->default === null) { |
| 2144: | return; |
| 2145: | } |
| 2146: | |
| 2147: | $this->processExprNode($stmt, $param->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); |
| 2148: | } |
| 2149: | |
| 2150: | |
| 2151: | |
| 2152: | |
| 2153: | |
| 2154: | public function processAttributeGroups( |
| 2155: | Node\Stmt $stmt, |
| 2156: | array $attrGroups, |
| 2157: | MutatingScope $scope, |
| 2158: | ExpressionResultStorage $storage, |
| 2159: | callable $nodeCallback, |
| 2160: | ): void |
| 2161: | { |
| 2162: | foreach ($attrGroups as $attrGroup) { |
| 2163: | foreach ($attrGroup->attrs as $attr) { |
| 2164: | $className = $scope->resolveName($attr->name); |
| 2165: | if ($this->reflectionProvider->hasClass($className)) { |
| 2166: | $classReflection = $this->reflectionProvider->getClass($className); |
| 2167: | if ($classReflection->hasConstructor()) { |
| 2168: | $constructorReflection = $classReflection->getConstructor(); |
| 2169: | $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization( |
| 2170: | $attr->args, |
| 2171: | $constructorReflection->getVariants(), |
| 2172: | $constructorReflection->getNamedArgumentsVariants(), |
| 2173: | ); |
| 2174: | $expr = new New_($attr->name, $attr->args); |
| 2175: | $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr; |
| 2176: | $this->processArgs($stmt, $constructorReflection, null, $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants(), $expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); |
| 2177: | $this->callNodeCallback($nodeCallback, $attr, $scope, $storage); |
| 2178: | continue; |
| 2179: | } |
| 2180: | } |
| 2181: | |
| 2182: | foreach ($attr->args as $arg) { |
| 2183: | $this->processExprNode($stmt, $arg->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep()); |
| 2184: | $this->callNodeCallback($nodeCallback, $arg, $scope, $storage); |
| 2185: | } |
| 2186: | $this->callNodeCallback($nodeCallback, $attr, $scope, $storage); |
| 2187: | } |
| 2188: | $this->callNodeCallback($nodeCallback, $attrGroup, $scope, $storage); |
| 2189: | } |
| 2190: | } |
| 2191: | |
| 2192: | |
| 2193: | |
| 2194: | |
| 2195: | private function resolveClosureThisType( |
| 2196: | ?CallLike $call, |
| 2197: | $calleeReflection, |
| 2198: | ParameterReflection $parameter, |
| 2199: | MutatingScope $scope, |
| 2200: | ): ?Type |
| 2201: | { |
| 2202: | if ($call instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { |
| 2203: | foreach ($this->functionParameterClosureThisExtensions->getAll() as $extension) { |
| 2204: | if (! $extension->isFunctionSupported($calleeReflection, $parameter)) { |
| 2205: | continue; |
| 2206: | } |
| 2207: | $type = $extension->getClosureThisTypeFromFunctionCall($calleeReflection, $call, $parameter, $scope); |
| 2208: | if ($type !== null) { |
| 2209: | return $type; |
| 2210: | } |
| 2211: | } |
| 2212: | } elseif ($call instanceof StaticCall && $calleeReflection instanceof MethodReflection) { |
| 2213: | foreach ($this->staticMethodParameterClosureThisExtensions->getAll() as $extension) { |
| 2214: | if (! $extension->isStaticMethodSupported($calleeReflection, $parameter)) { |
| 2215: | continue; |
| 2216: | } |
| 2217: | $type = $extension->getClosureThisTypeFromStaticMethodCall($calleeReflection, $call, $parameter, $scope); |
| 2218: | if ($type !== null) { |
| 2219: | return $type; |
| 2220: | } |
| 2221: | } |
| 2222: | } elseif ($call instanceof MethodCall && $calleeReflection instanceof MethodReflection) { |
| 2223: | foreach ($this->methodParameterClosureThisExtensions->getAll() as $extension) { |
| 2224: | if (! $extension->isMethodSupported($calleeReflection, $parameter)) { |
| 2225: | continue; |
| 2226: | } |
| 2227: | $type = $extension->getClosureThisTypeFromMethodCall($calleeReflection, $call, $parameter, $scope); |
| 2228: | if ($type !== null) { |
| 2229: | return $type; |
| 2230: | } |
| 2231: | } |
| 2232: | } |
| 2233: | |
| 2234: | if ($parameter instanceof ExtendedParameterReflection) { |
| 2235: | return $parameter->getClosureThisType(); |
| 2236: | } |
| 2237: | |
| 2238: | return null; |
| 2239: | } |
| 2240: | |
| 2241: | |
| 2242: | |
| 2243: | |
| 2244: | |
| 2245: | |
| 2246: | |
| 2247: | |
| 2248: | public function processArgs( |
| 2249: | Node\Stmt $stmt, |
| 2250: | $calleeReflection, |
| 2251: | ?ExtendedMethodReflection $nakedMethodReflection, |
| 2252: | array $parametersAcceptors, |
| 2253: | ?array $namedArgumentsVariants, |
| 2254: | CallLike $callLike, |
| 2255: | MutatingScope $scope, |
| 2256: | ExpressionResultStorage $storage, |
| 2257: | callable $nodeCallback, |
| 2258: | ExpressionContext $context, |
| 2259: | ?callable $closureBindScopeFactory = null, |
| 2260: | ): ArgsResult |
| 2261: | { |
| 2262: | $args = $callLike->getArgs(); |
| 2263: | |
| 2264: | |
| 2265: | |
| 2266: | |
| 2267: | |
| 2268: | $gatheredTypes = []; |
| 2269: | $gatheredUnpack = false; |
| 2270: | $gatheredHasName = false; |
| 2271: | $gatheredArgTypeByIndex = []; |
| 2272: | |
| 2273: | |
| 2274: | |
| 2275: | |
| 2276: | |
| 2277: | |
| 2278: | $metadataAcceptor = $parametersAcceptors[0] ?? null; |
| 2279: | |
| 2280: | |
| 2281: | |
| 2282: | |
| 2283: | |
| 2284: | |
| 2285: | |
| 2286: | $typeDrivenAcceptorSelection = count($parametersAcceptors) > 1 |
| 2287: | || $namedArgumentsVariants !== null |
| 2288: | || ($metadataAcceptor !== null && ParametersAcceptorSelector::hasAcceptorTemplateOrLateResolvableType($metadataAcceptor)); |
| 2289: | |
| 2290: | |
| 2291: | |
| 2292: | $hasTemplateParameterType = $metadataAcceptor !== null |
| 2293: | && ParametersAcceptorSelector::hasAcceptorTemplateOrLateResolvableParameterType($metadataAcceptor); |
| 2294: | $argMetadataIsTypeDriven = count($parametersAcceptors) > 1 || $hasTemplateParameterType; |
| 2295: | |
| 2296: | $hasYield = false; |
| 2297: | $throwPoints = []; |
| 2298: | $impurePoints = []; |
| 2299: | $isAlwaysTerminating = false; |
| 2300: | |
| 2301: | $deferredInvalidateExpressions = []; |
| 2302: | |
| 2303: | $deferredByRefClosureResults = []; |
| 2304: | |
| 2305: | $processingOrder = array_keys($args); |
| 2306: | usort($processingOrder, static function (int $a, int $b) use ($args): int { |
| 2307: | $aOriginalArg = $args[$a]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); |
| 2308: | $bOriginalArg = $args[$b]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); |
| 2309: | $aValue = $aOriginalArg !== null ? $aOriginalArg->value : $args[$a]->value; |
| 2310: | $bValue = $bOriginalArg !== null ? $bOriginalArg->value : $args[$b]->value; |
| 2311: | $aIsClosure = $aValue instanceof Expr\Closure || $aValue instanceof Expr\ArrowFunction; |
| 2312: | $bIsClosure = $bValue instanceof Expr\Closure || $bValue instanceof Expr\ArrowFunction; |
| 2313: | if ($aIsClosure !== $bIsClosure) { |
| 2314: | |
| 2315: | |
| 2316: | return $aIsClosure ? 1 : -1; |
| 2317: | } |
| 2318: | |
| 2319: | $aOriginal = $args[$a]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); |
| 2320: | $bOriginal = $args[$b]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE); |
| 2321: | if ($aOriginal === null && $bOriginal === null) { |
| 2322: | return $a <=> $b; |
| 2323: | } |
| 2324: | if ($aOriginal === null) { |
| 2325: | return 1; |
| 2326: | } |
| 2327: | if ($bOriginal === null) { |
| 2328: | return -1; |
| 2329: | } |
| 2330: | |
| 2331: | return $aOriginal->getStartTokenPos() <=> $bOriginal->getStartTokenPos(); |
| 2332: | }); |
| 2333: | |
| 2334: | $argResults = []; |
| 2335: | $countStableMetadataAcceptor = null; |
| 2336: | foreach ($processingOrder as $i) { |
| 2337: | $arg = $args[$i]; |
| 2338: | |
| 2339: | if ($arg->value instanceof Expr\Closure || $arg->value instanceof Expr\ArrowFunction) { |
| 2340: | |
| 2341: | |
| 2342: | |
| 2343: | |
| 2344: | |
| 2345: | |
| 2346: | $originalArgForGather = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; |
| 2347: | $gatheredArgTypeByIndex[$i] = $typeDrivenAcceptorSelection |
| 2348: | ? $this->gatherClosureArgType($parametersAcceptors, $i, $arg->value, $scope) |
| 2349: | : $this->container->getByType(ClosureTypeResolver::class)->getClosureType($scope, $arg->value, true, $storage); |
| 2350: | $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArgForGather, $i, $gatheredArgTypeByIndex[$i]); |
| 2351: | } |
| 2352: | |
| 2353: | $argMetadataAcceptor = $metadataAcceptor; |
| 2354: | if ($metadataAcceptor !== null && $argMetadataIsTypeDriven) { |
| 2355: | if ($this->argConsumesResolvedParameterType($arg->value)) { |
| 2356: | |
| 2357: | |
| 2358: | |
| 2359: | |
| 2360: | |
| 2361: | $paddedTypes = []; |
| 2362: | $paddedUnpack = false; |
| 2363: | $paddedHasName = false; |
| 2364: | foreach ($args as $j => $paddedArg) { |
| 2365: | $paddedOriginalArg = $paddedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $paddedArg; |
| 2366: | $this->addGatheredArgType($paddedTypes, $paddedUnpack, $paddedHasName, $paddedOriginalArg, $j, $gatheredArgTypeByIndex[$j] ?? new MixedType()); |
| 2367: | } |
| 2368: | $argMetadataAcceptor = $this->selectArgsMetadataAcceptor($args, $paddedTypes, $parametersAcceptors, $namedArgumentsVariants, $paddedHasName, $paddedUnpack, $scope); |
| 2369: | } else { |
| 2370: | |
| 2371: | |
| 2372: | |
| 2373: | |
| 2374: | |
| 2375: | |
| 2376: | |
| 2377: | if ($countStableMetadataAcceptor === null) { |
| 2378: | $paddedTypes = []; |
| 2379: | $paddedUnpack = false; |
| 2380: | $paddedHasName = false; |
| 2381: | foreach ($args as $j => $paddedArg) { |
| 2382: | $paddedOriginalArg = $paddedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $paddedArg; |
| 2383: | $this->addGatheredArgType($paddedTypes, $paddedUnpack, $paddedHasName, $paddedOriginalArg, $j, new MixedType()); |
| 2384: | } |
| 2385: | $countStableMetadataAcceptor = $this->selectArgsMetadataAcceptor($args, $paddedTypes, $parametersAcceptors, $namedArgumentsVariants, $paddedHasName, $paddedUnpack, $scope); |
| 2386: | } |
| 2387: | $argMetadataAcceptor = $countStableMetadataAcceptor; |
| 2388: | } |
| 2389: | } |
| 2390: | $parameters = $argMetadataAcceptor !== null ? $argMetadataAcceptor->getParameters() : null; |
| 2391: | |
| 2392: | $assignByReference = false; |
| 2393: | $parameter = null; |
| 2394: | $parameterType = null; |
| 2395: | $parameterNativeType = null; |
| 2396: | if ($parameters !== null) { |
| 2397: | $matchedParameter = null; |
| 2398: | if ($arg->name !== null) { |
| 2399: | foreach ($parameters as $p) { |
| 2400: | if ($p->getName() === $arg->name->toString()) { |
| 2401: | $matchedParameter = $p; |
| 2402: | break; |
| 2403: | } |
| 2404: | } |
| 2405: | } elseif (isset($parameters[$i])) { |
| 2406: | $matchedParameter = $parameters[$i]; |
| 2407: | } |
| 2408: | |
| 2409: | if ($matchedParameter !== null) { |
| 2410: | $assignByReference = $matchedParameter->passedByReference()->createsNewVariable(); |
| 2411: | $parameterType = $matchedParameter->getType(); |
| 2412: | |
| 2413: | if ($matchedParameter instanceof ExtendedParameterReflection) { |
| 2414: | $parameterNativeType = $matchedParameter->getNativeType(); |
| 2415: | } |
| 2416: | $parameter = $matchedParameter; |
| 2417: | } elseif (count($parameters) > 0 && $argMetadataAcceptor->isVariadic()) { |
| 2418: | $lastParameter = array_last($parameters); |
| 2419: | $assignByReference = $lastParameter->passedByReference()->createsNewVariable(); |
| 2420: | $parameterType = $lastParameter->getType(); |
| 2421: | |
| 2422: | if ($lastParameter instanceof ExtendedParameterReflection) { |
| 2423: | $parameterNativeType = $lastParameter->getNativeType(); |
| 2424: | } |
| 2425: | $parameter = $lastParameter; |
| 2426: | } |
| 2427: | } |
| 2428: | |
| 2429: | $lookForUnset = false; |
| 2430: | if ($assignByReference) { |
| 2431: | $isBuiltin = false; |
| 2432: | if ($calleeReflection instanceof FunctionReflection && $calleeReflection->isBuiltin()) { |
| 2433: | $isBuiltin = true; |
| 2434: | } elseif ($calleeReflection instanceof ExtendedMethodReflection && $calleeReflection->getDeclaringClass()->isBuiltin()) { |
| 2435: | $isBuiltin = true; |
| 2436: | } |
| 2437: | if ( |
| 2438: | $isBuiltin |
| 2439: | || ($parameterNativeType === null || !$parameterNativeType->isNull()->no()) |
| 2440: | ) { |
| 2441: | $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $arg->value); |
| 2442: | $lookForUnset = true; |
| 2443: | } |
| 2444: | } |
| 2445: | |
| 2446: | $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg; |
| 2447: | if ($calleeReflection !== null) { |
| 2448: | $rememberTypes = !$originalArg->value instanceof Expr\Closure && !$originalArg->value instanceof Expr\ArrowFunction; |
| 2449: | $scope = $scope->pushInFunctionCall($calleeReflection, $parameter, $rememberTypes); |
| 2450: | } |
| 2451: | |
| 2452: | $this->callNodeCallback($nodeCallback, $originalArg, $scope, $storage); |
| 2453: | |
| 2454: | $originalScope = $scope; |
| 2455: | $scopeToPass = $scope; |
| 2456: | if ($i === 0 && $closureBindScopeFactory !== null && ($arg->value instanceof Expr\Closure || $arg->value instanceof Expr\ArrowFunction)) { |
| 2457: | $scopeToPass = $closureBindScopeFactory($scope); |
| 2458: | } |
| 2459: | |
| 2460: | if ($arg->value instanceof Expr\Closure) { |
| 2461: | |
| 2462: | $storedClosureArgResult = null; |
| 2463: | if ($this->returnStoredExpressionResults || $this->consumeStoredExpressionResults) { |
| 2464: | |
| 2465: | |
| 2466: | |
| 2467: | |
| 2468: | |
| 2469: | $storedClosureArgResult = $storage->findExpressionResult($arg->value); |
| 2470: | |
| 2471: | |
| 2472: | |
| 2473: | |
| 2474: | if ($storedClosureArgResult === null && $this->returnStoredExpressionResults) { |
| 2475: | $closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); |
| 2476: | $storedClosureArgResult = $this->expressionResultFactory->create( |
| 2477: | $scopeToPass, |
| 2478: | beforeScope: $scopeToPass, |
| 2479: | expr: $arg->value, |
| 2480: | hasYield: false, |
| 2481: | isAlwaysTerminating: false, |
| 2482: | throwPoints: [], |
| 2483: | impurePoints: [], |
| 2484: | type: $closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage), |
| 2485: | nativeType: $closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage), |
| 2486: | typeCallback: null, |
| 2487: | specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), |
| 2488: | ); |
| 2489: | $this->storeExpressionResult($storage, $arg->value, $storedClosureArgResult); |
| 2490: | } |
| 2491: | } |
| 2492: | if ($storedClosureArgResult !== null) { |
| 2493: | $argResults[spl_object_id($arg->value)] = $storedClosureArgResult; |
| 2494: | } else { |
| 2495: | $restoreThisScope = null; |
| 2496: | if ( |
| 2497: | $closureBindScopeFactory === null |
| 2498: | && $parameter instanceof ExtendedParameterReflection |
| 2499: | && !$arg->value->static |
| 2500: | ) { |
| 2501: | $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); |
| 2502: | if ($closureThisType !== null) { |
| 2503: | $restoreThisScope = $scopeToPass; |
| 2504: | $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()) |
| 2505: | ->withClosureBindScopeClasses($closureThisType->getObjectClassNames()); |
| 2506: | } |
| 2507: | } |
| 2508: | |
| 2509: | if ($parameter !== null) { |
| 2510: | $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); |
| 2511: | |
| 2512: | if ($overwritingParameterType !== null) { |
| 2513: | $parameterType = $overwritingParameterType; |
| 2514: | |
| 2515: | |
| 2516: | |
| 2517: | |
| 2518: | $overwritingParameterNativeType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass->doNotTreatPhpDocTypesAsCertain()); |
| 2519: | if ($overwritingParameterNativeType !== null) { |
| 2520: | $parameterNativeType = $overwritingParameterNativeType; |
| 2521: | } |
| 2522: | } |
| 2523: | } |
| 2524: | |
| 2525: | $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType, $parameterNativeType); |
| 2526: | if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { |
| 2527: | $throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints())); |
| 2528: | $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints()); |
| 2529: | } |
| 2530: | |
| 2531: | $closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); |
| 2532: | $storedClosureResult = $this->expressionResultFactory->create( |
| 2533: | $closureResult->getScope(), |
| 2534: | $scopeToPass, |
| 2535: | $arg->value, |
| 2536: | hasYield: false, |
| 2537: | isAlwaysTerminating: false, |
| 2538: | throwPoints: [], |
| 2539: | impurePoints: [], |
| 2540: | type: $closureTypeResolver->buildClosureTypeForClosure( |
| 2541: | $scopeToPass, |
| 2542: | $arg->value, |
| 2543: | $closureResult->getGatheredReturnStatements(), |
| 2544: | $closureResult->getGatheredYieldStatements(), |
| 2545: | $closureResult->getExecutionEnds(), |
| 2546: | $closureResult->getThrowPoints(), |
| 2547: | $closureResult->getClosureTypeImpurePoints(), |
| 2548: | $closureResult->getInvalidateExpressions(), |
| 2549: | false, |
| 2550: | $storage, |
| 2551: | ), |
| 2552: | |
| 2553: | |
| 2554: | nativeType: $closureTypeResolver->buildClosureTypeForClosure( |
| 2555: | $scopeToPass, |
| 2556: | $arg->value, |
| 2557: | $closureResult->getGatheredReturnStatements(), |
| 2558: | $closureResult->getGatheredYieldStatements(), |
| 2559: | $closureResult->getExecutionEnds(), |
| 2560: | $closureResult->getThrowPoints(), |
| 2561: | $closureResult->getClosureTypeImpurePoints(), |
| 2562: | $closureResult->getInvalidateExpressions(), |
| 2563: | true, |
| 2564: | $storage, |
| 2565: | ), |
| 2566: | typeCallback: null, |
| 2567: | specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), |
| 2568: | ); |
| 2569: | $this->storeExpressionResult($storage, $arg->value, $storedClosureResult); |
| 2570: | |
| 2571: | |
| 2572: | |
| 2573: | $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); |
| 2574: | |
| 2575: | |
| 2576: | $argResults[spl_object_id($arg->value)] = $storedClosureResult; |
| 2577: | |
| 2578: | $uses = []; |
| 2579: | foreach ($arg->value->uses as $use) { |
| 2580: | if (!is_string($use->var->name)) { |
| 2581: | continue; |
| 2582: | } |
| 2583: | |
| 2584: | $uses[] = $use->var->name; |
| 2585: | } |
| 2586: | |
| 2587: | $scope = $closureResult->getScope(); |
| 2588: | $deferredByRefClosureResults[] = $closureResult; |
| 2589: | |
| 2590: | |
| 2591: | |
| 2592: | $closureExprType = $storedClosureResult->getType(); |
| 2593: | $invalidateExpressions = $closureExprType instanceof ClosureType |
| 2594: | ? $closureExprType->getInvalidateExpressions() |
| 2595: | : $closureResult->getInvalidateExpressions(); |
| 2596: | if ($restoreThisScope !== null) { |
| 2597: | $nodeFinder = new NodeFinder(); |
| 2598: | $cb = static fn ($expr) => $expr instanceof Variable && $expr->name === 'this'; |
| 2599: | foreach ($invalidateExpressions as $j => $invalidateExprNode) { |
| 2600: | $foundThis = $nodeFinder->findFirst([$invalidateExprNode->getExpr()], $cb); |
| 2601: | if ($foundThis === null) { |
| 2602: | continue; |
| 2603: | } |
| 2604: | |
| 2605: | unset($invalidateExpressions[$j]); |
| 2606: | } |
| 2607: | $invalidateExpressions = array_values($invalidateExpressions); |
| 2608: | $scope = $scope->restoreThis($restoreThisScope); |
| 2609: | } |
| 2610: | |
| 2611: | if ($this->shouldInvalidateCallbackExpressions($parameter)) { |
| 2612: | $deferredInvalidateExpressions[] = [$invalidateExpressions, $uses]; |
| 2613: | } |
| 2614: | } |
| 2615: | } elseif ($arg->value instanceof Expr\ArrowFunction) { |
| 2616: | |
| 2617: | $storedClosureArgResult = null; |
| 2618: | if ($this->returnStoredExpressionResults || $this->consumeStoredExpressionResults) { |
| 2619: | |
| 2620: | $storedClosureArgResult = $storage->findExpressionResult($arg->value); |
| 2621: | |
| 2622: | |
| 2623: | if ($storedClosureArgResult === null && $this->returnStoredExpressionResults) { |
| 2624: | $closureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); |
| 2625: | $storedClosureArgResult = $this->expressionResultFactory->create( |
| 2626: | $scopeToPass, |
| 2627: | beforeScope: $scopeToPass, |
| 2628: | expr: $arg->value, |
| 2629: | hasYield: false, |
| 2630: | isAlwaysTerminating: false, |
| 2631: | throwPoints: [], |
| 2632: | impurePoints: [], |
| 2633: | type: $closureTypeResolver->getClosureType($scopeToPass, $arg->value, false, $storage), |
| 2634: | nativeType: $closureTypeResolver->getClosureType($scopeToPass->doNotTreatPhpDocTypesAsCertain(), $arg->value, false, $storage), |
| 2635: | typeCallback: null, |
| 2636: | specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), |
| 2637: | ); |
| 2638: | $this->storeExpressionResult($storage, $arg->value, $storedClosureArgResult); |
| 2639: | } |
| 2640: | } |
| 2641: | if ($storedClosureArgResult !== null) { |
| 2642: | $argResults[spl_object_id($arg->value)] = $storedClosureArgResult; |
| 2643: | } else { |
| 2644: | if ( |
| 2645: | $closureBindScopeFactory === null |
| 2646: | && $parameter instanceof ExtendedParameterReflection |
| 2647: | && !$arg->value->static |
| 2648: | ) { |
| 2649: | $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass); |
| 2650: | if ($closureThisType !== null) { |
| 2651: | $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes()) |
| 2652: | ->withClosureBindScopeClasses($closureThisType->getObjectClassNames()); |
| 2653: | } |
| 2654: | } |
| 2655: | |
| 2656: | if ($parameter !== null) { |
| 2657: | $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass); |
| 2658: | |
| 2659: | if ($overwritingParameterType !== null) { |
| 2660: | $parameterType = $overwritingParameterType; |
| 2661: | |
| 2662: | |
| 2663: | |
| 2664: | |
| 2665: | $overwritingParameterNativeType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass->doNotTreatPhpDocTypesAsCertain()); |
| 2666: | if ($overwritingParameterNativeType !== null) { |
| 2667: | $parameterNativeType = $overwritingParameterNativeType; |
| 2668: | } |
| 2669: | } |
| 2670: | } |
| 2671: | |
| 2672: | $arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType, $parameterNativeType); |
| 2673: | $arrowFunctionExprResult = $arrowFunctionResult->getExpressionResult(); |
| 2674: | if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { |
| 2675: | $throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $arrowFunctionExprResult->getThrowPoints())); |
| 2676: | $impurePoints = array_merge($impurePoints, $arrowFunctionExprResult->getImpurePoints()); |
| 2677: | } |
| 2678: | $arrowFunctionClosureTypeResolver = $this->container->getByType(ClosureTypeResolver::class); |
| 2679: | $arrowFunctionScope = $arrowFunctionResult->getArrowFunctionScope(); |
| 2680: | |
| 2681: | |
| 2682: | |
| 2683: | |
| 2684: | $arrowFunctionType = $arrowFunctionClosureTypeResolver->buildClosureTypeForArrowFunction( |
| 2685: | $scopeToPass, |
| 2686: | $arg->value, |
| 2687: | $arrowFunctionScope, |
| 2688: | $arrowFunctionResult->getClosureTypeThrowPoints(), |
| 2689: | $arrowFunctionResult->getClosureTypeImpurePoints(), |
| 2690: | $arrowFunctionResult->getInvalidateExpressions(), |
| 2691: | false, |
| 2692: | $storage, |
| 2693: | ); |
| 2694: | $storedArrowResult = $this->expressionResultFactory->create( |
| 2695: | $arrowFunctionExprResult->getScope(), |
| 2696: | beforeScope: $scopeToPass, |
| 2697: | expr: $arg->value, |
| 2698: | hasYield: $arrowFunctionExprResult->hasYield(), |
| 2699: | isAlwaysTerminating: $arrowFunctionExprResult->isAlwaysTerminating(), |
| 2700: | throwPoints: $arrowFunctionExprResult->getThrowPoints(), |
| 2701: | impurePoints: $arrowFunctionExprResult->getImpurePoints(), |
| 2702: | type: $arrowFunctionType, |
| 2703: | nativeType: $arrowFunctionClosureTypeResolver->buildClosureTypeForArrowFunction( |
| 2704: | $scopeToPass, |
| 2705: | $arg->value, |
| 2706: | $arrowFunctionScope, |
| 2707: | $arrowFunctionResult->getClosureTypeThrowPoints(), |
| 2708: | $arrowFunctionResult->getClosureTypeImpurePoints(), |
| 2709: | $arrowFunctionResult->getInvalidateExpressions(), |
| 2710: | true, |
| 2711: | $storage, |
| 2712: | ), |
| 2713: | typeCallback: null, |
| 2714: | specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), |
| 2715: | ); |
| 2716: | $this->storeExpressionResult($storage, $arg->value, $storedArrowResult); |
| 2717: | |
| 2718: | |
| 2719: | |
| 2720: | $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context); |
| 2721: | |
| 2722: | |
| 2723: | |
| 2724: | $argResults[spl_object_id($arg->value)] = $storedArrowResult; |
| 2725: | if ($this->shouldInvalidateCallbackExpressions($parameter)) { |
| 2726: | $deferredInvalidateExpressions[] = [$arrowFunctionType->getInvalidateExpressions(), $arrowFunctionType->getUsedVariables()]; |
| 2727: | } |
| 2728: | } |
| 2729: | } else { |
| 2730: | $enterExpressionAssignForByRef = $assignByReference && $arg->value instanceof ArrayDimFetch && $arg->value->dim === null; |
| 2731: | if ($enterExpressionAssignForByRef) { |
| 2732: | $scopeToPass = $scopeToPass->enterExpressionAssign($arg->value); |
| 2733: | } |
| 2734: | $exprResult = $this->processExprNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context->enterDeep()); |
| 2735: | $argResults[spl_object_id($arg->value)] = $exprResult; |
| 2736: | $exprType = $exprResult->getType(); |
| 2737: | $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints()); |
| 2738: | $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints()); |
| 2739: | $isAlwaysTerminating = $isAlwaysTerminating || $exprResult->isAlwaysTerminating(); |
| 2740: | $scope = $exprResult->getScope(); |
| 2741: | if ($enterExpressionAssignForByRef) { |
| 2742: | $scope = $scope->exitExpressionAssign($arg->value); |
| 2743: | } |
| 2744: | $hasYield = $hasYield || $exprResult->hasYield(); |
| 2745: | |
| 2746: | if ($exprType->isCallable()->yes()) { |
| 2747: | $acceptors = $exprType->getCallableParametersAcceptors($scope); |
| 2748: | if (count($acceptors) === 1) { |
| 2749: | if ($this->shouldInvalidateCallbackExpressions($parameter)) { |
| 2750: | $deferredInvalidateExpressions[] = [$acceptors[0]->getInvalidateExpressions(), $acceptors[0]->getUsedVariables()]; |
| 2751: | } |
| 2752: | if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) { |
| 2753: | $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $acceptors[0]->getThrowPoints()); |
| 2754: | if (!$this->implicitThrows) { |
| 2755: | $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit())); |
| 2756: | } |
| 2757: | $throwPoints = array_merge($throwPoints, $callableThrowPoints); |
| 2758: | $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $arg->value, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $acceptors[0]->getImpurePoints())); |
| 2759: | } |
| 2760: | } |
| 2761: | } |
| 2762: | |
| 2763: | $gatheredArgTypeByIndex[$i] = $exprResult->getType(); |
| 2764: | $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArg, $i, $gatheredArgTypeByIndex[$i]); |
| 2765: | } |
| 2766: | |
| 2767: | if ($assignByReference && $lookForUnset) { |
| 2768: | $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value); |
| 2769: | } |
| 2770: | |
| 2771: | if ($calleeReflection !== null) { |
| 2772: | $scope = $scope->popInFunctionCall(); |
| 2773: | } |
| 2774: | |
| 2775: | if ($i !== 0 || $closureBindScopeFactory === null) { |
| 2776: | continue; |
| 2777: | } |
| 2778: | |
| 2779: | $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope); |
| 2780: | } |
| 2781: | |
| 2782: | foreach ($deferredInvalidateExpressions as [$invalidateExpressions, $uses]) { |
| 2783: | $scope = $this->processImmediatelyCalledCallable($scope, $invalidateExpressions, $uses); |
| 2784: | } |
| 2785: | |
| 2786: | foreach ($deferredByRefClosureResults as $deferredClosureResult) { |
| 2787: | $scope = $deferredClosureResult->applyByRefUseScope($scope); |
| 2788: | } |
| 2789: | |
| 2790: | |
| 2791: | |
| 2792: | |
| 2793: | |
| 2794: | |
| 2795: | |
| 2796: | $resolvedAcceptor = null; |
| 2797: | if ($parametersAcceptors !== []) { |
| 2798: | $resolvedAcceptor = $typeDrivenAcceptorSelection |
| 2799: | ? $this->selectArgsMetadataAcceptor($args, $gatheredTypes, $parametersAcceptors, $namedArgumentsVariants, $gatheredHasName, $gatheredUnpack, $scope) |
| 2800: | : $metadataAcceptor; |
| 2801: | } |
| 2802: | |
| 2803: | |
| 2804: | |
| 2805: | |
| 2806: | |
| 2807: | |
| 2808: | $writebackAcceptor = $metadataAcceptor; |
| 2809: | if ( |
| 2810: | $metadataAcceptor !== null |
| 2811: | && $argMetadataIsTypeDriven |
| 2812: | ) { |
| 2813: | $writebackAcceptor = $resolvedAcceptor; |
| 2814: | } |
| 2815: | $writebackParameters = $writebackAcceptor !== null ? $writebackAcceptor->getParameters() : null; |
| 2816: | if ($writebackParameters !== null) { |
| 2817: | foreach ($args as $i => $arg) { |
| 2818: | $assignByReference = false; |
| 2819: | $currentParameter = null; |
| 2820: | if (isset($writebackParameters[$i])) { |
| 2821: | $currentParameter = $writebackParameters[$i]; |
| 2822: | } elseif (count($writebackParameters) > 0 && $writebackAcceptor->isVariadic()) { |
| 2823: | $currentParameter = array_last($writebackParameters); |
| 2824: | } |
| 2825: | |
| 2826: | if ($currentParameter !== null) { |
| 2827: | $assignByReference = $currentParameter->passedByReference()->createsNewVariable(); |
| 2828: | } |
| 2829: | |
| 2830: | if ($assignByReference) { |
| 2831: | if ($currentParameter === null) { |
| 2832: | throw new ShouldNotHappenException(); |
| 2833: | } |
| 2834: | |
| 2835: | $argValue = $arg->value; |
| 2836: | if (!$argValue instanceof Variable || $argValue->name !== 'this') { |
| 2837: | $paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope); |
| 2838: | if ($paramOutType !== null) { |
| 2839: | $byRefType = $paramOutType; |
| 2840: | } elseif ( |
| 2841: | $currentParameter instanceof ExtendedParameterReflection |
| 2842: | && $currentParameter->getOutType() !== null |
| 2843: | ) { |
| 2844: | $byRefType = $currentParameter->getOutType(); |
| 2845: | } elseif ( |
| 2846: | $calleeReflection instanceof MethodReflection |
| 2847: | && !$calleeReflection->getDeclaringClass()->isBuiltin() |
| 2848: | ) { |
| 2849: | $byRefType = $currentParameter->getType(); |
| 2850: | } elseif ( |
| 2851: | $calleeReflection instanceof FunctionReflection |
| 2852: | && !$calleeReflection->isBuiltin() |
| 2853: | ) { |
| 2854: | $byRefType = $currentParameter->getType(); |
| 2855: | } else { |
| 2856: | $byRefType = new MixedType(); |
| 2857: | } |
| 2858: | |
| 2859: | $scope = $this->processVirtualAssign( |
| 2860: | $scope, |
| 2861: | $storage, |
| 2862: | $stmt, |
| 2863: | $argValue, |
| 2864: | new TypeExpr($byRefType), |
| 2865: | $nodeCallback, |
| 2866: | )->getScope(); |
| 2867: | $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $argValue); |
| 2868: | } |
| 2869: | } elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) { |
| 2870: | $argType = $this->readArgResult($argResults, $arg->value)->getTypeOnScope($scope, false); |
| 2871: | if (!$argType->isObject()->no()) { |
| 2872: | $nakedReturnType = null; |
| 2873: | if ($nakedMethodReflection !== null) { |
| 2874: | $nakedParametersAcceptor = $this->selectArgsAcceptor( |
| 2875: | $gatheredTypes, |
| 2876: | $nakedMethodReflection->getVariants(), |
| 2877: | $nakedMethodReflection->getNamedArgumentsVariants(), |
| 2878: | $gatheredHasName, |
| 2879: | $gatheredUnpack, |
| 2880: | ); |
| 2881: | $nakedReturnType = $nakedParametersAcceptor->getReturnType(); |
| 2882: | } |
| 2883: | if ( |
| 2884: | $nakedReturnType === null |
| 2885: | || !(new ThisType($nakedMethodReflection->getDeclaringClass()))->isSuperTypeOf($nakedReturnType)->yes() |
| 2886: | || $nakedMethodReflection->isPure()->no() |
| 2887: | ) { |
| 2888: | $this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage); |
| 2889: | $scope = $scope->invalidateExpression($arg->value, true); |
| 2890: | } |
| 2891: | } elseif (!(new ResourceType())->isSuperTypeOf($argType)->no()) { |
| 2892: | $this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage); |
| 2893: | $scope = $scope->invalidateExpression($arg->value, true); |
| 2894: | } |
| 2895: | } |
| 2896: | } |
| 2897: | } |
| 2898: | |
| 2899: | |
| 2900: | return new ArgsResult( |
| 2901: | $this->expressionResultFactory->create( |
| 2902: | $scope, |
| 2903: | $scope, |
| 2904: | $callLike, |
| 2905: | $hasYield, |
| 2906: | $isAlwaysTerminating, |
| 2907: | $throwPoints, |
| 2908: | $impurePoints, |
| 2909: | typeCallback: static fn () => new MixedType(), |
| 2910: | specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), |
| 2911: | ), |
| 2912: | $resolvedAcceptor, |
| 2913: | $argResults, |
| 2914: | ); |
| 2915: | } |
| 2916: | |
| 2917: | |
| 2918: | |
| 2919: | |
| 2920: | |
| 2921: | |
| 2922: | |
| 2923: | |
| 2924: | |
| 2925: | private function addGatheredArgType(array &$types, bool &$unpack, bool &$hasName, Node\Arg $originalArg, int $i, Type $type): void |
| 2926: | { |
| 2927: | if ($originalArg->name !== null) { |
| 2928: | $index = $originalArg->name->toString(); |
| 2929: | $hasName = true; |
| 2930: | } else { |
| 2931: | $index = $i; |
| 2932: | } |
| 2933: | |
| 2934: | if ($originalArg->unpack) { |
| 2935: | $unpack = true; |
| 2936: | $constantArrays = $type->getConstantArrays(); |
| 2937: | if (count($constantArrays) > 0) { |
| 2938: | foreach ($constantArrays as $constantArray) { |
| 2939: | $values = $constantArray->getValueTypes(); |
| 2940: | foreach ($constantArray->getKeyTypes() as $j => $keyType) { |
| 2941: | $valueType = $values[$j]; |
| 2942: | $valueIndex = $keyType->getValue(); |
| 2943: | if (is_string($valueIndex)) { |
| 2944: | $hasName = true; |
| 2945: | } else { |
| 2946: | $valueIndex = $i + $j; |
| 2947: | } |
| 2948: | |
| 2949: | $types[$valueIndex] = isset($types[$valueIndex]) |
| 2950: | ? TypeCombinator::union($types[$valueIndex], $valueType) |
| 2951: | : $valueType; |
| 2952: | } |
| 2953: | } |
| 2954: | } else { |
| 2955: | $types[$index] = $type->getIterableValueType(); |
| 2956: | } |
| 2957: | } else { |
| 2958: | $types[$index] = $type; |
| 2959: | } |
| 2960: | } |
| 2961: | |
| 2962: | |
| 2963: | |
| 2964: | |
| 2965: | |
| 2966: | |
| 2967: | |
| 2968: | |
| 2969: | |
| 2970: | private function argConsumesResolvedParameterType(Expr $value): bool |
| 2971: | { |
| 2972: | if ($value instanceof Expr\Closure || $value instanceof Expr\ArrowFunction) { |
| 2973: | return true; |
| 2974: | } |
| 2975: | |
| 2976: | |
| 2977: | $cached = $value->getAttribute('phpstanArgContainsClosure'); |
| 2978: | if ($cached !== null) { |
| 2979: | return $cached; |
| 2980: | } |
| 2981: | |
| 2982: | $contains = (new NodeFinder())->findFirst( |
| 2983: | [$value], |
| 2984: | static fn (Node $node): bool => $node instanceof Expr\Closure || $node instanceof Expr\ArrowFunction, |
| 2985: | ) !== null; |
| 2986: | $value->setAttribute('phpstanArgContainsClosure', $contains); |
| 2987: | |
| 2988: | return $contains; |
| 2989: | } |
| 2990: | |
| 2991: | |
| 2992: | |
| 2993: | |
| 2994: | |
| 2995: | |
| 2996: | |
| 2997: | |
| 2998: | |
| 2999: | |
| 3000: | |
| 3001: | |
| 3002: | private function gatherClosureArgType(array $parametersAcceptors, int $i, Expr $closureExpr, MutatingScope $scope): Type |
| 3003: | { |
| 3004: | $rawParameter = null; |
| 3005: | if (count($parametersAcceptors) === 1) { |
| 3006: | $rawParameters = $parametersAcceptors[0]->getParameters(); |
| 3007: | if (isset($rawParameters[$i])) { |
| 3008: | $rawParameter = $rawParameters[$i]; |
| 3009: | } elseif (count($rawParameters) > 0 && $parametersAcceptors[0]->isVariadic()) { |
| 3010: | $rawParameter = array_last($rawParameters); |
| 3011: | } |
| 3012: | } |
| 3013: | |
| 3014: | if ($rawParameter !== null) { |
| 3015: | $scope = $scope->pushInFunctionCall(null, $rawParameter, false); |
| 3016: | } |
| 3017: | |
| 3018: | return $this->resolveCallableTypeForScope($closureExpr, $scope); |
| 3019: | } |
| 3020: | |
| 3021: | |
| 3022: | |
| 3023: | |
| 3024: | |
| 3025: | |
| 3026: | private function selectArgsAcceptor(array $types, array $parametersAcceptors, ?array $namedArgumentsVariants, bool $hasName, bool $unpack): ParametersAcceptor |
| 3027: | { |
| 3028: | return $hasName && $namedArgumentsVariants !== null |
| 3029: | ? ParametersAcceptorSelector::selectFromTypes($types, $namedArgumentsVariants, $unpack) |
| 3030: | : ParametersAcceptorSelector::selectFromTypes($types, $parametersAcceptors, $unpack); |
| 3031: | } |
| 3032: | |
| 3033: | |
| 3034: | |
| 3035: | |
| 3036: | |
| 3037: | |
| 3038: | |
| 3039: | |
| 3040: | |
| 3041: | |
| 3042: | |
| 3043: | |
| 3044: | |
| 3045: | private function selectArgsMetadataAcceptor(array $args, array $gatheredTypes, array $parametersAcceptors, ?array $namedArgumentsVariants, bool $hasName, bool $unpack, MutatingScope $scope): ParametersAcceptor |
| 3046: | { |
| 3047: | $overridden = ParametersAcceptorSelector::applyIntrinsicArgOverrides( |
| 3048: | $args, |
| 3049: | $parametersAcceptors, |
| 3050: | $namedArgumentsVariants, |
| 3051: | $scope, |
| 3052: | fn (Expr $e): Type => $this->readTypeOfMaybeStored($e, $scope), |
| 3053: | fn (Expr $e): Type => $this->readTypeOfMaybeStored($e, $scope->doNotTreatPhpDocTypesAsCertain()), |
| 3054: | static fn (Type $t): Type => $scope->getIterableValueType($t), |
| 3055: | static fn (Type $t): Type => $scope->getIterableKeyType($t), |
| 3056: | ); |
| 3057: | |
| 3058: | return $this->selectArgsAcceptor($gatheredTypes, $overridden, $namedArgumentsVariants, $hasName, $unpack); |
| 3059: | } |
| 3060: | |
| 3061: | |
| 3062: | |
| 3063: | |
| 3064: | |
| 3065: | |
| 3066: | |
| 3067: | |
| 3068: | |
| 3069: | public function processDroppedArgs( |
| 3070: | Node\Stmt $stmt, |
| 3071: | CallLike $originalCall, |
| 3072: | CallLike $normalizedCall, |
| 3073: | MutatingScope $scope, |
| 3074: | ExpressionResultStorage $storage, |
| 3075: | ExpressionContext $context, |
| 3076: | ): void |
| 3077: | { |
| 3078: | if ($originalCall === $normalizedCall) { |
| 3079: | return; |
| 3080: | } |
| 3081: | |
| 3082: | $keptValueIds = []; |
| 3083: | foreach ($normalizedCall->getArgs() as $normalizedArg) { |
| 3084: | $keptValueIds[spl_object_id($normalizedArg->value)] = true; |
| 3085: | } |
| 3086: | |
| 3087: | foreach ($originalCall->getArgs() as $originalArg) { |
| 3088: | if (isset($keptValueIds[spl_object_id($originalArg->value)])) { |
| 3089: | continue; |
| 3090: | } |
| 3091: | |
| 3092: | $this->processExprNode($stmt, $originalArg->value, $scope, $storage, new NoopNodeCallback(), $context->enterDeep()); |
| 3093: | } |
| 3094: | } |
| 3095: | |
| 3096: | |
| 3097: | |
| 3098: | |
| 3099: | private function callCallbackImmediately(?ParameterReflection $parameter, ?Type $parameterType, $calleeReflection): bool |
| 3100: | { |
| 3101: | $parameterCallableType = null; |
| 3102: | if ($parameterType !== null && $calleeReflection instanceof FunctionReflection) { |
| 3103: | $parameterCallableType = TypeUtils::findCallableType($parameterType); |
| 3104: | } |
| 3105: | |
| 3106: | if ($parameter instanceof ExtendedParameterReflection) { |
| 3107: | $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable(); |
| 3108: | if ($parameterCallImmediately->maybe()) { |
| 3109: | $callCallbackImmediately = $parameterCallableType !== null; |
| 3110: | } else { |
| 3111: | $callCallbackImmediately = $parameterCallImmediately->yes(); |
| 3112: | } |
| 3113: | } else { |
| 3114: | $callCallbackImmediately = $parameterCallableType !== null; |
| 3115: | } |
| 3116: | |
| 3117: | return $callCallbackImmediately; |
| 3118: | } |
| 3119: | |
| 3120: | |
| 3121: | |
| 3122: | |
| 3123: | |
| 3124: | |
| 3125: | |
| 3126: | private function shouldInvalidateCallbackExpressions(?ParameterReflection $parameter): bool |
| 3127: | { |
| 3128: | if ($parameter instanceof ExtendedParameterReflection) { |
| 3129: | return !$parameter->isImmediatelyInvokedCallable()->no(); |
| 3130: | } |
| 3131: | |
| 3132: | return true; |
| 3133: | } |
| 3134: | |
| 3135: | |
| 3136: | |
| 3137: | |
| 3138: | private function getParameterTypeFromParameterClosureTypeExtension(CallLike $callLike, $calleeReflection, ParameterReflection $parameter, MutatingScope $scope): ?Type |
| 3139: | { |
| 3140: | if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { |
| 3141: | foreach ($this->functionParameterClosureTypeExtensions->getAll() as $functionParameterClosureTypeExtension) { |
| 3142: | if ($functionParameterClosureTypeExtension->isFunctionSupported($calleeReflection, $parameter)) { |
| 3143: | return $functionParameterClosureTypeExtension->getTypeFromFunctionCall($calleeReflection, $callLike, $parameter, $scope); |
| 3144: | } |
| 3145: | } |
| 3146: | } elseif ($calleeReflection instanceof MethodReflection) { |
| 3147: | if ($callLike instanceof StaticCall) { |
| 3148: | foreach ($this->staticMethodParameterClosureTypeExtensions->getAll() as $staticMethodParameterClosureTypeExtension) { |
| 3149: | if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) { |
| 3150: | return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $callLike, $parameter, $scope); |
| 3151: | } |
| 3152: | } |
| 3153: | } elseif ($callLike instanceof New_ && $callLike->class instanceof Name) { |
| 3154: | $staticCall = new StaticCall( |
| 3155: | $callLike->class, |
| 3156: | new Identifier('__construct'), |
| 3157: | $callLike->getArgs(), |
| 3158: | ); |
| 3159: | foreach ($this->staticMethodParameterClosureTypeExtensions->getAll() as $staticMethodParameterClosureTypeExtension) { |
| 3160: | if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) { |
| 3161: | return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $staticCall, $parameter, $scope); |
| 3162: | } |
| 3163: | } |
| 3164: | } elseif ($callLike instanceof MethodCall) { |
| 3165: | foreach ($this->methodParameterClosureTypeExtensions->getAll() as $methodParameterClosureTypeExtension) { |
| 3166: | if ($methodParameterClosureTypeExtension->isMethodSupported($calleeReflection, $parameter)) { |
| 3167: | return $methodParameterClosureTypeExtension->getTypeFromMethodCall($calleeReflection, $callLike, $parameter, $scope); |
| 3168: | } |
| 3169: | } |
| 3170: | } |
| 3171: | } |
| 3172: | |
| 3173: | return null; |
| 3174: | } |
| 3175: | |
| 3176: | |
| 3177: | |
| 3178: | |
| 3179: | private function getParameterOutExtensionsType(CallLike $callLike, $calleeReflection, ParameterReflection $currentParameter, MutatingScope $scope): ?Type |
| 3180: | { |
| 3181: | $paramOutTypes = []; |
| 3182: | if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) { |
| 3183: | foreach ($this->functionParameterOutTypeExtensions->getAll() as $functionParameterOutTypeExtension) { |
| 3184: | if (!$functionParameterOutTypeExtension->isFunctionSupported($calleeReflection, $currentParameter)) { |
| 3185: | continue; |
| 3186: | } |
| 3187: | |
| 3188: | $resolvedType = $functionParameterOutTypeExtension->getParameterOutTypeFromFunctionCall($calleeReflection, $callLike, $currentParameter, $scope); |
| 3189: | if ($resolvedType === null) { |
| 3190: | continue; |
| 3191: | } |
| 3192: | $paramOutTypes[] = $resolvedType; |
| 3193: | } |
| 3194: | } elseif ($callLike instanceof MethodCall && $calleeReflection instanceof MethodReflection) { |
| 3195: | foreach ($this->methodParameterOutTypeExtensions->getAll() as $methodParameterOutTypeExtension) { |
| 3196: | if (!$methodParameterOutTypeExtension->isMethodSupported($calleeReflection, $currentParameter)) { |
| 3197: | continue; |
| 3198: | } |
| 3199: | |
| 3200: | $resolvedType = $methodParameterOutTypeExtension->getParameterOutTypeFromMethodCall($calleeReflection, $callLike, $currentParameter, $scope); |
| 3201: | if ($resolvedType === null) { |
| 3202: | continue; |
| 3203: | } |
| 3204: | $paramOutTypes[] = $resolvedType; |
| 3205: | } |
| 3206: | } elseif ($callLike instanceof StaticCall && $calleeReflection instanceof MethodReflection) { |
| 3207: | foreach ($this->staticMethodParameterOutTypeExtensions->getAll() as $staticMethodParameterOutTypeExtension) { |
| 3208: | if (!$staticMethodParameterOutTypeExtension->isStaticMethodSupported($calleeReflection, $currentParameter)) { |
| 3209: | continue; |
| 3210: | } |
| 3211: | |
| 3212: | $resolvedType = $staticMethodParameterOutTypeExtension->getParameterOutTypeFromStaticMethodCall($calleeReflection, $callLike, $currentParameter, $scope); |
| 3213: | if ($resolvedType === null) { |
| 3214: | continue; |
| 3215: | } |
| 3216: | $paramOutTypes[] = $resolvedType; |
| 3217: | } |
| 3218: | } |
| 3219: | |
| 3220: | if (count($paramOutTypes) === 1) { |
| 3221: | return $paramOutTypes[0]; |
| 3222: | } |
| 3223: | |
| 3224: | if (count($paramOutTypes) > 1) { |
| 3225: | return TypeCombinator::union(...$paramOutTypes); |
| 3226: | } |
| 3227: | |
| 3228: | return null; |
| 3229: | } |
| 3230: | |
| 3231: | |
| 3232: | |
| 3233: | |
| 3234: | public function processVirtualAssign(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, Expr $var, Expr $assignedExpr, callable $nodeCallback, ?ExpressionResult $assignedExprResult = null): ExpressionResult |
| 3235: | { |
| 3236: | |
| 3237: | |
| 3238: | |
| 3239: | |
| 3240: | |
| 3241: | if ( |
| 3242: | $assignedExprResult === null |
| 3243: | && ($assignedExpr instanceof TypeExpr || $assignedExpr instanceof NativeTypeExpr) |
| 3244: | && $storage->findExpressionResult($assignedExpr) === null |
| 3245: | ) { |
| 3246: | $assignedExprResult = $this->container->getByType(VirtualExprResultHelper::class)->createTypeExprResult($scope, $assignedExpr); |
| 3247: | } |
| 3248: | |
| 3249: | $assignHandler = $this->container->getByType(AssignHandler::class); |
| 3250: | $virtualAssignNodeCallback = VirtualAssignNodeCallback::create($nodeCallback); |
| 3251: | $target = $assignHandler->prepareTarget( |
| 3252: | $this, |
| 3253: | $scope, |
| 3254: | $storage, |
| 3255: | $stmt, |
| 3256: | $var, |
| 3257: | $assignedExpr, |
| 3258: | $virtualAssignNodeCallback, |
| 3259: | ExpressionContext::createDeep(), |
| 3260: | AssignTargetWalkMode::virtualAssign(), |
| 3261: | ); |
| 3262: | |
| 3263: | return $assignHandler->applyWrite( |
| 3264: | $this, |
| 3265: | $target, |
| 3266: | $this->expressionResultFactory->create( |
| 3267: | $target->getScope(), |
| 3268: | beforeScope: $target->getScope(), |
| 3269: | expr: $assignedExpr, |
| 3270: | hasYield: false, |
| 3271: | isAlwaysTerminating: false, |
| 3272: | throwPoints: [], |
| 3273: | impurePoints: [], |
| 3274: | typeCallback: static fn () => new MixedType(), |
| 3275: | specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), |
| 3276: | ), |
| 3277: | $assignedExprResult, |
| 3278: | $stmt, |
| 3279: | $storage, |
| 3280: | $virtualAssignNodeCallback, |
| 3281: | ExpressionContext::createDeep(), |
| 3282: | ); |
| 3283: | } |
| 3284: | |
| 3285: | |
| 3286: | |
| 3287: | |
| 3288: | public function processStmtVarAnnotation(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, ?Expr $defaultExpr, callable $nodeCallback): MutatingScope |
| 3289: | { |
| 3290: | $function = $scope->getFunction(); |
| 3291: | $variableLessTags = []; |
| 3292: | |
| 3293: | foreach ($stmt->getComments() as $comment) { |
| 3294: | if (!$comment instanceof Doc) { |
| 3295: | continue; |
| 3296: | } |
| 3297: | |
| 3298: | $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( |
| 3299: | $scope->getFile(), |
| 3300: | $scope->isInClass() ? $scope->getClassReflection()->getName() : null, |
| 3301: | $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, |
| 3302: | $function !== null ? $function->getName() : null, |
| 3303: | $comment->getText(), |
| 3304: | ); |
| 3305: | |
| 3306: | $assignedVariable = null; |
| 3307: | if ( |
| 3308: | $stmt instanceof Node\Stmt\Expression |
| 3309: | && ($stmt->expr instanceof Assign || $stmt->expr instanceof AssignRef) |
| 3310: | && $stmt->expr->var instanceof Variable |
| 3311: | && is_string($stmt->expr->var->name) |
| 3312: | ) { |
| 3313: | $assignedVariable = $stmt->expr->var->name; |
| 3314: | } |
| 3315: | |
| 3316: | foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) { |
| 3317: | if (is_int($name)) { |
| 3318: | $variableLessTags[] = $varTag; |
| 3319: | continue; |
| 3320: | } |
| 3321: | |
| 3322: | if ($name === $assignedVariable) { |
| 3323: | continue; |
| 3324: | } |
| 3325: | |
| 3326: | $certainty = $scope->hasVariableType($name); |
| 3327: | if ($certainty->no()) { |
| 3328: | continue; |
| 3329: | } |
| 3330: | |
| 3331: | if ($scope->isInClass() && $scope->getFunction() === null) { |
| 3332: | continue; |
| 3333: | } |
| 3334: | |
| 3335: | if ($scope->canAnyVariableExist()) { |
| 3336: | $certainty = TrinaryLogic::createYes(); |
| 3337: | } |
| 3338: | |
| 3339: | $variableNode = new Variable($name, $stmt->getAttributes()); |
| 3340: | $originalType = $scope->getVariableType($name); |
| 3341: | if (!$originalType->equals($varTag->getType())) { |
| 3342: | $this->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $variableNode), $scope, $storage); |
| 3343: | } |
| 3344: | |
| 3345: | $nativeScope = $scope->doNotTreatPhpDocTypesAsCertain(); |
| 3346: | $scope = $scope->assignVariable( |
| 3347: | $name, |
| 3348: | $varTag->getType(), |
| 3349: | |
| 3350: | $nativeScope->hasVariableType($name)->no() ? new ErrorType() : $nativeScope->getVariableType($name), |
| 3351: | $certainty, |
| 3352: | ); |
| 3353: | } |
| 3354: | } |
| 3355: | |
| 3356: | if (count($variableLessTags) === 1 && $defaultExpr !== null) { |
| 3357: | |
| 3358: | |
| 3359: | |
| 3360: | |
| 3361: | $scope = $scope->assignExpression($defaultExpr, $variableLessTags[0]->getType(), new MixedType()); |
| 3362: | } |
| 3363: | |
| 3364: | return $scope; |
| 3365: | } |
| 3366: | |
| 3367: | |
| 3368: | |
| 3369: | |
| 3370: | |
| 3371: | |
| 3372: | private function findSingleVariableLessVarTag(MutatingScope $scope, Node\Stmt $stmt): ?VarTag |
| 3373: | { |
| 3374: | $function = $scope->getFunction(); |
| 3375: | $variableLessTags = []; |
| 3376: | foreach ($stmt->getComments() as $comment) { |
| 3377: | if (!$comment instanceof Doc) { |
| 3378: | continue; |
| 3379: | } |
| 3380: | |
| 3381: | $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc( |
| 3382: | $scope->getFile(), |
| 3383: | $scope->isInClass() ? $scope->getClassReflection()->getName() : null, |
| 3384: | $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null, |
| 3385: | $function !== null ? $function->getName() : null, |
| 3386: | $comment->getText(), |
| 3387: | ); |
| 3388: | foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) { |
| 3389: | if (!is_int($name)) { |
| 3390: | continue; |
| 3391: | } |
| 3392: | |
| 3393: | $variableLessTags[] = $varTag; |
| 3394: | } |
| 3395: | } |
| 3396: | |
| 3397: | return count($variableLessTags) === 1 ? $variableLessTags[0] : null; |
| 3398: | } |
| 3399: | |
| 3400: | |
| 3401: | |
| 3402: | |
| 3403: | |
| 3404: | |
| 3405: | |
| 3406: | |
| 3407: | public function emitVarTagChangedNode(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, Expr $defaultExpr, callable $nodeCallback): void |
| 3408: | { |
| 3409: | $varTag = $this->findSingleVariableLessVarTag($scope, $stmt); |
| 3410: | if ($varTag === null) { |
| 3411: | return; |
| 3412: | } |
| 3413: | |
| 3414: | $this->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope, $storage); |
| 3415: | } |
| 3416: | |
| 3417: | |
| 3418: | |
| 3419: | |
| 3420: | |
| 3421: | private function getNextUnreachableStatements(array $nodes, bool $earlyBinding): array |
| 3422: | { |
| 3423: | $stmts = []; |
| 3424: | $isPassedUnreachableStatement = false; |
| 3425: | foreach ($nodes as $node) { |
| 3426: | if ($node instanceof Node\Stmt\Label) { |
| 3427: | break; |
| 3428: | } |
| 3429: | if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) { |
| 3430: | continue; |
| 3431: | } |
| 3432: | if ($isPassedUnreachableStatement && $node instanceof Node\Stmt) { |
| 3433: | $stmts[] = $node; |
| 3434: | continue; |
| 3435: | } |
| 3436: | if ($node instanceof Node\Stmt\Nop || $node instanceof Node\Stmt\InlineHTML) { |
| 3437: | continue; |
| 3438: | } |
| 3439: | if (!$node instanceof Node\Stmt) { |
| 3440: | continue; |
| 3441: | } |
| 3442: | $stmts[] = $node; |
| 3443: | $isPassedUnreachableStatement = true; |
| 3444: | } |
| 3445: | return $stmts; |
| 3446: | } |
| 3447: | |
| 3448: | } |
| 3449: | |