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