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