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