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