1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use ArrayAccess;
6: use Closure;
7: use DivisionByZeroError;
8: use Override;
9: use PhpParser\Comment\Doc;
10: use PhpParser\Modifiers;
11: use PhpParser\Node;
12: use PhpParser\Node\Arg;
13: use PhpParser\Node\AttributeGroup;
14: use PhpParser\Node\ComplexType;
15: use PhpParser\Node\Expr;
16: use PhpParser\Node\Expr\Array_;
17: use PhpParser\Node\Expr\ArrayDimFetch;
18: use PhpParser\Node\Expr\Assign;
19: use PhpParser\Node\Expr\AssignRef;
20: use PhpParser\Node\Expr\BinaryOp;
21: use PhpParser\Node\Expr\BinaryOp\BooleanAnd;
22: use PhpParser\Node\Expr\BinaryOp\BooleanOr;
23: use PhpParser\Node\Expr\BinaryOp\Coalesce;
24: use PhpParser\Node\Expr\BooleanNot;
25: use PhpParser\Node\Expr\CallLike;
26: use PhpParser\Node\Expr\Cast;
27: use PhpParser\Node\Expr\ConstFetch;
28: use PhpParser\Node\Expr\ErrorSuppress;
29: use PhpParser\Node\Expr\Exit_;
30: use PhpParser\Node\Expr\FuncCall;
31: use PhpParser\Node\Expr\Instanceof_;
32: use PhpParser\Node\Expr\List_;
33: use PhpParser\Node\Expr\MethodCall;
34: use PhpParser\Node\Expr\New_;
35: use PhpParser\Node\Expr\PropertyFetch;
36: use PhpParser\Node\Expr\StaticCall;
37: use PhpParser\Node\Expr\StaticPropertyFetch;
38: use PhpParser\Node\Expr\Ternary;
39: use PhpParser\Node\Expr\Variable;
40: use PhpParser\Node\Identifier;
41: use PhpParser\Node\Name;
42: use PhpParser\Node\Stmt\Break_;
43: use PhpParser\Node\Stmt\Class_;
44: use PhpParser\Node\Stmt\Continue_;
45: use PhpParser\Node\Stmt\Do_;
46: use PhpParser\Node\Stmt\Echo_;
47: use PhpParser\Node\Stmt\For_;
48: use PhpParser\Node\Stmt\Foreach_;
49: use PhpParser\Node\Stmt\If_;
50: use PhpParser\Node\Stmt\InlineHTML;
51: use PhpParser\Node\Stmt\Return_;
52: use PhpParser\Node\Stmt\Static_;
53: use PhpParser\Node\Stmt\Switch_;
54: use PhpParser\Node\Stmt\TryCatch;
55: use PhpParser\Node\Stmt\Unset_;
56: use PhpParser\Node\Stmt\While_;
57: use PhpParser\NodeFinder;
58: use PhpParser\NodeTraverser;
59: use PhpParser\NodeVisitorAbstract;
60: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass;
61: use PHPStan\BetterReflection\Reflection\ReflectionEnum;
62: use PHPStan\BetterReflection\Reflector\Reflector;
63: use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection;
64: use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
65: use PHPStan\DependencyInjection\AutowiredParameter;
66: use PHPStan\DependencyInjection\AutowiredService;
67: use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
68: use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider;
69: use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
70: use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider;
71: use PHPStan\File\FileHelper;
72: use PHPStan\File\FileReader;
73: use PHPStan\Node\BooleanAndNode;
74: use PHPStan\Node\BooleanOrNode;
75: use PHPStan\Node\BreaklessWhileLoopNode;
76: use PHPStan\Node\CatchWithUnthrownExceptionNode;
77: use PHPStan\Node\ClassConstantsNode;
78: use PHPStan\Node\ClassMethodsNode;
79: use PHPStan\Node\ClassPropertiesNode;
80: use PHPStan\Node\ClassPropertyNode;
81: use PHPStan\Node\ClassStatementsGatherer;
82: use PHPStan\Node\ClosureReturnStatementsNode;
83: use PHPStan\Node\DeepNodeCloner;
84: use PHPStan\Node\DoWhileLoopConditionNode;
85: use PHPStan\Node\ExecutionEndNode;
86: use PHPStan\Node\Expr\AlwaysRememberedExpr;
87: use PHPStan\Node\Expr\ExistingArrayDimFetch;
88: use PHPStan\Node\Expr\ForeachValueByRefExpr;
89: use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
90: use PHPStan\Node\Expr\GetIterableValueTypeExpr;
91: use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
92: use PHPStan\Node\Expr\NativeTypeExpr;
93: use PHPStan\Node\Expr\OriginalForeachKeyExpr;
94: use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
95: use PHPStan\Node\Expr\PropertyInitializationExpr;
96: use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr;
97: use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
98: use PHPStan\Node\Expr\TypeExpr;
99: use PHPStan\Node\Expr\UnsetOffsetExpr;
100: use PHPStan\Node\FinallyExitPointsNode;
101: use PHPStan\Node\FunctionCallableNode;
102: use PHPStan\Node\FunctionReturnStatementsNode;
103: use PHPStan\Node\InArrowFunctionNode;
104: use PHPStan\Node\InClassMethodNode;
105: use PHPStan\Node\InClassNode;
106: use PHPStan\Node\InClosureNode;
107: use PHPStan\Node\InForeachNode;
108: use PHPStan\Node\InFunctionNode;
109: use PHPStan\Node\InPropertyHookNode;
110: use PHPStan\Node\InstantiationCallableNode;
111: use PHPStan\Node\InTraitNode;
112: use PHPStan\Node\InvalidateExprNode;
113: use PHPStan\Node\LiteralArrayItem;
114: use PHPStan\Node\LiteralArrayNode;
115: use PHPStan\Node\MatchExpressionArm;
116: use PHPStan\Node\MatchExpressionArmBody;
117: use PHPStan\Node\MatchExpressionArmCondition;
118: use PHPStan\Node\MatchExpressionNode;
119: use PHPStan\Node\MethodCallableNode;
120: use PHPStan\Node\MethodReturnStatementsNode;
121: use PHPStan\Node\NoopExpressionNode;
122: use PHPStan\Node\Printer\ExprPrinter;
123: use PHPStan\Node\PropertyAssignNode;
124: use PHPStan\Node\PropertyHookReturnStatementsNode;
125: use PHPStan\Node\PropertyHookStatementNode;
126: use PHPStan\Node\ReturnStatement;
127: use PHPStan\Node\StaticMethodCallableNode;
128: use PHPStan\Node\UnreachableStatementNode;
129: use PHPStan\Node\VariableAssignNode;
130: use PHPStan\Node\VarTagChangedExpressionTypeNode;
131: use PHPStan\Parser\ArrowFunctionArgVisitor;
132: use PHPStan\Parser\ClosureArgVisitor;
133: use PHPStan\Parser\ImmediatelyInvokedClosureVisitor;
134: use PHPStan\Parser\LineAttributesVisitor;
135: use PHPStan\Parser\Parser;
136: use PHPStan\Parser\ReversePipeTransformerVisitor;
137: use PHPStan\Php\PhpVersion;
138: use PHPStan\PhpDoc\PhpDocInheritanceResolver;
139: use PHPStan\PhpDoc\ResolvedPhpDocBlock;
140: use PHPStan\PhpDoc\Tag\VarTag;
141: use PHPStan\Reflection\Assertions;
142: use PHPStan\Reflection\Callables\CallableParametersAcceptor;
143: use PHPStan\Reflection\Callables\SimpleImpurePoint;
144: use PHPStan\Reflection\Callables\SimpleThrowPoint;
145: use PHPStan\Reflection\ClassReflection;
146: use PHPStan\Reflection\ClassReflectionFactory;
147: use PHPStan\Reflection\ExtendedMethodReflection;
148: use PHPStan\Reflection\ExtendedParameterReflection;
149: use PHPStan\Reflection\ExtendedParametersAcceptor;
150: use PHPStan\Reflection\FunctionReflection;
151: use PHPStan\Reflection\InitializerExprContext;
152: use PHPStan\Reflection\InitializerExprTypeResolver;
153: use PHPStan\Reflection\MethodReflection;
154: use PHPStan\Reflection\Native\NativeMethodReflection;
155: use PHPStan\Reflection\Native\NativeParameterReflection;
156: use PHPStan\Reflection\ParameterReflection;
157: use PHPStan\Reflection\ParametersAcceptor;
158: use PHPStan\Reflection\ParametersAcceptorSelector;
159: use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection;
160: use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
161: use PHPStan\Reflection\Php\PhpMethodReflection;
162: use PHPStan\Reflection\Php\PhpPropertyReflection;
163: use PHPStan\Reflection\ReflectionProvider;
164: use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
165: use PHPStan\ShouldNotHappenException;
166: use PHPStan\TrinaryLogic;
167: use PHPStan\Type\Accessory\AccessoryArrayListType;
168: use PHPStan\Type\Accessory\HasOffsetValueType;
169: use PHPStan\Type\Accessory\NonEmptyArrayType;
170: use PHPStan\Type\ArrayType;
171: use PHPStan\Type\ClosureType;
172: use PHPStan\Type\Constant\ConstantArrayType;
173: use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
174: use PHPStan\Type\Constant\ConstantIntegerType;
175: use PHPStan\Type\Constant\ConstantStringType;
176: use PHPStan\Type\FileTypeMapper;
177: use PHPStan\Type\GeneralizePrecision;
178: use PHPStan\Type\Generic\TemplateTypeHelper;
179: use PHPStan\Type\Generic\TemplateTypeMap;
180: use PHPStan\Type\Generic\TemplateTypeVariance;
181: use PHPStan\Type\Generic\TemplateTypeVarianceMap;
182: use PHPStan\Type\IntegerRangeType;
183: use PHPStan\Type\IntegerType;
184: use PHPStan\Type\IntersectionType;
185: use PHPStan\Type\MixedType;
186: use PHPStan\Type\NeverType;
187: use PHPStan\Type\NullType;
188: use PHPStan\Type\ObjectType;
189: use PHPStan\Type\ObjectWithoutClassType;
190: use PHPStan\Type\ParserNodeTypeToPHPStanType;
191: use PHPStan\Type\ResourceType;
192: use PHPStan\Type\StaticType;
193: use PHPStan\Type\StaticTypeFactory;
194: use PHPStan\Type\StringType;
195: use PHPStan\Type\ThisType;
196: use PHPStan\Type\Type;
197: use PHPStan\Type\TypeCombinator;
198: use PHPStan\Type\TypeTraverser;
199: use PHPStan\Type\TypeUtils;
200: use PHPStan\Type\UnionType;
201: use ReflectionProperty;
202: use Throwable;
203: use Traversable;
204: use TypeError;
205: use UnhandledMatchError;
206: use function array_fill_keys;
207: use function array_filter;
208: use function array_key_exists;
209: use function array_key_last;
210: use function array_keys;
211: use function array_last;
212: use function array_map;
213: use function array_merge;
214: use function array_pop;
215: use function array_reverse;
216: use function array_slice;
217: use function array_unshift;
218: use function array_values;
219: use function base64_decode;
220: use function count;
221: use function in_array;
222: use function is_array;
223: use function is_int;
224: use function is_string;
225: use function ksort;
226: use function sprintf;
227: use function str_starts_with;
228: use function strtolower;
229: use function trim;
230: use function usort;
231: use const PHP_VERSION_ID;
232: use const SORT_NUMERIC;
233:
234: #[AutowiredService]
235: class NodeScopeResolver
236: {
237:
238: private const LOOP_SCOPE_ITERATIONS = 3;
239: private const GENERALIZE_AFTER_ITERATION = 1;
240:
241: /** @var array<string, true> filePath(string) => bool(true) */
242: private array $analysedFiles = [];
243:
244: /** @var array<string, true> */
245: private array $earlyTerminatingMethodNames;
246:
247: /** @var array<string, true> */
248: private array $calledMethodStack = [];
249:
250: /** @var array<string, MutatingScope|null> */
251: private array $calledMethodResults = [];
252:
253: /**
254: * @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[])
255: * @param array<int, string> $earlyTerminatingFunctionCalls
256: */
257: public function __construct(
258: private readonly ReflectionProvider $reflectionProvider,
259: private readonly InitializerExprTypeResolver $initializerExprTypeResolver,
260: #[AutowiredParameter(ref: '@nodeScopeResolverReflector')]
261: private readonly Reflector $reflector,
262: private readonly ClassReflectionFactory $classReflectionFactory,
263: private readonly ParameterOutTypeExtensionProvider $parameterOutTypeExtensionProvider,
264: #[AutowiredParameter(ref: '@defaultAnalysisParser')]
265: private readonly Parser $parser,
266: private readonly FileTypeMapper $fileTypeMapper,
267: private readonly PhpVersion $phpVersion,
268: private readonly PhpDocInheritanceResolver $phpDocInheritanceResolver,
269: private readonly FileHelper $fileHelper,
270: private readonly TypeSpecifier $typeSpecifier,
271: private readonly DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
272: private readonly ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider,
273: private readonly ParameterClosureThisExtensionProvider $parameterClosureThisExtensionProvider,
274: private readonly ParameterClosureTypeExtensionProvider $parameterClosureTypeExtensionProvider,
275: private readonly ScopeFactory $scopeFactory,
276: private readonly DeepNodeCloner $deepNodeCloner,
277: #[AutowiredParameter]
278: private readonly bool $polluteScopeWithLoopInitialAssignments,
279: #[AutowiredParameter]
280: private readonly bool $polluteScopeWithAlwaysIterableForeach,
281: #[AutowiredParameter]
282: private readonly bool $polluteScopeWithBlock,
283: #[AutowiredParameter]
284: private readonly array $earlyTerminatingMethodCalls,
285: #[AutowiredParameter]
286: private readonly array $earlyTerminatingFunctionCalls,
287: #[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
288: private readonly bool $implicitThrows,
289: #[AutowiredParameter]
290: private readonly bool $treatPhpDocTypesAsCertain,
291: )
292: {
293: $earlyTerminatingMethodNames = [];
294: foreach ($this->earlyTerminatingMethodCalls as $methodNames) {
295: foreach ($methodNames as $methodName) {
296: $earlyTerminatingMethodNames[strtolower($methodName)] = true;
297: }
298: }
299: $this->earlyTerminatingMethodNames = $earlyTerminatingMethodNames;
300: }
301:
302: /**
303: * @api
304: * @param string[] $files
305: */
306: public function setAnalysedFiles(array $files): void
307: {
308: $this->analysedFiles = array_fill_keys($files, true);
309: }
310:
311: /**
312: * @api
313: * @param Node[] $nodes
314: * @param callable(Node $node, Scope $scope): void $nodeCallback
315: */
316: public function processNodes(
317: array $nodes,
318: MutatingScope $scope,
319: callable $nodeCallback,
320: ): void
321: {
322: $expressionResultStorage = new ExpressionResultStorage();
323: $alreadyTerminated = false;
324: foreach ($nodes as $i => $node) {
325: if (
326: !$node instanceof Node\Stmt
327: || ($alreadyTerminated && !($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike))
328: ) {
329: continue;
330: }
331:
332: $statementResult = $this->processStmtNode($node, $scope, $expressionResultStorage, $nodeCallback, StatementContext::createTopLevel());
333: $scope = $statementResult->getScope();
334: if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) {
335: continue;
336: }
337:
338: $alreadyTerminated = true;
339: $nextStmts = $this->getNextUnreachableStatements(array_slice($nodes, $i + 1), true);
340: $this->processUnreachableStatement($nextStmts, $scope, $expressionResultStorage, $nodeCallback);
341: }
342:
343: $this->processPendingFibers($expressionResultStorage);
344: }
345:
346: protected function storeBeforeScope(ExpressionResultStorage $storage, Expr $expr, Scope $beforeScope): void
347: {
348: }
349:
350: protected function processPendingFibers(ExpressionResultStorage $storage): void
351: {
352: }
353:
354: /**
355: * @param Node\Stmt[] $nextStmts
356: * @param callable(Node $node, Scope $scope): void $nodeCallback
357: */
358: private function processUnreachableStatement(array $nextStmts, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback): void
359: {
360: if ($nextStmts === []) {
361: return;
362: }
363:
364: $unreachableStatement = null;
365: $nextStatements = [];
366:
367: foreach ($nextStmts as $key => $nextStmt) {
368: if ($key === 0) {
369: $unreachableStatement = $nextStmt;
370: continue;
371: }
372:
373: $nextStatements[] = $nextStmt;
374: }
375:
376: if (!$unreachableStatement instanceof Node\Stmt) {
377: return;
378: }
379:
380: $this->callNodeCallback($nodeCallback, new UnreachableStatementNode($unreachableStatement, $nextStatements), $scope, $storage);
381: }
382:
383: /**
384: * @api
385: * @param Node\Stmt[] $stmts
386: * @param callable(Node $node, Scope $scope): void $nodeCallback
387: */
388: public function processStmtNodes(
389: Node $parentNode,
390: array $stmts,
391: MutatingScope $scope,
392: callable $nodeCallback,
393: StatementContext $context,
394: ): StatementResult
395: {
396: $storage = new ExpressionResultStorage();
397: return $this->processStmtNodesInternal(
398: $parentNode,
399: $stmts,
400: $scope,
401: $storage,
402: $nodeCallback,
403: $context,
404: )->toPublic();
405: }
406:
407: /**
408: * @param Node\Stmt[] $stmts
409: * @param callable(Node $node, Scope $scope): void $nodeCallback
410: */
411: private function processStmtNodesInternal(
412: Node $parentNode,
413: array $stmts,
414: MutatingScope $scope,
415: ExpressionResultStorage $storage,
416: callable $nodeCallback,
417: StatementContext $context,
418: ): InternalStatementResult
419: {
420: $statementResult = $this->processStmtNodesInternalWithoutFlushingPendingFibers(
421: $parentNode,
422: $stmts,
423: $scope,
424: $storage,
425: $nodeCallback,
426: $context,
427: );
428: $this->processPendingFibers($storage);
429:
430: return $statementResult;
431: }
432:
433: /**
434: * @param Node\Stmt[] $stmts
435: * @param callable(Node $node, Scope $scope): void $nodeCallback
436: */
437: private function processStmtNodesInternalWithoutFlushingPendingFibers(
438: Node $parentNode,
439: array $stmts,
440: MutatingScope $scope,
441: ExpressionResultStorage $storage,
442: callable $nodeCallback,
443: StatementContext $context,
444: ): InternalStatementResult
445: {
446: $exitPoints = [];
447: $throwPoints = [];
448: $impurePoints = [];
449: $alreadyTerminated = false;
450: $hasYield = false;
451: $stmtCount = count($stmts);
452: $shouldCheckLastStatement = $parentNode instanceof Node\Stmt\Function_
453: || $parentNode instanceof Node\Stmt\ClassMethod
454: || $parentNode instanceof PropertyHookStatementNode
455: || $parentNode instanceof Expr\Closure;
456: foreach ($stmts as $i => $stmt) {
457: if ($alreadyTerminated && !($stmt instanceof Node\Stmt\Function_ || $stmt instanceof Node\Stmt\ClassLike)) {
458: continue;
459: }
460:
461: $isLast = $i === $stmtCount - 1;
462: $statementResult = $this->processStmtNode(
463: $stmt,
464: $scope,
465: $storage,
466: $nodeCallback,
467: $context,
468: );
469: $scope = $statementResult->getScope();
470: $hasYield = $hasYield || $statementResult->hasYield();
471:
472: if ($shouldCheckLastStatement && $isLast) {
473: $endStatements = $statementResult->getEndStatements();
474: if (count($endStatements) > 0) {
475: foreach ($endStatements as $endStatement) {
476: $endStatementResult = $endStatement->getResult();
477: $this->callNodeCallback($nodeCallback, new ExecutionEndNode(
478: $endStatement->getStatement(),
479: (new InternalStatementResult(
480: $endStatementResult->getScope(),
481: $hasYield,
482: $endStatementResult->isAlwaysTerminating(),
483: $endStatementResult->getExitPoints(),
484: $endStatementResult->getThrowPoints(),
485: $endStatementResult->getImpurePoints(),
486: ))->toPublic(),
487: $parentNode->getReturnType() !== null,
488: ), $endStatementResult->getScope(), $storage);
489: }
490: } else {
491: $this->callNodeCallback($nodeCallback, new ExecutionEndNode(
492: $stmt,
493: (new InternalStatementResult(
494: $scope,
495: $hasYield,
496: $statementResult->isAlwaysTerminating(),
497: $statementResult->getExitPoints(),
498: $statementResult->getThrowPoints(),
499: $statementResult->getImpurePoints(),
500: ))->toPublic(),
501: $parentNode->getReturnType() !== null,
502: ), $scope, $storage);
503: }
504: }
505:
506: $exitPoints = array_merge($exitPoints, $statementResult->getExitPoints());
507: $throwPoints = array_merge($throwPoints, $statementResult->getThrowPoints());
508: $impurePoints = array_merge($impurePoints, $statementResult->getImpurePoints());
509:
510: if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) {
511: continue;
512: }
513:
514: $alreadyTerminated = true;
515: $nextStmts = $this->getNextUnreachableStatements(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_);
516: $this->processUnreachableStatement($nextStmts, $scope, $storage, $nodeCallback);
517: }
518:
519: $statementResult = new InternalStatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints, $impurePoints);
520: if ($stmtCount === 0 && $shouldCheckLastStatement) {
521: $returnTypeNode = $parentNode->getReturnType();
522: if ($parentNode instanceof Expr\Closure) {
523: $parentNode = new Node\Stmt\Expression($parentNode, $parentNode->getAttributes());
524: }
525: $this->callNodeCallback($nodeCallback, new ExecutionEndNode(
526: $parentNode,
527: $statementResult->toPublic(),
528: $returnTypeNode !== null,
529: ), $scope, $storage);
530: }
531:
532: return $statementResult;
533: }
534:
535: /**
536: * @param callable(Node $node, Scope $scope): void $nodeCallback
537: */
538: private function processStmtNode(
539: Node\Stmt $stmt,
540: MutatingScope $scope,
541: ExpressionResultStorage $storage,
542: callable $nodeCallback,
543: StatementContext $context,
544: ): InternalStatementResult
545: {
546: $overridingThrowPoints = null;
547: if (
548: !$stmt instanceof Static_
549: && !$stmt instanceof Node\Stmt\Global_
550: && !$stmt instanceof Node\Stmt\Property
551: && !$stmt instanceof Node\Stmt\ClassConst
552: && !$stmt instanceof Node\Stmt\Const_
553: && !$stmt instanceof Node\Stmt\ClassLike
554: && !$stmt instanceof Node\Stmt\Function_
555: && !$stmt instanceof Node\Stmt\ClassMethod
556: ) {
557: if (!$stmt instanceof Foreach_) {
558: $scope = $this->processStmtVarAnnotation($scope, $storage, $stmt, null, $nodeCallback);
559: }
560: $overridingThrowPoints = $this->getOverridingThrowPoints($stmt, $scope);
561: }
562:
563: if ($stmt instanceof Node\Stmt\ClassMethod) {
564: if (!$scope->isInClass()) {
565: throw new ShouldNotHappenException();
566: }
567: if (
568: $scope->isInTrait()
569: && $scope->getClassReflection()->hasNativeMethod($stmt->name->toString())
570: ) {
571: $methodReflection = $scope->getClassReflection()->getNativeMethod($stmt->name->toString());
572: if ($methodReflection instanceof NativeMethodReflection) {
573: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []);
574: }
575: if ($methodReflection instanceof PhpMethodReflection) {
576: $declaringTrait = $methodReflection->getDeclaringTrait();
577: if ($declaringTrait === null || $declaringTrait->getName() !== $scope->getTraitReflection()->getName()) {
578: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []);
579: }
580: }
581: }
582: }
583:
584: $stmtScope = $scope;
585: if ($stmt instanceof Node\Stmt\Expression && $stmt->expr instanceof Expr\Throw_) {
586: $stmtScope = $this->processStmtVarAnnotation($scope, $storage, $stmt, $stmt->expr->expr, $nodeCallback);
587: }
588: if ($stmt instanceof Return_) {
589: $stmtScope = $this->processStmtVarAnnotation($scope, $storage, $stmt, $stmt->expr, $nodeCallback);
590: }
591:
592: $this->callNodeCallback($nodeCallback, $stmt, $stmtScope, $storage);
593:
594: if ($stmt instanceof Node\Stmt\Declare_) {
595: $hasYield = false;
596: $throwPoints = [];
597: $impurePoints = [];
598: $alwaysTerminating = false;
599: $exitPoints = [];
600: foreach ($stmt->declares as $declare) {
601: $this->callNodeCallback($nodeCallback, $declare, $scope, $storage);
602: $this->callNodeCallback($nodeCallback, $declare->value, $scope, $storage);
603: if (
604: $declare->key->name !== 'strict_types'
605: || !($declare->value instanceof Node\Scalar\Int_)
606: || $declare->value->value !== 1
607: ) {
608: continue;
609: }
610:
611: $scope = $scope->enterDeclareStrictTypes();
612: }
613:
614: if ($stmt->stmts !== null) {
615: $result = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context);
616: $scope = $result->getScope();
617: $hasYield = $result->hasYield();
618: $throwPoints = $result->getThrowPoints();
619: $impurePoints = $result->getImpurePoints();
620: $alwaysTerminating = $result->isAlwaysTerminating();
621: $exitPoints = $result->getExitPoints();
622: }
623:
624: return new InternalStatementResult($scope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints);
625: } elseif ($stmt instanceof Node\Stmt\Function_) {
626: $hasYield = false;
627: $throwPoints = [];
628: $impurePoints = [];
629: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
630: [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, , $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt);
631:
632: foreach ($stmt->params as $param) {
633: $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
634: }
635:
636: if ($stmt->returnType !== null) {
637: $this->callNodeCallback($nodeCallback, $stmt->returnType, $scope, $storage);
638: }
639:
640: if (!$isDeprecated) {
641: [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $stmt);
642: }
643:
644: $functionScope = $scope->enterFunction(
645: $stmt,
646: $templateTypeMap,
647: $phpDocParameterTypes,
648: $phpDocReturnType,
649: $phpDocThrowType,
650: $deprecatedDescription,
651: $isDeprecated,
652: $isInternal,
653: $isPure,
654: $acceptsNamedArguments,
655: $asserts,
656: $phpDocComment,
657: $phpDocParameterOutTypes,
658: $phpDocImmediatelyInvokedCallableParameters,
659: $phpDocClosureThisTypeParameters,
660: );
661: $functionReflection = $functionScope->getFunction();
662: if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) {
663: throw new ShouldNotHappenException();
664: }
665:
666: $this->callNodeCallback($nodeCallback, new InFunctionNode($functionReflection, $stmt), $functionScope, $storage);
667:
668: $gatheredReturnStatements = [];
669: $gatheredYieldStatements = [];
670: $executionEnds = [];
671: $functionImpurePoints = [];
672: $statementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $functionScope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $functionScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$functionImpurePoints): void {
673: $nodeCallback($node, $scope);
674: if ($scope->getFunction() !== $functionScope->getFunction()) {
675: return;
676: }
677: if ($scope->isInAnonymousFunction()) {
678: return;
679: }
680: if ($node instanceof PropertyAssignNode) {
681: $functionImpurePoints[] = new ImpurePoint(
682: $scope,
683: $node,
684: 'propertyAssign',
685: 'property assignment',
686: true,
687: );
688: return;
689: }
690: if ($node instanceof ExecutionEndNode) {
691: $executionEnds[] = $node;
692: return;
693: }
694: if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) {
695: $gatheredYieldStatements[] = $node;
696: }
697: if (!$node instanceof Return_) {
698: return;
699: }
700:
701: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
702: }, StatementContext::createTopLevel())->toPublic();
703:
704: $this->callNodeCallback($nodeCallback, new FunctionReturnStatementsNode(
705: $stmt,
706: $gatheredReturnStatements,
707: $gatheredYieldStatements,
708: $statementResult,
709: $executionEnds,
710: array_merge($statementResult->getImpurePoints(), $functionImpurePoints),
711: $functionReflection,
712: ), $functionScope, $storage);
713: if (!$scope->isInAnonymousFunction()) {
714: $this->processPendingFibers($storage);
715: }
716: } elseif ($stmt instanceof Node\Stmt\ClassMethod) {
717: $hasYield = false;
718: $throwPoints = [];
719: $impurePoints = [];
720: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
721: [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt);
722:
723: foreach ($stmt->params as $param) {
724: $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
725: }
726:
727: if ($stmt->returnType !== null) {
728: $this->callNodeCallback($nodeCallback, $stmt->returnType, $scope, $storage);
729: }
730:
731: if (!$isDeprecated) {
732: [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $stmt);
733: }
734:
735: $isFromTrait = $stmt->getAttribute('originalTraitMethodName') === '__construct';
736: $isConstructor = $isFromTrait || $stmt->name->toLowerString() === '__construct';
737:
738: $methodScope = $scope->enterClassMethod(
739: $stmt,
740: $templateTypeMap,
741: $phpDocParameterTypes,
742: $phpDocReturnType,
743: $phpDocThrowType,
744: $deprecatedDescription,
745: $isDeprecated,
746: $isInternal,
747: $isFinal,
748: $isPure,
749: $acceptsNamedArguments,
750: $asserts,
751: $selfOutType,
752: $phpDocComment,
753: $phpDocParameterOutTypes,
754: $phpDocImmediatelyInvokedCallableParameters,
755: $phpDocClosureThisTypeParameters,
756: $isConstructor,
757: );
758:
759: if (!$scope->isInClass()) {
760: throw new ShouldNotHappenException();
761: }
762:
763: $classReflection = $scope->getClassReflection();
764:
765: if ($isConstructor) {
766: foreach ($stmt->params as $param) {
767: if ($param->flags === 0 && $param->hooks === []) {
768: continue;
769: }
770:
771: if (!$param->var instanceof Variable || !is_string($param->var->name) || $param->var->name === '') {
772: throw new ShouldNotHappenException();
773: }
774: $phpDoc = null;
775: if ($param->getDocComment() !== null) {
776: $phpDoc = $param->getDocComment()->getText();
777: }
778: $this->callNodeCallback($nodeCallback, new ClassPropertyNode(
779: $param->var->name,
780: $param->flags,
781: $param->type !== null ? ParserNodeTypeToPHPStanType::resolve($param->type, $classReflection) : null,
782: null,
783: $phpDoc,
784: $phpDocParameterTypes[$param->var->name] ?? null,
785: true,
786: $isFromTrait,
787: $param,
788: $isReadOnly,
789: $scope->isInTrait(),
790: $classReflection->isReadOnly(),
791: false,
792: $classReflection,
793: ), $methodScope, $storage);
794: $this->processPropertyHooks(
795: $stmt,
796: $param->type,
797: $phpDocParameterTypes[$param->var->name] ?? null,
798: $param->var->name,
799: $param->hooks,
800: $scope,
801: $storage,
802: $nodeCallback,
803: );
804: $methodScope = $methodScope->assignExpression(new PropertyInitializationExpr($param->var->name), new MixedType(), new MixedType());
805: }
806: }
807:
808: if ($stmt->getAttribute('virtual', false) === false) {
809: $methodReflection = $methodScope->getFunction();
810: if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) {
811: throw new ShouldNotHappenException();
812: }
813: $this->callNodeCallback($nodeCallback, new InClassMethodNode($classReflection, $methodReflection, $stmt), $methodScope, $storage);
814: }
815:
816: if ($stmt->stmts !== null) {
817: $gatheredReturnStatements = [];
818: $gatheredYieldStatements = [];
819: $executionEnds = [];
820: $methodImpurePoints = [];
821: $statementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $methodScope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$methodImpurePoints): void {
822: $nodeCallback($node, $scope);
823: if ($scope->getFunction() !== $methodScope->getFunction()) {
824: return;
825: }
826: if ($scope->isInAnonymousFunction()) {
827: return;
828: }
829: if ($node instanceof PropertyAssignNode) {
830: if (
831: $node->getPropertyFetch() instanceof Expr\PropertyFetch
832: && $scope->getFunction() instanceof PhpMethodFromParserNodeReflection
833: && $scope->getFunction()->getDeclaringClass()->hasConstructor()
834: && $scope->getFunction()->getDeclaringClass()->getConstructor()->getName() === $scope->getFunction()->getName()
835: && TypeUtils::findThisType($scope->getType($node->getPropertyFetch()->var)) !== null
836: ) {
837: return;
838: }
839: $methodImpurePoints[] = new ImpurePoint(
840: $scope,
841: $node,
842: 'propertyAssign',
843: 'property assignment',
844: true,
845: );
846: return;
847: }
848: if ($node instanceof ExecutionEndNode) {
849: $executionEnds[] = $node;
850: return;
851: }
852: if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) {
853: $gatheredYieldStatements[] = $node;
854: }
855: if (!$node instanceof Return_) {
856: return;
857: }
858:
859: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
860: }, StatementContext::createTopLevel())->toPublic();
861:
862: $methodReflection = $methodScope->getFunction();
863: if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) {
864: throw new ShouldNotHappenException();
865: }
866:
867: $this->callNodeCallback($nodeCallback, new MethodReturnStatementsNode(
868: $stmt,
869: $gatheredReturnStatements,
870: $gatheredYieldStatements,
871: $statementResult,
872: $executionEnds,
873: array_merge($statementResult->getImpurePoints(), $methodImpurePoints),
874: $classReflection,
875: $methodReflection,
876: ), $methodScope, $storage);
877:
878: if ($isConstructor) {
879: $finalScope = null;
880:
881: foreach ($executionEnds as $executionEnd) {
882: if ($executionEnd->getStatementResult()->isAlwaysTerminating()) {
883: continue;
884: }
885:
886: $endScope = $executionEnd->getStatementResult()->getScope();
887: if ($finalScope === null) {
888: $finalScope = $endScope;
889: continue;
890: }
891:
892: $finalScope = $finalScope->mergeWith($endScope);
893: }
894:
895: foreach ($gatheredReturnStatements as $statement) {
896: if ($finalScope === null) {
897: $finalScope = $statement->getScope()->toMutatingScope();
898: continue;
899: }
900:
901: $finalScope = $finalScope->mergeWith($statement->getScope()->toMutatingScope());
902: }
903:
904: if ($finalScope !== null) {
905: $scope = $finalScope->rememberConstructorScope();
906: }
907:
908: }
909: }
910: if (!$scope->getClassReflection()->isAnonymous() && !$scope->isInAnonymousFunction()) {
911: $this->processPendingFibers($storage);
912: }
913: } elseif ($stmt instanceof Echo_) {
914: $hasYield = false;
915: $throwPoints = [];
916: $isAlwaysTerminating = false;
917: foreach ($stmt->exprs as $echoExpr) {
918: $result = $this->processExprNode($stmt, $echoExpr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
919: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
920: $scope = $result->getScope();
921: $hasYield = $hasYield || $result->hasYield();
922: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
923: }
924:
925: $throwPoints = $overridingThrowPoints ?? $throwPoints;
926: $impurePoints = [
927: new ImpurePoint($scope, $stmt, 'echo', 'echo', true),
928: ];
929: return new InternalStatementResult($scope, $hasYield, $isAlwaysTerminating, [], $throwPoints, $impurePoints);
930: } elseif ($stmt instanceof Return_) {
931: if ($stmt->expr !== null) {
932: $result = $this->processExprNode($stmt, $stmt->expr, $stmtScope, $storage, $nodeCallback, ExpressionContext::createDeep());
933: $throwPoints = $result->getThrowPoints();
934: $impurePoints = $result->getImpurePoints();
935: $scope = $result->getScope();
936: $hasYield = $result->hasYield();
937: } else {
938: $hasYield = false;
939: $throwPoints = [];
940: $impurePoints = [];
941: }
942:
943: return new InternalStatementResult($scope, $hasYield, true, [
944: new InternalStatementExitPoint($stmt, $scope),
945: ], $overridingThrowPoints ?? $throwPoints, $impurePoints);
946: } elseif ($stmt instanceof Continue_ || $stmt instanceof Break_) {
947: if ($stmt->num !== null) {
948: $result = $this->processExprNode($stmt, $stmt->num, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
949: $scope = $result->getScope();
950: $hasYield = $result->hasYield();
951: $throwPoints = $result->getThrowPoints();
952: $impurePoints = $result->getImpurePoints();
953: } else {
954: $hasYield = false;
955: $throwPoints = [];
956: $impurePoints = [];
957: }
958:
959: return new InternalStatementResult($scope, $hasYield, true, [
960: new InternalStatementExitPoint($stmt, $scope),
961: ], $overridingThrowPoints ?? $throwPoints, $impurePoints);
962: } elseif ($stmt instanceof Node\Stmt\Expression) {
963: if ($stmt->expr instanceof Expr\Throw_) {
964: $scope = $stmtScope;
965: }
966: $earlyTerminationExpr = $this->findEarlyTerminatingExpr($stmt->expr, $scope);
967: $hasAssign = false;
968: $currentScope = $scope;
969: $result = $this->processExprNode($stmt, $stmt->expr, $scope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $currentScope, &$hasAssign): void {
970: if (
971: ($node instanceof VariableAssignNode || $node instanceof PropertyAssignNode)
972: && $scope->getAnonymousFunctionReflection() === $currentScope->getAnonymousFunctionReflection()
973: && $scope->getFunction() === $currentScope->getFunction()
974: ) {
975: $hasAssign = true;
976: }
977: $nodeCallback($node, $scope);
978: }, ExpressionContext::createTopLevel());
979: $throwPoints = array_filter($result->getThrowPoints(), static fn ($throwPoint) => $throwPoint->isExplicit());
980: if (
981: count($result->getImpurePoints()) === 0
982: && count($throwPoints) === 0
983: && !$stmt->expr instanceof Expr\PostInc
984: && !$stmt->expr instanceof Expr\PreInc
985: && !$stmt->expr instanceof Expr\PostDec
986: && !$stmt->expr instanceof Expr\PreDec
987: ) {
988: $this->callNodeCallback($nodeCallback, new NoopExpressionNode($stmt->expr, $hasAssign), $scope, $storage);
989: }
990: $scope = $result->getScope();
991: $scope = $scope->filterBySpecifiedTypes($this->typeSpecifier->specifyTypesInCondition(
992: $scope,
993: $stmt->expr,
994: TypeSpecifierContext::createNull(),
995: ));
996: $hasYield = $result->hasYield();
997: $throwPoints = $result->getThrowPoints();
998: $impurePoints = $result->getImpurePoints();
999: $isAlwaysTerminating = $result->isAlwaysTerminating();
1000:
1001: if ($earlyTerminationExpr !== null) {
1002: return new InternalStatementResult($scope, $hasYield, true, [
1003: new InternalStatementExitPoint($stmt, $scope),
1004: ], $overridingThrowPoints ?? $throwPoints, $impurePoints);
1005: }
1006: return new InternalStatementResult($scope, $hasYield, $isAlwaysTerminating, [], $overridingThrowPoints ?? $throwPoints, $impurePoints);
1007: } elseif ($stmt instanceof Node\Stmt\Namespace_) {
1008: if ($stmt->name !== null) {
1009: $scope = $scope->enterNamespace($stmt->name->toString());
1010: }
1011:
1012: $scope = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context)->getScope();
1013: $hasYield = false;
1014: $throwPoints = [];
1015: $impurePoints = [];
1016: } elseif ($stmt instanceof Node\Stmt\Trait_) {
1017: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []);
1018: } elseif ($stmt instanceof Node\Stmt\ClassLike) {
1019: if (!$context->isTopLevel()) {
1020: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []);
1021: }
1022: $hasYield = false;
1023: $throwPoints = [];
1024: $impurePoints = [];
1025: if (isset($stmt->namespacedName)) {
1026: $classReflection = $this->getCurrentClassReflection($stmt, $stmt->namespacedName->toString(), $scope);
1027: $classScope = $scope->enterClass($classReflection);
1028: $this->callNodeCallback($nodeCallback, new InClassNode($stmt, $classReflection), $classScope, $storage);
1029: } elseif ($stmt instanceof Class_) {
1030: if ($stmt->name === null) {
1031: throw new ShouldNotHappenException();
1032: }
1033: if (!$stmt->isAnonymous()) {
1034: $classReflection = $this->reflectionProvider->getClass($stmt->name->toString());
1035: } else {
1036: $classReflection = $this->reflectionProvider->getAnonymousClassReflection($stmt, $scope);
1037: }
1038: $classScope = $scope->enterClass($classReflection);
1039: $this->callNodeCallback($nodeCallback, new InClassNode($stmt, $classReflection), $classScope, $storage);
1040: } else {
1041: throw new ShouldNotHappenException();
1042: }
1043:
1044: $classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback);
1045: $this->processAttributeGroups($stmt, $stmt->attrGroups, $classScope, $storage, $classStatementsGatherer);
1046:
1047: $classLikeStatements = $stmt->stmts;
1048: // analyze static methods first; constructor next; instance methods and property hooks last so we can carry over the scope
1049: usort($classLikeStatements, static function ($a, $b) {
1050: if ($a instanceof Node\Stmt\Property) {
1051: return 1;
1052: }
1053: if ($b instanceof Node\Stmt\Property) {
1054: return -1;
1055: }
1056:
1057: if (!$a instanceof Node\Stmt\ClassMethod || !$b instanceof Node\Stmt\ClassMethod) {
1058: return 0;
1059: }
1060:
1061: return [!$a->isStatic(), $a->name->toLowerString() !== '__construct'] <=> [!$b->isStatic(), $b->name->toLowerString() !== '__construct'];
1062: });
1063:
1064: $this->processStmtNodesInternal($stmt, $classLikeStatements, $classScope, $storage, $classStatementsGatherer, $context);
1065: $this->callNodeCallback($nodeCallback, new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensionProvider, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classStatementsGatherer->getPropertyAssigns(), $classReflection), $classScope, $storage);
1066: $this->callNodeCallback($nodeCallback, new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls(), $classReflection), $classScope, $storage);
1067: $this->callNodeCallback($nodeCallback, new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches(), $classReflection), $classScope, $storage);
1068: $classReflection->evictPrivateSymbols();
1069: $this->calledMethodResults = [];
1070: } elseif ($stmt instanceof Node\Stmt\Property) {
1071: $hasYield = false;
1072: $throwPoints = [];
1073: $impurePoints = [];
1074: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
1075:
1076: $nativePropertyType = $stmt->type !== null ? ParserNodeTypeToPHPStanType::resolve($stmt->type, $scope->getClassReflection()) : null;
1077:
1078: [,,,,,,,,,,,,$isReadOnly, $docComment, ,,,$varTags, $isAllowedPrivateMutation] = $this->getPhpDocs($scope, $stmt);
1079: $phpDocType = null;
1080: if (isset($varTags[0]) && count($varTags) === 1) {
1081: $phpDocType = $varTags[0]->getType();
1082: }
1083:
1084: foreach ($stmt->props as $prop) {
1085: $this->callNodeCallback($nodeCallback, $prop, $scope, $storage);
1086: if ($prop->default !== null) {
1087: $this->processExprNode($stmt, $prop->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
1088: }
1089:
1090: if (!$scope->isInClass()) {
1091: throw new ShouldNotHappenException();
1092: }
1093: $propertyName = $prop->name->toString();
1094:
1095: if ($phpDocType === null) {
1096: if (isset($varTags[$propertyName])) {
1097: $phpDocType = $varTags[$propertyName]->getType();
1098: }
1099: }
1100:
1101: $propStmt = clone $stmt;
1102: $propStmt->setAttributes($prop->getAttributes());
1103: $propStmt->setAttribute('originalPropertyStmt', $stmt);
1104: $this->callNodeCallback(
1105: $nodeCallback,
1106: new ClassPropertyNode(
1107: $propertyName,
1108: $stmt->flags,
1109: $nativePropertyType,
1110: $prop->default,
1111: $docComment,
1112: $phpDocType,
1113: false,
1114: false,
1115: $propStmt,
1116: $isReadOnly,
1117: $scope->isInTrait(),
1118: $scope->getClassReflection()->isReadOnly(),
1119: $isAllowedPrivateMutation,
1120: $scope->getClassReflection(),
1121: ),
1122: $scope,
1123: $storage,
1124: );
1125: }
1126:
1127: if (count($stmt->hooks) > 0) {
1128: if (!isset($propertyName)) {
1129: throw new ShouldNotHappenException('Property name should be known when analysing hooks.');
1130: }
1131: $this->processPropertyHooks(
1132: $stmt,
1133: $stmt->type,
1134: $phpDocType,
1135: $propertyName,
1136: $stmt->hooks,
1137: $scope,
1138: $storage,
1139: $nodeCallback,
1140: );
1141: }
1142:
1143: if ($stmt->type !== null) {
1144: $this->callNodeCallback($nodeCallback, $stmt->type, $scope, $storage);
1145: }
1146: } elseif ($stmt instanceof If_) {
1147: $conditionType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean();
1148: $ifAlwaysTrue = $conditionType->isTrue()->yes();
1149: $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
1150: $exitPoints = [];
1151: $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints();
1152: $impurePoints = $condResult->getImpurePoints();
1153: $endStatements = [];
1154: $finalScope = null;
1155: $alwaysTerminating = true;
1156: $hasYield = $condResult->hasYield();
1157:
1158: $branchScopeStatementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $condResult->getTruthyScope(), $storage, $nodeCallback, $context);
1159:
1160: if (!$conditionType->isTrue()->no()) {
1161: $exitPoints = $branchScopeStatementResult->getExitPoints();
1162: $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints());
1163: $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints());
1164: $branchScope = $branchScopeStatementResult->getScope();
1165: $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? null : $branchScope;
1166: $alwaysTerminating = $branchScopeStatementResult->isAlwaysTerminating();
1167: if (count($branchScopeStatementResult->getEndStatements()) > 0) {
1168: $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements());
1169: } elseif (count($stmt->stmts) > 0) {
1170: $endStatements[] = new InternalEndStatementResult($stmt->stmts[count($stmt->stmts) - 1], $branchScopeStatementResult);
1171: } else {
1172: $endStatements[] = new InternalEndStatementResult($stmt, $branchScopeStatementResult);
1173: }
1174: $hasYield = $branchScopeStatementResult->hasYield() || $hasYield;
1175: }
1176:
1177: $scope = $condResult->getFalseyScope();
1178: $lastElseIfConditionIsTrue = false;
1179:
1180: $condScope = $scope;
1181: foreach ($stmt->elseifs as $elseif) {
1182: $this->callNodeCallback($nodeCallback, $elseif, $scope, $storage);
1183: $elseIfConditionType = ($this->treatPhpDocTypesAsCertain ? $condScope->getType($elseif->cond) : $scope->getNativeType($elseif->cond))->toBoolean();
1184: $condResult = $this->processExprNode($stmt, $elseif->cond, $condScope, $storage, $nodeCallback, ExpressionContext::createDeep());
1185: $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints());
1186: $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints());
1187: $condScope = $condResult->getScope();
1188: $branchScopeStatementResult = $this->processStmtNodesInternal($elseif, $elseif->stmts, $condResult->getTruthyScope(), $storage, $nodeCallback, $context);
1189:
1190: if (
1191: !$ifAlwaysTrue
1192: && !$lastElseIfConditionIsTrue
1193: && !$elseIfConditionType->isTrue()->no()
1194: ) {
1195: $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints());
1196: $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints());
1197: $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints());
1198: $branchScope = $branchScopeStatementResult->getScope();
1199: $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope);
1200: $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating();
1201: if (count($branchScopeStatementResult->getEndStatements()) > 0) {
1202: $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements());
1203: } elseif (count($elseif->stmts) > 0) {
1204: $endStatements[] = new InternalEndStatementResult($elseif->stmts[count($elseif->stmts) - 1], $branchScopeStatementResult);
1205: } else {
1206: $endStatements[] = new InternalEndStatementResult($elseif, $branchScopeStatementResult);
1207: }
1208: $hasYield = $hasYield || $branchScopeStatementResult->hasYield();
1209: }
1210:
1211: if (
1212: $elseIfConditionType->isTrue()->yes()
1213: ) {
1214: $lastElseIfConditionIsTrue = true;
1215: }
1216:
1217: $condScope = $condScope->filterByFalseyValue($elseif->cond);
1218: $scope = $condScope;
1219: }
1220:
1221: if ($stmt->else === null) {
1222: if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) {
1223: $finalScope = $scope->mergeWith($finalScope);
1224: $alwaysTerminating = false;
1225: }
1226: } else {
1227: $this->callNodeCallback($nodeCallback, $stmt->else, $scope, $storage);
1228: $branchScopeStatementResult = $this->processStmtNodesInternal($stmt->else, $stmt->else->stmts, $scope, $storage, $nodeCallback, $context);
1229:
1230: if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) {
1231: $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints());
1232: $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints());
1233: $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints());
1234: $branchScope = $branchScopeStatementResult->getScope();
1235: $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope);
1236: $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating();
1237: if (count($branchScopeStatementResult->getEndStatements()) > 0) {
1238: $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements());
1239: } elseif (count($stmt->else->stmts) > 0) {
1240: $endStatements[] = new InternalEndStatementResult($stmt->else->stmts[count($stmt->else->stmts) - 1], $branchScopeStatementResult);
1241: } else {
1242: $endStatements[] = new InternalEndStatementResult($stmt->else, $branchScopeStatementResult);
1243: }
1244: $hasYield = $hasYield || $branchScopeStatementResult->hasYield();
1245: }
1246: }
1247:
1248: if ($finalScope === null) {
1249: $finalScope = $scope;
1250: }
1251:
1252: if ($stmt->else === null && !$ifAlwaysTrue && !$lastElseIfConditionIsTrue) {
1253: $endStatements[] = new InternalEndStatementResult($stmt, new InternalStatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints));
1254: }
1255:
1256: return new InternalStatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints, $endStatements);
1257: } elseif ($stmt instanceof Node\Stmt\TraitUse) {
1258: $hasYield = false;
1259: $throwPoints = [];
1260: $impurePoints = [];
1261:
1262: $traitStorage = $storage->duplicate();
1263: $traitStorage->pendingFibers = [];
1264: $this->processTraitUse($stmt, $scope, $traitStorage, $nodeCallback);
1265: $this->processPendingFibers($traitStorage);
1266: } elseif ($stmt instanceof Foreach_) {
1267: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
1268: $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt);
1269: }
1270: $condResult = $this->processExprNode($stmt, $stmt->expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
1271: $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints();
1272: $impurePoints = $condResult->getImpurePoints();
1273: $scope = $condResult->getScope();
1274: $arrayComparisonExpr = new BinaryOp\NotIdentical(
1275: $stmt->expr,
1276: new Array_([]),
1277: );
1278: $this->callNodeCallback($nodeCallback, new InForeachNode($stmt), $scope, $storage);
1279: $originalScope = $scope;
1280: $bodyScope = $scope;
1281:
1282: if ($stmt->keyVar instanceof Variable) {
1283: $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->keyVar, new GetIterableKeyTypeExpr($stmt->expr)), $originalScope, $storage);
1284: }
1285:
1286: if ($stmt->valueVar instanceof Variable) {
1287: $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->valueVar, new GetIterableValueTypeExpr($stmt->expr)), $originalScope, $storage);
1288: }
1289:
1290: $originalStorage = $storage;
1291: if ($context->isTopLevel()) {
1292: $storage = $originalStorage->duplicate();
1293:
1294: $originalScope = $this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope;
1295: $bodyScope = $this->enterForeach($originalScope, $storage, $originalScope, $stmt, $nodeCallback);
1296: $count = 0;
1297: do {
1298: $prevScope = $bodyScope;
1299: $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope);
1300: $storage = $originalStorage->duplicate();
1301: $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $nodeCallback);
1302: $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints();
1303: $bodyScope = $bodyScopeResult->getScope();
1304: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1305: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1306: }
1307: if ($bodyScope->equals($prevScope)) {
1308: break;
1309: }
1310:
1311: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
1312: $bodyScope = $prevScope->generalizeWith($bodyScope);
1313: }
1314: $count++;
1315: } while ($count < self::LOOP_SCOPE_ITERATIONS);
1316: }
1317:
1318: $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope);
1319: $storage = $originalStorage;
1320: $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $nodeCallback);
1321: $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints();
1322: $finalScope = $finalScopeResult->getScope();
1323: $scopesWithIterableValueType = [];
1324:
1325: $originalKeyVarExpr = null;
1326: $continueExitPointHasUnoriginalKeyType = false;
1327: if ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)) {
1328: $originalKeyVarExpr = new OriginalForeachKeyExpr($stmt->keyVar->name);
1329: if ($finalScope->hasExpressionType($originalKeyVarExpr)->yes()) {
1330: $scopesWithIterableValueType[] = $finalScope;
1331: } else {
1332: $continueExitPointHasUnoriginalKeyType = true;
1333: }
1334: }
1335:
1336: foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1337: $continueScope = $continueExitPoint->getScope();
1338: $finalScope = $continueScope->mergeWith($finalScope);
1339: if ($originalKeyVarExpr === null || !$continueScope->hasExpressionType($originalKeyVarExpr)->yes()) {
1340: $continueExitPointHasUnoriginalKeyType = true;
1341: continue;
1342: }
1343: $scopesWithIterableValueType[] = $continueScope;
1344: }
1345: $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class);
1346: foreach ($breakExitPoints as $breakExitPoint) {
1347: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1348: }
1349:
1350: $exprType = $scope->getType($stmt->expr);
1351: $hasExpr = $scope->hasExpressionType($stmt->expr);
1352: if (
1353: count($breakExitPoints) === 0
1354: && count($scopesWithIterableValueType) > 0
1355: && !$continueExitPointHasUnoriginalKeyType
1356: && $stmt->keyVar !== null
1357: && (!$hasExpr->no() || !$stmt->expr instanceof Variable)
1358: && $exprType->isArray()->yes()
1359: && $exprType->isConstantArray()->no()
1360: ) {
1361: $arrayExprDimFetch = new ArrayDimFetch($stmt->expr, $stmt->keyVar);
1362: $arrayDimFetchLoopTypes = [];
1363: foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) {
1364: $arrayDimFetchLoopTypes[] = $scopeWithIterableValueType->getType($arrayExprDimFetch);
1365: }
1366:
1367: $arrayDimFetchLoopType = TypeCombinator::union(...$arrayDimFetchLoopTypes);
1368:
1369: $arrayDimFetchLoopNativeTypes = [];
1370: foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) {
1371: $arrayDimFetchLoopNativeTypes[] = $scopeWithIterableValueType->getNativeType($arrayExprDimFetch);
1372: }
1373:
1374: $arrayDimFetchLoopNativeType = TypeCombinator::union(...$arrayDimFetchLoopNativeTypes);
1375:
1376: if (!$arrayDimFetchLoopType->equals($exprType->getIterableValueType())) {
1377: $newExprType = TypeTraverser::map($exprType, static function (Type $type, callable $traverse) use ($arrayDimFetchLoopType): Type {
1378: if ($type instanceof UnionType || $type instanceof IntersectionType) {
1379: return $traverse($type);
1380: }
1381:
1382: if (!$type instanceof ArrayType) {
1383: return $type;
1384: }
1385:
1386: return new ArrayType($type->getKeyType(), $arrayDimFetchLoopType);
1387: });
1388: $newExprNativeType = TypeTraverser::map($scope->getNativeType($stmt->expr), static function (Type $type, callable $traverse) use ($arrayDimFetchLoopNativeType): Type {
1389: if ($type instanceof UnionType || $type instanceof IntersectionType) {
1390: return $traverse($type);
1391: }
1392:
1393: if (!$type instanceof ArrayType) {
1394: return $type;
1395: }
1396:
1397: return new ArrayType($type->getKeyType(), $arrayDimFetchLoopNativeType);
1398: });
1399:
1400: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
1401: $finalScope = $finalScope->assignVariable(
1402: $stmt->expr->name,
1403: $newExprType,
1404: $newExprNativeType,
1405: $hasExpr,
1406: );
1407: } else {
1408: $finalScope = $finalScope->assignExpression(
1409: $stmt->expr,
1410: $newExprType,
1411: $newExprNativeType,
1412: );
1413: }
1414: }
1415: }
1416:
1417: $isIterableAtLeastOnce = $exprType->isIterableAtLeastOnce();
1418: if ($isIterableAtLeastOnce->maybe() || $exprType->isIterable()->no()) {
1419: $finalScope = $finalScope->mergeWith($scope->filterByTruthyValue(new BooleanOr(
1420: new BinaryOp\Identical(
1421: $stmt->expr,
1422: new Array_([]),
1423: ),
1424: new FuncCall(new Name\FullyQualified('is_object'), [
1425: new Arg($stmt->expr),
1426: ]),
1427: )));
1428: } elseif ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) {
1429: $finalScope = $scope;
1430: } elseif (!$this->polluteScopeWithAlwaysIterableForeach) {
1431: $finalScope = $scope->processAlwaysIterableForeachScopeWithoutPollute($finalScope);
1432: // get types from finalScope, but don't create new variables
1433: }
1434:
1435: if (!$isIterableAtLeastOnce->no()) {
1436: $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints());
1437: $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints());
1438: }
1439: if (!(new ObjectType(Traversable::class))->isSuperTypeOf($scope->getType($stmt->expr))->no()) {
1440: $throwPoints[] = InternalThrowPoint::createImplicit($scope, $stmt->expr);
1441: }
1442: if ($context->isTopLevel() && $stmt->byRef) {
1443: $finalScope = $finalScope->assignExpression(new ForeachValueByRefExpr($stmt->valueVar), new MixedType(), new MixedType());
1444: }
1445:
1446: return new InternalStatementResult(
1447: $finalScope,
1448: $finalScopeResult->hasYield() || $condResult->hasYield(),
1449: $isIterableAtLeastOnce->yes() && $finalScopeResult->isAlwaysTerminating(),
1450: $finalScopeResult->getExitPointsForOuterLoop(),
1451: $throwPoints,
1452: $impurePoints,
1453: );
1454: } elseif ($stmt instanceof While_) {
1455: $originalStorage = $storage;
1456: $storage = $originalStorage->duplicate();
1457: $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep());
1458: $beforeCondBooleanType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean();
1459: $condScope = $condResult->getFalseyScope();
1460: if (!$context->isTopLevel() && $beforeCondBooleanType->isFalse()->yes()) {
1461: if (!$this->polluteScopeWithLoopInitialAssignments) {
1462: $scope = $condScope->mergeWith($scope);
1463: }
1464:
1465: return new InternalStatementResult(
1466: $scope,
1467: $condResult->hasYield(),
1468: false,
1469: [],
1470: $condResult->getThrowPoints(),
1471: $condResult->getImpurePoints(),
1472: );
1473: }
1474: $bodyScope = $condResult->getTruthyScope();
1475:
1476: if ($context->isTopLevel()) {
1477: $count = 0;
1478: do {
1479: $prevScope = $bodyScope;
1480: $bodyScope = $bodyScope->mergeWith($scope);
1481: $storage = $originalStorage->duplicate();
1482: $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep())->getTruthyScope();
1483: $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints();
1484: $bodyScope = $bodyScopeResult->getScope();
1485: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1486: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1487: }
1488: if ($bodyScope->equals($prevScope)) {
1489: break;
1490: }
1491:
1492: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
1493: $bodyScope = $prevScope->generalizeWith($bodyScope);
1494: }
1495: $count++;
1496: } while ($count < self::LOOP_SCOPE_ITERATIONS);
1497: }
1498:
1499: $bodyScope = $bodyScope->mergeWith($scope);
1500: $bodyScopeMaybeRan = $bodyScope;
1501: $storage = $originalStorage;
1502: $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope();
1503: $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints();
1504: $finalScope = $finalScopeResult->getScope()->filterByFalseyValue($stmt->cond);
1505:
1506: $alwaysIterates = false;
1507: $neverIterates = false;
1508: if ($context->isTopLevel()) {
1509: $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScopeMaybeRan->getType($stmt->cond) : $bodyScopeMaybeRan->getNativeType($stmt->cond))->toBoolean();
1510: $alwaysIterates = $condBooleanType->isTrue()->yes();
1511: $neverIterates = $condBooleanType->isFalse()->yes();
1512: }
1513: if (!$alwaysIterates) {
1514: foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1515: $finalScope = $finalScope->mergeWith($continueExitPoint->getScope());
1516: }
1517: }
1518:
1519: $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class);
1520: foreach ($breakExitPoints as $breakExitPoint) {
1521: $finalScope = $finalScope->mergeWith($breakExitPoint->getScope());
1522: }
1523:
1524: $isIterableAtLeastOnce = $beforeCondBooleanType->isTrue()->yes();
1525: $this->callNodeCallback($nodeCallback, new BreaklessWhileLoopNode($stmt, $finalScopeResult->toPublic()->getExitPoints()), $bodyScopeMaybeRan, $storage);
1526:
1527: if ($alwaysIterates) {
1528: $isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0;
1529: } elseif ($isIterableAtLeastOnce) {
1530: $isAlwaysTerminating = $finalScopeResult->isAlwaysTerminating();
1531: } else {
1532: $isAlwaysTerminating = false;
1533: }
1534: if (!$isIterableAtLeastOnce) {
1535: if (!$this->polluteScopeWithLoopInitialAssignments) {
1536: $condScope = $condScope->mergeWith($scope);
1537: }
1538: $finalScope = $finalScope->mergeWith($condScope);
1539: }
1540:
1541: $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints();
1542: $impurePoints = $condResult->getImpurePoints();
1543: if (!$neverIterates) {
1544: $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints());
1545: $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints());
1546: }
1547:
1548: return new InternalStatementResult(
1549: $finalScope,
1550: $finalScopeResult->hasYield() || $condResult->hasYield(),
1551: $isAlwaysTerminating,
1552: $finalScopeResult->getExitPointsForOuterLoop(),
1553: $throwPoints,
1554: $impurePoints,
1555: );
1556: } elseif ($stmt instanceof Do_) {
1557: $finalScope = null;
1558: $bodyScope = $scope;
1559: $count = 0;
1560: $hasYield = false;
1561: $throwPoints = [];
1562: $impurePoints = [];
1563: $originalStorage = $storage;
1564:
1565: if ($context->isTopLevel()) {
1566: do {
1567: $prevScope = $bodyScope;
1568: $bodyScope = $bodyScope->mergeWith($scope);
1569: $storage = $originalStorage->duplicate();
1570: $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints();
1571: $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating();
1572: $bodyScope = $bodyScopeResult->getScope();
1573: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1574: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1575: }
1576: $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope);
1577: foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
1578: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1579: }
1580: $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep())->getTruthyScope();
1581: if ($bodyScope->equals($prevScope)) {
1582: break;
1583: }
1584:
1585: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
1586: $bodyScope = $prevScope->generalizeWith($bodyScope);
1587: }
1588: $count++;
1589: } while ($count < self::LOOP_SCOPE_ITERATIONS);
1590:
1591: $bodyScope = $bodyScope->mergeWith($scope);
1592: }
1593:
1594: $storage = $originalStorage;
1595: $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints();
1596: $bodyScope = $bodyScopeResult->getScope();
1597: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1598: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1599: }
1600:
1601: $alwaysIterates = false;
1602: if ($context->isTopLevel()) {
1603: $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScope->getType($stmt->cond) : $bodyScope->getNativeType($stmt->cond))->toBoolean();
1604: $alwaysIterates = $condBooleanType->isTrue()->yes();
1605: }
1606:
1607: $this->callNodeCallback($nodeCallback, new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->toPublic()->getExitPoints()), $bodyScope, $storage);
1608:
1609: if ($alwaysIterates) {
1610: $alwaysTerminating = count($bodyScopeResult->getExitPointsByType(Break_::class)) === 0;
1611: } else {
1612: $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating();
1613: }
1614: $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope);
1615: if ($finalScope === null) {
1616: $finalScope = $scope;
1617: }
1618: if (!$alwaysTerminating) {
1619: $condResult = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep());
1620: $hasYield = $condResult->hasYield();
1621: $throwPoints = $condResult->getThrowPoints();
1622: $impurePoints = $condResult->getImpurePoints();
1623: $finalScope = $condResult->getFalseyScope();
1624: } else {
1625: $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep());
1626: }
1627: foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
1628: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1629: }
1630:
1631: return new InternalStatementResult(
1632: $finalScope,
1633: $bodyScopeResult->hasYield() || $hasYield,
1634: $alwaysTerminating,
1635: $bodyScopeResult->getExitPointsForOuterLoop(),
1636: array_merge($throwPoints, $bodyScopeResult->getThrowPoints()),
1637: array_merge($impurePoints, $bodyScopeResult->getImpurePoints()),
1638: );
1639: } elseif ($stmt instanceof For_) {
1640: $initScope = $scope;
1641: $hasYield = false;
1642: $throwPoints = [];
1643: $impurePoints = [];
1644: foreach ($stmt->init as $initExpr) {
1645: $initResult = $this->processExprNode($stmt, $initExpr, $initScope, $storage, $nodeCallback, ExpressionContext::createTopLevel());
1646: $initScope = $initResult->getScope();
1647: $hasYield = $hasYield || $initResult->hasYield();
1648: $throwPoints = array_merge($throwPoints, $initResult->getThrowPoints());
1649: $impurePoints = array_merge($impurePoints, $initResult->getImpurePoints());
1650: }
1651:
1652: $originalStorage = $storage;
1653:
1654: $bodyScope = $initScope;
1655: $isIterableAtLeastOnce = TrinaryLogic::createYes();
1656: $lastCondExpr = array_last($stmt->cond) ?? null;
1657: if (count($stmt->cond) > 0) {
1658: $storage = $originalStorage->duplicate();
1659:
1660: foreach ($stmt->cond as $condExpr) {
1661: $condResult = $this->processExprNode($stmt, $condExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep());
1662: $initScope = $condResult->getScope();
1663: $condResultScope = $condResult->getScope();
1664:
1665: // only the last condition expression is relevant whether the loop continues
1666: // see https://www.php.net/manual/en/control-structures.for.php
1667: if ($condExpr === $lastCondExpr) {
1668: $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResultScope->getType($condExpr) : $condResultScope->getNativeType($condExpr))->toBoolean();
1669: $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthiness->isTrue());
1670: }
1671:
1672: $hasYield = $hasYield || $condResult->hasYield();
1673: $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints());
1674: $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints());
1675: $bodyScope = $condResult->getTruthyScope();
1676: }
1677: }
1678:
1679: if ($context->isTopLevel()) {
1680: $count = 0;
1681: do {
1682: $prevScope = $bodyScope;
1683: $storage = $originalStorage->duplicate();
1684: $bodyScope = $bodyScope->mergeWith($initScope);
1685: if ($lastCondExpr !== null) {
1686: $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep())->getTruthyScope();
1687: }
1688: $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints();
1689: $bodyScope = $bodyScopeResult->getScope();
1690: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1691: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1692: }
1693:
1694: foreach ($stmt->loop as $loopExpr) {
1695: $exprResult = $this->processExprNode($stmt, $loopExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createTopLevel());
1696: $bodyScope = $exprResult->getScope();
1697: $hasYield = $hasYield || $exprResult->hasYield();
1698: $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
1699: $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints());
1700: }
1701:
1702: if ($bodyScope->equals($prevScope)) {
1703: break;
1704: }
1705:
1706: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
1707: $bodyScope = $prevScope->generalizeWith($bodyScope);
1708: }
1709: $count++;
1710: } while ($count < self::LOOP_SCOPE_ITERATIONS);
1711: }
1712:
1713: $storage = $originalStorage;
1714: $bodyScope = $bodyScope->mergeWith($initScope);
1715:
1716: $alwaysIterates = TrinaryLogic::createFromBoolean($context->isTopLevel());
1717: if ($lastCondExpr !== null) {
1718: $alwaysIterates = $alwaysIterates->and($bodyScope->getType($lastCondExpr)->toBoolean()->isTrue());
1719: $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope();
1720: $bodyScope = $this->inferForLoopExpressions($stmt, $lastCondExpr, $bodyScope);
1721: }
1722:
1723: $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints();
1724: $finalScope = $finalScopeResult->getScope();
1725: foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1726: $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
1727: }
1728:
1729: $loopScope = $finalScope;
1730: foreach ($stmt->loop as $loopExpr) {
1731: $loopScope = $this->processExprNode($stmt, $loopExpr, $loopScope, $storage, $nodeCallback, ExpressionContext::createTopLevel())->getScope();
1732: }
1733: $finalScope = $finalScope->generalizeWith($loopScope);
1734:
1735: if ($lastCondExpr !== null) {
1736: $finalScope = $finalScope->filterByFalseyValue($lastCondExpr);
1737: }
1738:
1739: foreach ($finalScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
1740: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1741: }
1742:
1743: if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) {
1744: if ($this->polluteScopeWithLoopInitialAssignments) {
1745: $finalScope = $initScope;
1746: } else {
1747: $finalScope = $scope;
1748: }
1749:
1750: } elseif ($isIterableAtLeastOnce->maybe()) {
1751: if ($this->polluteScopeWithLoopInitialAssignments) {
1752: $finalScope = $finalScope->mergeWith($initScope);
1753: } else {
1754: $finalScope = $finalScope->mergeWith($scope);
1755: }
1756: } else {
1757: if (!$this->polluteScopeWithLoopInitialAssignments) {
1758: $finalScope = $finalScope->mergeWith($scope);
1759: }
1760: }
1761:
1762: if ($alwaysIterates->yes()) {
1763: $isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0;
1764: } elseif ($isIterableAtLeastOnce->yes()) {
1765: $isAlwaysTerminating = $finalScopeResult->isAlwaysTerminating();
1766: } else {
1767: $isAlwaysTerminating = false;
1768: }
1769:
1770: return new InternalStatementResult(
1771: $finalScope,
1772: $finalScopeResult->hasYield() || $hasYield,
1773: $isAlwaysTerminating,
1774: $finalScopeResult->getExitPointsForOuterLoop(),
1775: array_merge($throwPoints, $finalScopeResult->getThrowPoints()),
1776: array_merge($impurePoints, $finalScopeResult->getImpurePoints()),
1777: );
1778: } elseif ($stmt instanceof Switch_) {
1779: $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
1780: $scope = $condResult->getScope();
1781: $scopeForBranches = $scope;
1782: $finalScope = null;
1783: $prevScope = null;
1784: $hasDefaultCase = false;
1785: $alwaysTerminating = true;
1786: $hasYield = $condResult->hasYield();
1787: $exitPointsForOuterLoop = [];
1788: $throwPoints = $condResult->getThrowPoints();
1789: $impurePoints = $condResult->getImpurePoints();
1790: $fullCondExpr = null;
1791: foreach ($stmt->cases as $caseNode) {
1792: if ($caseNode->cond !== null) {
1793: $condExpr = new BinaryOp\Equal($stmt->cond, $caseNode->cond);
1794: $fullCondExpr = $fullCondExpr === null ? $condExpr : new BooleanOr($fullCondExpr, $condExpr);
1795: $caseResult = $this->processExprNode($stmt, $caseNode->cond, $scopeForBranches, $storage, $nodeCallback, ExpressionContext::createDeep());
1796: $scopeForBranches = $caseResult->getScope();
1797: $hasYield = $hasYield || $caseResult->hasYield();
1798: $throwPoints = array_merge($throwPoints, $caseResult->getThrowPoints());
1799: $impurePoints = array_merge($impurePoints, $caseResult->getImpurePoints());
1800: $branchScope = $caseResult->getTruthyScope()->filterByTruthyValue($condExpr);
1801: } else {
1802: $hasDefaultCase = true;
1803: $fullCondExpr = null;
1804: $branchScope = $scopeForBranches;
1805: }
1806:
1807: $branchScope = $branchScope->mergeWith($prevScope);
1808: $branchScopeResult = $this->processStmtNodesInternal($caseNode, $caseNode->stmts, $branchScope, $storage, $nodeCallback, $context);
1809: $branchScope = $branchScopeResult->getScope();
1810: $branchFinalScopeResult = $branchScopeResult->filterOutLoopExitPoints();
1811: $hasYield = $hasYield || $branchFinalScopeResult->hasYield();
1812: foreach ($branchScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
1813: $alwaysTerminating = false;
1814: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1815: }
1816: foreach ($branchScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1817: $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
1818: }
1819: $exitPointsForOuterLoop = array_merge($exitPointsForOuterLoop, $branchFinalScopeResult->getExitPointsForOuterLoop());
1820: $throwPoints = array_merge($throwPoints, $branchFinalScopeResult->getThrowPoints());
1821: $impurePoints = array_merge($impurePoints, $branchFinalScopeResult->getImpurePoints());
1822: if ($branchScopeResult->isAlwaysTerminating()) {
1823: $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating();
1824: $prevScope = null;
1825: if (isset($fullCondExpr)) {
1826: $scopeForBranches = $scopeForBranches->filterByFalseyValue($fullCondExpr);
1827: $fullCondExpr = null;
1828: }
1829: if (!$branchFinalScopeResult->isAlwaysTerminating()) {
1830: $finalScope = $branchScope->mergeWith($finalScope);
1831: }
1832: } else {
1833: $prevScope = $branchScope;
1834: }
1835: }
1836:
1837: $exhaustive = $scopeForBranches->getType($stmt->cond) instanceof NeverType;
1838:
1839: if (!$hasDefaultCase && !$exhaustive) {
1840: $alwaysTerminating = false;
1841: }
1842:
1843: if ($prevScope !== null && isset($branchFinalScopeResult)) {
1844: $finalScope = $prevScope->mergeWith($finalScope);
1845: $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating();
1846: }
1847:
1848: if ((!$hasDefaultCase && !$exhaustive) || $finalScope === null) {
1849: $finalScope = $scopeForBranches->mergeWith($finalScope);
1850: }
1851:
1852: return new InternalStatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPointsForOuterLoop, $throwPoints, $impurePoints);
1853: } elseif ($stmt instanceof TryCatch) {
1854: $branchScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context);
1855: $branchScope = $branchScopeResult->getScope();
1856: $finalScope = $branchScopeResult->isAlwaysTerminating() ? null : $branchScope;
1857:
1858: $exitPoints = [];
1859: $finallyExitPoints = [];
1860: $alwaysTerminating = $branchScopeResult->isAlwaysTerminating();
1861: $hasYield = $branchScopeResult->hasYield();
1862:
1863: if ($stmt->finally !== null) {
1864: $finallyScope = $branchScope;
1865: } else {
1866: $finallyScope = null;
1867: }
1868: foreach ($branchScopeResult->getExitPoints() as $exitPoint) {
1869: $finallyExitPoints[] = $exitPoint->toPublic();
1870: if ($exitPoint->getStatement() instanceof Node\Stmt\Expression && $exitPoint->getStatement()->expr instanceof Expr\Throw_) {
1871: continue;
1872: }
1873: if ($finallyScope !== null) {
1874: $finallyScope = $finallyScope->mergeWith($exitPoint->getScope());
1875: }
1876: $exitPoints[] = $exitPoint;
1877: }
1878:
1879: $throwPoints = $branchScopeResult->getThrowPoints();
1880: $impurePoints = $branchScopeResult->getImpurePoints();
1881: $throwPointsForLater = [];
1882: $pastCatchTypes = new NeverType();
1883:
1884: foreach ($stmt->catches as $catchNode) {
1885: $this->callNodeCallback($nodeCallback, $catchNode, $scope, $storage);
1886:
1887: $originalCatchTypes = array_map(static fn (Name $name): Type => new ObjectType($name->toString()), $catchNode->types);
1888: $catchTypes = array_map(static fn (Type $type): Type => TypeCombinator::remove($type, $pastCatchTypes), $originalCatchTypes);
1889:
1890: $originalCatchType = TypeCombinator::union(...$originalCatchTypes);
1891: $catchType = TypeCombinator::union(...$catchTypes);
1892: $pastCatchTypes = TypeCombinator::union($pastCatchTypes, $originalCatchType);
1893:
1894: $matchingThrowPoints = [];
1895: $matchingCatchTypes = array_fill_keys(array_keys($originalCatchTypes), false);
1896:
1897: // throwable matches all
1898: foreach ($originalCatchTypes as $catchTypeIndex => $catchTypeItem) {
1899: if (!$catchTypeItem->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) {
1900: continue;
1901: }
1902:
1903: foreach ($throwPoints as $throwPointIndex => $throwPoint) {
1904: $matchingThrowPoints[$throwPointIndex] = $throwPoint;
1905: $matchingCatchTypes[$catchTypeIndex] = true;
1906: }
1907: }
1908:
1909: // explicit only
1910: $onlyExplicitIsThrow = true;
1911: if (count($matchingThrowPoints) === 0) {
1912: foreach ($throwPoints as $throwPointIndex => $throwPoint) {
1913: foreach ($catchTypes as $catchTypeIndex => $catchTypeItem) {
1914: if ($catchTypeItem->isSuperTypeOf($throwPoint->getType())->no()) {
1915: continue;
1916: }
1917:
1918: $matchingCatchTypes[$catchTypeIndex] = true;
1919: if (!$throwPoint->isExplicit()) {
1920: continue;
1921: }
1922: $throwNode = $throwPoint->getNode();
1923: if (
1924: !$throwNode instanceof Expr\Throw_
1925: && !($throwNode instanceof Node\Stmt\Expression && $throwNode->expr instanceof Expr\Throw_)
1926: ) {
1927: $onlyExplicitIsThrow = false;
1928: }
1929: $matchingThrowPoints[$throwPointIndex] = $throwPoint;
1930: }
1931: }
1932: }
1933:
1934: // implicit only
1935: if (count($matchingThrowPoints) === 0 || $onlyExplicitIsThrow) {
1936: foreach ($throwPoints as $throwPointIndex => $throwPoint) {
1937: if ($throwPoint->isExplicit()) {
1938: continue;
1939: }
1940:
1941: foreach ($catchTypes as $catchTypeItem) {
1942: if ($catchTypeItem->isSuperTypeOf($throwPoint->getType())->no()) {
1943: continue;
1944: }
1945:
1946: $matchingThrowPoints[$throwPointIndex] = $throwPoint;
1947: }
1948: }
1949: }
1950:
1951: // include previously removed throw points
1952: if (count($matchingThrowPoints) === 0) {
1953: if ($originalCatchType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) {
1954: foreach ($branchScopeResult->getThrowPoints() as $originalThrowPoint) {
1955: if (!$originalThrowPoint->canContainAnyThrowable()) {
1956: continue;
1957: }
1958:
1959: $matchingThrowPoints[] = $originalThrowPoint;
1960: $matchingCatchTypes = array_fill_keys(array_keys($originalCatchTypes), true);
1961: }
1962: }
1963: }
1964:
1965: // emit error
1966: foreach ($matchingCatchTypes as $catchTypeIndex => $matched) {
1967: if ($matched) {
1968: continue;
1969: }
1970: $this->callNodeCallback($nodeCallback, new CatchWithUnthrownExceptionNode($catchNode, $catchTypes[$catchTypeIndex], $originalCatchTypes[$catchTypeIndex]), $scope, $storage);
1971: }
1972:
1973: if (count($matchingThrowPoints) === 0) {
1974: continue;
1975: }
1976:
1977: // recompute throw points
1978: $newThrowPoints = [];
1979: foreach ($throwPoints as $throwPoint) {
1980: $newThrowPoint = $throwPoint->subtractCatchType($originalCatchType);
1981:
1982: if ($newThrowPoint->getType() instanceof NeverType) {
1983: continue;
1984: }
1985:
1986: $newThrowPoints[] = $newThrowPoint;
1987: }
1988: $throwPoints = $newThrowPoints;
1989:
1990: $catchScope = null;
1991: foreach ($matchingThrowPoints as $matchingThrowPoint) {
1992: if ($catchScope === null) {
1993: $catchScope = $matchingThrowPoint->getScope();
1994: } else {
1995: $catchScope = $catchScope->mergeWith($matchingThrowPoint->getScope());
1996: }
1997: }
1998:
1999: $variableName = null;
2000: if ($catchNode->var !== null) {
2001: if (!is_string($catchNode->var->name)) {
2002: throw new ShouldNotHappenException();
2003: }
2004:
2005: $variableName = $catchNode->var->name;
2006: }
2007:
2008: $catchScopeResult = $this->processStmtNodesInternal($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $storage, $nodeCallback, $context);
2009: $catchScopeForFinally = $catchScopeResult->getScope();
2010:
2011: $finalScope = $catchScopeResult->isAlwaysTerminating() ? $finalScope : $catchScopeResult->getScope()->mergeWith($finalScope);
2012: $alwaysTerminating = $alwaysTerminating && $catchScopeResult->isAlwaysTerminating();
2013: $hasYield = $hasYield || $catchScopeResult->hasYield();
2014: $catchThrowPoints = $catchScopeResult->getThrowPoints();
2015: $impurePoints = array_merge($impurePoints, $catchScopeResult->getImpurePoints());
2016: $throwPointsForLater = array_merge($throwPointsForLater, $catchThrowPoints);
2017:
2018: if ($finallyScope !== null) {
2019: $finallyScope = $finallyScope->mergeWith($catchScopeForFinally);
2020: }
2021: foreach ($catchScopeResult->getExitPoints() as $exitPoint) {
2022: $finallyExitPoints[] = $exitPoint->toPublic();
2023: if ($exitPoint->getStatement() instanceof Node\Stmt\Expression && $exitPoint->getStatement()->expr instanceof Expr\Throw_) {
2024: continue;
2025: }
2026: if ($finallyScope !== null) {
2027: $finallyScope = $finallyScope->mergeWith($exitPoint->getScope());
2028: }
2029: $exitPoints[] = $exitPoint;
2030: }
2031:
2032: foreach ($catchThrowPoints as $catchThrowPoint) {
2033: if ($finallyScope === null) {
2034: continue;
2035: }
2036: $finallyScope = $finallyScope->mergeWith($catchThrowPoint->getScope());
2037: }
2038: }
2039:
2040: if ($finalScope === null) {
2041: $finalScope = $scope;
2042: }
2043:
2044: foreach ($throwPoints as $throwPoint) {
2045: if ($finallyScope === null) {
2046: continue;
2047: }
2048: $finallyScope = $finallyScope->mergeWith($throwPoint->getScope());
2049: }
2050:
2051: if ($finallyScope !== null) {
2052: $originalFinallyScope = $finallyScope;
2053: $finallyResult = $this->processStmtNodesInternal($stmt->finally, $stmt->finally->stmts, $finallyScope, $storage, $nodeCallback, $context);
2054: $alwaysTerminating = $alwaysTerminating || $finallyResult->isAlwaysTerminating();
2055: $hasYield = $hasYield || $finallyResult->hasYield();
2056: $throwPointsForLater = array_merge($throwPointsForLater, $finallyResult->getThrowPoints());
2057: $impurePoints = array_merge($impurePoints, $finallyResult->getImpurePoints());
2058: $finallyScope = $finallyResult->getScope();
2059: $finalScope = $finallyResult->isAlwaysTerminating() ? $finalScope : $finalScope->processFinallyScope($finallyScope, $originalFinallyScope);
2060: if (count($finallyResult->getExitPoints()) > 0) {
2061: $this->callNodeCallback($nodeCallback, new FinallyExitPointsNode(
2062: $finallyResult->toPublic()->getExitPoints(),
2063: $finallyExitPoints,
2064: ), $scope, $storage);
2065: }
2066: $exitPoints = array_merge($exitPoints, $finallyResult->getExitPoints());
2067: }
2068:
2069: return new InternalStatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, array_merge($throwPoints, $throwPointsForLater), $impurePoints);
2070: } elseif ($stmt instanceof Unset_) {
2071: $hasYield = false;
2072: $throwPoints = [];
2073: $impurePoints = [];
2074: foreach ($stmt->vars as $var) {
2075: $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var);
2076: $exprResult = $this->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2077: $scope = $exprResult->getScope();
2078: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
2079: $hasYield = $hasYield || $exprResult->hasYield();
2080: $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
2081: $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints());
2082: if ($var instanceof ArrayDimFetch && $var->dim !== null) {
2083: $clonedVar = $this->deepNodeCloner->cloneNode($var->var);
2084: $traverser = new NodeTraverser();
2085: $traverser->addVisitor(new class () extends NodeVisitorAbstract {
2086:
2087: #[Override]
2088: public function leaveNode(Node $node): ?ExistingArrayDimFetch
2089: {
2090: if (!$node instanceof ArrayDimFetch || $node->dim === null) {
2091: return null;
2092: }
2093:
2094: return new ExistingArrayDimFetch($node->var, $node->dim);
2095: }
2096:
2097: });
2098:
2099: /** @var Expr $clonedVar */
2100: [$clonedVar] = $traverser->traverse([$clonedVar]);
2101: $scope = $this->processVirtualAssign($scope, $storage, $stmt, $clonedVar, new UnsetOffsetExpr($var->var, $var->dim), $nodeCallback)->getScope();
2102: } elseif ($var instanceof PropertyFetch) {
2103: $scope = $scope->invalidateExpression($var);
2104: $impurePoints[] = new ImpurePoint(
2105: $scope,
2106: $var,
2107: 'propertyUnset',
2108: 'property unset',
2109: true,
2110: );
2111: } else {
2112: $scope = $scope->invalidateExpression($var);
2113: }
2114:
2115: $scope = $scope->invalidateExpression(new ForeachValueByRefExpr($var));
2116: }
2117: } elseif ($stmt instanceof Node\Stmt\Use_) {
2118: $hasYield = false;
2119: $throwPoints = [];
2120: $impurePoints = [];
2121: foreach ($stmt->uses as $use) {
2122: $this->callNodeCallback($nodeCallback, $use, $scope, $storage);
2123: }
2124: } elseif ($stmt instanceof Node\Stmt\Global_) {
2125: $hasYield = false;
2126: $throwPoints = [];
2127: $impurePoints = [
2128: new ImpurePoint(
2129: $scope,
2130: $stmt,
2131: 'global',
2132: 'global variable',
2133: true,
2134: ),
2135: ];
2136: $vars = [];
2137: foreach ($stmt->vars as $var) {
2138: if (!$var instanceof Variable) {
2139: throw new ShouldNotHappenException();
2140: }
2141: $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var);
2142: $varResult = $this->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2143: $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints());
2144: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
2145:
2146: if (!is_string($var->name)) {
2147: continue;
2148: }
2149:
2150: $scope = $scope->assignVariable($var->name, new MixedType(), new MixedType(), TrinaryLogic::createYes());
2151: $vars[] = $var->name;
2152: }
2153: $scope = $this->processVarAnnotation($scope, $vars, $stmt);
2154: } elseif ($stmt instanceof Static_) {
2155: $hasYield = false;
2156: $throwPoints = [];
2157: $impurePoints = [
2158: new ImpurePoint(
2159: $scope,
2160: $stmt,
2161: 'static',
2162: 'static variable',
2163: true,
2164: ),
2165: ];
2166:
2167: $vars = [];
2168: foreach ($stmt->vars as $var) {
2169: if (!is_string($var->var->name)) {
2170: throw new ShouldNotHappenException();
2171: }
2172:
2173: if ($var->default !== null) {
2174: $defaultExprResult = $this->processExprNode($stmt, $var->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2175: $impurePoints = array_merge($impurePoints, $defaultExprResult->getImpurePoints());
2176: }
2177:
2178: $scope = $scope->enterExpressionAssign($var->var);
2179: $varResult = $this->processExprNode($stmt, $var->var, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2180: $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints());
2181: $scope = $scope->exitExpressionAssign($var->var);
2182:
2183: $scope = $scope->assignVariable($var->var->name, new MixedType(), new MixedType(), TrinaryLogic::createYes());
2184: $vars[] = $var->var->name;
2185: }
2186:
2187: $scope = $this->processVarAnnotation($scope, $vars, $stmt);
2188: } elseif ($stmt instanceof Node\Stmt\Const_) {
2189: $hasYield = false;
2190: $throwPoints = [];
2191: $impurePoints = [];
2192: foreach ($stmt->consts as $const) {
2193: $this->callNodeCallback($nodeCallback, $const, $scope, $storage);
2194: $constResult = $this->processExprNode($stmt, $const->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2195: $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints());
2196: if ($const->namespacedName !== null) {
2197: $constantName = new Name\FullyQualified($const->namespacedName->toString());
2198: } else {
2199: $constantName = new Name\FullyQualified($const->name->toString());
2200: }
2201: $scope = $scope->assignExpression(new ConstFetch($constantName), $scope->getType($const->value), $scope->getNativeType($const->value));
2202: }
2203: } elseif ($stmt instanceof Node\Stmt\ClassConst) {
2204: $hasYield = false;
2205: $throwPoints = [];
2206: $impurePoints = [];
2207: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
2208: foreach ($stmt->consts as $const) {
2209: $this->callNodeCallback($nodeCallback, $const, $scope, $storage);
2210: $constResult = $this->processExprNode($stmt, $const->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2211: $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints());
2212: if ($scope->getClassReflection() === null) {
2213: throw new ShouldNotHappenException();
2214: }
2215: $scope = $scope->assignExpression(
2216: new Expr\ClassConstFetch(new Name\FullyQualified($scope->getClassReflection()->getName()), $const->name),
2217: $scope->getType($const->value),
2218: $scope->getNativeType($const->value),
2219: );
2220: }
2221: } elseif ($stmt instanceof Node\Stmt\EnumCase) {
2222: $hasYield = false;
2223: $throwPoints = [];
2224: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
2225: $impurePoints = [];
2226: if ($stmt->expr !== null) {
2227: $exprResult = $this->processExprNode($stmt, $stmt->expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2228: $impurePoints = $exprResult->getImpurePoints();
2229: }
2230: } elseif ($stmt instanceof InlineHTML) {
2231: $hasYield = false;
2232: $throwPoints = [];
2233: $impurePoints = [
2234: new ImpurePoint($scope, $stmt, 'betweenPhpTags', 'output between PHP opening and closing tags', true),
2235: ];
2236: } elseif ($stmt instanceof Node\Stmt\Block) {
2237: $result = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context);
2238: if ($this->polluteScopeWithBlock) {
2239: return $result;
2240: }
2241:
2242: return new InternalStatementResult(
2243: $scope->mergeWith($result->getScope()),
2244: $result->hasYield(),
2245: $result->isAlwaysTerminating(),
2246: $result->getExitPoints(),
2247: $result->getThrowPoints(),
2248: $result->getImpurePoints(),
2249: $result->getEndStatements(),
2250: );
2251: } elseif ($stmt instanceof Node\Stmt\Nop) {
2252: $hasYield = false;
2253: $throwPoints = $overridingThrowPoints ?? [];
2254: $impurePoints = [];
2255: } elseif ($stmt instanceof Node\Stmt\GroupUse) {
2256: $hasYield = false;
2257: $throwPoints = [];
2258: foreach ($stmt->uses as $use) {
2259: $this->callNodeCallback($nodeCallback, $use, $scope, $storage);
2260: }
2261: $impurePoints = [];
2262: } else {
2263: $hasYield = false;
2264: $throwPoints = $overridingThrowPoints ?? [];
2265: $impurePoints = [];
2266: }
2267:
2268: return new InternalStatementResult($scope, $hasYield, false, [], $throwPoints, $impurePoints);
2269: }
2270:
2271: /**
2272: * @return array{bool, string|null}
2273: */
2274: private function getDeprecatedAttribute(Scope $scope, Node\Stmt\Function_|Node\Stmt\ClassMethod|Node\PropertyHook $stmt): array
2275: {
2276: $initializerExprContext = InitializerExprContext::fromStubParameter(
2277: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
2278: $scope->getFile(),
2279: $stmt,
2280: );
2281: $isDeprecated = false;
2282: $deprecatedDescription = null;
2283: $deprecatedDescriptionType = null;
2284: foreach ($stmt->attrGroups as $attrGroup) {
2285: foreach ($attrGroup->attrs as $attr) {
2286: if ($attr->name->toString() !== 'Deprecated') {
2287: continue;
2288: }
2289: $isDeprecated = true;
2290: $arguments = $attr->args;
2291: foreach ($arguments as $i => $arg) {
2292: $argName = $arg->name;
2293: if ($argName === null) {
2294: if ($i !== 0) {
2295: continue;
2296: }
2297:
2298: $deprecatedDescriptionType = $this->initializerExprTypeResolver->getType($arg->value, $initializerExprContext);
2299: break;
2300: }
2301:
2302: if ($argName->toString() !== 'message') {
2303: continue;
2304: }
2305:
2306: $deprecatedDescriptionType = $this->initializerExprTypeResolver->getType($arg->value, $initializerExprContext);
2307: break;
2308: }
2309: }
2310: }
2311:
2312: if ($deprecatedDescriptionType !== null) {
2313: $constantStrings = $deprecatedDescriptionType->getConstantStrings();
2314: if (count($constantStrings) === 1) {
2315: $deprecatedDescription = $constantStrings[0]->getValue();
2316: }
2317: }
2318:
2319: return [$isDeprecated, $deprecatedDescription];
2320: }
2321:
2322: /**
2323: * @return InternalThrowPoint[]|null
2324: */
2325: private function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $scope): ?array
2326: {
2327: foreach ($statement->getComments() as $comment) {
2328: if (!$comment instanceof Doc) {
2329: continue;
2330: }
2331:
2332: $function = $scope->getFunction();
2333: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
2334: $scope->getFile(),
2335: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
2336: $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
2337: $function !== null ? $function->getName() : null,
2338: $comment->getText(),
2339: );
2340:
2341: $throwsTag = $resolvedPhpDoc->getThrowsTag();
2342: if ($throwsTag !== null) {
2343: $throwsType = $throwsTag->getType();
2344: if ($throwsType->isVoid()->yes()) {
2345: return [];
2346: }
2347:
2348: return [InternalThrowPoint::createExplicit($scope, $throwsType, $statement, false)];
2349: }
2350: }
2351:
2352: return null;
2353: }
2354:
2355: private function getCurrentClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection
2356: {
2357: if (!$this->reflectionProvider->hasClass($className)) {
2358: return $this->createAstClassReflection($stmt, $className, $scope);
2359: }
2360:
2361: $defaultClassReflection = $this->reflectionProvider->getClass($className);
2362: if ($defaultClassReflection->getFileName() !== $scope->getFile()) {
2363: return $this->createAstClassReflection($stmt, $className, $scope);
2364: }
2365:
2366: $startLine = $defaultClassReflection->getNativeReflection()->getStartLine();
2367: if ($startLine !== $stmt->getStartLine()) {
2368: return $this->createAstClassReflection($stmt, $className, $scope);
2369: }
2370:
2371: return $defaultClassReflection;
2372: }
2373:
2374: private function createAstClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection
2375: {
2376: $nodeToReflection = new NodeToReflection();
2377: $betterReflectionClass = $nodeToReflection->__invoke(
2378: $this->reflector,
2379: $stmt,
2380: new LocatedSource(FileReader::read($scope->getFile()), $className, $scope->getFile()),
2381: $scope->getNamespace() !== null ? new Node\Stmt\Namespace_(new Name($scope->getNamespace())) : null,
2382: );
2383: if (!$betterReflectionClass instanceof \PHPStan\BetterReflection\Reflection\ReflectionClass) {
2384: throw new ShouldNotHappenException();
2385: }
2386:
2387: $enumAdapter = base64_decode('UEhQU3RhblxCZXR0ZXJSZWZsZWN0aW9uXFJlZmxlY3Rpb25cQWRhcHRlclxSZWZsZWN0aW9uRW51bQ==', true);
2388:
2389: return $this->classReflectionFactory->create(
2390: $betterReflectionClass->getName(),
2391: $betterReflectionClass instanceof ReflectionEnum && PHP_VERSION_ID >= 80000 ? new $enumAdapter($betterReflectionClass) : new ReflectionClass($betterReflectionClass),
2392: null,
2393: null,
2394: null,
2395: sprintf('%s:%d', $scope->getFile(), $stmt->getStartLine()),
2396: );
2397: }
2398:
2399: private function lookForSetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope
2400: {
2401: return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr));
2402: }
2403:
2404: private function lookForUnsetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope
2405: {
2406: return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->unsetAllowedUndefinedExpression($expr));
2407: }
2408:
2409: /**
2410: * @param Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback
2411: */
2412: private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope
2413: {
2414: if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) {
2415: $scope = $callback($scope, $expr);
2416: }
2417:
2418: if ($expr instanceof ArrayDimFetch) {
2419: $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback);
2420: } elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) {
2421: $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback);
2422: } elseif ($expr instanceof StaticPropertyFetch && $expr->class instanceof Expr) {
2423: $scope = $this->lookForExpressionCallback($scope, $expr->class, $callback);
2424: } elseif ($expr instanceof List_) {
2425: foreach ($expr->items as $item) {
2426: if ($item === null) {
2427: continue;
2428: }
2429:
2430: $scope = $this->lookForExpressionCallback($scope, $item->value, $callback);
2431: }
2432: }
2433:
2434: return $scope;
2435: }
2436:
2437: private function ensureShallowNonNullability(MutatingScope $scope, Scope $originalScope, Expr $exprToSpecify): EnsuredNonNullabilityResult
2438: {
2439: $exprType = $scope->getType($exprToSpecify);
2440: $isNull = $exprType->isNull();
2441: if ($isNull->yes()) {
2442: return new EnsuredNonNullabilityResult($scope, []);
2443: }
2444:
2445: // keep certainty
2446: $certainty = TrinaryLogic::createYes();
2447: $hasExpressionType = $originalScope->hasExpressionType($exprToSpecify);
2448: if (!$hasExpressionType->no()) {
2449: $certainty = $hasExpressionType;
2450: }
2451:
2452: $exprTypeWithoutNull = TypeCombinator::removeNull($exprType);
2453: if ($exprType->equals($exprTypeWithoutNull)) {
2454: $originalExprType = $originalScope->getType($exprToSpecify);
2455: if (!$originalExprType->equals($exprTypeWithoutNull)) {
2456: $originalNativeType = $originalScope->getNativeType($exprToSpecify);
2457:
2458: return new EnsuredNonNullabilityResult($scope, [
2459: new EnsuredNonNullabilityResultExpression($exprToSpecify, $originalExprType, $originalNativeType, $certainty),
2460: ]);
2461: }
2462: return new EnsuredNonNullabilityResult($scope, []);
2463: }
2464:
2465: $nativeType = $scope->getNativeType($exprToSpecify);
2466:
2467: $specifiedExpressions = [
2468: new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType, $nativeType, $certainty),
2469: ];
2470:
2471: // When narrowing an ArrayDimFetch, specifyExpressionType also recursively
2472: // narrows the parent array's offset type via intersection with HasOffsetValueType.
2473: // To properly revert this, we must also save and restore the parent expression's type.
2474: if ($exprToSpecify instanceof Expr\ArrayDimFetch && $exprToSpecify->dim !== null) {
2475: $parentExpr = $exprToSpecify->var;
2476: $parentCertainty = TrinaryLogic::createYes();
2477: $hasParentExpressionType = $originalScope->hasExpressionType($parentExpr);
2478: if (!$hasParentExpressionType->no()) {
2479: $parentCertainty = $hasParentExpressionType;
2480: }
2481: array_unshift($specifiedExpressions, new EnsuredNonNullabilityResultExpression(
2482: $parentExpr,
2483: $scope->getType($parentExpr),
2484: $scope->getNativeType($parentExpr),
2485: $parentCertainty,
2486: ));
2487: }
2488:
2489: $scope = $scope->specifyExpressionType(
2490: $exprToSpecify,
2491: $exprTypeWithoutNull,
2492: TypeCombinator::removeNull($nativeType),
2493: TrinaryLogic::createYes(),
2494: );
2495:
2496: return new EnsuredNonNullabilityResult(
2497: $scope,
2498: $specifiedExpressions,
2499: );
2500: }
2501:
2502: private function ensureNonNullability(MutatingScope $scope, Expr $expr): EnsuredNonNullabilityResult
2503: {
2504: $specifiedExpressions = [];
2505: $originalScope = $scope;
2506: $scope = $this->lookForExpressionCallback($scope, $expr, function ($scope, $expr) use (&$specifiedExpressions, $originalScope) {
2507: $result = $this->ensureShallowNonNullability($scope, $originalScope, $expr);
2508: foreach ($result->getSpecifiedExpressions() as $specifiedExpression) {
2509: $specifiedExpressions[] = $specifiedExpression;
2510: }
2511: return $result->getScope();
2512: });
2513:
2514: return new EnsuredNonNullabilityResult($scope, $specifiedExpressions);
2515: }
2516:
2517: /**
2518: * @param EnsuredNonNullabilityResultExpression[] $specifiedExpressions
2519: */
2520: private function revertNonNullability(MutatingScope $scope, array $specifiedExpressions): MutatingScope
2521: {
2522: foreach ($specifiedExpressions as $specifiedExpressionResult) {
2523: $scope = $scope->specifyExpressionType(
2524: $specifiedExpressionResult->getExpression(),
2525: $specifiedExpressionResult->getOriginalType(),
2526: $specifiedExpressionResult->getOriginalNativeType(),
2527: $specifiedExpressionResult->getCertainty(),
2528: );
2529: }
2530:
2531: return $scope;
2532: }
2533:
2534: private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr
2535: {
2536: if (($expr instanceof MethodCall || $expr instanceof Expr\StaticCall) && $expr->name instanceof Node\Identifier) {
2537: if (array_key_exists($expr->name->toLowerString(), $this->earlyTerminatingMethodNames)) {
2538: if ($expr instanceof MethodCall) {
2539: $methodCalledOnType = $scope->getType($expr->var);
2540: } else {
2541: if ($expr->class instanceof Name) {
2542: $methodCalledOnType = $scope->resolveTypeByName($expr->class);
2543: } else {
2544: $methodCalledOnType = $scope->getType($expr->class);
2545: }
2546: }
2547:
2548: foreach ($methodCalledOnType->getObjectClassNames() as $referencedClass) {
2549: if (!$this->reflectionProvider->hasClass($referencedClass)) {
2550: continue;
2551: }
2552:
2553: $classReflection = $this->reflectionProvider->getClass($referencedClass);
2554: foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames(), $classReflection->getNativeReflection()->getInterfaceNames()) as $className) {
2555: if (!isset($this->earlyTerminatingMethodCalls[$className])) {
2556: continue;
2557: }
2558:
2559: if (in_array((string) $expr->name, $this->earlyTerminatingMethodCalls[$className], true)) {
2560: return $expr;
2561: }
2562: }
2563: }
2564: }
2565: }
2566:
2567: if ($expr instanceof FuncCall && $expr->name instanceof Name) {
2568: if (in_array((string) $expr->name, $this->earlyTerminatingFunctionCalls, true)) {
2569: return $expr;
2570: }
2571: }
2572:
2573: if ($expr instanceof Expr\Exit_ || $expr instanceof Expr\Throw_) {
2574: return $expr;
2575: }
2576:
2577: $exprType = $scope->getType($expr);
2578: if ($exprType instanceof NeverType && $exprType->isExplicit()) {
2579: return $expr;
2580: }
2581:
2582: return null;
2583: }
2584:
2585: /**
2586: * @param callable(Node $node, Scope $scope): void $nodeCallback
2587: */
2588: public function processExprNode(
2589: Node\Stmt $stmt,
2590: Expr $expr,
2591: MutatingScope $scope,
2592: ExpressionResultStorage $storage,
2593: callable $nodeCallback,
2594: ExpressionContext $context,
2595: ): ExpressionResult
2596: {
2597: $this->storeBeforeScope($storage, $expr, $scope);
2598: if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) {
2599: if ($expr instanceof FuncCall) {
2600: $newExpr = new FunctionCallableNode($expr->name, $expr);
2601: } elseif ($expr instanceof MethodCall) {
2602: $newExpr = new MethodCallableNode($expr->var, $expr->name, $expr);
2603: } elseif ($expr instanceof StaticCall) {
2604: $newExpr = new StaticMethodCallableNode($expr->class, $expr->name, $expr);
2605: } elseif ($expr instanceof New_ && !$expr->class instanceof Class_) {
2606: $newExpr = new InstantiationCallableNode($expr->class, $expr);
2607: } else {
2608: throw new ShouldNotHappenException();
2609: }
2610:
2611: return $this->processExprNode($stmt, $newExpr, $scope, $storage, $nodeCallback, $context);
2612: }
2613:
2614: $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $storage, $context);
2615:
2616: if ($expr instanceof Variable) {
2617: $hasYield = false;
2618: $throwPoints = [];
2619: $impurePoints = [];
2620: $isAlwaysTerminating = false;
2621: if ($expr->name instanceof Expr) {
2622: $result = $this->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep());
2623: $hasYield = $result->hasYield();
2624: $throwPoints = $result->getThrowPoints();
2625: $impurePoints = $result->getImpurePoints();
2626: $isAlwaysTerminating = $result->isAlwaysTerminating();
2627: $scope = $result->getScope();
2628: } elseif (in_array($expr->name, Scope::SUPERGLOBAL_VARIABLES, true)) {
2629: $impurePoints[] = new ImpurePoint($scope, $expr, 'superglobal', 'access to superglobal variable', true);
2630: }
2631: } elseif ($expr instanceof Assign || $expr instanceof AssignRef) {
2632: $result = $this->processAssignVar(
2633: $scope,
2634: $storage,
2635: $stmt,
2636: $expr->var,
2637: $expr->expr,
2638: $nodeCallback,
2639: $context,
2640: function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $storage): ExpressionResult {
2641: $impurePoints = [];
2642: if ($expr instanceof AssignRef) {
2643: $referencedExpr = $expr->expr;
2644: while ($referencedExpr instanceof ArrayDimFetch) {
2645: $referencedExpr = $referencedExpr->var;
2646: }
2647:
2648: if ($referencedExpr instanceof PropertyFetch || $referencedExpr instanceof StaticPropertyFetch) {
2649: $impurePoints[] = new ImpurePoint(
2650: $scope,
2651: $expr,
2652: 'propertyAssignByRef',
2653: 'property assignment by reference',
2654: false,
2655: );
2656: }
2657:
2658: $scope = $scope->enterExpressionAssign($expr->expr);
2659: }
2660:
2661: if ($expr->var instanceof Variable && is_string($expr->var->name)) {
2662: $context = $context->enterRightSideAssign(
2663: $expr->var->name,
2664: $expr->expr,
2665: );
2666: }
2667:
2668: $this->storeBeforeScope($storage, $expr, $scope);
2669: $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
2670: $hasYield = $result->hasYield();
2671: $throwPoints = $result->getThrowPoints();
2672: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
2673: $isAlwaysTerminating = $result->isAlwaysTerminating();
2674: $scope = $result->getScope();
2675:
2676: if ($expr instanceof AssignRef) {
2677: $scope = $scope->exitExpressionAssign($expr->expr);
2678: }
2679:
2680: return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints);
2681: },
2682: true,
2683: );
2684: $scope = $result->getScope();
2685: $hasYield = $result->hasYield();
2686: $throwPoints = $result->getThrowPoints();
2687: $impurePoints = $result->getImpurePoints();
2688: $isAlwaysTerminating = $result->isAlwaysTerminating();
2689: $vars = $this->getAssignedVariables($expr->var);
2690: if (count($vars) > 0) {
2691: $varChangedScope = false;
2692: $scope = $this->processVarAnnotation($scope, $vars, $stmt, $varChangedScope);
2693: if (!$varChangedScope) {
2694: $scope = $this->processStmtVarAnnotation($scope, $storage, $stmt, null, $nodeCallback);
2695: }
2696: }
2697:
2698: return new ExpressionResult(
2699: $scope,
2700: $hasYield,
2701: $isAlwaysTerminating,
2702: $throwPoints,
2703: $impurePoints,
2704: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
2705: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
2706: );
2707: } elseif ($expr instanceof Expr\AssignOp) {
2708: $result = $this->processAssignVar(
2709: $scope,
2710: $storage,
2711: $stmt,
2712: $expr->var,
2713: $expr,
2714: $nodeCallback,
2715: $context,
2716: function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context, $storage): ExpressionResult {
2717: $originalScope = $scope;
2718: if ($expr instanceof Expr\AssignOp\Coalesce) {
2719: $scope = $scope->filterByFalseyValue(
2720: new BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))),
2721: );
2722: }
2723:
2724: $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
2725: if ($expr instanceof Expr\AssignOp\Coalesce) {
2726: $this->storeBeforeScope($storage, $expr, $originalScope);
2727: return new ExpressionResult(
2728: $result->getScope()->mergeWith($originalScope),
2729: $result->hasYield(),
2730: $result->isAlwaysTerminating(),
2731: $result->getThrowPoints(),
2732: $result->getImpurePoints(),
2733: );
2734: }
2735:
2736: return $result;
2737: },
2738: $expr instanceof Expr\AssignOp\Coalesce,
2739: );
2740: if (!$expr instanceof Expr\AssignOp\Coalesce) {
2741: $this->storeBeforeScope($storage, $expr, $scope);
2742: }
2743: $scope = $result->getScope();
2744: $hasYield = $result->hasYield();
2745: $throwPoints = $result->getThrowPoints();
2746: $impurePoints = $result->getImpurePoints();
2747: $isAlwaysTerminating = $result->isAlwaysTerminating();
2748: if (
2749: ($expr instanceof Expr\AssignOp\Div || $expr instanceof Expr\AssignOp\Mod) &&
2750: !$scope->getType($expr->expr)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no()
2751: ) {
2752: $throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false);
2753: }
2754:
2755: return new ExpressionResult(
2756: $scope,
2757: $hasYield,
2758: $isAlwaysTerminating,
2759: $throwPoints,
2760: $impurePoints,
2761: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
2762: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
2763: );
2764: } elseif ($expr instanceof FuncCall) {
2765: $parametersAcceptor = null;
2766: $functionReflection = null;
2767: $throwPoints = [];
2768: $impurePoints = [];
2769: $isAlwaysTerminating = false;
2770: if ($expr->name instanceof Expr) {
2771: $nameType = $scope->getType($expr->name);
2772: if (!$nameType->isCallable()->no()) {
2773: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
2774: $scope,
2775: $expr->getArgs(),
2776: $nameType->getCallableParametersAcceptors($scope),
2777: null,
2778: );
2779: }
2780:
2781: $nameResult = $this->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep());
2782: $scope = $nameResult->getScope();
2783: $throwPoints = $nameResult->getThrowPoints();
2784: $impurePoints = $nameResult->getImpurePoints();
2785: $isAlwaysTerminating = $nameResult->isAlwaysTerminating();
2786: if ( // phpcs:ignore
2787: $nameType->isObject()->yes()
2788: && $nameType->isCallable()->yes()
2789: && (new ObjectType(Closure::class))->isSuperTypeOf($nameType)->no()
2790: ) {
2791: // processed later
2792: } elseif ($parametersAcceptor instanceof CallableParametersAcceptor) {
2793: $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $expr, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $expr), $parametersAcceptor->getThrowPoints());
2794: if (!$this->implicitThrows) {
2795: $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit()));
2796: }
2797: $throwPoints = array_merge($throwPoints, $callableThrowPoints);
2798: $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $parametersAcceptor->getImpurePoints()));
2799:
2800: $scope = $this->processImmediatelyCalledCallable($scope, $parametersAcceptor->getInvalidateExpressions(), $parametersAcceptor->getUsedVariables());
2801: }
2802: } elseif ($this->reflectionProvider->hasFunction($expr->name, $scope)) {
2803: $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope);
2804: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
2805: $scope,
2806: $expr->getArgs(),
2807: $functionReflection->getVariants(),
2808: $functionReflection->getNamedArgumentsVariants(),
2809: );
2810: $impurePoint = SimpleImpurePoint::createFromVariant($functionReflection, $parametersAcceptor, $scope, $expr->getArgs());
2811: if ($impurePoint !== null) {
2812: $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain());
2813: }
2814: } else {
2815: $impurePoints[] = new ImpurePoint(
2816: $scope,
2817: $expr,
2818: 'functionCall',
2819: 'call to unknown function',
2820: false,
2821: );
2822: }
2823:
2824: $normalizedExpr = $expr;
2825: if ($parametersAcceptor !== null) {
2826: $normalizedExpr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr;
2827: $returnType = $parametersAcceptor->getReturnType();
2828: $isAlwaysTerminating = $isAlwaysTerminating || $returnType instanceof NeverType && $returnType->isExplicit();
2829: }
2830:
2831: if (
2832: $normalizedExpr->name instanceof Name
2833: && $functionReflection !== null
2834: && $functionReflection->getName() === 'clone'
2835: && count($normalizedExpr->getArgs()) === 2
2836: ) {
2837: $clonePropertiesArgType = $scope->getType($normalizedExpr->getArgs()[1]->value);
2838: $cloneExpr = new TypeExpr($scope->getType(new Expr\Clone_($normalizedExpr->getArgs()[0]->value)));
2839: $clonePropertiesArgTypeConstantArrays = $clonePropertiesArgType->getConstantArrays();
2840: foreach ($clonePropertiesArgTypeConstantArrays as $clonePropertiesArgTypeConstantArray) {
2841: foreach ($clonePropertiesArgTypeConstantArray->getKeyTypes() as $i => $clonePropertyKeyType) {
2842: $clonePropertyKeyTypeScalars = $clonePropertyKeyType->getConstantScalarValues();
2843: $propertyAttributes = $normalizedExpr->getAttributes();
2844: $propertyAttributes['inCloneWith'] = true;
2845: if (count($clonePropertyKeyTypeScalars) === 1) {
2846: $this->processVirtualAssign(
2847: $scope,
2848: $storage,
2849: $stmt,
2850: new PropertyFetch($cloneExpr, (string) $clonePropertyKeyTypeScalars[0], $propertyAttributes),
2851: new TypeExpr($clonePropertiesArgTypeConstantArray->getValueTypes()[$i]),
2852: $nodeCallback,
2853: );
2854: continue;
2855: }
2856:
2857: $this->processVirtualAssign(
2858: $scope,
2859: $storage,
2860: $stmt,
2861: new PropertyFetch($cloneExpr, new TypeExpr($clonePropertyKeyType), $propertyAttributes),
2862: new TypeExpr($clonePropertiesArgTypeConstantArray->getValueTypes()[$i]),
2863: $nodeCallback,
2864: );
2865: }
2866: }
2867: }
2868:
2869: $result = $this->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallback, $context);
2870: $scope = $result->getScope();
2871: $hasYield = $result->hasYield();
2872: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2873: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
2874: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
2875:
2876: if ($normalizedExpr->name instanceof Expr) {
2877: $nameType = $scope->getType($normalizedExpr->name);
2878: if (
2879: $nameType->isObject()->yes()
2880: && $nameType->isCallable()->yes()
2881: && (new ObjectType(Closure::class))->isSuperTypeOf($nameType)->no()
2882: ) {
2883: $invokeResult = $this->processExprNode(
2884: $stmt,
2885: new MethodCall($normalizedExpr->name, '__invoke', $normalizedExpr->getArgs(), $normalizedExpr->getAttributes()),
2886: $scope,
2887: $storage,
2888: new NoopNodeCallback(),
2889: $context->enterDeep(),
2890: );
2891: $throwPoints = array_merge($throwPoints, $invokeResult->getThrowPoints());
2892: $impurePoints = array_merge($impurePoints, $invokeResult->getImpurePoints());
2893: $isAlwaysTerminating = $invokeResult->isAlwaysTerminating();
2894: }
2895: }
2896:
2897: if ($functionReflection !== null) {
2898: $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $normalizedExpr, $scope);
2899: if ($functionThrowPoint !== null) {
2900: $throwPoints[] = $functionThrowPoint;
2901: }
2902: } else {
2903: $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
2904: }
2905:
2906: if (
2907: $parametersAcceptor instanceof ClosureType && count($parametersAcceptor->getImpurePoints()) > 0
2908: && $scope->isInClass()
2909: ) {
2910: $scope = $scope->invalidateExpression(new Variable('this'), true);
2911: }
2912:
2913: if (
2914: $functionReflection !== null
2915: && in_array($functionReflection->getName(), ['json_encode', 'json_decode'], true)
2916: ) {
2917: $scope = $scope->invalidateExpression(new FuncCall(new Name('json_last_error'), []))
2918: ->invalidateExpression(new FuncCall(new Name\FullyQualified('json_last_error'), []))
2919: ->invalidateExpression(new FuncCall(new Name('json_last_error_msg'), []))
2920: ->invalidateExpression(new FuncCall(new Name\FullyQualified('json_last_error_msg'), []));
2921: }
2922:
2923: if (
2924: $functionReflection !== null
2925: && $functionReflection->getName() === 'file_put_contents'
2926: && count($normalizedExpr->getArgs()) > 0
2927: ) {
2928: $scope = $scope->invalidateExpression(new FuncCall(new Name('file_get_contents'), [$normalizedExpr->getArgs()[0]]))
2929: ->invalidateExpression(new FuncCall(new Name\FullyQualified('file_get_contents'), [$normalizedExpr->getArgs()[0]]));
2930: }
2931:
2932: if (
2933: $functionReflection !== null
2934: && in_array($functionReflection->getName(), ['array_pop', 'array_shift'], true)
2935: && count($normalizedExpr->getArgs()) >= 1
2936: ) {
2937: $arrayArg = $normalizedExpr->getArgs()[0]->value;
2938:
2939: $arrayArgType = $scope->getType($arrayArg);
2940: $arrayArgNativeType = $scope->getNativeType($arrayArg);
2941: $isArrayPop = $functionReflection->getName() === 'array_pop';
2942:
2943: $scope = $this->processVirtualAssign(
2944: $scope,
2945: $storage,
2946: $stmt,
2947: $arrayArg,
2948: new NativeTypeExpr(
2949: $isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray(),
2950: $isArrayPop ? $arrayArgNativeType->popArray() : $arrayArgNativeType->shiftArray(),
2951: ),
2952: $nodeCallback,
2953: )->getScope();
2954: }
2955:
2956: if (
2957: $functionReflection !== null
2958: && in_array($functionReflection->getName(), ['array_push', 'array_unshift'], true)
2959: && count($normalizedExpr->getArgs()) >= 2
2960: ) {
2961: $arrayArg = $normalizedExpr->getArgs()[0]->value;
2962:
2963: $scope = $this->processVirtualAssign(
2964: $scope,
2965: $storage,
2966: $stmt,
2967: $arrayArg,
2968: new NativeTypeExpr(
2969: $this->getArrayFunctionAppendingType($functionReflection, $scope, $normalizedExpr),
2970: $this->getArrayFunctionAppendingType($functionReflection, $scope->doNotTreatPhpDocTypesAsCertain(), $normalizedExpr),
2971: ),
2972: $nodeCallback,
2973: )->getScope();
2974: }
2975:
2976: if (
2977: $functionReflection !== null
2978: && in_array($functionReflection->getName(), ['fopen', 'file_get_contents'], true)
2979: ) {
2980: $scope = $scope->assignVariable('http_response_header', new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()), new AccessoryArrayListType()]), new ArrayType(new IntegerType(), new StringType()), TrinaryLogic::createYes());
2981: }
2982:
2983: if (
2984: $functionReflection !== null
2985: && $functionReflection->getName() === 'shuffle'
2986: ) {
2987: $arrayArg = $normalizedExpr->getArgs()[0]->value;
2988:
2989: $scope = $this->processVirtualAssign(
2990: $scope,
2991: $storage,
2992: $stmt,
2993: $arrayArg,
2994: new NativeTypeExpr($scope->getType($arrayArg)->shuffleArray(), $scope->getNativeType($arrayArg)->shuffleArray()),
2995: $nodeCallback,
2996: )->getScope();
2997: }
2998:
2999: if (
3000: $functionReflection !== null
3001: && $functionReflection->getName() === 'array_splice'
3002: && count($normalizedExpr->getArgs()) >= 2
3003: ) {
3004: $arrayArg = $normalizedExpr->getArgs()[0]->value;
3005: $arrayArgType = $scope->getType($arrayArg);
3006: $arrayArgNativeType = $scope->getNativeType($arrayArg);
3007:
3008: $offsetType = $scope->getType($normalizedExpr->getArgs()[1]->value);
3009: $lengthType = isset($normalizedExpr->getArgs()[2]) ? $scope->getType($normalizedExpr->getArgs()[2]->value) : new NullType();
3010: $replacementType = isset($normalizedExpr->getArgs()[3]) ? $scope->getType($normalizedExpr->getArgs()[3]->value) : new ConstantArrayType([], []);
3011:
3012: $scope = $this->processVirtualAssign(
3013: $scope,
3014: $storage,
3015: $stmt,
3016: $arrayArg,
3017: new NativeTypeExpr(
3018: $arrayArgType->spliceArray($offsetType, $lengthType, $replacementType),
3019: $arrayArgNativeType->spliceArray($offsetType, $lengthType, $replacementType),
3020: ),
3021: $nodeCallback,
3022: )->getScope();
3023: }
3024:
3025: if (
3026: $functionReflection !== null
3027: && in_array($functionReflection->getName(), ['sort', 'rsort', 'usort'], true)
3028: && count($normalizedExpr->getArgs()) >= 1
3029: ) {
3030: $arrayArg = $normalizedExpr->getArgs()[0]->value;
3031:
3032: $scope = $this->processVirtualAssign(
3033: $scope,
3034: $storage,
3035: $stmt,
3036: $arrayArg,
3037: new NativeTypeExpr($this->getArraySortPreserveListFunctionType($scope->getType($arrayArg)), $this->getArraySortPreserveListFunctionType($scope->getNativeType($arrayArg))),
3038: $nodeCallback,
3039: )->getScope();
3040: }
3041:
3042: if (
3043: $functionReflection !== null
3044: && in_array($functionReflection->getName(), ['natcasesort', 'natsort', 'arsort', 'asort', 'ksort', 'krsort', 'uasort', 'uksort'], true)
3045: && count($normalizedExpr->getArgs()) >= 1
3046: ) {
3047: $arrayArg = $normalizedExpr->getArgs()[0]->value;
3048:
3049: $scope = $this->processVirtualAssign(
3050: $scope,
3051: $storage,
3052: $stmt,
3053: $arrayArg,
3054: new NativeTypeExpr($this->getArraySortDoNotPreserveListFunctionType($scope->getType($arrayArg)), $this->getArraySortDoNotPreserveListFunctionType($scope->getNativeType($arrayArg))),
3055: $nodeCallback,
3056: )->getScope();
3057: }
3058:
3059: if (
3060: $functionReflection !== null
3061: && $functionReflection->getName() === 'extract'
3062: ) {
3063: $extractedArg = $normalizedExpr->getArgs()[0]->value;
3064: $extractedType = $scope->getType($extractedArg);
3065: $constantArrays = $extractedType->getConstantArrays();
3066: if (count($constantArrays) > 0) {
3067: $properties = [];
3068: $optionalProperties = [];
3069: $refCount = [];
3070: foreach ($constantArrays as $constantArray) {
3071: foreach ($constantArray->getKeyTypes() as $i => $keyType) {
3072: if ($keyType->isString()->no()) {
3073: // integers as variable names not allowed
3074: continue;
3075: }
3076: $key = (string) $keyType->getValue();
3077: $valueType = $constantArray->getValueTypes()[$i];
3078: $optional = $constantArray->isOptionalKey($i);
3079: if ($optional) {
3080: $optionalProperties[] = $key;
3081: }
3082: if (isset($properties[$key])) {
3083: $properties[$key] = TypeCombinator::union($properties[$key], $valueType);
3084: $refCount[$key]++;
3085: } else {
3086: $properties[$key] = $valueType;
3087: $refCount[$key] = 1;
3088: }
3089: }
3090: }
3091: foreach ($properties as $name => $type) {
3092: $optional = in_array($name, $optionalProperties, true) || $refCount[$name] < count($constantArrays);
3093:
3094: if (!$optional) {
3095: $scope = $scope->assignVariable($name, $type, $type, TrinaryLogic::createYes());
3096: } else {
3097: $hasVariable = $scope->hasVariableType($name);
3098: if (!$hasVariable->no()) {
3099: $type = TypeCombinator::union($scope->getVariableType($name), $type);
3100: }
3101:
3102: $scope = $scope->assignVariable($name, $type, $type, $scope->hasVariableType($name)->or(TrinaryLogic::createMaybe()));
3103: }
3104: }
3105: } else {
3106: $scope = $scope->afterExtractCall();
3107: }
3108: }
3109:
3110: if (
3111: $functionReflection !== null
3112: && in_array($functionReflection->getName(), ['clearstatcache', 'unlink'], true)
3113: ) {
3114: $scope = $scope->afterClearstatcacheCall();
3115: }
3116:
3117: if (
3118: $functionReflection !== null
3119: && str_starts_with($functionReflection->getName(), 'openssl')
3120: ) {
3121: $scope = $scope->afterOpenSslCall($functionReflection->getName());
3122: }
3123:
3124: } elseif ($expr instanceof MethodCall) {
3125: $originalScope = $scope;
3126: if (
3127: ($expr->var instanceof Expr\Closure || $expr->var instanceof Expr\ArrowFunction)
3128: && $expr->name instanceof Node\Identifier
3129: && strtolower($expr->name->name) === 'call'
3130: && isset($expr->getArgs()[0])
3131: ) {
3132: $closureCallScope = $scope->enterClosureCall(
3133: $scope->getType($expr->getArgs()[0]->value),
3134: $scope->getNativeType($expr->getArgs()[0]->value),
3135: );
3136: }
3137:
3138: $result = $this->processExprNode($stmt, $expr->var, $closureCallScope ?? $scope, $storage, $nodeCallback, $context->enterDeep());
3139: $hasYield = $result->hasYield();
3140: $throwPoints = $result->getThrowPoints();
3141: $impurePoints = $result->getImpurePoints();
3142: $isAlwaysTerminating = $result->isAlwaysTerminating();
3143: $scope = $result->getScope();
3144: if (isset($closureCallScope)) {
3145: $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope);
3146: }
3147: $parametersAcceptor = null;
3148: $methodReflection = null;
3149: $calledOnType = $scope->getType($expr->var);
3150: if ($expr->name instanceof Expr) {
3151: $methodNameResult = $this->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep());
3152: $throwPoints = array_merge($throwPoints, $methodNameResult->getThrowPoints());
3153: $scope = $methodNameResult->getScope();
3154: } else {
3155: $methodName = $expr->name->name;
3156: $methodReflection = $scope->getMethodReflection($calledOnType, $methodName);
3157: if ($methodReflection !== null) {
3158: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
3159: $scope,
3160: $expr->getArgs(),
3161: $methodReflection->getVariants(),
3162: $methodReflection->getNamedArgumentsVariants(),
3163: );
3164:
3165: $methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope);
3166: if ($methodThrowPoint !== null) {
3167: $throwPoints[] = $methodThrowPoint;
3168: }
3169: }
3170: }
3171:
3172: if ($methodReflection !== null) {
3173: $impurePoint = SimpleImpurePoint::createFromVariant($methodReflection, $parametersAcceptor, $scope, $expr->getArgs());
3174: if ($impurePoint !== null) {
3175: $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain());
3176: }
3177: } else {
3178: $impurePoints[] = new ImpurePoint(
3179: $scope,
3180: $expr,
3181: 'methodCall',
3182: 'call to unknown method',
3183: false,
3184: );
3185: }
3186:
3187: $normalizedExpr = $expr;
3188: if ($parametersAcceptor !== null) {
3189: $normalizedExpr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr;
3190: $returnType = $parametersAcceptor->getReturnType();
3191: $isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit();
3192: }
3193:
3194: $result = $this->processArgs(
3195: $stmt,
3196: $methodReflection,
3197: $methodReflection !== null ? $scope->getNakedMethod($calledOnType, $methodReflection->getName()) : null,
3198: $parametersAcceptor,
3199: $normalizedExpr,
3200: $scope,
3201: $storage,
3202: $nodeCallback,
3203: $context,
3204: );
3205: $scope = $result->getScope();
3206:
3207: if ($methodReflection !== null) {
3208: if ($methodReflection->getName() === '__construct' || $methodReflection->hasSideEffects()->yes()) {
3209: $this->callNodeCallback($nodeCallback, new InvalidateExprNode($normalizedExpr->var), $scope, $storage);
3210: $scope = $scope->invalidateExpression($normalizedExpr->var, true, $methodReflection->getDeclaringClass());
3211: }
3212: if ($parametersAcceptor !== null && !$methodReflection->isStatic()) {
3213: $selfOutType = $methodReflection->getSelfOutType();
3214: if ($selfOutType !== null) {
3215: $scope = $scope->assignExpression(
3216: $normalizedExpr->var,
3217: TemplateTypeHelper::resolveTemplateTypes(
3218: $selfOutType,
3219: $parametersAcceptor->getResolvedTemplateTypeMap(),
3220: $parametersAcceptor instanceof ExtendedParametersAcceptor ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
3221: TemplateTypeVariance::createCovariant(),
3222: ),
3223: $scope->getNativeType($normalizedExpr->var),
3224: );
3225: }
3226: }
3227:
3228: if (
3229: $scope->isInClass()
3230: && $scope->getClassReflection()->getName() === $methodReflection->getDeclaringClass()->getName()
3231: && ($scope->getFunctionName() !== null && strtolower($scope->getFunctionName()) === '__construct')
3232: /*&& (
3233: // should not be allowed but in practice has to be
3234: $scope->getClassReflection()->isFinal()
3235: || $methodReflection->isFinal()->yes()
3236: || $methodReflection->isPrivate()
3237: )*/
3238: && TypeUtils::findThisType($calledOnType) !== null
3239: ) {
3240: $calledMethodScope = $this->processCalledMethod($methodReflection);
3241: if ($calledMethodScope !== null) {
3242: $scope = $scope->mergeInitializedProperties($calledMethodScope);
3243: }
3244: }
3245: } else {
3246: $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
3247: }
3248: $hasYield = $hasYield || $result->hasYield();
3249: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3250: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3251: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
3252: } elseif ($expr instanceof Expr\NullsafeMethodCall) {
3253: $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var);
3254: $attributes = array_merge($expr->getAttributes(), ['virtualNullsafeMethodCall' => true]);
3255: unset($attributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]);
3256: $exprResult = $this->processExprNode(
3257: $stmt,
3258: new MethodCall(
3259: $expr->var,
3260: $expr->name,
3261: $expr->args,
3262: $attributes,
3263: ),
3264: $nonNullabilityResult->getScope(),
3265: $storage,
3266: $nodeCallback,
3267: $context,
3268: );
3269: $scope = $this->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions());
3270:
3271: return new ExpressionResult(
3272: $scope,
3273: $exprResult->hasYield(),
3274: false,
3275: $exprResult->getThrowPoints(),
3276: $exprResult->getImpurePoints(),
3277: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
3278: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
3279: );
3280: } elseif ($expr instanceof StaticCall) {
3281: $hasYield = false;
3282: $throwPoints = [];
3283: $impurePoints = [];
3284: $isAlwaysTerminating = false;
3285: if ($expr->class instanceof Expr) {
3286: $classResult = $this->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep());
3287: $hasYield = $classResult->hasYield();
3288: $throwPoints = array_merge($throwPoints, $classResult->getThrowPoints());
3289: $impurePoints = array_merge($impurePoints, $classResult->getImpurePoints());
3290: $isAlwaysTerminating = $classResult->isAlwaysTerminating();
3291:
3292: $scope = $classResult->getScope();
3293: }
3294:
3295: $parametersAcceptor = null;
3296: $methodReflection = null;
3297: if ($expr->name instanceof Expr) {
3298: $result = $this->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep());
3299: $hasYield = $hasYield || $result->hasYield();
3300: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3301: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3302: $scope = $result->getScope();
3303: } elseif ($expr->class instanceof Name) {
3304: $classType = $scope->resolveTypeByName($expr->class);
3305: $methodName = $expr->name->name;
3306: if ($classType->hasMethod($methodName)->yes()) {
3307: $methodReflection = $classType->getMethod($methodName, $scope);
3308: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
3309: $scope,
3310: $expr->getArgs(),
3311: $methodReflection->getVariants(),
3312: $methodReflection->getNamedArgumentsVariants(),
3313: );
3314:
3315: $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope);
3316: if ($methodThrowPoint !== null) {
3317: $throwPoints[] = $methodThrowPoint;
3318: }
3319:
3320: $declaringClass = $methodReflection->getDeclaringClass();
3321: if (
3322: $declaringClass->getName() === 'Closure'
3323: && strtolower($methodName) === 'bind'
3324: ) {
3325: $thisType = null;
3326: $nativeThisType = null;
3327: if (isset($expr->getArgs()[1])) {
3328: $argType = $scope->getType($expr->getArgs()[1]->value);
3329: if ($argType->isNull()->yes()) {
3330: $thisType = null;
3331: } else {
3332: $thisType = $argType;
3333: }
3334:
3335: $nativeArgType = $scope->getNativeType($expr->getArgs()[1]->value);
3336: if ($nativeArgType->isNull()->yes()) {
3337: $nativeThisType = null;
3338: } else {
3339: $nativeThisType = $nativeArgType;
3340: }
3341: }
3342: $scopeClasses = ['static'];
3343: if (isset($expr->getArgs()[2])) {
3344: $argValue = $expr->getArgs()[2]->value;
3345: $argValueType = $scope->getType($argValue);
3346:
3347: $directClassNames = $argValueType->getObjectClassNames();
3348: if (count($directClassNames) > 0) {
3349: $scopeClasses = $directClassNames;
3350: $thisTypes = [];
3351: foreach ($directClassNames as $directClassName) {
3352: $thisTypes[] = new ObjectType($directClassName);
3353: }
3354: $thisType = TypeCombinator::union(...$thisTypes);
3355: } else {
3356: $thisType = $argValueType->getClassStringObjectType();
3357: $scopeClasses = $thisType->getObjectClassNames();
3358: }
3359: }
3360: $closureBindScope = $scope->enterClosureBind($thisType, $nativeThisType, $scopeClasses);
3361: }
3362: } else {
3363: $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
3364: }
3365: }
3366:
3367: if ($expr->class instanceof Expr) {
3368: $objectClasses = $scope->getType($expr->class)->getObjectClassNames();
3369: if (count($objectClasses) !== 1) {
3370: $objectClasses = $scope->getType(new New_($expr->class))->getObjectClassNames();
3371: }
3372: if (count($objectClasses) === 1) {
3373: $objectExprResult = $this->processExprNode($stmt, new StaticCall(new Name($objectClasses[0]), $expr->name, []), $scope, $storage, new NoopNodeCallback(), $context->enterDeep());
3374: $additionalThrowPoints = $objectExprResult->getThrowPoints();
3375: } else {
3376: $additionalThrowPoints = [InternalThrowPoint::createImplicit($scope, $expr)];
3377: }
3378: foreach ($additionalThrowPoints as $throwPoint) {
3379: $throwPoints[] = $throwPoint;
3380: }
3381: }
3382:
3383: if ($methodReflection !== null) {
3384: $impurePoint = SimpleImpurePoint::createFromVariant($methodReflection, $parametersAcceptor, $scope, $expr->getArgs());
3385: if ($impurePoint !== null) {
3386: $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain());
3387: }
3388: } else {
3389: $impurePoints[] = new ImpurePoint(
3390: $scope,
3391: $expr,
3392: 'methodCall',
3393: 'call to unknown method',
3394: false,
3395: );
3396: }
3397:
3398: $normalizedExpr = $expr;
3399: if ($parametersAcceptor !== null) {
3400: $normalizedExpr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr;
3401: $returnType = $parametersAcceptor->getReturnType();
3402: $isAlwaysTerminating = $returnType instanceof NeverType && $returnType->isExplicit();
3403: }
3404: $result = $this->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallback, $context, $closureBindScope ?? null);
3405: $scope = $result->getScope();
3406: $scopeFunction = $scope->getFunction();
3407:
3408: if (
3409: $methodReflection !== null
3410: && (
3411: (
3412: !$methodReflection->isStatic()
3413: && $methodReflection->getName() === '__construct'
3414: )
3415: || $methodReflection->hasSideEffects()->yes()
3416: )
3417: && $scope->isInClass()
3418: && $scope->getClassReflection()->is($methodReflection->getDeclaringClass()->getName())
3419: ) {
3420: $scope = $scope->invalidateExpression(new Variable('this'), true, $methodReflection->getDeclaringClass());
3421: }
3422:
3423: if (
3424: $methodReflection !== null
3425: && !$methodReflection->isStatic()
3426: && $methodReflection->getName() === '__construct'
3427: && $scopeFunction instanceof MethodReflection
3428: && !$scopeFunction->isStatic()
3429: && $scope->isInClass()
3430: && $scope->getClassReflection()->isSubclassOfClass($methodReflection->getDeclaringClass())
3431: ) {
3432: $thisType = $scope->getType(new Variable('this'));
3433: $methodClassReflection = $methodReflection->getDeclaringClass();
3434: foreach ($methodClassReflection->getNativeReflection()->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED) as $property) {
3435: if (!$property->isPromoted() || $property->getDeclaringClass()->getName() !== $methodClassReflection->getName()) {
3436: continue;
3437: }
3438:
3439: $scope = $scope->assignInitializedProperty($thisType, $property->getName());
3440: }
3441: }
3442:
3443: $hasYield = $hasYield || $result->hasYield();
3444: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3445: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3446: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
3447: } elseif ($expr instanceof PropertyFetch) {
3448: $scopeBeforeVar = $scope;
3449: $result = $this->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep());
3450: $hasYield = $result->hasYield();
3451: $throwPoints = $result->getThrowPoints();
3452: $impurePoints = $result->getImpurePoints();
3453: $isAlwaysTerminating = $result->isAlwaysTerminating();
3454: $scope = $result->getScope();
3455: if ($expr->name instanceof Expr) {
3456: $result = $this->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep());
3457: $hasYield = $hasYield || $result->hasYield();
3458: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3459: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3460: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
3461: $scope = $result->getScope();
3462: if ($this->phpVersion->supportsPropertyHooks()) {
3463: $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
3464: }
3465: } else {
3466: $propertyName = $expr->name->toString();
3467: $propertyHolderType = $scopeBeforeVar->getType($expr->var);
3468: $propertyReflection = $scopeBeforeVar->getInstancePropertyReflection($propertyHolderType, $propertyName);
3469: if ($propertyReflection !== null && $this->phpVersion->supportsPropertyHooks()) {
3470: $propertyDeclaringClass = $propertyReflection->getDeclaringClass();
3471: if ($propertyDeclaringClass->hasNativeProperty($propertyName)) {
3472: $nativeProperty = $propertyDeclaringClass->getNativeProperty($propertyName);
3473: $throwPoints = array_merge($throwPoints, $this->getPropertyReadThrowPointsFromGetHook($scopeBeforeVar, $expr, $nativeProperty));
3474: }
3475: }
3476: }
3477: } elseif ($expr instanceof Expr\NullsafePropertyFetch) {
3478: $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var);
3479: $attributes = array_merge($expr->getAttributes(), ['virtualNullsafePropertyFetch' => true]);
3480: unset($attributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]);
3481: $exprResult = $this->processExprNode($stmt, new PropertyFetch(
3482: $expr->var,
3483: $expr->name,
3484: $attributes,
3485: ), $nonNullabilityResult->getScope(), $storage, $nodeCallback, $context);
3486: $scope = $this->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions());
3487:
3488: return new ExpressionResult(
3489: $scope,
3490: $exprResult->hasYield(),
3491: false,
3492: $exprResult->getThrowPoints(),
3493: $exprResult->getImpurePoints(),
3494: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
3495: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
3496: );
3497: } elseif ($expr instanceof StaticPropertyFetch) {
3498: $hasYield = false;
3499: $throwPoints = [];
3500: $impurePoints = [
3501: new ImpurePoint(
3502: $scope,
3503: $expr,
3504: 'staticPropertyAccess',
3505: 'static property access',
3506: true,
3507: ),
3508: ];
3509: $isAlwaysTerminating = false;
3510: if ($expr->class instanceof Expr) {
3511: $result = $this->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep());
3512: $hasYield = $result->hasYield();
3513: $throwPoints = $result->getThrowPoints();
3514: $impurePoints = $result->getImpurePoints();
3515: $isAlwaysTerminating = $result->isAlwaysTerminating();
3516: $scope = $result->getScope();
3517: }
3518: if ($expr->name instanceof Expr) {
3519: $result = $this->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep());
3520: $hasYield = $hasYield || $result->hasYield();
3521: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3522: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3523: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
3524: $scope = $result->getScope();
3525: }
3526: } elseif ($expr instanceof Expr\Closure) {
3527: $processClosureResult = $this->processClosureNode($stmt, $expr, $scope, $storage, $nodeCallback, $context, null);
3528: $scope = $processClosureResult->applyByRefUseScope($processClosureResult->getScope());
3529:
3530: return new ExpressionResult(
3531: $scope,
3532: false,
3533: false,
3534: [],
3535: [],
3536: );
3537: } elseif ($expr instanceof Expr\ArrowFunction) {
3538: $result = $this->processArrowFunctionNode($stmt, $expr, $scope, $storage, $nodeCallback, null);
3539: return new ExpressionResult(
3540: $result->getScope(),
3541: $result->hasYield(),
3542: false,
3543: [],
3544: [],
3545: );
3546: } elseif ($expr instanceof ErrorSuppress) {
3547: $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context);
3548: $hasYield = $result->hasYield();
3549: $throwPoints = $result->getThrowPoints();
3550: $impurePoints = $result->getImpurePoints();
3551: $isAlwaysTerminating = $result->isAlwaysTerminating();
3552: $scope = $result->getScope();
3553: } elseif ($expr instanceof Exit_) {
3554: $hasYield = false;
3555: $throwPoints = [];
3556: $kind = $expr->getAttribute('kind', Exit_::KIND_EXIT);
3557: $identifier = $kind === Exit_::KIND_DIE ? 'die' : 'exit';
3558: $impurePoints = [
3559: new ImpurePoint($scope, $expr, $identifier, $identifier, true),
3560: ];
3561: $isAlwaysTerminating = true;
3562: if ($expr->expr !== null) {
3563: $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
3564: $hasYield = $result->hasYield();
3565: $throwPoints = $result->getThrowPoints();
3566: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3567: $scope = $result->getScope();
3568: }
3569: } elseif ($expr instanceof Node\Scalar\InterpolatedString) {
3570: $hasYield = false;
3571: $throwPoints = [];
3572: $impurePoints = [];
3573: $isAlwaysTerminating = false;
3574: foreach ($expr->parts as $part) {
3575: if (!$part instanceof Expr) {
3576: continue;
3577: }
3578: $result = $this->processExprNode($stmt, $part, $scope, $storage, $nodeCallback, $context->enterDeep());
3579: $hasYield = $hasYield || $result->hasYield();
3580: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3581: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3582: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
3583: $scope = $result->getScope();
3584: }
3585: } elseif ($expr instanceof ArrayDimFetch) {
3586: $hasYield = false;
3587: $throwPoints = [];
3588: $impurePoints = [];
3589: $isAlwaysTerminating = false;
3590: if ($expr->dim !== null) {
3591: $result = $this->processExprNode($stmt, $expr->dim, $scope, $storage, $nodeCallback, $context->enterDeep());
3592: $hasYield = $result->hasYield();
3593: $throwPoints = $result->getThrowPoints();
3594: $impurePoints = $result->getImpurePoints();
3595: $isAlwaysTerminating = $result->isAlwaysTerminating();
3596: $scope = $result->getScope();
3597: }
3598:
3599: $result = $this->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep());
3600: $hasYield = $hasYield || $result->hasYield();
3601: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3602: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3603: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
3604: $scope = $result->getScope();
3605: } elseif ($expr instanceof Array_) {
3606: $itemNodes = [];
3607: $hasYield = false;
3608: $throwPoints = [];
3609: $impurePoints = [];
3610: $isAlwaysTerminating = false;
3611: foreach ($expr->items as $arrayItem) {
3612: $itemNodes[] = new LiteralArrayItem($scope, $arrayItem);
3613: $this->callNodeCallback($nodeCallback, $arrayItem, $scope, $storage);
3614: if ($arrayItem->key !== null) {
3615: $keyResult = $this->processExprNode($stmt, $arrayItem->key, $scope, $storage, $nodeCallback, $context->enterDeep());
3616: $hasYield = $hasYield || $keyResult->hasYield();
3617: $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints());
3618: $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints());
3619: $isAlwaysTerminating = $isAlwaysTerminating || $keyResult->isAlwaysTerminating();
3620: $scope = $keyResult->getScope();
3621: }
3622:
3623: $valueResult = $this->processExprNode($stmt, $arrayItem->value, $scope, $storage, $nodeCallback, $context->enterDeep());
3624: $hasYield = $hasYield || $valueResult->hasYield();
3625: $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints());
3626: $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints());
3627: $isAlwaysTerminating = $isAlwaysTerminating || $valueResult->isAlwaysTerminating();
3628: $scope = $valueResult->getScope();
3629: }
3630: $this->callNodeCallback($nodeCallback, new LiteralArrayNode($expr, $itemNodes), $scope, $storage);
3631: } elseif ($expr instanceof BooleanAnd || $expr instanceof BinaryOp\LogicalAnd) {
3632: $leftResult = $this->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep());
3633: $rightResult = $this->processExprNode($stmt, $expr->right, $leftResult->getTruthyScope(), $storage, $nodeCallback, $context);
3634: $rightExprType = $rightResult->getScope()->getType($expr->right);
3635: if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) {
3636: $leftMergedWithRightScope = $leftResult->getFalseyScope();
3637: } else {
3638: $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope());
3639: }
3640:
3641: $this->callNodeCallbackWithExpression($nodeCallback, new BooleanAndNode($expr, $leftResult->getTruthyScope()), $scope, $storage, $context);
3642:
3643: $result = new ExpressionResult(
3644: $leftMergedWithRightScope,
3645: $leftResult->hasYield() || $rightResult->hasYield(),
3646: $leftResult->isAlwaysTerminating(),
3647: array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()),
3648: array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()),
3649: static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr),
3650: static fn (): MutatingScope => $leftMergedWithRightScope->filterByFalseyValue($expr),
3651: );
3652: return $result;
3653: } elseif ($expr instanceof BooleanOr || $expr instanceof BinaryOp\LogicalOr) {
3654: $leftResult = $this->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep());
3655: $rightResult = $this->processExprNode($stmt, $expr->right, $leftResult->getFalseyScope(), $storage, $nodeCallback, $context);
3656: $rightExprType = $rightResult->getScope()->getType($expr->right);
3657: if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) {
3658: $leftMergedWithRightScope = $leftResult->getTruthyScope();
3659: } else {
3660: $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope());
3661: }
3662:
3663: $this->callNodeCallbackWithExpression($nodeCallback, new BooleanOrNode($expr, $leftResult->getFalseyScope()), $scope, $storage, $context);
3664:
3665: return new ExpressionResult(
3666: $leftMergedWithRightScope,
3667: $leftResult->hasYield() || $rightResult->hasYield(),
3668: $leftResult->isAlwaysTerminating(),
3669: array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()),
3670: array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()),
3671: static fn (): MutatingScope => $leftMergedWithRightScope->filterByTruthyValue($expr),
3672: static fn (): MutatingScope => $rightResult->getScope()->filterByFalseyValue($expr),
3673: );
3674: } elseif ($expr instanceof Coalesce) {
3675: $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->left);
3676: $condScope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->left);
3677: $condResult = $this->processExprNode($stmt, $expr->left, $condScope, $storage, $nodeCallback, $context->enterDeep());
3678: $scope = $this->revertNonNullability($condResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions());
3679: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->left);
3680:
3681: $rightScope = $scope->filterByFalseyValue($expr);
3682: $rightResult = $this->processExprNode($stmt, $expr->right, $rightScope, $storage, $nodeCallback, $context->enterDeep());
3683: $rightExprType = $scope->getType($expr->right);
3684: if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) {
3685: $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left]));
3686: } else {
3687: $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left]))->mergeWith($rightResult->getScope());
3688: }
3689:
3690: $hasYield = $condResult->hasYield() || $rightResult->hasYield();
3691: $throwPoints = array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints());
3692: $impurePoints = array_merge($condResult->getImpurePoints(), $rightResult->getImpurePoints());
3693: $isAlwaysTerminating = $condResult->isAlwaysTerminating();
3694: } elseif ($expr instanceof BinaryOp\Pipe) {
3695: $rightAttributes = array_merge($expr->right->getAttributes(), ['virtualPipeOperatorCall' => true]);
3696: unset($rightAttributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]);
3697: if ($expr->right instanceof FuncCall && $expr->right->isFirstClassCallable()) {
3698: $exprResult = $this->processExprNode($stmt, new FuncCall($expr->right->name, [
3699: new Arg($expr->left, attributes: $expr->getAttribute(ReversePipeTransformerVisitor::ARG_ATTRIBUTES_NAME, [])),
3700: ], $rightAttributes), $scope, $storage, $nodeCallback, $context);
3701: $scope = $exprResult->getScope();
3702: $hasYield = $exprResult->hasYield();
3703: $throwPoints = $exprResult->getThrowPoints();
3704: $impurePoints = $exprResult->getImpurePoints();
3705: $isAlwaysTerminating = $exprResult->isAlwaysTerminating();
3706: } elseif ($expr->right instanceof MethodCall && $expr->right->isFirstClassCallable()) {
3707: $exprResult = $this->processExprNode($stmt, new MethodCall($expr->right->var, $expr->right->name, [
3708: new Arg($expr->left, attributes: $expr->getAttribute(ReversePipeTransformerVisitor::ARG_ATTRIBUTES_NAME, [])),
3709: ], $rightAttributes), $scope, $storage, $nodeCallback, $context);
3710: $scope = $exprResult->getScope();
3711: $hasYield = $exprResult->hasYield();
3712: $throwPoints = $exprResult->getThrowPoints();
3713: $impurePoints = $exprResult->getImpurePoints();
3714: $isAlwaysTerminating = $exprResult->isAlwaysTerminating();
3715: } elseif ($expr->right instanceof StaticCall && $expr->right->isFirstClassCallable()) {
3716: $exprResult = $this->processExprNode($stmt, new StaticCall($expr->right->class, $expr->right->name, [
3717: new Arg($expr->left, attributes: $expr->getAttribute(ReversePipeTransformerVisitor::ARG_ATTRIBUTES_NAME, [])),
3718: ], $rightAttributes), $scope, $storage, $nodeCallback, $context);
3719: $scope = $exprResult->getScope();
3720: $hasYield = $exprResult->hasYield();
3721: $throwPoints = $exprResult->getThrowPoints();
3722: $impurePoints = $exprResult->getImpurePoints();
3723: $isAlwaysTerminating = $exprResult->isAlwaysTerminating();
3724: } else {
3725: $exprResult = $this->processExprNode($stmt, new FuncCall($expr->right, [
3726: new Arg($expr->left, attributes: $expr->getAttribute(ReversePipeTransformerVisitor::ARG_ATTRIBUTES_NAME, [])),
3727: ], $rightAttributes), $scope, $storage, $nodeCallback, $context);
3728: $scope = $exprResult->getScope();
3729: $hasYield = $exprResult->hasYield();
3730: $throwPoints = $exprResult->getThrowPoints();
3731: $impurePoints = $exprResult->getImpurePoints();
3732: $isAlwaysTerminating = $exprResult->isAlwaysTerminating();
3733: }
3734: } elseif ($expr instanceof BinaryOp) {
3735: $result = $this->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep());
3736: $scope = $result->getScope();
3737: $hasYield = $result->hasYield();
3738: $throwPoints = $result->getThrowPoints();
3739: $impurePoints = $result->getImpurePoints();
3740: $isAlwaysTerminating = $result->isAlwaysTerminating();
3741: $result = $this->processExprNode($stmt, $expr->right, $scope, $storage, $nodeCallback, $context->enterDeep());
3742: if (
3743: ($expr instanceof BinaryOp\Div || $expr instanceof BinaryOp\Mod) &&
3744: !$scope->getType($expr->right)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no()
3745: ) {
3746: $throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false);
3747: }
3748: $scope = $result->getScope();
3749: $hasYield = $hasYield || $result->hasYield();
3750: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3751: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3752: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
3753: } elseif ($expr instanceof Expr\Include_) {
3754: $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
3755: $throwPoints = $result->getThrowPoints();
3756: $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
3757: $impurePoints = $result->getImpurePoints();
3758: $impurePoints[] = new ImpurePoint(
3759: $scope,
3760: $expr,
3761: in_array($expr->type, [Expr\Include_::TYPE_INCLUDE, Expr\Include_::TYPE_INCLUDE_ONCE], true) ? 'include' : 'require',
3762: in_array($expr->type, [Expr\Include_::TYPE_INCLUDE, Expr\Include_::TYPE_INCLUDE_ONCE], true) ? 'include' : 'require',
3763: true,
3764: );
3765: $hasYield = $result->hasYield();
3766: $isAlwaysTerminating = $result->isAlwaysTerminating();
3767: $scope = $result->getScope()->afterExtractCall();
3768: } elseif ($expr instanceof Expr\Print_) {
3769: $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
3770: $throwPoints = $result->getThrowPoints();
3771: $impurePoints = $result->getImpurePoints();
3772: $isAlwaysTerminating = $result->isAlwaysTerminating();
3773: $impurePoints[] = new ImpurePoint($scope, $expr, 'print', 'print', true);
3774: $hasYield = $result->hasYield();
3775:
3776: $scope = $result->getScope();
3777: } elseif ($expr instanceof Cast\String_) {
3778: $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
3779: $throwPoints = $result->getThrowPoints();
3780: $impurePoints = $result->getImpurePoints();
3781: $isAlwaysTerminating = $result->isAlwaysTerminating();
3782: $hasYield = $result->hasYield();
3783:
3784: $exprType = $scope->getType($expr->expr);
3785: $toStringMethod = $scope->getMethodReflection($exprType, '__toString');
3786: if ($toStringMethod !== null) {
3787: if (!$toStringMethod->hasSideEffects()->no()) {
3788: $impurePoints[] = new ImpurePoint(
3789: $scope,
3790: $expr,
3791: 'methodCall',
3792: sprintf('call to method %s::%s()', $toStringMethod->getDeclaringClass()->getDisplayName(), $toStringMethod->getName()),
3793: $toStringMethod->isPure()->no(),
3794: );
3795: }
3796: }
3797:
3798: $scope = $result->getScope();
3799: } elseif (
3800: $expr instanceof Expr\BitwiseNot
3801: || $expr instanceof Cast
3802: || $expr instanceof Expr\Clone_
3803: || $expr instanceof Expr\UnaryMinus
3804: || $expr instanceof Expr\UnaryPlus
3805: ) {
3806: $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
3807: $throwPoints = $result->getThrowPoints();
3808: $impurePoints = $result->getImpurePoints();
3809: $isAlwaysTerminating = $result->isAlwaysTerminating();
3810: $hasYield = $result->hasYield();
3811:
3812: $scope = $result->getScope();
3813: } elseif ($expr instanceof Expr\Eval_) {
3814: $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
3815: $throwPoints = $result->getThrowPoints();
3816: $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
3817: $impurePoints = $result->getImpurePoints();
3818: $impurePoints[] = new ImpurePoint($scope, $expr, 'eval', 'eval', true);
3819: $hasYield = $result->hasYield();
3820: $isAlwaysTerminating = $result->isAlwaysTerminating();
3821:
3822: $scope = $result->getScope();
3823: } elseif ($expr instanceof Expr\YieldFrom) {
3824: $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
3825: $throwPoints = $result->getThrowPoints();
3826: $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
3827: $impurePoints = $result->getImpurePoints();
3828: $impurePoints[] = new ImpurePoint(
3829: $scope,
3830: $expr,
3831: 'yieldFrom',
3832: 'yield from',
3833: true,
3834: );
3835: $hasYield = true;
3836: $isAlwaysTerminating = $result->isAlwaysTerminating();
3837:
3838: $scope = $result->getScope();
3839: } elseif ($expr instanceof BooleanNot) {
3840: $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
3841: $scope = $result->getScope();
3842: $hasYield = $result->hasYield();
3843: $throwPoints = $result->getThrowPoints();
3844: $impurePoints = $result->getImpurePoints();
3845: $isAlwaysTerminating = $result->isAlwaysTerminating();
3846: } elseif ($expr instanceof Expr\ClassConstFetch) {
3847: $isAlwaysTerminating = false;
3848:
3849: if ($expr->class instanceof Expr) {
3850: $result = $this->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep());
3851: $scope = $result->getScope();
3852: $hasYield = $result->hasYield();
3853: $throwPoints = $result->getThrowPoints();
3854: $impurePoints = $result->getImpurePoints();
3855: $isAlwaysTerminating = $result->isAlwaysTerminating();
3856: } else {
3857: $hasYield = false;
3858: $throwPoints = [];
3859: $impurePoints = [];
3860: $this->callNodeCallback($nodeCallback, $expr->class, $scope, $storage);
3861: }
3862:
3863: if ($expr->name instanceof Expr) {
3864: $result = $this->processExprNode($stmt, $expr->name, $scope, $storage, $nodeCallback, $context->enterDeep());
3865: $scope = $result->getScope();
3866: $hasYield = $hasYield || $result->hasYield();
3867: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3868: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3869: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
3870: } else {
3871: $this->callNodeCallback($nodeCallback, $expr->name, $scope, $storage);
3872: }
3873: } elseif ($expr instanceof Expr\Empty_) {
3874: $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->expr);
3875: $scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->expr);
3876: $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
3877: $scope = $result->getScope();
3878: $hasYield = $result->hasYield();
3879: $throwPoints = $result->getThrowPoints();
3880: $impurePoints = $result->getImpurePoints();
3881: $isAlwaysTerminating = $result->isAlwaysTerminating();
3882: $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions());
3883: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->expr);
3884: } elseif ($expr instanceof Expr\Isset_) {
3885: $hasYield = false;
3886: $throwPoints = [];
3887: $impurePoints = [];
3888: $nonNullabilityResults = [];
3889: $isAlwaysTerminating = false;
3890: foreach ($expr->vars as $var) {
3891: $nonNullabilityResult = $this->ensureNonNullability($scope, $var);
3892: $scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $var);
3893: $result = $this->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, $context->enterDeep());
3894: $scope = $result->getScope();
3895: $hasYield = $hasYield || $result->hasYield();
3896: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3897: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3898: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
3899: $nonNullabilityResults[] = $nonNullabilityResult;
3900: }
3901: foreach (array_reverse($expr->vars) as $var) {
3902: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
3903: }
3904: foreach (array_reverse($nonNullabilityResults) as $nonNullabilityResult) {
3905: $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions());
3906: }
3907: } elseif ($expr instanceof Instanceof_) {
3908: $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
3909: $scope = $result->getScope();
3910: $hasYield = $result->hasYield();
3911: $throwPoints = $result->getThrowPoints();
3912: $impurePoints = $result->getImpurePoints();
3913: $isAlwaysTerminating = $result->isAlwaysTerminating();
3914: if ($expr->class instanceof Expr) {
3915: $result = $this->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep());
3916: $scope = $result->getScope();
3917: $hasYield = $hasYield || $result->hasYield();
3918: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3919: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3920: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
3921: }
3922: } elseif ($expr instanceof List_) {
3923: // only in assign and foreach, processed elsewhere
3924: return new ExpressionResult($scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []);
3925: } elseif ($expr instanceof New_) {
3926: $parametersAcceptor = null;
3927: $constructorReflection = null;
3928: $hasYield = false;
3929: $throwPoints = [];
3930: $impurePoints = [];
3931: $isAlwaysTerminating = false;
3932: $className = null;
3933: $normalizedExpr = $expr;
3934: if ($expr->class instanceof Expr || $expr->class instanceof Name) {
3935: if ($expr->class instanceof Expr) {
3936: $objectClasses = $scope->getType($expr)->getObjectClassNames();
3937: if (count($objectClasses) === 1) {
3938: $objectExprResult = $this->processExprNode($stmt, new New_(new Name($objectClasses[0])), $scope, $storage, new NoopNodeCallback(), $context->enterDeep());
3939: $className = $objectClasses[0];
3940: $additionalThrowPoints = $objectExprResult->getThrowPoints();
3941: } else {
3942: $additionalThrowPoints = [InternalThrowPoint::createImplicit($scope, $expr)];
3943: }
3944:
3945: $result = $this->processExprNode($stmt, $expr->class, $scope, $storage, $nodeCallback, $context->enterDeep());
3946: $scope = $result->getScope();
3947: $hasYield = $result->hasYield();
3948: $throwPoints = $result->getThrowPoints();
3949: $impurePoints = $result->getImpurePoints();
3950: $isAlwaysTerminating = $result->isAlwaysTerminating();
3951: foreach ($additionalThrowPoints as $throwPoint) {
3952: $throwPoints[] = $throwPoint;
3953: }
3954: } else {
3955: $className = $scope->resolveName($expr->class);
3956: }
3957:
3958: $classReflection = null;
3959: if ($className !== null && $this->reflectionProvider->hasClass($className)) {
3960: $classReflection = $this->reflectionProvider->getClass($className);
3961: if ($classReflection->hasConstructor()) {
3962: $constructorReflection = $classReflection->getConstructor();
3963: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
3964: $scope,
3965: $expr->getArgs(),
3966: $constructorReflection->getVariants(),
3967: $constructorReflection->getNamedArgumentsVariants(),
3968: );
3969: $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $parametersAcceptor, $classReflection, $expr, new Name\FullyQualified($className), $expr->getArgs(), $scope);
3970: if ($constructorThrowPoint !== null) {
3971: $throwPoints[] = $constructorThrowPoint;
3972: }
3973: }
3974: } else {
3975: $throwPoints[] = InternalThrowPoint::createImplicit($scope, $expr);
3976: }
3977:
3978: if ($constructorReflection !== null) {
3979: if (!$constructorReflection->hasSideEffects()->no()) {
3980: $certain = $constructorReflection->isPure()->no();
3981: $impurePoints[] = new ImpurePoint(
3982: $scope,
3983: $expr,
3984: 'new',
3985: sprintf('instantiation of class %s', $constructorReflection->getDeclaringClass()->getDisplayName()),
3986: $certain,
3987: );
3988: }
3989: } elseif ($classReflection === null) {
3990: $impurePoints[] = new ImpurePoint(
3991: $scope,
3992: $expr,
3993: 'new',
3994: 'instantiation of unknown class',
3995: false,
3996: );
3997: }
3998:
3999: if ($parametersAcceptor !== null) {
4000: $normalizedExpr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr;
4001: }
4002:
4003: } else {
4004: $classReflection = $this->reflectionProvider->getAnonymousClassReflection($expr->class, $scope); // populates $expr->class->name
4005: if ($classReflection->hasConstructor()) {
4006: $constructorReflection = $classReflection->getConstructor();
4007: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
4008: $scope,
4009: $expr->getArgs(),
4010: $constructorReflection->getVariants(),
4011: $constructorReflection->getNamedArgumentsVariants(),
4012: );
4013:
4014: if ($constructorReflection->getDeclaringClass()->getName() === $classReflection->getName()) {
4015: $constructorResult = null;
4016: $this->processStmtNode($expr->class, $scope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $classReflection, &$constructorResult): void {
4017: $nodeCallback($node, $scope);
4018: if (!$node instanceof MethodReturnStatementsNode) {
4019: return;
4020: }
4021: if ($constructorResult !== null) {
4022: return;
4023: }
4024: $currentClassReflection = $node->getClassReflection();
4025: if ($currentClassReflection->getName() !== $classReflection->getName()) {
4026: return;
4027: }
4028: if (!$currentClassReflection->hasConstructor()) {
4029: return;
4030: }
4031: if ($currentClassReflection->getConstructor()->getName() !== $node->getMethodReflection()->getName()) {
4032: return;
4033: }
4034: $constructorResult = $node;
4035: }, StatementContext::createTopLevel());
4036: if ($constructorResult !== null) {
4037: $throwPoints = array_map(static fn (ThrowPoint $point) => InternalThrowPoint::createFromPublic($point), $constructorResult->getStatementResult()->getThrowPoints());
4038: $impurePoints = $constructorResult->getImpurePoints();
4039: }
4040: } else {
4041: $this->processStmtNode($expr->class, $scope, $storage, $nodeCallback, StatementContext::createTopLevel());
4042: $declaringClass = $constructorReflection->getDeclaringClass();
4043: $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $parametersAcceptor, $classReflection, $expr, new Name\FullyQualified($declaringClass->getName()), $expr->getArgs(), $scope);
4044: if ($constructorThrowPoint !== null) {
4045: $throwPoints[] = $constructorThrowPoint;
4046: }
4047:
4048: if (!$constructorReflection->hasSideEffects()->no()) {
4049: $certain = $constructorReflection->isPure()->no();
4050: $impurePoints[] = new ImpurePoint(
4051: $scope,
4052: $expr,
4053: 'new',
4054: sprintf('instantiation of class %s', $declaringClass->getDisplayName()),
4055: $certain,
4056: );
4057: }
4058: }
4059: } else {
4060: $this->processStmtNode($expr->class, $scope, $storage, $nodeCallback, StatementContext::createTopLevel());
4061: }
4062: }
4063:
4064: $result = $this->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $normalizedExpr, $scope, $storage, $nodeCallback, $context);
4065: $scope = $result->getScope();
4066: $hasYield = $hasYield || $result->hasYield();
4067: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
4068: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
4069: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
4070: } elseif (
4071: $expr instanceof Expr\PreInc
4072: || $expr instanceof Expr\PostInc
4073: || $expr instanceof Expr\PreDec
4074: || $expr instanceof Expr\PostDec
4075: ) {
4076: $result = $this->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep());
4077: $scope = $result->getScope();
4078: $hasYield = $result->hasYield();
4079: $throwPoints = $result->getThrowPoints();
4080: $impurePoints = $result->getImpurePoints();
4081: $isAlwaysTerminating = $result->isAlwaysTerminating();
4082:
4083: $newExpr = $expr;
4084: if ($expr instanceof Expr\PostInc) {
4085: $newExpr = new Expr\PreInc($expr->var);
4086: } elseif ($expr instanceof Expr\PostDec) {
4087: $newExpr = new Expr\PreDec($expr->var);
4088: }
4089:
4090: $scope = $this->processVirtualAssign(
4091: $scope,
4092: $storage,
4093: $stmt,
4094: $expr->var,
4095: $newExpr,
4096: $nodeCallback,
4097: )->getScope();
4098: } elseif ($expr instanceof Ternary) {
4099: $ternaryCondResult = $this->processExprNode($stmt, $expr->cond, $scope, $storage, $nodeCallback, $context->enterDeep());
4100: $throwPoints = $ternaryCondResult->getThrowPoints();
4101: $impurePoints = $ternaryCondResult->getImpurePoints();
4102: $isAlwaysTerminating = $ternaryCondResult->isAlwaysTerminating();
4103: $ifTrueScope = $ternaryCondResult->getTruthyScope();
4104: $ifFalseScope = $ternaryCondResult->getFalseyScope();
4105: $ifTrueType = null;
4106: if ($expr->if !== null) {
4107: $ifResult = $this->processExprNode($stmt, $expr->if, $ifTrueScope, $storage, $nodeCallback, $context);
4108: $throwPoints = array_merge($throwPoints, $ifResult->getThrowPoints());
4109: $impurePoints = array_merge($impurePoints, $ifResult->getImpurePoints());
4110: $ifTrueScope = $ifResult->getScope();
4111: $ifTrueType = $ifTrueScope->getType($expr->if);
4112: }
4113:
4114: $elseResult = $this->processExprNode($stmt, $expr->else, $ifFalseScope, $storage, $nodeCallback, $context);
4115: $throwPoints = array_merge($throwPoints, $elseResult->getThrowPoints());
4116: $impurePoints = array_merge($impurePoints, $elseResult->getImpurePoints());
4117: $ifFalseScope = $elseResult->getScope();
4118:
4119: $condType = $scope->getType($expr->cond);
4120: if ($condType->isTrue()->yes()) {
4121: $finalScope = $ifTrueScope;
4122: } elseif ($condType->isFalse()->yes()) {
4123: $finalScope = $ifFalseScope;
4124: } else {
4125: if ($ifTrueType instanceof NeverType && $ifTrueType->isExplicit()) {
4126: $finalScope = $ifFalseScope;
4127: } else {
4128: $ifFalseType = $ifFalseScope->getType($expr->else);
4129:
4130: if ($ifFalseType instanceof NeverType && $ifFalseType->isExplicit()) {
4131: $finalScope = $ifTrueScope;
4132: } else {
4133: $finalScope = $ifTrueScope->mergeWith($ifFalseScope);
4134: }
4135: }
4136: }
4137:
4138: return new ExpressionResult(
4139: $finalScope,
4140: $ternaryCondResult->hasYield(),
4141: $isAlwaysTerminating,
4142: $throwPoints,
4143: $impurePoints,
4144: static fn (): MutatingScope => $finalScope->filterByTruthyValue($expr),
4145: static fn (): MutatingScope => $finalScope->filterByFalseyValue($expr),
4146: );
4147: } elseif ($expr instanceof Expr\Yield_) {
4148: $throwPoints = [
4149: InternalThrowPoint::createImplicit($scope, $expr),
4150: ];
4151: $impurePoints = [
4152: new ImpurePoint(
4153: $scope,
4154: $expr,
4155: 'yield',
4156: 'yield',
4157: true,
4158: ),
4159: ];
4160: $isAlwaysTerminating = false;
4161: if ($expr->key !== null) {
4162: $keyResult = $this->processExprNode($stmt, $expr->key, $scope, $storage, $nodeCallback, $context->enterDeep());
4163: $scope = $keyResult->getScope();
4164: $throwPoints = $keyResult->getThrowPoints();
4165: $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints());
4166: $isAlwaysTerminating = $keyResult->isAlwaysTerminating();
4167: }
4168: if ($expr->value !== null) {
4169: $valueResult = $this->processExprNode($stmt, $expr->value, $scope, $storage, $nodeCallback, $context->enterDeep());
4170: $scope = $valueResult->getScope();
4171: $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints());
4172: $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints());
4173: $isAlwaysTerminating = $isAlwaysTerminating || $valueResult->isAlwaysTerminating();
4174: }
4175: $hasYield = true;
4176: } elseif ($expr instanceof Expr\Match_) {
4177: $deepContext = $context->enterDeep();
4178: $condType = $scope->getType($expr->cond);
4179: $condResult = $this->processExprNode($stmt, $expr->cond, $scope, $storage, $nodeCallback, $deepContext);
4180: $scope = $condResult->getScope();
4181: $hasYield = $condResult->hasYield();
4182: $throwPoints = $condResult->getThrowPoints();
4183: $impurePoints = $condResult->getImpurePoints();
4184: $isAlwaysTerminating = $condResult->isAlwaysTerminating();
4185: $matchScope = $scope->enterMatch($expr);
4186: $armNodes = [];
4187: $hasDefaultCond = false;
4188: $hasAlwaysTrueCond = false;
4189: $arms = $expr->arms;
4190: $armCondsToSkip = [];
4191: $armBodyScopes = [];
4192: if ($condType->isEnum()->yes()) {
4193: // enum match analysis would work even without this if branch
4194: // but would be much slower
4195: // this avoids using ObjectType::$subtractedType which is slow for huge enums
4196: // because of repeated union type normalization
4197: $enumCases = $condType->getEnumCases();
4198: if (count($enumCases) > 0) {
4199: $indexedEnumCases = [];
4200: foreach ($enumCases as $enumCase) {
4201: $indexedEnumCases[strtolower($enumCase->getClassName())][$enumCase->getEnumCaseName()] = $enumCase;
4202: }
4203: $unusedIndexedEnumCases = $indexedEnumCases;
4204: foreach ($arms as $i => $arm) {
4205: if ($arm->conds === null) {
4206: continue;
4207: }
4208:
4209: $condNodes = [];
4210: $conditionCases = [];
4211: $conditionExprs = [];
4212: foreach ($arm->conds as $j => $cond) {
4213: if (!$cond instanceof Expr\ClassConstFetch) {
4214: continue 2;
4215: }
4216: if (!$cond->class instanceof Name) {
4217: continue 2;
4218: }
4219: if (!$cond->name instanceof Node\Identifier) {
4220: continue 2;
4221: }
4222: $fetchedClassName = $scope->resolveName($cond->class);
4223: $loweredFetchedClassName = strtolower($fetchedClassName);
4224: if (!array_key_exists($loweredFetchedClassName, $indexedEnumCases)) {
4225: continue 2;
4226: }
4227:
4228: if (!array_key_exists($loweredFetchedClassName, $unusedIndexedEnumCases)) {
4229: throw new ShouldNotHappenException();
4230: }
4231:
4232: $caseName = $cond->name->toString();
4233: if (!array_key_exists($caseName, $indexedEnumCases[$loweredFetchedClassName])) {
4234: continue 2;
4235: }
4236:
4237: $enumCase = $indexedEnumCases[$loweredFetchedClassName][$caseName];
4238: $conditionCases[] = $enumCase;
4239: $armConditionScope = $matchScope;
4240: if (!array_key_exists($caseName, $unusedIndexedEnumCases[$loweredFetchedClassName])) {
4241: // force "always false"
4242: $armConditionScope = $armConditionScope->removeTypeFromExpression(
4243: $expr->cond,
4244: $enumCase,
4245: );
4246: } else {
4247: $unusedCasesCount = 0;
4248: foreach ($unusedIndexedEnumCases as $cases) {
4249: $unusedCasesCount += count($cases);
4250: }
4251: if ($unusedCasesCount === 1) {
4252: $hasAlwaysTrueCond = true;
4253:
4254: // force "always true"
4255: $armConditionScope = $armConditionScope->addTypeToExpression(
4256: $expr->cond,
4257: $enumCase,
4258: );
4259: }
4260: }
4261:
4262: $this->processExprNode($stmt, $cond, $armConditionScope, $storage, $nodeCallback, $deepContext);
4263:
4264: $condNodes[] = new MatchExpressionArmCondition(
4265: $cond,
4266: $armConditionScope,
4267: $cond->getStartLine(),
4268: );
4269: $conditionExprs[] = $cond;
4270:
4271: unset($unusedIndexedEnumCases[$loweredFetchedClassName][$caseName]);
4272: $armCondsToSkip[$i][$j] = true;
4273: }
4274:
4275: $conditionCasesCount = count($conditionCases);
4276: if ($conditionCasesCount === 0) {
4277: throw new ShouldNotHappenException();
4278: } elseif ($conditionCasesCount === 1) {
4279: $conditionCaseType = $conditionCases[0];
4280: } else {
4281: $conditionCaseType = new UnionType($conditionCases);
4282: }
4283:
4284: $filteringExpr = $this->getFilteringExprForMatchArm($expr, $conditionExprs);
4285: $matchArmBodyScope = $matchScope->addTypeToExpression(
4286: $expr->cond,
4287: $conditionCaseType,
4288: )->filterByTruthyValue($filteringExpr);
4289: $matchArmBody = new MatchExpressionArmBody($matchArmBodyScope, $arm->body);
4290: $armNodes[$i] = new MatchExpressionArm($matchArmBody, $condNodes, $arm->getStartLine());
4291:
4292: $armResult = $this->processExprNode(
4293: $stmt,
4294: $arm->body,
4295: $matchArmBodyScope,
4296: $storage,
4297: $nodeCallback,
4298: ExpressionContext::createTopLevel(),
4299: );
4300: $armScope = $armResult->getScope();
4301: $armBodyScopes[] = $armScope;
4302: $hasYield = $hasYield || $armResult->hasYield();
4303: $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints());
4304: $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints());
4305:
4306: unset($arms[$i]);
4307: }
4308:
4309: $remainingCases = [];
4310: foreach ($unusedIndexedEnumCases as $cases) {
4311: foreach ($cases as $case) {
4312: $remainingCases[] = $case;
4313: }
4314: }
4315:
4316: $remainingCasesCount = count($remainingCases);
4317: if ($remainingCasesCount === 0) {
4318: $remainingType = new NeverType();
4319: } elseif ($remainingCasesCount === 1) {
4320: $remainingType = $remainingCases[0];
4321: } else {
4322: $remainingType = new UnionType($remainingCases);
4323: }
4324:
4325: $matchScope = $matchScope->addTypeToExpression($expr->cond, $remainingType);
4326: }
4327: }
4328: foreach ($arms as $i => $arm) {
4329: if ($arm->conds === null) {
4330: $hasDefaultCond = true;
4331: $matchArmBody = new MatchExpressionArmBody($matchScope, $arm->body);
4332: $armNodes[$i] = new MatchExpressionArm($matchArmBody, [], $arm->getStartLine());
4333: $armResult = $this->processExprNode($stmt, $arm->body, $matchScope, $storage, $nodeCallback, ExpressionContext::createTopLevel());
4334: $matchScope = $armResult->getScope();
4335: $hasYield = $hasYield || $armResult->hasYield();
4336: $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints());
4337: $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints());
4338: $armBodyScopes[] = $matchScope;
4339: continue;
4340: }
4341:
4342: if (count($arm->conds) === 0) {
4343: throw new ShouldNotHappenException();
4344: }
4345:
4346: $filteringExprs = [];
4347: $armCondScope = $matchScope;
4348: $condNodes = [];
4349: foreach ($arm->conds as $j => $armCond) {
4350: if (isset($armCondsToSkip[$i][$j])) {
4351: continue;
4352: }
4353: $condNodes[] = new MatchExpressionArmCondition($armCond, $armCondScope, $armCond->getStartLine());
4354: $armCondResult = $this->processExprNode($stmt, $armCond, $armCondScope, $storage, $nodeCallback, $deepContext);
4355: $hasYield = $hasYield || $armCondResult->hasYield();
4356: $throwPoints = array_merge($throwPoints, $armCondResult->getThrowPoints());
4357: $impurePoints = array_merge($impurePoints, $armCondResult->getImpurePoints());
4358: $armCondExpr = new BinaryOp\Identical($expr->cond, $armCond);
4359: $armCondResultScope = $armCondResult->getScope();
4360: $armCondType = $this->treatPhpDocTypesAsCertain ? $armCondResultScope->getType($armCondExpr) : $armCondResultScope->getNativeType($armCondExpr);
4361: if ($armCondType->isTrue()->yes()) {
4362: $hasAlwaysTrueCond = true;
4363: }
4364: $armCondScope = $armCondResult->getScope()->filterByFalseyValue($armCondExpr);
4365: $filteringExprs[] = $armCond;
4366: }
4367:
4368: $filteringExpr = $this->getFilteringExprForMatchArm($expr, $filteringExprs);
4369: $bodyScope = $matchScope->filterByTruthyValue($filteringExpr);
4370: $matchArmBody = new MatchExpressionArmBody($bodyScope, $arm->body);
4371: $armNodes[$i] = new MatchExpressionArm($matchArmBody, $condNodes, $arm->getStartLine());
4372:
4373: $armResult = $this->processExprNode(
4374: $stmt,
4375: $arm->body,
4376: $bodyScope,
4377: $storage,
4378: $nodeCallback,
4379: ExpressionContext::createTopLevel(),
4380: );
4381: $armScope = $armResult->getScope();
4382: $armBodyScopes[] = $armScope;
4383: $hasYield = $hasYield || $armResult->hasYield();
4384: $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints());
4385: $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints());
4386: $matchScope = $matchScope->filterByFalseyValue($filteringExpr);
4387: }
4388:
4389: if (!$hasDefaultCond && !$hasAlwaysTrueCond && $condType->isBoolean()->yes() && $condType->isConstantScalarValue()->yes()) {
4390: if ($this->isScopeConditionallyImpossible($matchScope)) {
4391: $hasAlwaysTrueCond = true;
4392: $matchScope = $matchScope->addTypeToExpression($expr->cond, new NeverType());
4393: }
4394: }
4395:
4396: $isExhaustive = $hasDefaultCond || $hasAlwaysTrueCond;
4397: if (!$isExhaustive) {
4398: $remainingType = $matchScope->getType($expr->cond);
4399: if ($remainingType instanceof NeverType) {
4400: $isExhaustive = true;
4401: }
4402: }
4403:
4404: if ($isExhaustive) {
4405: $armBodyFinalScope = null;
4406: foreach ($armBodyScopes as $armBodyScope) {
4407: $armBodyFinalScope = $armBodyScope->mergeWith($armBodyFinalScope);
4408: }
4409: $scope = $armBodyFinalScope ?? $scope;
4410: } else {
4411: $armBodyFinalScope = null;
4412: foreach ($armBodyScopes as $armBodyScope) {
4413: $armBodyFinalScope = $armBodyScope->mergeWith($armBodyFinalScope);
4414: }
4415: if ($armBodyFinalScope !== null) {
4416: $scope = $scope->mergeWith($armBodyFinalScope);
4417: }
4418: $throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(UnhandledMatchError::class), $expr, false);
4419: }
4420:
4421: ksort($armNodes, SORT_NUMERIC);
4422:
4423: $this->callNodeCallback($nodeCallback, new MatchExpressionNode($expr->cond, array_values($armNodes), $expr, $matchScope), $scope, $storage);
4424:
4425: if ($expr->cond instanceof AlwaysRememberedExpr) {
4426: $expr->cond = $expr->cond->getExpr();
4427: }
4428: } elseif ($expr instanceof AlwaysRememberedExpr) {
4429: $result = $this->processExprNode($stmt, $expr->getExpr(), $scope, $storage, $nodeCallback, $context);
4430: $hasYield = $result->hasYield();
4431: $throwPoints = $result->getThrowPoints();
4432: $impurePoints = $result->getImpurePoints();
4433: $isAlwaysTerminating = $result->isAlwaysTerminating();
4434: $scope = $result->getScope();
4435: $expr = $expr->getExpr();
4436: } elseif ($expr instanceof Expr\Throw_) {
4437: $hasYield = false;
4438: $result = $this->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
4439: $throwPoints = $result->getThrowPoints();
4440: $impurePoints = $result->getImpurePoints();
4441: $isAlwaysTerminating = $result->isAlwaysTerminating();
4442: $throwPoints[] = InternalThrowPoint::createExplicit($scope, $scope->getType($expr->expr), $expr, false);
4443: } elseif ($expr instanceof FunctionCallableNode) {
4444: $throwPoints = [];
4445: $impurePoints = [];
4446: $hasYield = false;
4447: $isAlwaysTerminating = false;
4448: if ($expr->getName() instanceof Expr) {
4449: $result = $this->processExprNode($stmt, $expr->getName(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
4450: $scope = $result->getScope();
4451: $hasYield = $result->hasYield();
4452: $throwPoints = $result->getThrowPoints();
4453: $impurePoints = $result->getImpurePoints();
4454: $isAlwaysTerminating = $result->isAlwaysTerminating();
4455: }
4456: } elseif ($expr instanceof MethodCallableNode) {
4457: $result = $this->processExprNode($stmt, $expr->getVar(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
4458: $scope = $result->getScope();
4459: $hasYield = $result->hasYield();
4460: $throwPoints = $result->getThrowPoints();
4461: $impurePoints = $result->getImpurePoints();
4462: $isAlwaysTerminating = false;
4463: if ($expr->getName() instanceof Expr) {
4464: $nameResult = $this->processExprNode($stmt, $expr->getName(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
4465: $scope = $nameResult->getScope();
4466: $hasYield = $hasYield || $nameResult->hasYield();
4467: $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints());
4468: $impurePoints = array_merge($impurePoints, $nameResult->getImpurePoints());
4469: $isAlwaysTerminating = $nameResult->isAlwaysTerminating();
4470: }
4471: } elseif ($expr instanceof StaticMethodCallableNode) {
4472: $throwPoints = [];
4473: $impurePoints = [];
4474: $hasYield = false;
4475: $isAlwaysTerminating = false;
4476: if ($expr->getClass() instanceof Expr) {
4477: $classResult = $this->processExprNode($stmt, $expr->getClass(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
4478: $scope = $classResult->getScope();
4479: $hasYield = $classResult->hasYield();
4480: $throwPoints = $classResult->getThrowPoints();
4481: $impurePoints = $classResult->getImpurePoints();
4482: $isAlwaysTerminating = $classResult->isAlwaysTerminating();
4483: }
4484: if ($expr->getName() instanceof Expr) {
4485: $nameResult = $this->processExprNode($stmt, $expr->getName(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
4486: $scope = $nameResult->getScope();
4487: $hasYield = $hasYield || $nameResult->hasYield();
4488: $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints());
4489: $impurePoints = array_merge($impurePoints, $nameResult->getImpurePoints());
4490: $isAlwaysTerminating = $isAlwaysTerminating || $nameResult->isAlwaysTerminating();
4491: }
4492: } elseif ($expr instanceof InstantiationCallableNode) {
4493: $throwPoints = [];
4494: $impurePoints = [];
4495: $hasYield = false;
4496: $isAlwaysTerminating = false;
4497: if ($expr->getClass() instanceof Expr) {
4498: $classResult = $this->processExprNode($stmt, $expr->getClass(), $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
4499: $scope = $classResult->getScope();
4500: $hasYield = $classResult->hasYield();
4501: $throwPoints = $classResult->getThrowPoints();
4502: $impurePoints = $classResult->getImpurePoints();
4503: $isAlwaysTerminating = $classResult->isAlwaysTerminating();
4504: }
4505: } elseif ($expr instanceof Node\Scalar) {
4506: $hasYield = false;
4507: $throwPoints = [];
4508: $impurePoints = [];
4509: $isAlwaysTerminating = false;
4510: } elseif ($expr instanceof ConstFetch) {
4511: $hasYield = false;
4512: $throwPoints = [];
4513: $impurePoints = [];
4514: $isAlwaysTerminating = false;
4515: $this->callNodeCallback($nodeCallback, $expr->name, $scope, $storage);
4516: } else {
4517: $hasYield = false;
4518: $throwPoints = [];
4519: $impurePoints = [];
4520: $isAlwaysTerminating = false;
4521: }
4522:
4523: $result = new ExpressionResult(
4524: $scope,
4525: $hasYield,
4526: $isAlwaysTerminating,
4527: $throwPoints,
4528: $impurePoints,
4529: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
4530: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
4531: );
4532: return $result;
4533: }
4534:
4535: private function getArrayFunctionAppendingType(FunctionReflection $functionReflection, Scope $scope, FuncCall $expr): Type
4536: {
4537: $arrayArg = $expr->getArgs()[0]->value;
4538: $arrayType = $scope->getType($arrayArg);
4539: $callArgs = array_slice($expr->getArgs(), 1);
4540:
4541: /**
4542: * @param Arg[] $callArgs
4543: * @param callable(?Type, Type, bool): void $setOffsetValueType
4544: */
4545: $setOffsetValueTypes = static function (Scope $scope, array $callArgs, callable $setOffsetValueType, ?bool &$nonConstantArrayWasUnpacked = null): void {
4546: foreach ($callArgs as $callArg) {
4547: $callArgType = $scope->getType($callArg->value);
4548: if ($callArg->unpack) {
4549: $constantArrays = $callArgType->getConstantArrays();
4550: if (count($constantArrays) === 1) {
4551: $iterableValueTypes = $constantArrays[0]->getValueTypes();
4552: } else {
4553: $iterableValueTypes = [$callArgType->getIterableValueType()];
4554: $nonConstantArrayWasUnpacked = true;
4555: }
4556:
4557: $isOptional = !$callArgType->isIterableAtLeastOnce()->yes();
4558: foreach ($iterableValueTypes as $iterableValueType) {
4559: if ($iterableValueType instanceof UnionType) {
4560: foreach ($iterableValueType->getTypes() as $innerType) {
4561: $setOffsetValueType(null, $innerType, $isOptional);
4562: }
4563: } else {
4564: $setOffsetValueType(null, $iterableValueType, $isOptional);
4565: }
4566: }
4567: continue;
4568: }
4569: $setOffsetValueType(null, $callArgType, false);
4570: }
4571: };
4572:
4573: $constantArrays = $arrayType->getConstantArrays();
4574: if (count($constantArrays) > 0) {
4575: $newArrayTypes = [];
4576: $prepend = $functionReflection->getName() === 'array_unshift';
4577: foreach ($constantArrays as $constantArray) {
4578: $arrayTypeBuilder = $prepend ? ConstantArrayTypeBuilder::createEmpty() : ConstantArrayTypeBuilder::createFromConstantArray($constantArray);
4579:
4580: $setOffsetValueTypes(
4581: $scope,
4582: $callArgs,
4583: static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayTypeBuilder): void {
4584: $arrayTypeBuilder->setOffsetValueType($offsetType, $valueType, $optional);
4585: },
4586: $nonConstantArrayWasUnpacked,
4587: );
4588:
4589: if ($prepend) {
4590: $keyTypes = $constantArray->getKeyTypes();
4591: $valueTypes = $constantArray->getValueTypes();
4592: foreach ($keyTypes as $k => $keyType) {
4593: $arrayTypeBuilder->setOffsetValueType(
4594: count($keyType->getConstantStrings()) === 1 ? $keyType->getConstantStrings()[0] : null,
4595: $valueTypes[$k],
4596: $constantArray->isOptionalKey($k),
4597: );
4598: }
4599: }
4600:
4601: $constantArray = $arrayTypeBuilder->getArray();
4602:
4603: if ($constantArray->isConstantArray()->yes() && $nonConstantArrayWasUnpacked) {
4604: $array = new ArrayType($constantArray->generalize(GeneralizePrecision::lessSpecific())->getIterableKeyType(), $constantArray->getIterableValueType());
4605: $isList = $constantArray->isList()->yes();
4606: $constantArray = $constantArray->isIterableAtLeastOnce()->yes()
4607: ? new IntersectionType([$array, new NonEmptyArrayType()])
4608: : $array;
4609: $constantArray = $isList
4610: ? TypeCombinator::intersect($constantArray, new AccessoryArrayListType())
4611: : $constantArray;
4612: }
4613:
4614: $newArrayTypes[] = $constantArray;
4615: }
4616:
4617: return TypeCombinator::union(...$newArrayTypes);
4618: }
4619:
4620: $setOffsetValueTypes(
4621: $scope,
4622: $callArgs,
4623: static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayType): void {
4624: $isIterableAtLeastOnce = $arrayType->isIterableAtLeastOnce()->yes() || !$optional;
4625: $arrayType = $arrayType->setOffsetValueType($offsetType, $valueType);
4626: if ($isIterableAtLeastOnce) {
4627: return;
4628: }
4629:
4630: $arrayType = TypeCombinator::union($arrayType, new ConstantArrayType([], []));
4631: },
4632: );
4633:
4634: return $arrayType;
4635: }
4636:
4637: private function getArraySortPreserveListFunctionType(Type $type): Type
4638: {
4639: $isIterableAtLeastOnce = $type->isIterableAtLeastOnce();
4640: if ($isIterableAtLeastOnce->no()) {
4641: return $type;
4642: }
4643:
4644: return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($isIterableAtLeastOnce): Type {
4645: if ($type instanceof UnionType || $type instanceof IntersectionType) {
4646: return $traverse($type);
4647: }
4648:
4649: if (!$type instanceof ArrayType && !$type instanceof ConstantArrayType) {
4650: return $type;
4651: }
4652:
4653: $newArrayType = new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $type->getIterableValueType()), new AccessoryArrayListType()]);
4654: if ($isIterableAtLeastOnce->yes()) {
4655: $newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType());
4656: }
4657:
4658: return $newArrayType;
4659: });
4660: }
4661:
4662: private function getArraySortDoNotPreserveListFunctionType(Type $type): Type
4663: {
4664: $isIterableAtLeastOnce = $type->isIterableAtLeastOnce();
4665: if ($isIterableAtLeastOnce->no()) {
4666: return $type;
4667: }
4668:
4669: return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($isIterableAtLeastOnce): Type {
4670: if ($type instanceof UnionType) {
4671: return $traverse($type);
4672: }
4673:
4674: $constantArrays = $type->getConstantArrays();
4675: if (count($constantArrays) > 0) {
4676: $types = [];
4677: foreach ($constantArrays as $constantArray) {
4678: $types[] = new ConstantArrayType(
4679: $constantArray->getKeyTypes(),
4680: $constantArray->getValueTypes(),
4681: $constantArray->getNextAutoIndexes(),
4682: $constantArray->getOptionalKeys(),
4683: $constantArray->isList()->and(TrinaryLogic::createMaybe()),
4684: );
4685: }
4686:
4687: return TypeCombinator::union(...$types);
4688: }
4689:
4690: $newArrayType = new ArrayType($type->getIterableKeyType(), $type->getIterableValueType());
4691: if ($isIterableAtLeastOnce->yes()) {
4692: $newArrayType = new IntersectionType([$newArrayType, new NonEmptyArrayType()]);
4693: }
4694:
4695: return $newArrayType;
4696: });
4697: }
4698:
4699: private function getFunctionThrowPoint(
4700: FunctionReflection $functionReflection,
4701: ?ParametersAcceptor $parametersAcceptor,
4702: FuncCall $funcCall,
4703: MutatingScope $scope,
4704: ): ?InternalThrowPoint
4705: {
4706: $normalizedFuncCall = $funcCall;
4707: if ($parametersAcceptor !== null) {
4708: $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $funcCall);
4709: }
4710:
4711: if ($normalizedFuncCall !== null) {
4712: foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicFunctionThrowTypeExtensions() as $extension) {
4713: if (!$extension->isFunctionSupported($functionReflection)) {
4714: continue;
4715: }
4716:
4717: $throwType = $extension->getThrowTypeFromFunctionCall($functionReflection, $normalizedFuncCall, $scope);
4718: if ($throwType === null) {
4719: return null;
4720: }
4721:
4722: return InternalThrowPoint::createExplicit($scope, $throwType, $funcCall, false);
4723: }
4724: }
4725:
4726: $throwType = $functionReflection->getThrowType();
4727: if ($throwType === null && $parametersAcceptor !== null) {
4728: $returnType = $parametersAcceptor->getReturnType();
4729: if ($returnType instanceof NeverType && $returnType->isExplicit()) {
4730: $throwType = new ObjectType(Throwable::class);
4731: }
4732: }
4733:
4734: if ($throwType !== null) {
4735: if (!$throwType->isVoid()->yes()) {
4736: return InternalThrowPoint::createExplicit($scope, $throwType, $funcCall, true);
4737: }
4738: } elseif ($this->implicitThrows) {
4739: $requiredParameters = null;
4740: if ($parametersAcceptor !== null) {
4741: $requiredParameters = 0;
4742: foreach ($parametersAcceptor->getParameters() as $parameter) {
4743: if ($parameter->isOptional()) {
4744: continue;
4745: }
4746:
4747: $requiredParameters++;
4748: }
4749: }
4750: if (
4751: !$functionReflection->isBuiltin()
4752: || $requiredParameters === null
4753: || $requiredParameters > 0
4754: || count($funcCall->getArgs()) > 0
4755: ) {
4756: $functionReturnedType = $scope->getType($funcCall);
4757: if (!(new ObjectType(Throwable::class))->isSuperTypeOf($functionReturnedType)->yes()) {
4758: return InternalThrowPoint::createImplicit($scope, $funcCall);
4759: }
4760: }
4761: }
4762:
4763: return null;
4764: }
4765:
4766: private function getMethodThrowPoint(MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, MethodCall $methodCall, MutatingScope $scope): ?InternalThrowPoint
4767: {
4768: $normalizedMethodCall = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $methodCall);
4769: if ($normalizedMethodCall !== null) {
4770: foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) {
4771: if (!$extension->isMethodSupported($methodReflection)) {
4772: continue;
4773: }
4774:
4775: $throwType = $extension->getThrowTypeFromMethodCall($methodReflection, $normalizedMethodCall, $scope);
4776: if ($throwType === null) {
4777: return null;
4778: }
4779:
4780: return InternalThrowPoint::createExplicit($scope, $throwType, $methodCall, false);
4781: }
4782: }
4783:
4784: $throwType = $methodReflection->getThrowType();
4785: if ($throwType === null) {
4786: $returnType = $parametersAcceptor->getReturnType();
4787: if ($returnType instanceof NeverType && $returnType->isExplicit()) {
4788: $throwType = new ObjectType(Throwable::class);
4789: }
4790: }
4791:
4792: if ($throwType !== null) {
4793: if (!$throwType->isVoid()->yes()) {
4794: return InternalThrowPoint::createExplicit($scope, $throwType, $methodCall, true);
4795: }
4796: } elseif ($this->implicitThrows) {
4797: $methodReturnedType = $scope->getType($methodCall);
4798: if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
4799: return InternalThrowPoint::createImplicit($scope, $methodCall);
4800: }
4801: }
4802:
4803: return null;
4804: }
4805:
4806: /**
4807: * @param list<Node\Arg> $args
4808: */
4809: private function getConstructorThrowPoint(MethodReflection $constructorReflection, ParametersAcceptor $parametersAcceptor, ClassReflection $classReflection, New_ $new, Name $className, array $args, MutatingScope $scope): ?InternalThrowPoint
4810: {
4811: $methodCall = new StaticCall($className, $constructorReflection->getName(), $args);
4812: $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall);
4813: if ($normalizedMethodCall !== null) {
4814: foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) {
4815: if (!$extension->isStaticMethodSupported($constructorReflection)) {
4816: continue;
4817: }
4818:
4819: $throwType = $extension->getThrowTypeFromStaticMethodCall($constructorReflection, $normalizedMethodCall, $scope);
4820: if ($throwType === null) {
4821: return null;
4822: }
4823:
4824: return InternalThrowPoint::createExplicit($scope, $throwType, $new, false);
4825: }
4826: }
4827:
4828: if ($constructorReflection->getThrowType() !== null) {
4829: $throwType = $constructorReflection->getThrowType();
4830: if (!$throwType->isVoid()->yes()) {
4831: return InternalThrowPoint::createExplicit($scope, $throwType, $new, true);
4832: }
4833: } elseif ($this->implicitThrows) {
4834: if (!$classReflection->is(Throwable::class)) {
4835: return InternalThrowPoint::createImplicit($scope, $methodCall);
4836: }
4837: }
4838:
4839: return null;
4840: }
4841:
4842: private function getStaticMethodThrowPoint(MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, StaticCall $methodCall, MutatingScope $scope): ?InternalThrowPoint
4843: {
4844: $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall);
4845: if ($normalizedMethodCall !== null) {
4846: foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) {
4847: if (!$extension->isStaticMethodSupported($methodReflection)) {
4848: continue;
4849: }
4850:
4851: $throwType = $extension->getThrowTypeFromStaticMethodCall($methodReflection, $normalizedMethodCall, $scope);
4852: if ($throwType === null) {
4853: return null;
4854: }
4855:
4856: return InternalThrowPoint::createExplicit($scope, $throwType, $methodCall, false);
4857: }
4858: }
4859:
4860: if ($methodReflection->getThrowType() !== null) {
4861: $throwType = $methodReflection->getThrowType();
4862: if (!$throwType->isVoid()->yes()) {
4863: return InternalThrowPoint::createExplicit($scope, $throwType, $methodCall, true);
4864: }
4865: } elseif ($this->implicitThrows) {
4866: $methodReturnedType = $scope->getType($methodCall);
4867: if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
4868: return InternalThrowPoint::createImplicit($scope, $methodCall);
4869: }
4870: }
4871:
4872: return null;
4873: }
4874:
4875: /**
4876: * @return InternalThrowPoint[]
4877: */
4878: private function getPropertyReadThrowPointsFromGetHook(
4879: MutatingScope $scope,
4880: PropertyFetch $propertyFetch,
4881: PhpPropertyReflection $propertyReflection,
4882: ): array
4883: {
4884: return $this->getThrowPointsFromPropertyHook($scope, $propertyFetch, $propertyReflection, 'get');
4885: }
4886:
4887: /**
4888: * @return InternalThrowPoint[]
4889: */
4890: private function getPropertyAssignThrowPointsFromSetHook(
4891: MutatingScope $scope,
4892: PropertyFetch $propertyFetch,
4893: PhpPropertyReflection $propertyReflection,
4894: ): array
4895: {
4896: return $this->getThrowPointsFromPropertyHook($scope, $propertyFetch, $propertyReflection, 'set');
4897: }
4898:
4899: /**
4900: * @param 'get'|'set' $hookName
4901: * @return InternalThrowPoint[]
4902: */
4903: private function getThrowPointsFromPropertyHook(
4904: MutatingScope $scope,
4905: PropertyFetch $propertyFetch,
4906: PhpPropertyReflection $propertyReflection,
4907: string $hookName,
4908: ): array
4909: {
4910: $scopeFunction = $scope->getFunction();
4911: if (
4912: $scopeFunction instanceof PhpMethodFromParserNodeReflection
4913: && $scopeFunction->isPropertyHook()
4914: && $propertyFetch->var instanceof Variable
4915: && $propertyFetch->var->name === 'this'
4916: && $propertyFetch->name instanceof Identifier
4917: && $propertyFetch->name->toString() === $scopeFunction->getHookedPropertyName()
4918: ) {
4919: return [];
4920: }
4921: $declaringClass = $propertyReflection->getDeclaringClass();
4922: if (!$propertyReflection->hasHook($hookName)) {
4923: if (
4924: $propertyReflection->isPrivate()
4925: || $propertyReflection->isFinal()->yes()
4926: || $declaringClass->isFinal()
4927: ) {
4928: return [];
4929: }
4930:
4931: if ($this->implicitThrows) {
4932: return [InternalThrowPoint::createImplicit($scope, $propertyFetch)];
4933: }
4934:
4935: return [];
4936: }
4937:
4938: $getHook = $propertyReflection->getHook($hookName);
4939: $throwType = $getHook->getThrowType();
4940:
4941: if ($throwType !== null) {
4942: if (!$throwType->isVoid()->yes()) {
4943: return [InternalThrowPoint::createExplicit($scope, $throwType, $propertyFetch, true)];
4944: }
4945: } elseif ($this->implicitThrows) {
4946: return [InternalThrowPoint::createImplicit($scope, $propertyFetch)];
4947: }
4948:
4949: return [];
4950: }
4951:
4952: /**
4953: * @return string[]
4954: */
4955: private function getAssignedVariables(Expr $expr): array
4956: {
4957: if ($expr instanceof Expr\Variable) {
4958: if (is_string($expr->name)) {
4959: return [$expr->name];
4960: }
4961:
4962: return [];
4963: }
4964:
4965: if ($expr instanceof Expr\List_) {
4966: $names = [];
4967: foreach ($expr->items as $item) {
4968: if ($item === null) {
4969: continue;
4970: }
4971:
4972: $names = array_merge($names, $this->getAssignedVariables($item->value));
4973: }
4974:
4975: return $names;
4976: }
4977:
4978: if ($expr instanceof ArrayDimFetch) {
4979: return $this->getAssignedVariables($expr->var);
4980: }
4981:
4982: return [];
4983: }
4984:
4985: /**
4986: * @param callable(Node $node, Scope $scope): void $nodeCallback
4987: */
4988: private function callNodeCallbackWithExpression(
4989: callable $nodeCallback,
4990: Expr $expr,
4991: MutatingScope $scope,
4992: ExpressionResultStorage $storage,
4993: ExpressionContext $context,
4994: ): void
4995: {
4996: if ($context->isDeep()) {
4997: $scope = $scope->exitFirstLevelStatements();
4998: }
4999: $this->callNodeCallback($nodeCallback, $expr, $scope, $storage);
5000: }
5001:
5002: /**
5003: * @param callable(Node $node, Scope $scope): void $nodeCallback
5004: */
5005: protected function callNodeCallback(
5006: callable $nodeCallback,
5007: Node $node,
5008: MutatingScope $scope,
5009: ExpressionResultStorage $storage,
5010: ): void
5011: {
5012: $nodeCallback($node, $scope);
5013: }
5014:
5015: /**
5016: * @param callable(Node $node, Scope $scope): void $nodeCallback
5017: */
5018: private function processClosureNode(
5019: Node\Stmt $stmt,
5020: Expr\Closure $expr,
5021: MutatingScope $scope,
5022: ExpressionResultStorage $storage,
5023: callable $nodeCallback,
5024: ExpressionContext $context,
5025: ?Type $passedToType,
5026: ): ProcessClosureResult
5027: {
5028: foreach ($expr->params as $param) {
5029: $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
5030: }
5031:
5032: $byRefUses = [];
5033:
5034: $closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME);
5035: $callableParameters = $this->createCallableParameters(
5036: $scope,
5037: $expr,
5038: $closureCallArgs,
5039: $passedToType,
5040: );
5041:
5042: $useScope = $scope;
5043: foreach ($expr->uses as $use) {
5044: if ($use->byRef) {
5045: $byRefUses[] = $use;
5046: $useScope = $useScope->enterExpressionAssign($use->var);
5047:
5048: $inAssignRightSideVariableName = $context->getInAssignRightSideVariableName();
5049: $inAssignRightSideExpr = $context->getInAssignRightSideExpr();
5050: if (
5051: $inAssignRightSideVariableName === $use->var->name
5052: && $inAssignRightSideExpr !== null
5053: ) {
5054: $inAssignRightSideType = $scope->getType($inAssignRightSideExpr);
5055: if ($inAssignRightSideType instanceof ClosureType) {
5056: $variableType = $inAssignRightSideType;
5057: } else {
5058: $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName);
5059: if ($alreadyHasVariableType->no()) {
5060: $variableType = TypeCombinator::union(new NullType(), $inAssignRightSideType);
5061: } else {
5062: $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType);
5063: }
5064: }
5065: $inAssignRightSideNativeType = $scope->getNativeType($inAssignRightSideExpr);
5066: if ($inAssignRightSideNativeType instanceof ClosureType) {
5067: $variableNativeType = $inAssignRightSideNativeType;
5068: } else {
5069: $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName);
5070: if ($alreadyHasVariableType->no()) {
5071: $variableNativeType = TypeCombinator::union(new NullType(), $inAssignRightSideNativeType);
5072: } else {
5073: $variableNativeType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideNativeType);
5074: }
5075: }
5076: $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType, $variableNativeType, TrinaryLogic::createYes());
5077: }
5078: }
5079: $this->processExprNode($stmt, $use->var, $useScope, $storage, $nodeCallback, $context);
5080: if (!$use->byRef) {
5081: continue;
5082: }
5083:
5084: $useScope = $useScope->exitExpressionAssign($use->var);
5085: }
5086:
5087: if ($expr->returnType !== null) {
5088: $this->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage);
5089: }
5090:
5091: $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters);
5092: $closureScope = $closureScope->processClosureScope($scope, null, $byRefUses);
5093: $closureType = $closureScope->getAnonymousFunctionReflection();
5094: if (!$closureType instanceof ClosureType) {
5095: throw new ShouldNotHappenException();
5096: }
5097:
5098: $returnType = $closureType->getReturnType();
5099: $isAlwaysTerminating = ($returnType instanceof NeverType && $returnType->isExplicit());
5100:
5101: $this->callNodeCallback($nodeCallback, new InClosureNode($closureType, $expr), $closureScope, $storage);
5102:
5103: $executionEnds = [];
5104: $gatheredReturnStatements = [];
5105: $gatheredYieldStatements = [];
5106: $closureImpurePoints = [];
5107: $invalidateExpressions = [];
5108: $closureStmtsCallback = static function (Node $node, Scope $scope) use ($nodeCallback, &$executionEnds, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void {
5109: $nodeCallback($node, $scope);
5110: if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) {
5111: return;
5112: }
5113: if ($node instanceof PropertyAssignNode) {
5114: $closureImpurePoints[] = new ImpurePoint(
5115: $scope,
5116: $node,
5117: 'propertyAssign',
5118: 'property assignment',
5119: true,
5120: );
5121: return;
5122: }
5123: if ($node instanceof ExecutionEndNode) {
5124: $executionEnds[] = $node;
5125: return;
5126: }
5127: if ($node instanceof InvalidateExprNode) {
5128: $invalidateExpressions[] = $node;
5129: return;
5130: }
5131: if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) {
5132: $gatheredYieldStatements[] = $node;
5133: }
5134: if (!$node instanceof Return_) {
5135: return;
5136: }
5137:
5138: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
5139: };
5140:
5141: if (count($byRefUses) === 0) {
5142: $statementResult = $this->processStmtNodesInternalWithoutFlushingPendingFibers($expr, $expr->stmts, $closureScope, $storage, $closureStmtsCallback, StatementContext::createTopLevel());
5143: $publicStatementResult = $statementResult->toPublic();
5144: $this->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode(
5145: $expr,
5146: $gatheredReturnStatements,
5147: $gatheredYieldStatements,
5148: $publicStatementResult,
5149: $executionEnds,
5150: array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints),
5151: ), $closureScope, $storage);
5152:
5153: return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $isAlwaysTerminating);
5154: }
5155:
5156: $originalStorage = $storage;
5157:
5158: $count = 0;
5159: $closureResultScope = null;
5160: do {
5161: $prevScope = $closureScope;
5162:
5163: $storage = $originalStorage->duplicate();
5164: $intermediaryClosureScopeResult = $this->processStmtNodesInternalWithoutFlushingPendingFibers($expr, $expr->stmts, $closureScope, $storage, new NoopNodeCallback(), StatementContext::createTopLevel());
5165: $intermediaryClosureScope = $intermediaryClosureScopeResult->getScope();
5166: foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) {
5167: $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope());
5168: }
5169:
5170: if ($expr->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) === true) {
5171: $closureResultScope = $intermediaryClosureScope;
5172: break;
5173: }
5174:
5175: $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters);
5176: $closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses);
5177:
5178: if ($closureScope->equals($prevScope)) {
5179: break;
5180: }
5181: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
5182: $closureScope = $prevScope->generalizeWith($closureScope);
5183: }
5184: $count++;
5185: } while ($count < self::LOOP_SCOPE_ITERATIONS);
5186:
5187: if ($closureResultScope === null) {
5188: $closureResultScope = $closureScope;
5189: }
5190:
5191: $storage = $originalStorage;
5192: $statementResult = $this->processStmtNodesInternalWithoutFlushingPendingFibers($expr, $expr->stmts, $closureScope, $storage, $closureStmtsCallback, StatementContext::createTopLevel());
5193: $publicStatementResult = $statementResult->toPublic();
5194: $this->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode(
5195: $expr,
5196: $gatheredReturnStatements,
5197: $gatheredYieldStatements,
5198: $publicStatementResult,
5199: $executionEnds,
5200: array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints),
5201: ), $closureScope, $storage);
5202:
5203: return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $isAlwaysTerminating, $closureResultScope, $byRefUses);
5204: }
5205:
5206: /**
5207: * @param InvalidateExprNode[] $invalidatedExpressions
5208: * @param string[] $uses
5209: */
5210: private function processImmediatelyCalledCallable(MutatingScope $scope, array $invalidatedExpressions, array $uses): MutatingScope
5211: {
5212: if ($scope->isInClass()) {
5213: $uses[] = 'this';
5214: }
5215:
5216: $finder = new NodeFinder();
5217: foreach ($invalidatedExpressions as $invalidateExpression) {
5218: $result = $finder->findFirst([$invalidateExpression->getExpr()], static fn ($node) => $node instanceof Variable && in_array($node->name, $uses, true));
5219: if ($result === null) {
5220: continue;
5221: }
5222:
5223: $scope = $scope->invalidateExpression($invalidateExpression->getExpr(), true);
5224: }
5225:
5226: return $scope;
5227: }
5228:
5229: /**
5230: * @param callable(Node $node, Scope $scope): void $nodeCallback
5231: */
5232: private function processArrowFunctionNode(
5233: Node\Stmt $stmt,
5234: Expr\ArrowFunction $expr,
5235: MutatingScope $scope,
5236: ExpressionResultStorage $storage,
5237: callable $nodeCallback,
5238: ?Type $passedToType,
5239: ): ExpressionResult
5240: {
5241: foreach ($expr->params as $param) {
5242: $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
5243: }
5244: if ($expr->returnType !== null) {
5245: $this->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage);
5246: }
5247:
5248: $arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME);
5249: $arrowFunctionScope = $scope->enterArrowFunction($expr, $this->createCallableParameters(
5250: $scope,
5251: $expr,
5252: $arrowFunctionCallArgs,
5253: $passedToType,
5254: ));
5255: $arrowFunctionType = $arrowFunctionScope->getAnonymousFunctionReflection();
5256: if ($arrowFunctionType === null) {
5257: throw new ShouldNotHappenException();
5258: }
5259: $this->callNodeCallback($nodeCallback, new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope, $storage);
5260: $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $nodeCallback, ExpressionContext::createTopLevel());
5261:
5262: return new ExpressionResult($scope, false, $exprResult->isAlwaysTerminating(), $exprResult->getThrowPoints(), $exprResult->getImpurePoints());
5263: }
5264:
5265: /**
5266: * @param Node\Arg[] $args
5267: * @return ParameterReflection[]|null
5268: */
5269: public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array
5270: {
5271: $callableParameters = null;
5272: if ($args !== null) {
5273: $closureType = $scope->getType($closureExpr);
5274:
5275: if ($closureType->isCallable()->no()) {
5276: return null;
5277: }
5278:
5279: $acceptors = $closureType->getCallableParametersAcceptors($scope);
5280: if (count($acceptors) === 1) {
5281: $callableParameters = $acceptors[0]->getParameters();
5282:
5283: foreach ($callableParameters as $index => $callableParameter) {
5284: if (!isset($args[$index])) {
5285: continue;
5286: }
5287:
5288: $type = $scope->getType($args[$index]->value);
5289: $callableParameters[$index] = new NativeParameterReflection(
5290: $callableParameter->getName(),
5291: $callableParameter->isOptional(),
5292: $type,
5293: $callableParameter->passedByReference(),
5294: $callableParameter->isVariadic(),
5295: $callableParameter->getDefaultValue(),
5296: );
5297: }
5298: }
5299: } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) {
5300: if ($passedToType instanceof UnionType) {
5301: $passedToType = $passedToType->filterTypes(static fn (Type $innerType) => $innerType->isCallable()->yes());
5302:
5303: if ($passedToType->isCallable()->no()) {
5304: return null;
5305: }
5306: }
5307:
5308: $acceptors = $passedToType->getCallableParametersAcceptors($scope);
5309: foreach ($acceptors as $acceptor) {
5310: if ($callableParameters === null) {
5311: $callableParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection(
5312: $callableParameter->getName(),
5313: $callableParameter->isOptional(),
5314: $callableParameter->getType(),
5315: $callableParameter->passedByReference(),
5316: $callableParameter->isVariadic(),
5317: $callableParameter->getDefaultValue(),
5318: ), $acceptor->getParameters());
5319: continue;
5320: }
5321:
5322: $newParameters = [];
5323: foreach ($acceptor->getParameters() as $i => $callableParameter) {
5324: if (!array_key_exists($i, $callableParameters)) {
5325: $newParameters[] = $callableParameter;
5326: continue;
5327: }
5328:
5329: $newParameters[] = $callableParameters[$i]->union(new NativeParameterReflection(
5330: $callableParameter->getName(),
5331: $callableParameter->isOptional(),
5332: $callableParameter->getType(),
5333: $callableParameter->passedByReference(),
5334: $callableParameter->isVariadic(),
5335: $callableParameter->getDefaultValue(),
5336: ));
5337: }
5338:
5339: $callableParameters = $newParameters;
5340: }
5341: }
5342:
5343: return $callableParameters;
5344: }
5345:
5346: /**
5347: * @param callable(Node $node, Scope $scope): void $nodeCallback
5348: */
5349: private function processParamNode(
5350: Node\Stmt $stmt,
5351: Node\Param $param,
5352: MutatingScope $scope,
5353: ExpressionResultStorage $storage,
5354: callable $nodeCallback,
5355: ): void
5356: {
5357: $this->processAttributeGroups($stmt, $param->attrGroups, $scope, $storage, $nodeCallback);
5358: $this->callNodeCallback($nodeCallback, $param, $scope, $storage);
5359: if ($param->type !== null) {
5360: $this->callNodeCallback($nodeCallback, $param->type, $scope, $storage);
5361: }
5362: if ($param->default === null) {
5363: return;
5364: }
5365:
5366: $this->processExprNode($stmt, $param->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
5367: }
5368:
5369: /**
5370: * @param AttributeGroup[] $attrGroups
5371: * @param callable(Node $node, Scope $scope): void $nodeCallback
5372: */
5373: private function processAttributeGroups(
5374: Node\Stmt $stmt,
5375: array $attrGroups,
5376: MutatingScope $scope,
5377: ExpressionResultStorage $storage,
5378: callable $nodeCallback,
5379: ): void
5380: {
5381: foreach ($attrGroups as $attrGroup) {
5382: foreach ($attrGroup->attrs as $attr) {
5383: $className = $scope->resolveName($attr->name);
5384: if ($this->reflectionProvider->hasClass($className)) {
5385: $classReflection = $this->reflectionProvider->getClass($className);
5386: if ($classReflection->hasConstructor()) {
5387: $constructorReflection = $classReflection->getConstructor();
5388: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
5389: $scope,
5390: $attr->args,
5391: $constructorReflection->getVariants(),
5392: $constructorReflection->getNamedArgumentsVariants(),
5393: );
5394: $expr = new New_($attr->name, $attr->args);
5395: $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr;
5396: $this->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
5397: $this->callNodeCallback($nodeCallback, $attr, $scope, $storage);
5398: continue;
5399: }
5400: }
5401:
5402: foreach ($attr->args as $arg) {
5403: $this->processExprNode($stmt, $arg->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
5404: $this->callNodeCallback($nodeCallback, $arg, $scope, $storage);
5405: }
5406: $this->callNodeCallback($nodeCallback, $attr, $scope, $storage);
5407: }
5408: $this->callNodeCallback($nodeCallback, $attrGroup, $scope, $storage);
5409: }
5410: }
5411:
5412: /**
5413: * @param Node\PropertyHook[] $hooks
5414: * @param callable(Node $node, Scope $scope): void $nodeCallback
5415: */
5416: private function processPropertyHooks(
5417: Node\Stmt $stmt,
5418: Identifier|Name|ComplexType|null $nativeTypeNode,
5419: ?Type $phpDocType,
5420: string $propertyName,
5421: array $hooks,
5422: MutatingScope $scope,
5423: ExpressionResultStorage $storage,
5424: callable $nodeCallback,
5425: ): void
5426: {
5427: if (!$scope->isInClass()) {
5428: throw new ShouldNotHappenException();
5429: }
5430:
5431: $classReflection = $scope->getClassReflection();
5432:
5433: foreach ($hooks as $hook) {
5434: $this->callNodeCallback($nodeCallback, $hook, $scope, $storage);
5435: $this->processAttributeGroups($stmt, $hook->attrGroups, $scope, $storage, $nodeCallback);
5436:
5437: [, $phpDocParameterTypes,,,, $phpDocThrowType,,,,,,,, $phpDocComment,,,,,, $resolvedPhpDoc] = $this->getPhpDocs($scope, $hook);
5438:
5439: foreach ($hook->params as $param) {
5440: $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
5441: }
5442:
5443: [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $hook);
5444:
5445: $hookScope = $scope->enterPropertyHook(
5446: $hook,
5447: $propertyName,
5448: $nativeTypeNode,
5449: $phpDocType,
5450: $phpDocParameterTypes,
5451: $phpDocThrowType,
5452: $deprecatedDescription,
5453: $isDeprecated,
5454: $phpDocComment,
5455: $resolvedPhpDoc,
5456: );
5457: $hookReflection = $hookScope->getFunction();
5458: if (!$hookReflection instanceof PhpMethodFromParserNodeReflection) {
5459: throw new ShouldNotHappenException();
5460: }
5461:
5462: if (!$classReflection->hasNativeProperty($propertyName)) {
5463: throw new ShouldNotHappenException();
5464: }
5465:
5466: $propertyReflection = $classReflection->getNativeProperty($propertyName);
5467:
5468: $this->callNodeCallback($nodeCallback, new InPropertyHookNode(
5469: $classReflection,
5470: $hookReflection,
5471: $propertyReflection,
5472: $hook,
5473: ), $hookScope, $storage);
5474:
5475: $stmts = $hook->getStmts();
5476: if ($stmts === null) {
5477: return;
5478: }
5479:
5480: if ($hook->body instanceof Expr) {
5481: // enrich attributes of nodes in short hook body statements
5482: $traverser = new NodeTraverser(
5483: new LineAttributesVisitor($hook->body->getStartLine(), $hook->body->getEndLine()),
5484: );
5485: $traverser->traverse($stmts);
5486: }
5487:
5488: $gatheredReturnStatements = [];
5489: $executionEnds = [];
5490: $methodImpurePoints = [];
5491: $statementResult = $this->processStmtNodesInternal(new PropertyHookStatementNode($hook), $stmts, $hookScope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $hookScope, &$gatheredReturnStatements, &$executionEnds, &$hookImpurePoints): void {
5492: $nodeCallback($node, $scope);
5493: if ($scope->getFunction() !== $hookScope->getFunction()) {
5494: return;
5495: }
5496: if ($scope->isInAnonymousFunction()) {
5497: return;
5498: }
5499: if ($node instanceof PropertyAssignNode) {
5500: $hookImpurePoints[] = new ImpurePoint(
5501: $scope,
5502: $node,
5503: 'propertyAssign',
5504: 'property assignment',
5505: true,
5506: );
5507: return;
5508: }
5509: if ($node instanceof ExecutionEndNode) {
5510: $executionEnds[] = $node;
5511: return;
5512: }
5513: if (!$node instanceof Return_) {
5514: return;
5515: }
5516:
5517: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
5518: }, StatementContext::createTopLevel())->toPublic();
5519:
5520: $this->callNodeCallback($nodeCallback, new PropertyHookReturnStatementsNode(
5521: $hook,
5522: $gatheredReturnStatements,
5523: $statementResult,
5524: $executionEnds,
5525: array_merge($statementResult->getImpurePoints(), $methodImpurePoints),
5526: $classReflection,
5527: $hookReflection,
5528: $propertyReflection,
5529: ), $hookScope, $storage);
5530: }
5531: }
5532:
5533: /**
5534: * @param FunctionReflection|MethodReflection|null $calleeReflection
5535: */
5536: private function resolveClosureThisType(
5537: ?CallLike $call,
5538: $calleeReflection,
5539: ParameterReflection $parameter,
5540: MutatingScope $scope,
5541: ): ?Type
5542: {
5543: if ($call instanceof FuncCall && $calleeReflection instanceof FunctionReflection) {
5544: foreach ($this->parameterClosureThisExtensionProvider->getFunctionParameterClosureThisExtensions() as $extension) {
5545: if (! $extension->isFunctionSupported($calleeReflection, $parameter)) {
5546: continue;
5547: }
5548: $type = $extension->getClosureThisTypeFromFunctionCall($calleeReflection, $call, $parameter, $scope);
5549: if ($type !== null) {
5550: return $type;
5551: }
5552: }
5553: } elseif ($call instanceof StaticCall && $calleeReflection instanceof MethodReflection) {
5554: foreach ($this->parameterClosureThisExtensionProvider->getStaticMethodParameterClosureThisExtensions() as $extension) {
5555: if (! $extension->isStaticMethodSupported($calleeReflection, $parameter)) {
5556: continue;
5557: }
5558: $type = $extension->getClosureThisTypeFromStaticMethodCall($calleeReflection, $call, $parameter, $scope);
5559: if ($type !== null) {
5560: return $type;
5561: }
5562: }
5563: } elseif ($call instanceof MethodCall && $calleeReflection instanceof MethodReflection) {
5564: foreach ($this->parameterClosureThisExtensionProvider->getMethodParameterClosureThisExtensions() as $extension) {
5565: if (! $extension->isMethodSupported($calleeReflection, $parameter)) {
5566: continue;
5567: }
5568: $type = $extension->getClosureThisTypeFromMethodCall($calleeReflection, $call, $parameter, $scope);
5569: if ($type !== null) {
5570: return $type;
5571: }
5572: }
5573: }
5574:
5575: if ($parameter instanceof ExtendedParameterReflection) {
5576: return $parameter->getClosureThisType();
5577: }
5578:
5579: return null;
5580: }
5581:
5582: /**
5583: * @param MethodReflection|FunctionReflection|null $calleeReflection
5584: * @param callable(Node $node, Scope $scope): void $nodeCallback
5585: */
5586: private function processArgs(
5587: Node\Stmt $stmt,
5588: $calleeReflection,
5589: ?ExtendedMethodReflection $nakedMethodReflection,
5590: ?ParametersAcceptor $parametersAcceptor,
5591: CallLike $callLike,
5592: MutatingScope $scope,
5593: ExpressionResultStorage $storage,
5594: callable $nodeCallback,
5595: ExpressionContext $context,
5596: ?MutatingScope $closureBindScope = null,
5597: ): ExpressionResult
5598: {
5599: $args = $callLike->getArgs();
5600:
5601: $parameters = null;
5602: if ($parametersAcceptor !== null) {
5603: $parameters = $parametersAcceptor->getParameters();
5604: }
5605:
5606: $hasYield = false;
5607: $throwPoints = [];
5608: $impurePoints = [];
5609: $isAlwaysTerminating = false;
5610: /** @var list<array{InvalidateExprNode[], string[]}> $deferredInvalidateExpressions */
5611: $deferredInvalidateExpressions = [];
5612: /** @var ProcessClosureResult[] $deferredByRefClosureResults */
5613: $deferredByRefClosureResults = [];
5614: foreach ($args as $i => $arg) {
5615: $assignByReference = false;
5616: $parameter = null;
5617: $parameterType = null;
5618: $parameterNativeType = null;
5619: if ($parameters !== null) {
5620: $matchedParameter = null;
5621: if ($arg->name !== null) {
5622: foreach ($parameters as $p) {
5623: if ($p->getName() === $arg->name->toString()) {
5624: $matchedParameter = $p;
5625: break;
5626: }
5627: }
5628: } elseif (isset($parameters[$i])) {
5629: $matchedParameter = $parameters[$i];
5630: }
5631:
5632: if ($matchedParameter !== null) {
5633: $assignByReference = $matchedParameter->passedByReference()->createsNewVariable();
5634: $parameterType = $matchedParameter->getType();
5635:
5636: if ($matchedParameter instanceof ExtendedParameterReflection) {
5637: $parameterNativeType = $matchedParameter->getNativeType();
5638: }
5639: $parameter = $matchedParameter;
5640: } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) {
5641: $lastParameter = array_last($parameters);
5642: $assignByReference = $lastParameter->passedByReference()->createsNewVariable();
5643: $parameterType = $lastParameter->getType();
5644:
5645: if ($lastParameter instanceof ExtendedParameterReflection) {
5646: $parameterNativeType = $lastParameter->getNativeType();
5647: }
5648: $parameter = $lastParameter;
5649: }
5650: }
5651:
5652: $lookForUnset = false;
5653: if ($assignByReference) {
5654: $isBuiltin = false;
5655: if ($calleeReflection instanceof FunctionReflection && $calleeReflection->isBuiltin()) {
5656: $isBuiltin = true;
5657: } elseif ($calleeReflection instanceof ExtendedMethodReflection && $calleeReflection->getDeclaringClass()->isBuiltin()) {
5658: $isBuiltin = true;
5659: }
5660: if (
5661: $isBuiltin
5662: || ($parameterNativeType === null || !$parameterNativeType->isNull()->no())
5663: ) {
5664: $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $arg->value);
5665: $lookForUnset = true;
5666: }
5667: }
5668:
5669: $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg;
5670: if ($calleeReflection !== null) {
5671: $rememberTypes = !$originalArg->value instanceof Expr\Closure && !$originalArg->value instanceof Expr\ArrowFunction;
5672: $scope = $scope->pushInFunctionCall($calleeReflection, $parameter, $rememberTypes);
5673: }
5674:
5675: $this->callNodeCallback($nodeCallback, $originalArg, $scope, $storage);
5676:
5677: $originalScope = $scope;
5678: $scopeToPass = $scope;
5679: if ($i === 0 && $closureBindScope !== null && ($arg->value instanceof Expr\Closure || $arg->value instanceof Expr\ArrowFunction)) {
5680: $scopeToPass = $closureBindScope;
5681: }
5682:
5683: if ($arg->value instanceof Expr\Closure) {
5684: $restoreThisScope = null;
5685: if (
5686: $closureBindScope === null
5687: && $parameter instanceof ExtendedParameterReflection
5688: && !$arg->value->static
5689: ) {
5690: $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass);
5691: if ($closureThisType !== null) {
5692: $restoreThisScope = $scopeToPass;
5693: $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes());
5694: }
5695: }
5696:
5697: if ($parameter !== null) {
5698: $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass);
5699:
5700: if ($overwritingParameterType !== null) {
5701: $parameterType = $overwritingParameterType;
5702: }
5703: }
5704:
5705: $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context);
5706: $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType ?? null);
5707: if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
5708: $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()));
5709: $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints());
5710: $isAlwaysTerminating = $isAlwaysTerminating || $closureResult->isAlwaysTerminating();
5711: }
5712:
5713: $this->storeBeforeScope($storage, $arg->value, $scopeToPass);
5714:
5715: $uses = [];
5716: foreach ($arg->value->uses as $use) {
5717: if (!is_string($use->var->name)) {
5718: continue;
5719: }
5720:
5721: $uses[] = $use->var->name;
5722: }
5723:
5724: $scope = $closureResult->getScope();
5725: $deferredByRefClosureResults[] = $closureResult;
5726: $invalidateExpressions = $closureResult->getInvalidateExpressions();
5727: if ($restoreThisScope !== null) {
5728: $nodeFinder = new NodeFinder();
5729: $cb = static fn ($expr) => $expr instanceof Variable && $expr->name === 'this';
5730: foreach ($invalidateExpressions as $j => $invalidateExprNode) {
5731: $foundThis = $nodeFinder->findFirst([$invalidateExprNode->getExpr()], $cb);
5732: if ($foundThis === null) {
5733: continue;
5734: }
5735:
5736: unset($invalidateExpressions[$j]);
5737: }
5738: $invalidateExpressions = array_values($invalidateExpressions);
5739: $scope = $scope->restoreThis($restoreThisScope);
5740: }
5741:
5742: $deferredInvalidateExpressions[] = [$invalidateExpressions, $uses];
5743: } elseif ($arg->value instanceof Expr\ArrowFunction) {
5744: if (
5745: $closureBindScope === null
5746: && $parameter instanceof ExtendedParameterReflection
5747: && !$arg->value->static
5748: ) {
5749: $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass);
5750: if ($closureThisType !== null) {
5751: $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes());
5752: }
5753: }
5754:
5755: if ($parameter !== null) {
5756: $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass);
5757:
5758: if ($overwritingParameterType !== null) {
5759: $parameterType = $overwritingParameterType;
5760: }
5761: }
5762:
5763: $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context);
5764: $arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType ?? null);
5765: if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
5766: $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()));
5767: $impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints());
5768: $isAlwaysTerminating = $isAlwaysTerminating || $arrowFunctionResult->isAlwaysTerminating();
5769: }
5770: $this->storeBeforeScope($storage, $arg->value, $scopeToPass);
5771: } else {
5772: $exprType = $scope->getType($arg->value);
5773: $exprResult = $this->processExprNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context->enterDeep());
5774: $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
5775: $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints());
5776: $isAlwaysTerminating = $isAlwaysTerminating || $exprResult->isAlwaysTerminating();
5777: $scope = $exprResult->getScope();
5778: $hasYield = $hasYield || $exprResult->hasYield();
5779:
5780: if ($exprType->isCallable()->yes()) {
5781: $acceptors = $exprType->getCallableParametersAcceptors($scope);
5782: if (count($acceptors) === 1) {
5783: $deferredInvalidateExpressions[] = [$acceptors[0]->getInvalidateExpressions(), $acceptors[0]->getUsedVariables()];
5784: if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
5785: $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());
5786: if (!$this->implicitThrows) {
5787: $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit()));
5788: }
5789: $throwPoints = array_merge($throwPoints, $callableThrowPoints);
5790: $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $arg->value, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $acceptors[0]->getImpurePoints()));
5791: $returnType = $acceptors[0]->getReturnType();
5792: $isAlwaysTerminating = $isAlwaysTerminating || ($returnType instanceof NeverType && $returnType->isExplicit());
5793: }
5794: }
5795: }
5796: }
5797:
5798: if ($assignByReference && $lookForUnset) {
5799: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value);
5800: }
5801:
5802: if ($calleeReflection !== null) {
5803: $scope = $scope->popInFunctionCall();
5804: }
5805:
5806: if ($i !== 0 || $closureBindScope === null) {
5807: continue;
5808: }
5809:
5810: $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope);
5811: }
5812:
5813: foreach ($deferredInvalidateExpressions as [$invalidateExpressions, $uses]) {
5814: $scope = $this->processImmediatelyCalledCallable($scope, $invalidateExpressions, $uses);
5815: }
5816:
5817: foreach ($deferredByRefClosureResults as $deferredClosureResult) {
5818: $scope = $deferredClosureResult->applyByRefUseScope($scope);
5819: }
5820:
5821: if ($parameters !== null) {
5822: foreach ($args as $i => $arg) {
5823: $assignByReference = false;
5824: $currentParameter = null;
5825: if (isset($parameters[$i])) {
5826: $currentParameter = $parameters[$i];
5827: } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) {
5828: $currentParameter = array_last($parameters);
5829: }
5830:
5831: if ($currentParameter !== null) {
5832: $assignByReference = $currentParameter->passedByReference()->createsNewVariable();
5833: }
5834:
5835: if ($assignByReference) {
5836: if ($currentParameter === null) {
5837: throw new ShouldNotHappenException();
5838: }
5839:
5840: $argValue = $arg->value;
5841: if (!$argValue instanceof Variable || $argValue->name !== 'this') {
5842: $paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope);
5843: if ($paramOutType !== null) {
5844: $byRefType = $paramOutType;
5845: } elseif (
5846: $currentParameter instanceof ExtendedParameterReflection
5847: && $currentParameter->getOutType() !== null
5848: ) {
5849: $byRefType = $currentParameter->getOutType();
5850: } elseif (
5851: $calleeReflection instanceof MethodReflection
5852: && !$calleeReflection->getDeclaringClass()->isBuiltin()
5853: ) {
5854: $byRefType = $currentParameter->getType();
5855: } elseif (
5856: $calleeReflection instanceof FunctionReflection
5857: && !$calleeReflection->isBuiltin()
5858: ) {
5859: $byRefType = $currentParameter->getType();
5860: } else {
5861: $byRefType = new MixedType();
5862: }
5863:
5864: $scope = $this->processVirtualAssign(
5865: $scope,
5866: $storage,
5867: $stmt,
5868: $argValue,
5869: new TypeExpr($byRefType),
5870: $nodeCallback,
5871: )->getScope();
5872: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $argValue);
5873: }
5874: } elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) {
5875: $argType = $scope->getType($arg->value);
5876: if (!$argType->isObject()->no()) {
5877: $nakedReturnType = null;
5878: if ($nakedMethodReflection !== null) {
5879: $nakedParametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
5880: $scope,
5881: $args,
5882: $nakedMethodReflection->getVariants(),
5883: $nakedMethodReflection->getNamedArgumentsVariants(),
5884: );
5885: $nakedReturnType = $nakedParametersAcceptor->getReturnType();
5886: }
5887: if (
5888: $nakedReturnType === null
5889: || !(new ThisType($nakedMethodReflection->getDeclaringClass()))->isSuperTypeOf($nakedReturnType)->yes()
5890: || $nakedMethodReflection->isPure()->no()
5891: ) {
5892: $this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage);
5893: $scope = $scope->invalidateExpression($arg->value, true);
5894: }
5895: } elseif (!(new ResourceType())->isSuperTypeOf($argType)->no()) {
5896: $this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage);
5897: $scope = $scope->invalidateExpression($arg->value, true);
5898: }
5899: }
5900: }
5901: }
5902:
5903: // not storing this, it's scope after processing all args
5904: return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints);
5905: }
5906:
5907: /**
5908: * @param MethodReflection|FunctionReflection|null $calleeReflection
5909: */
5910: private function callCallbackImmediately(?ParameterReflection $parameter, ?Type $parameterType, $calleeReflection): bool
5911: {
5912: $parameterCallableType = null;
5913: if ($parameterType !== null && $calleeReflection instanceof FunctionReflection) {
5914: $parameterCallableType = TypeUtils::findCallableType($parameterType);
5915: }
5916:
5917: if ($parameter instanceof ExtendedParameterReflection) {
5918: $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable();
5919: if ($parameterCallImmediately->maybe()) {
5920: $callCallbackImmediately = $parameterCallableType !== null;
5921: } else {
5922: $callCallbackImmediately = $parameterCallImmediately->yes();
5923: }
5924: } else {
5925: $callCallbackImmediately = $parameterCallableType !== null;
5926: }
5927:
5928: return $callCallbackImmediately;
5929: }
5930:
5931: /**
5932: * @param MethodReflection|FunctionReflection|null $calleeReflection
5933: */
5934: private function getParameterTypeFromParameterClosureTypeExtension(CallLike $callLike, $calleeReflection, ParameterReflection $parameter, MutatingScope $scope): ?Type
5935: {
5936: if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) {
5937: foreach ($this->parameterClosureTypeExtensionProvider->getFunctionParameterClosureTypeExtensions() as $functionParameterClosureTypeExtension) {
5938: if ($functionParameterClosureTypeExtension->isFunctionSupported($calleeReflection, $parameter)) {
5939: return $functionParameterClosureTypeExtension->getTypeFromFunctionCall($calleeReflection, $callLike, $parameter, $scope);
5940: }
5941: }
5942: } elseif ($calleeReflection instanceof MethodReflection) {
5943: if ($callLike instanceof StaticCall) {
5944: foreach ($this->parameterClosureTypeExtensionProvider->getStaticMethodParameterClosureTypeExtensions() as $staticMethodParameterClosureTypeExtension) {
5945: if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) {
5946: return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $callLike, $parameter, $scope);
5947: }
5948: }
5949: } elseif ($callLike instanceof New_ && $callLike->class instanceof Name) {
5950: $staticCall = new StaticCall(
5951: $callLike->class,
5952: new Identifier('__construct'),
5953: $callLike->getArgs(),
5954: );
5955: foreach ($this->parameterClosureTypeExtensionProvider->getStaticMethodParameterClosureTypeExtensions() as $staticMethodParameterClosureTypeExtension) {
5956: if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) {
5957: return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $staticCall, $parameter, $scope);
5958: }
5959: }
5960: } elseif ($callLike instanceof MethodCall) {
5961: foreach ($this->parameterClosureTypeExtensionProvider->getMethodParameterClosureTypeExtensions() as $methodParameterClosureTypeExtension) {
5962: if ($methodParameterClosureTypeExtension->isMethodSupported($calleeReflection, $parameter)) {
5963: return $methodParameterClosureTypeExtension->getTypeFromMethodCall($calleeReflection, $callLike, $parameter, $scope);
5964: }
5965: }
5966: }
5967: }
5968:
5969: return null;
5970: }
5971:
5972: /**
5973: * @param MethodReflection|FunctionReflection|null $calleeReflection
5974: */
5975: private function getParameterOutExtensionsType(CallLike $callLike, $calleeReflection, ParameterReflection $currentParameter, MutatingScope $scope): ?Type
5976: {
5977: $paramOutTypes = [];
5978: if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) {
5979: foreach ($this->parameterOutTypeExtensionProvider->getFunctionParameterOutTypeExtensions() as $functionParameterOutTypeExtension) {
5980: if (!$functionParameterOutTypeExtension->isFunctionSupported($calleeReflection, $currentParameter)) {
5981: continue;
5982: }
5983:
5984: $resolvedType = $functionParameterOutTypeExtension->getParameterOutTypeFromFunctionCall($calleeReflection, $callLike, $currentParameter, $scope);
5985: if ($resolvedType === null) {
5986: continue;
5987: }
5988: $paramOutTypes[] = $resolvedType;
5989: }
5990: } elseif ($callLike instanceof MethodCall && $calleeReflection instanceof MethodReflection) {
5991: foreach ($this->parameterOutTypeExtensionProvider->getMethodParameterOutTypeExtensions() as $methodParameterOutTypeExtension) {
5992: if (!$methodParameterOutTypeExtension->isMethodSupported($calleeReflection, $currentParameter)) {
5993: continue;
5994: }
5995:
5996: $resolvedType = $methodParameterOutTypeExtension->getParameterOutTypeFromMethodCall($calleeReflection, $callLike, $currentParameter, $scope);
5997: if ($resolvedType === null) {
5998: continue;
5999: }
6000: $paramOutTypes[] = $resolvedType;
6001: }
6002: } elseif ($callLike instanceof StaticCall && $calleeReflection instanceof MethodReflection) {
6003: foreach ($this->parameterOutTypeExtensionProvider->getStaticMethodParameterOutTypeExtensions() as $staticMethodParameterOutTypeExtension) {
6004: if (!$staticMethodParameterOutTypeExtension->isStaticMethodSupported($calleeReflection, $currentParameter)) {
6005: continue;
6006: }
6007:
6008: $resolvedType = $staticMethodParameterOutTypeExtension->getParameterOutTypeFromStaticMethodCall($calleeReflection, $callLike, $currentParameter, $scope);
6009: if ($resolvedType === null) {
6010: continue;
6011: }
6012: $paramOutTypes[] = $resolvedType;
6013: }
6014: }
6015:
6016: if (count($paramOutTypes) === 1) {
6017: return $paramOutTypes[0];
6018: }
6019:
6020: if (count($paramOutTypes) > 1) {
6021: return TypeCombinator::union(...$paramOutTypes);
6022: }
6023:
6024: return null;
6025: }
6026:
6027: /**
6028: * @param callable(Node $node, Scope $scope): void $nodeCallback
6029: * @param Closure(MutatingScope $scope): ExpressionResult $processExprCallback
6030: */
6031: private function processAssignVar(
6032: MutatingScope $scope,
6033: ExpressionResultStorage $storage,
6034: Node\Stmt $stmt,
6035: Expr $var,
6036: Expr $assignedExpr,
6037: callable $nodeCallback,
6038: ExpressionContext $context,
6039: Closure $processExprCallback,
6040: bool $enterExpressionAssign,
6041: ): ExpressionResult
6042: {
6043: $this->callNodeCallback($nodeCallback, $var, $enterExpressionAssign ? $scope->enterExpressionAssign($var) : $scope, $storage);
6044: $hasYield = false;
6045: $throwPoints = [];
6046: $impurePoints = [];
6047: $isAlwaysTerminating = false;
6048: $isAssignOp = $assignedExpr instanceof Expr\AssignOp && !$enterExpressionAssign;
6049: if ($var instanceof Variable) {
6050: $this->storeBeforeScope($storage, $var, $scope);
6051: $result = $processExprCallback($scope);
6052: $hasYield = $result->hasYield();
6053: $throwPoints = $result->getThrowPoints();
6054: $impurePoints = $result->getImpurePoints();
6055: $isAlwaysTerminating = $result->isAlwaysTerminating();
6056: $scopeBeforeAssignEval = $scope;
6057: $scope = $result->getScope();
6058: if (is_string($var->name)) {
6059: if (in_array($var->name, Scope::SUPERGLOBAL_VARIABLES, true)) {
6060: $impurePoints[] = new ImpurePoint($scopeBeforeAssignEval, $var, 'superglobal', 'assign to superglobal variable', true);
6061: }
6062: $assignedExpr = $this->unwrapAssign($assignedExpr);
6063: $type = $scopeBeforeAssignEval->getType($assignedExpr);
6064:
6065: $conditionalExpressions = [];
6066: if ($assignedExpr instanceof Ternary) {
6067: $if = $assignedExpr->if;
6068: if ($if === null) {
6069: $if = $assignedExpr->cond;
6070: }
6071: $condScope = $this->processExprNode($stmt, $assignedExpr->cond, $scope, $storage->duplicate(), new NoopNodeCallback(), ExpressionContext::createDeep())->getScope();
6072: $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($condScope, $assignedExpr->cond, TypeSpecifierContext::createTruthy());
6073: $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($condScope, $assignedExpr->cond, TypeSpecifierContext::createFalsey());
6074: $truthyScope = $condScope->filterBySpecifiedTypes($truthySpecifiedTypes);
6075: $falsyScope = $condScope->filterBySpecifiedTypes($falseySpecifiedTypes);
6076: $truthyType = $truthyScope->getType($if);
6077: $falseyType = $falsyScope->getType($assignedExpr->else);
6078:
6079: if (
6080: $truthyType->isSuperTypeOf($falseyType)->no()
6081: && $falseyType->isSuperTypeOf($truthyType)->no()
6082: ) {
6083: $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType);
6084: $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType);
6085: $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType);
6086: $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType);
6087: }
6088: }
6089:
6090: $truthyType = TypeCombinator::removeFalsey($type);
6091: if ($truthyType !== $type) {
6092: $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy());
6093: $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType);
6094: $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType);
6095:
6096: $falseyType = TypeCombinator::intersect($type, StaticTypeFactory::falsey());
6097: $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createFalsey());
6098: $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType);
6099: $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType);
6100: }
6101:
6102: $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedExpr), $scopeBeforeAssignEval, $storage);
6103: $scope = $scope->assignVariable($var->name, $type, $scope->getNativeType($assignedExpr), TrinaryLogic::createYes());
6104: foreach ($conditionalExpressions as $exprString => $holders) {
6105: $scope = $scope->addConditionalExpressions($exprString, $holders);
6106: }
6107: } else {
6108: $nameExprResult = $this->processExprNode($stmt, $var->name, $scope, $storage, $nodeCallback, $context);
6109: $hasYield = $hasYield || $nameExprResult->hasYield();
6110: $throwPoints = array_merge($throwPoints, $nameExprResult->getThrowPoints());
6111: $impurePoints = array_merge($impurePoints, $nameExprResult->getImpurePoints());
6112: $isAlwaysTerminating = $isAlwaysTerminating || $nameExprResult->isAlwaysTerminating();
6113: $scope = $nameExprResult->getScope();
6114: }
6115: } elseif ($var instanceof ArrayDimFetch) {
6116: $dimFetchStack = [];
6117: $originalVar = $var;
6118: $assignedPropertyExpr = $assignedExpr;
6119: while ($var instanceof ArrayDimFetch) {
6120: $varForSetOffsetValue = $var->var;
6121: if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) {
6122: $varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue);
6123: }
6124:
6125: if (
6126: $var === $originalVar
6127: && $var->dim !== null
6128: && $scope->hasExpressionType($var)->yes()
6129: ) {
6130: $assignedPropertyExpr = new SetExistingOffsetValueTypeExpr(
6131: $varForSetOffsetValue,
6132: $var->dim,
6133: $assignedPropertyExpr,
6134: );
6135: } else {
6136: $assignedPropertyExpr = new SetOffsetValueTypeExpr(
6137: $varForSetOffsetValue,
6138: $var->dim,
6139: $assignedPropertyExpr,
6140: );
6141: }
6142: $dimFetchStack[] = $var;
6143: $var = $var->var;
6144: }
6145:
6146: // 1. eval root expr
6147: if ($enterExpressionAssign) {
6148: $scope = $scope->enterExpressionAssign($var);
6149: }
6150: $result = $this->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, $context->enterDeep());
6151: $hasYield = $result->hasYield();
6152: $throwPoints = $result->getThrowPoints();
6153: $impurePoints = $result->getImpurePoints();
6154: $isAlwaysTerminating = $result->isAlwaysTerminating();
6155: $scope = $result->getScope();
6156: if ($enterExpressionAssign) {
6157: $scope = $scope->exitExpressionAssign($var);
6158: }
6159:
6160: // 2. eval dimensions
6161: $offsetTypes = [];
6162: $offsetNativeTypes = [];
6163: $dimFetchStack = array_reverse($dimFetchStack);
6164: $lastDimKey = array_key_last($dimFetchStack);
6165: foreach ($dimFetchStack as $key => $dimFetch) {
6166: $dimExpr = $dimFetch->dim;
6167:
6168: // Callback was already called for last dim at the beginning of the method.
6169: if ($key !== $lastDimKey) {
6170: $this->callNodeCallback($nodeCallback, $dimFetch, $enterExpressionAssign ? $scope->enterExpressionAssign($dimFetch) : $scope, $storage);
6171: }
6172:
6173: if ($dimExpr === null) {
6174: $offsetTypes[] = [null, $dimFetch];
6175: $offsetNativeTypes[] = [null, $dimFetch];
6176: $this->storeBeforeScope($storage, $dimFetch, $scope);
6177:
6178: } else {
6179: $offsetTypes[] = [$scope->getType($dimExpr), $dimFetch];
6180: $offsetNativeTypes[] = [$scope->getNativeType($dimExpr), $dimFetch];
6181:
6182: if ($enterExpressionAssign) {
6183: $scope->enterExpressionAssign($dimExpr);
6184: }
6185: $this->storeBeforeScope($storage, $dimFetch, $scope);
6186: $result = $this->processExprNode($stmt, $dimExpr, $scope, $storage, $nodeCallback, $context->enterDeep());
6187: $hasYield = $hasYield || $result->hasYield();
6188: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
6189: $scope = $result->getScope();
6190:
6191: if ($enterExpressionAssign) {
6192: $scope = $scope->exitExpressionAssign($dimExpr);
6193: }
6194: }
6195: }
6196:
6197: $valueToWrite = $scope->getType($assignedExpr);
6198: $nativeValueToWrite = $scope->getNativeType($assignedExpr);
6199: $scopeBeforeAssignEval = $scope;
6200:
6201: // 3. eval assigned expr
6202: $result = $processExprCallback($scope);
6203: $hasYield = $hasYield || $result->hasYield();
6204: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
6205: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
6206: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
6207: $scope = $result->getScope();
6208:
6209: $varType = $scope->getType($var);
6210: $varNativeType = $scope->getNativeType($var);
6211:
6212: // 4. compose types
6213: $isImplicitArrayCreation = $this->isImplicitArrayCreation($dimFetchStack, $scope);
6214: if ($isImplicitArrayCreation->yes()) {
6215: $varType = new ConstantArrayType([], []);
6216: $varNativeType = new ConstantArrayType([], []);
6217: }
6218: $offsetValueType = $varType;
6219: $offsetNativeValueType = $varNativeType;
6220:
6221: [$valueToWrite, $additionalExpressions] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope);
6222:
6223: if (!$offsetValueType->equals($offsetNativeValueType) || !$valueToWrite->equals($nativeValueToWrite)) {
6224: [$nativeValueToWrite, $additionalNativeExpressions] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope);
6225: } else {
6226: $rewritten = false;
6227: foreach ($offsetTypes as $i => [$offsetType]) {
6228: [$offsetNativeType] = $offsetNativeTypes[$i];
6229:
6230: if ($offsetType === null) {
6231: if ($offsetNativeType !== null) {
6232: throw new ShouldNotHappenException();
6233: }
6234:
6235: continue;
6236: } elseif ($offsetNativeType === null) {
6237: throw new ShouldNotHappenException();
6238: }
6239: if ($offsetType->equals($offsetNativeType)) {
6240: continue;
6241: }
6242:
6243: [$nativeValueToWrite] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope);
6244: $rewritten = true;
6245: break;
6246: }
6247:
6248: if (!$rewritten) {
6249: $nativeValueToWrite = $valueToWrite;
6250: }
6251: }
6252:
6253: if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) {
6254: if ($var instanceof Variable && is_string($var->name)) {
6255: $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scopeBeforeAssignEval, $storage);
6256: $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes());
6257: } else {
6258: if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) {
6259: $this->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval, $storage);
6260: if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) {
6261: $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString());
6262: }
6263: }
6264: $scope = $scope->assignExpression(
6265: $var,
6266: $valueToWrite,
6267: $nativeValueToWrite,
6268: );
6269: }
6270: } else {
6271: if ($var instanceof Variable) {
6272: $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scopeBeforeAssignEval, $storage);
6273: } elseif ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) {
6274: $this->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scopeBeforeAssignEval, $storage);
6275: if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) {
6276: $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString());
6277: }
6278: }
6279: }
6280:
6281: foreach ($additionalExpressions as $k => $additionalExpression) {
6282: [$expr, $type] = $additionalExpression;
6283: $nativeType = $type;
6284: if (isset($additionalNativeExpressions[$k])) {
6285: [, $nativeType] = $additionalNativeExpressions[$k];
6286: }
6287:
6288: $scope = $scope->assignExpression($expr, $type, $nativeType);
6289: }
6290:
6291: if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) {
6292: $throwPoints = array_merge($throwPoints, $this->processExprNode(
6293: $stmt,
6294: new MethodCall($var, 'offsetSet'),
6295: $scope,
6296: $storage,
6297: new NoopNodeCallback(),
6298: $context,
6299: )->getThrowPoints());
6300: }
6301: } elseif ($var instanceof PropertyFetch) {
6302: $this->storeBeforeScope($storage, $var, $scope);
6303: $objectResult = $this->processExprNode($stmt, $var->var, $scope, $storage, $nodeCallback, $context);
6304: $hasYield = $objectResult->hasYield();
6305: $throwPoints = $objectResult->getThrowPoints();
6306: $impurePoints = $objectResult->getImpurePoints();
6307: $isAlwaysTerminating = $objectResult->isAlwaysTerminating();
6308: $scope = $objectResult->getScope();
6309:
6310: $propertyName = null;
6311: if ($var->name instanceof Node\Identifier) {
6312: $propertyName = $var->name->name;
6313: } else {
6314: $propertyNameResult = $this->processExprNode($stmt, $var->name, $scope, $storage, $nodeCallback, $context);
6315: $hasYield = $hasYield || $propertyNameResult->hasYield();
6316: $throwPoints = array_merge($throwPoints, $propertyNameResult->getThrowPoints());
6317: $impurePoints = array_merge($impurePoints, $propertyNameResult->getImpurePoints());
6318: $isAlwaysTerminating = $isAlwaysTerminating || $propertyNameResult->isAlwaysTerminating();
6319: $scope = $propertyNameResult->getScope();
6320: }
6321:
6322: $scopeBeforeAssignEval = $scope;
6323: $result = $processExprCallback($scope);
6324: $hasYield = $hasYield || $result->hasYield();
6325: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
6326: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
6327: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
6328: $scope = $result->getScope();
6329:
6330: if ($var->name instanceof Expr && $this->phpVersion->supportsPropertyHooks()) {
6331: $throwPoints[] = InternalThrowPoint::createImplicit($scope, $var);
6332: }
6333:
6334: $propertyHolderType = $scope->getType($var->var);
6335: if ($propertyName !== null && $propertyHolderType->hasInstanceProperty($propertyName)->yes()) {
6336: $propertyReflection = $propertyHolderType->getInstanceProperty($propertyName, $scope);
6337: $assignedExprType = $scope->getType($assignedExpr);
6338: $this->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage);
6339: if ($propertyReflection->canChangeTypeAfterAssignment()) {
6340: if ($propertyReflection->hasNativeType()) {
6341: $propertyNativeType = $propertyReflection->getNativeType();
6342:
6343: $assignedTypeIsCompatible = $propertyNativeType->isSuperTypeOf($assignedExprType)->yes();
6344: if (!$assignedTypeIsCompatible) {
6345: foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) {
6346: if ($type->isSuperTypeOf($assignedExprType)->yes()) {
6347: $assignedTypeIsCompatible = true;
6348: break;
6349: }
6350: }
6351: }
6352:
6353: if ($assignedTypeIsCompatible) {
6354: $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
6355: } else {
6356: $scope = $scope->assignExpression(
6357: $var,
6358: TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType),
6359: TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType),
6360: );
6361: }
6362: } else {
6363: $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
6364: }
6365: }
6366: $declaringClass = $propertyReflection->getDeclaringClass();
6367: if ($declaringClass->hasNativeProperty($propertyName)) {
6368: $nativeProperty = $declaringClass->getNativeProperty($propertyName);
6369: if (
6370: !$nativeProperty->getNativeType()->accepts($assignedExprType, true)->yes()
6371: ) {
6372: $throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(TypeError::class), $assignedExpr, false);
6373: }
6374: if ($this->phpVersion->supportsPropertyHooks()) {
6375: $throwPoints = array_merge($throwPoints, $this->getPropertyAssignThrowPointsFromSetHook($scope, $var, $nativeProperty));
6376: }
6377: if ($enterExpressionAssign) {
6378: $scope = $scope->assignInitializedProperty($propertyHolderType, $propertyName);
6379: }
6380: }
6381: } else {
6382: // fallback
6383: $assignedExprType = $scope->getType($assignedExpr);
6384: $this->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage);
6385: $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
6386: // simulate dynamic property assign by __set to get throw points
6387: if (!$propertyHolderType->hasMethod('__set')->no()) {
6388: $throwPoints = array_merge($throwPoints, $this->processExprNode(
6389: $stmt,
6390: new MethodCall($var->var, '__set'),
6391: $scope,
6392: $storage,
6393: new NoopNodeCallback(),
6394: $context,
6395: )->getThrowPoints());
6396: }
6397: }
6398:
6399: } elseif ($var instanceof Expr\StaticPropertyFetch) {
6400: $this->storeBeforeScope($storage, $var, $scope);
6401: if ($var->class instanceof Node\Name) {
6402: $propertyHolderType = $scope->resolveTypeByName($var->class);
6403: } else {
6404: $this->processExprNode($stmt, $var->class, $scope, $storage, $nodeCallback, $context);
6405: $propertyHolderType = $scope->getType($var->class);
6406: }
6407:
6408: $propertyName = null;
6409: if ($var->name instanceof Node\Identifier) {
6410: $propertyName = $var->name->name;
6411: } else {
6412: $propertyNameResult = $this->processExprNode($stmt, $var->name, $scope, $storage, $nodeCallback, $context);
6413: $hasYield = $propertyNameResult->hasYield();
6414: $throwPoints = $propertyNameResult->getThrowPoints();
6415: $impurePoints = $propertyNameResult->getImpurePoints();
6416: $isAlwaysTerminating = $propertyNameResult->isAlwaysTerminating();
6417: $scope = $propertyNameResult->getScope();
6418: }
6419:
6420: $scopeBeforeAssignEval = $scope;
6421: $result = $processExprCallback($scope);
6422: $hasYield = $hasYield || $result->hasYield();
6423: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
6424: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
6425: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
6426: $scope = $result->getScope();
6427:
6428: if ($propertyName !== null) {
6429: $propertyReflection = $scope->getStaticPropertyReflection($propertyHolderType, $propertyName);
6430: $assignedExprType = $scope->getType($assignedExpr);
6431: $this->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage);
6432: if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) {
6433: if ($propertyReflection->hasNativeType()) {
6434: $propertyNativeType = $propertyReflection->getNativeType();
6435: $assignedTypeIsCompatible = $propertyNativeType->isSuperTypeOf($assignedExprType)->yes();
6436:
6437: if (!$assignedTypeIsCompatible) {
6438: foreach (TypeUtils::flattenTypes($propertyNativeType) as $type) {
6439: if ($type->isSuperTypeOf($assignedExprType)->yes()) {
6440: $assignedTypeIsCompatible = true;
6441: break;
6442: }
6443: }
6444: }
6445:
6446: if ($assignedTypeIsCompatible) {
6447: $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
6448: } else {
6449: $scope = $scope->assignExpression(
6450: $var,
6451: TypeCombinator::intersect($assignedExprType->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType),
6452: TypeCombinator::intersect($scope->getNativeType($assignedExpr)->toCoercedArgumentType($scope->isDeclareStrictTypes()), $propertyNativeType),
6453: );
6454: }
6455: } else {
6456: $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
6457: }
6458: }
6459: } else {
6460: // fallback
6461: $assignedExprType = $scope->getType($assignedExpr);
6462: $this->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage);
6463: $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
6464: }
6465: } elseif ($var instanceof List_) {
6466: $this->storeBeforeScope($storage, $var, $scope);
6467: $result = $processExprCallback($scope);
6468: $hasYield = $result->hasYield();
6469: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
6470: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
6471: $isAlwaysTerminating = $result->isAlwaysTerminating();
6472: $scope = $result->getScope();
6473: foreach ($var->items as $i => $arrayItem) {
6474: if ($arrayItem === null) {
6475: continue;
6476: }
6477:
6478: $itemScope = $scope;
6479: if ($enterExpressionAssign) {
6480: $itemScope = $itemScope->enterExpressionAssign($arrayItem->value);
6481: }
6482: $itemScope = $this->lookForSetAllowedUndefinedExpressions($itemScope, $arrayItem->value);
6483: $this->callNodeCallback($nodeCallback, $arrayItem, $itemScope, $storage);
6484: if ($arrayItem->key !== null) {
6485: $keyResult = $this->processExprNode($stmt, $arrayItem->key, $itemScope, $storage, $nodeCallback, $context->enterDeep());
6486: $hasYield = $hasYield || $keyResult->hasYield();
6487: $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints());
6488: $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints());
6489: $isAlwaysTerminating = $isAlwaysTerminating || $keyResult->isAlwaysTerminating();
6490: // no need for $keyResult->getScope()
6491: }
6492:
6493: if ($arrayItem->key === null) {
6494: $dimExpr = new Node\Scalar\Int_($i);
6495: } else {
6496: $dimExpr = $arrayItem->key;
6497: }
6498: $result = $this->processAssignVar(
6499: $scope,
6500: $storage,
6501: $stmt,
6502: $arrayItem->value,
6503: new GetOffsetValueTypeExpr($assignedExpr, $dimExpr),
6504: $nodeCallback,
6505: $context,
6506: static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []),
6507: $enterExpressionAssign,
6508: );
6509: $scope = $result->getScope();
6510: $hasYield = $hasYield || $result->hasYield();
6511: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
6512: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
6513: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
6514: }
6515: } elseif ($var instanceof ExistingArrayDimFetch) {
6516: $dimFetchStack = [];
6517: $assignedPropertyExpr = $assignedExpr;
6518: while ($var instanceof ExistingArrayDimFetch) {
6519: $varForSetOffsetValue = $var->getVar();
6520: if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) {
6521: $varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue);
6522: }
6523: $assignedPropertyExpr = new SetExistingOffsetValueTypeExpr(
6524: $varForSetOffsetValue,
6525: $var->getDim(),
6526: $assignedPropertyExpr,
6527: );
6528: $dimFetchStack[] = $var;
6529: $var = $var->getVar();
6530: }
6531:
6532: $offsetTypes = [];
6533: $offsetNativeTypes = [];
6534: foreach (array_reverse($dimFetchStack) as $dimFetch) {
6535: $dimExpr = $dimFetch->getDim();
6536: $offsetTypes[] = [$scope->getType($dimExpr), $dimFetch];
6537: $offsetNativeTypes[] = [$scope->getNativeType($dimExpr), $dimFetch];
6538: }
6539:
6540: $valueToWrite = $scope->getType($assignedExpr);
6541: $nativeValueToWrite = $scope->getNativeType($assignedExpr);
6542: $varType = $scope->getType($var);
6543: $varNativeType = $scope->getNativeType($var);
6544:
6545: $offsetValueType = $varType;
6546: $offsetNativeValueType = $varNativeType;
6547: $offsetValueTypeStack = [$offsetValueType];
6548: $offsetValueNativeTypeStack = [$offsetNativeValueType];
6549: foreach (array_slice($offsetTypes, 0, -1) as [$offsetType]) {
6550: $offsetValueType = $offsetValueType->getOffsetValueType($offsetType);
6551: $offsetValueTypeStack[] = $offsetValueType;
6552: }
6553: foreach (array_slice($offsetNativeTypes, 0, -1) as [$offsetNativeType]) {
6554: $offsetNativeValueType = $offsetNativeValueType->getOffsetValueType($offsetNativeType);
6555: $offsetValueNativeTypeStack[] = $offsetNativeValueType;
6556: }
6557:
6558: foreach (array_reverse($offsetTypes) as [$offsetType]) {
6559: /** @var Type $offsetValueType */
6560: $offsetValueType = array_pop($offsetValueTypeStack);
6561: $valueToWrite = $offsetValueType->setExistingOffsetValueType($offsetType, $valueToWrite);
6562: }
6563: foreach (array_reverse($offsetNativeTypes) as [$offsetNativeType]) {
6564: /** @var Type $offsetNativeValueType */
6565: $offsetNativeValueType = array_pop($offsetValueNativeTypeStack);
6566: $nativeValueToWrite = $offsetNativeValueType->setExistingOffsetValueType($offsetNativeType, $nativeValueToWrite);
6567: }
6568:
6569: if ($var instanceof Variable && is_string($var->name)) {
6570: $this->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scope, $storage);
6571: $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite, TrinaryLogic::createYes());
6572: } else {
6573: if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) {
6574: $this->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope, $storage);
6575: }
6576: $scope = $scope->assignExpression(
6577: $var,
6578: $valueToWrite,
6579: $nativeValueToWrite,
6580: );
6581: }
6582: } else {
6583: $varResult = $this->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, $context);
6584: $hasYield = $varResult->hasYield();
6585: $throwPoints = array_merge($throwPoints, $varResult->getThrowPoints());
6586: $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints());
6587: $isAlwaysTerminating = $varResult->isAlwaysTerminating();
6588: $scope = $varResult->getScope();
6589: $result = $processExprCallback($scope);
6590: $hasYield = $hasYield || $result->hasYield();
6591: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
6592: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
6593: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
6594: $scope = $result->getScope();
6595: }
6596:
6597: // stored where processAssignVar is called
6598: return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints);
6599: }
6600:
6601: /**
6602: * @param callable(Node $node, Scope $scope): void $nodeCallback
6603: */
6604: private function processVirtualAssign(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, Expr $var, Expr $assignedExpr, callable $nodeCallback): ExpressionResult
6605: {
6606: return $this->processAssignVar(
6607: $scope,
6608: $storage,
6609: $stmt,
6610: $var,
6611: $assignedExpr,
6612: new VirtualAssignNodeCallback($nodeCallback),
6613: ExpressionContext::createDeep(),
6614: static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []),
6615: false,
6616: );
6617: }
6618:
6619: /**
6620: * @param list<ArrayDimFetch> $dimFetchStack
6621: */
6622: private function isImplicitArrayCreation(array $dimFetchStack, Scope $scope): TrinaryLogic
6623: {
6624: if (count($dimFetchStack) === 0) {
6625: return TrinaryLogic::createNo();
6626: }
6627:
6628: $varNode = $dimFetchStack[0]->var;
6629: if (!$varNode instanceof Variable) {
6630: return TrinaryLogic::createNo();
6631: }
6632:
6633: if (!is_string($varNode->name)) {
6634: return TrinaryLogic::createNo();
6635: }
6636:
6637: return $scope->hasVariableType($varNode->name)->negate();
6638: }
6639:
6640: /**
6641: * @param list<ArrayDimFetch> $dimFetchStack
6642: * @param list<array{Type|null, ArrayDimFetch}> $offsetTypes
6643: *
6644: * @return array{Type, list<array{Expr, Type}>}
6645: */
6646: private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, Scope $scope): array
6647: {
6648: $originalValueToWrite = $valueToWrite;
6649:
6650: $offsetValueTypeStack = [$offsetValueType];
6651: foreach (array_slice($offsetTypes, 0, -1) as [$offsetType, $dimFetch]) {
6652: if ($offsetType === null) {
6653: $offsetValueType = new ConstantArrayType([], []);
6654:
6655: } else {
6656: $has = $offsetValueType->hasOffsetValueType($offsetType);
6657: if ($has->yes()) {
6658: $offsetValueType = $offsetValueType->getOffsetValueType($offsetType);
6659: } elseif ($has->maybe()) {
6660: if (!$scope->hasExpressionType($dimFetch)->yes()) {
6661: $offsetValueType = TypeCombinator::union($offsetValueType->getOffsetValueType($offsetType), new ConstantArrayType([], []));
6662: } else {
6663: $offsetValueType = $offsetValueType->getOffsetValueType($offsetType);
6664: }
6665: } else {
6666: $offsetValueType = new ConstantArrayType([], []);
6667: }
6668: }
6669:
6670: $offsetValueTypeStack[] = $offsetValueType;
6671: }
6672:
6673: foreach (array_reverse($offsetTypes) as $i => [$offsetType]) {
6674: /** @var Type $offsetValueType */
6675: $offsetValueType = array_pop($offsetValueTypeStack);
6676: if (
6677: !$offsetValueType instanceof MixedType
6678: && !$offsetValueType->isArray()->yes()
6679: ) {
6680: if ($offsetType !== null && $offsetType->isInteger()->yes()) {
6681: $offsetValueType = TypeCombinator::intersect($offsetValueType, StaticTypeFactory::intOffsetAccessibleType());
6682: } else {
6683: $offsetValueType = TypeCombinator::intersect($offsetValueType, StaticTypeFactory::generalOffsetAccessibleType());
6684: }
6685: }
6686:
6687: $arrayDimFetch = $dimFetchStack[$i] ?? null;
6688: if (
6689: $offsetType !== null
6690: && $arrayDimFetch !== null
6691: && $scope->hasExpressionType($arrayDimFetch)->yes()
6692: ) {
6693: $hasOffsetType = null;
6694: if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) {
6695: $hasOffsetType = new HasOffsetValueType($offsetType, $valueToWrite);
6696: }
6697: $valueToWrite = $offsetValueType->setExistingOffsetValueType($offsetType, $valueToWrite);
6698:
6699: if ($valueToWrite->isArray()->yes()) {
6700: if ($hasOffsetType !== null) {
6701: $valueToWrite = TypeCombinator::intersect(
6702: $valueToWrite,
6703: $hasOffsetType,
6704: );
6705: } else {
6706: $valueToWrite = TypeCombinator::intersect(
6707: $valueToWrite,
6708: new NonEmptyArrayType(),
6709: );
6710: }
6711: }
6712:
6713: } else {
6714: $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0);
6715: }
6716:
6717: if ($arrayDimFetch === null || !$offsetValueType->isList()->yes()) {
6718: continue;
6719: }
6720:
6721: if (!$arrayDimFetch->dim instanceof BinaryOp\Plus) {
6722: continue;
6723: }
6724:
6725: if ( // keep list for $list[$index + 1] assignments
6726: $arrayDimFetch->dim->right instanceof Variable
6727: && $arrayDimFetch->dim->left instanceof Node\Scalar\Int_
6728: && $arrayDimFetch->dim->left->value === 1
6729: && $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->right))->yes()
6730: ) {
6731: $valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType());
6732: } elseif ( // keep list for $list[1 + $index] assignments
6733: $arrayDimFetch->dim->left instanceof Variable
6734: && $arrayDimFetch->dim->right instanceof Node\Scalar\Int_
6735: && $arrayDimFetch->dim->right->value === 1
6736: && $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->left))->yes()
6737: ) {
6738: $valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType());
6739: }
6740: }
6741:
6742: $additionalExpressions = [];
6743: $offsetValueType = $valueToWrite;
6744: $lastDimKey = array_key_last($dimFetchStack);
6745: foreach ($dimFetchStack as $key => $dimFetch) {
6746: if ($dimFetch->dim === null) {
6747: continue;
6748: }
6749:
6750: if ($key === $lastDimKey) {
6751: $offsetValueType = $originalValueToWrite;
6752: } else {
6753: $offsetType = $scope->getType($dimFetch->dim);
6754: $offsetValueType = $offsetValueType->getOffsetValueType($offsetType);
6755: }
6756:
6757: $additionalExpressions[] = [$dimFetch, $offsetValueType];
6758: }
6759:
6760: return [$valueToWrite, $additionalExpressions];
6761: }
6762:
6763: private function unwrapAssign(Expr $expr): Expr
6764: {
6765: if ($expr instanceof Assign) {
6766: return $this->unwrapAssign($expr->expr);
6767: }
6768:
6769: return $expr;
6770: }
6771:
6772: /**
6773: * @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
6774: * @return array<string, ConditionalExpressionHolder[]>
6775: */
6776: private function processSureTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType): array
6777: {
6778: foreach ($specifiedTypes->getSureTypes() as $exprString => [$expr, $exprType]) {
6779: if (!$expr instanceof Variable) {
6780: continue;
6781: }
6782: if (!is_string($expr->name)) {
6783: continue;
6784: }
6785:
6786: if ($expr->name === $variableName) {
6787: continue;
6788: }
6789:
6790: if (!isset($conditionalExpressions[$exprString])) {
6791: $conditionalExpressions[$exprString] = [];
6792: }
6793:
6794: $holder = new ConditionalExpressionHolder([
6795: '$' . $variableName => ExpressionTypeHolder::createYes(new Variable($variableName), $variableType),
6796: ], ExpressionTypeHolder::createYes(
6797: $expr,
6798: TypeCombinator::intersect($scope->getType($expr), $exprType),
6799: ));
6800: $conditionalExpressions[$exprString][$holder->getKey()] = $holder;
6801: }
6802:
6803: return $conditionalExpressions;
6804: }
6805:
6806: /**
6807: * @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
6808: * @return array<string, ConditionalExpressionHolder[]>
6809: */
6810: private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType): array
6811: {
6812: foreach ($specifiedTypes->getSureNotTypes() as $exprString => [$expr, $exprType]) {
6813: if (!$expr instanceof Variable) {
6814: continue;
6815: }
6816: if (!is_string($expr->name)) {
6817: continue;
6818: }
6819:
6820: if ($expr->name === $variableName) {
6821: continue;
6822: }
6823:
6824: if (!isset($conditionalExpressions[$exprString])) {
6825: $conditionalExpressions[$exprString] = [];
6826: }
6827:
6828: $holder = new ConditionalExpressionHolder([
6829: '$' . $variableName => ExpressionTypeHolder::createYes(new Variable($variableName), $variableType),
6830: ], ExpressionTypeHolder::createYes(
6831: $expr,
6832: TypeCombinator::remove($scope->getType($expr), $exprType),
6833: ));
6834: $conditionalExpressions[$exprString][$holder->getKey()] = $holder;
6835: }
6836:
6837: return $conditionalExpressions;
6838: }
6839:
6840: /**
6841: * @param callable(Node $node, Scope $scope): void $nodeCallback
6842: */
6843: private function processStmtVarAnnotation(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, ?Expr $defaultExpr, callable $nodeCallback): MutatingScope
6844: {
6845: $function = $scope->getFunction();
6846: $variableLessTags = [];
6847:
6848: foreach ($stmt->getComments() as $comment) {
6849: if (!$comment instanceof Doc) {
6850: continue;
6851: }
6852:
6853: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
6854: $scope->getFile(),
6855: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
6856: $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
6857: $function !== null ? $function->getName() : null,
6858: $comment->getText(),
6859: );
6860:
6861: $assignedVariable = null;
6862: if (
6863: $stmt instanceof Node\Stmt\Expression
6864: && ($stmt->expr instanceof Assign || $stmt->expr instanceof AssignRef)
6865: && $stmt->expr->var instanceof Variable
6866: && is_string($stmt->expr->var->name)
6867: ) {
6868: $assignedVariable = $stmt->expr->var->name;
6869: }
6870:
6871: foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) {
6872: if (is_int($name)) {
6873: $variableLessTags[] = $varTag;
6874: continue;
6875: }
6876:
6877: if ($name === $assignedVariable) {
6878: continue;
6879: }
6880:
6881: $certainty = $scope->hasVariableType($name);
6882: if ($certainty->no()) {
6883: continue;
6884: }
6885:
6886: if ($scope->isInClass() && $scope->getFunction() === null) {
6887: continue;
6888: }
6889:
6890: if ($scope->canAnyVariableExist()) {
6891: $certainty = TrinaryLogic::createYes();
6892: }
6893:
6894: $variableNode = new Variable($name, $stmt->getAttributes());
6895: $originalType = $scope->getVariableType($name);
6896: if (!$originalType->equals($varTag->getType())) {
6897: $this->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $variableNode), $scope, $storage);
6898: }
6899:
6900: $scope = $scope->assignVariable(
6901: $name,
6902: $varTag->getType(),
6903: $scope->getNativeType($variableNode),
6904: $certainty,
6905: );
6906: }
6907: }
6908:
6909: if (count($variableLessTags) === 1 && $defaultExpr !== null) {
6910: $originalType = $scope->getType($defaultExpr);
6911: $varTag = $variableLessTags[0];
6912: if (!$originalType->equals($varTag->getType())) {
6913: $this->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope, $storage);
6914: }
6915: $scope = $scope->assignExpression($defaultExpr, $varTag->getType(), new MixedType());
6916: }
6917:
6918: return $scope;
6919: }
6920:
6921: /**
6922: * @param array<int, string> $variableNames
6923: */
6924: private function processVarAnnotation(MutatingScope $scope, array $variableNames, Node\Stmt $node, bool &$changed = false): MutatingScope
6925: {
6926: $function = $scope->getFunction();
6927: $varTags = [];
6928: foreach ($node->getComments() as $comment) {
6929: if (!$comment instanceof Doc) {
6930: continue;
6931: }
6932:
6933: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
6934: $scope->getFile(),
6935: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
6936: $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
6937: $function !== null ? $function->getName() : null,
6938: $comment->getText(),
6939: );
6940: foreach ($resolvedPhpDoc->getVarTags() as $key => $varTag) {
6941: $varTags[$key] = $varTag;
6942: }
6943: }
6944:
6945: if (count($varTags) === 0) {
6946: return $scope;
6947: }
6948:
6949: foreach ($variableNames as $variableName) {
6950: if (!isset($varTags[$variableName])) {
6951: continue;
6952: }
6953:
6954: $variableType = $varTags[$variableName]->getType();
6955: $changed = true;
6956: $scope = $scope->assignVariable($variableName, $variableType, new MixedType(), TrinaryLogic::createYes());
6957: }
6958:
6959: if (count($variableNames) === 1 && count($varTags) === 1 && isset($varTags[0])) {
6960: $variableType = $varTags[0]->getType();
6961: $changed = true;
6962: $scope = $scope->assignVariable($variableNames[0], $variableType, new MixedType(), TrinaryLogic::createYes());
6963: }
6964:
6965: return $scope;
6966: }
6967:
6968: /**
6969: * @param callable(Node $node, Scope $scope): void $nodeCallback
6970: */
6971: private function enterForeach(MutatingScope $scope, ExpressionResultStorage $storage, MutatingScope $originalScope, Foreach_ $stmt, callable $nodeCallback): MutatingScope
6972: {
6973: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
6974: $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt);
6975: }
6976:
6977: $iterateeType = $originalScope->getType($stmt->expr);
6978: if (
6979: ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name))
6980: && ($stmt->keyVar === null || ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)))
6981: ) {
6982: $keyVarName = null;
6983: if ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)) {
6984: $keyVarName = $stmt->keyVar->name;
6985: }
6986: $scope = $scope->enterForeach(
6987: $originalScope,
6988: $stmt->expr,
6989: $stmt->valueVar->name,
6990: $keyVarName,
6991: $stmt->byRef,
6992: );
6993: $vars = [$stmt->valueVar->name];
6994: if ($keyVarName !== null) {
6995: $vars[] = $keyVarName;
6996: }
6997: } else {
6998: $scope = $this->processVirtualAssign(
6999: $scope,
7000: $storage,
7001: $stmt,
7002: $stmt->valueVar,
7003: new GetIterableValueTypeExpr($stmt->expr),
7004: $nodeCallback,
7005: )->getScope();
7006: $vars = $this->getAssignedVariables($stmt->valueVar);
7007: if (
7008: $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)
7009: ) {
7010: $scope = $scope->enterForeachKey($originalScope, $stmt->expr, $stmt->keyVar->name);
7011: $vars[] = $stmt->keyVar->name;
7012: } elseif ($stmt->keyVar !== null) {
7013: $scope = $this->processVirtualAssign(
7014: $scope,
7015: $storage,
7016: $stmt,
7017: $stmt->keyVar,
7018: new GetIterableKeyTypeExpr($stmt->expr),
7019: $nodeCallback,
7020: )->getScope();
7021: $vars = array_merge($vars, $this->getAssignedVariables($stmt->keyVar));
7022: }
7023: }
7024:
7025: $constantArrays = $iterateeType->getConstantArrays();
7026: if (
7027: $stmt->getDocComment() === null
7028: && $iterateeType->isConstantArray()->yes()
7029: && count($constantArrays) === 1
7030: && $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)
7031: && $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)
7032: ) {
7033: $valueConditionalHolders = [];
7034: $arrayDimFetchConditionalHolders = [];
7035: foreach ($constantArrays[0]->getKeyTypes() as $i => $keyType) {
7036: $valueType = $constantArrays[0]->getValueTypes()[$i];
7037: $keyExpressionTypeHolder = ExpressionTypeHolder::createYes(new Variable($stmt->keyVar->name), $keyType);
7038:
7039: $holder = new ConditionalExpressionHolder([
7040: '$' . $stmt->keyVar->name => $keyExpressionTypeHolder,
7041: ], ExpressionTypeHolder::createYes($stmt->valueVar, $valueType));
7042: $valueConditionalHolders[$holder->getKey()] = $holder;
7043: $arrayDimFetchHolder = new ConditionalExpressionHolder([
7044: '$' . $stmt->keyVar->name => $keyExpressionTypeHolder,
7045: ], ExpressionTypeHolder::createYes(new ArrayDimFetch($stmt->expr, $stmt->keyVar), $valueType));
7046: $arrayDimFetchConditionalHolders[$arrayDimFetchHolder->getKey()] = $arrayDimFetchHolder;
7047: }
7048:
7049: $scope = $scope->addConditionalExpressions(
7050: '$' . $stmt->valueVar->name,
7051: $valueConditionalHolders,
7052: );
7053: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
7054: $scope = $scope->addConditionalExpressions(
7055: sprintf('$%s[$%s]', $stmt->expr->name, $stmt->keyVar->name),
7056: $arrayDimFetchConditionalHolders,
7057: );
7058: }
7059: }
7060:
7061: if (
7062: $stmt->expr instanceof FuncCall
7063: && $stmt->expr->name instanceof Name
7064: && $stmt->expr->name->toLowerString() === 'array_keys'
7065: && $stmt->valueVar instanceof Variable
7066: ) {
7067: $args = $stmt->expr->getArgs();
7068: if (count($args) >= 1) {
7069: $arrayArg = $args[0]->value;
7070: $scope = $scope->assignExpression(
7071: new ArrayDimFetch($arrayArg, $stmt->valueVar),
7072: $scope->getType($arrayArg)->getIterableValueType(),
7073: $scope->getNativeType($arrayArg)->getIterableValueType(),
7074: );
7075: }
7076: }
7077:
7078: return $this->processVarAnnotation($scope, $vars, $stmt);
7079: }
7080:
7081: /**
7082: * @param callable(Node $node, Scope $scope): void $nodeCallback
7083: */
7084: private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classScope, ExpressionResultStorage $storage, callable $nodeCallback): void
7085: {
7086: $parentTraitNames = [];
7087: $parent = $classScope->getParentScope();
7088: while ($parent !== null) {
7089: if ($parent->isInTrait()) {
7090: $parentTraitNames[] = $parent->getTraitReflection()->getName();
7091: }
7092: $parent = $parent->getParentScope();
7093: }
7094:
7095: foreach ($node->traits as $trait) {
7096: $traitName = (string) $trait;
7097: if (in_array($traitName, $parentTraitNames, true)) {
7098: continue;
7099: }
7100: if (!$this->reflectionProvider->hasClass($traitName)) {
7101: continue;
7102: }
7103: $traitReflection = $this->reflectionProvider->getClass($traitName);
7104: $traitFileName = $traitReflection->getFileName();
7105: if ($traitFileName === null) {
7106: continue; // trait from eval or from PHP itself
7107: }
7108: $fileName = $this->fileHelper->normalizePath($traitFileName);
7109: if (!isset($this->analysedFiles[$fileName])) {
7110: continue;
7111: }
7112: $adaptations = [];
7113: foreach ($node->adaptations as $adaptation) {
7114: if ($adaptation->trait === null) {
7115: $adaptations[] = $adaptation;
7116: continue;
7117: }
7118: if ($adaptation->trait->toLowerString() !== $trait->toLowerString()) {
7119: continue;
7120: }
7121:
7122: $adaptations[] = $adaptation;
7123: }
7124: $parserNodes = $this->parser->parseFile($fileName);
7125: $this->processNodesForTraitUse($parserNodes, $traitReflection, $classScope, $storage, $adaptations, $nodeCallback);
7126: }
7127: }
7128:
7129: /**
7130: * @param Node[]|Node|scalar|null $node
7131: * @param Node\Stmt\TraitUseAdaptation[] $adaptations
7132: * @param callable(Node $node, Scope $scope): void $nodeCallback
7133: */
7134: private function processNodesForTraitUse($node, ClassReflection $traitReflection, MutatingScope $scope, ExpressionResultStorage $storage, array $adaptations, callable $nodeCallback): void
7135: {
7136: if ($node instanceof Node) {
7137: if ($node instanceof Node\Stmt\Trait_ && $traitReflection->getName() === (string) $node->namespacedName && $traitReflection->getNativeReflection()->getStartLine() === $node->getStartLine()) {
7138: $methodModifiers = [];
7139: $methodNames = [];
7140: foreach ($adaptations as $adaptation) {
7141: if (!$adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
7142: continue;
7143: }
7144:
7145: $methodName = $adaptation->method->toLowerString();
7146: if ($adaptation->newModifier !== null) {
7147: $methodModifiers[$methodName] = $adaptation->newModifier;
7148: }
7149:
7150: if ($adaptation->newName === null) {
7151: continue;
7152: }
7153:
7154: $methodNames[$methodName] = $adaptation->newName;
7155: }
7156:
7157: $stmts = $node->stmts;
7158: foreach ($stmts as $i => $stmt) {
7159: if (!$stmt instanceof Node\Stmt\ClassMethod) {
7160: continue;
7161: }
7162: $methodName = $stmt->name->toLowerString();
7163: $methodAst = clone $stmt;
7164: $stmts[$i] = $methodAst;
7165: if (array_key_exists($methodName, $methodModifiers)) {
7166: $methodAst->flags = ($methodAst->flags & ~ Modifiers::VISIBILITY_MASK) | $methodModifiers[$methodName];
7167: }
7168:
7169: if (!array_key_exists($methodName, $methodNames)) {
7170: continue;
7171: }
7172:
7173: $methodAst->setAttribute('originalTraitMethodName', $methodAst->name->toLowerString());
7174: $methodAst->name = $methodNames[$methodName];
7175: }
7176:
7177: if (!$scope->isInClass()) {
7178: throw new ShouldNotHappenException();
7179: }
7180: $traitScope = $scope->enterTrait($traitReflection);
7181: $this->callNodeCallback($nodeCallback, new InTraitNode($node, $traitReflection, $scope->getClassReflection()), $traitScope, $storage);
7182: $this->processStmtNodesInternal($node, $stmts, $traitScope, $storage, $nodeCallback, StatementContext::createTopLevel());
7183: return;
7184: }
7185: if ($node instanceof Node\Stmt\ClassLike) {
7186: return;
7187: }
7188: if ($node instanceof Node\FunctionLike) {
7189: return;
7190: }
7191: foreach ($node->getSubNodeNames() as $subNodeName) {
7192: $subNode = $node->{$subNodeName};
7193: $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $storage, $adaptations, $nodeCallback);
7194: }
7195: } elseif (is_array($node)) {
7196: foreach ($node as $subNode) {
7197: $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $storage, $adaptations, $nodeCallback);
7198: }
7199: }
7200: }
7201:
7202: private function processCalledMethod(MethodReflection $methodReflection): ?MutatingScope
7203: {
7204: $declaringClass = $methodReflection->getDeclaringClass();
7205: if ($declaringClass->isAnonymous()) {
7206: return null;
7207: }
7208: if ($declaringClass->getFileName() === null) {
7209: return null;
7210: }
7211:
7212: $stackName = sprintf('%s::%s', $declaringClass->getName(), $methodReflection->getName());
7213: if (array_key_exists($stackName, $this->calledMethodResults)) {
7214: return $this->calledMethodResults[$stackName];
7215: }
7216:
7217: if (array_key_exists($stackName, $this->calledMethodStack)) {
7218: return null;
7219: }
7220:
7221: if (count($this->calledMethodStack) > 0) {
7222: return null;
7223: }
7224:
7225: $this->calledMethodStack[$stackName] = true;
7226:
7227: $fileName = $this->fileHelper->normalizePath($declaringClass->getFileName());
7228: if (!isset($this->analysedFiles[$fileName])) {
7229: return null;
7230: }
7231: $parserNodes = $this->parser->parseFile($fileName);
7232:
7233: $returnStatement = null;
7234: $this->processNodesForCalledMethod($parserNodes, new ExpressionResultStorage(), $fileName, $methodReflection, static function (Node $node, Scope $scope) use ($methodReflection, &$returnStatement): void {
7235: if (!$node instanceof MethodReturnStatementsNode) {
7236: return;
7237: }
7238:
7239: if ($node->getClassReflection()->getName() !== $methodReflection->getDeclaringClass()->getName()) {
7240: return;
7241: }
7242:
7243: if ($returnStatement !== null) {
7244: return;
7245: }
7246:
7247: $returnStatement = $node;
7248: });
7249:
7250: $calledMethodEndScope = null;
7251: if ($returnStatement !== null) {
7252: foreach ($returnStatement->getExecutionEnds() as $executionEnd) {
7253: $statementResult = $executionEnd->getStatementResult();
7254: $endNode = $executionEnd->getNode();
7255: if ($endNode instanceof Node\Stmt\Expression) {
7256: $exprType = $statementResult->getScope()->getType($endNode->expr);
7257: if ($exprType instanceof NeverType && $exprType->isExplicit()) {
7258: continue;
7259: }
7260: }
7261: if ($calledMethodEndScope === null) {
7262: $calledMethodEndScope = $statementResult->getScope();
7263: continue;
7264: }
7265:
7266: $calledMethodEndScope = $calledMethodEndScope->mergeWith($statementResult->getScope());
7267: }
7268: foreach ($returnStatement->getReturnStatements() as $statement) {
7269: if ($calledMethodEndScope === null) {
7270: $calledMethodEndScope = $statement->getScope();
7271: continue;
7272: }
7273:
7274: $calledMethodEndScope = $calledMethodEndScope->mergeWith($statement->getScope());
7275: }
7276: }
7277:
7278: unset($this->calledMethodStack[$stackName]);
7279:
7280: $this->calledMethodResults[$stackName] = $calledMethodEndScope;
7281:
7282: return $calledMethodEndScope;
7283: }
7284:
7285: /**
7286: * @param Node[]|Node|scalar|null $node
7287: * @param callable(Node $node, Scope $scope): void $nodeCallback
7288: */
7289: private function processNodesForCalledMethod($node, ExpressionResultStorage $storage, string $fileName, MethodReflection $methodReflection, callable $nodeCallback): void
7290: {
7291: if ($node instanceof Node) {
7292: $declaringClass = $methodReflection->getDeclaringClass();
7293: if (
7294: $node instanceof Node\Stmt\Class_
7295: && isset($node->namespacedName)
7296: && $declaringClass->getName() === (string) $node->namespacedName
7297: && $declaringClass->getNativeReflection()->getStartLine() === $node->getStartLine()
7298: ) {
7299:
7300: $stmts = $node->stmts;
7301: foreach ($stmts as $stmt) {
7302: if (!$stmt instanceof Node\Stmt\ClassMethod) {
7303: continue;
7304: }
7305:
7306: if ($stmt->name->toString() !== $methodReflection->getName()) {
7307: continue;
7308: }
7309:
7310: if ($stmt->getEndLine() - $stmt->getStartLine() > 50) {
7311: continue;
7312: }
7313:
7314: $scope = $this->scopeFactory->create(ScopeContext::create($fileName))->enterClass($declaringClass);
7315: $this->processStmtNode($stmt, $scope, $storage, $nodeCallback, StatementContext::createTopLevel());
7316: }
7317: return;
7318: }
7319: if ($node instanceof Node\Stmt\ClassLike) {
7320: return;
7321: }
7322: if ($node instanceof Node\FunctionLike) {
7323: return;
7324: }
7325: foreach ($node->getSubNodeNames() as $subNodeName) {
7326: $subNode = $node->{$subNodeName};
7327: $this->processNodesForCalledMethod($subNode, $storage, $fileName, $methodReflection, $nodeCallback);
7328: }
7329: } elseif (is_array($node)) {
7330: foreach ($node as $subNode) {
7331: $this->processNodesForCalledMethod($subNode, $storage, $fileName, $methodReflection, $nodeCallback);
7332: }
7333: }
7334: }
7335:
7336: /**
7337: * @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}
7338: */
7339: public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array
7340: {
7341: $templateTypeMap = TemplateTypeMap::createEmpty();
7342: $phpDocParameterTypes = [];
7343: $phpDocImmediatelyInvokedCallableParameters = [];
7344: $phpDocClosureThisTypeParameters = [];
7345: $phpDocReturnType = null;
7346: $phpDocThrowType = null;
7347: $deprecatedDescription = null;
7348: $isDeprecated = false;
7349: $isInternal = false;
7350: $isFinal = false;
7351: $isPure = null;
7352: $isAllowedPrivateMutation = false;
7353: $acceptsNamedArguments = true;
7354: $isReadOnly = $scope->isInClass() && $scope->getClassReflection()->isImmutable();
7355: $asserts = Assertions::createEmpty();
7356: $selfOutType = null;
7357: $docComment = $node->getDocComment() !== null
7358: ? $node->getDocComment()->getText()
7359: : null;
7360:
7361: $file = $scope->getFile();
7362: $class = $scope->isInClass() ? $scope->getClassReflection()->getName() : null;
7363: $trait = $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null;
7364: $resolvedPhpDoc = null;
7365: $functionName = null;
7366: $phpDocParameterOutTypes = [];
7367:
7368: if ($node instanceof Node\Stmt\ClassMethod) {
7369: if (!$scope->isInClass()) {
7370: throw new ShouldNotHappenException();
7371: }
7372: $functionName = $node->name->name;
7373: $positionalParameterNames = array_map(static function (Node\Param $param): string {
7374: if (!$param->var instanceof Variable || !is_string($param->var->name)) {
7375: throw new ShouldNotHappenException();
7376: }
7377:
7378: return $param->var->name;
7379: }, $node->getParams());
7380: $currentResolvedPhpDoc = null;
7381: if ($docComment !== null) {
7382: $currentResolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
7383: $file,
7384: $class,
7385: $trait,
7386: $node->name->name,
7387: $docComment,
7388: );
7389: }
7390: $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod(
7391: $scope->getClassReflection(),
7392: $node->name->name,
7393: $currentResolvedPhpDoc,
7394: $positionalParameterNames,
7395: );
7396:
7397: if ($node->name->toLowerString() === '__construct') {
7398: foreach ($node->params as $param) {
7399: if ($param->flags === 0) {
7400: continue;
7401: }
7402:
7403: if ($param->getDocComment() === null) {
7404: continue;
7405: }
7406:
7407: if (
7408: !$param->var instanceof Variable
7409: || !is_string($param->var->name)
7410: ) {
7411: throw new ShouldNotHappenException();
7412: }
7413:
7414: $paramPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
7415: $file,
7416: $class,
7417: $trait,
7418: '__construct',
7419: $param->getDocComment()->getText(),
7420: );
7421: $varTags = $paramPhpDoc->getVarTags();
7422: if (isset($varTags[0]) && count($varTags) === 1) {
7423: $phpDocType = $varTags[0]->getType();
7424: } elseif (isset($varTags[$param->var->name])) {
7425: $phpDocType = $varTags[$param->var->name]->getType();
7426: } else {
7427: continue;
7428: }
7429:
7430: $phpDocParameterTypes[$param->var->name] = $phpDocType;
7431: }
7432: }
7433: } elseif ($node instanceof Node\Stmt\Function_) {
7434: $functionName = trim($scope->getNamespace() . '\\' . $node->name->name, '\\');
7435: } elseif ($node instanceof Node\PropertyHook) {
7436: $propertyName = $node->getAttribute('propertyName');
7437: if ($propertyName !== null) {
7438: $functionName = sprintf('$%s::%s', $propertyName, $node->name->toString());
7439: }
7440: }
7441:
7442: if ($docComment !== null && $resolvedPhpDoc === null) {
7443: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
7444: $file,
7445: $class,
7446: $trait,
7447: $functionName,
7448: $docComment,
7449: );
7450: }
7451:
7452: $varTags = [];
7453: if ($resolvedPhpDoc !== null) {
7454: $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap();
7455: $phpDocImmediatelyInvokedCallableParameters = $resolvedPhpDoc->getParamsImmediatelyInvokedCallable();
7456: foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) {
7457: if (array_key_exists($paramName, $phpDocParameterTypes)) {
7458: continue;
7459: }
7460: $paramType = $paramTag->getType();
7461: if ($scope->isInClass()) {
7462: $paramType = $this->transformStaticType($scope->getClassReflection(), $paramType);
7463: }
7464: $phpDocParameterTypes[$paramName] = $paramType;
7465: }
7466: foreach ($resolvedPhpDoc->getParamClosureThisTags() as $paramName => $paramClosureThisTag) {
7467: if (array_key_exists($paramName, $phpDocClosureThisTypeParameters)) {
7468: continue;
7469: }
7470: $paramClosureThisType = $paramClosureThisTag->getType();
7471: if ($scope->isInClass()) {
7472: $paramClosureThisType = $this->transformStaticType($scope->getClassReflection(), $paramClosureThisType);
7473: }
7474: $phpDocClosureThisTypeParameters[$paramName] = $paramClosureThisType;
7475: }
7476:
7477: foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) {
7478: $phpDocParameterOutTypes[$paramName] = $paramOutTag->getType();
7479: }
7480: if ($node instanceof Node\FunctionLike) {
7481: $nativeReturnType = $scope->getFunctionType($node->getReturnType(), false, false);
7482: $phpDocReturnType = $this->getPhpDocReturnType($resolvedPhpDoc, $nativeReturnType);
7483: if ($phpDocReturnType !== null && $scope->isInClass()) {
7484: $phpDocReturnType = $this->transformStaticType($scope->getClassReflection(), $phpDocReturnType);
7485: }
7486: }
7487: $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null;
7488: $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null;
7489: $isDeprecated = $resolvedPhpDoc->isDeprecated();
7490: $isInternal = $resolvedPhpDoc->isInternal();
7491: $isFinal = $resolvedPhpDoc->isFinal();
7492: $isPure = $resolvedPhpDoc->isPure();
7493: $isAllowedPrivateMutation = $resolvedPhpDoc->isAllowedPrivateMutation();
7494: $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments();
7495: $isReadOnly = $isReadOnly || $resolvedPhpDoc->isReadOnly();
7496: $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc);
7497: $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null;
7498: $varTags = $resolvedPhpDoc->getVarTags();
7499: }
7500:
7501: if ($acceptsNamedArguments && $scope->isInClass()) {
7502: $acceptsNamedArguments = $scope->getClassReflection()->acceptsNamedArguments();
7503: }
7504:
7505: if ($isPure === null && $node instanceof Node\FunctionLike && $scope->isInClass()) {
7506: $classResolvedPhpDoc = $scope->getClassReflection()->getResolvedPhpDoc();
7507: if ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsPure()) {
7508: if (
7509: strtolower($functionName ?? '') === '__construct'
7510: || (
7511: ($phpDocReturnType === null || !$phpDocReturnType->isVoid()->yes())
7512: && !$scope->getFunctionType($node->getReturnType(), false, false)->isVoid()->yes()
7513: )
7514: ) {
7515: $isPure = true;
7516: }
7517: } elseif ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsImpure()) {
7518: $isPure = false;
7519: }
7520: }
7521:
7522: return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation, $resolvedPhpDoc];
7523: }
7524:
7525: private function transformStaticType(ClassReflection $declaringClass, Type $type): Type
7526: {
7527: return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($declaringClass): Type {
7528: if ($type instanceof StaticType) {
7529: $changedType = $type->changeBaseClass($declaringClass);
7530: if ($declaringClass->isFinal() && !$type instanceof ThisType) {
7531: $changedType = $changedType->getStaticObjectType();
7532: }
7533: return $traverse($changedType);
7534: }
7535:
7536: return $traverse($type);
7537: });
7538: }
7539:
7540: private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $nativeReturnType): ?Type
7541: {
7542: $returnTag = $resolvedPhpDoc->getReturnTag();
7543:
7544: if ($returnTag === null) {
7545: return null;
7546: }
7547:
7548: $phpDocReturnType = $returnTag->getType();
7549:
7550: if ($returnTag->isExplicit()) {
7551: return $phpDocReturnType;
7552: }
7553:
7554: if ($nativeReturnType->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocReturnType))->yes()) {
7555: return $phpDocReturnType;
7556: }
7557:
7558: if ($phpDocReturnType instanceof UnionType) {
7559: $types = [];
7560: foreach ($phpDocReturnType->getTypes() as $innerType) {
7561: if (!$nativeReturnType->isSuperTypeOf($innerType)->yes()) {
7562: continue;
7563: }
7564:
7565: $types[] = $innerType;
7566: }
7567:
7568: if (count($types) === 0) {
7569: return null;
7570: }
7571:
7572: return TypeCombinator::union(...$types);
7573: }
7574:
7575: return null;
7576: }
7577:
7578: /**
7579: * @param array<Node> $nodes
7580: * @return list<Node\Stmt>
7581: */
7582: private function getNextUnreachableStatements(array $nodes, bool $earlyBinding): array
7583: {
7584: $stmts = [];
7585: $isPassedUnreachableStatement = false;
7586: foreach ($nodes as $node) {
7587: if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) {
7588: continue;
7589: }
7590: if ($isPassedUnreachableStatement && $node instanceof Node\Stmt) {
7591: $stmts[] = $node;
7592: continue;
7593: }
7594: if ($node instanceof Node\Stmt\Nop || $node instanceof Node\Stmt\InlineHTML) {
7595: continue;
7596: }
7597: if (!$node instanceof Node\Stmt) {
7598: continue;
7599: }
7600: $stmts[] = $node;
7601: $isPassedUnreachableStatement = true;
7602: }
7603: return $stmts;
7604: }
7605:
7606: /**
7607: * @param array<Expr> $conditions
7608: */
7609: private function getFilteringExprForMatchArm(Expr\Match_ $expr, array $conditions): BinaryOp\Identical|FuncCall
7610: {
7611: if (count($conditions) === 1) {
7612: return new BinaryOp\Identical($expr->cond, $conditions[0]);
7613: }
7614:
7615: $items = [];
7616: foreach ($conditions as $filteringExpr) {
7617: $items[] = new Node\ArrayItem($filteringExpr);
7618: }
7619:
7620: return new FuncCall(
7621: new Name\FullyQualified('in_array'),
7622: [
7623: new Arg($expr->cond),
7624: new Arg(new Array_($items)),
7625: new Arg(new ConstFetch(new Name\FullyQualified('true'))),
7626: ],
7627: );
7628: }
7629:
7630: /**
7631: * Checks if a scope's conditional expressions form a contradiction,
7632: * meaning no combination of variable values is possible.
7633: * Used for match(true) exhaustiveness detection.
7634: */
7635: private function isScopeConditionallyImpossible(MutatingScope $scope): bool
7636: {
7637: $boolVars = [];
7638: foreach ($scope->getDefinedVariables() as $varName) {
7639: $varType = $scope->getVariableType($varName);
7640: if (!$varType->isBoolean()->yes() || $varType->isConstantScalarValue()->yes()) {
7641: continue;
7642: }
7643:
7644: $boolVars[] = $varName;
7645: }
7646:
7647: if ($boolVars === []) {
7648: return false;
7649: }
7650:
7651: // Check if any boolean variable's both truth values lead to contradictions
7652: foreach ($boolVars as $varName) {
7653: $varExpr = new Variable($varName);
7654:
7655: $truthyScope = $scope->filterByTruthyValue($varExpr);
7656: $truthyContradiction = $this->scopeHasNeverVariable($truthyScope, $boolVars);
7657: if (!$truthyContradiction) {
7658: continue;
7659: }
7660:
7661: $falseyScope = $scope->filterByFalseyValue($varExpr);
7662: $falseyContradiction = $this->scopeHasNeverVariable($falseyScope, $boolVars);
7663: if ($falseyContradiction) {
7664: return true;
7665: }
7666: }
7667:
7668: return false;
7669: }
7670:
7671: /**
7672: * @param string[] $varNames
7673: */
7674: private function scopeHasNeverVariable(MutatingScope $scope, array $varNames): bool
7675: {
7676: foreach ($varNames as $varName) {
7677: $type = $scope->getVariableType($varName);
7678: if ($type instanceof NeverType) {
7679: return true;
7680: }
7681: }
7682:
7683: return false;
7684: }
7685:
7686: private function inferForLoopExpressions(For_ $stmt, Expr $lastCondExpr, MutatingScope $bodyScope): MutatingScope
7687: {
7688: // infer $items[$i] type from for ($i = 0; $i < count($items); $i++) {...}
7689:
7690: if (
7691: // $i = 0
7692: count($stmt->init) === 1
7693: && $stmt->init[0] instanceof Assign
7694: && $stmt->init[0]->var instanceof Variable
7695: && $stmt->init[0]->expr instanceof Node\Scalar\Int_
7696: && $stmt->init[0]->expr->value === 0
7697: // $i++ or ++$i
7698: && count($stmt->loop) === 1
7699: && ($stmt->loop[0] instanceof Expr\PreInc || $stmt->loop[0] instanceof Expr\PostInc)
7700: && $stmt->loop[0]->var instanceof Variable
7701: ) {
7702: // $i < count($items)
7703: if (
7704: $lastCondExpr instanceof BinaryOp\Smaller
7705: && $lastCondExpr->left instanceof Variable
7706: && $lastCondExpr->right instanceof FuncCall
7707: && $lastCondExpr->right->name instanceof Name
7708: && $lastCondExpr->right->name->toLowerString() === 'count'
7709: && count($lastCondExpr->right->getArgs()) > 0
7710: && $lastCondExpr->right->getArgs()[0]->value instanceof Variable
7711: && is_string($stmt->init[0]->var->name)
7712: && $stmt->init[0]->var->name === $stmt->loop[0]->var->name
7713: && $stmt->init[0]->var->name === $lastCondExpr->left->name
7714: ) {
7715: $arrayArg = $lastCondExpr->right->getArgs()[0]->value;
7716: $bodyScope = $bodyScope->assignExpression(
7717: new ArrayDimFetch($lastCondExpr->right->getArgs()[0]->value, $lastCondExpr->left),
7718: $bodyScope->getType($arrayArg)->getIterableValueType(),
7719: $bodyScope->getNativeType($arrayArg)->getIterableValueType(),
7720: );
7721: }
7722:
7723: // count($items) > $i
7724: if (
7725: $lastCondExpr instanceof BinaryOp\Greater
7726: && $lastCondExpr->right instanceof Variable
7727: && $lastCondExpr->left instanceof FuncCall
7728: && $lastCondExpr->left->name instanceof Name
7729: && $lastCondExpr->left->name->toLowerString() === 'count'
7730: && count($lastCondExpr->left->getArgs()) > 0
7731: && $lastCondExpr->left->getArgs()[0]->value instanceof Variable
7732: && is_string($stmt->init[0]->var->name)
7733: && $stmt->init[0]->var->name === $stmt->loop[0]->var->name
7734: && $stmt->init[0]->var->name === $lastCondExpr->right->name
7735: ) {
7736: $arrayArg = $lastCondExpr->left->getArgs()[0]->value;
7737: $bodyScope = $bodyScope->assignExpression(
7738: new ArrayDimFetch($lastCondExpr->left->getArgs()[0]->value, $lastCondExpr->right),
7739: $bodyScope->getType($arrayArg)->getIterableValueType(),
7740: $bodyScope->getNativeType($arrayArg)->getIterableValueType(),
7741: );
7742: }
7743: }
7744:
7745: return $bodyScope;
7746: }
7747:
7748: }
7749: