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