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