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