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