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