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