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