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