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