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