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