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\Node;
10: use PhpParser\Node\Arg;
11: use PhpParser\Node\AttributeGroup;
12: use PhpParser\Node\Expr;
13: use PhpParser\Node\Expr\Array_;
14: use PhpParser\Node\Expr\ArrayDimFetch;
15: use PhpParser\Node\Expr\ArrayItem;
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\Cast;
24: use PhpParser\Node\Expr\ConstFetch;
25: use PhpParser\Node\Expr\ErrorSuppress;
26: use PhpParser\Node\Expr\Exit_;
27: use PhpParser\Node\Expr\FuncCall;
28: use PhpParser\Node\Expr\Instanceof_;
29: use PhpParser\Node\Expr\List_;
30: use PhpParser\Node\Expr\MethodCall;
31: use PhpParser\Node\Expr\New_;
32: use PhpParser\Node\Expr\PropertyFetch;
33: use PhpParser\Node\Expr\StaticCall;
34: use PhpParser\Node\Expr\StaticPropertyFetch;
35: use PhpParser\Node\Expr\Ternary;
36: use PhpParser\Node\Expr\Variable;
37: use PhpParser\Node\Name;
38: use PhpParser\Node\Stmt\Break_;
39: use PhpParser\Node\Stmt\Class_;
40: use PhpParser\Node\Stmt\Continue_;
41: use PhpParser\Node\Stmt\Do_;
42: use PhpParser\Node\Stmt\Echo_;
43: use PhpParser\Node\Stmt\For_;
44: use PhpParser\Node\Stmt\Foreach_;
45: use PhpParser\Node\Stmt\If_;
46: use PhpParser\Node\Stmt\Return_;
47: use PhpParser\Node\Stmt\Static_;
48: use PhpParser\Node\Stmt\StaticVar;
49: use PhpParser\Node\Stmt\Switch_;
50: use PhpParser\Node\Stmt\Throw_;
51: use PhpParser\Node\Stmt\TryCatch;
52: use PhpParser\Node\Stmt\Unset_;
53: use PhpParser\Node\Stmt\While_;
54: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass;
55: use PHPStan\BetterReflection\Reflection\ReflectionEnum;
56: use PHPStan\BetterReflection\Reflector\Reflector;
57: use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection;
58: use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
59: use PHPStan\DependencyInjection\Reflection\ClassReflectionExtensionRegistryProvider;
60: use PHPStan\DependencyInjection\Type\DynamicThrowTypeExtensionProvider;
61: use PHPStan\File\FileHelper;
62: use PHPStan\File\FileReader;
63: use PHPStan\Node\BooleanAndNode;
64: use PHPStan\Node\BooleanOrNode;
65: use PHPStan\Node\BreaklessWhileLoopNode;
66: use PHPStan\Node\CatchWithUnthrownExceptionNode;
67: use PHPStan\Node\ClassConstantsNode;
68: use PHPStan\Node\ClassMethodsNode;
69: use PHPStan\Node\ClassPropertiesNode;
70: use PHPStan\Node\ClassPropertyNode;
71: use PHPStan\Node\ClassStatementsGatherer;
72: use PHPStan\Node\ClosureReturnStatementsNode;
73: use PHPStan\Node\DoWhileLoopConditionNode;
74: use PHPStan\Node\ExecutionEndNode;
75: use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
76: use PHPStan\Node\Expr\GetIterableValueTypeExpr;
77: use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
78: use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
79: use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
80: use PHPStan\Node\FinallyExitPointsNode;
81: use PHPStan\Node\FunctionCallableNode;
82: use PHPStan\Node\FunctionReturnStatementsNode;
83: use PHPStan\Node\InArrowFunctionNode;
84: use PHPStan\Node\InClassMethodNode;
85: use PHPStan\Node\InClassNode;
86: use PHPStan\Node\InClosureNode;
87: use PHPStan\Node\InForeachNode;
88: use PHPStan\Node\InFunctionNode;
89: use PHPStan\Node\InstantiationCallableNode;
90: use PHPStan\Node\LiteralArrayItem;
91: use PHPStan\Node\LiteralArrayNode;
92: use PHPStan\Node\MatchExpressionArm;
93: use PHPStan\Node\MatchExpressionArmBody;
94: use PHPStan\Node\MatchExpressionArmCondition;
95: use PHPStan\Node\MatchExpressionNode;
96: use PHPStan\Node\MethodCallableNode;
97: use PHPStan\Node\MethodReturnStatementsNode;
98: use PHPStan\Node\PropertyAssignNode;
99: use PHPStan\Node\ReturnStatement;
100: use PHPStan\Node\StaticMethodCallableNode;
101: use PHPStan\Node\UnreachableStatementNode;
102: use PHPStan\Node\VarTagChangedExpressionTypeNode;
103: use PHPStan\Parser\ArrowFunctionArgVisitor;
104: use PHPStan\Parser\ClosureArgVisitor;
105: use PHPStan\Parser\Parser;
106: use PHPStan\Php\PhpVersion;
107: use PHPStan\PhpDoc\PhpDocInheritanceResolver;
108: use PHPStan\PhpDoc\ResolvedPhpDocBlock;
109: use PHPStan\PhpDoc\StubPhpDocProvider;
110: use PHPStan\PhpDoc\Tag\VarTag;
111: use PHPStan\Reflection\Assertions;
112: use PHPStan\Reflection\ClassReflection;
113: use PHPStan\Reflection\FunctionReflection;
114: use PHPStan\Reflection\InitializerExprTypeResolver;
115: use PHPStan\Reflection\MethodReflection;
116: use PHPStan\Reflection\Native\NativeMethodReflection;
117: use PHPStan\Reflection\Native\NativeParameterReflection;
118: use PHPStan\Reflection\ParameterReflectionWithPhpDocs;
119: use PHPStan\Reflection\ParametersAcceptor;
120: use PHPStan\Reflection\ParametersAcceptorSelector;
121: use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection;
122: use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
123: use PHPStan\Reflection\Php\PhpMethodReflection;
124: use PHPStan\Reflection\ReflectionProvider;
125: use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
126: use PHPStan\ShouldNotHappenException;
127: use PHPStan\TrinaryLogic;
128: use PHPStan\Type\Accessory\NonEmptyArrayType;
129: use PHPStan\Type\ArrayType;
130: use PHPStan\Type\ClosureType;
131: use PHPStan\Type\Constant\ConstantArrayType;
132: use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
133: use PHPStan\Type\Constant\ConstantBooleanType;
134: use PHPStan\Type\Constant\ConstantIntegerType;
135: use PHPStan\Type\ErrorType;
136: use PHPStan\Type\FileTypeMapper;
137: use PHPStan\Type\GeneralizePrecision;
138: use PHPStan\Type\Generic\TemplateTypeHelper;
139: use PHPStan\Type\Generic\TemplateTypeMap;
140: use PHPStan\Type\IntegerType;
141: use PHPStan\Type\MixedType;
142: use PHPStan\Type\NeverType;
143: use PHPStan\Type\NullType;
144: use PHPStan\Type\ObjectType;
145: use PHPStan\Type\StaticType;
146: use PHPStan\Type\StaticTypeFactory;
147: use PHPStan\Type\StringType;
148: use PHPStan\Type\Type;
149: use PHPStan\Type\TypeCombinator;
150: use PHPStan\Type\TypeTraverser;
151: use PHPStan\Type\UnionType;
152: use Throwable;
153: use Traversable;
154: use TypeError;
155: use UnhandledMatchError;
156: use function array_fill_keys;
157: use function array_filter;
158: use function array_key_exists;
159: use function array_map;
160: use function array_merge;
161: use function array_pop;
162: use function array_reverse;
163: use function array_slice;
164: use function base64_decode;
165: use function count;
166: use function in_array;
167: use function is_array;
168: use function is_int;
169: use function is_string;
170: use function sprintf;
171: use function str_starts_with;
172: use function strtolower;
173: use function trim;
174: use const PHP_VERSION_ID;
175:
176: class NodeScopeResolver
177: {
178:
179: private const LOOP_SCOPE_ITERATIONS = 3;
180: private const GENERALIZE_AFTER_ITERATION = 1;
181:
182: /** @var bool[] filePath(string) => bool(true) */
183: private array $analysedFiles = [];
184:
185: /** @var array<string, true> */
186: private array $earlyTerminatingMethodNames;
187:
188: /**
189: * @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[])
190: * @param array<int, string> $earlyTerminatingFunctionCalls
191: */
192: public function __construct(
193: private readonly ReflectionProvider $reflectionProvider,
194: private readonly InitializerExprTypeResolver $initializerExprTypeResolver,
195: private readonly Reflector $reflector,
196: private readonly ClassReflectionExtensionRegistryProvider $classReflectionExtensionRegistryProvider,
197: private readonly Parser $parser,
198: private readonly FileTypeMapper $fileTypeMapper,
199: private readonly StubPhpDocProvider $stubPhpDocProvider,
200: private readonly PhpVersion $phpVersion,
201: private readonly PhpDocInheritanceResolver $phpDocInheritanceResolver,
202: private readonly FileHelper $fileHelper,
203: private readonly TypeSpecifier $typeSpecifier,
204: private readonly DynamicThrowTypeExtensionProvider $dynamicThrowTypeExtensionProvider,
205: private readonly ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider,
206: private readonly bool $polluteScopeWithLoopInitialAssignments,
207: private readonly bool $polluteScopeWithAlwaysIterableForeach,
208: private readonly array $earlyTerminatingMethodCalls,
209: private readonly array $earlyTerminatingFunctionCalls,
210: private readonly bool $implicitThrows,
211: private readonly bool $treatPhpDocTypesAsCertain,
212: )
213: {
214: $earlyTerminatingMethodNames = [];
215: foreach ($this->earlyTerminatingMethodCalls as $methodNames) {
216: foreach ($methodNames as $methodName) {
217: $earlyTerminatingMethodNames[strtolower($methodName)] = true;
218: }
219: }
220: $this->earlyTerminatingMethodNames = $earlyTerminatingMethodNames;
221: }
222:
223: /**
224: * @api
225: * @param string[] $files
226: */
227: public function setAnalysedFiles(array $files): void
228: {
229: $this->analysedFiles = array_fill_keys($files, true);
230: }
231:
232: /**
233: * @api
234: * @param Node[] $nodes
235: * @param callable(Node $node, Scope $scope): void $nodeCallback
236: */
237: public function processNodes(
238: array $nodes,
239: MutatingScope $scope,
240: callable $nodeCallback,
241: ): void
242: {
243: foreach ($nodes as $i => $node) {
244: if (!$node instanceof Node\Stmt) {
245: continue;
246: }
247:
248: $statementResult = $this->processStmtNode($node, $scope, $nodeCallback, StatementContext::createTopLevel());
249: $scope = $statementResult->getScope();
250: if (!$statementResult->isAlwaysTerminating()) {
251: continue;
252: }
253:
254: $nextStmt = $this->getFirstNonNopNode(array_slice($nodes, $i + 1));
255: if (!$nextStmt instanceof Node\Stmt) {
256: continue;
257: }
258:
259: $nodeCallback(new UnreachableStatementNode($nextStmt), $scope);
260: break;
261: }
262: }
263:
264: /**
265: * @api
266: * @param Node\Stmt[] $stmts
267: * @param callable(Node $node, Scope $scope): void $nodeCallback
268: */
269: public function processStmtNodes(
270: Node $parentNode,
271: array $stmts,
272: MutatingScope $scope,
273: callable $nodeCallback,
274: ?StatementContext $context = null,
275: ): StatementResult
276: {
277: if ($context === null) {
278: $context = StatementContext::createTopLevel();
279: }
280: $exitPoints = [];
281: $throwPoints = [];
282: $alreadyTerminated = false;
283: $hasYield = false;
284: $stmtCount = count($stmts);
285: $shouldCheckLastStatement = $parentNode instanceof Node\Stmt\Function_
286: || $parentNode instanceof Node\Stmt\ClassMethod
287: || $parentNode instanceof Expr\Closure;
288: foreach ($stmts as $i => $stmt) {
289: $isLast = $i === $stmtCount - 1;
290: $statementResult = $this->processStmtNode(
291: $stmt,
292: $scope,
293: $nodeCallback,
294: $context,
295: );
296: $scope = $statementResult->getScope();
297: $hasYield = $hasYield || $statementResult->hasYield();
298:
299: if ($shouldCheckLastStatement && $isLast) {
300: /** @var Node\Stmt\Function_|Node\Stmt\ClassMethod|Expr\Closure $parentNode */
301: $parentNode = $parentNode;
302: $nodeCallback(new ExecutionEndNode(
303: $stmt,
304: new StatementResult(
305: $scope,
306: $hasYield,
307: $statementResult->isAlwaysTerminating(),
308: $statementResult->getExitPoints(),
309: $statementResult->getThrowPoints(),
310: ),
311: $parentNode->returnType !== null,
312: ), $scope);
313: }
314:
315: $exitPoints = array_merge($exitPoints, $statementResult->getExitPoints());
316: $throwPoints = array_merge($throwPoints, $statementResult->getThrowPoints());
317:
318: if (!$statementResult->isAlwaysTerminating()) {
319: continue;
320: }
321:
322: $alreadyTerminated = true;
323: $nextStmt = $this->getFirstNonNopNode(array_slice($stmts, $i + 1));
324: if ($nextStmt !== null) {
325: $nodeCallback(new UnreachableStatementNode($nextStmt), $scope);
326: }
327: break;
328: }
329:
330: $statementResult = new StatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints);
331: if ($stmtCount === 0 && $shouldCheckLastStatement) {
332: /** @var Node\Stmt\Function_|Node\Stmt\ClassMethod|Expr\Closure $parentNode */
333: $parentNode = $parentNode;
334: $nodeCallback(new ExecutionEndNode(
335: $parentNode,
336: $statementResult,
337: $parentNode->returnType !== null,
338: ), $scope);
339: }
340:
341: return $statementResult;
342: }
343:
344: /**
345: * @param callable(Node $node, Scope $scope): void $nodeCallback
346: */
347: private function processStmtNode(
348: Node\Stmt $stmt,
349: MutatingScope $scope,
350: callable $nodeCallback,
351: StatementContext $context,
352: ): StatementResult
353: {
354: if (
355: $stmt instanceof Throw_
356: || $stmt instanceof Return_
357: ) {
358: $scope = $this->processStmtVarAnnotation($scope, $stmt, $stmt->expr, $nodeCallback);
359: } elseif (
360: !$stmt instanceof Static_
361: && !$stmt instanceof Foreach_
362: && !$stmt instanceof Node\Stmt\Global_
363: && !$stmt instanceof Node\Stmt\Property
364: && !$stmt instanceof Node\Stmt\PropertyProperty
365: && !$stmt instanceof Node\Stmt\ClassConst
366: && !$stmt instanceof Node\Stmt\Const_
367: ) {
368: $scope = $this->processStmtVarAnnotation($scope, $stmt, null, $nodeCallback);
369: }
370:
371: if ($stmt instanceof Node\Stmt\ClassMethod) {
372: if (!$scope->isInClass()) {
373: throw new ShouldNotHappenException();
374: }
375: if (
376: $scope->isInTrait()
377: && $scope->getClassReflection()->hasNativeMethod($stmt->name->toString())
378: ) {
379: $methodReflection = $scope->getClassReflection()->getNativeMethod($stmt->name->toString());
380: if ($methodReflection instanceof NativeMethodReflection) {
381: return new StatementResult($scope, false, false, [], []);
382: }
383: if ($methodReflection instanceof PhpMethodReflection) {
384: $declaringTrait = $methodReflection->getDeclaringTrait();
385: if ($declaringTrait === null || $declaringTrait->getName() !== $scope->getTraitReflection()->getName()) {
386: return new StatementResult($scope, false, false, [], []);
387: }
388: }
389: }
390: }
391:
392: $nodeCallback($stmt, $scope);
393:
394: $overridingThrowPoints = $this->getOverridingThrowPoints($stmt, $scope);
395:
396: if ($stmt instanceof Node\Stmt\Declare_) {
397: $hasYield = false;
398: $throwPoints = [];
399: foreach ($stmt->declares as $declare) {
400: $nodeCallback($declare, $scope);
401: $nodeCallback($declare->value, $scope);
402: if (
403: $declare->key->name !== 'strict_types'
404: || !($declare->value instanceof Node\Scalar\LNumber)
405: || $declare->value->value !== 1
406: ) {
407: continue;
408: }
409:
410: $scope = $scope->enterDeclareStrictTypes();
411: }
412: } elseif ($stmt instanceof Node\Stmt\Function_) {
413: $hasYield = false;
414: $throwPoints = [];
415: $this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback);
416: [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt);
417:
418: foreach ($stmt->params as $param) {
419: $this->processParamNode($param, $scope, $nodeCallback);
420: }
421:
422: if ($stmt->returnType !== null) {
423: $nodeCallback($stmt->returnType, $scope);
424: }
425:
426: $functionScope = $scope->enterFunction(
427: $stmt,
428: $templateTypeMap,
429: $phpDocParameterTypes,
430: $phpDocReturnType,
431: $phpDocThrowType,
432: $deprecatedDescription,
433: $isDeprecated,
434: $isInternal,
435: $isFinal,
436: $isPure,
437: $acceptsNamedArguments,
438: $asserts,
439: $phpDocComment,
440: $phpDocParameterOutTypes,
441: );
442: $functionReflection = $functionScope->getFunction();
443: if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) {
444: throw new ShouldNotHappenException();
445: }
446:
447: $nodeCallback(new InFunctionNode($functionReflection, $stmt), $functionScope);
448:
449: $gatheredReturnStatements = [];
450: $gatheredYieldStatements = [];
451: $executionEnds = [];
452: $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $functionScope, static function (Node $node, Scope $scope) use ($nodeCallback, $functionScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds): void {
453: $nodeCallback($node, $scope);
454: if ($scope->getFunction() !== $functionScope->getFunction()) {
455: return;
456: }
457: if ($scope->isInAnonymousFunction()) {
458: return;
459: }
460: if ($node instanceof ExecutionEndNode) {
461: $executionEnds[] = $node;
462: return;
463: }
464: if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) {
465: $gatheredYieldStatements[] = $node;
466: }
467: if (!$node instanceof Return_) {
468: return;
469: }
470:
471: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
472: }, StatementContext::createTopLevel());
473:
474: $nodeCallback(new FunctionReturnStatementsNode(
475: $stmt,
476: $gatheredReturnStatements,
477: $gatheredYieldStatements,
478: $statementResult,
479: $executionEnds,
480: ), $functionScope);
481: } elseif ($stmt instanceof Node\Stmt\ClassMethod) {
482: $hasYield = false;
483: $throwPoints = [];
484: $this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback);
485: [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt);
486:
487: foreach ($stmt->params as $param) {
488: $this->processParamNode($param, $scope, $nodeCallback);
489: }
490:
491: if ($stmt->returnType !== null) {
492: $nodeCallback($stmt->returnType, $scope);
493: }
494:
495: $methodScope = $scope->enterClassMethod(
496: $stmt,
497: $templateTypeMap,
498: $phpDocParameterTypes,
499: $phpDocReturnType,
500: $phpDocThrowType,
501: $deprecatedDescription,
502: $isDeprecated,
503: $isInternal,
504: $isFinal,
505: $isPure,
506: $acceptsNamedArguments,
507: $asserts,
508: $selfOutType,
509: $phpDocComment,
510: $phpDocParameterOutTypes,
511: );
512:
513: if (!$scope->isInClass()) {
514: throw new ShouldNotHappenException();
515: }
516:
517: if ($stmt->name->toLowerString() === '__construct') {
518: foreach ($stmt->params as $param) {
519: if ($param->flags === 0) {
520: continue;
521: }
522:
523: if (!$param->var instanceof Variable || !is_string($param->var->name)) {
524: throw new ShouldNotHappenException();
525: }
526: $phpDoc = null;
527: if ($param->getDocComment() !== null) {
528: $phpDoc = $param->getDocComment()->getText();
529: }
530: $nodeCallback(new ClassPropertyNode(
531: $param->var->name,
532: $param->flags,
533: $param->type,
534: null,
535: $phpDoc,
536: $phpDocParameterTypes[$param->var->name] ?? null,
537: true,
538: $param,
539: false,
540: $scope->isInTrait(),
541: $scope->getClassReflection()->isReadOnly(),
542: false,
543: ), $methodScope);
544: }
545: }
546:
547: if ($stmt->getAttribute('virtual', false) === false) {
548: $methodReflection = $methodScope->getFunction();
549: if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) {
550: throw new ShouldNotHappenException();
551: }
552: $nodeCallback(new InClassMethodNode($scope->getClassReflection(), $methodReflection, $stmt), $methodScope);
553: }
554:
555: if ($stmt->stmts !== null) {
556: $gatheredReturnStatements = [];
557: $gatheredYieldStatements = [];
558: $executionEnds = [];
559: $statementResult = $this->processStmtNodes($stmt, $stmt->stmts, $methodScope, static function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds): void {
560: $nodeCallback($node, $scope);
561: if ($scope->getFunction() !== $methodScope->getFunction()) {
562: return;
563: }
564: if ($scope->isInAnonymousFunction()) {
565: return;
566: }
567: if ($node instanceof ExecutionEndNode) {
568: $executionEnds[] = $node;
569: return;
570: }
571: if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) {
572: $gatheredYieldStatements[] = $node;
573: }
574: if (!$node instanceof Return_) {
575: return;
576: }
577:
578: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
579: }, StatementContext::createTopLevel());
580: $nodeCallback(new MethodReturnStatementsNode(
581: $stmt,
582: $gatheredReturnStatements,
583: $gatheredYieldStatements,
584: $statementResult,
585: $executionEnds,
586: ), $methodScope);
587: }
588: } elseif ($stmt instanceof Echo_) {
589: $hasYield = false;
590: $throwPoints = [];
591: foreach ($stmt->exprs as $echoExpr) {
592: $result = $this->processExprNode($echoExpr, $scope, $nodeCallback, ExpressionContext::createDeep());
593: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
594: $scope = $result->getScope();
595: $hasYield = $hasYield || $result->hasYield();
596: }
597:
598: $throwPoints = $overridingThrowPoints ?? $throwPoints;
599: } elseif ($stmt instanceof Return_) {
600: if ($stmt->expr !== null) {
601: $result = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep());
602: $throwPoints = $result->getThrowPoints();
603: $scope = $result->getScope();
604: $hasYield = $result->hasYield();
605: } else {
606: $hasYield = false;
607: $throwPoints = [];
608: }
609:
610: return new StatementResult($scope, $hasYield, true, [
611: new StatementExitPoint($stmt, $scope),
612: ], $overridingThrowPoints ?? $throwPoints);
613: } elseif ($stmt instanceof Continue_ || $stmt instanceof Break_) {
614: if ($stmt->num !== null) {
615: $result = $this->processExprNode($stmt->num, $scope, $nodeCallback, ExpressionContext::createDeep());
616: $scope = $result->getScope();
617: $hasYield = $result->hasYield();
618: $throwPoints = $result->getThrowPoints();
619: } else {
620: $hasYield = false;
621: $throwPoints = [];
622: }
623:
624: return new StatementResult($scope, $hasYield, true, [
625: new StatementExitPoint($stmt, $scope),
626: ], $overridingThrowPoints ?? $throwPoints);
627: } elseif ($stmt instanceof Node\Stmt\Expression) {
628: $earlyTerminationExpr = $this->findEarlyTerminatingExpr($stmt->expr, $scope);
629: $result = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createTopLevel());
630: $scope = $result->getScope();
631: $scope = $scope->filterBySpecifiedTypes($this->typeSpecifier->specifyTypesInCondition(
632: $scope,
633: $stmt->expr,
634: TypeSpecifierContext::createNull(),
635: ));
636: $hasYield = $result->hasYield();
637: $throwPoints = $result->getThrowPoints();
638: if ($earlyTerminationExpr !== null) {
639: return new StatementResult($scope, $hasYield, true, [
640: new StatementExitPoint($stmt, $scope),
641: ], $overridingThrowPoints ?? $throwPoints);
642: }
643: return new StatementResult($scope, $hasYield, false, [], $overridingThrowPoints ?? $throwPoints);
644: } elseif ($stmt instanceof Node\Stmt\Namespace_) {
645: if ($stmt->name !== null) {
646: $scope = $scope->enterNamespace($stmt->name->toString());
647: }
648:
649: $scope = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context)->getScope();
650: $hasYield = false;
651: $throwPoints = [];
652: } elseif ($stmt instanceof Node\Stmt\Trait_) {
653: return new StatementResult($scope, false, false, [], []);
654: } elseif ($stmt instanceof Node\Stmt\ClassLike) {
655: $hasYield = false;
656: $throwPoints = [];
657: if (isset($stmt->namespacedName)) {
658: $classReflection = $this->getCurrentClassReflection($stmt, $stmt->namespacedName->toString(), $scope);
659: $classScope = $scope->enterClass($classReflection);
660: $nodeCallback(new InClassNode($stmt, $classReflection), $classScope);
661: } elseif ($stmt instanceof Class_) {
662: if ($stmt->name === null) {
663: throw new ShouldNotHappenException();
664: }
665: if ($stmt->getAttribute('anonymousClass', false) === false) {
666: $classReflection = $this->reflectionProvider->getClass($stmt->name->toString());
667: } else {
668: $classReflection = $this->reflectionProvider->getAnonymousClassReflection($stmt, $scope);
669: }
670: $classScope = $scope->enterClass($classReflection);
671: $nodeCallback(new InClassNode($stmt, $classReflection), $classScope);
672: } else {
673: throw new ShouldNotHappenException();
674: }
675:
676: $classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback);
677: $this->processAttributeGroups($stmt->attrGroups, $classScope, $classStatementsGatherer);
678:
679: $this->processStmtNodes($stmt, $stmt->stmts, $classScope, $classStatementsGatherer, $context);
680: $nodeCallback(new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensionProvider, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls()), $classScope);
681: $nodeCallback(new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls()), $classScope);
682: $nodeCallback(new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches()), $classScope);
683: $classReflection->evictPrivateSymbols();
684: } elseif ($stmt instanceof Node\Stmt\Property) {
685: $hasYield = false;
686: $throwPoints = [];
687: $this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback);
688:
689: foreach ($stmt->props as $prop) {
690: $this->processStmtNode($prop, $scope, $nodeCallback, $context);
691: [,,,,,,,,,,$isReadOnly, $docComment, ,,,$varTags, $isAllowedPrivateMutation] = $this->getPhpDocs($scope, $stmt);
692: if (!$scope->isInClass()) {
693: throw new ShouldNotHappenException();
694: }
695: $propertyName = $prop->name->toString();
696: $phpDocType = null;
697: if (isset($varTags[0]) && count($varTags) === 1) {
698: $phpDocType = $varTags[0]->getType();
699: } elseif (isset($varTags[$propertyName])) {
700: $phpDocType = $varTags[$propertyName]->getType();
701: }
702: $nodeCallback(
703: new ClassPropertyNode(
704: $propertyName,
705: $stmt->flags,
706: $stmt->type,
707: $prop->default,
708: $docComment,
709: $phpDocType,
710: false,
711: $prop,
712: $isReadOnly,
713: $scope->isInTrait(),
714: $scope->getClassReflection()->isReadOnly(),
715: $isAllowedPrivateMutation,
716: ),
717: $scope,
718: );
719: }
720:
721: if ($stmt->type !== null) {
722: $nodeCallback($stmt->type, $scope);
723: }
724: } elseif ($stmt instanceof Node\Stmt\PropertyProperty) {
725: $hasYield = false;
726: $throwPoints = [];
727: if ($stmt->default !== null) {
728: $this->processExprNode($stmt->default, $scope, $nodeCallback, ExpressionContext::createDeep());
729: }
730: } elseif ($stmt instanceof Throw_) {
731: $result = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep());
732: $throwPoints = $result->getThrowPoints();
733: $throwPoints[] = ThrowPoint::createExplicit($result->getScope(), $scope->getType($stmt->expr), $stmt, false);
734: return new StatementResult($result->getScope(), $result->hasYield(), true, [
735: new StatementExitPoint($stmt, $scope),
736: ], $throwPoints);
737: } elseif ($stmt instanceof If_) {
738: $conditionType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean();
739: $ifAlwaysTrue = $conditionType->isTrue()->yes();
740: $condResult = $this->processExprNode($stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep());
741: $exitPoints = [];
742: $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints();
743: $finalScope = null;
744: $alwaysTerminating = true;
745: $hasYield = $condResult->hasYield();
746:
747: $branchScopeStatementResult = $this->processStmtNodes($stmt, $stmt->stmts, $condResult->getTruthyScope(), $nodeCallback, $context);
748:
749: if (!$conditionType instanceof ConstantBooleanType || $conditionType->getValue()) {
750: $exitPoints = $branchScopeStatementResult->getExitPoints();
751: $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints());
752: $branchScope = $branchScopeStatementResult->getScope();
753: $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? null : $branchScope;
754: $alwaysTerminating = $branchScopeStatementResult->isAlwaysTerminating();
755: $hasYield = $branchScopeStatementResult->hasYield() || $hasYield;
756: }
757:
758: $scope = $condResult->getFalseyScope();
759: $lastElseIfConditionIsTrue = false;
760:
761: $condScope = $scope;
762: foreach ($stmt->elseifs as $elseif) {
763: $nodeCallback($elseif, $scope);
764: $elseIfConditionType = ($this->treatPhpDocTypesAsCertain ? $condScope->getType($elseif->cond) : $scope->getNativeType($elseif->cond))->toBoolean();
765: $condResult = $this->processExprNode($elseif->cond, $condScope, $nodeCallback, ExpressionContext::createDeep());
766: $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints());
767: $condScope = $condResult->getScope();
768: $branchScopeStatementResult = $this->processStmtNodes($elseif, $elseif->stmts, $condResult->getTruthyScope(), $nodeCallback, $context);
769:
770: if (
771: !$ifAlwaysTrue
772: && (
773: !$lastElseIfConditionIsTrue
774: && (
775: !$elseIfConditionType instanceof ConstantBooleanType
776: || $elseIfConditionType->getValue()
777: )
778: )
779: ) {
780: $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints());
781: $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints());
782: $branchScope = $branchScopeStatementResult->getScope();
783: $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope);
784: $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating();
785: $hasYield = $hasYield || $branchScopeStatementResult->hasYield();
786: }
787:
788: if (
789: $elseIfConditionType->isTrue()->yes()
790: ) {
791: $lastElseIfConditionIsTrue = true;
792: }
793:
794: $condScope = $condScope->filterByFalseyValue($elseif->cond);
795: $scope = $condScope;
796: }
797:
798: if ($stmt->else === null) {
799: if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) {
800: $finalScope = $scope->mergeWith($finalScope);
801: $alwaysTerminating = false;
802: }
803: } else {
804: $nodeCallback($stmt->else, $scope);
805: $branchScopeStatementResult = $this->processStmtNodes($stmt->else, $stmt->else->stmts, $scope, $nodeCallback, $context);
806:
807: if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) {
808: $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints());
809: $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints());
810: $branchScope = $branchScopeStatementResult->getScope();
811: $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope);
812: $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating();
813: $hasYield = $hasYield || $branchScopeStatementResult->hasYield();
814: }
815: }
816:
817: if ($finalScope === null) {
818: $finalScope = $scope;
819: }
820:
821: return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints);
822: } elseif ($stmt instanceof Node\Stmt\TraitUse) {
823: $hasYield = false;
824: $throwPoints = [];
825: $this->processTraitUse($stmt, $scope, $nodeCallback);
826: } elseif ($stmt instanceof Foreach_) {
827: $condResult = $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep());
828: $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints();
829: $scope = $condResult->getScope();
830: $arrayComparisonExpr = new BinaryOp\NotIdentical(
831: $stmt->expr,
832: new Array_([]),
833: );
834: $inForeachScope = $scope;
835: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
836: $inForeachScope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt);
837: }
838: $nodeCallback(new InForeachNode($stmt), $inForeachScope);
839: $bodyScope = $scope;
840:
841: if ($context->isTopLevel()) {
842: $bodyScope = $this->polluteScopeWithAlwaysIterableForeach ? $this->enterForeach($scope->filterByTruthyValue($arrayComparisonExpr), $stmt) : $this->enterForeach($scope, $stmt);
843: $count = 0;
844: do {
845: $prevScope = $bodyScope;
846: $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope);
847: $bodyScope = $this->enterForeach($bodyScope, $stmt);
848: $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void {
849: }, $context->enterDeep())->filterOutLoopExitPoints();
850: $bodyScope = $bodyScopeResult->getScope();
851: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
852: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
853: }
854: if ($bodyScope->equals($prevScope)) {
855: break;
856: }
857:
858: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
859: $bodyScope = $prevScope->generalizeWith($bodyScope);
860: }
861: $count++;
862: } while ($count < self::LOOP_SCOPE_ITERATIONS);
863: }
864:
865: $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope);
866: $bodyScope = $this->enterForeach($bodyScope, $stmt);
867: $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints();
868: $finalScope = $finalScopeResult->getScope();
869: foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
870: $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
871: }
872: foreach ($finalScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
873: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
874: }
875:
876: $isIterableAtLeastOnce = $scope->getType($stmt->expr)->isIterableAtLeastOnce();
877: if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) {
878: $finalScope = $scope;
879: } elseif ($isIterableAtLeastOnce->maybe()) {
880: if ($this->polluteScopeWithAlwaysIterableForeach) {
881: $finalScope = $finalScope->mergeWith($scope->filterByFalseyValue($arrayComparisonExpr));
882: } else {
883: $finalScope = $finalScope->mergeWith($scope);
884: }
885: } elseif (!$this->polluteScopeWithAlwaysIterableForeach) {
886: $finalScope = $scope->processAlwaysIterableForeachScopeWithoutPollute($finalScope);
887: // get types from finalScope, but don't create new variables
888: }
889:
890: if (!$isIterableAtLeastOnce->no()) {
891: $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints());
892: }
893: if (!(new ObjectType(Traversable::class))->isSuperTypeOf($scope->getType($stmt->expr))->no()) {
894: $throwPoints[] = ThrowPoint::createImplicit($scope, $stmt->expr);
895: }
896:
897: return new StatementResult(
898: $finalScope,
899: $finalScopeResult->hasYield() || $condResult->hasYield(),
900: $isIterableAtLeastOnce->yes() && $finalScopeResult->isAlwaysTerminating(),
901: $finalScopeResult->getExitPointsForOuterLoop(),
902: $throwPoints,
903: );
904: } elseif ($stmt instanceof While_) {
905: $condResult = $this->processExprNode($stmt->cond, $scope, static function (): void {
906: }, ExpressionContext::createDeep());
907: $bodyScope = $condResult->getTruthyScope();
908:
909: if ($context->isTopLevel()) {
910: $count = 0;
911: do {
912: $prevScope = $bodyScope;
913: $bodyScope = $bodyScope->mergeWith($scope);
914: $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, static function (): void {
915: }, ExpressionContext::createDeep())->getTruthyScope();
916: $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void {
917: }, $context->enterDeep())->filterOutLoopExitPoints();
918: $bodyScope = $bodyScopeResult->getScope();
919: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
920: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
921: }
922: if ($bodyScope->equals($prevScope)) {
923: break;
924: }
925:
926: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
927: $bodyScope = $prevScope->generalizeWith($bodyScope);
928: }
929: $count++;
930: } while ($count < self::LOOP_SCOPE_ITERATIONS);
931: }
932:
933: $bodyScope = $bodyScope->mergeWith($scope);
934: $bodyScopeMaybeRan = $bodyScope;
935: $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope();
936: $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints();
937: $finalScope = $finalScopeResult->getScope()->filterByFalseyValue($stmt->cond);
938: foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
939: $finalScope = $finalScope->mergeWith($continueExitPoint->getScope());
940: }
941: $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class);
942: foreach ($breakExitPoints as $breakExitPoint) {
943: $finalScope = $finalScope->mergeWith($breakExitPoint->getScope());
944: }
945:
946: $beforeCondBooleanType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean();
947: $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScopeMaybeRan->getType($stmt->cond) : $bodyScopeMaybeRan->getNativeType($stmt->cond))->toBoolean();
948: $isIterableAtLeastOnce = $beforeCondBooleanType->isTrue()->yes();
949: $alwaysIterates = $condBooleanType->isTrue()->yes() && $context->isTopLevel();
950: $neverIterates = $condBooleanType->isFalse()->yes() && $context->isTopLevel();
951: $nodeCallback(new BreaklessWhileLoopNode($stmt, $finalScopeResult->getExitPoints()), $bodyScopeMaybeRan);
952:
953: if ($alwaysIterates) {
954: $isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0;
955: } elseif ($isIterableAtLeastOnce) {
956: $isAlwaysTerminating = $finalScopeResult->isAlwaysTerminating();
957: } else {
958: $isAlwaysTerminating = false;
959: }
960: $condScope = $condResult->getFalseyScope();
961: if (!$isIterableAtLeastOnce) {
962: if (!$this->polluteScopeWithLoopInitialAssignments) {
963: $condScope = $condScope->mergeWith($scope);
964: }
965: $finalScope = $finalScope->mergeWith($condScope);
966: }
967:
968: $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints();
969: if (!$neverIterates) {
970: $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints());
971: }
972:
973: return new StatementResult(
974: $finalScope,
975: $finalScopeResult->hasYield() || $condResult->hasYield(),
976: $isAlwaysTerminating,
977: $finalScopeResult->getExitPointsForOuterLoop(),
978: $throwPoints,
979: );
980: } elseif ($stmt instanceof Do_) {
981: $finalScope = null;
982: $bodyScope = $scope;
983: $count = 0;
984: $hasYield = false;
985: $throwPoints = [];
986:
987: if ($context->isTopLevel()) {
988: do {
989: $prevScope = $bodyScope;
990: $bodyScope = $bodyScope->mergeWith($scope);
991: $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void {
992: }, $context->enterDeep())->filterOutLoopExitPoints();
993: $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating();
994: $bodyScope = $bodyScopeResult->getScope();
995: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
996: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
997: }
998: $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope);
999: foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
1000: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1001: }
1002: $bodyScope = $this->processExprNode($stmt->cond, $bodyScope, static function (): void {
1003: }, ExpressionContext::createDeep())->getTruthyScope();
1004: if ($bodyScope->equals($prevScope)) {
1005: break;
1006: }
1007:
1008: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
1009: $bodyScope = $prevScope->generalizeWith($bodyScope);
1010: }
1011: $count++;
1012: } while ($count < self::LOOP_SCOPE_ITERATIONS);
1013:
1014: $bodyScope = $bodyScope->mergeWith($scope);
1015: }
1016:
1017: $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints();
1018: $bodyScope = $bodyScopeResult->getScope();
1019: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1020: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1021: }
1022: $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScope->getType($stmt->cond) : $bodyScope->getNativeType($stmt->cond))->toBoolean();
1023: $alwaysIterates = $condBooleanType->isTrue()->yes() && $context->isTopLevel();
1024:
1025: $nodeCallback(new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->getExitPoints()), $bodyScope);
1026:
1027: if ($alwaysIterates) {
1028: $alwaysTerminating = count($bodyScopeResult->getExitPointsByType(Break_::class)) === 0;
1029: } else {
1030: $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating();
1031: }
1032: $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope);
1033: if ($finalScope === null) {
1034: $finalScope = $scope;
1035: }
1036: if (!$alwaysTerminating) {
1037: $condResult = $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep());
1038: $hasYield = $condResult->hasYield();
1039: $throwPoints = $condResult->getThrowPoints();
1040: $finalScope = $condResult->getFalseyScope();
1041: } else {
1042: $this->processExprNode($stmt->cond, $bodyScope, $nodeCallback, ExpressionContext::createDeep());
1043: }
1044: foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
1045: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1046: }
1047:
1048: return new StatementResult(
1049: $finalScope,
1050: $bodyScopeResult->hasYield() || $hasYield,
1051: $alwaysTerminating,
1052: $bodyScopeResult->getExitPointsForOuterLoop(),
1053: array_merge($throwPoints, $bodyScopeResult->getThrowPoints()),
1054: );
1055: } elseif ($stmt instanceof For_) {
1056: $initScope = $scope;
1057: $hasYield = false;
1058: $throwPoints = [];
1059: foreach ($stmt->init as $initExpr) {
1060: $initResult = $this->processExprNode($initExpr, $initScope, $nodeCallback, ExpressionContext::createTopLevel());
1061: $initScope = $initResult->getScope();
1062: $hasYield = $hasYield || $initResult->hasYield();
1063: $throwPoints = array_merge($throwPoints, $initResult->getThrowPoints());
1064: }
1065:
1066: $bodyScope = $initScope;
1067: $isIterableAtLeastOnce = TrinaryLogic::createYes();
1068: foreach ($stmt->cond as $condExpr) {
1069: $condResult = $this->processExprNode($condExpr, $bodyScope, static function (): void {
1070: }, ExpressionContext::createDeep());
1071: $initScope = $condResult->getScope();
1072: $condResultScope = $condResult->getScope();
1073: $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResultScope->getType($condExpr) : $condResultScope->getNativeType($condExpr))->toBoolean();
1074: if ($condTruthiness instanceof ConstantBooleanType) {
1075: $condTruthinessTrinary = TrinaryLogic::createFromBoolean($condTruthiness->getValue());
1076: } else {
1077: $condTruthinessTrinary = TrinaryLogic::createMaybe();
1078: }
1079: $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthinessTrinary);
1080: $hasYield = $hasYield || $condResult->hasYield();
1081: $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints());
1082: $bodyScope = $condResult->getTruthyScope();
1083: }
1084:
1085: if ($context->isTopLevel()) {
1086: $count = 0;
1087: do {
1088: $prevScope = $bodyScope;
1089: $bodyScope = $bodyScope->mergeWith($initScope);
1090: foreach ($stmt->cond as $condExpr) {
1091: $bodyScope = $this->processExprNode($condExpr, $bodyScope, static function (): void {
1092: }, ExpressionContext::createDeep())->getTruthyScope();
1093: }
1094: $bodyScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, static function (): void {
1095: }, $context->enterDeep())->filterOutLoopExitPoints();
1096: $bodyScope = $bodyScopeResult->getScope();
1097: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1098: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1099: }
1100: foreach ($stmt->loop as $loopExpr) {
1101: $exprResult = $this->processExprNode($loopExpr, $bodyScope, static function (): void {
1102: }, ExpressionContext::createTopLevel());
1103: $bodyScope = $exprResult->getScope();
1104: $hasYield = $hasYield || $exprResult->hasYield();
1105: $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
1106: }
1107:
1108: if ($bodyScope->equals($prevScope)) {
1109: break;
1110: }
1111:
1112: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
1113: $bodyScope = $prevScope->generalizeWith($bodyScope);
1114: }
1115: $count++;
1116: } while ($count < self::LOOP_SCOPE_ITERATIONS);
1117: }
1118:
1119: $bodyScope = $bodyScope->mergeWith($initScope);
1120: foreach ($stmt->cond as $condExpr) {
1121: $bodyScope = $this->processExprNode($condExpr, $bodyScope, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope();
1122: }
1123:
1124: $finalScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $bodyScope, $nodeCallback, $context)->filterOutLoopExitPoints();
1125: $finalScope = $finalScopeResult->getScope();
1126: foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1127: $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
1128: }
1129:
1130: $loopScope = $finalScope;
1131: foreach ($stmt->loop as $loopExpr) {
1132: $loopScope = $this->processExprNode($loopExpr, $loopScope, $nodeCallback, ExpressionContext::createTopLevel())->getScope();
1133: }
1134: $finalScope = $finalScope->generalizeWith($loopScope);
1135: foreach ($stmt->cond as $condExpr) {
1136: $finalScope = $finalScope->filterByFalseyValue($condExpr);
1137: }
1138:
1139: foreach ($finalScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
1140: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1141: }
1142:
1143: if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) {
1144: if ($this->polluteScopeWithLoopInitialAssignments) {
1145: $finalScope = $initScope;
1146: } else {
1147: $finalScope = $scope;
1148: }
1149:
1150: } elseif ($isIterableAtLeastOnce->maybe()) {
1151: if ($this->polluteScopeWithLoopInitialAssignments) {
1152: $finalScope = $finalScope->mergeWith($initScope);
1153: } else {
1154: $finalScope = $finalScope->mergeWith($scope);
1155: }
1156: } else {
1157: if (!$this->polluteScopeWithLoopInitialAssignments) {
1158: $finalScope = $finalScope->mergeWith($scope);
1159: }
1160: }
1161:
1162: return new StatementResult(
1163: $finalScope,
1164: $finalScopeResult->hasYield() || $hasYield,
1165: false/* $finalScopeResult->isAlwaysTerminating() && $isAlwaysIterable*/,
1166: $finalScopeResult->getExitPointsForOuterLoop(),
1167: array_merge($throwPoints, $finalScopeResult->getThrowPoints()),
1168: );
1169: } elseif ($stmt instanceof Switch_) {
1170: $condResult = $this->processExprNode($stmt->cond, $scope, $nodeCallback, ExpressionContext::createDeep());
1171: $scope = $condResult->getScope();
1172: $scopeForBranches = $scope;
1173: $finalScope = null;
1174: $prevScope = null;
1175: $hasDefaultCase = false;
1176: $alwaysTerminating = true;
1177: $hasYield = $condResult->hasYield();
1178: $exitPointsForOuterLoop = [];
1179: $throwPoints = $condResult->getThrowPoints();
1180: foreach ($stmt->cases as $caseNode) {
1181: if ($caseNode->cond !== null) {
1182: $condExpr = new BinaryOp\Equal($stmt->cond, $caseNode->cond);
1183: $caseResult = $this->processExprNode($caseNode->cond, $scopeForBranches, $nodeCallback, ExpressionContext::createDeep());
1184: $scopeForBranches = $caseResult->getScope();
1185: $hasYield = $hasYield || $caseResult->hasYield();
1186: $throwPoints = array_merge($throwPoints, $caseResult->getThrowPoints());
1187: $branchScope = $scopeForBranches->filterByTruthyValue($condExpr);
1188: } else {
1189: $hasDefaultCase = true;
1190: $branchScope = $scopeForBranches;
1191: }
1192:
1193: $branchScope = $branchScope->mergeWith($prevScope);
1194: $branchScopeResult = $this->processStmtNodes($caseNode, $caseNode->stmts, $branchScope, $nodeCallback, $context);
1195: $branchScope = $branchScopeResult->getScope();
1196: $branchFinalScopeResult = $branchScopeResult->filterOutLoopExitPoints();
1197: $hasYield = $hasYield || $branchFinalScopeResult->hasYield();
1198: foreach ($branchScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
1199: $alwaysTerminating = false;
1200: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1201: }
1202: foreach ($branchScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1203: $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
1204: }
1205: $exitPointsForOuterLoop = array_merge($exitPointsForOuterLoop, $branchFinalScopeResult->getExitPointsForOuterLoop());
1206: $throwPoints = array_merge($throwPoints, $branchFinalScopeResult->getThrowPoints());
1207: if ($branchScopeResult->isAlwaysTerminating()) {
1208: $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating();
1209: $prevScope = null;
1210: if (isset($condExpr)) {
1211: $scopeForBranches = $scopeForBranches->filterByFalseyValue($condExpr);
1212: }
1213: if (!$branchFinalScopeResult->isAlwaysTerminating()) {
1214: $finalScope = $branchScope->mergeWith($finalScope);
1215: }
1216: } else {
1217: $prevScope = $branchScope;
1218: }
1219: }
1220:
1221: $exhaustive = $scopeForBranches->getType($stmt->cond) instanceof NeverType;
1222:
1223: if (!$hasDefaultCase && !$exhaustive) {
1224: $alwaysTerminating = false;
1225: }
1226:
1227: if ($prevScope !== null && isset($branchFinalScopeResult)) {
1228: $finalScope = $prevScope->mergeWith($finalScope);
1229: $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating();
1230: }
1231:
1232: if ((!$hasDefaultCase && !$exhaustive) || $finalScope === null) {
1233: $finalScope = $scope->mergeWith($finalScope);
1234: }
1235:
1236: return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPointsForOuterLoop, $throwPoints);
1237: } elseif ($stmt instanceof TryCatch) {
1238: $branchScopeResult = $this->processStmtNodes($stmt, $stmt->stmts, $scope, $nodeCallback, $context);
1239: $branchScope = $branchScopeResult->getScope();
1240: $finalScope = $branchScopeResult->isAlwaysTerminating() ? null : $branchScope;
1241:
1242: $exitPoints = [];
1243: $finallyExitPoints = [];
1244: $alwaysTerminating = $branchScopeResult->isAlwaysTerminating();
1245: $hasYield = $branchScopeResult->hasYield();
1246:
1247: if ($stmt->finally !== null) {
1248: $finallyScope = $branchScope;
1249: } else {
1250: $finallyScope = null;
1251: }
1252: foreach ($branchScopeResult->getExitPoints() as $exitPoint) {
1253: $finallyExitPoints[] = $exitPoint;
1254: if ($exitPoint->getStatement() instanceof Throw_) {
1255: continue;
1256: }
1257: if ($finallyScope !== null) {
1258: $finallyScope = $finallyScope->mergeWith($exitPoint->getScope());
1259: }
1260: $exitPoints[] = $exitPoint;
1261: }
1262:
1263: $throwPoints = $branchScopeResult->getThrowPoints();
1264: $throwPointsForLater = [];
1265: $pastCatchTypes = new NeverType();
1266:
1267: foreach ($stmt->catches as $catchNode) {
1268: $nodeCallback($catchNode, $scope);
1269:
1270: $catchType = TypeCombinator::union(...array_map(static fn (Name $name): Type => new ObjectType($name->toString()), $catchNode->types));
1271: $originalCatchType = $catchType;
1272: $isThrowable = TrinaryLogic::createNo()->lazyOr(
1273: $originalCatchType->getObjectClassNames(),
1274: static fn (string $objectClassName) => TrinaryLogic::createFromBoolean(strtolower($objectClassName) === 'throwable'),
1275: );
1276: $catchType = TypeCombinator::remove($catchType, $pastCatchTypes);
1277: $pastCatchTypes = TypeCombinator::union($pastCatchTypes, $originalCatchType);
1278: $matchingThrowPoints = [];
1279: $newThrowPoints = [];
1280: foreach ($throwPoints as $throwPoint) {
1281: if (!$throwPoint->isExplicit() && !$catchType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) {
1282: continue;
1283: }
1284: $isSuperType = $catchType->isSuperTypeOf($throwPoint->getType());
1285: if ($isSuperType->no()) {
1286: continue;
1287: }
1288: $matchingThrowPoints[] = $throwPoint;
1289: }
1290: $hasExplicit = count($matchingThrowPoints) > 0;
1291: foreach ($throwPoints as $throwPoint) {
1292: $isSuperType = $catchType->isSuperTypeOf($throwPoint->getType());
1293: if (!$hasExplicit && !$isSuperType->no()) {
1294: $matchingThrowPoints[] = $throwPoint;
1295: }
1296: if ($isSuperType->yes()) {
1297: continue;
1298: }
1299: if ($isThrowable->yes()) {
1300: continue;
1301: }
1302: $newThrowPoints[] = $throwPoint->subtractCatchType($catchType);
1303: }
1304: $throwPoints = $newThrowPoints;
1305:
1306: if (count($matchingThrowPoints) === 0) {
1307: $throwableThrowPoints = [];
1308: if ($originalCatchType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) {
1309: foreach ($branchScopeResult->getThrowPoints() as $originalThrowPoint) {
1310: if (!$originalThrowPoint->canContainAnyThrowable()) {
1311: continue;
1312: }
1313:
1314: $throwableThrowPoints[] = $originalThrowPoint;
1315: }
1316: }
1317:
1318: if (count($throwableThrowPoints) === 0) {
1319: $nodeCallback(new CatchWithUnthrownExceptionNode($catchNode, $catchType, $originalCatchType), $scope);
1320: continue;
1321: }
1322:
1323: $matchingThrowPoints = $throwableThrowPoints;
1324: }
1325:
1326: $catchScope = null;
1327: foreach ($matchingThrowPoints as $matchingThrowPoint) {
1328: if ($catchScope === null) {
1329: $catchScope = $matchingThrowPoint->getScope();
1330: } else {
1331: $catchScope = $catchScope->mergeWith($matchingThrowPoint->getScope());
1332: }
1333: }
1334:
1335: $variableName = null;
1336: if ($catchNode->var !== null) {
1337: if (!is_string($catchNode->var->name)) {
1338: throw new ShouldNotHappenException();
1339: }
1340:
1341: $variableName = $catchNode->var->name;
1342: }
1343:
1344: $catchScopeResult = $this->processStmtNodes($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $nodeCallback, $context);
1345: $catchScopeForFinally = $catchScopeResult->getScope();
1346:
1347: $finalScope = $catchScopeResult->isAlwaysTerminating() ? $finalScope : $catchScopeResult->getScope()->mergeWith($finalScope);
1348: $alwaysTerminating = $alwaysTerminating && $catchScopeResult->isAlwaysTerminating();
1349: $hasYield = $hasYield || $catchScopeResult->hasYield();
1350: $catchThrowPoints = $catchScopeResult->getThrowPoints();
1351: $throwPointsForLater = array_merge($throwPointsForLater, $catchThrowPoints);
1352:
1353: if ($finallyScope !== null) {
1354: $finallyScope = $finallyScope->mergeWith($catchScopeForFinally);
1355: }
1356: foreach ($catchScopeResult->getExitPoints() as $exitPoint) {
1357: $finallyExitPoints[] = $exitPoint;
1358: if ($exitPoint->getStatement() instanceof Throw_) {
1359: continue;
1360: }
1361: if ($finallyScope !== null) {
1362: $finallyScope = $finallyScope->mergeWith($exitPoint->getScope());
1363: }
1364: $exitPoints[] = $exitPoint;
1365: }
1366:
1367: foreach ($catchThrowPoints as $catchThrowPoint) {
1368: if ($finallyScope === null) {
1369: continue;
1370: }
1371: $finallyScope = $finallyScope->mergeWith($catchThrowPoint->getScope());
1372: }
1373: }
1374:
1375: if ($finalScope === null) {
1376: $finalScope = $scope;
1377: }
1378:
1379: foreach ($throwPoints as $throwPoint) {
1380: if ($finallyScope === null) {
1381: continue;
1382: }
1383: $finallyScope = $finallyScope->mergeWith($throwPoint->getScope());
1384: }
1385:
1386: if ($finallyScope !== null && $stmt->finally !== null) {
1387: $originalFinallyScope = $finallyScope;
1388: $finallyResult = $this->processStmtNodes($stmt->finally, $stmt->finally->stmts, $finallyScope, $nodeCallback, $context);
1389: $alwaysTerminating = $alwaysTerminating || $finallyResult->isAlwaysTerminating();
1390: $hasYield = $hasYield || $finallyResult->hasYield();
1391: $throwPointsForLater = array_merge($throwPointsForLater, $finallyResult->getThrowPoints());
1392: $finallyScope = $finallyResult->getScope();
1393: $finalScope = $finallyResult->isAlwaysTerminating() ? $finalScope : $finalScope->processFinallyScope($finallyScope, $originalFinallyScope);
1394: if (count($finallyResult->getExitPoints()) > 0) {
1395: $nodeCallback(new FinallyExitPointsNode(
1396: $finallyResult->getExitPoints(),
1397: $finallyExitPoints,
1398: ), $scope);
1399: }
1400: $exitPoints = array_merge($exitPoints, $finallyResult->getExitPoints());
1401: }
1402:
1403: return new StatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, array_merge($throwPoints, $throwPointsForLater));
1404: } elseif ($stmt instanceof Unset_) {
1405: $hasYield = false;
1406: $throwPoints = [];
1407: foreach ($stmt->vars as $var) {
1408: $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var);
1409: $scope = $this->processExprNode($var, $scope, $nodeCallback, ExpressionContext::createDeep())->getScope();
1410: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
1411: $scope = $scope->unsetExpression($var);
1412: }
1413: } elseif ($stmt instanceof Node\Stmt\Use_) {
1414: $hasYield = false;
1415: $throwPoints = [];
1416: foreach ($stmt->uses as $use) {
1417: $this->processStmtNode($use, $scope, $nodeCallback, $context);
1418: }
1419: } elseif ($stmt instanceof Node\Stmt\Global_) {
1420: $hasYield = false;
1421: $throwPoints = [];
1422: $vars = [];
1423: foreach ($stmt->vars as $var) {
1424: if (!$var instanceof Variable) {
1425: throw new ShouldNotHappenException();
1426: }
1427: $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var);
1428: $this->processExprNode($var, $scope, $nodeCallback, ExpressionContext::createDeep());
1429: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
1430:
1431: if (!is_string($var->name)) {
1432: continue;
1433: }
1434:
1435: $scope = $scope->assignVariable($var->name, new MixedType(), new MixedType());
1436: $vars[] = $var->name;
1437: }
1438: $scope = $this->processVarAnnotation($scope, $vars, $stmt);
1439: } elseif ($stmt instanceof Static_) {
1440: $hasYield = false;
1441: $throwPoints = [];
1442:
1443: $vars = [];
1444: foreach ($stmt->vars as $var) {
1445: $scope = $this->processStmtNode($var, $scope, $nodeCallback, $context)->getScope();
1446: if (!is_string($var->var->name)) {
1447: continue;
1448: }
1449:
1450: $vars[] = $var->var->name;
1451: }
1452:
1453: $scope = $this->processVarAnnotation($scope, $vars, $stmt);
1454: } elseif ($stmt instanceof StaticVar) {
1455: $hasYield = false;
1456: $throwPoints = [];
1457: if (!is_string($stmt->var->name)) {
1458: throw new ShouldNotHappenException();
1459: }
1460: if ($stmt->default !== null) {
1461: $this->processExprNode($stmt->default, $scope, $nodeCallback, ExpressionContext::createDeep());
1462: }
1463: $scope = $scope->enterExpressionAssign($stmt->var);
1464: $this->processExprNode($stmt->var, $scope, $nodeCallback, ExpressionContext::createDeep());
1465: $scope = $scope->exitExpressionAssign($stmt->var);
1466: $scope = $scope->assignVariable($stmt->var->name, new MixedType(), new MixedType());
1467: } elseif ($stmt instanceof Node\Stmt\Const_) {
1468: $hasYield = false;
1469: $throwPoints = [];
1470: foreach ($stmt->consts as $const) {
1471: $nodeCallback($const, $scope);
1472: $this->processExprNode($const->value, $scope, $nodeCallback, ExpressionContext::createDeep());
1473: if ($const->namespacedName !== null) {
1474: $constantName = new Name\FullyQualified($const->namespacedName->toString());
1475: } else {
1476: $constantName = new Name\FullyQualified($const->name->toString());
1477: }
1478: $scope = $scope->assignExpression(new ConstFetch($constantName), $scope->getType($const->value), $scope->getNativeType($const->value));
1479: }
1480: } elseif ($stmt instanceof Node\Stmt\ClassConst) {
1481: $hasYield = false;
1482: $throwPoints = [];
1483: $this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback);
1484: foreach ($stmt->consts as $const) {
1485: $nodeCallback($const, $scope);
1486: $this->processExprNode($const->value, $scope, $nodeCallback, ExpressionContext::createDeep());
1487: if ($scope->getClassReflection() === null) {
1488: throw new ShouldNotHappenException();
1489: }
1490: $scope = $scope->assignExpression(
1491: new Expr\ClassConstFetch(new Name\FullyQualified($scope->getClassReflection()->getName()), $const->name),
1492: $scope->getType($const->value),
1493: $scope->getNativeType($const->value),
1494: );
1495: }
1496: } elseif ($stmt instanceof Node\Stmt\EnumCase) {
1497: $hasYield = false;
1498: $throwPoints = [];
1499: $this->processAttributeGroups($stmt->attrGroups, $scope, $nodeCallback);
1500: if ($stmt->expr !== null) {
1501: $this->processExprNode($stmt->expr, $scope, $nodeCallback, ExpressionContext::createDeep());
1502: }
1503: } elseif ($stmt instanceof Node\Stmt\Nop) {
1504: $hasYield = false;
1505: $throwPoints = $overridingThrowPoints ?? [];
1506: } elseif ($stmt instanceof Node\Stmt\UseUse) {
1507: $hasYield = false;
1508: $throwPoints = [];
1509: } elseif ($stmt instanceof Node\Stmt\GroupUse) {
1510: $hasYield = false;
1511: $throwPoints = [];
1512: foreach ($stmt->uses as $use) {
1513: $this->processStmtNode($use, $scope, $nodeCallback, $context);
1514: }
1515: } else {
1516: $hasYield = false;
1517: $throwPoints = $overridingThrowPoints ?? [];
1518: }
1519:
1520: return new StatementResult($scope, $hasYield, false, [], $throwPoints);
1521: }
1522:
1523: /**
1524: * @return ThrowPoint[]|null
1525: */
1526: private function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $scope): ?array
1527: {
1528: foreach ($statement->getComments() as $comment) {
1529: if (!$comment instanceof Doc) {
1530: continue;
1531: }
1532:
1533: $function = $scope->getFunction();
1534: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
1535: $scope->getFile(),
1536: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
1537: $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
1538: $function !== null ? $function->getName() : null,
1539: $comment->getText(),
1540: );
1541:
1542: $throwsTag = $resolvedPhpDoc->getThrowsTag();
1543: if ($throwsTag !== null) {
1544: $throwsType = $throwsTag->getType();
1545: if ($throwsType->isVoid()->yes()) {
1546: return [];
1547: }
1548:
1549: return [ThrowPoint::createExplicit($scope, $throwsType, $statement, false)];
1550: }
1551: }
1552:
1553: return null;
1554: }
1555:
1556: private function getCurrentClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection
1557: {
1558: if (!$this->reflectionProvider->hasClass($className)) {
1559: return $this->createAstClassReflection($stmt, $className, $scope);
1560: }
1561:
1562: $defaultClassReflection = $this->reflectionProvider->getClass($className);
1563: if ($defaultClassReflection->getFileName() !== $scope->getFile()) {
1564: return $this->createAstClassReflection($stmt, $className, $scope);
1565: }
1566:
1567: $startLine = $defaultClassReflection->getNativeReflection()->getStartLine();
1568: if ($startLine !== $stmt->getStartLine()) {
1569: return $this->createAstClassReflection($stmt, $className, $scope);
1570: }
1571:
1572: return $defaultClassReflection;
1573: }
1574:
1575: private function createAstClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection
1576: {
1577: $nodeToReflection = new NodeToReflection();
1578: $betterReflectionClass = $nodeToReflection->__invoke(
1579: $this->reflector,
1580: $stmt,
1581: new LocatedSource(FileReader::read($scope->getFile()), $className, $scope->getFile()),
1582: $scope->getNamespace() !== null ? new Node\Stmt\Namespace_(new Name($scope->getNamespace())) : null,
1583: );
1584: if (!$betterReflectionClass instanceof \PHPStan\BetterReflection\Reflection\ReflectionClass) {
1585: throw new ShouldNotHappenException();
1586: }
1587:
1588: $enumAdapter = base64_decode('UEhQU3RhblxCZXR0ZXJSZWZsZWN0aW9uXFJlZmxlY3Rpb25cQWRhcHRlclxSZWZsZWN0aW9uRW51bQ==', true);
1589:
1590: return new ClassReflection(
1591: $this->reflectionProvider,
1592: $this->initializerExprTypeResolver,
1593: $this->fileTypeMapper,
1594: $this->stubPhpDocProvider,
1595: $this->phpDocInheritanceResolver,
1596: $this->phpVersion,
1597: $this->classReflectionExtensionRegistryProvider->getRegistry()->getPropertiesClassReflectionExtensions(),
1598: $this->classReflectionExtensionRegistryProvider->getRegistry()->getMethodsClassReflectionExtensions(),
1599: $this->classReflectionExtensionRegistryProvider->getRegistry()->getAllowedSubTypesClassReflectionExtensions(),
1600: $betterReflectionClass->getName(),
1601: $betterReflectionClass instanceof ReflectionEnum && PHP_VERSION_ID >= 80000 ? new $enumAdapter($betterReflectionClass) : new ReflectionClass($betterReflectionClass),
1602: null,
1603: null,
1604: null,
1605: sprintf('%s:%d', $scope->getFile(), $stmt->getStartLine()),
1606: );
1607: }
1608:
1609: private function lookForSetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope
1610: {
1611: return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr));
1612: }
1613:
1614: private function lookForUnsetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope
1615: {
1616: return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->unsetAllowedUndefinedExpression($expr));
1617: }
1618:
1619: /**
1620: * @param Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback
1621: */
1622: private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope
1623: {
1624: if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) {
1625: $scope = $callback($scope, $expr);
1626: }
1627:
1628: if ($expr instanceof ArrayDimFetch) {
1629: $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback);
1630: } elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) {
1631: $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback);
1632: } elseif ($expr instanceof StaticPropertyFetch && $expr->class instanceof Expr) {
1633: $scope = $this->lookForExpressionCallback($scope, $expr->class, $callback);
1634: } elseif ($expr instanceof Array_ || $expr instanceof List_) {
1635: foreach ($expr->items as $item) {
1636: if ($item === null) {
1637: continue;
1638: }
1639:
1640: $scope = $this->lookForExpressionCallback($scope, $item->value, $callback);
1641: }
1642: }
1643:
1644: return $scope;
1645: }
1646:
1647: private function ensureShallowNonNullability(MutatingScope $scope, Scope $originalScope, Expr $exprToSpecify): EnsuredNonNullabilityResult
1648: {
1649: $exprType = $scope->getType($exprToSpecify);
1650: $exprTypeWithoutNull = TypeCombinator::removeNull($exprType);
1651: if ($exprType->equals($exprTypeWithoutNull)) {
1652: $originalExprType = $originalScope->getType($exprToSpecify);
1653: if (!$originalExprType->equals($exprTypeWithoutNull)) {
1654: $originalNativeType = $originalScope->getNativeType($exprToSpecify);
1655:
1656: return new EnsuredNonNullabilityResult($scope, [
1657: new EnsuredNonNullabilityResultExpression($exprToSpecify, $originalExprType, $originalNativeType),
1658: ]);
1659: }
1660: return new EnsuredNonNullabilityResult($scope, []);
1661: }
1662:
1663: $nativeType = $scope->getNativeType($exprToSpecify);
1664: $scope = $scope->specifyExpressionType(
1665: $exprToSpecify,
1666: $exprTypeWithoutNull,
1667: TypeCombinator::removeNull($nativeType),
1668: );
1669:
1670: return new EnsuredNonNullabilityResult(
1671: $scope,
1672: [
1673: new EnsuredNonNullabilityResultExpression($exprToSpecify, $exprType, $nativeType),
1674: ],
1675: );
1676: }
1677:
1678: private function ensureNonNullability(MutatingScope $scope, Expr $expr): EnsuredNonNullabilityResult
1679: {
1680: $specifiedExpressions = [];
1681: $originalScope = $scope;
1682: $scope = $this->lookForExpressionCallback($scope, $expr, function ($scope, $expr) use (&$specifiedExpressions, $originalScope) {
1683: $result = $this->ensureShallowNonNullability($scope, $originalScope, $expr);
1684: foreach ($result->getSpecifiedExpressions() as $specifiedExpression) {
1685: $specifiedExpressions[] = $specifiedExpression;
1686: }
1687: return $result->getScope();
1688: });
1689:
1690: return new EnsuredNonNullabilityResult($scope, $specifiedExpressions);
1691: }
1692:
1693: /**
1694: * @param EnsuredNonNullabilityResultExpression[] $specifiedExpressions
1695: */
1696: private function revertNonNullability(MutatingScope $scope, array $specifiedExpressions): MutatingScope
1697: {
1698: foreach ($specifiedExpressions as $specifiedExpressionResult) {
1699: $scope = $scope->specifyExpressionType(
1700: $specifiedExpressionResult->getExpression(),
1701: $specifiedExpressionResult->getOriginalType(),
1702: $specifiedExpressionResult->getOriginalNativeType(),
1703: );
1704: }
1705:
1706: return $scope;
1707: }
1708:
1709: private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr
1710: {
1711: if (($expr instanceof MethodCall || $expr instanceof Expr\StaticCall) && $expr->name instanceof Node\Identifier) {
1712: if (array_key_exists($expr->name->toLowerString(), $this->earlyTerminatingMethodNames)) {
1713: if ($expr instanceof MethodCall) {
1714: $methodCalledOnType = $scope->getType($expr->var);
1715: } else {
1716: if ($expr->class instanceof Name) {
1717: $methodCalledOnType = $scope->resolveTypeByName($expr->class);
1718: } else {
1719: $methodCalledOnType = $scope->getType($expr->class);
1720: }
1721: }
1722:
1723: foreach ($methodCalledOnType->getObjectClassNames() as $referencedClass) {
1724: if (!$this->reflectionProvider->hasClass($referencedClass)) {
1725: continue;
1726: }
1727:
1728: $classReflection = $this->reflectionProvider->getClass($referencedClass);
1729: foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames(), $classReflection->getNativeReflection()->getInterfaceNames()) as $className) {
1730: if (!isset($this->earlyTerminatingMethodCalls[$className])) {
1731: continue;
1732: }
1733:
1734: if (in_array((string) $expr->name, $this->earlyTerminatingMethodCalls[$className], true)) {
1735: return $expr;
1736: }
1737: }
1738: }
1739: }
1740: }
1741:
1742: if ($expr instanceof FuncCall && $expr->name instanceof Name) {
1743: if (in_array((string) $expr->name, $this->earlyTerminatingFunctionCalls, true)) {
1744: return $expr;
1745: }
1746: }
1747:
1748: if ($expr instanceof Expr\Exit_ || $expr instanceof Expr\Throw_) {
1749: return $expr;
1750: }
1751:
1752: $exprType = $scope->getType($expr);
1753: if ($exprType instanceof NeverType && $exprType->isExplicit()) {
1754: return $expr;
1755: }
1756:
1757: return null;
1758: }
1759:
1760: /**
1761: * @param callable(Node $node, Scope $scope): void $nodeCallback
1762: */
1763: private function processExprNode(Expr $expr, MutatingScope $scope, callable $nodeCallback, ExpressionContext $context): ExpressionResult
1764: {
1765: if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) {
1766: if ($expr instanceof FuncCall) {
1767: $newExpr = new FunctionCallableNode($expr->name, $expr);
1768: } elseif ($expr instanceof MethodCall) {
1769: $newExpr = new MethodCallableNode($expr->var, $expr->name, $expr);
1770: } elseif ($expr instanceof StaticCall) {
1771: $newExpr = new StaticMethodCallableNode($expr->class, $expr->name, $expr);
1772: } elseif ($expr instanceof New_ && !$expr->class instanceof Class_) {
1773: $newExpr = new InstantiationCallableNode($expr->class, $expr);
1774: } else {
1775: throw new ShouldNotHappenException();
1776: }
1777:
1778: return $this->processExprNode($newExpr, $scope, $nodeCallback, $context);
1779: }
1780:
1781: $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $context);
1782:
1783: if ($expr instanceof Variable) {
1784: $hasYield = false;
1785: $throwPoints = [];
1786: if ($expr->name instanceof Expr) {
1787: return $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep());
1788: }
1789: } elseif ($expr instanceof Assign || $expr instanceof AssignRef) {
1790: $result = $this->processAssignVar(
1791: $scope,
1792: $expr->var,
1793: $expr->expr,
1794: $nodeCallback,
1795: $context,
1796: function (MutatingScope $scope) use ($expr, $nodeCallback, $context): ExpressionResult {
1797: if ($expr instanceof AssignRef) {
1798: $scope = $scope->enterExpressionAssign($expr->expr);
1799: }
1800:
1801: if ($expr->var instanceof Variable && is_string($expr->var->name)) {
1802: $context = $context->enterRightSideAssign(
1803: $expr->var->name,
1804: $scope->getType($expr->expr),
1805: $scope->getNativeType($expr->expr),
1806: );
1807: }
1808:
1809: $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep());
1810: $hasYield = $result->hasYield();
1811: $throwPoints = $result->getThrowPoints();
1812: $scope = $result->getScope();
1813:
1814: if ($expr instanceof AssignRef) {
1815: $scope = $scope->exitExpressionAssign($expr->expr);
1816: }
1817:
1818: return new ExpressionResult($scope, $hasYield, $throwPoints);
1819: },
1820: true,
1821: );
1822: $scope = $result->getScope();
1823: $hasYield = $result->hasYield();
1824: $throwPoints = $result->getThrowPoints();
1825: $vars = $this->getAssignedVariables($expr->var);
1826: if (count($vars) > 0) {
1827: $varChangedScope = false;
1828: $scope = $this->processVarAnnotation($scope, $vars, $expr, $varChangedScope);
1829: if (!$varChangedScope) {
1830: $scope = $this->processStmtVarAnnotation($scope, new Node\Stmt\Expression($expr, [
1831: 'comments' => $expr->getAttribute('comments'),
1832: ]), null, $nodeCallback);
1833: }
1834: }
1835: } elseif ($expr instanceof Expr\AssignOp) {
1836: $result = $this->processAssignVar(
1837: $scope,
1838: $expr->var,
1839: $expr,
1840: $nodeCallback,
1841: $context,
1842: fn (MutatingScope $scope): ExpressionResult => $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep()),
1843: $expr instanceof Expr\AssignOp\Coalesce,
1844: );
1845: $scope = $result->getScope();
1846: $hasYield = $result->hasYield();
1847: $throwPoints = $result->getThrowPoints();
1848: if (
1849: ($expr instanceof Expr\AssignOp\Div || $expr instanceof Expr\AssignOp\Mod) &&
1850: !$scope->getType($expr->expr)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no()
1851: ) {
1852: $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false);
1853: }
1854: } elseif ($expr instanceof FuncCall) {
1855: $parametersAcceptor = null;
1856: $functionReflection = null;
1857: $throwPoints = [];
1858: if ($expr->name instanceof Expr) {
1859: $nameType = $scope->getType($expr->name);
1860: if ($nameType->isCallable()->yes()) {
1861: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
1862: $scope,
1863: $expr->getArgs(),
1864: $nameType->getCallableParametersAcceptors($scope),
1865: );
1866: }
1867: $nameResult = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep());
1868: $throwPoints = $nameResult->getThrowPoints();
1869: $scope = $nameResult->getScope();
1870: } elseif ($this->reflectionProvider->hasFunction($expr->name, $scope)) {
1871: $functionReflection = $this->reflectionProvider->getFunction($expr->name, $scope);
1872: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
1873: $scope,
1874: $expr->getArgs(),
1875: $functionReflection->getVariants(),
1876: );
1877: }
1878:
1879: if ($parametersAcceptor !== null) {
1880: $expr = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $expr) ?? $expr;
1881: }
1882: $result = $this->processArgs($functionReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context);
1883: $scope = $result->getScope();
1884: $hasYield = $result->hasYield();
1885: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
1886:
1887: if (isset($functionReflection)) {
1888: $functionThrowPoint = $this->getFunctionThrowPoint($functionReflection, $parametersAcceptor, $expr, $scope);
1889: if ($functionThrowPoint !== null) {
1890: $throwPoints[] = $functionThrowPoint;
1891: }
1892: } else {
1893: $throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
1894: }
1895:
1896: if (
1897: isset($functionReflection)
1898: && in_array($functionReflection->getName(), ['json_encode', 'json_decode'], true)
1899: ) {
1900: $scope = $scope->invalidateExpression(new FuncCall(new Name('json_last_error'), []))
1901: ->invalidateExpression(new FuncCall(new Name\FullyQualified('json_last_error'), []))
1902: ->invalidateExpression(new FuncCall(new Name('json_last_error_msg'), []))
1903: ->invalidateExpression(new FuncCall(new Name\FullyQualified('json_last_error_msg'), []));
1904: }
1905:
1906: if (
1907: isset($functionReflection)
1908: && in_array($functionReflection->getName(), ['array_pop', 'array_shift'], true)
1909: && count($expr->getArgs()) >= 1
1910: ) {
1911: $arrayArg = $expr->getArgs()[0]->value;
1912:
1913: $arrayArgType = $scope->getType($arrayArg);
1914: $arrayArgNativeType = $scope->getNativeType($arrayArg);
1915:
1916: $isArrayPop = $functionReflection->getName() === 'array_pop';
1917: $scope = $scope->invalidateExpression($arrayArg)->assignExpression(
1918: $arrayArg,
1919: $isArrayPop ? $arrayArgType->popArray() : $arrayArgType->shiftArray(),
1920: $isArrayPop ? $arrayArgNativeType->popArray() : $arrayArgNativeType->shiftArray(),
1921: );
1922: }
1923:
1924: if (
1925: isset($functionReflection)
1926: && in_array($functionReflection->getName(), ['array_push', 'array_unshift'], true)
1927: && count($expr->getArgs()) >= 2
1928: ) {
1929: $arrayArg = $expr->getArgs()[0]->value;
1930: $arrayType = $scope->getType($arrayArg);
1931: $callArgs = array_slice($expr->getArgs(), 1);
1932:
1933: /**
1934: * @param Arg[] $callArgs
1935: * @param callable(?Type, Type, bool): void $setOffsetValueType
1936: */
1937: $setOffsetValueTypes = static function (Scope $scope, array $callArgs, callable $setOffsetValueType, ?bool &$nonConstantArrayWasUnpacked = null): void {
1938: foreach ($callArgs as $callArg) {
1939: $callArgType = $scope->getType($callArg->value);
1940: if ($callArg->unpack) {
1941: if (count($callArgType->getConstantArrays()) === 1) {
1942: $iterableValueTypes = $callArgType->getConstantArrays()[0]->getValueTypes();
1943: } else {
1944: $iterableValueTypes = [$callArgType->getIterableValueType()];
1945: $nonConstantArrayWasUnpacked = true;
1946: }
1947:
1948: $isOptional = !$callArgType->isIterableAtLeastOnce()->yes();
1949: foreach ($iterableValueTypes as $iterableValueType) {
1950: if ($iterableValueType instanceof UnionType) {
1951: foreach ($iterableValueType->getTypes() as $innerType) {
1952: $setOffsetValueType(null, $innerType, $isOptional);
1953: }
1954: } else {
1955: $setOffsetValueType(null, $iterableValueType, $isOptional);
1956: }
1957: }
1958: continue;
1959: }
1960: $setOffsetValueType(null, $callArgType, false);
1961: }
1962: };
1963:
1964: $constantArrays = $arrayType->getConstantArrays();
1965: if (count($constantArrays) > 0) {
1966: $newArrayTypes = [];
1967: $prepend = $functionReflection->getName() === 'array_unshift';
1968: foreach ($constantArrays as $constantArray) {
1969: $arrayTypeBuilder = $prepend ? ConstantArrayTypeBuilder::createEmpty() : ConstantArrayTypeBuilder::createFromConstantArray($constantArray);
1970:
1971: $setOffsetValueTypes(
1972: $scope,
1973: $callArgs,
1974: static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayTypeBuilder): void {
1975: $arrayTypeBuilder->setOffsetValueType($offsetType, $valueType, $optional);
1976: },
1977: $nonConstantArrayWasUnpacked,
1978: );
1979:
1980: if ($prepend) {
1981: $keyTypes = $constantArray->getKeyTypes();
1982: $valueTypes = $constantArray->getValueTypes();
1983: foreach ($keyTypes as $k => $keyType) {
1984: $arrayTypeBuilder->setOffsetValueType(
1985: count($keyType->getConstantStrings()) === 1 ? $keyType->getConstantStrings()[0] : null,
1986: $valueTypes[$k],
1987: $constantArray->isOptionalKey($k),
1988: );
1989: }
1990: }
1991:
1992: $constantArray = $arrayTypeBuilder->getArray();
1993:
1994: if ($constantArray->isConstantArray()->yes() && $nonConstantArrayWasUnpacked) {
1995: $array = new ArrayType($constantArray->generalize(GeneralizePrecision::lessSpecific())->getIterableKeyType(), $constantArray->getIterableValueType());
1996: $constantArray = $constantArray->isIterableAtLeastOnce()->yes()
1997: ? TypeCombinator::intersect($array, new NonEmptyArrayType())
1998: : $array;
1999: }
2000:
2001: $newArrayTypes[] = $constantArray;
2002: }
2003:
2004: $arrayType = TypeCombinator::union(...$newArrayTypes);
2005: } else {
2006: $setOffsetValueTypes(
2007: $scope,
2008: $callArgs,
2009: static function (?Type $offsetType, Type $valueType, bool $optional) use (&$arrayType): void {
2010: $isIterableAtLeastOnce = $arrayType->isIterableAtLeastOnce()->yes() || !$optional;
2011: $arrayType = $arrayType->setOffsetValueType($offsetType, $valueType);
2012: if ($isIterableAtLeastOnce) {
2013: return;
2014: }
2015:
2016: $arrayType = new ArrayType($arrayType->getIterableKeyType(), $arrayType->getIterableValueType());
2017: },
2018: );
2019: }
2020:
2021: $scope = $scope->invalidateExpression($arrayArg)->assignExpression($arrayArg, $arrayType, $scope->getNativeType($arrayArg));
2022: }
2023:
2024: if (
2025: isset($functionReflection)
2026: && in_array($functionReflection->getName(), ['fopen', 'file_get_contents'], true)
2027: ) {
2028: $scope = $scope->assignVariable('http_response_header', new ArrayType(new IntegerType(), new StringType()), new ArrayType(new IntegerType(), new StringType()));
2029: }
2030:
2031: if (isset($functionReflection) && $functionReflection->getName() === 'shuffle') {
2032: $arrayArg = $expr->getArgs()[0]->value;
2033: $scope = $scope->assignExpression(
2034: $arrayArg,
2035: $scope->getType($arrayArg)->shuffleArray(),
2036: $scope->getNativeType($arrayArg)->shuffleArray(),
2037: );
2038: }
2039:
2040: if (
2041: isset($functionReflection)
2042: && $functionReflection->getName() === 'array_splice'
2043: && count($expr->getArgs()) >= 1
2044: ) {
2045: $arrayArg = $expr->getArgs()[0]->value;
2046: $arrayArgType = $scope->getType($arrayArg);
2047: $valueType = $arrayArgType->getIterableValueType();
2048: if (count($expr->getArgs()) >= 4) {
2049: $valueType = TypeCombinator::union($valueType, $scope->getType($expr->getArgs()[3]->value)->getIterableValueType());
2050: }
2051: $scope = $scope->invalidateExpression($arrayArg)->assignExpression(
2052: $arrayArg,
2053: new ArrayType($arrayArgType->getIterableKeyType(), $valueType),
2054: new ArrayType($arrayArgType->getIterableKeyType(), $valueType),
2055: );
2056: }
2057:
2058: if (isset($functionReflection) && $functionReflection->getName() === 'extract') {
2059: $scope = $scope->afterExtractCall();
2060: }
2061:
2062: if (isset($functionReflection) && ($functionReflection->getName() === 'clearstatcache' || $functionReflection->getName() === 'unlink')) {
2063: $scope = $scope->afterClearstatcacheCall();
2064: }
2065:
2066: if (isset($functionReflection) && str_starts_with($functionReflection->getName(), 'openssl')) {
2067: $scope = $scope->afterOpenSslCall($functionReflection->getName());
2068: }
2069:
2070: if (isset($functionReflection) && $functionReflection->hasSideEffects()->yes()) {
2071: foreach ($expr->getArgs() as $arg) {
2072: $scope = $scope->invalidateExpression($arg->value, true);
2073: }
2074: }
2075:
2076: } elseif ($expr instanceof MethodCall) {
2077: $originalScope = $scope;
2078: if (
2079: ($expr->var instanceof Expr\Closure || $expr->var instanceof Expr\ArrowFunction)
2080: && $expr->name instanceof Node\Identifier
2081: && strtolower($expr->name->name) === 'call'
2082: && isset($expr->getArgs()[0])
2083: ) {
2084: $closureCallScope = $scope->enterClosureCall(
2085: $scope->getType($expr->getArgs()[0]->value),
2086: $scope->getNativeType($expr->getArgs()[0]->value),
2087: );
2088: }
2089:
2090: $result = $this->processExprNode($expr->var, $closureCallScope ?? $scope, $nodeCallback, $context->enterDeep());
2091: $hasYield = $result->hasYield();
2092: $throwPoints = $result->getThrowPoints();
2093: $scope = $result->getScope();
2094: if (isset($closureCallScope)) {
2095: $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope);
2096: }
2097: $parametersAcceptor = null;
2098: $methodReflection = null;
2099: if ($expr->name instanceof Expr) {
2100: $methodNameResult = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep());
2101: $throwPoints = array_merge($throwPoints, $methodNameResult->getThrowPoints());
2102: $scope = $methodNameResult->getScope();
2103: } else {
2104: $calledOnType = $scope->getType($expr->var);
2105: $methodName = $expr->name->name;
2106: $methodReflection = $scope->getMethodReflection($calledOnType, $methodName);
2107: if ($methodReflection !== null) {
2108: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
2109: $scope,
2110: $expr->getArgs(),
2111: $methodReflection->getVariants(),
2112: );
2113:
2114: $methodThrowPoint = $this->getMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope);
2115: if ($methodThrowPoint !== null) {
2116: $throwPoints[] = $methodThrowPoint;
2117: }
2118: }
2119: }
2120:
2121: if ($parametersAcceptor !== null) {
2122: $expr = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $expr) ?? $expr;
2123: }
2124: $result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context);
2125: $scope = $result->getScope();
2126:
2127: if ($methodReflection !== null) {
2128: $hasSideEffects = $methodReflection->hasSideEffects();
2129: if ($hasSideEffects->yes() || $methodReflection->getName() === '__construct') {
2130: $scope = $scope->invalidateExpression($expr->var, true);
2131: foreach ($expr->getArgs() as $arg) {
2132: $scope = $scope->invalidateExpression($arg->value, true);
2133: }
2134: }
2135:
2136: if ($parametersAcceptor !== null) {
2137: $selfOutType = $methodReflection->getSelfOutType();
2138: if ($selfOutType !== null) {
2139: $scope = $scope->assignExpression($expr->var, TemplateTypeHelper::resolveTemplateTypes($selfOutType, $parametersAcceptor->getResolvedTemplateTypeMap()), $scope->getNativeType($expr->var));
2140: }
2141: }
2142: } else {
2143: $throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
2144: }
2145: $hasYield = $hasYield || $result->hasYield();
2146: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2147: } elseif ($expr instanceof Expr\NullsafeMethodCall) {
2148: $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var);
2149: $exprResult = $this->processExprNode(new MethodCall($expr->var, $expr->name, $expr->args, $expr->getAttributes()), $nonNullabilityResult->getScope(), $nodeCallback, $context);
2150: $scope = $this->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions());
2151:
2152: return new ExpressionResult(
2153: $scope,
2154: $exprResult->hasYield(),
2155: $exprResult->getThrowPoints(),
2156: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
2157: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
2158: );
2159: } elseif ($expr instanceof StaticCall) {
2160: $hasYield = false;
2161: $throwPoints = [];
2162: if ($expr->class instanceof Expr) {
2163: $objectClasses = $scope->getType($expr->class)->getObjectClassNames();
2164: if (count($objectClasses) !== 1) {
2165: $objectClasses = $scope->getType(new New_($expr->class))->getObjectClassNames();
2166: }
2167: if (count($objectClasses) === 1) {
2168: $objectExprResult = $this->processExprNode(new StaticCall(new Name($objectClasses[0]), $expr->name, []), $scope, static function (): void {
2169: }, $context->enterDeep());
2170: $additionalThrowPoints = $objectExprResult->getThrowPoints();
2171: } else {
2172: $additionalThrowPoints = [ThrowPoint::createImplicit($scope, $expr)];
2173: }
2174: $classResult = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep());
2175: $hasYield = $classResult->hasYield();
2176: $throwPoints = array_merge($throwPoints, $classResult->getThrowPoints());
2177: foreach ($additionalThrowPoints as $throwPoint) {
2178: $throwPoints[] = $throwPoint;
2179: }
2180: $scope = $classResult->getScope();
2181: }
2182:
2183: $parametersAcceptor = null;
2184: $methodReflection = null;
2185: if ($expr->name instanceof Expr) {
2186: $result = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep());
2187: $hasYield = $hasYield || $result->hasYield();
2188: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2189: $scope = $result->getScope();
2190: } elseif ($expr->class instanceof Name) {
2191: $className = $scope->resolveName($expr->class);
2192: if ($this->reflectionProvider->hasClass($className)) {
2193: $classReflection = $this->reflectionProvider->getClass($className);
2194: $methodName = $expr->name->name;
2195: if ($classReflection->hasMethod($methodName)) {
2196: $methodReflection = $classReflection->getMethod($methodName, $scope);
2197: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
2198: $scope,
2199: $expr->getArgs(),
2200: $methodReflection->getVariants(),
2201: );
2202:
2203: $methodThrowPoint = $this->getStaticMethodThrowPoint($methodReflection, $parametersAcceptor, $expr, $scope);
2204: if ($methodThrowPoint !== null) {
2205: $throwPoints[] = $methodThrowPoint;
2206: }
2207: if (
2208: $classReflection->getName() === 'Closure'
2209: && strtolower($methodName) === 'bind'
2210: ) {
2211: $thisType = null;
2212: $nativeThisType = null;
2213: if (isset($expr->getArgs()[1])) {
2214: $argType = $scope->getType($expr->getArgs()[1]->value);
2215: if ($argType->isNull()->yes()) {
2216: $thisType = null;
2217: } else {
2218: $thisType = $argType;
2219: }
2220:
2221: $nativeArgType = $scope->getNativeType($expr->getArgs()[1]->value);
2222: if ($nativeArgType->isNull()->yes()) {
2223: $nativeThisType = null;
2224: } else {
2225: $nativeThisType = $nativeArgType;
2226: }
2227: }
2228: $scopeClasses = ['static'];
2229: if (isset($expr->getArgs()[2])) {
2230: $argValue = $expr->getArgs()[2]->value;
2231: $argValueType = $scope->getType($argValue);
2232:
2233: $scopeClasses = [];
2234: $directClassNames = $argValueType->getObjectClassNames();
2235: if (count($directClassNames) > 0) {
2236: $scopeClasses = $directClassNames;
2237: $thisTypes = [];
2238: foreach ($directClassNames as $directClassName) {
2239: $thisTypes[] = new ObjectType($directClassName);
2240: }
2241: $thisType = TypeCombinator::union(...$thisTypes);
2242: } else {
2243: $thisType = $argValueType->getClassStringObjectType();
2244: $scopeClasses = $thisType->getObjectClassNames();
2245: }
2246: }
2247: $closureBindScope = $scope->enterClosureBind($thisType, $nativeThisType, $scopeClasses);
2248: }
2249: } else {
2250: $throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
2251: }
2252: } else {
2253: $throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
2254: }
2255: }
2256:
2257: if ($parametersAcceptor !== null) {
2258: $expr = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $expr) ?? $expr;
2259: }
2260: $result = $this->processArgs($methodReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context, $closureBindScope ?? null);
2261: $scope = $result->getScope();
2262: $scopeFunction = $scope->getFunction();
2263:
2264: if (
2265: $methodReflection !== null
2266: && !$methodReflection->isStatic()
2267: && (
2268: $methodReflection->hasSideEffects()->yes()
2269: || $methodReflection->getName() === '__construct'
2270: )
2271: && $scopeFunction instanceof MethodReflection
2272: && !$scopeFunction->isStatic()
2273: && $scope->isInClass()
2274: && (
2275: $scope->getClassReflection()->getName() === $methodReflection->getDeclaringClass()->getName()
2276: || $scope->getClassReflection()->isSubclassOf($methodReflection->getDeclaringClass()->getName())
2277: )
2278: ) {
2279: $scope = $scope->invalidateExpression(new Variable('this'), true);
2280: }
2281:
2282: if ($methodReflection !== null) {
2283: if ($methodReflection->hasSideEffects()->yes() || $methodReflection->getName() === '__construct') {
2284: foreach ($expr->getArgs() as $arg) {
2285: $scope = $scope->invalidateExpression($arg->value, true);
2286: }
2287: }
2288: }
2289:
2290: $hasYield = $hasYield || $result->hasYield();
2291: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2292: } elseif ($expr instanceof PropertyFetch) {
2293: $result = $this->processExprNode($expr->var, $scope, $nodeCallback, $context->enterDeep());
2294: $hasYield = $result->hasYield();
2295: $throwPoints = $result->getThrowPoints();
2296: $scope = $result->getScope();
2297: if ($expr->name instanceof Expr) {
2298: $result = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep());
2299: $hasYield = $hasYield || $result->hasYield();
2300: $scope = $result->getScope();
2301: }
2302: } elseif ($expr instanceof Expr\NullsafePropertyFetch) {
2303: $nonNullabilityResult = $this->ensureShallowNonNullability($scope, $scope, $expr->var);
2304: $exprResult = $this->processExprNode(new PropertyFetch($expr->var, $expr->name, $expr->getAttributes()), $nonNullabilityResult->getScope(), $nodeCallback, $context);
2305: $scope = $this->revertNonNullability($exprResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions());
2306:
2307: return new ExpressionResult(
2308: $scope,
2309: $exprResult->hasYield(),
2310: $exprResult->getThrowPoints(),
2311: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
2312: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
2313: );
2314: } elseif ($expr instanceof StaticPropertyFetch) {
2315: $hasYield = false;
2316: $throwPoints = [];
2317: if ($expr->class instanceof Expr) {
2318: $result = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep());
2319: $hasYield = $result->hasYield();
2320: $throwPoints = $result->getThrowPoints();
2321: $scope = $result->getScope();
2322: }
2323: if ($expr->name instanceof Expr) {
2324: $result = $this->processExprNode($expr->name, $scope, $nodeCallback, $context->enterDeep());
2325: $hasYield = $hasYield || $result->hasYield();
2326: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2327: $scope = $result->getScope();
2328: }
2329: } elseif ($expr instanceof Expr\Closure) {
2330: return $this->processClosureNode($expr, $scope, $nodeCallback, $context, null);
2331: } elseif ($expr instanceof Expr\ClosureUse) {
2332: $this->processExprNode($expr->var, $scope, $nodeCallback, $context);
2333: $hasYield = false;
2334: $throwPoints = [];
2335: } elseif ($expr instanceof Expr\ArrowFunction) {
2336: return $this->processArrowFunctionNode($expr, $scope, $nodeCallback, $context, null);
2337: } elseif ($expr instanceof ErrorSuppress) {
2338: $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context);
2339: $hasYield = $result->hasYield();
2340: $throwPoints = $result->getThrowPoints();
2341: $scope = $result->getScope();
2342: } elseif ($expr instanceof Exit_) {
2343: $hasYield = false;
2344: $throwPoints = [];
2345: if ($expr->expr !== null) {
2346: $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep());
2347: $hasYield = $result->hasYield();
2348: $throwPoints = $result->getThrowPoints();
2349: $scope = $result->getScope();
2350: }
2351: } elseif ($expr instanceof Node\Scalar\Encapsed) {
2352: $hasYield = false;
2353: $throwPoints = [];
2354: foreach ($expr->parts as $part) {
2355: $result = $this->processExprNode($part, $scope, $nodeCallback, $context->enterDeep());
2356: $hasYield = $hasYield || $result->hasYield();
2357: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2358: $scope = $result->getScope();
2359: }
2360: } elseif ($expr instanceof ArrayDimFetch) {
2361: $hasYield = false;
2362: $throwPoints = [];
2363: if ($expr->dim !== null) {
2364: $result = $this->processExprNode($expr->dim, $scope, $nodeCallback, $context->enterDeep());
2365: $hasYield = $result->hasYield();
2366: $throwPoints = $result->getThrowPoints();
2367: $scope = $result->getScope();
2368: }
2369:
2370: $result = $this->processExprNode($expr->var, $scope, $nodeCallback, $context->enterDeep());
2371: $hasYield = $hasYield || $result->hasYield();
2372: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2373: $scope = $result->getScope();
2374: } elseif ($expr instanceof Array_) {
2375: $itemNodes = [];
2376: $hasYield = false;
2377: $throwPoints = [];
2378: foreach ($expr->items as $arrayItem) {
2379: $itemNodes[] = new LiteralArrayItem($scope, $arrayItem);
2380: if ($arrayItem === null) {
2381: continue;
2382: }
2383: $result = $this->processExprNode($arrayItem, $scope, $nodeCallback, $context->enterDeep());
2384: $hasYield = $hasYield || $result->hasYield();
2385: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2386: $scope = $result->getScope();
2387: }
2388: $nodeCallback(new LiteralArrayNode($expr, $itemNodes), $scope);
2389: } elseif ($expr instanceof ArrayItem) {
2390: $hasYield = false;
2391: $throwPoints = [];
2392: if ($expr->key !== null) {
2393: $result = $this->processExprNode($expr->key, $scope, $nodeCallback, $context->enterDeep());
2394: $hasYield = $result->hasYield();
2395: $throwPoints = $result->getThrowPoints();
2396: $scope = $result->getScope();
2397: }
2398: $result = $this->processExprNode($expr->value, $scope, $nodeCallback, $context->enterDeep());
2399: $hasYield = $hasYield || $result->hasYield();
2400: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2401: $scope = $result->getScope();
2402: } elseif ($expr instanceof BooleanAnd || $expr instanceof BinaryOp\LogicalAnd) {
2403: $leftResult = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep());
2404: $rightResult = $this->processExprNode($expr->right, $leftResult->getTruthyScope(), $nodeCallback, $context);
2405: $rightExprType = $rightResult->getScope()->getType($expr->right);
2406: if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) {
2407: $leftMergedWithRightScope = $leftResult->getFalseyScope();
2408: } else {
2409: $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope());
2410: }
2411:
2412: $this->callNodeCallbackWithExpression($nodeCallback, new BooleanAndNode($expr, $leftResult->getTruthyScope()), $scope, $context);
2413:
2414: return new ExpressionResult(
2415: $leftMergedWithRightScope,
2416: $leftResult->hasYield() || $rightResult->hasYield(),
2417: array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()),
2418: static fn (): MutatingScope => $rightResult->getScope()->filterByTruthyValue($expr),
2419: static fn (): MutatingScope => $leftMergedWithRightScope->filterByFalseyValue($expr),
2420: );
2421: } elseif ($expr instanceof BooleanOr || $expr instanceof BinaryOp\LogicalOr) {
2422: $leftResult = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep());
2423: $rightResult = $this->processExprNode($expr->right, $leftResult->getFalseyScope(), $nodeCallback, $context);
2424: $rightExprType = $rightResult->getScope()->getType($expr->right);
2425: if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) {
2426: $leftMergedWithRightScope = $leftResult->getTruthyScope();
2427: } else {
2428: $leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope());
2429: }
2430:
2431: $this->callNodeCallbackWithExpression($nodeCallback, new BooleanOrNode($expr, $leftResult->getFalseyScope()), $scope, $context);
2432:
2433: return new ExpressionResult(
2434: $leftMergedWithRightScope,
2435: $leftResult->hasYield() || $rightResult->hasYield(),
2436: array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints()),
2437: static fn (): MutatingScope => $leftMergedWithRightScope->filterByTruthyValue($expr),
2438: static fn (): MutatingScope => $rightResult->getScope()->filterByFalseyValue($expr),
2439: );
2440: } elseif ($expr instanceof Coalesce) {
2441: $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->left);
2442: $condScope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->left);
2443: $condResult = $this->processExprNode($expr->left, $condScope, $nodeCallback, $context->enterDeep());
2444: $scope = $this->revertNonNullability($condResult->getScope(), $nonNullabilityResult->getSpecifiedExpressions());
2445: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->left);
2446:
2447: $rightScope = $scope->filterByFalseyValue(new Expr\Isset_([$expr->left]));
2448: $rightResult = $this->processExprNode($expr->right, $rightScope, $nodeCallback, $context->enterDeep());
2449: $rightExprType = $scope->getType($expr->right);
2450: if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) {
2451: $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left]));
2452: } else {
2453: $scope = $scope->filterByTruthyValue(new Expr\Isset_([$expr->left]))->mergeWith($rightResult->getScope());
2454: }
2455:
2456: $hasYield = $condResult->hasYield() || $rightResult->hasYield();
2457: $throwPoints = array_merge($condResult->getThrowPoints(), $rightResult->getThrowPoints());
2458: } elseif ($expr instanceof BinaryOp) {
2459: $result = $this->processExprNode($expr->left, $scope, $nodeCallback, $context->enterDeep());
2460: $scope = $result->getScope();
2461: $hasYield = $result->hasYield();
2462: $throwPoints = $result->getThrowPoints();
2463: $result = $this->processExprNode($expr->right, $scope, $nodeCallback, $context->enterDeep());
2464: if (
2465: ($expr instanceof BinaryOp\Div || $expr instanceof BinaryOp\Mod) &&
2466: !$scope->getType($expr->right)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no()
2467: ) {
2468: $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false);
2469: }
2470: $scope = $result->getScope();
2471: $hasYield = $hasYield || $result->hasYield();
2472: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2473: } elseif ($expr instanceof Expr\Include_) {
2474: $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep());
2475: $throwPoints = $result->getThrowPoints();
2476: $throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
2477: $hasYield = $result->hasYield();
2478: $scope = $result->getScope();
2479: } elseif (
2480: $expr instanceof Expr\BitwiseNot
2481: || $expr instanceof Cast
2482: || $expr instanceof Expr\Clone_
2483: || $expr instanceof Expr\Print_
2484: || $expr instanceof Expr\UnaryMinus
2485: || $expr instanceof Expr\UnaryPlus
2486: ) {
2487: $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep());
2488: $throwPoints = $result->getThrowPoints();
2489: $hasYield = $result->hasYield();
2490:
2491: $scope = $result->getScope();
2492: } elseif ($expr instanceof Expr\Eval_) {
2493: $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep());
2494: $throwPoints = $result->getThrowPoints();
2495: $throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
2496: $hasYield = $result->hasYield();
2497:
2498: $scope = $result->getScope();
2499: } elseif ($expr instanceof Expr\YieldFrom) {
2500: $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep());
2501: $throwPoints = $result->getThrowPoints();
2502: $throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
2503: $hasYield = true;
2504:
2505: $scope = $result->getScope();
2506: } elseif ($expr instanceof BooleanNot) {
2507: $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep());
2508: $scope = $result->getScope();
2509: $hasYield = $result->hasYield();
2510: $throwPoints = $result->getThrowPoints();
2511: } elseif ($expr instanceof Expr\ClassConstFetch) {
2512: $hasYield = false;
2513: $throwPoints = [];
2514: if ($expr->class instanceof Expr) {
2515: $result = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep());
2516: $scope = $result->getScope();
2517: $hasYield = $result->hasYield();
2518: $throwPoints = $result->getThrowPoints();
2519: }
2520: } elseif ($expr instanceof Expr\Empty_) {
2521: $nonNullabilityResult = $this->ensureNonNullability($scope, $expr->expr);
2522: $scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $expr->expr);
2523: $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep());
2524: $scope = $result->getScope();
2525: $hasYield = $result->hasYield();
2526: $throwPoints = $result->getThrowPoints();
2527: $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions());
2528: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $expr->expr);
2529: } elseif ($expr instanceof Expr\Isset_) {
2530: $hasYield = false;
2531: $throwPoints = [];
2532: $nonNullabilityResults = [];
2533: foreach ($expr->vars as $var) {
2534: $nonNullabilityResult = $this->ensureNonNullability($scope, $var);
2535: $scope = $this->lookForSetAllowedUndefinedExpressions($nonNullabilityResult->getScope(), $var);
2536: $result = $this->processExprNode($var, $scope, $nodeCallback, $context->enterDeep());
2537: $scope = $result->getScope();
2538: $hasYield = $hasYield || $result->hasYield();
2539: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2540: $nonNullabilityResults[] = $nonNullabilityResult;
2541: }
2542: foreach (array_reverse($expr->vars) as $var) {
2543: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
2544: }
2545: foreach (array_reverse($nonNullabilityResults) as $nonNullabilityResult) {
2546: $scope = $this->revertNonNullability($scope, $nonNullabilityResult->getSpecifiedExpressions());
2547: }
2548: } elseif ($expr instanceof Instanceof_) {
2549: $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, $context->enterDeep());
2550: $scope = $result->getScope();
2551: $hasYield = $result->hasYield();
2552: $throwPoints = $result->getThrowPoints();
2553: if ($expr->class instanceof Expr) {
2554: $result = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep());
2555: $scope = $result->getScope();
2556: $hasYield = $hasYield || $result->hasYield();
2557: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2558: }
2559: } elseif ($expr instanceof List_) {
2560: // only in assign and foreach, processed elsewhere
2561: return new ExpressionResult($scope, false, []);
2562: } elseif ($expr instanceof New_) {
2563: $parametersAcceptor = null;
2564: $constructorReflection = null;
2565: $hasYield = false;
2566: $throwPoints = [];
2567: if ($expr->class instanceof Expr) {
2568: $objectClasses = $scope->getType($expr)->getObjectClassNames();
2569: if (count($objectClasses) === 1) {
2570: $objectExprResult = $this->processExprNode(new New_(new Name($objectClasses[0])), $scope, static function (): void {
2571: }, $context->enterDeep());
2572: $additionalThrowPoints = $objectExprResult->getThrowPoints();
2573: } else {
2574: $additionalThrowPoints = [ThrowPoint::createImplicit($scope, $expr)];
2575: }
2576:
2577: $result = $this->processExprNode($expr->class, $scope, $nodeCallback, $context->enterDeep());
2578: $scope = $result->getScope();
2579: $hasYield = $result->hasYield();
2580: $throwPoints = $result->getThrowPoints();
2581: foreach ($additionalThrowPoints as $throwPoint) {
2582: $throwPoints[] = $throwPoint;
2583: }
2584: } elseif ($expr->class instanceof Class_) {
2585: $this->reflectionProvider->getAnonymousClassReflection($expr->class, $scope); // populates $expr->class->name
2586: $this->processStmtNode($expr->class, $scope, $nodeCallback, StatementContext::createTopLevel());
2587: } else {
2588: $className = $scope->resolveName($expr->class);
2589: if ($this->reflectionProvider->hasClass($className)) {
2590: $classReflection = $this->reflectionProvider->getClass($className);
2591: if ($classReflection->hasConstructor()) {
2592: $constructorReflection = $classReflection->getConstructor();
2593: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
2594: $scope,
2595: $expr->getArgs(),
2596: $constructorReflection->getVariants(),
2597: );
2598: $hasSideEffects = $constructorReflection->hasSideEffects();
2599: if ($hasSideEffects->yes()) {
2600: foreach ($expr->getArgs() as $arg) {
2601: $scope = $scope->invalidateExpression($arg->value, true);
2602: }
2603: }
2604: $constructorThrowPoint = $this->getConstructorThrowPoint($constructorReflection, $parametersAcceptor, $classReflection, $expr, $expr->class, $expr->getArgs(), $scope);
2605: if ($constructorThrowPoint !== null) {
2606: $throwPoints[] = $constructorThrowPoint;
2607: }
2608: }
2609: } else {
2610: $throwPoints[] = ThrowPoint::createImplicit($scope, $expr);
2611: }
2612: }
2613:
2614: if ($parametersAcceptor !== null) {
2615: $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr;
2616: }
2617: $result = $this->processArgs($constructorReflection, $parametersAcceptor, $expr->getArgs(), $scope, $nodeCallback, $context);
2618: $scope = $result->getScope();
2619: $hasYield = $hasYield || $result->hasYield();
2620: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
2621: } elseif (
2622: $expr instanceof Expr\PreInc
2623: || $expr instanceof Expr\PostInc
2624: || $expr instanceof Expr\PreDec
2625: || $expr instanceof Expr\PostDec
2626: ) {
2627: $result = $this->processExprNode($expr->var, $scope, $nodeCallback, $context->enterDeep());
2628: $scope = $result->getScope();
2629: $hasYield = $result->hasYield();
2630: $throwPoints = [];
2631:
2632: $newExpr = $expr;
2633: if ($expr instanceof Expr\PostInc) {
2634: $newExpr = new Expr\PreInc($expr->var);
2635: } elseif ($expr instanceof Expr\PostDec) {
2636: $newExpr = new Expr\PreDec($expr->var);
2637: }
2638:
2639: $scope = $this->processAssignVar(
2640: $scope,
2641: $expr->var,
2642: $newExpr,
2643: static function (Node $node, Scope $scope) use ($nodeCallback): void {
2644: if (!$node instanceof PropertyAssignNode) {
2645: return;
2646: }
2647:
2648: $nodeCallback($node, $scope);
2649: },
2650: $context,
2651: static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, []),
2652: false,
2653: )->getScope();
2654: } elseif ($expr instanceof Ternary) {
2655: $ternaryCondResult = $this->processExprNode($expr->cond, $scope, $nodeCallback, $context->enterDeep());
2656: $throwPoints = $ternaryCondResult->getThrowPoints();
2657: $ifTrueScope = $ternaryCondResult->getTruthyScope();
2658: $ifFalseScope = $ternaryCondResult->getFalseyScope();
2659: $ifTrueType = null;
2660: if ($expr->if !== null) {
2661: $ifResult = $this->processExprNode($expr->if, $ifTrueScope, $nodeCallback, $context);
2662: $throwPoints = array_merge($throwPoints, $ifResult->getThrowPoints());
2663: $ifTrueScope = $ifResult->getScope();
2664: $ifTrueType = $ifTrueScope->getType($expr->if);
2665: }
2666:
2667: $elseResult = $this->processExprNode($expr->else, $ifFalseScope, $nodeCallback, $context);
2668: $throwPoints = array_merge($throwPoints, $elseResult->getThrowPoints());
2669: $ifFalseScope = $elseResult->getScope();
2670:
2671: if ($ifTrueType instanceof NeverType && $ifTrueType->isExplicit()) {
2672: $finalScope = $ifFalseScope;
2673: } else {
2674: $ifFalseType = $ifFalseScope->getType($expr->else);
2675:
2676: if ($ifFalseType instanceof NeverType && $ifFalseType->isExplicit()) {
2677: $finalScope = $ifTrueScope;
2678: } else {
2679: $finalScope = $ifTrueScope->mergeWith($ifFalseScope);
2680: }
2681: }
2682:
2683: return new ExpressionResult(
2684: $finalScope,
2685: $ternaryCondResult->hasYield(),
2686: $throwPoints,
2687: static fn (): MutatingScope => $finalScope->filterByTruthyValue($expr),
2688: static fn (): MutatingScope => $finalScope->filterByFalseyValue($expr),
2689: );
2690:
2691: } elseif ($expr instanceof Expr\Yield_) {
2692: $throwPoints = [
2693: ThrowPoint::createImplicit($scope, $expr),
2694: ];
2695: if ($expr->key !== null) {
2696: $keyResult = $this->processExprNode($expr->key, $scope, $nodeCallback, $context->enterDeep());
2697: $scope = $keyResult->getScope();
2698: $throwPoints = $keyResult->getThrowPoints();
2699: }
2700: if ($expr->value !== null) {
2701: $valueResult = $this->processExprNode($expr->value, $scope, $nodeCallback, $context->enterDeep());
2702: $scope = $valueResult->getScope();
2703: $throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints());
2704: }
2705: $hasYield = true;
2706: } elseif ($expr instanceof Expr\Match_) {
2707: $deepContext = $context->enterDeep();
2708: $condResult = $this->processExprNode($expr->cond, $scope, $nodeCallback, $deepContext);
2709: $scope = $condResult->getScope();
2710: $hasYield = $condResult->hasYield();
2711: $throwPoints = $condResult->getThrowPoints();
2712: $matchScope = $scope;
2713: $armNodes = [];
2714: $hasDefaultCond = false;
2715: $hasAlwaysTrueCond = false;
2716: foreach ($expr->arms as $arm) {
2717: if ($arm->conds === null) {
2718: $hasDefaultCond = true;
2719: $matchArmBody = new MatchExpressionArmBody($matchScope, $arm->body);
2720: $armNodes[] = new MatchExpressionArm($matchArmBody, [], $arm->getLine());
2721: $armResult = $this->processExprNode($arm->body, $matchScope, $nodeCallback, ExpressionContext::createTopLevel());
2722: $matchScope = $armResult->getScope();
2723: $hasYield = $hasYield || $armResult->hasYield();
2724: $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints());
2725: $scope = $scope->mergeWith($matchScope);
2726: continue;
2727: }
2728:
2729: if (count($arm->conds) === 0) {
2730: throw new ShouldNotHappenException();
2731: }
2732:
2733: $filteringExpr = null;
2734: $armCondScope = $matchScope;
2735: $condNodes = [];
2736: foreach ($arm->conds as $armCond) {
2737: $condNodes[] = new MatchExpressionArmCondition($armCond, $armCondScope, $armCond->getLine());
2738: $armCondResult = $this->processExprNode($armCond, $armCondScope, $nodeCallback, $deepContext);
2739: $hasYield = $hasYield || $armCondResult->hasYield();
2740: $throwPoints = array_merge($throwPoints, $armCondResult->getThrowPoints());
2741: $armCondExpr = new BinaryOp\Identical($expr->cond, $armCond);
2742: $armCondResultScope = $armCondResult->getScope();
2743: $armCondType = $this->treatPhpDocTypesAsCertain ? $armCondResultScope->getType($armCondExpr) : $armCondResultScope->getNativeType($armCondExpr);
2744: if ($armCondType->isTrue()->yes()) {
2745: $hasAlwaysTrueCond = true;
2746: }
2747: $armCondScope = $armCondResult->getScope()->filterByFalseyValue($armCondExpr);
2748: if ($filteringExpr === null) {
2749: $filteringExpr = $armCondExpr;
2750: continue;
2751: }
2752:
2753: $filteringExpr = new BinaryOp\BooleanOr($filteringExpr, $armCondExpr);
2754: }
2755:
2756: $bodyScope = $matchScope->filterByTruthyValue($filteringExpr);
2757: $matchArmBody = new MatchExpressionArmBody($bodyScope, $arm->body);
2758: $armNodes[] = new MatchExpressionArm($matchArmBody, $condNodes, $arm->getLine());
2759:
2760: $armResult = $this->processExprNode(
2761: $arm->body,
2762: $bodyScope,
2763: $nodeCallback,
2764: ExpressionContext::createTopLevel(),
2765: );
2766: $armScope = $armResult->getScope();
2767: $scope = $scope->mergeWith($armScope);
2768: $hasYield = $hasYield || $armResult->hasYield();
2769: $throwPoints = array_merge($throwPoints, $armResult->getThrowPoints());
2770: $matchScope = $matchScope->filterByFalseyValue($filteringExpr);
2771: }
2772:
2773: $remainingType = $matchScope->getType($expr->cond);
2774: if (!$hasDefaultCond && !$hasAlwaysTrueCond && !$remainingType instanceof NeverType) {
2775: $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(UnhandledMatchError::class), $expr, false);
2776: }
2777:
2778: $nodeCallback(new MatchExpressionNode($expr->cond, $armNodes, $expr, $matchScope), $scope);
2779: } elseif ($expr instanceof Expr\Throw_) {
2780: $hasYield = false;
2781: $result = $this->processExprNode($expr->expr, $scope, $nodeCallback, ExpressionContext::createDeep());
2782: $throwPoints = $result->getThrowPoints();
2783: $throwPoints[] = ThrowPoint::createExplicit($scope, $scope->getType($expr->expr), $expr, false);
2784: } elseif ($expr instanceof FunctionCallableNode) {
2785: $throwPoints = [];
2786: $hasYield = false;
2787: if ($expr->getName() instanceof Expr) {
2788: $result = $this->processExprNode($expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep());
2789: $scope = $result->getScope();
2790: $hasYield = $result->hasYield();
2791: $throwPoints = $result->getThrowPoints();
2792: }
2793: } elseif ($expr instanceof MethodCallableNode) {
2794: $result = $this->processExprNode($expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep());
2795: $scope = $result->getScope();
2796: $hasYield = $result->hasYield();
2797: $throwPoints = $result->getThrowPoints();
2798: if ($expr->getName() instanceof Expr) {
2799: $nameResult = $this->processExprNode($expr->getVar(), $scope, $nodeCallback, ExpressionContext::createDeep());
2800: $scope = $nameResult->getScope();
2801: $hasYield = $hasYield || $nameResult->hasYield();
2802: $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints());
2803: }
2804: } elseif ($expr instanceof StaticMethodCallableNode) {
2805: $throwPoints = [];
2806: $hasYield = false;
2807: if ($expr->getClass() instanceof Expr) {
2808: $classResult = $this->processExprNode($expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep());
2809: $scope = $classResult->getScope();
2810: $hasYield = $classResult->hasYield();
2811: $throwPoints = $classResult->getThrowPoints();
2812: }
2813: if ($expr->getName() instanceof Expr) {
2814: $nameResult = $this->processExprNode($expr->getName(), $scope, $nodeCallback, ExpressionContext::createDeep());
2815: $scope = $nameResult->getScope();
2816: $hasYield = $hasYield || $nameResult->hasYield();
2817: $throwPoints = array_merge($throwPoints, $nameResult->getThrowPoints());
2818: }
2819: } elseif ($expr instanceof InstantiationCallableNode) {
2820: $throwPoints = [];
2821: $hasYield = false;
2822: if ($expr->getClass() instanceof Expr) {
2823: $classResult = $this->processExprNode($expr->getClass(), $scope, $nodeCallback, ExpressionContext::createDeep());
2824: $scope = $classResult->getScope();
2825: $hasYield = $classResult->hasYield();
2826: $throwPoints = $classResult->getThrowPoints();
2827: }
2828: } elseif ($expr instanceof Node\Scalar) {
2829: $hasYield = false;
2830: $throwPoints = [];
2831: } else {
2832: $hasYield = false;
2833: $throwPoints = [];
2834: }
2835:
2836: return new ExpressionResult(
2837: $scope,
2838: $hasYield,
2839: $throwPoints,
2840: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
2841: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
2842: );
2843: }
2844:
2845: private function getFunctionThrowPoint(
2846: FunctionReflection $functionReflection,
2847: ?ParametersAcceptor $parametersAcceptor,
2848: FuncCall $funcCall,
2849: MutatingScope $scope,
2850: ): ?ThrowPoint
2851: {
2852: $normalizedFuncCall = $funcCall;
2853: if ($parametersAcceptor !== null) {
2854: $normalizedFuncCall = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $funcCall);
2855: }
2856:
2857: if ($normalizedFuncCall !== null) {
2858: foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicFunctionThrowTypeExtensions() as $extension) {
2859: if (!$extension->isFunctionSupported($functionReflection)) {
2860: continue;
2861: }
2862:
2863: $throwType = $extension->getThrowTypeFromFunctionCall($functionReflection, $normalizedFuncCall, $scope);
2864: if ($throwType === null) {
2865: return null;
2866: }
2867:
2868: return ThrowPoint::createExplicit($scope, $throwType, $funcCall, false);
2869: }
2870: }
2871:
2872: $throwType = $functionReflection->getThrowType();
2873: if ($throwType === null && $parametersAcceptor !== null) {
2874: $returnType = $parametersAcceptor->getReturnType();
2875: if ($returnType instanceof NeverType && $returnType->isExplicit()) {
2876: $throwType = new ObjectType(Throwable::class);
2877: }
2878: }
2879:
2880: if ($throwType !== null) {
2881: if (!$throwType->isVoid()->yes()) {
2882: return ThrowPoint::createExplicit($scope, $throwType, $funcCall, true);
2883: }
2884: } elseif ($this->implicitThrows) {
2885: $requiredParameters = null;
2886: if ($parametersAcceptor !== null) {
2887: $requiredParameters = 0;
2888: foreach ($parametersAcceptor->getParameters() as $parameter) {
2889: if ($parameter->isOptional()) {
2890: continue;
2891: }
2892:
2893: $requiredParameters++;
2894: }
2895: }
2896: if (
2897: !$functionReflection->isBuiltin()
2898: || $requiredParameters === null
2899: || $requiredParameters > 0
2900: || count($funcCall->getArgs()) > 0
2901: ) {
2902: $functionReturnedType = $scope->getType($funcCall);
2903: if (!(new ObjectType(Throwable::class))->isSuperTypeOf($functionReturnedType)->yes()) {
2904: return ThrowPoint::createImplicit($scope, $funcCall);
2905: }
2906: }
2907: }
2908:
2909: return null;
2910: }
2911:
2912: private function getMethodThrowPoint(MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, MethodCall $methodCall, MutatingScope $scope): ?ThrowPoint
2913: {
2914: $normalizedMethodCall = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $methodCall);
2915: if ($normalizedMethodCall !== null) {
2916: foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicMethodThrowTypeExtensions() as $extension) {
2917: if (!$extension->isMethodSupported($methodReflection)) {
2918: continue;
2919: }
2920:
2921: $throwType = $extension->getThrowTypeFromMethodCall($methodReflection, $normalizedMethodCall, $scope);
2922: if ($throwType === null) {
2923: return null;
2924: }
2925:
2926: return ThrowPoint::createExplicit($scope, $throwType, $methodCall, false);
2927: }
2928: }
2929:
2930: $throwType = $methodReflection->getThrowType();
2931: if ($throwType === null) {
2932: $returnType = $parametersAcceptor->getReturnType();
2933: if ($returnType instanceof NeverType && $returnType->isExplicit()) {
2934: $throwType = new ObjectType(Throwable::class);
2935: }
2936: }
2937:
2938: if ($throwType !== null) {
2939: if (!$throwType->isVoid()->yes()) {
2940: return ThrowPoint::createExplicit($scope, $throwType, $methodCall, true);
2941: }
2942: } elseif ($this->implicitThrows) {
2943: $methodReturnedType = $scope->getType($methodCall);
2944: if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
2945: return ThrowPoint::createImplicit($scope, $methodCall);
2946: }
2947: }
2948:
2949: return null;
2950: }
2951:
2952: /**
2953: * @param Node\Arg[] $args
2954: */
2955: private function getConstructorThrowPoint(MethodReflection $constructorReflection, ParametersAcceptor $parametersAcceptor, ClassReflection $classReflection, New_ $new, Name $className, array $args, MutatingScope $scope): ?ThrowPoint
2956: {
2957: $methodCall = new StaticCall($className, $constructorReflection->getName(), $args);
2958: $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall);
2959: if ($normalizedMethodCall !== null) {
2960: foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) {
2961: if (!$extension->isStaticMethodSupported($constructorReflection)) {
2962: continue;
2963: }
2964:
2965: $throwType = $extension->getThrowTypeFromStaticMethodCall($constructorReflection, $normalizedMethodCall, $scope);
2966: if ($throwType === null) {
2967: return null;
2968: }
2969:
2970: return ThrowPoint::createExplicit($scope, $throwType, $new, false);
2971: }
2972: }
2973:
2974: if ($constructorReflection->getThrowType() !== null) {
2975: $throwType = $constructorReflection->getThrowType();
2976: if (!$throwType->isVoid()->yes()) {
2977: return ThrowPoint::createExplicit($scope, $throwType, $new, true);
2978: }
2979: } elseif ($this->implicitThrows) {
2980: if ($classReflection->getName() !== Throwable::class && !$classReflection->isSubclassOf(Throwable::class)) {
2981: return ThrowPoint::createImplicit($scope, $methodCall);
2982: }
2983: }
2984:
2985: return null;
2986: }
2987:
2988: private function getStaticMethodThrowPoint(MethodReflection $methodReflection, ParametersAcceptor $parametersAcceptor, StaticCall $methodCall, MutatingScope $scope): ?ThrowPoint
2989: {
2990: $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall);
2991: if ($normalizedMethodCall !== null) {
2992: foreach ($this->dynamicThrowTypeExtensionProvider->getDynamicStaticMethodThrowTypeExtensions() as $extension) {
2993: if (!$extension->isStaticMethodSupported($methodReflection)) {
2994: continue;
2995: }
2996:
2997: $throwType = $extension->getThrowTypeFromStaticMethodCall($methodReflection, $normalizedMethodCall, $scope);
2998: if ($throwType === null) {
2999: return null;
3000: }
3001:
3002: return ThrowPoint::createExplicit($scope, $throwType, $methodCall, false);
3003: }
3004: }
3005:
3006: if ($methodReflection->getThrowType() !== null) {
3007: $throwType = $methodReflection->getThrowType();
3008: if (!$throwType->isVoid()->yes()) {
3009: return ThrowPoint::createExplicit($scope, $throwType, $methodCall, true);
3010: }
3011: } elseif ($this->implicitThrows) {
3012: $methodReturnedType = $scope->getType($methodCall);
3013: if (!(new ObjectType(Throwable::class))->isSuperTypeOf($methodReturnedType)->yes()) {
3014: return ThrowPoint::createImplicit($scope, $methodCall);
3015: }
3016: }
3017:
3018: return null;
3019: }
3020:
3021: /**
3022: * @return string[]
3023: */
3024: private function getAssignedVariables(Expr $expr): array
3025: {
3026: if ($expr instanceof Expr\Variable) {
3027: if (is_string($expr->name)) {
3028: return [$expr->name];
3029: }
3030:
3031: return [];
3032: }
3033:
3034: if ($expr instanceof Expr\List_ || $expr instanceof Expr\Array_) {
3035: $names = [];
3036: foreach ($expr->items as $item) {
3037: if ($item === null) {
3038: continue;
3039: }
3040:
3041: $names = array_merge($names, $this->getAssignedVariables($item->value));
3042: }
3043:
3044: return $names;
3045: }
3046:
3047: if ($expr instanceof ArrayDimFetch) {
3048: return $this->getAssignedVariables($expr->var);
3049: }
3050:
3051: return [];
3052: }
3053:
3054: /**
3055: * @param callable(Node $node, Scope $scope): void $nodeCallback
3056: */
3057: private function callNodeCallbackWithExpression(
3058: callable $nodeCallback,
3059: Expr $expr,
3060: MutatingScope $scope,
3061: ExpressionContext $context,
3062: ): void
3063: {
3064: if ($context->isDeep()) {
3065: $scope = $scope->exitFirstLevelStatements();
3066: }
3067: $nodeCallback($expr, $scope);
3068: }
3069:
3070: /**
3071: * @param callable(Node $node, Scope $scope): void $nodeCallback
3072: */
3073: private function processClosureNode(
3074: Expr\Closure $expr,
3075: MutatingScope $scope,
3076: callable $nodeCallback,
3077: ExpressionContext $context,
3078: ?Type $passedToType,
3079: ): ExpressionResult
3080: {
3081: foreach ($expr->params as $param) {
3082: $this->processParamNode($param, $scope, $nodeCallback);
3083: }
3084:
3085: $byRefUses = [];
3086:
3087: $callableParameters = null;
3088: $closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME);
3089:
3090: if ($closureCallArgs !== null) {
3091: $acceptors = $scope->getType($expr)->getCallableParametersAcceptors($scope);
3092: if (count($acceptors) === 1) {
3093: $callableParameters = $acceptors[0]->getParameters();
3094:
3095: foreach ($callableParameters as $index => $callableParameter) {
3096: if (!isset($closureCallArgs[$index])) {
3097: continue;
3098: }
3099:
3100: $type = $scope->getType($closureCallArgs[$index]->value);
3101: $callableParameters[$index] = new NativeParameterReflection(
3102: $callableParameter->getName(),
3103: $callableParameter->isOptional(),
3104: $type,
3105: $callableParameter->passedByReference(),
3106: $callableParameter->isVariadic(),
3107: $callableParameter->getDefaultValue(),
3108: );
3109: }
3110: }
3111: } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) {
3112: if ($passedToType instanceof UnionType) {
3113: $passedToType = TypeCombinator::union(...array_filter(
3114: $passedToType->getTypes(),
3115: static fn (Type $type) => $type->isCallable()->yes(),
3116: ));
3117: }
3118:
3119: $acceptors = $passedToType->getCallableParametersAcceptors($scope);
3120: if (count($acceptors) === 1) {
3121: $callableParameters = $acceptors[0]->getParameters();
3122: }
3123: }
3124:
3125: $useScope = $scope;
3126: foreach ($expr->uses as $use) {
3127: if ($use->byRef) {
3128: $byRefUses[] = $use;
3129: $useScope = $useScope->enterExpressionAssign($use->var);
3130:
3131: $inAssignRightSideVariableName = $context->getInAssignRightSideVariableName();
3132: $inAssignRightSideType = $context->getInAssignRightSideType();
3133: $inAssignRightSideNativeType = $context->getInAssignRightSideNativeType();
3134: if (
3135: $inAssignRightSideVariableName === $use->var->name
3136: && $inAssignRightSideType !== null
3137: && $inAssignRightSideNativeType !== null
3138: ) {
3139: if ($inAssignRightSideType instanceof ClosureType) {
3140: $variableType = $inAssignRightSideType;
3141: } else {
3142: $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName);
3143: if ($alreadyHasVariableType->no()) {
3144: $variableType = TypeCombinator::union(new NullType(), $inAssignRightSideType);
3145: } else {
3146: $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType);
3147: }
3148: }
3149: if ($inAssignRightSideNativeType instanceof ClosureType) {
3150: $variableNativeType = $inAssignRightSideNativeType;
3151: } else {
3152: $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName);
3153: if ($alreadyHasVariableType->no()) {
3154: $variableNativeType = TypeCombinator::union(new NullType(), $inAssignRightSideNativeType);
3155: } else {
3156: $variableNativeType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideNativeType);
3157: }
3158: }
3159: $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType, $variableNativeType);
3160: }
3161: }
3162: $this->processExprNode($use, $useScope, $nodeCallback, $context);
3163: if (!$use->byRef) {
3164: continue;
3165: }
3166:
3167: $useScope = $useScope->exitExpressionAssign($use->var);
3168: }
3169:
3170: if ($expr->returnType !== null) {
3171: $nodeCallback($expr->returnType, $scope);
3172: }
3173:
3174: $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters);
3175: $closureScope = $closureScope->processClosureScope($scope, null, $byRefUses);
3176: $closureType = $closureScope->getAnonymousFunctionReflection();
3177: if (!$closureType instanceof ClosureType) {
3178: throw new ShouldNotHappenException();
3179: }
3180:
3181: $nodeCallback(new InClosureNode($closureType, $expr), $closureScope);
3182:
3183: $executionEnds = [];
3184: $gatheredReturnStatements = [];
3185: $gatheredYieldStatements = [];
3186: $closureStmtsCallback = static function (Node $node, Scope $scope) use ($nodeCallback, &$executionEnds, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope): void {
3187: $nodeCallback($node, $scope);
3188: if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) {
3189: return;
3190: }
3191: if ($node instanceof ExecutionEndNode) {
3192: $executionEnds[] = $node;
3193: return;
3194: }
3195: if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) {
3196: $gatheredYieldStatements[] = $node;
3197: }
3198: if (!$node instanceof Return_) {
3199: return;
3200: }
3201:
3202: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
3203: };
3204: if (count($byRefUses) === 0) {
3205: $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel());
3206: $nodeCallback(new ClosureReturnStatementsNode(
3207: $expr,
3208: $gatheredReturnStatements,
3209: $gatheredYieldStatements,
3210: $statementResult,
3211: $executionEnds,
3212: ), $closureScope);
3213:
3214: return new ExpressionResult($scope, false, []);
3215: }
3216:
3217: $count = 0;
3218: do {
3219: $prevScope = $closureScope;
3220:
3221: $intermediaryClosureScopeResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, static function (): void {
3222: }, StatementContext::createTopLevel());
3223: $intermediaryClosureScope = $intermediaryClosureScopeResult->getScope();
3224: foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) {
3225: $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope());
3226: }
3227: $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters);
3228: $closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses);
3229: if ($closureScope->equals($prevScope)) {
3230: break;
3231: }
3232: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
3233: $closureScope = $prevScope->generalizeWith($closureScope);
3234: }
3235: $count++;
3236: } while ($count < self::LOOP_SCOPE_ITERATIONS);
3237:
3238: $statementResult = $this->processStmtNodes($expr, $expr->stmts, $closureScope, $closureStmtsCallback, StatementContext::createTopLevel());
3239: $nodeCallback(new ClosureReturnStatementsNode(
3240: $expr,
3241: $gatheredReturnStatements,
3242: $gatheredYieldStatements,
3243: $statementResult,
3244: $executionEnds,
3245: ), $closureScope);
3246:
3247: return new ExpressionResult($scope->processClosureScope($closureScope, null, $byRefUses), false, []);
3248: }
3249:
3250: /**
3251: * @param callable(Node $node, Scope $scope): void $nodeCallback
3252: */
3253: private function processArrowFunctionNode(
3254: Expr\ArrowFunction $expr,
3255: MutatingScope $scope,
3256: callable $nodeCallback,
3257: ExpressionContext $context,
3258: ?Type $passedToType,
3259: ): ExpressionResult
3260: {
3261: foreach ($expr->params as $param) {
3262: $this->processParamNode($param, $scope, $nodeCallback);
3263: }
3264: if ($expr->returnType !== null) {
3265: $nodeCallback($expr->returnType, $scope);
3266: }
3267:
3268: $callableParameters = null;
3269: $arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME);
3270:
3271: if ($arrowFunctionCallArgs !== null) {
3272: $acceptors = $scope->getType($expr)->getCallableParametersAcceptors($scope);
3273: if (count($acceptors) === 1) {
3274: $callableParameters = $acceptors[0]->getParameters();
3275:
3276: foreach ($callableParameters as $index => $callableParameter) {
3277: if (!isset($arrowFunctionCallArgs[$index])) {
3278: continue;
3279: }
3280:
3281: $type = $scope->getType($arrowFunctionCallArgs[$index]->value);
3282: $callableParameters[$index] = new NativeParameterReflection(
3283: $callableParameter->getName(),
3284: $callableParameter->isOptional(),
3285: $type,
3286: $callableParameter->passedByReference(),
3287: $callableParameter->isVariadic(),
3288: $callableParameter->getDefaultValue(),
3289: );
3290: }
3291: }
3292: } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) {
3293: if ($passedToType instanceof UnionType) {
3294: $passedToType = TypeCombinator::union(...array_filter(
3295: $passedToType->getTypes(),
3296: static fn (Type $type) => $type->isCallable()->yes(),
3297: ));
3298: }
3299:
3300: $acceptors = $passedToType->getCallableParametersAcceptors($scope);
3301: if (count($acceptors) === 1) {
3302: $callableParameters = $acceptors[0]->getParameters();
3303: }
3304: }
3305:
3306: $arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters);
3307: $arrowFunctionType = $arrowFunctionScope->getAnonymousFunctionReflection();
3308: if (!$arrowFunctionType instanceof ClosureType) {
3309: throw new ShouldNotHappenException();
3310: }
3311: $nodeCallback(new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope);
3312: $this->processExprNode($expr->expr, $arrowFunctionScope, $nodeCallback, ExpressionContext::createTopLevel());
3313:
3314: return new ExpressionResult($scope, false, []);
3315: }
3316:
3317: /**
3318: * @param callable(Node $node, Scope $scope): void $nodeCallback
3319: */
3320: private function processParamNode(
3321: Node\Param $param,
3322: MutatingScope $scope,
3323: callable $nodeCallback,
3324: ): void
3325: {
3326: $this->processAttributeGroups($param->attrGroups, $scope, $nodeCallback);
3327: $nodeCallback($param, $scope);
3328: if ($param->type !== null) {
3329: $nodeCallback($param->type, $scope);
3330: }
3331: if ($param->default === null) {
3332: return;
3333: }
3334:
3335: $this->processExprNode($param->default, $scope, $nodeCallback, ExpressionContext::createDeep());
3336: }
3337:
3338: /**
3339: * @param AttributeGroup[] $attrGroups
3340: * @param callable(Node $node, Scope $scope): void $nodeCallback
3341: */
3342: private function processAttributeGroups(
3343: array $attrGroups,
3344: MutatingScope $scope,
3345: callable $nodeCallback,
3346: ): void
3347: {
3348: foreach ($attrGroups as $attrGroup) {
3349: foreach ($attrGroup->attrs as $attr) {
3350: foreach ($attr->args as $arg) {
3351: $this->processExprNode($arg->value, $scope, $nodeCallback, ExpressionContext::createDeep());
3352: $nodeCallback($arg, $scope);
3353: }
3354: $nodeCallback($attr, $scope);
3355: }
3356: $nodeCallback($attrGroup, $scope);
3357: }
3358: }
3359:
3360: /**
3361: * @param MethodReflection|FunctionReflection|null $calleeReflection
3362: * @param Node\Arg[] $args
3363: * @param callable(Node $node, Scope $scope): void $nodeCallback
3364: */
3365: private function processArgs(
3366: $calleeReflection,
3367: ?ParametersAcceptor $parametersAcceptor,
3368: array $args,
3369: MutatingScope $scope,
3370: callable $nodeCallback,
3371: ExpressionContext $context,
3372: ?MutatingScope $closureBindScope = null,
3373: ): ExpressionResult
3374: {
3375: $paramOutTypes = [];
3376: if ($parametersAcceptor !== null) {
3377: $parameters = $parametersAcceptor->getParameters();
3378:
3379: foreach ($parameters as $parameter) {
3380: if (!$parameter instanceof ParameterReflectionWithPhpDocs) {
3381: continue;
3382: }
3383:
3384: if ($parameter->getOutType() === null) {
3385: continue;
3386: }
3387:
3388: $paramOutTypes[$parameter->getName()] = TemplateTypeHelper::resolveTemplateTypes($parameter->getOutType(), $parametersAcceptor->getResolvedTemplateTypeMap());
3389: }
3390: }
3391:
3392: if ($calleeReflection !== null) {
3393: $scope = $scope->pushInFunctionCall($calleeReflection);
3394: }
3395:
3396: $hasYield = false;
3397: $throwPoints = [];
3398: foreach ($args as $i => $arg) {
3399: $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg;
3400: $nodeCallback($originalArg, $scope);
3401: if (isset($parameters) && $parametersAcceptor !== null) {
3402: $byRefType = new MixedType();
3403: $assignByReference = false;
3404: if (isset($parameters[$i])) {
3405: $assignByReference = $parameters[$i]->passedByReference()->createsNewVariable();
3406: $parameterType = $parameters[$i]->getType();
3407:
3408: if (isset($paramOutTypes[$parameters[$i]->getName()])) {
3409: $byRefType = $paramOutTypes[$parameters[$i]->getName()];
3410: }
3411: } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) {
3412: $lastParameter = $parameters[count($parameters) - 1];
3413: $assignByReference = $lastParameter->passedByReference()->createsNewVariable();
3414: $parameterType = $lastParameter->getType();
3415:
3416: if (isset($paramOutTypes[$lastParameter->getName()])) {
3417: $byRefType = $paramOutTypes[$lastParameter->getName()];
3418: }
3419: }
3420:
3421: if ($assignByReference) {
3422: $argValue = $arg->value;
3423: if ($argValue instanceof Variable && is_string($argValue->name)) {
3424: $scope = $scope->assignVariable($argValue->name, $byRefType, new MixedType());
3425: }
3426: }
3427: }
3428:
3429: $originalScope = $scope;
3430: $scopeToPass = $scope;
3431: if ($i === 0 && $closureBindScope !== null) {
3432: $scopeToPass = $closureBindScope;
3433: }
3434:
3435: if ($arg->value instanceof Expr\Closure) {
3436: $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context);
3437: $result = $this->processClosureNode($arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null);
3438: } elseif ($arg->value instanceof Expr\ArrowFunction) {
3439: $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $context);
3440: $result = $this->processArrowFunctionNode($arg->value, $scopeToPass, $nodeCallback, $context, $parameterType ?? null);
3441: } else {
3442: $result = $this->processExprNode($arg->value, $scopeToPass, $nodeCallback, $context->enterDeep());
3443: }
3444: $scope = $result->getScope();
3445: $hasYield = $hasYield || $result->hasYield();
3446: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3447: if ($i !== 0 || $closureBindScope === null) {
3448: continue;
3449: }
3450:
3451: $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope);
3452: }
3453:
3454: if ($calleeReflection !== null) {
3455: $scope = $scope->popInFunctionCall();
3456: }
3457:
3458: return new ExpressionResult($scope, $hasYield, $throwPoints);
3459: }
3460:
3461: /**
3462: * @param callable(Node $node, Scope $scope): void $nodeCallback
3463: * @param Closure(MutatingScope $scope): ExpressionResult $processExprCallback
3464: */
3465: private function processAssignVar(
3466: MutatingScope $scope,
3467: Expr $var,
3468: Expr $assignedExpr,
3469: callable $nodeCallback,
3470: ExpressionContext $context,
3471: Closure $processExprCallback,
3472: bool $enterExpressionAssign,
3473: ): ExpressionResult
3474: {
3475: $nodeCallback($var, $enterExpressionAssign ? $scope->enterExpressionAssign($var) : $scope);
3476: $hasYield = false;
3477: $throwPoints = [];
3478: $isAssignOp = $assignedExpr instanceof Expr\AssignOp && !$enterExpressionAssign;
3479: if ($var instanceof Variable && is_string($var->name)) {
3480: $result = $processExprCallback($scope);
3481: $hasYield = $result->hasYield();
3482: $throwPoints = $result->getThrowPoints();
3483: $assignedExpr = $this->unwrapAssign($assignedExpr);
3484: $type = $scope->getType($assignedExpr);
3485: $truthySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createTruthy());
3486: $falseySpecifiedTypes = $this->typeSpecifier->specifyTypesInCondition($scope, $assignedExpr, TypeSpecifierContext::createFalsey());
3487:
3488: $conditionalExpressions = [];
3489:
3490: $truthyType = TypeCombinator::removeFalsey($type);
3491: $falseyType = TypeCombinator::intersect($type, StaticTypeFactory::falsey());
3492:
3493: $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType);
3494: $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $truthySpecifiedTypes, $truthyType);
3495: $conditionalExpressions = $this->processSureTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType);
3496: $conditionalExpressions = $this->processSureNotTypesForConditionalExpressionsAfterAssign($scope, $var->name, $conditionalExpressions, $falseySpecifiedTypes, $falseyType);
3497:
3498: // TODO conditional expressions for native type should be handled too
3499: $scope = $result->getScope()->assignVariable($var->name, $type, $scope->getNativeType($assignedExpr));
3500: foreach ($conditionalExpressions as $exprString => $holders) {
3501: $scope = $scope->addConditionalExpressions($exprString, $holders);
3502: }
3503: } elseif ($var instanceof ArrayDimFetch) {
3504: $dimExprStack = [];
3505: $originalVar = $var;
3506: $assignedPropertyExpr = $assignedExpr;
3507: while ($var instanceof ArrayDimFetch) {
3508: $varForSetOffsetValue = $var->var;
3509: if ($varForSetOffsetValue instanceof PropertyFetch || $varForSetOffsetValue instanceof StaticPropertyFetch) {
3510: $varForSetOffsetValue = new OriginalPropertyTypeExpr($varForSetOffsetValue);
3511: }
3512: $assignedPropertyExpr = new SetOffsetValueTypeExpr(
3513: $varForSetOffsetValue,
3514: $var->dim,
3515: $assignedPropertyExpr,
3516: );
3517: $dimExprStack[] = $var->dim;
3518: $var = $var->var;
3519: }
3520:
3521: // 1. eval root expr
3522: if ($enterExpressionAssign) {
3523: $scope = $scope->enterExpressionAssign($var);
3524: }
3525: $result = $this->processExprNode($var, $scope, $nodeCallback, $context->enterDeep());
3526: $hasYield = $result->hasYield();
3527: $throwPoints = $result->getThrowPoints();
3528: $scope = $result->getScope();
3529: if ($enterExpressionAssign) {
3530: $scope = $scope->exitExpressionAssign($var);
3531: }
3532:
3533: // 2. eval dimensions
3534: $offsetTypes = [];
3535: $offsetNativeTypes = [];
3536: foreach (array_reverse($dimExprStack) as $dimExpr) {
3537: if ($dimExpr === null) {
3538: $offsetTypes[] = null;
3539: $offsetNativeTypes[] = null;
3540:
3541: } else {
3542: $offsetTypes[] = $scope->getType($dimExpr);
3543: $offsetNativeTypes[] = $scope->getNativeType($dimExpr);
3544:
3545: if ($enterExpressionAssign) {
3546: $scope->enterExpressionAssign($dimExpr);
3547: }
3548: $result = $this->processExprNode($dimExpr, $scope, $nodeCallback, $context->enterDeep());
3549: $hasYield = $hasYield || $result->hasYield();
3550: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3551: $scope = $result->getScope();
3552:
3553: if ($enterExpressionAssign) {
3554: $scope = $scope->exitExpressionAssign($dimExpr);
3555: }
3556: }
3557: }
3558:
3559: $valueToWrite = $scope->getType($assignedExpr);
3560: $nativeValueToWrite = $scope->getNativeType($assignedExpr);
3561: $originalValueToWrite = $valueToWrite;
3562: $originalNativeValueToWrite = $valueToWrite;
3563:
3564: // 3. eval assigned expr
3565: $result = $processExprCallback($scope);
3566: $hasYield = $hasYield || $result->hasYield();
3567: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3568: $scope = $result->getScope();
3569:
3570: $varType = $scope->getType($var);
3571: $varNativeType = $scope->getNativeType($var);
3572:
3573: // 4. compose types
3574: if ($varType instanceof ErrorType) {
3575: $varType = new ConstantArrayType([], []);
3576: }
3577: if ($varNativeType instanceof ErrorType) {
3578: $varNativeType = new ConstantArrayType([], []);
3579: }
3580: $offsetValueType = $varType;
3581: $offsetNativeValueType = $varNativeType;
3582: $offsetValueTypeStack = [$offsetValueType];
3583: $offsetValueNativeTypeStack = [$offsetNativeValueType];
3584: foreach (array_slice($offsetTypes, 0, -1) as $offsetType) {
3585: if ($offsetType === null) {
3586: $offsetValueType = new ConstantArrayType([], []);
3587:
3588: } else {
3589: $offsetValueType = $offsetValueType->getOffsetValueType($offsetType);
3590: if ($offsetValueType instanceof ErrorType) {
3591: $offsetValueType = new ConstantArrayType([], []);
3592: }
3593: }
3594:
3595: $offsetValueTypeStack[] = $offsetValueType;
3596: }
3597: foreach (array_slice($offsetNativeTypes, 0, -1) as $offsetNativeType) {
3598: if ($offsetNativeType === null) {
3599: $offsetNativeValueType = new ConstantArrayType([], []);
3600:
3601: } else {
3602: $offsetNativeValueType = $offsetNativeValueType->getOffsetValueType($offsetNativeType);
3603: if ($offsetNativeValueType instanceof ErrorType) {
3604: $offsetNativeValueType = new ConstantArrayType([], []);
3605: }
3606: }
3607:
3608: $offsetValueNativeTypeStack[] = $offsetNativeValueType;
3609: }
3610:
3611: foreach (array_reverse($offsetTypes) as $i => $offsetType) {
3612: /** @var Type $offsetValueType */
3613: $offsetValueType = array_pop($offsetValueTypeStack);
3614: if (!$offsetValueType instanceof MixedType) {
3615: $types = [
3616: new ArrayType(new MixedType(), new MixedType()),
3617: new ObjectType(ArrayAccess::class),
3618: new NullType(),
3619: ];
3620: if ($offsetType !== null && (new IntegerType())->isSuperTypeOf($offsetType)->yes()) {
3621: $types[] = new StringType();
3622: }
3623: $offsetValueType = TypeCombinator::intersect($offsetValueType, TypeCombinator::union(...$types));
3624: }
3625: $valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0);
3626: }
3627: foreach (array_reverse($offsetNativeTypes) as $i => $offsetNativeType) {
3628: /** @var Type $offsetNativeValueType */
3629: $offsetNativeValueType = array_pop($offsetValueNativeTypeStack);
3630: if (!$offsetNativeValueType instanceof MixedType) {
3631: $types = [
3632: new ArrayType(new MixedType(), new MixedType()),
3633: new ObjectType(ArrayAccess::class),
3634: new NullType(),
3635: ];
3636: if ($offsetNativeType !== null && (new IntegerType())->isSuperTypeOf($offsetNativeType)->yes()) {
3637: $types[] = new StringType();
3638: }
3639: $offsetNativeValueType = TypeCombinator::intersect($offsetNativeValueType, TypeCombinator::union(...$types));
3640: }
3641: $nativeValueToWrite = $offsetNativeValueType->setOffsetValueType($offsetNativeType, $nativeValueToWrite, $i === 0);
3642: }
3643:
3644: if ($varType->isArray()->yes() || !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->yes()) {
3645: if ($var instanceof Variable && is_string($var->name)) {
3646: $scope = $scope->assignVariable($var->name, $valueToWrite, $nativeValueToWrite);
3647: } else {
3648: if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) {
3649: $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope);
3650: }
3651: $scope = $scope->assignExpression(
3652: $var,
3653: $valueToWrite,
3654: $nativeValueToWrite,
3655: );
3656: }
3657:
3658: if ($originalVar->dim instanceof Variable || $originalVar->dim instanceof Node\Scalar) {
3659: $currentVarType = $scope->getType($originalVar);
3660: if (!$originalValueToWrite->isSuperTypeOf($currentVarType)->yes()) {
3661: $scope = $scope->assignExpression(
3662: $originalVar,
3663: $originalValueToWrite,
3664: $originalNativeValueToWrite,
3665: );
3666: }
3667: }
3668: } else {
3669: if ($var instanceof PropertyFetch || $var instanceof StaticPropertyFetch) {
3670: $nodeCallback(new PropertyAssignNode($var, $assignedPropertyExpr, $isAssignOp), $scope);
3671: }
3672: }
3673:
3674: if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) {
3675: $throwPoints = array_merge($throwPoints, $this->processExprNode(
3676: new MethodCall($var, 'offsetSet'),
3677: $scope,
3678: static function (): void {
3679: },
3680: $context,
3681: )->getThrowPoints());
3682: }
3683: } elseif ($var instanceof PropertyFetch) {
3684: $objectResult = $this->processExprNode($var->var, $scope, $nodeCallback, $context);
3685: $hasYield = $objectResult->hasYield();
3686: $throwPoints = $objectResult->getThrowPoints();
3687: $scope = $objectResult->getScope();
3688:
3689: $propertyName = null;
3690: if ($var->name instanceof Node\Identifier) {
3691: $propertyName = $var->name->name;
3692: } else {
3693: $propertyNameResult = $this->processExprNode($var->name, $scope, $nodeCallback, $context);
3694: $hasYield = $hasYield || $propertyNameResult->hasYield();
3695: $throwPoints = array_merge($throwPoints, $propertyNameResult->getThrowPoints());
3696: $scope = $propertyNameResult->getScope();
3697: }
3698:
3699: $result = $processExprCallback($scope);
3700: $hasYield = $hasYield || $result->hasYield();
3701: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3702: $scope = $result->getScope();
3703:
3704: $propertyHolderType = $scope->getType($var->var);
3705: if ($propertyName !== null && $propertyHolderType->hasProperty($propertyName)->yes()) {
3706: $propertyReflection = $propertyHolderType->getProperty($propertyName, $scope);
3707: $assignedExprType = $scope->getType($assignedExpr);
3708: $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope);
3709: if ($propertyReflection->canChangeTypeAfterAssignment()) {
3710: $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
3711: }
3712: $declaringClass = $propertyReflection->getDeclaringClass();
3713: if (
3714: $declaringClass->hasNativeProperty($propertyName)
3715: && !$declaringClass->getNativeProperty($propertyName)->getNativeType()->accepts($assignedExprType, true)->yes()
3716: ) {
3717: $throwPoints[] = ThrowPoint::createExplicit($scope, new ObjectType(TypeError::class), $assignedExpr, false);
3718: }
3719: } else {
3720: // fallback
3721: $assignedExprType = $scope->getType($assignedExpr);
3722: $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope);
3723: $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
3724: // simulate dynamic property assign by __set to get throw points
3725: if (!$propertyHolderType->hasMethod('__set')->no()) {
3726: $throwPoints = array_merge($throwPoints, $this->processExprNode(
3727: new MethodCall($var->var, '__set'),
3728: $scope,
3729: static function (): void {
3730: },
3731: $context,
3732: )->getThrowPoints());
3733: }
3734: }
3735:
3736: } elseif ($var instanceof Expr\StaticPropertyFetch) {
3737: if ($var->class instanceof Node\Name) {
3738: $propertyHolderType = $scope->resolveTypeByName($var->class);
3739: } else {
3740: $this->processExprNode($var->class, $scope, $nodeCallback, $context);
3741: $propertyHolderType = $scope->getType($var->class);
3742: }
3743:
3744: $propertyName = null;
3745: if ($var->name instanceof Node\Identifier) {
3746: $propertyName = $var->name->name;
3747: $hasYield = false;
3748: $throwPoints = [];
3749: } else {
3750: $propertyNameResult = $this->processExprNode($var->name, $scope, $nodeCallback, $context);
3751: $hasYield = $propertyNameResult->hasYield();
3752: $throwPoints = $propertyNameResult->getThrowPoints();
3753: $scope = $propertyNameResult->getScope();
3754: }
3755:
3756: $result = $processExprCallback($scope);
3757: $hasYield = $hasYield || $result->hasYield();
3758: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3759: $scope = $result->getScope();
3760:
3761: if ($propertyName !== null) {
3762: $propertyReflection = $scope->getPropertyReflection($propertyHolderType, $propertyName);
3763: $assignedExprType = $scope->getType($assignedExpr);
3764: $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope);
3765: if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) {
3766: $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
3767: }
3768: } else {
3769: // fallback
3770: $assignedExprType = $scope->getType($assignedExpr);
3771: $nodeCallback(new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scope);
3772: $scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
3773: }
3774: } elseif ($var instanceof List_ || $var instanceof Array_) {
3775: $result = $processExprCallback($scope);
3776: $hasYield = $result->hasYield();
3777: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3778: $scope = $result->getScope();
3779: foreach ($var->items as $i => $arrayItem) {
3780: if ($arrayItem === null) {
3781: continue;
3782: }
3783:
3784: $itemScope = $scope;
3785: if ($enterExpressionAssign) {
3786: $itemScope = $itemScope->enterExpressionAssign($arrayItem->value);
3787: }
3788: $itemScope = $this->lookForSetAllowedUndefinedExpressions($itemScope, $arrayItem->value);
3789: $itemResult = $this->processExprNode($arrayItem, $itemScope, $nodeCallback, $context->enterDeep());
3790: $hasYield = $hasYield || $itemResult->hasYield();
3791: $throwPoints = array_merge($throwPoints, $itemResult->getThrowPoints());
3792:
3793: if ($arrayItem->key === null) {
3794: $dimExpr = new Node\Scalar\LNumber($i);
3795: } else {
3796: $dimExpr = $arrayItem->key;
3797: }
3798: $result = $this->processAssignVar(
3799: $scope,
3800: $arrayItem->value,
3801: new GetOffsetValueTypeExpr($assignedExpr, $dimExpr),
3802: $nodeCallback,
3803: $context,
3804: static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, []),
3805: $enterExpressionAssign,
3806: );
3807: $scope = $result->getScope();
3808: $hasYield = $hasYield || $result->hasYield();
3809: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
3810: }
3811: }
3812:
3813: return new ExpressionResult($scope, $hasYield, $throwPoints);
3814: }
3815:
3816: private function unwrapAssign(Expr $expr): Expr
3817: {
3818: if ($expr instanceof Assign) {
3819: return $this->unwrapAssign($expr->expr);
3820: }
3821:
3822: return $expr;
3823: }
3824:
3825: /**
3826: * @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
3827: * @return array<string, ConditionalExpressionHolder[]>
3828: */
3829: private function processSureTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType): array
3830: {
3831: foreach ($specifiedTypes->getSureTypes() as $exprString => [$expr, $exprType]) {
3832: if (!$expr instanceof Variable) {
3833: continue;
3834: }
3835: if (!is_string($expr->name)) {
3836: continue;
3837: }
3838:
3839: if ($expr->name === $variableName) {
3840: continue;
3841: }
3842:
3843: if (!isset($conditionalExpressions[$exprString])) {
3844: $conditionalExpressions[$exprString] = [];
3845: }
3846:
3847: $holder = new ConditionalExpressionHolder([
3848: '$' . $variableName => ExpressionTypeHolder::createYes(new Variable($variableName), $variableType),
3849: ], ExpressionTypeHolder::createYes(
3850: $expr,
3851: TypeCombinator::intersect($scope->getType($expr), $exprType),
3852: ));
3853: $conditionalExpressions[$exprString][$holder->getKey()] = $holder;
3854: }
3855:
3856: return $conditionalExpressions;
3857: }
3858:
3859: /**
3860: * @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
3861: * @return array<string, ConditionalExpressionHolder[]>
3862: */
3863: private function processSureNotTypesForConditionalExpressionsAfterAssign(Scope $scope, string $variableName, array $conditionalExpressions, SpecifiedTypes $specifiedTypes, Type $variableType): array
3864: {
3865: foreach ($specifiedTypes->getSureNotTypes() as $exprString => [$expr, $exprType]) {
3866: if (!$expr instanceof Variable) {
3867: continue;
3868: }
3869: if (!is_string($expr->name)) {
3870: continue;
3871: }
3872:
3873: if ($expr->name === $variableName) {
3874: continue;
3875: }
3876:
3877: if (!isset($conditionalExpressions[$exprString])) {
3878: $conditionalExpressions[$exprString] = [];
3879: }
3880:
3881: $holder = new ConditionalExpressionHolder([
3882: '$' . $variableName => ExpressionTypeHolder::createYes(new Variable($variableName), $variableType),
3883: ], ExpressionTypeHolder::createYes(
3884: $expr,
3885: TypeCombinator::remove($scope->getType($expr), $exprType),
3886: ));
3887: $conditionalExpressions[$exprString][$holder->getKey()] = $holder;
3888: }
3889:
3890: return $conditionalExpressions;
3891: }
3892:
3893: /**
3894: * @param callable(Node $node, Scope $scope): void $nodeCallback
3895: */
3896: private function processStmtVarAnnotation(MutatingScope $scope, Node\Stmt $stmt, ?Expr $defaultExpr, callable $nodeCallback): MutatingScope
3897: {
3898: $function = $scope->getFunction();
3899: $variableLessTags = [];
3900:
3901: foreach ($stmt->getComments() as $comment) {
3902: if (!$comment instanceof Doc) {
3903: continue;
3904: }
3905:
3906: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
3907: $scope->getFile(),
3908: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
3909: $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
3910: $function !== null ? $function->getName() : null,
3911: $comment->getText(),
3912: );
3913:
3914: $assignedVariable = null;
3915: if (
3916: $stmt instanceof Node\Stmt\Expression
3917: && ($stmt->expr instanceof Assign || $stmt->expr instanceof AssignRef)
3918: && $stmt->expr->var instanceof Variable
3919: && is_string($stmt->expr->var->name)
3920: ) {
3921: $assignedVariable = $stmt->expr->var->name;
3922: }
3923:
3924: foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) {
3925: if (is_int($name)) {
3926: $variableLessTags[] = $varTag;
3927: continue;
3928: }
3929:
3930: if ($name === $assignedVariable) {
3931: continue;
3932: }
3933:
3934: $certainty = $scope->hasVariableType($name);
3935: if ($certainty->no()) {
3936: continue;
3937: }
3938:
3939: if ($scope->isInClass() && $scope->getFunction() === null) {
3940: continue;
3941: }
3942:
3943: if ($scope->canAnyVariableExist()) {
3944: $certainty = TrinaryLogic::createYes();
3945: }
3946:
3947: $variableNode = new Variable($name, $stmt->getAttributes());
3948: $originalType = $scope->getVariableType($name);
3949: if (!$originalType->equals($varTag->getType())) {
3950: $nodeCallback(new VarTagChangedExpressionTypeNode($varTag, $variableNode), $scope);
3951: }
3952:
3953: $scope = $scope->assignVariable(
3954: $name,
3955: $varTag->getType(),
3956: $scope->getNativeType($variableNode),
3957: $certainty,
3958: );
3959: }
3960: }
3961:
3962: if (count($variableLessTags) === 1 && $defaultExpr !== null) {
3963: $originalType = $scope->getType($defaultExpr);
3964: $varTag = $variableLessTags[0];
3965: if (!$originalType->equals($varTag->getType())) {
3966: $nodeCallback(new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope);
3967: }
3968: $scope = $scope->assignExpression($defaultExpr, $varTag->getType(), new MixedType());
3969: }
3970:
3971: return $scope;
3972: }
3973:
3974: /**
3975: * @param array<int, string> $variableNames
3976: */
3977: private function processVarAnnotation(MutatingScope $scope, array $variableNames, Node $node, bool &$changed = false): MutatingScope
3978: {
3979: $function = $scope->getFunction();
3980: $varTags = [];
3981: foreach ($node->getComments() as $comment) {
3982: if (!$comment instanceof Doc) {
3983: continue;
3984: }
3985:
3986: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
3987: $scope->getFile(),
3988: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
3989: $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
3990: $function !== null ? $function->getName() : null,
3991: $comment->getText(),
3992: );
3993: foreach ($resolvedPhpDoc->getVarTags() as $key => $varTag) {
3994: $varTags[$key] = $varTag;
3995: }
3996: }
3997:
3998: if (count($varTags) === 0) {
3999: return $scope;
4000: }
4001:
4002: foreach ($variableNames as $variableName) {
4003: if (!isset($varTags[$variableName])) {
4004: continue;
4005: }
4006:
4007: $variableType = $varTags[$variableName]->getType();
4008: $changed = true;
4009: $scope = $scope->assignVariable($variableName, $variableType, new MixedType());
4010: }
4011:
4012: if (count($variableNames) === 1 && count($varTags) === 1 && isset($varTags[0])) {
4013: $variableType = $varTags[0]->getType();
4014: $changed = true;
4015: $scope = $scope->assignVariable($variableNames[0], $variableType, new MixedType());
4016: }
4017:
4018: return $scope;
4019: }
4020:
4021: private function enterForeach(MutatingScope $scope, Foreach_ $stmt): MutatingScope
4022: {
4023: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
4024: $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt);
4025: }
4026: $iterateeType = $scope->getType($stmt->expr);
4027: if (
4028: ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name))
4029: && ($stmt->keyVar === null || ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)))
4030: ) {
4031: $keyVarName = null;
4032: if ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)) {
4033: $keyVarName = $stmt->keyVar->name;
4034: }
4035: $scope = $scope->enterForeach(
4036: $stmt->expr,
4037: $stmt->valueVar->name,
4038: $keyVarName,
4039: );
4040: $vars = [$stmt->valueVar->name];
4041: if ($keyVarName !== null) {
4042: $vars[] = $keyVarName;
4043: }
4044: } else {
4045: $scope = $this->processAssignVar(
4046: $scope,
4047: $stmt->valueVar,
4048: new GetIterableValueTypeExpr($stmt->expr),
4049: static function (): void {
4050: },
4051: ExpressionContext::createDeep(),
4052: static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, []),
4053: true,
4054: )->getScope();
4055: $vars = $this->getAssignedVariables($stmt->valueVar);
4056: if (
4057: $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)
4058: ) {
4059: $scope = $scope->enterForeachKey($stmt->expr, $stmt->keyVar->name);
4060: $vars[] = $stmt->keyVar->name;
4061: } elseif ($stmt->keyVar !== null) {
4062: $scope = $this->processAssignVar(
4063: $scope,
4064: $stmt->keyVar,
4065: new GetIterableKeyTypeExpr($stmt->expr),
4066: static function (): void {
4067: },
4068: ExpressionContext::createDeep(),
4069: static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, false, []),
4070: true,
4071: )->getScope();
4072: $vars = array_merge($vars, $this->getAssignedVariables($stmt->keyVar));
4073: }
4074: }
4075:
4076: $constantArrays = $iterateeType->getConstantArrays();
4077: if (
4078: $stmt->getDocComment() === null
4079: && $iterateeType->isConstantArray()->yes()
4080: && count($constantArrays) === 1
4081: && $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)
4082: && $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)
4083: ) {
4084: $valueConditionalHolders = [];
4085: $arrayDimFetchConditionalHolders = [];
4086: foreach ($constantArrays[0]->getKeyTypes() as $i => $keyType) {
4087: $valueType = $constantArrays[0]->getValueTypes()[$i];
4088: $holder = new ConditionalExpressionHolder([
4089: '$' . $stmt->keyVar->name => ExpressionTypeHolder::createYes(new Variable($stmt->keyVar->name), $keyType),
4090: ], new ExpressionTypeHolder($stmt->valueVar, $valueType, TrinaryLogic::createYes()));
4091: $valueConditionalHolders[$holder->getKey()] = $holder;
4092: $arrayDimFetchHolder = new ConditionalExpressionHolder([
4093: '$' . $stmt->keyVar->name => ExpressionTypeHolder::createYes(new Variable($stmt->keyVar->name), $keyType),
4094: ], new ExpressionTypeHolder(new ArrayDimFetch($stmt->expr, $stmt->keyVar), $valueType, TrinaryLogic::createYes()));
4095: $arrayDimFetchConditionalHolders[$arrayDimFetchHolder->getKey()] = $arrayDimFetchHolder;
4096: }
4097:
4098: $scope = $scope->addConditionalExpressions(
4099: '$' . $stmt->valueVar->name,
4100: $valueConditionalHolders,
4101: );
4102: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
4103: $scope = $scope->addConditionalExpressions(
4104: sprintf('$%s[$%s]', $stmt->expr->name, $stmt->keyVar->name),
4105: $arrayDimFetchConditionalHolders,
4106: );
4107: }
4108: }
4109:
4110: return $this->processVarAnnotation($scope, $vars, $stmt);
4111: }
4112:
4113: /**
4114: * @param callable(Node $node, Scope $scope): void $nodeCallback
4115: */
4116: private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classScope, callable $nodeCallback): void
4117: {
4118: $parentTraitNames = [];
4119: $parent = $classScope->getParentScope();
4120: while ($parent !== null) {
4121: if ($parent->isInTrait()) {
4122: $parentTraitNames[] = $parent->getTraitReflection()->getName();
4123: }
4124: $parent = $parent->getParentScope();
4125: }
4126:
4127: foreach ($node->traits as $trait) {
4128: $traitName = (string) $trait;
4129: if (in_array($traitName, $parentTraitNames, true)) {
4130: continue;
4131: }
4132: if (!$this->reflectionProvider->hasClass($traitName)) {
4133: continue;
4134: }
4135: $traitReflection = $this->reflectionProvider->getClass($traitName);
4136: $traitFileName = $traitReflection->getFileName();
4137: if ($traitFileName === null) {
4138: continue; // trait from eval or from PHP itself
4139: }
4140: $fileName = $this->fileHelper->normalizePath($traitFileName);
4141: if (!isset($this->analysedFiles[$fileName])) {
4142: continue;
4143: }
4144: $parserNodes = $this->parser->parseFile($fileName);
4145: $this->processNodesForTraitUse($parserNodes, $traitReflection, $classScope, $node->adaptations, $nodeCallback);
4146: }
4147: }
4148:
4149: /**
4150: * @param Node[]|Node|scalar|null $node
4151: * @param Node\Stmt\TraitUseAdaptation[] $adaptations
4152: * @param callable(Node $node, Scope $scope): void $nodeCallback
4153: */
4154: private function processNodesForTraitUse($node, ClassReflection $traitReflection, MutatingScope $scope, array $adaptations, callable $nodeCallback): void
4155: {
4156: if ($node instanceof Node) {
4157: if ($node instanceof Node\Stmt\Trait_ && $traitReflection->getName() === (string) $node->namespacedName && $traitReflection->getNativeReflection()->getStartLine() === $node->getStartLine()) {
4158: $methodModifiers = [];
4159: foreach ($adaptations as $adaptation) {
4160: if (!$adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
4161: continue;
4162: }
4163:
4164: if ($adaptation->newModifier === null) {
4165: continue;
4166: }
4167:
4168: $methodModifiers[$adaptation->method->toLowerString()] = $adaptation->newModifier;
4169: }
4170:
4171: $stmts = $node->stmts;
4172: foreach ($stmts as $i => $stmt) {
4173: if (!$stmt instanceof Node\Stmt\ClassMethod) {
4174: continue;
4175: }
4176: $methodName = $stmt->name->toLowerString();
4177: if (!array_key_exists($methodName, $methodModifiers)) {
4178: continue;
4179: }
4180:
4181: $methodAst = clone $stmt;
4182: $methodAst->flags = ($methodAst->flags & ~ Node\Stmt\Class_::VISIBILITY_MODIFIER_MASK) | $methodModifiers[$methodName];
4183: $stmts[$i] = $methodAst;
4184: }
4185: $this->processStmtNodes($node, $stmts, $scope->enterTrait($traitReflection), $nodeCallback, StatementContext::createTopLevel());
4186: return;
4187: }
4188: if ($node instanceof Node\Stmt\ClassLike) {
4189: return;
4190: }
4191: if ($node instanceof Node\FunctionLike) {
4192: return;
4193: }
4194: foreach ($node->getSubNodeNames() as $subNodeName) {
4195: $subNode = $node->{$subNodeName};
4196: $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $adaptations, $nodeCallback);
4197: }
4198: } elseif (is_array($node)) {
4199: foreach ($node as $subNode) {
4200: $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $adaptations, $nodeCallback);
4201: }
4202: }
4203: }
4204:
4205: /**
4206: * @return array{TemplateTypeMap, 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}
4207: */
4208: public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array
4209: {
4210: $templateTypeMap = TemplateTypeMap::createEmpty();
4211: $phpDocParameterTypes = [];
4212: $phpDocReturnType = null;
4213: $phpDocThrowType = null;
4214: $deprecatedDescription = null;
4215: $isDeprecated = false;
4216: $isInternal = false;
4217: $isFinal = false;
4218: $isPure = false;
4219: $isAllowedPrivateMutation = false;
4220: $acceptsNamedArguments = true;
4221: $isReadOnly = $scope->isInClass() && $scope->getClassReflection()->isImmutable();
4222: $asserts = Assertions::createEmpty();
4223: $selfOutType = null;
4224: $docComment = $node->getDocComment() !== null
4225: ? $node->getDocComment()->getText()
4226: : null;
4227:
4228: $file = $scope->getFile();
4229: $class = $scope->isInClass() ? $scope->getClassReflection()->getName() : null;
4230: $trait = $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null;
4231: $resolvedPhpDoc = null;
4232: $functionName = null;
4233: $phpDocParameterOutTypes = [];
4234:
4235: if ($node instanceof Node\Stmt\ClassMethod) {
4236: if (!$scope->isInClass()) {
4237: throw new ShouldNotHappenException();
4238: }
4239: $functionName = $node->name->name;
4240: $positionalParameterNames = array_map(static function (Node\Param $param): string {
4241: if (!$param->var instanceof Variable || !is_string($param->var->name)) {
4242: throw new ShouldNotHappenException();
4243: }
4244:
4245: return $param->var->name;
4246: }, $node->getParams());
4247: $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod(
4248: $docComment,
4249: $file,
4250: $scope->getClassReflection(),
4251: $trait,
4252: $node->name->name,
4253: $positionalParameterNames,
4254: );
4255:
4256: if ($node->name->toLowerString() === '__construct') {
4257: foreach ($node->params as $param) {
4258: if ($param->flags === 0) {
4259: continue;
4260: }
4261:
4262: if ($param->getDocComment() === null) {
4263: continue;
4264: }
4265:
4266: if (
4267: !$param->var instanceof Variable
4268: || !is_string($param->var->name)
4269: ) {
4270: throw new ShouldNotHappenException();
4271: }
4272:
4273: $paramPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
4274: $file,
4275: $class,
4276: $trait,
4277: '__construct',
4278: $param->getDocComment()->getText(),
4279: );
4280: $varTags = $paramPhpDoc->getVarTags();
4281: if (isset($varTags[0]) && count($varTags) === 1) {
4282: $phpDocType = $varTags[0]->getType();
4283: } elseif (isset($varTags[$param->var->name])) {
4284: $phpDocType = $varTags[$param->var->name]->getType();
4285: } else {
4286: continue;
4287: }
4288:
4289: $phpDocParameterTypes[$param->var->name] = $phpDocType;
4290: }
4291: }
4292: } elseif ($node instanceof Node\Stmt\Function_) {
4293: $functionName = trim($scope->getNamespace() . '\\' . $node->name->name, '\\');
4294: }
4295:
4296: if ($docComment !== null && $resolvedPhpDoc === null) {
4297: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
4298: $file,
4299: $class,
4300: $trait,
4301: $functionName,
4302: $docComment,
4303: );
4304: }
4305:
4306: $varTags = [];
4307: if ($resolvedPhpDoc !== null) {
4308: $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap();
4309: foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) {
4310: if (array_key_exists($paramName, $phpDocParameterTypes)) {
4311: continue;
4312: }
4313: $paramType = $paramTag->getType();
4314: if ($scope->isInClass()) {
4315: $paramType = $this->transformStaticType($scope->getClassReflection(), $paramType);
4316: }
4317: $phpDocParameterTypes[$paramName] = $paramType;
4318: }
4319: foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) {
4320: $phpDocParameterOutTypes[$paramName] = $paramOutTag->getType();
4321: }
4322: if ($node instanceof Node\FunctionLike) {
4323: $nativeReturnType = $scope->getFunctionType($node->getReturnType(), false, false);
4324: $phpDocReturnType = $this->getPhpDocReturnType($resolvedPhpDoc, $nativeReturnType);
4325: if ($phpDocReturnType !== null && $scope->isInClass()) {
4326: $phpDocReturnType = $this->transformStaticType($scope->getClassReflection(), $phpDocReturnType);
4327: }
4328: }
4329: $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null;
4330: $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null;
4331: $isDeprecated = $resolvedPhpDoc->isDeprecated();
4332: $isInternal = $resolvedPhpDoc->isInternal();
4333: $isFinal = $resolvedPhpDoc->isFinal();
4334: $isPure = $resolvedPhpDoc->isPure();
4335: $isAllowedPrivateMutation = $resolvedPhpDoc->isAllowedPrivateMutation();
4336: $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments();
4337: $isReadOnly = $isReadOnly || $resolvedPhpDoc->isReadOnly();
4338: $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc);
4339: $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null;
4340: $varTags = $resolvedPhpDoc->getVarTags();
4341: }
4342:
4343: return [$templateTypeMap, $phpDocParameterTypes, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation];
4344: }
4345:
4346: private function transformStaticType(ClassReflection $declaringClass, Type $type): Type
4347: {
4348: return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($declaringClass): Type {
4349: if ($type instanceof StaticType) {
4350: $changedType = $type->changeBaseClass($declaringClass);
4351: if ($declaringClass->isFinal()) {
4352: $changedType = $changedType->getStaticObjectType();
4353: }
4354: return $traverse($changedType);
4355: }
4356:
4357: return $traverse($type);
4358: });
4359: }
4360:
4361: private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $nativeReturnType): ?Type
4362: {
4363: $returnTag = $resolvedPhpDoc->getReturnTag();
4364:
4365: if ($returnTag === null) {
4366: return null;
4367: }
4368:
4369: $phpDocReturnType = $returnTag->getType();
4370:
4371: if ($returnTag->isExplicit()) {
4372: return $phpDocReturnType;
4373: }
4374:
4375: if ($nativeReturnType->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocReturnType))->yes()) {
4376: return $phpDocReturnType;
4377: }
4378:
4379: return null;
4380: }
4381:
4382: /**
4383: * @template T of Node
4384: * @param array<T> $nodes
4385: * @return T
4386: */
4387: private function getFirstNonNopNode(array $nodes): ?Node
4388: {
4389: foreach ($nodes as $node) {
4390: if (!$node instanceof Node\Stmt\Nop) {
4391: return $node;
4392: }
4393: }
4394:
4395: return null;
4396: }
4397:
4398: }
4399: