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