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