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