1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use ArrayAccess;
6: use Closure;
7: use Override;
8: use PhpParser\Comment\Doc;
9: use PhpParser\Modifiers;
10: use PhpParser\Node;
11: use PhpParser\Node\Arg;
12: use PhpParser\Node\AttributeGroup;
13: use PhpParser\Node\ComplexType;
14: use PhpParser\Node\Expr;
15: use PhpParser\Node\Expr\Array_;
16: use PhpParser\Node\Expr\ArrayDimFetch;
17: use PhpParser\Node\Expr\Assign;
18: use PhpParser\Node\Expr\AssignRef;
19: use PhpParser\Node\Expr\BinaryOp;
20: use PhpParser\Node\Expr\BinaryOp\BooleanOr;
21: use PhpParser\Node\Expr\CallLike;
22: use PhpParser\Node\Expr\ConstFetch;
23: use PhpParser\Node\Expr\FuncCall;
24: use PhpParser\Node\Expr\List_;
25: use PhpParser\Node\Expr\MethodCall;
26: use PhpParser\Node\Expr\New_;
27: use PhpParser\Node\Expr\PropertyFetch;
28: use PhpParser\Node\Expr\StaticCall;
29: use PhpParser\Node\Expr\StaticPropertyFetch;
30: use PhpParser\Node\Expr\Variable;
31: use PhpParser\Node\Identifier;
32: use PhpParser\Node\Name;
33: use PhpParser\Node\Stmt\Break_;
34: use PhpParser\Node\Stmt\Class_;
35: use PhpParser\Node\Stmt\Continue_;
36: use PhpParser\Node\Stmt\Do_;
37: use PhpParser\Node\Stmt\Echo_;
38: use PhpParser\Node\Stmt\For_;
39: use PhpParser\Node\Stmt\Foreach_;
40: use PhpParser\Node\Stmt\If_;
41: use PhpParser\Node\Stmt\InlineHTML;
42: use PhpParser\Node\Stmt\Return_;
43: use PhpParser\Node\Stmt\Static_;
44: use PhpParser\Node\Stmt\Switch_;
45: use PhpParser\Node\Stmt\TryCatch;
46: use PhpParser\Node\Stmt\Unset_;
47: use PhpParser\Node\Stmt\While_;
48: use PhpParser\NodeFinder;
49: use PhpParser\NodeTraverser;
50: use PhpParser\NodeVisitorAbstract;
51: use PHPStan\Analyser\ExprHandler\AssignHandler;
52: use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper;
53: use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass;
54: use PHPStan\BetterReflection\Reflection\ReflectionEnum;
55: use PHPStan\BetterReflection\Reflector\Reflector;
56: use PHPStan\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection;
57: use PHPStan\BetterReflection\SourceLocator\Located\LocatedSource;
58: use PHPStan\DependencyInjection\AutowiredParameter;
59: use PHPStan\DependencyInjection\AutowiredService;
60: use PHPStan\DependencyInjection\Container;
61: use PHPStan\DependencyInjection\Type\ParameterClosureThisExtensionProvider;
62: use PHPStan\DependencyInjection\Type\ParameterClosureTypeExtensionProvider;
63: use PHPStan\DependencyInjection\Type\ParameterOutTypeExtensionProvider;
64: use PHPStan\File\FileHelper;
65: use PHPStan\File\FileReader;
66: use PHPStan\Node\BreaklessWhileLoopNode;
67: use PHPStan\Node\CatchWithUnthrownExceptionNode;
68: use PHPStan\Node\ClassConstantsNode;
69: use PHPStan\Node\ClassMethodsNode;
70: use PHPStan\Node\ClassPropertiesNode;
71: use PHPStan\Node\ClassPropertyNode;
72: use PHPStan\Node\ClassStatementsGatherer;
73: use PHPStan\Node\ClosureReturnStatementsNode;
74: use PHPStan\Node\DeepNodeCloner;
75: use PHPStan\Node\DoWhileLoopConditionNode;
76: use PHPStan\Node\ExecutionEndNode;
77: use PHPStan\Node\Expr\ExistingArrayDimFetch;
78: use PHPStan\Node\Expr\ForeachValueByRefExpr;
79: use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
80: use PHPStan\Node\Expr\GetIterableValueTypeExpr;
81: use PHPStan\Node\Expr\OriginalForeachKeyExpr;
82: use PHPStan\Node\Expr\PropertyInitializationExpr;
83: use PHPStan\Node\Expr\TypeExpr;
84: use PHPStan\Node\Expr\UnsetOffsetExpr;
85: use PHPStan\Node\FinallyExitPointsNode;
86: use PHPStan\Node\FunctionCallableNode;
87: use PHPStan\Node\FunctionReturnStatementsNode;
88: use PHPStan\Node\InArrowFunctionNode;
89: use PHPStan\Node\InClassMethodNode;
90: use PHPStan\Node\InClassNode;
91: use PHPStan\Node\InClosureNode;
92: use PHPStan\Node\InForeachNode;
93: use PHPStan\Node\InFunctionNode;
94: use PHPStan\Node\InPropertyHookNode;
95: use PHPStan\Node\InstantiationCallableNode;
96: use PHPStan\Node\InTraitNode;
97: use PHPStan\Node\InvalidateExprNode;
98: use PHPStan\Node\MethodCallableNode;
99: use PHPStan\Node\MethodReturnStatementsNode;
100: use PHPStan\Node\NoopExpressionNode;
101: use PHPStan\Node\PropertyAssignNode;
102: use PHPStan\Node\PropertyHookReturnStatementsNode;
103: use PHPStan\Node\PropertyHookStatementNode;
104: use PHPStan\Node\ReturnStatement;
105: use PHPStan\Node\StaticMethodCallableNode;
106: use PHPStan\Node\UnreachableStatementNode;
107: use PHPStan\Node\VariableAssignNode;
108: use PHPStan\Node\VarTagChangedExpressionTypeNode;
109: use PHPStan\Parser\ArrowFunctionArgVisitor;
110: use PHPStan\Parser\ClosureArgVisitor;
111: use PHPStan\Parser\ImmediatelyInvokedClosureVisitor;
112: use PHPStan\Parser\LineAttributesVisitor;
113: use PHPStan\Parser\Parser;
114: use PHPStan\PhpDoc\PhpDocInheritanceResolver;
115: use PHPStan\PhpDoc\ResolvedPhpDocBlock;
116: use PHPStan\PhpDoc\Tag\VarTag;
117: use PHPStan\Reflection\Assertions;
118: use PHPStan\Reflection\Callables\SimpleImpurePoint;
119: use PHPStan\Reflection\Callables\SimpleThrowPoint;
120: use PHPStan\Reflection\ClassReflection;
121: use PHPStan\Reflection\ClassReflectionFactory;
122: use PHPStan\Reflection\ExtendedMethodReflection;
123: use PHPStan\Reflection\ExtendedParameterReflection;
124: use PHPStan\Reflection\FunctionReflection;
125: use PHPStan\Reflection\InitializerExprContext;
126: use PHPStan\Reflection\InitializerExprTypeResolver;
127: use PHPStan\Reflection\MethodReflection;
128: use PHPStan\Reflection\Native\NativeMethodReflection;
129: use PHPStan\Reflection\Native\NativeParameterReflection;
130: use PHPStan\Reflection\ParameterReflection;
131: use PHPStan\Reflection\ParametersAcceptor;
132: use PHPStan\Reflection\ParametersAcceptorSelector;
133: use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection;
134: use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
135: use PHPStan\Reflection\Php\PhpMethodReflection;
136: use PHPStan\Reflection\Php\PhpPropertyReflection;
137: use PHPStan\Reflection\ReflectionProvider;
138: use PHPStan\Rules\Properties\ReadWritePropertiesExtensionProvider;
139: use PHPStan\ShouldNotHappenException;
140: use PHPStan\TrinaryLogic;
141: use PHPStan\Type\ArrayType;
142: use PHPStan\Type\ClosureType;
143: use PHPStan\Type\FileTypeMapper;
144: use PHPStan\Type\Generic\TemplateTypeHelper;
145: use PHPStan\Type\Generic\TemplateTypeMap;
146: use PHPStan\Type\IntersectionType;
147: use PHPStan\Type\MixedType;
148: use PHPStan\Type\NeverType;
149: use PHPStan\Type\NullType;
150: use PHPStan\Type\ObjectType;
151: use PHPStan\Type\ObjectWithoutClassType;
152: use PHPStan\Type\ParserNodeTypeToPHPStanType;
153: use PHPStan\Type\ResourceType;
154: use PHPStan\Type\StaticType;
155: use PHPStan\Type\ThisType;
156: use PHPStan\Type\Type;
157: use PHPStan\Type\TypeCombinator;
158: use PHPStan\Type\TypeTraverser;
159: use PHPStan\Type\TypeUtils;
160: use PHPStan\Type\UnionType;
161: use Throwable;
162: use Traversable;
163: use function array_fill_keys;
164: use function array_filter;
165: use function array_key_exists;
166: use function array_keys;
167: use function array_last;
168: use function array_map;
169: use function array_merge;
170: use function array_slice;
171: use function array_values;
172: use function base64_decode;
173: use function count;
174: use function in_array;
175: use function is_array;
176: use function is_int;
177: use function is_string;
178: use function sprintf;
179: use function strtolower;
180: use function trim;
181: use function usort;
182: use const PHP_VERSION_ID;
183:
184: #[AutowiredService]
185: class NodeScopeResolver
186: {
187:
188: private const LOOP_SCOPE_ITERATIONS = 3;
189: private const GENERALIZE_AFTER_ITERATION = 1;
190: private const FOREACH_UNROLL_LIMIT = 16;
191:
192: /** @var array<string, true> filePath(string) => bool(true) */
193: private array $analysedFiles = [];
194:
195: /** @var array<string, true> */
196: private array $earlyTerminatingMethodNames;
197:
198: /** @var array<string, true> */
199: private array $calledMethodStack = [];
200:
201: /** @var array<string, MutatingScope|null> */
202: private array $calledMethodResults = [];
203:
204: /**
205: * @param string[][] $earlyTerminatingMethodCalls className(string) => methods(string[])
206: * @param array<int, string> $earlyTerminatingFunctionCalls
207: */
208: public function __construct(
209: private readonly Container $container,
210: private readonly ReflectionProvider $reflectionProvider,
211: private readonly InitializerExprTypeResolver $initializerExprTypeResolver,
212: #[AutowiredParameter(ref: '@nodeScopeResolverReflector')]
213: private readonly Reflector $reflector,
214: private readonly ClassReflectionFactory $classReflectionFactory,
215: private readonly ParameterOutTypeExtensionProvider $parameterOutTypeExtensionProvider,
216: #[AutowiredParameter(ref: '@defaultAnalysisParser')]
217: private readonly Parser $parser,
218: private readonly FileTypeMapper $fileTypeMapper,
219: private readonly PhpDocInheritanceResolver $phpDocInheritanceResolver,
220: private readonly FileHelper $fileHelper,
221: private readonly TypeSpecifier $typeSpecifier,
222: private readonly ReadWritePropertiesExtensionProvider $readWritePropertiesExtensionProvider,
223: private readonly ParameterClosureThisExtensionProvider $parameterClosureThisExtensionProvider,
224: private readonly ParameterClosureTypeExtensionProvider $parameterClosureTypeExtensionProvider,
225: private readonly ScopeFactory $scopeFactory,
226: private readonly DeepNodeCloner $deepNodeCloner,
227: #[AutowiredParameter]
228: private readonly bool $polluteScopeWithLoopInitialAssignments,
229: #[AutowiredParameter]
230: private readonly bool $polluteScopeWithAlwaysIterableForeach,
231: #[AutowiredParameter]
232: private readonly bool $polluteScopeWithBlock,
233: #[AutowiredParameter]
234: private readonly array $earlyTerminatingMethodCalls,
235: #[AutowiredParameter]
236: private readonly array $earlyTerminatingFunctionCalls,
237: #[AutowiredParameter(ref: '%exceptions.implicitThrows%')]
238: private readonly bool $implicitThrows,
239: #[AutowiredParameter]
240: private readonly bool $treatPhpDocTypesAsCertain,
241: private readonly ImplicitToStringCallHelper $implicitToStringCallHelper,
242: )
243: {
244: $earlyTerminatingMethodNames = [];
245: foreach ($this->earlyTerminatingMethodCalls as $methodNames) {
246: foreach ($methodNames as $methodName) {
247: $earlyTerminatingMethodNames[strtolower($methodName)] = true;
248: }
249: }
250: $this->earlyTerminatingMethodNames = $earlyTerminatingMethodNames;
251: }
252:
253: /**
254: * @api
255: * @param string[] $files
256: */
257: public function setAnalysedFiles(array $files): void
258: {
259: $this->analysedFiles = array_fill_keys($files, true);
260: }
261:
262: /**
263: * @api
264: * @param Node[] $nodes
265: * @param callable(Node $node, Scope $scope): void $nodeCallback
266: */
267: public function processNodes(
268: array $nodes,
269: MutatingScope $scope,
270: callable $nodeCallback,
271: ): void
272: {
273: $expressionResultStorage = new ExpressionResultStorage();
274: $alreadyTerminated = false;
275: foreach ($nodes as $i => $node) {
276: if (
277: !$node instanceof Node\Stmt
278: || ($alreadyTerminated && !($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike))
279: ) {
280: continue;
281: }
282:
283: $statementResult = $this->processStmtNode($node, $scope, $expressionResultStorage, $nodeCallback, StatementContext::createTopLevel());
284: $scope = $statementResult->getScope();
285: if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) {
286: continue;
287: }
288:
289: $alreadyTerminated = true;
290: $nextStmts = $this->getNextUnreachableStatements(array_slice($nodes, $i + 1), true);
291: $this->processUnreachableStatement($nextStmts, $scope, $expressionResultStorage, $nodeCallback);
292: }
293:
294: $this->processPendingFibers($expressionResultStorage);
295: }
296:
297: public function storeBeforeScope(ExpressionResultStorage $storage, Expr $expr, Scope $beforeScope): void
298: {
299: }
300:
301: protected function processPendingFibers(ExpressionResultStorage $storage): void
302: {
303: }
304:
305: /**
306: * @param Node\Stmt[] $nextStmts
307: * @param callable(Node $node, Scope $scope): void $nodeCallback
308: */
309: private function processUnreachableStatement(array $nextStmts, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback): void
310: {
311: if ($nextStmts === []) {
312: return;
313: }
314:
315: $unreachableStatement = null;
316: $nextStatements = [];
317:
318: foreach ($nextStmts as $key => $nextStmt) {
319: if ($key === 0) {
320: $unreachableStatement = $nextStmt;
321: continue;
322: }
323:
324: $nextStatements[] = $nextStmt;
325: }
326:
327: if (!$unreachableStatement instanceof Node\Stmt) {
328: return;
329: }
330:
331: $this->callNodeCallback($nodeCallback, new UnreachableStatementNode($unreachableStatement, $nextStatements), $scope, $storage);
332: }
333:
334: /**
335: * @api
336: * @param Node\Stmt[] $stmts
337: * @param callable(Node $node, Scope $scope): void $nodeCallback
338: */
339: public function processStmtNodes(
340: Node $parentNode,
341: array $stmts,
342: MutatingScope $scope,
343: callable $nodeCallback,
344: StatementContext $context,
345: ): StatementResult
346: {
347: $storage = new ExpressionResultStorage();
348: return $this->processStmtNodesInternal(
349: $parentNode,
350: $stmts,
351: $scope,
352: $storage,
353: $nodeCallback,
354: $context,
355: )->toPublic();
356: }
357:
358: /**
359: * @param Node\Stmt[] $stmts
360: * @param callable(Node $node, Scope $scope): void $nodeCallback
361: */
362: private function processStmtNodesInternal(
363: Node $parentNode,
364: array $stmts,
365: MutatingScope $scope,
366: ExpressionResultStorage $storage,
367: callable $nodeCallback,
368: StatementContext $context,
369: ): InternalStatementResult
370: {
371: $statementResult = $this->processStmtNodesInternalWithoutFlushingPendingFibers(
372: $parentNode,
373: $stmts,
374: $scope,
375: $storage,
376: $nodeCallback,
377: $context,
378: );
379: $this->processPendingFibers($storage);
380:
381: return $statementResult;
382: }
383:
384: /**
385: * @param Node\Stmt[] $stmts
386: * @param callable(Node $node, Scope $scope): void $nodeCallback
387: */
388: private function processStmtNodesInternalWithoutFlushingPendingFibers(
389: Node $parentNode,
390: array $stmts,
391: MutatingScope $scope,
392: ExpressionResultStorage $storage,
393: callable $nodeCallback,
394: StatementContext $context,
395: ): InternalStatementResult
396: {
397: $exitPoints = [];
398: $throwPoints = [];
399: $impurePoints = [];
400: $alreadyTerminated = false;
401: $hasYield = false;
402: $stmtCount = count($stmts);
403: $shouldCheckLastStatement = $parentNode instanceof Node\Stmt\Function_
404: || $parentNode instanceof Node\Stmt\ClassMethod
405: || $parentNode instanceof PropertyHookStatementNode
406: || $parentNode instanceof Expr\Closure;
407: foreach ($stmts as $i => $stmt) {
408: if ($alreadyTerminated && !($stmt instanceof Node\Stmt\Function_ || $stmt instanceof Node\Stmt\ClassLike)) {
409: continue;
410: }
411:
412: $isLast = $i === $stmtCount - 1;
413: $statementResult = $this->processStmtNode(
414: $stmt,
415: $scope,
416: $storage,
417: $nodeCallback,
418: $context,
419: );
420: $scope = $statementResult->getScope();
421: $hasYield = $hasYield || $statementResult->hasYield();
422:
423: if ($shouldCheckLastStatement && $isLast) {
424: $endStatements = $statementResult->getEndStatements();
425: if (count($endStatements) > 0) {
426: foreach ($endStatements as $endStatement) {
427: $endStatementResult = $endStatement->getResult();
428: $this->callNodeCallback($nodeCallback, new ExecutionEndNode(
429: $endStatement->getStatement(),
430: (new InternalStatementResult(
431: $endStatementResult->getScope(),
432: $hasYield,
433: $endStatementResult->isAlwaysTerminating(),
434: $endStatementResult->getExitPoints(),
435: $endStatementResult->getThrowPoints(),
436: $endStatementResult->getImpurePoints(),
437: ))->toPublic(),
438: $parentNode->getReturnType() !== null,
439: ), $endStatementResult->getScope(), $storage);
440: }
441: } else {
442: $this->callNodeCallback($nodeCallback, new ExecutionEndNode(
443: $stmt,
444: (new InternalStatementResult(
445: $scope,
446: $hasYield,
447: $statementResult->isAlwaysTerminating(),
448: $statementResult->getExitPoints(),
449: $statementResult->getThrowPoints(),
450: $statementResult->getImpurePoints(),
451: ))->toPublic(),
452: $parentNode->getReturnType() !== null,
453: ), $scope, $storage);
454: }
455: }
456:
457: $exitPoints = array_merge($exitPoints, $statementResult->getExitPoints());
458: $throwPoints = array_merge($throwPoints, $statementResult->getThrowPoints());
459: $impurePoints = array_merge($impurePoints, $statementResult->getImpurePoints());
460:
461: if ($alreadyTerminated || !$statementResult->isAlwaysTerminating()) {
462: continue;
463: }
464:
465: $alreadyTerminated = true;
466: $nextStmts = $this->getNextUnreachableStatements(array_slice($stmts, $i + 1), $parentNode instanceof Node\Stmt\Namespace_);
467: $this->processUnreachableStatement($nextStmts, $scope, $storage, $nodeCallback);
468: }
469:
470: $statementResult = new InternalStatementResult($scope, $hasYield, $alreadyTerminated, $exitPoints, $throwPoints, $impurePoints);
471: if ($stmtCount === 0 && $shouldCheckLastStatement) {
472: $returnTypeNode = $parentNode->getReturnType();
473: if ($parentNode instanceof Expr\Closure) {
474: $parentNode = new Node\Stmt\Expression($parentNode, $parentNode->getAttributes());
475: }
476: $this->callNodeCallback($nodeCallback, new ExecutionEndNode(
477: $parentNode,
478: $statementResult->toPublic(),
479: $returnTypeNode !== null,
480: ), $scope, $storage);
481: }
482:
483: return $statementResult;
484: }
485:
486: /**
487: * @param callable(Node $node, Scope $scope): void $nodeCallback
488: */
489: public function processStmtNode(
490: Node\Stmt $stmt,
491: MutatingScope $scope,
492: ExpressionResultStorage $storage,
493: callable $nodeCallback,
494: StatementContext $context,
495: ): InternalStatementResult
496: {
497: $overridingThrowPoints = null;
498: if (
499: !$stmt instanceof Static_
500: && !$stmt instanceof Node\Stmt\Global_
501: && !$stmt instanceof Node\Stmt\Property
502: && !$stmt instanceof Node\Stmt\ClassConst
503: && !$stmt instanceof Node\Stmt\Const_
504: && !$stmt instanceof Node\Stmt\ClassLike
505: && !$stmt instanceof Node\Stmt\Function_
506: && !$stmt instanceof Node\Stmt\ClassMethod
507: ) {
508: if (!$stmt instanceof Foreach_) {
509: $scope = $this->processStmtVarAnnotation($scope, $storage, $stmt, null, $nodeCallback);
510: }
511: $overridingThrowPoints = $this->getOverridingThrowPoints($stmt, $scope);
512: }
513:
514: if ($stmt instanceof Node\Stmt\ClassMethod) {
515: if (!$scope->isInClass()) {
516: throw new ShouldNotHappenException();
517: }
518: if (
519: $scope->isInTrait()
520: && $scope->getClassReflection()->hasNativeMethod($stmt->name->toString())
521: ) {
522: $methodReflection = $scope->getClassReflection()->getNativeMethod($stmt->name->toString());
523: if ($methodReflection instanceof NativeMethodReflection) {
524: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []);
525: }
526: if ($methodReflection instanceof PhpMethodReflection) {
527: $declaringTrait = $methodReflection->getDeclaringTrait();
528: if ($declaringTrait === null || $declaringTrait->getName() !== $scope->getTraitReflection()->getName()) {
529: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []);
530: }
531: }
532: }
533: }
534:
535: $stmtScope = $scope;
536: if ($stmt instanceof Node\Stmt\Expression && $stmt->expr instanceof Expr\Throw_) {
537: $stmtScope = $this->processStmtVarAnnotation($scope, $storage, $stmt, $stmt->expr->expr, $nodeCallback);
538: }
539: if ($stmt instanceof Return_) {
540: $stmtScope = $this->processStmtVarAnnotation($scope, $storage, $stmt, $stmt->expr, $nodeCallback);
541: }
542:
543: $this->callNodeCallback($nodeCallback, $stmt, $stmtScope, $storage);
544:
545: if ($stmt instanceof Node\Stmt\Declare_) {
546: $hasYield = false;
547: $throwPoints = [];
548: $impurePoints = [];
549: $alwaysTerminating = false;
550: $exitPoints = [];
551: foreach ($stmt->declares as $declare) {
552: $this->callNodeCallback($nodeCallback, $declare, $scope, $storage);
553: $this->callNodeCallback($nodeCallback, $declare->value, $scope, $storage);
554: if (
555: $declare->key->name !== 'strict_types'
556: || !($declare->value instanceof Node\Scalar\Int_)
557: || $declare->value->value !== 1
558: ) {
559: continue;
560: }
561:
562: $scope = $scope->enterDeclareStrictTypes();
563: }
564:
565: if ($stmt->stmts !== null) {
566: $result = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context);
567: $scope = $result->getScope();
568: $hasYield = $result->hasYield();
569: $throwPoints = $result->getThrowPoints();
570: $impurePoints = $result->getImpurePoints();
571: $alwaysTerminating = $result->isAlwaysTerminating();
572: $exitPoints = $result->getExitPoints();
573: }
574:
575: return new InternalStatementResult($scope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints);
576: } elseif ($stmt instanceof Node\Stmt\Function_) {
577: $hasYield = false;
578: $throwPoints = [];
579: $impurePoints = [];
580: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
581: [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, , $isPure, $acceptsNamedArguments, , $phpDocComment, $asserts,, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt);
582:
583: foreach ($stmt->params as $param) {
584: $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
585: }
586:
587: if ($stmt->returnType !== null) {
588: $this->callNodeCallback($nodeCallback, $stmt->returnType, $scope, $storage);
589: }
590:
591: if (!$isDeprecated) {
592: [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $stmt);
593: }
594:
595: $functionScope = $scope->enterFunction(
596: $stmt,
597: $templateTypeMap,
598: $phpDocParameterTypes,
599: $phpDocReturnType,
600: $phpDocThrowType,
601: $deprecatedDescription,
602: $isDeprecated,
603: $isInternal,
604: $isPure,
605: $acceptsNamedArguments,
606: $asserts,
607: $phpDocComment,
608: $phpDocParameterOutTypes,
609: $phpDocImmediatelyInvokedCallableParameters,
610: $phpDocClosureThisTypeParameters,
611: );
612: $functionReflection = $functionScope->getFunction();
613: if (!$functionReflection instanceof PhpFunctionFromParserNodeReflection) {
614: throw new ShouldNotHappenException();
615: }
616:
617: $this->callNodeCallback($nodeCallback, new InFunctionNode($functionReflection, $stmt), $functionScope, $storage);
618:
619: $gatheredReturnStatements = [];
620: $gatheredYieldStatements = [];
621: $executionEnds = [];
622: $functionImpurePoints = [];
623: $statementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $functionScope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $functionScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$functionImpurePoints): void {
624: $nodeCallback($node, $scope);
625: if ($scope->getFunction() !== $functionScope->getFunction()) {
626: return;
627: }
628: if ($scope->isInAnonymousFunction()) {
629: return;
630: }
631: if ($node instanceof PropertyAssignNode) {
632: $functionImpurePoints[] = new ImpurePoint(
633: $scope,
634: $node,
635: 'propertyAssign',
636: 'property assignment',
637: true,
638: );
639: return;
640: }
641: if ($node instanceof ExecutionEndNode) {
642: $executionEnds[] = $node;
643: return;
644: }
645: if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) {
646: $gatheredYieldStatements[] = $node;
647: }
648: if (!$node instanceof Return_) {
649: return;
650: }
651:
652: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
653: }, StatementContext::createTopLevel())->toPublic();
654:
655: $this->callNodeCallback($nodeCallback, new FunctionReturnStatementsNode(
656: $stmt,
657: $gatheredReturnStatements,
658: $gatheredYieldStatements,
659: $statementResult,
660: $executionEnds,
661: array_merge($statementResult->getImpurePoints(), $functionImpurePoints),
662: $functionReflection,
663: ), $functionScope, $storage);
664: if (!$scope->isInAnonymousFunction()) {
665: $this->processPendingFibers($storage);
666: }
667: } elseif ($stmt instanceof Node\Stmt\ClassMethod) {
668: $hasYield = false;
669: $throwPoints = [];
670: $impurePoints = [];
671: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
672: [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $phpDocComment, $asserts, $selfOutType, $phpDocParameterOutTypes] = $this->getPhpDocs($scope, $stmt);
673:
674: foreach ($stmt->params as $param) {
675: $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
676: }
677:
678: if ($stmt->returnType !== null) {
679: $this->callNodeCallback($nodeCallback, $stmt->returnType, $scope, $storage);
680: }
681:
682: if (!$isDeprecated) {
683: [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $stmt);
684: }
685:
686: $isFromTrait = $stmt->getAttribute('originalTraitMethodName') === '__construct';
687: $isConstructor = $isFromTrait || $stmt->name->toLowerString() === '__construct';
688:
689: $methodScope = $scope->enterClassMethod(
690: $stmt,
691: $templateTypeMap,
692: $phpDocParameterTypes,
693: $phpDocReturnType,
694: $phpDocThrowType,
695: $deprecatedDescription,
696: $isDeprecated,
697: $isInternal,
698: $isFinal,
699: $isPure,
700: $acceptsNamedArguments,
701: $asserts,
702: $selfOutType,
703: $phpDocComment,
704: $phpDocParameterOutTypes,
705: $phpDocImmediatelyInvokedCallableParameters,
706: $phpDocClosureThisTypeParameters,
707: $isConstructor,
708: );
709:
710: if (!$scope->isInClass()) {
711: throw new ShouldNotHappenException();
712: }
713:
714: $classReflection = $scope->getClassReflection();
715:
716: if ($isConstructor) {
717: foreach ($stmt->params as $param) {
718: if ($param->flags === 0 && $param->hooks === []) {
719: continue;
720: }
721:
722: if (!$param->var instanceof Variable || !is_string($param->var->name) || $param->var->name === '') {
723: throw new ShouldNotHappenException();
724: }
725: $phpDoc = null;
726: if ($param->getDocComment() !== null) {
727: $phpDoc = $param->getDocComment()->getText();
728: }
729: $this->callNodeCallback($nodeCallback, new ClassPropertyNode(
730: $param->var->name,
731: $param->flags,
732: $param->type !== null ? ParserNodeTypeToPHPStanType::resolve($param->type, $classReflection) : null,
733: null,
734: $phpDoc,
735: $phpDocParameterTypes[$param->var->name] ?? null,
736: true,
737: $isFromTrait,
738: $param,
739: $isReadOnly,
740: $scope->isInTrait(),
741: $classReflection->isReadOnly(),
742: false,
743: $classReflection,
744: ), $methodScope, $storage);
745: $this->processPropertyHooks(
746: $stmt,
747: $param->type,
748: $phpDocParameterTypes[$param->var->name] ?? null,
749: $param->var->name,
750: $param->hooks,
751: $scope,
752: $storage,
753: $nodeCallback,
754: );
755: $methodScope = $methodScope->assignExpression(new PropertyInitializationExpr($param->var->name), new MixedType(), new MixedType());
756: }
757: }
758:
759: if ($stmt->getAttribute('virtual', false) === false) {
760: $methodReflection = $methodScope->getFunction();
761: if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) {
762: throw new ShouldNotHappenException();
763: }
764: $this->callNodeCallback($nodeCallback, new InClassMethodNode($classReflection, $methodReflection, $stmt), $methodScope, $storage);
765: }
766:
767: if ($stmt->stmts !== null) {
768: $gatheredReturnStatements = [];
769: $gatheredYieldStatements = [];
770: $executionEnds = [];
771: $methodImpurePoints = [];
772: $statementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $methodScope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $methodScope, &$gatheredReturnStatements, &$gatheredYieldStatements, &$executionEnds, &$methodImpurePoints): void {
773: $nodeCallback($node, $scope);
774: if ($scope->getFunction() !== $methodScope->getFunction()) {
775: return;
776: }
777: if ($scope->isInAnonymousFunction()) {
778: return;
779: }
780: if ($node instanceof PropertyAssignNode) {
781: if (
782: $node->getPropertyFetch() instanceof Expr\PropertyFetch
783: && $scope->getFunction() instanceof PhpMethodFromParserNodeReflection
784: && $scope->getFunction()->getDeclaringClass()->hasConstructor()
785: && $scope->getFunction()->getDeclaringClass()->getConstructor()->getName() === $scope->getFunction()->getName()
786: && TypeUtils::findThisType($scope->getType($node->getPropertyFetch()->var)) !== null
787: ) {
788: return;
789: }
790: $methodImpurePoints[] = new ImpurePoint(
791: $scope,
792: $node,
793: 'propertyAssign',
794: 'property assignment',
795: true,
796: );
797: return;
798: }
799: if ($node instanceof ExecutionEndNode) {
800: $executionEnds[] = $node;
801: return;
802: }
803: if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) {
804: $gatheredYieldStatements[] = $node;
805: }
806: if (!$node instanceof Return_) {
807: return;
808: }
809:
810: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
811: }, StatementContext::createTopLevel())->toPublic();
812:
813: $methodReflection = $methodScope->getFunction();
814: if (!$methodReflection instanceof PhpMethodFromParserNodeReflection) {
815: throw new ShouldNotHappenException();
816: }
817:
818: $this->callNodeCallback($nodeCallback, new MethodReturnStatementsNode(
819: $stmt,
820: $gatheredReturnStatements,
821: $gatheredYieldStatements,
822: $statementResult,
823: $executionEnds,
824: array_merge($statementResult->getImpurePoints(), $methodImpurePoints),
825: $classReflection,
826: $methodReflection,
827: ), $methodScope, $storage);
828:
829: if ($isConstructor) {
830: $finalScope = null;
831:
832: foreach ($executionEnds as $executionEnd) {
833: if ($executionEnd->getStatementResult()->isAlwaysTerminating()) {
834: continue;
835: }
836:
837: $endScope = $executionEnd->getStatementResult()->getScope();
838: if ($finalScope === null) {
839: $finalScope = $endScope;
840: continue;
841: }
842:
843: $finalScope = $finalScope->mergeWith($endScope);
844: }
845:
846: foreach ($gatheredReturnStatements as $statement) {
847: if ($finalScope === null) {
848: $finalScope = $statement->getScope()->toMutatingScope();
849: continue;
850: }
851:
852: $finalScope = $finalScope->mergeWith($statement->getScope()->toMutatingScope());
853: }
854:
855: if ($finalScope !== null) {
856: $scope = $finalScope->rememberConstructorScope();
857: }
858:
859: }
860: }
861: if (!$scope->getClassReflection()->isAnonymous() && !$scope->isInAnonymousFunction()) {
862: $this->processPendingFibers($storage);
863: }
864: } elseif ($stmt instanceof Echo_) {
865: $hasYield = false;
866: $throwPoints = [];
867: $impurePoints = [];
868: $isAlwaysTerminating = false;
869: foreach ($stmt->exprs as $echoExpr) {
870: $result = $this->processExprNode($stmt, $echoExpr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
871: $throwPoints = array_merge($throwPoints, $result->getThrowPoints());
872: $impurePoints = array_merge($impurePoints, $result->getImpurePoints());
873: $toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($echoExpr, $scope);
874: $throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints());
875: $impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints());
876: $scope = $result->getScope();
877: $hasYield = $hasYield || $result->hasYield();
878: $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating();
879: }
880:
881: $throwPoints = $overridingThrowPoints ?? $throwPoints;
882: $impurePoints[] = new ImpurePoint($scope, $stmt, 'echo', 'echo', true);
883: return new InternalStatementResult($scope, $hasYield, $isAlwaysTerminating, [], $throwPoints, $impurePoints);
884: } elseif ($stmt instanceof Return_) {
885: if ($stmt->expr !== null) {
886: $result = $this->processExprNode($stmt, $stmt->expr, $stmtScope, $storage, $nodeCallback, ExpressionContext::createDeep());
887: $throwPoints = $result->getThrowPoints();
888: $impurePoints = $result->getImpurePoints();
889: $scope = $result->getScope();
890: $hasYield = $result->hasYield();
891: } else {
892: $hasYield = false;
893: $throwPoints = [];
894: $impurePoints = [];
895: }
896:
897: return new InternalStatementResult($scope, $hasYield, true, [
898: new InternalStatementExitPoint($stmt, $scope),
899: ], $overridingThrowPoints ?? $throwPoints, $impurePoints);
900: } elseif ($stmt instanceof Continue_ || $stmt instanceof Break_) {
901: if ($stmt->num !== null) {
902: $result = $this->processExprNode($stmt, $stmt->num, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
903: $scope = $result->getScope();
904: $hasYield = $result->hasYield();
905: $throwPoints = $result->getThrowPoints();
906: $impurePoints = $result->getImpurePoints();
907: } else {
908: $hasYield = false;
909: $throwPoints = [];
910: $impurePoints = [];
911: }
912:
913: return new InternalStatementResult($scope, $hasYield, true, [
914: new InternalStatementExitPoint($stmt, $scope),
915: ], $overridingThrowPoints ?? $throwPoints, $impurePoints);
916: } elseif ($stmt instanceof Node\Stmt\Expression) {
917: if ($stmt->expr instanceof Expr\Throw_) {
918: $scope = $stmtScope;
919: }
920: $earlyTerminationExpr = $this->findEarlyTerminatingExpr($stmt->expr, $scope);
921: $hasAssign = false;
922: $currentScope = $scope;
923: $result = $this->processExprNode($stmt, $stmt->expr, $scope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $currentScope, &$hasAssign): void {
924: if (
925: ($node instanceof VariableAssignNode || $node instanceof PropertyAssignNode)
926: && $scope->getAnonymousFunctionReflection() === $currentScope->getAnonymousFunctionReflection()
927: && $scope->getFunction() === $currentScope->getFunction()
928: ) {
929: $hasAssign = true;
930: }
931: $nodeCallback($node, $scope);
932: }, ExpressionContext::createTopLevel());
933: $throwPoints = array_filter($result->getThrowPoints(), static fn ($throwPoint) => $throwPoint->isExplicit());
934: if (
935: count($result->getImpurePoints()) === 0
936: && count($throwPoints) === 0
937: && !$stmt->expr instanceof Expr\PostInc
938: && !$stmt->expr instanceof Expr\PreInc
939: && !$stmt->expr instanceof Expr\PostDec
940: && !$stmt->expr instanceof Expr\PreDec
941: ) {
942: $this->callNodeCallback($nodeCallback, new NoopExpressionNode($stmt->expr, $hasAssign), $scope, $storage);
943: }
944: $scope = $result->getScope();
945: $scope = $scope->filterBySpecifiedTypes($this->typeSpecifier->specifyTypesInCondition(
946: $scope,
947: $stmt->expr,
948: TypeSpecifierContext::createNull(),
949: ));
950: $hasYield = $result->hasYield();
951: $throwPoints = $result->getThrowPoints();
952: $impurePoints = $result->getImpurePoints();
953: $isAlwaysTerminating = $result->isAlwaysTerminating();
954:
955: if ($earlyTerminationExpr !== null) {
956: return new InternalStatementResult($scope, $hasYield, true, [
957: new InternalStatementExitPoint($stmt, $scope),
958: ], $overridingThrowPoints ?? $throwPoints, $impurePoints);
959: }
960: return new InternalStatementResult($scope, $hasYield, $isAlwaysTerminating, [], $overridingThrowPoints ?? $throwPoints, $impurePoints);
961: } elseif ($stmt instanceof Node\Stmt\Namespace_) {
962: if ($stmt->name !== null) {
963: $scope = $scope->enterNamespace($stmt->name->toString());
964: } else {
965: $scope = $scope->enterNamespace('');
966: }
967:
968: $scope = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context)->getScope();
969: $hasYield = false;
970: $throwPoints = [];
971: $impurePoints = [];
972: } elseif ($stmt instanceof Node\Stmt\Trait_) {
973: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []);
974: } elseif ($stmt instanceof Node\Stmt\ClassLike) {
975: if (!$context->isTopLevel()) {
976: return new InternalStatementResult($scope, hasYield: false, isAlwaysTerminating: false, exitPoints: [], throwPoints: [], impurePoints: []);
977: }
978: $hasYield = false;
979: $throwPoints = [];
980: $impurePoints = [];
981: if (isset($stmt->namespacedName)) {
982: $classReflection = $this->getCurrentClassReflection($stmt, $stmt->namespacedName->toString(), $scope);
983: $classScope = $scope->enterClass($classReflection);
984: $this->callNodeCallback($nodeCallback, new InClassNode($stmt, $classReflection), $classScope, $storage);
985: } elseif ($stmt instanceof Class_) {
986: if ($stmt->name === null) {
987: throw new ShouldNotHappenException();
988: }
989: if (!$stmt->isAnonymous()) {
990: $classReflection = $this->reflectionProvider->getClass($stmt->name->toString());
991: } else {
992: $classReflection = $this->reflectionProvider->getAnonymousClassReflection($stmt, $scope);
993: }
994: $classScope = $scope->enterClass($classReflection);
995: $this->callNodeCallback($nodeCallback, new InClassNode($stmt, $classReflection), $classScope, $storage);
996: } else {
997: throw new ShouldNotHappenException();
998: }
999:
1000: $classStatementsGatherer = new ClassStatementsGatherer($classReflection, $nodeCallback);
1001: $this->processAttributeGroups($stmt, $stmt->attrGroups, $classScope, $storage, $classStatementsGatherer);
1002:
1003: $classLikeStatements = $stmt->stmts;
1004: // analyze static methods first; constructor next; instance methods and property hooks last so we can carry over the scope
1005: usort($classLikeStatements, static function ($a, $b) {
1006: if ($a instanceof Node\Stmt\Property) {
1007: return 1;
1008: }
1009: if ($b instanceof Node\Stmt\Property) {
1010: return -1;
1011: }
1012:
1013: if (!$a instanceof Node\Stmt\ClassMethod || !$b instanceof Node\Stmt\ClassMethod) {
1014: return 0;
1015: }
1016:
1017: return [!$a->isStatic(), $a->name->toLowerString() !== '__construct'] <=> [!$b->isStatic(), $b->name->toLowerString() !== '__construct'];
1018: });
1019:
1020: $this->processStmtNodesInternal($stmt, $classLikeStatements, $classScope, $storage, $classStatementsGatherer, $context);
1021: $this->callNodeCallback($nodeCallback, new ClassPropertiesNode($stmt, $this->readWritePropertiesExtensionProvider, $classStatementsGatherer->getProperties(), $classStatementsGatherer->getPropertyUsages(), $classStatementsGatherer->getMethodCalls(), $classStatementsGatherer->getReturnStatementsNodes(), $classStatementsGatherer->getPropertyAssigns(), $classReflection), $classScope, $storage);
1022: $this->callNodeCallback($nodeCallback, new ClassMethodsNode($stmt, $classStatementsGatherer->getMethods(), $classStatementsGatherer->getMethodCalls(), $classReflection), $classScope, $storage);
1023: $this->callNodeCallback($nodeCallback, new ClassConstantsNode($stmt, $classStatementsGatherer->getConstants(), $classStatementsGatherer->getConstantFetches(), $classReflection), $classScope, $storage);
1024: $classReflection->evictPrivateSymbols();
1025: $this->calledMethodResults = [];
1026: } elseif ($stmt instanceof Node\Stmt\Property) {
1027: $hasYield = false;
1028: $throwPoints = [];
1029: $impurePoints = [];
1030: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
1031:
1032: $nativePropertyType = $stmt->type !== null ? ParserNodeTypeToPHPStanType::resolve($stmt->type, $scope->getClassReflection()) : null;
1033:
1034: [,,,,,,,,,,,,$isReadOnly, $docComment, ,,,$varTags, $isAllowedPrivateMutation] = $this->getPhpDocs($scope, $stmt);
1035: $phpDocType = null;
1036: if (isset($varTags[0]) && count($varTags) === 1) {
1037: $phpDocType = $varTags[0]->getType();
1038: }
1039:
1040: foreach ($stmt->props as $prop) {
1041: $this->callNodeCallback($nodeCallback, $prop, $scope, $storage);
1042: if ($prop->default !== null) {
1043: $this->processExprNode($stmt, $prop->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
1044: }
1045:
1046: if (!$scope->isInClass()) {
1047: throw new ShouldNotHappenException();
1048: }
1049: $propertyName = $prop->name->toString();
1050:
1051: if ($phpDocType === null) {
1052: if (isset($varTags[$propertyName])) {
1053: $phpDocType = $varTags[$propertyName]->getType();
1054: }
1055: }
1056:
1057: $propStmt = clone $stmt;
1058: $propStmt->setAttributes($prop->getAttributes());
1059: $propStmt->setAttribute('originalPropertyStmt', $stmt);
1060: $this->callNodeCallback(
1061: $nodeCallback,
1062: new ClassPropertyNode(
1063: $propertyName,
1064: $stmt->flags,
1065: $nativePropertyType,
1066: $prop->default,
1067: $docComment,
1068: $phpDocType,
1069: false,
1070: false,
1071: $propStmt,
1072: $isReadOnly,
1073: $scope->isInTrait(),
1074: $scope->getClassReflection()->isReadOnly(),
1075: $isAllowedPrivateMutation,
1076: $scope->getClassReflection(),
1077: ),
1078: $scope,
1079: $storage,
1080: );
1081: }
1082:
1083: if (count($stmt->hooks) > 0) {
1084: if (!isset($propertyName)) {
1085: throw new ShouldNotHappenException('Property name should be known when analysing hooks.');
1086: }
1087: $this->processPropertyHooks(
1088: $stmt,
1089: $stmt->type,
1090: $phpDocType,
1091: $propertyName,
1092: $stmt->hooks,
1093: $scope,
1094: $storage,
1095: $nodeCallback,
1096: );
1097: }
1098:
1099: if ($stmt->type !== null) {
1100: $this->callNodeCallback($nodeCallback, $stmt->type, $scope, $storage);
1101: }
1102: } elseif ($stmt instanceof If_) {
1103: $conditionType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean();
1104: $ifAlwaysTrue = $conditionType->isTrue()->yes();
1105: $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
1106: $exitPoints = [];
1107: $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints();
1108: $impurePoints = $condResult->getImpurePoints();
1109: $endStatements = [];
1110: $finalScope = null;
1111: $alwaysTerminating = true;
1112: $hasYield = $condResult->hasYield();
1113:
1114: $branchScopeStatementResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $condResult->getTruthyScope(), $storage, $nodeCallback, $context);
1115:
1116: if (!$conditionType->isTrue()->no()) {
1117: $exitPoints = $branchScopeStatementResult->getExitPoints();
1118: $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints());
1119: $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints());
1120: $branchScope = $branchScopeStatementResult->getScope();
1121: $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? null : $branchScope;
1122: $alwaysTerminating = $branchScopeStatementResult->isAlwaysTerminating();
1123: if (count($branchScopeStatementResult->getEndStatements()) > 0) {
1124: $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements());
1125: } elseif (count($stmt->stmts) > 0) {
1126: $endStatements[] = new InternalEndStatementResult($stmt->stmts[count($stmt->stmts) - 1], $branchScopeStatementResult);
1127: } else {
1128: $endStatements[] = new InternalEndStatementResult($stmt, $branchScopeStatementResult);
1129: }
1130: $hasYield = $branchScopeStatementResult->hasYield() || $hasYield;
1131: }
1132:
1133: $scope = $condResult->getFalseyScope();
1134: $lastElseIfConditionIsTrue = false;
1135:
1136: $condScope = $scope;
1137: foreach ($stmt->elseifs as $elseif) {
1138: $this->callNodeCallback($nodeCallback, $elseif, $scope, $storage);
1139: $elseIfConditionType = ($this->treatPhpDocTypesAsCertain ? $condScope->getType($elseif->cond) : $scope->getNativeType($elseif->cond))->toBoolean();
1140: $condResult = $this->processExprNode($stmt, $elseif->cond, $condScope, $storage, $nodeCallback, ExpressionContext::createDeep());
1141: $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints());
1142: $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints());
1143: $condScope = $condResult->getScope();
1144: $branchScopeStatementResult = $this->processStmtNodesInternal($elseif, $elseif->stmts, $condResult->getTruthyScope(), $storage, $nodeCallback, $context);
1145:
1146: if (
1147: !$ifAlwaysTrue
1148: && !$lastElseIfConditionIsTrue
1149: && !$elseIfConditionType->isTrue()->no()
1150: ) {
1151: $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints());
1152: $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints());
1153: $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints());
1154: $branchScope = $branchScopeStatementResult->getScope();
1155: $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope, true);
1156: $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating();
1157: if (count($branchScopeStatementResult->getEndStatements()) > 0) {
1158: $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements());
1159: } elseif (count($elseif->stmts) > 0) {
1160: $endStatements[] = new InternalEndStatementResult($elseif->stmts[count($elseif->stmts) - 1], $branchScopeStatementResult);
1161: } else {
1162: $endStatements[] = new InternalEndStatementResult($elseif, $branchScopeStatementResult);
1163: }
1164: $hasYield = $hasYield || $branchScopeStatementResult->hasYield();
1165: }
1166:
1167: if (
1168: $elseIfConditionType->isTrue()->yes()
1169: ) {
1170: $lastElseIfConditionIsTrue = true;
1171: }
1172:
1173: $condScope = $condScope->filterByFalseyValue($elseif->cond);
1174: $scope = $condScope;
1175: }
1176:
1177: if ($stmt->else === null) {
1178: if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) {
1179: $finalScope = $scope->mergeWith($finalScope, true);
1180: $alwaysTerminating = false;
1181: }
1182: } else {
1183: $this->callNodeCallback($nodeCallback, $stmt->else, $scope, $storage);
1184: $branchScopeStatementResult = $this->processStmtNodesInternal($stmt->else, $stmt->else->stmts, $scope, $storage, $nodeCallback, $context);
1185:
1186: if (!$ifAlwaysTrue && !$lastElseIfConditionIsTrue) {
1187: $exitPoints = array_merge($exitPoints, $branchScopeStatementResult->getExitPoints());
1188: $throwPoints = array_merge($throwPoints, $branchScopeStatementResult->getThrowPoints());
1189: $impurePoints = array_merge($impurePoints, $branchScopeStatementResult->getImpurePoints());
1190: $branchScope = $branchScopeStatementResult->getScope();
1191: $finalScope = $branchScopeStatementResult->isAlwaysTerminating() ? $finalScope : $branchScope->mergeWith($finalScope, true);
1192: $alwaysTerminating = $alwaysTerminating && $branchScopeStatementResult->isAlwaysTerminating();
1193: if (count($branchScopeStatementResult->getEndStatements()) > 0) {
1194: $endStatements = array_merge($endStatements, $branchScopeStatementResult->getEndStatements());
1195: } elseif (count($stmt->else->stmts) > 0) {
1196: $endStatements[] = new InternalEndStatementResult($stmt->else->stmts[count($stmt->else->stmts) - 1], $branchScopeStatementResult);
1197: } else {
1198: $endStatements[] = new InternalEndStatementResult($stmt->else, $branchScopeStatementResult);
1199: }
1200: $hasYield = $hasYield || $branchScopeStatementResult->hasYield();
1201: }
1202: }
1203:
1204: if ($finalScope === null) {
1205: $finalScope = $scope;
1206: }
1207:
1208: if ($stmt->else === null && !$ifAlwaysTrue && !$lastElseIfConditionIsTrue) {
1209: $endStatements[] = new InternalEndStatementResult($stmt, new InternalStatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints));
1210: }
1211:
1212: return new InternalStatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, $throwPoints, $impurePoints, $endStatements);
1213: } elseif ($stmt instanceof Node\Stmt\TraitUse) {
1214: $hasYield = false;
1215: $throwPoints = [];
1216: $impurePoints = [];
1217:
1218: $traitStorage = $storage->duplicate();
1219: $traitStorage->pendingFibers = [];
1220: $this->processTraitUse($stmt, $scope, $traitStorage, $nodeCallback);
1221: $this->processPendingFibers($traitStorage);
1222: } elseif ($stmt instanceof Foreach_) {
1223: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
1224: $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt);
1225: }
1226: $condResult = $this->processExprNode($stmt, $stmt->expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
1227: $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints();
1228: $impurePoints = $condResult->getImpurePoints();
1229: $scope = $condResult->getScope();
1230: $arrayComparisonExpr = new BinaryOp\NotIdentical(
1231: $stmt->expr,
1232: new Array_([]),
1233: );
1234: $this->callNodeCallback($nodeCallback, new InForeachNode($stmt), $scope, $storage);
1235: $originalScope = $scope;
1236: $bodyScope = $scope;
1237:
1238: if ($stmt->keyVar instanceof Variable) {
1239: $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->keyVar, new GetIterableKeyTypeExpr($stmt->expr)), $originalScope, $storage);
1240: }
1241:
1242: if ($stmt->valueVar instanceof Variable) {
1243: $this->callNodeCallback($nodeCallback, new VariableAssignNode($stmt->valueVar, new GetIterableValueTypeExpr($stmt->expr)), $originalScope, $storage);
1244: } elseif ($stmt->valueVar instanceof List_) {
1245: $virtualAssign = new Assign($stmt->valueVar, new GetIterableValueTypeExpr($stmt->expr));
1246: $virtualAssign->setAttributes($stmt->valueVar->getAttributes());
1247: $this->callNodeCallback($nodeCallback, $virtualAssign, $scope, $storage);
1248: }
1249:
1250: $originalStorage = $storage;
1251: $unrolledEndScope = null;
1252: if ($context->isTopLevel()) {
1253: $storage = $originalStorage->duplicate();
1254:
1255: $originalScope = $this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope;
1256: $unrolledResult = $this->tryProcessUnrolledConstantArrayForeach($stmt, $originalScope, $originalStorage, $context);
1257: if ($unrolledResult !== null) {
1258: $bodyScope = $unrolledResult['bodyScope'];
1259: $unrolledEndScope = $unrolledResult['endScope'];
1260: } else {
1261: $bodyScope = $this->enterForeach($originalScope, $storage, $originalScope, $stmt, $nodeCallback);
1262: $count = 0;
1263: do {
1264: $prevScope = $bodyScope;
1265: $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope);
1266: $storage = $originalStorage->duplicate();
1267: $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $nodeCallback);
1268: $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints();
1269: $bodyScope = $bodyScopeResult->getScope();
1270: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1271: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1272: }
1273: if ($bodyScope->equals($prevScope)) {
1274: break;
1275: }
1276:
1277: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
1278: $bodyScope = $prevScope->generalizeWith($bodyScope);
1279: }
1280: $count++;
1281: } while ($count < self::LOOP_SCOPE_ITERATIONS);
1282: }
1283: }
1284:
1285: $bodyScope = $bodyScope->mergeWith($this->polluteScopeWithAlwaysIterableForeach ? $scope->filterByTruthyValue($arrayComparisonExpr) : $scope);
1286: $storage = $originalStorage;
1287: $bodyScope = $this->enterForeach($bodyScope, $storage, $originalScope, $stmt, $nodeCallback);
1288: $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints();
1289: $finalScope = $finalScopeResult->getScope();
1290: $scopesWithIterableValueType = [];
1291:
1292: $originalKeyVarExpr = null;
1293: $continueExitPointHasUnoriginalKeyType = false;
1294: if ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)) {
1295: $originalKeyVarExpr = new OriginalForeachKeyExpr($stmt->keyVar->name);
1296: if ($finalScope->hasExpressionType($originalKeyVarExpr)->yes()) {
1297: $scopesWithIterableValueType[] = $finalScope;
1298: } else {
1299: $continueExitPointHasUnoriginalKeyType = true;
1300: }
1301: }
1302:
1303: foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1304: $continueScope = $continueExitPoint->getScope();
1305: $finalScope = $continueScope->mergeWith($finalScope);
1306: if ($originalKeyVarExpr === null || !$continueScope->hasExpressionType($originalKeyVarExpr)->yes()) {
1307: $continueExitPointHasUnoriginalKeyType = true;
1308: continue;
1309: }
1310: $scopesWithIterableValueType[] = $continueScope;
1311: }
1312: $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class);
1313: foreach ($breakExitPoints as $breakExitPoint) {
1314: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1315: }
1316:
1317: if ($unrolledEndScope !== null) {
1318: $finalScope = $unrolledEndScope;
1319: }
1320:
1321: $exprType = $scope->getType($stmt->expr);
1322: $hasExpr = $scope->hasExpressionType($stmt->expr);
1323: if (
1324: count($breakExitPoints) === 0
1325: && count($scopesWithIterableValueType) > 0
1326: && !$continueExitPointHasUnoriginalKeyType
1327: && $stmt->keyVar !== null
1328: && (!$hasExpr->no() || !$stmt->expr instanceof Variable)
1329: && $exprType->isArray()->yes()
1330: && $exprType->isConstantArray()->no()
1331: ) {
1332: $arrayExprDimFetch = new ArrayDimFetch($stmt->expr, $stmt->keyVar);
1333: $arrayDimFetchLoopTypes = [];
1334: $keyLoopTypes = [];
1335: foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) {
1336: $arrayDimFetchLoopTypes[] = $scopeWithIterableValueType->getType($arrayExprDimFetch);
1337: $keyLoopTypes[] = $scopeWithIterableValueType->getType($stmt->keyVar);
1338: }
1339:
1340: $arrayDimFetchLoopType = TypeCombinator::union(...$arrayDimFetchLoopTypes);
1341: $keyLoopType = TypeCombinator::union(...$keyLoopTypes);
1342:
1343: $arrayDimFetchLoopNativeTypes = [];
1344: $keyLoopNativeTypes = [];
1345: foreach ($scopesWithIterableValueType as $scopeWithIterableValueType) {
1346: $arrayDimFetchLoopNativeTypes[] = $scopeWithIterableValueType->getNativeType($arrayExprDimFetch);
1347: $keyLoopNativeTypes[] = $scopeWithIterableValueType->getNativeType($stmt->keyVar);
1348: }
1349:
1350: $arrayDimFetchLoopNativeType = TypeCombinator::union(...$arrayDimFetchLoopNativeTypes);
1351: $keyLoopNativeType = TypeCombinator::union(...$keyLoopNativeTypes);
1352:
1353: $valueTypeChanged = !$arrayDimFetchLoopType->equals($exprType->getIterableValueType());
1354: $keyTypeChanged = !$keyLoopType->equals($exprType->getIterableKeyType());
1355:
1356: if ($valueTypeChanged || $keyTypeChanged) {
1357: $newExprType = TypeTraverser::map($exprType, static function (Type $type, callable $traverse) use ($arrayDimFetchLoopType, $keyLoopType, $valueTypeChanged, $keyTypeChanged): Type {
1358: if ($type instanceof UnionType || $type instanceof IntersectionType) {
1359: return $traverse($type);
1360: }
1361:
1362: if (!$type instanceof ArrayType) {
1363: return $type;
1364: }
1365:
1366: return new ArrayType(
1367: $keyTypeChanged ? $keyLoopType : $type->getKeyType(),
1368: $valueTypeChanged ? $arrayDimFetchLoopType : $type->getIterableValueType(),
1369: );
1370: });
1371: $nativeExprType = $scope->getNativeType($stmt->expr);
1372: $newExprNativeType = TypeTraverser::map($nativeExprType, static function (Type $type, callable $traverse) use ($arrayDimFetchLoopNativeType, $keyLoopNativeType, $valueTypeChanged, $keyTypeChanged): Type {
1373: if ($type instanceof UnionType || $type instanceof IntersectionType) {
1374: return $traverse($type);
1375: }
1376:
1377: if (!$type instanceof ArrayType) {
1378: return $type;
1379: }
1380:
1381: return new ArrayType(
1382: $keyTypeChanged ? $keyLoopNativeType : $type->getKeyType(),
1383: $valueTypeChanged ? $arrayDimFetchLoopNativeType : $type->getIterableValueType(),
1384: );
1385: });
1386:
1387: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
1388: $finalScope = $finalScope->assignVariable(
1389: $stmt->expr->name,
1390: $newExprType,
1391: $newExprNativeType,
1392: $hasExpr,
1393: );
1394: } else {
1395: $finalScope = $finalScope->assignExpression(
1396: $stmt->expr,
1397: $newExprType,
1398: $newExprNativeType,
1399: );
1400: }
1401: }
1402: }
1403:
1404: $isIterableAtLeastOnce = $exprType->isIterableAtLeastOnce();
1405: if ($isIterableAtLeastOnce->maybe() || $exprType->isIterable()->no()) {
1406: $finalScope = $finalScope->mergeWith($scope->filterByTruthyValue(new BooleanOr(
1407: new BinaryOp\Identical(
1408: $stmt->expr,
1409: new Array_([]),
1410: ),
1411: new FuncCall(new Name\FullyQualified('is_object'), [
1412: new Arg($stmt->expr),
1413: ]),
1414: )));
1415: } elseif ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) {
1416: $finalScope = $scope;
1417: } elseif (!$this->polluteScopeWithAlwaysIterableForeach) {
1418: $finalScope = $scope->processAlwaysIterableForeachScopeWithoutPollute($finalScope);
1419: // get types from finalScope, but don't create new variables
1420: }
1421:
1422: if (!$isIterableAtLeastOnce->no()) {
1423: $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints());
1424: $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints());
1425: }
1426: if (!(new ObjectType(Traversable::class))->isSuperTypeOf($scope->getType($stmt->expr))->no()) {
1427: $throwPoints[] = InternalThrowPoint::createImplicit($scope, $stmt->expr);
1428: }
1429: if ($context->isTopLevel() && $stmt->byRef) {
1430: $finalScope = $finalScope->assignExpression(new ForeachValueByRefExpr($stmt->valueVar), new MixedType(), new MixedType());
1431: }
1432:
1433: return new InternalStatementResult(
1434: $finalScope,
1435: $finalScopeResult->hasYield() || $condResult->hasYield(),
1436: $isIterableAtLeastOnce->yes() && $finalScopeResult->isAlwaysTerminating(),
1437: $finalScopeResult->getExitPointsForOuterLoop(),
1438: $throwPoints,
1439: $impurePoints,
1440: );
1441: } elseif ($stmt instanceof While_) {
1442: $originalStorage = $storage;
1443: $storage = $originalStorage->duplicate();
1444: $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep());
1445: $beforeCondBooleanType = ($this->treatPhpDocTypesAsCertain ? $scope->getType($stmt->cond) : $scope->getNativeType($stmt->cond))->toBoolean();
1446: $condScope = $condResult->getFalseyScope();
1447: if (!$context->isTopLevel() && $beforeCondBooleanType->isFalse()->yes()) {
1448: if (!$this->polluteScopeWithLoopInitialAssignments) {
1449: $scope = $condScope->mergeWith($scope);
1450: }
1451:
1452: return new InternalStatementResult(
1453: $scope,
1454: $condResult->hasYield(),
1455: false,
1456: [],
1457: $condResult->getThrowPoints(),
1458: $condResult->getImpurePoints(),
1459: );
1460: }
1461: $bodyScope = $condResult->getTruthyScope();
1462:
1463: if ($context->isTopLevel()) {
1464: $count = 0;
1465: do {
1466: $prevScope = $bodyScope;
1467: $bodyScope = $bodyScope->mergeWith($scope);
1468: $storage = $originalStorage->duplicate();
1469: $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep())->getTruthyScope();
1470: $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints();
1471: $bodyScope = $bodyScopeResult->getScope();
1472: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1473: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1474: }
1475: if ($bodyScope->equals($prevScope)) {
1476: break;
1477: }
1478:
1479: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
1480: $bodyScope = $prevScope->generalizeWith($bodyScope);
1481: }
1482: $count++;
1483: } while ($count < self::LOOP_SCOPE_ITERATIONS);
1484: }
1485:
1486: $bodyScope = $bodyScope->mergeWith($scope);
1487: $bodyScopeMaybeRan = $bodyScope;
1488: $storage = $originalStorage;
1489: $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope();
1490: $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints();
1491: $finalScope = $finalScopeResult->getScope()->filterByFalseyValue($stmt->cond);
1492:
1493: $alwaysIterates = false;
1494: $neverIterates = false;
1495: if ($context->isTopLevel()) {
1496: $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScopeMaybeRan->getType($stmt->cond) : $bodyScopeMaybeRan->getNativeType($stmt->cond))->toBoolean();
1497: $alwaysIterates = $condBooleanType->isTrue()->yes();
1498: $neverIterates = $condBooleanType->isFalse()->yes();
1499: }
1500: if (!$alwaysIterates) {
1501: foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1502: $finalScope = $finalScope->mergeWith($continueExitPoint->getScope());
1503: }
1504: }
1505:
1506: $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class);
1507: if (count($breakExitPoints) > 0) {
1508: $breakScope = $alwaysIterates ? null : $finalScope;
1509: foreach ($breakExitPoints as $breakExitPoint) {
1510: $breakScope = $breakScope === null ? $breakExitPoint->getScope() : $breakScope->mergeWith($breakExitPoint->getScope());
1511: }
1512: $finalScope = $breakScope;
1513: }
1514:
1515: $isIterableAtLeastOnce = $beforeCondBooleanType->isTrue()->yes();
1516: $this->callNodeCallback($nodeCallback, new BreaklessWhileLoopNode($stmt, $finalScopeResult->toPublic()->getExitPoints(), $finalScopeResult->hasYield()), $bodyScopeMaybeRan, $storage);
1517:
1518: if ($alwaysIterates) {
1519: $isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0;
1520: } elseif ($isIterableAtLeastOnce) {
1521: $isAlwaysTerminating = $finalScopeResult->isAlwaysTerminating();
1522: } else {
1523: $isAlwaysTerminating = false;
1524: }
1525: if (!$isIterableAtLeastOnce) {
1526: if (!$this->polluteScopeWithLoopInitialAssignments) {
1527: $condScope = $condScope->mergeWith($scope);
1528: }
1529: $finalScope = $finalScope->mergeWith($condScope);
1530: }
1531:
1532: $throwPoints = $overridingThrowPoints ?? $condResult->getThrowPoints();
1533: $impurePoints = $condResult->getImpurePoints();
1534: if (!$neverIterates) {
1535: $throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints());
1536: $impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints());
1537: }
1538:
1539: return new InternalStatementResult(
1540: $finalScope,
1541: $finalScopeResult->hasYield() || $condResult->hasYield(),
1542: $isAlwaysTerminating,
1543: $finalScopeResult->getExitPointsForOuterLoop(),
1544: $throwPoints,
1545: $impurePoints,
1546: );
1547: } elseif ($stmt instanceof Do_) {
1548: $finalScope = null;
1549: $bodyScope = $scope;
1550: $count = 0;
1551: $hasYield = false;
1552: $throwPoints = [];
1553: $impurePoints = [];
1554: $originalStorage = $storage;
1555:
1556: if ($context->isTopLevel()) {
1557: do {
1558: $prevScope = $bodyScope;
1559: $bodyScope = $bodyScope->mergeWith($scope);
1560: $storage = $originalStorage->duplicate();
1561: $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints();
1562: $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating();
1563: $bodyScope = $bodyScopeResult->getScope();
1564: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1565: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1566: }
1567: $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope);
1568: foreach ($bodyScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
1569: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1570: }
1571: $bodyScope = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep())->getTruthyScope();
1572: if ($bodyScope->equals($prevScope)) {
1573: break;
1574: }
1575:
1576: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
1577: $bodyScope = $prevScope->generalizeWith($bodyScope);
1578: }
1579: $count++;
1580: } while ($count < self::LOOP_SCOPE_ITERATIONS);
1581:
1582: $bodyScope = $bodyScope->mergeWith($scope);
1583: }
1584:
1585: $storage = $originalStorage;
1586: $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints();
1587: $bodyScope = $bodyScopeResult->getScope();
1588: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1589: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1590: }
1591:
1592: $alwaysIterates = false;
1593: if ($context->isTopLevel()) {
1594: $condBooleanType = ($this->treatPhpDocTypesAsCertain ? $bodyScope->getType($stmt->cond) : $bodyScope->getNativeType($stmt->cond))->toBoolean();
1595: $alwaysIterates = $condBooleanType->isTrue()->yes();
1596: }
1597:
1598: $this->callNodeCallback($nodeCallback, new DoWhileLoopConditionNode($stmt->cond, $bodyScopeResult->toPublic()->getExitPoints(), $bodyScopeResult->hasYield()), $bodyScope, $storage);
1599:
1600: if ($alwaysIterates) {
1601: $alwaysTerminating = count($bodyScopeResult->getExitPointsByType(Break_::class)) === 0;
1602: } else {
1603: $alwaysTerminating = $bodyScopeResult->isAlwaysTerminating();
1604: }
1605: $finalScope = $alwaysTerminating ? $finalScope : $bodyScope->mergeWith($finalScope);
1606: if ($finalScope === null) {
1607: $finalScope = $scope;
1608: }
1609: if (!$alwaysTerminating) {
1610: $condResult = $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep());
1611: $hasYield = $condResult->hasYield();
1612: $throwPoints = $condResult->getThrowPoints();
1613: $impurePoints = $condResult->getImpurePoints();
1614: $finalScope = $condResult->getFalseyScope();
1615: } else {
1616: $this->processExprNode($stmt, $stmt->cond, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep());
1617: }
1618:
1619: $breakExitPoints = $bodyScopeResult->getExitPointsByType(Break_::class);
1620: if (count($breakExitPoints) > 0) {
1621: $breakScope = $alwaysIterates ? null : $finalScope;
1622: foreach ($breakExitPoints as $breakExitPoint) {
1623: $breakScope = $breakScope === null ? $breakExitPoint->getScope() : $breakScope->mergeWith($breakExitPoint->getScope());
1624: }
1625: $finalScope = $breakScope;
1626: }
1627:
1628: return new InternalStatementResult(
1629: $finalScope,
1630: $bodyScopeResult->hasYield() || $hasYield,
1631: $alwaysTerminating,
1632: $bodyScopeResult->getExitPointsForOuterLoop(),
1633: array_merge($throwPoints, $bodyScopeResult->getThrowPoints()),
1634: array_merge($impurePoints, $bodyScopeResult->getImpurePoints()),
1635: );
1636: } elseif ($stmt instanceof For_) {
1637: $initScope = $scope;
1638: $hasYield = false;
1639: $throwPoints = [];
1640: $impurePoints = [];
1641: foreach ($stmt->init as $initExpr) {
1642: $initResult = $this->processExprNode($stmt, $initExpr, $initScope, $storage, $nodeCallback, ExpressionContext::createTopLevel());
1643: $initScope = $initResult->getScope();
1644: $hasYield = $hasYield || $initResult->hasYield();
1645: $throwPoints = array_merge($throwPoints, $initResult->getThrowPoints());
1646: $impurePoints = array_merge($impurePoints, $initResult->getImpurePoints());
1647: }
1648:
1649: $originalStorage = $storage;
1650:
1651: $bodyScope = $initScope;
1652: $isIterableAtLeastOnce = TrinaryLogic::createYes();
1653: $lastCondExpr = array_last($stmt->cond) ?? null;
1654: if (count($stmt->cond) > 0) {
1655: $storage = $originalStorage->duplicate();
1656:
1657: foreach ($stmt->cond as $condExpr) {
1658: $condResult = $this->processExprNode($stmt, $condExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep());
1659: $initScope = $condResult->getScope();
1660: $condResultScope = $condResult->getScope();
1661:
1662: // only the last condition expression is relevant whether the loop continues
1663: // see https://www.php.net/manual/en/control-structures.for.php
1664: if ($condExpr === $lastCondExpr) {
1665: $condTruthiness = ($this->treatPhpDocTypesAsCertain ? $condResultScope->getType($condExpr) : $condResultScope->getNativeType($condExpr))->toBoolean();
1666: $isIterableAtLeastOnce = $isIterableAtLeastOnce->and($condTruthiness->isTrue());
1667: }
1668:
1669: $hasYield = $hasYield || $condResult->hasYield();
1670: $throwPoints = array_merge($throwPoints, $condResult->getThrowPoints());
1671: $impurePoints = array_merge($impurePoints, $condResult->getImpurePoints());
1672: $bodyScope = $condResult->getTruthyScope();
1673: }
1674: }
1675:
1676: if ($context->isTopLevel()) {
1677: $count = 0;
1678: do {
1679: $prevScope = $bodyScope;
1680: $storage = $originalStorage->duplicate();
1681: $bodyScope = $bodyScope->mergeWith($initScope);
1682: if ($lastCondExpr !== null) {
1683: $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createDeep())->getTruthyScope();
1684: }
1685: $bodyScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, new NoopNodeCallback(), $context->enterDeep())->filterOutLoopExitPoints();
1686: $bodyScope = $bodyScopeResult->getScope();
1687: foreach ($bodyScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1688: $bodyScope = $bodyScope->mergeWith($continueExitPoint->getScope());
1689: }
1690:
1691: foreach ($stmt->loop as $loopExpr) {
1692: $exprResult = $this->processExprNode($stmt, $loopExpr, $bodyScope, $storage, new NoopNodeCallback(), ExpressionContext::createTopLevel());
1693: $bodyScope = $exprResult->getScope();
1694: $hasYield = $hasYield || $exprResult->hasYield();
1695: $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
1696: $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints());
1697: }
1698:
1699: if ($bodyScope->equals($prevScope)) {
1700: break;
1701: }
1702:
1703: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
1704: $bodyScope = $prevScope->generalizeWith($bodyScope);
1705: }
1706: $count++;
1707: } while ($count < self::LOOP_SCOPE_ITERATIONS);
1708: }
1709:
1710: $storage = $originalStorage;
1711: $bodyScope = $bodyScope->mergeWith($initScope);
1712:
1713: $alwaysIterates = TrinaryLogic::createFromBoolean($context->isTopLevel());
1714: if ($lastCondExpr !== null) {
1715: $alwaysIterates = $alwaysIterates->and($bodyScope->getType($lastCondExpr)->toBoolean()->isTrue());
1716: $bodyScope = $this->processExprNode($stmt, $lastCondExpr, $bodyScope, $storage, $nodeCallback, ExpressionContext::createDeep())->getTruthyScope();
1717: $bodyScope = $this->inferForLoopExpressions($stmt, $lastCondExpr, $bodyScope);
1718: }
1719:
1720: $finalScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $bodyScope, $storage, $nodeCallback, $context)->filterOutLoopExitPoints();
1721: $finalScope = $finalScopeResult->getScope();
1722: foreach ($finalScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1723: $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
1724: }
1725:
1726: $loopScope = $finalScope;
1727: foreach ($stmt->loop as $loopExpr) {
1728: $loopScope = $this->processExprNode($stmt, $loopExpr, $loopScope, $storage, $nodeCallback, ExpressionContext::createTopLevel())->getScope();
1729: }
1730: $finalScope = $finalScope->generalizeWith($loopScope);
1731:
1732: if ($lastCondExpr !== null) {
1733: $finalScope = $finalScope->filterByFalseyValue($lastCondExpr);
1734: }
1735:
1736: $breakExitPoints = $finalScopeResult->getExitPointsByType(Break_::class);
1737: if (count($breakExitPoints) > 0) {
1738: $breakScope = $alwaysIterates->yes() ? null : $finalScope;
1739: foreach ($breakExitPoints as $breakExitPoint) {
1740: $breakScope = $breakScope === null ? $breakExitPoint->getScope() : $breakScope->mergeWith($breakExitPoint->getScope());
1741: }
1742: $finalScope = $breakScope;
1743: }
1744:
1745: if ($isIterableAtLeastOnce->no() || $finalScopeResult->isAlwaysTerminating()) {
1746: if ($this->polluteScopeWithLoopInitialAssignments) {
1747: $finalScope = $initScope;
1748: } else {
1749: $finalScope = $scope;
1750: }
1751:
1752: } elseif ($isIterableAtLeastOnce->maybe()) {
1753: if ($this->polluteScopeWithLoopInitialAssignments) {
1754: $finalScope = $finalScope->mergeWith($initScope);
1755: } else {
1756: $finalScope = $finalScope->mergeWith($scope);
1757: }
1758: } else {
1759: if (!$this->polluteScopeWithLoopInitialAssignments) {
1760: $finalScope = $finalScope->mergeWith($scope);
1761: }
1762: }
1763:
1764: if ($alwaysIterates->yes()) {
1765: $isAlwaysTerminating = count($finalScopeResult->getExitPointsByType(Break_::class)) === 0;
1766: } elseif ($isIterableAtLeastOnce->yes()) {
1767: $isAlwaysTerminating = $finalScopeResult->isAlwaysTerminating();
1768: } else {
1769: $isAlwaysTerminating = false;
1770: }
1771:
1772: return new InternalStatementResult(
1773: $finalScope,
1774: $finalScopeResult->hasYield() || $hasYield,
1775: $isAlwaysTerminating,
1776: $finalScopeResult->getExitPointsForOuterLoop(),
1777: array_merge($throwPoints, $finalScopeResult->getThrowPoints()),
1778: array_merge($impurePoints, $finalScopeResult->getImpurePoints()),
1779: );
1780: } elseif ($stmt instanceof Switch_) {
1781: $condResult = $this->processExprNode($stmt, $stmt->cond, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
1782: $scope = $condResult->getScope();
1783: $scopeForBranches = $scope;
1784: $finalScope = null;
1785: $prevScope = null;
1786: $hasDefaultCase = false;
1787: $alwaysTerminating = true;
1788: $hasYield = $condResult->hasYield();
1789: $exitPointsForOuterLoop = [];
1790: $throwPoints = $condResult->getThrowPoints();
1791: $impurePoints = $condResult->getImpurePoints();
1792: $fullCondExpr = null;
1793: foreach ($stmt->cases as $caseNode) {
1794: if ($caseNode->cond !== null) {
1795: $condExpr = new BinaryOp\Equal($stmt->cond, $caseNode->cond);
1796: $fullCondExpr = $fullCondExpr === null ? $condExpr : new BooleanOr($fullCondExpr, $condExpr);
1797: $caseResult = $this->processExprNode($stmt, $caseNode->cond, $scopeForBranches, $storage, $nodeCallback, ExpressionContext::createDeep());
1798: $scopeForBranches = $caseResult->getScope();
1799: $hasYield = $hasYield || $caseResult->hasYield();
1800: $throwPoints = array_merge($throwPoints, $caseResult->getThrowPoints());
1801: $impurePoints = array_merge($impurePoints, $caseResult->getImpurePoints());
1802: $branchScope = $caseResult->getTruthyScope()->filterByTruthyValue($condExpr);
1803: } else {
1804: $hasDefaultCase = true;
1805: $fullCondExpr = null;
1806: $branchScope = $scopeForBranches;
1807: }
1808:
1809: $branchScope = $branchScope->mergeWith($prevScope);
1810: $branchScopeResult = $this->processStmtNodesInternal($caseNode, $caseNode->stmts, $branchScope, $storage, $nodeCallback, $context);
1811: $branchScope = $branchScopeResult->getScope();
1812: $branchFinalScopeResult = $branchScopeResult->filterOutLoopExitPoints();
1813: $hasYield = $hasYield || $branchFinalScopeResult->hasYield();
1814: foreach ($branchScopeResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
1815: $alwaysTerminating = false;
1816: $finalScope = $breakExitPoint->getScope()->mergeWith($finalScope);
1817: }
1818: foreach ($branchScopeResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
1819: $finalScope = $continueExitPoint->getScope()->mergeWith($finalScope);
1820: }
1821: $exitPointsForOuterLoop = array_merge($exitPointsForOuterLoop, $branchFinalScopeResult->getExitPointsForOuterLoop());
1822: $throwPoints = array_merge($throwPoints, $branchFinalScopeResult->getThrowPoints());
1823: $impurePoints = array_merge($impurePoints, $branchFinalScopeResult->getImpurePoints());
1824: if ($branchScopeResult->isAlwaysTerminating()) {
1825: $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating();
1826: $prevScope = null;
1827: if (isset($fullCondExpr)) {
1828: $scopeForBranches = $scopeForBranches->filterByFalseyValue($fullCondExpr);
1829: $fullCondExpr = null;
1830: }
1831: if (!$branchFinalScopeResult->isAlwaysTerminating()) {
1832: $finalScope = $branchScope->mergeWith($finalScope);
1833: }
1834: } else {
1835: $prevScope = $branchScope;
1836: }
1837: }
1838:
1839: $exhaustive = $scopeForBranches->getType($stmt->cond) instanceof NeverType;
1840:
1841: if (!$hasDefaultCase && !$exhaustive) {
1842: $alwaysTerminating = false;
1843: }
1844:
1845: if ($prevScope !== null && isset($branchFinalScopeResult)) {
1846: $finalScope = $prevScope->mergeWith($finalScope);
1847: $alwaysTerminating = $alwaysTerminating && $branchFinalScopeResult->isAlwaysTerminating();
1848: }
1849:
1850: if ((!$hasDefaultCase && !$exhaustive) || $finalScope === null) {
1851: $finalScope = $scopeForBranches->mergeWith($finalScope);
1852: }
1853:
1854: return new InternalStatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPointsForOuterLoop, $throwPoints, $impurePoints);
1855: } elseif ($stmt instanceof TryCatch) {
1856: $branchScopeResult = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context);
1857: $branchScope = $branchScopeResult->getScope();
1858: $finalScope = $branchScopeResult->isAlwaysTerminating() ? null : $branchScope;
1859:
1860: $exitPoints = [];
1861: $finallyExitPoints = [];
1862: $alwaysTerminating = $branchScopeResult->isAlwaysTerminating();
1863: $hasYield = $branchScopeResult->hasYield();
1864:
1865: if ($stmt->finally !== null) {
1866: $finallyScope = $branchScope;
1867: } else {
1868: $finallyScope = null;
1869: }
1870: foreach ($branchScopeResult->getExitPoints() as $exitPoint) {
1871: $finallyExitPoints[] = $exitPoint->toPublic();
1872: if ($exitPoint->getStatement() instanceof Node\Stmt\Expression && $exitPoint->getStatement()->expr instanceof Expr\Throw_) {
1873: continue;
1874: }
1875: if ($finallyScope !== null) {
1876: $finallyScope = $finallyScope->mergeWith($exitPoint->getScope());
1877: }
1878: $exitPoints[] = $exitPoint;
1879: }
1880:
1881: $throwPoints = $branchScopeResult->getThrowPoints();
1882: $impurePoints = $branchScopeResult->getImpurePoints();
1883: $throwPointsForLater = [];
1884: $pastCatchTypes = new NeverType();
1885:
1886: foreach ($stmt->catches as $catchNode) {
1887: $this->callNodeCallback($nodeCallback, $catchNode, $scope, $storage);
1888:
1889: $originalCatchTypes = [];
1890: $catchTypes = [];
1891: foreach ($catchNode->types as $catchNodeType) {
1892: $catchType = new ObjectType($catchNodeType->toString());
1893: $originalCatchTypes[] = $catchType;
1894: $catchTypes[] = TypeCombinator::remove($catchType, $pastCatchTypes);
1895: }
1896:
1897: $originalCatchType = TypeCombinator::union(...$originalCatchTypes);
1898: $catchType = TypeCombinator::union(...$catchTypes);
1899: $pastCatchTypes = TypeCombinator::union($pastCatchTypes, $originalCatchType);
1900:
1901: $matchingThrowPoints = [];
1902: $matchingCatchTypes = array_fill_keys(array_keys($originalCatchTypes), false);
1903:
1904: // throwable matches all
1905: foreach ($originalCatchTypes as $catchTypeIndex => $catchTypeItem) {
1906: if (!$catchTypeItem->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) {
1907: continue;
1908: }
1909:
1910: foreach ($throwPoints as $throwPointIndex => $throwPoint) {
1911: $matchingThrowPoints[$throwPointIndex] = $throwPoint;
1912: $matchingCatchTypes[$catchTypeIndex] = true;
1913: }
1914: }
1915:
1916: // explicit only
1917: $onlyExplicitIsThrow = true;
1918: if (count($matchingThrowPoints) === 0) {
1919: foreach ($throwPoints as $throwPointIndex => $throwPoint) {
1920: foreach ($catchTypes as $catchTypeIndex => $catchTypeItem) {
1921: if ($catchTypeItem->isSuperTypeOf($throwPoint->getType())->no()) {
1922: continue;
1923: }
1924:
1925: $matchingCatchTypes[$catchTypeIndex] = true;
1926: if (!$throwPoint->isExplicit()) {
1927: continue;
1928: }
1929: $throwNode = $throwPoint->getNode();
1930: if (
1931: !$throwNode instanceof Expr\Throw_
1932: && !($throwNode instanceof Node\Stmt\Expression && $throwNode->expr instanceof Expr\Throw_)
1933: ) {
1934: $onlyExplicitIsThrow = false;
1935: }
1936:
1937: $matchingThrowPoints[$throwPointIndex] = $throwPoint;
1938: }
1939: }
1940: }
1941:
1942: // implicit only
1943: if (count($matchingThrowPoints) === 0 || $onlyExplicitIsThrow) {
1944: foreach ($throwPoints as $throwPointIndex => $throwPoint) {
1945: if ($throwPoint->isExplicit()) {
1946: continue;
1947: }
1948:
1949: foreach ($catchTypes as $catchTypeItem) {
1950: if ($catchTypeItem->isSuperTypeOf($throwPoint->getType())->no()) {
1951: continue;
1952: }
1953:
1954: $matchingThrowPoints[$throwPointIndex] = $throwPoint;
1955: }
1956: }
1957: }
1958:
1959: // include previously removed throw points
1960: if (count($matchingThrowPoints) === 0) {
1961: if ($originalCatchType->isSuperTypeOf(new ObjectType(Throwable::class))->yes()) {
1962: foreach ($branchScopeResult->getThrowPoints() as $originalThrowPoint) {
1963: if (!$originalThrowPoint->canContainAnyThrowable()) {
1964: continue;
1965: }
1966:
1967: $matchingThrowPoints[] = $originalThrowPoint;
1968: $matchingCatchTypes = array_fill_keys(array_keys($originalCatchTypes), true);
1969: }
1970: }
1971: }
1972:
1973: // emit error
1974: foreach ($matchingCatchTypes as $catchTypeIndex => $matched) {
1975: if ($matched) {
1976: continue;
1977: }
1978: $this->callNodeCallback($nodeCallback, new CatchWithUnthrownExceptionNode($catchNode, $catchTypes[$catchTypeIndex], $originalCatchTypes[$catchTypeIndex]), $scope, $storage);
1979: }
1980:
1981: if (count($matchingThrowPoints) === 0) {
1982: continue;
1983: }
1984:
1985: // recompute throw points
1986: $newThrowPoints = [];
1987: foreach ($throwPoints as $throwPoint) {
1988: $newThrowPoint = $throwPoint->subtractCatchType($originalCatchType);
1989:
1990: if ($newThrowPoint->getType() instanceof NeverType) {
1991: continue;
1992: }
1993:
1994: $newThrowPoints[] = $newThrowPoint;
1995: }
1996: $throwPoints = $newThrowPoints;
1997:
1998: $catchScope = null;
1999: foreach ($matchingThrowPoints as $matchingThrowPoint) {
2000: if ($catchScope === null) {
2001: $catchScope = $matchingThrowPoint->getScope();
2002: } else {
2003: $catchScope = $catchScope->mergeWith($matchingThrowPoint->getScope());
2004: }
2005: }
2006:
2007: $variableName = null;
2008: if ($catchNode->var !== null) {
2009: if (!is_string($catchNode->var->name)) {
2010: throw new ShouldNotHappenException();
2011: }
2012:
2013: $variableName = $catchNode->var->name;
2014: $this->callNodeCallback($nodeCallback, new VariableAssignNode($catchNode->var, new TypeExpr($catchType)), $scope, $storage);
2015: }
2016:
2017: $catchScopeResult = $this->processStmtNodesInternal($catchNode, $catchNode->stmts, $catchScope->enterCatchType($catchType, $variableName), $storage, $nodeCallback, $context);
2018: $catchScopeForFinally = $catchScopeResult->getScope();
2019:
2020: $finalScope = $catchScopeResult->isAlwaysTerminating() ? $finalScope : $catchScopeResult->getScope()->mergeWith($finalScope);
2021: $alwaysTerminating = $alwaysTerminating && $catchScopeResult->isAlwaysTerminating();
2022: $hasYield = $hasYield || $catchScopeResult->hasYield();
2023: $catchThrowPoints = $catchScopeResult->getThrowPoints();
2024: $impurePoints = array_merge($impurePoints, $catchScopeResult->getImpurePoints());
2025: $throwPointsForLater = array_merge($throwPointsForLater, $catchThrowPoints);
2026:
2027: if ($finallyScope !== null) {
2028: $finallyScope = $finallyScope->mergeWith($catchScopeForFinally);
2029: }
2030: foreach ($catchScopeResult->getExitPoints() as $exitPoint) {
2031: $finallyExitPoints[] = $exitPoint->toPublic();
2032: if ($exitPoint->getStatement() instanceof Node\Stmt\Expression && $exitPoint->getStatement()->expr instanceof Expr\Throw_) {
2033: continue;
2034: }
2035: if ($finallyScope !== null) {
2036: $finallyScope = $finallyScope->mergeWith($exitPoint->getScope());
2037: }
2038: $exitPoints[] = $exitPoint;
2039: }
2040:
2041: foreach ($catchThrowPoints as $catchThrowPoint) {
2042: if ($finallyScope === null) {
2043: continue;
2044: }
2045: $finallyScope = $finallyScope->mergeWith($catchThrowPoint->getScope());
2046: }
2047: }
2048:
2049: if ($finalScope === null) {
2050: $finalScope = $scope;
2051: }
2052:
2053: foreach ($throwPoints as $throwPoint) {
2054: if ($finallyScope === null) {
2055: continue;
2056: }
2057: $finallyScope = $finallyScope->mergeWith($throwPoint->getScope());
2058: }
2059:
2060: if ($finallyScope !== null) {
2061: $originalFinallyScope = $finallyScope;
2062: $finallyResult = $this->processStmtNodesInternal($stmt->finally, $stmt->finally->stmts, $finallyScope, $storage, $nodeCallback, $context);
2063: $alwaysTerminating = $alwaysTerminating || $finallyResult->isAlwaysTerminating();
2064: $hasYield = $hasYield || $finallyResult->hasYield();
2065: $throwPointsForLater = array_merge($throwPointsForLater, $finallyResult->getThrowPoints());
2066: $impurePoints = array_merge($impurePoints, $finallyResult->getImpurePoints());
2067: $finallyScope = $finallyResult->getScope();
2068: $finalScope = $finallyResult->isAlwaysTerminating() ? $finalScope : $finalScope->processFinallyScope($finallyScope, $originalFinallyScope);
2069: if (count($finallyResult->getExitPoints()) > 0 && $finallyResult->isAlwaysTerminating()) {
2070: $this->callNodeCallback($nodeCallback, new FinallyExitPointsNode(
2071: $finallyResult->toPublic()->getExitPoints(),
2072: $finallyExitPoints,
2073: ), $scope, $storage);
2074: }
2075: $exitPoints = array_merge($exitPoints, $finallyResult->getExitPoints());
2076: }
2077:
2078: return new InternalStatementResult($finalScope, $hasYield, $alwaysTerminating, $exitPoints, array_merge($throwPoints, $throwPointsForLater), $impurePoints);
2079: } elseif ($stmt instanceof Unset_) {
2080: $hasYield = false;
2081: $throwPoints = [];
2082: $impurePoints = [];
2083: foreach ($stmt->vars as $var) {
2084: $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var);
2085: $exprResult = $this->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2086: $scope = $exprResult->getScope();
2087: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
2088: $hasYield = $hasYield || $exprResult->hasYield();
2089: $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
2090: $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints());
2091: if ($var instanceof ArrayDimFetch && $var->dim !== null) {
2092: $varType = $scope->getType($var->var);
2093: if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) {
2094: $throwPoints = array_merge($throwPoints, $this->processExprNode(
2095: $stmt,
2096: new MethodCall(new TypeExpr($varType), 'offsetUnset'),
2097: $scope,
2098: $storage,
2099: new NoopNodeCallback(),
2100: ExpressionContext::createDeep(),
2101: )->getThrowPoints());
2102: }
2103:
2104: $clonedVar = $this->deepNodeCloner->cloneNode($var->var);
2105: $traverser = new NodeTraverser();
2106: $traverser->addVisitor(new class () extends NodeVisitorAbstract {
2107:
2108: #[Override]
2109: public function leaveNode(Node $node): ?ExistingArrayDimFetch
2110: {
2111: if (!$node instanceof ArrayDimFetch || $node->dim === null) {
2112: return null;
2113: }
2114:
2115: return new ExistingArrayDimFetch($node->var, $node->dim);
2116: }
2117:
2118: });
2119:
2120: /** @var Expr $clonedVar */
2121: [$clonedVar] = $traverser->traverse([$clonedVar]);
2122: $scope = $this->processVirtualAssign($scope, $storage, $stmt, $clonedVar, new UnsetOffsetExpr($var->var, $var->dim), $nodeCallback)->getScope();
2123: } elseif ($var instanceof PropertyFetch) {
2124: $scope = $scope->invalidateExpression($var);
2125: $impurePoints[] = new ImpurePoint(
2126: $scope,
2127: $var,
2128: 'propertyUnset',
2129: 'property unset',
2130: true,
2131: );
2132: } else {
2133: $scope = $scope->invalidateExpression($var);
2134: }
2135:
2136: $scope = $scope->invalidateExpression(new ForeachValueByRefExpr($var));
2137: }
2138: } elseif ($stmt instanceof Node\Stmt\Use_) {
2139: $hasYield = false;
2140: $throwPoints = [];
2141: $impurePoints = [];
2142: foreach ($stmt->uses as $use) {
2143: $this->callNodeCallback($nodeCallback, $use, $scope, $storage);
2144: }
2145: } elseif ($stmt instanceof Node\Stmt\Global_) {
2146: $hasYield = false;
2147: $throwPoints = [];
2148: $impurePoints = [
2149: new ImpurePoint(
2150: $scope,
2151: $stmt,
2152: 'global',
2153: 'global variable',
2154: true,
2155: ),
2156: ];
2157: $vars = [];
2158: foreach ($stmt->vars as $var) {
2159: if (!$var instanceof Variable) {
2160: throw new ShouldNotHappenException();
2161: }
2162: $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $var);
2163: $varResult = $this->processExprNode($stmt, $var, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2164: $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints());
2165: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $var);
2166:
2167: if (!is_string($var->name)) {
2168: continue;
2169: }
2170:
2171: $scope = $scope->assignVariable($var->name, new MixedType(), new MixedType(), TrinaryLogic::createYes());
2172: $vars[] = $var->name;
2173: }
2174: $scope = $this->processVarAnnotation($scope, $vars, $stmt);
2175: } elseif ($stmt instanceof Static_) {
2176: $hasYield = false;
2177: $throwPoints = [];
2178: $impurePoints = [
2179: new ImpurePoint(
2180: $scope,
2181: $stmt,
2182: 'static',
2183: 'static variable',
2184: true,
2185: ),
2186: ];
2187:
2188: $vars = [];
2189: foreach ($stmt->vars as $var) {
2190: if (!is_string($var->var->name)) {
2191: throw new ShouldNotHappenException();
2192: }
2193:
2194: if ($var->default !== null) {
2195: $defaultExprResult = $this->processExprNode($stmt, $var->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2196: $impurePoints = array_merge($impurePoints, $defaultExprResult->getImpurePoints());
2197: }
2198:
2199: $scope = $scope->enterExpressionAssign($var->var);
2200: $varResult = $this->processExprNode($stmt, $var->var, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2201: $impurePoints = array_merge($impurePoints, $varResult->getImpurePoints());
2202: $scope = $scope->exitExpressionAssign($var->var);
2203:
2204: $scope = $scope->assignVariable($var->var->name, new MixedType(), new MixedType(), TrinaryLogic::createYes());
2205: $vars[] = $var->var->name;
2206: }
2207:
2208: $scope = $this->processVarAnnotation($scope, $vars, $stmt);
2209: } elseif ($stmt instanceof Node\Stmt\Const_) {
2210: $hasYield = false;
2211: $throwPoints = [];
2212: $impurePoints = [];
2213: foreach ($stmt->consts as $const) {
2214: $this->callNodeCallback($nodeCallback, $const, $scope, $storage);
2215: $constResult = $this->processExprNode($stmt, $const->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2216: $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints());
2217: if ($const->namespacedName !== null) {
2218: $constantName = new Name\FullyQualified($const->namespacedName->toString());
2219: } else {
2220: $constantName = new Name\FullyQualified($const->name->toString());
2221: }
2222: $scope = $scope->assignExpression(new ConstFetch($constantName), $scope->getType($const->value), $scope->getNativeType($const->value));
2223: }
2224: } elseif ($stmt instanceof Node\Stmt\ClassConst) {
2225: $hasYield = false;
2226: $throwPoints = [];
2227: $impurePoints = [];
2228: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
2229: foreach ($stmt->consts as $const) {
2230: $this->callNodeCallback($nodeCallback, $const, $scope, $storage);
2231: $constResult = $this->processExprNode($stmt, $const->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2232: $impurePoints = array_merge($impurePoints, $constResult->getImpurePoints());
2233: if ($scope->getClassReflection() === null) {
2234: throw new ShouldNotHappenException();
2235: }
2236: $scope = $scope->assignExpression(
2237: new Expr\ClassConstFetch(new Name\FullyQualified($scope->getClassReflection()->getName()), $const->name),
2238: $scope->getType($const->value),
2239: $scope->getNativeType($const->value),
2240: );
2241: }
2242: } elseif ($stmt instanceof Node\Stmt\EnumCase) {
2243: $hasYield = false;
2244: $throwPoints = [];
2245: $this->processAttributeGroups($stmt, $stmt->attrGroups, $scope, $storage, $nodeCallback);
2246: $impurePoints = [];
2247: if ($stmt->expr !== null) {
2248: $exprResult = $this->processExprNode($stmt, $stmt->expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
2249: $impurePoints = $exprResult->getImpurePoints();
2250: }
2251: } elseif ($stmt instanceof InlineHTML) {
2252: $hasYield = false;
2253: $throwPoints = [];
2254: $impurePoints = [
2255: new ImpurePoint($scope, $stmt, 'betweenPhpTags', 'output between PHP opening and closing tags', true),
2256: ];
2257: } elseif ($stmt instanceof Node\Stmt\Block) {
2258: $result = $this->processStmtNodesInternal($stmt, $stmt->stmts, $scope, $storage, $nodeCallback, $context);
2259: if ($this->polluteScopeWithBlock) {
2260: return $result;
2261: }
2262:
2263: return new InternalStatementResult(
2264: $scope->mergeWith($result->getScope()),
2265: $result->hasYield(),
2266: $result->isAlwaysTerminating(),
2267: $result->getExitPoints(),
2268: $result->getThrowPoints(),
2269: $result->getImpurePoints(),
2270: $result->getEndStatements(),
2271: );
2272: } elseif ($stmt instanceof Node\Stmt\Nop) {
2273: $hasYield = false;
2274: $throwPoints = $overridingThrowPoints ?? [];
2275: $impurePoints = [];
2276: } elseif ($stmt instanceof Node\Stmt\GroupUse) {
2277: $hasYield = false;
2278: $throwPoints = [];
2279: foreach ($stmt->uses as $use) {
2280: $this->callNodeCallback($nodeCallback, $use, $scope, $storage);
2281: }
2282: $impurePoints = [];
2283: } else {
2284: $hasYield = false;
2285: $throwPoints = $overridingThrowPoints ?? [];
2286: $impurePoints = [];
2287: }
2288:
2289: return new InternalStatementResult($scope, $hasYield, false, [], $throwPoints, $impurePoints);
2290: }
2291:
2292: /**
2293: * @return array{bool, string|null}
2294: */
2295: private function getDeprecatedAttribute(Scope $scope, Node\Stmt\Function_|Node\Stmt\ClassMethod|Node\PropertyHook $stmt): array
2296: {
2297: $initializerExprContext = InitializerExprContext::fromStubParameter(
2298: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
2299: $scope->getFile(),
2300: $stmt,
2301: );
2302: $isDeprecated = false;
2303: $deprecatedDescription = null;
2304: $deprecatedDescriptionType = null;
2305: foreach ($stmt->attrGroups as $attrGroup) {
2306: foreach ($attrGroup->attrs as $attr) {
2307: if ($attr->name->toString() !== 'Deprecated') {
2308: continue;
2309: }
2310: $isDeprecated = true;
2311: $arguments = $attr->args;
2312: foreach ($arguments as $i => $arg) {
2313: $argName = $arg->name;
2314: if ($argName === null) {
2315: if ($i !== 0) {
2316: continue;
2317: }
2318:
2319: $deprecatedDescriptionType = $this->initializerExprTypeResolver->getType($arg->value, $initializerExprContext);
2320: break;
2321: }
2322:
2323: if ($argName->toString() !== 'message') {
2324: continue;
2325: }
2326:
2327: $deprecatedDescriptionType = $this->initializerExprTypeResolver->getType($arg->value, $initializerExprContext);
2328: break;
2329: }
2330: }
2331: }
2332:
2333: if ($deprecatedDescriptionType !== null) {
2334: $constantStrings = $deprecatedDescriptionType->getConstantStrings();
2335: if (count($constantStrings) === 1) {
2336: $deprecatedDescription = $constantStrings[0]->getValue();
2337: }
2338: }
2339:
2340: return [$isDeprecated, $deprecatedDescription];
2341: }
2342:
2343: /**
2344: * @return InternalThrowPoint[]|null
2345: */
2346: private function getOverridingThrowPoints(Node\Stmt $statement, MutatingScope $scope): ?array
2347: {
2348: foreach ($statement->getComments() as $comment) {
2349: if (!$comment instanceof Doc) {
2350: continue;
2351: }
2352:
2353: $function = $scope->getFunction();
2354: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
2355: $scope->getFile(),
2356: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
2357: $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
2358: $function !== null ? $function->getName() : null,
2359: $comment->getText(),
2360: );
2361:
2362: $throwsTag = $resolvedPhpDoc->getThrowsTag();
2363: if ($throwsTag !== null) {
2364: $throwsType = $throwsTag->getType();
2365: if ($throwsType->isVoid()->yes()) {
2366: return [];
2367: }
2368:
2369: return [InternalThrowPoint::createExplicit($scope, $throwsType, $statement, false)];
2370: }
2371: }
2372:
2373: return null;
2374: }
2375:
2376: private function getCurrentClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection
2377: {
2378: if (!$this->reflectionProvider->hasClass($className)) {
2379: return $this->createAstClassReflection($stmt, $className, $scope);
2380: }
2381:
2382: $defaultClassReflection = $this->reflectionProvider->getClass($className);
2383: if ($defaultClassReflection->getFileName() !== $scope->getFile()) {
2384: return $this->createAstClassReflection($stmt, $className, $scope);
2385: }
2386:
2387: $startLine = $defaultClassReflection->getNativeReflection()->getStartLine();
2388: if ($startLine !== $stmt->getStartLine()) {
2389: return $this->createAstClassReflection($stmt, $className, $scope);
2390: }
2391:
2392: return $defaultClassReflection;
2393: }
2394:
2395: private function createAstClassReflection(Node\Stmt\ClassLike $stmt, string $className, Scope $scope): ClassReflection
2396: {
2397: $nodeToReflection = new NodeToReflection();
2398: $betterReflectionClass = $nodeToReflection->__invoke(
2399: $this->reflector,
2400: $stmt,
2401: new LocatedSource(FileReader::read($scope->getFile()), $className, $scope->getFile()),
2402: $scope->getNamespace() !== null ? new Node\Stmt\Namespace_(new Name($scope->getNamespace())) : null,
2403: );
2404: if (!$betterReflectionClass instanceof \PHPStan\BetterReflection\Reflection\ReflectionClass) {
2405: throw new ShouldNotHappenException();
2406: }
2407:
2408: $enumAdapter = base64_decode('UEhQU3RhblxCZXR0ZXJSZWZsZWN0aW9uXFJlZmxlY3Rpb25cQWRhcHRlclxSZWZsZWN0aW9uRW51bQ==', true);
2409:
2410: return $this->classReflectionFactory->create(
2411: $betterReflectionClass->getName(),
2412: $betterReflectionClass instanceof ReflectionEnum && PHP_VERSION_ID >= 80000 ? new $enumAdapter($betterReflectionClass) : new ReflectionClass($betterReflectionClass),
2413: null,
2414: null,
2415: null,
2416: sprintf('%s:%d', $scope->getFile(), $stmt->getStartLine()),
2417: );
2418: }
2419:
2420: public function lookForSetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope
2421: {
2422: return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->setAllowedUndefinedExpression($expr));
2423: }
2424:
2425: public function lookForUnsetAllowedUndefinedExpressions(MutatingScope $scope, Expr $expr): MutatingScope
2426: {
2427: return $this->lookForExpressionCallback($scope, $expr, static fn (MutatingScope $scope, Expr $expr): MutatingScope => $scope->unsetAllowedUndefinedExpression($expr));
2428: }
2429:
2430: /**
2431: * @param Closure(MutatingScope $scope, Expr $expr): MutatingScope $callback
2432: */
2433: private function lookForExpressionCallback(MutatingScope $scope, Expr $expr, Closure $callback): MutatingScope
2434: {
2435: if (!$expr instanceof ArrayDimFetch || $expr->dim !== null) {
2436: $scope = $callback($scope, $expr);
2437: }
2438:
2439: if ($expr instanceof ArrayDimFetch) {
2440: $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback);
2441: } elseif ($expr instanceof PropertyFetch || $expr instanceof Expr\NullsafePropertyFetch) {
2442: $scope = $this->lookForExpressionCallback($scope, $expr->var, $callback);
2443: } elseif ($expr instanceof StaticPropertyFetch && $expr->class instanceof Expr) {
2444: $scope = $this->lookForExpressionCallback($scope, $expr->class, $callback);
2445: } elseif ($expr instanceof List_) {
2446: foreach ($expr->items as $item) {
2447: if ($item === null) {
2448: continue;
2449: }
2450:
2451: $scope = $this->lookForExpressionCallback($scope, $item->value, $callback);
2452: }
2453: }
2454:
2455: return $scope;
2456: }
2457:
2458: private function findEarlyTerminatingExpr(Expr $expr, Scope $scope): ?Expr
2459: {
2460: if (($expr instanceof MethodCall || $expr instanceof Expr\StaticCall) && $expr->name instanceof Node\Identifier) {
2461: if (array_key_exists($expr->name->toLowerString(), $this->earlyTerminatingMethodNames)) {
2462: if ($expr instanceof MethodCall) {
2463: $methodCalledOnType = $scope->getType($expr->var);
2464: } else {
2465: if ($expr->class instanceof Name) {
2466: $methodCalledOnType = $scope->resolveTypeByName($expr->class);
2467: } else {
2468: $methodCalledOnType = $scope->getType($expr->class);
2469: }
2470: }
2471:
2472: foreach ($methodCalledOnType->getObjectClassNames() as $referencedClass) {
2473: if (!$this->reflectionProvider->hasClass($referencedClass)) {
2474: continue;
2475: }
2476:
2477: $classReflection = $this->reflectionProvider->getClass($referencedClass);
2478: foreach (array_merge([$referencedClass], $classReflection->getParentClassesNames(), $classReflection->getNativeReflection()->getInterfaceNames()) as $className) {
2479: if (!isset($this->earlyTerminatingMethodCalls[$className])) {
2480: continue;
2481: }
2482:
2483: if (in_array((string) $expr->name, $this->earlyTerminatingMethodCalls[$className], true)) {
2484: return $expr;
2485: }
2486: }
2487: }
2488: }
2489: }
2490:
2491: if ($expr instanceof FuncCall && $expr->name instanceof Name) {
2492: if (in_array((string) $expr->name, $this->earlyTerminatingFunctionCalls, true)) {
2493: return $expr;
2494: }
2495: }
2496:
2497: if ($expr instanceof Expr\Exit_ || $expr instanceof Expr\Throw_) {
2498: return $expr;
2499: }
2500:
2501: $exprType = $scope->getType($expr);
2502: if ($exprType instanceof NeverType && $exprType->isExplicit()) {
2503: return $expr;
2504: }
2505:
2506: return null;
2507: }
2508:
2509: /**
2510: * @param callable(Node $node, Scope $scope): void $nodeCallback
2511: */
2512: public function processExprNode(
2513: Node\Stmt $stmt,
2514: Expr $expr,
2515: MutatingScope $scope,
2516: ExpressionResultStorage $storage,
2517: callable $nodeCallback,
2518: ExpressionContext $context,
2519: ): ExpressionResult
2520: {
2521: $this->storeBeforeScope($storage, $expr, $scope);
2522: if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) {
2523: if ($expr instanceof FuncCall) {
2524: $newExpr = new FunctionCallableNode($expr->name, $expr);
2525: } elseif ($expr instanceof MethodCall) {
2526: $newExpr = new MethodCallableNode($expr->var, $expr->name, $expr);
2527: } elseif ($expr instanceof StaticCall) {
2528: $newExpr = new StaticMethodCallableNode($expr->class, $expr->name, $expr);
2529: } elseif ($expr instanceof New_ && !$expr->class instanceof Class_) {
2530: $newExpr = new InstantiationCallableNode($expr->class, $expr);
2531: } else {
2532: throw new ShouldNotHappenException();
2533: }
2534:
2535: return $this->processExprNode($stmt, $newExpr, $scope, $storage, $nodeCallback, $context);
2536: }
2537:
2538: $this->callNodeCallbackWithExpression($nodeCallback, $expr, $scope, $storage, $context);
2539:
2540: /** @var ExprHandler<Expr> $exprHandler */
2541: foreach ($this->container->getServicesByTag(ExprHandler::EXTENSION_TAG) as $exprHandler) {
2542: if (!$exprHandler->supports($expr)) {
2543: continue;
2544: }
2545:
2546: return $exprHandler->processExpr($this, $stmt, $expr, $scope, $storage, $nodeCallback, $context);
2547: }
2548:
2549: if ($expr instanceof List_) {
2550: // only in assign and foreach, processed elsewhere
2551: return new ExpressionResult($scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []);
2552: }
2553:
2554: return new ExpressionResult(
2555: $scope,
2556: hasYield: false,
2557: isAlwaysTerminating: false,
2558: throwPoints: [],
2559: impurePoints: [],
2560: truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
2561: falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
2562: );
2563: }
2564:
2565: /**
2566: * @param 'get'|'set' $hookName
2567: * @return InternalThrowPoint[]
2568: */
2569: public function getThrowPointsFromPropertyHook(
2570: MutatingScope $scope,
2571: PropertyFetch $propertyFetch,
2572: PhpPropertyReflection $propertyReflection,
2573: string $hookName,
2574: ): array
2575: {
2576: $scopeFunction = $scope->getFunction();
2577: if (
2578: $scopeFunction instanceof PhpMethodFromParserNodeReflection
2579: && $scopeFunction->isPropertyHook()
2580: && $propertyFetch->var instanceof Variable
2581: && $propertyFetch->var->name === 'this'
2582: && $propertyFetch->name instanceof Identifier
2583: && $propertyFetch->name->toString() === $scopeFunction->getHookedPropertyName()
2584: ) {
2585: return [];
2586: }
2587: $declaringClass = $propertyReflection->getDeclaringClass();
2588: if (!$propertyReflection->hasHook($hookName)) {
2589: if (
2590: $propertyReflection->isPrivate()
2591: || $propertyReflection->isFinal()->yes()
2592: || $declaringClass->isFinal()
2593: ) {
2594: return [];
2595: }
2596:
2597: if ($this->implicitThrows) {
2598: return [InternalThrowPoint::createImplicit($scope, $propertyFetch)];
2599: }
2600:
2601: return [];
2602: }
2603:
2604: $getHook = $propertyReflection->getHook($hookName);
2605: $throwType = $getHook->getThrowType();
2606:
2607: if ($throwType !== null) {
2608: if (!$throwType->isVoid()->yes()) {
2609: return [InternalThrowPoint::createExplicit($scope, $throwType, $propertyFetch, true)];
2610: }
2611: } elseif ($this->implicitThrows) {
2612: return [InternalThrowPoint::createImplicit($scope, $propertyFetch)];
2613: }
2614:
2615: return [];
2616: }
2617:
2618: /**
2619: * @return string[]
2620: */
2621: public function getAssignedVariables(Expr $expr): array
2622: {
2623: if ($expr instanceof Expr\Variable) {
2624: if (is_string($expr->name)) {
2625: return [$expr->name];
2626: }
2627:
2628: return [];
2629: }
2630:
2631: if ($expr instanceof Expr\List_) {
2632: $names = [];
2633: foreach ($expr->items as $item) {
2634: if ($item === null) {
2635: continue;
2636: }
2637:
2638: $names = array_merge($names, $this->getAssignedVariables($item->value));
2639: }
2640:
2641: return $names;
2642: }
2643:
2644: if ($expr instanceof ArrayDimFetch) {
2645: return $this->getAssignedVariables($expr->var);
2646: }
2647:
2648: return [];
2649: }
2650:
2651: /**
2652: * @param callable(Node $node, Scope $scope): void $nodeCallback
2653: */
2654: public function callNodeCallbackWithExpression(
2655: callable $nodeCallback,
2656: Expr $expr,
2657: MutatingScope $scope,
2658: ExpressionResultStorage $storage,
2659: ExpressionContext $context,
2660: ): void
2661: {
2662: if ($context->isDeep()) {
2663: $scope = $scope->exitFirstLevelStatements();
2664: }
2665: $this->callNodeCallback($nodeCallback, $expr, $scope, $storage);
2666: }
2667:
2668: /**
2669: * @param callable(Node $node, Scope $scope): void $nodeCallback
2670: */
2671: public function callNodeCallback(
2672: callable $nodeCallback,
2673: Node $node,
2674: MutatingScope $scope,
2675: ExpressionResultStorage $storage,
2676: ): void
2677: {
2678: $nodeCallback($node, $scope);
2679: }
2680:
2681: /**
2682: * @param callable(Node $node, Scope $scope): void $nodeCallback
2683: */
2684: public function processClosureNode(
2685: Node\Stmt $stmt,
2686: Expr\Closure $expr,
2687: MutatingScope $scope,
2688: ExpressionResultStorage $storage,
2689: callable $nodeCallback,
2690: ExpressionContext $context,
2691: ?Type $passedToType,
2692: ): ProcessClosureResult
2693: {
2694: foreach ($expr->params as $param) {
2695: $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
2696: }
2697:
2698: $byRefUses = [];
2699:
2700: $closureCallArgs = $expr->getAttribute(ClosureArgVisitor::ATTRIBUTE_NAME);
2701: $callableParameters = $this->createCallableParameters(
2702: $scope,
2703: $expr,
2704: $closureCallArgs,
2705: $passedToType,
2706: );
2707:
2708: $useScope = $scope;
2709: foreach ($expr->uses as $use) {
2710: if ($use->byRef) {
2711: $byRefUses[] = $use;
2712: $useScope = $useScope->enterExpressionAssign($use->var);
2713:
2714: $inAssignRightSideVariableName = $context->getInAssignRightSideVariableName();
2715: $inAssignRightSideExpr = $context->getInAssignRightSideExpr();
2716: if (
2717: $inAssignRightSideVariableName === $use->var->name
2718: && $inAssignRightSideExpr !== null
2719: ) {
2720: $inAssignRightSideType = $scope->getType($inAssignRightSideExpr);
2721: if ($inAssignRightSideType instanceof ClosureType) {
2722: $variableType = $inAssignRightSideType;
2723: } else {
2724: $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName);
2725: if ($alreadyHasVariableType->no()) {
2726: $variableType = TypeCombinator::union(new NullType(), $inAssignRightSideType);
2727: } else {
2728: $variableType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideType);
2729: }
2730: }
2731: $inAssignRightSideNativeType = $scope->getNativeType($inAssignRightSideExpr);
2732: if ($inAssignRightSideNativeType instanceof ClosureType) {
2733: $variableNativeType = $inAssignRightSideNativeType;
2734: } else {
2735: $alreadyHasVariableType = $scope->hasVariableType($inAssignRightSideVariableName);
2736: if ($alreadyHasVariableType->no()) {
2737: $variableNativeType = TypeCombinator::union(new NullType(), $inAssignRightSideNativeType);
2738: } else {
2739: $variableNativeType = TypeCombinator::union($scope->getVariableType($inAssignRightSideVariableName), $inAssignRightSideNativeType);
2740: }
2741: }
2742: $scope = $scope->assignVariable($inAssignRightSideVariableName, $variableType, $variableNativeType, TrinaryLogic::createYes());
2743: }
2744: }
2745: $this->processExprNode($stmt, $use->var, $useScope, $storage, $nodeCallback, $context);
2746: if (!$use->byRef) {
2747: continue;
2748: }
2749:
2750: $useScope = $useScope->exitExpressionAssign($use->var);
2751: }
2752:
2753: if ($expr->returnType !== null) {
2754: $this->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage);
2755: }
2756:
2757: $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters);
2758: $closureScope = $closureScope->processClosureScope($scope, null, $byRefUses);
2759: $closureType = $closureScope->getAnonymousFunctionReflection();
2760: if (!$closureType instanceof ClosureType) {
2761: throw new ShouldNotHappenException();
2762: }
2763:
2764: $returnType = $closureType->getReturnType();
2765: $isAlwaysTerminating = ($returnType instanceof NeverType && $returnType->isExplicit());
2766:
2767: $this->callNodeCallback($nodeCallback, new InClosureNode($closureType, $expr), $closureScope, $storage);
2768:
2769: $executionEnds = [];
2770: $gatheredReturnStatements = [];
2771: $gatheredYieldStatements = [];
2772: $closureImpurePoints = [];
2773: $invalidateExpressions = [];
2774: $closureStmtsCallback = static function (Node $node, Scope $scope) use ($nodeCallback, &$executionEnds, &$gatheredReturnStatements, &$gatheredYieldStatements, &$closureScope, &$closureImpurePoints, &$invalidateExpressions): void {
2775: $nodeCallback($node, $scope);
2776: if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) {
2777: return;
2778: }
2779: if ($node instanceof PropertyAssignNode) {
2780: $closureImpurePoints[] = new ImpurePoint(
2781: $scope,
2782: $node,
2783: 'propertyAssign',
2784: 'property assignment',
2785: true,
2786: );
2787: $invalidateExpressions[] = new InvalidateExprNode($node->getPropertyFetch());
2788: return;
2789: }
2790: if ($node instanceof ExecutionEndNode) {
2791: $executionEnds[] = $node;
2792: return;
2793: }
2794: if ($node instanceof InvalidateExprNode) {
2795: $invalidateExpressions[] = $node;
2796: return;
2797: }
2798: if ($node instanceof Expr\Yield_ || $node instanceof Expr\YieldFrom) {
2799: $gatheredYieldStatements[] = $node;
2800: }
2801: if (!$node instanceof Return_) {
2802: return;
2803: }
2804:
2805: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
2806: };
2807:
2808: if (count($byRefUses) === 0) {
2809: $statementResult = $this->processStmtNodesInternalWithoutFlushingPendingFibers($expr, $expr->stmts, $closureScope, $storage, $closureStmtsCallback, StatementContext::createTopLevel());
2810: $publicStatementResult = $statementResult->toPublic();
2811: $this->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode(
2812: $expr,
2813: $gatheredReturnStatements,
2814: $gatheredYieldStatements,
2815: $publicStatementResult,
2816: $executionEnds,
2817: array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints),
2818: ), $closureScope, $storage);
2819:
2820: return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $isAlwaysTerminating);
2821: }
2822:
2823: $originalStorage = $storage;
2824:
2825: $count = 0;
2826: $closureResultScope = null;
2827: do {
2828: $prevScope = $closureScope;
2829:
2830: $storage = $originalStorage->duplicate();
2831: $intermediaryClosureScopeResult = $this->processStmtNodesInternalWithoutFlushingPendingFibers($expr, $expr->stmts, $closureScope, $storage, new NoopNodeCallback(), StatementContext::createTopLevel());
2832: $intermediaryClosureScope = $intermediaryClosureScopeResult->getScope();
2833: foreach ($intermediaryClosureScopeResult->getExitPoints() as $exitPoint) {
2834: $intermediaryClosureScope = $intermediaryClosureScope->mergeWith($exitPoint->getScope());
2835: }
2836:
2837: if ($expr->getAttribute(ImmediatelyInvokedClosureVisitor::ATTRIBUTE_NAME) === true) {
2838: $closureResultScope = $intermediaryClosureScope;
2839: break;
2840: }
2841:
2842: $closureScope = $scope->enterAnonymousFunction($expr, $callableParameters);
2843: $closureScope = $closureScope->processClosureScope($intermediaryClosureScope, $prevScope, $byRefUses);
2844:
2845: if ($closureScope->equals($prevScope)) {
2846: break;
2847: }
2848: if ($count >= self::GENERALIZE_AFTER_ITERATION) {
2849: $closureScope = $prevScope->generalizeWith($closureScope);
2850: }
2851: $count++;
2852: } while ($count < self::LOOP_SCOPE_ITERATIONS);
2853:
2854: if ($closureResultScope === null) {
2855: $closureResultScope = $closureScope;
2856: }
2857:
2858: $storage = $originalStorage;
2859: $statementResult = $this->processStmtNodesInternalWithoutFlushingPendingFibers($expr, $expr->stmts, $closureScope, $storage, $closureStmtsCallback, StatementContext::createTopLevel());
2860: $publicStatementResult = $statementResult->toPublic();
2861: $this->callNodeCallback($nodeCallback, new ClosureReturnStatementsNode(
2862: $expr,
2863: $gatheredReturnStatements,
2864: $gatheredYieldStatements,
2865: $publicStatementResult,
2866: $executionEnds,
2867: array_merge($publicStatementResult->getImpurePoints(), $closureImpurePoints),
2868: ), $closureScope, $storage);
2869:
2870: return new ProcessClosureResult($scope, $statementResult->getThrowPoints(), $statementResult->getImpurePoints(), $invalidateExpressions, $isAlwaysTerminating, $closureResultScope, $byRefUses);
2871: }
2872:
2873: /**
2874: * @param InvalidateExprNode[] $invalidatedExpressions
2875: * @param string[] $uses
2876: */
2877: public function processImmediatelyCalledCallable(MutatingScope $scope, array $invalidatedExpressions, array $uses): MutatingScope
2878: {
2879: if ($scope->isInClass()) {
2880: $uses[] = 'this';
2881: }
2882:
2883: $finder = new NodeFinder();
2884: foreach ($invalidatedExpressions as $invalidateExpression) {
2885: $result = $finder->findFirst([$invalidateExpression->getExpr()], static fn ($node) => $node instanceof Variable && in_array($node->name, $uses, true));
2886: if ($result === null) {
2887: continue;
2888: }
2889:
2890: $requireMoreCharacters = $invalidateExpression->getExpr() instanceof Variable;
2891: $scope = $scope->invalidateExpression($invalidateExpression->getExpr(), $requireMoreCharacters);
2892: }
2893:
2894: return $scope;
2895: }
2896:
2897: /**
2898: * @param callable(Node $node, Scope $scope): void $nodeCallback
2899: */
2900: public function processArrowFunctionNode(
2901: Node\Stmt $stmt,
2902: Expr\ArrowFunction $expr,
2903: MutatingScope $scope,
2904: ExpressionResultStorage $storage,
2905: callable $nodeCallback,
2906: ?Type $passedToType,
2907: ): ExpressionResult
2908: {
2909: foreach ($expr->params as $param) {
2910: $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
2911: }
2912: if ($expr->returnType !== null) {
2913: $this->callNodeCallback($nodeCallback, $expr->returnType, $scope, $storage);
2914: }
2915:
2916: $arrowFunctionCallArgs = $expr->getAttribute(ArrowFunctionArgVisitor::ATTRIBUTE_NAME);
2917: $arrowFunctionScope = $scope->enterArrowFunction($expr, $this->createCallableParameters(
2918: $scope,
2919: $expr,
2920: $arrowFunctionCallArgs,
2921: $passedToType,
2922: ));
2923: $arrowFunctionType = $arrowFunctionScope->getAnonymousFunctionReflection();
2924: if ($arrowFunctionType === null) {
2925: throw new ShouldNotHappenException();
2926: }
2927: $this->callNodeCallback($nodeCallback, new InArrowFunctionNode($arrowFunctionType, $expr), $arrowFunctionScope, $storage);
2928: $exprResult = $this->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $nodeCallback, ExpressionContext::createTopLevel());
2929:
2930: return new ExpressionResult($scope, false, $exprResult->isAlwaysTerminating(), $exprResult->getThrowPoints(), $exprResult->getImpurePoints());
2931: }
2932:
2933: /**
2934: * @param Node\Arg[] $args
2935: * @return ParameterReflection[]|null
2936: */
2937: public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array $args, ?Type $passedToType): ?array
2938: {
2939: $callableParameters = null;
2940: if ($args !== null) {
2941: $closureType = $scope->getType($closureExpr);
2942:
2943: if ($closureType->isCallable()->no()) {
2944: return null;
2945: }
2946:
2947: $acceptors = $closureType->getCallableParametersAcceptors($scope);
2948: if (count($acceptors) === 1) {
2949: $callableParameters = $acceptors[0]->getParameters();
2950:
2951: foreach ($callableParameters as $index => $callableParameter) {
2952: if (!isset($args[$index])) {
2953: continue;
2954: }
2955:
2956: $type = $scope->getType($args[$index]->value);
2957: $callableParameters[$index] = new NativeParameterReflection(
2958: $callableParameter->getName(),
2959: $callableParameter->isOptional(),
2960: $type,
2961: $callableParameter->passedByReference(),
2962: $callableParameter->isVariadic(),
2963: $callableParameter->getDefaultValue(),
2964: );
2965: }
2966: }
2967: } elseif ($passedToType !== null && !$passedToType->isCallable()->no()) {
2968: if ($passedToType instanceof UnionType) {
2969: $passedToType = $passedToType->filterTypes(static fn (Type $innerType) => $innerType->isCallable()->yes());
2970:
2971: if ($passedToType->isCallable()->no()) {
2972: return null;
2973: }
2974: }
2975:
2976: $acceptors = $passedToType->getCallableParametersAcceptors($scope);
2977: foreach ($acceptors as $acceptor) {
2978: if ($callableParameters === null) {
2979: $callableParameters = array_map(static fn (ParameterReflection $callableParameter) => new NativeParameterReflection(
2980: $callableParameter->getName(),
2981: $callableParameter->isOptional(),
2982: $callableParameter->getType(),
2983: $callableParameter->passedByReference(),
2984: $callableParameter->isVariadic(),
2985: $callableParameter->getDefaultValue(),
2986: ), $acceptor->getParameters());
2987: continue;
2988: }
2989:
2990: $newParameters = [];
2991: foreach ($acceptor->getParameters() as $i => $callableParameter) {
2992: if (!array_key_exists($i, $callableParameters)) {
2993: $newParameters[] = $callableParameter;
2994: continue;
2995: }
2996:
2997: $newParameters[] = $callableParameters[$i]->union(new NativeParameterReflection(
2998: $callableParameter->getName(),
2999: $callableParameter->isOptional(),
3000: $callableParameter->getType(),
3001: $callableParameter->passedByReference(),
3002: $callableParameter->isVariadic(),
3003: $callableParameter->getDefaultValue(),
3004: ));
3005: }
3006:
3007: $callableParameters = $newParameters;
3008: }
3009: }
3010:
3011: return $callableParameters;
3012: }
3013:
3014: /**
3015: * @param callable(Node $node, Scope $scope): void $nodeCallback
3016: */
3017: private function processParamNode(
3018: Node\Stmt $stmt,
3019: Node\Param $param,
3020: MutatingScope $scope,
3021: ExpressionResultStorage $storage,
3022: callable $nodeCallback,
3023: ): void
3024: {
3025: $this->processAttributeGroups($stmt, $param->attrGroups, $scope, $storage, $nodeCallback);
3026: $this->callNodeCallback($nodeCallback, $param, $scope, $storage);
3027: if ($param->type !== null) {
3028: $this->callNodeCallback($nodeCallback, $param->type, $scope, $storage);
3029: }
3030: if ($param->default === null) {
3031: return;
3032: }
3033:
3034: $this->processExprNode($stmt, $param->default, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
3035: }
3036:
3037: /**
3038: * @param AttributeGroup[] $attrGroups
3039: * @param callable(Node $node, Scope $scope): void $nodeCallback
3040: */
3041: private function processAttributeGroups(
3042: Node\Stmt $stmt,
3043: array $attrGroups,
3044: MutatingScope $scope,
3045: ExpressionResultStorage $storage,
3046: callable $nodeCallback,
3047: ): void
3048: {
3049: foreach ($attrGroups as $attrGroup) {
3050: foreach ($attrGroup->attrs as $attr) {
3051: $className = $scope->resolveName($attr->name);
3052: if ($this->reflectionProvider->hasClass($className)) {
3053: $classReflection = $this->reflectionProvider->getClass($className);
3054: if ($classReflection->hasConstructor()) {
3055: $constructorReflection = $classReflection->getConstructor();
3056: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
3057: $scope,
3058: $attr->args,
3059: $constructorReflection->getVariants(),
3060: $constructorReflection->getNamedArgumentsVariants(),
3061: );
3062: $expr = new New_($attr->name, $attr->args);
3063: $expr = ArgumentsNormalizer::reorderNewArguments($parametersAcceptor, $expr) ?? $expr;
3064: $this->processArgs($stmt, $constructorReflection, null, $parametersAcceptor, $expr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
3065: $this->callNodeCallback($nodeCallback, $attr, $scope, $storage);
3066: continue;
3067: }
3068: }
3069:
3070: foreach ($attr->args as $arg) {
3071: $this->processExprNode($stmt, $arg->value, $scope, $storage, $nodeCallback, ExpressionContext::createDeep());
3072: $this->callNodeCallback($nodeCallback, $arg, $scope, $storage);
3073: }
3074: $this->callNodeCallback($nodeCallback, $attr, $scope, $storage);
3075: }
3076: $this->callNodeCallback($nodeCallback, $attrGroup, $scope, $storage);
3077: }
3078: }
3079:
3080: /**
3081: * @param Node\PropertyHook[] $hooks
3082: * @param callable(Node $node, Scope $scope): void $nodeCallback
3083: */
3084: private function processPropertyHooks(
3085: Node\Stmt $stmt,
3086: Identifier|Name|ComplexType|null $nativeTypeNode,
3087: ?Type $phpDocType,
3088: string $propertyName,
3089: array $hooks,
3090: MutatingScope $scope,
3091: ExpressionResultStorage $storage,
3092: callable $nodeCallback,
3093: ): void
3094: {
3095: if (!$scope->isInClass()) {
3096: throw new ShouldNotHappenException();
3097: }
3098:
3099: $classReflection = $scope->getClassReflection();
3100:
3101: foreach ($hooks as $hook) {
3102: $this->callNodeCallback($nodeCallback, $hook, $scope, $storage);
3103: $this->processAttributeGroups($stmt, $hook->attrGroups, $scope, $storage, $nodeCallback);
3104:
3105: [, $phpDocParameterTypes,,,, $phpDocThrowType,,,,,,,, $phpDocComment,,,,,, $resolvedPhpDoc] = $this->getPhpDocs($scope, $hook);
3106:
3107: foreach ($hook->params as $param) {
3108: $this->processParamNode($stmt, $param, $scope, $storage, $nodeCallback);
3109: }
3110:
3111: [$isDeprecated, $deprecatedDescription] = $this->getDeprecatedAttribute($scope, $hook);
3112:
3113: $hookScope = $scope->enterPropertyHook(
3114: $hook,
3115: $propertyName,
3116: $nativeTypeNode,
3117: $phpDocType,
3118: $phpDocParameterTypes,
3119: $phpDocThrowType,
3120: $deprecatedDescription,
3121: $isDeprecated,
3122: $phpDocComment,
3123: $resolvedPhpDoc,
3124: );
3125: $hookReflection = $hookScope->getFunction();
3126: if (!$hookReflection instanceof PhpMethodFromParserNodeReflection) {
3127: throw new ShouldNotHappenException();
3128: }
3129:
3130: if (!$classReflection->hasNativeProperty($propertyName)) {
3131: throw new ShouldNotHappenException();
3132: }
3133:
3134: $propertyReflection = $classReflection->getNativeProperty($propertyName);
3135:
3136: $this->callNodeCallback($nodeCallback, new InPropertyHookNode(
3137: $classReflection,
3138: $hookReflection,
3139: $propertyReflection,
3140: $hook,
3141: ), $hookScope, $storage);
3142:
3143: $stmts = $hook->getStmts();
3144: if ($stmts === null) {
3145: return;
3146: }
3147:
3148: if ($hook->body instanceof Expr) {
3149: // enrich attributes of nodes in short hook body statements
3150: $traverser = new NodeTraverser(
3151: new LineAttributesVisitor($hook->body->getStartLine(), $hook->body->getEndLine()),
3152: );
3153: $traverser->traverse($stmts);
3154: }
3155:
3156: $gatheredReturnStatements = [];
3157: $executionEnds = [];
3158: $methodImpurePoints = [];
3159: $statementResult = $this->processStmtNodesInternal(new PropertyHookStatementNode($hook), $stmts, $hookScope, $storage, static function (Node $node, Scope $scope) use ($nodeCallback, $hookScope, &$gatheredReturnStatements, &$executionEnds, &$hookImpurePoints): void {
3160: $nodeCallback($node, $scope);
3161: if ($scope->getFunction() !== $hookScope->getFunction()) {
3162: return;
3163: }
3164: if ($scope->isInAnonymousFunction()) {
3165: return;
3166: }
3167: if ($node instanceof PropertyAssignNode) {
3168: $hookImpurePoints[] = new ImpurePoint(
3169: $scope,
3170: $node,
3171: 'propertyAssign',
3172: 'property assignment',
3173: true,
3174: );
3175: return;
3176: }
3177: if ($node instanceof ExecutionEndNode) {
3178: $executionEnds[] = $node;
3179: return;
3180: }
3181: if (!$node instanceof Return_) {
3182: return;
3183: }
3184:
3185: $gatheredReturnStatements[] = new ReturnStatement($scope, $node);
3186: }, StatementContext::createTopLevel())->toPublic();
3187:
3188: $this->callNodeCallback($nodeCallback, new PropertyHookReturnStatementsNode(
3189: $hook,
3190: $gatheredReturnStatements,
3191: $statementResult,
3192: $executionEnds,
3193: array_merge($statementResult->getImpurePoints(), $methodImpurePoints),
3194: $classReflection,
3195: $hookReflection,
3196: $propertyReflection,
3197: ), $hookScope, $storage);
3198: }
3199: }
3200:
3201: /**
3202: * @param FunctionReflection|MethodReflection|null $calleeReflection
3203: */
3204: private function resolveClosureThisType(
3205: ?CallLike $call,
3206: $calleeReflection,
3207: ParameterReflection $parameter,
3208: MutatingScope $scope,
3209: ): ?Type
3210: {
3211: if ($call instanceof FuncCall && $calleeReflection instanceof FunctionReflection) {
3212: foreach ($this->parameterClosureThisExtensionProvider->getFunctionParameterClosureThisExtensions() as $extension) {
3213: if (! $extension->isFunctionSupported($calleeReflection, $parameter)) {
3214: continue;
3215: }
3216: $type = $extension->getClosureThisTypeFromFunctionCall($calleeReflection, $call, $parameter, $scope);
3217: if ($type !== null) {
3218: return $type;
3219: }
3220: }
3221: } elseif ($call instanceof StaticCall && $calleeReflection instanceof MethodReflection) {
3222: foreach ($this->parameterClosureThisExtensionProvider->getStaticMethodParameterClosureThisExtensions() as $extension) {
3223: if (! $extension->isStaticMethodSupported($calleeReflection, $parameter)) {
3224: continue;
3225: }
3226: $type = $extension->getClosureThisTypeFromStaticMethodCall($calleeReflection, $call, $parameter, $scope);
3227: if ($type !== null) {
3228: return $type;
3229: }
3230: }
3231: } elseif ($call instanceof MethodCall && $calleeReflection instanceof MethodReflection) {
3232: foreach ($this->parameterClosureThisExtensionProvider->getMethodParameterClosureThisExtensions() as $extension) {
3233: if (! $extension->isMethodSupported($calleeReflection, $parameter)) {
3234: continue;
3235: }
3236: $type = $extension->getClosureThisTypeFromMethodCall($calleeReflection, $call, $parameter, $scope);
3237: if ($type !== null) {
3238: return $type;
3239: }
3240: }
3241: }
3242:
3243: if ($parameter instanceof ExtendedParameterReflection) {
3244: return $parameter->getClosureThisType();
3245: }
3246:
3247: return null;
3248: }
3249:
3250: /**
3251: * @param MethodReflection|FunctionReflection|null $calleeReflection
3252: * @param callable(Node $node, Scope $scope): void $nodeCallback
3253: */
3254: public function processArgs(
3255: Node\Stmt $stmt,
3256: $calleeReflection,
3257: ?ExtendedMethodReflection $nakedMethodReflection,
3258: ?ParametersAcceptor $parametersAcceptor,
3259: CallLike $callLike,
3260: MutatingScope $scope,
3261: ExpressionResultStorage $storage,
3262: callable $nodeCallback,
3263: ExpressionContext $context,
3264: ?MutatingScope $closureBindScope = null,
3265: ): ExpressionResult
3266: {
3267: $args = $callLike->getArgs();
3268:
3269: $parameters = null;
3270: if ($parametersAcceptor !== null) {
3271: $parameters = $parametersAcceptor->getParameters();
3272: }
3273:
3274: $hasYield = false;
3275: $throwPoints = [];
3276: $impurePoints = [];
3277: $isAlwaysTerminating = false;
3278: /** @var list<array{InvalidateExprNode[], string[]}> $deferredInvalidateExpressions */
3279: $deferredInvalidateExpressions = [];
3280: /** @var ProcessClosureResult[] $deferredByRefClosureResults */
3281: $deferredByRefClosureResults = [];
3282: foreach ($args as $i => $arg) {
3283: $assignByReference = false;
3284: $parameter = null;
3285: $parameterType = null;
3286: $parameterNativeType = null;
3287: if ($parameters !== null) {
3288: $matchedParameter = null;
3289: if ($arg->name !== null) {
3290: foreach ($parameters as $p) {
3291: if ($p->getName() === $arg->name->toString()) {
3292: $matchedParameter = $p;
3293: break;
3294: }
3295: }
3296: } elseif (isset($parameters[$i])) {
3297: $matchedParameter = $parameters[$i];
3298: }
3299:
3300: if ($matchedParameter !== null) {
3301: $assignByReference = $matchedParameter->passedByReference()->createsNewVariable();
3302: $parameterType = $matchedParameter->getType();
3303:
3304: if ($matchedParameter instanceof ExtendedParameterReflection) {
3305: $parameterNativeType = $matchedParameter->getNativeType();
3306: }
3307: $parameter = $matchedParameter;
3308: } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) {
3309: $lastParameter = array_last($parameters);
3310: $assignByReference = $lastParameter->passedByReference()->createsNewVariable();
3311: $parameterType = $lastParameter->getType();
3312:
3313: if ($lastParameter instanceof ExtendedParameterReflection) {
3314: $parameterNativeType = $lastParameter->getNativeType();
3315: }
3316: $parameter = $lastParameter;
3317: }
3318: }
3319:
3320: $lookForUnset = false;
3321: if ($assignByReference) {
3322: $isBuiltin = false;
3323: if ($calleeReflection instanceof FunctionReflection && $calleeReflection->isBuiltin()) {
3324: $isBuiltin = true;
3325: } elseif ($calleeReflection instanceof ExtendedMethodReflection && $calleeReflection->getDeclaringClass()->isBuiltin()) {
3326: $isBuiltin = true;
3327: }
3328: if (
3329: $isBuiltin
3330: || ($parameterNativeType === null || !$parameterNativeType->isNull()->no())
3331: ) {
3332: $scope = $this->lookForSetAllowedUndefinedExpressions($scope, $arg->value);
3333: $lookForUnset = true;
3334: }
3335: }
3336:
3337: $originalArg = $arg->getAttribute(ArgumentsNormalizer::ORIGINAL_ARG_ATTRIBUTE) ?? $arg;
3338: if ($calleeReflection !== null) {
3339: $rememberTypes = !$originalArg->value instanceof Expr\Closure && !$originalArg->value instanceof Expr\ArrowFunction;
3340: $scope = $scope->pushInFunctionCall($calleeReflection, $parameter, $rememberTypes);
3341: }
3342:
3343: $this->callNodeCallback($nodeCallback, $originalArg, $scope, $storage);
3344:
3345: $originalScope = $scope;
3346: $scopeToPass = $scope;
3347: if ($i === 0 && $closureBindScope !== null && ($arg->value instanceof Expr\Closure || $arg->value instanceof Expr\ArrowFunction)) {
3348: $scopeToPass = $closureBindScope;
3349: }
3350:
3351: if ($arg->value instanceof Expr\Closure) {
3352: $restoreThisScope = null;
3353: if (
3354: $closureBindScope === null
3355: && $parameter instanceof ExtendedParameterReflection
3356: && !$arg->value->static
3357: ) {
3358: $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass);
3359: if ($closureThisType !== null) {
3360: $restoreThisScope = $scopeToPass;
3361: $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes());
3362: }
3363: }
3364:
3365: if ($parameter !== null) {
3366: $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass);
3367:
3368: if ($overwritingParameterType !== null) {
3369: $parameterType = $overwritingParameterType;
3370: }
3371: }
3372:
3373: $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context);
3374: $closureResult = $this->processClosureNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context, $parameterType ?? null);
3375: if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
3376: $throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $closureResult->getThrowPoints()));
3377: $impurePoints = array_merge($impurePoints, $closureResult->getImpurePoints());
3378: $isAlwaysTerminating = $isAlwaysTerminating || $closureResult->isAlwaysTerminating();
3379: }
3380:
3381: $this->storeBeforeScope($storage, $arg->value, $scopeToPass);
3382:
3383: $uses = [];
3384: foreach ($arg->value->uses as $use) {
3385: if (!is_string($use->var->name)) {
3386: continue;
3387: }
3388:
3389: $uses[] = $use->var->name;
3390: }
3391:
3392: $scope = $closureResult->getScope();
3393: $deferredByRefClosureResults[] = $closureResult;
3394: $invalidateExpressions = $closureResult->getInvalidateExpressions();
3395: if ($restoreThisScope !== null) {
3396: $nodeFinder = new NodeFinder();
3397: $cb = static fn ($expr) => $expr instanceof Variable && $expr->name === 'this';
3398: foreach ($invalidateExpressions as $j => $invalidateExprNode) {
3399: $foundThis = $nodeFinder->findFirst([$invalidateExprNode->getExpr()], $cb);
3400: if ($foundThis === null) {
3401: continue;
3402: }
3403:
3404: unset($invalidateExpressions[$j]);
3405: }
3406: $invalidateExpressions = array_values($invalidateExpressions);
3407: $scope = $scope->restoreThis($restoreThisScope);
3408: }
3409:
3410: if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
3411: $deferredInvalidateExpressions[] = [$invalidateExpressions, $uses];
3412: }
3413: } elseif ($arg->value instanceof Expr\ArrowFunction) {
3414: if (
3415: $closureBindScope === null
3416: && $parameter instanceof ExtendedParameterReflection
3417: && !$arg->value->static
3418: ) {
3419: $closureThisType = $this->resolveClosureThisType($callLike, $calleeReflection, $parameter, $scopeToPass);
3420: if ($closureThisType !== null) {
3421: $scopeToPass = $scopeToPass->assignVariable('this', $closureThisType, new ObjectWithoutClassType(), TrinaryLogic::createYes());
3422: }
3423: }
3424:
3425: if ($parameter !== null) {
3426: $overwritingParameterType = $this->getParameterTypeFromParameterClosureTypeExtension($callLike, $calleeReflection, $parameter, $scopeToPass);
3427:
3428: if ($overwritingParameterType !== null) {
3429: $parameterType = $overwritingParameterType;
3430: }
3431: }
3432:
3433: $this->callNodeCallbackWithExpression($nodeCallback, $arg->value, $scopeToPass, $storage, $context);
3434: $arrowFunctionResult = $this->processArrowFunctionNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $parameterType ?? null);
3435: if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
3436: $throwPoints = array_merge($throwPoints, array_map(static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $arrowFunctionResult->getThrowPoints()));
3437: $impurePoints = array_merge($impurePoints, $arrowFunctionResult->getImpurePoints());
3438: $isAlwaysTerminating = $isAlwaysTerminating || $arrowFunctionResult->isAlwaysTerminating();
3439: }
3440: $this->storeBeforeScope($storage, $arg->value, $scopeToPass);
3441: } else {
3442: $exprType = $scope->getType($arg->value);
3443: $enterExpressionAssignForByRef = $assignByReference && $arg->value instanceof ArrayDimFetch && $arg->value->dim === null;
3444: if ($enterExpressionAssignForByRef) {
3445: $scopeToPass = $scopeToPass->enterExpressionAssign($arg->value);
3446: }
3447: $exprResult = $this->processExprNode($stmt, $arg->value, $scopeToPass, $storage, $nodeCallback, $context->enterDeep());
3448: $throwPoints = array_merge($throwPoints, $exprResult->getThrowPoints());
3449: $impurePoints = array_merge($impurePoints, $exprResult->getImpurePoints());
3450: $isAlwaysTerminating = $isAlwaysTerminating || $exprResult->isAlwaysTerminating();
3451: $scope = $exprResult->getScope();
3452: if ($enterExpressionAssignForByRef) {
3453: $scope = $scope->exitExpressionAssign($arg->value);
3454: }
3455: $hasYield = $hasYield || $exprResult->hasYield();
3456:
3457: if ($exprType->isCallable()->yes()) {
3458: $acceptors = $exprType->getCallableParametersAcceptors($scope);
3459: if (count($acceptors) === 1) {
3460: if ($this->callCallbackImmediately($parameter, $parameterType, $calleeReflection)) {
3461: $deferredInvalidateExpressions[] = [$acceptors[0]->getInvalidateExpressions(), $acceptors[0]->getUsedVariables()];
3462: $callableThrowPoints = array_map(static fn (SimpleThrowPoint $throwPoint) => $throwPoint->isExplicit() ? InternalThrowPoint::createExplicit($scope, $throwPoint->getType(), $arg->value, $throwPoint->canContainAnyThrowable()) : InternalThrowPoint::createImplicit($scope, $arg->value), $acceptors[0]->getThrowPoints());
3463: if (!$this->implicitThrows) {
3464: $callableThrowPoints = array_values(array_filter($callableThrowPoints, static fn (InternalThrowPoint $throwPoint) => $throwPoint->isExplicit()));
3465: }
3466: $throwPoints = array_merge($throwPoints, $callableThrowPoints);
3467: $impurePoints = array_merge($impurePoints, array_map(static fn (SimpleImpurePoint $impurePoint) => new ImpurePoint($scope, $arg->value, $impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $acceptors[0]->getImpurePoints()));
3468: $returnType = $acceptors[0]->getReturnType();
3469: $isAlwaysTerminating = $isAlwaysTerminating || ($returnType instanceof NeverType && $returnType->isExplicit());
3470: }
3471: }
3472: }
3473: }
3474:
3475: if ($assignByReference && $lookForUnset) {
3476: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $arg->value);
3477: }
3478:
3479: if ($calleeReflection !== null) {
3480: $scope = $scope->popInFunctionCall();
3481: }
3482:
3483: if ($i !== 0 || $closureBindScope === null) {
3484: continue;
3485: }
3486:
3487: $scope = $scope->restoreOriginalScopeAfterClosureBind($originalScope);
3488: }
3489:
3490: foreach ($deferredInvalidateExpressions as [$invalidateExpressions, $uses]) {
3491: $scope = $this->processImmediatelyCalledCallable($scope, $invalidateExpressions, $uses);
3492: }
3493:
3494: foreach ($deferredByRefClosureResults as $deferredClosureResult) {
3495: $scope = $deferredClosureResult->applyByRefUseScope($scope);
3496: }
3497:
3498: if ($parameters !== null) {
3499: foreach ($args as $i => $arg) {
3500: $assignByReference = false;
3501: $currentParameter = null;
3502: if (isset($parameters[$i])) {
3503: $currentParameter = $parameters[$i];
3504: } elseif (count($parameters) > 0 && $parametersAcceptor->isVariadic()) {
3505: $currentParameter = array_last($parameters);
3506: }
3507:
3508: if ($currentParameter !== null) {
3509: $assignByReference = $currentParameter->passedByReference()->createsNewVariable();
3510: }
3511:
3512: if ($assignByReference) {
3513: if ($currentParameter === null) {
3514: throw new ShouldNotHappenException();
3515: }
3516:
3517: $argValue = $arg->value;
3518: if (!$argValue instanceof Variable || $argValue->name !== 'this') {
3519: $paramOutType = $this->getParameterOutExtensionsType($callLike, $calleeReflection, $currentParameter, $scope);
3520: if ($paramOutType !== null) {
3521: $byRefType = $paramOutType;
3522: } elseif (
3523: $currentParameter instanceof ExtendedParameterReflection
3524: && $currentParameter->getOutType() !== null
3525: ) {
3526: $byRefType = $currentParameter->getOutType();
3527: } elseif (
3528: $calleeReflection instanceof MethodReflection
3529: && !$calleeReflection->getDeclaringClass()->isBuiltin()
3530: ) {
3531: $byRefType = $currentParameter->getType();
3532: } elseif (
3533: $calleeReflection instanceof FunctionReflection
3534: && !$calleeReflection->isBuiltin()
3535: ) {
3536: $byRefType = $currentParameter->getType();
3537: } else {
3538: $byRefType = new MixedType();
3539: }
3540:
3541: $scope = $this->processVirtualAssign(
3542: $scope,
3543: $storage,
3544: $stmt,
3545: $argValue,
3546: new TypeExpr($byRefType),
3547: $nodeCallback,
3548: )->getScope();
3549: $scope = $this->lookForUnsetAllowedUndefinedExpressions($scope, $argValue);
3550: }
3551: } elseif ($calleeReflection !== null && $calleeReflection->hasSideEffects()->yes()) {
3552: $argType = $scope->getType($arg->value);
3553: if (!$argType->isObject()->no()) {
3554: $nakedReturnType = null;
3555: if ($nakedMethodReflection !== null) {
3556: $nakedParametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
3557: $scope,
3558: $args,
3559: $nakedMethodReflection->getVariants(),
3560: $nakedMethodReflection->getNamedArgumentsVariants(),
3561: );
3562: $nakedReturnType = $nakedParametersAcceptor->getReturnType();
3563: }
3564: if (
3565: $nakedReturnType === null
3566: || !(new ThisType($nakedMethodReflection->getDeclaringClass()))->isSuperTypeOf($nakedReturnType)->yes()
3567: || $nakedMethodReflection->isPure()->no()
3568: ) {
3569: $this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage);
3570: $scope = $scope->invalidateExpression($arg->value, true);
3571: }
3572: } elseif (!(new ResourceType())->isSuperTypeOf($argType)->no()) {
3573: $this->callNodeCallback($nodeCallback, new InvalidateExprNode($arg->value), $scope, $storage);
3574: $scope = $scope->invalidateExpression($arg->value, true);
3575: }
3576: }
3577: }
3578: }
3579:
3580: // not storing this, it's scope after processing all args
3581: return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints);
3582: }
3583:
3584: /**
3585: * @param MethodReflection|FunctionReflection|null $calleeReflection
3586: */
3587: private function callCallbackImmediately(?ParameterReflection $parameter, ?Type $parameterType, $calleeReflection): bool
3588: {
3589: $parameterCallableType = null;
3590: if ($parameterType !== null && $calleeReflection instanceof FunctionReflection) {
3591: $parameterCallableType = TypeUtils::findCallableType($parameterType);
3592: }
3593:
3594: if ($parameter instanceof ExtendedParameterReflection) {
3595: $parameterCallImmediately = $parameter->isImmediatelyInvokedCallable();
3596: if ($parameterCallImmediately->maybe()) {
3597: $callCallbackImmediately = $parameterCallableType !== null;
3598: } else {
3599: $callCallbackImmediately = $parameterCallImmediately->yes();
3600: }
3601: } else {
3602: $callCallbackImmediately = $parameterCallableType !== null;
3603: }
3604:
3605: return $callCallbackImmediately;
3606: }
3607:
3608: /**
3609: * @param MethodReflection|FunctionReflection|null $calleeReflection
3610: */
3611: private function getParameterTypeFromParameterClosureTypeExtension(CallLike $callLike, $calleeReflection, ParameterReflection $parameter, MutatingScope $scope): ?Type
3612: {
3613: if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) {
3614: foreach ($this->parameterClosureTypeExtensionProvider->getFunctionParameterClosureTypeExtensions() as $functionParameterClosureTypeExtension) {
3615: if ($functionParameterClosureTypeExtension->isFunctionSupported($calleeReflection, $parameter)) {
3616: return $functionParameterClosureTypeExtension->getTypeFromFunctionCall($calleeReflection, $callLike, $parameter, $scope);
3617: }
3618: }
3619: } elseif ($calleeReflection instanceof MethodReflection) {
3620: if ($callLike instanceof StaticCall) {
3621: foreach ($this->parameterClosureTypeExtensionProvider->getStaticMethodParameterClosureTypeExtensions() as $staticMethodParameterClosureTypeExtension) {
3622: if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) {
3623: return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $callLike, $parameter, $scope);
3624: }
3625: }
3626: } elseif ($callLike instanceof New_ && $callLike->class instanceof Name) {
3627: $staticCall = new StaticCall(
3628: $callLike->class,
3629: new Identifier('__construct'),
3630: $callLike->getArgs(),
3631: );
3632: foreach ($this->parameterClosureTypeExtensionProvider->getStaticMethodParameterClosureTypeExtensions() as $staticMethodParameterClosureTypeExtension) {
3633: if ($staticMethodParameterClosureTypeExtension->isStaticMethodSupported($calleeReflection, $parameter)) {
3634: return $staticMethodParameterClosureTypeExtension->getTypeFromStaticMethodCall($calleeReflection, $staticCall, $parameter, $scope);
3635: }
3636: }
3637: } elseif ($callLike instanceof MethodCall) {
3638: foreach ($this->parameterClosureTypeExtensionProvider->getMethodParameterClosureTypeExtensions() as $methodParameterClosureTypeExtension) {
3639: if ($methodParameterClosureTypeExtension->isMethodSupported($calleeReflection, $parameter)) {
3640: return $methodParameterClosureTypeExtension->getTypeFromMethodCall($calleeReflection, $callLike, $parameter, $scope);
3641: }
3642: }
3643: }
3644: }
3645:
3646: return null;
3647: }
3648:
3649: /**
3650: * @param MethodReflection|FunctionReflection|null $calleeReflection
3651: */
3652: private function getParameterOutExtensionsType(CallLike $callLike, $calleeReflection, ParameterReflection $currentParameter, MutatingScope $scope): ?Type
3653: {
3654: $paramOutTypes = [];
3655: if ($callLike instanceof FuncCall && $calleeReflection instanceof FunctionReflection) {
3656: foreach ($this->parameterOutTypeExtensionProvider->getFunctionParameterOutTypeExtensions() as $functionParameterOutTypeExtension) {
3657: if (!$functionParameterOutTypeExtension->isFunctionSupported($calleeReflection, $currentParameter)) {
3658: continue;
3659: }
3660:
3661: $resolvedType = $functionParameterOutTypeExtension->getParameterOutTypeFromFunctionCall($calleeReflection, $callLike, $currentParameter, $scope);
3662: if ($resolvedType === null) {
3663: continue;
3664: }
3665: $paramOutTypes[] = $resolvedType;
3666: }
3667: } elseif ($callLike instanceof MethodCall && $calleeReflection instanceof MethodReflection) {
3668: foreach ($this->parameterOutTypeExtensionProvider->getMethodParameterOutTypeExtensions() as $methodParameterOutTypeExtension) {
3669: if (!$methodParameterOutTypeExtension->isMethodSupported($calleeReflection, $currentParameter)) {
3670: continue;
3671: }
3672:
3673: $resolvedType = $methodParameterOutTypeExtension->getParameterOutTypeFromMethodCall($calleeReflection, $callLike, $currentParameter, $scope);
3674: if ($resolvedType === null) {
3675: continue;
3676: }
3677: $paramOutTypes[] = $resolvedType;
3678: }
3679: } elseif ($callLike instanceof StaticCall && $calleeReflection instanceof MethodReflection) {
3680: foreach ($this->parameterOutTypeExtensionProvider->getStaticMethodParameterOutTypeExtensions() as $staticMethodParameterOutTypeExtension) {
3681: if (!$staticMethodParameterOutTypeExtension->isStaticMethodSupported($calleeReflection, $currentParameter)) {
3682: continue;
3683: }
3684:
3685: $resolvedType = $staticMethodParameterOutTypeExtension->getParameterOutTypeFromStaticMethodCall($calleeReflection, $callLike, $currentParameter, $scope);
3686: if ($resolvedType === null) {
3687: continue;
3688: }
3689: $paramOutTypes[] = $resolvedType;
3690: }
3691: }
3692:
3693: if (count($paramOutTypes) === 1) {
3694: return $paramOutTypes[0];
3695: }
3696:
3697: if (count($paramOutTypes) > 1) {
3698: return TypeCombinator::union(...$paramOutTypes);
3699: }
3700:
3701: return null;
3702: }
3703:
3704: /**
3705: * @param callable(Node $node, Scope $scope): void $nodeCallback
3706: */
3707: public function processVirtualAssign(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, Expr $var, Expr $assignedExpr, callable $nodeCallback): ExpressionResult
3708: {
3709: return $this->container->getByType(AssignHandler::class)->processAssignVar(
3710: $this,
3711: $scope,
3712: $storage,
3713: $stmt,
3714: $var,
3715: $assignedExpr,
3716: new VirtualAssignNodeCallback($nodeCallback),
3717: ExpressionContext::createDeep(),
3718: static fn (MutatingScope $scope): ExpressionResult => new ExpressionResult($scope, hasYield: false, isAlwaysTerminating: false, throwPoints: [], impurePoints: []),
3719: false,
3720: );
3721: }
3722:
3723: /**
3724: * @param callable(Node $node, Scope $scope): void $nodeCallback
3725: */
3726: public function processStmtVarAnnotation(MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, ?Expr $defaultExpr, callable $nodeCallback): MutatingScope
3727: {
3728: $function = $scope->getFunction();
3729: $variableLessTags = [];
3730:
3731: foreach ($stmt->getComments() as $comment) {
3732: if (!$comment instanceof Doc) {
3733: continue;
3734: }
3735:
3736: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
3737: $scope->getFile(),
3738: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
3739: $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
3740: $function !== null ? $function->getName() : null,
3741: $comment->getText(),
3742: );
3743:
3744: $assignedVariable = null;
3745: if (
3746: $stmt instanceof Node\Stmt\Expression
3747: && ($stmt->expr instanceof Assign || $stmt->expr instanceof AssignRef)
3748: && $stmt->expr->var instanceof Variable
3749: && is_string($stmt->expr->var->name)
3750: ) {
3751: $assignedVariable = $stmt->expr->var->name;
3752: }
3753:
3754: foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) {
3755: if (is_int($name)) {
3756: $variableLessTags[] = $varTag;
3757: continue;
3758: }
3759:
3760: if ($name === $assignedVariable) {
3761: continue;
3762: }
3763:
3764: $certainty = $scope->hasVariableType($name);
3765: if ($certainty->no()) {
3766: continue;
3767: }
3768:
3769: if ($scope->isInClass() && $scope->getFunction() === null) {
3770: continue;
3771: }
3772:
3773: if ($scope->canAnyVariableExist()) {
3774: $certainty = TrinaryLogic::createYes();
3775: }
3776:
3777: $variableNode = new Variable($name, $stmt->getAttributes());
3778: $originalType = $scope->getVariableType($name);
3779: if (!$originalType->equals($varTag->getType())) {
3780: $this->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $variableNode), $scope, $storage);
3781: }
3782:
3783: $scope = $scope->assignVariable(
3784: $name,
3785: $varTag->getType(),
3786: $scope->getNativeType($variableNode),
3787: $certainty,
3788: );
3789: }
3790: }
3791:
3792: if (count($variableLessTags) === 1 && $defaultExpr !== null) {
3793: $originalType = $scope->getType($defaultExpr);
3794: $varTag = $variableLessTags[0];
3795: if (!$originalType->equals($varTag->getType())) {
3796: $this->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope, $storage);
3797: }
3798: $scope = $scope->assignExpression($defaultExpr, $varTag->getType(), new MixedType());
3799: }
3800:
3801: return $scope;
3802: }
3803:
3804: /**
3805: * @param array<int, string> $variableNames
3806: */
3807: public function processVarAnnotation(MutatingScope $scope, array $variableNames, Node\Stmt $node, bool &$changed = false): MutatingScope
3808: {
3809: $function = $scope->getFunction();
3810: $varTags = [];
3811: foreach ($node->getComments() as $comment) {
3812: if (!$comment instanceof Doc) {
3813: continue;
3814: }
3815:
3816: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
3817: $scope->getFile(),
3818: $scope->isInClass() ? $scope->getClassReflection()->getName() : null,
3819: $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null,
3820: $function !== null ? $function->getName() : null,
3821: $comment->getText(),
3822: );
3823: foreach ($resolvedPhpDoc->getVarTags() as $key => $varTag) {
3824: $varTags[$key] = $varTag;
3825: }
3826: }
3827:
3828: if (count($varTags) === 0) {
3829: return $scope;
3830: }
3831:
3832: foreach ($variableNames as $variableName) {
3833: if (!isset($varTags[$variableName])) {
3834: continue;
3835: }
3836:
3837: $variableType = $varTags[$variableName]->getType();
3838: $changed = true;
3839: $scope = $scope->assignVariable($variableName, $variableType, new MixedType(), TrinaryLogic::createYes());
3840: }
3841:
3842: if (count($variableNames) === 1 && count($varTags) === 1 && isset($varTags[0])) {
3843: $variableType = $varTags[0]->getType();
3844: $changed = true;
3845: $scope = $scope->assignVariable($variableNames[0], $variableType, new MixedType(), TrinaryLogic::createYes());
3846: }
3847:
3848: return $scope;
3849: }
3850:
3851: /**
3852: * @return array{bodyScope: MutatingScope, endScope: MutatingScope}|null
3853: */
3854: private function tryProcessUnrolledConstantArrayForeach(
3855: Foreach_ $stmt,
3856: MutatingScope $originalScope,
3857: ExpressionResultStorage $originalStorage,
3858: StatementContext $context,
3859: ): ?array
3860: {
3861: if ($stmt->byRef) {
3862: return null;
3863: }
3864: if (!($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name))) {
3865: return null;
3866: }
3867: if ($stmt->keyVar !== null && !($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name))) {
3868: return null;
3869: }
3870:
3871: $iterateeType = $originalScope->getType($stmt->expr);
3872: if (!$iterateeType->isConstantArray()->yes()) {
3873: return null;
3874: }
3875: $constantArrays = $iterateeType->getConstantArrays();
3876: if (count($constantArrays) !== 1) {
3877: return null;
3878: }
3879: $constantArray = $constantArrays[0];
3880: $keyTypes = $constantArray->getKeyTypes();
3881: $valueTypes = $constantArray->getValueTypes();
3882: if (count($keyTypes) === 0 || count($keyTypes) > self::FOREACH_UNROLL_LIMIT) {
3883: return null;
3884: }
3885:
3886: $nativeIterateeType = $originalScope->getNativeType($stmt->expr);
3887: $nativeConstantArrays = $nativeIterateeType->getConstantArrays();
3888: $nativeConstantArray = count($nativeConstantArrays) === 1 ? $nativeConstantArrays[0] : null;
3889:
3890: $optionalKeys = array_fill_keys($constantArray->getOptionalKeys(), true);
3891: $valueVarName = $stmt->valueVar->name;
3892: $keyVarName = $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name) ? $stmt->keyVar->name : null;
3893:
3894: $chainScope = $originalScope;
3895: $entryScopes = [];
3896: $breakScopes = [];
3897: foreach ($keyTypes as $i => $keyType) {
3898: $valueType = $valueTypes[$i];
3899: $isOptional = isset($optionalKeys[$i]);
3900:
3901: $nativeKeyType = $nativeConstantArray !== null && isset($nativeConstantArray->getKeyTypes()[$i])
3902: ? $nativeConstantArray->getKeyTypes()[$i]
3903: : $keyType;
3904: $nativeValueType = $nativeConstantArray !== null && isset($nativeConstantArray->getValueTypes()[$i])
3905: ? $nativeConstantArray->getValueTypes()[$i]
3906: : $valueType;
3907:
3908: $iterScope = $chainScope->assignVariable(
3909: $valueVarName,
3910: $valueType,
3911: $nativeValueType,
3912: TrinaryLogic::createYes(),
3913: );
3914: if ($keyVarName !== null) {
3915: $iterScope = $iterScope->assignVariable(
3916: $keyVarName,
3917: $keyType,
3918: $nativeKeyType,
3919: TrinaryLogic::createYes(),
3920: );
3921: $iterScope = $iterScope->assignExpression(
3922: new OriginalForeachKeyExpr($keyVarName),
3923: $keyType,
3924: $nativeKeyType,
3925: );
3926: $iterScope = $iterScope->assignExpression(
3927: new ArrayDimFetch($stmt->expr, $stmt->keyVar),
3928: $valueType,
3929: $nativeValueType,
3930: );
3931: }
3932:
3933: $entryScopes[] = $iterScope;
3934:
3935: $iterStorage = $originalStorage->duplicate();
3936: $bodyResult = $this->processStmtNodesInternal(
3937: $stmt,
3938: $stmt->stmts,
3939: $iterScope,
3940: $iterStorage,
3941: new NoopNodeCallback(),
3942: $context->enterDeep(),
3943: )->filterOutLoopExitPoints();
3944:
3945: $iterEndScope = $bodyResult->getScope();
3946: foreach ($bodyResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
3947: $iterEndScope = $iterEndScope->mergeWith($continueExitPoint->getScope());
3948: }
3949: foreach ($bodyResult->getExitPointsByType(Break_::class) as $breakExitPoint) {
3950: $breakScopes[] = $breakExitPoint->getScope();
3951: }
3952:
3953: if ($isOptional) {
3954: $chainScope = $iterEndScope->mergeWith($chainScope);
3955: } else {
3956: $chainScope = $iterEndScope;
3957: }
3958: }
3959:
3960: $bodyScope = $entryScopes[0];
3961: for ($i = 1, $c = count($entryScopes); $i < $c; $i++) {
3962: $bodyScope = $bodyScope->mergeWith($entryScopes[$i]);
3963: }
3964: if (count($entryScopes) === 1) {
3965: // For a single-iteration unrolling, the merged entry scope does
3966: // not include any post-body state. Merge the chain end scope in
3967: // so that rules analysing the body see that prior iterations
3968: // (which in this case means: this same iteration, from a rule
3969: // author's perspective) could have modified variables.
3970: $bodyScope = $bodyScope->mergeWith($chainScope);
3971: }
3972:
3973: foreach ($breakScopes as $breakScope) {
3974: $chainScope = $chainScope->mergeWith($breakScope);
3975: }
3976:
3977: return ['bodyScope' => $bodyScope, 'endScope' => $chainScope];
3978: }
3979:
3980: /**
3981: * @param callable(Node $node, Scope $scope): void $nodeCallback
3982: */
3983: private function enterForeach(MutatingScope $scope, ExpressionResultStorage $storage, MutatingScope $originalScope, Foreach_ $stmt, callable $nodeCallback): MutatingScope
3984: {
3985: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
3986: $scope = $this->processVarAnnotation($scope, [$stmt->expr->name], $stmt);
3987: }
3988:
3989: $iterateeType = $originalScope->getType($stmt->expr);
3990: if (
3991: ($stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name))
3992: && ($stmt->keyVar === null || ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)))
3993: ) {
3994: $keyVarName = null;
3995: if ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)) {
3996: $keyVarName = $stmt->keyVar->name;
3997: }
3998: $scope = $scope->enterForeach(
3999: $originalScope,
4000: $stmt->expr,
4001: $stmt->valueVar->name,
4002: $keyVarName,
4003: $stmt->byRef,
4004: );
4005: $vars = [$stmt->valueVar->name];
4006: if ($keyVarName !== null) {
4007: $vars[] = $keyVarName;
4008: }
4009: } else {
4010: $scope = $this->processVirtualAssign(
4011: $scope,
4012: $storage,
4013: $stmt,
4014: $stmt->valueVar,
4015: new GetIterableValueTypeExpr($stmt->expr),
4016: $nodeCallback,
4017: )->getScope();
4018: $vars = $this->getAssignedVariables($stmt->valueVar);
4019: if (
4020: $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)
4021: ) {
4022: $scope = $scope->enterForeachKey($originalScope, $stmt->expr, $stmt->keyVar->name);
4023: $vars[] = $stmt->keyVar->name;
4024: } elseif ($stmt->keyVar !== null) {
4025: $scope = $this->processVirtualAssign(
4026: $scope,
4027: $storage,
4028: $stmt,
4029: $stmt->keyVar,
4030: new GetIterableKeyTypeExpr($stmt->expr),
4031: $nodeCallback,
4032: )->getScope();
4033: $vars = array_merge($vars, $this->getAssignedVariables($stmt->keyVar));
4034: }
4035: }
4036:
4037: $constantArrays = $iterateeType->getConstantArrays();
4038: if (
4039: $stmt->getDocComment() === null
4040: && $iterateeType->isConstantArray()->yes()
4041: && count($constantArrays) === 1
4042: && $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)
4043: && $stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)
4044: ) {
4045: $valueConditionalHolders = [];
4046: $arrayDimFetchConditionalHolders = [];
4047: foreach ($constantArrays[0]->getKeyTypes() as $i => $keyType) {
4048: $valueType = $constantArrays[0]->getValueTypes()[$i];
4049: $keyExpressionTypeHolder = ExpressionTypeHolder::createYes(new Variable($stmt->keyVar->name), $keyType);
4050:
4051: $holder = new ConditionalExpressionHolder([
4052: '$' . $stmt->keyVar->name => $keyExpressionTypeHolder,
4053: ], ExpressionTypeHolder::createYes($stmt->valueVar, $valueType));
4054: $valueConditionalHolders[$holder->getKey()] = $holder;
4055: $arrayDimFetchHolder = new ConditionalExpressionHolder([
4056: '$' . $stmt->keyVar->name => $keyExpressionTypeHolder,
4057: ], ExpressionTypeHolder::createYes(new ArrayDimFetch($stmt->expr, $stmt->keyVar), $valueType));
4058: $arrayDimFetchConditionalHolders[$arrayDimFetchHolder->getKey()] = $arrayDimFetchHolder;
4059: }
4060:
4061: $scope = $scope->addConditionalExpressions(
4062: '$' . $stmt->valueVar->name,
4063: $valueConditionalHolders,
4064: );
4065: if ($stmt->expr instanceof Variable && is_string($stmt->expr->name)) {
4066: $scope = $scope->addConditionalExpressions(
4067: sprintf('$%s[$%s]', $stmt->expr->name, $stmt->keyVar->name),
4068: $arrayDimFetchConditionalHolders,
4069: );
4070: }
4071: }
4072:
4073: if (
4074: $stmt->expr instanceof FuncCall
4075: && $stmt->expr->name instanceof Name
4076: && $stmt->expr->name->toLowerString() === 'array_keys'
4077: && $stmt->valueVar instanceof Variable
4078: ) {
4079: $args = $stmt->expr->getArgs();
4080: if (count($args) >= 1) {
4081: $arrayArg = $args[0]->value;
4082: $scope = $scope->assignExpression(
4083: new ArrayDimFetch($arrayArg, $stmt->valueVar),
4084: $scope->getType($arrayArg)->getIterableValueType(),
4085: $scope->getNativeType($arrayArg)->getIterableValueType(),
4086: );
4087: }
4088: }
4089:
4090: return $this->processVarAnnotation($scope, $vars, $stmt);
4091: }
4092:
4093: /**
4094: * @param callable(Node $node, Scope $scope): void $nodeCallback
4095: */
4096: private function processTraitUse(Node\Stmt\TraitUse $node, MutatingScope $classScope, ExpressionResultStorage $storage, callable $nodeCallback): void
4097: {
4098: $parentTraitNames = [];
4099: $parent = $classScope->getParentScope();
4100: while ($parent !== null) {
4101: if ($parent->isInTrait()) {
4102: $parentTraitNames[] = $parent->getTraitReflection()->getName();
4103: }
4104: $parent = $parent->getParentScope();
4105: }
4106:
4107: foreach ($node->traits as $trait) {
4108: $traitName = (string) $trait;
4109: if (in_array($traitName, $parentTraitNames, true)) {
4110: continue;
4111: }
4112: if (!$this->reflectionProvider->hasClass($traitName)) {
4113: continue;
4114: }
4115: $traitReflection = $this->reflectionProvider->getClass($traitName);
4116: $traitFileName = $traitReflection->getFileName();
4117: if ($traitFileName === null) {
4118: continue; // trait from eval or from PHP itself
4119: }
4120: $fileName = $this->fileHelper->normalizePath($traitFileName);
4121: if (!isset($this->analysedFiles[$fileName])) {
4122: continue;
4123: }
4124: $adaptations = [];
4125: foreach ($node->adaptations as $adaptation) {
4126: if ($adaptation->trait === null) {
4127: $adaptations[] = $adaptation;
4128: continue;
4129: }
4130: if ($adaptation->trait->toLowerString() !== $trait->toLowerString()) {
4131: continue;
4132: }
4133:
4134: $adaptations[] = $adaptation;
4135: }
4136: $parserNodes = $this->parser->parseFile($fileName);
4137: $this->processNodesForTraitUse($parserNodes, $traitReflection, $classScope, $storage, $adaptations, $nodeCallback);
4138: }
4139: }
4140:
4141: /**
4142: * @param Node[]|Node|scalar|null $node
4143: * @param Node\Stmt\TraitUseAdaptation[] $adaptations
4144: * @param callable(Node $node, Scope $scope): void $nodeCallback
4145: */
4146: private function processNodesForTraitUse($node, ClassReflection $traitReflection, MutatingScope $scope, ExpressionResultStorage $storage, array $adaptations, callable $nodeCallback): void
4147: {
4148: if ($node instanceof Node) {
4149: if ($node instanceof Node\Stmt\Trait_ && $traitReflection->getName() === (string) $node->namespacedName && $traitReflection->getNativeReflection()->getStartLine() === $node->getStartLine()) {
4150: $methodModifiers = [];
4151: $methodNames = [];
4152: foreach ($adaptations as $adaptation) {
4153: if (!$adaptation instanceof Node\Stmt\TraitUseAdaptation\Alias) {
4154: continue;
4155: }
4156:
4157: $methodName = $adaptation->method->toLowerString();
4158: if ($adaptation->newModifier !== null) {
4159: $methodModifiers[$methodName] = $adaptation->newModifier;
4160: }
4161:
4162: if ($adaptation->newName === null) {
4163: continue;
4164: }
4165:
4166: $methodNames[$methodName] = $adaptation->newName;
4167: }
4168:
4169: $stmts = $node->stmts;
4170: foreach ($stmts as $i => $stmt) {
4171: if (!$stmt instanceof Node\Stmt\ClassMethod) {
4172: continue;
4173: }
4174: $methodName = $stmt->name->toLowerString();
4175: $methodAst = clone $stmt;
4176: $stmts[$i] = $methodAst;
4177: if (array_key_exists($methodName, $methodModifiers)) {
4178: $methodAst->flags = ($methodAst->flags & ~ Modifiers::VISIBILITY_MASK) | $methodModifiers[$methodName];
4179: }
4180:
4181: if (!array_key_exists($methodName, $methodNames)) {
4182: continue;
4183: }
4184:
4185: $methodAst->setAttribute('originalTraitMethodName', $methodAst->name->toLowerString());
4186: $methodAst->name = $methodNames[$methodName];
4187: }
4188:
4189: if (!$scope->isInClass()) {
4190: throw new ShouldNotHappenException();
4191: }
4192: $traitScope = $scope->enterTrait($traitReflection);
4193: $this->callNodeCallback($nodeCallback, new InTraitNode($node, $traitReflection, $scope->getClassReflection()), $traitScope, $storage);
4194: $this->processStmtNodesInternal($node, $stmts, $traitScope, $storage, $nodeCallback, StatementContext::createTopLevel());
4195: return;
4196: }
4197: if ($node instanceof Node\Stmt\ClassLike) {
4198: return;
4199: }
4200: if ($node instanceof Node\FunctionLike) {
4201: return;
4202: }
4203: foreach ($node->getSubNodeNames() as $subNodeName) {
4204: $subNode = $node->{$subNodeName};
4205: $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $storage, $adaptations, $nodeCallback);
4206: }
4207: } elseif (is_array($node)) {
4208: foreach ($node as $subNode) {
4209: $this->processNodesForTraitUse($subNode, $traitReflection, $scope, $storage, $adaptations, $nodeCallback);
4210: }
4211: }
4212: }
4213:
4214: public function processCalledMethod(MethodReflection $methodReflection): ?MutatingScope
4215: {
4216: $declaringClass = $methodReflection->getDeclaringClass();
4217: if ($declaringClass->isAnonymous()) {
4218: return null;
4219: }
4220: if ($declaringClass->getFileName() === null) {
4221: return null;
4222: }
4223:
4224: $stackName = sprintf('%s::%s', $declaringClass->getName(), $methodReflection->getName());
4225: if (array_key_exists($stackName, $this->calledMethodResults)) {
4226: return $this->calledMethodResults[$stackName];
4227: }
4228:
4229: if (array_key_exists($stackName, $this->calledMethodStack)) {
4230: return null;
4231: }
4232:
4233: if (count($this->calledMethodStack) > 0) {
4234: return null;
4235: }
4236:
4237: $this->calledMethodStack[$stackName] = true;
4238:
4239: $fileName = $this->fileHelper->normalizePath($declaringClass->getFileName());
4240: if (!isset($this->analysedFiles[$fileName])) {
4241: unset($this->calledMethodStack[$stackName]);
4242: return null;
4243: }
4244: $parserNodes = $this->parser->parseFile($fileName);
4245:
4246: $returnStatement = null;
4247: $this->processNodesForCalledMethod($parserNodes, new ExpressionResultStorage(), $fileName, $methodReflection, static function (Node $node, Scope $scope) use ($methodReflection, &$returnStatement): void {
4248: if (!$node instanceof MethodReturnStatementsNode) {
4249: return;
4250: }
4251:
4252: if ($node->getClassReflection()->getName() !== $methodReflection->getDeclaringClass()->getName()) {
4253: return;
4254: }
4255:
4256: if ($returnStatement !== null) {
4257: return;
4258: }
4259:
4260: $returnStatement = $node;
4261: });
4262:
4263: $calledMethodEndScope = null;
4264: if ($returnStatement !== null) {
4265: foreach ($returnStatement->getExecutionEnds() as $executionEnd) {
4266: $statementResult = $executionEnd->getStatementResult();
4267: $endNode = $executionEnd->getNode();
4268: if ($endNode instanceof Node\Stmt\Expression) {
4269: $exprType = $statementResult->getScope()->getType($endNode->expr);
4270: if ($exprType instanceof NeverType && $exprType->isExplicit()) {
4271: continue;
4272: }
4273: }
4274: if ($calledMethodEndScope === null) {
4275: $calledMethodEndScope = $statementResult->getScope();
4276: continue;
4277: }
4278:
4279: $calledMethodEndScope = $calledMethodEndScope->mergeWith($statementResult->getScope());
4280: }
4281: foreach ($returnStatement->getReturnStatements() as $statement) {
4282: if ($calledMethodEndScope === null) {
4283: $calledMethodEndScope = $statement->getScope();
4284: continue;
4285: }
4286:
4287: $calledMethodEndScope = $calledMethodEndScope->mergeWith($statement->getScope());
4288: }
4289: }
4290:
4291: unset($this->calledMethodStack[$stackName]);
4292:
4293: $this->calledMethodResults[$stackName] = $calledMethodEndScope;
4294:
4295: return $calledMethodEndScope;
4296: }
4297:
4298: /**
4299: * @param Node[]|Node|scalar|null $node
4300: * @param callable(Node $node, Scope $scope): void $nodeCallback
4301: */
4302: private function processNodesForCalledMethod($node, ExpressionResultStorage $storage, string $fileName, MethodReflection $methodReflection, callable $nodeCallback): void
4303: {
4304: if ($node instanceof Node) {
4305: $declaringClass = $methodReflection->getDeclaringClass();
4306: if (
4307: $node instanceof Node\Stmt\Class_
4308: && isset($node->namespacedName)
4309: && $declaringClass->getName() === (string) $node->namespacedName
4310: && $declaringClass->getNativeReflection()->getStartLine() === $node->getStartLine()
4311: ) {
4312:
4313: $stmts = $node->stmts;
4314: foreach ($stmts as $stmt) {
4315: if (!$stmt instanceof Node\Stmt\ClassMethod) {
4316: continue;
4317: }
4318:
4319: if ($stmt->name->toString() !== $methodReflection->getName()) {
4320: continue;
4321: }
4322:
4323: if ($stmt->getEndLine() - $stmt->getStartLine() > 50) {
4324: continue;
4325: }
4326:
4327: $scope = $this->scopeFactory->create(ScopeContext::create($fileName))->enterClass($declaringClass);
4328: $this->processStmtNode($stmt, $scope, $storage, $nodeCallback, StatementContext::createTopLevel());
4329: }
4330: return;
4331: }
4332: if ($node instanceof Node\Stmt\ClassLike) {
4333: return;
4334: }
4335: if ($node instanceof Node\FunctionLike) {
4336: return;
4337: }
4338: foreach ($node->getSubNodeNames() as $subNodeName) {
4339: $subNode = $node->{$subNodeName};
4340: $this->processNodesForCalledMethod($subNode, $storage, $fileName, $methodReflection, $nodeCallback);
4341: }
4342: } elseif (is_array($node)) {
4343: foreach ($node as $subNode) {
4344: $this->processNodesForCalledMethod($subNode, $storage, $fileName, $methodReflection, $nodeCallback);
4345: }
4346: }
4347: }
4348:
4349: /**
4350: * @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, ?ResolvedPhpDocBlock}
4351: */
4352: public function getPhpDocs(Scope $scope, Node\FunctionLike|Node\Stmt\Property $node): array
4353: {
4354: $templateTypeMap = TemplateTypeMap::createEmpty();
4355: $phpDocParameterTypes = [];
4356: $phpDocImmediatelyInvokedCallableParameters = [];
4357: $phpDocClosureThisTypeParameters = [];
4358: $phpDocReturnType = null;
4359: $phpDocThrowType = null;
4360: $deprecatedDescription = null;
4361: $isDeprecated = false;
4362: $isInternal = false;
4363: $isFinal = false;
4364: $isPure = null;
4365: $isAllowedPrivateMutation = false;
4366: $acceptsNamedArguments = true;
4367: $isReadOnly = $scope->isInClass() && $scope->getClassReflection()->isImmutable();
4368: $asserts = Assertions::createEmpty();
4369: $selfOutType = null;
4370: $docComment = $node->getDocComment() !== null
4371: ? $node->getDocComment()->getText()
4372: : null;
4373:
4374: $file = $scope->getFile();
4375: $class = $scope->isInClass() ? $scope->getClassReflection()->getName() : null;
4376: $trait = $scope->isInTrait() ? $scope->getTraitReflection()->getName() : null;
4377: $resolvedPhpDoc = null;
4378: $functionName = null;
4379: $phpDocParameterOutTypes = [];
4380:
4381: if ($node instanceof Node\Stmt\ClassMethod) {
4382: if (!$scope->isInClass()) {
4383: throw new ShouldNotHappenException();
4384: }
4385: $functionName = $node->name->name;
4386: $positionalParameterNames = array_map(static function (Node\Param $param): string {
4387: if (!$param->var instanceof Variable || !is_string($param->var->name)) {
4388: throw new ShouldNotHappenException();
4389: }
4390:
4391: return $param->var->name;
4392: }, $node->getParams());
4393: $currentResolvedPhpDoc = null;
4394: if ($docComment !== null) {
4395: $currentResolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
4396: $file,
4397: $class,
4398: $trait,
4399: $node->name->name,
4400: $docComment,
4401: );
4402: }
4403: $methodNameForInheritance = $node->getAttribute('originalTraitMethodName') ?? $node->name->name;
4404: $resolvedPhpDoc = $this->phpDocInheritanceResolver->resolvePhpDocForMethod(
4405: $scope->getClassReflection(),
4406: $methodNameForInheritance,
4407: $currentResolvedPhpDoc,
4408: $positionalParameterNames,
4409: );
4410:
4411: if ($node->name->toLowerString() === '__construct') {
4412: foreach ($node->params as $param) {
4413: if ($param->flags === 0) {
4414: continue;
4415: }
4416:
4417: if ($param->getDocComment() === null) {
4418: continue;
4419: }
4420:
4421: if (
4422: !$param->var instanceof Variable
4423: || !is_string($param->var->name)
4424: ) {
4425: throw new ShouldNotHappenException();
4426: }
4427:
4428: $paramPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
4429: $file,
4430: $class,
4431: $trait,
4432: '__construct',
4433: $param->getDocComment()->getText(),
4434: );
4435: $varTags = $paramPhpDoc->getVarTags();
4436: if (isset($varTags[0]) && count($varTags) === 1) {
4437: $phpDocType = $varTags[0]->getType();
4438: } elseif (isset($varTags[$param->var->name])) {
4439: $phpDocType = $varTags[$param->var->name]->getType();
4440: } else {
4441: continue;
4442: }
4443:
4444: $phpDocParameterTypes[$param->var->name] = $phpDocType;
4445: }
4446: }
4447: } elseif ($node instanceof Node\Stmt\Function_) {
4448: $functionName = trim($scope->getNamespace() . '\\' . $node->name->name, '\\');
4449: } elseif ($node instanceof Node\PropertyHook) {
4450: $propertyName = $node->getAttribute('propertyName');
4451: if ($propertyName !== null) {
4452: $functionName = sprintf('$%s::%s', $propertyName, $node->name->toString());
4453: }
4454: }
4455:
4456: if ($docComment !== null && $resolvedPhpDoc === null) {
4457: $resolvedPhpDoc = $this->fileTypeMapper->getResolvedPhpDoc(
4458: $file,
4459: $class,
4460: $trait,
4461: $functionName,
4462: $docComment,
4463: );
4464: }
4465:
4466: $varTags = [];
4467: if ($resolvedPhpDoc !== null) {
4468: $templateTypeMap = $resolvedPhpDoc->getTemplateTypeMap();
4469: $phpDocImmediatelyInvokedCallableParameters = $resolvedPhpDoc->getParamsImmediatelyInvokedCallable();
4470: foreach ($resolvedPhpDoc->getParamTags() as $paramName => $paramTag) {
4471: if (array_key_exists($paramName, $phpDocParameterTypes)) {
4472: continue;
4473: }
4474: $paramType = $paramTag->getType();
4475: if ($scope->isInClass()) {
4476: $paramType = $this->transformStaticType($scope->getClassReflection(), $paramType);
4477: }
4478: $phpDocParameterTypes[$paramName] = $paramType;
4479: }
4480: foreach ($resolvedPhpDoc->getParamClosureThisTags() as $paramName => $paramClosureThisTag) {
4481: if (array_key_exists($paramName, $phpDocClosureThisTypeParameters)) {
4482: continue;
4483: }
4484: $paramClosureThisType = $paramClosureThisTag->getType();
4485: if ($scope->isInClass()) {
4486: $paramClosureThisType = $this->transformStaticType($scope->getClassReflection(), $paramClosureThisType);
4487: }
4488: $phpDocClosureThisTypeParameters[$paramName] = $paramClosureThisType;
4489: }
4490:
4491: foreach ($resolvedPhpDoc->getParamOutTags() as $paramName => $paramOutTag) {
4492: $phpDocParameterOutTypes[$paramName] = $paramOutTag->getType();
4493: }
4494: if ($node instanceof Node\FunctionLike) {
4495: $nativeReturnType = $scope->getFunctionType($node->getReturnType(), false, false);
4496: $phpDocReturnType = $this->getPhpDocReturnType($resolvedPhpDoc, $nativeReturnType);
4497: if ($phpDocReturnType !== null && $scope->isInClass()) {
4498: $phpDocReturnType = $this->transformStaticType($scope->getClassReflection(), $phpDocReturnType);
4499: }
4500: }
4501: $phpDocThrowType = $resolvedPhpDoc->getThrowsTag() !== null ? $resolvedPhpDoc->getThrowsTag()->getType() : null;
4502: $deprecatedDescription = $resolvedPhpDoc->getDeprecatedTag() !== null ? $resolvedPhpDoc->getDeprecatedTag()->getMessage() : null;
4503: $isDeprecated = $resolvedPhpDoc->isDeprecated();
4504: $isInternal = $resolvedPhpDoc->isInternal();
4505: $isFinal = $resolvedPhpDoc->isFinal();
4506: $isPure = $resolvedPhpDoc->isPure();
4507: $isAllowedPrivateMutation = $resolvedPhpDoc->isAllowedPrivateMutation();
4508: $acceptsNamedArguments = $resolvedPhpDoc->acceptsNamedArguments();
4509: $isReadOnly = $isReadOnly || $resolvedPhpDoc->isReadOnly();
4510: $asserts = Assertions::createFromResolvedPhpDocBlock($resolvedPhpDoc);
4511: $selfOutType = $resolvedPhpDoc->getSelfOutTag() !== null ? $resolvedPhpDoc->getSelfOutTag()->getType() : null;
4512: $varTags = $resolvedPhpDoc->getVarTags();
4513: }
4514:
4515: if ($acceptsNamedArguments && $scope->isInClass()) {
4516: $acceptsNamedArguments = $scope->getClassReflection()->acceptsNamedArguments();
4517: }
4518:
4519: if ($isPure === null && $node instanceof Node\FunctionLike && $scope->isInClass()) {
4520: $classResolvedPhpDoc = $scope->getClassReflection()->getResolvedPhpDoc();
4521: if ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsPure()) {
4522: if (
4523: strtolower($functionName ?? '') === '__construct'
4524: || (
4525: ($phpDocReturnType === null || !$phpDocReturnType->isVoid()->yes())
4526: && !$scope->getFunctionType($node->getReturnType(), false, false)->isVoid()->yes()
4527: )
4528: ) {
4529: $isPure = true;
4530: }
4531: } elseif ($classResolvedPhpDoc !== null && $classResolvedPhpDoc->areAllMethodsImpure()) {
4532: $isPure = false;
4533: }
4534: }
4535:
4536: return [$templateTypeMap, $phpDocParameterTypes, $phpDocImmediatelyInvokedCallableParameters, $phpDocClosureThisTypeParameters, $phpDocReturnType, $phpDocThrowType, $deprecatedDescription, $isDeprecated, $isInternal, $isFinal, $isPure, $acceptsNamedArguments, $isReadOnly, $docComment, $asserts, $selfOutType, $phpDocParameterOutTypes, $varTags, $isAllowedPrivateMutation, $resolvedPhpDoc];
4537: }
4538:
4539: private function transformStaticType(ClassReflection $declaringClass, Type $type): Type
4540: {
4541: return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($declaringClass): Type {
4542: if ($type instanceof StaticType) {
4543: $changedType = $type->changeBaseClass($declaringClass);
4544: if ($declaringClass->isFinal() && !$type instanceof ThisType) {
4545: $changedType = $changedType->getStaticObjectType();
4546: }
4547: return $traverse($changedType);
4548: }
4549:
4550: return $traverse($type);
4551: });
4552: }
4553:
4554: private function getPhpDocReturnType(ResolvedPhpDocBlock $resolvedPhpDoc, Type $nativeReturnType): ?Type
4555: {
4556: $returnTag = $resolvedPhpDoc->getReturnTag();
4557:
4558: if ($returnTag === null) {
4559: return null;
4560: }
4561:
4562: $phpDocReturnType = $returnTag->getType();
4563:
4564: if ($returnTag->isExplicit()) {
4565: return $phpDocReturnType;
4566: }
4567:
4568: if ($nativeReturnType->isSuperTypeOf(TemplateTypeHelper::resolveToBounds($phpDocReturnType))->yes()) {
4569: return $phpDocReturnType;
4570: }
4571:
4572: if ($phpDocReturnType instanceof UnionType) {
4573: $types = [];
4574: foreach ($phpDocReturnType->getTypes() as $innerType) {
4575: if (!$nativeReturnType->isSuperTypeOf($innerType)->yes()) {
4576: continue;
4577: }
4578:
4579: $types[] = $innerType;
4580: }
4581:
4582: if (count($types) === 0) {
4583: return null;
4584: }
4585:
4586: return TypeCombinator::union(...$types);
4587: }
4588:
4589: return null;
4590: }
4591:
4592: /**
4593: * @param array<Node> $nodes
4594: * @return list<Node\Stmt>
4595: */
4596: private function getNextUnreachableStatements(array $nodes, bool $earlyBinding): array
4597: {
4598: $stmts = [];
4599: $isPassedUnreachableStatement = false;
4600: foreach ($nodes as $node) {
4601: if ($earlyBinding && ($node instanceof Node\Stmt\Function_ || $node instanceof Node\Stmt\ClassLike || $node instanceof Node\Stmt\HaltCompiler)) {
4602: continue;
4603: }
4604: if ($isPassedUnreachableStatement && $node instanceof Node\Stmt) {
4605: $stmts[] = $node;
4606: continue;
4607: }
4608: if ($node instanceof Node\Stmt\Nop || $node instanceof Node\Stmt\InlineHTML) {
4609: continue;
4610: }
4611: if (!$node instanceof Node\Stmt) {
4612: continue;
4613: }
4614: $stmts[] = $node;
4615: $isPassedUnreachableStatement = true;
4616: }
4617: return $stmts;
4618: }
4619:
4620: private function inferForLoopExpressions(For_ $stmt, Expr $lastCondExpr, MutatingScope $bodyScope): MutatingScope
4621: {
4622: // infer $items[$i] type from for ($i = 0; $i < count($items); $i++) {...}
4623:
4624: if (
4625: // $i = 0
4626: count($stmt->init) === 1
4627: && $stmt->init[0] instanceof Assign
4628: && $stmt->init[0]->var instanceof Variable
4629: && $stmt->init[0]->expr instanceof Node\Scalar\Int_
4630: && $stmt->init[0]->expr->value === 0
4631: // $i++ or ++$i
4632: && count($stmt->loop) === 1
4633: && ($stmt->loop[0] instanceof Expr\PreInc || $stmt->loop[0] instanceof Expr\PostInc)
4634: && $stmt->loop[0]->var instanceof Variable
4635: ) {
4636: // $i < count($items)
4637: if (
4638: $lastCondExpr instanceof BinaryOp\Smaller
4639: && $lastCondExpr->left instanceof Variable
4640: && $lastCondExpr->right instanceof FuncCall
4641: && $lastCondExpr->right->name instanceof Name
4642: && in_array($lastCondExpr->right->name->toLowerString(), ['count', 'sizeof'], true)
4643: && count($lastCondExpr->right->getArgs()) > 0
4644: && $lastCondExpr->right->getArgs()[0]->value instanceof Variable
4645: && is_string($stmt->init[0]->var->name)
4646: && $stmt->init[0]->var->name === $stmt->loop[0]->var->name
4647: && $stmt->init[0]->var->name === $lastCondExpr->left->name
4648: ) {
4649: $arrayArg = $lastCondExpr->right->getArgs()[0]->value;
4650: $arrayType = $bodyScope->getType($arrayArg);
4651: if ($arrayType->isList()->yes()) {
4652: $bodyScope = $bodyScope->assignExpression(
4653: new ArrayDimFetch($lastCondExpr->right->getArgs()[0]->value, $lastCondExpr->left),
4654: $arrayType->getIterableValueType(),
4655: $bodyScope->getNativeType($arrayArg)->getIterableValueType(),
4656: );
4657: }
4658: }
4659:
4660: // count($items) > $i
4661: if (
4662: $lastCondExpr instanceof BinaryOp\Greater
4663: && $lastCondExpr->right instanceof Variable
4664: && $lastCondExpr->left instanceof FuncCall
4665: && $lastCondExpr->left->name instanceof Name
4666: && in_array($lastCondExpr->left->name->toLowerString(), ['count', 'sizeof'], true)
4667: && count($lastCondExpr->left->getArgs()) > 0
4668: && $lastCondExpr->left->getArgs()[0]->value instanceof Variable
4669: && is_string($stmt->init[0]->var->name)
4670: && $stmt->init[0]->var->name === $stmt->loop[0]->var->name
4671: && $stmt->init[0]->var->name === $lastCondExpr->right->name
4672: ) {
4673: $arrayArg = $lastCondExpr->left->getArgs()[0]->value;
4674: $arrayType = $bodyScope->getType($arrayArg);
4675: if ($arrayType->isList()->yes()) {
4676: $bodyScope = $bodyScope->assignExpression(
4677: new ArrayDimFetch($lastCondExpr->left->getArgs()[0]->value, $lastCondExpr->right),
4678: $arrayType->getIterableValueType(),
4679: $bodyScope->getNativeType($arrayArg)->getIterableValueType(),
4680: );
4681: }
4682: }
4683: }
4684:
4685: return $bodyScope;
4686: }
4687:
4688: }
4689: