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