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