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