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