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