1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use PhpParser\Node;
6: use PhpParser\Node\Arg;
7: use PhpParser\Node\ComplexType;
8: use PhpParser\Node\Expr;
9: use PhpParser\Node\Expr\Array_;
10: use PhpParser\Node\Expr\ConstFetch;
11: use PhpParser\Node\Expr\FuncCall;
12: use PhpParser\Node\Expr\Match_;
13: use PhpParser\Node\Expr\MethodCall;
14: use PhpParser\Node\Expr\PropertyFetch;
15: use PhpParser\Node\Expr\Variable;
16: use PhpParser\Node\Identifier;
17: use PhpParser\Node\Name;
18: use PhpParser\Node\Name\FullyQualified;
19: use PhpParser\Node\PropertyHook;
20: use PhpParser\Node\Scalar;
21: use PhpParser\Node\Scalar\String_;
22: use PhpParser\Node\Stmt\ClassMethod;
23: use PhpParser\Node\Stmt\Function_;
24: use PhpParser\NodeFinder;
25: use PHPStan\Analyser\Traverser\TransformStaticTypeTraverser;
26: use PHPStan\Collectors\Collector;
27: use PHPStan\DependencyInjection\Container;
28: use PHPStan\Node\EmitCollectedDataNode;
29: use PHPStan\Node\Expr\AlwaysRememberedExpr;
30: use PHPStan\Node\Expr\CloneReinitializationExpr;
31: use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
32: use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr;
33: use PHPStan\Node\Expr\OriginalForeachKeyExpr;
34: use PHPStan\Node\Expr\OriginalForeachValueExpr;
35: use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr;
36: use PHPStan\Node\Expr\PossiblyImpureCallExpr;
37: use PHPStan\Node\Expr\PropertyInitializationExpr;
38: use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr;
39: use PHPStan\Node\IssetExpr;
40: use PHPStan\Node\Printer\ExprPrinter;
41: use PHPStan\Node\VirtualNode;
42: use PHPStan\Parser\ArrayMapArgVisitor;
43: use PHPStan\Parser\Parser;
44: use PHPStan\Php\PhpVersion;
45: use PHPStan\Php\PhpVersionFactory;
46: use PHPStan\Php\PhpVersions;
47: use PHPStan\PhpDoc\ResolvedPhpDocBlock;
48: use PHPStan\Reflection\Assertions;
49: use PHPStan\Reflection\AttributeReflection;
50: use PHPStan\Reflection\AttributeReflectionFactory;
51: use PHPStan\Reflection\ClassConstantReflection;
52: use PHPStan\Reflection\ClassMemberReflection;
53: use PHPStan\Reflection\ClassReflection;
54: use PHPStan\Reflection\ExtendedMethodReflection;
55: use PHPStan\Reflection\ExtendedPropertyReflection;
56: use PHPStan\Reflection\FunctionReflection;
57: use PHPStan\Reflection\InitializerExprContext;
58: use PHPStan\Reflection\InitializerExprTypeResolver;
59: use PHPStan\Reflection\MethodReflection;
60: use PHPStan\Reflection\ParameterReflection;
61: use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection;
62: use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
63: use PHPStan\Reflection\PropertyReflection;
64: use PHPStan\Reflection\ReflectionProvider;
65: use PHPStan\Rules\Properties\PropertyReflectionFinder;
66: use PHPStan\ShouldNotHappenException;
67: use PHPStan\TrinaryLogic;
68: use PHPStan\Type\Accessory\AccessoryArrayListType;
69: use PHPStan\Type\Accessory\HasOffsetValueType;
70: use PHPStan\Type\Accessory\NonEmptyArrayType;
71: use PHPStan\Type\Accessory\OversizedArrayType;
72: use PHPStan\Type\ArrayType;
73: use PHPStan\Type\BenevolentUnionType;
74: use PHPStan\Type\ClosureType;
75: use PHPStan\Type\ConditionalTypeForParameter;
76: use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
77: use PHPStan\Type\Constant\ConstantBooleanType;
78: use PHPStan\Type\Constant\ConstantFloatType;
79: use PHPStan\Type\Constant\ConstantIntegerType;
80: use PHPStan\Type\Constant\ConstantStringType;
81: use PHPStan\Type\ConstantTypeHelper;
82: use PHPStan\Type\ErrorType;
83: use PHPStan\Type\ExpressionTypeResolverExtensionRegistry;
84: use PHPStan\Type\GeneralizePrecision;
85: use PHPStan\Type\Generic\TemplateTypeHelper;
86: use PHPStan\Type\Generic\TemplateTypeMap;
87: use PHPStan\Type\IntegerRangeType;
88: use PHPStan\Type\IntegerType;
89: use PHPStan\Type\IntersectionType;
90: use PHPStan\Type\MixedType;
91: use PHPStan\Type\NeverType;
92: use PHPStan\Type\NullType;
93: use PHPStan\Type\ObjectType;
94: use PHPStan\Type\StaticType;
95: use PHPStan\Type\StaticTypeFactory;
96: use PHPStan\Type\StringType;
97: use PHPStan\Type\ThisType;
98: use PHPStan\Type\Type;
99: use PHPStan\Type\TypeCombinator;
100: use PHPStan\Type\TypeTraverser;
101: use PHPStan\Type\TypeUtils;
102: use PHPStan\Type\TypeWithClassName;
103: use PHPStan\Type\UnionType;
104: use PHPStan\Type\VerbosityLevel;
105: use PHPStan\Type\VoidType;
106: use Throwable;
107: use function abs;
108: use function array_filter;
109: use function array_key_exists;
110: use function array_key_first;
111: use function array_keys;
112: use function array_last;
113: use function array_map;
114: use function array_merge;
115: use function array_pop;
116: use function array_slice;
117: use function array_unique;
118: use function array_values;
119: use function assert;
120: use function count;
121: use function explode;
122: use function get_class;
123: use function implode;
124: use function in_array;
125: use function is_array;
126: use function is_string;
127: use function ltrim;
128: use function md5;
129: use function sprintf;
130: use function str_contains;
131: use function str_starts_with;
132: use function strlen;
133: use function strtolower;
134: use function substr;
135: use function uksort;
136: use function usort;
137: use const PHP_INT_MAX;
138: use const PHP_INT_MIN;
139: use const PHP_VERSION_ID;
140:
141: class MutatingScope implements Scope, NodeCallbackInvoker, CollectedDataEmitter
142: {
143:
144: public const KEEP_VOID_ATTRIBUTE_NAME = 'keepVoid';
145: private const CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME = 'containsSuperGlobal';
146:
147: private const COMPLEX_UNION_TYPE_MEMBER_LIMIT = 8;
148:
149: /** @var Type[] */
150: private array $resolvedTypes = [];
151:
152: /** @var array<string, static> */
153: private array $truthyScopes = [];
154:
155: /** @var array<string, static> */
156: private array $falseyScopes = [];
157:
158: private ?self $fiberScope = null;
159:
160: /** @var non-empty-string|null */
161: private ?string $namespace;
162:
163: private ?self $scopeOutOfFirstLevelStatement = null;
164:
165: private ?self $scopeWithPromotedNativeTypes = null;
166:
167: /**
168: * @param int|array{min: int, max: int}|null $configPhpVersion
169: * @param callable(Node $node, Scope $scope): void|null $nodeCallback
170: * @param array<string, ExpressionTypeHolder> $expressionTypes
171: * @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
172: * @param list<non-empty-string> $inClosureBindScopeClasses
173: * @param array<string, true> $currentlyAssignedExpressions
174: * @param array<string, true> $currentlyAllowedUndefinedExpressions
175: * @param array<string, ExpressionTypeHolder> $nativeExpressionTypes
176: * @param list<array{MethodReflection|FunctionReflection|null, ParameterReflection|null}> $inFunctionCallsStack
177: */
178: public function __construct(
179: private Container $container,
180: protected InternalScopeFactory $scopeFactory,
181: private ReflectionProvider $reflectionProvider,
182: private InitializerExprTypeResolver $initializerExprTypeResolver,
183: private ExpressionTypeResolverExtensionRegistry $expressionTypeResolverExtensionRegistry,
184: private ExprPrinter $exprPrinter,
185: private TypeSpecifier $typeSpecifier,
186: private PropertyReflectionFinder $propertyReflectionFinder,
187: private Parser $parser,
188: private ConstantResolver $constantResolver,
189: protected ScopeContext $context,
190: private PhpVersion $phpVersion,
191: private AttributeReflectionFactory $attributeReflectionFactory,
192: private int|array|null $configPhpVersion,
193: private $nodeCallback = null,
194: private bool $declareStrictTypes = false,
195: private PhpFunctionFromParserNodeReflection|null $function = null,
196: ?string $namespace = null,
197: public array $expressionTypes = [],
198: protected array $nativeExpressionTypes = [],
199: protected array $conditionalExpressions = [],
200: protected array $inClosureBindScopeClasses = [],
201: private ?ClosureType $anonymousFunctionReflection = null,
202: private bool $inFirstLevelStatement = true,
203: protected array $currentlyAssignedExpressions = [],
204: protected array $currentlyAllowedUndefinedExpressions = [],
205: public array $inFunctionCallsStack = [],
206: protected bool $afterExtractCall = false,
207: private ?self $parentScope = null,
208: public bool $nativeTypesPromoted = false,
209: )
210: {
211: if ($namespace === '') {
212: $namespace = null;
213: }
214:
215: $this->namespace = $namespace;
216: }
217:
218: public function toFiberScope(): self
219: {
220: if (PHP_VERSION_ID < 80100) {
221: throw new ShouldNotHappenException('Cannot create FiberScope below PHP 8.1');
222: }
223:
224: if ($this->fiberScope !== null) {
225: return $this->fiberScope;
226: }
227:
228: return $this->fiberScope = $this->scopeFactory->toFiberFactory()->create(
229: $this->context,
230: $this->isDeclareStrictTypes(),
231: $this->getFunction(),
232: $this->getNamespace(),
233: $this->expressionTypes,
234: $this->nativeExpressionTypes,
235: $this->conditionalExpressions,
236: $this->inClosureBindScopeClasses,
237: $this->anonymousFunctionReflection,
238: $this->isInFirstLevelStatement(),
239: $this->currentlyAssignedExpressions,
240: $this->currentlyAllowedUndefinedExpressions,
241: $this->inFunctionCallsStack,
242: $this->afterExtractCall,
243: $this->parentScope,
244: $this->nativeTypesPromoted,
245: );
246: }
247:
248: public function toMutatingScope(): self
249: {
250: return $this;
251: }
252:
253: /** @api */
254: public function getFile(): string
255: {
256: return $this->context->getFile();
257: }
258:
259: /** @api */
260: public function getFileDescription(): string
261: {
262: if ($this->context->getTraitReflection() === null) {
263: return $this->getFile();
264: }
265:
266: /** @var ClassReflection $classReflection */
267: $classReflection = $this->context->getClassReflection();
268:
269: $className = $classReflection->getDisplayName();
270: if (!$classReflection->isAnonymous()) {
271: $className = sprintf('class %s', $className);
272: }
273:
274: $traitReflection = $this->context->getTraitReflection();
275: if ($traitReflection->getFileName() === null) {
276: throw new ShouldNotHappenException();
277: }
278:
279: return sprintf(
280: '%s (in context of %s)',
281: $traitReflection->getFileName(),
282: $className,
283: );
284: }
285:
286: /** @api */
287: public function isDeclareStrictTypes(): bool
288: {
289: return $this->declareStrictTypes;
290: }
291:
292: public function enterDeclareStrictTypes(): self
293: {
294: return $this->scopeFactory->create(
295: $this->context,
296: true,
297: null,
298: null,
299: $this->expressionTypes,
300: $this->nativeExpressionTypes,
301: );
302: }
303:
304: /**
305: * @param array<string, ExpressionTypeHolder> $currentExpressionTypes
306: * @return array<string, ExpressionTypeHolder>
307: */
308: private function rememberConstructorExpressions(array $currentExpressionTypes): array
309: {
310: $expressionTypes = [];
311: foreach ($currentExpressionTypes as $exprString => $expressionTypeHolder) {
312: $expr = $expressionTypeHolder->getExpr();
313: if ($expr instanceof FuncCall) {
314: if (
315: !$expr->name instanceof Name
316: // interface_exists() etc. imply class_exists() therefore not listed here
317: || !in_array($expr->name->name, ['class_exists', 'function_exists'], true)
318: ) {
319: continue;
320: }
321: } elseif ($expr instanceof PropertyFetch) {
322: if (!$this->isReadonlyPropertyFetch($expr, true)) {
323: continue;
324: }
325: } elseif (!$expr instanceof ConstFetch && !$expr instanceof PropertyInitializationExpr) {
326: continue;
327: }
328:
329: $expressionTypes[$exprString] = $expressionTypeHolder;
330: }
331:
332: if (array_key_exists('$this', $currentExpressionTypes)) {
333: $expressionTypes['$this'] = $currentExpressionTypes['$this'];
334: }
335:
336: return $expressionTypes;
337: }
338:
339: public function rememberConstructorScope(): self
340: {
341: return $this->scopeFactory->create(
342: $this->context,
343: $this->isDeclareStrictTypes(),
344: null,
345: $this->getNamespace(),
346: $this->rememberConstructorExpressions($this->expressionTypes),
347: $this->rememberConstructorExpressions($this->nativeExpressionTypes),
348: $this->conditionalExpressions,
349: $this->inClosureBindScopeClasses,
350: $this->anonymousFunctionReflection,
351: $this->inFirstLevelStatement,
352: [],
353: [],
354: $this->inFunctionCallsStack,
355: $this->afterExtractCall,
356: $this->parentScope,
357: $this->nativeTypesPromoted,
358: );
359: }
360:
361: private function isReadonlyPropertyFetch(PropertyFetch $expr, bool $allowOnlyOnThis): bool
362: {
363: if (!$this->phpVersion->supportsReadOnlyProperties()) {
364: return false;
365: }
366:
367: while ($expr instanceof PropertyFetch) {
368: if ($expr->var instanceof Variable) {
369: if (
370: $allowOnlyOnThis
371: && (
372: ! $expr->name instanceof Node\Identifier
373: || !is_string($expr->var->name)
374: || $expr->var->name !== 'this'
375: )
376: ) {
377: return false;
378: }
379: } elseif (!$expr->var instanceof PropertyFetch) {
380: return false;
381: }
382:
383: $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this);
384: if ($propertyReflection === null) {
385: return false;
386: }
387:
388: $nativePropertyReflection = $propertyReflection->getNativeReflection();
389: if ($nativePropertyReflection === null || !$nativePropertyReflection->isReadOnly()) {
390: return false;
391: }
392:
393: $expr = $expr->var;
394: }
395:
396: return true;
397: }
398:
399: /** @api */
400: public function isInClass(): bool
401: {
402: return $this->context->getClassReflection() !== null;
403: }
404:
405: /** @api */
406: public function isInTrait(): bool
407: {
408: return $this->context->getTraitReflection() !== null;
409: }
410:
411: /** @api */
412: public function getClassReflection(): ?ClassReflection
413: {
414: return $this->context->getClassReflection();
415: }
416:
417: /** @api */
418: public function getTraitReflection(): ?ClassReflection
419: {
420: return $this->context->getTraitReflection();
421: }
422:
423: /**
424: * @api
425: */
426: public function getFunction(): ?PhpFunctionFromParserNodeReflection
427: {
428: return $this->function;
429: }
430:
431: /** @api */
432: public function getFunctionName(): ?string
433: {
434: return $this->function !== null ? $this->function->getName() : null;
435: }
436:
437: /** @api */
438: public function getNamespace(): ?string
439: {
440: return $this->namespace;
441: }
442:
443: /** @api */
444: public function getParentScope(): ?self
445: {
446: return $this->parentScope;
447: }
448:
449: /** @api */
450: public function canAnyVariableExist(): bool
451: {
452: return ($this->function === null && !$this->isInAnonymousFunction()) || $this->afterExtractCall;
453: }
454:
455: public function afterExtractCall(): self
456: {
457: return $this->scopeFactory->create(
458: $this->context,
459: $this->isDeclareStrictTypes(),
460: $this->getFunction(),
461: $this->getNamespace(),
462: $this->expressionTypes,
463: $this->nativeExpressionTypes,
464: [],
465: $this->inClosureBindScopeClasses,
466: $this->anonymousFunctionReflection,
467: $this->isInFirstLevelStatement(),
468: $this->currentlyAssignedExpressions,
469: $this->currentlyAllowedUndefinedExpressions,
470: $this->inFunctionCallsStack,
471: true,
472: $this->parentScope,
473: $this->nativeTypesPromoted,
474: );
475: }
476:
477: public function afterClearstatcacheCall(): self
478: {
479: $changed = false;
480:
481: $expressionTypes = $this->expressionTypes;
482: $nativeExpressionTypes = $this->nativeExpressionTypes;
483: foreach (array_keys($expressionTypes) as $exprString) {
484: // list from https://www.php.net/manual/en/function.clearstatcache.php
485:
486: // 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().
487: foreach ([
488: 'stat',
489: 'lstat',
490: 'file_exists',
491: 'is_writable',
492: 'is_writeable',
493: 'is_readable',
494: 'is_executable',
495: 'is_file',
496: 'is_dir',
497: 'is_link',
498: 'filectime',
499: 'fileatime',
500: 'filemtime',
501: 'fileinode',
502: 'filegroup',
503: 'fileowner',
504: 'filesize',
505: 'filetype',
506: 'fileperms',
507: ] as $functionName) {
508: if (!str_starts_with($exprString, $functionName . '(') && !str_starts_with($exprString, '\\' . $functionName . '(')) {
509: continue;
510: }
511:
512: unset($expressionTypes[$exprString]);
513: unset($nativeExpressionTypes[$exprString]);
514: $changed = true;
515: continue 2;
516: }
517: }
518:
519: if (!$changed) {
520: return $this;
521: }
522:
523: return $this->scopeFactory->create(
524: $this->context,
525: $this->isDeclareStrictTypes(),
526: $this->getFunction(),
527: $this->getNamespace(),
528: $expressionTypes,
529: $nativeExpressionTypes,
530: $this->conditionalExpressions,
531: $this->inClosureBindScopeClasses,
532: $this->anonymousFunctionReflection,
533: $this->isInFirstLevelStatement(),
534: $this->currentlyAssignedExpressions,
535: $this->currentlyAllowedUndefinedExpressions,
536: $this->inFunctionCallsStack,
537: $this->afterExtractCall,
538: $this->parentScope,
539: $this->nativeTypesPromoted,
540: );
541: }
542:
543: public function afterOpenSslCall(string $openSslFunctionName): self
544: {
545: $expressionTypes = $this->expressionTypes;
546: $nativeExpressionTypes = $this->nativeExpressionTypes;
547:
548: $errorStringFunction = '\openssl_error_string()';
549: if (
550: !array_key_exists($errorStringFunction, $expressionTypes)
551: && !array_key_exists($errorStringFunction, $nativeExpressionTypes)
552: ) {
553: return $this;
554: }
555:
556: $changed = false;
557: if (in_array($openSslFunctionName, [
558: 'openssl_cipher_iv_length',
559: 'openssl_cms_decrypt',
560: 'openssl_cms_encrypt',
561: 'openssl_cms_read',
562: 'openssl_cms_sign',
563: 'openssl_cms_verify',
564: 'openssl_csr_export_to_file',
565: 'openssl_csr_export',
566: 'openssl_csr_get_public_key',
567: 'openssl_csr_get_subject',
568: 'openssl_csr_new',
569: 'openssl_csr_sign',
570: 'openssl_decrypt',
571: 'openssl_dh_compute_key',
572: 'openssl_digest',
573: 'openssl_encrypt',
574: 'openssl_get_curve_names',
575: 'openssl_get_privatekey',
576: 'openssl_get_publickey',
577: 'openssl_open',
578: 'openssl_pbkdf2',
579: 'openssl_pkcs12_export_to_file',
580: 'openssl_pkcs12_export',
581: 'openssl_pkcs12_read',
582: 'openssl_pkcs7_decrypt',
583: 'openssl_pkcs7_encrypt',
584: 'openssl_pkcs7_read',
585: 'openssl_pkcs7_sign',
586: 'openssl_pkcs7_verify',
587: 'openssl_pkey_derive',
588: 'openssl_pkey_export_to_file',
589: 'openssl_pkey_export',
590: 'openssl_pkey_get_private',
591: 'openssl_pkey_get_public',
592: 'openssl_pkey_new',
593: 'openssl_private_decrypt',
594: 'openssl_private_encrypt',
595: 'openssl_public_decrypt',
596: 'openssl_public_encrypt',
597: 'openssl_random_pseudo_bytes',
598: 'openssl_seal',
599: 'openssl_sign',
600: 'openssl_spki_export_challenge',
601: 'openssl_spki_export',
602: 'openssl_spki_new',
603: 'openssl_spki_verify',
604: 'openssl_verify',
605: 'openssl_x509_checkpurpose',
606: 'openssl_x509_export_to_file',
607: 'openssl_x509_export',
608: 'openssl_x509_fingerprint',
609: 'openssl_x509_read',
610: 'openssl_x509_verify',
611: ], true)) {
612: unset($expressionTypes[$errorStringFunction]);
613: unset($nativeExpressionTypes[$errorStringFunction]);
614: $changed = true;
615: }
616:
617: if (!$changed) {
618: return $this;
619: }
620:
621: return $this->scopeFactory->create(
622: $this->context,
623: $this->isDeclareStrictTypes(),
624: $this->getFunction(),
625: $this->getNamespace(),
626: $expressionTypes,
627: $nativeExpressionTypes,
628: $this->conditionalExpressions,
629: $this->inClosureBindScopeClasses,
630: $this->anonymousFunctionReflection,
631: $this->isInFirstLevelStatement(),
632: $this->currentlyAssignedExpressions,
633: $this->currentlyAllowedUndefinedExpressions,
634: $this->inFunctionCallsStack,
635: $this->afterExtractCall,
636: $this->parentScope,
637: $this->nativeTypesPromoted,
638: );
639: }
640:
641: /** @api */
642: public function hasVariableType(string $variableName): TrinaryLogic
643: {
644: if ($this->isGlobalVariable($variableName)) {
645: return TrinaryLogic::createYes();
646: }
647:
648: $varExprString = '$' . $variableName;
649: if (!isset($this->expressionTypes[$varExprString])) {
650: if ($this->canAnyVariableExist()) {
651: return TrinaryLogic::createMaybe();
652: }
653:
654: return TrinaryLogic::createNo();
655: }
656:
657: return $this->expressionTypes[$varExprString]->getCertainty();
658: }
659:
660: /** @api */
661: public function getVariableType(string $variableName): Type
662: {
663: $hasVariableType = $this->hasVariableType($variableName);
664:
665: if ($hasVariableType->maybe()) {
666: if ($variableName === 'argc') {
667: return StaticTypeFactory::argc();
668: }
669: if ($variableName === 'argv') {
670: return StaticTypeFactory::argv();
671: }
672: if ($this->canAnyVariableExist()) {
673: return new MixedType();
674: }
675: }
676:
677: if ($hasVariableType->no()) {
678: throw new UndefinedVariableException($this, $variableName);
679: }
680:
681: $varExprString = '$' . $variableName;
682: if (!array_key_exists($varExprString, $this->expressionTypes)) {
683: if ($this->isGlobalVariable($variableName)) {
684: return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType(true));
685: }
686: return new MixedType();
687: }
688:
689: return $this->expressionTypes[$varExprString]->getType();
690: }
691:
692: /**
693: * @api
694: * @return list<string>
695: */
696: public function getDefinedVariables(): array
697: {
698: $variables = [];
699: foreach ($this->expressionTypes as $exprString => $holder) {
700: if (!$holder->getExpr() instanceof Variable) {
701: continue;
702: }
703: if (!$holder->getCertainty()->yes()) {
704: continue;
705: }
706:
707: $variables[] = substr($exprString, 1);
708: }
709:
710: return $variables;
711: }
712:
713: /**
714: * @api
715: * @return list<string>
716: */
717: public function getMaybeDefinedVariables(): array
718: {
719: $variables = [];
720: foreach ($this->expressionTypes as $exprString => $holder) {
721: if (!$holder->getExpr() instanceof Variable) {
722: continue;
723: }
724: if (!$holder->getCertainty()->maybe()) {
725: continue;
726: }
727:
728: $variables[] = substr($exprString, 1);
729: }
730:
731: return $variables;
732: }
733:
734: /**
735: * @return list<string>
736: */
737: public function findPossiblyImpureCallDescriptions(Expr $expr): array
738: {
739: $nodeFinder = new NodeFinder();
740: $callExprDescriptions = [];
741: $foundCallExprMatch = false;
742: $matchedCallExprKeys = [];
743: foreach ($this->expressionTypes as $holder) {
744: $holderExpr = $holder->getExpr();
745: if (!$holderExpr instanceof PossiblyImpureCallExpr) {
746: continue;
747: }
748:
749: $callExprKey = $this->getNodeKey($holderExpr->callExpr);
750:
751: $found = $nodeFinder->findFirst([$expr], function (Node $node) use ($callExprKey): bool {
752: if (!$node instanceof Expr) {
753: return false;
754: }
755:
756: return $this->getNodeKey($node) === $callExprKey;
757: });
758:
759: if ($found === null) {
760: continue;
761: }
762:
763: $foundCallExprMatch = true;
764: $matchedCallExprKeys[$callExprKey] = true;
765:
766: // Only show the tip when the scope's type for the call expression
767: // differs from the declared return type, meaning control flow
768: // narrowing affected the type (the cached value was narrowed).
769: assert($found instanceof Expr);
770: $scopeType = $this->getType($found);
771: $declaredReturnType = $holder->getType();
772: if ($declaredReturnType->isSuperTypeOf($scopeType)->yes() && $scopeType->isSuperTypeOf($declaredReturnType)->yes()) {
773: continue;
774: }
775:
776: $callExprDescriptions[] = $holderExpr->getCallDescription();
777: }
778:
779: // If the first pass found a callExpr in the error expression but
780: // filtered it out (return type wasn't narrowed), the error is
781: // explained by the return type alone - skip the fallback.
782: if ($foundCallExprMatch && count($callExprDescriptions) === 0) {
783: return [];
784: }
785:
786: // Second pass: match by impactedExpr for cases where a maybe-impure method
787: // on an object didn't invalidate it, but a different method's return
788: // value was narrowed on that object.
789: // Skip when the expression itself is a direct method/static call -
790: // those are passed by ImpossibleCheckType rules where the error is
791: // about the call's arguments, not about object state.
792: if (!($expr instanceof Expr\MethodCall || $expr instanceof Expr\StaticCall)) {
793: $impactedExprDescriptions = [];
794: foreach ($this->expressionTypes as $holder) {
795: $holderExpr = $holder->getExpr();
796: if (!$holderExpr instanceof PossiblyImpureCallExpr) {
797: continue;
798: }
799:
800: $impactedExprKey = $this->getNodeKey($holderExpr->impactedExpr);
801:
802: // Skip if impactedExpr is the same as callExpr (function calls)
803: if ($impactedExprKey === $this->getNodeKey($holderExpr->callExpr)) {
804: continue;
805: }
806:
807: // Skip if this entry's callExpr was already matched in the first pass
808: $callExprKey = $this->getNodeKey($holderExpr->callExpr);
809: if (isset($matchedCallExprKeys[$callExprKey])) {
810: continue;
811: }
812:
813: $found = $nodeFinder->findFirst([$expr], function (Node $node) use ($impactedExprKey): bool {
814: if (!$node instanceof Expr) {
815: return false;
816: }
817:
818: return $this->getNodeKey($node) === $impactedExprKey;
819: });
820:
821: if ($found === null) {
822: continue;
823: }
824:
825: $impactedExprDescriptions[] = $holderExpr->getCallDescription();
826: }
827:
828: // Prefer impactedExpr matches (intermediate calls that could have
829: // invalidated the object) over callExpr matches
830: if (count($impactedExprDescriptions) > 0) {
831: return array_values(array_unique($impactedExprDescriptions));
832: }
833: }
834:
835: if (count($callExprDescriptions) > 0) {
836: return array_values(array_unique($callExprDescriptions));
837: }
838:
839: return [];
840: }
841:
842: private function isGlobalVariable(string $variableName): bool
843: {
844: return in_array($variableName, self::SUPERGLOBAL_VARIABLES, true);
845: }
846:
847: /** @api */
848: public function hasConstant(Name $name): bool
849: {
850: $isCompilerHaltOffset = $name->toString() === '__COMPILER_HALT_OFFSET__';
851: if ($isCompilerHaltOffset) {
852: return $this->fileHasCompilerHaltStatementCalls();
853: }
854:
855: if ($this->getGlobalConstantType($name) !== null) {
856: return true;
857: }
858:
859: return $this->reflectionProvider->hasConstant($name, $this);
860: }
861:
862: private function fileHasCompilerHaltStatementCalls(): bool
863: {
864: $nodes = $this->parser->parseFile($this->getFile());
865: foreach ($nodes as $node) {
866: if ($node instanceof Node\Stmt\HaltCompiler) {
867: return true;
868: }
869: }
870:
871: return false;
872: }
873:
874: /** @api */
875: public function isInAnonymousFunction(): bool
876: {
877: return $this->anonymousFunctionReflection !== null;
878: }
879:
880: /** @api */
881: public function getAnonymousFunctionReflection(): ?ClosureType
882: {
883: return $this->anonymousFunctionReflection;
884: }
885:
886: /** @api */
887: public function getAnonymousFunctionReturnType(): ?Type
888: {
889: if ($this->anonymousFunctionReflection === null) {
890: return null;
891: }
892:
893: return $this->anonymousFunctionReflection->getReturnType();
894: }
895:
896: /** @api */
897: public function getType(Expr $node): Type
898: {
899: $key = $this->getNodeKey($node);
900:
901: if (!array_key_exists($key, $this->resolvedTypes)) {
902: $this->resolvedTypes[$key] = TypeUtils::resolveLateResolvableTypes($this->resolveType($key, $node));
903: }
904: return $this->resolvedTypes[$key];
905: }
906:
907: public function getScopeType(Expr $expr): Type
908: {
909: return $this->getType($expr);
910: }
911:
912: public function getScopeNativeType(Expr $expr): Type
913: {
914: return $this->getNativeType($expr);
915: }
916:
917: public function getNodeKey(Expr $node): string
918: {
919: // perf optimize for the most common path
920: if ($node instanceof Variable && !$node->name instanceof Expr) {
921: return '$' . $node->name;
922: }
923:
924: $key = $this->exprPrinter->printExpr($node);
925: $attributes = $node->getAttributes();
926: if (
927: $node instanceof Node\FunctionLike
928: && (($attributes[ArrayMapArgVisitor::ATTRIBUTE_NAME] ?? null) !== null)
929: && (($attributes['startFilePos'] ?? null) !== null)
930: ) {
931: $key .= '/*' . $attributes['startFilePos'];
932: foreach ($attributes[ArrayMapArgVisitor::ATTRIBUTE_NAME] as $arg) {
933: $key .= ':' . $this->exprPrinter->printExpr($arg->value);
934: }
935: $key .= '*/';
936: }
937:
938: if (($attributes[self::KEEP_VOID_ATTRIBUTE_NAME] ?? null) === true) {
939: $key .= '/*' . self::KEEP_VOID_ATTRIBUTE_NAME . '*/';
940: }
941:
942: return $key;
943: }
944:
945: public function getClosureScopeCacheKey(): string
946: {
947: $parts = [];
948: foreach ($this->expressionTypes as $exprString => $expressionTypeHolder) {
949: if ($expressionTypeHolder->getExpr() instanceof VirtualNode) {
950: continue;
951: }
952: $parts[] = sprintf('%s::%s', $exprString, $expressionTypeHolder->getType()->describe(VerbosityLevel::cache()));
953: }
954: $parts[] = '---';
955:
956: $parts[] = sprintf(':%d', count($this->inFunctionCallsStack));
957: foreach ($this->inFunctionCallsStack as [$method, $parameter]) {
958: if ($parameter === null) {
959: $parts[] = ',null';
960: continue;
961: }
962:
963: $parts[] = sprintf(',%s', $parameter->getType()->describe(VerbosityLevel::cache()));
964: }
965:
966: return md5(implode("\n", $parts));
967: }
968:
969: private function resolveType(string $exprString, Expr $node): Type
970: {
971: foreach ($this->expressionTypeResolverExtensionRegistry->getExtensions() as $extension) {
972: $type = $extension->getType($node, $this);
973: if ($type !== null) {
974: return $type;
975: }
976: }
977:
978: if (
979: !$node instanceof Variable
980: && !$node instanceof Expr\Closure
981: && !$node instanceof Expr\ArrowFunction
982: && $this->hasExpressionType($node)->yes()
983: ) {
984: return $this->expressionTypes[$exprString]->getType();
985: }
986:
987: /** @var ExprHandler<Expr> $exprHandler */
988: foreach ($this->container->getServicesByTag(ExprHandler::EXTENSION_TAG) as $exprHandler) {
989: if (!$exprHandler->supports($node)) {
990: continue;
991: }
992:
993: return $exprHandler->resolveType($this, $node);
994: }
995:
996: return new MixedType();
997: }
998:
999: /**
1000: * @param callable(Type): ?bool $typeCallback
1001: */
1002: public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = null): ?bool
1003: {
1004: // mirrored in PHPStan\Rules\IssetCheck
1005: if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) {
1006: $hasVariable = $this->hasVariableType($expr->name);
1007: if ($hasVariable->maybe()) {
1008: return null;
1009: }
1010:
1011: if ($result === null) {
1012: if ($hasVariable->yes()) {
1013: if ($expr->name === '_SESSION') {
1014: return null;
1015: }
1016:
1017: return $typeCallback($this->getVariableType($expr->name));
1018: }
1019:
1020: return false;
1021: }
1022:
1023: return $result;
1024: } elseif ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) {
1025: $type = $this->getType($expr->var);
1026: if (!$type->isOffsetAccessible()->yes()) {
1027: return $result ?? $this->issetCheckUndefined($expr->var);
1028: }
1029:
1030: $dimType = $this->getType($expr->dim);
1031: $hasOffsetValue = $type->hasOffsetValueType($dimType);
1032: if ($hasOffsetValue->no()) {
1033: return false;
1034: }
1035:
1036: // If offset cannot be null, store this error message and see if one of the earlier offsets is.
1037: // E.g. $array['a']['b']['c'] ?? null; is a valid coalesce if a OR b or C might be null.
1038: if ($hasOffsetValue->yes()) {
1039: $result = $typeCallback($type->getOffsetValueType($dimType));
1040:
1041: if ($result !== null) {
1042: return $this->issetCheck($expr->var, $typeCallback, $result);
1043: }
1044: }
1045:
1046: // Has offset, it is nullable
1047: return null;
1048:
1049: } elseif ($expr instanceof Node\Expr\PropertyFetch || $expr instanceof Node\Expr\StaticPropertyFetch) {
1050:
1051: $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this);
1052:
1053: if ($propertyReflection === null) {
1054: if ($expr instanceof Node\Expr\PropertyFetch) {
1055: return $this->issetCheckUndefined($expr->var);
1056: }
1057:
1058: if ($expr->class instanceof Expr) {
1059: return $this->issetCheckUndefined($expr->class);
1060: }
1061:
1062: return null;
1063: }
1064:
1065: if (!$propertyReflection->isNative()) {
1066: if ($expr instanceof Node\Expr\PropertyFetch) {
1067: return $this->issetCheckUndefined($expr->var);
1068: }
1069:
1070: if ($expr->class instanceof Expr) {
1071: return $this->issetCheckUndefined($expr->class);
1072: }
1073:
1074: return null;
1075: }
1076:
1077: if ($propertyReflection->hasNativeType() && !$propertyReflection->isVirtual()->yes()) {
1078: if (!$this->hasExpressionType($expr)->yes()) {
1079: $nativeReflection = $propertyReflection->getNativeReflection();
1080: if ($nativeReflection === null || !$nativeReflection->isPromoted() || (!$nativeReflection->isReadOnly() && !$nativeReflection->isHooked())) {
1081: if ($expr instanceof Node\Expr\PropertyFetch) {
1082: return $this->issetCheckUndefined($expr->var);
1083: }
1084:
1085: if ($expr->class instanceof Expr) {
1086: return $this->issetCheckUndefined($expr->class);
1087: }
1088:
1089: return null;
1090: }
1091: }
1092: }
1093:
1094: if ($result !== null) {
1095: if ($expr instanceof Node\Expr\PropertyFetch) {
1096: return $this->issetCheck($expr->var, $typeCallback, $result);
1097: }
1098:
1099: if ($expr->class instanceof Expr) {
1100: return $this->issetCheck($expr->class, $typeCallback, $result);
1101: }
1102:
1103: return $result;
1104: }
1105:
1106: $result = $typeCallback($propertyReflection->getWritableType());
1107: if ($result !== null) {
1108: if ($expr instanceof Node\Expr\PropertyFetch) {
1109: return $this->issetCheck($expr->var, $typeCallback, $result);
1110: }
1111:
1112: if ($expr->class instanceof Expr) {
1113: return $this->issetCheck($expr->class, $typeCallback, $result);
1114: }
1115: }
1116:
1117: return $result;
1118: }
1119:
1120: if ($result !== null) {
1121: return $result;
1122: }
1123:
1124: return $typeCallback($this->getType($expr));
1125: }
1126:
1127: private function issetCheckUndefined(Expr $expr): ?bool
1128: {
1129: if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) {
1130: $hasVariable = $this->hasVariableType($expr->name);
1131: if (!$hasVariable->no()) {
1132: return null;
1133: }
1134:
1135: return false;
1136: }
1137:
1138: if ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) {
1139: $type = $this->getType($expr->var);
1140: if (!$type->isOffsetAccessible()->yes()) {
1141: return $this->issetCheckUndefined($expr->var);
1142: }
1143:
1144: $dimType = $this->getType($expr->dim);
1145: $hasOffsetValue = $type->hasOffsetValueType($dimType);
1146:
1147: if (!$hasOffsetValue->no()) {
1148: return $this->issetCheckUndefined($expr->var);
1149: }
1150:
1151: return false;
1152: }
1153:
1154: if ($expr instanceof Expr\PropertyFetch) {
1155: return $this->issetCheckUndefined($expr->var);
1156: }
1157:
1158: if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) {
1159: return $this->issetCheckUndefined($expr->class);
1160: }
1161:
1162: return null;
1163: }
1164:
1165: /** @api */
1166: public function getNativeType(Expr $expr): Type
1167: {
1168: return $this->promoteNativeTypes()->getType($expr);
1169: }
1170:
1171: public function getKeepVoidType(Expr $node): Type
1172: {
1173: if (
1174: !$node instanceof Match_
1175: && (
1176: (
1177: !$node instanceof FuncCall
1178: && !$node instanceof MethodCall
1179: && !$node instanceof Expr\NullsafeMethodCall
1180: && !$node instanceof Expr\StaticCall
1181: ) || $node->isFirstClassCallable()
1182: )
1183: ) {
1184: return $this->getType($node);
1185: }
1186:
1187: $originalType = $this->getType($node);
1188: if (!TypeCombinator::containsNull($originalType)) {
1189: return $originalType;
1190: }
1191:
1192: $clonedNode = clone $node;
1193: $clonedNode->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, true);
1194:
1195: return $this->getType($clonedNode);
1196: }
1197:
1198: public function doNotTreatPhpDocTypesAsCertain(): Scope
1199: {
1200: return $this->promoteNativeTypes();
1201: }
1202:
1203: private function promoteNativeTypes(): self
1204: {
1205: if ($this->nativeTypesPromoted) {
1206: return $this;
1207: }
1208:
1209: if ($this->scopeWithPromotedNativeTypes !== null) {
1210: return $this->scopeWithPromotedNativeTypes;
1211: }
1212:
1213: return $this->scopeWithPromotedNativeTypes = $this->scopeFactory->create(
1214: $this->context,
1215: $this->declareStrictTypes,
1216: $this->function,
1217: $this->namespace,
1218: $this->nativeExpressionTypes,
1219: [],
1220: [],
1221: $this->inClosureBindScopeClasses,
1222: $this->anonymousFunctionReflection,
1223: $this->inFirstLevelStatement,
1224: $this->currentlyAssignedExpressions,
1225: $this->currentlyAllowedUndefinedExpressions,
1226: $this->inFunctionCallsStack,
1227: $this->afterExtractCall,
1228: $this->parentScope,
1229: true,
1230: );
1231: }
1232:
1233: /** @api */
1234: public function resolveName(Name $name): string
1235: {
1236: $originalClass = (string) $name;
1237: if ($this->isInClass()) {
1238: $lowerClass = strtolower($originalClass);
1239: if (in_array($lowerClass, [
1240: 'self',
1241: 'static',
1242: ], true)) {
1243: if ($this->inClosureBindScopeClasses !== [] && $this->inClosureBindScopeClasses !== ['static']) {
1244: return $this->inClosureBindScopeClasses[0];
1245: }
1246: return $this->getClassReflection()->getName();
1247: } elseif ($lowerClass === 'parent') {
1248: $currentClassReflection = $this->getClassReflection();
1249: if ($currentClassReflection->getParentClass() !== null) {
1250: return $currentClassReflection->getParentClass()->getName();
1251: }
1252: }
1253: }
1254:
1255: return $originalClass;
1256: }
1257:
1258: /** @api */
1259: public function resolveTypeByName(Name $name): TypeWithClassName
1260: {
1261: if ($name->toLowerString() === 'static' && $this->isInClass()) {
1262: if ($this->inClosureBindScopeClasses !== [] && $this->inClosureBindScopeClasses !== ['static']) {
1263: if ($this->reflectionProvider->hasClass($this->inClosureBindScopeClasses[0])) {
1264: return new StaticType($this->reflectionProvider->getClass($this->inClosureBindScopeClasses[0]));
1265: }
1266: }
1267:
1268: return new StaticType($this->getClassReflection());
1269: }
1270:
1271: $originalClass = $this->resolveName($name);
1272: if ($this->isInClass()) {
1273: if ($this->inClosureBindScopeClasses === [$originalClass]) {
1274: if ($this->reflectionProvider->hasClass($originalClass)) {
1275: return new ThisType($this->reflectionProvider->getClass($originalClass));
1276: }
1277: return new ObjectType($originalClass);
1278: }
1279:
1280: $thisType = new ThisType($this->getClassReflection());
1281: $ancestor = $thisType->getAncestorWithClassName($originalClass);
1282: if ($ancestor !== null) {
1283: return $ancestor;
1284: }
1285: }
1286:
1287: return new ObjectType($originalClass);
1288: }
1289:
1290: /**
1291: * @api
1292: * @param mixed $value
1293: */
1294: public function getTypeFromValue($value): Type
1295: {
1296: return ConstantTypeHelper::getTypeFromValue($value);
1297: }
1298:
1299: /** @api */
1300: public function hasExpressionType(Expr $node): TrinaryLogic
1301: {
1302: if ($node instanceof Variable && is_string($node->name)) {
1303: return $this->hasVariableType($node->name);
1304: }
1305:
1306: $exprString = $this->getNodeKey($node);
1307: if (!isset($this->expressionTypes[$exprString])) {
1308: return TrinaryLogic::createNo();
1309: }
1310: return $this->expressionTypes[$exprString]->getCertainty();
1311: }
1312:
1313: /**
1314: * @param MethodReflection|FunctionReflection|null $reflection
1315: */
1316: public function pushInFunctionCall($reflection, ?ParameterReflection $parameter, bool $rememberTypes): self
1317: {
1318: $stack = $this->inFunctionCallsStack;
1319: $stack[] = [$reflection, $parameter];
1320:
1321: $functionScope = $this->scopeFactory->create(
1322: $this->context,
1323: $this->isDeclareStrictTypes(),
1324: $this->getFunction(),
1325: $this->getNamespace(),
1326: $this->expressionTypes,
1327: $this->nativeExpressionTypes,
1328: $this->conditionalExpressions,
1329: $this->inClosureBindScopeClasses,
1330: $this->anonymousFunctionReflection,
1331: $this->isInFirstLevelStatement(),
1332: $this->currentlyAssignedExpressions,
1333: $this->currentlyAllowedUndefinedExpressions,
1334: $stack,
1335: $this->afterExtractCall,
1336: $this->parentScope,
1337: $this->nativeTypesPromoted,
1338: );
1339:
1340: if ($rememberTypes) {
1341: $functionScope->resolvedTypes = $this->resolvedTypes;
1342: }
1343:
1344: return $functionScope;
1345: }
1346:
1347: public function popInFunctionCall(): self
1348: {
1349: $stack = $this->inFunctionCallsStack;
1350: array_pop($stack);
1351:
1352: $parentScope = $this->scopeFactory->create(
1353: $this->context,
1354: $this->isDeclareStrictTypes(),
1355: $this->getFunction(),
1356: $this->getNamespace(),
1357: $this->expressionTypes,
1358: $this->nativeExpressionTypes,
1359: $this->conditionalExpressions,
1360: $this->inClosureBindScopeClasses,
1361: $this->anonymousFunctionReflection,
1362: $this->isInFirstLevelStatement(),
1363: $this->currentlyAssignedExpressions,
1364: $this->currentlyAllowedUndefinedExpressions,
1365: $stack,
1366: $this->afterExtractCall,
1367: $this->parentScope,
1368: $this->nativeTypesPromoted,
1369: );
1370:
1371: $parentScope->resolvedTypes = $this->resolvedTypes;
1372:
1373: return $parentScope;
1374: }
1375:
1376: /** @api */
1377: public function isInClassExists(string $className): bool
1378: {
1379: foreach ($this->inFunctionCallsStack as [$inFunctionCall]) {
1380: if (!$inFunctionCall instanceof FunctionReflection) {
1381: continue;
1382: }
1383:
1384: if (in_array($inFunctionCall->getName(), [
1385: 'class_exists',
1386: 'interface_exists',
1387: 'trait_exists',
1388: 'enum_exists',
1389: ], true)) {
1390: return true;
1391: }
1392: }
1393:
1394: // interface_exists() etc. imply class_exists() therefore not listed here
1395: $expr = new FuncCall(new FullyQualified('class_exists'), [
1396: new Arg(new String_(ltrim($className, '\\'))),
1397: ]);
1398:
1399: return $this->getType($expr)->isTrue()->yes();
1400: }
1401:
1402: public function getFunctionCallStack(): array
1403: {
1404: return array_values(array_filter(
1405: array_map(static fn ($values) => $values[0], $this->inFunctionCallsStack),
1406: static fn (FunctionReflection|MethodReflection|null $reflection) => $reflection !== null,
1407: ));
1408: }
1409:
1410: public function getFunctionCallStackWithParameters(): array
1411: {
1412: return array_values(array_filter(
1413: $this->inFunctionCallsStack,
1414: static fn ($item) => $item[0] !== null,
1415: ));
1416: }
1417:
1418: /** @api */
1419: public function isInFunctionExists(string $functionName): bool
1420: {
1421: $expr = new FuncCall(new FullyQualified('function_exists'), [
1422: new Arg(new String_(ltrim($functionName, '\\'))),
1423: ]);
1424:
1425: return $this->getType($expr)->isTrue()->yes();
1426: }
1427:
1428: /** @api */
1429: public function enterClass(ClassReflection $classReflection): self
1430: {
1431: $thisHolder = ExpressionTypeHolder::createYes(new Variable('this'), new ThisType($classReflection));
1432: $constantTypes = $this->getConstantTypes();
1433: $constantTypes['$this'] = $thisHolder;
1434: $nativeConstantTypes = $this->getNativeConstantTypes();
1435: $nativeConstantTypes['$this'] = $thisHolder;
1436:
1437: return $this->scopeFactory->create(
1438: $this->context->enterClass($classReflection),
1439: $this->isDeclareStrictTypes(),
1440: null,
1441: $this->getNamespace(),
1442: $constantTypes,
1443: $nativeConstantTypes,
1444: [],
1445: [],
1446: null,
1447: true,
1448: [],
1449: [],
1450: [],
1451: false,
1452: $classReflection->isAnonymous() ? $this : null,
1453: );
1454: }
1455:
1456: public function enterTrait(ClassReflection $traitReflection): self
1457: {
1458: $namespace = null;
1459: $traitName = $traitReflection->getName();
1460: $traitNameParts = explode('\\', $traitName);
1461: if (count($traitNameParts) > 1) {
1462: $namespace = implode('\\', array_slice($traitNameParts, 0, -1));
1463: }
1464: return $this->scopeFactory->create(
1465: $this->context->enterTrait($traitReflection),
1466: $this->isDeclareStrictTypes(),
1467: $this->getFunction(),
1468: $namespace,
1469: $this->expressionTypes,
1470: $this->nativeExpressionTypes,
1471: [],
1472: $this->inClosureBindScopeClasses,
1473: $this->anonymousFunctionReflection,
1474: );
1475: }
1476:
1477: /**
1478: * @api
1479: * @param Type[] $phpDocParameterTypes
1480: * @param Type[] $parameterOutTypes
1481: * @param array<string, bool> $immediatelyInvokedCallableParameters
1482: * @param array<string, Type> $phpDocClosureThisTypeParameters
1483: */
1484: public function enterClassMethod(
1485: Node\Stmt\ClassMethod $classMethod,
1486: TemplateTypeMap $templateTypeMap,
1487: array $phpDocParameterTypes,
1488: ?Type $phpDocReturnType,
1489: ?Type $throwType,
1490: ?string $deprecatedDescription,
1491: bool $isDeprecated,
1492: bool $isInternal,
1493: bool $isFinal,
1494: ?bool $isPure = null,
1495: bool $acceptsNamedArguments = true,
1496: ?Assertions $asserts = null,
1497: ?Type $selfOutType = null,
1498: ?string $phpDocComment = null,
1499: array $parameterOutTypes = [],
1500: array $immediatelyInvokedCallableParameters = [],
1501: array $phpDocClosureThisTypeParameters = [],
1502: bool $isConstructor = false,
1503: ?ResolvedPhpDocBlock $resolvedPhpDocBlock = null,
1504: ): self
1505: {
1506: if (!$this->isInClass()) {
1507: throw new ShouldNotHappenException();
1508: }
1509:
1510: return $this->enterFunctionLike(
1511: new PhpMethodFromParserNodeReflection(
1512: $this->getClassReflection(),
1513: $classMethod,
1514: null,
1515: $this->getFile(),
1516: $templateTypeMap,
1517: $this->getRealParameterTypes($classMethod),
1518: array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocParameterTypes),
1519: $this->getRealParameterDefaultValues($classMethod),
1520: $this->getParameterAttributes($classMethod),
1521: $this->transformStaticType($this->getFunctionType($classMethod->returnType, false, false)),
1522: $phpDocReturnType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocReturnType)) : null,
1523: $throwType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($throwType)) : null,
1524: $deprecatedDescription,
1525: $isDeprecated,
1526: $isInternal,
1527: $isFinal,
1528: $isPure,
1529: $acceptsNamedArguments,
1530: $asserts ?? Assertions::createEmpty(),
1531: $selfOutType,
1532: $phpDocComment,
1533: $resolvedPhpDocBlock,
1534: array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $parameterOutTypes),
1535: $immediatelyInvokedCallableParameters,
1536: array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocClosureThisTypeParameters),
1537: $isConstructor,
1538: $this->attributeReflectionFactory->fromAttrGroups($classMethod->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $classMethod)),
1539: ),
1540: !$classMethod->isStatic(),
1541: );
1542: }
1543:
1544: /**
1545: * @param Type[] $phpDocParameterTypes
1546: */
1547: public function enterPropertyHook(
1548: Node\PropertyHook $hook,
1549: string $propertyName,
1550: Identifier|Name|ComplexType|null $nativePropertyTypeNode,
1551: ?Type $phpDocPropertyType,
1552: array $phpDocParameterTypes,
1553: ?Type $throwType,
1554: ?string $deprecatedDescription,
1555: bool $isDeprecated,
1556: ?string $phpDocComment,
1557: ?ResolvedPhpDocBlock $resolvedPhpDocBlock = null,
1558: ): self
1559: {
1560: if (!$this->isInClass()) {
1561: throw new ShouldNotHappenException();
1562: }
1563:
1564: $phpDocParameterTypes = array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocParameterTypes);
1565:
1566: $hookName = $hook->name->toLowerString();
1567: if ($hookName === 'set') {
1568: if ($hook->params === []) {
1569: $hook = clone $hook;
1570: $hook->params = [
1571: new Node\Param(new Variable('value'), type: $nativePropertyTypeNode),
1572: ];
1573: }
1574:
1575: $firstParam = $hook->params[0] ?? null;
1576: if (
1577: $firstParam !== null
1578: && $phpDocPropertyType !== null
1579: && $firstParam->var instanceof Variable
1580: && is_string($firstParam->var->name)
1581: ) {
1582: $valueParamPhpDocType = $phpDocParameterTypes[$firstParam->var->name] ?? null;
1583: if ($valueParamPhpDocType === null) {
1584: $phpDocParameterTypes[$firstParam->var->name] = $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocPropertyType));
1585: }
1586: }
1587:
1588: $realReturnType = new VoidType();
1589: $phpDocReturnType = null;
1590: } elseif ($hookName === 'get') {
1591: $realReturnType = $this->getFunctionType($nativePropertyTypeNode, false, false);
1592: $phpDocReturnType = $phpDocPropertyType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocPropertyType)) : null;
1593: } else {
1594: throw new ShouldNotHappenException();
1595: }
1596:
1597: $realParameterTypes = $this->getRealParameterTypes($hook);
1598:
1599: return $this->enterFunctionLike(
1600: new PhpMethodFromParserNodeReflection(
1601: $this->getClassReflection(),
1602: $hook,
1603: $propertyName,
1604: $this->getFile(),
1605: TemplateTypeMap::createEmpty(),
1606: $realParameterTypes,
1607: $phpDocParameterTypes,
1608: [],
1609: $this->getParameterAttributes($hook),
1610: $realReturnType,
1611: $phpDocReturnType,
1612: $throwType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($throwType)) : null,
1613: $deprecatedDescription,
1614: $isDeprecated,
1615: false,
1616: false,
1617: false,
1618: true,
1619: Assertions::createEmpty(),
1620: null,
1621: $phpDocComment,
1622: $resolvedPhpDocBlock,
1623: [],
1624: [],
1625: [],
1626: false,
1627: $this->attributeReflectionFactory->fromAttrGroups($hook->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $hook)),
1628: ),
1629: true,
1630: );
1631: }
1632:
1633: private function transformStaticType(Type $type): Type
1634: {
1635: return TypeTraverser::map($type, new TransformStaticTypeTraverser($this));
1636: }
1637:
1638: /**
1639: * @return Type[]
1640: */
1641: private function getRealParameterTypes(Node\FunctionLike $functionLike): array
1642: {
1643: $realParameterTypes = [];
1644: foreach ($functionLike->getParams() as $parameter) {
1645: if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
1646: throw new ShouldNotHappenException();
1647: }
1648: $realParameterTypes[$parameter->var->name] = $this->getFunctionType(
1649: $parameter->type,
1650: $this->isParameterValueNullable($parameter) && $parameter->flags === 0,
1651: false,
1652: );
1653: }
1654:
1655: return $realParameterTypes;
1656: }
1657:
1658: /**
1659: * @return Type[]
1660: */
1661: private function getRealParameterDefaultValues(Node\FunctionLike $functionLike): array
1662: {
1663: $realParameterDefaultValues = [];
1664: foreach ($functionLike->getParams() as $parameter) {
1665: if ($parameter->default === null) {
1666: continue;
1667: }
1668: if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
1669: throw new ShouldNotHappenException();
1670: }
1671: $realParameterDefaultValues[$parameter->var->name] = $this->getType($parameter->default);
1672: }
1673:
1674: return $realParameterDefaultValues;
1675: }
1676:
1677: /**
1678: * @return array<string, list<AttributeReflection>>
1679: */
1680: private function getParameterAttributes(ClassMethod|Function_|PropertyHook $functionLike): array
1681: {
1682: $parameterAttributes = [];
1683: $className = null;
1684: if ($this->isInClass()) {
1685: $className = $this->getClassReflection()->getName();
1686: }
1687: foreach ($functionLike->getParams() as $parameter) {
1688: if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
1689: throw new ShouldNotHappenException();
1690: }
1691:
1692: $parameterAttributes[$parameter->var->name] = $this->attributeReflectionFactory->fromAttrGroups($parameter->attrGroups, InitializerExprContext::fromStubParameter($className, $this->getFile(), $functionLike));
1693: }
1694:
1695: return $parameterAttributes;
1696: }
1697:
1698: /**
1699: * @api
1700: * @param Type[] $phpDocParameterTypes
1701: * @param Type[] $parameterOutTypes
1702: * @param array<string, bool> $immediatelyInvokedCallableParameters
1703: * @param array<string, Type> $phpDocClosureThisTypeParameters
1704: */
1705: public function enterFunction(
1706: Node\Stmt\Function_ $function,
1707: TemplateTypeMap $templateTypeMap,
1708: array $phpDocParameterTypes,
1709: ?Type $phpDocReturnType,
1710: ?Type $throwType,
1711: ?string $deprecatedDescription,
1712: bool $isDeprecated,
1713: bool $isInternal,
1714: ?bool $isPure = null,
1715: bool $acceptsNamedArguments = true,
1716: ?Assertions $asserts = null,
1717: ?string $phpDocComment = null,
1718: array $parameterOutTypes = [],
1719: array $immediatelyInvokedCallableParameters = [],
1720: array $phpDocClosureThisTypeParameters = [],
1721: ): self
1722: {
1723: return $this->enterFunctionLike(
1724: new PhpFunctionFromParserNodeReflection(
1725: $function,
1726: $this->getFile(),
1727: $templateTypeMap,
1728: $this->getRealParameterTypes($function),
1729: array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $phpDocParameterTypes),
1730: $this->getRealParameterDefaultValues($function),
1731: $this->getParameterAttributes($function),
1732: $this->getFunctionType($function->returnType, $function->returnType === null, false),
1733: $phpDocReturnType !== null ? TemplateTypeHelper::toArgument($phpDocReturnType) : null,
1734: $throwType,
1735: $deprecatedDescription,
1736: $isDeprecated,
1737: $isInternal,
1738: $isPure,
1739: $acceptsNamedArguments,
1740: $asserts ?? Assertions::createEmpty(),
1741: $phpDocComment,
1742: array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $parameterOutTypes),
1743: $immediatelyInvokedCallableParameters,
1744: $phpDocClosureThisTypeParameters,
1745: $this->attributeReflectionFactory->fromAttrGroups($function->attrGroups, InitializerExprContext::fromStubParameter(null, $this->getFile(), $function)),
1746: ),
1747: false,
1748: );
1749: }
1750:
1751: private function enterFunctionLike(
1752: PhpFunctionFromParserNodeReflection $functionReflection,
1753: bool $preserveConstructorScope,
1754: ): self
1755: {
1756: $parametersByName = [];
1757:
1758: $functionParameters = $functionReflection->getParameters();
1759: foreach ($functionParameters as $parameter) {
1760: $parametersByName[$parameter->getName()] = $parameter;
1761: }
1762:
1763: $expressionTypes = [];
1764: $nativeExpressionTypes = [];
1765: $conditionalTypes = [];
1766:
1767: if ($preserveConstructorScope) {
1768: $expressionTypes = $this->expressionTypes;
1769: $nativeExpressionTypes = $this->nativeExpressionTypes;
1770: }
1771:
1772: foreach ($functionParameters as $parameter) {
1773: $parameterType = $parameter->getType();
1774:
1775: if ($parameterType instanceof ConditionalTypeForParameter) {
1776: $targetParameterName = substr($parameterType->getParameterName(), 1);
1777: if (array_key_exists($targetParameterName, $parametersByName)) {
1778: $targetParameter = $parametersByName[$targetParameterName];
1779:
1780: $ifType = $parameterType->isNegated() ? $parameterType->getElse() : $parameterType->getIf();
1781: $elseType = $parameterType->isNegated() ? $parameterType->getIf() : $parameterType->getElse();
1782:
1783: $holder = new ConditionalExpressionHolder([
1784: $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::intersect($targetParameter->getType(), $parameterType->getTarget())),
1785: ], ExpressionTypeHolder::createYes(new Variable($parameter->getName()), $ifType));
1786: $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder;
1787:
1788: $holder = new ConditionalExpressionHolder([
1789: $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::remove($targetParameter->getType(), $parameterType->getTarget())),
1790: ], ExpressionTypeHolder::createYes(new Variable($parameter->getName()), $elseType));
1791: $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder;
1792: }
1793: }
1794:
1795: $paramExprString = '$' . $parameter->getName();
1796: if ($parameter->isVariadic()) {
1797: if (!$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) {
1798: $parameterType = new ArrayType(new UnionType([IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()]), $parameterType);
1799: } else {
1800: $parameterType = new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $parameterType), new AccessoryArrayListType()]);
1801: }
1802: }
1803: $parameterNode = new Variable($parameter->getName());
1804: $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameterNode, $parameterType);
1805:
1806: $parameterOriginalValueExpr = new ParameterVariableOriginalValueExpr($parameter->getName());
1807: $parameterOriginalValueExprString = $this->getNodeKey($parameterOriginalValueExpr);
1808: $expressionTypes[$parameterOriginalValueExprString] = ExpressionTypeHolder::createYes($parameterOriginalValueExpr, $parameterType);
1809:
1810: $nativeParameterType = $parameter->getNativeType();
1811: if ($parameter->isVariadic()) {
1812: if (!$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) {
1813: $nativeParameterType = new ArrayType(new UnionType([IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()]), $nativeParameterType);
1814: } else {
1815: $nativeParameterType = new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $nativeParameterType), new AccessoryArrayListType()]);
1816: }
1817: }
1818: $nativeExpressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameterNode, $nativeParameterType);
1819: $nativeExpressionTypes[$parameterOriginalValueExprString] = ExpressionTypeHolder::createYes($parameterOriginalValueExpr, $nativeParameterType);
1820: }
1821:
1822: return $this->scopeFactory->create(
1823: $this->context,
1824: $this->isDeclareStrictTypes(),
1825: $functionReflection,
1826: $this->getNamespace(),
1827: array_merge($this->getConstantTypes(), $expressionTypes),
1828: array_merge($this->getNativeConstantTypes(), $nativeExpressionTypes),
1829: $conditionalTypes,
1830: );
1831: }
1832:
1833: /** @api */
1834: public function enterNamespace(string $namespaceName): self
1835: {
1836: return $this->scopeFactory->create(
1837: $this->context->beginFile(),
1838: $this->isDeclareStrictTypes(),
1839: null,
1840: $namespaceName,
1841: );
1842: }
1843:
1844: /**
1845: * @param list<non-empty-string> $scopeClasses
1846: */
1847: public function enterClosureBind(?Type $thisType, ?Type $nativeThisType, array $scopeClasses): self
1848: {
1849: $expressionTypes = $this->expressionTypes;
1850: if ($thisType !== null) {
1851: $expressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $thisType);
1852: } else {
1853: unset($expressionTypes['$this']);
1854: }
1855:
1856: $nativeExpressionTypes = $this->nativeExpressionTypes;
1857: if ($nativeThisType !== null) {
1858: $nativeExpressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $nativeThisType);
1859: } else {
1860: unset($nativeExpressionTypes['$this']);
1861: }
1862:
1863: if ($scopeClasses === ['static'] && $this->isInClass()) {
1864: $scopeClasses = [$this->getClassReflection()->getName()];
1865: }
1866:
1867: return $this->scopeFactory->create(
1868: $this->context,
1869: $this->isDeclareStrictTypes(),
1870: $this->getFunction(),
1871: $this->getNamespace(),
1872: $expressionTypes,
1873: $nativeExpressionTypes,
1874: $this->conditionalExpressions,
1875: $scopeClasses,
1876: $this->anonymousFunctionReflection,
1877: );
1878: }
1879:
1880: public function restoreOriginalScopeAfterClosureBind(self $originalScope): self
1881: {
1882: $expressionTypes = $this->expressionTypes;
1883: if (isset($originalScope->expressionTypes['$this'])) {
1884: $expressionTypes['$this'] = $originalScope->expressionTypes['$this'];
1885: } else {
1886: unset($expressionTypes['$this']);
1887: }
1888:
1889: $nativeExpressionTypes = $this->nativeExpressionTypes;
1890: if (isset($originalScope->nativeExpressionTypes['$this'])) {
1891: $nativeExpressionTypes['$this'] = $originalScope->nativeExpressionTypes['$this'];
1892: } else {
1893: unset($nativeExpressionTypes['$this']);
1894: }
1895:
1896: return $this->scopeFactory->create(
1897: $this->context,
1898: $this->isDeclareStrictTypes(),
1899: $this->getFunction(),
1900: $this->getNamespace(),
1901: $expressionTypes,
1902: $nativeExpressionTypes,
1903: $this->conditionalExpressions,
1904: $originalScope->inClosureBindScopeClasses,
1905: $this->anonymousFunctionReflection,
1906: );
1907: }
1908:
1909: public function restoreThis(self $restoreThisScope): self
1910: {
1911: $expressionTypes = $this->expressionTypes;
1912: $nativeExpressionTypes = $this->nativeExpressionTypes;
1913:
1914: if ($restoreThisScope->isInClass()) {
1915: foreach ($restoreThisScope->expressionTypes as $exprString => $expressionTypeHolder) {
1916: if (!str_starts_with($exprString, '$this')) {
1917: continue;
1918: }
1919:
1920: $expressionTypes[$exprString] = $expressionTypeHolder;
1921: }
1922:
1923: foreach ($restoreThisScope->nativeExpressionTypes as $exprString => $expressionTypeHolder) {
1924: if (!str_starts_with($exprString, '$this')) {
1925: continue;
1926: }
1927:
1928: $nativeExpressionTypes[$exprString] = $expressionTypeHolder;
1929: }
1930: } else {
1931: unset($expressionTypes['$this']);
1932: unset($nativeExpressionTypes['$this']);
1933: }
1934:
1935: return $this->scopeFactory->create(
1936: $this->context,
1937: $this->isDeclareStrictTypes(),
1938: $this->getFunction(),
1939: $this->getNamespace(),
1940: $expressionTypes,
1941: $nativeExpressionTypes,
1942: $this->conditionalExpressions,
1943: $this->inClosureBindScopeClasses,
1944: $this->anonymousFunctionReflection,
1945: $this->inFirstLevelStatement,
1946: [],
1947: [],
1948: $this->inFunctionCallsStack,
1949: $this->afterExtractCall,
1950: $this->parentScope,
1951: $this->nativeTypesPromoted,
1952: );
1953: }
1954:
1955: public function enterClosureCall(Type $thisType, Type $nativeThisType): self
1956: {
1957: $expressionTypes = $this->expressionTypes;
1958: $expressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $thisType);
1959:
1960: $nativeExpressionTypes = $this->nativeExpressionTypes;
1961: $nativeExpressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $nativeThisType);
1962:
1963: return $this->scopeFactory->create(
1964: $this->context,
1965: $this->isDeclareStrictTypes(),
1966: $this->getFunction(),
1967: $this->getNamespace(),
1968: $expressionTypes,
1969: $nativeExpressionTypes,
1970: $this->conditionalExpressions,
1971: $thisType->getObjectClassNames(),
1972: $this->anonymousFunctionReflection,
1973: );
1974: }
1975:
1976: /** @api */
1977: public function isInClosureBind(): bool
1978: {
1979: return $this->inClosureBindScopeClasses !== [];
1980: }
1981:
1982: /**
1983: * @api
1984: * @param ParameterReflection[]|null $callableParameters
1985: * @param ParameterReflection[]|null $nativeCallableParameters
1986: */
1987: public function enterAnonymousFunction(
1988: Expr\Closure $closure,
1989: ?array $callableParameters,
1990: ?array $nativeCallableParameters = null,
1991: ): self
1992: {
1993: $anonymousFunctionReflection = $this->resolveType('__phpstanClosure', $closure);
1994: if (!$anonymousFunctionReflection instanceof ClosureType) {
1995: throw new ShouldNotHappenException();
1996: }
1997:
1998: $scope = $this->enterAnonymousFunctionWithoutReflection($closure, $callableParameters, $nativeCallableParameters);
1999:
2000: return $this->scopeFactory->create(
2001: $scope->context,
2002: $scope->isDeclareStrictTypes(),
2003: $scope->getFunction(),
2004: $scope->getNamespace(),
2005: $scope->expressionTypes,
2006: $scope->nativeExpressionTypes,
2007: $scope->conditionalExpressions,
2008: $scope->inClosureBindScopeClasses,
2009: $anonymousFunctionReflection,
2010: true,
2011: [],
2012: [],
2013: $this->inFunctionCallsStack,
2014: false,
2015: $this,
2016: $this->nativeTypesPromoted,
2017: );
2018: }
2019:
2020: /**
2021: * @param ParameterReflection[]|null $callableParameters
2022: * @param ParameterReflection[]|null $nativeCallableParameters
2023: */
2024: public function enterAnonymousFunctionWithoutReflection(
2025: Expr\Closure $closure,
2026: ?array $callableParameters,
2027: ?array $nativeCallableParameters,
2028: ): self
2029: {
2030: $expressionTypes = [];
2031: $nativeTypes = [];
2032: foreach ($closure->params as $i => $parameter) {
2033: if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
2034: throw new ShouldNotHappenException();
2035: }
2036: $paramExprString = sprintf('$%s', $parameter->var->name);
2037: $isNullable = $this->isParameterValueNullable($parameter);
2038: $nativeParameterType = $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
2039: if ($callableParameters !== null) {
2040: $parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($parameter, $callableParameters, $i));
2041: }
2042: if ($nativeCallableParameters !== null) {
2043: $nativeParameterType = self::intersectButNotNever($nativeParameterType, $this->getCallableParameterType($parameter, $nativeCallableParameters, $i));
2044: }
2045: $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameter->var, $parameterType);
2046: $nativeTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameter->var, $nativeParameterType);
2047: }
2048:
2049: $nonRefVariableNames = [];
2050: $useVariableNames = [];
2051: foreach ($closure->uses as $use) {
2052: if (!is_string($use->var->name)) {
2053: throw new ShouldNotHappenException();
2054: }
2055: $variableName = $use->var->name;
2056: $paramExprString = '$' . $use->var->name;
2057: $useVariableNames[$paramExprString] = true;
2058: if ($use->byRef) {
2059: $holder = ExpressionTypeHolder::createYes($use->var, new MixedType());
2060: $expressionTypes[$paramExprString] = $holder;
2061: $nativeTypes[$paramExprString] = $holder;
2062: continue;
2063: }
2064: $nonRefVariableNames[$variableName] = true;
2065: if ($this->hasVariableType($variableName)->no()) {
2066: $variableType = new ErrorType();
2067: $variableNativeType = new ErrorType();
2068: } else {
2069: $variableType = $this->getVariableType($variableName);
2070: $variableNativeType = $this->getNativeType($use->var);
2071: }
2072: $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($use->var, $variableType);
2073: $nativeTypes[$paramExprString] = ExpressionTypeHolder::createYes($use->var, $variableNativeType);
2074: }
2075:
2076: $nonStaticExpressions = $this->invalidateStaticExpressions($this->expressionTypes);
2077: foreach ($nonStaticExpressions as $exprString => $typeHolder) {
2078: $expr = $typeHolder->getExpr();
2079:
2080: if ($expr instanceof Variable) {
2081: continue;
2082: }
2083:
2084: $variables = (new NodeFinder())->findInstanceOf([$expr], Variable::class);
2085: if ($variables === [] && !$this->expressionTypeIsUnchangeable($typeHolder)) {
2086: continue;
2087: }
2088:
2089: foreach ($variables as $variable) {
2090: if (!is_string($variable->name)) {
2091: continue 2;
2092: }
2093: if (!array_key_exists($variable->name, $nonRefVariableNames)) {
2094: continue 2;
2095: }
2096: }
2097:
2098: $expressionTypes[$exprString] = $typeHolder;
2099: }
2100:
2101: if ($this->hasVariableType('this')->yes() && !$closure->static) {
2102: $node = new Variable('this');
2103: $expressionTypes['$this'] = ExpressionTypeHolder::createYes($node, $this->getType($node));
2104: $nativeTypes['$this'] = ExpressionTypeHolder::createYes($node, $this->getNativeType($node));
2105:
2106: if ($this->phpVersion->supportsReadOnlyProperties()) {
2107: foreach ($nonStaticExpressions as $exprString => $typeHolder) {
2108: $expr = $typeHolder->getExpr();
2109:
2110: if (!$expr instanceof PropertyFetch) {
2111: continue;
2112: }
2113:
2114: if (!$this->isReadonlyPropertyFetch($expr, true)) {
2115: continue;
2116: }
2117:
2118: $expressionTypes[$exprString] = $typeHolder;
2119: }
2120: }
2121: }
2122:
2123: $filteredConditionalExpressions = [];
2124: foreach ($this->conditionalExpressions as $conditionalExprString => $holders) {
2125: if (!array_key_exists($conditionalExprString, $useVariableNames)) {
2126: continue;
2127: }
2128: $filteredHolders = [];
2129: foreach ($holders as $holder) {
2130: foreach (array_keys($holder->getConditionExpressionTypeHolders()) as $holderExprString) {
2131: if (!array_key_exists($holderExprString, $useVariableNames)) {
2132: continue 2;
2133: }
2134: }
2135: $filteredHolders[] = $holder;
2136: }
2137: if ($filteredHolders === []) {
2138: continue;
2139: }
2140:
2141: $filteredConditionalExpressions[$conditionalExprString] = $filteredHolders;
2142: }
2143:
2144: return $this->scopeFactory->create(
2145: $this->context,
2146: $this->isDeclareStrictTypes(),
2147: $this->getFunction(),
2148: $this->getNamespace(),
2149: array_merge($this->getConstantTypes(), $expressionTypes),
2150: array_merge($this->getNativeConstantTypes(), $nativeTypes),
2151: $filteredConditionalExpressions,
2152: $this->inClosureBindScopeClasses,
2153: new ClosureType(),
2154: true,
2155: [],
2156: [],
2157: [],
2158: false,
2159: $this,
2160: $this->nativeTypesPromoted,
2161: );
2162: }
2163:
2164: private function expressionTypeIsUnchangeable(ExpressionTypeHolder $typeHolder): bool
2165: {
2166: $expr = $typeHolder->getExpr();
2167: $type = $typeHolder->getType();
2168:
2169: return $expr instanceof FuncCall
2170: && !$expr->isFirstClassCallable()
2171: && $expr->name instanceof FullyQualified
2172: && in_array(
2173: $expr->name->toLowerString(),
2174: [
2175: 'class_exists',
2176: 'interface_exists',
2177: 'trait_exists',
2178: 'enum_exists',
2179: 'function_exists',
2180: ],
2181: true,
2182: )
2183: && isset($expr->getArgs()[0])
2184: && count($this->getType($expr->getArgs()[0]->value)->getConstantStrings()) === 1
2185: && $type->isTrue()->yes();
2186: }
2187:
2188: /**
2189: * @param array<string, ExpressionTypeHolder> $expressionTypes
2190: * @return array<string, ExpressionTypeHolder>
2191: */
2192: private function invalidateStaticExpressions(array $expressionTypes): array
2193: {
2194: $filteredExpressionTypes = [];
2195: $nodeFinder = new NodeFinder();
2196: foreach ($expressionTypes as $exprString => $expressionType) {
2197: $staticExpression = $nodeFinder->findFirst(
2198: [$expressionType->getExpr()],
2199: static fn ($node) => $node instanceof Expr\StaticCall || $node instanceof Expr\StaticPropertyFetch,
2200: );
2201: if ($staticExpression !== null) {
2202: continue;
2203: }
2204: $filteredExpressionTypes[$exprString] = $expressionType;
2205: }
2206: return $filteredExpressionTypes;
2207: }
2208:
2209: /**
2210: * @api
2211: * @param ParameterReflection[]|null $callableParameters
2212: * @param ParameterReflection[]|null $nativeCallableParameters
2213: */
2214: public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $callableParameters, ?array $nativeCallableParameters = null): self
2215: {
2216: $anonymousFunctionReflection = $this->resolveType('__phpStanArrowFn', $arrowFunction);
2217: if (!$anonymousFunctionReflection instanceof ClosureType) {
2218: throw new ShouldNotHappenException();
2219: }
2220:
2221: $scope = $this->enterArrowFunctionWithoutReflection($arrowFunction, $callableParameters, $nativeCallableParameters);
2222:
2223: return $this->scopeFactory->create(
2224: $scope->context,
2225: $scope->isDeclareStrictTypes(),
2226: $scope->getFunction(),
2227: $scope->getNamespace(),
2228: $scope->expressionTypes,
2229: $scope->nativeExpressionTypes,
2230: $scope->conditionalExpressions,
2231: $scope->inClosureBindScopeClasses,
2232: $anonymousFunctionReflection,
2233: true,
2234: [],
2235: [],
2236: $this->inFunctionCallsStack,
2237: $scope->afterExtractCall,
2238: $scope->parentScope,
2239: $this->nativeTypesPromoted,
2240: );
2241: }
2242:
2243: /**
2244: * @param ParameterReflection[]|null $callableParameters
2245: * @param ParameterReflection[]|null $nativeCallableParameters
2246: */
2247: public function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFunction, ?array $callableParameters, ?array $nativeCallableParameters): self
2248: {
2249: $arrowFunctionScope = $this;
2250: foreach ($arrowFunction->params as $i => $parameter) {
2251: $isNullable = $this->isParameterValueNullable($parameter);
2252: $nativeParameterType = $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
2253: if ($callableParameters !== null) {
2254: $parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($parameter, $callableParameters, $i));
2255: }
2256: if ($nativeCallableParameters !== null) {
2257: $nativeParameterType = self::intersectButNotNever($nativeParameterType, $this->getCallableParameterType($parameter, $nativeCallableParameters, $i));
2258: }
2259:
2260: if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
2261: throw new ShouldNotHappenException();
2262: }
2263: $arrowFunctionScope = $arrowFunctionScope->assignVariable($parameter->var->name, $parameterType, $nativeParameterType, TrinaryLogic::createYes());
2264: }
2265:
2266: if ($arrowFunction->static) {
2267: $arrowFunctionScope = $arrowFunctionScope->invalidateExpression(new Variable('this'));
2268: }
2269:
2270: return $this->scopeFactory->create(
2271: $arrowFunctionScope->context,
2272: $this->isDeclareStrictTypes(),
2273: $arrowFunctionScope->getFunction(),
2274: $arrowFunctionScope->getNamespace(),
2275: $this->invalidateStaticExpressions($arrowFunctionScope->expressionTypes),
2276: $arrowFunctionScope->nativeExpressionTypes,
2277: $arrowFunctionScope->conditionalExpressions,
2278: $arrowFunctionScope->inClosureBindScopeClasses,
2279: new ClosureType(),
2280: true,
2281: [],
2282: [],
2283: [],
2284: $arrowFunctionScope->afterExtractCall,
2285: $arrowFunctionScope->parentScope,
2286: $this->nativeTypesPromoted,
2287: );
2288: }
2289:
2290: public function isParameterValueNullable(Node\Param $parameter): bool
2291: {
2292: if ($parameter->default instanceof ConstFetch) {
2293: return strtolower((string) $parameter->default->name) === 'null';
2294: }
2295:
2296: return false;
2297: }
2298:
2299: /**
2300: * @api
2301: * @param Node\Name|Node\Identifier|Node\ComplexType|null $type
2302: */
2303: public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type
2304: {
2305: if ($isVariadic) {
2306: if (!$this->getPhpVersion()->supportsNamedArguments()->no()) {
2307: return new ArrayType(new UnionType([IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()]), $this->getFunctionType(
2308: $type,
2309: $isNullable,
2310: false,
2311: ));
2312: }
2313:
2314: return new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $this->getFunctionType(
2315: $type,
2316: $isNullable,
2317: false,
2318: )), new AccessoryArrayListType()]);
2319: }
2320: return $this->initializerExprTypeResolver->getFunctionType($type, $isNullable, false, InitializerExprContext::fromScope($this));
2321: }
2322:
2323: /**
2324: * @param ParameterReflection[] $callableParameters
2325: */
2326: private function getCallableParameterType(Node\Param $parameter, array $callableParameters, int $index): Type
2327: {
2328: if ($parameter->variadic) {
2329: return $this->buildVariadicArrayTypeFromCallableParameters($callableParameters, $index);
2330: }
2331:
2332: if (isset($callableParameters[$index])) {
2333: return $callableParameters[$index]->getType();
2334: }
2335:
2336: if (count($callableParameters) === 0) {
2337: return new MixedType();
2338: }
2339:
2340: $lastParameter = array_last($callableParameters);
2341: if ($lastParameter->isVariadic()) {
2342: return $lastParameter->getType();
2343: }
2344:
2345: return new MixedType();
2346: }
2347:
2348: /**
2349: * @param array<ParameterReflection> $callableParameters
2350: */
2351: private function buildVariadicArrayTypeFromCallableParameters(array $callableParameters, int $startIndex): Type
2352: {
2353: $elementTypes = [];
2354: $callableParametersCount = count($callableParameters);
2355: for ($j = $startIndex; $j < $callableParametersCount; $j++) {
2356: $elementTypes[] = $callableParameters[$j]->getType();
2357: if ($callableParameters[$j]->isVariadic()) {
2358: break;
2359: }
2360: }
2361:
2362: if ($elementTypes === [] && $callableParametersCount > 0) {
2363: $lastParameter = array_last($callableParameters);
2364: if ($lastParameter->isVariadic()) {
2365: $elementTypes[] = $lastParameter->getType();
2366: }
2367: }
2368:
2369: if ($elementTypes === []) {
2370: return new MixedType();
2371: }
2372:
2373: $elementType = TypeCombinator::union(...$elementTypes);
2374:
2375: if (!$this->getPhpVersion()->supportsNamedArguments()->no()) {
2376: return new ArrayType(new UnionType([IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()]), $elementType);
2377: }
2378:
2379: return new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $elementType), new AccessoryArrayListType()]);
2380: }
2381:
2382: public static function intersectButNotNever(Type $nativeType, Type $inferredType): Type
2383: {
2384: if ($nativeType->isSuperTypeOf($inferredType)->no()) {
2385: return $nativeType;
2386: }
2387:
2388: $result = TypeCombinator::intersect($nativeType, $inferredType);
2389: if (TypeCombinator::containsNull($nativeType)) {
2390: return TypeCombinator::addNull($result);
2391: }
2392:
2393: return $result;
2394: }
2395:
2396: public function enterMatch(Expr\Match_ $expr, Type $condType, Type $condNativeType): self
2397: {
2398: if ($expr->cond instanceof Variable) {
2399: return $this;
2400: }
2401: if ($expr->cond instanceof AlwaysRememberedExpr) {
2402: $cond = $expr->cond->expr;
2403: } else {
2404: $cond = $expr->cond;
2405: }
2406: if ($cond instanceof Scalar) {
2407: return $this;
2408: }
2409:
2410: $type = $condType;
2411: $nativeType = $condNativeType;
2412: $condExpr = new AlwaysRememberedExpr($cond, $type, $nativeType);
2413: $expr->cond = $condExpr;
2414:
2415: return $this->assignExpression($condExpr, $type, $nativeType);
2416: }
2417:
2418: public function enterForeach(self $originalScope, Expr $iteratee, string $valueName, ?string $keyName, bool $valueByRef): self
2419: {
2420: $iterateeType = $originalScope->getType($iteratee);
2421: $nativeIterateeType = $originalScope->getNativeType($iteratee);
2422: $valueType = $originalScope->getIterableValueType($iterateeType);
2423: $nativeValueType = $originalScope->getIterableValueType($nativeIterateeType);
2424: $scope = $this->assignVariable(
2425: $valueName,
2426: $valueType,
2427: $nativeValueType,
2428: TrinaryLogic::createYes(),
2429: );
2430: // Track the original foreach value so narrowings applied to the value
2431: // variable (e.g. is_string($type)) can later be projected back onto the
2432: // corresponding array dim fetch without being confused by a reassignment
2433: // ($type = 'foo' invalidates this expression, same as OriginalForeachKeyExpr).
2434: $scope = $scope->assignExpression(new OriginalForeachValueExpr($valueName), $valueType, $nativeValueType);
2435: if ($valueByRef && $iterateeType->isArray()->yes() && $iterateeType->isConstantArray()->no()) {
2436: $scope = $scope->assignExpression(
2437: new IntertwinedVariableByReferenceWithExpr($valueName, $iteratee, new SetExistingOffsetValueTypeExpr(
2438: $iteratee,
2439: new GetIterableKeyTypeExpr($iteratee),
2440: new Variable($valueName),
2441: )),
2442: $valueType,
2443: $nativeValueType,
2444: );
2445: }
2446: if ($keyName !== null) {
2447: $scope = $scope->enterForeachKey($originalScope, $iteratee, $keyName);
2448:
2449: if ($valueByRef && $iterateeType->isArray()->yes() && $iterateeType->isConstantArray()->no()) {
2450: $scope = $scope->assignExpression(
2451: new IntertwinedVariableByReferenceWithExpr($valueName, new Expr\ArrayDimFetch($iteratee, new Variable($keyName)), new Variable($valueName)),
2452: $valueType,
2453: $nativeValueType,
2454: );
2455: }
2456: }
2457:
2458: return $scope;
2459: }
2460:
2461: public function enterForeachKey(self $originalScope, Expr $iteratee, string $keyName): self
2462: {
2463: $iterateeType = $originalScope->getType($iteratee);
2464: $nativeIterateeType = $originalScope->getNativeType($iteratee);
2465:
2466: $keyType = $originalScope->getIterableKeyType($iterateeType);
2467: $nativeKeyType = $originalScope->getIterableKeyType($nativeIterateeType);
2468:
2469: $scope = $this->assignVariable(
2470: $keyName,
2471: $keyType,
2472: $nativeKeyType,
2473: TrinaryLogic::createYes(),
2474: );
2475:
2476: $originalForeachKeyExpr = new OriginalForeachKeyExpr($keyName);
2477: $scope = $scope->assignExpression($originalForeachKeyExpr, $keyType, $nativeKeyType);
2478: if ($iterateeType->isArray()->yes()) {
2479: $scope = $scope->assignExpression(
2480: new Expr\ArrayDimFetch($iteratee, new Variable($keyName)),
2481: $originalScope->getIterableValueType($iterateeType),
2482: $originalScope->getIterableValueType($nativeIterateeType),
2483: );
2484: }
2485:
2486: return $scope;
2487: }
2488:
2489: public function enterCatchType(Type $catchType, ?string $variableName): self
2490: {
2491: if ($variableName === null) {
2492: return $this;
2493: }
2494:
2495: return $this->assignVariable(
2496: $variableName,
2497: TypeCombinator::intersect($catchType, new ObjectType(Throwable::class)),
2498: TypeCombinator::intersect($catchType, new ObjectType(Throwable::class)),
2499: TrinaryLogic::createYes(),
2500: );
2501: }
2502:
2503: public function enterExpressionAssign(Expr $expr): self
2504: {
2505: $exprString = $this->getNodeKey($expr);
2506: $currentlyAssignedExpressions = $this->currentlyAssignedExpressions;
2507: $currentlyAssignedExpressions[$exprString] = true;
2508:
2509: $scope = $this->scopeFactory->create(
2510: $this->context,
2511: $this->isDeclareStrictTypes(),
2512: $this->getFunction(),
2513: $this->getNamespace(),
2514: $this->expressionTypes,
2515: $this->nativeExpressionTypes,
2516: $this->conditionalExpressions,
2517: $this->inClosureBindScopeClasses,
2518: $this->anonymousFunctionReflection,
2519: $this->isInFirstLevelStatement(),
2520: $currentlyAssignedExpressions,
2521: $this->currentlyAllowedUndefinedExpressions,
2522: [],
2523: $this->afterExtractCall,
2524: $this->parentScope,
2525: $this->nativeTypesPromoted,
2526: );
2527: $scope->resolvedTypes = $this->resolvedTypes;
2528: $scope->truthyScopes = $this->truthyScopes;
2529: $scope->falseyScopes = $this->falseyScopes;
2530:
2531: return $scope;
2532: }
2533:
2534: public function exitExpressionAssign(Expr $expr): self
2535: {
2536: $exprString = $this->getNodeKey($expr);
2537: $currentlyAssignedExpressions = $this->currentlyAssignedExpressions;
2538: unset($currentlyAssignedExpressions[$exprString]);
2539:
2540: $scope = $this->scopeFactory->create(
2541: $this->context,
2542: $this->isDeclareStrictTypes(),
2543: $this->getFunction(),
2544: $this->getNamespace(),
2545: $this->expressionTypes,
2546: $this->nativeExpressionTypes,
2547: $this->conditionalExpressions,
2548: $this->inClosureBindScopeClasses,
2549: $this->anonymousFunctionReflection,
2550: $this->isInFirstLevelStatement(),
2551: $currentlyAssignedExpressions,
2552: $this->currentlyAllowedUndefinedExpressions,
2553: [],
2554: $this->afterExtractCall,
2555: $this->parentScope,
2556: $this->nativeTypesPromoted,
2557: );
2558: $scope->resolvedTypes = $this->resolvedTypes;
2559: $scope->truthyScopes = $this->truthyScopes;
2560: $scope->falseyScopes = $this->falseyScopes;
2561:
2562: return $scope;
2563: }
2564:
2565: /** @api */
2566: public function isInExpressionAssign(Expr $expr): bool
2567: {
2568: if (count($this->currentlyAssignedExpressions) === 0) {
2569: return false;
2570: }
2571:
2572: $exprString = $this->getNodeKey($expr);
2573: return array_key_exists($exprString, $this->currentlyAssignedExpressions);
2574: }
2575:
2576: public function setAllowedUndefinedExpression(Expr $expr): self
2577: {
2578: if ($expr instanceof Expr\StaticPropertyFetch) {
2579: return $this;
2580: }
2581:
2582: $exprString = $this->getNodeKey($expr);
2583: $currentlyAllowedUndefinedExpressions = $this->currentlyAllowedUndefinedExpressions;
2584: $currentlyAllowedUndefinedExpressions[$exprString] = true;
2585:
2586: $scope = $this->scopeFactory->create(
2587: $this->context,
2588: $this->isDeclareStrictTypes(),
2589: $this->getFunction(),
2590: $this->getNamespace(),
2591: $this->expressionTypes,
2592: $this->nativeExpressionTypes,
2593: $this->conditionalExpressions,
2594: $this->inClosureBindScopeClasses,
2595: $this->anonymousFunctionReflection,
2596: $this->isInFirstLevelStatement(),
2597: $this->currentlyAssignedExpressions,
2598: $currentlyAllowedUndefinedExpressions,
2599: [],
2600: $this->afterExtractCall,
2601: $this->parentScope,
2602: $this->nativeTypesPromoted,
2603: );
2604: $scope->resolvedTypes = $this->resolvedTypes;
2605: $scope->truthyScopes = $this->truthyScopes;
2606: $scope->falseyScopes = $this->falseyScopes;
2607:
2608: return $scope;
2609: }
2610:
2611: public function unsetAllowedUndefinedExpression(Expr $expr): self
2612: {
2613: $exprString = $this->getNodeKey($expr);
2614: $currentlyAllowedUndefinedExpressions = $this->currentlyAllowedUndefinedExpressions;
2615: unset($currentlyAllowedUndefinedExpressions[$exprString]);
2616:
2617: $scope = $this->scopeFactory->create(
2618: $this->context,
2619: $this->isDeclareStrictTypes(),
2620: $this->getFunction(),
2621: $this->getNamespace(),
2622: $this->expressionTypes,
2623: $this->nativeExpressionTypes,
2624: $this->conditionalExpressions,
2625: $this->inClosureBindScopeClasses,
2626: $this->anonymousFunctionReflection,
2627: $this->isInFirstLevelStatement(),
2628: $this->currentlyAssignedExpressions,
2629: $currentlyAllowedUndefinedExpressions,
2630: [],
2631: $this->afterExtractCall,
2632: $this->parentScope,
2633: $this->nativeTypesPromoted,
2634: );
2635: $scope->resolvedTypes = $this->resolvedTypes;
2636: $scope->truthyScopes = $this->truthyScopes;
2637: $scope->falseyScopes = $this->falseyScopes;
2638:
2639: return $scope;
2640: }
2641:
2642: /** @api */
2643: public function isUndefinedExpressionAllowed(Expr $expr): bool
2644: {
2645: if (count($this->currentlyAllowedUndefinedExpressions) === 0) {
2646: return false;
2647: }
2648: $exprString = $this->getNodeKey($expr);
2649: return array_key_exists($exprString, $this->currentlyAllowedUndefinedExpressions);
2650: }
2651:
2652: /**
2653: * @param list<string> $intertwinedPropagatedFrom
2654: */
2655: public function assignVariable(string $variableName, Type $type, Type $nativeType, TrinaryLogic $certainty, array $intertwinedPropagatedFrom = []): self
2656: {
2657: $node = new Variable($variableName);
2658: $scope = $this->assignExpression($node, $type, $nativeType);
2659: if ($certainty->no()) {
2660: throw new ShouldNotHappenException();
2661: } elseif (!$certainty->yes()) {
2662: $exprString = '$' . $variableName;
2663: $scope->expressionTypes[$exprString] = new ExpressionTypeHolder($node, $type, $certainty);
2664: $scope->nativeExpressionTypes[$exprString] = new ExpressionTypeHolder($node, $nativeType, $certainty);
2665: }
2666:
2667: foreach ($scope->expressionTypes as $exprString => $expressionType) {
2668: if (!$expressionType->getExpr() instanceof IntertwinedVariableByReferenceWithExpr) {
2669: continue;
2670: }
2671: if (!$expressionType->getCertainty()->yes()) {
2672: continue;
2673: }
2674: if ($expressionType->getExpr()->getVariableName() !== $variableName) {
2675: continue;
2676: }
2677:
2678: $assignedExpr = $expressionType->getExpr()->getAssignedExpr();
2679: if (
2680: $assignedExpr instanceof Expr\ArrayDimFetch
2681: && !$this->isDimFetchPathReachable($scope, $assignedExpr)
2682: ) {
2683: unset($scope->expressionTypes[$exprString]);
2684: unset($scope->nativeExpressionTypes[$exprString]);
2685: continue;
2686: }
2687:
2688: // When the byref's dim is non-constant AND not enumerable as a
2689: // finite set of scalars (e.g. general `int` or `mixed`), the just-
2690: // performed write to $array might or might not have hit the byref's
2691: // slot. Union the new $array[dim] read with the byref's previous
2692: // type and the pre-write $array[dim] so values that could still be
2693: // at the slot (unmodified or shadowed by an explicit-key overwrite)
2694: // survive. For finitely-enumerable dims (e.g. `bool`, `int<0, 5>`)
2695: // the array literal builder enumerates all possibilities, so the
2696: // new $array[dim] read already covers every reachable slot.
2697: $unionWithOld = false;
2698: if ($assignedExpr instanceof Expr\ArrayDimFetch && $assignedExpr->dim !== null) {
2699: $dimType = $scope->getType($assignedExpr->dim);
2700: if (count($dimType->getConstantScalarValues()) !== 1 && count($dimType->getFiniteTypes()) === 0) {
2701: $unionWithOld = true;
2702: }
2703: }
2704:
2705: $assignedType = $scope->getType($assignedExpr);
2706: $assignedNativeType = $scope->getNativeType($assignedExpr);
2707:
2708: $has = $scope->hasExpressionType($expressionType->getExpr()->getExpr());
2709: if (
2710: $expressionType->getExpr()->getExpr() instanceof Variable
2711: && is_string($expressionType->getExpr()->getExpr()->name)
2712: && !$has->no()
2713: ) {
2714: $targetVarName = $expressionType->getExpr()->getExpr()->name;
2715: if (in_array($targetVarName, $intertwinedPropagatedFrom, true)) {
2716: continue;
2717: }
2718: if ($unionWithOld) {
2719: $targetVarNode = new Variable($targetVarName);
2720: $assignedType = TypeCombinator::union(
2721: $assignedType,
2722: $this->getType($assignedExpr),
2723: $scope->getType($targetVarNode),
2724: );
2725: $assignedNativeType = TypeCombinator::union(
2726: $assignedNativeType,
2727: $this->getNativeType($assignedExpr),
2728: $scope->getNativeType($targetVarNode),
2729: );
2730: }
2731: $scope = $scope->assignVariable(
2732: $targetVarName,
2733: $assignedType,
2734: $assignedNativeType,
2735: $has,
2736: array_merge($intertwinedPropagatedFrom, [$variableName]),
2737: );
2738: } else {
2739: $targetRootVar = $this->getIntertwinedRefRootVariableName($expressionType->getExpr()->getExpr());
2740: if ($targetRootVar !== null && in_array($targetRootVar, $intertwinedPropagatedFrom, true)) {
2741: continue;
2742: }
2743: $scope = $scope->assignExpression(
2744: $expressionType->getExpr()->getExpr(),
2745: $assignedType,
2746: $assignedNativeType,
2747: );
2748: }
2749: }
2750:
2751: return $scope;
2752: }
2753:
2754: private function isDimFetchPathReachable(self $scope, Expr\ArrayDimFetch $dimFetch): bool
2755: {
2756: if ($dimFetch->dim === null) {
2757: return false;
2758: }
2759:
2760: if (!$dimFetch->var instanceof Expr\ArrayDimFetch) {
2761: return true;
2762: }
2763:
2764: $varType = $scope->getType($dimFetch->var);
2765: $dimType = $scope->getType($dimFetch->dim);
2766:
2767: if (!$varType->hasOffsetValueType($dimType)->yes()) {
2768: return false;
2769: }
2770:
2771: return $this->isDimFetchPathReachable($scope, $dimFetch->var);
2772: }
2773:
2774: private function unsetExpression(Expr $expr): self
2775: {
2776: $scope = $this;
2777: if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) {
2778: $exprVarType = $scope->getType($expr->var);
2779: $dimType = $scope->getType($expr->dim);
2780: $unsetType = $exprVarType->unsetOffset($dimType);
2781: $exprVarNativeType = $scope->getNativeType($expr->var);
2782: $dimNativeType = $scope->getNativeType($expr->dim);
2783: $unsetNativeType = $exprVarNativeType->unsetOffset($dimNativeType);
2784: $scope = $scope->assignExpression($expr->var, $unsetType, $unsetNativeType)->invalidateExpression(
2785: new FuncCall(new FullyQualified('count'), [new Arg($expr->var)]),
2786: )->invalidateExpression(
2787: new FuncCall(new FullyQualified('sizeof'), [new Arg($expr->var)]),
2788: )->invalidateExpression(
2789: new FuncCall(new Name('count'), [new Arg($expr->var)]),
2790: )->invalidateExpression(
2791: new FuncCall(new Name('sizeof'), [new Arg($expr->var)]),
2792: );
2793:
2794: if ($expr->var instanceof Expr\ArrayDimFetch && $expr->var->dim !== null) {
2795: $scope = $scope->assignExpression(
2796: $expr->var->var,
2797: $this->getType($expr->var->var)->setOffsetValueType(
2798: $scope->getType($expr->var->dim),
2799: $scope->getType($expr->var),
2800: ),
2801: $this->getNativeType($expr->var->var)->setOffsetValueType(
2802: $scope->getNativeType($expr->var->dim),
2803: $scope->getNativeType($expr->var),
2804: ),
2805: );
2806: }
2807: }
2808:
2809: return $scope->invalidateExpression($expr);
2810: }
2811:
2812: public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, TrinaryLogic $certainty): self
2813: {
2814: if ($expr instanceof Scalar) {
2815: return $this;
2816: }
2817:
2818: if ($expr instanceof ConstFetch) {
2819: $loweredConstName = strtolower($expr->name->toString());
2820: if (in_array($loweredConstName, ['true', 'false', 'null'], true)) {
2821: return $this;
2822: }
2823: }
2824:
2825: if ($expr instanceof FuncCall && $expr->name instanceof Name && $type->isFalse()->yes()) {
2826: $functionName = $this->reflectionProvider->resolveFunctionName($expr->name, $this);
2827: if ($functionName !== null && in_array(strtolower($functionName), [
2828: 'is_dir',
2829: 'is_file',
2830: 'file_exists',
2831: ], true)) {
2832: return $this;
2833: }
2834: }
2835:
2836: $scope = $this;
2837: if (
2838: $expr instanceof Expr\ArrayDimFetch
2839: && $expr->dim !== null
2840: && !$expr->dim instanceof Expr\PreInc
2841: && !$expr->dim instanceof Expr\PreDec
2842: && !$expr->dim instanceof Expr\PostDec
2843: && !$expr->dim instanceof Expr\PostInc
2844: ) {
2845: $dimType = $scope->getType($expr->dim)->toArrayKey();
2846: if ($dimType->isInteger()->yes() || $dimType->isString()->yes()) {
2847: $exprVarType = $scope->getType($expr->var);
2848: $isArray = $exprVarType->isArray();
2849: if (!$exprVarType instanceof MixedType && !$isArray->no()) {
2850: $varType = $exprVarType;
2851: if (!$isArray->yes()) {
2852: if ($dimType->isInteger()->yes()) {
2853: $varType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::intOffsetAccessibleType());
2854: } else {
2855: $varType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::generalOffsetAccessibleType());
2856: }
2857: }
2858:
2859: if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) {
2860: if (!$this->isComplexUnionType($varType)) {
2861: $varType = TypeCombinator::intersect(
2862: $varType,
2863: new HasOffsetValueType($dimType, $type),
2864: );
2865: }
2866: }
2867:
2868: $scope = $scope->specifyExpressionType(
2869: $expr->var,
2870: $varType,
2871: $scope->getNativeType($expr->var),
2872: $certainty,
2873: );
2874: }
2875: }
2876: }
2877:
2878: if ($certainty->no()) {
2879: throw new ShouldNotHappenException();
2880: }
2881:
2882: $exprString = $this->getNodeKey($expr);
2883: $expressionTypes = $scope->expressionTypes;
2884: $expressionTypes[$exprString] = new ExpressionTypeHolder($expr, $type, $certainty);
2885: $nativeTypes = $scope->nativeExpressionTypes;
2886: $nativeTypes[$exprString] = new ExpressionTypeHolder($expr, $nativeType, $certainty);
2887:
2888: $scope = $this->scopeFactory->create(
2889: $this->context,
2890: $this->isDeclareStrictTypes(),
2891: $this->getFunction(),
2892: $this->getNamespace(),
2893: $expressionTypes,
2894: $nativeTypes,
2895: $this->conditionalExpressions,
2896: $this->inClosureBindScopeClasses,
2897: $this->anonymousFunctionReflection,
2898: $this->inFirstLevelStatement,
2899: $this->currentlyAssignedExpressions,
2900: $this->currentlyAllowedUndefinedExpressions,
2901: $this->inFunctionCallsStack,
2902: $this->afterExtractCall,
2903: $this->parentScope,
2904: $this->nativeTypesPromoted,
2905: );
2906:
2907: if ($expr instanceof AlwaysRememberedExpr) {
2908: return $scope->specifyExpressionType($expr->expr, $type, $nativeType, $certainty);
2909: }
2910:
2911: return $scope;
2912: }
2913:
2914: public function assignExpression(Expr $expr, Type $type, Type $nativeType): self
2915: {
2916: $scope = $this;
2917: if ($expr instanceof PropertyFetch) {
2918: $scope = $this->invalidateExpression($expr)
2919: ->invalidateMethodsOnExpression($expr->var);
2920: } elseif ($expr instanceof Expr\StaticPropertyFetch) {
2921: $scope = $this->invalidateExpression($expr);
2922: } elseif ($expr instanceof Variable) {
2923: $scope = $this->invalidateExpression($expr);
2924: }
2925:
2926: return $scope->specifyExpressionType($expr, $type, $nativeType, TrinaryLogic::createYes());
2927: }
2928:
2929: public function assignInitializedProperty(Type $fetchedOnType, string $propertyName): self
2930: {
2931: if (!$this->isInClass()) {
2932: return $this;
2933: }
2934:
2935: if (TypeUtils::findThisType($fetchedOnType) === null) {
2936: return $this;
2937: }
2938:
2939: $propertyReflection = $this->getInstancePropertyReflection($fetchedOnType, $propertyName);
2940: if ($propertyReflection === null) {
2941: return $this;
2942: }
2943: $declaringClass = $propertyReflection->getDeclaringClass();
2944: if ($this->getClassReflection()->getName() !== $declaringClass->getName()) {
2945: return $this;
2946: }
2947: if (!$declaringClass->hasNativeProperty($propertyName)) {
2948: return $this;
2949: }
2950:
2951: $scope = $this->assignExpression(new PropertyInitializationExpr($propertyName), new MixedType(), new MixedType());
2952:
2953: $function = $scope->getFunction();
2954: if (
2955: $function instanceof MethodReflection
2956: && strtolower($function->getName()) === '__clone'
2957: && $scope->phpVersion->supportsReadonlyPropertyReinitializationOnClone()
2958: ) {
2959: $scope = $scope->assignExpression(new CloneReinitializationExpr($propertyName), new MixedType(), new MixedType());
2960: }
2961:
2962: return $scope;
2963: }
2964:
2965: public function invalidateExpression(Expr $expressionToInvalidate, bool $requireMoreCharacters = false, ?ClassReflection $invalidatingClass = null): self
2966: {
2967: $expressionTypes = $this->expressionTypes;
2968: $nativeExpressionTypes = $this->nativeExpressionTypes;
2969: $invalidated = false;
2970: $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate);
2971:
2972: foreach ($expressionTypes as $exprString => $exprTypeHolder) {
2973: $exprExpr = $exprTypeHolder->getExpr();
2974: if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr, $exprString, $requireMoreCharacters, $invalidatingClass)) {
2975: continue;
2976: }
2977:
2978: unset($expressionTypes[$exprString]);
2979: unset($nativeExpressionTypes[$exprString]);
2980: $invalidated = true;
2981: }
2982:
2983: $newConditionalExpressions = [];
2984: foreach ($this->conditionalExpressions as $conditionalExprString => $holders) {
2985: if (count($holders) === 0) {
2986: continue;
2987: }
2988: $firstExpr = $holders[array_key_first($holders)]->getTypeHolder()->getExpr();
2989: if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $firstExpr, $this->getNodeKey($firstExpr), $requireMoreCharacters, $invalidatingClass)) {
2990: $invalidated = true;
2991: continue;
2992: }
2993: $filteredHolders = [];
2994: foreach ($holders as $key => $holder) {
2995: $shouldKeep = true;
2996: $conditionalTypeHolders = $holder->getConditionExpressionTypeHolders();
2997: foreach ($conditionalTypeHolders as $conditionalTypeHolderExprString => $conditionalTypeHolder) {
2998: if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $conditionalTypeHolder->getExpr(), $conditionalTypeHolderExprString, false, $invalidatingClass)) {
2999: $invalidated = true;
3000: $shouldKeep = false;
3001: break;
3002: }
3003: }
3004: if (!$shouldKeep) {
3005: continue;
3006: }
3007:
3008: $filteredHolders[$key] = $holder;
3009: }
3010: if (count($filteredHolders) <= 0) {
3011: continue;
3012: }
3013:
3014: $newConditionalExpressions[$conditionalExprString] = $filteredHolders;
3015: }
3016:
3017: if (!$invalidated) {
3018: return $this;
3019: }
3020:
3021: return $this->scopeFactory->create(
3022: $this->context,
3023: $this->isDeclareStrictTypes(),
3024: $this->getFunction(),
3025: $this->getNamespace(),
3026: $expressionTypes,
3027: $nativeExpressionTypes,
3028: $newConditionalExpressions,
3029: $this->inClosureBindScopeClasses,
3030: $this->anonymousFunctionReflection,
3031: $this->inFirstLevelStatement,
3032: $this->currentlyAssignedExpressions,
3033: $this->currentlyAllowedUndefinedExpressions,
3034: [],
3035: $this->afterExtractCall,
3036: $this->parentScope,
3037: $this->nativeTypesPromoted,
3038: );
3039: }
3040:
3041: private function getIntertwinedRefRootVariableName(Expr $expr): ?string
3042: {
3043: if ($expr instanceof Variable && is_string($expr->name)) {
3044: return $expr->name;
3045: }
3046: if ($expr instanceof Expr\ArrayDimFetch) {
3047: return $this->getIntertwinedRefRootVariableName($expr->var);
3048: }
3049: return null;
3050: }
3051:
3052: private function shouldInvalidateExpression(string $exprStringToInvalidate, Expr $exprToInvalidate, Expr $expr, string $exprString, bool $requireMoreCharacters = false, ?ClassReflection $invalidatingClass = null): bool
3053: {
3054: if (
3055: $expr instanceof IntertwinedVariableByReferenceWithExpr
3056: && $exprToInvalidate instanceof Variable
3057: && is_string($exprToInvalidate->name)
3058: && (
3059: $expr->getVariableName() === $exprToInvalidate->name
3060: || $this->getIntertwinedRefRootVariableName($expr->getExpr()) === $exprToInvalidate->name
3061: || $this->getIntertwinedRefRootVariableName($expr->getAssignedExpr()) === $exprToInvalidate->name
3062: )
3063: ) {
3064: return false;
3065: }
3066:
3067: if ($requireMoreCharacters && $exprStringToInvalidate === $exprString) {
3068: return false;
3069: }
3070:
3071: // Variables will not contain traversable expressions. skip the NodeFinder overhead
3072: if ($expr instanceof Variable && is_string($expr->name) && !$requireMoreCharacters) {
3073: return $exprStringToInvalidate === $exprString;
3074: }
3075:
3076: // getNodeKey() is the pretty-printed expression, and the standard printer is
3077: // compositional: the key of any sub-expression appears verbatim as a substring of
3078: // the key of the expression containing it. So if the invalidated expression's key
3079: // does not appear anywhere in this expression's key, this expression cannot contain
3080: // it and we can skip the expensive AST traversal below.
3081: // Carve-outs where that invariant does not hold:
3082: // - '$this' is special-cased in the visitor to also match self/static/parent,
3083: // - PHPStan's virtual nodes (printed as '__phpstan…') use non-compositional printers
3084: // (e.g. a wrapped variable is printed by name, not as '$name'),
3085: // - keys carrying a getNodeKey() suffix ('/*…*/') are not plain substrings.
3086: if (
3087: $exprStringToInvalidate !== '$this'
3088: && !str_contains($exprStringToInvalidate, '__phpstan')
3089: && !str_contains($exprStringToInvalidate, '/*')
3090: && !str_contains($exprString, '__phpstan')
3091: && !str_contains($exprString, $exprStringToInvalidate)
3092: ) {
3093: return false;
3094: }
3095:
3096: $nodeFinder = new NodeFinder();
3097: $expressionToInvalidateClass = get_class($exprToInvalidate);
3098: $found = $nodeFinder->findFirst([$expr], function (Node $node) use ($expressionToInvalidateClass, $exprStringToInvalidate): bool {
3099: if (
3100: $exprStringToInvalidate === '$this'
3101: && $node instanceof Name
3102: && (
3103: in_array($node->toLowerString(), ['self', 'static', 'parent'], true)
3104: || ($this->getClassReflection() !== null && $this->getClassReflection()->is($this->resolveName($node)))
3105: )
3106: ) {
3107: return true;
3108: }
3109:
3110: if (!$node instanceof $expressionToInvalidateClass) {
3111: return false;
3112: }
3113:
3114: $nodeString = $this->getNodeKey($node);
3115:
3116: return $nodeString === $exprStringToInvalidate;
3117: });
3118:
3119: if ($found === null) {
3120: return false;
3121: }
3122:
3123: if (
3124: $expr instanceof PropertyFetch
3125: && $requireMoreCharacters
3126: && $this->isReadonlyPropertyFetch($expr, false)
3127: ) {
3128: return false;
3129: }
3130:
3131: if (
3132: $invalidatingClass !== null
3133: && $requireMoreCharacters
3134: && $this->isPrivatePropertyOfDifferentClass($expr, $invalidatingClass)
3135: ) {
3136: return false;
3137: }
3138:
3139: return true;
3140: }
3141:
3142: private function isPrivatePropertyOfDifferentClass(Expr $expr, ClassReflection $invalidatingClass): bool
3143: {
3144: if ($expr instanceof Expr\StaticPropertyFetch || $expr instanceof PropertyFetch) {
3145: $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this);
3146: if ($propertyReflection === null) {
3147: return false;
3148: }
3149: if (!$propertyReflection->isPrivate()) {
3150: return false;
3151: }
3152: return $propertyReflection->getDeclaringClass()->getName() !== $invalidatingClass->getName();
3153: }
3154:
3155: return false;
3156: }
3157:
3158: private function invalidateMethodsOnExpression(Expr $expressionToInvalidate): self
3159: {
3160: $exprStringToInvalidate = null;
3161: $expressionTypes = $this->expressionTypes;
3162: $nativeExpressionTypes = $this->nativeExpressionTypes;
3163: $invalidated = false;
3164: foreach ($expressionTypes as $exprString => $exprTypeHolder) {
3165: $expr = $exprTypeHolder->getExpr();
3166: if (!$expr instanceof MethodCall) {
3167: continue;
3168: }
3169:
3170: $exprStringToInvalidate ??= $this->getNodeKey($expressionToInvalidate);
3171: if ($this->getNodeKey($expr->var) !== $exprStringToInvalidate) {
3172: continue;
3173: }
3174:
3175: unset($expressionTypes[$exprString]);
3176: unset($nativeExpressionTypes[$exprString]);
3177: $invalidated = true;
3178: }
3179:
3180: if (!$invalidated) {
3181: return $this;
3182: }
3183:
3184: return $this->scopeFactory->create(
3185: $this->context,
3186: $this->isDeclareStrictTypes(),
3187: $this->getFunction(),
3188: $this->getNamespace(),
3189: $expressionTypes,
3190: $nativeExpressionTypes,
3191: $this->conditionalExpressions,
3192: $this->inClosureBindScopeClasses,
3193: $this->anonymousFunctionReflection,
3194: $this->inFirstLevelStatement,
3195: $this->currentlyAssignedExpressions,
3196: $this->currentlyAllowedUndefinedExpressions,
3197: [],
3198: $this->afterExtractCall,
3199: $this->parentScope,
3200: $this->nativeTypesPromoted,
3201: );
3202: }
3203:
3204: private function setExpressionCertainty(Expr $expr, TrinaryLogic $certainty): self
3205: {
3206: if ($this->hasExpressionType($expr)->no()) {
3207: throw new ShouldNotHappenException();
3208: }
3209:
3210: $originalExprType = $this->getType($expr);
3211: $nativeType = $this->getNativeType($expr);
3212:
3213: return $this->specifyExpressionType(
3214: $expr,
3215: $originalExprType,
3216: $nativeType,
3217: $certainty,
3218: );
3219: }
3220:
3221: /**
3222: * Returns true when the type is a large union with intersection
3223: * members that carry HasOffsetValueType — a sign of combinatorial
3224: * growth from successive array|object offset access patterns.
3225: * Operating on such types is expensive and should be skipped.
3226: */
3227: private function isComplexUnionType(Type $type): bool
3228: {
3229: if (!$type instanceof UnionType) {
3230: return false;
3231: }
3232: $types = $type->getTypes();
3233: if (count($types) <= self::COMPLEX_UNION_TYPE_MEMBER_LIMIT) {
3234: return false;
3235: }
3236: foreach ($types as $member) {
3237: if (!$member instanceof IntersectionType) {
3238: continue;
3239: }
3240: foreach ($member->getTypes() as $innerType) {
3241: if ($innerType instanceof HasOffsetValueType) {
3242: return true;
3243: }
3244: }
3245: }
3246: return false;
3247: }
3248:
3249: public function addTypeToExpression(Expr $expr, Type $type): self
3250: {
3251: $originalExprType = $this->getType($expr);
3252: if ($this->isComplexUnionType($originalExprType)) {
3253: return $this;
3254: }
3255:
3256: $nativeType = $this->getNativeType($expr);
3257:
3258: if ($originalExprType->equals($nativeType)) {
3259: $newType = TypeCombinator::intersect($type, $originalExprType);
3260: return $this->specifyExpressionType($expr, $newType, $newType, TrinaryLogic::createYes());
3261: }
3262:
3263: return $this->specifyExpressionType(
3264: $expr,
3265: TypeCombinator::intersect($type, $originalExprType),
3266: TypeCombinator::intersect($type, $nativeType),
3267: TrinaryLogic::createYes(),
3268: );
3269: }
3270:
3271: public function removeTypeFromExpression(Expr $expr, Type $typeToRemove): self
3272: {
3273: if ($typeToRemove instanceof NeverType) {
3274: return $this;
3275: }
3276:
3277: $exprType = $this->getType($expr);
3278: if ($exprType instanceof NeverType) {
3279: return $this;
3280: }
3281:
3282: if ($this->isComplexUnionType($exprType)) {
3283: return $this;
3284: }
3285:
3286: return $this->specifyExpressionType(
3287: $expr,
3288: TypeCombinator::remove($exprType, $typeToRemove),
3289: TypeCombinator::remove($this->getNativeType($expr), $typeToRemove),
3290: TrinaryLogic::createYes(),
3291: );
3292: }
3293:
3294: /**
3295: * @api
3296: */
3297: public function filterByTruthyValue(Expr $expr): self
3298: {
3299: $exprString = $this->getNodeKey($expr);
3300: if (array_key_exists($exprString, $this->truthyScopes)) {
3301: return $this->truthyScopes[$exprString];
3302: }
3303:
3304: $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createTruthy());
3305: $scope = $this->filterBySpecifiedTypes($specifiedTypes);
3306: $this->truthyScopes[$exprString] = $scope;
3307:
3308: return $scope;
3309: }
3310:
3311: /**
3312: * @api
3313: */
3314: public function filterByFalseyValue(Expr $expr): self
3315: {
3316: $exprString = $this->getNodeKey($expr);
3317: if (array_key_exists($exprString, $this->falseyScopes)) {
3318: return $this->falseyScopes[$exprString];
3319: }
3320:
3321: $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createFalsey());
3322: $scope = $this->filterBySpecifiedTypes($specifiedTypes);
3323: $this->falseyScopes[$exprString] = $scope;
3324:
3325: return $scope;
3326: }
3327:
3328: /**
3329: * @return static
3330: */
3331: public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
3332: {
3333: $typeSpecifications = [];
3334: foreach ($specifiedTypes->getSureTypes() as $exprString => [$expr, $type]) {
3335: if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) {
3336: continue;
3337: }
3338: $typeSpecifications[] = [
3339: 'sure' => true,
3340: 'exprString' => (string) $exprString,
3341: 'expr' => $expr,
3342: 'type' => $type,
3343: ];
3344: }
3345: foreach ($specifiedTypes->getSureNotTypes() as $exprString => [$expr, $type]) {
3346: if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) {
3347: continue;
3348: }
3349: $typeSpecifications[] = [
3350: 'sure' => false,
3351: 'exprString' => (string) $exprString,
3352: 'expr' => $expr,
3353: 'type' => $type,
3354: ];
3355: }
3356:
3357: usort($typeSpecifications, static function (array $a, array $b): int {
3358: $length = strlen($a['exprString']) - strlen($b['exprString']);
3359: if ($length !== 0) {
3360: return $length;
3361: }
3362:
3363: return $b['sure'] - $a['sure']; // @phpstan-ignore minus.leftNonNumeric, minus.rightNonNumeric
3364: });
3365:
3366: $scope = $this;
3367: $specifiedExpressions = [];
3368: foreach ($typeSpecifications as $typeSpecification) {
3369: $expr = $typeSpecification['expr'];
3370: $type = $typeSpecification['type'];
3371:
3372: if ($expr instanceof IssetExpr) {
3373: $issetExpr = $expr;
3374: $expr = $issetExpr->getExpr();
3375:
3376: if ($typeSpecification['sure']) {
3377: $scope = $scope->setExpressionCertainty(
3378: $expr,
3379: TrinaryLogic::createMaybe(),
3380: );
3381: } else {
3382: $scope = $scope->unsetExpression($expr);
3383: }
3384:
3385: continue;
3386: }
3387:
3388: if ($typeSpecification['sure']) {
3389: if ($specifiedTypes->shouldOverwrite()) {
3390: $scope = $scope->assignExpression($expr, $type, $type);
3391: } else {
3392: $scope = $scope->addTypeToExpression($expr, $type);
3393: }
3394: } else {
3395: $scope = $scope->removeTypeFromExpression($expr, $type);
3396: }
3397: $specifiedExpressions[$typeSpecification['exprString']] = ExpressionTypeHolder::createYes($expr, $scope->getScopeType($expr));
3398: }
3399:
3400: $conditions = [];
3401: $originallySpecifiedExprStrings = $specifiedExpressions;
3402: $prevSpecifiedCount = -1;
3403: while (count($specifiedExpressions) !== $prevSpecifiedCount) {
3404: $prevSpecifiedCount = count($specifiedExpressions);
3405: foreach ($scope->conditionalExpressions as $conditionalExprString => $conditionalExpressions) {
3406: if (array_key_exists($conditionalExprString, $conditions)) {
3407: continue;
3408: }
3409:
3410: // Pass 1: Prefer exact matches
3411: foreach ($conditionalExpressions as $conditionalExpression) {
3412: if (
3413: $conditionalExpression->getTypeHolder()->getCertainty()->no()
3414: && array_key_exists($conditionalExprString, $originallySpecifiedExprStrings)
3415: ) {
3416: continue;
3417: }
3418: foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) {
3419: if (
3420: !array_key_exists($holderExprString, $specifiedExpressions)
3421: || !$conditionalTypeHolder->equals($specifiedExpressions[$holderExprString])
3422: ) {
3423: continue 2;
3424: }
3425: }
3426:
3427: $conditions[$conditionalExprString][] = $conditionalExpression;
3428: $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder();
3429: }
3430:
3431: if (array_key_exists($conditionalExprString, $conditions)) {
3432: continue;
3433: }
3434:
3435: // Pass 2: Supertype match. Only runs when Pass 1 found no exact match for this expression.
3436: foreach ($conditionalExpressions as $conditionalExpression) {
3437: if ($conditionalExpression->getTypeHolder()->getCertainty()->no()) {
3438: continue;
3439: }
3440: foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) {
3441: if (
3442: !array_key_exists($holderExprString, $specifiedExpressions)
3443: || !$conditionalTypeHolder->getCertainty()->equals($specifiedExpressions[$holderExprString]->getCertainty())
3444: || !$conditionalTypeHolder->getType()->isSuperTypeOf($specifiedExpressions[$holderExprString]->getType())->yes()
3445: ) {
3446: continue 2;
3447: }
3448: }
3449:
3450: $conditions[$conditionalExprString][] = $conditionalExpression;
3451: $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder();
3452: }
3453: }
3454: }
3455:
3456: foreach ($conditions as $conditionalExprString => $expressions) {
3457: $certainty = TrinaryLogic::lazyExtremeIdentity($expressions, static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getCertainty());
3458: if ($certainty->no()) {
3459: unset($scope->expressionTypes[$conditionalExprString]);
3460: } else {
3461: if (array_key_exists($conditionalExprString, $scope->expressionTypes)) {
3462: $type = $expressions[0]->getTypeHolder()->getType();
3463: for ($i = 1, $count = count($expressions); $i < $count; $i++) {
3464: $type = TypeCombinator::intersect($type, $expressions[$i]->getTypeHolder()->getType());
3465: }
3466:
3467: $scope->expressionTypes[$conditionalExprString] = new ExpressionTypeHolder(
3468: $scope->expressionTypes[$conditionalExprString]->getExpr(),
3469: TypeCombinator::intersect($scope->expressionTypes[$conditionalExprString]->getType(), $type),
3470: TrinaryLogic::maxMin($scope->expressionTypes[$conditionalExprString]->getCertainty(), $certainty),
3471: );
3472: } else {
3473: $scope->expressionTypes[$conditionalExprString] = $expressions[0]->getTypeHolder();
3474: }
3475: }
3476: }
3477:
3478: /** @var static */
3479: return $scope->scopeFactory->create(
3480: $scope->context,
3481: $scope->isDeclareStrictTypes(),
3482: $scope->getFunction(),
3483: $scope->getNamespace(),
3484: $scope->expressionTypes,
3485: $scope->nativeExpressionTypes,
3486: $this->mergeConditionalExpressions($specifiedTypes->getNewConditionalExpressionHolders(), $scope->conditionalExpressions),
3487: $scope->inClosureBindScopeClasses,
3488: $scope->anonymousFunctionReflection,
3489: $scope->inFirstLevelStatement,
3490: $scope->currentlyAssignedExpressions,
3491: $scope->currentlyAllowedUndefinedExpressions,
3492: $scope->inFunctionCallsStack,
3493: $scope->afterExtractCall,
3494: $scope->parentScope,
3495: $scope->nativeTypesPromoted,
3496: );
3497: }
3498:
3499: /**
3500: * @return array<string, ConditionalExpressionHolder[]>
3501: */
3502: public function getConditionalExpressions(): array
3503: {
3504: return $this->conditionalExpressions;
3505: }
3506:
3507: /**
3508: * @param ConditionalExpressionHolder[] $conditionalExpressionHolders
3509: */
3510: public function addConditionalExpressions(string $exprString, array $conditionalExpressionHolders): self
3511: {
3512: $conditionalExpressions = $this->conditionalExpressions;
3513: // Merge rather than overwrite: multiple independent holders can target the same
3514: // expression (e.g. `$xIsA = $x instanceof A && $y instanceof A` stores a holder
3515: // for `$x` keyed on `$xIsA`; later `$yIsA = $y instanceof A && $x instanceof A`
3516: // stores another holder for the same target `$x` keyed on `$yIsA`). Replacing
3517: // the existing entry here would throw away the earlier binding, breaking
3518: // narrowing inside later `if ($xIsA) { … }` inside `if ($xIsA || $yIsA)`.
3519: // Holder keys (`getKey()`) disambiguate identical entries so we still dedupe.
3520: $existing = $conditionalExpressions[$exprString] ?? [];
3521: foreach ($conditionalExpressionHolders as $holder) {
3522: $existing[$holder->getKey()] = $holder;
3523: }
3524: $conditionalExpressions[$exprString] = $existing;
3525: return $this->scopeFactory->create(
3526: $this->context,
3527: $this->isDeclareStrictTypes(),
3528: $this->getFunction(),
3529: $this->getNamespace(),
3530: $this->expressionTypes,
3531: $this->nativeExpressionTypes,
3532: $conditionalExpressions,
3533: $this->inClosureBindScopeClasses,
3534: $this->anonymousFunctionReflection,
3535: $this->inFirstLevelStatement,
3536: $this->currentlyAssignedExpressions,
3537: $this->currentlyAllowedUndefinedExpressions,
3538: $this->inFunctionCallsStack,
3539: $this->afterExtractCall,
3540: $this->parentScope,
3541: $this->nativeTypesPromoted,
3542: );
3543: }
3544:
3545: public function exitFirstLevelStatements(): self
3546: {
3547: if (!$this->inFirstLevelStatement) {
3548: return $this;
3549: }
3550:
3551: if ($this->scopeOutOfFirstLevelStatement !== null) {
3552: return $this->scopeOutOfFirstLevelStatement;
3553: }
3554:
3555: $scope = $this->scopeFactory->create(
3556: $this->context,
3557: $this->isDeclareStrictTypes(),
3558: $this->getFunction(),
3559: $this->getNamespace(),
3560: $this->expressionTypes,
3561: $this->nativeExpressionTypes,
3562: $this->conditionalExpressions,
3563: $this->inClosureBindScopeClasses,
3564: $this->anonymousFunctionReflection,
3565: false,
3566: $this->currentlyAssignedExpressions,
3567: $this->currentlyAllowedUndefinedExpressions,
3568: $this->inFunctionCallsStack,
3569: $this->afterExtractCall,
3570: $this->parentScope,
3571: $this->nativeTypesPromoted,
3572: );
3573: $scope->resolvedTypes = $this->resolvedTypes;
3574: $scope->truthyScopes = $this->truthyScopes;
3575: $scope->falseyScopes = $this->falseyScopes;
3576: $this->scopeOutOfFirstLevelStatement = $scope;
3577:
3578: return $scope;
3579: }
3580:
3581: /** @api */
3582: public function isInFirstLevelStatement(): bool
3583: {
3584: return $this->inFirstLevelStatement;
3585: }
3586:
3587: public function mergeWith(?self $otherScope, bool $preserveVacuousConditionals = false): self
3588: {
3589: if ($otherScope === null || $this === $otherScope) {
3590: return $this;
3591: }
3592: $ourExpressionTypes = $this->expressionTypes;
3593: $theirExpressionTypes = $otherScope->expressionTypes;
3594:
3595: $mergedExpressionTypes = $this->mergeVariableHolders($ourExpressionTypes, $theirExpressionTypes);
3596: $conditionalExpressions = $this->intersectConditionalExpressions($otherScope->conditionalExpressions);
3597: if ($preserveVacuousConditionals) {
3598: $conditionalExpressions = $this->preserveVacuousConditionalExpressions(
3599: $conditionalExpressions,
3600: $this->conditionalExpressions,
3601: $theirExpressionTypes,
3602: );
3603: $conditionalExpressions = $this->preserveVacuousConditionalExpressions(
3604: $conditionalExpressions,
3605: $otherScope->conditionalExpressions,
3606: $ourExpressionTypes,
3607: );
3608: }
3609: $conditionalExpressions = $this->createConditionalExpressions(
3610: $conditionalExpressions,
3611: $ourExpressionTypes,
3612: $theirExpressionTypes,
3613: $mergedExpressionTypes,
3614: );
3615: $conditionalExpressions = $this->createConditionalExpressions(
3616: $conditionalExpressions,
3617: $theirExpressionTypes,
3618: $ourExpressionTypes,
3619: $mergedExpressionTypes,
3620: );
3621:
3622: $filter = static function (ExpressionTypeHolder $expressionTypeHolder) {
3623: if ($expressionTypeHolder->getCertainty()->yes()) {
3624: return true;
3625: }
3626:
3627: $expr = $expressionTypeHolder->getExpr();
3628:
3629: return $expr instanceof Variable
3630: || $expr instanceof FuncCall
3631: || $expr instanceof VirtualNode;
3632: };
3633:
3634: $mergedExpressionTypes = array_filter($mergedExpressionTypes, $filter);
3635:
3636: $ourNativeExpressionTypes = $this->nativeExpressionTypes;
3637: $theirNativeExpressionTypes = $otherScope->nativeExpressionTypes;
3638: $mergedNativeExpressionTypes = [];
3639: foreach ($ourNativeExpressionTypes as $exprString => $expressionTypeHolder) {
3640: if (!array_key_exists($exprString, $theirNativeExpressionTypes)) {
3641: continue;
3642: }
3643: if (!array_key_exists($exprString, $ourExpressionTypes)) {
3644: continue;
3645: }
3646: if (!array_key_exists($exprString, $theirExpressionTypes)) {
3647: continue;
3648: }
3649: if (!$expressionTypeHolder->equals($ourExpressionTypes[$exprString])) {
3650: continue;
3651: }
3652: if (!$theirNativeExpressionTypes[$exprString]->equals($theirExpressionTypes[$exprString])) {
3653: continue;
3654: }
3655: if (!array_key_exists($exprString, $mergedExpressionTypes)) {
3656: continue;
3657: }
3658: $mergedNativeExpressionTypes[$exprString] = $mergedExpressionTypes[$exprString];
3659: unset($ourNativeExpressionTypes[$exprString]);
3660: unset($theirNativeExpressionTypes[$exprString]);
3661: }
3662:
3663: return $this->scopeFactory->create(
3664: $this->context,
3665: $this->isDeclareStrictTypes(),
3666: $this->getFunction(),
3667: $this->getNamespace(),
3668: $mergedExpressionTypes,
3669: array_merge($mergedNativeExpressionTypes, array_filter($this->mergeVariableHolders($ourNativeExpressionTypes, $theirNativeExpressionTypes), $filter)),
3670: $conditionalExpressions,
3671: $this->inClosureBindScopeClasses,
3672: $this->anonymousFunctionReflection,
3673: $this->inFirstLevelStatement,
3674: [],
3675: [],
3676: [],
3677: $this->afterExtractCall && $otherScope->afterExtractCall,
3678: $this->parentScope,
3679: $this->nativeTypesPromoted,
3680: );
3681: }
3682:
3683: /**
3684: * @param array<string, ConditionalExpressionHolder[]> $otherConditionalExpressions
3685: * @return array<string, ConditionalExpressionHolder[]>
3686: */
3687: private function intersectConditionalExpressions(array $otherConditionalExpressions): array
3688: {
3689: $newConditionalExpressions = [];
3690: foreach ($this->conditionalExpressions as $exprString => $holders) {
3691: if (!array_key_exists($exprString, $otherConditionalExpressions)) {
3692: continue;
3693: }
3694:
3695: $otherHolders = $otherConditionalExpressions[$exprString];
3696: $intersectedHolders = [];
3697: foreach ($holders as $key => $holder) {
3698: if (!array_key_exists($key, $otherHolders)) {
3699: continue;
3700: }
3701: $intersectedHolders[$key] = $holder;
3702: }
3703:
3704: if (count($intersectedHolders) === 0) {
3705: continue;
3706: }
3707:
3708: $newConditionalExpressions[$exprString] = $intersectedHolders;
3709: }
3710:
3711: return $newConditionalExpressions;
3712: }
3713:
3714: /**
3715: * @param array<string, ConditionalExpressionHolder[]> $currentConditionalExpressions
3716: * @param array<string, ConditionalExpressionHolder[]> $sourceConditionalExpressions
3717: * @param array<string, ExpressionTypeHolder> $otherExpressionTypes
3718: * @return array<string, ConditionalExpressionHolder[]>
3719: */
3720: private function preserveVacuousConditionalExpressions(
3721: array $currentConditionalExpressions,
3722: array $sourceConditionalExpressions,
3723: array $otherExpressionTypes,
3724: ): array
3725: {
3726: foreach ($sourceConditionalExpressions as $exprString => $holders) {
3727: foreach ($holders as $key => $holder) {
3728: if (isset($currentConditionalExpressions[$exprString][$key])) {
3729: continue;
3730: }
3731:
3732: $typeHolder = $holder->getTypeHolder();
3733: if ($typeHolder->getCertainty()->no() && !$typeHolder->getExpr() instanceof Variable) {
3734: continue;
3735: }
3736:
3737: foreach ($holder->getConditionExpressionTypeHolders() as $guardExprString => $guardTypeHolder) {
3738: if (!array_key_exists($guardExprString, $otherExpressionTypes)) {
3739: continue;
3740: }
3741:
3742: $otherType = $otherExpressionTypes[$guardExprString]->getType();
3743: $guardType = $guardTypeHolder->getType();
3744:
3745: if ($otherType->isSuperTypeOf($guardType)->no()) {
3746: $currentConditionalExpressions[$exprString][$key] = $holder;
3747: break;
3748: }
3749: }
3750: }
3751: }
3752:
3753: return $currentConditionalExpressions;
3754: }
3755:
3756: /**
3757: * @param array<string, ConditionalExpressionHolder[]> $newConditionalExpressions
3758: * @param array<string, ConditionalExpressionHolder[]> $existingConditionalExpressions
3759: * @return array<string, ConditionalExpressionHolder[]>
3760: */
3761: private function mergeConditionalExpressions(array $newConditionalExpressions, array $existingConditionalExpressions): array
3762: {
3763: $result = $existingConditionalExpressions;
3764: foreach ($newConditionalExpressions as $exprString => $holders) {
3765: if (!array_key_exists($exprString, $result)) {
3766: $result[$exprString] = $holders;
3767: } else {
3768: $result[$exprString] = array_merge($result[$exprString], $holders);
3769: }
3770: }
3771:
3772: return $result;
3773: }
3774:
3775: /**
3776: * @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
3777: * @param array<string, ExpressionTypeHolder> $ourExpressionTypes
3778: * @param array<string, ExpressionTypeHolder> $theirExpressionTypes
3779: * @param array<string, ExpressionTypeHolder> $mergedExpressionTypes
3780: * @return array<string, ConditionalExpressionHolder[]>
3781: */
3782: private function createConditionalExpressions(
3783: array $conditionalExpressions,
3784: array $ourExpressionTypes,
3785: array $theirExpressionTypes,
3786: array $mergedExpressionTypes,
3787: ): array
3788: {
3789: $newVariableTypes = $ourExpressionTypes;
3790:
3791: // When our-branch type is a subtype of their-branch type, the union
3792: // absorbs it (merged === their). Such a variable is a poor *guard* —
3793: // asserting its our-branch type later wouldn't reliably select this
3794: // branch — but it remains a valid conditional *target*, so only exclude
3795: // it from guard selection instead of dropping it entirely.
3796: $guardsToExclude = [];
3797: foreach ($theirExpressionTypes as $exprString => $holder) {
3798: if (!array_key_exists($exprString, $mergedExpressionTypes)) {
3799: continue;
3800: }
3801:
3802: if (!$mergedExpressionTypes[$exprString]->equalTypes($holder)) {
3803: continue;
3804: }
3805:
3806: if (
3807: array_key_exists($exprString, $newVariableTypes)
3808: && !$newVariableTypes[$exprString]->getCertainty()->equals($holder->getCertainty())
3809: && $newVariableTypes[$exprString]->equalTypes($holder)
3810: ) {
3811: continue;
3812: }
3813:
3814: $guardsToExclude[$exprString] = true;
3815: }
3816:
3817: $typeGuards = [];
3818: foreach ($newVariableTypes as $exprString => $holder) {
3819: if ($holder->getExpr() instanceof VirtualNode) {
3820: continue;
3821: }
3822: if (!array_key_exists($exprString, $mergedExpressionTypes)) {
3823: continue;
3824: }
3825: if (!$holder->getCertainty()->yes()) {
3826: continue;
3827: }
3828: if (array_key_exists($exprString, $guardsToExclude)) {
3829: continue;
3830: }
3831:
3832: if (
3833: array_key_exists($exprString, $theirExpressionTypes)
3834: && !$theirExpressionTypes[$exprString]->getCertainty()->yes()
3835: ) {
3836: continue;
3837: }
3838:
3839: if ($mergedExpressionTypes[$exprString]->equalTypes($holder)) {
3840: continue;
3841: }
3842:
3843: $typeGuards[$exprString] = $holder;
3844: }
3845:
3846: if (count($typeGuards) === 0) {
3847: return $conditionalExpressions;
3848: }
3849:
3850: foreach ($newVariableTypes as $exprString => $holder) {
3851: if ($holder->getExpr() instanceof VirtualNode) {
3852: continue;
3853: }
3854: if (
3855: array_key_exists($exprString, $mergedExpressionTypes)
3856: && $mergedExpressionTypes[$exprString]->equals($holder)
3857: ) {
3858: continue;
3859: }
3860:
3861: $variableTypeGuards = $typeGuards;
3862: unset($variableTypeGuards[$exprString]);
3863:
3864: if (count($variableTypeGuards) === 0) {
3865: continue;
3866: }
3867:
3868: foreach ($variableTypeGuards as $guardExprString => $guardHolder) {
3869: if (
3870: array_key_exists($guardExprString, $theirExpressionTypes)
3871: && $theirExpressionTypes[$guardExprString]->getCertainty()->yes()
3872: ) {
3873: $guardIsSuperTypeOfTheirExpr = $guardHolder->getType()->isSuperTypeOf($theirExpressionTypes[$guardExprString]->getType());
3874: $theirExprIsSuperTypeOfGuard = $theirExpressionTypes[$guardExprString]->getType()->isSuperTypeOf($guardHolder->getType());
3875:
3876: if (
3877: $guardIsSuperTypeOfTheirExpr->yes()
3878: || $theirExprIsSuperTypeOfGuard->yes()
3879: || (
3880: array_key_exists($exprString, $theirExpressionTypes)
3881: && $theirExpressionTypes[$exprString]->getCertainty()->yes()
3882: && !$guardIsSuperTypeOfTheirExpr->no()
3883: )
3884: || (
3885: !array_key_exists($exprString, $theirExpressionTypes)
3886: && $holder->getType()->equals($guardHolder->getType())
3887: && !$guardIsSuperTypeOfTheirExpr->no()
3888: )
3889: ) {
3890: continue;
3891: }
3892: }
3893:
3894: $conditionalExpression = new ConditionalExpressionHolder([$guardExprString => $guardHolder], $holder);
3895: $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression;
3896: }
3897: }
3898:
3899: foreach ($mergedExpressionTypes as $exprString => $mergedExprTypeHolder) {
3900: if (array_key_exists($exprString, $ourExpressionTypes)) {
3901: continue;
3902: }
3903:
3904: foreach ($typeGuards as $guardExprString => $guardHolder) {
3905: $conditionalExpression = new ConditionalExpressionHolder([$guardExprString => $guardHolder], new ExpressionTypeHolder($mergedExprTypeHolder->getExpr(), new ErrorType(), TrinaryLogic::createNo()));
3906: $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression;
3907: }
3908: }
3909:
3910: return $conditionalExpressions;
3911: }
3912:
3913: /**
3914: * @param array<string, ExpressionTypeHolder> $ourVariableTypeHolders
3915: * @param array<string, ExpressionTypeHolder> $theirVariableTypeHolders
3916: * @return array<string, ExpressionTypeHolder>
3917: */
3918: private function mergeVariableHolders(array $ourVariableTypeHolders, array $theirVariableTypeHolders): array
3919: {
3920: $intersectedVariableTypeHolders = [];
3921: $globalVariableCallback = fn (Node $node) => $node instanceof Variable && is_string($node->name) && $this->isGlobalVariable($node->name);
3922: $nodeFinder = new NodeFinder();
3923: foreach ($ourVariableTypeHolders as $exprString => $variableTypeHolder) {
3924: if (isset($theirVariableTypeHolders[$exprString])) {
3925: if ($variableTypeHolder === $theirVariableTypeHolders[$exprString]) {
3926: $intersectedVariableTypeHolders[$exprString] = $variableTypeHolder;
3927: continue;
3928: }
3929:
3930: $intersectedVariableTypeHolders[$exprString] = $variableTypeHolder->and($theirVariableTypeHolders[$exprString]);
3931: } else {
3932: $expr = $variableTypeHolder->getExpr();
3933:
3934: $containsSuperGlobal = $expr->getAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME);
3935: if ($containsSuperGlobal === null) {
3936: $containsSuperGlobal = $nodeFinder->findFirst($expr, $globalVariableCallback) !== null;
3937: $expr->setAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME, $containsSuperGlobal);
3938: }
3939: if ($containsSuperGlobal === true) {
3940: continue;
3941: }
3942:
3943: $intersectedVariableTypeHolders[$exprString] = ExpressionTypeHolder::createMaybe($expr, $variableTypeHolder->getType());
3944: }
3945: }
3946:
3947: foreach ($theirVariableTypeHolders as $exprString => $variableTypeHolder) {
3948: if (isset($intersectedVariableTypeHolders[$exprString])) {
3949: continue;
3950: }
3951:
3952: $expr = $variableTypeHolder->getExpr();
3953:
3954: $containsSuperGlobal = $expr->getAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME);
3955: if ($containsSuperGlobal === null) {
3956: $containsSuperGlobal = $nodeFinder->findFirst($expr, $globalVariableCallback) !== null;
3957: $expr->setAttribute(self::CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME, $containsSuperGlobal);
3958: }
3959: if ($containsSuperGlobal === true) {
3960: continue;
3961: }
3962:
3963: $intersectedVariableTypeHolders[$exprString] = ExpressionTypeHolder::createMaybe($expr, $variableTypeHolder->getType());
3964: }
3965:
3966: return $intersectedVariableTypeHolders;
3967: }
3968:
3969: public function mergeInitializedProperties(self $calledMethodScope): self
3970: {
3971: $scope = $this;
3972: foreach ($calledMethodScope->expressionTypes as $exprString => $typeHolder) {
3973: $exprString = (string) $exprString;
3974: if (!str_starts_with($exprString, '__phpstanPropertyInitialization(')) {
3975: continue;
3976: }
3977: $propertyName = substr($exprString, strlen('__phpstanPropertyInitialization('), -1);
3978: $propertyExpr = new PropertyInitializationExpr($propertyName);
3979: if (!array_key_exists($exprString, $scope->expressionTypes)) {
3980: $scope = $scope->assignExpression($propertyExpr, new MixedType(), new MixedType());
3981: $scope->expressionTypes[$exprString] = $typeHolder;
3982: continue;
3983: }
3984:
3985: $certainty = $scope->expressionTypes[$exprString]->getCertainty();
3986: $scope = $scope->assignExpression($propertyExpr, new MixedType(), new MixedType());
3987: $scope->expressionTypes[$exprString] = new ExpressionTypeHolder(
3988: $typeHolder->getExpr(),
3989: $typeHolder->getType(),
3990: $typeHolder->getCertainty()->or($certainty),
3991: );
3992: }
3993:
3994: return $scope;
3995: }
3996:
3997: public function processFinallyScope(self $finallyScope, self $originalFinallyScope): self
3998: {
3999: return $this->scopeFactory->create(
4000: $this->context,
4001: $this->isDeclareStrictTypes(),
4002: $this->getFunction(),
4003: $this->getNamespace(),
4004: $this->processFinallyScopeVariableTypeHolders(
4005: $this->expressionTypes,
4006: $finallyScope->expressionTypes,
4007: $originalFinallyScope->expressionTypes,
4008: ),
4009: $this->processFinallyScopeVariableTypeHolders(
4010: $this->nativeExpressionTypes,
4011: $finallyScope->nativeExpressionTypes,
4012: $originalFinallyScope->nativeExpressionTypes,
4013: ),
4014: $this->intersectConditionalExpressions($finallyScope->conditionalExpressions),
4015: $this->inClosureBindScopeClasses,
4016: $this->anonymousFunctionReflection,
4017: $this->inFirstLevelStatement,
4018: [],
4019: [],
4020: [],
4021: $this->afterExtractCall,
4022: $this->parentScope,
4023: $this->nativeTypesPromoted,
4024: );
4025: }
4026:
4027: /**
4028: * @param array<string, ExpressionTypeHolder> $ourVariableTypeHolders
4029: * @param array<string, ExpressionTypeHolder> $finallyVariableTypeHolders
4030: * @param array<string, ExpressionTypeHolder> $originalVariableTypeHolders
4031: * @return array<string, ExpressionTypeHolder>
4032: */
4033: private function processFinallyScopeVariableTypeHolders(
4034: array $ourVariableTypeHolders,
4035: array $finallyVariableTypeHolders,
4036: array $originalVariableTypeHolders,
4037: ): array
4038: {
4039: foreach ($finallyVariableTypeHolders as $exprString => $variableTypeHolder) {
4040: if (
4041: isset($originalVariableTypeHolders[$exprString])
4042: && !$originalVariableTypeHolders[$exprString]->equalTypes($variableTypeHolder)
4043: ) {
4044: $ourVariableTypeHolders[$exprString] = $variableTypeHolder;
4045: continue;
4046: }
4047:
4048: if (isset($originalVariableTypeHolders[$exprString])) {
4049: continue;
4050: }
4051:
4052: $ourVariableTypeHolders[$exprString] = $variableTypeHolder;
4053: }
4054:
4055: return $ourVariableTypeHolders;
4056: }
4057:
4058: /**
4059: * @param Node\ClosureUse[] $byRefUses
4060: */
4061: public function processClosureScope(
4062: self $closureScope,
4063: ?self $prevScope,
4064: array $byRefUses,
4065: ): self
4066: {
4067: $nativeExpressionTypes = $this->nativeExpressionTypes;
4068: $expressionTypes = $this->expressionTypes;
4069: if (count($byRefUses) === 0) {
4070: return $this;
4071: }
4072:
4073: foreach ($byRefUses as $use) {
4074: if (!is_string($use->var->name)) {
4075: throw new ShouldNotHappenException();
4076: }
4077:
4078: $variableName = $use->var->name;
4079: $variableExprString = '$' . $variableName;
4080:
4081: if (!$closureScope->hasVariableType($variableName)->yes()) {
4082: $holder = ExpressionTypeHolder::createYes($use->var, new NullType());
4083: $expressionTypes[$variableExprString] = $holder;
4084: $nativeExpressionTypes[$variableExprString] = $holder;
4085: continue;
4086: }
4087:
4088: $variableType = $closureScope->getVariableType($variableName);
4089:
4090: if ($prevScope !== null) {
4091: $prevVariableType = $prevScope->getVariableType($variableName);
4092: if (!$variableType->equals($prevVariableType)) {
4093: $variableType = TypeCombinator::union($variableType, $prevVariableType);
4094: $variableType = $this->generalizeType($variableType, $prevVariableType, 0);
4095: }
4096: }
4097:
4098: $holder = ExpressionTypeHolder::createYes($use->var, $variableType);
4099: $expressionTypes[$variableExprString] = $holder;
4100: $nativeExpressionTypes[$variableExprString] = $holder;
4101: }
4102:
4103: return $this->scopeFactory->create(
4104: $this->context,
4105: $this->isDeclareStrictTypes(),
4106: $this->getFunction(),
4107: $this->getNamespace(),
4108: $expressionTypes,
4109: $nativeExpressionTypes,
4110: $this->conditionalExpressions,
4111: $this->inClosureBindScopeClasses,
4112: $this->anonymousFunctionReflection,
4113: $this->inFirstLevelStatement,
4114: [],
4115: [],
4116: $this->inFunctionCallsStack,
4117: $this->afterExtractCall,
4118: $this->parentScope,
4119: $this->nativeTypesPromoted,
4120: );
4121: }
4122:
4123: public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope): self
4124: {
4125: $expressionTypes = $this->expressionTypes;
4126: foreach ($finalScope->expressionTypes as $variableExprString => $variableTypeHolder) {
4127: if (!isset($expressionTypes[$variableExprString])) {
4128: $expressionTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType());
4129: continue;
4130: }
4131:
4132: $expressionTypes[$variableExprString] = new ExpressionTypeHolder(
4133: $variableTypeHolder->getExpr(),
4134: $variableTypeHolder->getType(),
4135: $variableTypeHolder->getCertainty()->and($expressionTypes[$variableExprString]->getCertainty()),
4136: );
4137: }
4138: $nativeTypes = $this->nativeExpressionTypes;
4139: foreach ($finalScope->nativeExpressionTypes as $variableExprString => $variableTypeHolder) {
4140: if (!isset($nativeTypes[$variableExprString])) {
4141: $nativeTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType());
4142: continue;
4143: }
4144:
4145: $nativeTypes[$variableExprString] = new ExpressionTypeHolder(
4146: $variableTypeHolder->getExpr(),
4147: $variableTypeHolder->getType(),
4148: $variableTypeHolder->getCertainty()->and($nativeTypes[$variableExprString]->getCertainty()),
4149: );
4150: }
4151:
4152: return $this->scopeFactory->create(
4153: $this->context,
4154: $this->isDeclareStrictTypes(),
4155: $this->getFunction(),
4156: $this->getNamespace(),
4157: $expressionTypes,
4158: $nativeTypes,
4159: $this->intersectConditionalExpressions($finalScope->conditionalExpressions),
4160: $this->inClosureBindScopeClasses,
4161: $this->anonymousFunctionReflection,
4162: $this->inFirstLevelStatement,
4163: [],
4164: [],
4165: [],
4166: $this->afterExtractCall,
4167: $this->parentScope,
4168: $this->nativeTypesPromoted,
4169: );
4170: }
4171:
4172: public function generalizeWith(self $otherScope): self
4173: {
4174: $variableTypeHolders = $this->generalizeVariableTypeHolders(
4175: $this->expressionTypes,
4176: $otherScope->expressionTypes,
4177: );
4178: $nativeTypes = $this->generalizeVariableTypeHolders(
4179: $this->nativeExpressionTypes,
4180: $otherScope->nativeExpressionTypes,
4181: );
4182:
4183: return $this->scopeFactory->create(
4184: $this->context,
4185: $this->isDeclareStrictTypes(),
4186: $this->getFunction(),
4187: $this->getNamespace(),
4188: $variableTypeHolders,
4189: $nativeTypes,
4190: $this->conditionalExpressions,
4191: $this->inClosureBindScopeClasses,
4192: $this->anonymousFunctionReflection,
4193: $this->inFirstLevelStatement,
4194: [],
4195: [],
4196: [],
4197: $this->afterExtractCall,
4198: $this->parentScope,
4199: $this->nativeTypesPromoted,
4200: );
4201: }
4202:
4203: /**
4204: * @param array<string, ExpressionTypeHolder> $variableTypeHolders
4205: * @param array<string, ExpressionTypeHolder> $otherVariableTypeHolders
4206: * @return array<string, ExpressionTypeHolder>
4207: */
4208: private function generalizeVariableTypeHolders(
4209: array $variableTypeHolders,
4210: array $otherVariableTypeHolders,
4211: ): array
4212: {
4213: uksort($variableTypeHolders, static fn (string $exprA, string $exprB): int => strlen($exprA) <=> strlen($exprB));
4214:
4215: $generalizedExpressions = [];
4216: $newVariableTypeHolders = [];
4217: foreach ($variableTypeHolders as $variableExprString => $variableTypeHolder) {
4218: foreach ($generalizedExpressions as $generalizedExprString => $generalizedExpr) {
4219: if (!$this->shouldInvalidateExpression($generalizedExprString, $generalizedExpr, $variableTypeHolder->getExpr(), $variableExprString)) {
4220: continue;
4221: }
4222:
4223: continue 2;
4224: }
4225: if (!isset($otherVariableTypeHolders[$variableExprString])) {
4226: $newVariableTypeHolders[$variableExprString] = $variableTypeHolder;
4227: continue;
4228: }
4229:
4230: $generalizedType = $this->generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$variableExprString]->getType(), 0);
4231: if (
4232: !$generalizedType->equals($variableTypeHolder->getType())
4233: ) {
4234: $generalizedExpressions[$variableExprString] = $variableTypeHolder->getExpr();
4235: }
4236: $newVariableTypeHolders[$variableExprString] = new ExpressionTypeHolder(
4237: $variableTypeHolder->getExpr(),
4238: $generalizedType,
4239: $variableTypeHolder->getCertainty(),
4240: );
4241: }
4242:
4243: return $newVariableTypeHolders;
4244: }
4245:
4246: private function generalizeType(Type $a, Type $b, int $depth): Type
4247: {
4248: if ($a->equals($b)) {
4249: return $a;
4250: }
4251:
4252: // Track whether either input carries a BenevolentUnion so the result
4253: // can be re-wrapped at the end. `flattenTypes` below drops the
4254: // BenevolentUnion wrapper, which would silently downgrade e.g.
4255: // `(float|int)` (numeric-accepting) to a strict `float|int`. Inside a
4256: // loop's fixed-point this propagates into the iterable value type of
4257: // an array and turns `return [..., $int]` checks into false positives
4258: // when the iteration body's `+ 1` arithmetic was originally produced
4259: // by an `ErrorType`-derived `int|float` benevolent union (the typical
4260: // case for reads of literally-missing keys inside the body).
4261: $wrapBenevolent = $a instanceof BenevolentUnionType || $b instanceof BenevolentUnionType;
4262:
4263: $constantIntegers = ['a' => [], 'b' => []];
4264: $constantFloats = ['a' => [], 'b' => []];
4265: $constantBooleans = ['a' => [], 'b' => []];
4266: $constantStrings = ['a' => [], 'b' => []];
4267: $constantArrays = ['a' => [], 'b' => []];
4268: $generalArrays = ['a' => [], 'b' => []];
4269: $integerRanges = ['a' => [], 'b' => []];
4270: $otherTypes = [];
4271:
4272: foreach ([
4273: 'a' => TypeUtils::flattenTypes($a),
4274: 'b' => TypeUtils::flattenTypes($b),
4275: ] as $key => $types) {
4276: foreach ($types as $type) {
4277: if ($type instanceof ConstantIntegerType) {
4278: $constantIntegers[$key][] = $type;
4279: continue;
4280: }
4281: if ($type instanceof ConstantFloatType) {
4282: $constantFloats[$key][] = $type;
4283: continue;
4284: }
4285: if ($type instanceof ConstantBooleanType) {
4286: $constantBooleans[$key][] = $type;
4287: continue;
4288: }
4289: if ($type instanceof ConstantStringType) {
4290: $constantStrings[$key][] = $type;
4291: continue;
4292: }
4293: if ($type->isConstantArray()->yes()) {
4294: $constantArrays[$key][] = $type;
4295: continue;
4296: }
4297: if ($type->isArray()->yes()) {
4298: $generalArrays[$key][] = $type;
4299: continue;
4300: }
4301: if ($type instanceof IntegerRangeType) {
4302: $integerRanges[$key][] = $type;
4303: continue;
4304: }
4305:
4306: $otherTypes[] = $type;
4307: }
4308: }
4309:
4310: $resultTypes = [];
4311: foreach ([
4312: $constantFloats,
4313: $constantBooleans,
4314: $constantStrings,
4315: ] as $constantTypes) {
4316: if (count($constantTypes['a']) === 0) {
4317: if (count($constantTypes['b']) > 0) {
4318: $resultTypes[] = TypeCombinator::union(...$constantTypes['b']);
4319: }
4320: continue;
4321: } elseif (count($constantTypes['b']) === 0) {
4322: $resultTypes[] = TypeCombinator::union(...$constantTypes['a']);
4323: continue;
4324: }
4325:
4326: $aTypes = TypeCombinator::union(...$constantTypes['a']);
4327: $bTypes = TypeCombinator::union(...$constantTypes['b']);
4328: if ($aTypes->equals($bTypes)) {
4329: $resultTypes[] = $aTypes;
4330: continue;
4331: }
4332:
4333: $resultTypes[] = TypeCombinator::union(...$constantTypes['a'], ...$constantTypes['b'])->generalize(GeneralizePrecision::moreSpecific());
4334: }
4335:
4336: if (count($constantArrays['a']) > 0) {
4337: if (count($constantArrays['b']) === 0) {
4338: $resultTypes[] = TypeCombinator::union(...$constantArrays['a']);
4339: } else {
4340: $constantArraysA = TypeCombinator::union(...$constantArrays['a']);
4341: $constantArraysB = TypeCombinator::union(...$constantArrays['b']);
4342: if (
4343: $constantArraysA->getIterableKeyType()->equals($constantArraysB->getIterableKeyType())
4344: && $constantArraysA->getArraySize()->getGreaterOrEqualType($this->phpVersion)->isSuperTypeOf($constantArraysB->getArraySize())->yes()
4345: ) {
4346: $resultArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
4347: foreach (TypeUtils::flattenTypes($constantArraysA->getIterableKeyType()) as $keyType) {
4348: $resultArrayBuilder->setOffsetValueType(
4349: $keyType,
4350: $this->generalizeType(
4351: $constantArraysA->getOffsetValueType($keyType),
4352: $constantArraysB->getOffsetValueType($keyType),
4353: $depth + 1,
4354: ),
4355: !$constantArraysA->hasOffsetValueType($keyType)->and($constantArraysB->hasOffsetValueType($keyType))->negate()->no(),
4356: );
4357: }
4358:
4359: $resultTypes[] = $resultArrayBuilder->getArray();
4360: } else {
4361: // Both inputs are sealed constant array shapes — their key
4362: // sets are finite by construction. On the fall-through
4363: // ArrayType path, recursing into `generalizeType` would
4364: // widen e.g. `0|1` to `int<0, max>` — for both the keys and
4365: // the values — losing the loop's per-iteration precision.
4366: // Keep the literal union instead so the loop's bounds stay
4367: // visible. (Scoped to sealed shapes so the general
4368: // `generalize()` widening contract for legacy arrays — see
4369: // ScopeTest::testGeneralize — is unaffected.)
4370: $bothSealed = true;
4371: foreach ([...$constantArrays['a'], ...$constantArrays['b']] as $constantArrayCheck) {
4372: foreach ($constantArrayCheck->getConstantArrays() as $constantArrayInstance) {
4373: if (!$constantArrayInstance->isSealed()->yes()) {
4374: $bothSealed = false;
4375: break 2;
4376: }
4377: }
4378: }
4379: if ($bothSealed) {
4380: $resultKeyType = TypeCombinator::union($constantArraysA->getIterableKeyType(), $constantArraysB->getIterableKeyType());
4381: $resultValueType = TypeCombinator::union($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType());
4382: if ($resultValueType->isOversizedArray()->yes()) {
4383: // The literal value union outgrew the shape limit (a
4384: // deeply/widely nested value): fall back to generalizing
4385: // it into a bounded range-keyed array rather than
4386: // keeping an oversized literal shape.
4387: $resultValueType = TypeCombinator::union($this->generalizeType($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType(), $depth + 1));
4388: }
4389: } else {
4390: $resultKeyType = TypeCombinator::union($this->generalizeType($constantArraysA->getIterableKeyType(), $constantArraysB->getIterableKeyType(), $depth + 1));
4391: $resultValueType = TypeCombinator::union($this->generalizeType($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType(), $depth + 1));
4392: }
4393: $resultType = new ArrayType(
4394: $resultKeyType,
4395: $resultValueType,
4396: );
4397: $accessories = [];
4398: if (
4399: $constantArraysA->isIterableAtLeastOnce()->yes()
4400: && $constantArraysB->isIterableAtLeastOnce()->yes()
4401: && $constantArraysA->getArraySize()->getGreaterOrEqualType($this->phpVersion)->isSuperTypeOf($constantArraysB->getArraySize())->yes()
4402: ) {
4403: $accessories[] = new NonEmptyArrayType();
4404: }
4405: if ($constantArraysA->isList()->yes() && $constantArraysB->isList()->yes()) {
4406: $accessories[] = new AccessoryArrayListType();
4407: }
4408:
4409: if (count($accessories) === 0) {
4410: $resultTypes[] = $resultType;
4411: } else {
4412: $resultTypes[] = TypeCombinator::intersect($resultType, ...$accessories);
4413: }
4414: }
4415: }
4416: } elseif (count($constantArrays['b']) > 0) {
4417: $resultTypes[] = TypeCombinator::union(...$constantArrays['b']);
4418: }
4419:
4420: if (count($generalArrays['a']) > 0) {
4421: if (count($generalArrays['b']) === 0) {
4422: $resultTypes[] = TypeCombinator::union(...$generalArrays['a']);
4423: } else {
4424: $generalArraysA = TypeCombinator::union(...$generalArrays['a']);
4425: $generalArraysB = TypeCombinator::union(...$generalArrays['b']);
4426:
4427: $aValueType = $generalArraysA->getIterableValueType();
4428: $bValueType = $generalArraysB->getIterableValueType();
4429: if (
4430: $aValueType->isArray()->yes()
4431: && $aValueType->isConstantArray()->no()
4432: && $bValueType->isArray()->yes()
4433: && $bValueType->isConstantArray()->no()
4434: ) {
4435: $aDepth = self::getArrayDepth($aValueType) + $depth;
4436: $bDepth = self::getArrayDepth($bValueType) + $depth;
4437: if (
4438: ($aDepth > 2 || $bDepth > 2)
4439: && abs($aDepth - $bDepth) > 0
4440: ) {
4441: $aValueType = new MixedType();
4442: $bValueType = new MixedType();
4443: }
4444: }
4445:
4446: $resultType = new ArrayType(
4447: TypeCombinator::union($this->generalizeType($generalArraysA->getIterableKeyType(), $generalArraysB->getIterableKeyType(), $depth + 1)),
4448: TypeCombinator::union($this->generalizeType($aValueType, $bValueType, $depth + 1)),
4449: );
4450:
4451: $accessories = [];
4452: if ($generalArraysA->isIterableAtLeastOnce()->yes() && $generalArraysB->isIterableAtLeastOnce()->yes()) {
4453: $accessories[] = new NonEmptyArrayType();
4454: }
4455: if ($generalArraysA->isList()->yes() && $generalArraysB->isList()->yes()) {
4456: $accessories[] = new AccessoryArrayListType();
4457: }
4458: if ($generalArraysA->isOversizedArray()->yes() && $generalArraysB->isOversizedArray()->yes()) {
4459: $accessories[] = new OversizedArrayType();
4460: }
4461:
4462: if (count($accessories) === 0) {
4463: $resultTypes[] = $resultType;
4464: } else {
4465: $resultTypes[] = TypeCombinator::intersect($resultType, ...$accessories);
4466: }
4467: }
4468: } elseif (count($generalArrays['b']) > 0) {
4469: $resultTypes[] = TypeCombinator::union(...$generalArrays['b']);
4470: }
4471:
4472: if (count($constantIntegers['a']) > 0) {
4473: if (count($constantIntegers['b']) === 0) {
4474: $resultTypes[] = TypeCombinator::union(...$constantIntegers['a']);
4475: } else {
4476: $constantIntegersA = TypeCombinator::union(...$constantIntegers['a']);
4477: $constantIntegersB = TypeCombinator::union(...$constantIntegers['b']);
4478:
4479: if ($constantIntegersA->equals($constantIntegersB)) {
4480: $resultTypes[] = $constantIntegersA;
4481: } else {
4482: $min = null;
4483: $max = null;
4484: foreach ($constantIntegers['a'] as $int) {
4485: if ($min === null || $int->getValue() < $min) {
4486: $min = $int->getValue();
4487: }
4488: if ($max !== null && $int->getValue() <= $max) {
4489: continue;
4490: }
4491:
4492: $max = $int->getValue();
4493: }
4494:
4495: $newMin = $min;
4496: $newMax = $max;
4497: foreach ($constantIntegers['b'] as $int) {
4498: if ($int->getValue() > $newMax) {
4499: $newMax = $int->getValue();
4500: }
4501: if ($int->getValue() >= $newMin) {
4502: continue;
4503: }
4504:
4505: $newMin = $int->getValue();
4506: }
4507:
4508: if ($newMax > $max && $newMin < $min) {
4509: $resultTypes[] = IntegerRangeType::fromInterval($newMin, $newMax);
4510: } elseif ($newMax > $max) {
4511: $resultTypes[] = IntegerRangeType::fromInterval($min, null);
4512: } elseif ($newMin < $min) {
4513: $resultTypes[] = IntegerRangeType::fromInterval(null, $max);
4514: } else {
4515: $resultTypes[] = TypeCombinator::union($constantIntegersA, $constantIntegersB);
4516: }
4517: }
4518: }
4519: } elseif (count($constantIntegers['b']) > 0) {
4520: $resultTypes[] = TypeCombinator::union(...$constantIntegers['b']);
4521: }
4522:
4523: if (count($integerRanges['a']) > 0) {
4524: if (count($integerRanges['b']) === 0) {
4525: $resultTypes[] = TypeCombinator::union(...$integerRanges['a']);
4526: } else {
4527: $integerRangesA = TypeCombinator::union(...$integerRanges['a']);
4528: $integerRangesB = TypeCombinator::union(...$integerRanges['b']);
4529:
4530: if ($integerRangesA->equals($integerRangesB)) {
4531: $resultTypes[] = $integerRangesA;
4532: } else {
4533: $min = null;
4534: $max = null;
4535: foreach ($integerRanges['a'] as $range) {
4536: if ($range->getMin() === null) {
4537: $rangeMin = PHP_INT_MIN;
4538: } else {
4539: $rangeMin = $range->getMin();
4540: }
4541: if ($range->getMax() === null) {
4542: $rangeMax = PHP_INT_MAX;
4543: } else {
4544: $rangeMax = $range->getMax();
4545: }
4546:
4547: if ($min === null || $rangeMin < $min) {
4548: $min = $rangeMin;
4549: }
4550: if ($max !== null && $rangeMax <= $max) {
4551: continue;
4552: }
4553:
4554: $max = $rangeMax;
4555: }
4556:
4557: $newMin = $min;
4558: $newMax = $max;
4559: foreach ($integerRanges['b'] as $range) {
4560: if ($range->getMin() === null) {
4561: $rangeMin = PHP_INT_MIN;
4562: } else {
4563: $rangeMin = $range->getMin();
4564: }
4565: if ($range->getMax() === null) {
4566: $rangeMax = PHP_INT_MAX;
4567: } else {
4568: $rangeMax = $range->getMax();
4569: }
4570:
4571: if ($rangeMax > $newMax) {
4572: $newMax = $rangeMax;
4573: }
4574: if ($rangeMin >= $newMin) {
4575: continue;
4576: }
4577:
4578: $newMin = $rangeMin;
4579: }
4580:
4581: $gotGreater = $newMax > $max;
4582: $gotSmaller = $newMin < $min;
4583:
4584: if ($min === PHP_INT_MIN) {
4585: $min = null;
4586: }
4587: if ($max === PHP_INT_MAX) {
4588: $max = null;
4589: }
4590: if ($newMin === PHP_INT_MIN) {
4591: $newMin = null;
4592: }
4593: if ($newMax === PHP_INT_MAX) {
4594: $newMax = null;
4595: }
4596:
4597: if ($gotGreater && $gotSmaller) {
4598: $resultTypes[] = IntegerRangeType::fromInterval($newMin, $newMax);
4599: } elseif ($gotGreater) {
4600: $resultTypes[] = IntegerRangeType::fromInterval($min, null);
4601: } elseif ($gotSmaller) {
4602: $resultTypes[] = IntegerRangeType::fromInterval(null, $max);
4603: } else {
4604: $resultTypes[] = TypeCombinator::union($integerRangesA, $integerRangesB);
4605: }
4606: }
4607: }
4608: } elseif (count($integerRanges['b']) > 0) {
4609: $resultTypes[] = TypeCombinator::union(...$integerRanges['b']);
4610: }
4611:
4612: $accessoryTypes = array_map(
4613: static fn (Type $type): Type => $type->generalize(GeneralizePrecision::moreSpecific()),
4614: TypeUtils::getAccessoryTypes($a),
4615: );
4616:
4617: $combined = TypeCombinator::union(...$resultTypes, ...$otherTypes);
4618: if ($wrapBenevolent) {
4619: $combined = TypeUtils::toBenevolentUnion($combined);
4620: }
4621:
4622: return TypeCombinator::union(TypeCombinator::intersect(
4623: $combined,
4624: ...$accessoryTypes,
4625: ), ...$otherTypes);
4626: }
4627:
4628: private static function getArrayDepth(Type $type): int
4629: {
4630: $depth = 0;
4631: $arrays = TypeUtils::toBenevolentUnion($type)->getArrays();
4632: while (count($arrays) > 0) {
4633: $temp = $type->getIterableValueType();
4634: $type = $temp;
4635: $arrays = TypeUtils::toBenevolentUnion($type)->getArrays();
4636: $depth++;
4637: }
4638:
4639: return $depth;
4640: }
4641:
4642: public function equals(self $otherScope): bool
4643: {
4644: if (!$this->context->equals($otherScope->context)) {
4645: return false;
4646: }
4647:
4648: if (!$this->compareVariableTypeHolders($this->expressionTypes, $otherScope->expressionTypes)) {
4649: return false;
4650: }
4651: return $this->compareVariableTypeHolders($this->nativeExpressionTypes, $otherScope->nativeExpressionTypes);
4652: }
4653:
4654: /**
4655: * @param array<string, ExpressionTypeHolder> $variableTypeHolders
4656: * @param array<string, ExpressionTypeHolder> $otherVariableTypeHolders
4657: */
4658: private function compareVariableTypeHolders(array $variableTypeHolders, array $otherVariableTypeHolders): bool
4659: {
4660: if (count($variableTypeHolders) !== count($otherVariableTypeHolders)) {
4661: return false;
4662: }
4663: foreach ($variableTypeHolders as $variableExprString => $variableTypeHolder) {
4664: if (!isset($otherVariableTypeHolders[$variableExprString])) {
4665: return false;
4666: }
4667:
4668: if (!$variableTypeHolder->getCertainty()->equals($otherVariableTypeHolders[$variableExprString]->getCertainty())) {
4669: return false;
4670: }
4671:
4672: if (!$variableTypeHolder->equalTypes($otherVariableTypeHolders[$variableExprString])) {
4673: return false;
4674: }
4675: }
4676:
4677: return true;
4678: }
4679:
4680: /**
4681: * @api
4682: * @deprecated Use canReadProperty() or canWriteProperty()
4683: */
4684: public function canAccessProperty(PropertyReflection $propertyReflection): bool
4685: {
4686: return $this->canAccessClassMember($propertyReflection);
4687: }
4688:
4689: /** @api */
4690: public function canReadProperty(ExtendedPropertyReflection $propertyReflection): bool
4691: {
4692: return $this->canAccessClassMember($propertyReflection);
4693: }
4694:
4695: /** @api */
4696: public function canWriteProperty(ExtendedPropertyReflection $propertyReflection): bool
4697: {
4698: if (!$propertyReflection->isPrivateSet() && !$propertyReflection->isProtectedSet()) {
4699: return $this->canAccessClassMember($propertyReflection);
4700: }
4701:
4702: if (!$this->phpVersion->supportsAsymmetricVisibility()) {
4703: return $this->canAccessClassMember($propertyReflection);
4704: }
4705:
4706: $propertyDeclaringClass = $propertyReflection->getDeclaringClass();
4707: $canAccessClassMember = static function (ClassReflection $classReflection) use ($propertyReflection, $propertyDeclaringClass) {
4708: if ($propertyReflection->isPrivateSet()) {
4709: return $classReflection->getName() === $propertyDeclaringClass->getName();
4710: }
4711:
4712: // protected set
4713:
4714: if (
4715: $classReflection->getName() === $propertyDeclaringClass->getName()
4716: || $classReflection->isSubclassOfClass($propertyDeclaringClass->removeFinalKeywordOverride())
4717: ) {
4718: return true;
4719: }
4720:
4721: return $propertyReflection->getDeclaringClass()->isSubclassOfClass($classReflection);
4722: };
4723:
4724: foreach ($this->inClosureBindScopeClasses as $inClosureBindScopeClass) {
4725: if (!$this->reflectionProvider->hasClass($inClosureBindScopeClass)) {
4726: continue;
4727: }
4728:
4729: if ($canAccessClassMember($this->reflectionProvider->getClass($inClosureBindScopeClass))) {
4730: return true;
4731: }
4732: }
4733:
4734: if ($this->isInClass()) {
4735: return $canAccessClassMember($this->getClassReflection());
4736: }
4737:
4738: return false;
4739: }
4740:
4741: /** @api */
4742: public function canCallMethod(MethodReflection $methodReflection): bool
4743: {
4744: if ($this->canAccessClassMember($methodReflection)) {
4745: return true;
4746: }
4747:
4748: return $this->canAccessClassMember($methodReflection->getPrototype());
4749: }
4750:
4751: /** @api */
4752: public function canAccessConstant(ClassConstantReflection $constantReflection): bool
4753: {
4754: return $this->canAccessClassMember($constantReflection);
4755: }
4756:
4757: private function canAccessClassMember(ClassMemberReflection $classMemberReflection): bool
4758: {
4759: if ($classMemberReflection->isPublic()) {
4760: return true;
4761: }
4762:
4763: $classMemberDeclaringClass = $classMemberReflection->getDeclaringClass();
4764: $canAccessClassMember = static function (ClassReflection $classReflection) use ($classMemberReflection, $classMemberDeclaringClass) {
4765: if ($classMemberReflection->isPrivate()) {
4766: return $classReflection->getName() === $classMemberDeclaringClass->getName();
4767: }
4768:
4769: // protected
4770:
4771: if (
4772: $classReflection->getName() === $classMemberDeclaringClass->getName()
4773: || $classReflection->isSubclassOfClass($classMemberDeclaringClass->removeFinalKeywordOverride())
4774: ) {
4775: return true;
4776: }
4777:
4778: return $classMemberReflection->getDeclaringClass()->isSubclassOfClass($classReflection);
4779: };
4780:
4781: foreach ($this->inClosureBindScopeClasses as $inClosureBindScopeClass) {
4782: if (!$this->reflectionProvider->hasClass($inClosureBindScopeClass)) {
4783: continue;
4784: }
4785:
4786: if ($canAccessClassMember($this->reflectionProvider->getClass($inClosureBindScopeClass))) {
4787: return true;
4788: }
4789: }
4790:
4791: if ($this->isInClass()) {
4792: return $canAccessClassMember($this->getClassReflection());
4793: }
4794:
4795: return false;
4796: }
4797:
4798: /**
4799: * @return string[]
4800: */
4801: public function debug(): array
4802: {
4803: $descriptions = [];
4804: foreach ($this->expressionTypes as $name => $variableTypeHolder) {
4805: $key = sprintf('%s (%s)', $name, $variableTypeHolder->getCertainty()->describe());
4806: $descriptions[$key] = $variableTypeHolder->getType()->describe(VerbosityLevel::precise());
4807: }
4808: foreach ($this->nativeExpressionTypes as $exprString => $nativeTypeHolder) {
4809: $key = sprintf('native %s (%s)', $exprString, $nativeTypeHolder->getCertainty()->describe());
4810: $descriptions[$key] = $nativeTypeHolder->getType()->describe(VerbosityLevel::precise());
4811: }
4812:
4813: foreach (array_keys($this->currentlyAssignedExpressions) as $exprString) {
4814: $descriptions[sprintf('currently assigned %s', $exprString)] = 'true';
4815: }
4816:
4817: foreach (array_keys($this->currentlyAllowedUndefinedExpressions) as $exprString) {
4818: $descriptions[sprintf('currently allowed undefined %s', $exprString)] = 'true';
4819: }
4820:
4821: foreach ($this->conditionalExpressions as $exprString => $holders) {
4822: foreach (array_values($holders) as $i => $holder) {
4823: $key = sprintf('condition about %s #%d', $exprString, $i + 1);
4824: $parts = [];
4825: foreach ($holder->getConditionExpressionTypeHolders() as $conditionalExprString => $expressionTypeHolder) {
4826: $parts[] = $conditionalExprString . '=' . $expressionTypeHolder->getType()->describe(VerbosityLevel::precise());
4827: }
4828: $condition = implode(' && ', $parts);
4829: $descriptions[$key] = sprintf(
4830: 'if %s then %s is %s (%s)',
4831: $condition,
4832: $exprString,
4833: $holder->getTypeHolder()->getType()->describe(VerbosityLevel::precise()),
4834: $holder->getTypeHolder()->getCertainty()->describe(),
4835: );
4836: }
4837: }
4838:
4839: return $descriptions;
4840: }
4841:
4842: public function filterTypeWithMethod(Type $typeWithMethod, string $methodName): ?Type
4843: {
4844: if ($typeWithMethod instanceof UnionType) {
4845: $typeWithMethod = $typeWithMethod->filterTypes(static fn (Type $innerType) => $innerType->hasMethod($methodName)->yes());
4846: }
4847:
4848: if (!$typeWithMethod->hasMethod($methodName)->yes()) {
4849: return null;
4850: }
4851:
4852: return $typeWithMethod;
4853: }
4854:
4855: /** @api */
4856: public function getMethodReflection(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection
4857: {
4858: $type = $this->filterTypeWithMethod($typeWithMethod, $methodName);
4859: if ($type === null) {
4860: return null;
4861: }
4862:
4863: return $type->getMethod($methodName, $this);
4864: }
4865:
4866: public function getNakedMethod(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection
4867: {
4868: $type = $this->filterTypeWithMethod($typeWithMethod, $methodName);
4869: if ($type === null) {
4870: return null;
4871: }
4872:
4873: return $type->getUnresolvedMethodPrototype($methodName, $this)->getNakedMethod();
4874: }
4875:
4876: /**
4877: * @api
4878: * @deprecated Use getInstancePropertyReflection or getStaticPropertyReflection instead
4879: */
4880: public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection
4881: {
4882: if ($typeWithProperty instanceof UnionType) {
4883: $typeWithProperty = $typeWithProperty->filterTypes(static fn (Type $innerType) => $innerType->hasProperty($propertyName)->yes());
4884: }
4885: if (!$typeWithProperty->hasProperty($propertyName)->yes()) {
4886: return null;
4887: }
4888:
4889: return $typeWithProperty->getProperty($propertyName, $this);
4890: }
4891:
4892: /** @api */
4893: public function getInstancePropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection
4894: {
4895: if ($typeWithProperty instanceof UnionType) {
4896: $typeWithProperty = $typeWithProperty->filterTypes(static fn (Type $innerType) => $innerType->hasInstanceProperty($propertyName)->yes());
4897: }
4898: if (!$typeWithProperty->hasInstanceProperty($propertyName)->yes()) {
4899: return null;
4900: }
4901:
4902: return $typeWithProperty->getInstanceProperty($propertyName, $this);
4903: }
4904:
4905: /** @api */
4906: public function getStaticPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection
4907: {
4908: if ($typeWithProperty instanceof UnionType) {
4909: $typeWithProperty = $typeWithProperty->filterTypes(static fn (Type $innerType) => $innerType->hasStaticProperty($propertyName)->yes());
4910: }
4911: if (!$typeWithProperty->hasStaticProperty($propertyName)->yes()) {
4912: return null;
4913: }
4914:
4915: return $typeWithProperty->getStaticProperty($propertyName, $this);
4916: }
4917:
4918: public function getConstantReflection(Type $typeWithConstant, string $constantName): ?ClassConstantReflection
4919: {
4920: if ($typeWithConstant instanceof UnionType) {
4921: $typeWithConstant = $typeWithConstant->filterTypes(static fn (Type $innerType) => $innerType->hasConstant($constantName)->yes());
4922: }
4923: if (!$typeWithConstant->hasConstant($constantName)->yes()) {
4924: return null;
4925: }
4926:
4927: return $typeWithConstant->getConstant($constantName);
4928: }
4929:
4930: public function getConstantExplicitTypeFromConfig(string $constantName, Type $constantType): Type
4931: {
4932: return $this->constantResolver->resolveConstantType($constantName, $constantType);
4933: }
4934:
4935: /**
4936: * @return array<string, ExpressionTypeHolder>
4937: */
4938: private function getConstantTypes(): array
4939: {
4940: $constantTypes = [];
4941: foreach ($this->expressionTypes as $exprString => $typeHolder) {
4942: $expr = $typeHolder->getExpr();
4943: if (!$expr instanceof ConstFetch) {
4944: continue;
4945: }
4946: $constantTypes[$exprString] = $typeHolder;
4947: }
4948: return $constantTypes;
4949: }
4950:
4951: private function getGlobalConstantType(Name $name): ?Type
4952: {
4953: $fetches = [];
4954: if (!$name->isFullyQualified() && $this->getNamespace() !== null) {
4955: $fetches[] = new ConstFetch(new FullyQualified([$this->getNamespace(), $name->toString()]));
4956: }
4957:
4958: $fetches[] = new ConstFetch(new FullyQualified($name->toString()));
4959: $fetches[] = new ConstFetch($name);
4960:
4961: foreach ($fetches as $constFetch) {
4962: if ($this->hasExpressionType($constFetch)->yes()) {
4963: return $this->getType($constFetch);
4964: }
4965: }
4966:
4967: return null;
4968: }
4969:
4970: /**
4971: * @return array<string, ExpressionTypeHolder>
4972: */
4973: private function getNativeConstantTypes(): array
4974: {
4975: $constantTypes = [];
4976: foreach ($this->nativeExpressionTypes as $exprString => $typeHolder) {
4977: $expr = $typeHolder->getExpr();
4978: if (!$expr instanceof ConstFetch) {
4979: continue;
4980: }
4981: $constantTypes[$exprString] = $typeHolder;
4982: }
4983: return $constantTypes;
4984: }
4985:
4986: public function getIterableKeyType(Type $iteratee): Type
4987: {
4988: if ($iteratee instanceof UnionType) {
4989: $filtered = $iteratee->filterTypes(static fn (Type $innerType) => $innerType->isIterable()->yes());
4990: if (!$filtered instanceof NeverType) {
4991: $iteratee = $filtered;
4992: }
4993: }
4994:
4995: return $iteratee->getIterableKeyType();
4996: }
4997:
4998: public function getIterableValueType(Type $iteratee): Type
4999: {
5000: if ($iteratee instanceof UnionType) {
5001: $filtered = $iteratee->filterTypes(static fn (Type $innerType) => $innerType->isIterable()->yes());
5002: if (!$filtered instanceof NeverType) {
5003: $iteratee = $filtered;
5004: }
5005: }
5006:
5007: return $iteratee->getIterableValueType();
5008: }
5009:
5010: public function getPhpVersion(): PhpVersions
5011: {
5012: $constType = $this->getGlobalConstantType(new Name('PHP_VERSION_ID'));
5013:
5014: $isOverallPhpVersionRange = false;
5015: if (
5016: $constType instanceof IntegerRangeType
5017: && $constType->getMin() === ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID
5018: && ($constType->getMax() === null || $constType->getMax() === PhpVersionFactory::MAX_PHP_VERSION)
5019: ) {
5020: $isOverallPhpVersionRange = true;
5021: }
5022:
5023: if ($constType !== null && !$isOverallPhpVersionRange) {
5024: return new PhpVersions($constType);
5025: }
5026:
5027: if (is_array($this->configPhpVersion)) {
5028: return new PhpVersions(IntegerRangeType::fromInterval($this->configPhpVersion['min'], $this->configPhpVersion['max']));
5029: }
5030: return new PhpVersions(new ConstantIntegerType($this->phpVersion->getVersionId()));
5031: }
5032:
5033: public function invokeNodeCallback(Node $node): void
5034: {
5035: $nodeCallback = $this->nodeCallback;
5036: if ($nodeCallback === null) {
5037: throw new ShouldNotHappenException('Node callback is not present in this scope');
5038: }
5039:
5040: $nodeCallback($node, $this);
5041: }
5042:
5043: /**
5044: * @template TNodeType of Node
5045: * @template TValue
5046: * @param class-string<Collector<TNodeType, TValue>> $collectorType
5047: * @param TValue $data
5048: */
5049: public function emitCollectedData(string $collectorType, mixed $data): void
5050: {
5051: $nodeCallback = $this->nodeCallback;
5052: if ($nodeCallback === null) {
5053: throw new ShouldNotHappenException('Node callback is not present in this scope');
5054: }
5055:
5056: $nodeCallback(new EmitCollectedDataNode($collectorType, $data), $this);
5057: }
5058:
5059: }
5060: