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