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