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