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