1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use Closure;
6: use PhpParser\Node;
7: use PhpParser\Node\Expr;
8: use PhpParser\Node\Expr\ArrayDimFetch;
9: use PhpParser\Node\Expr\FuncCall;
10: use PhpParser\Node\Expr\List_;
11: use PhpParser\Node\Expr\MethodCall;
12: use PhpParser\Node\Expr\New_;
13: use PhpParser\Node\Expr\PropertyFetch;
14: use PhpParser\Node\Expr\StaticCall;
15: use PhpParser\Node\Expr\StaticPropertyFetch;
16: use PhpParser\Node\Expr\Variable;
17: use PhpParser\Node\Identifier;
18: use PhpParser\Node\Stmt\Class_;
19: use PhpParser\Node\Stmt\Echo_;
20: use PhpParser\Node\Stmt\Foreach_;
21: use PhpParser\Node\Stmt\If_;
22: use PhpParser\Node\Stmt\Return_;
23: use PhpParser\Node\Stmt\Static_;
24: use PhpParser\Node\Stmt\Switch_;
25: use PhpParser\NodeFinder;
26: use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper;
27: use PHPStan\Analyser\Generics\TemplateArgumentConstraints;
28: use PHPStan\Analyser\Generics\TemplateArgumentFrame;
29: use PHPStan\Analyser\Generics\TemplateArgumentObserver;
30: use PHPStan\Analyser\Generics\TemplateArgumentStats;
31: use PHPStan\Dependency\Dependencies;
32: use PHPStan\DependencyInjection\AutowiredExtensions;
33: use PHPStan\DependencyInjection\AutowiredService;
34: use PHPStan\DependencyInjection\Container;
35: use PHPStan\DependencyInjection\ExtensionsCollection;
36: use PHPStan\File\FileHelper;
37: use PHPStan\Node\FunctionCallableNode;
38: use PHPStan\Node\FunctionCallExpressionNode;
39: use PHPStan\Node\InstantiationCallableNode;
40: use PHPStan\Node\MethodCallableNode;
41: use PHPStan\Node\MethodCallExpressionNode;
42: use PHPStan\Node\StaticMethodCallableNode;
43: use PHPStan\Node\StaticMethodCallExpressionNode;
44: use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
45: use PHPStan\Reflection\Php\PhpPropertyReflection;
46: use PHPStan\ShouldNotHappenException;
47: use PHPStan\Turbo\ShadowedByTurboExtension;
48: use PHPStan\Type\ErrorType;
49: use PHPStan\Type\MixedType;
50: use PHPStan\Type\Type;
51: use PHPStan\Type\TypeUtils;
52: use function array_merge;
53: use function array_pop;
54: use function get_class;
55: use function getenv;
56: use function is_array;
57: use function is_string;
58: use function spl_object_id;
59: use function sprintf;
60:
61: #[AutowiredService]
62: #[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/NodeScopeResolver.cpp')]
63: class NodeScopeResolver
64: {
65:
66: public const LOOP_SCOPE_ITERATIONS = 3;
67: public const GENERALIZE_AFTER_ITERATION = 1;
68:
69: /** @var array<string, true> filePath(string) => bool(true) */
70: private array $analysedFiles = [];
71:
72: /**
73: * When processing a synthetic node on demand, real AST
74: * nodes contained in it were already processed and must not be processed again.
75: */
76: protected bool $returnStoredExpressionResults = false;
77:
78: /**
79: * Consume-stored mode: a walk that deliberately re-enters an
80: * already-walked subtree (the nullsafe plain twin re-walking its
81: * receiver) consumes this node's stored result unconditionally instead
82: * of re-processing it - node callbacks fired during the original walk.
83: * Only this exact node is consumed: the rest of the twin (arguments,
84: * a dynamic name) is walked normally, even when the storage was seeded
85: * with results of an earlier loop convergence pass.
86: */
87: private ?Expr $consumeStoredExpression = null;
88:
89: private ?NonNullabilityHelper $nonNullabilityHelper = null;
90:
91: /**
92: * Engine-feeding gatherer frames (return statements, execution ends,
93: * impure points, ...), innermost last. callNodeCallback() feeds every
94: * frame the raw walk scope at the emission position - gatherers are
95: * engine code and never ask about types, and their arrays are read as
96: * soon as the enclosing body walk returns.
97: *
98: * @var list<callable(Node, Scope): void>
99: */
100: private array $nodeGatherers = [];
101:
102: /** Whether the PHPSTAN_GUARD_NW diagnostic is enabled (cached from the env). */
103: public static bool $guardNewWorld = false;
104:
105: /**
106: * spl_object_id => true of every Expr in the file's parsed AST. Populated
107: * only when the PHPSTAN_GUARD_NW diagnostic is enabled, so the guards can
108: * tell a real AST node from a node a rule built during analysis (which
109: * legitimately resolves on demand). Static so MutatingScope can read it.
110: *
111: * @var array<int, true>
112: */
113: public static array $guardRealExprIds = [];
114:
115: /**
116: * spl_object_id => true of every Expr already processed by processExprNode
117: * in the current file. Used by the MutatingScope::getType guard to detect a
118: * real AST node whose type is asked before it was processed.
119: *
120: * @var array<int, true>
121: */
122: public static array $guardProcessedExprIds = [];
123:
124: /**
125: * @param ExtensionsCollection<PerFileAnalysisResettable> $perFileAnalysisResettables
126: */
127: public function __construct(
128: private readonly Container $container,
129: private readonly TemplateArgumentObserver $templateArgumentObserver,
130: private readonly FileHelper $fileHelper,
131: #[AutowiredExtensions(of: PerFileAnalysisResettable::class)]
132: private readonly ExtensionsCollection $perFileAnalysisResettables,
133: private readonly ExpressionResultFactory $expressionResultFactory,
134: private readonly StatementsHandler $statementsHandler,
135: )
136: {
137: self::$guardNewWorld = getenv('PHPSTAN_GUARD_NW') === '1';
138: TemplateArgumentStats::enableFromEnvironment();
139: }
140:
141: /**
142: * The lookups (isAnalysedFile()) are keyed by normalized paths, so the
143: * given paths are normalized here - a caller-provided unnormalized path
144: * (mixed directory separators on Windows) must not silently skip the
145: * in-class-context analysis of a trait.
146: *
147: * @api
148: * @param string[] $files
149: */
150: public function setAnalysedFiles(array $files): void
151: {
152: $analysedFiles = [];
153: foreach ($files as $file) {
154: $analysedFiles[$this->fileHelper->normalizePath($file)] = true;
155: }
156: $this->analysedFiles = $analysedFiles;
157: }
158:
159: /**
160: * Releases the previous file's node-keyed captures: the parser cache
161: * retains ASTs, so node-keyed cache entries never die on
162: * their own and would hold that file's whole result graph alive.
163: *
164: * Called at the per-file boundary (FileAnalyser), NOT in processNodes():
165: * extensions start nested processNodes() walks mid-file (phpstan-doctrine
166: * parsing a query-builder method, rule tooling re-analysing a callee) and
167: * wiping the per-file caches there forces the outer file to rebuild them -
168: * closure types re-converge, narrowing memos recompute.
169: */
170: private function getNonNullabilityHelper(): NonNullabilityHelper
171: {
172: return $this->nonNullabilityHelper ??= $this->container->getByType(NonNullabilityHelper::class);
173: }
174:
175: public function resetPerFileAnalysisState(): void
176: {
177: foreach ($this->perFileAnalysisResettables->getAll() as $resettableService) {
178: $resettableService->resetFileAnalysisState();
179: }
180: }
181:
182: /**
183: * @api
184: * @param Node[] $nodes
185: * @param callable(Node $node, Scope $scope): void $nodeCallback
186: */
187: public function processNodes(
188: array $nodes,
189: MutatingScope $scope,
190: callable $nodeCallback,
191: ): void
192: {
193: $this->processFileNodes($nodes, $scope, $nodeCallback);
194: }
195:
196: /**
197: * processNodes() for the analysis of a file: returns what the file depends on besides itself.
198: *
199: * @param Node[] $nodes
200: * @param callable(Node $node, Scope $scope): void $nodeCallback
201: */
202: public function processFileNodes(
203: array $nodes,
204: MutatingScope $scope,
205: callable $nodeCallback,
206: ): ?Dependencies
207: {
208: $scope = $scope->toWalkScope();
209: if (self::$guardNewWorld) {
210: self::$guardRealExprIds = [];
211: self::$guardProcessedExprIds = [];
212: foreach ((new NodeFinder())->findInstanceOf($nodes, Expr::class) as $realExpr) {
213: self::$guardRealExprIds[spl_object_id($realExpr)] = true;
214: }
215: }
216:
217: $expressionResultStorage = new ExpressionResultStorage();
218: $scope->pushExpressionResultStorage($expressionResultStorage);
219: // a fresh walk an extension starts mid-analysis must not feed the
220: // interrupted walk's gatherer frames (see processStmtNodes())
221: $gatherers = $this->suspendNodeGatherers();
222: try {
223: return $this->statementsHandler->processNodesWithStorage($this, $nodes, $scope, $expressionResultStorage, $nodeCallback);
224: } finally {
225: $this->restoreNodeGatherers($gatherers);
226: $scope->popExpressionResultStorage();
227: }
228: }
229:
230: public function storeExpressionResult(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $expressionResult): void
231: {
232: if (self::$guardNewWorld) {
233: self::$guardProcessedExprIds[spl_object_id($expr)] = true;
234: }
235: // handlers are answered from stored results in both worlds
236: $storage->storeExpressionResult($expr, $expressionResult);
237: }
238:
239: /**
240: * Narrows a scope by a (often synthetic) control-flow condition the new-world
241: * way: resolve its narrowing through the scope's on-demand dispatcher and apply
242: * it via applySpecifiedTypes, instead of the old-world filterBy*Value().
243: */
244: public function narrowScopeWithCondition(MutatingScope $scope, Expr $expr, TypeSpecifierContext $context): MutatingScope
245: {
246: $specifiedTypes = $scope->specifyTypesOfNewWorldHandlerNode($expr, $context);
247:
248: return $scope->applySpecifiedTypes($specifiedTypes);
249: }
250:
251: /**
252: * @api
253: * @param Node\Stmt[] $stmts
254: * @param callable(Node $node, Scope $scope): void $nodeCallback
255: */
256: public function processStmtNodes(
257: Node $parentNode,
258: array $stmts,
259: MutatingScope $scope,
260: callable $nodeCallback,
261: StatementContext $context,
262: ): StatementResult
263: {
264: // a rule may pass the scope it was handed - the rule-facing NodeCallbackScope -
265: // as the walk's initial scope; the walk must anchor its results to the
266: // state-identical MutatingScope or their consumption re-enters the
267: // rule-facing ask paths
268: $scope = $scope->toWalkScope();
269: $storage = new ExpressionResultStorage();
270: $scope->pushExpressionResultStorage($storage);
271: // a fresh walk an extension starts mid-analysis must not feed the
272: // interrupted walk's gatherer frames - they describe the body walk
273: // that was interrupted, not the nested one
274: $gatherers = $this->suspendNodeGatherers();
275: try {
276: return $this->processStmtNodesInternal(
277: $parentNode,
278: $stmts,
279: $scope,
280: $storage,
281: $nodeCallback,
282: $context,
283: )->toPublic();
284: } finally {
285: $this->restoreNodeGatherers($gatherers);
286: $scope->popExpressionResultStorage();
287: }
288: }
289:
290: /**
291: * The final walk of a loop body against its last convergence pass (see
292: * StatementsHandler::processLoopFinalStmtNodes()).
293: *
294: * @param Node\Stmt[] $stmts
295: * @param callable(Node $node, Scope $scope): void $nodeCallback
296: * @param array{array<int, array{StatementListWalkState, int}>, RecordingNodeCallback} $lastPass
297: */
298: public function processLoopFinalStmtNodes(
299: Node $parentNode,
300: array $stmts,
301: MutatingScope $scope,
302: ExpressionResultStorage $storage,
303: callable $nodeCallback,
304: StatementContext $context,
305: array $lastPass,
306: ): InternalStatementResult
307: {
308: $pushStorage = $scope->getCurrentExpressionResultStorage() !== $storage;
309: if ($pushStorage) {
310: $scope->pushExpressionResultStorage($storage);
311: }
312: try {
313: return $this->statementsHandler->processLoopFinalStmtNodes($this, $parentNode, $stmts, $scope, $storage, $nodeCallback, $context, $lastPass);
314: } finally {
315: if ($pushStorage) {
316: $scope->popExpressionResultStorage();
317: }
318: }
319: }
320:
321: /**
322: * One loop convergence pass over $stmts, re-walking only what differs from
323: * $previous, the previous pass (see StatementsHandler::processLoopPassStmtNodes()).
324: *
325: * @param Node\Stmt[] $stmts
326: * @param callable(Node $node, Scope $scope): void $nodeCallback
327: * @param array{array<int, array{StatementListWalkState, int}>, ?RecordingNodeCallback}|null $previous
328: * @param array{array<int, array{StatementListWalkState, int}>, ?RecordingNodeCallback}|null $record
329: */
330: public function processLoopPassStmtNodes(
331: Node $parentNode,
332: array $stmts,
333: MutatingScope $scope,
334: ExpressionResultStorage $storage,
335: callable $nodeCallback,
336: StatementContext $context,
337: ?array $previous,
338: ?array &$record,
339: ): InternalStatementResult
340: {
341: $pushStorage = $scope->getCurrentExpressionResultStorage() !== $storage;
342: if ($pushStorage) {
343: $scope->pushExpressionResultStorage($storage);
344: }
345: try {
346: return $this->statementsHandler->processLoopPassStmtNodes($this, $parentNode, $stmts, $scope, $storage, $nodeCallback, $context, $previous, $record);
347: } finally {
348: if ($pushStorage) {
349: $scope->popExpressionResultStorage();
350: }
351: }
352: }
353:
354: /**
355: * @param Node\Stmt[] $stmts
356: * @param callable(Node $node, Scope $scope): void $nodeCallback
357: */
358: public function processStmtNodesInternal(
359: Node $parentNode,
360: array $stmts,
361: MutatingScope $scope,
362: ExpressionResultStorage $storage,
363: callable $nodeCallback,
364: StatementContext $context,
365: ): InternalStatementResult
366: {
367: // make the storage this walk writes into scope-visible: loop-convergence
368: // passes (including the closure by-ref convergence, which calls this
369: // method directly) thread a throwaway duplicate that would otherwise
370: // never reach the storage stack, so every in-pass ask
371: // (applySpecifiedTypes pricing, rules via Scope::getType) would miss the
372: // pass's own results and re-process real nodes on demand
373: $pushStorage = $scope->getCurrentExpressionResultStorage() !== $storage;
374: if ($pushStorage) {
375: $scope->pushExpressionResultStorage($storage);
376: }
377: try {
378: return $this->statementsHandler->doProcessStmtNodes($this, $parentNode, $stmts, $scope, $storage, $nodeCallback, $context);
379: } finally {
380: if ($pushStorage) {
381: $scope->popExpressionResultStorage();
382: }
383: }
384: }
385:
386: /**
387: * @param callable(Node $node, Scope $scope): void $nodeCallback
388: */
389: public function processStmtNode(
390: Node\Stmt $stmt,
391: MutatingScope $scope,
392: ExpressionResultStorage $storage,
393: callable $nodeCallback,
394: StatementContext $context,
395: ): InternalStatementResult
396: {
397: $overridingThrowPoints = null;
398: if (
399: !$stmt instanceof Static_
400: && !$stmt instanceof Node\Stmt\Global_
401: && !$stmt instanceof Node\Stmt\Property
402: && !$stmt instanceof Node\Stmt\ClassConst
403: && !$stmt instanceof Node\Stmt\Const_
404: && !$stmt instanceof Node\Stmt\ClassLike
405: && !$stmt instanceof Node\Stmt\Function_
406: && !$stmt instanceof Node\Stmt\ClassMethod
407: ) {
408: if (!$stmt instanceof Foreach_) {
409: $scope = $this->statementsHandler->processStmtVarAnnotation($this, $scope, $storage, $stmt, null, $nodeCallback);
410: }
411: $overridingThrowPoints = $this->statementsHandler->getOverridingThrowPoints($stmt, $scope);
412: }
413: $varTagDependencies = $this->statementsHandler->getVarTagDependencies($stmt, $scope);
414:
415: if ($stmt instanceof Node\Stmt\ClassMethod) {
416: // a trait method the using class overrides is not analysed here at all -
417: // decided before the node callback is emitted; the override can be the
418: // class's own method, another trait's, or an engine-provided one like
419: // enum cases(), from() and tryFrom(), so it is told by the declaring
420: // class of the native method, not by the kind of its method reflection
421: if (!$scope->isInClass()) {
422: throw new ShouldNotHappenException();
423: }
424: if ($scope->isInTrait()) {
425: $nativeClassReflection = $scope->getClassReflection()->getNativeReflection();
426: $methodName = $stmt->name->toString();
427: if (
428: $nativeClassReflection->hasMethod($methodName)
429: && $nativeClassReflection->getMethod($methodName)->getBetterReflection()->getDeclaringClass()->getName() !== $scope->getTraitReflection()->getName()
430: ) {
431: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []);
432: }
433: }
434: }
435:
436: // Statements whose work is processing their expressions emit their node
437: // callback AFTER that processing, inside their branches below, with the
438: // entry scope - a synchronously invoked rule (the plain resolver,
439: // PHP < 8.1) then finds the expressions' results in the storage instead
440: // of re-walking them on demand, mirroring processExprNodeInternal().
441: $deferredStmtCallback = $stmt instanceof Return_ || $stmt instanceof Node\Stmt\Expression || $stmt instanceof Echo_
442: || $stmt instanceof If_ || $stmt instanceof Switch_ || $stmt instanceof Foreach_
443: || $stmt instanceof Node\Stmt\Unset_ || $stmt instanceof Node\Stmt\ClassConst
444: || $stmt instanceof Node\Stmt\Const_ || $stmt instanceof Node\Stmt\While_
445: || $stmt instanceof Node\Stmt\Do_;
446: if (!$deferredStmtCallback) {
447: $this->callNodeCallback($nodeCallback, $stmt, $scope, $storage);
448: }
449:
450: $stmtHandler = StmtHandlerRegistry::resolve($stmt, $this->container);
451: if ($stmtHandler !== null) {
452: $stmtResult = $stmtHandler->processStmt($this, $stmt, $scope, $storage, $nodeCallback, $context);
453: if ($varTagDependencies !== null) {
454: $stmtResult = $stmtResult->withDependencies(Dependencies::merge($stmtResult->getDependencies(), $varTagDependencies));
455: }
456: if ($overridingThrowPoints !== null) {
457: // the overriding throw points use the scope before the statement,
458: // so the variable flow throws before the statement does its work
459: $overridingThrowFlows = [];
460: foreach ($overridingThrowPoints as $overridingThrowPoint) {
461: $overridingThrowFlows[] = VariableFlow::throwing($overridingThrowPoint->getType(), true, $overridingThrowPoint->canContainAnyThrowable());
462: }
463:
464: return new InternalStatementResult(
465: $stmtResult->getScope(),
466: hasYield: $stmtResult->hasYield(),
467: isAlwaysTerminating: $stmtResult->isAlwaysTerminating(),
468: exitPoints: $stmtResult->getExitPoints(),
469: throwPoints: $overridingThrowPoints,
470: impurePoints: $stmtResult->getImpurePoints(),
471: endStatements: $stmtResult->getEndStatements(),
472: variableFlow: VariableFlow::sequence(...$overridingThrowFlows, ...[$stmtResult->getVariableFlow()]),
473: dependencies: $stmtResult->getDependencies(),
474: );
475: }
476:
477: return $stmtResult;
478: }
479:
480: // statements with no analysis of their own (e.g. HaltCompiler)
481: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: $overridingThrowPoints ?? [], impurePoints: [], dependencies: $varTagDependencies);
482: }
483:
484: public function isAnalysedFile(string $fileName): bool
485: {
486: return isset($this->analysedFiles[$fileName]);
487: }
488:
489: /** Whether an on-demand walk answers already processed real nodes from their stored results (see processExprOnDemand()). */
490: public function isReturningStoredExpressionResults(): bool
491: {
492: return $this->returnStoredExpressionResults;
493: }
494:
495: public function lookForSetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope
496: {
497: return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr));
498: }
499:
500: public function lookForUnsetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope
501: {
502: return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->unsetAllowedUndefinedExpression($expr));
503: }
504:
505: /**
506: * @param Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback
507: */
508: private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope
509: {
510: if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) {
511: $scope = $callback($scope, $expr);
512: }
513:
514: if ($expr instanceof ArrayDimFetch) {
515: $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback);
516: } elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch || $expr instanceof Expr\NullsafeMethodCall) {
517: $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback);
518: } elseif ($expr instanceof StaticPropertyFetch && $expr->class instanceof Expr) {
519: $scope = $this->lookForExpressionCallback($scope, $expr->class, $callback);
520: } elseif ($expr instanceof List_) {
521: foreach ($expr->items as $item) {
522: if ($item === null) {
523: continue;
524: }
525:
526: $scope = $this->lookForExpressionCallback($scope, $item->value, $callback);
527: }
528: }
529:
530: return $scope;
531: }
532:
533: /**
534: * Processes an expression outside the normal AST traversal - e.g. a synthetic
535: * node a rule or extension asks about. Real AST nodes contained in it return
536: * their already-stored results instead of being processed again. New results
537: * are stored into the given storage - pass a duplicate to keep them isolated.
538: */
539: /**
540: * Processes an expression whose already-walked receiver must be CONSUMED
541: * from its stored result instead of re-walked: the nullsafe handlers
542: * process the receiver once (real callbacks) and then walk the plain twin,
543: * whose receiver answers from storage, re-anchored to the twin's
544: * (ensured) scope.
545: *
546: * @param callable(Node $node, Scope $scope): void $nodeCallback
547: */
548: public function processExprNodeConsumingStored(Node\Stmt $stmt, Expr $expr, Expr $consumedExpr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult
549: {
550: $previous = $this->consumeStoredExpression;
551: $this->consumeStoredExpression = $consumedExpr;
552: try {
553: return $this->processExprNode($stmt, $expr, $scope, $storage, $nodeCallback, $context);
554: } finally {
555: $this->consumeStoredExpression = $previous;
556: }
557: }
558:
559: public function processExprOnDemand(Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage): ExpressionResult
560: {
561: // A node no handler supports - a virtual node (BooleanOrNode, ...) a
562: // rule asked the type of - degrades to mixed, mirroring
563: // MutatingScope::resolveType()'s fallback. The main walk's unhandled
564: // throw stays: real source nodes must have a handler.
565: if (
566: ExprHandlerRegistry::resolve($expr, $this->container) === null
567: && !($expr instanceof Expr\CallLike && $expr->isFirstClassCallable())
568: ) {
569: $mixed = new MixedType();
570: return $this->expressionResultFactory->create(
571: $scope,
572: beforeScope: $scope,
573: expr: $expr,
574: hasYield: false,
575: isAlwaysTerminating: false,
576: throwPoints: [],
577: impurePoints: [],
578: typeCallback: null,
579: specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(),
580: type: $mixed,
581: nativeType: $mixed,
582: );
583: }
584:
585: // save/restore, never reset: on-demand walks nest (a typeCallback
586: // evaluated mid-walk prices another synthetic node) and a hard reset
587: // would turn stored-result consumption off for the rest of the outer
588: // walk - re-processing every remaining subtree and bypassing the
589: // closure-argument consume guards in ArgumentsHandler::processArgs()
590: $previous = $this->returnStoredExpressionResults;
591: $this->returnStoredExpressionResults = true;
592: $scope->pushExpressionResultStorage($storage);
593: try {
594: return $this->processExprNode(
595: new Node\Stmt\Expression($expr),
596: $expr,
597: $scope,
598: $storage,
599: new NoopNodeCallback(),
600: ExpressionContext::createTopLevel(resolveTemplateArguments: false),
601: );
602: } finally {
603: $scope->popExpressionResultStorage();
604: $this->returnStoredExpressionResults = $previous;
605: }
606: }
607:
608: /**
609: * The stored ExpressionResult of a node processExprNode() already processed
610: * into the given storage - the caller asserts the processing order by
611: * holding the very storage it processed the node into (a scope-based lookup
612: * would miss loop-convergence storages, which are never scope-visible).
613: * Throws when the node has no stored result.
614: */
615: public function readStoredResult(Expr $expr, ExpressionResultStorage $storage): ExpressionResult
616: {
617: $result = $storage->findExpressionResult($expr);
618: if ($result === null) {
619: throw new ShouldNotHappenException(sprintf(
620: '%s on line %d has no stored ExpressionResult - it was not processed by processExprNode().',
621: get_class($expr),
622: $expr->getStartLine(),
623: ));
624: }
625:
626: return $result;
627: }
628:
629: /**
630: * The type, on the given scope, of a node that may or may not have a stored
631: * ExpressionResult. Every call site of this method is UNDECIDED about whether
632: * the node was already analysed - each should eventually either consume the
633: * node's ExpressionResult where it was processed or be a synthetic node
634: * (processSyntheticOnDemand()).
635: */
636: public function readTypeOfMaybeStored(Expr $expr, MutatingScope $scope): Type
637: {
638: $storage = $scope->getCurrentExpressionResultStorage();
639: $result = $storage !== null ? $storage->findExpressionResult($expr) : null;
640: if ($result !== null) {
641: return $result->getTypeOnScope($scope, $scope->nativeTypesPromoted);
642: }
643:
644: return $this->readScopeStateOrSyntheticType($expr, $scope);
645: }
646:
647: /**
648: * The type the scope itself knows for the expression, without any node
649: * processing: a string-named variable read is scope state (mirrors
650: * VariableHandler's typeCallback), and a type tracked for the whole
651: * expression answers directly - an on-demand walk would return that very
652: * holder anyway (the fresh result's beforeScope is the asking scope),
653: * after paying the walk. Null when the scope has no answer; the caller
654: * decides whether that means a synthetic walk (processSyntheticOnDemand())
655: * or an invariant violation.
656: */
657: public function findScopeStateType(Expr $expr, MutatingScope $scope): ?Type
658: {
659: if ($expr instanceof Expr\Variable && is_string($expr->name)) {
660: if ($scope->hasVariableType($expr->name)->no()) {
661: return new ErrorType();
662: }
663:
664: return $scope->getVariableType($expr->name);
665: }
666:
667: // a literal is position-independent: the scope prices it without a walk,
668: // so an argument the walk reaches only later (an IIFE's or a pipe's
669: // operand) never has to be processed ahead of its turn
670: if ($expr instanceof Node\Scalar\String_ || $expr instanceof Node\Scalar\Int_ || $expr instanceof Node\Scalar\Float_) {
671: return $scope->getStateType($expr);
672: }
673:
674: // A variable whose name is an expression ($$name) never reaches the read
675: // above, and the scope tracks it like any other expression - so it belongs
676: // here rather than falling through to a walk.
677: if (
678: !$expr instanceof Expr\Closure
679: && !$expr instanceof Expr\ArrowFunction
680: && $scope->hasExpressionType($expr)->yes()
681: ) {
682: return TypeUtils::resolveLateResolvableTypes($scope->getTrackedExpressionType($expr));
683: }
684:
685: return null;
686: }
687:
688: /**
689: * The type, on the given scope, of a node the caller knows has no stored
690: * ExpressionResult in its walk: scope state (variable read / tracked
691: * holder) answers without a walk, anything else is priced as a synthetic
692: * node.
693: */
694: public function readScopeStateOrSyntheticType(Expr $expr, MutatingScope $scope): Type
695: {
696: return $this->findScopeStateType($expr, $scope) ?? $this->processSyntheticOnDemand($expr, $scope)->getTypeOnScope($scope, $scope->nativeTypesPromoted);
697: }
698:
699: /**
700: * The type the scope knows for an expression the caller has already pinned as
701: * tracked there (hasExpressionType() yes, or a string-named variable). Unlike
702: * readScopeStateOrSyntheticType() this never falls back to a synthetic walk -
703: * the caller decided that the scope answers.
704: */
705: public function requireScopeStateType(Expr $expr, MutatingScope $scope): Type
706: {
707: $type = $this->findScopeStateType($expr, $scope);
708: if ($type === null) {
709: throw new ShouldNotHappenException(sprintf(
710: '%s on line %d is not tracked on the scope it was pinned as tracked on.',
711: get_class($expr),
712: $expr->getStartLine(),
713: ));
714: }
715:
716: return $type;
717: }
718:
719: /**
720: * Fires the PHPSTAN_GUARD_NW diagnostic when a real (non-synthetic) AST node
721: * reaches an on-demand pricing path without having been processed and stored
722: * by processExprNode() first. Mirrors the guard in MutatingScope::getType():
723: * such a node should be answered from its stored ExpressionResult, never
724: * re-priced as if it were synthetic. Dormant unless PHPSTAN_GUARD_NW=1.
725: */
726: private function guardAgainstUnprocessedRealNode(Expr $expr, string $caller): void
727: {
728: if (
729: !self::$guardNewWorld
730: || !isset(self::$guardRealExprIds[spl_object_id($expr)])
731: || isset(self::$guardProcessedExprIds[spl_object_id($expr)])
732: ) {
733: return;
734: }
735:
736: throw new ShouldNotHappenException(sprintf(
737: '%s() asked about non-synthetic %s on line %d before it was processed by processExprNode() - it should consume the node\'s ExpressionResult instead.',
738: $caller,
739: get_class($expr),
740: $expr->getStartLine(),
741: ));
742: }
743:
744: /**
745: * Processes a synthetic node (one an ExprHandler built itself) on a duplicate
746: * of the storage of the analysis currently in progress, mirroring
747: * MutatingScope::resolveTypeOfNewWorldHandlerNode(): the duplicate isolates
748: * the synthetic node's own stored result from the live storage while its real
749: * subnodes still resolve from the fallback.
750: */
751: public function processSyntheticOnDemand(Expr $expr, MutatingScope $scope): ExpressionResult
752: {
753: $this->guardAgainstUnprocessedRealNode($expr, __FUNCTION__);
754: $current = $scope->getCurrentExpressionResultStorage() ?? new ExpressionResultStorage();
755:
756: return $this->processExprOnDemand($expr, $scope, $current->duplicate());
757: }
758:
759: /**
760: * @param callable(Node $node, Scope $scope): void $nodeCallback
761: */
762: public function processExprNode(
763: Node\Stmt $stmt,
764: Expr $expr,
765: MutatingScope $scope,
766: ExpressionResultStorage $storage,
767: callable $nodeCallback,
768: ExpressionContext $context,
769: ): ExpressionResult
770: {
771: $consume = $this->consumeStoredExpression === $expr;
772: if ($this->returnStoredExpressionResults || $consume) {
773: $storedResult = $storage->findExpressionResult($expr);
774: // a stored result only answers when the current scope agrees with its
775: // evaluation position on the variables the expression reads - a
776: // counterfactual walk (an extension re-binding a variable and pricing
777: // a real subtree, e.g. array_filter's per-element callback evaluation)
778: // re-processes the node on its own scope instead. In CONSUME mode the
779: // divergence is intentional (an ensured-non-null device) and the
780: // stored result is consumed unconditionally, re-anchored below.
781: if ($storedResult !== null && ($consume || $storedResult->askScopeVariableStateMatches($scope, $scope->nativeTypesPromoted))) {
782: // a foreign-position answer must not thread its original walk
783: // scopes into THIS walk - re-anchor it to the asking position so
784: // subsequent operands keep evaluating on the asking scope
785: if ($storedResult->getBeforeScope() === $scope) {
786: return $storedResult;
787: }
788:
789: $reanchored = $storedResult->atAskPosition($scope);
790: if ($consume) {
791: // the re-anchored view IS this walk's result for the node
792: // (the nullsafe twin's receiver at the ensured position) -
793: // store it so later asks (rules' storage reads) see the same
794: // result the twin walk itself consumed, exactly like the
795: // receiver walked inside the twin used to be stored
796: $this->storeExpressionResult($storage, $expr, $reanchored);
797: }
798:
799: return $reanchored;
800: }
801: }
802:
803: return $this->processExprNodeInternal($stmt, $expr, $scope, $storage, $nodeCallback, $context);
804: }
805:
806: /**
807: * @param callable(Node $node, Scope $scope): void $nodeCallback
808: */
809: private function processExprNodeInternal(
810: Node\Stmt $stmt,
811: Expr $expr,
812: MutatingScope $scope,
813: ExpressionResultStorage $storage,
814: callable $nodeCallback,
815: ExpressionContext $context,
816: ): ExpressionResult
817: {
818: if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) {
819: if ($expr instanceof FuncCall) {
820: $newExpr = new FunctionCallableNode($expr->name, $expr);
821: } elseif ($expr instanceof MethodCall) {
822: $newExpr = new MethodCallableNode($expr->var, $expr->name, $expr);
823: } elseif ($expr instanceof StaticCall) {
824: $newExpr = new StaticMethodCallableNode($expr->class, $expr->name, $expr);
825: } elseif ($expr instanceof New_ && !$expr->class instanceof Class_) {
826: $newExpr = new InstantiationCallableNode($expr->class, $expr);
827: } else {
828: throw new ShouldNotHappenException();
829: }
830:
831: $newExprResult = $this->processExprNode($stmt, $newExpr, $scope, $storage, $nodeCallback, $context);
832: $expressionResult = $this->expressionResultFactory->create(
833: $newExprResult->getScope(),
834: beforeScope: $scope,
835: expr: $expr,
836: hasYield: $newExprResult->hasYield(),
837: isAlwaysTerminating: $newExprResult->isAlwaysTerminating(),
838: throwPoints: $newExprResult->getThrowPoints(),
839: impurePoints: $newExprResult->getImpurePoints(),
840: variableFlow: $newExprResult->getVariableFlow(),
841: // the first-class callable closure type lives on the *CallableNode
842: // result; delegate so getType() of the original CallLike answers from it
843: typeCallback: static fn (bool $nativeTypesPromoted): Type => ($nativeTypesPromoted ? $newExprResult->getNativeType() : $newExprResult->getType()),
844: specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(),
845: dependencies: $newExprResult->getDependencies(),
846: );
847: $this->storeExpressionResult($storage, $expr, $expressionResult);
848: return $expressionResult;
849: }
850:
851: $exprHandler = ExprHandlerRegistry::resolve($expr, $this->container);
852: if ($exprHandler !== null) {
853: $expressionResult = $exprHandler->processExpr($this, $stmt, $expr, $scope, $storage, $nodeCallback, $context);
854: // a chain link an enclosing isset/empty/?? could not device ahead
855: // of its walk (an untracked call) is deviced now, from the type
856: // the walk produced
857: $expressionResult = $this->getNonNullabilityHelper()->applyPendingEnsure($expr, $expressionResult);
858: $this->storeExpressionResult($storage, $expr, $expressionResult);
859: // Force potential producers before collecting the body's constraints.
860: // Type reads only construct markers; they never register sites as a side effect.
861: $frame = $scope->getCurrentTemplateArgumentFrame();
862: if (
863: $frame !== null && $frame->isObserving()
864: && $expr instanceof Expr\CallLike
865: ) {
866: $constraints = $this->templateArgumentObserver->collectSites($expressionResult->getType());
867: $expressionResult = $expressionResult->withScope($expressionResult->getScope()->addTemplateArgumentConstraints($constraints));
868: $this->storeExpressionResult($storage, $expr, $expressionResult);
869: }
870: // The node's own callback fires AFTER its result is stored, with the
871: // scope captured before processing. Rules observe the same (scope,
872: // answer) pair as at a pre-order emission - previously a pre-order
873: // rule parks on its first ask and resumes at this store anyway - but
874: // a synchronously invoked rule (the plain resolver, PHP < 8.1) now
875: // finds the node's and its subtree's results in the storage instead
876: // of re-walking them on demand.
877: $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $storage, $context);
878: // the call is now processed and stored; emit a virtual node so
879: // impossible-check rules read its specified types from the result
880: // instead of asking the scope before the call node is processed
881: if ($expr instanceof FuncCall) {
882: $this->callNodeCallbackWithExpression($nodeCallback, new FunctionCallExpressionNode($expr, $expressionResult, $expressionResult->getArgsResult()), $scope, $storage, $context);
883: } elseif ($expr instanceof MethodCall) {
884: $this->callNodeCallbackWithExpression($nodeCallback, new MethodCallExpressionNode($expr, $expressionResult, $expressionResult->getArgsResult()), $scope, $storage, $context);
885: } elseif ($expr instanceof StaticCall) {
886: $this->callNodeCallbackWithExpression($nodeCallback, new StaticMethodCallExpressionNode($expr, $expressionResult, $expressionResult->getArgsResult()), $scope, $storage, $context);
887: }
888: return $expressionResult;
889: }
890:
891: throw new ShouldNotHappenException(sprintf('Unhandled expr: %s', get_class($expr)));
892: }
893:
894: /**
895: * Unlike a method call, a property read defaults to pure: only a hook we're
896: * certain about and that is certainly side-effecting makes the read impure.
897: *
898: * The reset is assumed pure as reporting those would make accessing them
899: * unreasonably annoying.
900: *
901: * @param 'get'|'set' $hookName
902: * @return ImpurePoint[]
903: */
904: public function getImpurePointsFromPropertyHook(
905: MutatingScope $scope,
906: PropertyFetch $propertyFetch,
907: PhpPropertyReflection $propertyReflection,
908: string $hookName,
909: ): array
910: {
911: if ($this->isPropertyHookBackingValueAccess($scope, $propertyFetch)) {
912: return [];
913: }
914:
915: if (!$propertyReflection->hasHook($hookName)) {
916: return [];
917: }
918:
919: if (!$propertyReflection->getHook($hookName)->hasSideEffects()->yes()) {
920: return [];
921: }
922:
923: return [
924: new ImpurePoint(
925: $scope,
926: $propertyFetch,
927: 'propertyHookCall',
928: sprintf(
929: 'call to %s hook of property %s::$%s',
930: $hookName,
931: $propertyReflection->getDeclaringClass()->getDisplayName(),
932: $propertyReflection->getName(),
933: ),
934: true,
935: ),
936: ];
937: }
938:
939: /**
940: * Inside a hook of the same property, $this->prop is the backing value, not
941: * a re-entrant hook call.
942: */
943: private function isPropertyHookBackingValueAccess(MutatingScope $scope, PropertyFetch $propertyFetch): bool
944: {
945: $scopeFunction = $scope->getFunction();
946:
947: return $scopeFunction instanceof PhpMethodFromParserNodeReflection
948: && $scopeFunction->isPropertyHook()
949: && $propertyFetch->var instanceof Variable
950: && $propertyFetch->var->name === 'this'
951: && $propertyFetch->name instanceof Identifier
952: && $propertyFetch->name->toString() === $scopeFunction->getHookedPropertyName();
953: }
954:
955: /**
956: * @return string[]
957: */
958: public function getAssignedVariables(Expr $expr): array
959: {
960: if ($expr instanceof Expr\Variable) {
961: if (is_string($expr->name)) {
962: return [$expr->name];
963: }
964:
965: return [];
966: }
967:
968: if ($expr instanceof Expr\List_) {
969: $names = [];
970: foreach ($expr->items as $item) {
971: if ($item === null) {
972: continue;
973: }
974:
975: $names = array_merge($names, $this->getAssignedVariables($item->value));
976: }
977:
978: return $names;
979: }
980:
981: if ($expr instanceof ArrayDimFetch) {
982: return $this->getAssignedVariables($expr->var);
983: }
984:
985: return [];
986: }
987:
988: private const REPLAYABLE_BODY_ATTRIBUTE = 'convergenceReplayableBody';
989:
990: private const CONTEXT_SENSITIVE_STATEMENT_ATTRIBUTE = 'convergenceContextSensitiveStatement';
991:
992: /**
993: * Whether a recorded convergence pass over the loop body can replace the
994: * final walk. A pass runs at deep statement context, the final walk at top
995: * level - constructs that analyse differently between the two (nested
996: * loop/label fixpoints run only at top level, statement-level classes are
997: * skipped at deep context) disqualify the body. Closure bodies process
998: * context-independently and are not traversed.
999: *
1000: * @param Node\Stmt[] $bodyStmts
1001: */
1002: public function isReplayableConvergenceBody(Node $loopNode, array $bodyStmts): bool
1003: {
1004: $cached = $loopNode->getAttribute(self::REPLAYABLE_BODY_ATTRIBUTE);
1005: if ($cached !== null) {
1006: return $cached;
1007: }
1008:
1009: $replayable = true;
1010: foreach ($bodyStmts as $bodyStmt) {
1011: if ($this->hasContextSensitiveConstruct($bodyStmt)) {
1012: $replayable = false;
1013: break;
1014: }
1015: }
1016: $loopNode->setAttribute(self::REPLAYABLE_BODY_ATTRIBUTE, $replayable);
1017:
1018: return $replayable;
1019: }
1020:
1021: /** A statement that walks differently in a convergence pass than in the final walk. */
1022: public function isContextSensitiveStatement(Node\Stmt $stmt): bool
1023: {
1024: $cached = $stmt->getAttribute(self::CONTEXT_SENSITIVE_STATEMENT_ATTRIBUTE);
1025: if ($cached !== null) {
1026: return $cached;
1027: }
1028:
1029: $sensitive = $this->hasContextSensitiveConstruct($stmt);
1030: $stmt->setAttribute(self::CONTEXT_SENSITIVE_STATEMENT_ATTRIBUTE, $sensitive);
1031:
1032: return $sensitive;
1033: }
1034:
1035: private function hasContextSensitiveConstruct(Node $node): bool
1036: {
1037: if ($node instanceof Expr\Closure) {
1038: return false;
1039: }
1040: if (
1041: $node instanceof Node\Stmt\While_
1042: || $node instanceof Node\Stmt\Do_
1043: || $node instanceof Node\Stmt\For_
1044: || $node instanceof Foreach_
1045: || $node instanceof Node\Stmt\Label
1046: || $node instanceof Node\Stmt\ClassLike
1047: ) {
1048: return true;
1049: }
1050:
1051: foreach ($node->getSubNodeNames() as $subNodeName) {
1052: $subNode = $node->$subNodeName;
1053: if ($subNode instanceof Node) {
1054: if ($this->hasContextSensitiveConstruct($subNode)) {
1055: return true;
1056: }
1057: } elseif (is_array($subNode)) {
1058: foreach ($subNode as $item) {
1059: if ($item instanceof Node && $this->hasContextSensitiveConstruct($item)) {
1060: return true;
1061: }
1062: }
1063: }
1064: }
1065:
1066: return false;
1067: }
1068:
1069: /**
1070: * Opens an engine-feeding gatherer frame for the duration of a body walk.
1071: * The caller closes it in a finally block via popNodeGatherer().
1072: *
1073: * @param callable(Node, Scope): void $gatherer
1074: */
1075: public function pushNodeGatherer(callable $gatherer): void
1076: {
1077: $this->nodeGatherers[] = $gatherer;
1078: }
1079:
1080: public function popNodeGatherer(): void
1081: {
1082: array_pop($this->nodeGatherers);
1083: }
1084:
1085: /**
1086: * Detaches all gatherer frames for a walk that must not feed them. The
1087: * caller reattaches them in a finally block via restoreNodeGatherers().
1088: *
1089: * @return list<callable(Node, Scope): void>
1090: */
1091: public function suspendNodeGatherers(): array
1092: {
1093: $gatherers = $this->nodeGatherers;
1094: $this->nodeGatherers = [];
1095:
1096: return $gatherers;
1097: }
1098:
1099: /**
1100: * @param list<callable(Node, Scope): void> $gatherers
1101: */
1102: public function restoreNodeGatherers(array $gatherers): void
1103: {
1104: $this->nodeGatherers = $gatherers;
1105: }
1106:
1107: /**
1108: * @param callable(Node $node, Scope $scope): void $nodeCallback
1109: */
1110: public function replayRecording(RecordingNodeCallback $recording, callable $nodeCallback, ExpressionResultStorage $storage, MutatingScope $scope): void
1111: {
1112: $this->replayRecordingRange($recording, 0, $recording->count(), $nodeCallback, $storage, $scope);
1113: }
1114:
1115: /**
1116: * Replays the recorded pairs [$from, $to) the way callNodeCallback() would
1117: * have emitted them: gatherer frames observe them exactly like live ones
1118: * (with the raw walk scope), a real callback gets the callback scope - and
1119: * a recording callback records them again (a loop's fixpoint replay running
1120: * inside a body's observation pass).
1121: *
1122: * @param callable(Node $node, Scope $scope): void $nodeCallback
1123: */
1124: public function replayRecordingRange(RecordingNodeCallback $recording, int $from, int $to, callable $nodeCallback, ExpressionResultStorage $storage, MutatingScope $scope): void
1125: {
1126: $pairs = $recording->getPairs();
1127: $scope->pushExpressionResultStorage($storage);
1128: try {
1129: for ($i = $from; $i < $to; $i++) {
1130: [$node, $pairScope] = $pairs[$i];
1131: if (!$pairScope instanceof MutatingScope) {
1132: throw new ShouldNotHappenException();
1133: }
1134: $this->callNodeCallback($nodeCallback, $node, $pairScope, $storage);
1135: }
1136: } finally {
1137: $scope->popExpressionResultStorage();
1138: }
1139: }
1140:
1141: /**
1142: * @param callable(Node $node, Scope $scope): void $nodeCallback
1143: */
1144: public function callNodeCallbackWithExpression(
1145: callable $nodeCallback,
1146: Node $expr,
1147: MutatingScope $scope,
1148: ExpressionResultStorage $storage,
1149: ExpressionContext $context,
1150: ): void
1151: {
1152: if ($context->isDeep()) {
1153: $scope = $scope->exitFirstLevelStatements();
1154: }
1155: $this->callNodeCallback($nodeCallback, $expr, $scope, $storage);
1156: }
1157:
1158: /**
1159: * @param callable(Node $node, Scope $scope): void $nodeCallback
1160: */
1161: public function callNodeCallback(
1162: callable $nodeCallback,
1163: Node $node,
1164: MutatingScope $scope,
1165: ExpressionResultStorage $storage,
1166: ): void
1167: {
1168: // Engine-feeding gatherer frames observe the node at the emission
1169: // position - their arrays are read as soon as the enclosing body walk
1170: // returns. Gatherers are engine code and never ask about types -
1171: // handing them the raw scope skips a NodeCallbackScope construction per
1172: // emission; the scopes they capture (return statements, impure points)
1173: // answer later asks through the storage hub like any MutatingScope.
1174: foreach ($this->nodeGatherers as $gatherer) {
1175: $gatherer($node, $scope);
1176: }
1177:
1178: if ($nodeCallback instanceof NoopNodeCallback) {
1179: return;
1180: }
1181:
1182: if ($nodeCallback instanceof RecordingNodeCallback) {
1183: // recording never asks about types - the pairs are wrapped and
1184: // bound to the storage at replay time instead
1185: $nodeCallback($node, $scope);
1186: return;
1187: }
1188:
1189: // post-order emission means the node's own result and every subnode
1190: // result are already stored when the callback fires - NodeCallbackScope
1191: // answers every ask synchronously from the storage
1192: $nodeCallback($node, $scope->toNodeCallbackScope());
1193: }
1194:
1195: /**
1196: * The template argument frame of the body being walked while it observes
1197: * a body that created unresolved template arguments (or, in the closure
1198: * observation pass, closure signatures) - null otherwise, so every
1199: * observation hook costs a null check outside the observation passes.
1200: */
1201: public function observingTemplateArgumentFrame(MutatingScope $scope): ?TemplateArgumentFrame
1202: {
1203: $frame = $scope->getCurrentTemplateArgumentFrame();
1204: if ($frame === null || !$frame->isObservingClosures() || $scope->getTemplateArgumentConstraints() === null) {
1205: return null;
1206: }
1207:
1208: return $frame;
1209: }
1210:
1211: /**
1212: * A value leaves the function: the declared return type is a send target for
1213: * the unresolved template arguments it carries.
1214: */
1215: public function collectReturnSend(MutatingScope $scope, ExpressionResult $returnedResult): TemplateArgumentConstraints
1216: {
1217: $frame = $this->observingTemplateArgumentFrame($returnedResult->getScope());
1218: if ($frame === null) {
1219: return TemplateArgumentConstraints::createEmpty();
1220: }
1221: $declaredReturnType = self::getDeclaredReturnType($scope);
1222: if ($declaredReturnType === null) {
1223: return TemplateArgumentConstraints::createEmpty();
1224: }
1225:
1226: return $this->templateArgumentObserver->collectSend($declaredReturnType, $returnedResult->getType());
1227: }
1228:
1229: /**
1230: * A generator hands out a key and a value: the key and value types of the
1231: * declared return type (Generator<TKey, TValue>, iterable<K, V>) are send
1232: * targets for the unresolved template arguments and the closures they carry.
1233: * A return type that describes no iterable lets the closures escape.
1234: */
1235: public function collectYieldSend(MutatingScope $scope, ?Type $keyType, ?Type $valueType): TemplateArgumentConstraints
1236: {
1237: $constraints = TemplateArgumentConstraints::createEmpty();
1238: if ($this->observingTemplateArgumentFrame($scope) === null) {
1239: return $constraints;
1240: }
1241: $declaredReturnType = self::getDeclaredReturnType($scope);
1242: if ($declaredReturnType === null || !$declaredReturnType->isIterable()->yes()) {
1243: foreach ([$keyType, $valueType] as $yieldedType) {
1244: if ($yieldedType === null) {
1245: continue;
1246: }
1247: $constraints = $constraints->merge($this->templateArgumentObserver->collectEscape($yieldedType));
1248: }
1249:
1250: return $constraints;
1251: }
1252: if ($keyType !== null) {
1253: $constraints = $constraints->merge($this->templateArgumentObserver->collectSend($declaredReturnType->getIterableKeyType(), $keyType));
1254: }
1255: if ($valueType !== null) {
1256: $constraints = $constraints->merge($this->templateArgumentObserver->collectSend($declaredReturnType->getIterableValueType(), $valueType));
1257: }
1258:
1259: return $constraints;
1260: }
1261:
1262: /**
1263: * `yield from` hands out the keys and values of the iterable it delegates to.
1264: */
1265: public function collectYieldFromSend(MutatingScope $scope, Type $delegatedType): TemplateArgumentConstraints
1266: {
1267: if ($this->observingTemplateArgumentFrame($scope) === null) {
1268: return TemplateArgumentConstraints::createEmpty();
1269: }
1270: if (!$delegatedType->isIterable()->yes()) {
1271: return $this->templateArgumentObserver->collectEscape($delegatedType);
1272: }
1273:
1274: return $this->collectYieldSend($scope, $delegatedType->getIterableKeyType(), $delegatedType->getIterableValueType());
1275: }
1276:
1277: private static function getDeclaredReturnType(MutatingScope $scope): ?Type
1278: {
1279: if ($scope->isInAnonymousFunction()) {
1280: return $scope->getAnonymousFunctionReturnType();
1281: }
1282:
1283: $function = $scope->getFunction();
1284:
1285: return $function !== null ? $function->getReturnType() : null;
1286: }
1287:
1288: }
1289: