1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use ArrayAccess;
6: use Closure;
7: use IteratorAggregate;
8: use PhpParser\Comment\Doc;
9: use PhpParser\Modifiers;
10: use PhpParser\Node;
11: use PhpParser\Node\Arg;
12: use PhpParser\Node\AttributeGroup;
13: use PhpParser\Node\ComplexType;
14: use PhpParser\Node\Expr;
15: use PhpParser\Node\Expr\Array_;
16: use PhpParser\Node\Expr\ArrayDimFetch;
17: use PhpParser\Node\Expr\Assign;
18: use PhpParser\Node\Expr\AssignRef;
19: use PhpParser\Node\Expr\BinaryOp;
20: use PhpParser\Node\Expr\BinaryOp\BooleanOr;
21: use PhpParser\Node\Expr\CallLike;
22: use PhpParser\Node\Expr\ConstFetch;
23: use PhpParser\Node\Expr\FuncCall;
24: use PhpParser\Node\Expr\List_;
25: use PhpParser\Node\Expr\MethodCall;
26: use PhpParser\Node\Expr\New_;
27: use PhpParser\Node\Expr\PropertyFetch;
28: use PhpParser\Node\Expr\StaticCall;
29: use PhpParser\Node\Expr\StaticPropertyFetch;
30: use PhpParser\Node\Expr\Variable;
31: use PhpParser\Node\Identifier;
32: use PhpParser\Node\Name;
33: use PhpParser\Node\Stmt\Break_;
34: use PhpParser\Node\Stmt\Class_;
35: use PhpParser\Node\Stmt\Continue_;
36: use PhpParser\Node\Stmt\Do_;
37: use PhpParser\Node\Stmt\Echo_;
38: use PhpParser\Node\Stmt\For_;
39: use PhpParser\Node\Stmt\Foreach_;
40: use PhpParser\Node\Stmt\Goto_;
41: use PhpParser\Node\Stmt\If_;
42: use PhpParser\Node\Stmt\InlineHTML;
43: use PhpParser\Node\Stmt\Return_;
44: use PhpParser\Node\Stmt\Static_;
45: use PhpParser\Node\Stmt\Switch_;
46: use PhpParser\Node\Stmt\TryCatch;
47: use PhpParser\Node\Stmt\Unset_;
48: use PhpParser\Node\Stmt\While_;
49: use PhpParser\NodeFinder;
50: use PhpParser\NodeTraverser;
51: use PHPStan\Analyser\ExprHandler\AssignHandler;
52: use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver;
53: use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper;
54: use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
55: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass;
56: use PHPStan\BetterReflection\Reflection\ReflectionEnum;
57: use PHPStan\BetterReflection\Reflector\Reflector;
58: use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection;
59: use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
60: use PHPStan\DependencyInjection\AutowiredExtensions;
61: use PHPStan\DependencyInjection\AutowiredParameter;
62: use PHPStan\DependencyInjection\AutowiredService;
63: use PHPStan\DependencyInjection\Container;
64: use PHPStan\DependencyInjection\ExtensionsCollection;
65: use PHPStan\File\FileHelper;
66: use PHPStan\File\FileReader;
67: use PHPStan\Node\BreaklessWhileLoopNode;
68: use PHPStan\Node\CatchWithUnthrownExceptionNode;
69: use PHPStan\Node\ClassConstantsNode;
70: use PHPStan\Node\ClassMethodsNode;
71: use PHPStan\Node\ClassPropertiesNode;
72: use PHPStan\Node\ClassPropertyNode;
73: use PHPStan\Node\ClassStatementsGatherer;
74: use PHPStan\Node\ClosureReturnStatementsNode;
75: use PHPStan\Node\DoWhileLoopConditionNode;
76: use PHPStan\Node\ExecutionEndNode;
77: use PHPStan\Node\Expr\ExistingArrayDimFetch;
78: use PHPStan\Node\Expr\ForeachValueByRefExpr;
79: use PHPStan\Node\Expr\NativeTypeExpr;
80: use PHPStan\Node\Expr\OriginalForeachKeyExpr;
81: use PHPStan\Node\Expr\OriginalForeachValueExpr;
82: use PHPStan\Node\Expr\PropertyInitializationExpr;
83: use PHPStan\Node\Expr\TypeExpr;
84: use PHPStan\Node\Expr\UnsetOffsetExpr;
85: use PHPStan\Node\FinallyExitPointsNode;
86: use PHPStan\Node\FunctionCallableNode;
87: use PHPStan\Node\FunctionCallExpressionNode;
88: use PHPStan\Node\FunctionReturnStatementsNode;
89: use PHPStan\Node\InArrowFunctionNode;
90: use PHPStan\Node\InClassMethodNode;
91: use PHPStan\Node\InClassNode;
92: use PHPStan\Node\InClosureNode;
93: use PHPStan\Node\InForeachNode;
94: use PHPStan\Node\InFunctionNode;
95: use PHPStan\Node\InPropertyHookNode;
96: use PHPStan\Node\InstantiationCallableNode;
97: use PHPStan\Node\InTraitNode;
98: use PHPStan\Node\InvalidateExprNode;
99: use PHPStan\Node\MethodCallableNode;
100: use PHPStan\Node\MethodCallExpressionNode;
101: use PHPStan\Node\MethodReturnStatementsNode;
102: use PHPStan\Node\NoopExpressionNode;
103: use PHPStan\Node\PropertyAssignNode;
104: use PHPStan\Node\PropertyHookReturnStatementsNode;
105: use PHPStan\Node\PropertyHookStatementNode;
106: use PHPStan\Node\ReturnStatement;
107: use PHPStan\Node\StaticMethodCallableNode;
108: use PHPStan\Node\StaticMethodCallExpressionNode;
109: use PHPStan\Node\SwitchConditionArm;
110: use PHPStan\Node\SwitchConditionNode;
111: use PHPStan\Node\UnreachableStatementNode;
112: use PHPStan\Node\VariableAssignNode;
113: use PHPStan\Node\VarTagChangedExpressionTypeNode;
114: use PHPStan\Parser\ArrowFunctionArgVisitor;
115: use PHPStan\Parser\ClosureArgVisitor;
116: use PHPStan\Parser\GotoLabelVisitor;
117: use PHPStan\Parser\ImmediatelyInvokedClosureVisitor;
118: use PHPStan\Parser\LineAttributesVisitor;
119: use PHPStan\Parser\Parser;
120: use PHPStan\PhpDoc\PhpDocInheritanceResolver;
121: use PHPStan\PhpDoc\ResolvedPhpDocBlock;
122: use PHPStan\PhpDoc\Tag\VarTag;
123: use PHPStan\Reflection\Assertions;
124: use PHPStan\Reflection\Callables\SimpleImpurePoint;
125: use PHPStan\Reflection\Callables\SimpleThrowPoint;
126: use PHPStan\Reflection\ClassReflection;
127: use PHPStan\Reflection\ClassReflectionFactory;
128: use PHPStan\Reflection\ExtendedMethodReflection;
129: use PHPStan\Reflection\ExtendedParameterReflection;
130: use PHPStan\Reflection\FunctionReflection;
131: use PHPStan\Reflection\InitializerExprContext;
132: use PHPStan\Reflection\InitializerExprTypeResolver;
133: use PHPStan\Reflection\MethodReflection;
134: use PHPStan\Reflection\Native\NativeMethodReflection;
135: use PHPStan\Reflection\Native\NativeParameterReflection;
136: use PHPStan\Reflection\ParameterReflection;
137: use PHPStan\Reflection\ParametersAcceptor;
138: use PHPStan\Reflection\ParametersAcceptorSelector;
139: use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection;
140: use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
141: use PHPStan\Reflection\Php\PhpMethodReflection;
142: use PHPStan\Reflection\Php\PhpPropertyReflection;
143: use PHPStan\Reflection\ReflectionProvider;
144: use PHPStan\Rules\Properties\ReadWritePropertiesExtension;
145: use PHPStan\ShouldNotHappenException;
146: use PHPStan\TrinaryLogic;
147: use PHPStan\Type\ClosureType;
148: use PHPStan\Type\Constant\ConstantIntegerType;
149: use PHPStan\Type\Constant\ConstantStringType;
150: use PHPStan\Type\FileTypeMapper;
151: use PHPStan\Type\FunctionParameterClosureThisExtension;
152: use PHPStan\Type\FunctionParameterClosureTypeExtension;
153: use PHPStan\Type\FunctionParameterOutTypeExtension;
154: use PHPStan\Type\Generic\TemplateTypeHelper;
155: use PHPStan\Type\Generic\TemplateTypeMap;
156: use PHPStan\Type\MethodParameterClosureThisExtension;
157: use PHPStan\Type\MethodParameterClosureTypeExtension;
158: use PHPStan\Type\MethodParameterOutTypeExtension;
159: use PHPStan\Type\MixedType;
160: use PHPStan\Type\NeverType;
161: use PHPStan\Type\NullType;
162: use PHPStan\Type\ObjectType;
163: use PHPStan\Type\ObjectWithoutClassType;
164: use PHPStan\Type\ParserNodeTypeToPHPStanType;
165: use PHPStan\Type\ResourceType;
166: use PHPStan\Type\StaticMethodParameterClosureThisExtension;
167: use PHPStan\Type\StaticMethodParameterClosureTypeExtension;
168: use PHPStan\Type\StaticMethodParameterOutTypeExtension;
169: use PHPStan\Type\StaticType;
170: use PHPStan\Type\StaticTypeFactory;
171: use PHPStan\Type\ThisType;
172: use PHPStan\Type\Type;
173: use PHPStan\Type\TypeCombinator;
174: use PHPStan\Type\TypeTraverser;
175: use PHPStan\Type\TypeUtils;
176: use PHPStan\Type\UnionType;
177: use Throwable;
178: use Traversable;
179: use function array_fill_keys;
180: use function array_filter;
181: use function array_key_exists;
182: use function array_keys;
183: use function array_last;
184: use function array_map;
185: use function array_merge;
186: use function array_slice;
187: use function array_values;
188: use function count;
189: use function in_array;
190: use function is_array;
191: use function is_int;
192: use function is_string;
193: use function max;
194: use function sprintf;
195: use function strtolower;
196: use function trim;
197: use function usort;
198: use const PHP_VERSION_ID;
199:
200: #[AutowiredService]
201: class NodeScopeResolver
202: {
203:
204: private const LOOP_SCOPE_ITERATIONS = 3;
205: private const GENERALIZE_AFTER_ITERATION = 1;
206: private const FOREACH_UNROLL_LIMIT = 16;
207: private const FOREACH_UNROLL_NESTED_LIMIT = 8;
208:
209: /** @var array<string, true> filePath(string) => bool(true) */
210: private array $analysedFiles = [];
211:
212: /** @var array<string, true> */
213: private array $calledMethodStack = [];
214:
215: /** @var array<string, MutatingScope|null> */
216: private array $calledMethodResults = [];
217:
218: /**
219: * @param ExtensionsCollection<FunctionParameterOutTypeExtension> $functionParameterOutTypeExtensions
220: * @param ExtensionsCollection<MethodParameterOutTypeExtension> $methodParameterOutTypeExtensions
221: * @param ExtensionsCollection<StaticMethodParameterOutTypeExtension> $staticMethodParameterOutTypeExtensions
222: * @param ExtensionsCollection<ReadWritePropertiesExtension> $readWritePropertiesExtensions
223: * @param ExtensionsCollection<FunctionParameterClosureThisExtension> $functionParameterClosureThisExtensions
224: * @param ExtensionsCollection<MethodParameterClosureThisExtension> $methodParameterClosureThisExtensions
225: * @param ExtensionsCollection<StaticMethodParameterClosureThisExtension> $staticMethodParameterClosureThisExtensions
226: * @param ExtensionsCollection<FunctionParameterClosureTypeExtension> $functionParameterClosureTypeExtensions
227: * @param ExtensionsCollection<MethodParameterClosureTypeExtension> $methodParameterClosureTypeExtensions
228: * @param ExtensionsCollection<StaticMethodParameterClosureTypeExtension> $staticMethodParameterClosureTypeExtensions
229: * @param ExtensionsCollection<PerFileAnalysisResettable> $perFileAnalysisResettables
230: */
231: public function __construct(
232: private readonly Container $container,
233: private readonly ReflectionProvider $reflectionProvider,
234: private readonly InitializerExprTypeResolver $initializerExprTypeResolver,
235: private readonly Reflector $reflector,
236: private readonly ClassReflectionFactory $classReflectionFactory,
237: #[AutowiredExtensions(of: FunctionParameterOutTypeExtension::class)]
238: private readonly ExtensionsCollection $functionParameterOutTypeExtensions,
239: #[AutowiredExtensions(of: MethodParameterOutTypeExtension::class)]
240: private readonly ExtensionsCollection $methodParameterOutTypeExtensions,
241: #[AutowiredExtensions(of: StaticMethodParameterOutTypeExtension::class)]
242: private readonly ExtensionsCollection $staticMethodParameterOutTypeExtensions,
243: #[AutowiredParameter(ref: '@defaultAnalysisParser')]
244: private readonly Parser $parser,
245: private readonly FileTypeMapper $fileTypeMapper,
246: private readonly PhpDocInheritanceResolver $phpDocInheritanceResolver,
247: private readonly FileHelper $fileHelper,
248: private readonly TypeSpecifier $typeSpecifier,
249: #[AutowiredExtensions(of: ReadWritePropertiesExtension::class)]
250: private readonly ExtensionsCollection $readWritePropertiesExtensions,
251: #[AutowiredExtensions(of: FunctionParameterClosureThisExtension::class)]
252: private readonly ExtensionsCollection $functionParameterClosureThisExtensions,
253: #[AutowiredExtensions(of: MethodParameterClosureThisExtension::class)]
254: private readonly ExtensionsCollection $methodParameterClosureThisExtensions,
255: #[AutowiredExtensions(of: StaticMethodParameterClosureThisExtension::class)]
256: private readonly ExtensionsCollection $staticMethodParameterClosureThisExtensions,
257: #[AutowiredExtensions(of: FunctionParameterClosureTypeExtension::class)]
258: private readonly ExtensionsCollection $functionParameterClosureTypeExtensions,
259: #[AutowiredExtensions(of: MethodParameterClosureTypeExtension::class)]
260: private readonly ExtensionsCollection $methodParameterClosureTypeExtensions,
261: #[AutowiredExtensions(of: StaticMethodParameterClosureTypeExtension::class)]
262: private readonly ExtensionsCollection $staticMethodParameterClosureTypeExtensions,
263: #[AutowiredExtensions(of: PerFileAnalysisResettable::class)]
264: private readonly ExtensionsCollection $perFileAnalysisResettables,
265: private readonly ScopeFactory $scopeFactory,
266: #[AutowiredParameter]
267: private readonly bool $polluteScopeWithLoopInitialAssignments,
268: #[AutowiredParameter]
269: private readonly bool $polluteScopeWithAlwaysIterableForeach,
270: #[AutowiredParameter]
271: private readonly bool $polluteScopeWithBlock,
272: #[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
273: private readonly bool $implicitThrows,
274: #[AutowiredParameter]
275: private readonly bool $treatPhpDocTypesAsCertain,
276: private readonly ImplicitToStringCallHelper $implicitToStringCallHelper,
277: protected readonly ExpressionResultFactory $expressionResultFactory,
278: )
279: {
280: }
281:
282: /**
283: * @api
284: * @param string[] $files
285: */
286: public function setAnalysedFiles(array $files): void
287: {
288: $this->analysedFiles = array_fill_keys($files, true);
289: }
290:
291: /**
292: * Releases the previous file's node-keyed captures: the parser cache
293: * retains ASTs, so node-keyed cache entries never die on
294: * their own and would hold that file's whole result graph alive.
295: */
296: public function resetPerFileAnalysisState(): void
297: {
298: foreach ($this->perFileAnalysisResettables->getAll() as $resettableService) {
299: $resettableService->resetFileAnalysisState();
300: }
301: }
302:
303: /**
304: * @api
305: * @param Node[] $nodes
306: * @param callable(Node $node, Scope $scope): void $nodeCallback
307: */
308: public function processNodes(
309: array $nodes,
310: MutatingScope $scope,
311: callable $nodeCallback,
312: ): void
313: {
314: $this->resetPerFileAnalysisState();
315:
316: $expressionResultStorage = new ExpressionResultStorage();
317: $alreadyTerminated = false;
318: $exitPoints = [];
319:
320: $stmts = [];
321: $stmtToNodeIndex = [];
322: foreach ($nodes as $i => $node) {
323: if (!($node instanceof Node\Stmt)) {
324: continue;
325: }
326:
327: $stmtToNodeIndex[count($stmts)] = $i;
328: $stmts[] = $node;
329: }
330:
331: $dummyParent = new Node\Stmt\Nop();
332: foreach ($stmts as $si => $node) {
333: if ($alreadyTerminated && !($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\Label)) {
334: continue;
335: }
336:
337: $nestedLabelNames = $node->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE);
338: if ($nestedLabelNames !== null) {
339: $scope = $this->resolveBackwardGotoScope(
340: $dummyParent,
341: [$node],
342: $scope,
343: $expressionResultStorage,
344: StatementContext::createDeep(),
345: static fn (string $name): bool => isset($nestedLabelNames[$name]),
346: false,
347: );
348: }
349:
350: $statementResult = $this->processStmtNode($node, $scope, $expressionResultStorage, $nodeCallback, StatementContext::createTopLevel());
351: $scope = $statementResult->getScope();
352:
353: if ($node instanceof Node\Stmt\Label) {
354: $labelName = $node->name->toString();
355:
356: [$scope, $alreadyTerminated, $exitPoints] = $this->mergeForwardGotoExitPoints(
357: $labelName,
358: $scope,
359: $alreadyTerminated,
360: $exitPoints,
361: );
362:
363: if ($alreadyTerminated) {
364: continue;
365: }
366:
367: if ($node->getAttribute(GotoLabelVisitor::HAS_BACKWARD_GOTO_ATTRIBUTE) === true) {
368: $scope = $this->resolveBackwardGotoScope(
369: $dummyParent,
370: array_slice($stmts, $si + 1),
371: $scope,
372: $expressionResultStorage,
373: StatementContext::createDeep(),
374: static fn (string $name): bool => $name === $labelName,
375: true,
376: );
377: }
378: }
379:
380: $exitPoints = array_merge($exitPoints, $statementResult->getExitPoints());
381:
382: if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) {
383: continue;
384: }
385:
386: $alreadyTerminated = true;
387: $nextStmts = $this->getNextUnreachableStatements(array_slice($nodes, $stmtToNodeIndex[$si] + 1), true);
388: $this->processUnreachableStatement($nextStmts, $scope, $expressionResultStorage, $nodeCallback);
389: }
390:
391: $this->processPendingFibers($expressionResultStorage);
392: }
393:
394: public function storeExpressionResult(ExpressionResultStorage $storage, Expr $expr, ExpressionResult $expressionResult): void
395: {
396: }
397:
398: protected function processPendingFibers(ExpressionResultStorage $storage): void
399: {
400: }
401:
402: /**
403: * @param Node\Stmt[] $bodyStmts
404: * @param Closure(string): bool $gotoNameMatcher
405: */
406: private function resolveBackwardGotoScope(
407: Node $parentNode,
408: array $bodyStmts,
409: MutatingScope $scope,
410: ExpressionResultStorage $storage,
411: StatementContext $context,
412: Closure $gotoNameMatcher,
413: bool $mergeBodyScopeEachIteration,
414: ): MutatingScope
415: {
416: $bodyScope = $scope;
417: $count = 0;
418: $prevEntryScope = null;
419: do {
420: $prevScope = $bodyScope;
421: if ($mergeBodyScopeEachIteration) {
422: $bodyScope = $bodyScope->mergeWith($scope);
423: }
424: if ($prevEntryScope !== null && $bodyScope->equals($prevEntryScope)) {
425: // walking is deterministic in the entry scope - an unchanged entry
426: // reproduces the previous pass's exit, so the verification walk is
427: // skipped
428: $bodyScope = $prevScope;
429: break;
430: }
431: $prevEntryScope = $bodyScope;
432: $tempStorage = $storage->duplicate();
433: $bodyScopeResult = $this->processStmtNodesInternal(
434: $parentNode,
435: $bodyStmts,
436: $bodyScope,
437: $tempStorage,
438: new NoopNodeCallback(),
439: $context,
440: );
441:
442: $gotoScope = null;
443: foreach ($bodyScopeResult->getExitPoints() as $ep) {
444: $epStmt = $ep->getStatement();
445: if (!($epStmt instanceof Goto_) || !$gotoNameMatcher($epStmt->name->toString())) {
446: continue;
447: }
448:
449: $gotoScope = $gotoScope === null ? $ep->getScope() : $gotoScope->mergeWith($ep->getScope());
450: }
451:
452: if ($gotoScope !== null) {
453: $bodyScope = $scope->mergeWith($gotoScope);
454: }
455:
456: if ($bodyScope->equals($prevScope)) {
457: break;
458: }
459:
460: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
461: $bodyScope = $prevScope->generalizeWith($bodyScope);
462: }
463: $count++;
464: } while ($count < self::LOOP_SCOPE_ITERATIONS);
465:
466: return $bodyScope;
467: }
468:
469: /**
470: * @param InternalStatementExitPoint[] $exitPoints
471: * @return array{MutatingScope, bool, list<InternalStatementExitPoint>}
472: */
473: private function mergeForwardGotoExitPoints(
474: string $labelName,
475: MutatingScope $scope,
476: bool $alreadyTerminated,
477: array $exitPoints,
478: ): array
479: {
480: $newExitPoints = [];
481: foreach ($exitPoints as $exitPoint) {
482: $exitStmt = $exitPoint->getStatement();
483: if ($exitStmt instanceof Goto_ && $exitStmt->name->toString() === $labelName) {
484: if ($alreadyTerminated) {
485: $scope = $exitPoint->getScope();
486: $alreadyTerminated = false;
487: } else {
488: $scope = $scope->mergeWith($exitPoint->getScope());
489: }
490: } else {
491: $newExitPoints[] = $exitPoint;
492: }
493: }
494:
495: return [$scope, $alreadyTerminated, $newExitPoints];
496: }
497:
498: /**
499: * @param Node\Stmt[] $nextStmts
500: * @param callable(Node $node, Scope $scope): void $nodeCallback
501: */
502: private function processUnreachableStatement(array $nextStmts, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback): void
503: {
504: if ($nextStmts === []) {
505: return;
506: }
507:
508: $unreachableStatement = null;
509: $nextStatements = [];
510:
511: foreach ($nextStmts as $key => $nextStmt) {
512: if ($key === 0) {
513: $unreachableStatement = $nextStmt;
514: continue;
515: }
516:
517: $nextStatements[] = $nextStmt;
518: }
519:
520: if (!$unreachableStatement instanceof Node\Stmt) {
521: return;
522: }
523:
524: $this->callNodeCallback($nodeCallback, new UnreachableStatementNode($unreachableStatement, $nextStatements), $scope, $storage);
525: }
526:
527: /**
528: * @api
529: * @param Node\Stmt[] $stmts
530: * @param callable(Node $node, Scope $scope): void $nodeCallback
531: */
532: public function processStmtNodes(
533: Node $parentNode,
534: array $stmts,
535: MutatingScope $scope,
536: callable $nodeCallback,
537: StatementContext $context,
538: ): StatementResult
539: {
540: $storage = new ExpressionResultStorage();
541: return $this->processStmtNodesInternal(
542: $parentNode,
543: $stmts,
544: $scope,
545: $storage,
546: $nodeCallback,
547: $context,
548: )->toPublic();
549: }
550:
551: /**
552: * @param Node\Stmt[] $stmts
553: * @param callable(Node $node, Scope $scope): void $nodeCallback
554: */
555: private function processStmtNodesInternal(
556: Node $parentNode,
557: array $stmts,
558: MutatingScope $scope,
559: ExpressionResultStorage $storage,
560: callable $nodeCallback,
561: StatementContext $context,
562: ): InternalStatementResult
563: {
564: $statementResult = $this->processStmtNodesInternalWithoutFlushingPendingFibers(
565: $parentNode,
566: $stmts,
567: $scope,
568: $storage,
569: $nodeCallback,
570: $context,
571: );
572: $this->processPendingFibers($storage);
573:
574: return $statementResult;
575: }
576:
577: /**
578: * The statement-list walk without the per-list pending-fiber flush - for
579: * the callers that sit inside an enclosing statement walk (nested
580: * control-flow lists, convergence passes) whose own boundary flushes.
581: *
582: * @param Node\Stmt[] $stmts
583: * @param callable(Node $node, Scope $scope): void $nodeCallback
584: */
585: private function processStmtNodesInternalWithoutFlushingPendingFibers(
586: Node $parentNode,
587: array $stmts,
588: MutatingScope $scope,
589: ExpressionResultStorage $storage,
590: callable $nodeCallback,
591: StatementContext $context,
592: ): InternalStatementResult
593: {
594: return $this->doProcessStmtNodes($parentNode, $stmts, $scope, $storage, $nodeCallback, $context);
595: }
596:
597: /**
598: * @param Node\Stmt[] $stmts
599: * @param callable(Node $node, Scope $scope): void $nodeCallback
600: */
601: private function doProcessStmtNodes(
602: Node $parentNode,
603: array $stmts,
604: MutatingScope $scope,
605: ExpressionResultStorage $storage,
606: callable $nodeCallback,
607: StatementContext $context,
608: ): InternalStatementResult
609: {
610: $exitPoints = [];
611: $throwPoints = [];
612: $impurePoints = [];
613: $alreadyTerminated = false;
614: $hasYield = false;
615: $stmtCount = count($stmts);
616: $shouldCheckLastStatement = $parentNode instanceof Node\Stmt\Function_
617: || $parentNode instanceof Node\Stmt\ClassMethod
618: || $parentNode instanceof PropertyHookStatementNode
619: || $parentNode instanceof Expr\Closure;
620:
621: foreach ($stmts as $i => $stmt) {
622: if ($alreadyTerminated && !($stmt instanceof Node\Stmt\Function_ || $stmt instanceof Node\Stmt\ClassLike || $stmt instanceof Node\Stmt\Label)) {
623: continue;
624: }
625:
626: $isLast = $i === $stmtCount - 1;
627:
628: $nestedLabelNames = $stmt->getAttribute(GotoLabelVisitor::NESTED_BACKWARD_GOTO_LABELS_ATTRIBUTE);
629: if ($nestedLabelNames !== null && $context->isTopLevel()) {
630: $scope = $this->resolveBackwardGotoScope(
631: $parentNode,
632: [$stmt],
633: $scope,
634: $storage,
635: $context->enterDeep(),
636: static fn (string $name): bool => isset($nestedLabelNames[$name]),
637: false,
638: );
639: }
640:
641: $statementResult = $this->processStmtNode(
642: $stmt,
643: $scope,
644: $storage,
645: $nodeCallback,
646: $context,
647: );
648: $scope = $statementResult->getScope();
649: $hasYield = $hasYield || $statementResult->hasYield();
650:
651: if ($stmt instanceof Node\Stmt\Label) {
652: $labelName = $stmt->name->toString();
653:
654: [$scope, $alreadyTerminated, $exitPoints] = $this->mergeForwardGotoExitPoints(
655: $labelName,
656: $scope,
657: $alreadyTerminated,
658: $exitPoints,
659: );
660:
661: if ($alreadyTerminated) {
662: continue;
663: }
664:
665: if ($stmt->getAttribute(GotoLabelVisitor::HAS_BACKWARD_GOTO_ATTRIBUTE) === true && $context->isTopLevel()) {
666: $scope = $this->resolveBackwardGotoScope(
667: $parentNode,
668: array_slice($stmts, $i + 1),
669: $scope,
670: $storage,
671: $context->enterDeep(),
672: static fn (string $name): bool => $name === $labelName,
673: true,
674: );
675: }
676: }
677:
678: if ($shouldCheckLastStatement && $isLast) {
679: $endStatements = $statementResult->getEndStatements();
680: if (count($endStatements) > 0) {
681: foreach ($endStatements as $endStatement) {
682: $endStatementResult = $endStatement->getResult();
683: $this->callNodeCallback($nodeCallback, new ExecutionEndNode(
684: $endStatement->getStatement(),
685: (new InternalStatementResult(
686: $endStatementResult->getScope(),
687: $hasYield,
688: $endStatementResult->isAlwaysTerminating(),
689: $endStatementResult->getExitPoints(),
690: $endStatementResult->getThrowPoints(),
691: $endStatementResult->getImpurePoints(),
692: ))->toPublic(),
693: $parentNode->getReturnType() !== null,
694: ), $endStatementResult->getScope(), $storage);
695: }
696: } else {
697: $this->callNodeCallback($nodeCallback, new ExecutionEndNode(
698: $stmt,
699: (new InternalStatementResult(
700: $scope,
701: $hasYield,
702: $statementResult->isAlwaysTerminating(),
703: $statementResult->getExitPoints(),
704: $statementResult->getThrowPoints(),
705: $statementResult->getImpurePoints(),
706: ))->toPublic(),
707: $parentNode->getReturnType() !== null,
708: ), $scope, $storage);
709: }
710: }
711:
712: $exitPoints = array_merge($exitPoints, $statementResult->getExitPoints());
713: $throwPoints = array_merge($throwPoints, $statementResult->getThrowPoints());
714: $impurePoints = array_merge($impurePoints, $statementResult->getImpurePoints());
715:
716: if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) {
717: continue;
718: }
719:
720: $alreadyTerminated = true;
721: $nextStmts = $this->getNextUnreachableStatements(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_);
722: $this->processUnreachableStatement($nextStmts, $scope, $storage, $nodeCallback);
723: }
724:
725: $statementResult = new InternalStatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints, $impurePoints);
726: if ($stmtCount === 0 && $shouldCheckLastStatement) {
727: $returnTypeNode = $parentNode->getReturnType();
728: if ($parentNode instanceof Expr\Closure) {
729: $parentNode = new Node\Stmt\Expression($parentNode, $parentNode->getAttributes());
730: }
731: $this->callNodeCallback($nodeCallback, new ExecutionEndNode(
732: $parentNode,
733: $statementResult->toPublic(),
734: $returnTypeNode !== null,
735: ), $scope, $storage);
736: }
737:
738: return $statementResult;
739: }
740:
741: /**
742: * @param callable(Node $node, Scope $scope): void $nodeCallback
743: */
744: public function processStmtNode(
745: Node\Stmt $stmt,
746: MutatingScope $scope,
747: ExpressionResultStorage $storage,
748: callable $nodeCallback,
749: StatementContext $context,
750: ): InternalStatementResult
751: {
752: $overridingThrowPoints = null;
753: if (
754: !$stmt instanceof Static_
755: && !$stmt instanceof Node\Stmt\Global_
756: && !$stmt instanceof Node\Stmt\Property
757: && !$stmt instanceof Node\Stmt\ClassConst
758: && !$stmt instanceof Node\Stmt\Const_
759: && !$stmt instanceof Node\Stmt\ClassLike
760: && !$stmt instanceof Node\Stmt\Function_
761: && !$stmt instanceof Node\Stmt\ClassMethod
762: ) {
763: if (!$stmt instanceof Foreach_) {
764: $scope = $this->processStmtVarAnnotation($scope, $storage, $stmt, null, $nodeCallback);
765: }
766: $overridingThrowPoints = $this->getOverridingThrowPoints($stmt, $scope);
767: }
768:
769: if ($stmt instanceof Node\Stmt\ClassMethod) {
770: if (!$scope->isInClass()) {
771: throw new ShouldNotHappenException();
772: }
773: if (
774: $scope->isInTrait()
775: && $scope->getClassReflection()->hasNativeMethod($stmt->name->toString())
776: ) {
777: $methodReflection = $scope->getClassReflection()->getNativeMethod($stmt->name->toString());
778: if ($methodReflection instanceof NativeMethodReflection) {
779: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []);
780: }
781: if ($methodReflection instanceof PhpMethodReflection) {
782: $declaringTrait = $methodReflection->getDeclaringTrait();
783: if ($declaringTrait === null || $declaringTrait->getName() !== $scope->getTraitReflection()->getName()) {
784: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []);
785: }
786: }
787: }
788: }
789:
790: $stmtScope = $scope;
791: if ($stmt instanceof Node\Stmt\Expression && $stmt->expr instanceof Expr\Throw_) {
792: $stmtScope = $this->processStmtVarAnnotation($scope, $storage, $stmt, $stmt->expr->expr, $nodeCallback);
793: }
794: if ($stmt instanceof Return_) {
795: $stmtScope = $this->processStmtVarAnnotation($scope, $storage, $stmt, $stmt->expr, $nodeCallback);
796: }
797:
798: $this->callNodeCallback($nodeCallback, $stmt, $stmtScope, $storage);
799:
800: if ($stmt instanceof Node\Stmt\Declare_) {
801: $hasYield = false;
802: $throwPoints = [];
803: $impurePoints = [];
804: $alwaysTerminating = false;
805: $exitPoints = [];
806: foreach ($stmt->declares as $declare) {
807: $this->callNodeCallback($nodeCallback, $declare, $scope, $storage);
808: $this->callNodeCallback($nodeCallback, $declare->value, $scope, $storage);
809: if (
810: $declare->key->name !== 'strict_types'
811: || !($declare->value instanceof Node\Scalar\Int_)
812: || $declare->value->value !== 1
813: ) {
814: continue;
815: }
816:
817: $scope = $scope->enterDeclareStrictTypes();
818: }
819:
820: if ($stmt->stmts !== null) {
821: $result = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context);
822: $scope = $result->getScope();
823: $hasYield = $result->hasYield();
824: $throwPoints = $result->getThrowPoints();
825: $impurePoints = $result->getImpurePoints();
826: $alwaysTerminating = $result->isAlwaysTerminating();
827: $exitPoints = $result->getExitPoints();
828: }
829:
830: return new InternalStatementResult($scope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints);
831: } elseif ($stmt instanceof Node\Stmt\Function_) {
832: $hasYield = false;
833: $throwPoints = [];
834: $impurePoints = [];
835: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
836: [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, , $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters] = $this->getPhpDocs($scope, $stmt);
837:
838: foreach ($stmt->params as $param) {
839: $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
840: }
841:
842: if ($stmt->returnType !== null) {
843: $this->callNodeCallback($nodeCallback, $stmt->returnType, $scope, $storage);
844: }
845:
846: if (!$isDeprecated) {
847: [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $stmt);
848: }
849:
850: $functionScope = $scope->enterFunction(
851: $stmt,
852: $templateTypeMap,
853: $phpDocParameterTypes,
854: $phpDocReturnType,
855: $phpDocThrowType,
856: $deprecatedDescription,
857: $isDeprecated,
858: $isInternal,
859: $isPure,
860: $acceptsNamedArguments,
861: $asserts,
862: $phpDocComment,
863: $phpDocParameterOutTypes,
864: $phpDocImmediatelyInvokedCallableParameters,
865: $phpDocClosureThisTypeParameters,
866: $pureUnlessCallableIsImpureParameters,
867: );
868: $functionReflection = $functionScope->getFunction();
869: if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) {
870: throw new ShouldNotHappenException();
871: }
872:
873: $this->callNodeCallback($nodeCallback, new InFunctionNode($functionReflection, $stmt), $functionScope, $storage);
874:
875: $gatheredReturnStatements = [];
876: $gatheredYieldStatements = [];
877: $executionEnds = [];
878: $functionImpurePoints = [];
879: $statementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $functionScope, $storage, new GatheringNodeCallback(static function (Node $node, Scope $scope) use ($functionScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$functionImpurePoints): void {
880: if ($scope->getFunction() !== $functionScope->getFunction()) {
881: return;
882: }
883: if ($scope->isInAnonymousFunction()) {
884: return;
885: }
886: if ($node instanceof PropertyAssignNode) {
887: $functionImpurePoints[] = new ImpurePoint(
888: $scope,
889: $node,
890: 'propertyAssign',
891: 'property assignment',
892: true,
893: );
894: return;
895: }
896: if ($node instanceof ExecutionEndNode) {
897: $executionEnds[] = $node;
898: return;
899: }
900: if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) {
901: $gatheredYieldStatements[] = $node;
902: }
903: if (!$node instanceof Return_) {
904: return;
905: }
906:
907: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
908: }, $nodeCallback), StatementContext::createTopLevel())->toPublic();
909:
910: $this->callNodeCallback($nodeCallback, new FunctionReturnStatementsNode(
911: $stmt,
912: $gatheredReturnStatements,
913: $gatheredYieldStatements,
914: $statementResult,
915: $executionEnds,
916: array_merge($statementResult->getImpurePoints(), $functionImpurePoints),
917: $functionReflection,
918: ), $functionScope, $storage);
919: if (!$scope->isInAnonymousFunction()) {
920: $this->processPendingFibers($storage);
921: }
922:
923: // declaring the function defines it in global state, so a negative
924: // function_exists() narrowing that may refer to that function must be forgotten
925: $scope = $scope->invalidateExistenceCheckExpressions(['function_exists'], $functionReflection->getName());
926: } elseif ($stmt instanceof Node\Stmt\ClassMethod) {
927: $hasYield = false;
928: $throwPoints = [];
929: $impurePoints = [];
930: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
931: [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes, , , , $pureUnlessCallableIsImpureParameters] = $this->getPhpDocs($scope, $stmt);
932:
933: foreach ($stmt->params as $param) {
934: $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
935: }
936:
937: if ($stmt->returnType !== null) {
938: $this->callNodeCallback($nodeCallback, $stmt->returnType, $scope, $storage);
939: }
940:
941: if (!$isDeprecated) {
942: [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $stmt);
943: }
944:
945: $isFromTrait = $stmt->getAttribute('originalTraitMethodName') === '__construct';
946: $isConstructor = $isFromTrait || $stmt->name->toLowerString() === '__construct';
947:
948: $methodScope = $scope->enterClassMethod(
949: $stmt,
950: $templateTypeMap,
951: $phpDocParameterTypes,
952: $phpDocReturnType,
953: $phpDocThrowType,
954: $deprecatedDescription,
955: $isDeprecated,
956: $isInternal,
957: $isFinal,
958: $isPure,
959: $acceptsNamedArguments,
960: $asserts,
961: $selfOutType,
962: $phpDocComment,
963: $phpDocParameterOutTypes,
964: $phpDocImmediatelyInvokedCallableParameters,
965: $phpDocClosureThisTypeParameters,
966: $isConstructor,
967: null,
968: $pureUnlessCallableIsImpureParameters,
969: );
970:
971: if (!$scope->isInClass()) {
972: throw new ShouldNotHappenException();
973: }
974:
975: $classReflection = $scope->getClassReflection();
976:
977: if ($isConstructor) {
978: foreach ($stmt->params as $param) {
979: if ($param->flags === 0 && $param->hooks === []) {
980: continue;
981: }
982:
983: if (!$param->var instanceof Variable || !is_string($param->var->name) || $param->var->name === '') {
984: throw new ShouldNotHappenException();
985: }
986: $phpDoc = null;
987: if ($param->getDocComment() !== null) {
988: $phpDoc = $param->getDocComment()->getText();
989: }
990: $this->callNodeCallback($nodeCallback, new ClassPropertyNode(
991: $param->var->name,
992: $param->flags,
993: $param->type !== null ? ParserNodeTypeToPHPStanType::resolve($param->type, $classReflection) : null,
994: null,
995: $phpDoc,
996: $phpDocParameterTypes[$param->var->name] ?? null,
997: true,
998: $isFromTrait,
999: $param,
1000: $isReadOnly,
1001: $scope->isInTrait(),
1002: $classReflection->isReadOnly(),
1003: false,
1004: $classReflection,
1005: ), $methodScope, $storage);
1006: $this->processPropertyHooks(
1007: $stmt,
1008: $param->type,
1009: $phpDocParameterTypes[$param->var->name] ?? null,
1010: $param->var->name,
1011: $param->hooks,
1012: $scope,
1013: $storage,
1014: $nodeCallback,
1015: );
1016: $methodScope = $methodScope->assignExpression(new PropertyInitializationExpr($param->var->name), new MixedType(), new MixedType());
1017: }
1018: }
1019:
1020: if ($stmt->getAttribute('virtual', false) === false) {
1021: $methodReflection = $methodScope->getFunction();
1022: if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) {
1023: throw new ShouldNotHappenException();
1024: }
1025: $this->callNodeCallback($nodeCallback, new InClassMethodNode($classReflection, $methodReflection, $stmt), $methodScope, $storage);
1026: }
1027:
1028: if ($stmt->stmts !== null) {
1029: $gatheredReturnStatements = [];
1030: $gatheredYieldStatements = [];
1031: $executionEnds = [];
1032: $methodImpurePoints = [];
1033: $statementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $methodScope, $storage, new GatheringNodeCallback(static function (Node $node, Scope $scope) use ($methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$methodImpurePoints): void {
1034: if ($scope->getFunction() !== $methodScope->getFunction()) {
1035: return;
1036: }
1037: if ($scope->isInAnonymousFunction()) {
1038: return;
1039: }
1040: if ($node instanceof PropertyAssignNode) {
1041: if (
1042: $node->getPropertyFetch() instanceof Expr\PropertyFetch
1043: && $scope->getFunction() instanceof PhpMethodFromParserNodeReflection
1044: && $scope->getFunction()->getDeclaringClass()->hasConstructor()
1045: && $scope->getFunction()->getDeclaringClass()->getConstructor()->getName() === $scope->getFunction()->getName()
1046: && TypeUtils::findThisType($scope->getType($node->getPropertyFetch()->var)) !== null
1047: ) {
1048: return;
1049: }
1050: $methodImpurePoints[] = new ImpurePoint(
1051: $scope,
1052: $node,
1053: 'propertyAssign',
1054: 'property assignment',
1055: true,
1056: );
1057: return;
1058: }
1059: if ($node instanceof ExecutionEndNode) {
1060: $executionEnds[] = $node;
1061: return;
1062: }
1063: if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) {
1064: $gatheredYieldStatements[] = $node;
1065: }
1066: if (!$node instanceof Return_) {
1067: return;
1068: }
1069:
1070: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
1071: }, $nodeCallback), StatementContext::createTopLevel())->toPublic();
1072:
1073: $methodReflection = $methodScope->getFunction();
1074: if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) {
1075: throw new ShouldNotHappenException();
1076: }
1077:
1078: $this->callNodeCallback($nodeCallback, new MethodReturnStatementsNode(
1079: $stmt,
1080: $gatheredReturnStatements,
1081: $gatheredYieldStatements,
1082: $statementResult,
1083: $executionEnds,
1084: array_merge($statementResult->getImpurePoints(), $methodImpurePoints),
1085: $classReflection,
1086: $methodReflection,
1087: ), $methodScope, $storage);
1088:
1089: if ($isConstructor) {
1090: $finalScope = null;
1091:
1092: foreach ($executionEnds as $executionEnd) {
1093: if ($executionEnd->getStatementResult()->isAlwaysTerminating()) {
1094: continue;
1095: }
1096:
1097: $endScope = $executionEnd->getStatementResult()->getScope();
1098: if ($finalScope === null) {
1099: $finalScope = $endScope;
1100: continue;
1101: }
1102:
1103: $finalScope = $finalScope->mergeWith($endScope);
1104: }
1105:
1106: foreach ($gatheredReturnStatements as $statement) {
1107: if ($finalScope === null) {
1108: $finalScope = $statement->getScope()->toMutatingScope();
1109: continue;
1110: }
1111:
1112: $finalScope = $finalScope->mergeWith($statement->getScope()->toMutatingScope());
1113: }
1114:
1115: if ($finalScope !== null) {
1116: $scope = $finalScope->rememberConstructorScope();
1117: }
1118:
1119: }
1120: }
1121: if (!$scope->getClassReflection()->isAnonymous() && !$scope->isInAnonymousFunction()) {
1122: $this->processPendingFibers($storage);
1123: }
1124: } elseif ($stmt instanceof Echo_) {
1125: $hasYield = false;
1126: $throwPoints = [];
1127: $impurePoints = [];
1128: $isAlwaysTerminating = false;
1129: foreach ($stmt->exprs as $echoExpr) {
1130: $result = $this->processExprNode($stmt, $echoExpr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
1131: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
1132: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
1133: $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($echoExpr, $scope);
1134: $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints());
1135: $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints());
1136: $scope = $result->getScope();
1137: $hasYield = $hasYield || $result->hasYield();
1138: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
1139: }
1140:
1141: $throwPoints = $overridingThrowPoints ?? $throwPoints;
1142: $impurePoints[] = new ImpurePoint($scope, $stmt, 'echo', 'echo', true);
1143: return new InternalStatementResult($scope, $hasYield, $isAlwaysTerminating, [], $throwPoints, $impurePoints);
1144: } elseif ($stmt instanceof Return_) {
1145: if ($stmt->expr !== null) {
1146: $result = $this->processExprNode($stmt, $stmt->expr, $stmtScope, $storage, $nodeCallback, ExpressionContext::createDeep());
1147: $throwPoints = $result->getThrowPoints();
1148: $impurePoints = $result->getImpurePoints();
1149: $scope = $result->getScope();
1150: $hasYield = $result->hasYield();
1151: } else {
1152: $hasYield = false;
1153: $throwPoints = [];
1154: $impurePoints = [];
1155: }
1156:
1157: return new InternalStatementResult($scope, $hasYield, true, [
1158: new InternalStatementExitPoint($stmt, $scope),
1159: ], $overridingThrowPoints ?? $throwPoints, $impurePoints);
1160: } elseif ($stmt instanceof Continue_ || $stmt instanceof Break_) {
1161: if ($stmt->num !== null) {
1162: $result = $this->processExprNode($stmt, $stmt->num, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
1163: $scope = $result->getScope();
1164: $hasYield = $result->hasYield();
1165: $throwPoints = $result->getThrowPoints();
1166: $impurePoints = $result->getImpurePoints();
1167: } else {
1168: $hasYield = false;
1169: $throwPoints = [];
1170: $impurePoints = [];
1171: }
1172:
1173: return new InternalStatementResult($scope, $hasYield, true, [
1174: new InternalStatementExitPoint($stmt, $scope),
1175: ], $overridingThrowPoints ?? $throwPoints, $impurePoints);
1176: } elseif ($stmt instanceof Goto_) {
1177: $hasYield = false;
1178: $throwPoints = [];
1179: $impurePoints = [];
1180:
1181: return new InternalStatementResult($scope, $hasYield, true, [
1182: new InternalStatementExitPoint($stmt, $scope),
1183: ], $overridingThrowPoints ?? $throwPoints, $impurePoints);
1184: } elseif ($stmt instanceof Node\Stmt\Label) {
1185: $hasYield = false;
1186: $throwPoints = $overridingThrowPoints ?? [];
1187: $impurePoints = [];
1188: } elseif ($stmt instanceof Node\Stmt\Expression) {
1189: if ($stmt->expr instanceof Expr\Throw_) {
1190: $scope = $stmtScope;
1191: }
1192: $hasAssign = false;
1193: $currentScope = $scope;
1194: $result = $this->processExprNode($stmt, $stmt->expr, $scope, $storage, new GatheringNodeCallback(static function (Node $node, Scope $scope) use ($currentScope, &$hasAssign): void {
1195: if (
1196: !($node instanceof VariableAssignNode) && !($node instanceof PropertyAssignNode)
1197: || $scope->getAnonymousFunctionReflection() !== $currentScope->getAnonymousFunctionReflection()
1198: || $scope->getFunction() !== $currentScope->getFunction()
1199: ) {
1200: return;
1201: }
1202:
1203: $hasAssign = true;
1204: }, $nodeCallback), ExpressionContext::createTopLevel());
1205: $throwPoints = array_filter($result->getThrowPoints(), static fn ($throwPoint) => $throwPoint->isExplicit());
1206: if (
1207: count($result->getImpurePoints()) === 0
1208: && count($throwPoints) === 0
1209: && !$stmt->expr instanceof Expr\PostInc
1210: && !$stmt->expr instanceof Expr\PreInc
1211: && !$stmt->expr instanceof Expr\PostDec
1212: && !$stmt->expr instanceof Expr\PreDec
1213: ) {
1214: $this->callNodeCallback($nodeCallback, new NoopExpressionNode($stmt->expr, $hasAssign), $scope, $storage);
1215: }
1216: $scope = $result->getScope();
1217: $scope = $scope->applySpecifiedTypes($this->typeSpecifier->specifyTypesInCondition(
1218: $scope,
1219: $stmt->expr,
1220: TypeSpecifierContext::createNull(),
1221: ));
1222: $hasYield = $result->hasYield();
1223: $throwPoints = $result->getThrowPoints();
1224: $impurePoints = $result->getImpurePoints();
1225: $isAlwaysTerminating = $result->isAlwaysTerminating();
1226:
1227: // The expression statement is an exit point when its value type is an
1228: // explicit never: exit/die/throw, a never-returning call, or a call
1229: // configured as early-terminating (the call handlers give those never).
1230: // Asked on the pre-statement scope: a conditional return type must
1231: // resolve against the argument types the call was made with, not
1232: // against state the statement itself just changed (bug-11565).
1233: $statementType = $currentScope->getType($stmt->expr);
1234: if ($statementType instanceof NeverType && $statementType->isExplicit()) {
1235: return new InternalStatementResult($scope, $hasYield, true, [
1236: new InternalStatementExitPoint($stmt, $scope),
1237: ], $overridingThrowPoints ?? $throwPoints, $impurePoints);
1238: }
1239: return new InternalStatementResult($scope, $hasYield, $isAlwaysTerminating, [], $overridingThrowPoints ?? $throwPoints, $impurePoints);
1240: } elseif ($stmt instanceof Node\Stmt\Namespace_) {
1241: if ($stmt->name !== null) {
1242: $scope = $scope->enterNamespace($stmt->name->toString());
1243: } else {
1244: $scope = $scope->enterNamespace('');
1245: }
1246:
1247: $scope = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context)->getScope();
1248: $hasYield = false;
1249: $throwPoints = [];
1250: $impurePoints = [];
1251: } elseif ($stmt instanceof Node\Stmt\Trait_) {
1252: // declaring the trait defines it in global state,
1253: // so a negative trait_exists() narrowing that may refer to that trait must be forgotten
1254: $name = $stmt->namespacedName ?? $stmt->name;
1255: $scope = $scope->invalidateExistenceCheckExpressions(['trait_exists'], $name instanceof Name ? $name->toString() : null);
1256:
1257: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []);
1258: } elseif ($stmt instanceof Node\Stmt\ClassLike) {
1259: // declaring a class/interface/enum defines it in global state,
1260: // so a matching negative existence-check narrowing must be forgotten
1261: if ($stmt instanceof Node\Stmt\Interface_) {
1262: $existenceCheckFunctionNames = ['interface_exists'];
1263: } elseif ($stmt instanceof Node\Stmt\Enum_) {
1264: $existenceCheckFunctionNames = ['class_exists', 'enum_exists'];
1265: } else {
1266: $existenceCheckFunctionNames = ['class_exists'];
1267: }
1268: $name = $stmt->namespacedName ?? $stmt->name;
1269: $scope = $scope->invalidateExistenceCheckExpressions($existenceCheckFunctionNames, $name instanceof Name ? $name->toString() : null);
1270:
1271: if (!$context->isTopLevel()) {
1272: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []);
1273: }
1274: $hasYield = false;
1275: $throwPoints = [];
1276: $impurePoints = [];
1277: if (isset($stmt->namespacedName)) {
1278: $classReflection = $this->getCurrentClassReflection($stmt, $stmt->namespacedName->toString(), $scope);
1279: $classScope = $scope->enterClass($classReflection);
1280: $this->callNodeCallback($nodeCallback, new InClassNode($stmt, $classReflection), $classScope, $storage);
1281: } elseif ($stmt instanceof Class_) {
1282: if ($stmt->name === null) {
1283: throw new ShouldNotHappenException();
1284: }
1285: if (!$stmt->isAnonymous()) {
1286: $classReflection = $this->reflectionProvider->getClass($stmt->name->toString());
1287: } else {
1288: $classReflection = $this->reflectionProvider->getAnonymousClassReflection($stmt, $scope);
1289: }
1290: $classScope = $scope->enterClass($classReflection);
1291: $this->callNodeCallback($nodeCallback, new InClassNode($stmt, $classReflection), $classScope, $storage);
1292: } else {
1293: throw new ShouldNotHappenException();
1294: }
1295:
1296: $classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback);
1297: $this->processAttributeGroups($stmt, $stmt->attrGroups, $classScope, $storage, $classStatementsGatherer);
1298:
1299: $classLikeStatements = $stmt->stmts;
1300: // analyze static methods first; constructor next; instance methods and property hooks last so we can carry over the scope
1301: usort($classLikeStatements, static function ($a, $b) {
1302: if ($a instanceof Node\Stmt\Property) {
1303: return 1;
1304: }
1305: if ($b instanceof Node\Stmt\Property) {
1306: return -1;
1307: }
1308:
1309: if (!$a instanceof Node\Stmt\ClassMethod || !$b instanceof Node\Stmt\ClassMethod) {
1310: return 0;
1311: }
1312:
1313: return [!$a->isStatic(), $a->name->toLowerString() !== '__construct'] <=> [!$b->isStatic(), $b->name->toLowerString() !== '__construct'];
1314: });
1315:
1316: $this->processStmtNodesInternal($stmt, $classLikeStatements, $classScope, $storage, $classStatementsGatherer, $context);
1317: $this->callNodeCallback($nodeCallback, new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensions, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classStatementsGatherer->getPropertyAssigns(), $classReflection), $classScope, $storage);
1318: $this->callNodeCallback($nodeCallback, new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls(), $classReflection), $classScope, $storage);
1319: $this->callNodeCallback($nodeCallback, new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches(), $classReflection), $classScope, $storage);
1320: $classReflection->evictPrivateSymbols();
1321: $this->calledMethodResults = [];
1322: } elseif ($stmt instanceof Node\Stmt\Property) {
1323: $hasYield = false;
1324: $throwPoints = [];
1325: $impurePoints = [];
1326: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
1327:
1328: $nativePropertyType = $stmt->type !== null ? ParserNodeTypeToPHPStanType::resolve($stmt->type, $scope->getClassReflection()) : null;
1329:
1330: [,,,,,,,,,,,,$isReadOnly, $docComment, ,,,$varTags, $isAllowedPrivateMutation] = $this->getPhpDocs($scope, $stmt);
1331: $phpDocType = null;
1332: if (isset($varTags[0]) && count($varTags) === 1) {
1333: $phpDocType = $varTags[0]->getType();
1334: }
1335:
1336: foreach ($stmt->props as $prop) {
1337: $this->callNodeCallback($nodeCallback, $prop, $scope, $storage);
1338: if ($prop->default !== null) {
1339: $this->processExprNode($stmt, $prop->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
1340: }
1341:
1342: if (!$scope->isInClass()) {
1343: throw new ShouldNotHappenException();
1344: }
1345: $propertyName = $prop->name->toString();
1346:
1347: if ($phpDocType === null) {
1348: if (isset($varTags[$propertyName])) {
1349: $phpDocType = $varTags[$propertyName]->getType();
1350: }
1351: }
1352:
1353: $propStmt = clone $stmt;
1354: $propStmt->setAttributes($prop->getAttributes());
1355: $propStmt->setAttribute('originalPropertyStmt', $stmt);
1356: $this->callNodeCallback(
1357: $nodeCallback,
1358: new ClassPropertyNode(
1359: $propertyName,
1360: $stmt->flags,
1361: $nativePropertyType,
1362: $prop->default,
1363: $docComment,
1364: $phpDocType,
1365: false,
1366: false,
1367: $propStmt,
1368: $isReadOnly,
1369: $scope->isInTrait(),
1370: $scope->getClassReflection()->isReadOnly(),
1371: $isAllowedPrivateMutation,
1372: $scope->getClassReflection(),
1373: ),
1374: $scope,
1375: $storage,
1376: );
1377: }
1378:
1379: if (count($stmt->hooks) > 0) {
1380: if (!isset($propertyName)) {
1381: throw new ShouldNotHappenException('Property name should be known when analysing hooks.');
1382: }
1383: $this->processPropertyHooks(
1384: $stmt,
1385: $stmt->type,
1386: $phpDocType,
1387: $propertyName,
1388: $stmt->hooks,
1389: $scope,
1390: $storage,
1391: $nodeCallback,
1392: );
1393: }
1394:
1395: if ($stmt->type !== null) {
1396: $this->callNodeCallback($nodeCallback, $stmt->type, $scope, $storage);
1397: }
1398: } elseif ($stmt instanceof If_) {
1399: $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
1400: $conditionType = ($this->treatPhpDocTypesAsCertain ? $condResult->getType() : $condResult->getNativeType())->toBoolean();
1401: $ifAlwaysTrue = $conditionType->isTrue()->yes();
1402: $exitPoints = [];
1403: $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints();
1404: $impurePoints = $condResult->getImpurePoints();
1405: $endStatements = [];
1406: $finalScope = null;
1407: $alwaysTerminating = true;
1408: $hasYield = $condResult->hasYield();
1409:
1410: $branchScopeStatementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $condResult->getTruthyScope(), $storage, $nodeCallback, $context);
1411:
1412: if (!$conditionType->isTrue()->no()) {
1413: $exitPoints = $branchScopeStatementResult->getExitPoints();
1414: $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints());
1415: $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints());
1416: $branchScope = $branchScopeStatementResult->getScope();
1417: $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? null : $branchScope;
1418: $alwaysTerminating = $branchScopeStatementResult->isAlwaysTerminating();
1419: if (count($branchScopeStatementResult->getEndStatements()) > 0) {
1420: $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements());
1421: } elseif (count($stmt->stmts) > 0) {
1422: $endStatements[] = new InternalEndStatementResult($stmt->stmts[count($stmt->stmts) - 1], $branchScopeStatementResult);
1423: } else {
1424: $endStatements[] = new InternalEndStatementResult($stmt, $branchScopeStatementResult);
1425: }
1426: $hasYield = $branchScopeStatementResult->hasYield() || $hasYield;
1427: }
1428:
1429: $scope = $condResult->getFalseyScope();
1430: $lastElseIfConditionIsTrue = false;
1431:
1432: $condScope = $scope;
1433: foreach ($stmt->elseifs as $elseif) {
1434: $this->callNodeCallback($nodeCallback, $elseif, $scope, $storage);
1435: $condResult = $this->processExprNode($stmt, $elseif->cond, $condScope, $storage, $nodeCallback, ExpressionContext::createDeep());
1436: $elseIfConditionType = ($this->treatPhpDocTypesAsCertain ? $condResult->getType() : $condResult->getNativeType())->toBoolean();
1437: $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints());
1438: $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints());
1439: $branchScopeStatementResult = $this->processStmtNodesInternal($elseif, $elseif->stmts, $condResult->getTruthyScope(), $storage, $nodeCallback, $context);
1440:
1441: if (
1442: !$ifAlwaysTrue
1443: && !$lastElseIfConditionIsTrue
1444: && !$elseIfConditionType->isTrue()->no()
1445: ) {
1446: $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints());
1447: $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints());
1448: $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints());
1449: $branchScope = $branchScopeStatementResult->getScope();
1450: $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope, true);
1451: $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating();
1452: if (count($branchScopeStatementResult->getEndStatements()) > 0) {
1453: $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements());
1454: } elseif (count($elseif->stmts) > 0) {
1455: $endStatements[] = new InternalEndStatementResult($elseif->stmts[count($elseif->stmts) - 1], $branchScopeStatementResult);
1456: } else {
1457: $endStatements[] = new InternalEndStatementResult($elseif, $branchScopeStatementResult);
1458: }
1459: $hasYield = $hasYield || $branchScopeStatementResult->hasYield();
1460: }
1461:
1462: if (
1463: $elseIfConditionType->isTrue()->yes()
1464: ) {
1465: $lastElseIfConditionIsTrue = true;
1466: }
1467:
1468: $condScope = $condResult->getFalseyScope();
1469: $scope = $condScope;
1470: }
1471:
1472: if ($stmt->else === null) {
1473: if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) {
1474: $finalScope = $scope->mergeWith($finalScope, true);
1475: $alwaysTerminating = false;
1476: }
1477: } else {
1478: $this->callNodeCallback($nodeCallback, $stmt->else, $scope, $storage);
1479: $branchScopeStatementResult = $this->processStmtNodesInternal($stmt->else, $stmt->else->stmts, $scope, $storage, $nodeCallback, $context);
1480:
1481: if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) {
1482: $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints());
1483: $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints());
1484: $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints());
1485: $branchScope = $branchScopeStatementResult->getScope();
1486: $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope, true);
1487: $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating();
1488: if (count($branchScopeStatementResult->getEndStatements()) > 0) {
1489: $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements());
1490: } elseif (count($stmt->else->stmts) > 0) {
1491: $endStatements[] = new InternalEndStatementResult($stmt->else->stmts[count($stmt->else->stmts) - 1], $branchScopeStatementResult);
1492: } else {
1493: $endStatements[] = new InternalEndStatementResult($stmt->else, $branchScopeStatementResult);
1494: }
1495: $hasYield = $hasYield || $branchScopeStatementResult->hasYield();
1496: }
1497: }
1498:
1499: if ($finalScope === null) {
1500: $finalScope = $scope;
1501: }
1502:
1503: if ($stmt->else === null && !$ifAlwaysTrue && !$lastElseIfConditionIsTrue) {
1504: $endStatements[] = new InternalEndStatementResult($stmt, new InternalStatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints));
1505: }
1506:
1507: return new InternalStatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints, $endStatements);
1508: } elseif ($stmt instanceof Node\Stmt\TraitUse) {
1509: $hasYield = false;
1510: $throwPoints = [];
1511: $impurePoints = [];
1512:
1513: $traitStorage = $storage->duplicate();
1514: $traitStorage->pendingFibers = [];
1515: $this->processTraitUse($stmt, $scope, $traitStorage, $nodeCallback);
1516: $this->processPendingFibers($traitStorage);
1517: } elseif ($stmt instanceof Foreach_) {
1518: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
1519: $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt);
1520: }
1521: $condResult = $this->processExprNode($stmt, $stmt->expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
1522: $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints();
1523: $impurePoints = $condResult->getImpurePoints();
1524: $scope = $condResult->getScope();
1525: $arrayComparisonExpr = new BinaryOp\NotIdentical(
1526: $stmt->expr,
1527: new Array_([]),
1528: );
1529: $this->callNodeCallback($nodeCallback, new InForeachNode($stmt), $scope, $storage);
1530: $originalScope = $scope;
1531: $bodyScope = $scope;
1532: $foreachIterateeType = $originalScope->getType($stmt->expr);
1533: $foreachNativeIterateeType = $originalScope->getNativeType($stmt->expr);
1534:
1535: if ($stmt->keyVar instanceof Variable) {
1536: $keyTypeExpr = new NativeTypeExpr(
1537: $originalScope->getIterableKeyType($foreachIterateeType),
1538: $originalScope->getIterableKeyType($foreachNativeIterateeType),
1539: );
1540: $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->keyVar, $keyTypeExpr), $originalScope, $storage);
1541: }
1542:
1543: if ($stmt->valueVar instanceof Variable) {
1544: $valueTypeExpr = new NativeTypeExpr(
1545: $originalScope->getIterableValueType($foreachIterateeType),
1546: $originalScope->getIterableValueType($foreachNativeIterateeType),
1547: );
1548: $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->valueVar, $valueTypeExpr), $originalScope, $storage);
1549: } elseif ($stmt->valueVar instanceof List_) {
1550: $virtualAssign = new Assign($stmt->valueVar, new NativeTypeExpr(
1551: $originalScope->getIterableValueType($foreachIterateeType),
1552: $originalScope->getIterableValueType($foreachNativeIterateeType),
1553: ));
1554: $virtualAssign->setAttributes($stmt->valueVar->getAttributes());
1555: $this->callNodeCallback($nodeCallback, $virtualAssign, $scope, $storage);
1556: }
1557:
1558: $originalStorage = $storage;
1559: $unrolledEndScope = null;
1560: $unrolledTotalKeys = null;
1561: $iterateeScope = $this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope;
1562: if ($context->isTopLevel()) {
1563: $storage = $originalStorage->duplicate();
1564:
1565: $originalScope = $iterateeScope;
1566: $foreachIterateeType = $originalScope->getType($stmt->expr);
1567: $foreachNativeIterateeType = $originalScope->getNativeType($stmt->expr);
1568: $unrolledResult = $this->tryProcessUnrolledConstantArrayForeach($stmt, $originalScope, $originalStorage, $context, $foreachIterateeType, $foreachNativeIterateeType);
1569: if ($unrolledResult !== null) {
1570: $bodyScope = $unrolledResult['bodyScope'];
1571: $unrolledEndScope = $unrolledResult['endScope'];
1572: $unrolledTotalKeys = $unrolledResult['totalKeys'];
1573: } else {
1574: $bodyScope = $this->enterForeach($originalScope, $storage, $originalScope, $stmt, $foreachIterateeType, $foreachNativeIterateeType, $nodeCallback);
1575: $count = 0;
1576: $prevEntryScope = null;
1577: do {
1578: $prevScope = $bodyScope;
1579: $bodyScope = $bodyScope->mergeWith($iterateeScope);
1580: if ($prevEntryScope !== null && $bodyScope->equals($prevEntryScope)) {
1581: // walking is deterministic in the entry scope - an unchanged entry
1582: // reproduces the previous pass's exit, so the verification walk is
1583: // skipped
1584: $bodyScope = $prevScope;
1585: break;
1586: }
1587: $prevEntryScope = $bodyScope;
1588: $storage = $originalStorage->duplicate();
1589: $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $foreachIterateeType, $foreachNativeIterateeType, $nodeCallback);
1590: $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints();
1591: $bodyScope = $bodyScopeResult->getScope();
1592: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1593: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1594: }
1595: if ($bodyScope->equals($prevScope)) {
1596: break;
1597: }
1598:
1599: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
1600: $bodyScope = $prevScope->generalizeWith($bodyScope);
1601: }
1602: $count++;
1603: } while ($count < self::LOOP_SCOPE_ITERATIONS);
1604: }
1605: }
1606:
1607: $bodyScope = $bodyScope->mergeWith($iterateeScope);
1608: $storage = $originalStorage;
1609: $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $foreachIterateeType, $foreachNativeIterateeType, $nodeCallback);
1610: $finalPassContext = $unrolledTotalKeys !== null ? $context->enterUnrolledForeach($unrolledTotalKeys) : $context;
1611: $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $finalPassContext)->filterOutLoopExitPoints();
1612: $finalScope = $finalScopeResult->getScope();
1613: $scopesWithIterableValueType = [];
1614:
1615: $keyVarExpr = null;
1616: $originalKeyVarExpr = null;
1617: if ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)) {
1618: $keyVarExpr = $stmt->keyVar;
1619: $originalKeyVarExpr = new OriginalForeachKeyExpr($stmt->keyVar->name);
1620: }
1621: $originalValueExpr = null;
1622: if ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)) {
1623: $originalValueExpr = new OriginalForeachValueExpr($stmt->valueVar->name);
1624: }
1625:
1626: // With a key variable, each iteration is tracked through the original key
1627: // expression and the narrowed element is projected onto the array dim fetch.
1628: // Without one (`foreach ($a as $v)`) we instead track the original value
1629: // expression and rewrite the array value type directly from the value var.
1630: $trackingExpr = $originalKeyVarExpr ?? $originalValueExpr;
1631:
1632: $continueExitPointHasUnoriginalKeyType = false;
1633: if ($trackingExpr !== null) {
1634: if ($finalScope->hasExpressionType($trackingExpr)->yes()) {
1635: $scopesWithIterableValueType[] = $finalScope;
1636: } else {
1637: $continueExitPointHasUnoriginalKeyType = true;
1638: }
1639: }
1640:
1641: foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1642: $continueScope = $continueExitPoint->getScope();
1643: $finalScope = $continueScope->mergeWith($finalScope);
1644: if ($trackingExpr === null || !$continueScope->hasExpressionType($trackingExpr)->yes()) {
1645: $continueExitPointHasUnoriginalKeyType = true;
1646: continue;
1647: }
1648: $scopesWithIterableValueType[] = $continueScope;
1649: }
1650: $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class);
1651: foreach ($breakExitPoints as $breakExitPoint) {
1652: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1653: }
1654:
1655: if ($unrolledEndScope !== null) {
1656: $finalScope = $unrolledEndScope;
1657: }
1658:
1659: $exprType = $scope->getType($stmt->expr);
1660: $hasExpr = $scope->hasExpressionType($stmt->expr);
1661: if (
1662: count($breakExitPoints) === 0
1663: && count($scopesWithIterableValueType) > 0
1664: && !$continueExitPointHasUnoriginalKeyType
1665: && ($keyVarExpr !== null || $originalValueExpr !== null)
1666: && (!$hasExpr->no() || !$stmt->expr instanceof Variable)
1667: && $exprType->isArray()->yes()
1668: && $exprType->isConstantArray()->no()
1669: ) {
1670: $arrayDimFetchLoopTypes = [];
1671: $arrayDimFetchLoopNativeTypes = [];
1672: $keyLoopTypes = [];
1673: $keyLoopNativeTypes = [];
1674: foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) {
1675: if ($keyVarExpr !== null) {
1676: $arrayExprDimFetch = new ArrayDimFetch($stmt->expr, $keyVarExpr);
1677: $dimFetchType = $scopeWithIterableValueType->getType($arrayExprDimFetch);
1678: $dimFetchNativeType = $scopeWithIterableValueType->getNativeType($arrayExprDimFetch);
1679: // Condition-based narrowings like `is_string($type)` apply to the value
1680: // variable but not automatically to the array dim fetch, even though the
1681: // two describe the same element for a given iteration. If the value var
1682: // hasn't been reassigned (OriginalForeachValueExpr still tracked) we use
1683: // the narrowed value-var type in place of the broader dim fetch type so
1684: // the loop's final array rewrite below picks up the sharper element type.
1685: if ($originalValueExpr !== null && $scopeWithIterableValueType->hasExpressionType($originalValueExpr)->yes()) {
1686: // read the loop value variable's narrowed type directly by name -
1687: // it is an assigned (not processExprNode-processed) variable
1688: // ($originalValueExpr !== null implies a string-named Variable)
1689: $valueVarType = $scopeWithIterableValueType->getVariableType($stmt->valueVar->name);
1690: if ($dimFetchType->isSuperTypeOf($valueVarType)->yes()) {
1691: $dimFetchType = $valueVarType;
1692: }
1693: $valueVarNativeType = $scopeWithIterableValueType->getNativeType($stmt->valueVar);
1694: if ($dimFetchNativeType->isSuperTypeOf($valueVarNativeType)->yes()) {
1695: $dimFetchNativeType = $valueVarNativeType;
1696: }
1697: }
1698: $keyLoopTypes[] = $scopeWithIterableValueType->getType($keyVarExpr);
1699: $keyLoopNativeTypes[] = $scopeWithIterableValueType->getNativeType($keyVarExpr);
1700: } else {
1701: // No key variable: the narrowed value var is the array element type directly.
1702: $dimFetchType = $scopeWithIterableValueType->getVariableType($stmt->valueVar->name);
1703: $dimFetchNativeType = $scopeWithIterableValueType->getNativeType($stmt->valueVar);
1704: }
1705: $arrayDimFetchLoopTypes[] = $dimFetchType;
1706: $arrayDimFetchLoopNativeTypes[] = $dimFetchNativeType;
1707: }
1708:
1709: $arrayDimFetchLoopType = TypeCombinator::union(...$arrayDimFetchLoopTypes);
1710: $arrayDimFetchLoopNativeType = TypeCombinator::union(...$arrayDimFetchLoopNativeTypes);
1711:
1712: $valueTypeChanged = !$arrayDimFetchLoopType->equals($exprType->getIterableValueType());
1713: $keyTypeChanged = false;
1714: $keyLoopType = $exprType->getIterableKeyType();
1715: $keyLoopNativeType = $scope->getNativeType($stmt->expr)->getIterableKeyType();
1716: if ($keyVarExpr !== null) {
1717: $keyLoopType = TypeCombinator::union(...$keyLoopTypes);
1718: $keyLoopNativeType = TypeCombinator::union(...$keyLoopNativeTypes);
1719: $keyTypeChanged = !$keyLoopType->equals($exprType->getIterableKeyType());
1720: }
1721:
1722: if ($valueTypeChanged || $keyTypeChanged) {
1723: $newExprType = $exprType;
1724: if ($valueTypeChanged) {
1725: $newExprType = $newExprType->mapValueType(static fn (Type $type): Type => $arrayDimFetchLoopType);
1726: }
1727: if ($keyTypeChanged) {
1728: $newExprType = $newExprType->mapKeyType(static fn (Type $type): Type => $keyLoopType);
1729: }
1730:
1731: $nativeExprType = $scope->getNativeType($stmt->expr);
1732: $newExprNativeType = $nativeExprType;
1733: if ($valueTypeChanged) {
1734: $newExprNativeType = $newExprNativeType->mapValueType(static fn (Type $type): Type => $arrayDimFetchLoopNativeType);
1735: }
1736: if ($keyTypeChanged) {
1737: $newExprNativeType = $newExprNativeType->mapKeyType(static fn (Type $type): Type => $keyLoopNativeType);
1738: }
1739:
1740: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
1741: $finalScope = $finalScope->assignVariable(
1742: $stmt->expr->name,
1743: $newExprType,
1744: $newExprNativeType,
1745: $hasExpr,
1746: );
1747: } else {
1748: $finalScope = $finalScope->assignExpression(
1749: $stmt->expr,
1750: $newExprType,
1751: $newExprNativeType,
1752: );
1753: }
1754: }
1755: }
1756:
1757: $isIterableAtLeastOnce = $exprType->isIterableAtLeastOnce();
1758: if ($isIterableAtLeastOnce->maybe() || $exprType->isIterable()->no()) {
1759: $finalScope = $finalScope->mergeWith($scope->filterByTruthyValue(new BooleanOr(
1760: new BinaryOp\Identical(
1761: $stmt->expr,
1762: new Array_([]),
1763: ),
1764: new FuncCall(new Name\FullyQualified('is_object'), [
1765: new Arg($stmt->expr),
1766: ]),
1767: )));
1768: } elseif ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) {
1769: $finalScope = $scope;
1770: } elseif (!$this->polluteScopeWithAlwaysIterableForeach) {
1771: $finalScope = $scope->processAlwaysIterableForeachScopeWithoutPollute($finalScope);
1772: // get types from finalScope, but don't create new variables
1773: }
1774:
1775: if (!$isIterableAtLeastOnce->no()) {
1776: $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints());
1777: $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints());
1778: }
1779: $traversableThrowPoint = $this->getTraversableForeachThrowPoint($scope, $stmt->expr);
1780: if ($traversableThrowPoint !== null) {
1781: $throwPoints[] = $traversableThrowPoint;
1782: }
1783: if ($context->isTopLevel() && $stmt->byRef) {
1784: $finalScope = $finalScope->assignExpression(new ForeachValueByRefExpr($stmt->valueVar), new MixedType(), new MixedType());
1785: }
1786:
1787: return new InternalStatementResult(
1788: $finalScope,
1789: $finalScopeResult->hasYield() || $condResult->hasYield(),
1790: $isIterableAtLeastOnce->yes() && $finalScopeResult->isAlwaysTerminating(),
1791: $finalScopeResult->getExitPointsForOuterLoop(),
1792: $throwPoints,
1793: $impurePoints,
1794: );
1795: } elseif ($stmt instanceof While_) {
1796: $originalStorage = $storage;
1797: $storage = $originalStorage->duplicate();
1798: $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep());
1799: $beforeCondBooleanType = ($this->treatPhpDocTypesAsCertain ? $condResult->getType() : $condResult->getNativeType())->toBoolean();
1800: $condScope = $condResult->getFalseyScope();
1801: if (!$context->isTopLevel() && $beforeCondBooleanType->isFalse()->yes()) {
1802: if (!$this->polluteScopeWithLoopInitialAssignments) {
1803: $scope = $condScope->mergeWith($scope);
1804: }
1805:
1806: return new InternalStatementResult(
1807: $scope,
1808: $condResult->hasYield(),
1809: false,
1810: [],
1811: $condResult->getThrowPoints(),
1812: $condResult->getImpurePoints(),
1813: );
1814: }
1815: $bodyScope = $condResult->getTruthyScope();
1816:
1817: if ($context->isTopLevel()) {
1818: $count = 0;
1819: $prevEntryScope = null;
1820: do {
1821: $prevScope = $bodyScope;
1822: $bodyScope = $bodyScope->mergeWith($scope);
1823: if ($prevEntryScope !== null && $bodyScope->equals($prevEntryScope)) {
1824: // walking is deterministic in the entry scope - an unchanged entry
1825: // reproduces the previous pass's exit, so the verification walk is
1826: // skipped
1827: $bodyScope = $prevScope;
1828: break;
1829: }
1830: $prevEntryScope = $bodyScope;
1831: $storage = $originalStorage->duplicate();
1832: $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep())->getTruthyScope();
1833: $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints();
1834: $bodyScope = $bodyScopeResult->getScope();
1835: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1836: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1837: }
1838: if ($bodyScope->equals($prevScope)) {
1839: break;
1840: }
1841:
1842: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
1843: $bodyScope = $prevScope->generalizeWith($bodyScope);
1844: }
1845: $count++;
1846: } while ($count < self::LOOP_SCOPE_ITERATIONS);
1847: }
1848:
1849: $bodyScope = $bodyScope->mergeWith($scope);
1850: $bodyScopeMaybeRan = $bodyScope;
1851: $storage = $originalStorage;
1852: $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope();
1853: $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints();
1854: $finalScope = $finalScopeResult->getScope()->filterByFalseyValue($stmt->cond);
1855:
1856: $alwaysIterates = false;
1857: $neverIterates = false;
1858: if ($context->isTopLevel()) {
1859: $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScopeMaybeRan->getType($stmt->cond) : $bodyScopeMaybeRan->getNativeType($stmt->cond))->toBoolean();
1860: $alwaysIterates = $condBooleanType->isTrue()->yes();
1861: $neverIterates = $condBooleanType->isFalse()->yes();
1862: }
1863: if (!$alwaysIterates) {
1864: foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1865: $finalScope = $finalScope->mergeWith($continueExitPoint->getScope());
1866: }
1867: }
1868:
1869: $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class);
1870: if (count($breakExitPoints) > 0) {
1871: $breakScope = $alwaysIterates ? null : $finalScope;
1872: foreach ($breakExitPoints as $breakExitPoint) {
1873: $breakScope = $breakScope === null ? $breakExitPoint->getScope() : $breakScope->mergeWith($breakExitPoint->getScope());
1874: }
1875: $finalScope = $breakScope;
1876: }
1877:
1878: $isIterableAtLeastOnce = $beforeCondBooleanType->isTrue()->yes();
1879: $this->callNodeCallback($nodeCallback, new BreaklessWhileLoopNode($stmt, $finalScopeResult->toPublic()->getExitPoints(), $finalScopeResult->hasYield()), $bodyScopeMaybeRan, $storage);
1880:
1881: if ($alwaysIterates) {
1882: $isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0;
1883: } elseif ($isIterableAtLeastOnce) {
1884: $isAlwaysTerminating = $finalScopeResult->isAlwaysTerminating();
1885: } else {
1886: $isAlwaysTerminating = false;
1887: }
1888: if (!$isIterableAtLeastOnce) {
1889: if (!$this->polluteScopeWithLoopInitialAssignments) {
1890: $condScope = $condScope->mergeWith($scope);
1891: }
1892: $finalScope = $finalScope->mergeWith($condScope);
1893: }
1894:
1895: $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints();
1896: $impurePoints = $condResult->getImpurePoints();
1897: if (!$neverIterates) {
1898: $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints());
1899: $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints());
1900: }
1901:
1902: return new InternalStatementResult(
1903: $finalScope,
1904: $finalScopeResult->hasYield() || $condResult->hasYield(),
1905: $isAlwaysTerminating,
1906: $finalScopeResult->getExitPointsForOuterLoop(),
1907: $throwPoints,
1908: $impurePoints,
1909: );
1910: } elseif ($stmt instanceof Do_) {
1911: $finalScope = null;
1912: $bodyScope = $scope;
1913: $count = 0;
1914: $hasYield = false;
1915: $throwPoints = [];
1916: $impurePoints = [];
1917: $originalStorage = $storage;
1918:
1919: if ($context->isTopLevel()) {
1920: $prevEntryScope = null;
1921: do {
1922: $prevScope = $bodyScope;
1923: $bodyScope = $bodyScope->mergeWith($scope);
1924: if ($prevEntryScope !== null && $bodyScope->equals($prevEntryScope)) {
1925: // walking is deterministic in the entry scope - an unchanged entry
1926: // reproduces the previous pass's exit, so the verification walk is
1927: // skipped (and repeats only idempotent merges into the final scope)
1928: $bodyScope = $prevScope;
1929: break;
1930: }
1931: $prevEntryScope = $bodyScope;
1932: $storage = $originalStorage->duplicate();
1933: $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints();
1934: $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating();
1935: $bodyScope = $bodyScopeResult->getScope();
1936: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1937: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1938: }
1939: $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope);
1940: foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
1941: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1942: }
1943: $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep())->getTruthyScope();
1944: if ($bodyScope->equals($prevScope)) {
1945: break;
1946: }
1947:
1948: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
1949: $bodyScope = $prevScope->generalizeWith($bodyScope);
1950: }
1951: $count++;
1952: } while ($count < self::LOOP_SCOPE_ITERATIONS);
1953:
1954: $bodyScope = $bodyScope->mergeWith($scope);
1955: }
1956:
1957: $storage = $originalStorage;
1958: $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints();
1959: $bodyScope = $bodyScopeResult->getScope();
1960: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1961: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1962: }
1963:
1964: $alwaysIterates = false;
1965: if ($context->isTopLevel()) {
1966: $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScope->getType($stmt->cond) : $bodyScope->getNativeType($stmt->cond))->toBoolean();
1967: $alwaysIterates = $condBooleanType->isTrue()->yes();
1968: }
1969:
1970: $this->callNodeCallback($nodeCallback, new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->toPublic()->getExitPoints(), $bodyScopeResult->hasYield()), $bodyScope, $storage);
1971:
1972: if ($alwaysIterates) {
1973: $alwaysTerminating = count($bodyScopeResult->getExitPointsByType(Break_::class)) === 0;
1974: } else {
1975: $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating();
1976: }
1977: $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope);
1978: if ($finalScope === null) {
1979: $finalScope = $scope;
1980: }
1981: if (!$alwaysTerminating) {
1982: $condResult = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep());
1983: $hasYield = $condResult->hasYield();
1984: $throwPoints = $condResult->getThrowPoints();
1985: $impurePoints = $condResult->getImpurePoints();
1986: $finalScope = $condResult->getFalseyScope();
1987: } else {
1988: $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep());
1989: }
1990:
1991: $breakExitPoints = $bodyScopeResult->getExitPointsByType(Break_::class);
1992: if (count($breakExitPoints) > 0) {
1993: $breakScope = $alwaysIterates ? null : $finalScope;
1994: foreach ($breakExitPoints as $breakExitPoint) {
1995: $breakScope = $breakScope === null ? $breakExitPoint->getScope() : $breakScope->mergeWith($breakExitPoint->getScope());
1996: }
1997: $finalScope = $breakScope;
1998: }
1999:
2000: return new InternalStatementResult(
2001: $finalScope,
2002: $bodyScopeResult->hasYield() || $hasYield,
2003: $alwaysTerminating,
2004: $bodyScopeResult->getExitPointsForOuterLoop(),
2005: array_merge($throwPoints, $bodyScopeResult->getThrowPoints()),
2006: array_merge($impurePoints, $bodyScopeResult->getImpurePoints()),
2007: );
2008: } elseif ($stmt instanceof For_) {
2009: $initScope = $scope;
2010: $hasYield = false;
2011: $throwPoints = [];
2012: $impurePoints = [];
2013: foreach ($stmt->init as $initExpr) {
2014: $initResult = $this->processExprNode($stmt, $initExpr, $initScope, $storage, $nodeCallback, ExpressionContext::createTopLevel());
2015: $initScope = $initResult->getScope();
2016: $hasYield = $hasYield || $initResult->hasYield();
2017: $throwPoints = array_merge($throwPoints, $initResult->getThrowPoints());
2018: $impurePoints = array_merge($impurePoints, $initResult->getImpurePoints());
2019: }
2020:
2021: $originalStorage = $storage;
2022:
2023: $bodyScope = $initScope;
2024: $isIterableAtLeastOnce = TrinaryLogic::createYes();
2025: $lastCondExpr = array_last($stmt->cond);
2026: if (count($stmt->cond) > 0) {
2027: $storage = $originalStorage->duplicate();
2028:
2029: foreach ($stmt->cond as $condExpr) {
2030: $condResult = $this->processExprNode($stmt, $condExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep());
2031: $initScope = $condResult->getScope();
2032:
2033: // only the last condition expression is relevant whether the loop continues
2034: // see https://www.php.net/manual/en/control-structures.for.php
2035: if ($condExpr === $lastCondExpr) {
2036: $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResult->getType() : $condResult->getNativeType())->toBoolean();
2037: $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthiness->isTrue());
2038: }
2039:
2040: $hasYield = $hasYield || $condResult->hasYield();
2041: $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints());
2042: $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints());
2043: $bodyScope = $condResult->getTruthyScope();
2044: }
2045: }
2046:
2047: if ($context->isTopLevel()) {
2048: $count = 0;
2049: $prevEntryScope = null;
2050: do {
2051: $prevScope = $bodyScope;
2052: $storage = $originalStorage->duplicate();
2053: $bodyScope = $bodyScope->mergeWith($initScope);
2054: if ($prevEntryScope !== null && $bodyScope->equals($prevEntryScope)) {
2055: // walking is deterministic in the entry scope - an unchanged entry
2056: // reproduces the previous pass's exit, so the verification walk is
2057: // skipped
2058: $bodyScope = $prevScope;
2059: break;
2060: }
2061: $prevEntryScope = $bodyScope;
2062: if ($lastCondExpr !== null) {
2063: $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep())->getTruthyScope();
2064: }
2065: $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints();
2066: $bodyScope = $bodyScopeResult->getScope();
2067: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
2068: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
2069: }
2070:
2071: foreach ($stmt->loop as $loopExpr) {
2072: $exprResult = $this->processExprNode($stmt, $loopExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createTopLevel());
2073: $bodyScope = $exprResult->getScope();
2074: $hasYield = $hasYield || $exprResult->hasYield();
2075: $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
2076: $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints());
2077: }
2078:
2079: if ($bodyScope->equals($prevScope)) {
2080: break;
2081: }
2082:
2083: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
2084: $bodyScope = $prevScope->generalizeWith($bodyScope);
2085: }
2086: $count++;
2087: } while ($count < self::LOOP_SCOPE_ITERATIONS);
2088: }
2089:
2090: $storage = $originalStorage;
2091: $bodyScope = $bodyScope->mergeWith($initScope);
2092:
2093: $alwaysIterates = TrinaryLogic::createFromBoolean($context->isTopLevel());
2094: if ($lastCondExpr !== null) {
2095: $alwaysIterates = $alwaysIterates->and($bodyScope->getType($lastCondExpr)->toBoolean()->isTrue());
2096: $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope();
2097: $bodyScope = $this->inferForLoopExpressions($stmt, $lastCondExpr, $bodyScope);
2098: }
2099:
2100: $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints();
2101: $finalScope = $finalScopeResult->getScope();
2102: foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
2103: $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
2104: }
2105:
2106: $loopScope = $finalScope;
2107: foreach ($stmt->loop as $loopExpr) {
2108: $loopScope = $this->processExprNode($stmt, $loopExpr, $loopScope, $storage, $nodeCallback, ExpressionContext::createTopLevel())->getScope();
2109: }
2110: $finalScope = $finalScope->generalizeWith($loopScope);
2111:
2112: if ($lastCondExpr !== null) {
2113: $finalScope = $finalScope->filterByFalseyValue($lastCondExpr);
2114: }
2115:
2116: $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class);
2117: if (count($breakExitPoints) > 0) {
2118: $breakScope = $alwaysIterates->yes() ? null : $finalScope;
2119: foreach ($breakExitPoints as $breakExitPoint) {
2120: $breakScope = $breakScope === null ? $breakExitPoint->getScope() : $breakScope->mergeWith($breakExitPoint->getScope());
2121: }
2122: $finalScope = $breakScope;
2123: }
2124:
2125: if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) {
2126: if ($this->polluteScopeWithLoopInitialAssignments) {
2127: $finalScope = $initScope;
2128: } else {
2129: $finalScope = $scope;
2130: }
2131:
2132: } elseif ($isIterableAtLeastOnce->maybe()) {
2133: if ($this->polluteScopeWithLoopInitialAssignments) {
2134: $finalScope = $finalScope->mergeWith($initScope);
2135: } else {
2136: $finalScope = $finalScope->mergeWith($scope);
2137: }
2138: } else {
2139: if (!$this->polluteScopeWithLoopInitialAssignments) {
2140: $finalScope = $finalScope->mergeWith($scope);
2141: }
2142: }
2143:
2144: if ($alwaysIterates->yes()) {
2145: $isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0;
2146: } elseif ($isIterableAtLeastOnce->yes()) {
2147: $isAlwaysTerminating = $finalScopeResult->isAlwaysTerminating();
2148: } else {
2149: $isAlwaysTerminating = false;
2150: }
2151:
2152: return new InternalStatementResult(
2153: $finalScope,
2154: $finalScopeResult->hasYield() || $hasYield,
2155: $isAlwaysTerminating,
2156: $finalScopeResult->getExitPointsForOuterLoop(),
2157: array_merge($throwPoints, $finalScopeResult->getThrowPoints()),
2158: array_merge($impurePoints, $finalScopeResult->getImpurePoints()),
2159: );
2160: } elseif ($stmt instanceof Switch_) {
2161: $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2162: $scope = $condResult->getScope();
2163: $scopeForBranches = $scope;
2164: $finalScope = null;
2165: $prevScope = null;
2166: $hasDefaultCase = false;
2167: $alwaysTerminating = true;
2168: $hasYield = $condResult->hasYield();
2169: $exitPointsForOuterLoop = [];
2170: $throwPoints = $condResult->getThrowPoints();
2171: $impurePoints = $condResult->getImpurePoints();
2172: $fullCondExpr = null;
2173: $switchConditionArms = [];
2174: $lastNonDefaultCaseKey = null;
2175: foreach ($stmt->cases as $caseKey => $caseNode) {
2176: if ($caseNode->cond === null) {
2177: continue;
2178: }
2179:
2180: $lastNonDefaultCaseKey = $caseKey;
2181: }
2182: foreach ($stmt->cases as $caseKey => $caseNode) {
2183: if ($caseNode->cond !== null) {
2184: $condExpr = new BinaryOp\Equal($stmt->cond, $caseNode->cond);
2185: $fullCondExpr = $fullCondExpr === null ? $condExpr : new BooleanOr($fullCondExpr, $condExpr);
2186: $caseResult = $this->processExprNode($stmt, $caseNode->cond, $scopeForBranches, $storage, $nodeCallback, ExpressionContext::createDeep());
2187: $scopeForBranches = $caseResult->getScope();
2188: $hasYield = $hasYield || $caseResult->hasYield();
2189: $throwPoints = array_merge($throwPoints, $caseResult->getThrowPoints());
2190: $impurePoints = array_merge($impurePoints, $caseResult->getImpurePoints());
2191: $switchConditionArms[] = new SwitchConditionArm(
2192: $caseNode->cond,
2193: $scopeForBranches,
2194: $caseNode->cond->getStartLine(),
2195: $caseKey === $lastNonDefaultCaseKey,
2196: );
2197: $branchScope = $caseResult->getScope()->filterByTruthyValue($condExpr);
2198: } else {
2199: $hasDefaultCase = true;
2200: $fullCondExpr = null;
2201: $branchScope = $scopeForBranches;
2202: }
2203:
2204: $branchScope = $branchScope->mergeWith($prevScope);
2205: $branchScopeResult = $this->processStmtNodesInternal($caseNode, $caseNode->stmts, $branchScope, $storage, $nodeCallback, $context);
2206: $branchScope = $branchScopeResult->getScope();
2207: $branchFinalScopeResult = $branchScopeResult->filterOutLoopExitPoints();
2208: $hasYield = $hasYield || $branchFinalScopeResult->hasYield();
2209: foreach ($branchScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
2210: $alwaysTerminating = false;
2211: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
2212: }
2213: foreach ($branchScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
2214: $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
2215: }
2216: $exitPointsForOuterLoop = array_merge($exitPointsForOuterLoop, $branchFinalScopeResult->getExitPointsForOuterLoop());
2217: $throwPoints = array_merge($throwPoints, $branchFinalScopeResult->getThrowPoints());
2218: $impurePoints = array_merge($impurePoints, $branchFinalScopeResult->getImpurePoints());
2219: if ($branchScopeResult->isAlwaysTerminating()) {
2220: $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating();
2221: $prevScope = null;
2222: if (isset($fullCondExpr)) {
2223: $scopeForBranches = $scopeForBranches->filterByFalseyValue($fullCondExpr);
2224: $fullCondExpr = null;
2225: }
2226: if (!$branchFinalScopeResult->isAlwaysTerminating()) {
2227: $finalScope = $branchScope->mergeWith($finalScope);
2228: }
2229: } else {
2230: $prevScope = $branchScope;
2231: }
2232: }
2233:
2234: if ($switchConditionArms !== []) {
2235: $this->callNodeCallback($nodeCallback, new SwitchConditionNode($stmt->cond, $switchConditionArms, $stmt), $scope, $storage);
2236: }
2237:
2238: $exhaustive = $scopeForBranches->getType($stmt->cond) instanceof NeverType;
2239:
2240: if (!$hasDefaultCase && !$exhaustive) {
2241: $alwaysTerminating = false;
2242: }
2243:
2244: if ($prevScope !== null && isset($branchFinalScopeResult)) {
2245: $finalScope = $prevScope->mergeWith($finalScope);
2246: $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating();
2247: }
2248:
2249: if ((!$hasDefaultCase && !$exhaustive) || $finalScope === null) {
2250: $finalScope = $scopeForBranches->mergeWith($finalScope);
2251: }
2252:
2253: return new InternalStatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPointsForOuterLoop, $throwPoints, $impurePoints);
2254: } elseif ($stmt instanceof TryCatch) {
2255: $branchScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context);
2256: $branchScope = $branchScopeResult->getScope();
2257: $finalScope = $branchScopeResult->isAlwaysTerminating() ? null : $branchScope;
2258:
2259: $exitPoints = [];
2260: $finallyExitPoints = [];
2261: $alwaysTerminating = $branchScopeResult->isAlwaysTerminating();
2262: $hasYield = $branchScopeResult->hasYield();
2263:
2264: if ($stmt->finally !== null) {
2265: $finallyScope = $branchScope;
2266: } else {
2267: $finallyScope = null;
2268: }
2269: foreach ($branchScopeResult->getExitPoints() as $exitPoint) {
2270: $finallyExitPoints[] = $exitPoint->toPublic();
2271: if ($exitPoint->getStatement() instanceof Node\Stmt\Expression && $exitPoint->getStatement()->expr instanceof Expr\Throw_) {
2272: continue;
2273: }
2274: if ($finallyScope !== null) {
2275: $finallyScope = $finallyScope->mergeWith($exitPoint->getScope());
2276: }
2277: $exitPoints[] = $exitPoint;
2278: }
2279:
2280: $throwPoints = $branchScopeResult->getThrowPoints();
2281: $impurePoints = $branchScopeResult->getImpurePoints();
2282: $throwPointsForLater = [];
2283: $pastCatchTypes = new NeverType();
2284:
2285: foreach ($stmt->catches as $catchNode) {
2286: $this->callNodeCallback($nodeCallback, $catchNode, $scope, $storage);
2287:
2288: $originalCatchTypes = [];
2289: $catchTypes = [];
2290: foreach ($catchNode->types as $catchNodeType) {
2291: $catchType = new ObjectType($catchNodeType->toString());
2292: $originalCatchTypes[] = $catchType;
2293: $catchTypes[] = TypeCombinator::remove($catchType, $pastCatchTypes);
2294: }
2295:
2296: $originalCatchType = TypeCombinator::union(...$originalCatchTypes);
2297: $catchType = TypeCombinator::union(...$catchTypes);
2298: $pastCatchTypes = TypeCombinator::union($pastCatchTypes, $originalCatchType);
2299:
2300: $matchingThrowPoints = [];
2301: $matchingCatchTypes = array_fill_keys(array_keys($originalCatchTypes), false);
2302:
2303: // throwable matches all
2304: foreach ($originalCatchTypes as $catchTypeIndex => $catchTypeItem) {
2305: if (!$catchTypeItem->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) {
2306: continue;
2307: }
2308:
2309: foreach ($throwPoints as $throwPointIndex => $throwPoint) {
2310: $matchingThrowPoints[$throwPointIndex] = $throwPoint;
2311: $matchingCatchTypes[$catchTypeIndex] = true;
2312: }
2313: }
2314:
2315: // explicit only
2316: $onlyExplicitIsThrow = true;
2317: if (count($matchingThrowPoints) === 0) {
2318: foreach ($throwPoints as $throwPointIndex => $throwPoint) {
2319: foreach ($catchTypes as $catchTypeIndex => $catchTypeItem) {
2320: if ($catchTypeItem->isSuperTypeOf($throwPoint->getType())->no()) {
2321: continue;
2322: }
2323:
2324: $matchingCatchTypes[$catchTypeIndex] = true;
2325: if (!$throwPoint->isExplicit()) {
2326: continue;
2327: }
2328: $throwNode = $throwPoint->getNode();
2329: if (
2330: !$throwNode instanceof Expr\Throw_
2331: && !($throwNode instanceof Node\Stmt\Expression && $throwNode->expr instanceof Expr\Throw_)
2332: ) {
2333: $onlyExplicitIsThrow = false;
2334: }
2335:
2336: $matchingThrowPoints[$throwPointIndex] = $throwPoint;
2337: }
2338: }
2339: }
2340:
2341: // implicit only
2342: if (count($matchingThrowPoints) === 0 || $onlyExplicitIsThrow) {
2343: foreach ($throwPoints as $throwPointIndex => $throwPoint) {
2344: if ($throwPoint->isExplicit()) {
2345: continue;
2346: }
2347:
2348: foreach ($catchTypes as $catchTypeItem) {
2349: if ($catchTypeItem->isSuperTypeOf($throwPoint->getType())->no()) {
2350: continue;
2351: }
2352:
2353: $matchingThrowPoints[$throwPointIndex] = $throwPoint;
2354: }
2355: }
2356: }
2357:
2358: // include previously removed throw points
2359: if (count($matchingThrowPoints) === 0) {
2360: if ($originalCatchType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) {
2361: foreach ($branchScopeResult->getThrowPoints() as $originalThrowPoint) {
2362: if (!$originalThrowPoint->canContainAnyThrowable()) {
2363: continue;
2364: }
2365:
2366: $matchingThrowPoints[] = $originalThrowPoint;
2367: $matchingCatchTypes = array_fill_keys(array_keys($originalCatchTypes), true);
2368: }
2369: }
2370: }
2371:
2372: // emit error
2373: foreach ($matchingCatchTypes as $catchTypeIndex => $matched) {
2374: if ($matched) {
2375: continue;
2376: }
2377: $this->callNodeCallback($nodeCallback, new CatchWithUnthrownExceptionNode($catchNode, $catchTypes[$catchTypeIndex], $originalCatchTypes[$catchTypeIndex]), $scope, $storage);
2378: }
2379:
2380: if (count($matchingThrowPoints) === 0) {
2381: continue;
2382: }
2383:
2384: // recompute throw points
2385: $newThrowPoints = [];
2386: foreach ($throwPoints as $throwPoint) {
2387: $newThrowPoint = $throwPoint->subtractCatchType($originalCatchType);
2388:
2389: if ($newThrowPoint->getType() instanceof NeverType) {
2390: continue;
2391: }
2392:
2393: $newThrowPoints[] = $newThrowPoint;
2394: }
2395: $throwPoints = $newThrowPoints;
2396:
2397: $catchScope = null;
2398: foreach ($matchingThrowPoints as $matchingThrowPoint) {
2399: if ($catchScope === null) {
2400: $catchScope = $matchingThrowPoint->getScope();
2401: } else {
2402: $catchScope = $catchScope->mergeWith($matchingThrowPoint->getScope());
2403: }
2404: }
2405:
2406: $variableName = null;
2407: if ($catchNode->var !== null) {
2408: if (!is_string($catchNode->var->name)) {
2409: throw new ShouldNotHappenException();
2410: }
2411:
2412: $variableName = $catchNode->var->name;
2413: $this->callNodeCallback($nodeCallback, new VariableAssignNode($catchNode->var, new TypeExpr($catchType)), $scope, $storage);
2414: }
2415:
2416: $catchScopeResult = $this->processStmtNodesInternal($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $storage, $nodeCallback, $context);
2417: $catchScopeForFinally = $catchScopeResult->getScope();
2418:
2419: $finalScope = $catchScopeResult->isAlwaysTerminating() ? $finalScope : $catchScopeResult->getScope()->mergeWith($finalScope);
2420: $alwaysTerminating = $alwaysTerminating && $catchScopeResult->isAlwaysTerminating();
2421: $hasYield = $hasYield || $catchScopeResult->hasYield();
2422: $catchThrowPoints = $catchScopeResult->getThrowPoints();
2423: $impurePoints = array_merge($impurePoints, $catchScopeResult->getImpurePoints());
2424: $throwPointsForLater = array_merge($throwPointsForLater, $catchThrowPoints);
2425:
2426: if ($finallyScope !== null) {
2427: $finallyScope = $finallyScope->mergeWith($catchScopeForFinally);
2428: }
2429: foreach ($catchScopeResult->getExitPoints() as $exitPoint) {
2430: $finallyExitPoints[] = $exitPoint->toPublic();
2431: if ($exitPoint->getStatement() instanceof Node\Stmt\Expression && $exitPoint->getStatement()->expr instanceof Expr\Throw_) {
2432: continue;
2433: }
2434: if ($finallyScope !== null) {
2435: $finallyScope = $finallyScope->mergeWith($exitPoint->getScope());
2436: }
2437: $exitPoints[] = $exitPoint;
2438: }
2439:
2440: foreach ($catchThrowPoints as $catchThrowPoint) {
2441: if ($finallyScope === null) {
2442: continue;
2443: }
2444: $finallyScope = $finallyScope->mergeWith($catchThrowPoint->getScope());
2445: }
2446: }
2447:
2448: if ($finalScope === null) {
2449: $finalScope = $scope;
2450: }
2451:
2452: foreach ($throwPoints as $throwPoint) {
2453: if ($finallyScope === null) {
2454: continue;
2455: }
2456: $finallyScope = $finallyScope->mergeWith($throwPoint->getScope());
2457: }
2458:
2459: if ($finallyScope !== null) {
2460: $originalFinallyScope = $finallyScope;
2461: $finallyResult = $this->processStmtNodesInternal($stmt->finally, $stmt->finally->stmts, $finallyScope, $storage, $nodeCallback, $context);
2462: $alwaysTerminating = $alwaysTerminating || $finallyResult->isAlwaysTerminating();
2463: $hasYield = $hasYield || $finallyResult->hasYield();
2464: $throwPointsForLater = array_merge($throwPointsForLater, $finallyResult->getThrowPoints());
2465: $impurePoints = array_merge($impurePoints, $finallyResult->getImpurePoints());
2466: $finallyScope = $finallyResult->getScope();
2467: $finalScope = $finallyResult->isAlwaysTerminating() ? $finalScope : $finalScope->processFinallyScope($finallyScope, $originalFinallyScope);
2468: if (count($finallyResult->getExitPoints()) > 0 && $finallyResult->isAlwaysTerminating()) {
2469: $this->callNodeCallback($nodeCallback, new FinallyExitPointsNode(
2470: $finallyResult->toPublic()->getExitPoints(),
2471: $finallyExitPoints,
2472: ), $scope, $storage);
2473: }
2474: $exitPoints = array_merge($exitPoints, $finallyResult->getExitPoints());
2475: }
2476:
2477: return new InternalStatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, array_merge($throwPoints, $throwPointsForLater), $impurePoints);
2478: } elseif ($stmt instanceof Unset_) {
2479: $hasYield = false;
2480: $throwPoints = [];
2481: $impurePoints = [];
2482: foreach ($stmt->vars as $var) {
2483: $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var);
2484: $exprResult = $this->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2485: $scope = $exprResult->getScope();
2486: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
2487: $hasYield = $hasYield || $exprResult->hasYield();
2488: $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
2489: $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints());
2490: if ($var instanceof ArrayDimFetch && $var->dim !== null) {
2491: $varType = $scope->getType($var->var);
2492: if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) {
2493: $throwPoints = array_merge($throwPoints, $this->container->getByType(MethodThrowPointHelper::class)->getThrowPointsForCallOnType(
2494: $scope,
2495: ExpressionContext::createDeep(),
2496: $varType,
2497: new MethodCall(new TypeExpr($varType), 'offsetUnset'),
2498: ));
2499: }
2500:
2501: // wrap the already-processed chain in ExistingArrayDimFetch nodes
2502: // referencing the original sub-expressions - the unset statement's
2503: // own walk already processed them, and the assign target
2504: // preparation prices the chain without re-walking it
2505: $buildExistingChain = static function (Expr $node) use (&$buildExistingChain): Expr {
2506: if (!$node instanceof ArrayDimFetch || $node->dim === null) {
2507: return $node;
2508: }
2509:
2510: return new ExistingArrayDimFetch(
2511: $buildExistingChain($node->var),
2512: $node->dim,
2513: );
2514: };
2515: $scope = $this->processVirtualAssign($scope, $storage, $stmt, $buildExistingChain($var->var), new UnsetOffsetExpr($var->var, $var->dim), $nodeCallback)->getScope();
2516: } elseif ($var instanceof PropertyFetch) {
2517: $scope = $scope->invalidateExpression($var);
2518: $impurePoints[] = new ImpurePoint(
2519: $scope,
2520: $var,
2521: 'propertyUnset',
2522: 'property unset',
2523: true,
2524: );
2525: } else {
2526: $scope = $scope->invalidateExpression($var);
2527: }
2528:
2529: $scope = $scope->invalidateExpression(new ForeachValueByRefExpr($var));
2530: }
2531: } elseif ($stmt instanceof Node\Stmt\Use_) {
2532: $hasYield = false;
2533: $throwPoints = [];
2534: $impurePoints = [];
2535: foreach ($stmt->uses as $use) {
2536: $this->callNodeCallback($nodeCallback, $use, $scope, $storage);
2537: }
2538: } elseif ($stmt instanceof Node\Stmt\Global_) {
2539: $hasYield = false;
2540: $throwPoints = [];
2541: $impurePoints = [
2542: new ImpurePoint(
2543: $scope,
2544: $stmt,
2545: 'global',
2546: 'global variable',
2547: true,
2548: ),
2549: ];
2550: $vars = [];
2551: foreach ($stmt->vars as $var) {
2552: if (!$var instanceof Variable) {
2553: throw new ShouldNotHappenException();
2554: }
2555: $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var);
2556: $varResult = $this->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2557: $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints());
2558: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
2559:
2560: if (!is_string($var->name)) {
2561: continue;
2562: }
2563:
2564: $varType = $this->getGlobalVariableType($var->name);
2565: $scope = $scope->assignVariable($var->name, $varType, $varType, TrinaryLogic::createYes());
2566: $vars[] = $var->name;
2567: }
2568: $scope = $this->processVarAnnotation($scope, $vars, $stmt);
2569: } elseif ($stmt instanceof Static_) {
2570: $hasYield = false;
2571: $throwPoints = [];
2572: $impurePoints = [
2573: new ImpurePoint(
2574: $scope,
2575: $stmt,
2576: 'static',
2577: 'static variable',
2578: true,
2579: ),
2580: ];
2581:
2582: $vars = [];
2583: foreach ($stmt->vars as $var) {
2584: if (!is_string($var->var->name)) {
2585: throw new ShouldNotHappenException();
2586: }
2587:
2588: if ($var->default !== null) {
2589: $defaultExprResult = $this->processExprNode($stmt, $var->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2590: $impurePoints = array_merge($impurePoints, $defaultExprResult->getImpurePoints());
2591: }
2592:
2593: $scope = $scope->enterExpressionAssign($var->var);
2594: $varResult = $this->processExprNode($stmt, $var->var, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2595: $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints());
2596: $scope = $scope->exitExpressionAssign($var->var);
2597:
2598: $scope = $scope->assignVariable($var->var->name, new MixedType(), new MixedType(), TrinaryLogic::createYes());
2599: $vars[] = $var->var->name;
2600: }
2601:
2602: $scope = $this->processVarAnnotation($scope, $vars, $stmt);
2603: } elseif ($stmt instanceof Node\Stmt\Const_) {
2604: $hasYield = false;
2605: $throwPoints = [];
2606: $impurePoints = [];
2607: foreach ($stmt->consts as $const) {
2608: $this->callNodeCallback($nodeCallback, $const, $scope, $storage);
2609: $constResult = $this->processExprNode($stmt, $const->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2610: $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints());
2611: if ($const->namespacedName !== null) {
2612: $constantName = new Name\FullyQualified($const->namespacedName->toString());
2613: } else {
2614: $constantName = new Name\FullyQualified($const->name->toString());
2615: }
2616: $scope = $scope->assignExpression(new ConstFetch($constantName), $constResult->getType(), $constResult->getNativeType());
2617: }
2618: } elseif ($stmt instanceof Node\Stmt\ClassConst) {
2619: $hasYield = false;
2620: $throwPoints = [];
2621: $impurePoints = [];
2622: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
2623: foreach ($stmt->consts as $const) {
2624: $this->callNodeCallback($nodeCallback, $const, $scope, $storage);
2625: $constResult = $this->processExprNode($stmt, $const->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2626: $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints());
2627: if ($scope->getClassReflection() === null) {
2628: throw new ShouldNotHappenException();
2629: }
2630: $scope = $scope->assignExpression(
2631: new Expr\ClassConstFetch(new Name\FullyQualified($scope->getClassReflection()->getName()), $const->name),
2632: $constResult->getType(),
2633: $constResult->getNativeType(),
2634: );
2635: }
2636: } elseif ($stmt instanceof Node\Stmt\EnumCase) {
2637: $hasYield = false;
2638: $throwPoints = [];
2639: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
2640: $impurePoints = [];
2641: if ($stmt->expr !== null) {
2642: $exprResult = $this->processExprNode($stmt, $stmt->expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2643: $impurePoints = $exprResult->getImpurePoints();
2644: }
2645: } elseif ($stmt instanceof InlineHTML) {
2646: $hasYield = false;
2647: $throwPoints = [];
2648: $impurePoints = [
2649: new ImpurePoint($scope, $stmt, 'betweenPhpTags', 'output between PHP opening and closing tags', true),
2650: ];
2651: } elseif ($stmt instanceof Node\Stmt\Block) {
2652: $result = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context);
2653: if ($this->polluteScopeWithBlock) {
2654: return $result;
2655: }
2656:
2657: return new InternalStatementResult(
2658: $scope->mergeWith($result->getScope()),
2659: $result->hasYield(),
2660: $result->isAlwaysTerminating(),
2661: $result->getExitPoints(),
2662: $result->getThrowPoints(),
2663: $result->getImpurePoints(),
2664: $result->getEndStatements(),
2665: );
2666: } elseif ($stmt instanceof Node\Stmt\Nop) {
2667: $hasYield = false;
2668: $throwPoints = $overridingThrowPoints ?? [];
2669: $impurePoints = [];
2670: } elseif ($stmt instanceof Node\Stmt\GroupUse) {
2671: $hasYield = false;
2672: $throwPoints = [];
2673: foreach ($stmt->uses as $use) {
2674: $this->callNodeCallback($nodeCallback, $use, $scope, $storage);
2675: }
2676: $impurePoints = [];
2677: } else {
2678: $hasYield = false;
2679: $throwPoints = $overridingThrowPoints ?? [];
2680: $impurePoints = [];
2681: }
2682:
2683: return new InternalStatementResult($scope, $hasYield, false, [], $throwPoints, $impurePoints);
2684: }
2685:
2686: /**
2687: * @return array{bool, string|null}
2688: */
2689: private function getDeprecatedAttribute(Scope $scope, Node\Stmt\Function_|Node\Stmt\ClassMethod|Node\PropertyHook $stmt): array
2690: {
2691: $initializerExprContext = InitializerExprContext::fromStubParameter(
2692: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
2693: $scope->getFile(),
2694: $stmt,
2695: );
2696: $isDeprecated = false;
2697: $deprecatedDescription = null;
2698: $deprecatedDescriptionType = null;
2699: foreach ($stmt->attrGroups as $attrGroup) {
2700: foreach ($attrGroup->attrs as $attr) {
2701: if ($attr->name->toString() !== 'Deprecated') {
2702: continue;
2703: }
2704: $isDeprecated = true;
2705: $arguments = $attr->args;
2706: foreach ($arguments as $i => $arg) {
2707: $argName = $arg->name;
2708: if ($argName === null) {
2709: if ($i !== 0) {
2710: continue;
2711: }
2712:
2713: $deprecatedDescriptionType = $this->initializerExprTypeResolver->getType($arg->value, $initializerExprContext);
2714: break;
2715: }
2716:
2717: if ($argName->toString() !== 'message') {
2718: continue;
2719: }
2720:
2721: $deprecatedDescriptionType = $this->initializerExprTypeResolver->getType($arg->value, $initializerExprContext);
2722: break;
2723: }
2724: }
2725: }
2726:
2727: if ($deprecatedDescriptionType !== null) {
2728: $constantStrings = $deprecatedDescriptionType->getConstantStrings();
2729: if (count($constantStrings) === 1) {
2730: $deprecatedDescription = $constantStrings[0]->getValue();
2731: }
2732: }
2733:
2734: return [$isDeprecated, $deprecatedDescription];
2735: }
2736:
2737: /**
2738: * @return InternalThrowPoint[]|null
2739: */
2740: private function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $scope): ?array
2741: {
2742: foreach ($statement->getComments() as $comment) {
2743: if (!$comment instanceof Doc) {
2744: continue;
2745: }
2746:
2747: $function = $scope->getFunction();
2748: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
2749: $scope->getFile(),
2750: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
2751: $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
2752: $function !== null ? $function->getName() : null,
2753: $comment->getText(),
2754: );
2755:
2756: $throwsTag = $resolvedPhpDoc->getThrowsTag();
2757: if ($throwsTag !== null) {
2758: $throwsType = $throwsTag->getType();
2759: if ($throwsType->isVoid()->yes()) {
2760: return [];
2761: }
2762:
2763: return [InternalThrowPoint::createExplicit($scope, $throwsType, $statement, false)];
2764: }
2765: }
2766:
2767: return null;
2768: }
2769:
2770: private function getCurrentClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection
2771: {
2772: if (!$this->reflectionProvider->hasClass($className)) {
2773: return $this->createAstClassReflection($stmt, $className, $scope);
2774: }
2775:
2776: $defaultClassReflection = $this->reflectionProvider->getClass($className);
2777: if ($defaultClassReflection->getFileName() !== $scope->getFile()) {
2778: return $this->createAstClassReflection($stmt, $className, $scope);
2779: }
2780:
2781: $startLine = $defaultClassReflection->getNativeReflection()->getStartLine();
2782: if ($startLine !== $stmt->getStartLine()) {
2783: return $this->createAstClassReflection($stmt, $className, $scope);
2784: }
2785:
2786: return $defaultClassReflection;
2787: }
2788:
2789: private function createAstClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection
2790: {
2791: $nodeToReflection = new NodeToReflection();
2792: $betterReflectionClass = $nodeToReflection->__invoke(
2793: $this->reflector,
2794: $stmt,
2795: new LocatedSource(FileReader::read($scope->getFile()), $className, $scope->getFile()),
2796: $scope->getNamespace() !== null ? new Node\Stmt\Namespace_(new Name($scope->getNamespace())) : null,
2797: );
2798: if (!$betterReflectionClass instanceof \PHPStan\BetterReflection\Reflection\ReflectionClass) {
2799: throw new ShouldNotHappenException();
2800: }
2801:
2802: return $this->classReflectionFactory->create(
2803: $betterReflectionClass->getName(),
2804: $betterReflectionClass instanceof ReflectionEnum && PHP_VERSION_ID >= 80000
2805: ? new \PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnum($betterReflectionClass)
2806: : new ReflectionClass($betterReflectionClass),
2807: null,
2808: null,
2809: null,
2810: sprintf('%s:%d', $scope->getFile(), $stmt->getStartLine()),
2811: );
2812: }
2813:
2814: public function lookForSetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope
2815: {
2816: return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr));
2817: }
2818:
2819: public function lookForUnsetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope
2820: {
2821: return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->unsetAllowedUndefinedExpression($expr));
2822: }
2823:
2824: /**
2825: * @param Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback
2826: */
2827: private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope
2828: {
2829: if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) {
2830: $scope = $callback($scope, $expr);
2831: }
2832:
2833: if ($expr instanceof ArrayDimFetch) {
2834: $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback);
2835: } elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch || $expr instanceof Expr\NullsafeMethodCall) {
2836: $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback);
2837: } elseif ($expr instanceof StaticPropertyFetch && $expr->class instanceof Expr) {
2838: $scope = $this->lookForExpressionCallback($scope, $expr->class, $callback);
2839: } elseif ($expr instanceof List_) {
2840: foreach ($expr->items as $item) {
2841: if ($item === null) {
2842: continue;
2843: }
2844:
2845: $scope = $this->lookForExpressionCallback($scope, $item->value, $callback);
2846: }
2847: }
2848:
2849: return $scope;
2850: }
2851:
2852: /**
2853: * @param callable(Node $node, Scope $scope): void $nodeCallback
2854: */
2855: public function processExprNode(
2856: Node\Stmt $stmt,
2857: Expr $expr,
2858: MutatingScope $scope,
2859: ExpressionResultStorage $storage,
2860: callable $nodeCallback,
2861: ExpressionContext $context,
2862: ): ExpressionResult
2863: {
2864: if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) {
2865: if ($expr instanceof FuncCall) {
2866: $newExpr = new FunctionCallableNode($expr->name, $expr);
2867: } elseif ($expr instanceof MethodCall) {
2868: $newExpr = new MethodCallableNode($expr->var, $expr->name, $expr);
2869: } elseif ($expr instanceof StaticCall) {
2870: $newExpr = new StaticMethodCallableNode($expr->class, $expr->name, $expr);
2871: } elseif ($expr instanceof New_ && !$expr->class instanceof Class_) {
2872: $newExpr = new InstantiationCallableNode($expr->class, $expr);
2873: } else {
2874: throw new ShouldNotHappenException();
2875: }
2876:
2877: $newExprResult = $this->processExprNode($stmt, $newExpr, $scope, $storage, $nodeCallback, $context);
2878: $expressionResult = $this->expressionResultFactory->create(
2879: $newExprResult->getScope(),
2880: beforeScope: $scope,
2881: expr: $expr,
2882: hasYield: $newExprResult->hasYield(),
2883: isAlwaysTerminating: $newExprResult->isAlwaysTerminating(),
2884: throwPoints: $newExprResult->getThrowPoints(),
2885: impurePoints: $newExprResult->getImpurePoints(),
2886: );
2887: $this->storeExpressionResult($storage, $expr, $expressionResult);
2888: return $expressionResult;
2889: }
2890:
2891: $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $storage, $context);
2892:
2893: $exprHandler = ExprHandlerRegistry::resolve($expr, $this->container);
2894: if ($exprHandler !== null) {
2895: $expressionResult = $exprHandler->processExpr($this, $stmt, $expr, $scope, $storage, $nodeCallback, $context);
2896: $this->storeExpressionResult($storage, $expr, $expressionResult);
2897: // the call is now processed and stored; emit a virtual node so
2898: // impossible-check rules run on the fully processed call instead of
2899: // asking the scope before the call node itself is processed
2900: if ($expr instanceof FuncCall) {
2901: $this->callNodeCallbackWithExpression($nodeCallback, new FunctionCallExpressionNode($expr), $scope, $storage, $context);
2902: } elseif ($expr instanceof MethodCall) {
2903: $this->callNodeCallbackWithExpression($nodeCallback, new MethodCallExpressionNode($expr), $scope, $storage, $context);
2904: } elseif ($expr instanceof StaticCall) {
2905: $this->callNodeCallbackWithExpression($nodeCallback, new StaticMethodCallExpressionNode($expr), $scope, $storage, $context);
2906: }
2907: return $expressionResult;
2908: }
2909:
2910: $expressionResult = $this->expressionResultFactory->create(
2911: $scope,
2912: beforeScope: $scope,
2913: expr: $expr,
2914: hasYield: false,
2915: isAlwaysTerminating: false,
2916: throwPoints: [],
2917: impurePoints: [],
2918: );
2919: $this->storeExpressionResult($storage, $expr, $expressionResult);
2920:
2921: return $expressionResult;
2922: }
2923:
2924: /**
2925: * @param 'get'|'set' $hookName
2926: * @return InternalThrowPoint[]
2927: */
2928: public function getThrowPointsFromPropertyHook(
2929: MutatingScope $scope,
2930: PropertyFetch $propertyFetch,
2931: PhpPropertyReflection $propertyReflection,
2932: string $hookName,
2933: ): array
2934: {
2935: $scopeFunction = $scope->getFunction();
2936: if (
2937: $scopeFunction instanceof PhpMethodFromParserNodeReflection
2938: && $scopeFunction->isPropertyHook()
2939: && $propertyFetch->var instanceof Variable
2940: && $propertyFetch->var->name === 'this'
2941: && $propertyFetch->name instanceof Identifier
2942: && $propertyFetch->name->toString() === $scopeFunction->getHookedPropertyName()
2943: ) {
2944: return [];
2945: }
2946: $declaringClass = $propertyReflection->getDeclaringClass();
2947: if (!$propertyReflection->hasHook($hookName)) {
2948: if (
2949: $propertyReflection->isPrivate()
2950: || $propertyReflection->isFinal()->yes()
2951: || $declaringClass->isFinal()
2952: ) {
2953: return [];
2954: }
2955:
2956: if ($this->implicitThrows) {
2957: return [InternalThrowPoint::createImplicit($scope, $propertyFetch)];
2958: }
2959:
2960: return [];
2961: }
2962:
2963: $getHook = $propertyReflection->getHook($hookName);
2964: $throwType = $getHook->getThrowType();
2965:
2966: if ($throwType !== null) {
2967: if (!$throwType->isVoid()->yes()) {
2968: return [InternalThrowPoint::createExplicit($scope, $throwType, $propertyFetch, true)];
2969: }
2970: } elseif ($this->implicitThrows) {
2971: return [InternalThrowPoint::createImplicit($scope, $propertyFetch)];
2972: }
2973:
2974: return [];
2975: }
2976:
2977: /**
2978: * @return string[]
2979: */
2980: public function getAssignedVariables(Expr $expr): array
2981: {
2982: if ($expr instanceof Expr\Variable) {
2983: if (is_string($expr->name)) {
2984: return [$expr->name];
2985: }
2986:
2987: return [];
2988: }
2989:
2990: if ($expr instanceof Expr\List_) {
2991: $names = [];
2992: foreach ($expr->items as $item) {
2993: if ($item === null) {
2994: continue;
2995: }
2996:
2997: $names = array_merge($names, $this->getAssignedVariables($item->value));
2998: }
2999:
3000: return $names;
3001: }
3002:
3003: if ($expr instanceof ArrayDimFetch) {
3004: return $this->getAssignedVariables($expr->var);
3005: }
3006:
3007: return [];
3008: }
3009:
3010: /**
3011: * @param callable(Node $node, Scope $scope): void $nodeCallback
3012: */
3013: public function callNodeCallbackWithExpression(
3014: callable $nodeCallback,
3015: Node $expr,
3016: MutatingScope $scope,
3017: ExpressionResultStorage $storage,
3018: ExpressionContext $context,
3019: ): void
3020: {
3021: if ($context->isDeep()) {
3022: $scope = $scope->exitFirstLevelStatements();
3023: }
3024: $this->callNodeCallback($nodeCallback, $expr, $scope, $storage);
3025: }
3026:
3027: /**
3028: * @param callable(Node $node, Scope $scope): void $nodeCallback
3029: */
3030: public function callNodeCallback(
3031: callable $nodeCallback,
3032: Node $node,
3033: MutatingScope $scope,
3034: ExpressionResultStorage $storage,
3035: ): void
3036: {
3037: $nodeCallback($node, $scope);
3038: }
3039:
3040: /**
3041: * @param callable(Node $node, Scope $scope): void $nodeCallback
3042: */
3043: public function processClosureNode(
3044: Node\Stmt $stmt,
3045: Expr\Closure $expr,
3046: MutatingScope $scope,
3047: ExpressionResultStorage $storage,
3048: callable $nodeCallback,
3049: ExpressionContext $context,
3050: ?Type $passedToType,
3051: ?Type $nativePassedToType = null,
3052: ): ProcessClosureResult
3053: {
3054: foreach ($expr->params as $param) {
3055: $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
3056: }
3057:
3058: $byRefUses = [];
3059:
3060: $closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME);
3061: $callableParameters = $this->createCallableParameters($scope, $expr, $closureCallArgs, $passedToType);
3062: $nativeCallableParameters = $this->createNativeCallableParameters($scope, $expr, $closureCallArgs, $nativePassedToType);
3063:
3064: $useScope = $scope;
3065: foreach ($expr->uses as $use) {
3066: if ($use->byRef) {
3067: $byRefUses[] = $use;
3068: $useScope = $useScope->enterExpressionAssign($use->var);
3069:
3070: $inAssignRightSideVariableName = $context->getInAssignRightSideVariableName();
3071: $inAssignRightSideExpr = $context->getInAssignRightSideExpr();
3072: if (
3073: $inAssignRightSideVariableName === $use->var->name
3074: && $inAssignRightSideExpr !== null
3075: ) {
3076: $inAssignRightSideType = $scope->getType($inAssignRightSideExpr);
3077: if ($inAssignRightSideType instanceof ClosureType) {
3078: $variableType = $inAssignRightSideType;
3079: } else {
3080: $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName);
3081: if ($alreadyHasVariableType->no()) {
3082: $variableType = TypeCombinator::union(new NullType(), $inAssignRightSideType);
3083: } else {
3084: $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType);
3085: }
3086: }
3087: $inAssignRightSideNativeType = $scope->getNativeType($inAssignRightSideExpr);
3088: if ($inAssignRightSideNativeType instanceof ClosureType) {
3089: $variableNativeType = $inAssignRightSideNativeType;
3090: } else {
3091: $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName);
3092: if ($alreadyHasVariableType->no()) {
3093: $variableNativeType = TypeCombinator::union(new NullType(), $inAssignRightSideNativeType);
3094: } else {
3095: $variableNativeType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideNativeType);
3096: }
3097: }
3098: $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType, $variableNativeType, TrinaryLogic::createYes());
3099: }
3100: }
3101: $this->processExprNode($stmt, $use->var, $useScope, $storage, $nodeCallback, $context);
3102: if (!$use->byRef) {
3103: continue;
3104: }
3105:
3106: $useScope = $useScope->exitExpressionAssign($use->var);
3107: }
3108:
3109: if ($expr->returnType !== null) {
3110: $this->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage);
3111: }
3112:
3113: $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters, $nativeCallableParameters);
3114: $closureScope = $closureScope->processClosureScope($scope, null, $byRefUses);
3115: $closureType = $closureScope->getAnonymousFunctionReflection();
3116: if (!$closureType instanceof ClosureType) {
3117: throw new ShouldNotHappenException();
3118: }
3119:
3120: $this->callNodeCallback($nodeCallback, new InClosureNode($closureType, $expr), $closureScope, $storage);
3121:
3122: $executionEnds = [];
3123: $gatheredReturnStatements = [];
3124: $gatheredReturnStatementsWithScope = [];
3125: $gatheredYieldStatements = [];
3126: $gatheredYieldStatementsWithScope = [];
3127: $closureImpurePoints = [];
3128: $invalidateExpressions = [];
3129: $closureStmtsCallback = new GatheringNodeCallback(static function (Node $node, Scope $scope) use (&$executionEnds, &$gatheredReturnStatements, &$gatheredReturnStatementsWithScope, &$gatheredYieldStatements, &$gatheredYieldStatementsWithScope, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void {
3130: if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) {
3131: return;
3132: }
3133: if ($node instanceof PropertyAssignNode) {
3134: $closureImpurePoints[] = new ImpurePoint(
3135: $scope,
3136: $node,
3137: 'propertyAssign',
3138: 'property assignment',
3139: true,
3140: );
3141: $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch());
3142: return;
3143: }
3144: if ($node instanceof ExecutionEndNode) {
3145: $executionEnds[] = $node;
3146: return;
3147: }
3148: if ($node instanceof InvalidateExprNode) {
3149: $invalidateExpressions[] = $node;
3150: return;
3151: }
3152: if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) {
3153: $gatheredYieldStatements[] = $node;
3154: $gatheredYieldStatementsWithScope[] = [$node, $scope];
3155: }
3156: if (!$node instanceof Return_) {
3157: return;
3158: }
3159:
3160: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
3161: $gatheredReturnStatementsWithScope[] = [$node, $scope];
3162: }, $nodeCallback);
3163:
3164: if (count($byRefUses) === 0) {
3165: $statementResult = $this->processStmtNodesInternalWithoutFlushingPendingFibers($expr, $expr->stmts, $closureScope, $storage, $closureStmtsCallback, StatementContext::createTopLevel());
3166: $publicStatementResult = $statementResult->toPublic();
3167: $closureReturnStatementsNodeScope = $this->refineClosureNodeScope($closureScope, $scope, $expr, $gatheredReturnStatementsWithScope, $gatheredYieldStatementsWithScope, $executionEnds, $statementResult->getThrowPoints(), array_merge($closureImpurePoints, $statementResult->getImpurePoints()), $invalidateExpressions);
3168: $this->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode(
3169: $expr,
3170: $gatheredReturnStatements,
3171: $gatheredYieldStatements,
3172: $publicStatementResult,
3173: $executionEnds,
3174: array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints),
3175: ), $closureReturnStatementsNodeScope, $storage);
3176:
3177: return new ProcessClosureResult(
3178: $scope,
3179: $statementResult->getThrowPoints(),
3180: $statementResult->getImpurePoints(),
3181: $invalidateExpressions,
3182: $gatheredReturnStatementsWithScope,
3183: $gatheredYieldStatementsWithScope,
3184: $executionEnds,
3185: array_merge($closureImpurePoints, $statementResult->getImpurePoints()),
3186: );
3187: }
3188:
3189: $originalStorage = $storage;
3190:
3191: $count = 0;
3192: $closureResultScope = null;
3193: do {
3194: $prevScope = $closureScope;
3195:
3196: $storage = $originalStorage->duplicate();
3197: // deep context, like the loop handlers' own convergence passes: inner
3198: // loops walk single-pass here and only the final walk below (top-level)
3199: // runs their full convergence - otherwise every closure-convergence
3200: // pass would re-converge every inner loop from scratch
3201: $intermediaryClosureScopeResult = $this->processStmtNodesInternalWithoutFlushingPendingFibers($expr, $expr->stmts, $closureScope, $storage, new NoopNodeCallback(), StatementContext::createDeep());
3202: $intermediaryClosureScope = $intermediaryClosureScopeResult->getScope();
3203: foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) {
3204: $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope());
3205: }
3206:
3207: if ($expr->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) === true) {
3208: $closureResultScope = $intermediaryClosureScope;
3209: break;
3210: }
3211:
3212: $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters, $nativeCallableParameters);
3213: $closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses);
3214:
3215: if ($closureScope->equals($prevScope)) {
3216: break;
3217: }
3218: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
3219: $closureScope = $prevScope->generalizeWith($closureScope);
3220: }
3221: $count++;
3222: } while ($count < self::LOOP_SCOPE_ITERATIONS);
3223:
3224: if ($closureResultScope === null) {
3225: $closureResultScope = $closureScope;
3226: }
3227:
3228: $storage = $originalStorage;
3229: $statementResult = $this->processStmtNodesInternalWithoutFlushingPendingFibers($expr, $expr->stmts, $closureScope, $storage, $closureStmtsCallback, StatementContext::createTopLevel());
3230: $publicStatementResult = $statementResult->toPublic();
3231: $closureReturnStatementsNodeScope = $this->refineClosureNodeScope($closureScope, $scope, $expr, $gatheredReturnStatementsWithScope, $gatheredYieldStatementsWithScope, $executionEnds, $statementResult->getThrowPoints(), array_merge($closureImpurePoints, $statementResult->getImpurePoints()), $invalidateExpressions);
3232: $this->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode(
3233: $expr,
3234: $gatheredReturnStatements,
3235: $gatheredYieldStatements,
3236: $publicStatementResult,
3237: $executionEnds,
3238: array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints),
3239: ), $closureReturnStatementsNodeScope, $storage);
3240:
3241: return new ProcessClosureResult(
3242: $scope,
3243: $statementResult->getThrowPoints(),
3244: $statementResult->getImpurePoints(),
3245: $invalidateExpressions,
3246: $gatheredReturnStatementsWithScope,
3247: $gatheredYieldStatementsWithScope,
3248: $executionEnds,
3249: array_merge($closureImpurePoints, $statementResult->getImpurePoints()),
3250: $closureResultScope,
3251: $byRefUses,
3252: );
3253: }
3254:
3255: /**
3256: * The refined closure type built from the single body walk, swapped onto the
3257: * closure scope so ClosureReturnStatementsNode's rules see the refined
3258: * expected return instead of the shallow entry reflection.
3259: *
3260: * @param list<array{Node\Stmt\Return_, Scope}> $gatheredReturnStatementsWithScope
3261: * @param list<array{Expr\Yield_|Expr\YieldFrom, Scope}> $gatheredYieldStatementsWithScope
3262: * @param list<ExecutionEndNode> $executionEnds
3263: * @param InternalThrowPoint[] $throwPoints
3264: * @param ImpurePoint[] $impurePoints
3265: * @param InvalidateExprNode[] $invalidateExpressions
3266: */
3267: private function refineClosureNodeScope(
3268: MutatingScope $closureScope,
3269: MutatingScope $scope,
3270: Expr\Closure $expr,
3271: array $gatheredReturnStatementsWithScope,
3272: array $gatheredYieldStatementsWithScope,
3273: array $executionEnds,
3274: array $throwPoints,
3275: array $impurePoints,
3276: array $invalidateExpressions,
3277: ): MutatingScope
3278: {
3279: $refinedClosureType = $this->container->getByType(ClosureTypeResolver::class)->buildClosureTypeForClosure(
3280: $scope,
3281: $expr,
3282: $gatheredReturnStatementsWithScope,
3283: $gatheredYieldStatementsWithScope,
3284: $executionEnds,
3285: $throwPoints,
3286: $impurePoints,
3287: $invalidateExpressions,
3288: false,
3289: false,
3290: );
3291:
3292: return $closureScope->withAnonymousFunctionReflection($refinedClosureType);
3293: }
3294:
3295: /**
3296: * @param InvalidateExprNode[] $invalidatedExpressions
3297: * @param string[] $uses
3298: */
3299: public function processImmediatelyCalledCallable(MutatingScope $scope, array $invalidatedExpressions, array $uses): MutatingScope
3300: {
3301: if ($scope->isInClass()) {
3302: $uses[] = 'this';
3303: }
3304:
3305: $finder = new NodeFinder();
3306: foreach ($invalidatedExpressions as $invalidateExpression) {
3307: $result = $finder->findFirst([$invalidateExpression->getExpr()], static fn ($node) => $node instanceof Variable && in_array($node->name, $uses, true));
3308: if ($result === null) {
3309: continue;
3310: }
3311:
3312: $requireMoreCharacters = $invalidateExpression->getExpr() instanceof Variable;
3313: $scope = $scope->invalidateExpression($invalidateExpression->getExpr(), $requireMoreCharacters);
3314: }
3315:
3316: return $scope;
3317: }
3318:
3319: /**
3320: * @param callable(Node $node, Scope $scope): void $nodeCallback
3321: */
3322: public function processArrowFunctionNode(
3323: Node\Stmt $stmt,
3324: Expr\ArrowFunction $expr,
3325: MutatingScope $scope,
3326: ExpressionResultStorage $storage,
3327: callable $nodeCallback,
3328: ?Type $passedToType,
3329: ?Type $nativePassedToType = null,
3330: ): ProcessArrowFunctionResult
3331: {
3332: foreach ($expr->params as $param) {
3333: $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
3334: }
3335: if ($expr->returnType !== null) {
3336: $this->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage);
3337: }
3338:
3339: $arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME);
3340: $callableParameters = $this->createCallableParameters($scope, $expr, $arrowFunctionCallArgs, $passedToType);
3341: $nativeCallableParameters = $this->createNativeCallableParameters($scope, $expr, $arrowFunctionCallArgs, $nativePassedToType);
3342: $arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters, $nativeCallableParameters);
3343: if ($arrowFunctionScope->getAnonymousFunctionReflection() === null) {
3344: throw new ShouldNotHappenException();
3345: }
3346:
3347: // Gather the property-assign impure points and invalidate expressions the
3348: // arrow function type needs (mirroring ClosureTypeResolver::getClosureType()),
3349: // on top of the regular rule node callback, so the single body walk here
3350: // feeds ClosureTypeResolver::buildClosureTypeForArrowFunction().
3351: $arrowFunctionImpurePoints = [];
3352: $invalidateExpressions = [];
3353: $arrowFunctionStmtsCallback = new GatheringNodeCallback(static function (Node $node, Scope $innerScope) use ($arrowFunctionScope, &$arrowFunctionImpurePoints, &$invalidateExpressions): void {
3354: if ($innerScope->getAnonymousFunctionReflection() !== $arrowFunctionScope->getAnonymousFunctionReflection()) {
3355: return;
3356: }
3357:
3358: if ($node instanceof InvalidateExprNode) {
3359: $invalidateExpressions[] = $node;
3360: return;
3361: }
3362:
3363: if (!$node instanceof PropertyAssignNode) {
3364: return;
3365: }
3366:
3367: $arrowFunctionImpurePoints[] = new ImpurePoint(
3368: $innerScope,
3369: $node,
3370: 'propertyAssign',
3371: 'property assignment',
3372: true,
3373: );
3374: $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch());
3375: }, $nodeCallback);
3376:
3377: $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $arrowFunctionStmtsCallback, ExpressionContext::createTopLevel());
3378:
3379: $closureTypeThrowPoints = array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->toPublic(), $exprResult->getThrowPoints());
3380: $closureTypeImpurePoints = array_merge($arrowFunctionImpurePoints, $exprResult->getImpurePoints());
3381:
3382: // The arrow scope was entered with a shallow reflection (parameters +
3383: // declared return, no body walk). Now that the single body walk above has
3384: // run, build the refined arrow function type from the walked body (no
3385: // second walk) and fire InArrowFunctionNode with it, so the node and the
3386: // return-type rules see the refined expected return. The build must not
3387: // write the type cache: its values reflect this call's (possibly
3388: // extension-overridden) parameter typing while its key would match a
3389: // plain pricing ask.
3390: $refinedArrowFunctionType = $this->container->getByType(ClosureTypeResolver::class)->buildClosureTypeForArrowFunction(
3391: $scope,
3392: $expr,
3393: $arrowFunctionScope,
3394: $closureTypeThrowPoints,
3395: $closureTypeImpurePoints,
3396: $invalidateExpressions,
3397: false,
3398: false,
3399: );
3400: $refinedArrowFunctionScope = $arrowFunctionScope->withAnonymousFunctionReflection($refinedArrowFunctionType);
3401: $this->callNodeCallback($nodeCallback, new InArrowFunctionNode($refinedArrowFunctionType, $expr), $refinedArrowFunctionScope, $storage);
3402:
3403: return new ProcessArrowFunctionResult(
3404: $this->expressionResultFactory->create($scope, beforeScope: $scope, expr: $expr, hasYield: false, isAlwaysTerminating: $exprResult->isAlwaysTerminating(), throwPoints: $exprResult->getThrowPoints(), impurePoints: $exprResult->getImpurePoints()),
3405: $arrowFunctionScope,
3406: $closureTypeThrowPoints,
3407: $closureTypeImpurePoints,
3408: $invalidateExpressions,
3409: );
3410: }
3411:
3412: /**
3413: * @param Node\Arg[]|null $args
3414: * @return ParameterReflection[]|null
3415: */
3416: public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array
3417: {
3418: return $this->doCreateCallableParameters($scope, $closureExpr, $args, $passedToType, static fn (Scope $s, Expr $e) => $s->getType($e));
3419: }
3420:
3421: /**
3422: * @param Node\Arg[]|null $args
3423: * @return ParameterReflection[]|null
3424: */
3425: public function createNativeCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $nativePassedToType): ?array
3426: {
3427: return $this->doCreateCallableParameters($scope, $closureExpr, $args, $nativePassedToType, static fn (Scope $s, Expr $e) => $s->getNativeType($e));
3428: }
3429:
3430: /**
3431: * @param Node\Arg[]|null $args
3432: * @param Closure(Scope, Expr): Type $typeGetter
3433: * @return ParameterReflection[]|null
3434: */
3435: private function doCreateCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType, Closure $typeGetter): ?array
3436: {
3437: $callableParameters = null;
3438: if ($args !== null) {
3439: $closureType = $typeGetter($scope, $closureExpr);
3440:
3441: if ($closureType->isCallable()->no()) {
3442: return null;
3443: }
3444:
3445: $acceptors = $closureType->getCallableParametersAcceptors($scope);
3446: if (count($acceptors) === 1) {
3447: $callableParameters = $acceptors[0]->getParameters();
3448:
3449: foreach ($callableParameters as $index => $callableParameter) {
3450: if (!isset($args[$index])) {
3451: continue;
3452: }
3453:
3454: if ($callableParameter->isVariadic()) {
3455: $argTypes = [];
3456: $argNumber = count($args);
3457: for ($j = $index; $j < $argNumber; $j++) {
3458: $argTypes[] = $typeGetter($scope, $args[$j]->value);
3459: }
3460: $type = TypeCombinator::union(...$argTypes);
3461: } else {
3462: $type = $typeGetter($scope, $args[$index]->value);
3463: }
3464: $callableParameters[$index] = new NativeParameterReflection(
3465: $callableParameter->getName(),
3466: $callableParameter->isOptional(),
3467: $type,
3468: $callableParameter->passedByReference(),
3469: $callableParameter->isVariadic(),
3470: $callableParameter->getDefaultValue(),
3471: );
3472: }
3473: }
3474: } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) {
3475: if ($passedToType instanceof UnionType) {
3476: $passedToType = $passedToType->filterTypes(static fn (Type $innerType) => $innerType->isCallable()->yes());
3477:
3478: if ($passedToType->isCallable()->no()) {
3479: return null;
3480: }
3481: }
3482:
3483: $acceptors = $passedToType->getCallableParametersAcceptors($scope);
3484: foreach ($acceptors as $acceptor) {
3485: $acceptorParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection(
3486: $callableParameter->getName(),
3487: $callableParameter->isOptional(),
3488: $callableParameter->getType(),
3489: $callableParameter->passedByReference(),
3490: $callableParameter->isVariadic(),
3491: $callableParameter->getDefaultValue(),
3492: ), $acceptor->getParameters());
3493:
3494: if ($callableParameters === null) {
3495: $callableParameters = $acceptorParameters;
3496: continue;
3497: }
3498:
3499: $newParameters = [];
3500: $parameterCount = max(count($callableParameters), count($acceptorParameters));
3501: for ($i = 0; $i < $parameterCount; $i++) {
3502: if (!array_key_exists($i, $acceptorParameters)) {
3503: $newParameters[] = $callableParameters[$i]->toOptional();
3504: continue;
3505: }
3506:
3507: if (!array_key_exists($i, $callableParameters)) {
3508: $newParameters[] = $acceptorParameters[$i]->toOptional();
3509: continue;
3510: }
3511:
3512: $newParameters[] = $callableParameters[$i]->union($acceptorParameters[$i]);
3513: }
3514:
3515: $callableParameters = $newParameters;
3516: }
3517: }
3518:
3519: return $callableParameters;
3520: }
3521:
3522: /**
3523: * @param callable(Node $node, Scope $scope): void $nodeCallback
3524: */
3525: private function processParamNode(
3526: Node\Stmt $stmt,
3527: Node\Param $param,
3528: MutatingScope $scope,
3529: ExpressionResultStorage $storage,
3530: callable $nodeCallback,
3531: ): void
3532: {
3533: $this->processAttributeGroups($stmt, $param->attrGroups, $scope, $storage, $nodeCallback);
3534: $this->callNodeCallback($nodeCallback, $param, $scope, $storage);
3535: if ($param->type !== null) {
3536: $this->callNodeCallback($nodeCallback, $param->type, $scope, $storage);
3537: }
3538: if ($param->default === null) {
3539: return;
3540: }
3541:
3542: $this->processExprNode($stmt, $param->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
3543: }
3544:
3545: /**
3546: * @param AttributeGroup[] $attrGroups
3547: * @param callable(Node $node, Scope $scope): void $nodeCallback
3548: */
3549: private function processAttributeGroups(
3550: Node\Stmt $stmt,
3551: array $attrGroups,
3552: MutatingScope $scope,
3553: ExpressionResultStorage $storage,
3554: callable $nodeCallback,
3555: ): void
3556: {
3557: foreach ($attrGroups as $attrGroup) {
3558: foreach ($attrGroup->attrs as $attr) {
3559: $className = $scope->resolveName($attr->name);
3560: if ($this->reflectionProvider->hasClass($className)) {
3561: $classReflection = $this->reflectionProvider->getClass($className);
3562: if ($classReflection->hasConstructor()) {
3563: $constructorReflection = $classReflection->getConstructor();
3564: $parametersAcceptor = ParametersAcceptorSelector::combineVariantsForNormalization(
3565: $attr->args,
3566: $constructorReflection->getVariants(),
3567: $constructorReflection->getNamedArgumentsVariants(),
3568: );
3569: $expr = new New_($attr->name, $attr->args);
3570: $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr;
3571: $this->processArgs($stmt, $constructorReflection, null, $constructorReflection->getVariants(), $constructorReflection->getNamedArgumentsVariants(), $expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
3572: $this->callNodeCallback($nodeCallback, $attr, $scope, $storage);
3573: continue;
3574: }
3575: }
3576:
3577: foreach ($attr->args as $arg) {
3578: $this->processExprNode($stmt, $arg->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
3579: $this->callNodeCallback($nodeCallback, $arg, $scope, $storage);
3580: }
3581: $this->callNodeCallback($nodeCallback, $attr, $scope, $storage);
3582: }
3583: $this->callNodeCallback($nodeCallback, $attrGroup, $scope, $storage);
3584: }
3585: }
3586:
3587: /**
3588: * @param Node\PropertyHook[] $hooks
3589: * @param callable(Node $node, Scope $scope): void $nodeCallback
3590: */
3591: private function processPropertyHooks(
3592: Node\Stmt $stmt,
3593: Identifier|Name|ComplexType|null $nativeTypeNode,
3594: ?Type $phpDocType,
3595: string $propertyName,
3596: array $hooks,
3597: MutatingScope $scope,
3598: ExpressionResultStorage $storage,
3599: callable $nodeCallback,
3600: ): void
3601: {
3602: if (!$scope->isInClass()) {
3603: throw new ShouldNotHappenException();
3604: }
3605:
3606: $classReflection = $scope->getClassReflection();
3607:
3608: foreach ($hooks as $hook) {
3609: $this->callNodeCallback($nodeCallback, $hook, $scope, $storage);
3610: $this->processAttributeGroups($stmt, $hook->attrGroups, $scope, $storage, $nodeCallback);
3611:
3612: [, $phpDocParameterTypes,,,, $phpDocThrowType,,,,,,,, $phpDocComment,,,,,, $resolvedPhpDoc] = $this->getPhpDocs($scope, $hook);
3613:
3614: foreach ($hook->params as $param) {
3615: $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
3616: }
3617:
3618: [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $hook);
3619:
3620: $hookScope = $scope->enterPropertyHook(
3621: $hook,
3622: $propertyName,
3623: $nativeTypeNode,
3624: $phpDocType,
3625: $phpDocParameterTypes,
3626: $phpDocThrowType,
3627: $deprecatedDescription,
3628: $isDeprecated,
3629: $phpDocComment,
3630: $resolvedPhpDoc,
3631: );
3632: $hookReflection = $hookScope->getFunction();
3633: if (!$hookReflection instanceof PhpMethodFromParserNodeReflection) {
3634: throw new ShouldNotHappenException();
3635: }
3636:
3637: if (!$classReflection->hasNativeProperty($propertyName)) {
3638: throw new ShouldNotHappenException();
3639: }
3640:
3641: $propertyReflection = $classReflection->getNativeProperty($propertyName);
3642:
3643: $this->callNodeCallback($nodeCallback, new InPropertyHookNode(
3644: $classReflection,
3645: $hookReflection,
3646: $propertyReflection,
3647: $hook,
3648: ), $hookScope, $storage);
3649:
3650: $stmts = $hook->getStmts();
3651: if ($stmts === null) {
3652: return;
3653: }
3654:
3655: if ($hook->body instanceof Expr) {
3656: // enrich attributes of nodes in short hook body statements
3657: $traverser = new NodeTraverser(
3658: new LineAttributesVisitor($hook->body->getStartLine(), $hook->body->getEndLine()),
3659: );
3660: $traverser->traverse($stmts);
3661: }
3662:
3663: $gatheredReturnStatements = [];
3664: $executionEnds = [];
3665: $methodImpurePoints = [];
3666: $statementResult = $this->processStmtNodesInternal(new PropertyHookStatementNode($hook), $stmts, $hookScope, $storage, new GatheringNodeCallback(static function (Node $node, Scope $scope) use ($hookScope, &$gatheredReturnStatements, &$executionEnds, &$hookImpurePoints): void {
3667: if ($scope->getFunction() !== $hookScope->getFunction()) {
3668: return;
3669: }
3670: if ($scope->isInAnonymousFunction()) {
3671: return;
3672: }
3673: if ($node instanceof PropertyAssignNode) {
3674: $hookImpurePoints[] = new ImpurePoint(
3675: $scope,
3676: $node,
3677: 'propertyAssign',
3678: 'property assignment',
3679: true,
3680: );
3681: return;
3682: }
3683: if ($node instanceof ExecutionEndNode) {
3684: $executionEnds[] = $node;
3685: return;
3686: }
3687: if (!$node instanceof Return_) {
3688: return;
3689: }
3690:
3691: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
3692: }, $nodeCallback), StatementContext::createTopLevel())->toPublic();
3693:
3694: $this->callNodeCallback($nodeCallback, new PropertyHookReturnStatementsNode(
3695: $hook,
3696: $gatheredReturnStatements,
3697: $statementResult,
3698: $executionEnds,
3699: array_merge($statementResult->getImpurePoints(), $methodImpurePoints),
3700: $classReflection,
3701: $hookReflection,
3702: $propertyReflection,
3703: ), $hookScope, $storage);
3704: }
3705: }
3706:
3707: /**
3708: * @param FunctionReflection|MethodReflection|null $calleeReflection
3709: */
3710: private function resolveClosureThisType(
3711: ?CallLike $call,
3712: $calleeReflection,
3713: ParameterReflection $parameter,
3714: MutatingScope $scope,
3715: ): ?Type
3716: {
3717: if ($call instanceof FuncCall && $calleeReflection instanceof FunctionReflection) {
3718: foreach ($this->functionParameterClosureThisExtensions->getAll() as $extension) {
3719: if (! $extension->isFunctionSupported($calleeReflection, $parameter)) {
3720: continue;
3721: }
3722: $type = $extension->getClosureThisTypeFromFunctionCall($calleeReflection, $call, $parameter, $scope);
3723: if ($type !== null) {
3724: return $type;
3725: }
3726: }
3727: } elseif ($call instanceof StaticCall && $calleeReflection instanceof MethodReflection) {
3728: foreach ($this->staticMethodParameterClosureThisExtensions->getAll() as $extension) {
3729: if (! $extension->isStaticMethodSupported($calleeReflection, $parameter)) {
3730: continue;
3731: }
3732: $type = $extension->getClosureThisTypeFromStaticMethodCall($calleeReflection, $call, $parameter, $scope);
3733: if ($type !== null) {
3734: return $type;
3735: }
3736: }
3737: } elseif ($call instanceof MethodCall && $calleeReflection instanceof MethodReflection) {
3738: foreach ($this->methodParameterClosureThisExtensions->getAll() as $extension) {
3739: if (! $extension->isMethodSupported($calleeReflection, $parameter)) {
3740: continue;
3741: }
3742: $type = $extension->getClosureThisTypeFromMethodCall($calleeReflection, $call, $parameter, $scope);
3743: if ($type !== null) {
3744: return $type;
3745: }
3746: }
3747: }
3748:
3749: if ($parameter instanceof ExtendedParameterReflection) {
3750: return $parameter->getClosureThisType();
3751: }
3752:
3753: return null;
3754: }
3755:
3756: /**
3757: * @param MethodReflection|FunctionReflection|null $calleeReflection
3758: * @param ParametersAcceptor[] $parametersAcceptors
3759: * @param ParametersAcceptor[]|null $namedArgumentsVariants
3760: * @param callable(Node $node, Scope $scope): void $nodeCallback
3761: * @param (callable(MutatingScope): MutatingScope)|null $closureBindScopeFactory
3762: */
3763: public function processArgs(
3764: Node\Stmt $stmt,
3765: $calleeReflection,
3766: ?ExtendedMethodReflection $nakedMethodReflection,
3767: array $parametersAcceptors,
3768: ?array $namedArgumentsVariants,
3769: CallLike $callLike,
3770: MutatingScope $scope,
3771: ExpressionResultStorage $storage,
3772: callable $nodeCallback,
3773: ExpressionContext $context,
3774: ?callable $closureBindScopeFactory = null,
3775: ): ArgsResult
3776: {
3777: $args = $callLike->getArgs();
3778:
3779: // Evolving-scope arg types: gathered as each argument is processed on the
3780: // scope that evolves arg-to-arg. They select the FINAL resolved acceptor
3781: // (the call's return type, by-ref OUT types), which type-resolves generics
3782: // from the actual argument types.
3783: $gatheredTypes = [];
3784: $gatheredUnpack = false;
3785: $gatheredHasName = false;
3786: $gatheredArgTypeByIndex = [];
3787:
3788: // The intrinsic argument overrides (array_map/filter/walk/find, curl_setopt,
3789: // implode, Closure::bind) rewrite a callback parameter's type from its
3790: // sibling arguments. Apply them up front on the entry scope - the parameter
3791: // pushed on the in-function-call stack while each argument is processed (and
3792: // priced, e.g. a closure's inferred return type) must be the overridden one,
3793: // exactly as when the caller pre-selected via selectFromArgs().
3794: $parametersAcceptors = ParametersAcceptorSelector::applyIntrinsicArgOverrides(
3795: $args,
3796: $parametersAcceptors,
3797: $namedArgumentsVariants,
3798: $scope,
3799: static fn (Expr $e): Type => $scope->getType($e),
3800: static fn (Expr $e): Type => $scope->getNativeType($e),
3801: static fn (Type $t): Type => $scope->getIterableValueType($t),
3802: static fn (Type $t): Type => $scope->getIterableKeyType($t),
3803: );
3804:
3805: // Metadata acceptor base - NO forward read. The per-argument resolution below picks the
3806: // count-correct variant (the by-ref/variadic STRUCTURE is variant-stable except where it is
3807: // keyed off the argument count, e.g. sscanf - and the count is known structurally) and
3808: // resolves generic parameter types from the args gathered so far; the call's return type
3809: // comes from the post-loop resolved acceptor.
3810: $metadataAcceptor = $parametersAcceptors[0] ?? null;
3811:
3812: // Both predicates are hoisted out of the per-argument loop - they traverse
3813: // the acceptor's parameter/return types.
3814: $hasTemplateParameterType = $metadataAcceptor !== null
3815: && ParametersAcceptorSelector::hasAcceptorTemplateOrLateResolvableParameterType($metadataAcceptor);
3816: $argMetadataIsTypeDriven = count($parametersAcceptors) > 1 || $hasTemplateParameterType;
3817:
3818: // Whether selecting an acceptor is type-driven at all: multiple variants to
3819: // choose between, templates or conditionals to resolve from the arg types,
3820: // or named-argument variants. When it is not, the gathered arg types can
3821: // never influence the selected acceptor, so the faithful-return gather walk
3822: // of a closure/arrow argument (gatherClosureArgType()) would be pure waste -
3823: // a plain mixed keeps the count/name bookkeeping correct.
3824: $typeDrivenAcceptorSelection = count($parametersAcceptors) > 1
3825: || $namedArgumentsVariants !== null
3826: || $hasTemplateParameterType
3827: || ($metadataAcceptor !== null && $metadataAcceptor->getReturnType()->hasTemplateOrLateResolvableType());
3828:
3829: $hasYield = false;
3830: $throwPoints = [];
3831: $impurePoints = [];
3832: $isAlwaysTerminating = false;
3833: /** @var list<array{InvalidateExprNode[], string[]}> $deferredInvalidateExpressions */
3834: $deferredInvalidateExpressions = [];
3835: /** @var ProcessClosureResult[] $deferredByRefClosureResults */
3836: $deferredByRefClosureResults = [];
3837:
3838: $processingOrder = array_keys($args);
3839: usort($processingOrder, static function (int $a, int $b) use ($args): int {
3840: $aOriginalArg = $args[$a]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE);
3841: $bOriginalArg = $args[$b]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE);
3842: $aValue = $aOriginalArg !== null ? $aOriginalArg->value : $args[$a]->value;
3843: $bValue = $bOriginalArg !== null ? $bOriginalArg->value : $args[$b]->value;
3844: $aIsClosure = $aValue instanceof Expr\Closure || $aValue instanceof Expr\ArrowFunction;
3845: $bIsClosure = $bValue instanceof Expr\Closure || $bValue instanceof Expr\ArrowFunction;
3846: if ($aIsClosure !== $bIsClosure) {
3847: // closures sort after non-closures so every sibling feeding an
3848: // intrinsic override / generic callable(T) is in scope first
3849: return $aIsClosure ? 1 : -1;
3850: }
3851:
3852: $aOriginal = $args[$a]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE);
3853: $bOriginal = $args[$b]->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE);
3854: if ($aOriginal === null && $bOriginal === null) {
3855: return $a <=> $b;
3856: }
3857: if ($aOriginal === null) {
3858: return 1;
3859: }
3860: if ($bOriginal === null) {
3861: return -1;
3862: }
3863:
3864: return $aOriginal->getStartTokenPos() <=> $bOriginal->getStartTokenPos();
3865: });
3866:
3867: $countStableMetadataAcceptor = null;
3868: foreach ($processingOrder as $i) {
3869: $arg = $args[$i];
3870:
3871: if ($arg->value instanceof Expr\Closure || $arg->value instanceof Expr\ArrowFunction) {
3872: // Gather the closure/arrow type for the FINAL resolved acceptor on
3873: // the evolving scope, BEFORE the body is processed with a possibly
3874: // generic-resolved parameter injected, so the inferred return type
3875: // stays faithful to the closure's own declaration and its own
3876: // contribution (a TValue from its return) participates in the final
3877: // resolution (see gatherClosureArgType()).
3878: $originalArgForGather = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg;
3879: $gatheredArgTypeByIndex[$i] = $typeDrivenAcceptorSelection
3880: ? $this->gatherClosureArgType($parametersAcceptors, $i, $arg->value, $scope)
3881: : new MixedType();
3882: $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArgForGather, $i, $gatheredArgTypeByIndex[$i]);
3883: }
3884:
3885: $argMetadataAcceptor = $metadataAcceptor;
3886: if ($metadataAcceptor !== null && $argMetadataIsTypeDriven) {
3887: if ($this->argConsumesResolvedParameterType($arg->value)) {
3888: // Resolve the acceptor for this argument from the args gathered SO FAR, padded to the
3889: // full argument count with mixed. Closures sort last and by-ref out-params follow the
3890: // args that pin them, so determining siblings are already processed; the mixed pad keeps
3891: // the argument COUNT correct so the by-ref/variadic variant stays stable (e.g. sscanf),
3892: // while processed siblings resolve a generic callable(T) parameter. No forward read.
3893: $paddedTypes = [];
3894: $paddedUnpack = false;
3895: $paddedHasName = false;
3896: foreach ($args as $j => $paddedArg) {
3897: $paddedOriginalArg = $paddedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $paddedArg;
3898: $this->addGatheredArgType($paddedTypes, $paddedUnpack, $paddedHasName, $paddedOriginalArg, $j, $gatheredArgTypeByIndex[$j] ?? new MixedType());
3899: }
3900: $argMetadataAcceptor = $this->selectArgsMetadataAcceptor($args, $paddedTypes, $parametersAcceptors, $namedArgumentsVariants, $paddedHasName, $paddedUnpack, $scope);
3901: } else {
3902: // Only a closure/arrow function consumes the generic-RESOLVED
3903: // parameter type: its body is inferred from the resolved
3904: // callable(T) - directly, or through the in-function-call stack
3905: // when nested anywhere inside the argument. Every other argument
3906: // reads variant-stable facts off its parameter (by-ref flag,
3907: // callable bookkeeping), so one all-mixed count-stable selection
3908: // serves them all instead of a full template inference per argument.
3909: if ($countStableMetadataAcceptor === null) {
3910: $paddedTypes = [];
3911: $paddedUnpack = false;
3912: $paddedHasName = false;
3913: foreach ($args as $j => $paddedArg) {
3914: $paddedOriginalArg = $paddedArg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $paddedArg;
3915: $this->addGatheredArgType($paddedTypes, $paddedUnpack, $paddedHasName, $paddedOriginalArg, $j, new MixedType());
3916: }
3917: $countStableMetadataAcceptor = $this->selectArgsMetadataAcceptor($args, $paddedTypes, $parametersAcceptors, $namedArgumentsVariants, $paddedHasName, $paddedUnpack, $scope);
3918: }
3919: $argMetadataAcceptor = $countStableMetadataAcceptor;
3920: }
3921: }
3922: $parameters = $argMetadataAcceptor !== null ? $argMetadataAcceptor->getParameters() : null;
3923:
3924: $assignByReference = false;
3925: $parameter = null;
3926: $parameterType = null;
3927: $parameterNativeType = null;
3928: if ($parameters !== null) {
3929: $matchedParameter = null;
3930: if ($arg->name !== null) {
3931: foreach ($parameters as $p) {
3932: if ($p->getName() === $arg->name->toString()) {
3933: $matchedParameter = $p;
3934: break;
3935: }
3936: }
3937: } elseif (isset($parameters[$i])) {
3938: $matchedParameter = $parameters[$i];
3939: }
3940:
3941: if ($matchedParameter !== null) {
3942: $assignByReference = $matchedParameter->passedByReference()->createsNewVariable();
3943: $parameterType = $matchedParameter->getType();
3944:
3945: if ($matchedParameter instanceof ExtendedParameterReflection) {
3946: $parameterNativeType = $matchedParameter->getNativeType();
3947: }
3948: $parameter = $matchedParameter;
3949: } elseif (count($parameters) > 0 && $argMetadataAcceptor->isVariadic()) {
3950: $lastParameter = array_last($parameters);
3951: $assignByReference = $lastParameter->passedByReference()->createsNewVariable();
3952: $parameterType = $lastParameter->getType();
3953:
3954: if ($lastParameter instanceof ExtendedParameterReflection) {
3955: $parameterNativeType = $lastParameter->getNativeType();
3956: }
3957: $parameter = $lastParameter;
3958: }
3959: }
3960:
3961: $lookForUnset = false;
3962: if ($assignByReference) {
3963: $isBuiltin = false;
3964: if ($calleeReflection instanceof FunctionReflection && $calleeReflection->isBuiltin()) {
3965: $isBuiltin = true;
3966: } elseif ($calleeReflection instanceof ExtendedMethodReflection && $calleeReflection->getDeclaringClass()->isBuiltin()) {
3967: $isBuiltin = true;
3968: }
3969: if (
3970: $isBuiltin
3971: || ($parameterNativeType === null || !$parameterNativeType->isNull()->no())
3972: ) {
3973: $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $arg->value);
3974: $lookForUnset = true;
3975: }
3976: }
3977:
3978: $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg;
3979: if ($calleeReflection !== null) {
3980: $rememberTypes = !$originalArg->value instanceof Expr\Closure && !$originalArg->value instanceof Expr\ArrowFunction;
3981: $scope = $scope->pushInFunctionCall($calleeReflection, $parameter, $rememberTypes);
3982: }
3983:
3984: $this->callNodeCallback($nodeCallback, $originalArg, $scope, $storage);
3985:
3986: $originalScope = $scope;
3987: $scopeToPass = $scope;
3988: if ($i === 0 && $closureBindScopeFactory !== null && ($arg->value instanceof Expr\Closure || $arg->value instanceof Expr\ArrowFunction)) {
3989: $scopeToPass = $closureBindScopeFactory($scope);
3990: }
3991:
3992: if ($arg->value instanceof Expr\Closure) {
3993:
3994: $restoreThisScope = null;
3995: if (
3996: $closureBindScopeFactory === null
3997: && $parameter instanceof ExtendedParameterReflection
3998: && !$arg->value->static
3999: ) {
4000: $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass);
4001: if ($closureThisType !== null) {
4002: $restoreThisScope = $scopeToPass;
4003: $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes())
4004: ->withClosureBindScopeClasses($closureThisType->getObjectClassNames());
4005: }
4006: }
4007:
4008: if ($parameter !== null) {
4009: $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass);
4010:
4011: if ($overwritingParameterType !== null) {
4012: $parameterType = $overwritingParameterType;
4013:
4014: // resolve the native flavour through the same extension on the
4015: // natively-promoted scope, so the closure parameters keep
4016: // their native precision too
4017: $overwritingParameterNativeType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass->doNotTreatPhpDocTypesAsCertain());
4018: if ($overwritingParameterNativeType !== null) {
4019: $parameterNativeType = $overwritingParameterNativeType;
4020: }
4021: }
4022: }
4023:
4024: $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context);
4025: $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType, $parameterNativeType);
4026: // the preferred ClosureType read below now answers from this seed
4027: // instead of walking the body again (unless a parked fiber may
4028: // still complete the gathered data - then it keeps re-walking)
4029: $this->container->getByType(ClosureTypeResolver::class)->seedCacheFromClosureWalk($scopeToPass, $arg->value, $closureResult, $storage);
4030: if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
4031: $throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints()));
4032: $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints());
4033: }
4034:
4035: $this->storeExpressionResult($storage, $arg->value, $this->expressionResultFactory->create(
4036: $closureResult->getScope(),
4037: $scopeToPass,
4038: $arg->value,
4039: hasYield: false,
4040: isAlwaysTerminating: false,
4041: throwPoints: [],
4042: impurePoints: [],
4043: ));
4044:
4045: $uses = [];
4046: foreach ($arg->value->uses as $use) {
4047: if (!is_string($use->var->name)) {
4048: continue;
4049: }
4050:
4051: $uses[] = $use->var->name;
4052: }
4053:
4054: $scope = $closureResult->getScope();
4055: $deferredByRefClosureResults[] = $closureResult;
4056: // Prefer the invalidate expressions collected on the ClosureType: those
4057: // are gathered with the closure's pending fibers flushed, so they also
4058: // cover writes that go through a parked fiber (e.g. $this->prop[] = ...),
4059: // unlike $closureResult->getInvalidateExpressions().
4060: $closureExprType = $scope->getType($arg->value);
4061: $invalidateExpressions = $closureExprType instanceof ClosureType
4062: ? $closureExprType->getInvalidateExpressions()
4063: : $closureResult->getInvalidateExpressions();
4064: if ($restoreThisScope !== null) {
4065: $nodeFinder = new NodeFinder();
4066: $cb = static fn ($expr) => $expr instanceof Variable && $expr->name === 'this';
4067: foreach ($invalidateExpressions as $j => $invalidateExprNode) {
4068: $foundThis = $nodeFinder->findFirst([$invalidateExprNode->getExpr()], $cb);
4069: if ($foundThis === null) {
4070: continue;
4071: }
4072:
4073: unset($invalidateExpressions[$j]);
4074: }
4075: $invalidateExpressions = array_values($invalidateExpressions);
4076: $scope = $scope->restoreThis($restoreThisScope);
4077: }
4078:
4079: if ($this->shouldInvalidateCallbackExpressions($parameter)) {
4080: $deferredInvalidateExpressions[] = [$invalidateExpressions, $uses];
4081: }
4082: } elseif ($arg->value instanceof Expr\ArrowFunction) {
4083:
4084: if (
4085: $closureBindScopeFactory === null
4086: && $parameter instanceof ExtendedParameterReflection
4087: && !$arg->value->static
4088: ) {
4089: $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass);
4090: if ($closureThisType !== null) {
4091: $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes())
4092: ->withClosureBindScopeClasses($closureThisType->getObjectClassNames());
4093: }
4094: }
4095:
4096: if ($parameter !== null) {
4097: $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass);
4098:
4099: if ($overwritingParameterType !== null) {
4100: $parameterType = $overwritingParameterType;
4101:
4102: // resolve the native flavour through the same extension on the
4103: // natively-promoted scope, so the closure parameters keep
4104: // their native precision too
4105: $overwritingParameterNativeType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass->doNotTreatPhpDocTypesAsCertain());
4106: if ($overwritingParameterNativeType !== null) {
4107: $parameterNativeType = $overwritingParameterNativeType;
4108: }
4109: }
4110: }
4111:
4112: $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context);
4113: $processArrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType, $parameterNativeType);
4114: // the invalidation read below now answers from this seed instead
4115: // of walking the body again (unless a parked fiber may still
4116: // complete the gathered data - then it keeps re-walking)
4117: $this->container->getByType(ClosureTypeResolver::class)->seedCacheFromArrowFunctionWalk($scopeToPass, $arg->value, $processArrowFunctionResult, $storage);
4118: $arrowFunctionResult = $processArrowFunctionResult->getExpressionResult();
4119: if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
4120: $throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $arrowFunctionResult->getThrowPoints()));
4121: $impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints());
4122: }
4123: if ($this->shouldInvalidateCallbackExpressions($parameter)) {
4124: $arrowFunctionType = $scope->getType($arg->value);
4125: if ($arrowFunctionType instanceof ClosureType) {
4126: $deferredInvalidateExpressions[] = [$arrowFunctionType->getInvalidateExpressions(), $arrowFunctionType->getUsedVariables()];
4127: }
4128: }
4129: $this->storeExpressionResult($storage, $arg->value, $arrowFunctionResult);
4130: } else {
4131: $exprType = $scope->getType($arg->value);
4132: $enterExpressionAssignForByRef = $assignByReference && $arg->value instanceof ArrayDimFetch && $arg->value->dim === null;
4133: if ($enterExpressionAssignForByRef) {
4134: $scopeToPass = $scopeToPass->enterExpressionAssign($arg->value);
4135: }
4136: $exprResult = $this->processExprNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context->enterDeep());
4137: $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
4138: $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints());
4139: $isAlwaysTerminating = $isAlwaysTerminating || $exprResult->isAlwaysTerminating();
4140: $scope = $exprResult->getScope();
4141: if ($enterExpressionAssignForByRef) {
4142: $scope = $scope->exitExpressionAssign($arg->value);
4143: }
4144: $hasYield = $hasYield || $exprResult->hasYield();
4145:
4146: if ($exprType->isCallable()->yes()) {
4147: $acceptors = $exprType->getCallableParametersAcceptors($scope);
4148: if (count($acceptors) === 1) {
4149: if ($this->shouldInvalidateCallbackExpressions($parameter)) {
4150: $deferredInvalidateExpressions[] = [$acceptors[0]->getInvalidateExpressions(), $acceptors[0]->getUsedVariables()];
4151: }
4152: if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
4153: $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $acceptors[0]->getThrowPoints());
4154: if (!$this->implicitThrows) {
4155: $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit()));
4156: }
4157: $throwPoints = array_merge($throwPoints, $callableThrowPoints);
4158: $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $arg->value, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $acceptors[0]->getImpurePoints()));
4159: }
4160: }
4161: }
4162:
4163: $gatheredArgTypeByIndex[$i] = $exprType;
4164: $this->addGatheredArgType($gatheredTypes, $gatheredUnpack, $gatheredHasName, $originalArg, $i, $gatheredArgTypeByIndex[$i]);
4165: }
4166:
4167: if ($assignByReference && $lookForUnset) {
4168: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value);
4169: }
4170:
4171: if ($calleeReflection !== null) {
4172: $scope = $scope->popInFunctionCall();
4173: }
4174:
4175: if ($i !== 0 || $closureBindScopeFactory === null) {
4176: continue;
4177: }
4178:
4179: $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope);
4180: }
4181:
4182: foreach ($deferredInvalidateExpressions as [$invalidateExpressions, $uses]) {
4183: $scope = $this->processImmediatelyCalledCallable($scope, $invalidateExpressions, $uses);
4184: }
4185:
4186: foreach ($deferredByRefClosureResults as $deferredClosureResult) {
4187: $scope = $deferredClosureResult->applyByRefUseScope($scope);
4188: }
4189:
4190: // Type-driven resolved acceptor: the arg types gathered on the evolving
4191: // scope select (and generic-resolve) the acceptor that drives the call's
4192: // return type. Intrinsic overrides are applied on the final scope,
4193: // mirroring the original selectFromArgs(). When the selection is not
4194: // type-driven, the single (already-overridden) acceptor IS the resolved
4195: // acceptor - the fast path selectFromArgs() used to take.
4196: $resolvedAcceptor = null;
4197: if ($parametersAcceptors !== []) {
4198: $resolvedAcceptor = $typeDrivenAcceptorSelection
4199: ? $this->selectArgsMetadataAcceptor($args, $gatheredTypes, $parametersAcceptors, $namedArgumentsVariants, $gatheredHasName, $gatheredUnpack, $scope)
4200: : $metadataAcceptor;
4201: }
4202:
4203: // The by-ref OUT writeback reads the metadata acceptor: it is selected from
4204: // the full argument count (stable variant). When that single acceptor still
4205: // carries templates (fast path), its OUT types need generic-resolving from the
4206: // now-complete gathered arg types - the post-loop $resolvedAcceptor is exactly
4207: // that (same variant, resolved); otherwise the metadata acceptor is already resolved.
4208: $writebackAcceptor = $metadataAcceptor;
4209: if ($metadataAcceptor !== null && $argMetadataIsTypeDriven) {
4210: $writebackAcceptor = $resolvedAcceptor;
4211: }
4212: $writebackParameters = $writebackAcceptor !== null ? $writebackAcceptor->getParameters() : null;
4213: if ($writebackParameters !== null) {
4214: foreach ($args as $i => $arg) {
4215: $assignByReference = false;
4216: $currentParameter = null;
4217: if (isset($writebackParameters[$i])) {
4218: $currentParameter = $writebackParameters[$i];
4219: } elseif (count($writebackParameters) > 0 && $writebackAcceptor->isVariadic()) {
4220: $currentParameter = array_last($writebackParameters);
4221: }
4222:
4223: if ($currentParameter !== null) {
4224: $assignByReference = $currentParameter->passedByReference()->createsNewVariable();
4225: }
4226:
4227: if ($assignByReference) {
4228: if ($currentParameter === null) {
4229: throw new ShouldNotHappenException();
4230: }
4231:
4232: $argValue = $arg->value;
4233: if (!$argValue instanceof Variable || $argValue->name !== 'this') {
4234: $paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope);
4235: if ($paramOutType !== null) {
4236: $byRefType = $paramOutType;
4237: } elseif (
4238: $currentParameter instanceof ExtendedParameterReflection
4239: && $currentParameter->getOutType() !== null
4240: ) {
4241: $byRefType = $currentParameter->getOutType();
4242: } elseif (
4243: $calleeReflection instanceof MethodReflection
4244: && !$calleeReflection->getDeclaringClass()->isBuiltin()
4245: ) {
4246: $byRefType = $currentParameter->getType();
4247: } elseif (
4248: $calleeReflection instanceof FunctionReflection
4249: && !$calleeReflection->isBuiltin()
4250: ) {
4251: $byRefType = $currentParameter->getType();
4252: } else {
4253: $byRefType = new MixedType();
4254: }
4255:
4256: $scope = $this->processVirtualAssign(
4257: $scope,
4258: $storage,
4259: $stmt,
4260: $argValue,
4261: new TypeExpr($byRefType),
4262: $nodeCallback,
4263: )->getScope();
4264: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $argValue);
4265: }
4266: } elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) {
4267: $argType = $scope->getType($arg->value);
4268: if (!$argType->isObject()->no()) {
4269: $nakedReturnType = null;
4270: if ($nakedMethodReflection !== null) {
4271: $nakedParametersAcceptor = $this->selectArgsAcceptor(
4272: $gatheredTypes,
4273: $nakedMethodReflection->getVariants(),
4274: $nakedMethodReflection->getNamedArgumentsVariants(),
4275: $gatheredHasName,
4276: $gatheredUnpack,
4277: );
4278: $nakedReturnType = $nakedParametersAcceptor->getReturnType();
4279: }
4280: if (
4281: $nakedReturnType === null
4282: || !(new ThisType($nakedMethodReflection->getDeclaringClass()))->isSuperTypeOf($nakedReturnType)->yes()
4283: || $nakedMethodReflection->isPure()->no()
4284: ) {
4285: $this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage);
4286: $scope = $scope->invalidateExpression($arg->value, true);
4287: }
4288: } elseif (!(new ResourceType())->isSuperTypeOf($argType)->no()) {
4289: $this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage);
4290: $scope = $scope->invalidateExpression($arg->value, true);
4291: }
4292: }
4293: }
4294: }
4295:
4296: // not storing this, it's scope after processing all args
4297: return new ArgsResult(
4298: $this->expressionResultFactory->create($scope, $scope, $callLike, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints),
4299: $resolvedAcceptor,
4300: );
4301: }
4302:
4303: /**
4304: * Applies the intrinsic argument overrides (array_map/filter/walk/find,
4305: * curl_setopt, implode, Closure::bind) on the arg-to-arg evolved scope,
4306: * then type-selects the metadata acceptor over
4307: * the arg types gathered so far. The overrides read sibling arg types - which
4308: * closures-last ordering keeps in scope/$gatheredTypes before any closure.
4309: *
4310: * @param Node\Arg[] $args
4311: * @param array<int|string, Type> $gatheredTypes
4312: * @param ParametersAcceptor[] $parametersAcceptors
4313: * @param ParametersAcceptor[]|null $namedArgumentsVariants
4314: */
4315: private function selectArgsMetadataAcceptor(array $args, array $gatheredTypes, array $parametersAcceptors, ?array $namedArgumentsVariants, bool $hasName, bool $unpack, MutatingScope $scope): ParametersAcceptor
4316: {
4317: $overridden = ParametersAcceptorSelector::applyIntrinsicArgOverrides(
4318: $args,
4319: $parametersAcceptors,
4320: $namedArgumentsVariants,
4321: $scope,
4322: static fn (Expr $e): Type => $scope->getType($e),
4323: static fn (Expr $e): Type => $scope->getNativeType($e),
4324: static fn (Type $t): Type => $scope->getIterableValueType($t),
4325: static fn (Type $t): Type => $scope->getIterableKeyType($t),
4326: );
4327:
4328: return $this->selectArgsAcceptor($gatheredTypes, $overridden, $namedArgumentsVariants, $hasName, $unpack);
4329: }
4330:
4331: /**
4332: * @param array<int|string, Type> $types
4333: * @param ParametersAcceptor[] $parametersAcceptors
4334: * @param ParametersAcceptor[]|null $namedArgumentsVariants
4335: */
4336: private function selectArgsAcceptor(array $types, array $parametersAcceptors, ?array $namedArgumentsVariants, bool $hasName, bool $unpack): ParametersAcceptor
4337: {
4338: return $hasName && $namedArgumentsVariants !== null
4339: ? ParametersAcceptorSelector::selectFromTypes($types, $namedArgumentsVariants, $unpack)
4340: : ParametersAcceptorSelector::selectFromTypes($types, $parametersAcceptors, $unpack);
4341: }
4342:
4343: /**
4344: * Ports the gather-keying of ParametersAcceptorSelector::selectFromArgs():
4345: * indexes the gathered arg type by name (sets $hasName) vs position, and
4346: * expands unpacked constant arrays / falls back to the iterable value type
4347: * (sets $unpack), so selectFromTypes() picks the matching variant.
4348: *
4349: * @param array<int|string, Type> $types
4350: */
4351: private function addGatheredArgType(array &$types, bool &$unpack, bool &$hasName, Node\Arg $originalArg, int $i, Type $type): void
4352: {
4353: if ($originalArg->name !== null) {
4354: $index = $originalArg->name->toString();
4355: $hasName = true;
4356: } else {
4357: $index = $i;
4358: }
4359:
4360: if ($originalArg->unpack) {
4361: $unpack = true;
4362: $constantArrays = $type->getConstantArrays();
4363: if (count($constantArrays) > 0) {
4364: foreach ($constantArrays as $constantArray) {
4365: $values = $constantArray->getValueTypes();
4366: foreach ($constantArray->getKeyTypes() as $j => $keyType) {
4367: $valueType = $values[$j];
4368: $valueIndex = $keyType->getValue();
4369: if (is_string($valueIndex)) {
4370: $hasName = true;
4371: } else {
4372: $valueIndex = $i + $j;
4373: }
4374:
4375: $types[$valueIndex] = isset($types[$valueIndex])
4376: ? TypeCombinator::union($types[$valueIndex], $valueType)
4377: : $valueType;
4378: }
4379: }
4380: } else {
4381: $types[$index] = $type->getIterableValueType();
4382: }
4383: } else {
4384: $types[$index] = $type;
4385: }
4386: }
4387:
4388: /**
4389: * Resolves the type of a closure/arrow function argument for the generic
4390: * gather, mirroring ParametersAcceptorSelector::selectFromArgs(): the closure
4391: * type is read with the RAW (un-generic-resolved) acceptor parameter pushed
4392: * onto the in-function-call stack, so its body sees the template parameter
4393: * (effectively mixed for an untyped param) rather than a parameter already
4394: * resolved from sibling args. That keeps the inferred return type (the U in
4395: * callable(T): U) faithful to the closure's own declaration.
4396: *
4397: * @param ParametersAcceptor[] $parametersAcceptors
4398: */
4399: private function gatherClosureArgType(array $parametersAcceptors, int $i, Expr $closureExpr, MutatingScope $scope): Type
4400: {
4401: $rawParameter = null;
4402: if (count($parametersAcceptors) === 1) {
4403: $rawParameters = $parametersAcceptors[0]->getParameters();
4404: if (isset($rawParameters[$i])) {
4405: $rawParameter = $rawParameters[$i];
4406: } elseif (count($rawParameters) > 0 && $parametersAcceptors[0]->isVariadic()) {
4407: $rawParameter = array_last($rawParameters);
4408: }
4409: }
4410:
4411: if ($rawParameter !== null) {
4412: $scope = $scope->pushInFunctionCall(null, $rawParameter, false);
4413: }
4414:
4415: return $scope->getType($closureExpr);
4416: }
4417:
4418: /**
4419: * Whether processing this argument consumes the generic-RESOLVED parameter
4420: * type: a closure/arrow function does - its parameters and body scope are
4421: * typed from the resolved callable(T) - whether it IS the argument or is
4422: * nested anywhere inside it (the enclosing parameter is pushed on the
4423: * in-function-call stack and the nested closure types itself from there).
4424: * Every other argument only reads variant-stable facts off its parameter.
4425: */
4426: private function argConsumesResolvedParameterType(Expr $value): bool
4427: {
4428: if ($value instanceof Expr\Closure || $value instanceof Expr\ArrowFunction) {
4429: return true;
4430: }
4431:
4432: // cached on the node - args are re-processed across convergence passes
4433: $cached = $value->getAttribute('phpstanArgContainsClosure');
4434: if ($cached !== null) {
4435: return $cached;
4436: }
4437:
4438: $contains = (new NodeFinder())->findFirst(
4439: [$value],
4440: static fn (Node $node): bool => $node instanceof Expr\Closure || $node instanceof Expr\ArrowFunction,
4441: ) !== null;
4442: $value->setAttribute('phpstanArgContainsClosure', $contains);
4443:
4444: return $contains;
4445: }
4446:
4447: /**
4448: * @param MethodReflection|FunctionReflection|null $calleeReflection
4449: */
4450: private function callCallbackImmediately(?ParameterReflection $parameter, ?Type $parameterType, $calleeReflection): bool
4451: {
4452: $parameterCallableType = null;
4453: if ($parameterType !== null && $calleeReflection instanceof FunctionReflection) {
4454: $parameterCallableType = TypeUtils::findCallableType($parameterType);
4455: }
4456:
4457: if ($parameter instanceof ExtendedParameterReflection) {
4458: $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable();
4459: if ($parameterCallImmediately->maybe()) {
4460: $callCallbackImmediately = $parameterCallableType !== null;
4461: } else {
4462: $callCallbackImmediately = $parameterCallImmediately->yes();
4463: }
4464: } else {
4465: $callCallbackImmediately = $parameterCallableType !== null;
4466: }
4467:
4468: return $callCallbackImmediately;
4469: }
4470:
4471: /**
4472: * A callback passed as an argument escapes the current scope and may be invoked,
4473: * so its mutations have to invalidate the outer scope - unless the parameter is
4474: * explicitly marked as later-invoked, in which case the callback only runs after
4475: * the current function returns and its mutations are not visible here yet.
4476: */
4477: private function shouldInvalidateCallbackExpressions(?ParameterReflection $parameter): bool
4478: {
4479: if ($parameter instanceof ExtendedParameterReflection) {
4480: return !$parameter->isImmediatelyInvokedCallable()->no();
4481: }
4482:
4483: return true;
4484: }
4485:
4486: /**
4487: * @param MethodReflection|FunctionReflection|null $calleeReflection
4488: */
4489: private function getParameterTypeFromParameterClosureTypeExtension(CallLike $callLike, $calleeReflection, ParameterReflection $parameter, MutatingScope $scope): ?Type
4490: {
4491: if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) {
4492: foreach ($this->functionParameterClosureTypeExtensions->getAll() as $functionParameterClosureTypeExtension) {
4493: if ($functionParameterClosureTypeExtension->isFunctionSupported($calleeReflection, $parameter)) {
4494: return $functionParameterClosureTypeExtension->getTypeFromFunctionCall($calleeReflection, $callLike, $parameter, $scope);
4495: }
4496: }
4497: } elseif ($calleeReflection instanceof MethodReflection) {
4498: if ($callLike instanceof StaticCall) {
4499: foreach ($this->staticMethodParameterClosureTypeExtensions->getAll() as $staticMethodParameterClosureTypeExtension) {
4500: if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) {
4501: return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $callLike, $parameter, $scope);
4502: }
4503: }
4504: } elseif ($callLike instanceof New_ && $callLike->class instanceof Name) {
4505: $staticCall = new StaticCall(
4506: $callLike->class,
4507: new Identifier('__construct'),
4508: $callLike->getArgs(),
4509: );
4510: foreach ($this->staticMethodParameterClosureTypeExtensions->getAll() as $staticMethodParameterClosureTypeExtension) {
4511: if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) {
4512: return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $staticCall, $parameter, $scope);
4513: }
4514: }
4515: } elseif ($callLike instanceof MethodCall) {
4516: foreach ($this->methodParameterClosureTypeExtensions->getAll() as $methodParameterClosureTypeExtension) {
4517: if ($methodParameterClosureTypeExtension->isMethodSupported($calleeReflection, $parameter)) {
4518: return $methodParameterClosureTypeExtension->getTypeFromMethodCall($calleeReflection, $callLike, $parameter, $scope);
4519: }
4520: }
4521: }
4522: }
4523:
4524: return null;
4525: }
4526:
4527: /**
4528: * @param MethodReflection|FunctionReflection|null $calleeReflection
4529: */
4530: private function getParameterOutExtensionsType(CallLike $callLike, $calleeReflection, ParameterReflection $currentParameter, MutatingScope $scope): ?Type
4531: {
4532: $paramOutTypes = [];
4533: if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) {
4534: foreach ($this->functionParameterOutTypeExtensions->getAll() as $functionParameterOutTypeExtension) {
4535: if (!$functionParameterOutTypeExtension->isFunctionSupported($calleeReflection, $currentParameter)) {
4536: continue;
4537: }
4538:
4539: $resolvedType = $functionParameterOutTypeExtension->getParameterOutTypeFromFunctionCall($calleeReflection, $callLike, $currentParameter, $scope);
4540: if ($resolvedType === null) {
4541: continue;
4542: }
4543: $paramOutTypes[] = $resolvedType;
4544: }
4545: } elseif ($callLike instanceof MethodCall && $calleeReflection instanceof MethodReflection) {
4546: foreach ($this->methodParameterOutTypeExtensions->getAll() as $methodParameterOutTypeExtension) {
4547: if (!$methodParameterOutTypeExtension->isMethodSupported($calleeReflection, $currentParameter)) {
4548: continue;
4549: }
4550:
4551: $resolvedType = $methodParameterOutTypeExtension->getParameterOutTypeFromMethodCall($calleeReflection, $callLike, $currentParameter, $scope);
4552: if ($resolvedType === null) {
4553: continue;
4554: }
4555: $paramOutTypes[] = $resolvedType;
4556: }
4557: } elseif ($callLike instanceof StaticCall && $calleeReflection instanceof MethodReflection) {
4558: foreach ($this->staticMethodParameterOutTypeExtensions->getAll() as $staticMethodParameterOutTypeExtension) {
4559: if (!$staticMethodParameterOutTypeExtension->isStaticMethodSupported($calleeReflection, $currentParameter)) {
4560: continue;
4561: }
4562:
4563: $resolvedType = $staticMethodParameterOutTypeExtension->getParameterOutTypeFromStaticMethodCall($calleeReflection, $callLike, $currentParameter, $scope);
4564: if ($resolvedType === null) {
4565: continue;
4566: }
4567: $paramOutTypes[] = $resolvedType;
4568: }
4569: }
4570:
4571: if (count($paramOutTypes) === 1) {
4572: return $paramOutTypes[0];
4573: }
4574:
4575: if (count($paramOutTypes) > 1) {
4576: return TypeCombinator::union(...$paramOutTypes);
4577: }
4578:
4579: return null;
4580: }
4581:
4582: /**
4583: * @param callable(Node $node, Scope $scope): void $nodeCallback
4584: */
4585: public function processVirtualAssign(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, Expr $var, Expr $assignedExpr, callable $nodeCallback): ExpressionResult
4586: {
4587: $assignHandler = $this->container->getByType(AssignHandler::class);
4588: $virtualAssignNodeCallback = VirtualAssignNodeCallback::create($nodeCallback);
4589: $target = $assignHandler->prepareTarget(
4590: $this,
4591: $scope,
4592: $storage,
4593: $stmt,
4594: $var,
4595: $assignedExpr,
4596: $virtualAssignNodeCallback,
4597: ExpressionContext::createDeep(),
4598: AssignTargetWalkMode::virtualAssign(),
4599: );
4600:
4601: return $assignHandler->applyWrite(
4602: $this,
4603: $target,
4604: $this->expressionResultFactory->create($target->getScope(), beforeScope: $target->getScope(), expr: $assignedExpr, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []),
4605: $stmt,
4606: $storage,
4607: $virtualAssignNodeCallback,
4608: ExpressionContext::createDeep(),
4609: );
4610: }
4611:
4612: /**
4613: * @param callable(Node $node, Scope $scope): void $nodeCallback
4614: */
4615: public function processStmtVarAnnotation(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, ?Expr $defaultExpr, callable $nodeCallback): MutatingScope
4616: {
4617: $function = $scope->getFunction();
4618: $variableLessTags = [];
4619:
4620: foreach ($stmt->getComments() as $comment) {
4621: if (!$comment instanceof Doc) {
4622: continue;
4623: }
4624:
4625: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
4626: $scope->getFile(),
4627: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
4628: $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
4629: $function !== null ? $function->getName() : null,
4630: $comment->getText(),
4631: );
4632:
4633: $assignedVariable = null;
4634: if (
4635: $stmt instanceof Node\Stmt\Expression
4636: && ($stmt->expr instanceof Assign || $stmt->expr instanceof AssignRef)
4637: && $stmt->expr->var instanceof Variable
4638: && is_string($stmt->expr->var->name)
4639: ) {
4640: $assignedVariable = $stmt->expr->var->name;
4641: }
4642:
4643: foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) {
4644: if (is_int($name)) {
4645: $variableLessTags[] = $varTag;
4646: continue;
4647: }
4648:
4649: if ($name === $assignedVariable) {
4650: continue;
4651: }
4652:
4653: $certainty = $scope->hasVariableType($name);
4654: if ($certainty->no()) {
4655: continue;
4656: }
4657:
4658: if ($scope->isInClass() && $scope->getFunction() === null) {
4659: continue;
4660: }
4661:
4662: if ($scope->canAnyVariableExist()) {
4663: $certainty = TrinaryLogic::createYes();
4664: }
4665:
4666: $variableNode = new Variable($name, $stmt->getAttributes());
4667: $originalType = $scope->getVariableType($name);
4668: if (!$originalType->equals($varTag->getType())) {
4669: $this->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $variableNode), $scope, $storage);
4670: }
4671:
4672: $scope = $scope->assignVariable(
4673: $name,
4674: $varTag->getType(),
4675: $scope->getNativeType($variableNode),
4676: $certainty,
4677: );
4678: }
4679: }
4680:
4681: if (count($variableLessTags) === 1 && $defaultExpr !== null) {
4682: $originalType = $scope->getType($defaultExpr);
4683: $varTag = $variableLessTags[0];
4684: if (!$originalType->equals($varTag->getType())) {
4685: $this->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope, $storage);
4686: }
4687: $scope = $scope->assignExpression($defaultExpr, $varTag->getType(), new MixedType());
4688: }
4689:
4690: return $scope;
4691: }
4692:
4693: /**
4694: * @param array<int, string> $variableNames
4695: */
4696: public function processVarAnnotation(MutatingScope $scope, array $variableNames, Node\Stmt $node, bool &$changed = false): MutatingScope
4697: {
4698: $function = $scope->getFunction();
4699: $varTags = [];
4700: foreach ($node->getComments() as $comment) {
4701: if (!$comment instanceof Doc) {
4702: continue;
4703: }
4704:
4705: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
4706: $scope->getFile(),
4707: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
4708: $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
4709: $function !== null ? $function->getName() : null,
4710: $comment->getText(),
4711: );
4712: foreach ($resolvedPhpDoc->getVarTags() as $key => $varTag) {
4713: $varTags[$key] = $varTag;
4714: }
4715: }
4716:
4717: if (count($varTags) === 0) {
4718: return $scope;
4719: }
4720:
4721: foreach ($variableNames as $variableName) {
4722: if (!isset($varTags[$variableName])) {
4723: continue;
4724: }
4725:
4726: $variableType = $varTags[$variableName]->getType();
4727: $changed = true;
4728: $scope = $scope->assignVariable($variableName, $variableType, new MixedType(), TrinaryLogic::createYes());
4729: }
4730:
4731: if (count($variableNames) === 1 && count($varTags) === 1 && isset($varTags[0])) {
4732: $variableType = $varTags[0]->getType();
4733: $changed = true;
4734: $scope = $scope->assignVariable($variableNames[0], $variableType, new MixedType(), TrinaryLogic::createYes());
4735: }
4736:
4737: return $scope;
4738: }
4739:
4740: /**
4741: * @return array{bodyScope: MutatingScope, endScope: MutatingScope, totalKeys: int}|null
4742: */
4743: private function tryProcessUnrolledConstantArrayForeach(
4744: Foreach_ $stmt,
4745: MutatingScope $originalScope,
4746: ExpressionResultStorage $originalStorage,
4747: StatementContext $context,
4748: Type $iterateeType,
4749: Type $nativeIterateeType,
4750: ): ?array
4751: {
4752: if ($stmt->byRef) {
4753: return null;
4754: }
4755: if (!($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name))) {
4756: return null;
4757: }
4758: if ($stmt->keyVar !== null && !($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name))) {
4759: return null;
4760: }
4761:
4762: if (!$iterateeType->isConstantArray()->yes()) {
4763: return null;
4764: }
4765: $constantArrays = $iterateeType->getConstantArrays();
4766: if (count($constantArrays) === 0) {
4767: return null;
4768: }
4769:
4770: $totalKeys = 0;
4771: $hasUnsealed = false;
4772: foreach ($constantArrays as $constantArray) {
4773: $totalKeys += count($constantArray->getKeyTypes());
4774: if (!$constantArray->isUnsealed()->yes()) {
4775: continue;
4776: }
4777: $hasUnsealed = true;
4778: }
4779: if ($totalKeys === 0 || $totalKeys > self::FOREACH_UNROLL_LIMIT) {
4780: return null;
4781: }
4782: $foreachUnrollFactor = $context->getForeachUnrollFactor();
4783: if ($foreachUnrollFactor > 1 && $foreachUnrollFactor * $totalKeys > self::FOREACH_UNROLL_NESTED_LIMIT) {
4784: return null;
4785: }
4786:
4787: $nativeConstantArrays = $nativeIterateeType->getConstantArrays();
4788: $matchedNativeArrays = count($nativeConstantArrays) === count($constantArrays) ? $nativeConstantArrays : null;
4789:
4790: $valueVarName = $stmt->valueVar->name;
4791: $keyVarName = $stmt->keyVar instanceof Variable ? $stmt->keyVar->name : null;
4792:
4793: $allBodyScopes = [];
4794: $allChainScopes = [];
4795: $allBreakScopes = [];
4796:
4797: $bodyContext = $context->enterUnrolledForeach($totalKeys);
4798:
4799: foreach ($constantArrays as $arrayIndex => $constantArray) {
4800: $keyTypes = $constantArray->getKeyTypes();
4801: $valueTypes = $constantArray->getValueTypes();
4802: if (count($keyTypes) === 0) {
4803: continue;
4804: }
4805:
4806: $nativeConstantArray = $matchedNativeArrays !== null ? $matchedNativeArrays[$arrayIndex] : null;
4807: $optionalKeys = array_fill_keys($constantArray->getOptionalKeys(), true);
4808:
4809: $chainScope = $originalScope;
4810: $entryScopes = [];
4811:
4812: foreach ($keyTypes as $i => $keyType) {
4813: $valueType = $valueTypes[$i];
4814: $isOptional = isset($optionalKeys[$i]);
4815:
4816: $nativeKeyType = $nativeConstantArray !== null && isset($nativeConstantArray->getKeyTypes()[$i])
4817: ? $nativeConstantArray->getKeyTypes()[$i]
4818: : $keyType;
4819: $nativeValueType = $nativeConstantArray !== null && isset($nativeConstantArray->getValueTypes()[$i])
4820: ? $nativeConstantArray->getValueTypes()[$i]
4821: : $valueType;
4822:
4823: $iterScope = $chainScope->assignVariable(
4824: $valueVarName,
4825: $valueType,
4826: $nativeValueType,
4827: TrinaryLogic::createYes(),
4828: );
4829: $iterScope = $iterScope->assignExpression(
4830: new OriginalForeachValueExpr($valueVarName),
4831: $valueType,
4832: $nativeValueType,
4833: );
4834: if ($keyVarName !== null) {
4835: $iterScope = $iterScope->assignVariable(
4836: $keyVarName,
4837: $keyType,
4838: $nativeKeyType,
4839: TrinaryLogic::createYes(),
4840: );
4841: $iterScope = $iterScope->assignExpression(
4842: new OriginalForeachKeyExpr($keyVarName),
4843: $keyType,
4844: $nativeKeyType,
4845: );
4846: $iterScope = $iterScope->assignExpression(
4847: new ArrayDimFetch($stmt->expr, $stmt->keyVar),
4848: $valueType,
4849: $nativeValueType,
4850: );
4851: }
4852:
4853: $entryScopes[] = $iterScope;
4854:
4855: $iterStorage = $originalStorage->duplicate();
4856: $bodyResult = $this->processStmtNodesInternal(
4857: $stmt,
4858: $stmt->stmts,
4859: $iterScope,
4860: $iterStorage,
4861: new NoopNodeCallback(),
4862: $bodyContext,
4863: )->filterOutLoopExitPoints();
4864:
4865: $iterEndScope = $bodyResult->getScope();
4866: foreach ($bodyResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
4867: $iterEndScope = $iterEndScope->mergeWith($continueExitPoint->getScope());
4868: }
4869: foreach ($bodyResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
4870: $allBreakScopes[] = $breakExitPoint->getScope();
4871: }
4872:
4873: if ($isOptional) {
4874: $chainScope = $iterEndScope->mergeWith($chainScope);
4875: } else {
4876: $chainScope = $iterEndScope;
4877: }
4878: }
4879:
4880: $arrayBodyScope = $entryScopes[0];
4881: for ($i = 1, $c = count($entryScopes); $i < $c; $i++) {
4882: $arrayBodyScope = $arrayBodyScope->mergeWith($entryScopes[$i]);
4883: }
4884: if (count($entryScopes) === 1) {
4885: $arrayBodyScope = $arrayBodyScope->mergeWith($chainScope);
4886: }
4887:
4888: $allBodyScopes[] = $arrayBodyScope;
4889: $allChainScopes[] = $chainScope;
4890: }
4891:
4892: if ($allBodyScopes === []) {
4893: return null;
4894: }
4895:
4896: $bodyScope = $allBodyScopes[0];
4897: for ($i = 1, $c = count($allBodyScopes); $i < $c; $i++) {
4898: $bodyScope = $bodyScope->mergeWith($allBodyScopes[$i]);
4899: }
4900:
4901: $endScope = $allChainScopes[0];
4902: for ($i = 1, $c = count($allChainScopes); $i < $c; $i++) {
4903: $endScope = $endScope->mergeWith($allChainScopes[$i]);
4904: }
4905:
4906: foreach ($allBreakScopes as $breakScope) {
4907: $endScope = $endScope->mergeWith($breakScope);
4908: }
4909:
4910: // Unsealed shapes describe zero-or-more additional entries beyond the
4911: // explicit keys. Run the scope-generalizing loop on top of the
4912: // unrolled explicit iterations so body-scope variables (e.g. counters)
4913: // account for the extra iterations while keeping the lower bound
4914: // established by the non-optional explicit keys.
4915: if ($hasUnsealed) {
4916: $loopScope = $endScope;
4917: $count = 0;
4918: do {
4919: $prevLoopScope = $loopScope;
4920: $iterStorage = $originalStorage->duplicate();
4921: $iterBodyScope = $loopScope->mergeWith($endScope);
4922: $iterBodyScope = $this->enterForeach($iterBodyScope, $iterStorage, $originalScope, $stmt, $iterateeType, $nativeIterateeType, new NoopNodeCallback());
4923: $iterBodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $iterBodyScope, $iterStorage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints();
4924: $loopScope = $iterBodyScopeResult->getScope();
4925: foreach ($iterBodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
4926: $loopScope = $loopScope->mergeWith($continueExitPoint->getScope());
4927: }
4928: foreach ($iterBodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
4929: $endScope = $endScope->mergeWith($breakExitPoint->getScope());
4930: }
4931: $bodyScope = $bodyScope->mergeWith($loopScope);
4932: if ($loopScope->equals($prevLoopScope)) {
4933: break;
4934: }
4935: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
4936: $loopScope = $prevLoopScope->generalizeWith($loopScope);
4937: }
4938: $count++;
4939: } while ($count < self::LOOP_SCOPE_ITERATIONS);
4940:
4941: $endScope = $endScope->mergeWith($loopScope);
4942: }
4943:
4944: return ['bodyScope' => $bodyScope, 'endScope' => $endScope, 'totalKeys' => $totalKeys];
4945: }
4946:
4947: private function getTraversableForeachThrowPoint(MutatingScope $scope, Expr $iteratee): ?InternalThrowPoint
4948: {
4949: $exprType = $scope->getType($iteratee);
4950: $traversableType = new ObjectType(Traversable::class);
4951:
4952: if ($traversableType->isSuperTypeOf($exprType)->no()) {
4953: return null;
4954: }
4955:
4956: $traversablePart = TypeCombinator::intersect($exprType, $traversableType);
4957: $iteratorAggregateType = new ObjectType(IteratorAggregate::class);
4958:
4959: if ($iteratorAggregateType->isSuperTypeOf($traversablePart)->yes()
4960: && $traversablePart->hasMethod('getIterator')->yes()) {
4961: $method = $traversablePart->getMethod('getIterator', $scope);
4962: $throwType = $method->getThrowType();
4963: if ($throwType !== null) {
4964: if ($throwType->isVoid()->yes()) {
4965: return null;
4966: }
4967: return InternalThrowPoint::createExplicit($scope, $throwType, $iteratee, true);
4968: }
4969:
4970: if (!$this->implicitThrows) {
4971: return null;
4972: }
4973: }
4974:
4975: return InternalThrowPoint::createImplicit($scope, $iteratee);
4976: }
4977:
4978: /**
4979: * @param callable(Node $node, Scope $scope): void $nodeCallback
4980: */
4981: private function enterForeach(MutatingScope $scope, ExpressionResultStorage $storage, MutatingScope $originalScope, Foreach_ $stmt, Type $iterateeType, Type $nativeIterateeType, callable $nodeCallback): MutatingScope
4982: {
4983: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
4984: $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt);
4985: }
4986:
4987: if (
4988: ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name))
4989: && ($stmt->keyVar === null || ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)))
4990: ) {
4991: $keyVarName = $stmt->keyVar instanceof Variable ? $stmt->keyVar->name : null;
4992: $scope = $scope->enterForeach(
4993: $originalScope,
4994: $stmt->expr,
4995: $iterateeType,
4996: $nativeIterateeType,
4997: $stmt->valueVar->name,
4998: $keyVarName,
4999: $stmt->byRef,
5000: );
5001: $vars = [$stmt->valueVar->name];
5002: if ($keyVarName !== null) {
5003: $vars[] = $keyVarName;
5004: }
5005: } else {
5006: $scope = $this->processVirtualAssign(
5007: $scope,
5008: $storage,
5009: $stmt,
5010: $stmt->valueVar,
5011: new NativeTypeExpr(
5012: $originalScope->getIterableValueType($iterateeType),
5013: $originalScope->getIterableValueType($nativeIterateeType),
5014: ),
5015: $nodeCallback,
5016: )->getScope();
5017: $vars = $this->getAssignedVariables($stmt->valueVar);
5018: if (
5019: $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)
5020: ) {
5021: $scope = $scope->enterForeachKey($originalScope, $stmt->expr, $iterateeType, $nativeIterateeType, $stmt->keyVar->name);
5022: $vars[] = $stmt->keyVar->name;
5023: } elseif ($stmt->keyVar !== null) {
5024: $scope = $this->processVirtualAssign(
5025: $scope,
5026: $storage,
5027: $stmt,
5028: $stmt->keyVar,
5029: new NativeTypeExpr(
5030: $originalScope->getIterableKeyType($iterateeType),
5031: $originalScope->getIterableKeyType($nativeIterateeType),
5032: ),
5033: $nodeCallback,
5034: )->getScope();
5035: $vars = array_merge($vars, $this->getAssignedVariables($stmt->keyVar));
5036: }
5037:
5038: if ($stmt->valueVar instanceof List_) {
5039: $scope = $this->addDestructureTaggedUnionConditionalHolders(
5040: $scope,
5041: $originalScope->getIterableValueType($iterateeType),
5042: $stmt->valueVar,
5043: );
5044: }
5045: }
5046:
5047: $constantArrays = $iterateeType->getConstantArrays();
5048: if (
5049: $stmt->getDocComment() === null
5050: && $iterateeType->isConstantArray()->yes()
5051: && count($constantArrays) === 1
5052: && $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)
5053: && $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)
5054: ) {
5055: $valueConditionalHolders = [];
5056: $arrayDimFetchConditionalHolders = [];
5057: foreach ($constantArrays[0]->getKeyTypes() as $i => $keyType) {
5058: $valueType = $constantArrays[0]->getValueTypes()[$i];
5059: $keyExpressionTypeHolder = ExpressionTypeHolder::createYes(new Variable($stmt->keyVar->name), $keyType);
5060:
5061: $holder = new ConditionalExpressionHolder([
5062: '$' . $stmt->keyVar->name => $keyExpressionTypeHolder,
5063: ], ExpressionTypeHolder::createYes($stmt->valueVar, $valueType));
5064: $valueConditionalHolders[$holder->getKey()] = $holder;
5065: $arrayDimFetchHolder = new ConditionalExpressionHolder([
5066: '$' . $stmt->keyVar->name => $keyExpressionTypeHolder,
5067: ], ExpressionTypeHolder::createYes(new ArrayDimFetch($stmt->expr, $stmt->keyVar), $valueType));
5068: $arrayDimFetchConditionalHolders[$arrayDimFetchHolder->getKey()] = $arrayDimFetchHolder;
5069: }
5070:
5071: $scope = $scope->addConditionalExpressions(
5072: '$' . $stmt->valueVar->name,
5073: $valueConditionalHolders,
5074: );
5075: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
5076: $scope = $scope->addConditionalExpressions(
5077: sprintf('$%s[$%s]', $stmt->expr->name, $stmt->keyVar->name),
5078: $arrayDimFetchConditionalHolders,
5079: );
5080: }
5081: }
5082:
5083: if (
5084: $stmt->expr instanceof FuncCall
5085: && $stmt->expr->name instanceof Name
5086: && !$stmt->expr->isFirstClassCallable()
5087: && $stmt->expr->name->toLowerString() === 'array_keys'
5088: && $stmt->valueVar instanceof Variable
5089: ) {
5090: $args = $stmt->expr->getArgs();
5091: if (count($args) >= 1) {
5092: $arrayArg = $args[0]->value;
5093: $scope = $scope->assignExpression(
5094: new ArrayDimFetch($arrayArg, $stmt->valueVar),
5095: $scope->getType($arrayArg)->getIterableValueType(),
5096: $scope->getNativeType($arrayArg)->getIterableValueType(),
5097: );
5098: }
5099: }
5100:
5101: return $this->processVarAnnotation($scope, $vars, $stmt);
5102: }
5103:
5104: /**
5105: * When destructuring an iterable whose value type is a tagged union of
5106: * constant arrays — e.g. `array<array{null, int}|array{int, null}>` — the
5107: * variants describe a relationship between the destructured variables that
5108: * a per-variable narrowing would normally lose: knowing `$x === null` should
5109: * imply `$y === int`, but `foreach ($a as [$x, $y])` assigns `$x` and `$y`
5110: * independently, so each ends up as the union (`int|null`) and the link is
5111: * dropped.
5112: *
5113: * Recover the link by storing conditional-expression holders on each
5114: * destructured variable: for every variant, "when this variable matches the
5115: * variant's value at its position, the other variables match the variant's
5116: * values at their positions". A later `if ($x === null)` then fires the
5117: * matching holder and narrows `$y` accordingly.
5118: *
5119: * Only handles flat positional / keyed destructure patterns (List_) where
5120: * each item's target is a plain Variable; nested destructure is left for
5121: * the regular per-variable type tracking.
5122: */
5123: private function addDestructureTaggedUnionConditionalHolders(
5124: MutatingScope $scope,
5125: Type $iterableValueType,
5126: List_ $list,
5127: ): MutatingScope
5128: {
5129: $constantArrays = $iterableValueType->getConstantArrays();
5130: if (count($constantArrays) < 2) {
5131: return $scope;
5132: }
5133:
5134: // Collect each list item's array-key value and target variable.
5135: $items = [];
5136: foreach ($list->items as $position => $item) {
5137: if ($item === null) {
5138: continue;
5139: }
5140: if (!$item->value instanceof Variable || !is_string($item->value->name)) {
5141: return $scope;
5142: }
5143: if ($item->key === null) {
5144: $keyValue = $position;
5145: } elseif ($item->key instanceof Node\Scalar\String_) {
5146: $keyValue = $item->key->value;
5147: } elseif ($item->key instanceof Node\Scalar\Int_) {
5148: $keyValue = $item->key->value;
5149: } else {
5150: return $scope;
5151: }
5152: $items[] = ['key' => $keyValue, 'name' => $item->value->name];
5153: }
5154:
5155: if (count($items) < 2) {
5156: return $scope;
5157: }
5158:
5159: // For every variant, every item must have a matching key with a single
5160: // value type at it; otherwise the variants don't all describe the same
5161: // destructure shape and we can't form a sound holder set.
5162: $variantValuesByItem = [];
5163: foreach ($items as $itemIdx => $itemInfo) {
5164: $variantValuesByItem[$itemIdx] = [];
5165: foreach ($constantArrays as $variantIdx => $variant) {
5166: $keyType = is_int($itemInfo['key']) ? new ConstantIntegerType($itemInfo['key']) : new ConstantStringType($itemInfo['key']);
5167: if (!$variant->hasOffsetValueType($keyType)->yes()) {
5168: return $scope;
5169: }
5170: $variantValuesByItem[$itemIdx][$variantIdx] = $variant->getOffsetValueType($keyType);
5171: }
5172: }
5173:
5174: // For each item × variant, build a holder: "when item is variant's value
5175: // at this position, the *other* items are the variant's values at their
5176: // positions". Skip the variant if the condition value is too wide to be
5177: // a useful discriminator (i.e. equal to the union of all the variant
5178: // values at this position — narrowing it back wouldn't pick a variant).
5179: foreach ($items as $itemIdx => $itemInfo) {
5180: $exprString = '$' . $itemInfo['name'];
5181: $variantConditionTypes = $variantValuesByItem[$itemIdx];
5182: $itemUnionType = TypeCombinator::union(...array_values($variantConditionTypes));
5183: $holders = [];
5184: foreach (array_keys($constantArrays) as $variantIdx) {
5185: $conditionType = $variantConditionTypes[$variantIdx];
5186: if ($conditionType->equals($itemUnionType)) {
5187: continue;
5188: }
5189: $conditions = [
5190: $exprString => ExpressionTypeHolder::createYes(new Variable($itemInfo['name']), $conditionType),
5191: ];
5192: foreach ($items as $otherIdx => $otherInfo) {
5193: if ($otherIdx === $itemIdx) {
5194: continue;
5195: }
5196: $otherType = $variantValuesByItem[$otherIdx][$variantIdx];
5197: $holder = new ConditionalExpressionHolder(
5198: $conditions,
5199: ExpressionTypeHolder::createYes(new Variable($otherInfo['name']), $otherType),
5200: );
5201: $holders['$' . $otherInfo['name']][$holder->getKey()] = $holder;
5202: }
5203: }
5204:
5205: foreach ($holders as $targetExprString => $targetHolders) {
5206: $scope = $scope->addConditionalExpressions($targetExprString, $targetHolders);
5207: }
5208: }
5209:
5210: return $scope;
5211: }
5212:
5213: /**
5214: * @param callable(Node $node, Scope $scope): void $nodeCallback
5215: */
5216: private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classScope, ExpressionResultStorage $storage, callable $nodeCallback): void
5217: {
5218: $parentTraitNames = [];
5219: $parent = $classScope->getParentScope();
5220: while ($parent !== null) {
5221: if ($parent->isInTrait()) {
5222: $parentTraitNames[] = $parent->getTraitReflection()->getName();
5223: }
5224: $parent = $parent->getParentScope();
5225: }
5226:
5227: foreach ($node->traits as $trait) {
5228: $traitName = (string) $trait;
5229: if (in_array($traitName, $parentTraitNames, true)) {
5230: continue;
5231: }
5232: if (!$this->reflectionProvider->hasClass($traitName)) {
5233: continue;
5234: }
5235: $traitReflection = $this->reflectionProvider->getClass($traitName);
5236: $traitFileName = $traitReflection->getFileName();
5237: if ($traitFileName === null) {
5238: continue; // trait from eval or from PHP itself
5239: }
5240: $fileName = $this->fileHelper->normalizePath($traitFileName);
5241: if (!isset($this->analysedFiles[$fileName])) {
5242: continue;
5243: }
5244: $adaptations = [];
5245: foreach ($node->adaptations as $adaptation) {
5246: if ($adaptation->trait === null) {
5247: $adaptations[] = $adaptation;
5248: continue;
5249: }
5250: if ($adaptation->trait->toLowerString() !== $trait->toLowerString()) {
5251: continue;
5252: }
5253:
5254: $adaptations[] = $adaptation;
5255: }
5256: $parserNodes = $this->parser->parseFile($fileName);
5257: $this->processNodesForTraitUse($parserNodes, $traitReflection, $classScope, $storage, $adaptations, $nodeCallback);
5258: }
5259: }
5260:
5261: /**
5262: * @param Node[]|Node|scalar|null $node
5263: * @param Node\Stmt\TraitUseAdaptation[] $adaptations
5264: * @param callable(Node $node, Scope $scope): void $nodeCallback
5265: */
5266: private function processNodesForTraitUse($node, ClassReflection $traitReflection, MutatingScope $scope, ExpressionResultStorage $storage, array $adaptations, callable $nodeCallback): void
5267: {
5268: if ($node instanceof Node) {
5269: if ($node instanceof Node\Stmt\Trait_ && $traitReflection->getName() === (string) $node->namespacedName && $traitReflection->getNativeReflection()->getStartLine() === $node->getStartLine()) {
5270: $methodModifiers = [];
5271: $methodNames = [];
5272: foreach ($adaptations as $adaptation) {
5273: if (!$adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
5274: continue;
5275: }
5276:
5277: $methodName = $adaptation->method->toLowerString();
5278: if ($adaptation->newModifier !== null) {
5279: $methodModifiers[$methodName] = $adaptation->newModifier;
5280: }
5281:
5282: if ($adaptation->newName === null) {
5283: continue;
5284: }
5285:
5286: $methodNames[$methodName] = $adaptation->newName;
5287: }
5288:
5289: $stmts = $node->stmts;
5290: foreach ($stmts as $i => $stmt) {
5291: if (!$stmt instanceof Node\Stmt\ClassMethod) {
5292: continue;
5293: }
5294: $methodName = $stmt->name->toLowerString();
5295: $methodAst = clone $stmt;
5296: $stmts[$i] = $methodAst;
5297: if (array_key_exists($methodName, $methodModifiers)) {
5298: $methodAst->flags = ($methodAst->flags & ~ Modifiers::VISIBILITY_MASK) | $methodModifiers[$methodName];
5299: }
5300:
5301: if (!array_key_exists($methodName, $methodNames)) {
5302: continue;
5303: }
5304:
5305: $methodAst->setAttribute('originalTraitMethodName', $methodAst->name->toLowerString());
5306: $methodAst->name = $methodNames[$methodName];
5307: }
5308:
5309: if (!$scope->isInClass()) {
5310: throw new ShouldNotHappenException();
5311: }
5312: $traitScope = $scope->enterTrait($traitReflection);
5313:
5314: // attribute args are not processed as part of the trait statements
5315: // but rules like TraitAttributesRule ask about their types
5316: $this->processAttributeGroups($node, $node->attrGroups, $traitScope, $storage, new NoopNodeCallback());
5317:
5318: $this->callNodeCallback($nodeCallback, new InTraitNode($node, $traitReflection, $scope->getClassReflection()), $traitScope, $storage);
5319: $this->processStmtNodesInternal($node, $stmts, $traitScope, $storage, $nodeCallback, StatementContext::createTopLevel());
5320: return;
5321: }
5322: if ($node instanceof Node\Stmt\ClassLike) {
5323: return;
5324: }
5325: if ($node instanceof Node\FunctionLike) {
5326: return;
5327: }
5328: foreach ($node->getSubNodeNames() as $subNodeName) {
5329: $subNode = $node->{$subNodeName};
5330: $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $storage, $adaptations, $nodeCallback);
5331: }
5332: } elseif (is_array($node)) {
5333: foreach ($node as $subNode) {
5334: $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $storage, $adaptations, $nodeCallback);
5335: }
5336: }
5337: }
5338:
5339: public function processCalledMethod(MethodReflection $methodReflection): ?MutatingScope
5340: {
5341: $declaringClass = $methodReflection->getDeclaringClass();
5342: if ($declaringClass->isAnonymous()) {
5343: return null;
5344: }
5345: if ($declaringClass->getFileName() === null) {
5346: return null;
5347: }
5348:
5349: $stackName = sprintf('%s::%s', $declaringClass->getName(), $methodReflection->getName());
5350: if (array_key_exists($stackName, $this->calledMethodResults)) {
5351: return $this->calledMethodResults[$stackName];
5352: }
5353:
5354: if (array_key_exists($stackName, $this->calledMethodStack)) {
5355: return null;
5356: }
5357:
5358: if (count($this->calledMethodStack) > 0) {
5359: return null;
5360: }
5361:
5362: $this->calledMethodStack[$stackName] = true;
5363:
5364: $fileName = $this->fileHelper->normalizePath($declaringClass->getFileName());
5365: if (!isset($this->analysedFiles[$fileName])) {
5366: unset($this->calledMethodStack[$stackName]);
5367: return null;
5368: }
5369: $parserNodes = $this->parser->parseFile($fileName);
5370:
5371: $returnStatement = null;
5372: $this->processNodesForCalledMethod($parserNodes, new ExpressionResultStorage(), $fileName, $methodReflection, static function (Node $node, Scope $scope) use ($methodReflection, &$returnStatement): void {
5373: if (!$node instanceof MethodReturnStatementsNode) {
5374: return;
5375: }
5376:
5377: if ($node->getClassReflection()->getName() !== $methodReflection->getDeclaringClass()->getName()) {
5378: return;
5379: }
5380:
5381: if ($returnStatement !== null) {
5382: return;
5383: }
5384:
5385: $returnStatement = $node;
5386: });
5387:
5388: $calledMethodEndScope = null;
5389: if ($returnStatement !== null) {
5390: foreach ($returnStatement->getExecutionEnds() as $executionEnd) {
5391: $statementResult = $executionEnd->getStatementResult();
5392: $endNode = $executionEnd->getNode();
5393: if ($endNode instanceof Node\Stmt\Expression) {
5394: $exprType = $statementResult->getScope()->getType($endNode->expr);
5395: if ($exprType instanceof NeverType && $exprType->isExplicit()) {
5396: continue;
5397: }
5398: }
5399: if ($calledMethodEndScope === null) {
5400: $calledMethodEndScope = $statementResult->getScope();
5401: continue;
5402: }
5403:
5404: $calledMethodEndScope = $calledMethodEndScope->mergeWith($statementResult->getScope());
5405: }
5406: foreach ($returnStatement->getReturnStatements() as $statement) {
5407: if ($calledMethodEndScope === null) {
5408: $calledMethodEndScope = $statement->getScope();
5409: continue;
5410: }
5411:
5412: $calledMethodEndScope = $calledMethodEndScope->mergeWith($statement->getScope());
5413: }
5414: }
5415:
5416: unset($this->calledMethodStack[$stackName]);
5417:
5418: $this->calledMethodResults[$stackName] = $calledMethodEndScope;
5419:
5420: return $calledMethodEndScope;
5421: }
5422:
5423: /**
5424: * @param Node[]|Node|scalar|null $node
5425: * @param callable(Node $node, Scope $scope): void $nodeCallback
5426: */
5427: private function processNodesForCalledMethod($node, ExpressionResultStorage $storage, string $fileName, MethodReflection $methodReflection, callable $nodeCallback): void
5428: {
5429: if ($node instanceof Node) {
5430: $declaringClass = $methodReflection->getDeclaringClass();
5431: if (
5432: $node instanceof Node\Stmt\Class_
5433: && isset($node->namespacedName)
5434: && $declaringClass->getName() === (string) $node->namespacedName
5435: && $declaringClass->getNativeReflection()->getStartLine() === $node->getStartLine()
5436: ) {
5437:
5438: $stmts = $node->stmts;
5439: foreach ($stmts as $stmt) {
5440: if (!$stmt instanceof Node\Stmt\ClassMethod) {
5441: continue;
5442: }
5443:
5444: if ($stmt->name->toString() !== $methodReflection->getName()) {
5445: continue;
5446: }
5447:
5448: if ($stmt->getEndLine() - $stmt->getStartLine() > 50) {
5449: continue;
5450: }
5451:
5452: $scope = $this->scopeFactory->create(ScopeContext::create($fileName))->enterClass($declaringClass);
5453: $this->processStmtNode($stmt, $scope, $storage, $nodeCallback, StatementContext::createTopLevel());
5454: }
5455: return;
5456: }
5457: if ($node instanceof Node\Stmt\ClassLike) {
5458: return;
5459: }
5460: if ($node instanceof Node\FunctionLike) {
5461: return;
5462: }
5463: foreach ($node->getSubNodeNames() as $subNodeName) {
5464: $subNode = $node->{$subNodeName};
5465: $this->processNodesForCalledMethod($subNode, $storage, $fileName, $methodReflection, $nodeCallback);
5466: }
5467: } elseif (is_array($node)) {
5468: foreach ($node as $subNode) {
5469: $this->processNodesForCalledMethod($subNode, $storage, $fileName, $methodReflection, $nodeCallback);
5470: }
5471: }
5472: }
5473:
5474: /**
5475: * @return array{TemplateTypeMap, array<string, Type>, array<string, bool>, array<string, Type>, ?Type, ?Type, ?string, bool, bool, bool, bool|null, bool, bool, string|null, Assertions, ?Type, array<string, Type>, array<(string|int), VarTag>, bool, ?ResolvedPhpDocBlock, array<string, bool>}
5476: */
5477: public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array
5478: {
5479: $templateTypeMap = TemplateTypeMap::createEmpty();
5480: $phpDocParameterTypes = [];
5481: $phpDocImmediatelyInvokedCallableParameters = [];
5482: $phpDocClosureThisTypeParameters = [];
5483: $phpDocReturnType = null;
5484: $phpDocThrowType = null;
5485: $deprecatedDescription = null;
5486: $isDeprecated = false;
5487: $isInternal = false;
5488: $isFinal = false;
5489: $isPure = null;
5490: $isAllowedPrivateMutation = false;
5491: $acceptsNamedArguments = true;
5492: $isReadOnly = $scope->isInClass() && $scope->getClassReflection()->isImmutable();
5493: $asserts = Assertions::createEmpty();
5494: $selfOutType = null;
5495: $docComment = $node->getDocComment() !== null
5496: ? $node->getDocComment()->getText()
5497: : null;
5498:
5499: $file = $scope->getFile();
5500: $class = $scope->isInClass() ? $scope->getClassReflection()->getName() : null;
5501: $trait = $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null;
5502: $resolvedPhpDoc = null;
5503: $functionName = null;
5504: $phpDocParameterOutTypes = [];
5505: $phpDocPureUnlessCallableIsImpureParameters = [];
5506:
5507: if ($node instanceof Node\Stmt\ClassMethod) {
5508: if (!$scope->isInClass()) {
5509: throw new ShouldNotHappenException();
5510: }
5511: $functionName = $node->name->name;
5512: $positionalParameterNames = array_map(static function (Node\Param $param): string {
5513: if (!$param->var instanceof Variable || !is_string($param->var->name)) {
5514: throw new ShouldNotHappenException();
5515: }
5516:
5517: return $param->var->name;
5518: }, $node->getParams());
5519: $currentResolvedPhpDoc = null;
5520: if ($docComment !== null) {
5521: $currentResolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
5522: $file,
5523: $class,
5524: $trait,
5525: $node->name->name,
5526: $docComment,
5527: );
5528: }
5529: $methodNameForInheritance = $node->getAttribute('originalTraitMethodName') ?? $node->name->name;
5530: $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod(
5531: $scope->getClassReflection(),
5532: $methodNameForInheritance,
5533: $currentResolvedPhpDoc,
5534: $positionalParameterNames,
5535: );
5536:
5537: if ($node->name->toLowerString() === '__construct') {
5538: foreach ($node->params as $param) {
5539: if ($param->flags === 0) {
5540: continue;
5541: }
5542:
5543: if ($param->getDocComment() === null) {
5544: continue;
5545: }
5546:
5547: if (
5548: !$param->var instanceof Variable
5549: || !is_string($param->var->name)
5550: ) {
5551: throw new ShouldNotHappenException();
5552: }
5553:
5554: $paramPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
5555: $file,
5556: $class,
5557: $trait,
5558: '__construct',
5559: $param->getDocComment()->getText(),
5560: );
5561: $varTags = $paramPhpDoc->getVarTags();
5562: if (isset($varTags[0]) && count($varTags) === 1) {
5563: $phpDocType = $varTags[0]->getType();
5564: } elseif (isset($varTags[$param->var->name])) {
5565: $phpDocType = $varTags[$param->var->name]->getType();
5566: } else {
5567: continue;
5568: }
5569:
5570: $phpDocParameterTypes[$param->var->name] = $phpDocType;
5571: }
5572: }
5573: } elseif ($node instanceof Node\Stmt\Function_) {
5574: $functionName = trim($scope->getNamespace() . '\\' . $node->name->name, '\\');
5575: } elseif ($node instanceof Node\PropertyHook) {
5576: $propertyName = $node->getAttribute('propertyName');
5577: if ($propertyName !== null) {
5578: $functionName = sprintf('$%s::%s', $propertyName, $node->name->toString());
5579: }
5580: }
5581:
5582: if ($docComment !== null && $resolvedPhpDoc === null) {
5583: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
5584: $file,
5585: $class,
5586: $trait,
5587: $functionName,
5588: $docComment,
5589: );
5590: }
5591:
5592: $varTags = [];
5593: if ($resolvedPhpDoc !== null) {
5594: $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap();
5595: $phpDocImmediatelyInvokedCallableParameters = $resolvedPhpDoc->getParamsImmediatelyInvokedCallable();
5596: foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) {
5597: if (array_key_exists($paramName, $phpDocParameterTypes)) {
5598: continue;
5599: }
5600: $paramType = $paramTag->getType();
5601: if ($scope->isInClass()) {
5602: $paramType = $this->transformStaticType($scope->getClassReflection(), $paramType);
5603: }
5604: $phpDocParameterTypes[$paramName] = $paramType;
5605: }
5606: foreach ($resolvedPhpDoc->getParamClosureThisTags() as $paramName => $paramClosureThisTag) {
5607: if (array_key_exists($paramName, $phpDocClosureThisTypeParameters)) {
5608: continue;
5609: }
5610: $paramClosureThisType = $paramClosureThisTag->getType();
5611: if ($scope->isInClass()) {
5612: $paramClosureThisType = $this->transformStaticType($scope->getClassReflection(), $paramClosureThisType);
5613: }
5614: $phpDocClosureThisTypeParameters[$paramName] = $paramClosureThisType;
5615: }
5616:
5617: foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) {
5618: $phpDocParameterOutTypes[$paramName] = $paramOutTag->getType();
5619: }
5620: if ($node instanceof Node\FunctionLike) {
5621: $nativeReturnType = $scope->getFunctionType($node->getReturnType(), false, false);
5622: $phpDocReturnType = $this->getPhpDocReturnType($resolvedPhpDoc, $nativeReturnType);
5623: if ($phpDocReturnType !== null && $scope->isInClass()) {
5624: $phpDocReturnType = $this->transformStaticType($scope->getClassReflection(), $phpDocReturnType);
5625: }
5626: }
5627: $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null;
5628: $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null;
5629: $isDeprecated = $resolvedPhpDoc->isDeprecated();
5630: $isInternal = $resolvedPhpDoc->isInternal();
5631: $isFinal = $resolvedPhpDoc->isFinal();
5632: $isPure = $resolvedPhpDoc->isPure();
5633: $isAllowedPrivateMutation = $resolvedPhpDoc->isAllowedPrivateMutation();
5634: $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments();
5635: $isReadOnly = $isReadOnly || $resolvedPhpDoc->isReadOnly();
5636: $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc);
5637: $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null;
5638: $varTags = $resolvedPhpDoc->getVarTags();
5639: $phpDocPureUnlessCallableIsImpureParameters = $resolvedPhpDoc->getParamsPureUnlessCallableIsImpure();
5640: }
5641:
5642: if ($acceptsNamedArguments && $scope->isInClass()) {
5643: $acceptsNamedArguments = $scope->getClassReflection()->acceptsNamedArguments();
5644: }
5645:
5646: if ($isPure === null && $node instanceof Node\FunctionLike && $scope->isInClass()) {
5647: $classResolvedPhpDoc = $scope->getClassReflection()->getResolvedPhpDoc();
5648: if ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsPure()) {
5649: if (
5650: strtolower($functionName ?? '') === '__construct'
5651: || (
5652: ($phpDocReturnType === null || !$phpDocReturnType->isVoid()->yes())
5653: && !$scope->getFunctionType($node->getReturnType(), false, false)->isVoid()->yes()
5654: )
5655: ) {
5656: $isPure = true;
5657: }
5658: } elseif ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsImpure()) {
5659: $isPure = false;
5660: }
5661: }
5662:
5663: return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation, $resolvedPhpDoc, $phpDocPureUnlessCallableIsImpureParameters];
5664: }
5665:
5666: private function transformStaticType(ClassReflection $declaringClass, Type $type): Type
5667: {
5668: return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($declaringClass): Type {
5669: if ($type instanceof StaticType) {
5670: $changedType = $type->changeBaseClass($declaringClass);
5671: if ($declaringClass->isFinal() && !$type instanceof ThisType) {
5672: $changedType = $changedType->getStaticObjectType();
5673: }
5674: return $traverse($changedType);
5675: }
5676:
5677: return $traverse($type);
5678: });
5679: }
5680:
5681: private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $nativeReturnType): ?Type
5682: {
5683: $returnTag = $resolvedPhpDoc->getReturnTag();
5684:
5685: if ($returnTag === null) {
5686: return null;
5687: }
5688:
5689: $phpDocReturnType = $returnTag->getType();
5690:
5691: if ($returnTag->isExplicit()) {
5692: return $phpDocReturnType;
5693: }
5694:
5695: if ($nativeReturnType->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocReturnType))->yes()) {
5696: return $phpDocReturnType;
5697: }
5698:
5699: if ($phpDocReturnType instanceof UnionType) {
5700: $types = [];
5701: foreach ($phpDocReturnType->getTypes() as $innerType) {
5702: if (!$nativeReturnType->isSuperTypeOf($innerType)->yes()) {
5703: continue;
5704: }
5705:
5706: $types[] = $innerType;
5707: }
5708:
5709: if (count($types) === 0) {
5710: return null;
5711: }
5712:
5713: return TypeCombinator::union(...$types);
5714: }
5715:
5716: return null;
5717: }
5718:
5719: /**
5720: * @param array<Node> $nodes
5721: * @return list<Node\Stmt>
5722: */
5723: private function getNextUnreachableStatements(array $nodes, bool $earlyBinding): array
5724: {
5725: $stmts = [];
5726: $isPassedUnreachableStatement = false;
5727: foreach ($nodes as $node) {
5728: if ($node instanceof Node\Stmt\Label) {
5729: break;
5730: }
5731: if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) {
5732: continue;
5733: }
5734: if ($isPassedUnreachableStatement && $node instanceof Node\Stmt) {
5735: $stmts[] = $node;
5736: continue;
5737: }
5738: if ($node instanceof Node\Stmt\Nop || $node instanceof Node\Stmt\InlineHTML) {
5739: continue;
5740: }
5741: if (!$node instanceof Node\Stmt) {
5742: continue;
5743: }
5744: $stmts[] = $node;
5745: $isPassedUnreachableStatement = true;
5746: }
5747: return $stmts;
5748: }
5749:
5750: private function inferForLoopExpressions(For_ $stmt, Expr $lastCondExpr, MutatingScope $bodyScope): MutatingScope
5751: {
5752: // infer $items[$i] type from for ($i = 0; $i < count($items); $i++) {...}
5753:
5754: if (
5755: // $i = 0
5756: count($stmt->init) === 1
5757: && $stmt->init[0] instanceof Assign
5758: && $stmt->init[0]->var instanceof Variable
5759: && $stmt->init[0]->expr instanceof Node\Scalar\Int_
5760: && $stmt->init[0]->expr->value === 0
5761: // $i++ or ++$i
5762: && count($stmt->loop) === 1
5763: && ($stmt->loop[0] instanceof Expr\PreInc || $stmt->loop[0] instanceof Expr\PostInc)
5764: && $stmt->loop[0]->var instanceof Variable
5765: ) {
5766: // $i < count($items)
5767: if (
5768: $lastCondExpr instanceof BinaryOp\Smaller
5769: && $lastCondExpr->left instanceof Variable
5770: && $lastCondExpr->right instanceof FuncCall
5771: && $lastCondExpr->right->name instanceof Name
5772: && !$lastCondExpr->right->isFirstClassCallable()
5773: && in_array($lastCondExpr->right->name->toLowerString(), ['count', 'sizeof'], true)
5774: && count($lastCondExpr->right->getArgs()) > 0
5775: && $lastCondExpr->right->getArgs()[0]->value instanceof Variable
5776: && is_string($stmt->init[0]->var->name)
5777: && $stmt->init[0]->var->name === $stmt->loop[0]->var->name
5778: && $stmt->init[0]->var->name === $lastCondExpr->left->name
5779: ) {
5780: $arrayArg = $lastCondExpr->right->getArgs()[0]->value;
5781: $arrayType = $bodyScope->getType($arrayArg);
5782: if ($arrayType->isList()->yes()) {
5783: $bodyScope = $bodyScope->assignExpression(
5784: new ArrayDimFetch($lastCondExpr->right->getArgs()[0]->value, $lastCondExpr->left),
5785: $arrayType->getIterableValueType(),
5786: $bodyScope->getNativeType($arrayArg)->getIterableValueType(),
5787: );
5788: }
5789: }
5790:
5791: // count($items) > $i
5792: if (
5793: $lastCondExpr instanceof BinaryOp\Greater
5794: && $lastCondExpr->right instanceof Variable
5795: && $lastCondExpr->left instanceof FuncCall
5796: && $lastCondExpr->left->name instanceof Name
5797: && !$lastCondExpr->left->isFirstClassCallable()
5798: && in_array($lastCondExpr->left->name->toLowerString(), ['count', 'sizeof'], true)
5799: && count($lastCondExpr->left->getArgs()) > 0
5800: && $lastCondExpr->left->getArgs()[0]->value instanceof Variable
5801: && is_string($stmt->init[0]->var->name)
5802: && $stmt->init[0]->var->name === $stmt->loop[0]->var->name
5803: && $stmt->init[0]->var->name === $lastCondExpr->right->name
5804: ) {
5805: $arrayArg = $lastCondExpr->left->getArgs()[0]->value;
5806: $arrayType = $bodyScope->getType($arrayArg);
5807: if ($arrayType->isList()->yes()) {
5808: $bodyScope = $bodyScope->assignExpression(
5809: new ArrayDimFetch($lastCondExpr->left->getArgs()[0]->value, $lastCondExpr->right),
5810: $arrayType->getIterableValueType(),
5811: $bodyScope->getNativeType($arrayArg)->getIterableValueType(),
5812: );
5813: }
5814: }
5815: }
5816:
5817: return $bodyScope;
5818: }
5819:
5820: private function getGlobalVariableType(string $variableName): Type
5821: {
5822: if ($variableName === 'argc') {
5823: return StaticTypeFactory::argc();
5824: }
5825: if ($variableName === 'argv') {
5826: return StaticTypeFactory::argv();
5827: }
5828:
5829: return new MixedType();
5830: }
5831:
5832: }
5833: