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