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