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