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