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