1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use ArrayAccess;
6: use Closure;
7: use DivisionByZeroError;
8: use PhpParser\Comment\Doc;
9: use PhpParser\Node;
10: use PhpParser\Node\Arg;
11: use PhpParser\Node\AttributeGroup;
12: use PhpParser\Node\Expr;
13: use PhpParser\Node\Expr\Array_;
14: use PhpParser\Node\Expr\ArrayDimFetch;
15: use PhpParser\Node\Expr\ArrayItem;
16: use PhpParser\Node\Expr\Assign;
17: use PhpParser\Node\Expr\AssignRef;
18: use PhpParser\Node\Expr\BinaryOp;
19: use PhpParser\Node\Expr\BinaryOp\BooleanAnd;
20: use PhpParser\Node\Expr\BinaryOp\BooleanOr;
21: use PhpParser\Node\Expr\BinaryOp\Coalesce;
22: use PhpParser\Node\Expr\BooleanNot;
23: use PhpParser\Node\Expr\CallLike;
24: use PhpParser\Node\Expr\Cast;
25: use PhpParser\Node\Expr\ConstFetch;
26: use PhpParser\Node\Expr\ErrorSuppress;
27: use PhpParser\Node\Expr\Exit_;
28: use PhpParser\Node\Expr\FuncCall;
29: use PhpParser\Node\Expr\Instanceof_;
30: use PhpParser\Node\Expr\List_;
31: use PhpParser\Node\Expr\MethodCall;
32: use PhpParser\Node\Expr\New_;
33: use PhpParser\Node\Expr\PropertyFetch;
34: use PhpParser\Node\Expr\StaticCall;
35: use PhpParser\Node\Expr\StaticPropertyFetch;
36: use PhpParser\Node\Expr\Ternary;
37: use PhpParser\Node\Expr\Variable;
38: use PhpParser\Node\Name;
39: use PhpParser\Node\Stmt\Break_;
40: use PhpParser\Node\Stmt\Class_;
41: use PhpParser\Node\Stmt\Continue_;
42: use PhpParser\Node\Stmt\Do_;
43: use PhpParser\Node\Stmt\Echo_;
44: use PhpParser\Node\Stmt\For_;
45: use PhpParser\Node\Stmt\Foreach_;
46: use PhpParser\Node\Stmt\If_;
47: use PhpParser\Node\Stmt\InlineHTML;
48: use PhpParser\Node\Stmt\Return_;
49: use PhpParser\Node\Stmt\Static_;
50: use PhpParser\Node\Stmt\Switch_;
51: use PhpParser\Node\Stmt\Throw_;
52: use PhpParser\Node\Stmt\TryCatch;
53: use PhpParser\Node\Stmt\Unset_;
54: use PhpParser\Node\Stmt\While_;
55: use PhpParser\NodeFinder;
56: use PhpParser\NodeTraverser;
57: use PhpParser\NodeVisitor\CloningVisitor;
58: use PhpParser\NodeVisitorAbstract;
59: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass;
60: use PHPStan\BetterReflection\Reflection\ReflectionEnum;
61: use PHPStan\BetterReflection\Reflector\Reflector;
62: use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection;
63: use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
64: use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider;
65: use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
66: use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
67: use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider;
68: use PHPStan\File\FileHelper;
69: use PHPStan\File\FileReader;
70: use PHPStan\Node\BooleanAndNode;
71: use PHPStan\Node\BooleanOrNode;
72: use PHPStan\Node\BreaklessWhileLoopNode;
73: use PHPStan\Node\CatchWithUnthrownExceptionNode;
74: use PHPStan\Node\ClassConstantsNode;
75: use PHPStan\Node\ClassMethodsNode;
76: use PHPStan\Node\ClassPropertiesNode;
77: use PHPStan\Node\ClassPropertyNode;
78: use PHPStan\Node\ClassStatementsGatherer;
79: use PHPStan\Node\ClosureReturnStatementsNode;
80: use PHPStan\Node\DoWhileLoopConditionNode;
81: use PHPStan\Node\ExecutionEndNode;
82: use PHPStan\Node\Expr\AlwaysRememberedExpr;
83: use PHPStan\Node\Expr\ExistingArrayDimFetch;
84: use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
85: use PHPStan\Node\Expr\GetIterableValueTypeExpr;
86: use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
87: use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
88: use PHPStan\Node\Expr\PropertyInitializationExpr;
89: use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr;
90: use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
91: use PHPStan\Node\Expr\TypeExpr;
92: use PHPStan\Node\Expr\UnsetOffsetExpr;
93: use PHPStan\Node\FinallyExitPointsNode;
94: use PHPStan\Node\FunctionCallableNode;
95: use PHPStan\Node\FunctionReturnStatementsNode;
96: use PHPStan\Node\InArrowFunctionNode;
97: use PHPStan\Node\InClassMethodNode;
98: use PHPStan\Node\InClassNode;
99: use PHPStan\Node\InClosureNode;
100: use PHPStan\Node\InForeachNode;
101: use PHPStan\Node\InFunctionNode;
102: use PHPStan\Node\InstantiationCallableNode;
103: use PHPStan\Node\InTraitNode;
104: use PHPStan\Node\InvalidateExprNode;
105: use PHPStan\Node\LiteralArrayItem;
106: use PHPStan\Node\LiteralArrayNode;
107: use PHPStan\Node\MatchExpressionArm;
108: use PHPStan\Node\MatchExpressionArmBody;
109: use PHPStan\Node\MatchExpressionArmCondition;
110: use PHPStan\Node\MatchExpressionNode;
111: use PHPStan\Node\MethodCallableNode;
112: use PHPStan\Node\MethodReturnStatementsNode;
113: use PHPStan\Node\NoopExpressionNode;
114: use PHPStan\Node\PropertyAssignNode;
115: use PHPStan\Node\ReturnStatement;
116: use PHPStan\Node\StaticMethodCallableNode;
117: use PHPStan\Node\UnreachableStatementNode;
118: use PHPStan\Node\VariableAssignNode;
119: use PHPStan\Node\VarTagChangedExpressionTypeNode;
120: use PHPStan\Parser\ArrowFunctionArgVisitor;
121: use PHPStan\Parser\ClosureArgVisitor;
122: use PHPStan\Parser\Parser;
123: use PHPStan\Php\PhpVersion;
124: use PHPStan\PhpDoc\PhpDocInheritanceResolver;
125: use PHPStan\PhpDoc\ResolvedPhpDocBlock;
126: use PHPStan\PhpDoc\StubPhpDocProvider;
127: use PHPStan\PhpDoc\Tag\VarTag;
128: use PHPStan\Reflection\Assertions;
129: use PHPStan\Reflection\Callables\CallableParametersAcceptor;
130: use PHPStan\Reflection\Callables\SimpleImpurePoint;
131: use PHPStan\Reflection\Callables\SimpleThrowPoint;
132: use PHPStan\Reflection\ClassReflection;
133: use PHPStan\Reflection\ExtendedMethodReflection;
134: use PHPStan\Reflection\FunctionReflection;
135: use PHPStan\Reflection\InitializerExprTypeResolver;
136: use PHPStan\Reflection\MethodReflection;
137: use PHPStan\Reflection\Native\NativeMethodReflection;
138: use PHPStan\Reflection\Native\NativeParameterReflection;
139: use PHPStan\Reflection\ParameterReflection;
140: use PHPStan\Reflection\ParameterReflectionWithPhpDocs;
141: use PHPStan\Reflection\ParametersAcceptor;
142: use PHPStan\Reflection\ParametersAcceptorSelector;
143: use PHPStan\Reflection\ParametersAcceptorWithPhpDocs;
144: use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection;
145: use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
146: use PHPStan\Reflection\Php\PhpMethodReflection;
147: use PHPStan\Reflection\ReflectionProvider;
148: use PHPStan\Reflection\SignatureMap\SignatureMapProvider;
149: use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
150: use PHPStan\ShouldNotHappenException;
151: use PHPStan\TrinaryLogic;
152: use PHPStan\Type\Accessory\AccessoryArrayListType;
153: use PHPStan\Type\Accessory\NonEmptyArrayType;
154: use PHPStan\Type\ArrayType;
155: use PHPStan\Type\ClosureType;
156: use PHPStan\Type\Constant\ConstantArrayType;
157: use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
158: use PHPStan\Type\Constant\ConstantBooleanType;
159: use PHPStan\Type\Constant\ConstantIntegerType;
160: use PHPStan\Type\ErrorType;
161: use PHPStan\Type\FileTypeMapper;
162: use PHPStan\Type\GeneralizePrecision;
163: use PHPStan\Type\Generic\TemplateTypeHelper;
164: use PHPStan\Type\Generic\TemplateTypeMap;
165: use PHPStan\Type\Generic\TemplateTypeVariance;
166: use PHPStan\Type\Generic\TemplateTypeVarianceMap;
167: use PHPStan\Type\IntegerType;
168: use PHPStan\Type\IntersectionType;
169: use PHPStan\Type\MixedType;
170: use PHPStan\Type\NeverType;
171: use PHPStan\Type\NullType;
172: use PHPStan\Type\ObjectType;
173: use PHPStan\Type\ObjectWithoutClassType;
174: use PHPStan\Type\ResourceType;
175: use PHPStan\Type\StaticType;
176: use PHPStan\Type\StaticTypeFactory;
177: use PHPStan\Type\StringType;
178: use PHPStan\Type\ThisType;
179: use PHPStan\Type\Type;
180: use PHPStan\Type\TypeCombinator;
181: use PHPStan\Type\TypeTraverser;
182: use PHPStan\Type\TypeUtils;
183: use PHPStan\Type\UnionType;
184: use ReflectionProperty;
185: use Throwable;
186: use Traversable;
187: use TypeError;
188: use UnhandledMatchError;
189: use function array_fill_keys;
190: use function array_filter;
191: use function array_key_exists;
192: use function array_key_last;
193: use function array_keys;
194: use function array_map;
195: use function array_merge;
196: use function array_pop;
197: use function array_reverse;
198: use function array_slice;
199: use function array_values;
200: use function base64_decode;
201: use function count;
202: use function in_array;
203: use function is_array;
204: use function is_int;
205: use function is_string;
206: use function ksort;
207: use function sprintf;
208: use function str_starts_with;
209: use function strtolower;
210: use function trim;
211: use const PHP_VERSION_ID;
212: use const SORT_NUMERIC;
213:
214: class NodeScopeResolver
215: {
216:
217: private const LOOP_SCOPE_ITERATIONS = 3;
218: private const GENERALIZE_AFTER_ITERATION = 1;
219:
220: /** @var bool[] filePath(string) => bool(true) */
221: private array $analysedFiles = [];
222:
223: /** @var array<string, true> */
224: private array $earlyTerminatingMethodNames;
225:
226: /** @var array<string, true> */
227: private array $calledMethodStack = [];
228:
229: /** @var array<string, MutatingScope|null> */
230: private array $calledMethodResults = [];
231:
232: /**
233: * @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[])
234: * @param array<int, string> $earlyTerminatingFunctionCalls
235: * @param string[] $universalObjectCratesClasses
236: */
237: public function __construct(
238: private readonly ReflectionProvider $reflectionProvider,
239: private readonly InitializerExprTypeResolver $initializerExprTypeResolver,
240: private readonly Reflector $reflector,
241: private readonly ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider,
242: private readonly ParameterOutTypeExtensionProvider $parameterOutTypeExtensionProvider,
243: private readonly Parser $parser,
244: private readonly FileTypeMapper $fileTypeMapper,
245: private readonly StubPhpDocProvider $stubPhpDocProvider,
246: private readonly PhpVersion $phpVersion,
247: private readonly SignatureMapProvider $signatureMapProvider,
248: private readonly PhpDocInheritanceResolver $phpDocInheritanceResolver,
249: private readonly FileHelper $fileHelper,
250: private readonly TypeSpecifier $typeSpecifier,
251: private readonly DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
252: private readonly ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider,
253: private readonly ParameterClosureTypeExtensionProvider $parameterClosureTypeExtensionProvider,
254: private readonly ScopeFactory $scopeFactory,
255: private readonly bool $polluteScopeWithLoopInitialAssignments,
256: private readonly bool $polluteScopeWithAlwaysIterableForeach,
257: private readonly array $earlyTerminatingMethodCalls,
258: private readonly array $earlyTerminatingFunctionCalls,
259: private readonly array $universalObjectCratesClasses,
260: private readonly bool $implicitThrows,
261: private readonly bool $treatPhpDocTypesAsCertain,
262: private readonly bool $detectDeadTypeInMultiCatch,
263: private readonly bool $paramOutType,
264: )
265: {
266: $earlyTerminatingMethodNames = [];
267: foreach ($this->earlyTerminatingMethodCalls as $methodNames) {
268: foreach ($methodNames as $methodName) {
269: $earlyTerminatingMethodNames[strtolower($methodName)] = true;
270: }
271: }
272: $this->earlyTerminatingMethodNames = $earlyTerminatingMethodNames;
273: }
274:
275: /**
276: * @api
277: * @param string[] $files
278: */
279: public function setAnalysedFiles(array $files): void
280: {
281: $this->analysedFiles = array_fill_keys($files, true);
282: }
283:
284: /**
285: * @api
286: * @param Node[] $nodes
287: * @param callable(Node $node, Scope $scope): void $nodeCallback
288: */
289: public function processNodes(
290: array $nodes,
291: MutatingScope $scope,
292: callable $nodeCallback,
293: ): void
294: {
295: foreach ($nodes as $i => $node) {
296: if (!$node instanceof Node\Stmt) {
297: continue;
298: }
299:
300: $statementResult = $this->processStmtNode($node, $scope, $nodeCallback, StatementContext::createTopLevel());
301: $scope = $statementResult->getScope();
302: if (!$statementResult->isAlwaysTerminating()) {
303: continue;
304: }
305:
306: $nextStmt = $this->getFirstUnreachableNode(array_slice($nodes, $i + 1), true);
307: if (!$nextStmt instanceof Node\Stmt) {
308: continue;
309: }
310:
311: $nodeCallback(new UnreachableStatementNode($nextStmt), $scope);
312: break;
313: }
314: }
315:
316: /**
317: * @api
318: * @param Node\Stmt[] $stmts
319: * @param callable(Node $node, Scope $scope): void $nodeCallback
320: */
321: public function processStmtNodes(
322: Node $parentNode,
323: array $stmts,
324: MutatingScope $scope,
325: callable $nodeCallback,
326: ?StatementContext $context = null,
327: ): StatementResult
328: {
329: if ($context === null) {
330: $context = StatementContext::createTopLevel();
331: }
332: $exitPoints = [];
333: $throwPoints = [];
334: $impurePoints = [];
335: $alreadyTerminated = false;
336: $hasYield = false;
337: $stmtCount = count($stmts);
338: $shouldCheckLastStatement = $parentNode instanceof Node\Stmt\Function_
339: || $parentNode instanceof Node\Stmt\ClassMethod
340: || $parentNode instanceof Expr\Closure;
341: foreach ($stmts as $i => $stmt) {
342: $isLast = $i === $stmtCount - 1;
343: $statementResult = $this->processStmtNode(
344: $stmt,
345: $scope,
346: $nodeCallback,
347: $context,
348: );
349: $scope = $statementResult->getScope();
350: $hasYield = $hasYield || $statementResult->hasYield();
351:
352: if ($shouldCheckLastStatement && $isLast) {
353: /** @var Node\Stmt\Function_|Node\Stmt\ClassMethod|Expr\Closure $parentNode */
354: $parentNode = $parentNode;
355: $nodeCallback(new ExecutionEndNode(
356: $stmt,
357: new StatementResult(
358: $scope,
359: $hasYield,
360: $statementResult->isAlwaysTerminating(),
361: $statementResult->getExitPoints(),
362: $statementResult->getThrowPoints(),
363: $statementResult->getImpurePoints(),
364: ),
365: $parentNode->returnType !== null,
366: ), $scope);
367: }
368:
369: $exitPoints = array_merge($exitPoints, $statementResult->getExitPoints());
370: $throwPoints = array_merge($throwPoints, $statementResult->getThrowPoints());
371: $impurePoints = array_merge($impurePoints, $statementResult->getImpurePoints());
372:
373: if (!$statementResult->isAlwaysTerminating()) {
374: continue;
375: }
376:
377: $alreadyTerminated = true;
378: $nextStmt = $this->getFirstUnreachableNode(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_);
379: if ($nextStmt !== null) {
380: $nodeCallback(new UnreachableStatementNode($nextStmt), $scope);
381: }
382: break;
383: }
384:
385: $statementResult = new StatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints, $impurePoints);
386: if ($stmtCount === 0 && $shouldCheckLastStatement) {
387: /** @var Node\Stmt\Function_|Node\Stmt\ClassMethod|Expr\Closure $parentNode */
388: $parentNode = $parentNode;
389: $nodeCallback(new ExecutionEndNode(
390: $parentNode,
391: $statementResult,
392: $parentNode->returnType !== null,
393: ), $scope);
394: }
395:
396: return $statementResult;
397: }
398:
399: /**
400: * @param callable(Node $node, Scope $scope): void $nodeCallback
401: */
402: private function processStmtNode(
403: Node\Stmt $stmt,
404: MutatingScope $scope,
405: callable $nodeCallback,
406: StatementContext $context,
407: ): StatementResult
408: {
409: if (
410: !$stmt instanceof Static_
411: && !$stmt instanceof Foreach_
412: && !$stmt instanceof Node\Stmt\Global_
413: && !$stmt instanceof Node\Stmt\Property
414: && !$stmt instanceof Node\Stmt\PropertyProperty
415: && !$stmt instanceof Node\Stmt\ClassConst
416: && !$stmt instanceof Node\Stmt\Const_
417: ) {
418: $scope = $this->processStmtVarAnnotation($scope, $stmt, null, $nodeCallback);
419: }
420:
421: if ($stmt instanceof Node\Stmt\ClassMethod) {
422: if (!$scope->isInClass()) {
423: throw new ShouldNotHappenException();
424: }
425: if (
426: $scope->isInTrait()
427: && $scope->getClassReflection()->hasNativeMethod($stmt->name->toString())
428: ) {
429: $methodReflection = $scope->getClassReflection()->getNativeMethod($stmt->name->toString());
430: if ($methodReflection instanceof NativeMethodReflection) {
431: return new StatementResult($scope, false, false, [], [], []);
432: }
433: if ($methodReflection instanceof PhpMethodReflection) {
434: $declaringTrait = $methodReflection->getDeclaringTrait();
435: if ($declaringTrait === null || $declaringTrait->getName() !== $scope->getTraitReflection()->getName()) {
436: return new StatementResult($scope, false, false, [], [], []);
437: }
438: }
439: }
440: }
441:
442: $stmtScope = $scope;
443: if ($stmt instanceof Throw_ || $stmt instanceof Return_) {
444: $stmtScope = $this->processStmtVarAnnotation($scope, $stmt, $stmt->expr, $nodeCallback);
445: }
446:
447: $nodeCallback($stmt, $stmtScope);
448:
449: $overridingThrowPoints = $this->getOverridingThrowPoints($stmt, $scope);
450:
451: if ($stmt instanceof Node\Stmt\Declare_) {
452: $hasYield = false;
453: $throwPoints = [];
454: $impurePoints = [];
455: $alwaysTerminating = false;
456: $exitPoints = [];
457: foreach ($stmt->declares as $declare) {
458: $nodeCallback($declare, $scope);
459: $nodeCallback($declare->value, $scope);
460: if (
461: $declare->key->name !== 'strict_types'
462: || !($declare->value instanceof Node\Scalar\LNumber)
463: || $declare->value->value !== 1
464: ) {
465: continue;
466: }
467:
468: $scope = $scope->enterDeclareStrictTypes();
469: }
470:
471: if ($stmt->stmts !== null) {
472: $result = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context);
473: $scope = $result->getScope();
474: $hasYield = $result->hasYield();
475: $throwPoints = $result->getThrowPoints();
476: $impurePoints = $result->getImpurePoints();
477: $alwaysTerminating = $result->isAlwaysTerminating();
478: $exitPoints = $result->getExitPoints();
479: }
480:
481: return new StatementResult($scope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints);
482: } elseif ($stmt instanceof Node\Stmt\Function_) {
483: $hasYield = false;
484: $throwPoints = [];
485: $impurePoints = [];
486: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback);
487: [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt);
488:
489: foreach ($stmt->params as $param) {
490: $this->processParamNode($stmt, $param, $scope, $nodeCallback);
491: }
492:
493: if ($stmt->returnType !== null) {
494: $nodeCallback($stmt->returnType, $scope);
495: }
496:
497: $functionScope = $scope->enterFunction(
498: $stmt,
499: $templateTypeMap,
500: $phpDocParameterTypes,
501: $phpDocReturnType,
502: $phpDocThrowType,
503: $deprecatedDescription,
504: $isDeprecated,
505: $isInternal,
506: $isFinal,
507: $isPure,
508: $acceptsNamedArguments,
509: $asserts,
510: $phpDocComment,
511: $phpDocParameterOutTypes,
512: $phpDocImmediatelyInvokedCallableParameters,
513: $phpDocClosureThisTypeParameters,
514: );
515: $functionReflection = $functionScope->getFunction();
516: if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) {
517: throw new ShouldNotHappenException();
518: }
519:
520: $nodeCallback(new InFunctionNode($functionReflection, $stmt), $functionScope);
521:
522: $gatheredReturnStatements = [];
523: $gatheredYieldStatements = [];
524: $executionEnds = [];
525: $functionImpurePoints = [];
526: $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $functionScope, static function (Node $node, Scope $scope) use ($nodeCallback, $functionScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$functionImpurePoints): void {
527: $nodeCallback($node, $scope);
528: if ($scope->getFunction() !== $functionScope->getFunction()) {
529: return;
530: }
531: if ($scope->isInAnonymousFunction()) {
532: return;
533: }
534: if ($node instanceof PropertyAssignNode) {
535: $functionImpurePoints[] = new ImpurePoint(
536: $scope,
537: $node,
538: 'propertyAssign',
539: 'property assignment',
540: true,
541: );
542: return;
543: }
544: if ($node instanceof ExecutionEndNode) {
545: $executionEnds[] = $node;
546: return;
547: }
548: if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) {
549: $gatheredYieldStatements[] = $node;
550: }
551: if (!$node instanceof Return_) {
552: return;
553: }
554:
555: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
556: }, StatementContext::createTopLevel());
557:
558: $nodeCallback(new FunctionReturnStatementsNode(
559: $stmt,
560: $gatheredReturnStatements,
561: $gatheredYieldStatements,
562: $statementResult,
563: $executionEnds,
564: array_merge($statementResult->getImpurePoints(), $functionImpurePoints),
565: $functionReflection,
566: ), $functionScope);
567: } elseif ($stmt instanceof Node\Stmt\ClassMethod) {
568: $hasYield = false;
569: $throwPoints = [];
570: $impurePoints = [];
571: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback);
572: [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt);
573:
574: foreach ($stmt->params as $param) {
575: $this->processParamNode($stmt, $param, $scope, $nodeCallback);
576: }
577:
578: if ($stmt->returnType !== null) {
579: $nodeCallback($stmt->returnType, $scope);
580: }
581:
582: $methodScope = $scope->enterClassMethod(
583: $stmt,
584: $templateTypeMap,
585: $phpDocParameterTypes,
586: $phpDocReturnType,
587: $phpDocThrowType,
588: $deprecatedDescription,
589: $isDeprecated,
590: $isInternal,
591: $isFinal,
592: $isPure,
593: $acceptsNamedArguments,
594: $asserts,
595: $selfOutType,
596: $phpDocComment,
597: $phpDocParameterOutTypes,
598: $phpDocImmediatelyInvokedCallableParameters,
599: $phpDocClosureThisTypeParameters,
600: );
601:
602: if (!$scope->isInClass()) {
603: throw new ShouldNotHappenException();
604: }
605:
606: $isFromTrait = $stmt->getAttribute('originalTraitMethodName') === '__construct';
607: if ($isFromTrait || $stmt->name->toLowerString() === '__construct') {
608: foreach ($stmt->params as $param) {
609: if ($param->flags === 0) {
610: continue;
611: }
612:
613: if (!$param->var instanceof Variable || !is_string($param->var->name)) {
614: throw new ShouldNotHappenException();
615: }
616: $phpDoc = null;
617: if ($param->getDocComment() !== null) {
618: $phpDoc = $param->getDocComment()->getText();
619: }
620: $nodeCallback(new ClassPropertyNode(
621: $param->var->name,
622: $param->flags,
623: $param->type,
624: null,
625: $phpDoc,
626: $phpDocParameterTypes[$param->var->name] ?? null,
627: true,
628: $isFromTrait,
629: $param,
630: false,
631: $scope->isInTrait(),
632: $scope->getClassReflection()->isReadOnly(),
633: false,
634: $scope->getClassReflection(),
635: ), $methodScope);
636: $methodScope = $methodScope->assignExpression(new PropertyInitializationExpr($param->var->name), new MixedType(), new MixedType());
637: }
638: }
639:
640: if ($stmt->getAttribute('virtual', false) === false) {
641: $methodReflection = $methodScope->getFunction();
642: if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) {
643: throw new ShouldNotHappenException();
644: }
645: $nodeCallback(new InClassMethodNode($scope->getClassReflection(), $methodReflection, $stmt), $methodScope);
646: }
647:
648: if ($stmt->stmts !== null) {
649: $gatheredReturnStatements = [];
650: $gatheredYieldStatements = [];
651: $executionEnds = [];
652: $methodImpurePoints = [];
653: $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $methodScope, static function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$methodImpurePoints): void {
654: $nodeCallback($node, $scope);
655: if ($scope->getFunction() !== $methodScope->getFunction()) {
656: return;
657: }
658: if ($scope->isInAnonymousFunction()) {
659: return;
660: }
661: if ($node instanceof PropertyAssignNode) {
662: if (
663: $node->getPropertyFetch() instanceof Expr\PropertyFetch
664: && $scope->getFunction() instanceof PhpMethodFromParserNodeReflection
665: && $scope->getFunction()->getDeclaringClass()->hasConstructor()
666: && $scope->getFunction()->getDeclaringClass()->getConstructor()->getName() === $scope->getFunction()->getName()
667: && TypeUtils::findThisType($scope->getType($node->getPropertyFetch()->var)) !== null
668: ) {
669: return;
670: }
671: $methodImpurePoints[] = new ImpurePoint(
672: $scope,
673: $node,
674: 'propertyAssign',
675: 'property assignment',
676: true,
677: );
678: return;
679: }
680: if ($node instanceof ExecutionEndNode) {
681: $executionEnds[] = $node;
682: return;
683: }
684: if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) {
685: $gatheredYieldStatements[] = $node;
686: }
687: if (!$node instanceof Return_) {
688: return;
689: }
690:
691: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
692: }, StatementContext::createTopLevel());
693:
694: $classReflection = $scope->getClassReflection();
695:
696: $methodReflection = $methodScope->getFunction();
697: if (!$methodReflection instanceof ExtendedMethodReflection) {
698: throw new ShouldNotHappenException();
699: }
700:
701: $nodeCallback(new MethodReturnStatementsNode(
702: $stmt,
703: $gatheredReturnStatements,
704: $gatheredYieldStatements,
705: $statementResult,
706: $executionEnds,
707: array_merge($statementResult->getImpurePoints(), $methodImpurePoints),
708: $classReflection,
709: $methodReflection,
710: ), $methodScope);
711: }
712: } elseif ($stmt instanceof Echo_) {
713: $hasYield = false;
714: $throwPoints = [];
715: foreach ($stmt->exprs as $echoExpr) {
716: $result = $this->processExprNode($stmt, $echoExpr, $scope, $nodeCallback, ExpressionContext::createDeep());
717: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
718: $scope = $result->getScope();
719: $hasYield = $hasYield || $result->hasYield();
720: }
721:
722: $throwPoints = $overridingThrowPoints ?? $throwPoints;
723: $impurePoints = [
724: new ImpurePoint($scope, $stmt, 'echo', 'echo', true),
725: ];
726: } elseif ($stmt instanceof Return_) {
727: if ($stmt->expr !== null) {
728: $result = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep());
729: $throwPoints = $result->getThrowPoints();
730: $impurePoints = $result->getImpurePoints();
731: $scope = $result->getScope();
732: $hasYield = $result->hasYield();
733: } else {
734: $hasYield = false;
735: $throwPoints = [];
736: $impurePoints = [];
737: }
738:
739: return new StatementResult($scope, $hasYield, true, [
740: new StatementExitPoint($stmt, $scope),
741: ], $overridingThrowPoints ?? $throwPoints, $impurePoints);
742: } elseif ($stmt instanceof Continue_ || $stmt instanceof Break_) {
743: if ($stmt->num !== null) {
744: $result = $this->processExprNode($stmt, $stmt->num, $scope, $nodeCallback, ExpressionContext::createDeep());
745: $scope = $result->getScope();
746: $hasYield = $result->hasYield();
747: $throwPoints = $result->getThrowPoints();
748: $impurePoints = $result->getImpurePoints();
749: } else {
750: $hasYield = false;
751: $throwPoints = [];
752: $impurePoints = [];
753: }
754:
755: return new StatementResult($scope, $hasYield, true, [
756: new StatementExitPoint($stmt, $scope),
757: ], $overridingThrowPoints ?? $throwPoints, $impurePoints);
758: } elseif ($stmt instanceof Node\Stmt\Expression) {
759: $earlyTerminationExpr = $this->findEarlyTerminatingExpr($stmt->expr, $scope);
760: $hasAssign = false;
761: $currentScope = $scope;
762: $result = $this->processExprNode($stmt, $stmt->expr, $scope, static function (Node $node, Scope $scope) use ($nodeCallback, $currentScope, &$hasAssign): void {
763: $nodeCallback($node, $scope);
764: if ($scope->getAnonymousFunctionReflection() !== $currentScope->getAnonymousFunctionReflection()) {
765: return;
766: }
767: if ($scope->getFunction() !== $currentScope->getFunction()) {
768: return;
769: }
770: if (!$node instanceof VariableAssignNode && !$node instanceof PropertyAssignNode) {
771: return;
772: }
773:
774: $hasAssign = true;
775: }, ExpressionContext::createTopLevel());
776: $throwPoints = array_filter($result->getThrowPoints(), static fn ($throwPoint) => $throwPoint->isExplicit());
777: if (
778: count($result->getImpurePoints()) === 0
779: && count($throwPoints) === 0
780: && !$stmt->expr instanceof Expr\PostInc
781: && !$stmt->expr instanceof Expr\PreInc
782: && !$stmt->expr instanceof Expr\PostDec
783: && !$stmt->expr instanceof Expr\PreDec
784: ) {
785: $nodeCallback(new NoopExpressionNode($stmt->expr, $hasAssign), $scope);
786: }
787: $scope = $result->getScope();
788: $scope = $scope->filterBySpecifiedTypes($this->typeSpecifier->specifyTypesInCondition(
789: $scope,
790: $stmt->expr,
791: TypeSpecifierContext::createNull(),
792: ));
793: $hasYield = $result->hasYield();
794: $throwPoints = $result->getThrowPoints();
795: $impurePoints = $result->getImpurePoints();
796: if ($earlyTerminationExpr !== null) {
797: return new StatementResult($scope, $hasYield, true, [
798: new StatementExitPoint($stmt, $scope),
799: ], $overridingThrowPoints ?? $throwPoints, $impurePoints);
800: }
801: return new StatementResult($scope, $hasYield, false, [], $overridingThrowPoints ?? $throwPoints, $impurePoints);
802: } elseif ($stmt instanceof Node\Stmt\Namespace_) {
803: if ($stmt->name !== null) {
804: $scope = $scope->enterNamespace($stmt->name->toString());
805: }
806:
807: $scope = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context)->getScope();
808: $hasYield = false;
809: $throwPoints = [];
810: $impurePoints = [];
811: } elseif ($stmt instanceof Node\Stmt\Trait_) {
812: return new StatementResult($scope, false, false, [], [], []);
813: } elseif ($stmt instanceof Node\Stmt\ClassLike) {
814: $hasYield = false;
815: $throwPoints = [];
816: $impurePoints = [];
817: if (isset($stmt->namespacedName)) {
818: $classReflection = $this->getCurrentClassReflection($stmt, $stmt->namespacedName->toString(), $scope);
819: $classScope = $scope->enterClass($classReflection);
820: $nodeCallback(new InClassNode($stmt, $classReflection), $classScope);
821: } elseif ($stmt instanceof Class_) {
822: if ($stmt->name === null) {
823: throw new ShouldNotHappenException();
824: }
825: if ($stmt->getAttribute('anonymousClass', false) === false) {
826: $classReflection = $this->reflectionProvider->getClass($stmt->name->toString());
827: } else {
828: $classReflection = $this->reflectionProvider->getAnonymousClassReflection($stmt, $scope);
829: }
830: $classScope = $scope->enterClass($classReflection);
831: $nodeCallback(new InClassNode($stmt, $classReflection), $classScope);
832: } else {
833: throw new ShouldNotHappenException();
834: }
835:
836: $classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback);
837: $this->processAttributeGroups($stmt, $stmt->attrGroups, $classScope, $classStatementsGatherer);
838:
839: $this->processStmtNodes($stmt, $stmt->stmts, $classScope, $classStatementsGatherer, $context);
840: $nodeCallback(new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensionProvider, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classReflection), $classScope);
841: $nodeCallback(new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls(), $classReflection), $classScope);
842: $nodeCallback(new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches(), $classReflection), $classScope);
843: $classReflection->evictPrivateSymbols();
844: $this->calledMethodResults = [];
845: } elseif ($stmt instanceof Node\Stmt\Property) {
846: $hasYield = false;
847: $throwPoints = [];
848: $impurePoints = [];
849: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback);
850:
851: foreach ($stmt->props as $prop) {
852: $nodeCallback($prop, $scope);
853: if ($prop->default !== null) {
854: $this->processExprNode($stmt, $prop->default, $scope, $nodeCallback, ExpressionContext::createDeep());
855: }
856: [,,,,,,,,,,,,$isReadOnly, $docComment, ,,,$varTags, $isAllowedPrivateMutation] = $this->getPhpDocs($scope, $stmt);
857: if (!$scope->isInClass()) {
858: throw new ShouldNotHappenException();
859: }
860: $propertyName = $prop->name->toString();
861: $phpDocType = null;
862: if (isset($varTags[0]) && count($varTags) === 1) {
863: $phpDocType = $varTags[0]->getType();
864: } elseif (isset($varTags[$propertyName])) {
865: $phpDocType = $varTags[$propertyName]->getType();
866: }
867: $nodeCallback(
868: new ClassPropertyNode(
869: $propertyName,
870: $stmt->flags,
871: $stmt->type,
872: $prop->default,
873: $docComment,
874: $phpDocType,
875: false,
876: false,
877: $prop,
878: $isReadOnly,
879: $scope->isInTrait(),
880: $scope->getClassReflection()->isReadOnly(),
881: $isAllowedPrivateMutation,
882: $scope->getClassReflection(),
883: ),
884: $scope,
885: );
886: }
887:
888: if ($stmt->type !== null) {
889: $nodeCallback($stmt->type, $scope);
890: }
891: } elseif ($stmt instanceof Throw_) {
892: $result = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep());
893: $throwPoints = $result->getThrowPoints();
894: $throwPoints[] = ThrowPoint::createExplicit($result->getScope(), $scope->getType($stmt->expr), $stmt, false);
895: $impurePoints = $result->getImpurePoints();
896: return new StatementResult($result->getScope(), $result->hasYield(), true, [
897: new StatementExitPoint($stmt, $scope),
898: ], $throwPoints, $impurePoints);
899: } elseif ($stmt instanceof If_) {
900: $conditionType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean();
901: $ifAlwaysTrue = $conditionType->isTrue()->yes();
902: $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep());
903: $exitPoints = [];
904: $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints();
905: $impurePoints = $condResult->getImpurePoints();
906: $finalScope = null;
907: $alwaysTerminating = true;
908: $hasYield = $condResult->hasYield();
909:
910: $branchScopeStatementResult = $this->processStmtNodes($stmt, $stmt->stmts, $condResult->getTruthyScope(), $nodeCallback, $context);
911:
912: if (!$conditionType instanceof ConstantBooleanType || $conditionType->getValue()) {
913: $exitPoints = $branchScopeStatementResult->getExitPoints();
914: $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints());
915: $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints());
916: $branchScope = $branchScopeStatementResult->getScope();
917: $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? null : $branchScope;
918: $alwaysTerminating = $branchScopeStatementResult->isAlwaysTerminating();
919: $hasYield = $branchScopeStatementResult->hasYield() || $hasYield;
920: }
921:
922: $scope = $condResult->getFalseyScope();
923: $lastElseIfConditionIsTrue = false;
924:
925: $condScope = $scope;
926: foreach ($stmt->elseifs as $elseif) {
927: $nodeCallback($elseif, $scope);
928: $elseIfConditionType = ($this->treatPhpDocTypesAsCertain ? $condScope->getType($elseif->cond) : $scope->getNativeType($elseif->cond))->toBoolean();
929: $condResult = $this->processExprNode($stmt, $elseif->cond, $condScope, $nodeCallback, ExpressionContext::createDeep());
930: $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints());
931: $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints());
932: $condScope = $condResult->getScope();
933: $branchScopeStatementResult = $this->processStmtNodes($elseif, $elseif->stmts, $condResult->getTruthyScope(), $nodeCallback, $context);
934:
935: if (
936: !$ifAlwaysTrue
937: && (
938: !$lastElseIfConditionIsTrue
939: && (
940: !$elseIfConditionType instanceof ConstantBooleanType
941: || $elseIfConditionType->getValue()
942: )
943: )
944: ) {
945: $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints());
946: $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints());
947: $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints());
948: $branchScope = $branchScopeStatementResult->getScope();
949: $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope);
950: $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating();
951: $hasYield = $hasYield || $branchScopeStatementResult->hasYield();
952: }
953:
954: if (
955: $elseIfConditionType->isTrue()->yes()
956: ) {
957: $lastElseIfConditionIsTrue = true;
958: }
959:
960: $condScope = $condScope->filterByFalseyValue($elseif->cond);
961: $scope = $condScope;
962: }
963:
964: if ($stmt->else === null) {
965: if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) {
966: $finalScope = $scope->mergeWith($finalScope);
967: $alwaysTerminating = false;
968: }
969: } else {
970: $nodeCallback($stmt->else, $scope);
971: $branchScopeStatementResult = $this->processStmtNodes($stmt->else, $stmt->else->stmts, $scope, $nodeCallback, $context);
972:
973: if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) {
974: $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints());
975: $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints());
976: $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints());
977: $branchScope = $branchScopeStatementResult->getScope();
978: $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope);
979: $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating();
980: $hasYield = $hasYield || $branchScopeStatementResult->hasYield();
981: }
982: }
983:
984: if ($finalScope === null) {
985: $finalScope = $scope;
986: }
987:
988: return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints);
989: } elseif ($stmt instanceof Node\Stmt\TraitUse) {
990: $hasYield = false;
991: $throwPoints = [];
992: $impurePoints = [];
993: $this->processTraitUse($stmt, $scope, $nodeCallback);
994: } elseif ($stmt instanceof Foreach_) {
995: $condResult = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep());
996: $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints();
997: $impurePoints = $condResult->getImpurePoints();
998: $scope = $condResult->getScope();
999: $arrayComparisonExpr = new BinaryOp\NotIdentical(
1000: $stmt->expr,
1001: new Array_([]),
1002: );
1003: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
1004: $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt);
1005: }
1006: $nodeCallback(new InForeachNode($stmt), $scope);
1007: $originalScope = $scope;
1008: $bodyScope = $scope;
1009:
1010: if ($context->isTopLevel()) {
1011: $originalScope = $this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope;
1012: $bodyScope = $this->enterForeach($originalScope, $originalScope, $stmt);
1013: $count = 0;
1014: do {
1015: $prevScope = $bodyScope;
1016: $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope);
1017: $bodyScope = $this->enterForeach($bodyScope, $originalScope, $stmt);
1018: $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void {
1019: }, $context->enterDeep())->filterOutLoopExitPoints();
1020: $bodyScope = $bodyScopeResult->getScope();
1021: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1022: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1023: }
1024: if ($bodyScope->equals($prevScope)) {
1025: break;
1026: }
1027:
1028: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
1029: $bodyScope = $prevScope->generalizeWith($bodyScope);
1030: }
1031: $count++;
1032: } while ($count < self::LOOP_SCOPE_ITERATIONS);
1033: }
1034:
1035: $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope);
1036: $bodyScope = $this->enterForeach($bodyScope, $originalScope, $stmt);
1037: $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints();
1038: $finalScope = $finalScopeResult->getScope();
1039: foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1040: $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
1041: }
1042: foreach ($finalScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
1043: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1044: }
1045:
1046: $exprType = $scope->getType($stmt->expr);
1047: $isIterableAtLeastOnce = $exprType->isIterableAtLeastOnce();
1048: if ($exprType->isIterable()->no() || $isIterableAtLeastOnce->maybe()) {
1049: $finalScope = $finalScope->mergeWith($scope->filterByTruthyValue(new BooleanOr(
1050: new BinaryOp\Identical(
1051: $stmt->expr,
1052: new Array_([]),
1053: ),
1054: new FuncCall(new Name\FullyQualified('is_object'), [
1055: new Arg($stmt->expr),
1056: ]),
1057: )));
1058: } elseif ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) {
1059: $finalScope = $scope;
1060: } elseif (!$this->polluteScopeWithAlwaysIterableForeach) {
1061: $finalScope = $scope->processAlwaysIterableForeachScopeWithoutPollute($finalScope);
1062: // get types from finalScope, but don't create new variables
1063: }
1064:
1065: if (!$isIterableAtLeastOnce->no()) {
1066: $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints());
1067: $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints());
1068: }
1069: if (!(new ObjectType(Traversable::class))->isSuperTypeOf($scope->getType($stmt->expr))->no()) {
1070: $throwPoints[] = ThrowPoint::createImplicit($scope, $stmt->expr);
1071: }
1072:
1073: return new StatementResult(
1074: $finalScope,
1075: $finalScopeResult->hasYield() || $condResult->hasYield(),
1076: $isIterableAtLeastOnce->yes() && $finalScopeResult->isAlwaysTerminating(),
1077: $finalScopeResult->getExitPointsForOuterLoop(),
1078: $throwPoints,
1079: $impurePoints,
1080: );
1081: } elseif ($stmt instanceof While_) {
1082: $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, static function (): void {
1083: }, ExpressionContext::createDeep());
1084: $bodyScope = $condResult->getTruthyScope();
1085:
1086: if ($context->isTopLevel()) {
1087: $count = 0;
1088: do {
1089: $prevScope = $bodyScope;
1090: $bodyScope = $bodyScope->mergeWith($scope);
1091: $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, static function (): void {
1092: }, ExpressionContext::createDeep())->getTruthyScope();
1093: $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void {
1094: }, $context->enterDeep())->filterOutLoopExitPoints();
1095: $bodyScope = $bodyScopeResult->getScope();
1096: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1097: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1098: }
1099: if ($bodyScope->equals($prevScope)) {
1100: break;
1101: }
1102:
1103: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
1104: $bodyScope = $prevScope->generalizeWith($bodyScope);
1105: }
1106: $count++;
1107: } while ($count < self::LOOP_SCOPE_ITERATIONS);
1108: }
1109:
1110: $bodyScope = $bodyScope->mergeWith($scope);
1111: $bodyScopeMaybeRan = $bodyScope;
1112: $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope();
1113: $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints();
1114: $finalScope = $finalScopeResult->getScope()->filterByFalseyValue($stmt->cond);
1115:
1116: $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScopeMaybeRan->getType($stmt->cond) : $bodyScopeMaybeRan->getNativeType($stmt->cond))->toBoolean();
1117: $alwaysIterates = $condBooleanType->isTrue()->yes() && $context->isTopLevel();
1118: $neverIterates = $condBooleanType->isFalse()->yes() && $context->isTopLevel();
1119: if (!$alwaysIterates) {
1120: foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1121: $finalScope = $finalScope->mergeWith($continueExitPoint->getScope());
1122: }
1123: }
1124:
1125: $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class);
1126: foreach ($breakExitPoints as $breakExitPoint) {
1127: $finalScope = $finalScope->mergeWith($breakExitPoint->getScope());
1128: }
1129:
1130: $beforeCondBooleanType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean();
1131: $isIterableAtLeastOnce = $beforeCondBooleanType->isTrue()->yes();
1132: $nodeCallback(new BreaklessWhileLoopNode($stmt, $finalScopeResult->getExitPoints()), $bodyScopeMaybeRan);
1133:
1134: if ($alwaysIterates) {
1135: $isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0;
1136: } elseif ($isIterableAtLeastOnce) {
1137: $isAlwaysTerminating = $finalScopeResult->isAlwaysTerminating();
1138: } else {
1139: $isAlwaysTerminating = false;
1140: }
1141: $condScope = $condResult->getFalseyScope();
1142: if (!$isIterableAtLeastOnce) {
1143: if (!$this->polluteScopeWithLoopInitialAssignments) {
1144: $condScope = $condScope->mergeWith($scope);
1145: }
1146: $finalScope = $finalScope->mergeWith($condScope);
1147: }
1148:
1149: $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints();
1150: $impurePoints = $condResult->getImpurePoints();
1151: if (!$neverIterates) {
1152: $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints());
1153: $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints());
1154: }
1155:
1156: return new StatementResult(
1157: $finalScope,
1158: $finalScopeResult->hasYield() || $condResult->hasYield(),
1159: $isAlwaysTerminating,
1160: $finalScopeResult->getExitPointsForOuterLoop(),
1161: $throwPoints,
1162: $impurePoints,
1163: );
1164: } elseif ($stmt instanceof Do_) {
1165: $finalScope = null;
1166: $bodyScope = $scope;
1167: $count = 0;
1168: $hasYield = false;
1169: $throwPoints = [];
1170: $impurePoints = [];
1171:
1172: if ($context->isTopLevel()) {
1173: do {
1174: $prevScope = $bodyScope;
1175: $bodyScope = $bodyScope->mergeWith($scope);
1176: $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void {
1177: }, $context->enterDeep())->filterOutLoopExitPoints();
1178: $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating();
1179: $bodyScope = $bodyScopeResult->getScope();
1180: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1181: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1182: }
1183: $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope);
1184: foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
1185: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1186: }
1187: $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, static function (): void {
1188: }, ExpressionContext::createDeep())->getTruthyScope();
1189: if ($bodyScope->equals($prevScope)) {
1190: break;
1191: }
1192:
1193: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
1194: $bodyScope = $prevScope->generalizeWith($bodyScope);
1195: }
1196: $count++;
1197: } while ($count < self::LOOP_SCOPE_ITERATIONS);
1198:
1199: $bodyScope = $bodyScope->mergeWith($scope);
1200: }
1201:
1202: $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints();
1203: $bodyScope = $bodyScopeResult->getScope();
1204: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1205: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1206: }
1207: $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScope->getType($stmt->cond) : $bodyScope->getNativeType($stmt->cond))->toBoolean();
1208: $alwaysIterates = $condBooleanType->isTrue()->yes() && $context->isTopLevel();
1209:
1210: $nodeCallback(new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->getExitPoints()), $bodyScope);
1211:
1212: if ($alwaysIterates) {
1213: $alwaysTerminating = count($bodyScopeResult->getExitPointsByType(Break_::class)) === 0;
1214: } else {
1215: $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating();
1216: }
1217: $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope);
1218: if ($finalScope === null) {
1219: $finalScope = $scope;
1220: }
1221: if (!$alwaysTerminating) {
1222: $condResult = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep());
1223: $hasYield = $condResult->hasYield();
1224: $throwPoints = $condResult->getThrowPoints();
1225: $impurePoints = $condResult->getImpurePoints();
1226: $finalScope = $condResult->getFalseyScope();
1227: } else {
1228: $this->processExprNode($stmt, $stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep());
1229: }
1230: foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
1231: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1232: }
1233:
1234: return new StatementResult(
1235: $finalScope,
1236: $bodyScopeResult->hasYield() || $hasYield,
1237: $alwaysTerminating,
1238: $bodyScopeResult->getExitPointsForOuterLoop(),
1239: array_merge($throwPoints, $bodyScopeResult->getThrowPoints()),
1240: array_merge($impurePoints, $bodyScopeResult->getImpurePoints()),
1241: );
1242: } elseif ($stmt instanceof For_) {
1243: $initScope = $scope;
1244: $hasYield = false;
1245: $throwPoints = [];
1246: $impurePoints = [];
1247: foreach ($stmt->init as $initExpr) {
1248: $initResult = $this->processExprNode($stmt, $initExpr, $initScope, $nodeCallback, ExpressionContext::createTopLevel());
1249: $initScope = $initResult->getScope();
1250: $hasYield = $hasYield || $initResult->hasYield();
1251: $throwPoints = array_merge($throwPoints, $initResult->getThrowPoints());
1252: $impurePoints = array_merge($impurePoints, $initResult->getImpurePoints());
1253: }
1254:
1255: $bodyScope = $initScope;
1256: $isIterableAtLeastOnce = TrinaryLogic::createYes();
1257: foreach ($stmt->cond as $condExpr) {
1258: $condResult = $this->processExprNode($stmt, $condExpr, $bodyScope, static function (): void {
1259: }, ExpressionContext::createDeep());
1260: $initScope = $condResult->getScope();
1261: $condResultScope = $condResult->getScope();
1262: $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResultScope->getType($condExpr) : $condResultScope->getNativeType($condExpr))->toBoolean();
1263: if ($condTruthiness instanceof ConstantBooleanType) {
1264: $condTruthinessTrinary = TrinaryLogic::createFromBoolean($condTruthiness->getValue());
1265: } else {
1266: $condTruthinessTrinary = TrinaryLogic::createMaybe();
1267: }
1268: $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthinessTrinary);
1269: $hasYield = $hasYield || $condResult->hasYield();
1270: $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints());
1271: $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints());
1272: $bodyScope = $condResult->getTruthyScope();
1273: }
1274:
1275: if ($context->isTopLevel()) {
1276: $count = 0;
1277: do {
1278: $prevScope = $bodyScope;
1279: $bodyScope = $bodyScope->mergeWith($initScope);
1280: foreach ($stmt->cond as $condExpr) {
1281: $bodyScope = $this->processExprNode($stmt, $condExpr, $bodyScope, static function (): void {
1282: }, ExpressionContext::createDeep())->getTruthyScope();
1283: }
1284: $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void {
1285: }, $context->enterDeep())->filterOutLoopExitPoints();
1286: $bodyScope = $bodyScopeResult->getScope();
1287: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1288: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1289: }
1290: foreach ($stmt->loop as $loopExpr) {
1291: $exprResult = $this->processExprNode($stmt, $loopExpr, $bodyScope, static function (): void {
1292: }, ExpressionContext::createTopLevel());
1293: $bodyScope = $exprResult->getScope();
1294: $hasYield = $hasYield || $exprResult->hasYield();
1295: $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
1296: $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints());
1297: }
1298:
1299: if ($bodyScope->equals($prevScope)) {
1300: break;
1301: }
1302:
1303: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
1304: $bodyScope = $prevScope->generalizeWith($bodyScope);
1305: }
1306: $count++;
1307: } while ($count < self::LOOP_SCOPE_ITERATIONS);
1308: }
1309:
1310: $bodyScope = $bodyScope->mergeWith($initScope);
1311: foreach ($stmt->cond as $condExpr) {
1312: $bodyScope = $this->processExprNode($stmt, $condExpr, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope();
1313: }
1314:
1315: $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints();
1316: $finalScope = $finalScopeResult->getScope();
1317: foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1318: $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
1319: }
1320:
1321: $loopScope = $finalScope;
1322: foreach ($stmt->loop as $loopExpr) {
1323: $loopScope = $this->processExprNode($stmt, $loopExpr, $loopScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope();
1324: }
1325: $finalScope = $finalScope->generalizeWith($loopScope);
1326: foreach ($stmt->cond as $condExpr) {
1327: $finalScope = $finalScope->filterByFalseyValue($condExpr);
1328: }
1329:
1330: foreach ($finalScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
1331: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1332: }
1333:
1334: if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) {
1335: if ($this->polluteScopeWithLoopInitialAssignments) {
1336: $finalScope = $initScope;
1337: } else {
1338: $finalScope = $scope;
1339: }
1340:
1341: } elseif ($isIterableAtLeastOnce->maybe()) {
1342: if ($this->polluteScopeWithLoopInitialAssignments) {
1343: $finalScope = $finalScope->mergeWith($initScope);
1344: } else {
1345: $finalScope = $finalScope->mergeWith($scope);
1346: }
1347: } else {
1348: if (!$this->polluteScopeWithLoopInitialAssignments) {
1349: $finalScope = $finalScope->mergeWith($scope);
1350: }
1351: }
1352:
1353: return new StatementResult(
1354: $finalScope,
1355: $finalScopeResult->hasYield() || $hasYield,
1356: false/* $finalScopeResult->isAlwaysTerminating() && $isAlwaysIterable*/,
1357: $finalScopeResult->getExitPointsForOuterLoop(),
1358: array_merge($throwPoints, $finalScopeResult->getThrowPoints()),
1359: array_merge($impurePoints, $finalScopeResult->getImpurePoints()),
1360: );
1361: } elseif ($stmt instanceof Switch_) {
1362: $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep());
1363: $scope = $condResult->getScope();
1364: $scopeForBranches = $scope;
1365: $finalScope = null;
1366: $prevScope = null;
1367: $hasDefaultCase = false;
1368: $alwaysTerminating = true;
1369: $hasYield = $condResult->hasYield();
1370: $exitPointsForOuterLoop = [];
1371: $throwPoints = $condResult->getThrowPoints();
1372: $impurePoints = $condResult->getImpurePoints();
1373: foreach ($stmt->cases as $caseNode) {
1374: if ($caseNode->cond !== null) {
1375: $condExpr = new BinaryOp\Equal($stmt->cond, $caseNode->cond);
1376: $caseResult = $this->processExprNode($stmt, $caseNode->cond, $scopeForBranches, $nodeCallback, ExpressionContext::createDeep());
1377: $scopeForBranches = $caseResult->getScope();
1378: $hasYield = $hasYield || $caseResult->hasYield();
1379: $throwPoints = array_merge($throwPoints, $caseResult->getThrowPoints());
1380: $impurePoints = array_merge($impurePoints, $caseResult->getImpurePoints());
1381: $branchScope = $caseResult->getTruthyScope()->filterByTruthyValue($condExpr);
1382: } else {
1383: $hasDefaultCase = true;
1384: $branchScope = $scopeForBranches;
1385: }
1386:
1387: $branchScope = $branchScope->mergeWith($prevScope);
1388: $branchScopeResult = $this->processStmtNodes($caseNode, $caseNode->stmts, $branchScope, $nodeCallback, $context);
1389: $branchScope = $branchScopeResult->getScope();
1390: $branchFinalScopeResult = $branchScopeResult->filterOutLoopExitPoints();
1391: $hasYield = $hasYield || $branchFinalScopeResult->hasYield();
1392: foreach ($branchScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
1393: $alwaysTerminating = false;
1394: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1395: }
1396: foreach ($branchScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1397: $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
1398: }
1399: $exitPointsForOuterLoop = array_merge($exitPointsForOuterLoop, $branchFinalScopeResult->getExitPointsForOuterLoop());
1400: $throwPoints = array_merge($throwPoints, $branchFinalScopeResult->getThrowPoints());
1401: $impurePoints = array_merge($impurePoints, $branchFinalScopeResult->getImpurePoints());
1402: if ($branchScopeResult->isAlwaysTerminating()) {
1403: $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating();
1404: $prevScope = null;
1405: if (isset($condExpr)) {
1406: $scopeForBranches = $scopeForBranches->filterByFalseyValue($condExpr);
1407: }
1408: if (!$branchFinalScopeResult->isAlwaysTerminating()) {
1409: $finalScope = $branchScope->mergeWith($finalScope);
1410: }
1411: } else {
1412: $prevScope = $branchScope;
1413: }
1414: }
1415:
1416: $exhaustive = $scopeForBranches->getType($stmt->cond) instanceof NeverType;
1417:
1418: if (!$hasDefaultCase && !$exhaustive) {
1419: $alwaysTerminating = false;
1420: }
1421:
1422: if ($prevScope !== null && isset($branchFinalScopeResult)) {
1423: $finalScope = $prevScope->mergeWith($finalScope);
1424: $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating();
1425: }
1426:
1427: if ((!$hasDefaultCase && !$exhaustive) || $finalScope === null) {
1428: $finalScope = $scope->mergeWith($finalScope);
1429: }
1430:
1431: return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPointsForOuterLoop, $throwPoints, $impurePoints);
1432: } elseif ($stmt instanceof TryCatch) {
1433: $branchScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context);
1434: $branchScope = $branchScopeResult->getScope();
1435: $finalScope = $branchScopeResult->isAlwaysTerminating() ? null : $branchScope;
1436:
1437: $exitPoints = [];
1438: $finallyExitPoints = [];
1439: $alwaysTerminating = $branchScopeResult->isAlwaysTerminating();
1440: $hasYield = $branchScopeResult->hasYield();
1441:
1442: if ($stmt->finally !== null) {
1443: $finallyScope = $branchScope;
1444: } else {
1445: $finallyScope = null;
1446: }
1447: foreach ($branchScopeResult->getExitPoints() as $exitPoint) {
1448: $finallyExitPoints[] = $exitPoint;
1449: if ($exitPoint->getStatement() instanceof Throw_) {
1450: continue;
1451: }
1452: if ($finallyScope !== null) {
1453: $finallyScope = $finallyScope->mergeWith($exitPoint->getScope());
1454: }
1455: $exitPoints[] = $exitPoint;
1456: }
1457:
1458: $throwPoints = $branchScopeResult->getThrowPoints();
1459: $impurePoints = $branchScopeResult->getImpurePoints();
1460: $throwPointsForLater = [];
1461: $pastCatchTypes = new NeverType();
1462:
1463: foreach ($stmt->catches as $catchNode) {
1464: $nodeCallback($catchNode, $scope);
1465:
1466: $originalCatchTypes = array_map(static fn (Name $name): Type => new ObjectType($name->toString()), $catchNode->types);
1467: $catchTypes = array_map(static fn (Type $type): Type => TypeCombinator::remove($type, $pastCatchTypes), $originalCatchTypes);
1468:
1469: $originalCatchType = TypeCombinator::union(...$originalCatchTypes);
1470: $catchType = TypeCombinator::union(...$catchTypes);
1471: $pastCatchTypes = TypeCombinator::union($pastCatchTypes, $originalCatchType);
1472:
1473: $matchingThrowPoints = [];
1474: $matchingCatchTypes = array_fill_keys(array_keys($originalCatchTypes), false);
1475:
1476: // throwable matches all
1477: foreach ($originalCatchTypes as $catchTypeIndex => $catchTypeItem) {
1478: if (!$catchTypeItem->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) {
1479: continue;
1480: }
1481:
1482: foreach ($throwPoints as $throwPointIndex => $throwPoint) {
1483: $matchingThrowPoints[$throwPointIndex] = $throwPoint;
1484: $matchingCatchTypes[$catchTypeIndex] = true;
1485: }
1486: }
1487:
1488: // explicit only
1489: if (count($matchingThrowPoints) === 0) {
1490: foreach ($throwPoints as $throwPointIndex => $throwPoint) {
1491: foreach ($catchTypes as $catchTypeIndex => $catchTypeItem) {
1492: if ($catchTypeItem->isSuperTypeOf($throwPoint->getType())->no()) {
1493: continue;
1494: }
1495:
1496: $matchingCatchTypes[$catchTypeIndex] = true;
1497: if (!$throwPoint->isExplicit()) {
1498: continue;
1499: }
1500: $matchingThrowPoints[$throwPointIndex] = $throwPoint;
1501: }
1502: }
1503: }
1504:
1505: // implicit only
1506: if (count($matchingThrowPoints) === 0) {
1507: foreach ($throwPoints as $throwPointIndex => $throwPoint) {
1508: if ($throwPoint->isExplicit()) {
1509: continue;
1510: }
1511:
1512: foreach ($catchTypes as $catchTypeIndex => $catchTypeItem) {
1513: if ($catchTypeItem->isSuperTypeOf($throwPoint->getType())->no()) {
1514: continue;
1515: }
1516:
1517: $matchingThrowPoints[$throwPointIndex] = $throwPoint;
1518: }
1519: }
1520: }
1521:
1522: // include previously removed throw points
1523: if (count($matchingThrowPoints) === 0) {
1524: if ($originalCatchType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) {
1525: foreach ($branchScopeResult->getThrowPoints() as $originalThrowPoint) {
1526: if (!$originalThrowPoint->canContainAnyThrowable()) {
1527: continue;
1528: }
1529:
1530: $matchingThrowPoints[] = $originalThrowPoint;
1531: $matchingCatchTypes = array_fill_keys(array_keys($originalCatchTypes), true);
1532: }
1533: }
1534: }
1535:
1536: // emit error
1537: if ($this->detectDeadTypeInMultiCatch) {
1538: foreach ($matchingCatchTypes as $catchTypeIndex => $matched) {
1539: if ($matched) {
1540: continue;
1541: }
1542: $nodeCallback(new CatchWithUnthrownExceptionNode($catchNode, $catchTypes[$catchTypeIndex], $originalCatchTypes[$catchTypeIndex]), $scope);
1543: }
1544: }
1545:
1546: if (count($matchingThrowPoints) === 0) {
1547: if (!$this->detectDeadTypeInMultiCatch) {
1548: $nodeCallback(new CatchWithUnthrownExceptionNode($catchNode, $catchType, $originalCatchType), $scope);
1549: }
1550: continue;
1551: }
1552:
1553: // recompute throw points
1554: $newThrowPoints = [];
1555: foreach ($throwPoints as $throwPoint) {
1556: $newThrowPoint = $throwPoint->subtractCatchType($originalCatchType);
1557:
1558: if ($newThrowPoint->getType() instanceof NeverType) {
1559: continue;
1560: }
1561:
1562: $newThrowPoints[] = $newThrowPoint;
1563: }
1564: $throwPoints = $newThrowPoints;
1565:
1566: $catchScope = null;
1567: foreach ($matchingThrowPoints as $matchingThrowPoint) {
1568: if ($catchScope === null) {
1569: $catchScope = $matchingThrowPoint->getScope();
1570: } else {
1571: $catchScope = $catchScope->mergeWith($matchingThrowPoint->getScope());
1572: }
1573: }
1574:
1575: $variableName = null;
1576: if ($catchNode->var !== null) {
1577: if (!is_string($catchNode->var->name)) {
1578: throw new ShouldNotHappenException();
1579: }
1580:
1581: $variableName = $catchNode->var->name;
1582: }
1583:
1584: $catchScopeResult = $this->processStmtNodes($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $nodeCallback, $context);
1585: $catchScopeForFinally = $catchScopeResult->getScope();
1586:
1587: $finalScope = $catchScopeResult->isAlwaysTerminating() ? $finalScope : $catchScopeResult->getScope()->mergeWith($finalScope);
1588: $alwaysTerminating = $alwaysTerminating && $catchScopeResult->isAlwaysTerminating();
1589: $hasYield = $hasYield || $catchScopeResult->hasYield();
1590: $catchThrowPoints = $catchScopeResult->getThrowPoints();
1591: $impurePoints = array_merge($impurePoints, $catchScopeResult->getImpurePoints());
1592: $throwPointsForLater = array_merge($throwPointsForLater, $catchThrowPoints);
1593:
1594: if ($finallyScope !== null) {
1595: $finallyScope = $finallyScope->mergeWith($catchScopeForFinally);
1596: }
1597: foreach ($catchScopeResult->getExitPoints() as $exitPoint) {
1598: $finallyExitPoints[] = $exitPoint;
1599: if ($exitPoint->getStatement() instanceof Throw_) {
1600: continue;
1601: }
1602: if ($finallyScope !== null) {
1603: $finallyScope = $finallyScope->mergeWith($exitPoint->getScope());
1604: }
1605: $exitPoints[] = $exitPoint;
1606: }
1607:
1608: foreach ($catchThrowPoints as $catchThrowPoint) {
1609: if ($finallyScope === null) {
1610: continue;
1611: }
1612: $finallyScope = $finallyScope->mergeWith($catchThrowPoint->getScope());
1613: }
1614: }
1615:
1616: if ($finalScope === null) {
1617: $finalScope = $scope;
1618: }
1619:
1620: foreach ($throwPoints as $throwPoint) {
1621: if ($finallyScope === null) {
1622: continue;
1623: }
1624: $finallyScope = $finallyScope->mergeWith($throwPoint->getScope());
1625: }
1626:
1627: if ($finallyScope !== null && $stmt->finally !== null) {
1628: $originalFinallyScope = $finallyScope;
1629: $finallyResult = $this->processStmtNodes($stmt->finally, $stmt->finally->stmts, $finallyScope, $nodeCallback, $context);
1630: $alwaysTerminating = $alwaysTerminating || $finallyResult->isAlwaysTerminating();
1631: $hasYield = $hasYield || $finallyResult->hasYield();
1632: $throwPointsForLater = array_merge($throwPointsForLater, $finallyResult->getThrowPoints());
1633: $impurePoints = array_merge($impurePoints, $finallyResult->getImpurePoints());
1634: $finallyScope = $finallyResult->getScope();
1635: $finalScope = $finallyResult->isAlwaysTerminating() ? $finalScope : $finalScope->processFinallyScope($finallyScope, $originalFinallyScope);
1636: if (count($finallyResult->getExitPoints()) > 0) {
1637: $nodeCallback(new FinallyExitPointsNode(
1638: $finallyResult->getExitPoints(),
1639: $finallyExitPoints,
1640: ), $scope);
1641: }
1642: $exitPoints = array_merge($exitPoints, $finallyResult->getExitPoints());
1643: }
1644:
1645: return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, array_merge($throwPoints, $throwPointsForLater), $impurePoints);
1646: } elseif ($stmt instanceof Unset_) {
1647: $hasYield = false;
1648: $throwPoints = [];
1649: $impurePoints = [];
1650: foreach ($stmt->vars as $var) {
1651: $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var);
1652: $exprResult = $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep());
1653: $scope = $exprResult->getScope();
1654: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
1655: $hasYield = $hasYield || $exprResult->hasYield();
1656: $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
1657: $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints());
1658: if ($var instanceof ArrayDimFetch && $var->dim !== null) {
1659: $cloningTraverser = new NodeTraverser();
1660: $cloningTraverser->addVisitor(new CloningVisitor());
1661:
1662: /** @var Expr $clonedVar */
1663: [$clonedVar] = $cloningTraverser->traverse([$var->var]);
1664:
1665: $traverser = new NodeTraverser();
1666: $traverser->addVisitor(new class () extends NodeVisitorAbstract {
1667:
1668: public function leaveNode(Node $node): ?ExistingArrayDimFetch
1669: {
1670: if (!$node instanceof ArrayDimFetch || $node->dim === null) {
1671: return null;
1672: }
1673:
1674: return new ExistingArrayDimFetch($node->var, $node->dim);
1675: }
1676:
1677: });
1678:
1679: /** @var Expr $clonedVar */
1680: [$clonedVar] = $traverser->traverse([$clonedVar]);
1681: $scope = $this->processAssignVar(
1682: $scope,
1683: $stmt,
1684: $clonedVar,
1685: new UnsetOffsetExpr($var->var, $var->dim),
1686: static function (Node $node, Scope $scope) use ($nodeCallback): void {
1687: if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) {
1688: return;
1689: }
1690:
1691: $nodeCallback($node, $scope);
1692: },
1693: ExpressionContext::createDeep(),
1694: static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []),
1695: false,
1696: )->getScope();
1697: } elseif ($var instanceof PropertyFetch) {
1698: $scope = $scope->invalidateExpression($var);
1699: $impurePoints[] = new ImpurePoint(
1700: $scope,
1701: $var,
1702: 'propertyUnset',
1703: 'property unset',
1704: true,
1705: );
1706: } else {
1707: $scope = $scope->invalidateExpression($var);
1708: }
1709:
1710: }
1711: } elseif ($stmt instanceof Node\Stmt\Use_) {
1712: $hasYield = false;
1713: $throwPoints = [];
1714: $impurePoints = [];
1715: foreach ($stmt->uses as $use) {
1716: $nodeCallback($use, $scope);
1717: }
1718: } elseif ($stmt instanceof Node\Stmt\Global_) {
1719: $hasYield = false;
1720: $throwPoints = [];
1721: $impurePoints = [
1722: new ImpurePoint(
1723: $scope,
1724: $stmt,
1725: 'global',
1726: 'global variable',
1727: true,
1728: ),
1729: ];
1730: $vars = [];
1731: foreach ($stmt->vars as $var) {
1732: if (!$var instanceof Variable) {
1733: throw new ShouldNotHappenException();
1734: }
1735: $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var);
1736: $varResult = $this->processExprNode($stmt, $var, $scope, $nodeCallback, ExpressionContext::createDeep());
1737: $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints());
1738: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
1739:
1740: if (!is_string($var->name)) {
1741: continue;
1742: }
1743:
1744: $scope = $scope->assignVariable($var->name, new MixedType(), new MixedType());
1745: $vars[] = $var->name;
1746: }
1747: $scope = $this->processVarAnnotation($scope, $vars, $stmt);
1748: } elseif ($stmt instanceof Static_) {
1749: $hasYield = false;
1750: $throwPoints = [];
1751: $impurePoints = [
1752: new ImpurePoint(
1753: $scope,
1754: $stmt,
1755: 'static',
1756: 'static variable',
1757: true,
1758: ),
1759: ];
1760:
1761: $vars = [];
1762: foreach ($stmt->vars as $var) {
1763: if (!is_string($var->var->name)) {
1764: throw new ShouldNotHappenException();
1765: }
1766:
1767: if ($var->default !== null) {
1768: $defaultExprResult = $this->processExprNode($stmt, $var->default, $scope, $nodeCallback, ExpressionContext::createDeep());
1769: $impurePoints = array_merge($impurePoints, $defaultExprResult->getImpurePoints());
1770: }
1771:
1772: $scope = $scope->enterExpressionAssign($var->var);
1773: $varResult = $this->processExprNode($stmt, $var->var, $scope, $nodeCallback, ExpressionContext::createDeep());
1774: $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints());
1775: $scope = $scope->exitExpressionAssign($var->var);
1776:
1777: $scope = $scope->assignVariable($var->var->name, new MixedType(), new MixedType());
1778: $vars[] = $var->var->name;
1779: }
1780:
1781: $scope = $this->processVarAnnotation($scope, $vars, $stmt);
1782: } elseif ($stmt instanceof Node\Stmt\Const_) {
1783: $hasYield = false;
1784: $throwPoints = [];
1785: $impurePoints = [];
1786: foreach ($stmt->consts as $const) {
1787: $nodeCallback($const, $scope);
1788: $constResult = $this->processExprNode($stmt, $const->value, $scope, $nodeCallback, ExpressionContext::createDeep());
1789: $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints());
1790: if ($const->namespacedName !== null) {
1791: $constantName = new Name\FullyQualified($const->namespacedName->toString());
1792: } else {
1793: if ($const->name->toString() === '') {
1794: throw new ShouldNotHappenException('Constant cannot have a empty name');
1795: }
1796: $constantName = new Name\FullyQualified($const->name->toString());
1797: }
1798: $scope = $scope->assignExpression(new ConstFetch($constantName), $scope->getType($const->value), $scope->getNativeType($const->value));
1799: }
1800: } elseif ($stmt instanceof Node\Stmt\ClassConst) {
1801: $hasYield = false;
1802: $throwPoints = [];
1803: $impurePoints = [];
1804: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback);
1805: foreach ($stmt->consts as $const) {
1806: $nodeCallback($const, $scope);
1807: $constResult = $this->processExprNode($stmt, $const->value, $scope, $nodeCallback, ExpressionContext::createDeep());
1808: $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints());
1809: if ($scope->getClassReflection() === null) {
1810: throw new ShouldNotHappenException();
1811: }
1812: $scope = $scope->assignExpression(
1813: new Expr\ClassConstFetch(new Name\FullyQualified($scope->getClassReflection()->getName()), $const->name),
1814: $scope->getType($const->value),
1815: $scope->getNativeType($const->value),
1816: );
1817: }
1818: } elseif ($stmt instanceof Node\Stmt\EnumCase) {
1819: $hasYield = false;
1820: $throwPoints = [];
1821: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $nodeCallback);
1822: $impurePoints = [];
1823: if ($stmt->expr !== null) {
1824: $exprResult = $this->processExprNode($stmt, $stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep());
1825: $impurePoints = $exprResult->getImpurePoints();
1826: }
1827: } elseif ($stmt instanceof InlineHTML) {
1828: $hasYield = false;
1829: $throwPoints = [];
1830: $impurePoints = [
1831: new ImpurePoint($scope, $stmt, 'betweenPhpTags', 'output between PHP opening and closing tags', true),
1832: ];
1833: } elseif ($stmt instanceof Node\Stmt\Nop) {
1834: $hasYield = false;
1835: $throwPoints = $overridingThrowPoints ?? [];
1836: $impurePoints = [];
1837: } elseif ($stmt instanceof Node\Stmt\GroupUse) {
1838: $hasYield = false;
1839: $throwPoints = [];
1840: foreach ($stmt->uses as $use) {
1841: $nodeCallback($use, $scope);
1842: }
1843: $impurePoints = [];
1844: } else {
1845: $hasYield = false;
1846: $throwPoints = $overridingThrowPoints ?? [];
1847: $impurePoints = [];
1848: }
1849:
1850: return new StatementResult($scope, $hasYield, false, [], $throwPoints, $impurePoints);
1851: }
1852:
1853: /**
1854: * @return ThrowPoint[]|null
1855: */
1856: private function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $scope): ?array
1857: {
1858: foreach ($statement->getComments() as $comment) {
1859: if (!$comment instanceof Doc) {
1860: continue;
1861: }
1862:
1863: $function = $scope->getFunction();
1864: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
1865: $scope->getFile(),
1866: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
1867: $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
1868: $function !== null ? $function->getName() : null,
1869: $comment->getText(),
1870: );
1871:
1872: $throwsTag = $resolvedPhpDoc->getThrowsTag();
1873: if ($throwsTag !== null) {
1874: $throwsType = $throwsTag->getType();
1875: if ($throwsType->isVoid()->yes()) {
1876: return [];
1877: }
1878:
1879: return [ThrowPoint::createExplicit($scope, $throwsType, $statement, false)];
1880: }
1881: }
1882:
1883: return null;
1884: }
1885:
1886: private function getCurrentClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection
1887: {
1888: if (!$this->reflectionProvider->hasClass($className)) {
1889: return $this->createAstClassReflection($stmt, $className, $scope);
1890: }
1891:
1892: $defaultClassReflection = $this->reflectionProvider->getClass($className);
1893: if ($defaultClassReflection->getFileName() !== $scope->getFile()) {
1894: return $this->createAstClassReflection($stmt, $className, $scope);
1895: }
1896:
1897: $startLine = $defaultClassReflection->getNativeReflection()->getStartLine();
1898: if ($startLine !== $stmt->getStartLine()) {
1899: return $this->createAstClassReflection($stmt, $className, $scope);
1900: }
1901:
1902: return $defaultClassReflection;
1903: }
1904:
1905: private function createAstClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection
1906: {
1907: $nodeToReflection = new NodeToReflection();
1908: $betterReflectionClass = $nodeToReflection->__invoke(
1909: $this->reflector,
1910: $stmt,
1911: new LocatedSource(FileReader::read($scope->getFile()), $className, $scope->getFile()),
1912: $scope->getNamespace() !== null ? new Node\Stmt\Namespace_(new Name($scope->getNamespace())) : null,
1913: );
1914: if (!$betterReflectionClass instanceof \PHPStan\BetterReflection\Reflection\ReflectionClass) {
1915: throw new ShouldNotHappenException();
1916: }
1917:
1918: $enumAdapter = base64_decode('UEhQU3RhblxCZXR0ZXJSZWZsZWN0aW9uXFJlZmxlY3Rpb25cQWRhcHRlclxSZWZsZWN0aW9uRW51bQ==', true);
1919:
1920: return new ClassReflection(
1921: $this->reflectionProvider,
1922: $this->initializerExprTypeResolver,
1923: $this->fileTypeMapper,
1924: $this->stubPhpDocProvider,
1925: $this->phpDocInheritanceResolver,
1926: $this->phpVersion,
1927: $this->signatureMapProvider,
1928: $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(),
1929: $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(),
1930: $this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(),
1931: $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsPropertyClassReflectionExtension(),
1932: $this->classReflectionExtensionRegistryProvider->getRegistry()->getRequireExtendsMethodsClassReflectionExtension(),
1933: $betterReflectionClass->getName(),
1934: $betterReflectionClass instanceof ReflectionEnum && PHP_VERSION_ID >= 80000 ? new $enumAdapter($betterReflectionClass) : new ReflectionClass($betterReflectionClass),
1935: null,
1936: null,
1937: null,
1938: $this->universalObjectCratesClasses,
1939: sprintf('%s:%d', $scope->getFile(), $stmt->getStartLine()),
1940: );
1941: }
1942:
1943: private function lookForSetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope
1944: {
1945: return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr));
1946: }
1947:
1948: private function lookForUnsetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope
1949: {
1950: return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->unsetAllowedUndefinedExpression($expr));
1951: }
1952:
1953: /**
1954: * @param Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback
1955: */
1956: private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope
1957: {
1958: if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) {
1959: $scope = $callback($scope, $expr);
1960: }
1961:
1962: if ($expr instanceof ArrayDimFetch) {
1963: $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback);
1964: } elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) {
1965: $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback);
1966: } elseif ($expr instanceof StaticPropertyFetch && $expr->class instanceof Expr) {
1967: $scope = $this->lookForExpressionCallback($scope, $expr->class, $callback);
1968: } elseif ($expr instanceof Array_ || $expr instanceof List_) {
1969: foreach ($expr->items as $item) {
1970: if ($item === null) {
1971: continue;
1972: }
1973:
1974: $scope = $this->lookForExpressionCallback($scope, $item->value, $callback);
1975: }
1976: }
1977:
1978: return $scope;
1979: }
1980:
1981: private function ensureShallowNonNullability(MutatingScope $scope, Scope $originalScope, Expr $exprToSpecify): EnsuredNonNullabilityResult
1982: {
1983: $exprType = $scope->getType($exprToSpecify);
1984: $isNull = $exprType->isNull();
1985: if ($isNull->yes()) {
1986: return new EnsuredNonNullabilityResult($scope, []);
1987: }
1988:
1989: // keep certainty
1990: $certainty = TrinaryLogic::createYes();
1991: $hasExpressionType = $originalScope->hasExpressionType($exprToSpecify);
1992: if (!$hasExpressionType->no()) {
1993: $certainty = $hasExpressionType;
1994: }
1995:
1996: $exprTypeWithoutNull = TypeCombinator::removeNull($exprType);
1997: if ($exprType->equals($exprTypeWithoutNull)) {
1998: $originalExprType = $originalScope->getType($exprToSpecify);
1999: if (!$originalExprType->equals($exprTypeWithoutNull)) {
2000: $originalNativeType = $originalScope->getNativeType($exprToSpecify);
2001:
2002: return new EnsuredNonNullabilityResult($scope, [
2003: new EnsuredNonNullabilityResultExpression($exprToSpecify, $originalExprType, $originalNativeType, $certainty),
2004: ]);
2005: }
2006: return new EnsuredNonNullabilityResult($scope, []);
2007: }
2008:
2009: $nativeType = $scope->getNativeType($exprToSpecify);
2010: $scope = $scope->specifyExpressionType(
2011: $exprToSpecify,
2012: $exprTypeWithoutNull,
2013: TypeCombinator::removeNull($nativeType),
2014: );
2015:
2016: return new EnsuredNonNullabilityResult(
2017: $scope,
2018: [
2019: new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType, $nativeType, $certainty),
2020: ],
2021: );
2022: }
2023:
2024: private function ensureNonNullability(MutatingScope $scope, Expr $expr): EnsuredNonNullabilityResult
2025: {
2026: $specifiedExpressions = [];
2027: $originalScope = $scope;
2028: $scope = $this->lookForExpressionCallback($scope, $expr, function ($scope, $expr) use (&$specifiedExpressions, $originalScope) {
2029: $result = $this->ensureShallowNonNullability($scope, $originalScope, $expr);
2030: foreach ($result->getSpecifiedExpressions() as $specifiedExpression) {
2031: $specifiedExpressions[] = $specifiedExpression;
2032: }
2033: return $result->getScope();
2034: });
2035:
2036: return new EnsuredNonNullabilityResult($scope, $specifiedExpressions);
2037: }
2038:
2039: /**
2040: * @param EnsuredNonNullabilityResultExpression[] $specifiedExpressions
2041: */
2042: private function revertNonNullability(MutatingScope $scope, array $specifiedExpressions): MutatingScope
2043: {
2044: foreach ($specifiedExpressions as $specifiedExpressionResult) {
2045: $scope = $scope->specifyExpressionType(
2046: $specifiedExpressionResult->getExpression(),
2047: $specifiedExpressionResult->getOriginalType(),
2048: $specifiedExpressionResult->getOriginalNativeType(),
2049: $specifiedExpressionResult->getCertainty(),
2050: );
2051: }
2052:
2053: return $scope;
2054: }
2055:
2056: private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr
2057: {
2058: if (($expr instanceof MethodCall || $expr instanceof Expr\StaticCall) && $expr->name instanceof Node\Identifier) {
2059: if (array_key_exists($expr->name->toLowerString(), $this->earlyTerminatingMethodNames)) {
2060: if ($expr instanceof MethodCall) {
2061: $methodCalledOnType = $scope->getType($expr->var);
2062: } else {
2063: if ($expr->class instanceof Name) {
2064: $methodCalledOnType = $scope->resolveTypeByName($expr->class);
2065: } else {
2066: $methodCalledOnType = $scope->getType($expr->class);
2067: }
2068: }
2069:
2070: foreach ($methodCalledOnType->getObjectClassNames() as $referencedClass) {
2071: if (!$this->reflectionProvider->hasClass($referencedClass)) {
2072: continue;
2073: }
2074:
2075: $classReflection = $this->reflectionProvider->getClass($referencedClass);
2076: foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames(), $classReflection->getNativeReflection()->getInterfaceNames()) as $className) {
2077: if (!isset($this->earlyTerminatingMethodCalls[$className])) {
2078: continue;
2079: }
2080:
2081: if (in_array((string) $expr->name, $this->earlyTerminatingMethodCalls[$className], true)) {
2082: return $expr;
2083: }
2084: }
2085: }
2086: }
2087: }
2088:
2089: if ($expr instanceof FuncCall && $expr->name instanceof Name) {
2090: if (in_array((string) $expr->name, $this->earlyTerminatingFunctionCalls, true)) {
2091: return $expr;
2092: }
2093: }
2094:
2095: if ($expr instanceof Expr\Exit_ || $expr instanceof Expr\Throw_) {
2096: return $expr;
2097: }
2098:
2099: $exprType = $scope->getType($expr);
2100: if ($exprType instanceof NeverType && $exprType->isExplicit()) {
2101: return $expr;
2102: }
2103:
2104: return null;
2105: }
2106:
2107: /**
2108: * @param callable(Node $node, Scope $scope): void $nodeCallback
2109: */
2110: public function processExprNode(Node\Stmt $stmt, Expr $expr, MutatingScope $scope, callable $nodeCallback, ExpressionContext $context): ExpressionResult
2111: {
2112: if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) {
2113: if ($expr instanceof FuncCall) {
2114: $newExpr = new FunctionCallableNode($expr->name, $expr);
2115: } elseif ($expr instanceof MethodCall) {
2116: $newExpr = new MethodCallableNode($expr->var, $expr->name, $expr);
2117: } elseif ($expr instanceof StaticCall) {
2118: $newExpr = new StaticMethodCallableNode($expr->class, $expr->name, $expr);
2119: } elseif ($expr instanceof New_ && !$expr->class instanceof Class_) {
2120: $newExpr = new InstantiationCallableNode($expr->class, $expr);
2121: } else {
2122: throw new ShouldNotHappenException();
2123: }
2124:
2125: return $this->processExprNode($stmt, $newExpr, $scope, $nodeCallback, $context);
2126: }
2127:
2128: $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $context);
2129:
2130: if ($expr instanceof Variable) {
2131: $hasYield = false;
2132: $throwPoints = [];
2133: $impurePoints = [];
2134: if ($expr->name instanceof Expr) {
2135: return $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep());
2136: } elseif (in_array($expr->name, Scope::SUPERGLOBAL_VARIABLES, true)) {
2137: $impurePoints[] = new ImpurePoint($scope, $expr, 'superglobal', 'access to superglobal variable', true);
2138: }
2139: } elseif ($expr instanceof Assign || $expr instanceof AssignRef) {
2140: $result = $this->processAssignVar(
2141: $scope,
2142: $stmt,
2143: $expr->var,
2144: $expr->expr,
2145: $nodeCallback,
2146: $context,
2147: function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): ExpressionResult {
2148: $impurePoints = [];
2149: if ($expr instanceof AssignRef) {
2150: $referencedExpr = $expr->expr;
2151: while ($referencedExpr instanceof ArrayDimFetch) {
2152: $referencedExpr = $referencedExpr->var;
2153: }
2154:
2155: if ($referencedExpr instanceof PropertyFetch || $referencedExpr instanceof StaticPropertyFetch) {
2156: $impurePoints[] = new ImpurePoint(
2157: $scope,
2158: $expr,
2159: 'propertyAssignByRef',
2160: 'property assignment by reference',
2161: false,
2162: );
2163: }
2164:
2165: $scope = $scope->enterExpressionAssign($expr->expr);
2166: }
2167:
2168: if ($expr->var instanceof Variable && is_string($expr->var->name)) {
2169: $context = $context->enterRightSideAssign(
2170: $expr->var->name,
2171: $scope->getType($expr->expr),
2172: $scope->getNativeType($expr->expr),
2173: );
2174: }
2175:
2176: $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep());
2177: $hasYield = $result->hasYield();
2178: $throwPoints = $result->getThrowPoints();
2179: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
2180: $scope = $result->getScope();
2181:
2182: if ($expr instanceof AssignRef) {
2183: $scope = $scope->exitExpressionAssign($expr->expr);
2184: }
2185:
2186: return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints);
2187: },
2188: true,
2189: );
2190: $scope = $result->getScope();
2191: $hasYield = $result->hasYield();
2192: $throwPoints = $result->getThrowPoints();
2193: $impurePoints = $result->getImpurePoints();
2194: $vars = $this->getAssignedVariables($expr->var);
2195: if (count($vars) > 0) {
2196: $varChangedScope = false;
2197: $scope = $this->processVarAnnotation($scope, $vars, $stmt, $varChangedScope);
2198: if (!$varChangedScope) {
2199: $scope = $this->processStmtVarAnnotation($scope, $stmt, null, $nodeCallback);
2200: }
2201: }
2202: } elseif ($expr instanceof Expr\AssignOp) {
2203: $result = $this->processAssignVar(
2204: $scope,
2205: $stmt,
2206: $expr->var,
2207: $expr,
2208: $nodeCallback,
2209: $context,
2210: function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $context): ExpressionResult {
2211: $originalScope = $scope;
2212: if ($expr instanceof Expr\AssignOp\Coalesce) {
2213: $scope = $scope->filterByFalseyValue(
2214: new BinaryOp\NotIdentical($expr->var, new ConstFetch(new Name('null'))),
2215: );
2216: }
2217:
2218: $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep());
2219: if ($expr instanceof Expr\AssignOp\Coalesce) {
2220: return new ExpressionResult(
2221: $result->getScope()->mergeWith($originalScope),
2222: $result->hasYield(),
2223: $result->getThrowPoints(),
2224: $result->getImpurePoints(),
2225: );
2226: }
2227:
2228: return $result;
2229: },
2230: $expr instanceof Expr\AssignOp\Coalesce,
2231: );
2232: $scope = $result->getScope();
2233: $hasYield = $result->hasYield();
2234: $throwPoints = $result->getThrowPoints();
2235: $impurePoints = $result->getImpurePoints();
2236: if (
2237: ($expr instanceof Expr\AssignOp\Div || $expr instanceof Expr\AssignOp\Mod) &&
2238: !$scope->getType($expr->expr)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no()
2239: ) {
2240: $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false);
2241: }
2242: } elseif ($expr instanceof FuncCall) {
2243: $parametersAcceptor = null;
2244: $functionReflection = null;
2245: $throwPoints = [];
2246: $impurePoints = [];
2247: if ($expr->name instanceof Expr) {
2248: $nameType = $scope->getType($expr->name);
2249: if (!$nameType->isCallable()->no()) {
2250: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
2251: $scope,
2252: $expr->getArgs(),
2253: $nameType->getCallableParametersAcceptors($scope),
2254: null,
2255: );
2256: }
2257:
2258: $nameResult = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep());
2259: $scope = $nameResult->getScope();
2260: $throwPoints = $nameResult->getThrowPoints();
2261: $impurePoints = $nameResult->getImpurePoints();
2262: if (
2263: $nameType->isObject()->yes()
2264: && $nameType->isCallable()->yes()
2265: && (new ObjectType(Closure::class))->isSuperTypeOf($nameType)->no()
2266: ) {
2267: $invokeResult = $this->processExprNode(
2268: $stmt,
2269: new MethodCall($expr->name, '__invoke', $expr->getArgs(), $expr->getAttributes()),
2270: $scope,
2271: static function (): void {
2272: },
2273: $context->enterDeep(),
2274: );
2275: $throwPoints = array_merge($throwPoints, $invokeResult->getThrowPoints());
2276: $impurePoints = array_merge($impurePoints, $invokeResult->getImpurePoints());
2277: } elseif ($parametersAcceptor instanceof CallableParametersAcceptor) {
2278: $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $expr, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $expr), $parametersAcceptor->getThrowPoints());
2279: if (!$this->implicitThrows) {
2280: $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit()));
2281: }
2282: $throwPoints = array_merge($throwPoints, $callableThrowPoints);
2283: $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $parametersAcceptor->getImpurePoints()));
2284:
2285: $scope = $this->processImmediatelyCalledCallable($scope, $parametersAcceptor->getInvalidateExpressions(), $parametersAcceptor->getUsedVariables());
2286: }
2287: } elseif ($this->reflectionProvider->hasFunction($expr->name, $scope)) {
2288: $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope);
2289: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
2290: $scope,
2291: $expr->getArgs(),
2292: $functionReflection->getVariants(),
2293: $functionReflection->getNamedArgumentsVariants(),
2294: );
2295: $impurePoint = SimpleImpurePoint::createFromVariant($functionReflection, $parametersAcceptor);
2296: if ($impurePoint !== null) {
2297: $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain());
2298: }
2299: } else {
2300: $impurePoints[] = new ImpurePoint(
2301: $scope,
2302: $expr,
2303: 'functionCall',
2304: 'call to unknown function',
2305: false,
2306: );
2307: }
2308:
2309: if ($parametersAcceptor !== null) {
2310: $expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr;
2311: }
2312: $result = $this->processArgs($stmt, $functionReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context);
2313: $scope = $result->getScope();
2314: $hasYield = $result->hasYield();
2315: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2316: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
2317:
2318: if ($functionReflection !== null) {
2319: $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $expr, $scope);
2320: if ($functionThrowPoint !== null) {
2321: $throwPoints[] = $functionThrowPoint;
2322: }
2323: } else {
2324: $throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
2325: }
2326:
2327: if (
2328: $functionReflection !== null
2329: && in_array($functionReflection->getName(), ['json_encode', 'json_decode'], true)
2330: ) {
2331: $scope = $scope->invalidateExpression(new FuncCall(new Name('json_last_error'), []))
2332: ->invalidateExpression(new FuncCall(new Name\FullyQualified('json_last_error'), []))
2333: ->invalidateExpression(new FuncCall(new Name('json_last_error_msg'), []))
2334: ->invalidateExpression(new FuncCall(new Name\FullyQualified('json_last_error_msg'), []));
2335: }
2336:
2337: if (
2338: $functionReflection !== null
2339: && $functionReflection->getName() === 'file_put_contents'
2340: && count($expr->getArgs()) > 0
2341: ) {
2342: $scope = $scope->invalidateExpression(new FuncCall(new Name('file_get_contents'), [$expr->getArgs()[0]]))
2343: ->invalidateExpression(new FuncCall(new Name\FullyQualified('file_get_contents'), [$expr->getArgs()[0]]));
2344: }
2345:
2346: if (
2347: $functionReflection !== null
2348: && in_array($functionReflection->getName(), ['array_pop', 'array_shift'], true)
2349: && count($expr->getArgs()) >= 1
2350: ) {
2351: $arrayArg = $expr->getArgs()[0]->value;
2352:
2353: $arrayArgType = $scope->getType($arrayArg);
2354: $arrayArgNativeType = $scope->getNativeType($arrayArg);
2355:
2356: $isArrayPop = $functionReflection->getName() === 'array_pop';
2357: $scope = $scope->invalidateExpression($arrayArg)->assignExpression(
2358: $arrayArg,
2359: $isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray(),
2360: $isArrayPop ? $arrayArgNativeType->popArray() : $arrayArgNativeType->shiftArray(),
2361: );
2362: }
2363:
2364: if (
2365: $functionReflection !== null
2366: && in_array($functionReflection->getName(), ['array_push', 'array_unshift'], true)
2367: && count($expr->getArgs()) >= 2
2368: ) {
2369: $arrayType = $this->getArrayFunctionAppendingType($functionReflection, $scope, $expr);
2370: $arrayNativeType = $this->getArrayFunctionAppendingType($functionReflection, $scope->doNotTreatPhpDocTypesAsCertain(), $expr);
2371:
2372: $arrayArg = $expr->getArgs()[0]->value;
2373: $scope = $scope->invalidateExpression($arrayArg)->assignExpression($arrayArg, $arrayType, $arrayNativeType);
2374: }
2375:
2376: if (
2377: $functionReflection !== null
2378: && in_array($functionReflection->getName(), ['fopen', 'file_get_contents'], true)
2379: ) {
2380: $scope = $scope->assignVariable('http_response_header', AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), new StringType())), new ArrayType(new IntegerType(), new StringType()));
2381: }
2382:
2383: if (
2384: $functionReflection !== null
2385: && $functionReflection->getName() === 'shuffle'
2386: ) {
2387: $arrayArg = $expr->getArgs()[0]->value;
2388: $scope = $scope->assignExpression(
2389: $arrayArg,
2390: $scope->getType($arrayArg)->shuffleArray(),
2391: $scope->getNativeType($arrayArg)->shuffleArray(),
2392: );
2393: }
2394:
2395: if (
2396: $functionReflection !== null
2397: && $functionReflection->getName() === 'array_splice'
2398: && count($expr->getArgs()) >= 1
2399: ) {
2400: $arrayArg = $expr->getArgs()[0]->value;
2401: $arrayArgType = $scope->getType($arrayArg);
2402: $valueType = $arrayArgType->getIterableValueType();
2403: if (count($expr->getArgs()) >= 4) {
2404: $replacementType = $scope->getType($expr->getArgs()[3]->value)->toArray();
2405: $valueType = TypeCombinator::union($valueType, $replacementType->getIterableValueType());
2406: }
2407: $scope = $scope->invalidateExpression($arrayArg)->assignExpression(
2408: $arrayArg,
2409: new ArrayType($arrayArgType->getIterableKeyType(), $valueType),
2410: new ArrayType($arrayArgType->getIterableKeyType(), $valueType),
2411: );
2412: }
2413:
2414: if (
2415: $functionReflection !== null
2416: && in_array($functionReflection->getName(), ['sort', 'rsort', 'usort'], true)
2417: && count($expr->getArgs()) >= 1
2418: ) {
2419: $arrayArg = $expr->getArgs()[0]->value;
2420: $scope = $scope->assignExpression(
2421: $arrayArg,
2422: $this->getArraySortPreserveListFunctionType($scope->getType($arrayArg)),
2423: $this->getArraySortPreserveListFunctionType($scope->getNativeType($arrayArg)),
2424: );
2425: }
2426:
2427: if (
2428: $functionReflection !== null
2429: && in_array($functionReflection->getName(), ['natcasesort', 'natsort', 'arsort', 'asort', 'ksort', 'krsort', 'uasort', 'uksort'], true)
2430: && count($expr->getArgs()) >= 1
2431: ) {
2432: $arrayArg = $expr->getArgs()[0]->value;
2433: $scope = $scope->assignExpression(
2434: $arrayArg,
2435: $this->getArraySortDoNotPreserveListFunctionType($scope->getType($arrayArg)),
2436: $this->getArraySortDoNotPreserveListFunctionType($scope->getNativeType($arrayArg)),
2437: );
2438: }
2439:
2440: if (
2441: $functionReflection !== null
2442: && $functionReflection->getName() === 'extract'
2443: ) {
2444: $extractedArg = $expr->getArgs()[0]->value;
2445: $extractedType = $scope->getType($extractedArg);
2446: $constantArrays = $extractedType->getConstantArrays();
2447: if (count($constantArrays) > 0) {
2448: $properties = [];
2449: $optionalProperties = [];
2450: $refCount = [];
2451: foreach ($constantArrays as $constantArray) {
2452: foreach ($constantArray->getKeyTypes() as $i => $keyType) {
2453: if ($keyType->isString()->no()) {
2454: // integers as variable names not allowed
2455: continue;
2456: }
2457: $key = (string) $keyType->getValue();
2458: $valueType = $constantArray->getValueTypes()[$i];
2459: $optional = $constantArray->isOptionalKey($i);
2460: if ($optional) {
2461: $optionalProperties[] = $key;
2462: }
2463: if (isset($properties[$key])) {
2464: $properties[$key] = TypeCombinator::union($properties[$key], $valueType);
2465: $refCount[$key]++;
2466: } else {
2467: $properties[$key] = $valueType;
2468: $refCount[$key] = 1;
2469: }
2470: }
2471: }
2472: foreach ($properties as $name => $type) {
2473: $optional = in_array($name, $optionalProperties, true) || $refCount[$name] < count($constantArrays);
2474: $scope = $scope->assignVariable($name, $type, $type, $optional ? TrinaryLogic::createMaybe() : TrinaryLogic::createYes());
2475: }
2476: } else {
2477: $scope = $scope->afterExtractCall();
2478: }
2479: }
2480:
2481: if (
2482: $functionReflection !== null
2483: && in_array($functionReflection->getName(), ['clearstatcache', 'unlink'], true)
2484: ) {
2485: $scope = $scope->afterClearstatcacheCall();
2486: }
2487:
2488: if (
2489: $functionReflection !== null
2490: && str_starts_with($functionReflection->getName(), 'openssl')
2491: ) {
2492: $scope = $scope->afterOpenSslCall($functionReflection->getName());
2493: }
2494:
2495: } elseif ($expr instanceof MethodCall) {
2496: $originalScope = $scope;
2497: if (
2498: ($expr->var instanceof Expr\Closure || $expr->var instanceof Expr\ArrowFunction)
2499: && $expr->name instanceof Node\Identifier
2500: && strtolower($expr->name->name) === 'call'
2501: && isset($expr->getArgs()[0])
2502: ) {
2503: $closureCallScope = $scope->enterClosureCall(
2504: $scope->getType($expr->getArgs()[0]->value),
2505: $scope->getNativeType($expr->getArgs()[0]->value),
2506: );
2507: }
2508:
2509: $result = $this->processExprNode($stmt, $expr->var, $closureCallScope ?? $scope, $nodeCallback, $context->enterDeep());
2510: $hasYield = $result->hasYield();
2511: $throwPoints = $result->getThrowPoints();
2512: $impurePoints = $result->getImpurePoints();
2513: $scope = $result->getScope();
2514: if (isset($closureCallScope)) {
2515: $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope);
2516: }
2517: $parametersAcceptor = null;
2518: $methodReflection = null;
2519: $calledOnType = $scope->getType($expr->var);
2520: if ($expr->name instanceof Expr) {
2521: $methodNameResult = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep());
2522: $throwPoints = array_merge($throwPoints, $methodNameResult->getThrowPoints());
2523: $scope = $methodNameResult->getScope();
2524: } else {
2525: $methodName = $expr->name->name;
2526: $methodReflection = $scope->getMethodReflection($calledOnType, $methodName);
2527: if ($methodReflection !== null) {
2528: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
2529: $scope,
2530: $expr->getArgs(),
2531: $methodReflection->getVariants(),
2532: $methodReflection->getNamedArgumentsVariants(),
2533: );
2534:
2535: $methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope);
2536: if ($methodThrowPoint !== null) {
2537: $throwPoints[] = $methodThrowPoint;
2538: }
2539: }
2540: }
2541:
2542: if ($methodReflection !== null) {
2543: $impurePoint = SimpleImpurePoint::createFromVariant($methodReflection, $parametersAcceptor);
2544: if ($impurePoint !== null) {
2545: $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain());
2546: }
2547: } else {
2548: $impurePoints[] = new ImpurePoint(
2549: $scope,
2550: $expr,
2551: 'methodCall',
2552: 'call to unknown method',
2553: false,
2554: );
2555: }
2556:
2557: if ($parametersAcceptor !== null) {
2558: $expr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr;
2559: }
2560:
2561: $result = $this->processArgs(
2562: $stmt,
2563: $methodReflection,
2564: $methodReflection !== null ? $scope->getNakedMethod($calledOnType, $methodReflection->getName()) : null,
2565: $parametersAcceptor,
2566: $expr,
2567: $scope,
2568: $nodeCallback,
2569: $context,
2570: );
2571: $scope = $result->getScope();
2572:
2573: if ($methodReflection !== null) {
2574: $hasSideEffects = $methodReflection->hasSideEffects();
2575: if ($hasSideEffects->yes() || $methodReflection->getName() === '__construct') {
2576: $nodeCallback(new InvalidateExprNode($expr->var), $scope);
2577: $scope = $scope->invalidateExpression($expr->var, true);
2578: }
2579: if ($parametersAcceptor !== null) {
2580: $selfOutType = $methodReflection->getSelfOutType();
2581: if ($selfOutType !== null) {
2582: $scope = $scope->assignExpression(
2583: $expr->var,
2584: TemplateTypeHelper::resolveTemplateTypes(
2585: $selfOutType,
2586: $parametersAcceptor->getResolvedTemplateTypeMap(),
2587: $parametersAcceptor instanceof ParametersAcceptorWithPhpDocs ? $parametersAcceptor->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
2588: TemplateTypeVariance::createCovariant(),
2589: ),
2590: $scope->getNativeType($expr->var),
2591: );
2592: }
2593: }
2594:
2595: if (
2596: $scope->isInClass()
2597: && $scope->getClassReflection()->getName() === $methodReflection->getDeclaringClass()->getName()
2598: /*&& (
2599: // should not be allowed but in practice has to be
2600: $scope->getClassReflection()->isFinal()
2601: || $methodReflection->isFinal()->yes()
2602: || $methodReflection->isPrivate()
2603: )*/
2604: && TypeUtils::findThisType($calledOnType) !== null
2605: ) {
2606: $calledMethodScope = $this->processCalledMethod($methodReflection);
2607: if ($calledMethodScope !== null) {
2608: $scope = $scope->mergeInitializedProperties($calledMethodScope);
2609: }
2610: }
2611: } else {
2612: $throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
2613: }
2614: $hasYield = $hasYield || $result->hasYield();
2615: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2616: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
2617: } elseif ($expr instanceof Expr\NullsafeMethodCall) {
2618: $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var);
2619: $exprResult = $this->processExprNode($stmt, new MethodCall($expr->var, $expr->name, $expr->args, array_merge($expr->getAttributes(), ['virtualNullsafeMethodCall' => true])), $nonNullabilityResult->getScope(), $nodeCallback, $context);
2620: $scope = $this->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions());
2621:
2622: return new ExpressionResult(
2623: $scope,
2624: $exprResult->hasYield(),
2625: $exprResult->getThrowPoints(),
2626: $exprResult->getImpurePoints(),
2627: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
2628: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
2629: );
2630: } elseif ($expr instanceof StaticCall) {
2631: $hasYield = false;
2632: $throwPoints = [];
2633: $impurePoints = [];
2634: if ($expr->class instanceof Expr) {
2635: $objectClasses = $scope->getType($expr->class)->getObjectClassNames();
2636: if (count($objectClasses) !== 1) {
2637: $objectClasses = $scope->getType(new New_($expr->class))->getObjectClassNames();
2638: }
2639: if (count($objectClasses) === 1) {
2640: $objectExprResult = $this->processExprNode($stmt, new StaticCall(new Name($objectClasses[0]), $expr->name, []), $scope, static function (): void {
2641: }, $context->enterDeep());
2642: $additionalThrowPoints = $objectExprResult->getThrowPoints();
2643: } else {
2644: $additionalThrowPoints = [ThrowPoint::createImplicit($scope, $expr)];
2645: }
2646: $classResult = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep());
2647: $hasYield = $classResult->hasYield();
2648: $throwPoints = array_merge($throwPoints, $classResult->getThrowPoints());
2649: $impurePoints = array_merge($impurePoints, $classResult->getImpurePoints());
2650: foreach ($additionalThrowPoints as $throwPoint) {
2651: $throwPoints[] = $throwPoint;
2652: }
2653: $scope = $classResult->getScope();
2654: }
2655:
2656: $parametersAcceptor = null;
2657: $methodReflection = null;
2658: if ($expr->name instanceof Expr) {
2659: $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep());
2660: $hasYield = $hasYield || $result->hasYield();
2661: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2662: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
2663: $scope = $result->getScope();
2664: } elseif ($expr->class instanceof Name) {
2665: $classType = $scope->resolveTypeByName($expr->class);
2666: $methodName = $expr->name->name;
2667: if ($classType->hasMethod($methodName)->yes()) {
2668: $methodReflection = $classType->getMethod($methodName, $scope);
2669: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
2670: $scope,
2671: $expr->getArgs(),
2672: $methodReflection->getVariants(),
2673: $methodReflection->getNamedArgumentsVariants(),
2674: );
2675:
2676: $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope);
2677: if ($methodThrowPoint !== null) {
2678: $throwPoints[] = $methodThrowPoint;
2679: }
2680:
2681: $declaringClass = $methodReflection->getDeclaringClass();
2682: if (
2683: $declaringClass->getName() === 'Closure'
2684: && strtolower($methodName) === 'bind'
2685: ) {
2686: $thisType = null;
2687: $nativeThisType = null;
2688: if (isset($expr->getArgs()[1])) {
2689: $argType = $scope->getType($expr->getArgs()[1]->value);
2690: if ($argType->isNull()->yes()) {
2691: $thisType = null;
2692: } else {
2693: $thisType = $argType;
2694: }
2695:
2696: $nativeArgType = $scope->getNativeType($expr->getArgs()[1]->value);
2697: if ($nativeArgType->isNull()->yes()) {
2698: $nativeThisType = null;
2699: } else {
2700: $nativeThisType = $nativeArgType;
2701: }
2702: }
2703: $scopeClasses = ['static'];
2704: if (isset($expr->getArgs()[2])) {
2705: $argValue = $expr->getArgs()[2]->value;
2706: $argValueType = $scope->getType($argValue);
2707:
2708: $directClassNames = $argValueType->getObjectClassNames();
2709: if (count($directClassNames) > 0) {
2710: $scopeClasses = $directClassNames;
2711: $thisTypes = [];
2712: foreach ($directClassNames as $directClassName) {
2713: $thisTypes[] = new ObjectType($directClassName);
2714: }
2715: $thisType = TypeCombinator::union(...$thisTypes);
2716: } else {
2717: $thisType = $argValueType->getClassStringObjectType();
2718: $scopeClasses = $thisType->getObjectClassNames();
2719: }
2720: }
2721: $closureBindScope = $scope->enterClosureBind($thisType, $nativeThisType, $scopeClasses);
2722: }
2723: } else {
2724: $throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
2725: }
2726: }
2727:
2728: if ($methodReflection !== null) {
2729: $impurePoint = SimpleImpurePoint::createFromVariant($methodReflection, $parametersAcceptor);
2730: if ($impurePoint !== null) {
2731: $impurePoints[] = new ImpurePoint($scope, $expr, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain());
2732: }
2733: } else {
2734: $impurePoints[] = new ImpurePoint(
2735: $scope,
2736: $expr,
2737: 'methodCall',
2738: 'call to unknown method',
2739: false,
2740: );
2741: }
2742:
2743: if ($parametersAcceptor !== null) {
2744: $expr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr;
2745: }
2746: $result = $this->processArgs($stmt, $methodReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context, $closureBindScope ?? null);
2747: $scope = $result->getScope();
2748: $scopeFunction = $scope->getFunction();
2749:
2750: if (
2751: $methodReflection !== null
2752: && !$methodReflection->isStatic()
2753: && (
2754: $methodReflection->hasSideEffects()->yes()
2755: || $methodReflection->getName() === '__construct'
2756: )
2757: && $scopeFunction instanceof MethodReflection
2758: && !$scopeFunction->isStatic()
2759: && $scope->isInClass()
2760: && (
2761: $scope->getClassReflection()->getName() === $methodReflection->getDeclaringClass()->getName()
2762: || $scope->getClassReflection()->isSubclassOf($methodReflection->getDeclaringClass()->getName())
2763: )
2764: ) {
2765: $scope = $scope->invalidateExpression(new Variable('this'), true);
2766: }
2767:
2768: if (
2769: $methodReflection !== null
2770: && !$methodReflection->isStatic()
2771: && $methodReflection->getName() === '__construct'
2772: && $scopeFunction instanceof MethodReflection
2773: && !$scopeFunction->isStatic()
2774: && $scope->isInClass()
2775: && $scope->getClassReflection()->isSubclassOf($methodReflection->getDeclaringClass()->getName())
2776: ) {
2777: $thisType = $scope->getType(new Variable('this'));
2778: $methodClassReflection = $methodReflection->getDeclaringClass();
2779: foreach ($methodClassReflection->getNativeReflection()->getProperties(ReflectionProperty::IS_PUBLIC | ReflectionProperty::IS_PROTECTED) as $property) {
2780: if (!$property->isPromoted() || $property->getDeclaringClass()->getName() !== $methodClassReflection->getName()) {
2781: continue;
2782: }
2783:
2784: $scope = $scope->assignInitializedProperty($thisType, $property->getName());
2785: }
2786: }
2787:
2788: $hasYield = $hasYield || $result->hasYield();
2789: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2790: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
2791: } elseif ($expr instanceof PropertyFetch) {
2792: $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep());
2793: $hasYield = $result->hasYield();
2794: $throwPoints = $result->getThrowPoints();
2795: $impurePoints = $result->getImpurePoints();
2796: $scope = $result->getScope();
2797: if ($expr->name instanceof Expr) {
2798: $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep());
2799: $hasYield = $hasYield || $result->hasYield();
2800: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2801: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
2802: $scope = $result->getScope();
2803: }
2804: } elseif ($expr instanceof Expr\NullsafePropertyFetch) {
2805: $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var);
2806: $exprResult = $this->processExprNode($stmt, new PropertyFetch($expr->var, $expr->name, array_merge($expr->getAttributes(), ['virtualNullsafePropertyFetch' => true])), $nonNullabilityResult->getScope(), $nodeCallback, $context);
2807: $scope = $this->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions());
2808:
2809: return new ExpressionResult(
2810: $scope,
2811: $exprResult->hasYield(),
2812: $exprResult->getThrowPoints(),
2813: $exprResult->getImpurePoints(),
2814: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
2815: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
2816: );
2817: } elseif ($expr instanceof StaticPropertyFetch) {
2818: $hasYield = false;
2819: $throwPoints = [];
2820: $impurePoints = [];
2821: if ($expr->class instanceof Expr) {
2822: $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep());
2823: $hasYield = $result->hasYield();
2824: $throwPoints = $result->getThrowPoints();
2825: $impurePoints = $result->getImpurePoints();
2826: $scope = $result->getScope();
2827: }
2828: if ($expr->name instanceof Expr) {
2829: $result = $this->processExprNode($stmt, $expr->name, $scope, $nodeCallback, $context->enterDeep());
2830: $hasYield = $hasYield || $result->hasYield();
2831: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2832: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
2833: $scope = $result->getScope();
2834: }
2835: } elseif ($expr instanceof Expr\Closure) {
2836: $processClosureResult = $this->processClosureNode($stmt, $expr, $scope, $nodeCallback, $context, null);
2837:
2838: return new ExpressionResult(
2839: $processClosureResult->getScope(),
2840: false,
2841: [],
2842: [],
2843: );
2844: } elseif ($expr instanceof Expr\ArrowFunction) {
2845: $result = $this->processArrowFunctionNode($stmt, $expr, $scope, $nodeCallback, null);
2846: return new ExpressionResult(
2847: $result->getScope(),
2848: $result->hasYield(),
2849: [],
2850: [],
2851: );
2852: } elseif ($expr instanceof ErrorSuppress) {
2853: $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context);
2854: $hasYield = $result->hasYield();
2855: $throwPoints = $result->getThrowPoints();
2856: $impurePoints = $result->getImpurePoints();
2857: $scope = $result->getScope();
2858: } elseif ($expr instanceof Exit_) {
2859: $hasYield = false;
2860: $throwPoints = [];
2861: $kind = $expr->getAttribute('kind', Exit_::KIND_EXIT);
2862: $identifier = $kind === Exit_::KIND_DIE ? 'die' : 'exit';
2863: $impurePoints = [
2864: new ImpurePoint($scope, $expr, $identifier, $identifier, true),
2865: ];
2866: if ($expr->expr !== null) {
2867: $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep());
2868: $hasYield = $result->hasYield();
2869: $throwPoints = $result->getThrowPoints();
2870: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
2871: $scope = $result->getScope();
2872: }
2873: } elseif ($expr instanceof Node\Scalar\Encapsed) {
2874: $hasYield = false;
2875: $throwPoints = [];
2876: $impurePoints = [];
2877: foreach ($expr->parts as $part) {
2878: $result = $this->processExprNode($stmt, $part, $scope, $nodeCallback, $context->enterDeep());
2879: $hasYield = $hasYield || $result->hasYield();
2880: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2881: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
2882: $scope = $result->getScope();
2883: }
2884: } elseif ($expr instanceof ArrayDimFetch) {
2885: $hasYield = false;
2886: $throwPoints = [];
2887: $impurePoints = [];
2888: if ($expr->dim !== null) {
2889: $result = $this->processExprNode($stmt, $expr->dim, $scope, $nodeCallback, $context->enterDeep());
2890: $hasYield = $result->hasYield();
2891: $throwPoints = $result->getThrowPoints();
2892: $impurePoints = $result->getImpurePoints();
2893: $scope = $result->getScope();
2894: }
2895:
2896: $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep());
2897: $hasYield = $hasYield || $result->hasYield();
2898: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2899: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
2900: $scope = $result->getScope();
2901: } elseif ($expr instanceof Array_) {
2902: $itemNodes = [];
2903: $hasYield = false;
2904: $throwPoints = [];
2905: $impurePoints = [];
2906: foreach ($expr->items as $arrayItem) {
2907: $itemNodes[] = new LiteralArrayItem($scope, $arrayItem);
2908: if ($arrayItem === null) {
2909: continue;
2910: }
2911: $nodeCallback($arrayItem, $scope);
2912: if ($arrayItem->key !== null) {
2913: $keyResult = $this->processExprNode($stmt, $arrayItem->key, $scope, $nodeCallback, $context->enterDeep());
2914: $hasYield = $hasYield || $keyResult->hasYield();
2915: $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints());
2916: $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints());
2917: $scope = $keyResult->getScope();
2918: }
2919:
2920: $valueResult = $this->processExprNode($stmt, $arrayItem->value, $scope, $nodeCallback, $context->enterDeep());
2921: $hasYield = $hasYield || $valueResult->hasYield();
2922: $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints());
2923: $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints());
2924: $scope = $valueResult->getScope();
2925: }
2926: $nodeCallback(new LiteralArrayNode($expr, $itemNodes), $scope);
2927: } elseif ($expr instanceof BooleanAnd || $expr instanceof BinaryOp\LogicalAnd) {
2928: $leftResult = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep());
2929: $rightResult = $this->processExprNode($stmt, $expr->right, $leftResult->getTruthyScope(), $nodeCallback, $context);
2930: $rightExprType = $rightResult->getScope()->getType($expr->right);
2931: if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) {
2932: $leftMergedWithRightScope = $leftResult->getFalseyScope();
2933: } else {
2934: $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope());
2935: }
2936:
2937: $this->callNodeCallbackWithExpression($nodeCallback, new BooleanAndNode($expr, $leftResult->getTruthyScope()), $scope, $context);
2938:
2939: return new ExpressionResult(
2940: $leftMergedWithRightScope,
2941: $leftResult->hasYield() || $rightResult->hasYield(),
2942: array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()),
2943: array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()),
2944: static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr),
2945: static fn (): MutatingScope => $leftMergedWithRightScope->filterByFalseyValue($expr),
2946: );
2947: } elseif ($expr instanceof BooleanOr || $expr instanceof BinaryOp\LogicalOr) {
2948: $leftResult = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep());
2949: $rightResult = $this->processExprNode($stmt, $expr->right, $leftResult->getFalseyScope(), $nodeCallback, $context);
2950: $rightExprType = $rightResult->getScope()->getType($expr->right);
2951: if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) {
2952: $leftMergedWithRightScope = $leftResult->getTruthyScope();
2953: } else {
2954: $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope());
2955: }
2956:
2957: $this->callNodeCallbackWithExpression($nodeCallback, new BooleanOrNode($expr, $leftResult->getFalseyScope()), $scope, $context);
2958:
2959: return new ExpressionResult(
2960: $leftMergedWithRightScope,
2961: $leftResult->hasYield() || $rightResult->hasYield(),
2962: array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()),
2963: array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()),
2964: static fn (): MutatingScope => $leftMergedWithRightScope->filterByTruthyValue($expr),
2965: static fn (): MutatingScope => $rightResult->getScope()->filterByFalseyValue($expr),
2966: );
2967: } elseif ($expr instanceof Coalesce) {
2968: $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->left);
2969: $condScope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->left);
2970: $condResult = $this->processExprNode($stmt, $expr->left, $condScope, $nodeCallback, $context->enterDeep());
2971: $scope = $this->revertNonNullability($condResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions());
2972: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->left);
2973:
2974: $rightScope = $scope->filterByFalseyValue($expr);
2975: $rightResult = $this->processExprNode($stmt, $expr->right, $rightScope, $nodeCallback, $context->enterDeep());
2976: $rightExprType = $scope->getType($expr->right);
2977: if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) {
2978: $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left]));
2979: } else {
2980: $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left]))->mergeWith($rightResult->getScope());
2981: }
2982:
2983: $hasYield = $condResult->hasYield() || $rightResult->hasYield();
2984: $throwPoints = array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints());
2985: $impurePoints = array_merge($condResult->getImpurePoints(), $rightResult->getImpurePoints());
2986: } elseif ($expr instanceof BinaryOp) {
2987: $result = $this->processExprNode($stmt, $expr->left, $scope, $nodeCallback, $context->enterDeep());
2988: $scope = $result->getScope();
2989: $hasYield = $result->hasYield();
2990: $throwPoints = $result->getThrowPoints();
2991: $impurePoints = $result->getImpurePoints();
2992: $result = $this->processExprNode($stmt, $expr->right, $scope, $nodeCallback, $context->enterDeep());
2993: if (
2994: ($expr instanceof BinaryOp\Div || $expr instanceof BinaryOp\Mod) &&
2995: !$scope->getType($expr->right)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no()
2996: ) {
2997: $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false);
2998: }
2999: $scope = $result->getScope();
3000: $hasYield = $hasYield || $result->hasYield();
3001: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3002: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3003: } elseif ($expr instanceof Expr\Include_) {
3004: $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep());
3005: $throwPoints = $result->getThrowPoints();
3006: $throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
3007: $impurePoints = $result->getImpurePoints();
3008: $impurePoints[] = new ImpurePoint(
3009: $scope,
3010: $expr,
3011: in_array($expr->type, [Expr\Include_::TYPE_INCLUDE, Expr\Include_::TYPE_INCLUDE_ONCE], true) ? 'include' : 'require',
3012: in_array($expr->type, [Expr\Include_::TYPE_INCLUDE, Expr\Include_::TYPE_INCLUDE_ONCE], true) ? 'include' : 'require',
3013: true,
3014: );
3015: $hasYield = $result->hasYield();
3016: $scope = $result->getScope()->afterExtractCall();
3017: } elseif ($expr instanceof Expr\Print_) {
3018: $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep());
3019: $throwPoints = $result->getThrowPoints();
3020: $impurePoints = $result->getImpurePoints();
3021: $impurePoints[] = new ImpurePoint($scope, $expr, 'print', 'print', true);
3022: $hasYield = $result->hasYield();
3023:
3024: $scope = $result->getScope();
3025: } elseif ($expr instanceof Cast\String_) {
3026: $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep());
3027: $throwPoints = $result->getThrowPoints();
3028: $impurePoints = $result->getImpurePoints();
3029: $hasYield = $result->hasYield();
3030:
3031: $exprType = $scope->getType($expr->expr);
3032: $toStringMethod = $scope->getMethodReflection($exprType, '__toString');
3033: if ($toStringMethod !== null) {
3034: if (!$toStringMethod->hasSideEffects()->no()) {
3035: $impurePoints[] = new ImpurePoint(
3036: $scope,
3037: $expr,
3038: 'methodCall',
3039: sprintf('call to method %s::%s()', $toStringMethod->getDeclaringClass()->getDisplayName(), $toStringMethod->getName()),
3040: $toStringMethod->isPure()->no(),
3041: );
3042: }
3043: }
3044:
3045: $scope = $result->getScope();
3046: } elseif (
3047: $expr instanceof Expr\BitwiseNot
3048: || $expr instanceof Cast
3049: || $expr instanceof Expr\Clone_
3050: || $expr instanceof Expr\UnaryMinus
3051: || $expr instanceof Expr\UnaryPlus
3052: ) {
3053: $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep());
3054: $throwPoints = $result->getThrowPoints();
3055: $impurePoints = $result->getImpurePoints();
3056: $hasYield = $result->hasYield();
3057:
3058: $scope = $result->getScope();
3059: } elseif ($expr instanceof Expr\Eval_) {
3060: $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep());
3061: $throwPoints = $result->getThrowPoints();
3062: $throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
3063: $impurePoints = $result->getImpurePoints();
3064: $impurePoints[] = new ImpurePoint($scope, $expr, 'eval', 'eval', true);
3065: $hasYield = $result->hasYield();
3066:
3067: $scope = $result->getScope();
3068: } elseif ($expr instanceof Expr\YieldFrom) {
3069: $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep());
3070: $throwPoints = $result->getThrowPoints();
3071: $throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
3072: $impurePoints = $result->getImpurePoints();
3073: $impurePoints[] = new ImpurePoint(
3074: $scope,
3075: $expr,
3076: 'yieldFrom',
3077: 'yield from',
3078: true,
3079: );
3080: $hasYield = true;
3081:
3082: $scope = $result->getScope();
3083: } elseif ($expr instanceof BooleanNot) {
3084: $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep());
3085: $scope = $result->getScope();
3086: $hasYield = $result->hasYield();
3087: $throwPoints = $result->getThrowPoints();
3088: $impurePoints = $result->getImpurePoints();
3089: } elseif ($expr instanceof Expr\ClassConstFetch) {
3090: $hasYield = false;
3091: $throwPoints = [];
3092: $impurePoints = [];
3093: if ($expr->class instanceof Expr) {
3094: $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep());
3095: $scope = $result->getScope();
3096: $hasYield = $result->hasYield();
3097: $throwPoints = $result->getThrowPoints();
3098: $impurePoints = $result->getImpurePoints();
3099: }
3100: } elseif ($expr instanceof Expr\Empty_) {
3101: $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->expr);
3102: $scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->expr);
3103: $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep());
3104: $scope = $result->getScope();
3105: $hasYield = $result->hasYield();
3106: $throwPoints = $result->getThrowPoints();
3107: $impurePoints = $result->getImpurePoints();
3108: $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions());
3109: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->expr);
3110: } elseif ($expr instanceof Expr\Isset_) {
3111: $hasYield = false;
3112: $throwPoints = [];
3113: $impurePoints = [];
3114: $nonNullabilityResults = [];
3115: foreach ($expr->vars as $var) {
3116: $nonNullabilityResult = $this->ensureNonNullability($scope, $var);
3117: $scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $var);
3118: $result = $this->processExprNode($stmt, $var, $scope, $nodeCallback, $context->enterDeep());
3119: $scope = $result->getScope();
3120: $hasYield = $hasYield || $result->hasYield();
3121: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3122: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3123: $nonNullabilityResults[] = $nonNullabilityResult;
3124: }
3125: foreach (array_reverse($expr->vars) as $var) {
3126: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
3127: }
3128: foreach (array_reverse($nonNullabilityResults) as $nonNullabilityResult) {
3129: $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions());
3130: }
3131: } elseif ($expr instanceof Instanceof_) {
3132: $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, $context->enterDeep());
3133: $scope = $result->getScope();
3134: $hasYield = $result->hasYield();
3135: $throwPoints = $result->getThrowPoints();
3136: $impurePoints = $result->getImpurePoints();
3137: if ($expr->class instanceof Expr) {
3138: $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep());
3139: $scope = $result->getScope();
3140: $hasYield = $hasYield || $result->hasYield();
3141: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3142: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3143: }
3144: } elseif ($expr instanceof List_) {
3145: // only in assign and foreach, processed elsewhere
3146: return new ExpressionResult($scope, false, [], []);
3147: } elseif ($expr instanceof New_) {
3148: $parametersAcceptor = null;
3149: $constructorReflection = null;
3150: $hasYield = false;
3151: $throwPoints = [];
3152: $impurePoints = [];
3153: $className = null;
3154: if ($expr->class instanceof Expr || $expr->class instanceof Name) {
3155: if ($expr->class instanceof Expr) {
3156: $objectClasses = $scope->getType($expr)->getObjectClassNames();
3157: if (count($objectClasses) === 1) {
3158: $objectExprResult = $this->processExprNode($stmt, new New_(new Name($objectClasses[0])), $scope, static function (): void {
3159: }, $context->enterDeep());
3160: $className = $objectClasses[0];
3161: $additionalThrowPoints = $objectExprResult->getThrowPoints();
3162: } else {
3163: $additionalThrowPoints = [ThrowPoint::createImplicit($scope, $expr)];
3164: }
3165:
3166: $result = $this->processExprNode($stmt, $expr->class, $scope, $nodeCallback, $context->enterDeep());
3167: $scope = $result->getScope();
3168: $hasYield = $result->hasYield();
3169: $throwPoints = $result->getThrowPoints();
3170: $impurePoints = $result->getImpurePoints();
3171: foreach ($additionalThrowPoints as $throwPoint) {
3172: $throwPoints[] = $throwPoint;
3173: }
3174: } else {
3175: $className = $scope->resolveName($expr->class);
3176: }
3177:
3178: $classReflection = null;
3179: if ($className !== null && $this->reflectionProvider->hasClass($className)) {
3180: $classReflection = $this->reflectionProvider->getClass($className);
3181: if ($classReflection->hasConstructor()) {
3182: $constructorReflection = $classReflection->getConstructor();
3183: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
3184: $scope,
3185: $expr->getArgs(),
3186: $constructorReflection->getVariants(),
3187: $constructorReflection->getNamedArgumentsVariants(),
3188: );
3189: $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $parametersAcceptor, $classReflection, $expr, new Name\FullyQualified($className), $expr->getArgs(), $scope);
3190: if ($constructorThrowPoint !== null) {
3191: $throwPoints[] = $constructorThrowPoint;
3192: }
3193: }
3194: } else {
3195: $throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
3196: }
3197:
3198: if ($constructorReflection !== null) {
3199: if (!$constructorReflection->hasSideEffects()->no()) {
3200: $certain = $constructorReflection->isPure()->no();
3201: $impurePoints[] = new ImpurePoint(
3202: $scope,
3203: $expr,
3204: 'new',
3205: sprintf('instantiation of class %s', $constructorReflection->getDeclaringClass()->getDisplayName()),
3206: $certain,
3207: );
3208: }
3209: } elseif ($classReflection === null) {
3210: $impurePoints[] = new ImpurePoint(
3211: $scope,
3212: $expr,
3213: 'new',
3214: 'instantiation of unknown class',
3215: false,
3216: );
3217: }
3218:
3219: if ($parametersAcceptor !== null) {
3220: $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr;
3221: }
3222:
3223: } else {
3224: $classReflection = $this->reflectionProvider->getAnonymousClassReflection($expr->class, $scope); // populates $expr->class->name
3225: $constructorResult = null;
3226: $this->processStmtNode($expr->class, $scope, static function (Node $node, Scope $scope) use ($nodeCallback, $classReflection, &$constructorResult): void {
3227: $nodeCallback($node, $scope);
3228: if (!$node instanceof MethodReturnStatementsNode) {
3229: return;
3230: }
3231: if ($constructorResult !== null) {
3232: return;
3233: }
3234: $currentClassReflection = $node->getClassReflection();
3235: if ($currentClassReflection->getName() !== $classReflection->getName()) {
3236: return;
3237: }
3238: if (!$currentClassReflection->hasConstructor()) {
3239: return;
3240: }
3241: if ($currentClassReflection->getConstructor()->getName() !== $node->getMethodReflection()->getName()) {
3242: return;
3243: }
3244: $constructorResult = $node;
3245: }, StatementContext::createTopLevel());
3246: if ($constructorResult !== null) {
3247: $throwPoints = array_merge($throwPoints, $constructorResult->getStatementResult()->getThrowPoints());
3248: $impurePoints = array_merge($impurePoints, $constructorResult->getImpurePoints());
3249: }
3250: if ($classReflection->hasConstructor()) {
3251: $constructorReflection = $classReflection->getConstructor();
3252: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
3253: $scope,
3254: $expr->getArgs(),
3255: $constructorReflection->getVariants(),
3256: $constructorReflection->getNamedArgumentsVariants(),
3257: );
3258: }
3259: }
3260:
3261: $result = $this->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $expr, $scope, $nodeCallback, $context);
3262: $scope = $result->getScope();
3263: $hasYield = $hasYield || $result->hasYield();
3264: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3265: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
3266: } elseif (
3267: $expr instanceof Expr\PreInc
3268: || $expr instanceof Expr\PostInc
3269: || $expr instanceof Expr\PreDec
3270: || $expr instanceof Expr\PostDec
3271: ) {
3272: $result = $this->processExprNode($stmt, $expr->var, $scope, $nodeCallback, $context->enterDeep());
3273: $scope = $result->getScope();
3274: $hasYield = $result->hasYield();
3275: $throwPoints = $result->getThrowPoints();
3276: $impurePoints = $result->getImpurePoints();
3277:
3278: $newExpr = $expr;
3279: if ($expr instanceof Expr\PostInc) {
3280: $newExpr = new Expr\PreInc($expr->var);
3281: } elseif ($expr instanceof Expr\PostDec) {
3282: $newExpr = new Expr\PreDec($expr->var);
3283: }
3284:
3285: $scope = $this->processAssignVar(
3286: $scope,
3287: $stmt,
3288: $expr->var,
3289: $newExpr,
3290: static function (Node $node, Scope $scope) use ($nodeCallback): void {
3291: if (!$node instanceof PropertyAssignNode && !$node instanceof VariableAssignNode) {
3292: return;
3293: }
3294:
3295: $nodeCallback($node, $scope);
3296: },
3297: $context,
3298: static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []),
3299: false,
3300: )->getScope();
3301: } elseif ($expr instanceof Ternary) {
3302: $ternaryCondResult = $this->processExprNode($stmt, $expr->cond, $scope, $nodeCallback, $context->enterDeep());
3303: $throwPoints = $ternaryCondResult->getThrowPoints();
3304: $impurePoints = $ternaryCondResult->getImpurePoints();
3305: $ifTrueScope = $ternaryCondResult->getTruthyScope();
3306: $ifFalseScope = $ternaryCondResult->getFalseyScope();
3307: $ifTrueType = null;
3308: if ($expr->if !== null) {
3309: $ifResult = $this->processExprNode($stmt, $expr->if, $ifTrueScope, $nodeCallback, $context);
3310: $throwPoints = array_merge($throwPoints, $ifResult->getThrowPoints());
3311: $impurePoints = array_merge($impurePoints, $ifResult->getImpurePoints());
3312: $ifTrueScope = $ifResult->getScope();
3313: $ifTrueType = $ifTrueScope->getType($expr->if);
3314: }
3315:
3316: $elseResult = $this->processExprNode($stmt, $expr->else, $ifFalseScope, $nodeCallback, $context);
3317: $throwPoints = array_merge($throwPoints, $elseResult->getThrowPoints());
3318: $impurePoints = array_merge($impurePoints, $elseResult->getImpurePoints());
3319: $ifFalseScope = $elseResult->getScope();
3320:
3321: $condType = $scope->getType($expr->cond);
3322: if ($condType->isTrue()->yes()) {
3323: $finalScope = $ifTrueScope;
3324: } elseif ($condType->isFalse()->yes()) {
3325: $finalScope = $ifFalseScope;
3326: } else {
3327: if ($ifTrueType instanceof NeverType && $ifTrueType->isExplicit()) {
3328: $finalScope = $ifFalseScope;
3329: } else {
3330: $ifFalseType = $ifFalseScope->getType($expr->else);
3331:
3332: if ($ifFalseType instanceof NeverType && $ifFalseType->isExplicit()) {
3333: $finalScope = $ifTrueScope;
3334: } else {
3335: $finalScope = $ifTrueScope->mergeWith($ifFalseScope);
3336: }
3337: }
3338: }
3339:
3340: return new ExpressionResult(
3341: $finalScope,
3342: $ternaryCondResult->hasYield(),
3343: $throwPoints,
3344: $impurePoints,
3345: static fn (): MutatingScope => $finalScope->filterByTruthyValue($expr),
3346: static fn (): MutatingScope => $finalScope->filterByFalseyValue($expr),
3347: );
3348:
3349: } elseif ($expr instanceof Expr\Yield_) {
3350: $throwPoints = [
3351: ThrowPoint::createImplicit($scope, $expr),
3352: ];
3353: $impurePoints = [
3354: new ImpurePoint(
3355: $scope,
3356: $expr,
3357: 'yield',
3358: 'yield',
3359: true,
3360: ),
3361: ];
3362: if ($expr->key !== null) {
3363: $keyResult = $this->processExprNode($stmt, $expr->key, $scope, $nodeCallback, $context->enterDeep());
3364: $scope = $keyResult->getScope();
3365: $throwPoints = $keyResult->getThrowPoints();
3366: $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints());
3367: }
3368: if ($expr->value !== null) {
3369: $valueResult = $this->processExprNode($stmt, $expr->value, $scope, $nodeCallback, $context->enterDeep());
3370: $scope = $valueResult->getScope();
3371: $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints());
3372: $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints());
3373: }
3374: $hasYield = true;
3375: } elseif ($expr instanceof Expr\Match_) {
3376: $deepContext = $context->enterDeep();
3377: $condType = $scope->getType($expr->cond);
3378: $condResult = $this->processExprNode($stmt, $expr->cond, $scope, $nodeCallback, $deepContext);
3379: $scope = $condResult->getScope();
3380: $hasYield = $condResult->hasYield();
3381: $throwPoints = $condResult->getThrowPoints();
3382: $impurePoints = $condResult->getImpurePoints();
3383: $matchScope = $scope->enterMatch($expr);
3384: $armNodes = [];
3385: $hasDefaultCond = false;
3386: $hasAlwaysTrueCond = false;
3387: $arms = $expr->arms;
3388: if ($condType->isEnum()->yes()) {
3389: // enum match analysis would work even without this if branch
3390: // but would be much slower
3391: // this avoids using ObjectType::$subtractedType which is slow for huge enums
3392: // because of repeated union type normalization
3393: $enumCases = $condType->getEnumCases();
3394: if (count($enumCases) > 0) {
3395: $indexedEnumCases = [];
3396: foreach ($enumCases as $enumCase) {
3397: $indexedEnumCases[strtolower($enumCase->getClassName())][$enumCase->getEnumCaseName()] = $enumCase;
3398: }
3399: $unusedIndexedEnumCases = $indexedEnumCases;
3400: foreach ($arms as $i => $arm) {
3401: if ($arm->conds === null) {
3402: continue;
3403: }
3404:
3405: $condNodes = [];
3406: $conditionCases = [];
3407: foreach ($arm->conds as $cond) {
3408: if (!$cond instanceof Expr\ClassConstFetch) {
3409: continue 2;
3410: }
3411: if (!$cond->class instanceof Name) {
3412: continue 2;
3413: }
3414: if (!$cond->name instanceof Node\Identifier) {
3415: continue 2;
3416: }
3417: $fetchedClassName = $scope->resolveName($cond->class);
3418: $loweredFetchedClassName = strtolower($fetchedClassName);
3419: if (!array_key_exists($loweredFetchedClassName, $indexedEnumCases)) {
3420: continue 2;
3421: }
3422:
3423: if (!array_key_exists($loweredFetchedClassName, $unusedIndexedEnumCases)) {
3424: throw new ShouldNotHappenException();
3425: }
3426:
3427: $caseName = $cond->name->toString();
3428: if (!array_key_exists($caseName, $indexedEnumCases[$loweredFetchedClassName])) {
3429: continue 2;
3430: }
3431:
3432: $enumCase = $indexedEnumCases[$loweredFetchedClassName][$caseName];
3433: $conditionCases[] = $enumCase;
3434: $armConditionScope = $matchScope;
3435: if (!array_key_exists($caseName, $unusedIndexedEnumCases[$loweredFetchedClassName])) {
3436: // force "always false"
3437: $armConditionScope = $armConditionScope->removeTypeFromExpression(
3438: $expr->cond,
3439: $enumCase,
3440: );
3441: } elseif (count($unusedIndexedEnumCases[$loweredFetchedClassName]) === 1) {
3442: $hasAlwaysTrueCond = true;
3443:
3444: // force "always true"
3445: $armConditionScope = $armConditionScope->addTypeToExpression(
3446: $expr->cond,
3447: $enumCase,
3448: );
3449: }
3450:
3451: $this->processExprNode($stmt, $cond, $armConditionScope, $nodeCallback, $deepContext);
3452:
3453: $condNodes[] = new MatchExpressionArmCondition(
3454: $cond,
3455: $armConditionScope,
3456: $cond->getStartLine(),
3457: );
3458:
3459: unset($unusedIndexedEnumCases[$loweredFetchedClassName][$caseName]);
3460: }
3461:
3462: $conditionCasesCount = count($conditionCases);
3463: if ($conditionCasesCount === 0) {
3464: throw new ShouldNotHappenException();
3465: } elseif ($conditionCasesCount === 1) {
3466: $conditionCaseType = $conditionCases[0];
3467: } else {
3468: $conditionCaseType = new UnionType($conditionCases);
3469: }
3470:
3471: $matchArmBodyScope = $matchScope->addTypeToExpression(
3472: $expr->cond,
3473: $conditionCaseType,
3474: );
3475: $matchArmBody = new MatchExpressionArmBody($matchArmBodyScope, $arm->body);
3476: $armNodes[$i] = new MatchExpressionArm($matchArmBody, $condNodes, $arm->getStartLine());
3477:
3478: $armResult = $this->processExprNode(
3479: $stmt,
3480: $arm->body,
3481: $matchArmBodyScope,
3482: $nodeCallback,
3483: ExpressionContext::createTopLevel(),
3484: );
3485: $armScope = $armResult->getScope();
3486: $scope = $scope->mergeWith($armScope);
3487: $hasYield = $hasYield || $armResult->hasYield();
3488: $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints());
3489: $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints());
3490:
3491: unset($arms[$i]);
3492: }
3493:
3494: $remainingCases = [];
3495: foreach ($unusedIndexedEnumCases as $cases) {
3496: foreach ($cases as $case) {
3497: $remainingCases[] = $case;
3498: }
3499: }
3500:
3501: $remainingCasesCount = count($remainingCases);
3502: if ($remainingCasesCount === 0) {
3503: $remainingType = new NeverType();
3504: } elseif ($remainingCasesCount === 1) {
3505: $remainingType = $remainingCases[0];
3506: } else {
3507: $remainingType = new UnionType($remainingCases);
3508: }
3509:
3510: $matchScope = $matchScope->addTypeToExpression($expr->cond, $remainingType);
3511: }
3512: }
3513: foreach ($arms as $i => $arm) {
3514: if ($arm->conds === null) {
3515: $hasDefaultCond = true;
3516: $matchArmBody = new MatchExpressionArmBody($matchScope, $arm->body);
3517: $armNodes[$i] = new MatchExpressionArm($matchArmBody, [], $arm->getStartLine());
3518: $armResult = $this->processExprNode($stmt, $arm->body, $matchScope, $nodeCallback, ExpressionContext::createTopLevel());
3519: $matchScope = $armResult->getScope();
3520: $hasYield = $hasYield || $armResult->hasYield();
3521: $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints());
3522: $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints());
3523: $scope = $scope->mergeWith($matchScope);
3524: continue;
3525: }
3526:
3527: if (count($arm->conds) === 0) {
3528: throw new ShouldNotHappenException();
3529: }
3530:
3531: $filteringExprs = [];
3532: $armCondScope = $matchScope;
3533: $condNodes = [];
3534: foreach ($arm->conds as $armCond) {
3535: $condNodes[] = new MatchExpressionArmCondition($armCond, $armCondScope, $armCond->getStartLine());
3536: $armCondResult = $this->processExprNode($stmt, $armCond, $armCondScope, $nodeCallback, $deepContext);
3537: $hasYield = $hasYield || $armCondResult->hasYield();
3538: $throwPoints = array_merge($throwPoints, $armCondResult->getThrowPoints());
3539: $impurePoints = array_merge($impurePoints, $armCondResult->getImpurePoints());
3540: $armCondExpr = new BinaryOp\Identical($expr->cond, $armCond);
3541: $armCondResultScope = $armCondResult->getScope();
3542: $armCondType = $this->treatPhpDocTypesAsCertain ? $armCondResultScope->getType($armCondExpr) : $armCondResultScope->getNativeType($armCondExpr);
3543: if ($armCondType->isTrue()->yes()) {
3544: $hasAlwaysTrueCond = true;
3545: }
3546: $armCondScope = $armCondResult->getScope()->filterByFalseyValue($armCondExpr);
3547: $filteringExprs[] = $armCond;
3548: }
3549:
3550: if (count($filteringExprs) === 1) {
3551: $filteringExpr = new BinaryOp\Identical($expr->cond, $filteringExprs[0]);
3552: } else {
3553: $items = [];
3554: foreach ($filteringExprs as $filteringExpr) {
3555: $items[] = new ArrayItem($filteringExpr);
3556: }
3557: $filteringExpr = new FuncCall(
3558: new Name\FullyQualified('in_array'),
3559: [
3560: new Arg($expr->cond),
3561: new Arg(new Array_($items)),
3562: new Arg(new ConstFetch(new Name\FullyQualified('true'))),
3563: ],
3564: );
3565: }
3566:
3567: $bodyScope = $this->processExprNode($stmt, $filteringExpr, $matchScope, static function (): void {
3568: }, $deepContext)->getTruthyScope();
3569: $matchArmBody = new MatchExpressionArmBody($bodyScope, $arm->body);
3570: $armNodes[$i] = new MatchExpressionArm($matchArmBody, $condNodes, $arm->getStartLine());
3571:
3572: $armResult = $this->processExprNode(
3573: $stmt,
3574: $arm->body,
3575: $bodyScope,
3576: $nodeCallback,
3577: ExpressionContext::createTopLevel(),
3578: );
3579: $armScope = $armResult->getScope();
3580: $scope = $scope->mergeWith($armScope);
3581: $hasYield = $hasYield || $armResult->hasYield();
3582: $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints());
3583: $impurePoints = array_merge($impurePoints, $armResult->getImpurePoints());
3584: $matchScope = $matchScope->filterByFalseyValue($filteringExpr);
3585: }
3586:
3587: $remainingType = $matchScope->getType($expr->cond);
3588: if (!$hasDefaultCond && !$hasAlwaysTrueCond && !$remainingType instanceof NeverType) {
3589: $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(UnhandledMatchError::class), $expr, false);
3590: }
3591:
3592: ksort($armNodes, SORT_NUMERIC);
3593:
3594: $nodeCallback(new MatchExpressionNode($expr->cond, array_values($armNodes), $expr, $matchScope), $scope);
3595: } elseif ($expr instanceof AlwaysRememberedExpr) {
3596: $result = $this->processExprNode($stmt, $expr->getExpr(), $scope, $nodeCallback, $context);
3597: $hasYield = $result->hasYield();
3598: $throwPoints = $result->getThrowPoints();
3599: $impurePoints = $result->getImpurePoints();
3600: $scope = $result->getScope();
3601: } elseif ($expr instanceof Expr\Throw_) {
3602: $hasYield = false;
3603: $result = $this->processExprNode($stmt, $expr->expr, $scope, $nodeCallback, ExpressionContext::createDeep());
3604: $throwPoints = $result->getThrowPoints();
3605: $impurePoints = $result->getImpurePoints();
3606: $throwPoints[] = ThrowPoint::createExplicit($scope, $scope->getType($expr->expr), $expr, false);
3607: } elseif ($expr instanceof FunctionCallableNode) {
3608: $throwPoints = [];
3609: $impurePoints = [];
3610: $hasYield = false;
3611: if ($expr->getName() instanceof Expr) {
3612: $result = $this->processExprNode($stmt, $expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep());
3613: $scope = $result->getScope();
3614: $hasYield = $result->hasYield();
3615: $throwPoints = $result->getThrowPoints();
3616: $impurePoints = $result->getImpurePoints();
3617: }
3618: } elseif ($expr instanceof MethodCallableNode) {
3619: $result = $this->processExprNode($stmt, $expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep());
3620: $scope = $result->getScope();
3621: $hasYield = $result->hasYield();
3622: $throwPoints = $result->getThrowPoints();
3623: $impurePoints = $result->getImpurePoints();
3624: if ($expr->getName() instanceof Expr) {
3625: $nameResult = $this->processExprNode($stmt, $expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep());
3626: $scope = $nameResult->getScope();
3627: $hasYield = $hasYield || $nameResult->hasYield();
3628: $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints());
3629: $impurePoints = array_merge($impurePoints, $nameResult->getImpurePoints());
3630: }
3631: } elseif ($expr instanceof StaticMethodCallableNode) {
3632: $throwPoints = [];
3633: $impurePoints = [];
3634: $hasYield = false;
3635: if ($expr->getClass() instanceof Expr) {
3636: $classResult = $this->processExprNode($stmt, $expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep());
3637: $scope = $classResult->getScope();
3638: $hasYield = $classResult->hasYield();
3639: $throwPoints = $classResult->getThrowPoints();
3640: $impurePoints = $classResult->getImpurePoints();
3641: }
3642: if ($expr->getName() instanceof Expr) {
3643: $nameResult = $this->processExprNode($stmt, $expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep());
3644: $scope = $nameResult->getScope();
3645: $hasYield = $hasYield || $nameResult->hasYield();
3646: $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints());
3647: $impurePoints = array_merge($impurePoints, $nameResult->getImpurePoints());
3648: }
3649: } elseif ($expr instanceof InstantiationCallableNode) {
3650: $throwPoints = [];
3651: $impurePoints = [];
3652: $hasYield = false;
3653: if ($expr->getClass() instanceof Expr) {
3654: $classResult = $this->processExprNode($stmt, $expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep());
3655: $scope = $classResult->getScope();
3656: $hasYield = $classResult->hasYield();
3657: $throwPoints = $classResult->getThrowPoints();
3658: $impurePoints = $classResult->getImpurePoints();
3659: }
3660: } elseif ($expr instanceof Node\Scalar) {
3661: $hasYield = false;
3662: $throwPoints = [];
3663: $impurePoints = [];
3664: } elseif ($expr instanceof ConstFetch) {
3665: $hasYield = false;
3666: $throwPoints = [];
3667: $impurePoints = [];
3668: $nodeCallback($expr->name, $scope);
3669: } else {
3670: $hasYield = false;
3671: $throwPoints = [];
3672: $impurePoints = [];
3673: }
3674:
3675: return new ExpressionResult(
3676: $scope,
3677: $hasYield,
3678: $throwPoints,
3679: $impurePoints,
3680: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
3681: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
3682: );
3683: }
3684:
3685: private function getArrayFunctionAppendingType(FunctionReflection $functionReflection, Scope $scope, FuncCall $expr): Type
3686: {
3687: $arrayArg = $expr->getArgs()[0]->value;
3688: $arrayType = $scope->getType($arrayArg);
3689: $callArgs = array_slice($expr->getArgs(), 1);
3690:
3691: /**
3692: * @param Arg[] $callArgs
3693: * @param callable(?Type, Type, bool): void $setOffsetValueType
3694: */
3695: $setOffsetValueTypes = static function (Scope $scope, array $callArgs, callable $setOffsetValueType, ?bool &$nonConstantArrayWasUnpacked = null): void {
3696: foreach ($callArgs as $callArg) {
3697: $callArgType = $scope->getType($callArg->value);
3698: if ($callArg->unpack) {
3699: $constantArrays = $callArgType->getConstantArrays();
3700: if (count($constantArrays) === 1) {
3701: $iterableValueTypes = $constantArrays[0]->getValueTypes();
3702: } else {
3703: $iterableValueTypes = [$callArgType->getIterableValueType()];
3704: $nonConstantArrayWasUnpacked = true;
3705: }
3706:
3707: $isOptional = !$callArgType->isIterableAtLeastOnce()->yes();
3708: foreach ($iterableValueTypes as $iterableValueType) {
3709: if ($iterableValueType instanceof UnionType) {
3710: foreach ($iterableValueType->getTypes() as $innerType) {
3711: $setOffsetValueType(null, $innerType, $isOptional);
3712: }
3713: } else {
3714: $setOffsetValueType(null, $iterableValueType, $isOptional);
3715: }
3716: }
3717: continue;
3718: }
3719: $setOffsetValueType(null, $callArgType, false);
3720: }
3721: };
3722:
3723: $constantArrays = $arrayType->getConstantArrays();
3724: if (count($constantArrays) > 0) {
3725: $newArrayTypes = [];
3726: $prepend = $functionReflection->getName() === 'array_unshift';
3727: foreach ($constantArrays as $constantArray) {
3728: $arrayTypeBuilder = $prepend ? ConstantArrayTypeBuilder::createEmpty() : ConstantArrayTypeBuilder::createFromConstantArray($constantArray);
3729:
3730: $setOffsetValueTypes(
3731: $scope,
3732: $callArgs,
3733: static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayTypeBuilder): void {
3734: $arrayTypeBuilder->setOffsetValueType($offsetType, $valueType, $optional);
3735: },
3736: $nonConstantArrayWasUnpacked,
3737: );
3738:
3739: if ($prepend) {
3740: $keyTypes = $constantArray->getKeyTypes();
3741: $valueTypes = $constantArray->getValueTypes();
3742: foreach ($keyTypes as $k => $keyType) {
3743: $arrayTypeBuilder->setOffsetValueType(
3744: count($keyType->getConstantStrings()) === 1 ? $keyType->getConstantStrings()[0] : null,
3745: $valueTypes[$k],
3746: $constantArray->isOptionalKey($k),
3747: );
3748: }
3749: }
3750:
3751: $constantArray = $arrayTypeBuilder->getArray();
3752:
3753: if ($constantArray->isConstantArray()->yes() && $nonConstantArrayWasUnpacked) {
3754: $array = new ArrayType($constantArray->generalize(GeneralizePrecision::lessSpecific())->getIterableKeyType(), $constantArray->getIterableValueType());
3755: $isList = $constantArray->isList()->yes();
3756: $constantArray = $constantArray->isIterableAtLeastOnce()->yes()
3757: ? TypeCombinator::intersect($array, new NonEmptyArrayType())
3758: : $array;
3759: $constantArray = $isList
3760: ? AccessoryArrayListType::intersectWith($constantArray)
3761: : $constantArray;
3762: }
3763:
3764: $newArrayTypes[] = $constantArray;
3765: }
3766:
3767: return TypeCombinator::union(...$newArrayTypes);
3768: }
3769:
3770: $setOffsetValueTypes(
3771: $scope,
3772: $callArgs,
3773: static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayType): void {
3774: $isIterableAtLeastOnce = $arrayType->isIterableAtLeastOnce()->yes() || !$optional;
3775: $arrayType = $arrayType->setOffsetValueType($offsetType, $valueType);
3776: if ($isIterableAtLeastOnce) {
3777: return;
3778: }
3779:
3780: $arrayType = TypeCombinator::union($arrayType, new ConstantArrayType([], []));
3781: },
3782: );
3783:
3784: return $arrayType;
3785: }
3786:
3787: private function getArraySortPreserveListFunctionType(Type $type): Type
3788: {
3789: $isIterableAtLeastOnce = $type->isIterableAtLeastOnce();
3790: if ($isIterableAtLeastOnce->no()) {
3791: return $type;
3792: }
3793:
3794: return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($isIterableAtLeastOnce): Type {
3795: if ($type instanceof UnionType || $type instanceof IntersectionType) {
3796: return $traverse($type);
3797: }
3798:
3799: if (!$type instanceof ArrayType) {
3800: return $type;
3801: }
3802:
3803: $newArrayType = AccessoryArrayListType::intersectWith(new ArrayType(new IntegerType(), $type->getIterableValueType()));
3804: if ($isIterableAtLeastOnce->yes()) {
3805: $newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType());
3806: }
3807:
3808: return $newArrayType;
3809: });
3810: }
3811:
3812: private function getArraySortDoNotPreserveListFunctionType(Type $type): Type
3813: {
3814: $isIterableAtLeastOnce = $type->isIterableAtLeastOnce();
3815: if ($isIterableAtLeastOnce->no()) {
3816: return $type;
3817: }
3818:
3819: return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($isIterableAtLeastOnce): Type {
3820: if ($type instanceof UnionType) {
3821: return $traverse($type);
3822: }
3823:
3824: $constantArrays = $type->getConstantArrays();
3825: if (count($constantArrays) > 0) {
3826: $types = [];
3827: foreach ($constantArrays as $constantArray) {
3828: $types[] = new ConstantArrayType(
3829: $constantArray->getKeyTypes(),
3830: $constantArray->getValueTypes(),
3831: $constantArray->getNextAutoIndexes(),
3832: $constantArray->getOptionalKeys(),
3833: $constantArray->isList()->and(TrinaryLogic::createMaybe()),
3834: );
3835: }
3836:
3837: return TypeCombinator::union(...$types);
3838: }
3839:
3840: $newArrayType = new ArrayType($type->getIterableKeyType(), $type->getIterableValueType());
3841: if ($isIterableAtLeastOnce->yes()) {
3842: $newArrayType = TypeCombinator::intersect($newArrayType, new NonEmptyArrayType());
3843: }
3844:
3845: return $newArrayType;
3846: });
3847: }
3848:
3849: private function getFunctionThrowPoint(
3850: FunctionReflection $functionReflection,
3851: ?ParametersAcceptor $parametersAcceptor,
3852: FuncCall $funcCall,
3853: MutatingScope $scope,
3854: ): ?ThrowPoint
3855: {
3856: $normalizedFuncCall = $funcCall;
3857: if ($parametersAcceptor !== null) {
3858: $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $funcCall);
3859: }
3860:
3861: if ($normalizedFuncCall !== null) {
3862: foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicFunctionThrowTypeExtensions() as $extension) {
3863: if (!$extension->isFunctionSupported($functionReflection)) {
3864: continue;
3865: }
3866:
3867: $throwType = $extension->getThrowTypeFromFunctionCall($functionReflection, $normalizedFuncCall, $scope);
3868: if ($throwType === null) {
3869: return null;
3870: }
3871:
3872: return ThrowPoint::createExplicit($scope, $throwType, $funcCall, false);
3873: }
3874: }
3875:
3876: $throwType = $functionReflection->getThrowType();
3877: if ($throwType === null && $parametersAcceptor !== null) {
3878: $returnType = $parametersAcceptor->getReturnType();
3879: if ($returnType instanceof NeverType && $returnType->isExplicit()) {
3880: $throwType = new ObjectType(Throwable::class);
3881: }
3882: }
3883:
3884: if ($throwType !== null) {
3885: if (!$throwType->isVoid()->yes()) {
3886: return ThrowPoint::createExplicit($scope, $throwType, $funcCall, true);
3887: }
3888: } elseif ($this->implicitThrows) {
3889: $requiredParameters = null;
3890: if ($parametersAcceptor !== null) {
3891: $requiredParameters = 0;
3892: foreach ($parametersAcceptor->getParameters() as $parameter) {
3893: if ($parameter->isOptional()) {
3894: continue;
3895: }
3896:
3897: $requiredParameters++;
3898: }
3899: }
3900: if (
3901: !$functionReflection->isBuiltin()
3902: || $requiredParameters === null
3903: || $requiredParameters > 0
3904: || count($funcCall->getArgs()) > 0
3905: ) {
3906: $functionReturnedType = $scope->getType($funcCall);
3907: if (!(new ObjectType(Throwable::class))->isSuperTypeOf($functionReturnedType)->yes()) {
3908: return ThrowPoint::createImplicit($scope, $funcCall);
3909: }
3910: }
3911: }
3912:
3913: return null;
3914: }
3915:
3916: private function getMethodThrowPoint(MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, MethodCall $methodCall, MutatingScope $scope): ?ThrowPoint
3917: {
3918: $normalizedMethodCall = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $methodCall);
3919: if ($normalizedMethodCall !== null) {
3920: foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) {
3921: if (!$extension->isMethodSupported($methodReflection)) {
3922: continue;
3923: }
3924:
3925: $throwType = $extension->getThrowTypeFromMethodCall($methodReflection, $normalizedMethodCall, $scope);
3926: if ($throwType === null) {
3927: return null;
3928: }
3929:
3930: return ThrowPoint::createExplicit($scope, $throwType, $methodCall, false);
3931: }
3932: }
3933:
3934: $throwType = $methodReflection->getThrowType();
3935: if ($throwType === null) {
3936: $returnType = $parametersAcceptor->getReturnType();
3937: if ($returnType instanceof NeverType && $returnType->isExplicit()) {
3938: $throwType = new ObjectType(Throwable::class);
3939: }
3940: }
3941:
3942: if ($throwType !== null) {
3943: if (!$throwType->isVoid()->yes()) {
3944: return ThrowPoint::createExplicit($scope, $throwType, $methodCall, true);
3945: }
3946: } elseif ($this->implicitThrows) {
3947: $methodReturnedType = $scope->getType($methodCall);
3948: if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
3949: return ThrowPoint::createImplicit($scope, $methodCall);
3950: }
3951: }
3952:
3953: return null;
3954: }
3955:
3956: /**
3957: * @param Node\Arg[] $args
3958: */
3959: private function getConstructorThrowPoint(MethodReflection $constructorReflection, ParametersAcceptor $parametersAcceptor, ClassReflection $classReflection, New_ $new, Name $className, array $args, MutatingScope $scope): ?ThrowPoint
3960: {
3961: $methodCall = new StaticCall($className, $constructorReflection->getName(), $args);
3962: $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall);
3963: if ($normalizedMethodCall !== null) {
3964: foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) {
3965: if (!$extension->isStaticMethodSupported($constructorReflection)) {
3966: continue;
3967: }
3968:
3969: $throwType = $extension->getThrowTypeFromStaticMethodCall($constructorReflection, $normalizedMethodCall, $scope);
3970: if ($throwType === null) {
3971: return null;
3972: }
3973:
3974: return ThrowPoint::createExplicit($scope, $throwType, $new, false);
3975: }
3976: }
3977:
3978: if ($constructorReflection->getThrowType() !== null) {
3979: $throwType = $constructorReflection->getThrowType();
3980: if (!$throwType->isVoid()->yes()) {
3981: return ThrowPoint::createExplicit($scope, $throwType, $new, true);
3982: }
3983: } elseif ($this->implicitThrows) {
3984: if ($classReflection->getName() !== Throwable::class && !$classReflection->isSubclassOf(Throwable::class)) {
3985: return ThrowPoint::createImplicit($scope, $methodCall);
3986: }
3987: }
3988:
3989: return null;
3990: }
3991:
3992: private function getStaticMethodThrowPoint(MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, StaticCall $methodCall, MutatingScope $scope): ?ThrowPoint
3993: {
3994: $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall);
3995: if ($normalizedMethodCall !== null) {
3996: foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) {
3997: if (!$extension->isStaticMethodSupported($methodReflection)) {
3998: continue;
3999: }
4000:
4001: $throwType = $extension->getThrowTypeFromStaticMethodCall($methodReflection, $normalizedMethodCall, $scope);
4002: if ($throwType === null) {
4003: return null;
4004: }
4005:
4006: return ThrowPoint::createExplicit($scope, $throwType, $methodCall, false);
4007: }
4008: }
4009:
4010: if ($methodReflection->getThrowType() !== null) {
4011: $throwType = $methodReflection->getThrowType();
4012: if (!$throwType->isVoid()->yes()) {
4013: return ThrowPoint::createExplicit($scope, $throwType, $methodCall, true);
4014: }
4015: } elseif ($this->implicitThrows) {
4016: $methodReturnedType = $scope->getType($methodCall);
4017: if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
4018: return ThrowPoint::createImplicit($scope, $methodCall);
4019: }
4020: }
4021:
4022: return null;
4023: }
4024:
4025: /**
4026: * @return string[]
4027: */
4028: private function getAssignedVariables(Expr $expr): array
4029: {
4030: if ($expr instanceof Expr\Variable) {
4031: if (is_string($expr->name)) {
4032: return [$expr->name];
4033: }
4034:
4035: return [];
4036: }
4037:
4038: if ($expr instanceof Expr\List_ || $expr instanceof Expr\Array_) {
4039: $names = [];
4040: foreach ($expr->items as $item) {
4041: if ($item === null) {
4042: continue;
4043: }
4044:
4045: $names = array_merge($names, $this->getAssignedVariables($item->value));
4046: }
4047:
4048: return $names;
4049: }
4050:
4051: if ($expr instanceof ArrayDimFetch) {
4052: return $this->getAssignedVariables($expr->var);
4053: }
4054:
4055: return [];
4056: }
4057:
4058: /**
4059: * @param callable(Node $node, Scope $scope): void $nodeCallback
4060: */
4061: private function callNodeCallbackWithExpression(
4062: callable $nodeCallback,
4063: Expr $expr,
4064: MutatingScope $scope,
4065: ExpressionContext $context,
4066: ): void
4067: {
4068: if ($context->isDeep()) {
4069: $scope = $scope->exitFirstLevelStatements();
4070: }
4071: $nodeCallback($expr, $scope);
4072: }
4073:
4074: /**
4075: * @param callable(Node $node, Scope $scope): void $nodeCallback
4076: */
4077: private function processClosureNode(
4078: Node\Stmt $stmt,
4079: Expr\Closure $expr,
4080: MutatingScope $scope,
4081: callable $nodeCallback,
4082: ExpressionContext $context,
4083: ?Type $passedToType,
4084: ): ProcessClosureResult
4085: {
4086: foreach ($expr->params as $param) {
4087: $this->processParamNode($stmt, $param, $scope, $nodeCallback);
4088: }
4089:
4090: $byRefUses = [];
4091:
4092: $closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME);
4093: $callableParameters = $this->createCallableParameters(
4094: $scope,
4095: $expr,
4096: $closureCallArgs,
4097: $passedToType,
4098: );
4099:
4100: $useScope = $scope;
4101: foreach ($expr->uses as $use) {
4102: if ($use->byRef) {
4103: $byRefUses[] = $use;
4104: $useScope = $useScope->enterExpressionAssign($use->var);
4105:
4106: $inAssignRightSideVariableName = $context->getInAssignRightSideVariableName();
4107: $inAssignRightSideType = $context->getInAssignRightSideType();
4108: $inAssignRightSideNativeType = $context->getInAssignRightSideNativeType();
4109: if (
4110: $inAssignRightSideVariableName === $use->var->name
4111: && $inAssignRightSideType !== null
4112: && $inAssignRightSideNativeType !== null
4113: ) {
4114: if ($inAssignRightSideType instanceof ClosureType) {
4115: $variableType = $inAssignRightSideType;
4116: } else {
4117: $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName);
4118: if ($alreadyHasVariableType->no()) {
4119: $variableType = TypeCombinator::union(new NullType(), $inAssignRightSideType);
4120: } else {
4121: $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType);
4122: }
4123: }
4124: if ($inAssignRightSideNativeType instanceof ClosureType) {
4125: $variableNativeType = $inAssignRightSideNativeType;
4126: } else {
4127: $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName);
4128: if ($alreadyHasVariableType->no()) {
4129: $variableNativeType = TypeCombinator::union(new NullType(), $inAssignRightSideNativeType);
4130: } else {
4131: $variableNativeType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideNativeType);
4132: }
4133: }
4134: $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType, $variableNativeType);
4135: }
4136: }
4137: $this->processExprNode($stmt, $use->var, $useScope, $nodeCallback, $context);
4138: if (!$use->byRef) {
4139: continue;
4140: }
4141:
4142: $useScope = $useScope->exitExpressionAssign($use->var);
4143: }
4144:
4145: if ($expr->returnType !== null) {
4146: $nodeCallback($expr->returnType, $scope);
4147: }
4148:
4149: $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters);
4150: $closureScope = $closureScope->processClosureScope($scope, null, $byRefUses);
4151: $closureType = $closureScope->getAnonymousFunctionReflection();
4152: if (!$closureType instanceof ClosureType) {
4153: throw new ShouldNotHappenException();
4154: }
4155:
4156: $nodeCallback(new InClosureNode($closureType, $expr), $closureScope);
4157:
4158: $executionEnds = [];
4159: $gatheredReturnStatements = [];
4160: $gatheredYieldStatements = [];
4161: $closureImpurePoints = [];
4162: $invalidateExpressions = [];
4163: $closureStmtsCallback = static function (Node $node, Scope $scope) use ($nodeCallback, &$executionEnds, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void {
4164: $nodeCallback($node, $scope);
4165: if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) {
4166: return;
4167: }
4168: if ($node instanceof PropertyAssignNode) {
4169: $closureImpurePoints[] = new ImpurePoint(
4170: $scope,
4171: $node,
4172: 'propertyAssign',
4173: 'property assignment',
4174: true,
4175: );
4176: return;
4177: }
4178: if ($node instanceof ExecutionEndNode) {
4179: $executionEnds[] = $node;
4180: return;
4181: }
4182: if ($node instanceof InvalidateExprNode) {
4183: $invalidateExpressions[] = $node;
4184: return;
4185: }
4186: if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) {
4187: $gatheredYieldStatements[] = $node;
4188: }
4189: if (!$node instanceof Return_) {
4190: return;
4191: }
4192:
4193: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
4194: };
4195: if (count($byRefUses) === 0) {
4196: $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel());
4197: $nodeCallback(new ClosureReturnStatementsNode(
4198: $expr,
4199: $gatheredReturnStatements,
4200: $gatheredYieldStatements,
4201: $statementResult,
4202: $executionEnds,
4203: array_merge($statementResult->getImpurePoints(), $closureImpurePoints),
4204: ), $closureScope);
4205:
4206: return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions);
4207: }
4208:
4209: $count = 0;
4210: do {
4211: $prevScope = $closureScope;
4212:
4213: $intermediaryClosureScopeResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, static function (): void {
4214: }, StatementContext::createTopLevel());
4215: $intermediaryClosureScope = $intermediaryClosureScopeResult->getScope();
4216: foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) {
4217: $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope());
4218: }
4219: $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters);
4220: $closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses);
4221: if ($closureScope->equals($prevScope)) {
4222: break;
4223: }
4224: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
4225: $closureScope = $prevScope->generalizeWith($closureScope);
4226: }
4227: $count++;
4228: } while ($count < self::LOOP_SCOPE_ITERATIONS);
4229:
4230: $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel());
4231: $nodeCallback(new ClosureReturnStatementsNode(
4232: $expr,
4233: $gatheredReturnStatements,
4234: $gatheredYieldStatements,
4235: $statementResult,
4236: $executionEnds,
4237: array_merge($statementResult->getImpurePoints(), $closureImpurePoints),
4238: ), $closureScope);
4239:
4240: return new ProcessClosureResult($scope->processClosureScope($closureScope, null, $byRefUses), $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions);
4241: }
4242:
4243: /**
4244: * @param InvalidateExprNode[] $invalidatedExpressions
4245: * @param string[] $uses
4246: */
4247: private function processImmediatelyCalledCallable(MutatingScope $scope, array $invalidatedExpressions, array $uses): MutatingScope
4248: {
4249: if ($scope->isInClass()) {
4250: $uses[] = 'this';
4251: }
4252:
4253: $finder = new NodeFinder();
4254: foreach ($invalidatedExpressions as $invalidateExpression) {
4255: $found = false;
4256: foreach ($uses as $use) {
4257: $result = $finder->findFirst([$invalidateExpression->getExpr()], static fn ($node) => $node instanceof Variable && $node->name === $use);
4258: if ($result === null) {
4259: continue;
4260: }
4261:
4262: $found = true;
4263: break;
4264: }
4265:
4266: if (!$found) {
4267: continue;
4268: }
4269:
4270: $scope = $scope->invalidateExpression($invalidateExpression->getExpr(), true);
4271: }
4272:
4273: return $scope;
4274: }
4275:
4276: /**
4277: * @param callable(Node $node, Scope $scope): void $nodeCallback
4278: */
4279: private function processArrowFunctionNode(
4280: Node\Stmt $stmt,
4281: Expr\ArrowFunction $expr,
4282: MutatingScope $scope,
4283: callable $nodeCallback,
4284: ?Type $passedToType,
4285: ): ExpressionResult
4286: {
4287: foreach ($expr->params as $param) {
4288: $this->processParamNode($stmt, $param, $scope, $nodeCallback);
4289: }
4290: if ($expr->returnType !== null) {
4291: $nodeCallback($expr->returnType, $scope);
4292: }
4293:
4294: $arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME);
4295: $arrowFunctionScope = $scope->enterArrowFunction($expr, $this->createCallableParameters(
4296: $scope,
4297: $expr,
4298: $arrowFunctionCallArgs,
4299: $passedToType,
4300: ));
4301: $arrowFunctionType = $arrowFunctionScope->getAnonymousFunctionReflection();
4302: if (!$arrowFunctionType instanceof ClosureType) {
4303: throw new ShouldNotHappenException();
4304: }
4305: $nodeCallback(new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope);
4306: $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel());
4307:
4308: return new ExpressionResult($scope, false, $exprResult->getThrowPoints(), $exprResult->getImpurePoints());
4309: }
4310:
4311: /**
4312: * @param Node\Arg[] $args
4313: * @return ParameterReflection[]|null
4314: */
4315: public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array
4316: {
4317: $callableParameters = null;
4318: if ($args !== null) {
4319: $acceptors = $scope->getType($closureExpr)->getCallableParametersAcceptors($scope);
4320: if (count($acceptors) === 1) {
4321: $callableParameters = $acceptors[0]->getParameters();
4322:
4323: foreach ($callableParameters as $index => $callableParameter) {
4324: if (!isset($args[$index])) {
4325: continue;
4326: }
4327:
4328: $type = $scope->getType($args[$index]->value);
4329: $callableParameters[$index] = new NativeParameterReflection(
4330: $callableParameter->getName(),
4331: $callableParameter->isOptional(),
4332: $type,
4333: $callableParameter->passedByReference(),
4334: $callableParameter->isVariadic(),
4335: $callableParameter->getDefaultValue(),
4336: );
4337: }
4338: }
4339: } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) {
4340: if ($passedToType instanceof UnionType) {
4341: $passedToType = TypeCombinator::union(...array_filter(
4342: $passedToType->getTypes(),
4343: static fn (Type $type) => $type->isCallable()->yes(),
4344: ));
4345: }
4346:
4347: $acceptors = $passedToType->getCallableParametersAcceptors($scope);
4348: if (count($acceptors) > 0) {
4349: foreach ($acceptors as $acceptor) {
4350: if ($callableParameters === null) {
4351: $callableParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection(
4352: $callableParameter->getName(),
4353: $callableParameter->isOptional(),
4354: $callableParameter->getType(),
4355: $callableParameter->passedByReference(),
4356: $callableParameter->isVariadic(),
4357: $callableParameter->getDefaultValue(),
4358: ), $acceptor->getParameters());
4359: continue;
4360: }
4361:
4362: $newParameters = [];
4363: foreach ($acceptor->getParameters() as $i => $callableParameter) {
4364: if (!array_key_exists($i, $callableParameters)) {
4365: $newParameters[] = $callableParameter;
4366: continue;
4367: }
4368:
4369: $newParameters[] = $callableParameters[$i]->union(new NativeParameterReflection(
4370: $callableParameter->getName(),
4371: $callableParameter->isOptional(),
4372: $callableParameter->getType(),
4373: $callableParameter->passedByReference(),
4374: $callableParameter->isVariadic(),
4375: $callableParameter->getDefaultValue(),
4376: ));
4377: }
4378:
4379: $callableParameters = $newParameters;
4380: }
4381: }
4382: }
4383:
4384: return $callableParameters;
4385: }
4386:
4387: /**
4388: * @param callable(Node $node, Scope $scope): void $nodeCallback
4389: */
4390: private function processParamNode(
4391: Node\Stmt $stmt,
4392: Node\Param $param,
4393: MutatingScope $scope,
4394: callable $nodeCallback,
4395: ): void
4396: {
4397: $this->processAttributeGroups($stmt, $param->attrGroups, $scope, $nodeCallback);
4398: $nodeCallback($param, $scope);
4399: if ($param->type !== null) {
4400: $nodeCallback($param->type, $scope);
4401: }
4402: if ($param->default === null) {
4403: return;
4404: }
4405:
4406: $this->processExprNode($stmt, $param->default, $scope, $nodeCallback, ExpressionContext::createDeep());
4407: }
4408:
4409: /**
4410: * @param AttributeGroup[] $attrGroups
4411: * @param callable(Node $node, Scope $scope): void $nodeCallback
4412: */
4413: private function processAttributeGroups(
4414: Node\Stmt $stmt,
4415: array $attrGroups,
4416: MutatingScope $scope,
4417: callable $nodeCallback,
4418: ): void
4419: {
4420: foreach ($attrGroups as $attrGroup) {
4421: foreach ($attrGroup->attrs as $attr) {
4422: foreach ($attr->args as $arg) {
4423: $this->processExprNode($stmt, $arg->value, $scope, $nodeCallback, ExpressionContext::createDeep());
4424: $nodeCallback($arg, $scope);
4425: }
4426: $nodeCallback($attr, $scope);
4427: }
4428: $nodeCallback($attrGroup, $scope);
4429: }
4430: }
4431:
4432: /**
4433: * @param MethodReflection|FunctionReflection|null $calleeReflection
4434: * @param callable(Node $node, Scope $scope): void $nodeCallback
4435: */
4436: private function processArgs(
4437: Node\Stmt $stmt,
4438: $calleeReflection,
4439: ?ExtendedMethodReflection $nakedMethodReflection,
4440: ?ParametersAcceptor $parametersAcceptor,
4441: CallLike $callLike,
4442: MutatingScope $scope,
4443: callable $nodeCallback,
4444: ExpressionContext $context,
4445: ?MutatingScope $closureBindScope = null,
4446: ): ExpressionResult
4447: {
4448: $args = $callLike->getArgs();
4449:
4450: if ($parametersAcceptor !== null) {
4451: $parameters = $parametersAcceptor->getParameters();
4452: }
4453:
4454: $hasYield = false;
4455: $throwPoints = [];
4456: $impurePoints = [];
4457: foreach ($args as $i => $arg) {
4458: $assignByReference = false;
4459: $parameter = null;
4460: $parameterType = null;
4461: $parameterNativeType = null;
4462: if (isset($parameters) && $parametersAcceptor !== null) {
4463: if (isset($parameters[$i])) {
4464: $assignByReference = $parameters[$i]->passedByReference()->createsNewVariable();
4465: $parameterType = $parameters[$i]->getType();
4466:
4467: if ($parameters[$i] instanceof ParameterReflectionWithPhpDocs) {
4468: $parameterNativeType = $parameters[$i]->getNativeType();
4469: }
4470: $parameter = $parameters[$i];
4471: } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) {
4472: $lastParameter = $parameters[count($parameters) - 1];
4473: $assignByReference = $lastParameter->passedByReference()->createsNewVariable();
4474: $parameterType = $lastParameter->getType();
4475:
4476: if ($lastParameter instanceof ParameterReflectionWithPhpDocs) {
4477: $parameterNativeType = $lastParameter->getNativeType();
4478: }
4479: $parameter = $lastParameter;
4480: }
4481: }
4482:
4483: $lookForUnset = false;
4484: if ($assignByReference) {
4485: if ($arg->value instanceof Variable) {
4486: $isBuiltin = false;
4487: if ($calleeReflection instanceof FunctionReflection && $calleeReflection->isBuiltin()) {
4488: $isBuiltin = true;
4489: } elseif ($calleeReflection instanceof ExtendedMethodReflection && $calleeReflection->getDeclaringClass()->isBuiltin()) {
4490: $isBuiltin = true;
4491: }
4492: if (
4493: $isBuiltin
4494: || ($parameterNativeType === null || !$parameterNativeType->isNull()->no())
4495: ) {
4496: $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $arg->value);
4497: $lookForUnset = true;
4498: }
4499: }
4500: }
4501:
4502: if ($calleeReflection !== null) {
4503: $scope = $scope->pushInFunctionCall($calleeReflection, $parameter);
4504: }
4505:
4506: $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg;
4507: $nodeCallback($originalArg, $scope);
4508:
4509: $originalScope = $scope;
4510: $scopeToPass = $scope;
4511: if ($i === 0 && $closureBindScope !== null) {
4512: $scopeToPass = $closureBindScope;
4513: }
4514:
4515: if ($parameter instanceof ParameterReflectionWithPhpDocs) {
4516: $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable();
4517: if ($parameterCallImmediately->maybe()) {
4518: $callCallbackImmediately = $calleeReflection instanceof FunctionReflection;
4519: } else {
4520: $callCallbackImmediately = $parameterCallImmediately->yes();
4521: }
4522: } else {
4523: $callCallbackImmediately = $calleeReflection instanceof FunctionReflection;
4524: }
4525: if ($arg->value instanceof Expr\Closure) {
4526: $restoreThisScope = null;
4527: if (
4528: $closureBindScope === null
4529: && $parameter instanceof ParameterReflectionWithPhpDocs
4530: && $parameter->getClosureThisType() !== null
4531: && !$arg->value->static
4532: ) {
4533: $restoreThisScope = $scopeToPass;
4534: $scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType());
4535: }
4536:
4537: if ($parameter !== null) {
4538: $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass);
4539:
4540: if ($overwritingParameterType !== null) {
4541: $parameterType = $overwritingParameterType;
4542: }
4543: }
4544:
4545: $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context);
4546: $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null);
4547: if ($callCallbackImmediately) {
4548: $throwPoints = array_merge($throwPoints, array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints()));
4549: $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints());
4550: }
4551:
4552: $uses = [];
4553: foreach ($arg->value->uses as $use) {
4554: if (!is_string($use->var->name)) {
4555: continue;
4556: }
4557:
4558: $uses[] = $use->var->name;
4559: }
4560:
4561: $scope = $closureResult->getScope();
4562: $invalidateExpressions = $closureResult->getInvalidateExpressions();
4563: if ($restoreThisScope !== null) {
4564: $nodeFinder = new NodeFinder();
4565: $cb = static fn ($expr) => $expr instanceof Variable && $expr->name === 'this';
4566: foreach ($invalidateExpressions as $j => $invalidateExprNode) {
4567: $foundThis = $nodeFinder->findFirst([$invalidateExprNode->getExpr()], $cb);
4568: if ($foundThis === null) {
4569: continue;
4570: }
4571:
4572: unset($invalidateExpressions[$j]);
4573: }
4574: $invalidateExpressions = array_values($invalidateExpressions);
4575: $scope = $scope->restoreThis($restoreThisScope);
4576: }
4577:
4578: $scope = $this->processImmediatelyCalledCallable($scope, $invalidateExpressions, $uses);
4579: } elseif ($arg->value instanceof Expr\ArrowFunction) {
4580: if (
4581: $closureBindScope === null
4582: && $parameter instanceof ParameterReflectionWithPhpDocs
4583: && $parameter->getClosureThisType() !== null
4584: && !$arg->value->static
4585: ) {
4586: $scopeToPass = $scopeToPass->assignVariable('this', $parameter->getClosureThisType(), new ObjectWithoutClassType());
4587: }
4588:
4589: if ($parameter !== null) {
4590: $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass);
4591:
4592: if ($overwritingParameterType !== null) {
4593: $parameterType = $overwritingParameterType;
4594: }
4595: }
4596:
4597: $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context);
4598: $arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $parameterType ?? null);
4599: if ($callCallbackImmediately) {
4600: $throwPoints = array_merge($throwPoints, array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $arg->value), $arrowFunctionResult->getThrowPoints()));
4601: $impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints());
4602: }
4603: } else {
4604: $exprType = $scope->getType($arg->value);
4605: $exprResult = $this->processExprNode($stmt, $arg->value, $scopeToPass, $nodeCallback, $context->enterDeep());
4606: $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
4607: $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints());
4608: $scope = $exprResult->getScope();
4609: $hasYield = $hasYield || $exprResult->hasYield();
4610:
4611: if ($exprType->isCallable()->yes()) {
4612: $acceptors = $exprType->getCallableParametersAcceptors($scope);
4613: if (count($acceptors) === 1) {
4614: $scope = $this->processImmediatelyCalledCallable($scope, $acceptors[0]->getInvalidateExpressions(), $acceptors[0]->getUsedVariables());
4615: if ($callCallbackImmediately) {
4616: $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? ThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : ThrowPoint::createImplicit($scope, $arg->value), $acceptors[0]->getThrowPoints());
4617: if (!$this->implicitThrows) {
4618: $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit()));
4619: }
4620: $throwPoints = array_merge($throwPoints, $callableThrowPoints);
4621: $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $arg->value, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $acceptors[0]->getImpurePoints()));
4622: }
4623: }
4624: }
4625: }
4626:
4627: if ($assignByReference && $lookForUnset) {
4628: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value);
4629: }
4630:
4631: if ($calleeReflection !== null) {
4632: $scope = $scope->popInFunctionCall();
4633: }
4634:
4635: if ($i !== 0 || $closureBindScope === null) {
4636: continue;
4637: }
4638:
4639: $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope);
4640: }
4641: foreach ($args as $i => $arg) {
4642: if (!isset($parameters) || $parametersAcceptor === null) {
4643: continue;
4644: }
4645:
4646: $byRefType = new MixedType();
4647: $assignByReference = false;
4648: $currentParameter = null;
4649: if (isset($parameters[$i])) {
4650: $currentParameter = $parameters[$i];
4651: } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) {
4652: $currentParameter = $parameters[count($parameters) - 1];
4653: }
4654:
4655: if ($currentParameter !== null) {
4656: $assignByReference = $currentParameter->passedByReference()->createsNewVariable();
4657: if ($assignByReference) {
4658: if ($currentParameter instanceof ParameterReflectionWithPhpDocs && $currentParameter->getOutType() !== null) {
4659: $byRefType = $currentParameter->getOutType();
4660: } elseif (
4661: $calleeReflection instanceof MethodReflection
4662: && !$calleeReflection->getDeclaringClass()->isBuiltin()
4663: && $this->paramOutType
4664: ) {
4665: $byRefType = $currentParameter->getType();
4666: } elseif (
4667: $calleeReflection instanceof FunctionReflection
4668: && !$calleeReflection->isBuiltin()
4669: && $this->paramOutType
4670: ) {
4671: $byRefType = $currentParameter->getType();
4672: }
4673: }
4674: }
4675:
4676: if ($assignByReference) {
4677: if ($currentParameter === null) {
4678: throw new ShouldNotHappenException();
4679: }
4680:
4681: $argValue = $arg->value;
4682: if ($argValue instanceof Variable && is_string($argValue->name)) {
4683: if ($argValue->name !== 'this') {
4684: $paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope);
4685: if ($paramOutType !== null) {
4686: $byRefType = $paramOutType;
4687: }
4688:
4689: $nodeCallback(new VariableAssignNode($argValue, new TypeExpr($byRefType), false), $scope);
4690: $scope = $scope->assignVariable($argValue->name, $byRefType, new MixedType());
4691: }
4692: } else {
4693: $scope = $scope->invalidateExpression($argValue);
4694: }
4695: } elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) {
4696: $argType = $scope->getType($arg->value);
4697: if (!$argType->isObject()->no()) {
4698: $nakedReturnType = null;
4699: if ($nakedMethodReflection !== null) {
4700: $nakedParametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
4701: $scope,
4702: $args,
4703: $nakedMethodReflection->getVariants(),
4704: $nakedMethodReflection->getNamedArgumentsVariants(),
4705: );
4706: $nakedReturnType = $nakedParametersAcceptor->getReturnType();
4707: }
4708: if (
4709: $nakedReturnType === null
4710: || !(new ThisType($nakedMethodReflection->getDeclaringClass()))->isSuperTypeOf($nakedReturnType)->yes()
4711: || $nakedMethodReflection->isPure()->no()
4712: ) {
4713: $nodeCallback(new InvalidateExprNode($arg->value), $scope);
4714: $scope = $scope->invalidateExpression($arg->value, true);
4715: }
4716: } elseif (!(new ResourceType())->isSuperTypeOf($argType)->no()) {
4717: $nodeCallback(new InvalidateExprNode($arg->value), $scope);
4718: $scope = $scope->invalidateExpression($arg->value, true);
4719: }
4720: }
4721: }
4722:
4723: return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints);
4724: }
4725:
4726: /**
4727: * @param MethodReflection|FunctionReflection|null $calleeReflection
4728: */
4729: private function getParameterTypeFromParameterClosureTypeExtension(CallLike $callLike, $calleeReflection, ParameterReflection $parameter, MutatingScope $scope): ?Type
4730: {
4731: if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) {
4732: foreach ($this->parameterClosureTypeExtensionProvider->getFunctionParameterClosureTypeExtensions() as $functionParameterClosureTypeExtension) {
4733: if ($functionParameterClosureTypeExtension->isFunctionSupported($calleeReflection, $parameter)) {
4734: return $functionParameterClosureTypeExtension->getTypeFromFunctionCall($calleeReflection, $callLike, $parameter, $scope);
4735: }
4736: }
4737: } elseif ($calleeReflection instanceof MethodReflection) {
4738: if ($callLike instanceof StaticCall) {
4739: foreach ($this->parameterClosureTypeExtensionProvider->getStaticMethodParameterClosureTypeExtensions() as $staticMethodParameterClosureTypeExtension) {
4740: if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) {
4741: return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $callLike, $parameter, $scope);
4742: }
4743: }
4744: } elseif ($callLike instanceof MethodCall) {
4745: foreach ($this->parameterClosureTypeExtensionProvider->getMethodParameterClosureTypeExtensions() as $methodParameterClosureTypeExtension) {
4746: if ($methodParameterClosureTypeExtension->isMethodSupported($calleeReflection, $parameter)) {
4747: return $methodParameterClosureTypeExtension->getTypeFromMethodCall($calleeReflection, $callLike, $parameter, $scope);
4748: }
4749: }
4750: }
4751: }
4752:
4753: return null;
4754: }
4755:
4756: /**
4757: * @param MethodReflection|FunctionReflection|null $calleeReflection
4758: */
4759: private function getParameterOutExtensionsType(CallLike $callLike, $calleeReflection, ParameterReflection $currentParameter, MutatingScope $scope): ?Type
4760: {
4761: $paramOutTypes = [];
4762: if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) {
4763: foreach ($this->parameterOutTypeExtensionProvider->getFunctionParameterOutTypeExtensions() as $functionParameterOutTypeExtension) {
4764: if (!$functionParameterOutTypeExtension->isFunctionSupported($calleeReflection, $currentParameter)) {
4765: continue;
4766: }
4767:
4768: $resolvedType = $functionParameterOutTypeExtension->getParameterOutTypeFromFunctionCall($calleeReflection, $callLike, $currentParameter, $scope);
4769: if ($resolvedType === null) {
4770: continue;
4771: }
4772: $paramOutTypes[] = $resolvedType;
4773: }
4774: } elseif ($callLike instanceof MethodCall && $calleeReflection instanceof MethodReflection) {
4775: foreach ($this->parameterOutTypeExtensionProvider->getMethodParameterOutTypeExtensions() as $methodParameterOutTypeExtension) {
4776: if (!$methodParameterOutTypeExtension->isMethodSupported($calleeReflection, $currentParameter)) {
4777: continue;
4778: }
4779:
4780: $resolvedType = $methodParameterOutTypeExtension->getParameterOutTypeFromMethodCall($calleeReflection, $callLike, $currentParameter, $scope);
4781: if ($resolvedType === null) {
4782: continue;
4783: }
4784: $paramOutTypes[] = $resolvedType;
4785: }
4786: } elseif ($callLike instanceof StaticCall && $calleeReflection instanceof MethodReflection) {
4787: foreach ($this->parameterOutTypeExtensionProvider->getStaticMethodParameterOutTypeExtensions() as $staticMethodParameterOutTypeExtension) {
4788: if (!$staticMethodParameterOutTypeExtension->isStaticMethodSupported($calleeReflection, $currentParameter)) {
4789: continue;
4790: }
4791:
4792: $resolvedType = $staticMethodParameterOutTypeExtension->getParameterOutTypeFromStaticMethodCall($calleeReflection, $callLike, $currentParameter, $scope);
4793: if ($resolvedType === null) {
4794: continue;
4795: }
4796: $paramOutTypes[] = $resolvedType;
4797: }
4798: }
4799:
4800: if (count($paramOutTypes) === 1) {
4801: return $paramOutTypes[0];
4802: }
4803:
4804: if (count($paramOutTypes) > 1) {
4805: return TypeCombinator::union(...$paramOutTypes);
4806: }
4807:
4808: return null;
4809: }
4810:
4811: /**
4812: * @param callable(Node $node, Scope $scope): void $nodeCallback
4813: * @param Closure(MutatingScope $scope): ExpressionResult $processExprCallback
4814: */
4815: private function processAssignVar(
4816: MutatingScope $scope,
4817: Node\Stmt $stmt,
4818: Expr $var,
4819: Expr $assignedExpr,
4820: callable $nodeCallback,
4821: ExpressionContext $context,
4822: Closure $processExprCallback,
4823: bool $enterExpressionAssign,
4824: ): ExpressionResult
4825: {
4826: $nodeCallback($var, $enterExpressionAssign ? $scope->enterExpressionAssign($var) : $scope);
4827: $hasYield = false;
4828: $throwPoints = [];
4829: $impurePoints = [];
4830: $isAssignOp = $assignedExpr instanceof Expr\AssignOp && !$enterExpressionAssign;
4831: if ($var instanceof Variable && is_string($var->name)) {
4832: $result = $processExprCallback($scope);
4833: $hasYield = $result->hasYield();
4834: $throwPoints = $result->getThrowPoints();
4835: $impurePoints = $result->getImpurePoints();
4836: if (in_array($var->name, Scope::SUPERGLOBAL_VARIABLES, true)) {
4837: $impurePoints[] = new ImpurePoint($scope, $var, 'superglobal', 'assign to superglobal variable', true);
4838: }
4839: $assignedExpr = $this->unwrapAssign($assignedExpr);
4840: $type = $scope->getType($assignedExpr);
4841:
4842: $conditionalExpressions = [];
4843: if ($assignedExpr instanceof Ternary) {
4844: $if = $assignedExpr->if;
4845: if ($if === null) {
4846: $if = $assignedExpr->cond;
4847: }
4848: $condScope = $this->processExprNode($stmt, $assignedExpr->cond, $scope, static function (): void {
4849: }, ExpressionContext::createDeep())->getScope();
4850: $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($condScope, $assignedExpr->cond, TypeSpecifierContext::createTruthy());
4851: $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($condScope, $assignedExpr->cond, TypeSpecifierContext::createFalsey());
4852: $truthyScope = $condScope->filterBySpecifiedTypes($truthySpecifiedTypes);
4853: $falsyScope = $condScope->filterBySpecifiedTypes($falseySpecifiedTypes);
4854: $truthyType = $truthyScope->getType($if);
4855: $falseyType = $falsyScope->getType($assignedExpr->else);
4856:
4857: if (
4858: $truthyType->isSuperTypeOf($falseyType)->no()
4859: && $falseyType->isSuperTypeOf($truthyType)->no()
4860: ) {
4861: $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType);
4862: $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType);
4863: $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType);
4864: $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($condScope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType);
4865: }
4866: }
4867:
4868: $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy());
4869: $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createFalsey());
4870:
4871: $truthyType = TypeCombinator::removeFalsey($type);
4872: $falseyType = TypeCombinator::intersect($type, StaticTypeFactory::falsey());
4873:
4874: $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType);
4875: $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType);
4876: $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType);
4877: $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType);
4878:
4879: $nodeCallback(new VariableAssignNode($var, $assignedExpr, $isAssignOp), $result->getScope());
4880: $scope = $result->getScope()->assignVariable($var->name, $type, $scope->getNativeType($assignedExpr));
4881: foreach ($conditionalExpressions as $exprString => $holders) {
4882: $scope = $scope->addConditionalExpressions($exprString, $holders);
4883: }
4884: } elseif ($var instanceof ArrayDimFetch) {
4885: $dimFetchStack = [];
4886: $originalVar = $var;
4887: $assignedPropertyExpr = $assignedExpr;
4888: while ($var instanceof ArrayDimFetch) {
4889: $varForSetOffsetValue = $var->var;
4890: if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) {
4891: $varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue);
4892: }
4893: $assignedPropertyExpr = new SetOffsetValueTypeExpr(
4894: $varForSetOffsetValue,
4895: $var->dim,
4896: $assignedPropertyExpr,
4897: );
4898: $dimFetchStack[] = $var;
4899: $var = $var->var;
4900: }
4901:
4902: // 1. eval root expr
4903: if ($enterExpressionAssign) {
4904: $scope = $scope->enterExpressionAssign($var);
4905: }
4906: $result = $this->processExprNode($stmt, $var, $scope, $nodeCallback, $context->enterDeep());
4907: $hasYield = $result->hasYield();
4908: $throwPoints = $result->getThrowPoints();
4909: $impurePoints = $result->getImpurePoints();
4910: $scope = $result->getScope();
4911: if ($enterExpressionAssign) {
4912: $scope = $scope->exitExpressionAssign($var);
4913: }
4914:
4915: // 2. eval dimensions
4916: $offsetTypes = [];
4917: $offsetNativeTypes = [];
4918: $dimFetchStack = array_reverse($dimFetchStack);
4919: $lastDimKey = array_key_last($dimFetchStack);
4920: foreach ($dimFetchStack as $key => $dimFetch) {
4921: $dimExpr = $dimFetch->dim;
4922:
4923: // Callback was already called for last dim at the beginning of the method.
4924: if ($key !== $lastDimKey) {
4925: $nodeCallback($dimFetch, $enterExpressionAssign ? $scope->enterExpressionAssign($dimFetch) : $scope);
4926: }
4927:
4928: if ($dimExpr === null) {
4929: $offsetTypes[] = null;
4930: $offsetNativeTypes[] = null;
4931:
4932: } else {
4933: $offsetTypes[] = $scope->getType($dimExpr);
4934: $offsetNativeTypes[] = $scope->getNativeType($dimExpr);
4935:
4936: if ($enterExpressionAssign) {
4937: $scope->enterExpressionAssign($dimExpr);
4938: }
4939: $result = $this->processExprNode($stmt, $dimExpr, $scope, $nodeCallback, $context->enterDeep());
4940: $hasYield = $hasYield || $result->hasYield();
4941: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
4942: $scope = $result->getScope();
4943:
4944: if ($enterExpressionAssign) {
4945: $scope = $scope->exitExpressionAssign($dimExpr);
4946: }
4947: }
4948: }
4949:
4950: $valueToWrite = $scope->getType($assignedExpr);
4951: $nativeValueToWrite = $scope->getNativeType($assignedExpr);
4952: $originalValueToWrite = $valueToWrite;
4953: $originalNativeValueToWrite = $valueToWrite;
4954:
4955: // 3. eval assigned expr
4956: $result = $processExprCallback($scope);
4957: $hasYield = $hasYield || $result->hasYield();
4958: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
4959: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
4960: $scope = $result->getScope();
4961:
4962: $varType = $scope->getType($var);
4963: $varNativeType = $scope->getNativeType($var);
4964:
4965: // 4. compose types
4966: if ($varType instanceof ErrorType) {
4967: $varType = new ConstantArrayType([], []);
4968: }
4969: if ($varNativeType instanceof ErrorType) {
4970: $varNativeType = new ConstantArrayType([], []);
4971: }
4972: $offsetValueType = $varType;
4973: $offsetNativeValueType = $varNativeType;
4974: $offsetValueTypeStack = [$offsetValueType];
4975: $offsetValueNativeTypeStack = [$offsetNativeValueType];
4976: foreach (array_slice($offsetTypes, 0, -1) as $offsetType) {
4977: if ($offsetType === null) {
4978: $offsetValueType = new ConstantArrayType([], []);
4979:
4980: } else {
4981: $offsetValueType = $offsetValueType->getOffsetValueType($offsetType);
4982: if ($offsetValueType instanceof ErrorType) {
4983: $offsetValueType = new ConstantArrayType([], []);
4984: }
4985: }
4986:
4987: $offsetValueTypeStack[] = $offsetValueType;
4988: }
4989: foreach (array_slice($offsetNativeTypes, 0, -1) as $offsetNativeType) {
4990: if ($offsetNativeType === null) {
4991: $offsetNativeValueType = new ConstantArrayType([], []);
4992:
4993: } else {
4994: $offsetNativeValueType = $offsetNativeValueType->getOffsetValueType($offsetNativeType);
4995: if ($offsetNativeValueType instanceof ErrorType) {
4996: $offsetNativeValueType = new ConstantArrayType([], []);
4997: }
4998: }
4999:
5000: $offsetValueNativeTypeStack[] = $offsetNativeValueType;
5001: }
5002:
5003: foreach (array_reverse($offsetTypes) as $i => $offsetType) {
5004: /** @var Type $offsetValueType */
5005: $offsetValueType = array_pop($offsetValueTypeStack);
5006: if (!$offsetValueType instanceof MixedType) {
5007: $types = [
5008: new ArrayType(new MixedType(), new MixedType()),
5009: new ObjectType(ArrayAccess::class),
5010: new NullType(),
5011: ];
5012: if ($offsetType !== null && $offsetType->isInteger()->yes()) {
5013: $types[] = new StringType();
5014: }
5015: $offsetValueType = TypeCombinator::intersect($offsetValueType, TypeCombinator::union(...$types));
5016: }
5017: $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0);
5018: }
5019: foreach (array_reverse($offsetNativeTypes) as $i => $offsetNativeType) {
5020: /** @var Type $offsetNativeValueType */
5021: $offsetNativeValueType = array_pop($offsetValueNativeTypeStack);
5022: if (!$offsetNativeValueType instanceof MixedType) {
5023: $types = [
5024: new ArrayType(new MixedType(), new MixedType()),
5025: new ObjectType(ArrayAccess::class),
5026: new NullType(),
5027: ];
5028: if ($offsetNativeType !== null && $offsetNativeType->isInteger()->yes()) {
5029: $types[] = new StringType();
5030: }
5031: $offsetNativeValueType = TypeCombinator::intersect($offsetNativeValueType, TypeCombinator::union(...$types));
5032: }
5033: $nativeValueToWrite = $offsetNativeValueType->setOffsetValueType($offsetNativeType, $nativeValueToWrite, $i === 0);
5034: }
5035:
5036: if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) {
5037: if ($var instanceof Variable && is_string($var->name)) {
5038: $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope);
5039: $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite);
5040: } else {
5041: if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) {
5042: $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope);
5043: if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) {
5044: $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString());
5045: }
5046: }
5047: $scope = $scope->assignExpression(
5048: $var,
5049: $valueToWrite,
5050: $nativeValueToWrite,
5051: );
5052: }
5053:
5054: if ($originalVar->dim instanceof Variable || $originalVar->dim instanceof Node\Scalar) {
5055: $currentVarType = $scope->getType($originalVar);
5056: if (!$originalValueToWrite->isSuperTypeOf($currentVarType)->yes()) {
5057: $scope = $scope->assignExpression(
5058: $originalVar,
5059: $originalValueToWrite,
5060: $originalNativeValueToWrite,
5061: );
5062: }
5063: }
5064: } else {
5065: if ($var instanceof Variable) {
5066: $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope);
5067: } elseif ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) {
5068: $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope);
5069: if ($var instanceof PropertyFetch && $var->name instanceof Node\Identifier && !$isAssignOp) {
5070: $scope = $scope->assignInitializedProperty($scope->getType($var->var), $var->name->toString());
5071: }
5072: }
5073: }
5074:
5075: if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) {
5076: $throwPoints = array_merge($throwPoints, $this->processExprNode(
5077: $stmt,
5078: new MethodCall($var, 'offsetSet'),
5079: $scope,
5080: static function (): void {
5081: },
5082: $context,
5083: )->getThrowPoints());
5084: }
5085: } elseif ($var instanceof PropertyFetch) {
5086: $objectResult = $this->processExprNode($stmt, $var->var, $scope, $nodeCallback, $context);
5087: $hasYield = $objectResult->hasYield();
5088: $throwPoints = $objectResult->getThrowPoints();
5089: $impurePoints = $objectResult->getImpurePoints();
5090: $scope = $objectResult->getScope();
5091:
5092: $propertyName = null;
5093: if ($var->name instanceof Node\Identifier) {
5094: $propertyName = $var->name->name;
5095: } else {
5096: $propertyNameResult = $this->processExprNode($stmt, $var->name, $scope, $nodeCallback, $context);
5097: $hasYield = $hasYield || $propertyNameResult->hasYield();
5098: $throwPoints = array_merge($throwPoints, $propertyNameResult->getThrowPoints());
5099: $impurePoints = array_merge($impurePoints, $propertyNameResult->getImpurePoints());
5100: $scope = $propertyNameResult->getScope();
5101: }
5102:
5103: $result = $processExprCallback($scope);
5104: $hasYield = $hasYield || $result->hasYield();
5105: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
5106: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
5107: $scope = $result->getScope();
5108:
5109: $propertyHolderType = $scope->getType($var->var);
5110: if ($propertyName !== null && $propertyHolderType->hasProperty($propertyName)->yes()) {
5111: $propertyReflection = $propertyHolderType->getProperty($propertyName, $scope);
5112: $assignedExprType = $scope->getType($assignedExpr);
5113: $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope);
5114: if ($propertyReflection->canChangeTypeAfterAssignment()) {
5115: $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
5116: }
5117: $declaringClass = $propertyReflection->getDeclaringClass();
5118: if ($declaringClass->hasNativeProperty($propertyName)) {
5119: $nativeProperty = $declaringClass->getNativeProperty($propertyName);
5120: if (
5121: !$nativeProperty->getNativeType()->accepts($assignedExprType, true)->yes()
5122: ) {
5123: $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(TypeError::class), $assignedExpr, false);
5124: }
5125: if ($enterExpressionAssign) {
5126: $scope = $scope->assignInitializedProperty($propertyHolderType, $propertyName);
5127: }
5128: }
5129: } else {
5130: // fallback
5131: $assignedExprType = $scope->getType($assignedExpr);
5132: $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope);
5133: $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
5134: // simulate dynamic property assign by __set to get throw points
5135: if (!$propertyHolderType->hasMethod('__set')->no()) {
5136: $throwPoints = array_merge($throwPoints, $this->processExprNode(
5137: $stmt,
5138: new MethodCall($var->var, '__set'),
5139: $scope,
5140: static function (): void {
5141: },
5142: $context,
5143: )->getThrowPoints());
5144: }
5145: }
5146:
5147: } elseif ($var instanceof Expr\StaticPropertyFetch) {
5148: if ($var->class instanceof Node\Name) {
5149: $propertyHolderType = $scope->resolveTypeByName($var->class);
5150: } else {
5151: $this->processExprNode($stmt, $var->class, $scope, $nodeCallback, $context);
5152: $propertyHolderType = $scope->getType($var->class);
5153: }
5154:
5155: $propertyName = null;
5156: if ($var->name instanceof Node\Identifier) {
5157: $propertyName = $var->name->name;
5158: } else {
5159: $propertyNameResult = $this->processExprNode($stmt, $var->name, $scope, $nodeCallback, $context);
5160: $hasYield = $propertyNameResult->hasYield();
5161: $throwPoints = $propertyNameResult->getThrowPoints();
5162: $impurePoints = $propertyNameResult->getImpurePoints();
5163: $scope = $propertyNameResult->getScope();
5164: }
5165:
5166: $result = $processExprCallback($scope);
5167: $hasYield = $hasYield || $result->hasYield();
5168: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
5169: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
5170: $scope = $result->getScope();
5171:
5172: if ($propertyName !== null) {
5173: $propertyReflection = $scope->getPropertyReflection($propertyHolderType, $propertyName);
5174: $assignedExprType = $scope->getType($assignedExpr);
5175: $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope);
5176: if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) {
5177: $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
5178: }
5179: } else {
5180: // fallback
5181: $assignedExprType = $scope->getType($assignedExpr);
5182: $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope);
5183: $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
5184: }
5185: } elseif ($var instanceof List_ || $var instanceof Array_) {
5186: $result = $processExprCallback($scope);
5187: $hasYield = $result->hasYield();
5188: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
5189: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
5190: $scope = $result->getScope();
5191: foreach ($var->items as $i => $arrayItem) {
5192: if ($arrayItem === null) {
5193: continue;
5194: }
5195:
5196: $itemScope = $scope;
5197: if ($enterExpressionAssign) {
5198: $itemScope = $itemScope->enterExpressionAssign($arrayItem->value);
5199: }
5200: $itemScope = $this->lookForSetAllowedUndefinedExpressions($itemScope, $arrayItem->value);
5201: $nodeCallback($arrayItem, $itemScope);
5202: if ($arrayItem->key !== null) {
5203: $keyResult = $this->processExprNode($stmt, $arrayItem->key, $itemScope, $nodeCallback, $context->enterDeep());
5204: $hasYield = $hasYield || $keyResult->hasYield();
5205: $throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints());
5206: $impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints());
5207: $itemScope = $keyResult->getScope();
5208: }
5209:
5210: $valueResult = $this->processExprNode($stmt, $arrayItem->value, $itemScope, $nodeCallback, $context->enterDeep());
5211: $hasYield = $hasYield || $valueResult->hasYield();
5212: $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints());
5213: $impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints());
5214:
5215: if ($arrayItem->key === null) {
5216: $dimExpr = new Node\Scalar\LNumber($i);
5217: } else {
5218: $dimExpr = $arrayItem->key;
5219: }
5220: $result = $this->processAssignVar(
5221: $scope,
5222: $stmt,
5223: $arrayItem->value,
5224: new GetOffsetValueTypeExpr($assignedExpr, $dimExpr),
5225: $nodeCallback,
5226: $context,
5227: static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []),
5228: $enterExpressionAssign,
5229: );
5230: $scope = $result->getScope();
5231: $hasYield = $hasYield || $result->hasYield();
5232: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
5233: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
5234: }
5235: } elseif ($var instanceof ExistingArrayDimFetch) {
5236: $dimFetchStack = [];
5237: $assignedPropertyExpr = $assignedExpr;
5238: while ($var instanceof ExistingArrayDimFetch) {
5239: $varForSetOffsetValue = $var->getVar();
5240: if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) {
5241: $varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue);
5242: }
5243: $assignedPropertyExpr = new SetExistingOffsetValueTypeExpr(
5244: $varForSetOffsetValue,
5245: $var->getDim(),
5246: $assignedPropertyExpr,
5247: );
5248: $dimFetchStack[] = $var;
5249: $var = $var->getVar();
5250: }
5251:
5252: $offsetTypes = [];
5253: $offsetNativeTypes = [];
5254: foreach (array_reverse($dimFetchStack) as $dimFetch) {
5255: $dimExpr = $dimFetch->getDim();
5256: $offsetTypes[] = $scope->getType($dimExpr);
5257: $offsetNativeTypes[] = $scope->getNativeType($dimExpr);
5258: }
5259:
5260: $valueToWrite = $scope->getType($assignedExpr);
5261: $nativeValueToWrite = $scope->getNativeType($assignedExpr);
5262: $varType = $scope->getType($var);
5263: $varNativeType = $scope->getNativeType($var);
5264:
5265: $offsetValueType = $varType;
5266: $offsetNativeValueType = $varNativeType;
5267: $offsetValueTypeStack = [$offsetValueType];
5268: $offsetValueNativeTypeStack = [$offsetNativeValueType];
5269: foreach (array_slice($offsetTypes, 0, -1) as $offsetType) {
5270: $offsetValueType = $offsetValueType->getOffsetValueType($offsetType);
5271: $offsetValueTypeStack[] = $offsetValueType;
5272: }
5273: foreach (array_slice($offsetNativeTypes, 0, -1) as $offsetNativeType) {
5274: $offsetNativeValueType = $offsetNativeValueType->getOffsetValueType($offsetNativeType);
5275: $offsetValueNativeTypeStack[] = $offsetNativeValueType;
5276: }
5277:
5278: foreach (array_reverse($offsetTypes) as $offsetType) {
5279: /** @var Type $offsetValueType */
5280: $offsetValueType = array_pop($offsetValueTypeStack);
5281: $valueToWrite = $offsetValueType->setExistingOffsetValueType($offsetType, $valueToWrite);
5282: }
5283: foreach (array_reverse($offsetNativeTypes) as $offsetNativeType) {
5284: /** @var Type $offsetNativeValueType */
5285: $offsetNativeValueType = array_pop($offsetValueNativeTypeStack);
5286: $nativeValueToWrite = $offsetNativeValueType->setExistingOffsetValueType($offsetNativeType, $nativeValueToWrite);
5287: }
5288:
5289: if ($var instanceof Variable && is_string($var->name)) {
5290: $nodeCallback(new VariableAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope);
5291: $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite);
5292: } else {
5293: if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) {
5294: $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope);
5295: }
5296: $scope = $scope->assignExpression(
5297: $var,
5298: $valueToWrite,
5299: $nativeValueToWrite,
5300: );
5301: }
5302: }
5303:
5304: return new ExpressionResult($scope, $hasYield, $throwPoints, $impurePoints);
5305: }
5306:
5307: private function unwrapAssign(Expr $expr): Expr
5308: {
5309: if ($expr instanceof Assign) {
5310: return $this->unwrapAssign($expr->expr);
5311: }
5312:
5313: return $expr;
5314: }
5315:
5316: /**
5317: * @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
5318: * @return array<string, ConditionalExpressionHolder[]>
5319: */
5320: private function processSureTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType): array
5321: {
5322: foreach ($specifiedTypes->getSureTypes() as $exprString => [$expr, $exprType]) {
5323: if (!$expr instanceof Variable) {
5324: continue;
5325: }
5326: if (!is_string($expr->name)) {
5327: continue;
5328: }
5329:
5330: if ($expr->name === $variableName) {
5331: continue;
5332: }
5333:
5334: if (!isset($conditionalExpressions[$exprString])) {
5335: $conditionalExpressions[$exprString] = [];
5336: }
5337:
5338: $holder = new ConditionalExpressionHolder([
5339: '$' . $variableName => ExpressionTypeHolder::createYes(new Variable($variableName), $variableType),
5340: ], ExpressionTypeHolder::createYes(
5341: $expr,
5342: TypeCombinator::intersect($scope->getType($expr), $exprType),
5343: ));
5344: $conditionalExpressions[$exprString][$holder->getKey()] = $holder;
5345: }
5346:
5347: return $conditionalExpressions;
5348: }
5349:
5350: /**
5351: * @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
5352: * @return array<string, ConditionalExpressionHolder[]>
5353: */
5354: private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType): array
5355: {
5356: foreach ($specifiedTypes->getSureNotTypes() as $exprString => [$expr, $exprType]) {
5357: if (!$expr instanceof Variable) {
5358: continue;
5359: }
5360: if (!is_string($expr->name)) {
5361: continue;
5362: }
5363:
5364: if ($expr->name === $variableName) {
5365: continue;
5366: }
5367:
5368: if (!isset($conditionalExpressions[$exprString])) {
5369: $conditionalExpressions[$exprString] = [];
5370: }
5371:
5372: $holder = new ConditionalExpressionHolder([
5373: '$' . $variableName => ExpressionTypeHolder::createYes(new Variable($variableName), $variableType),
5374: ], ExpressionTypeHolder::createYes(
5375: $expr,
5376: TypeCombinator::remove($scope->getType($expr), $exprType),
5377: ));
5378: $conditionalExpressions[$exprString][$holder->getKey()] = $holder;
5379: }
5380:
5381: return $conditionalExpressions;
5382: }
5383:
5384: /**
5385: * @param callable(Node $node, Scope $scope): void $nodeCallback
5386: */
5387: private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, ?Expr $defaultExpr, callable $nodeCallback): MutatingScope
5388: {
5389: $function = $scope->getFunction();
5390: $variableLessTags = [];
5391:
5392: foreach ($stmt->getComments() as $comment) {
5393: if (!$comment instanceof Doc) {
5394: continue;
5395: }
5396:
5397: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
5398: $scope->getFile(),
5399: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
5400: $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
5401: $function !== null ? $function->getName() : null,
5402: $comment->getText(),
5403: );
5404:
5405: $assignedVariable = null;
5406: if (
5407: $stmt instanceof Node\Stmt\Expression
5408: && ($stmt->expr instanceof Assign || $stmt->expr instanceof AssignRef)
5409: && $stmt->expr->var instanceof Variable
5410: && is_string($stmt->expr->var->name)
5411: ) {
5412: $assignedVariable = $stmt->expr->var->name;
5413: }
5414:
5415: foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) {
5416: if (is_int($name)) {
5417: $variableLessTags[] = $varTag;
5418: continue;
5419: }
5420:
5421: if ($name === $assignedVariable) {
5422: continue;
5423: }
5424:
5425: $certainty = $scope->hasVariableType($name);
5426: if ($certainty->no()) {
5427: continue;
5428: }
5429:
5430: if ($scope->isInClass() && $scope->getFunction() === null) {
5431: continue;
5432: }
5433:
5434: if ($scope->canAnyVariableExist()) {
5435: $certainty = TrinaryLogic::createYes();
5436: }
5437:
5438: $variableNode = new Variable($name, $stmt->getAttributes());
5439: $originalType = $scope->getVariableType($name);
5440: if (!$originalType->equals($varTag->getType())) {
5441: $nodeCallback(new VarTagChangedExpressionTypeNode($varTag, $variableNode), $scope);
5442: }
5443:
5444: $scope = $scope->assignVariable(
5445: $name,
5446: $varTag->getType(),
5447: $scope->getNativeType($variableNode),
5448: $certainty,
5449: );
5450: }
5451: }
5452:
5453: if (count($variableLessTags) === 1 && $defaultExpr !== null) {
5454: $originalType = $scope->getType($defaultExpr);
5455: $varTag = $variableLessTags[0];
5456: if (!$originalType->equals($varTag->getType())) {
5457: $nodeCallback(new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope);
5458: }
5459: $scope = $scope->assignExpression($defaultExpr, $varTag->getType(), new MixedType());
5460: }
5461:
5462: return $scope;
5463: }
5464:
5465: /**
5466: * @param array<int, string> $variableNames
5467: */
5468: private function processVarAnnotation(MutatingScope $scope, array $variableNames, Node\Stmt $node, bool &$changed = false): MutatingScope
5469: {
5470: $function = $scope->getFunction();
5471: $varTags = [];
5472: foreach ($node->getComments() as $comment) {
5473: if (!$comment instanceof Doc) {
5474: continue;
5475: }
5476:
5477: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
5478: $scope->getFile(),
5479: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
5480: $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
5481: $function !== null ? $function->getName() : null,
5482: $comment->getText(),
5483: );
5484: foreach ($resolvedPhpDoc->getVarTags() as $key => $varTag) {
5485: $varTags[$key] = $varTag;
5486: }
5487: }
5488:
5489: if (count($varTags) === 0) {
5490: return $scope;
5491: }
5492:
5493: foreach ($variableNames as $variableName) {
5494: if (!isset($varTags[$variableName])) {
5495: continue;
5496: }
5497:
5498: $variableType = $varTags[$variableName]->getType();
5499: $changed = true;
5500: $scope = $scope->assignVariable($variableName, $variableType, new MixedType());
5501: }
5502:
5503: if (count($variableNames) === 1 && count($varTags) === 1 && isset($varTags[0])) {
5504: $variableType = $varTags[0]->getType();
5505: $changed = true;
5506: $scope = $scope->assignVariable($variableNames[0], $variableType, new MixedType());
5507: }
5508:
5509: return $scope;
5510: }
5511:
5512: private function enterForeach(MutatingScope $scope, MutatingScope $originalScope, Foreach_ $stmt): MutatingScope
5513: {
5514: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
5515: $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt);
5516: }
5517: $iterateeType = $originalScope->getType($stmt->expr);
5518: if (
5519: ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name))
5520: && ($stmt->keyVar === null || ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)))
5521: ) {
5522: $keyVarName = null;
5523: if ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)) {
5524: $keyVarName = $stmt->keyVar->name;
5525: }
5526: $scope = $scope->enterForeach(
5527: $originalScope,
5528: $stmt->expr,
5529: $stmt->valueVar->name,
5530: $keyVarName,
5531: );
5532: $vars = [$stmt->valueVar->name];
5533: if ($keyVarName !== null) {
5534: $vars[] = $keyVarName;
5535: }
5536: } else {
5537: $scope = $this->processAssignVar(
5538: $scope,
5539: $stmt,
5540: $stmt->valueVar,
5541: new GetIterableValueTypeExpr($stmt->expr),
5542: static function (): void {
5543: },
5544: ExpressionContext::createDeep(),
5545: static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []),
5546: true,
5547: )->getScope();
5548: $vars = $this->getAssignedVariables($stmt->valueVar);
5549: if (
5550: $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)
5551: ) {
5552: $scope = $scope->enterForeachKey($originalScope, $stmt->expr, $stmt->keyVar->name);
5553: $vars[] = $stmt->keyVar->name;
5554: } elseif ($stmt->keyVar !== null) {
5555: $scope = $this->processAssignVar(
5556: $scope,
5557: $stmt,
5558: $stmt->keyVar,
5559: new GetIterableKeyTypeExpr($stmt->expr),
5560: static function (): void {
5561: },
5562: ExpressionContext::createDeep(),
5563: static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, [], []),
5564: true,
5565: )->getScope();
5566: $vars = array_merge($vars, $this->getAssignedVariables($stmt->keyVar));
5567: }
5568: }
5569:
5570: $constantArrays = $iterateeType->getConstantArrays();
5571: if (
5572: $stmt->getDocComment() === null
5573: && $iterateeType->isConstantArray()->yes()
5574: && count($constantArrays) === 1
5575: && $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)
5576: && $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)
5577: ) {
5578: $valueConditionalHolders = [];
5579: $arrayDimFetchConditionalHolders = [];
5580: foreach ($constantArrays[0]->getKeyTypes() as $i => $keyType) {
5581: $valueType = $constantArrays[0]->getValueTypes()[$i];
5582: $holder = new ConditionalExpressionHolder([
5583: '$' . $stmt->keyVar->name => ExpressionTypeHolder::createYes(new Variable($stmt->keyVar->name), $keyType),
5584: ], new ExpressionTypeHolder($stmt->valueVar, $valueType, TrinaryLogic::createYes()));
5585: $valueConditionalHolders[$holder->getKey()] = $holder;
5586: $arrayDimFetchHolder = new ConditionalExpressionHolder([
5587: '$' . $stmt->keyVar->name => ExpressionTypeHolder::createYes(new Variable($stmt->keyVar->name), $keyType),
5588: ], new ExpressionTypeHolder(new ArrayDimFetch($stmt->expr, $stmt->keyVar), $valueType, TrinaryLogic::createYes()));
5589: $arrayDimFetchConditionalHolders[$arrayDimFetchHolder->getKey()] = $arrayDimFetchHolder;
5590: }
5591:
5592: $scope = $scope->addConditionalExpressions(
5593: '$' . $stmt->valueVar->name,
5594: $valueConditionalHolders,
5595: );
5596: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
5597: $scope = $scope->addConditionalExpressions(
5598: sprintf('$%s[$%s]', $stmt->expr->name, $stmt->keyVar->name),
5599: $arrayDimFetchConditionalHolders,
5600: );
5601: }
5602: }
5603:
5604: return $this->processVarAnnotation($scope, $vars, $stmt);
5605: }
5606:
5607: /**
5608: * @param callable(Node $node, Scope $scope): void $nodeCallback
5609: */
5610: private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classScope, callable $nodeCallback): void
5611: {
5612: $parentTraitNames = [];
5613: $parent = $classScope->getParentScope();
5614: while ($parent !== null) {
5615: if ($parent->isInTrait()) {
5616: $parentTraitNames[] = $parent->getTraitReflection()->getName();
5617: }
5618: $parent = $parent->getParentScope();
5619: }
5620:
5621: foreach ($node->traits as $trait) {
5622: $traitName = (string) $trait;
5623: if (in_array($traitName, $parentTraitNames, true)) {
5624: continue;
5625: }
5626: if (!$this->reflectionProvider->hasClass($traitName)) {
5627: continue;
5628: }
5629: $traitReflection = $this->reflectionProvider->getClass($traitName);
5630: $traitFileName = $traitReflection->getFileName();
5631: if ($traitFileName === null) {
5632: continue; // trait from eval or from PHP itself
5633: }
5634: $fileName = $this->fileHelper->normalizePath($traitFileName);
5635: if (!isset($this->analysedFiles[$fileName])) {
5636: continue;
5637: }
5638: $adaptations = [];
5639: foreach ($node->adaptations as $adaptation) {
5640: if ($adaptation->trait === null) {
5641: $adaptations[] = $adaptation;
5642: continue;
5643: }
5644: if ($adaptation->trait->toLowerString() !== $trait->toLowerString()) {
5645: continue;
5646: }
5647:
5648: $adaptations[] = $adaptation;
5649: }
5650: $parserNodes = $this->parser->parseFile($fileName);
5651: $this->processNodesForTraitUse($parserNodes, $traitReflection, $classScope, $adaptations, $nodeCallback);
5652: }
5653: }
5654:
5655: /**
5656: * @param Node[]|Node|scalar|null $node
5657: * @param Node\Stmt\TraitUseAdaptation[] $adaptations
5658: * @param callable(Node $node, Scope $scope): void $nodeCallback
5659: */
5660: private function processNodesForTraitUse($node, ClassReflection $traitReflection, MutatingScope $scope, array $adaptations, callable $nodeCallback): void
5661: {
5662: if ($node instanceof Node) {
5663: if ($node instanceof Node\Stmt\Trait_ && $traitReflection->getName() === (string) $node->namespacedName && $traitReflection->getNativeReflection()->getStartLine() === $node->getStartLine()) {
5664: $methodModifiers = [];
5665: $methodNames = [];
5666: foreach ($adaptations as $adaptation) {
5667: if (!$adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
5668: continue;
5669: }
5670:
5671: $methodName = $adaptation->method->toLowerString();
5672: if ($adaptation->newModifier !== null) {
5673: $methodModifiers[$methodName] = $adaptation->newModifier;
5674: }
5675:
5676: if ($adaptation->newName === null) {
5677: continue;
5678: }
5679:
5680: $methodNames[$methodName] = $adaptation->newName;
5681: }
5682:
5683: $stmts = $node->stmts;
5684: foreach ($stmts as $i => $stmt) {
5685: if (!$stmt instanceof Node\Stmt\ClassMethod) {
5686: continue;
5687: }
5688: $methodName = $stmt->name->toLowerString();
5689: $methodAst = clone $stmt;
5690: $stmts[$i] = $methodAst;
5691: if (array_key_exists($methodName, $methodModifiers)) {
5692: $methodAst->flags = ($methodAst->flags & ~ Node\Stmt\Class_::VISIBILITY_MODIFIER_MASK) | $methodModifiers[$methodName];
5693: }
5694:
5695: if (!array_key_exists($methodName, $methodNames)) {
5696: continue;
5697: }
5698:
5699: $methodAst->setAttribute('originalTraitMethodName', $methodAst->name->toLowerString());
5700: $methodAst->name = $methodNames[$methodName];
5701: }
5702:
5703: $traitScope = $scope->enterTrait($traitReflection);
5704: $nodeCallback(new InTraitNode($node, $traitReflection), $traitScope);
5705: $this->processStmtNodes($node, $stmts, $traitScope, $nodeCallback, StatementContext::createTopLevel());
5706: return;
5707: }
5708: if ($node instanceof Node\Stmt\ClassLike) {
5709: return;
5710: }
5711: if ($node instanceof Node\FunctionLike) {
5712: return;
5713: }
5714: foreach ($node->getSubNodeNames() as $subNodeName) {
5715: $subNode = $node->{$subNodeName};
5716: $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $adaptations, $nodeCallback);
5717: }
5718: } elseif (is_array($node)) {
5719: foreach ($node as $subNode) {
5720: $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $adaptations, $nodeCallback);
5721: }
5722: }
5723: }
5724:
5725: private function processCalledMethod(MethodReflection $methodReflection): ?MutatingScope
5726: {
5727: $declaringClass = $methodReflection->getDeclaringClass();
5728: if ($declaringClass->isAnonymous()) {
5729: return null;
5730: }
5731: if ($declaringClass->getFileName() === null) {
5732: return null;
5733: }
5734:
5735: $stackName = sprintf('%s::%s', $declaringClass->getName(), $methodReflection->getName());
5736: if (array_key_exists($stackName, $this->calledMethodResults)) {
5737: return $this->calledMethodResults[$stackName];
5738: }
5739:
5740: if (array_key_exists($stackName, $this->calledMethodStack)) {
5741: return null;
5742: }
5743:
5744: if (count($this->calledMethodStack) > 0) {
5745: return null;
5746: }
5747:
5748: $this->calledMethodStack[$stackName] = true;
5749:
5750: $fileName = $this->fileHelper->normalizePath($declaringClass->getFileName());
5751: if (!isset($this->analysedFiles[$fileName])) {
5752: return null;
5753: }
5754: $parserNodes = $this->parser->parseFile($fileName);
5755:
5756: $returnStatement = null;
5757: $this->processNodesForCalledMethod($parserNodes, $fileName, $methodReflection, static function (Node $node, Scope $scope) use ($methodReflection, &$returnStatement): void {
5758: if (!$node instanceof MethodReturnStatementsNode) {
5759: return;
5760: }
5761:
5762: if ($node->getClassReflection()->getName() !== $methodReflection->getDeclaringClass()->getName()) {
5763: return;
5764: }
5765:
5766: if ($returnStatement !== null) {
5767: return;
5768: }
5769:
5770: $returnStatement = $node;
5771: });
5772:
5773: $calledMethodEndScope = null;
5774: if ($returnStatement !== null) {
5775: foreach ($returnStatement->getExecutionEnds() as $executionEnd) {
5776: $statementResult = $executionEnd->getStatementResult();
5777: $endNode = $executionEnd->getNode();
5778: if ($endNode instanceof Node\Stmt\Throw_) {
5779: continue;
5780: }
5781: if ($endNode instanceof Node\Stmt\Expression) {
5782: $exprType = $statementResult->getScope()->getType($endNode->expr);
5783: if ($exprType instanceof NeverType && $exprType->isExplicit()) {
5784: continue;
5785: }
5786: }
5787: if ($calledMethodEndScope === null) {
5788: $calledMethodEndScope = $statementResult->getScope();
5789: continue;
5790: }
5791:
5792: $calledMethodEndScope = $calledMethodEndScope->mergeWith($statementResult->getScope());
5793: }
5794: foreach ($returnStatement->getReturnStatements() as $statement) {
5795: if ($calledMethodEndScope === null) {
5796: $calledMethodEndScope = $statement->getScope();
5797: continue;
5798: }
5799:
5800: $calledMethodEndScope = $calledMethodEndScope->mergeWith($statement->getScope());
5801: }
5802: }
5803:
5804: unset($this->calledMethodStack[$stackName]);
5805:
5806: $this->calledMethodResults[$stackName] = $calledMethodEndScope;
5807:
5808: return $calledMethodEndScope;
5809: }
5810:
5811: /**
5812: * @param Node[]|Node|scalar|null $node
5813: * @param callable(Node $node, Scope $scope): void $nodeCallback
5814: */
5815: private function processNodesForCalledMethod($node, string $fileName, MethodReflection $methodReflection, callable $nodeCallback): void
5816: {
5817: if ($node instanceof Node) {
5818: $declaringClass = $methodReflection->getDeclaringClass();
5819: if (
5820: $node instanceof Node\Stmt\Class_
5821: && $node->namespacedName !== null
5822: && $declaringClass->getName() === (string) $node->namespacedName
5823: && $declaringClass->getNativeReflection()->getStartLine() === $node->getStartLine()
5824: ) {
5825:
5826: $stmts = $node->stmts;
5827: foreach ($stmts as $stmt) {
5828: if (!$stmt instanceof Node\Stmt\ClassMethod) {
5829: continue;
5830: }
5831:
5832: if ($stmt->name->toString() !== $methodReflection->getName()) {
5833: continue;
5834: }
5835:
5836: if ($stmt->getEndLine() - $stmt->getStartLine() > 50) {
5837: continue;
5838: }
5839:
5840: $scope = $this->scopeFactory->create(ScopeContext::create($fileName))->enterClass($declaringClass);
5841: $this->processStmtNode($stmt, $scope, $nodeCallback, StatementContext::createTopLevel());
5842: }
5843: return;
5844: }
5845: if ($node instanceof Node\Stmt\ClassLike) {
5846: return;
5847: }
5848: if ($node instanceof Node\FunctionLike) {
5849: return;
5850: }
5851: foreach ($node->getSubNodeNames() as $subNodeName) {
5852: $subNode = $node->{$subNodeName};
5853: $this->processNodesForCalledMethod($subNode, $fileName, $methodReflection, $nodeCallback);
5854: }
5855: } elseif (is_array($node)) {
5856: foreach ($node as $subNode) {
5857: $this->processNodesForCalledMethod($subNode, $fileName, $methodReflection, $nodeCallback);
5858: }
5859: }
5860: }
5861:
5862: /**
5863: * @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}
5864: */
5865: public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array
5866: {
5867: $templateTypeMap = TemplateTypeMap::createEmpty();
5868: $phpDocParameterTypes = [];
5869: $phpDocImmediatelyInvokedCallableParameters = [];
5870: $phpDocClosureThisTypeParameters = [];
5871: $phpDocReturnType = null;
5872: $phpDocThrowType = null;
5873: $deprecatedDescription = null;
5874: $isDeprecated = false;
5875: $isInternal = false;
5876: $isFinal = false;
5877: $isPure = null;
5878: $isAllowedPrivateMutation = false;
5879: $acceptsNamedArguments = true;
5880: $isReadOnly = $scope->isInClass() && $scope->getClassReflection()->isImmutable();
5881: $asserts = Assertions::createEmpty();
5882: $selfOutType = null;
5883: $docComment = $node->getDocComment() !== null
5884: ? $node->getDocComment()->getText()
5885: : null;
5886:
5887: $file = $scope->getFile();
5888: $class = $scope->isInClass() ? $scope->getClassReflection()->getName() : null;
5889: $trait = $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null;
5890: $resolvedPhpDoc = null;
5891: $functionName = null;
5892: $phpDocParameterOutTypes = [];
5893:
5894: if ($node instanceof Node\Stmt\ClassMethod) {
5895: if (!$scope->isInClass()) {
5896: throw new ShouldNotHappenException();
5897: }
5898: $functionName = $node->name->name;
5899: $positionalParameterNames = array_map(static function (Node\Param $param): string {
5900: if (!$param->var instanceof Variable || !is_string($param->var->name)) {
5901: throw new ShouldNotHappenException();
5902: }
5903:
5904: return $param->var->name;
5905: }, $node->getParams());
5906: $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod(
5907: $docComment,
5908: $file,
5909: $scope->getClassReflection(),
5910: $trait,
5911: $node->name->name,
5912: $positionalParameterNames,
5913: );
5914:
5915: if ($node->name->toLowerString() === '__construct') {
5916: foreach ($node->params as $param) {
5917: if ($param->flags === 0) {
5918: continue;
5919: }
5920:
5921: if ($param->getDocComment() === null) {
5922: continue;
5923: }
5924:
5925: if (
5926: !$param->var instanceof Variable
5927: || !is_string($param->var->name)
5928: ) {
5929: throw new ShouldNotHappenException();
5930: }
5931:
5932: $paramPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
5933: $file,
5934: $class,
5935: $trait,
5936: '__construct',
5937: $param->getDocComment()->getText(),
5938: );
5939: $varTags = $paramPhpDoc->getVarTags();
5940: if (isset($varTags[0]) && count($varTags) === 1) {
5941: $phpDocType = $varTags[0]->getType();
5942: } elseif (isset($varTags[$param->var->name])) {
5943: $phpDocType = $varTags[$param->var->name]->getType();
5944: } else {
5945: continue;
5946: }
5947:
5948: $phpDocParameterTypes[$param->var->name] = $phpDocType;
5949: }
5950: }
5951: } elseif ($node instanceof Node\Stmt\Function_) {
5952: $functionName = trim($scope->getNamespace() . '\\' . $node->name->name, '\\');
5953: }
5954:
5955: if ($docComment !== null && $resolvedPhpDoc === null) {
5956: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
5957: $file,
5958: $class,
5959: $trait,
5960: $functionName,
5961: $docComment,
5962: );
5963: }
5964:
5965: $varTags = [];
5966: if ($resolvedPhpDoc !== null) {
5967: $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap();
5968: $phpDocImmediatelyInvokedCallableParameters = $resolvedPhpDoc->getParamsImmediatelyInvokedCallable();
5969: foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) {
5970: if (array_key_exists($paramName, $phpDocParameterTypes)) {
5971: continue;
5972: }
5973: $paramType = $paramTag->getType();
5974: if ($scope->isInClass()) {
5975: $paramType = $this->transformStaticType($scope->getClassReflection(), $paramType);
5976: }
5977: $phpDocParameterTypes[$paramName] = $paramType;
5978: }
5979: foreach ($resolvedPhpDoc->getParamClosureThisTags() as $paramName => $paramClosureThisTag) {
5980: if (array_key_exists($paramName, $phpDocClosureThisTypeParameters)) {
5981: continue;
5982: }
5983: $paramClosureThisType = $paramClosureThisTag->getType();
5984: if ($scope->isInClass()) {
5985: $paramClosureThisType = $this->transformStaticType($scope->getClassReflection(), $paramClosureThisType);
5986: }
5987: $phpDocClosureThisTypeParameters[$paramName] = $paramClosureThisType;
5988: }
5989:
5990: foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) {
5991: $phpDocParameterOutTypes[$paramName] = $paramOutTag->getType();
5992: }
5993: if ($node instanceof Node\FunctionLike) {
5994: $nativeReturnType = $scope->getFunctionType($node->getReturnType(), false, false);
5995: $phpDocReturnType = $this->getPhpDocReturnType($resolvedPhpDoc, $nativeReturnType);
5996: if ($phpDocReturnType !== null && $scope->isInClass()) {
5997: $phpDocReturnType = $this->transformStaticType($scope->getClassReflection(), $phpDocReturnType);
5998: }
5999: }
6000: $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null;
6001: $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null;
6002: $isDeprecated = $resolvedPhpDoc->isDeprecated();
6003: $isInternal = $resolvedPhpDoc->isInternal();
6004: $isFinal = $resolvedPhpDoc->isFinal();
6005: $isPure = $resolvedPhpDoc->isPure();
6006: $isAllowedPrivateMutation = $resolvedPhpDoc->isAllowedPrivateMutation();
6007: $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments();
6008: if ($acceptsNamedArguments && $scope->isInClass()) {
6009: $acceptsNamedArguments = $scope->getClassReflection()->acceptsNamedArguments();
6010: }
6011: $isReadOnly = $isReadOnly || $resolvedPhpDoc->isReadOnly();
6012: $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc);
6013: $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null;
6014: $varTags = $resolvedPhpDoc->getVarTags();
6015: }
6016:
6017: return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation];
6018: }
6019:
6020: private function transformStaticType(ClassReflection $declaringClass, Type $type): Type
6021: {
6022: return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($declaringClass): Type {
6023: if ($type instanceof StaticType) {
6024: $changedType = $type->changeBaseClass($declaringClass);
6025: if ($declaringClass->isFinal()) {
6026: $changedType = $changedType->getStaticObjectType();
6027: }
6028: return $traverse($changedType);
6029: }
6030:
6031: return $traverse($type);
6032: });
6033: }
6034:
6035: private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $nativeReturnType): ?Type
6036: {
6037: $returnTag = $resolvedPhpDoc->getReturnTag();
6038:
6039: if ($returnTag === null) {
6040: return null;
6041: }
6042:
6043: $phpDocReturnType = $returnTag->getType();
6044:
6045: if ($returnTag->isExplicit()) {
6046: return $phpDocReturnType;
6047: }
6048:
6049: if ($nativeReturnType->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocReturnType))->yes()) {
6050: return $phpDocReturnType;
6051: }
6052:
6053: return null;
6054: }
6055:
6056: /**
6057: * @template T of Node
6058: * @param array<T> $nodes
6059: * @return T|null
6060: */
6061: private function getFirstUnreachableNode(array $nodes, bool $earlyBinding): ?Node
6062: {
6063: foreach ($nodes as $node) {
6064: if ($node instanceof Node\Stmt\Nop) {
6065: continue;
6066: }
6067: if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike)) {
6068: continue;
6069: }
6070: return $node;
6071: }
6072: return null;
6073: }
6074:
6075: }
6076: