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