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