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