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: /** @var array<string, true> filePath(string) => bool(true) */
124: private array $analysedFiles = [];
125:
126: private ?ExpressionResultStorageStack $expressionResultStorageStack = null;
127:
128: /**
129: * Engine-feeding gatherer frames (return statements, execution ends,
130: * impure points, ...), innermost last. callNodeCallback() feeds every
131: * frame the raw walk scope at the emission position - gatherers are
132: * engine code and never ask about types, and their arrays are read as
133: * soon as the enclosing body walk returns. Frames replace the former
134: * GatheringNodeCallback wrapper chain: gatherers no longer ride the
135: * node-callback channel, so callback filters (VirtualAssignNodeCallback)
136: * cannot accidentally starve them.
137: *
138: * @var list<callable(Node, Scope): void>
139: */
140: private array $nodeGatherers = [];
141:
142: /**
143: * @param ExtensionsCollection<FunctionParameterOutTypeExtension> $functionParameterOutTypeExtensions
144: * @param ExtensionsCollection<MethodParameterOutTypeExtension> $methodParameterOutTypeExtensions
145: * @param ExtensionsCollection<StaticMethodParameterOutTypeExtension> $staticMethodParameterOutTypeExtensions
146: * @param ExtensionsCollection<ReadWritePropertiesExtension> $readWritePropertiesExtensions
147: * @param ExtensionsCollection<FunctionParameterClosureThisExtension> $functionParameterClosureThisExtensions
148: * @param ExtensionsCollection<MethodParameterClosureThisExtension> $methodParameterClosureThisExtensions
149: * @param ExtensionsCollection<StaticMethodParameterClosureThisExtension> $staticMethodParameterClosureThisExtensions
150: * @param ExtensionsCollection<FunctionParameterClosureTypeExtension> $functionParameterClosureTypeExtensions
151: * @param ExtensionsCollection<MethodParameterClosureTypeExtension> $methodParameterClosureTypeExtensions
152: * @param ExtensionsCollection<StaticMethodParameterClosureTypeExtension> $staticMethodParameterClosureTypeExtensions
153: * @param ExtensionsCollection<PerFileAnalysisResettable> $perFileAnalysisResettables
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: * @api
196: * @param string[] $files
197: */
198: public function setAnalysedFiles(array $files): void
199: {
200: $this->analysedFiles = array_fill_keys($files, true);
201: }
202:
203: /**
204: * Releases the previous file's node-keyed captures: the parser cache
205: * retains ASTs, so node-keyed cache entries never die on
206: * their own and would hold that file's whole result graph alive.
207: *
208: * Called at the per-file boundary (FileAnalyser), NOT in processNodes():
209: * extensions start nested processNodes() walks mid-file (phpstan-doctrine
210: * parsing a query-builder method, rule tooling re-analysing a callee) and
211: * wiping the per-file caches there forces the outer file to rebuild them -
212: * closure types re-converge, narrowing memos recompute.
213: */
214: public function resetPerFileAnalysisState(): void
215: {
216: foreach ($this->perFileAnalysisResettables->getAll() as $resettableService) {
217: $resettableService->resetFileAnalysisState();
218: }
219: }
220:
221: /**
222: * @api
223: * @param Node[] $nodes
224: * @param callable(Node $node, Scope $scope): void $nodeCallback
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: // a fresh walk an extension starts mid-analysis must not feed the
248: // interrupted walk's gatherer frames (see processStmtNodes())
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: * @param Node[] $nodes
260: * @param Node\Stmt[] $stmts
261: * @param array<int, int> $stmtToNodeIndex
262: * @param callable(Node $node, Scope $scope): void $nodeCallback
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: // The storage only ever answers type questions from NodeCallbackScope, which
339: // resolves them from the before-scope. Storing just the before-scope
340: // keeps the storage from pinning throw points, impure points, scope
341: // callbacks and the after-scope of every expression until the end of
342: // the file.
343: $storage->storeBeforeScope($expr, $expressionResult->getBeforeScope());
344: }
345:
346: /**
347: * @param Node\Stmt[] $bodyStmts
348: * @param Closure(string): bool $gotoNameMatcher
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: // walking is deterministic in the entry scope - an unchanged entry
370: // reproduces the previous pass's exit, so the verification walk is
371: // skipped
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: * @param InternalStatementExitPoint[] $exitPoints
415: * @return array{MutatingScope, bool, list<InternalStatementExitPoint>}
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: * @param Node\Stmt[] $nextStmts
444: * @param callable(Node $node, Scope $scope): void $nodeCallback
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: * @api
473: * @param Node\Stmt[] $stmts
474: * @param callable(Node $node, Scope $scope): void $nodeCallback
475: */
476: public function processStmtNodes(
477: Node $parentNode,
478: array $stmts,
479: MutatingScope $scope,
480: callable $nodeCallback,
481: StatementContext $context,
482: ): StatementResult
483: {
484: // a rule may pass the scope it was handed - the rule-facing NodeCallbackScope -
485: // as the walk's initial scope; the walk must anchor its results to the
486: // state-identical MutatingScope or their consumption re-enters the
487: // rule-facing ask paths
488: $scope = $scope->toWalkScope();
489: $storage = new ExpressionResultStorage();
490: // a fresh walk an extension starts mid-analysis must not feed the
491: // interrupted walk's gatherer frames - they describe the body walk
492: // that was interrupted, not the nested one
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: * @param Node\Stmt[] $stmts
511: * @param callable(Node $node, Scope $scope): void $nodeCallback
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: * @param Node\Stmt[] $stmts
527: * @param callable(Node $node, Scope $scope): void $nodeCallback
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: * @param callable(Node $node, Scope $scope): void $nodeCallback
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: // Statements whose work is processing their expressions emit their node
727: // callback AFTER that processing, inside their branches below, with the
728: // entry scope - a synchronously invoked rule (the plain resolver,
729: // PHP < 8.1) then finds the expressions' results in the storage instead
730: // of re-walking them on demand, mirroring processExprNodeInternal().
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: // statements with no analysis of their own (e.g. HaltCompiler)
759: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: $overridingThrowPoints ?? [], impurePoints: []);
760: }
761:
762: /**
763: * @return InternalThrowPoint[]|null
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: /** @return ExtensionsCollection<ReadWritePropertiesExtension> */
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: * @param Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback
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: * @param callable(Node $node, Scope $scope): void $nodeCallback
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: // The node's own callback fires AFTER its result is stored, with the
903: // scope captured before processing. Rules observe the same (scope,
904: // answer) pair as at a pre-order emission - under fibers a pre-order
905: // rule parks on its first ask and resumes at this store anyway - but
906: // a synchronously invoked rule (the plain resolver, PHP < 8.1) now
907: // finds the node's and its subtree's results in the storage instead
908: // of re-walking them on demand.
909: $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $storage, $context);
910: // the call is now processed and stored; emit a virtual node so
911: // impossible-check rules run on the fully processed call instead of
912: // asking the scope before the call node itself is processed
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: * @return string[]
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: * Whether a recorded convergence pass over the loop body can replace the
974: * final walk. A pass runs at deep statement context, the final walk at top
975: * level - constructs that analyse differently between the two (nested
976: * loop/label fixpoints run only at top level, statement-level classes are
977: * skipped at deep context) disqualify the body. Closure bodies process
978: * context-independently and are not traversed.
979: *
980: * @param Node\Stmt[] $bodyStmts
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: * Replays a recorded convergence pass's emissions through the real node
1037: * callback in place of the final loop walk. The pass's storage was merged
1038: * into $storage by the caller; binding it for the whole replay lets the
1039: * recorded scopes answer rule asks from the stored before-scopes, the
1040: * same way the repeated walk's per-emission binding would.
1041: *
1042: * @param callable(Node $node, Scope $scope): void $nodeCallback
1043: */
1044: /**
1045: * Opens an engine-feeding gatherer frame for the duration of a body walk.
1046: * The caller closes it in a finally block via popNodeGatherer().
1047: *
1048: * @param callable(Node, Scope): void $gatherer
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: * @param callable(Node $node, Scope $scope): void $nodeCallback
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: // gatherer frames observe replayed emissions exactly like live
1073: // ones - with the raw walk scope
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: * @param callable(Node $node, Scope $scope): void $nodeCallback
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: * @param callable(Node $node, Scope $scope): void $nodeCallback
1103: */
1104: public function callNodeCallback(
1105: callable $nodeCallback,
1106: Node $node,
1107: MutatingScope $scope,
1108: ExpressionResultStorage $storage,
1109: ): void
1110: {
1111: // Engine-feeding gatherer frames observe the node at the emission
1112: // position - their arrays are read as soon as the enclosing body walk
1113: // returns. Gatherers are engine code and never ask about types -
1114: // handing them the raw scope skips a NodeCallbackScope construction per
1115: // emission; the scopes they capture (return statements, impure points)
1116: // answer later asks through the storage hub like any MutatingScope.
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: // recording never asks about types - the pairs are wrapped and
1127: // bound to the storage at replay time instead
1128: $nodeCallback($node, $scope);
1129: return;
1130: }
1131:
1132: // post-order emission means the node's own result and every subnode
1133: // result are already stored when the callback fires - NodeCallbackScope
1134: // answers every ask synchronously from the storage; the emitting
1135: // walk's storage is bound for the duration of the callback
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: * @param callable(Node $node, Scope $scope): void $nodeCallback
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: // deep context, like the loop handlers' own convergence passes: inner
1319: // loops walk single-pass here and only the final walk below (top-level)
1320: // runs their full convergence - otherwise every closure-convergence
1321: // pass would re-converge every inner loop from scratch
1322: $intermediaryClosureScopeResult = $this->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $bodyRecording, StatementContext::createDeep());
1323: // the candidate to replace the final walk when this pass's entry
1324: // turns out to be the fixpoint
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: // the final walk would repeat the recorded fixpoint pass exactly
1366: // (same entry scope, deterministic walk) - adopt the pass's result
1367: // and replay its emissions through the real callback instead.
1368: // The pass's own entry scope takes over: the recorded pairs carry
1369: // its anonymous-function reflection, which the gatherer's filter
1370: // compares by identity (the state is equals-identical anyway).
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: * The refined closure type built from the single body walk, swapped onto the
1408: * closure scope so ClosureReturnStatementsNode's rules see the refined
1409: * expected return instead of the shallow entry reflection.
1410: *
1411: * @param list<array{Node\Stmt\Return_, Scope}> $gatheredReturnStatementsWithScope
1412: * @param list<array{Expr\Yield_|Expr\YieldFrom, Scope}> $gatheredYieldStatementsWithScope
1413: * @param list<ExecutionEndNode> $executionEnds
1414: * @param InternalThrowPoint[] $throwPoints
1415: * @param ImpurePoint[] $impurePoints
1416: * @param InvalidateExprNode[] $invalidateExpressions
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: * @param InvalidateExprNode[] $invalidatedExpressions
1448: * @param string[] $uses
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: * @param callable(Node $node, Scope $scope): void $nodeCallback
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: // Gather the property-assign impure points and invalidate expressions the
1499: // arrow function type needs (mirroring ClosureTypeResolver::getClosureType()),
1500: // on top of the regular rule node callback, so the single body walk here
1501: // feeds ClosureTypeResolver::buildClosureTypeForArrowFunction().
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: // The arrow scope was entered with a shallow reflection (parameters +
1539: // declared return, no body walk). Now that the single body walk above has
1540: // run, build the refined arrow function type from the walked body (no
1541: // second walk) and fire InArrowFunctionNode with it, so the node and the
1542: // return-type rules see the refined expected return. The build must not
1543: // write the type cache: its values reflect this call's (possibly
1544: // extension-overridden) parameter typing while its key would match a
1545: // plain pricing ask.
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: * @param Node\Arg[]|null $args
1570: * @return ParameterReflection[]|null
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: * @param Node\Arg[]|null $args
1579: * @return ParameterReflection[]|null
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: * @param Node\Arg[]|null $args
1588: * @param Closure(Scope, Expr): Type $typeGetter
1589: * @return ParameterReflection[]|null
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: * @param callable(Node $node, Scope $scope): void $nodeCallback
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: * @param AttributeGroup[] $attrGroups
1703: * @param callable(Node $node, Scope $scope): void $nodeCallback
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: * @param FunctionReflection|MethodReflection|null $calleeReflection
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: * @param MethodReflection|FunctionReflection|null $calleeReflection
1794: * @param ParametersAcceptor[] $parametersAcceptors
1795: * @param ParametersAcceptor[]|null $namedArgumentsVariants
1796: * @param callable(Node $node, Scope $scope): void $nodeCallback
1797: * @param (callable(MutatingScope): MutatingScope)|null $closureBindScopeFactory
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: // Evolving-scope arg types: gathered as each argument is processed on the
1816: // scope that evolves arg-to-arg. They select the FINAL resolved acceptor
1817: // (the call's return type, by-ref OUT types), which type-resolves generics
1818: // from the actual argument types.
1819: $gatheredTypes = [];
1820: $gatheredUnpack = false;
1821: $gatheredHasName = false;
1822: $gatheredArgTypeByIndex = [];
1823:
1824: // The intrinsic argument overrides (array_map/filter/walk/find, curl_setopt,
1825: // implode, Closure::bind) rewrite a callback parameter's type from its
1826: // sibling arguments. Apply them up front on the entry scope - the parameter
1827: // pushed on the in-function-call stack while each argument is processed (and
1828: // priced, e.g. a closure's inferred return type) must be the overridden one,
1829: // exactly as when the caller pre-selected via selectFromArgs().
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: // Metadata acceptor base - NO forward read. The per-argument resolution below picks the
1842: // count-correct variant (the by-ref/variadic STRUCTURE is variant-stable except where it is
1843: // keyed off the argument count, e.g. sscanf - and the count is known structurally) and
1844: // resolves generic parameter types from the args gathered so far; the call's return type
1845: // comes from the post-loop resolved acceptor.
1846: $metadataAcceptor = $parametersAcceptors[0] ?? null;
1847:
1848: // Both predicates are hoisted out of the per-argument loop - they traverse
1849: // the acceptor's parameter/return types.
1850: $hasTemplateParameterType = $metadataAcceptor !== null
1851: && ParametersAcceptorSelector::hasAcceptorTemplateOrLateResolvableParameterType($metadataAcceptor);
1852: $argMetadataIsTypeDriven = count($parametersAcceptors) > 1 || $hasTemplateParameterType;
1853:
1854: // Whether selecting an acceptor is type-driven at all: multiple variants to
1855: // choose between, templates or conditionals to resolve from the arg types,
1856: // or named-argument variants. When it is not, the gathered arg types can
1857: // never influence the selected acceptor, so the faithful-return gather walk
1858: // of a closure/arrow argument (gatherClosureArgType()) would be pure waste -
1859: // a plain mixed keeps the count/name bookkeeping correct.
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: /** @var list<array{InvalidateExprNode[], string[]}> $deferredInvalidateExpressions */
1870: $deferredInvalidateExpressions = [];
1871: /** @var ProcessClosureResult[] $deferredByRefClosureResults */
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: // closures sort after non-closures so every sibling feeding an
1884: // intrinsic override / generic callable(T) is in scope first
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: // Gather the closure/arrow type for the FINAL resolved acceptor on
1909: // the evolving scope, BEFORE the body is processed with a possibly
1910: // generic-resolved parameter injected, so the inferred return type
1911: // stays faithful to the closure's own declaration and its own
1912: // contribution (a TValue from its return) participates in the final
1913: // resolution (see gatherClosureArgType()).
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: // Resolve the acceptor for this argument from the args gathered SO FAR, padded to the
1925: // full argument count with mixed. Closures sort last and by-ref out-params follow the
1926: // args that pin them, so determining siblings are already processed; the mixed pad keeps
1927: // the argument COUNT correct so the by-ref/variadic variant stays stable (e.g. sscanf),
1928: // while processed siblings resolve a generic callable(T) parameter. No forward read.
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: // Only a closure/arrow function consumes the generic-RESOLVED
1939: // parameter type: its body is inferred from the resolved
1940: // callable(T) - directly, or through the in-function-call stack
1941: // when nested anywhere inside the argument. Every other argument
1942: // reads variant-stable facts off its parameter (by-ref flag,
1943: // callable bookkeeping), so one all-mixed count-stable selection
1944: // serves them all instead of a full template inference per argument.
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: // resolve the native flavour through the same extension on the
2051: // natively-promoted scope, so the closure parameters keep
2052: // their native precision too
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: // the preferred ClosureType read below now answers from this seed
2062: // instead of walking the body again (unless a parked fiber may
2063: // still complete the gathered data - then it keeps re-walking)
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: // the closure node's own callback fires after its result is
2080: // stored, mirroring processExprNodeInternal() - callback-side
2081: // getType() answers from the stored result
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: // Prefer the invalidate expressions collected on the ClosureType: those
2096: // are gathered with the closure's pending fibers flushed, so they also
2097: // cover writes that go through a parked fiber (e.g. $this->prop[] = ...),
2098: // unlike $closureResult->getInvalidateExpressions().
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: // resolve the native flavour through the same extension on the
2142: // natively-promoted scope, so the closure parameters keep
2143: // their native precision too
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: // the invalidation read below now answers from this seed instead
2153: // of walking the body again (unless a parked fiber may still
2154: // complete the gathered data - then it keeps re-walking)
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: // the arrow function node's own callback fires after its result
2169: // is stored, mirroring processExprNodeInternal() - callback-side
2170: // getType() answers from the stored result
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: // Type-driven resolved acceptor: the arg types gathered on the evolving
2233: // scope select (and generic-resolve) the acceptor that drives the call's
2234: // return type. Intrinsic overrides are applied on the final scope,
2235: // mirroring the original selectFromArgs(). When the selection is not
2236: // type-driven, the single (already-overridden) acceptor IS the resolved
2237: // acceptor - the fast path selectFromArgs() used to take.
2238: $resolvedAcceptor = null;
2239: if ($parametersAcceptors !== []) {
2240: $resolvedAcceptor = $typeDrivenAcceptorSelection
2241: ? $this->selectArgsMetadataAcceptor($args, $gatheredTypes, $parametersAcceptors, $namedArgumentsVariants, $gatheredHasName, $gatheredUnpack, $scope)
2242: : $metadataAcceptor;
2243: }
2244:
2245: // The by-ref OUT writeback reads the metadata acceptor: it is selected from
2246: // the full argument count (stable variant). When that single acceptor still
2247: // carries templates (fast path), its OUT types need generic-resolving from the
2248: // now-complete gathered arg types - the post-loop $resolvedAcceptor is exactly
2249: // that (same variant, resolved); otherwise the metadata acceptor is already resolved.
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: // not storing this, it's scope after processing all args
2339: return new ArgsResult(
2340: $this->expressionResultFactory->create($scope, $scope, $callLike, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints),
2341: $resolvedAcceptor,
2342: );
2343: }
2344:
2345: /**
2346: * Applies the intrinsic argument overrides (array_map/filter/walk/find,
2347: * curl_setopt, implode, Closure::bind) on the arg-to-arg evolved scope,
2348: * then type-selects the metadata acceptor over
2349: * the arg types gathered so far. The overrides read sibling arg types - which
2350: * closures-last ordering keeps in scope/$gatheredTypes before any closure.
2351: *
2352: * @param Node\Arg[] $args
2353: * @param array<int|string, Type> $gatheredTypes
2354: * @param ParametersAcceptor[] $parametersAcceptors
2355: * @param ParametersAcceptor[]|null $namedArgumentsVariants
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: * @param array<int|string, Type> $types
2375: * @param ParametersAcceptor[] $parametersAcceptors
2376: * @param ParametersAcceptor[]|null $namedArgumentsVariants
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: * Ports the gather-keying of ParametersAcceptorSelector::selectFromArgs():
2387: * indexes the gathered arg type by name (sets $hasName) vs position, and
2388: * expands unpacked constant arrays / falls back to the iterable value type
2389: * (sets $unpack), so selectFromTypes() picks the matching variant.
2390: *
2391: * @param array<int|string, Type> $types
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: * Resolves the type of a closure/arrow function argument for the generic
2432: * gather, mirroring ParametersAcceptorSelector::selectFromArgs(): the closure
2433: * type is read with the RAW (un-generic-resolved) acceptor parameter pushed
2434: * onto the in-function-call stack, so its body sees the template parameter
2435: * (effectively mixed for an untyped param) rather than a parameter already
2436: * resolved from sibling args. That keeps the inferred return type (the U in
2437: * callable(T): U) faithful to the closure's own declaration.
2438: *
2439: * @param ParametersAcceptor[] $parametersAcceptors
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: * Whether processing this argument consumes the generic-RESOLVED parameter
2462: * type: a closure/arrow function does - its parameters and body scope are
2463: * typed from the resolved callable(T) - whether it IS the argument or is
2464: * nested anywhere inside it (the enclosing parameter is pushed on the
2465: * in-function-call stack and the nested closure types itself from there).
2466: * Every other argument only reads variant-stable facts off its parameter.
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: // cached on the node - args are re-processed across convergence passes
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: * @param MethodReflection|FunctionReflection|null $calleeReflection
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: * A callback passed as an argument escapes the current scope and may be invoked,
2515: * so its mutations have to invalidate the outer scope - unless the parameter is
2516: * explicitly marked as later-invoked, in which case the callback only runs after
2517: * the current function returns and its mutations are not visible here yet.
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: * @param MethodReflection|FunctionReflection|null $calleeReflection
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: * @param MethodReflection|FunctionReflection|null $calleeReflection
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: * @param callable(Node $node, Scope $scope): void $nodeCallback
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: * @param callable(Node $node, Scope $scope): void $nodeCallback
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: * @param array<Node> $nodes
2737: * @return list<Node\Stmt>
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: