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