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