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