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