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