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