1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use ArrayAccess;
6: use Closure;
7: use Generator;
8: use PhpParser\Node;
9: use PhpParser\Node\Arg;
10: use PhpParser\Node\ComplexType;
11: use PhpParser\Node\Expr;
12: use PhpParser\Node\Expr\Array_;
13: use PhpParser\Node\Expr\BinaryOp;
14: use PhpParser\Node\Expr\Cast\Bool_;
15: use PhpParser\Node\Expr\Cast\Double;
16: use PhpParser\Node\Expr\Cast\Int_;
17: use PhpParser\Node\Expr\Cast\Object_;
18: use PhpParser\Node\Expr\Cast\Unset_;
19: use PhpParser\Node\Expr\ConstFetch;
20: use PhpParser\Node\Expr\FuncCall;
21: use PhpParser\Node\Expr\MethodCall;
22: use PhpParser\Node\Expr\New_;
23: use PhpParser\Node\Expr\PropertyFetch;
24: use PhpParser\Node\Expr\Variable;
25: use PhpParser\Node\Identifier;
26: use PhpParser\Node\InterpolatedStringPart;
27: use PhpParser\Node\Name;
28: use PhpParser\Node\Name\FullyQualified;
29: use PhpParser\Node\PropertyHook;
30: use PhpParser\Node\Scalar\String_;
31: use PhpParser\Node\Stmt\ClassMethod;
32: use PhpParser\Node\Stmt\Function_;
33: use PhpParser\NodeFinder;
34: use PHPStan\Node\ExecutionEndNode;
35: use PHPStan\Node\Expr\AlwaysRememberedExpr;
36: use PHPStan\Node\Expr\ExistingArrayDimFetch;
37: use PHPStan\Node\Expr\GetIterableKeyTypeExpr;
38: use PHPStan\Node\Expr\GetIterableValueTypeExpr;
39: use PHPStan\Node\Expr\GetOffsetValueTypeExpr;
40: use PHPStan\Node\Expr\NativeTypeExpr;
41: use PHPStan\Node\Expr\OriginalPropertyTypeExpr;
42: use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr;
43: use PHPStan\Node\Expr\PropertyInitializationExpr;
44: use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr;
45: use PHPStan\Node\Expr\SetOffsetValueTypeExpr;
46: use PHPStan\Node\Expr\TypeExpr;
47: use PHPStan\Node\Expr\UnsetOffsetExpr;
48: use PHPStan\Node\InvalidateExprNode;
49: use PHPStan\Node\IssetExpr;
50: use PHPStan\Node\Printer\ExprPrinter;
51: use PHPStan\Node\PropertyAssignNode;
52: use PHPStan\Parser\ArrayMapArgVisitor;
53: use PHPStan\Parser\NewAssignedToPropertyVisitor;
54: use PHPStan\Parser\Parser;
55: use PHPStan\Php\PhpVersion;
56: use PHPStan\Php\PhpVersionFactory;
57: use PHPStan\Php\PhpVersions;
58: use PHPStan\PhpDoc\Tag\TemplateTag;
59: use PHPStan\Reflection\Assertions;
60: use PHPStan\Reflection\AttributeReflection;
61: use PHPStan\Reflection\AttributeReflectionFactory;
62: use PHPStan\Reflection\Callables\CallableParametersAcceptor;
63: use PHPStan\Reflection\Callables\SimpleImpurePoint;
64: use PHPStan\Reflection\Callables\SimpleThrowPoint;
65: use PHPStan\Reflection\ClassConstantReflection;
66: use PHPStan\Reflection\ClassMemberReflection;
67: use PHPStan\Reflection\ClassReflection;
68: use PHPStan\Reflection\Dummy\DummyConstructorReflection;
69: use PHPStan\Reflection\ExtendedMethodReflection;
70: use PHPStan\Reflection\ExtendedParametersAcceptor;
71: use PHPStan\Reflection\ExtendedPropertyReflection;
72: use PHPStan\Reflection\FunctionReflection;
73: use PHPStan\Reflection\InitializerExprContext;
74: use PHPStan\Reflection\InitializerExprTypeResolver;
75: use PHPStan\Reflection\MethodReflection;
76: use PHPStan\Reflection\Native\NativeParameterReflection;
77: use PHPStan\Reflection\ParameterReflection;
78: use PHPStan\Reflection\ParametersAcceptor;
79: use PHPStan\Reflection\ParametersAcceptorSelector;
80: use PHPStan\Reflection\PassedByReference;
81: use PHPStan\Reflection\Php\DummyParameter;
82: use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection;
83: use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
84: use PHPStan\Reflection\PropertyReflection;
85: use PHPStan\Reflection\ReflectionProvider;
86: use PHPStan\Reflection\TrivialParametersAcceptor;
87: use PHPStan\Rules\Properties\PropertyReflectionFinder;
88: use PHPStan\ShouldNotHappenException;
89: use PHPStan\TrinaryLogic;
90: use PHPStan\Type\Accessory\AccessoryArrayListType;
91: use PHPStan\Type\Accessory\AccessoryLiteralStringType;
92: use PHPStan\Type\Accessory\HasOffsetValueType;
93: use PHPStan\Type\Accessory\NonEmptyArrayType;
94: use PHPStan\Type\Accessory\OversizedArrayType;
95: use PHPStan\Type\ArrayType;
96: use PHPStan\Type\BenevolentUnionType;
97: use PHPStan\Type\BooleanType;
98: use PHPStan\Type\ClosureType;
99: use PHPStan\Type\ConditionalTypeForParameter;
100: use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
101: use PHPStan\Type\Constant\ConstantBooleanType;
102: use PHPStan\Type\Constant\ConstantFloatType;
103: use PHPStan\Type\Constant\ConstantIntegerType;
104: use PHPStan\Type\Constant\ConstantStringType;
105: use PHPStan\Type\ConstantTypeHelper;
106: use PHPStan\Type\DynamicReturnTypeExtensionRegistry;
107: use PHPStan\Type\ErrorType;
108: use PHPStan\Type\ExpressionTypeResolverExtensionRegistry;
109: use PHPStan\Type\FloatType;
110: use PHPStan\Type\GeneralizePrecision;
111: use PHPStan\Type\Generic\GenericClassStringType;
112: use PHPStan\Type\Generic\GenericObjectType;
113: use PHPStan\Type\Generic\GenericStaticType;
114: use PHPStan\Type\Generic\TemplateType;
115: use PHPStan\Type\Generic\TemplateTypeHelper;
116: use PHPStan\Type\Generic\TemplateTypeMap;
117: use PHPStan\Type\Generic\TemplateTypeVarianceMap;
118: use PHPStan\Type\IntegerRangeType;
119: use PHPStan\Type\IntegerType;
120: use PHPStan\Type\IntersectionType;
121: use PHPStan\Type\MixedType;
122: use PHPStan\Type\NeverType;
123: use PHPStan\Type\NonAcceptingNeverType;
124: use PHPStan\Type\NonexistentParentClassType;
125: use PHPStan\Type\NullType;
126: use PHPStan\Type\ObjectShapeType;
127: use PHPStan\Type\ObjectType;
128: use PHPStan\Type\ParserNodeTypeToPHPStanType;
129: use PHPStan\Type\StaticType;
130: use PHPStan\Type\StringType;
131: use PHPStan\Type\ThisType;
132: use PHPStan\Type\Type;
133: use PHPStan\Type\TypeCombinator;
134: use PHPStan\Type\TypeTraverser;
135: use PHPStan\Type\TypeUtils;
136: use PHPStan\Type\TypeWithClassName;
137: use PHPStan\Type\UnionType;
138: use PHPStan\Type\VerbosityLevel;
139: use PHPStan\Type\VoidType;
140: use stdClass;
141: use Throwable;
142: use ValueError;
143: use function abs;
144: use function array_filter;
145: use function array_key_exists;
146: use function array_key_first;
147: use function array_keys;
148: use function array_map;
149: use function array_merge;
150: use function array_pop;
151: use function array_slice;
152: use function array_values;
153: use function count;
154: use function explode;
155: use function get_class;
156: use function implode;
157: use function in_array;
158: use function is_array;
159: use function is_bool;
160: use function is_numeric;
161: use function is_string;
162: use function ltrim;
163: use function md5;
164: use function sprintf;
165: use function str_decrement;
166: use function str_increment;
167: use function str_starts_with;
168: use function strlen;
169: use function strtolower;
170: use function substr;
171: use function usort;
172: use const PHP_INT_MAX;
173: use const PHP_INT_MIN;
174:
175: final class MutatingScope implements Scope
176: {
177:
178: private const BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH = 4;
179:
180: private const KEEP_VOID_ATTRIBUTE_NAME = 'keepVoid';
181:
182: /** @var Type[] */
183: private array $resolvedTypes = [];
184:
185: /** @var array<string, self> */
186: private array $truthyScopes = [];
187:
188: /** @var array<string, self> */
189: private array $falseyScopes = [];
190:
191: /** @var non-empty-string|null */
192: private ?string $namespace;
193:
194: private ?self $scopeOutOfFirstLevelStatement = null;
195:
196: private ?self $scopeWithPromotedNativeTypes = null;
197:
198: private static int $resolveClosureTypeDepth = 0;
199:
200: /**
201: * @param int|array{min: int, max: int}|null $configPhpVersion
202: * @param array<string, ExpressionTypeHolder> $expressionTypes
203: * @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
204: * @param list<string> $inClosureBindScopeClasses
205: * @param array<string, true> $currentlyAssignedExpressions
206: * @param array<string, true> $currentlyAllowedUndefinedExpressions
207: * @param array<string, ExpressionTypeHolder> $nativeExpressionTypes
208: * @param list<array{MethodReflection|FunctionReflection|null, ParameterReflection|null}> $inFunctionCallsStack
209: */
210: public function __construct(
211: private InternalScopeFactory $scopeFactory,
212: private ReflectionProvider $reflectionProvider,
213: private InitializerExprTypeResolver $initializerExprTypeResolver,
214: private DynamicReturnTypeExtensionRegistry $dynamicReturnTypeExtensionRegistry,
215: private ExpressionTypeResolverExtensionRegistry $expressionTypeResolverExtensionRegistry,
216: private ExprPrinter $exprPrinter,
217: private TypeSpecifier $typeSpecifier,
218: private PropertyReflectionFinder $propertyReflectionFinder,
219: private Parser $parser,
220: private NodeScopeResolver $nodeScopeResolver,
221: private RicherScopeGetTypeHelper $richerScopeGetTypeHelper,
222: private ConstantResolver $constantResolver,
223: private ScopeContext $context,
224: private PhpVersion $phpVersion,
225: private AttributeReflectionFactory $attributeReflectionFactory,
226: private int|array|null $configPhpVersion,
227: private bool $declareStrictTypes = false,
228: private PhpFunctionFromParserNodeReflection|null $function = null,
229: ?string $namespace = null,
230: private array $expressionTypes = [],
231: private array $nativeExpressionTypes = [],
232: private array $conditionalExpressions = [],
233: private array $inClosureBindScopeClasses = [],
234: private ?ParametersAcceptor $anonymousFunctionReflection = null,
235: private bool $inFirstLevelStatement = true,
236: private array $currentlyAssignedExpressions = [],
237: private array $currentlyAllowedUndefinedExpressions = [],
238: private array $inFunctionCallsStack = [],
239: private bool $afterExtractCall = false,
240: private ?Scope $parentScope = null,
241: private bool $nativeTypesPromoted = false,
242: )
243: {
244: if ($namespace === '') {
245: $namespace = null;
246: }
247:
248: $this->namespace = $namespace;
249: }
250:
251: /** @api */
252: public function getFile(): string
253: {
254: return $this->context->getFile();
255: }
256:
257: /** @api */
258: public function getFileDescription(): string
259: {
260: if ($this->context->getTraitReflection() === null) {
261: return $this->getFile();
262: }
263:
264: /** @var ClassReflection $classReflection */
265: $classReflection = $this->context->getClassReflection();
266:
267: $className = $classReflection->getDisplayName();
268: if (!$classReflection->isAnonymous()) {
269: $className = sprintf('class %s', $className);
270: }
271:
272: $traitReflection = $this->context->getTraitReflection();
273: if ($traitReflection->getFileName() === null) {
274: throw new ShouldNotHappenException();
275: }
276:
277: return sprintf(
278: '%s (in context of %s)',
279: $traitReflection->getFileName(),
280: $className,
281: );
282: }
283:
284: /** @api */
285: public function isDeclareStrictTypes(): bool
286: {
287: return $this->declareStrictTypes;
288: }
289:
290: public function enterDeclareStrictTypes(): self
291: {
292: return $this->scopeFactory->create(
293: $this->context,
294: true,
295: null,
296: null,
297: $this->expressionTypes,
298: $this->nativeExpressionTypes,
299: );
300: }
301:
302: /**
303: * @param array<string, ExpressionTypeHolder> $currentExpressionTypes
304: * @return array<string, ExpressionTypeHolder>
305: */
306: private function rememberConstructorExpressions(array $currentExpressionTypes): array
307: {
308: $expressionTypes = [];
309: foreach ($currentExpressionTypes as $exprString => $expressionTypeHolder) {
310: $expr = $expressionTypeHolder->getExpr();
311: if ($expr instanceof FuncCall) {
312: if (
313: !$expr->name instanceof Name
314: || !in_array($expr->name->name, ['class_exists', 'function_exists'], true)
315: ) {
316: continue;
317: }
318: } elseif ($expr instanceof PropertyFetch) {
319: if (!$this->isReadonlyPropertyFetch($expr, true)) {
320: continue;
321: }
322: } elseif (!$expr instanceof ConstFetch && !$expr instanceof PropertyInitializationExpr) {
323: continue;
324: }
325:
326: $expressionTypes[$exprString] = $expressionTypeHolder;
327: }
328:
329: if (array_key_exists('$this', $currentExpressionTypes)) {
330: $expressionTypes['$this'] = $currentExpressionTypes['$this'];
331: }
332:
333: return $expressionTypes;
334: }
335:
336: public function rememberConstructorScope(): self
337: {
338: return $this->scopeFactory->create(
339: $this->context,
340: $this->isDeclareStrictTypes(),
341: null,
342: $this->getNamespace(),
343: $this->rememberConstructorExpressions($this->expressionTypes),
344: $this->rememberConstructorExpressions($this->nativeExpressionTypes),
345: $this->conditionalExpressions,
346: $this->inClosureBindScopeClasses,
347: $this->anonymousFunctionReflection,
348: $this->inFirstLevelStatement,
349: [],
350: [],
351: $this->inFunctionCallsStack,
352: $this->afterExtractCall,
353: $this->parentScope,
354: $this->nativeTypesPromoted,
355: );
356: }
357:
358: private function isReadonlyPropertyFetch(PropertyFetch $expr, bool $allowOnlyOnThis): bool
359: {
360: if (!$this->phpVersion->supportsReadOnlyProperties()) {
361: return false;
362: }
363:
364: while ($expr instanceof PropertyFetch) {
365: if ($expr->var instanceof Variable) {
366: if (
367: $allowOnlyOnThis
368: && (
369: ! $expr->name instanceof Node\Identifier
370: || !is_string($expr->var->name)
371: || $expr->var->name !== 'this'
372: )
373: ) {
374: return false;
375: }
376: } elseif (!$expr->var instanceof PropertyFetch) {
377: return false;
378: }
379:
380: $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this);
381: if ($propertyReflection === null) {
382: return false;
383: }
384:
385: $nativePropertyReflection = $propertyReflection->getNativeReflection();
386: if ($nativePropertyReflection === null || !$nativePropertyReflection->isReadOnly()) {
387: return false;
388: }
389:
390: $expr = $expr->var;
391: }
392:
393: return true;
394: }
395:
396: /** @api */
397: public function isInClass(): bool
398: {
399: return $this->context->getClassReflection() !== null;
400: }
401:
402: /** @api */
403: public function isInTrait(): bool
404: {
405: return $this->context->getTraitReflection() !== null;
406: }
407:
408: /** @api */
409: public function getClassReflection(): ?ClassReflection
410: {
411: return $this->context->getClassReflection();
412: }
413:
414: /** @api */
415: public function getTraitReflection(): ?ClassReflection
416: {
417: return $this->context->getTraitReflection();
418: }
419:
420: /**
421: * @api
422: */
423: public function getFunction(): ?PhpFunctionFromParserNodeReflection
424: {
425: return $this->function;
426: }
427:
428: /** @api */
429: public function getFunctionName(): ?string
430: {
431: return $this->function !== null ? $this->function->getName() : null;
432: }
433:
434: /** @api */
435: public function getNamespace(): ?string
436: {
437: return $this->namespace;
438: }
439:
440: /** @api */
441: public function getParentScope(): ?Scope
442: {
443: return $this->parentScope;
444: }
445:
446: /** @api */
447: public function canAnyVariableExist(): bool
448: {
449: return ($this->function === null && !$this->isInAnonymousFunction()) || $this->afterExtractCall;
450: }
451:
452: public function afterExtractCall(): self
453: {
454: return $this->scopeFactory->create(
455: $this->context,
456: $this->isDeclareStrictTypes(),
457: $this->getFunction(),
458: $this->getNamespace(),
459: $this->expressionTypes,
460: $this->nativeExpressionTypes,
461: [],
462: $this->inClosureBindScopeClasses,
463: $this->anonymousFunctionReflection,
464: $this->isInFirstLevelStatement(),
465: $this->currentlyAssignedExpressions,
466: $this->currentlyAllowedUndefinedExpressions,
467: $this->inFunctionCallsStack,
468: true,
469: $this->parentScope,
470: $this->nativeTypesPromoted,
471: );
472: }
473:
474: public function afterClearstatcacheCall(): self
475: {
476: $expressionTypes = $this->expressionTypes;
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: continue 2;
508: }
509: }
510: return $this->scopeFactory->create(
511: $this->context,
512: $this->isDeclareStrictTypes(),
513: $this->getFunction(),
514: $this->getNamespace(),
515: $expressionTypes,
516: $this->nativeExpressionTypes,
517: $this->conditionalExpressions,
518: $this->inClosureBindScopeClasses,
519: $this->anonymousFunctionReflection,
520: $this->isInFirstLevelStatement(),
521: $this->currentlyAssignedExpressions,
522: $this->currentlyAllowedUndefinedExpressions,
523: $this->inFunctionCallsStack,
524: $this->afterExtractCall,
525: $this->parentScope,
526: $this->nativeTypesPromoted,
527: );
528: }
529:
530: public function afterOpenSslCall(string $openSslFunctionName): self
531: {
532: $expressionTypes = $this->expressionTypes;
533:
534: if (in_array($openSslFunctionName, [
535: 'openssl_cipher_iv_length',
536: 'openssl_cms_decrypt',
537: 'openssl_cms_encrypt',
538: 'openssl_cms_read',
539: 'openssl_cms_sign',
540: 'openssl_cms_verify',
541: 'openssl_csr_export_to_file',
542: 'openssl_csr_export',
543: 'openssl_csr_get_public_key',
544: 'openssl_csr_get_subject',
545: 'openssl_csr_new',
546: 'openssl_csr_sign',
547: 'openssl_decrypt',
548: 'openssl_dh_compute_key',
549: 'openssl_digest',
550: 'openssl_encrypt',
551: 'openssl_get_curve_names',
552: 'openssl_get_privatekey',
553: 'openssl_get_publickey',
554: 'openssl_open',
555: 'openssl_pbkdf2',
556: 'openssl_pkcs12_export_to_file',
557: 'openssl_pkcs12_export',
558: 'openssl_pkcs12_read',
559: 'openssl_pkcs7_decrypt',
560: 'openssl_pkcs7_encrypt',
561: 'openssl_pkcs7_read',
562: 'openssl_pkcs7_sign',
563: 'openssl_pkcs7_verify',
564: 'openssl_pkey_derive',
565: 'openssl_pkey_export_to_file',
566: 'openssl_pkey_export',
567: 'openssl_pkey_get_private',
568: 'openssl_pkey_get_public',
569: 'openssl_pkey_new',
570: 'openssl_private_decrypt',
571: 'openssl_private_encrypt',
572: 'openssl_public_decrypt',
573: 'openssl_public_encrypt',
574: 'openssl_random_pseudo_bytes',
575: 'openssl_seal',
576: 'openssl_sign',
577: 'openssl_spki_export_challenge',
578: 'openssl_spki_export',
579: 'openssl_spki_new',
580: 'openssl_spki_verify',
581: 'openssl_verify',
582: 'openssl_x509_checkpurpose',
583: 'openssl_x509_export_to_file',
584: 'openssl_x509_export',
585: 'openssl_x509_fingerprint',
586: 'openssl_x509_read',
587: 'openssl_x509_verify',
588: ], true)) {
589: unset($expressionTypes['\openssl_error_string()']);
590: }
591:
592: return $this->scopeFactory->create(
593: $this->context,
594: $this->isDeclareStrictTypes(),
595: $this->getFunction(),
596: $this->getNamespace(),
597: $expressionTypes,
598: $this->nativeExpressionTypes,
599: $this->conditionalExpressions,
600: $this->inClosureBindScopeClasses,
601: $this->anonymousFunctionReflection,
602: $this->isInFirstLevelStatement(),
603: $this->currentlyAssignedExpressions,
604: $this->currentlyAllowedUndefinedExpressions,
605: $this->inFunctionCallsStack,
606: $this->afterExtractCall,
607: $this->parentScope,
608: $this->nativeTypesPromoted,
609: );
610: }
611:
612: /** @api */
613: public function hasVariableType(string $variableName): TrinaryLogic
614: {
615: if ($this->isGlobalVariable($variableName)) {
616: return TrinaryLogic::createYes();
617: }
618:
619: $varExprString = '$' . $variableName;
620: if (!isset($this->expressionTypes[$varExprString])) {
621: if ($this->canAnyVariableExist()) {
622: return TrinaryLogic::createMaybe();
623: }
624:
625: return TrinaryLogic::createNo();
626: }
627:
628: return $this->expressionTypes[$varExprString]->getCertainty();
629: }
630:
631: /** @api */
632: public function getVariableType(string $variableName): Type
633: {
634: if ($this->hasVariableType($variableName)->maybe()) {
635: if ($variableName === 'argc') {
636: return IntegerRangeType::fromInterval(1, null);
637: }
638: if ($variableName === 'argv') {
639: return TypeCombinator::intersect(
640: new ArrayType(new IntegerType(), new StringType()),
641: new NonEmptyArrayType(),
642: new AccessoryArrayListType(),
643: );
644: }
645: if ($this->canAnyVariableExist()) {
646: return new MixedType();
647: }
648: }
649:
650: if ($this->hasVariableType($variableName)->no()) {
651: throw new UndefinedVariableException($this, $variableName);
652: }
653:
654: $varExprString = '$' . $variableName;
655: if (!array_key_exists($varExprString, $this->expressionTypes)) {
656: if ($this->isGlobalVariable($variableName)) {
657: return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType(true));
658: }
659: return new MixedType();
660: }
661:
662: return TypeUtils::resolveLateResolvableTypes($this->expressionTypes[$varExprString]->getType());
663: }
664:
665: /**
666: * @api
667: * @return list<string>
668: */
669: public function getDefinedVariables(): array
670: {
671: $variables = [];
672: foreach ($this->expressionTypes as $exprString => $holder) {
673: if (!$holder->getExpr() instanceof Variable) {
674: continue;
675: }
676: if (!$holder->getCertainty()->yes()) {
677: continue;
678: }
679:
680: $variables[] = substr($exprString, 1);
681: }
682:
683: return $variables;
684: }
685:
686: /**
687: * @api
688: * @return list<string>
689: */
690: public function getMaybeDefinedVariables(): array
691: {
692: $variables = [];
693: foreach ($this->expressionTypes as $exprString => $holder) {
694: if (!$holder->getExpr() instanceof Variable) {
695: continue;
696: }
697: if (!$holder->getCertainty()->maybe()) {
698: continue;
699: }
700:
701: $variables[] = substr($exprString, 1);
702: }
703:
704: return $variables;
705: }
706:
707: private function isGlobalVariable(string $variableName): bool
708: {
709: return in_array($variableName, self::SUPERGLOBAL_VARIABLES, true);
710: }
711:
712: /** @api */
713: public function hasConstant(Name $name): bool
714: {
715: $isCompilerHaltOffset = $name->toString() === '__COMPILER_HALT_OFFSET__';
716: if ($isCompilerHaltOffset) {
717: return $this->fileHasCompilerHaltStatementCalls();
718: }
719:
720: if ($this->getGlobalConstantType($name) !== null) {
721: return true;
722: }
723:
724: return $this->reflectionProvider->hasConstant($name, $this);
725: }
726:
727: private function fileHasCompilerHaltStatementCalls(): bool
728: {
729: $nodes = $this->parser->parseFile($this->getFile());
730: foreach ($nodes as $node) {
731: if ($node instanceof Node\Stmt\HaltCompiler) {
732: return true;
733: }
734: }
735:
736: return false;
737: }
738:
739: /** @api */
740: public function isInAnonymousFunction(): bool
741: {
742: return $this->anonymousFunctionReflection !== null;
743: }
744:
745: /** @api */
746: public function getAnonymousFunctionReflection(): ?ParametersAcceptor
747: {
748: return $this->anonymousFunctionReflection;
749: }
750:
751: /** @api */
752: public function getAnonymousFunctionReturnType(): ?Type
753: {
754: if ($this->anonymousFunctionReflection === null) {
755: return null;
756: }
757:
758: return $this->anonymousFunctionReflection->getReturnType();
759: }
760:
761: /** @api */
762: public function getType(Expr $node): Type
763: {
764: if ($node instanceof GetIterableKeyTypeExpr) {
765: return $this->getIterableKeyType($this->getType($node->getExpr()));
766: }
767: if ($node instanceof GetIterableValueTypeExpr) {
768: return $this->getIterableValueType($this->getType($node->getExpr()));
769: }
770: if ($node instanceof GetOffsetValueTypeExpr) {
771: return $this->getType($node->getVar())->getOffsetValueType($this->getType($node->getDim()));
772: }
773: if ($node instanceof ExistingArrayDimFetch) {
774: return $this->getType(new Expr\ArrayDimFetch($node->getVar(), $node->getDim()));
775: }
776: if ($node instanceof UnsetOffsetExpr) {
777: return $this->getType($node->getVar())->unsetOffset($this->getType($node->getDim()));
778: }
779: if ($node instanceof SetOffsetValueTypeExpr) {
780: return $this->getType($node->getVar())->setOffsetValueType(
781: $node->getDim() !== null ? $this->getType($node->getDim()) : null,
782: $this->getType($node->getValue()),
783: );
784: }
785: if ($node instanceof SetExistingOffsetValueTypeExpr) {
786: return $this->getType($node->getVar())->setExistingOffsetValueType(
787: $this->getType($node->getDim()),
788: $this->getType($node->getValue()),
789: );
790: }
791: if ($node instanceof TypeExpr) {
792: return $node->getExprType();
793: }
794: if ($node instanceof NativeTypeExpr) {
795: if ($this->nativeTypesPromoted) {
796: return $node->getNativeType();
797: }
798: return $node->getPhpDocType();
799: }
800:
801: if ($node instanceof OriginalPropertyTypeExpr) {
802: $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node->getPropertyFetch(), $this);
803: if ($propertyReflection === null) {
804: return new ErrorType();
805: }
806:
807: return $propertyReflection->getReadableType();
808: }
809:
810: $key = $this->getNodeKey($node);
811:
812: if (!array_key_exists($key, $this->resolvedTypes)) {
813: $this->resolvedTypes[$key] = TypeUtils::resolveLateResolvableTypes($this->resolveType($key, $node));
814: }
815: return $this->resolvedTypes[$key];
816: }
817:
818: private function getNodeKey(Expr $node): string
819: {
820: $key = $this->exprPrinter->printExpr($node);
821:
822: $attributes = $node->getAttributes();
823: if (
824: $node instanceof Node\FunctionLike
825: && (($attributes[ArrayMapArgVisitor::ATTRIBUTE_NAME] ?? null) !== null)
826: && (($attributes['startFilePos'] ?? null) !== null)
827: ) {
828: $key .= '/*' . $attributes['startFilePos'] . '*/';
829: }
830:
831: if (($attributes[self::KEEP_VOID_ATTRIBUTE_NAME] ?? null) === true) {
832: $key .= '/*' . self::KEEP_VOID_ATTRIBUTE_NAME . '*/';
833: }
834:
835: return $key;
836: }
837:
838: private function getClosureScopeCacheKey(): string
839: {
840: $parts = [];
841: foreach ($this->expressionTypes as $exprString => $expressionTypeHolder) {
842: $parts[] = sprintf('%s::%s', $exprString, $expressionTypeHolder->getType()->describe(VerbosityLevel::cache()));
843: }
844: $parts[] = '---';
845: foreach ($this->nativeExpressionTypes as $exprString => $expressionTypeHolder) {
846: $parts[] = sprintf('%s::%s', $exprString, $expressionTypeHolder->getType()->describe(VerbosityLevel::cache()));
847: }
848:
849: $parts[] = sprintf(':%d', count($this->inFunctionCallsStack));
850: foreach ($this->inFunctionCallsStack as [$method, $parameter]) {
851: if ($parameter === null) {
852: $parts[] = ',null';
853: continue;
854: }
855:
856: $parts[] = sprintf(',%s', $parameter->getType()->describe(VerbosityLevel::cache()));
857: }
858:
859: return md5(implode("\n", $parts));
860: }
861:
862: private function resolveType(string $exprString, Expr $node): Type
863: {
864: foreach ($this->expressionTypeResolverExtensionRegistry->getExtensions() as $extension) {
865: $type = $extension->getType($node, $this);
866: if ($type !== null) {
867: return $type;
868: }
869: }
870:
871: if ($node instanceof Expr\Exit_ || $node instanceof Expr\Throw_) {
872: return new NonAcceptingNeverType();
873: }
874:
875: if (!$node instanceof Variable && $this->hasExpressionType($node)->yes()) {
876: return $this->expressionTypes[$exprString]->getType();
877: }
878:
879: if ($node instanceof AlwaysRememberedExpr) {
880: return $this->nativeTypesPromoted ? $node->getNativeExprType() : $node->getExprType();
881: }
882:
883: if ($node instanceof Expr\BinaryOp\Smaller) {
884: return $this->getType($node->left)->isSmallerThan($this->getType($node->right), $this->phpVersion)->toBooleanType();
885: }
886:
887: if ($node instanceof Expr\BinaryOp\SmallerOrEqual) {
888: return $this->getType($node->left)->isSmallerThanOrEqual($this->getType($node->right), $this->phpVersion)->toBooleanType();
889: }
890:
891: if ($node instanceof Expr\BinaryOp\Greater) {
892: return $this->getType($node->right)->isSmallerThan($this->getType($node->left), $this->phpVersion)->toBooleanType();
893: }
894:
895: if ($node instanceof Expr\BinaryOp\GreaterOrEqual) {
896: return $this->getType($node->right)->isSmallerThanOrEqual($this->getType($node->left), $this->phpVersion)->toBooleanType();
897: }
898:
899: if ($node instanceof Expr\BinaryOp\Equal) {
900: if (
901: $node->left instanceof Variable
902: && is_string($node->left->name)
903: && $node->right instanceof Variable
904: && is_string($node->right->name)
905: && $node->left->name === $node->right->name
906: ) {
907: return new ConstantBooleanType(true);
908: }
909:
910: $leftType = $this->getType($node->left);
911: $rightType = $this->getType($node->right);
912:
913: return $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType)->type;
914: }
915:
916: if ($node instanceof Expr\BinaryOp\NotEqual) {
917: return $this->getType(new Expr\BooleanNot(new BinaryOp\Equal($node->left, $node->right)));
918: }
919:
920: if ($node instanceof Expr\Empty_) {
921: $result = $this->issetCheck($node->expr, static function (Type $type): ?bool {
922: $isNull = $type->isNull();
923: $isFalsey = $type->toBoolean()->isFalse();
924: if ($isNull->maybe()) {
925: return null;
926: }
927: if ($isFalsey->maybe()) {
928: return null;
929: }
930:
931: if ($isNull->yes()) {
932: return $isFalsey->no();
933: }
934:
935: return !$isFalsey->yes();
936: });
937: if ($result === null) {
938: return new BooleanType();
939: }
940:
941: return new ConstantBooleanType(!$result);
942: }
943:
944: if ($node instanceof Node\Expr\BooleanNot) {
945: $exprBooleanType = $this->getType($node->expr)->toBoolean();
946: if ($exprBooleanType instanceof ConstantBooleanType) {
947: return new ConstantBooleanType(!$exprBooleanType->getValue());
948: }
949:
950: return new BooleanType();
951: }
952:
953: if ($node instanceof Node\Expr\BitwiseNot) {
954: return $this->initializerExprTypeResolver->getBitwiseNotType($node->expr, fn (Expr $expr): Type => $this->getType($expr));
955: }
956:
957: if (
958: $node instanceof Node\Expr\BinaryOp\BooleanAnd
959: || $node instanceof Node\Expr\BinaryOp\LogicalAnd
960: ) {
961: $leftBooleanType = $this->getType($node->left)->toBoolean();
962: if ($leftBooleanType->isFalse()->yes()) {
963: return new ConstantBooleanType(false);
964: }
965:
966: if ($this->getBooleanExpressionDepth($node->left) <= self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) {
967: $noopCallback = static function (): void {
968: };
969: $leftResult = $this->nodeScopeResolver->processExprNode(new Node\Stmt\Expression($node->left), $node->left, $this, $noopCallback, ExpressionContext::createDeep());
970: $rightBooleanType = $leftResult->getTruthyScope()->getType($node->right)->toBoolean();
971: } else {
972: $rightBooleanType = $this->filterByTruthyValue($node->left)->getType($node->right)->toBoolean();
973: }
974:
975: if ($rightBooleanType->isFalse()->yes()) {
976: return new ConstantBooleanType(false);
977: }
978:
979: if (
980: $leftBooleanType->isTrue()->yes()
981: && $rightBooleanType->isTrue()->yes()
982: ) {
983: return new ConstantBooleanType(true);
984: }
985:
986: return new BooleanType();
987: }
988:
989: if (
990: $node instanceof Node\Expr\BinaryOp\BooleanOr
991: || $node instanceof Node\Expr\BinaryOp\LogicalOr
992: ) {
993: $leftBooleanType = $this->getType($node->left)->toBoolean();
994: if ($leftBooleanType->isTrue()->yes()) {
995: return new ConstantBooleanType(true);
996: }
997:
998: if ($this->getBooleanExpressionDepth($node->left) <= self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH) {
999: $noopCallback = static function (): void {
1000: };
1001: $leftResult = $this->nodeScopeResolver->processExprNode(new Node\Stmt\Expression($node->left), $node->left, $this, $noopCallback, ExpressionContext::createDeep());
1002: $rightBooleanType = $leftResult->getFalseyScope()->getType($node->right)->toBoolean();
1003: } else {
1004: $rightBooleanType = $this->filterByFalseyValue($node->left)->getType($node->right)->toBoolean();
1005: }
1006:
1007: if ($rightBooleanType->isTrue()->yes()) {
1008: return new ConstantBooleanType(true);
1009: }
1010:
1011: if (
1012: $leftBooleanType->isFalse()->yes()
1013: && $rightBooleanType->isFalse()->yes()
1014: ) {
1015: return new ConstantBooleanType(false);
1016: }
1017:
1018: return new BooleanType();
1019: }
1020:
1021: if ($node instanceof Node\Expr\BinaryOp\LogicalXor) {
1022: $leftBooleanType = $this->getType($node->left)->toBoolean();
1023: $rightBooleanType = $this->getType($node->right)->toBoolean();
1024:
1025: if (
1026: $leftBooleanType instanceof ConstantBooleanType
1027: && $rightBooleanType instanceof ConstantBooleanType
1028: ) {
1029: return new ConstantBooleanType(
1030: $leftBooleanType->getValue() xor $rightBooleanType->getValue(),
1031: );
1032: }
1033:
1034: return new BooleanType();
1035: }
1036:
1037: if ($node instanceof Expr\BinaryOp\Identical) {
1038: return $this->richerScopeGetTypeHelper->getIdenticalResult($this, $node)->type;
1039: }
1040:
1041: if ($node instanceof Expr\BinaryOp\NotIdentical) {
1042: return $this->richerScopeGetTypeHelper->getNotIdenticalResult($this, $node)->type;
1043: }
1044:
1045: if ($node instanceof Expr\Instanceof_) {
1046: $expressionType = $this->getType($node->expr);
1047: if (
1048: $this->isInTrait()
1049: && TypeUtils::findThisType($expressionType) !== null
1050: ) {
1051: return new BooleanType();
1052: }
1053: if ($expressionType instanceof NeverType) {
1054: return new ConstantBooleanType(false);
1055: }
1056:
1057: $uncertainty = false;
1058:
1059: if ($node->class instanceof Node\Name) {
1060: $unresolvedClassName = $node->class->toString();
1061: if (
1062: strtolower($unresolvedClassName) === 'static'
1063: && $this->isInClass()
1064: ) {
1065: $classType = new StaticType($this->getClassReflection());
1066: } else {
1067: $className = $this->resolveName($node->class);
1068: $classType = new ObjectType($className);
1069: }
1070: } else {
1071: $classType = $this->getType($node->class);
1072: $classType = TypeTraverser::map($classType, static function (Type $type, callable $traverse) use (&$uncertainty): Type {
1073: if ($type instanceof UnionType || $type instanceof IntersectionType) {
1074: return $traverse($type);
1075: }
1076: if ($type->getObjectClassNames() !== []) {
1077: $uncertainty = true;
1078: return $type;
1079: }
1080: if ($type instanceof GenericClassStringType) {
1081: $uncertainty = true;
1082: return $type->getGenericType();
1083: }
1084: if ($type instanceof ConstantStringType) {
1085: return new ObjectType($type->getValue());
1086: }
1087: return new MixedType();
1088: });
1089: }
1090:
1091: if ($classType->isSuperTypeOf(new MixedType())->yes()) {
1092: return new BooleanType();
1093: }
1094:
1095: $isSuperType = $classType->isSuperTypeOf($expressionType);
1096:
1097: if ($isSuperType->no()) {
1098: return new ConstantBooleanType(false);
1099: } elseif ($isSuperType->yes() && !$uncertainty) {
1100: return new ConstantBooleanType(true);
1101: }
1102:
1103: return new BooleanType();
1104: }
1105:
1106: if ($node instanceof Node\Expr\UnaryPlus) {
1107: return $this->getType($node->expr)->toNumber();
1108: }
1109:
1110: if ($node instanceof Expr\ErrorSuppress
1111: || $node instanceof Expr\Assign
1112: ) {
1113: return $this->getType($node->expr);
1114: }
1115:
1116: if ($node instanceof Node\Expr\UnaryMinus) {
1117: return $this->initializerExprTypeResolver->getUnaryMinusType($node->expr, fn (Expr $expr): Type => $this->getType($expr));
1118: }
1119:
1120: if ($node instanceof Expr\BinaryOp\Concat) {
1121: return $this->initializerExprTypeResolver->getConcatType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr));
1122: }
1123:
1124: if ($node instanceof Expr\AssignOp\Concat) {
1125: return $this->initializerExprTypeResolver->getConcatType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr));
1126: }
1127:
1128: if ($node instanceof BinaryOp\BitwiseAnd) {
1129: return $this->initializerExprTypeResolver->getBitwiseAndType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr));
1130: }
1131:
1132: if ($node instanceof Expr\AssignOp\BitwiseAnd) {
1133: return $this->initializerExprTypeResolver->getBitwiseAndType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr));
1134: }
1135:
1136: if ($node instanceof BinaryOp\BitwiseOr) {
1137: return $this->initializerExprTypeResolver->getBitwiseOrType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr));
1138: }
1139:
1140: if ($node instanceof Expr\AssignOp\BitwiseOr) {
1141: return $this->initializerExprTypeResolver->getBitwiseOrType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr));
1142: }
1143:
1144: if ($node instanceof BinaryOp\BitwiseXor) {
1145: return $this->initializerExprTypeResolver->getBitwiseXorType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr));
1146: }
1147:
1148: if ($node instanceof Expr\AssignOp\BitwiseXor) {
1149: return $this->initializerExprTypeResolver->getBitwiseXorType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr));
1150: }
1151:
1152: if ($node instanceof Expr\BinaryOp\Spaceship) {
1153: return $this->initializerExprTypeResolver->getSpaceshipType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr));
1154: }
1155:
1156: if ($node instanceof BinaryOp\Div) {
1157: return $this->initializerExprTypeResolver->getDivType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr));
1158: }
1159:
1160: if ($node instanceof Expr\AssignOp\Div) {
1161: return $this->initializerExprTypeResolver->getDivType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr));
1162: }
1163:
1164: if ($node instanceof BinaryOp\Mod) {
1165: return $this->initializerExprTypeResolver->getModType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr));
1166: }
1167:
1168: if ($node instanceof Expr\AssignOp\Mod) {
1169: return $this->initializerExprTypeResolver->getModType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr));
1170: }
1171:
1172: if ($node instanceof BinaryOp\Plus) {
1173: return $this->initializerExprTypeResolver->getPlusType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr));
1174: }
1175:
1176: if ($node instanceof Expr\AssignOp\Plus) {
1177: return $this->initializerExprTypeResolver->getPlusType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr));
1178: }
1179:
1180: if ($node instanceof BinaryOp\Minus) {
1181: return $this->initializerExprTypeResolver->getMinusType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr));
1182: }
1183:
1184: if ($node instanceof Expr\AssignOp\Minus) {
1185: return $this->initializerExprTypeResolver->getMinusType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr));
1186: }
1187:
1188: if ($node instanceof BinaryOp\Mul) {
1189: return $this->initializerExprTypeResolver->getMulType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr));
1190: }
1191:
1192: if ($node instanceof Expr\AssignOp\Mul) {
1193: return $this->initializerExprTypeResolver->getMulType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr));
1194: }
1195:
1196: if ($node instanceof BinaryOp\Pow) {
1197: return $this->initializerExprTypeResolver->getPowType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr));
1198: }
1199:
1200: if ($node instanceof Expr\AssignOp\Pow) {
1201: return $this->initializerExprTypeResolver->getPowType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr));
1202: }
1203:
1204: if ($node instanceof BinaryOp\ShiftLeft) {
1205: return $this->initializerExprTypeResolver->getShiftLeftType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr));
1206: }
1207:
1208: if ($node instanceof Expr\AssignOp\ShiftLeft) {
1209: return $this->initializerExprTypeResolver->getShiftLeftType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr));
1210: }
1211:
1212: if ($node instanceof BinaryOp\ShiftRight) {
1213: return $this->initializerExprTypeResolver->getShiftRightType($node->left, $node->right, fn (Expr $expr): Type => $this->getType($expr));
1214: }
1215:
1216: if ($node instanceof Expr\AssignOp\ShiftRight) {
1217: return $this->initializerExprTypeResolver->getShiftRightType($node->var, $node->expr, fn (Expr $expr): Type => $this->getType($expr));
1218: }
1219:
1220: if ($node instanceof Expr\Clone_) {
1221: return $this->getType($node->expr);
1222: }
1223:
1224: if ($node instanceof Node\Scalar\Int_) {
1225: return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this));
1226: } elseif ($node instanceof String_) {
1227: return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this));
1228: } elseif ($node instanceof Node\Scalar\InterpolatedString) {
1229: $resultType = null;
1230: foreach ($node->parts as $part) {
1231: if ($part instanceof InterpolatedStringPart) {
1232: $partType = new ConstantStringType($part->value);
1233: } else {
1234: $partType = $this->getType($part)->toString();
1235: }
1236: if ($resultType === null) {
1237: $resultType = $partType;
1238: continue;
1239: }
1240:
1241: $resultType = $this->initializerExprTypeResolver->resolveConcatType($resultType, $partType);
1242: }
1243:
1244: return $resultType ?? new ConstantStringType('');
1245: } elseif ($node instanceof Node\Scalar\Float_) {
1246: return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this));
1247: } elseif ($node instanceof Expr\CallLike && $node->isFirstClassCallable()) {
1248: if ($node instanceof FuncCall) {
1249: if ($node->name instanceof Name) {
1250: if ($this->reflectionProvider->hasFunction($node->name, $this)) {
1251: $function = $this->reflectionProvider->getFunction($node->name, $this);
1252: return $this->createFirstClassCallable(
1253: $function,
1254: $function->getVariants(),
1255: );
1256: }
1257:
1258: return new ObjectType(Closure::class);
1259: }
1260:
1261: $callableType = $this->getType($node->name);
1262: if (!$callableType->isCallable()->yes()) {
1263: return new ObjectType(Closure::class);
1264: }
1265:
1266: return $this->createFirstClassCallable(
1267: null,
1268: $callableType->getCallableParametersAcceptors($this),
1269: );
1270: }
1271:
1272: if ($node instanceof MethodCall) {
1273: if (!$node->name instanceof Node\Identifier) {
1274: return new ObjectType(Closure::class);
1275: }
1276:
1277: $varType = $this->getType($node->var);
1278: $method = $this->getMethodReflection($varType, $node->name->toString());
1279: if ($method === null) {
1280: return new ObjectType(Closure::class);
1281: }
1282:
1283: return $this->createFirstClassCallable(
1284: $method,
1285: $method->getVariants(),
1286: );
1287: }
1288:
1289: if ($node instanceof Expr\StaticCall) {
1290: if (!$node->class instanceof Name) {
1291: return new ObjectType(Closure::class);
1292: }
1293:
1294: if (!$node->name instanceof Node\Identifier) {
1295: return new ObjectType(Closure::class);
1296: }
1297:
1298: $classType = $this->resolveTypeByNameWithLateStaticBinding($node->class, $node->name);
1299: $methodName = $node->name->toString();
1300: if (!$classType->hasMethod($methodName)->yes()) {
1301: return new ObjectType(Closure::class);
1302: }
1303:
1304: $method = $classType->getMethod($methodName, $this);
1305: return $this->createFirstClassCallable(
1306: $method,
1307: $method->getVariants(),
1308: );
1309: }
1310:
1311: if ($node instanceof New_) {
1312: return new ErrorType();
1313: }
1314:
1315: throw new ShouldNotHappenException();
1316: } elseif ($node instanceof Expr\Closure || $node instanceof Expr\ArrowFunction) {
1317: $parameters = [];
1318: $isVariadic = false;
1319: $firstOptionalParameterIndex = null;
1320: foreach ($node->params as $i => $param) {
1321: $isOptionalCandidate = $param->default !== null || $param->variadic;
1322:
1323: if ($isOptionalCandidate) {
1324: if ($firstOptionalParameterIndex === null) {
1325: $firstOptionalParameterIndex = $i;
1326: }
1327: } else {
1328: $firstOptionalParameterIndex = null;
1329: }
1330: }
1331:
1332: foreach ($node->params as $i => $param) {
1333: if ($param->variadic) {
1334: $isVariadic = true;
1335: }
1336: if (!$param->var instanceof Variable || !is_string($param->var->name)) {
1337: throw new ShouldNotHappenException();
1338: }
1339: $parameters[] = new NativeParameterReflection(
1340: $param->var->name,
1341: $firstOptionalParameterIndex !== null && $i >= $firstOptionalParameterIndex,
1342: $this->getFunctionType($param->type, $this->isParameterValueNullable($param), false),
1343: $param->byRef
1344: ? PassedByReference::createCreatesNewVariable()
1345: : PassedByReference::createNo(),
1346: $param->variadic,
1347: $param->default !== null ? $this->getType($param->default) : null,
1348: );
1349: }
1350:
1351: $callableParameters = null;
1352: $arrayMapArgs = $node->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME);
1353: if ($arrayMapArgs !== null) {
1354: $callableParameters = [];
1355: foreach ($arrayMapArgs as $funcCallArg) {
1356: $callableParameters[] = new DummyParameter('item', $this->getType($funcCallArg->value)->getIterableValueType(), false, PassedByReference::createNo(), false, null);
1357: }
1358: } else {
1359: $inFunctionCallsStackCount = count($this->inFunctionCallsStack);
1360: if ($inFunctionCallsStackCount > 0) {
1361: [, $inParameter] = $this->inFunctionCallsStack[$inFunctionCallsStackCount - 1];
1362: if ($inParameter !== null) {
1363: $callableParameters = $this->nodeScopeResolver->createCallableParameters($this, $node, null, $inParameter->getType());
1364: }
1365: }
1366: }
1367:
1368: if ($node instanceof Expr\ArrowFunction) {
1369: $arrowScope = $this->enterArrowFunctionWithoutReflection($node, $callableParameters);
1370:
1371: if ($node->expr instanceof Expr\Yield_ || $node->expr instanceof Expr\YieldFrom) {
1372: $yieldNode = $node->expr;
1373:
1374: if ($yieldNode instanceof Expr\Yield_) {
1375: if ($yieldNode->key === null) {
1376: $keyType = new IntegerType();
1377: } else {
1378: $keyType = $arrowScope->getType($yieldNode->key);
1379: }
1380:
1381: if ($yieldNode->value === null) {
1382: $valueType = new NullType();
1383: } else {
1384: $valueType = $arrowScope->getType($yieldNode->value);
1385: }
1386: } else {
1387: $yieldFromType = $arrowScope->getType($yieldNode->expr);
1388: $keyType = $arrowScope->getIterableKeyType($yieldFromType);
1389: $valueType = $arrowScope->getIterableValueType($yieldFromType);
1390: }
1391:
1392: $returnType = new GenericObjectType(Generator::class, [
1393: $keyType,
1394: $valueType,
1395: new MixedType(),
1396: new VoidType(),
1397: ]);
1398: } else {
1399: $returnType = $arrowScope->getKeepVoidType($node->expr);
1400: if ($node->returnType !== null) {
1401: $nativeReturnType = $this->getFunctionType($node->returnType, false, false);
1402: $returnType = self::intersectButNotNever($nativeReturnType, $returnType);
1403: }
1404: }
1405:
1406: $arrowFunctionImpurePoints = [];
1407: $invalidateExpressions = [];
1408: $arrowFunctionExprResult = $this->nodeScopeResolver->processExprNode(
1409: new Node\Stmt\Expression($node->expr),
1410: $node->expr,
1411: $arrowScope,
1412: static function (Node $node, Scope $scope) use ($arrowScope, &$arrowFunctionImpurePoints, &$invalidateExpressions): void {
1413: if ($scope->getAnonymousFunctionReflection() !== $arrowScope->getAnonymousFunctionReflection()) {
1414: return;
1415: }
1416:
1417: if ($node instanceof InvalidateExprNode) {
1418: $invalidateExpressions[] = $node;
1419: return;
1420: }
1421:
1422: if (!$node instanceof PropertyAssignNode) {
1423: return;
1424: }
1425:
1426: $arrowFunctionImpurePoints[] = new ImpurePoint(
1427: $scope,
1428: $node,
1429: 'propertyAssign',
1430: 'property assignment',
1431: true,
1432: );
1433: },
1434: ExpressionContext::createDeep(),
1435: );
1436: $throwPoints = $arrowFunctionExprResult->getThrowPoints();
1437: $impurePoints = array_merge($arrowFunctionImpurePoints, $arrowFunctionExprResult->getImpurePoints());
1438: $usedVariables = [];
1439: } else {
1440: $cachedTypes = $node->getAttribute('phpstanCachedTypes', []);
1441: $cacheKey = $this->getClosureScopeCacheKey();
1442: if (array_key_exists($cacheKey, $cachedTypes)) {
1443: $cachedClosureData = $cachedTypes[$cacheKey];
1444:
1445: return new ClosureType(
1446: $parameters,
1447: $cachedClosureData['returnType'],
1448: $isVariadic,
1449: TemplateTypeMap::createEmpty(),
1450: TemplateTypeMap::createEmpty(),
1451: TemplateTypeVarianceMap::createEmpty(),
1452: throwPoints: $cachedClosureData['throwPoints'],
1453: impurePoints: $cachedClosureData['impurePoints'],
1454: invalidateExpressions: $cachedClosureData['invalidateExpressions'],
1455: usedVariables: $cachedClosureData['usedVariables'],
1456: acceptsNamedArguments: TrinaryLogic::createYes(),
1457: );
1458: }
1459: if (self::$resolveClosureTypeDepth >= 2) {
1460: return new ClosureType(
1461: $parameters,
1462: $this->getFunctionType($node->returnType, false, false),
1463: $isVariadic,
1464: );
1465: }
1466:
1467: self::$resolveClosureTypeDepth++;
1468:
1469: $closureScope = $this->enterAnonymousFunctionWithoutReflection($node, $callableParameters);
1470: $closureReturnStatements = [];
1471: $closureYieldStatements = [];
1472: $onlyNeverExecutionEnds = null;
1473: $closureImpurePoints = [];
1474: $invalidateExpressions = [];
1475:
1476: try {
1477: $closureStatementResult = $this->nodeScopeResolver->processStmtNodes($node, $node->stmts, $closureScope, static function (Node $node, Scope $scope) use ($closureScope, &$closureReturnStatements, &$closureYieldStatements, &$onlyNeverExecutionEnds, &$closureImpurePoints, &$invalidateExpressions): void {
1478: if ($scope->getAnonymousFunctionReflection() !== $closureScope->getAnonymousFunctionReflection()) {
1479: return;
1480: }
1481:
1482: if ($node instanceof InvalidateExprNode) {
1483: $invalidateExpressions[] = $node;
1484: return;
1485: }
1486:
1487: if ($node instanceof PropertyAssignNode) {
1488: $closureImpurePoints[] = new ImpurePoint(
1489: $scope,
1490: $node,
1491: 'propertyAssign',
1492: 'property assignment',
1493: true,
1494: );
1495: return;
1496: }
1497:
1498: if ($node instanceof ExecutionEndNode) {
1499: if ($node->getStatementResult()->isAlwaysTerminating()) {
1500: foreach ($node->getStatementResult()->getExitPoints() as $exitPoint) {
1501: if ($exitPoint->getStatement() instanceof Node\Stmt\Return_) {
1502: $onlyNeverExecutionEnds = false;
1503: continue;
1504: }
1505:
1506: if ($onlyNeverExecutionEnds === null) {
1507: $onlyNeverExecutionEnds = true;
1508: }
1509:
1510: break;
1511: }
1512:
1513: if (count($node->getStatementResult()->getExitPoints()) === 0) {
1514: if ($onlyNeverExecutionEnds === null) {
1515: $onlyNeverExecutionEnds = true;
1516: }
1517: }
1518: } else {
1519: $onlyNeverExecutionEnds = false;
1520: }
1521:
1522: return;
1523: }
1524:
1525: if ($node instanceof Node\Stmt\Return_) {
1526: $closureReturnStatements[] = [$node, $scope];
1527: }
1528:
1529: if (!$node instanceof Expr\Yield_ && !$node instanceof Expr\YieldFrom) {
1530: return;
1531: }
1532:
1533: $closureYieldStatements[] = [$node, $scope];
1534: }, StatementContext::createTopLevel());
1535: } finally {
1536: self::$resolveClosureTypeDepth--;
1537: }
1538:
1539: $throwPoints = $closureStatementResult->getThrowPoints();
1540: $impurePoints = array_merge($closureImpurePoints, $closureStatementResult->getImpurePoints());
1541:
1542: $returnTypes = [];
1543: $hasNull = false;
1544: foreach ($closureReturnStatements as [$returnNode, $returnScope]) {
1545: if ($returnNode->expr === null) {
1546: $hasNull = true;
1547: continue;
1548: }
1549:
1550: $returnTypes[] = $returnScope->getType($returnNode->expr);
1551: }
1552:
1553: if (count($returnTypes) === 0) {
1554: if ($onlyNeverExecutionEnds === true && !$hasNull) {
1555: $returnType = new NonAcceptingNeverType();
1556: } else {
1557: $returnType = new VoidType();
1558: }
1559: } else {
1560: if ($onlyNeverExecutionEnds === true) {
1561: $returnTypes[] = new NonAcceptingNeverType();
1562: }
1563: if ($hasNull) {
1564: $returnTypes[] = new NullType();
1565: }
1566: $returnType = TypeCombinator::union(...$returnTypes);
1567: }
1568:
1569: if (count($closureYieldStatements) > 0) {
1570: $keyTypes = [];
1571: $valueTypes = [];
1572: foreach ($closureYieldStatements as [$yieldNode, $yieldScope]) {
1573: if ($yieldNode instanceof Expr\Yield_) {
1574: if ($yieldNode->key === null) {
1575: $keyTypes[] = new IntegerType();
1576: } else {
1577: $keyTypes[] = $yieldScope->getType($yieldNode->key);
1578: }
1579:
1580: if ($yieldNode->value === null) {
1581: $valueTypes[] = new NullType();
1582: } else {
1583: $valueTypes[] = $yieldScope->getType($yieldNode->value);
1584: }
1585:
1586: continue;
1587: }
1588:
1589: $yieldFromType = $yieldScope->getType($yieldNode->expr);
1590: $keyTypes[] = $yieldScope->getIterableKeyType($yieldFromType);
1591: $valueTypes[] = $yieldScope->getIterableValueType($yieldFromType);
1592: }
1593:
1594: $returnType = new GenericObjectType(Generator::class, [
1595: TypeCombinator::union(...$keyTypes),
1596: TypeCombinator::union(...$valueTypes),
1597: new MixedType(),
1598: $returnType,
1599: ]);
1600: } else {
1601: if ($node->returnType !== null) {
1602: $nativeReturnType = $this->getFunctionType($node->returnType, false, false);
1603: $returnType = self::intersectButNotNever($nativeReturnType, $returnType);
1604: }
1605: }
1606:
1607: $usedVariables = [];
1608: foreach ($node->uses as $use) {
1609: if (!is_string($use->var->name)) {
1610: continue;
1611: }
1612:
1613: $usedVariables[] = $use->var->name;
1614: }
1615:
1616: foreach ($node->uses as $use) {
1617: if (!$use->byRef) {
1618: continue;
1619: }
1620:
1621: $impurePoints[] = new ImpurePoint(
1622: $this,
1623: $node,
1624: 'functionCall',
1625: 'call to a Closure with by-ref use',
1626: true,
1627: );
1628: break;
1629: }
1630: }
1631:
1632: foreach ($parameters as $parameter) {
1633: if ($parameter->passedByReference()->no()) {
1634: continue;
1635: }
1636:
1637: $impurePoints[] = new ImpurePoint(
1638: $this,
1639: $node,
1640: 'functionCall',
1641: 'call to a Closure with by-ref parameter',
1642: true,
1643: );
1644: }
1645:
1646: $throwPointsForClosureType = array_map(static fn (ThrowPoint $throwPoint) => $throwPoint->isExplicit() ? SimpleThrowPoint::createExplicit($throwPoint->getType(), $throwPoint->canContainAnyThrowable()) : SimpleThrowPoint::createImplicit(), $throwPoints);
1647: $impurePointsForClosureType = array_map(static fn (ImpurePoint $impurePoint) => new SimpleImpurePoint($impurePoint->getIdentifier(), $impurePoint->getDescription(), $impurePoint->isCertain()), $impurePoints);
1648:
1649: $cachedTypes = $node->getAttribute('phpstanCachedTypes', []);
1650: $cachedTypes[$this->getClosureScopeCacheKey()] = [
1651: 'returnType' => $returnType,
1652: 'throwPoints' => $throwPointsForClosureType,
1653: 'impurePoints' => $impurePointsForClosureType,
1654: 'invalidateExpressions' => $invalidateExpressions,
1655: 'usedVariables' => $usedVariables,
1656: ];
1657: $node->setAttribute('phpstanCachedTypes', $cachedTypes);
1658:
1659: return new ClosureType(
1660: $parameters,
1661: $returnType,
1662: $isVariadic,
1663: TemplateTypeMap::createEmpty(),
1664: TemplateTypeMap::createEmpty(),
1665: TemplateTypeVarianceMap::createEmpty(),
1666: throwPoints: $throwPointsForClosureType,
1667: impurePoints: $impurePointsForClosureType,
1668: invalidateExpressions: $invalidateExpressions,
1669: usedVariables: $usedVariables,
1670: acceptsNamedArguments: TrinaryLogic::createYes(),
1671: );
1672: } elseif ($node instanceof New_) {
1673: if ($node->class instanceof Name) {
1674: $type = $this->exactInstantiation($node, $node->class->toString());
1675: if ($type !== null) {
1676: return $type;
1677: }
1678:
1679: $lowercasedClassName = strtolower($node->class->toString());
1680: if ($lowercasedClassName === 'static') {
1681: if (!$this->isInClass()) {
1682: return new ErrorType();
1683: }
1684:
1685: return new StaticType($this->getClassReflection());
1686: }
1687: if ($lowercasedClassName === 'parent') {
1688: return new NonexistentParentClassType();
1689: }
1690:
1691: return new ObjectType($node->class->toString());
1692: }
1693: if ($node->class instanceof Node\Stmt\Class_) {
1694: $anonymousClassReflection = $this->reflectionProvider->getAnonymousClassReflection($node->class, $this);
1695:
1696: return new ObjectType($anonymousClassReflection->getName());
1697: }
1698:
1699: $exprType = $this->getType($node->class);
1700: return $exprType->getObjectTypeOrClassStringObjectType();
1701:
1702: } elseif ($node instanceof Array_) {
1703: return $this->initializerExprTypeResolver->getArrayType($node, fn (Expr $expr): Type => $this->getType($expr));
1704: } elseif ($node instanceof Int_) {
1705: return $this->getType($node->expr)->toInteger();
1706: } elseif ($node instanceof Bool_) {
1707: return $this->getType($node->expr)->toBoolean();
1708: } elseif ($node instanceof Double) {
1709: return $this->getType($node->expr)->toFloat();
1710: } elseif ($node instanceof Node\Expr\Cast\String_) {
1711: return $this->getType($node->expr)->toString();
1712: } elseif ($node instanceof Node\Expr\Cast\Array_) {
1713: return $this->getType($node->expr)->toArray();
1714: } elseif ($node instanceof Node\Scalar\MagicConst) {
1715: return $this->initializerExprTypeResolver->getType($node, InitializerExprContext::fromScope($this));
1716: } elseif ($node instanceof Object_) {
1717: $castToObject = static function (Type $type): Type {
1718: $constantArrays = $type->getConstantArrays();
1719: if (count($constantArrays) > 0) {
1720: $objects = [];
1721: foreach ($constantArrays as $constantArray) {
1722: $properties = [];
1723: $optionalProperties = [];
1724: foreach ($constantArray->getKeyTypes() as $i => $keyType) {
1725: if (!$keyType instanceof ConstantStringType) {
1726: // an object with integer properties is >weird<
1727: continue;
1728: }
1729: $valueType = $constantArray->getValueTypes()[$i];
1730: $optional = $constantArray->isOptionalKey($i);
1731: if ($optional) {
1732: $optionalProperties[] = $keyType->getValue();
1733: }
1734: $properties[$keyType->getValue()] = $valueType;
1735: }
1736:
1737: $objects[] = TypeCombinator::intersect(new ObjectShapeType($properties, $optionalProperties), new ObjectType(stdClass::class));
1738: }
1739:
1740: return TypeCombinator::union(...$objects);
1741: }
1742: if ($type->isObject()->yes()) {
1743: return $type;
1744: }
1745:
1746: return new ObjectType('stdClass');
1747: };
1748:
1749: $exprType = $this->getType($node->expr);
1750: if ($exprType instanceof UnionType) {
1751: return TypeCombinator::union(...array_map($castToObject, $exprType->getTypes()));
1752: }
1753:
1754: return $castToObject($exprType);
1755: } elseif ($node instanceof Unset_) {
1756: return new NullType();
1757: } elseif ($node instanceof Expr\PostInc || $node instanceof Expr\PostDec) {
1758: return $this->getType($node->var);
1759: } elseif ($node instanceof Expr\PreInc || $node instanceof Expr\PreDec) {
1760: $varType = $this->getType($node->var);
1761: $varScalars = $varType->getConstantScalarValues();
1762:
1763: if (count($varScalars) > 0) {
1764: $newTypes = [];
1765:
1766: foreach ($varScalars as $varValue) {
1767: // until PHP 8.5 it was valid to increment/decrement an empty string.
1768: // see https://github.com/php/php-src/issues/19597
1769: if ($node instanceof Expr\PreInc) {
1770: if ($varValue === '') {
1771: $varValue = '1';
1772: } elseif (is_string($varValue) && !is_numeric($varValue)) {
1773: try {
1774: $varValue = str_increment($varValue);
1775: } catch (ValueError) {
1776: return new NeverType();
1777: }
1778: } elseif (!is_bool($varValue)) {
1779: ++$varValue;
1780: }
1781: } else {
1782: if ($varValue === '') {
1783: $varValue = -1;
1784: } elseif (is_string($varValue) && !is_numeric($varValue)) {
1785: try {
1786: $varValue = str_decrement($varValue);
1787: } catch (ValueError) {
1788: return new NeverType();
1789: }
1790: } elseif (is_numeric($varValue)) {
1791: --$varValue;
1792: }
1793: }
1794:
1795: $newTypes[] = $this->getTypeFromValue($varValue);
1796: }
1797: return TypeCombinator::union(...$newTypes);
1798: } elseif ($varType->isString()->yes()) {
1799: if ($varType->isLiteralString()->yes()) {
1800: return new IntersectionType([
1801: new StringType(),
1802: new AccessoryLiteralStringType(),
1803: ]);
1804: }
1805:
1806: if ($varType->isNumericString()->yes()) {
1807: return new BenevolentUnionType([
1808: new IntegerType(),
1809: new FloatType(),
1810: ]);
1811: }
1812:
1813: return new BenevolentUnionType([
1814: new StringType(),
1815: new IntegerType(),
1816: new FloatType(),
1817: ]);
1818: }
1819:
1820: if ($node instanceof Expr\PreInc) {
1821: return $this->getType(new BinaryOp\Plus($node->var, new Node\Scalar\Int_(1)));
1822: }
1823:
1824: return $this->getType(new BinaryOp\Minus($node->var, new Node\Scalar\Int_(1)));
1825: } elseif ($node instanceof Expr\Yield_) {
1826: $functionReflection = $this->getFunction();
1827: if ($functionReflection === null) {
1828: return new MixedType();
1829: }
1830:
1831: $returnType = $functionReflection->getReturnType();
1832: $generatorSendType = $returnType->getTemplateType(Generator::class, 'TSend');
1833: if ($generatorSendType instanceof ErrorType) {
1834: return new MixedType();
1835: }
1836:
1837: return $generatorSendType;
1838: } elseif ($node instanceof Expr\YieldFrom) {
1839: $yieldFromType = $this->getType($node->expr);
1840: $generatorReturnType = $yieldFromType->getTemplateType(Generator::class, 'TReturn');
1841: if ($generatorReturnType instanceof ErrorType) {
1842: return new MixedType();
1843: }
1844:
1845: return $generatorReturnType;
1846: } elseif ($node instanceof Expr\Match_) {
1847: $cond = $node->cond;
1848: $condType = $this->getType($cond);
1849: $types = [];
1850:
1851: $matchScope = $this;
1852: $arms = $node->arms;
1853: if ($condType->isEnum()->yes()) {
1854: // enum match analysis would work even without this if branch
1855: // but would be much slower
1856: // this avoids using ObjectType::$subtractedType which is slow for huge enums
1857: // because of repeated union type normalization
1858: $enumCases = $condType->getEnumCases();
1859: if (count($enumCases) > 0) {
1860: $indexedEnumCases = [];
1861: foreach ($enumCases as $enumCase) {
1862: $indexedEnumCases[strtolower($enumCase->getClassName())][$enumCase->getEnumCaseName()] = $enumCase;
1863: }
1864: $unusedIndexedEnumCases = $indexedEnumCases;
1865:
1866: foreach ($arms as $i => $arm) {
1867: if ($arm->conds === null) {
1868: continue;
1869: }
1870:
1871: $conditionCases = [];
1872: foreach ($arm->conds as $armCond) {
1873: if (!$armCond instanceof Expr\ClassConstFetch) {
1874: continue 2;
1875: }
1876: if (!$armCond->class instanceof Name) {
1877: continue 2;
1878: }
1879: if (!$armCond->name instanceof Node\Identifier) {
1880: continue 2;
1881: }
1882: $fetchedClassName = $this->resolveName($armCond->class);
1883: $loweredFetchedClassName = strtolower($fetchedClassName);
1884: if (!array_key_exists($loweredFetchedClassName, $indexedEnumCases)) {
1885: continue 2;
1886: }
1887:
1888: $caseName = $armCond->name->toString();
1889: if (!array_key_exists($caseName, $indexedEnumCases[$loweredFetchedClassName])) {
1890: continue 2;
1891: }
1892:
1893: $conditionCases[] = $indexedEnumCases[$loweredFetchedClassName][$caseName];
1894: unset($unusedIndexedEnumCases[$loweredFetchedClassName][$caseName]);
1895: }
1896:
1897: $conditionCasesCount = count($conditionCases);
1898: if ($conditionCasesCount === 0) {
1899: throw new ShouldNotHappenException();
1900: } elseif ($conditionCasesCount === 1) {
1901: $conditionCaseType = $conditionCases[0];
1902: } else {
1903: $conditionCaseType = new UnionType($conditionCases);
1904: }
1905:
1906: $types[] = $matchScope->addTypeToExpression(
1907: $cond,
1908: $conditionCaseType,
1909: )->getType($arm->body);
1910: unset($arms[$i]);
1911: }
1912:
1913: $remainingCases = [];
1914: foreach ($unusedIndexedEnumCases as $cases) {
1915: foreach ($cases as $case) {
1916: $remainingCases[] = $case;
1917: }
1918: }
1919:
1920: $remainingCasesCount = count($remainingCases);
1921: if ($remainingCasesCount === 0) {
1922: $remainingType = new NeverType();
1923: } elseif ($remainingCasesCount === 1) {
1924: $remainingType = $remainingCases[0];
1925: } else {
1926: $remainingType = new UnionType($remainingCases);
1927: }
1928:
1929: $matchScope = $matchScope->addTypeToExpression($cond, $remainingType);
1930: }
1931: }
1932:
1933: foreach ($arms as $arm) {
1934: if ($arm->conds === null) {
1935: if ($node->hasAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)) {
1936: $arm->body->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, $node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME));
1937: }
1938: $types[] = $matchScope->getType($arm->body);
1939: continue;
1940: }
1941:
1942: if (count($arm->conds) === 0) {
1943: throw new ShouldNotHappenException();
1944: }
1945:
1946: if (count($arm->conds) === 1) {
1947: $filteringExpr = new BinaryOp\Identical($cond, $arm->conds[0]);
1948: } else {
1949: $items = [];
1950: foreach ($arm->conds as $filteringExpr) {
1951: $items[] = new Node\ArrayItem($filteringExpr);
1952: }
1953: $filteringExpr = new FuncCall(
1954: new Name\FullyQualified('in_array'),
1955: [
1956: new Arg($cond),
1957: new Arg(new Array_($items)),
1958: new Arg(new ConstFetch(new Name\FullyQualified('true'))),
1959: ],
1960: );
1961: }
1962:
1963: $filteringExprType = $matchScope->getType($filteringExpr);
1964:
1965: if (!$filteringExprType->isFalse()->yes()) {
1966: $truthyScope = $matchScope->filterByTruthyValue($filteringExpr);
1967: if ($node->hasAttribute(self::KEEP_VOID_ATTRIBUTE_NAME)) {
1968: $arm->body->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, $node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME));
1969: }
1970: $types[] = $truthyScope->getType($arm->body);
1971: }
1972:
1973: $matchScope = $matchScope->filterByFalseyValue($filteringExpr);
1974: }
1975:
1976: return TypeCombinator::union(...$types);
1977: }
1978:
1979: if ($node instanceof Expr\Isset_) {
1980: $issetResult = true;
1981: foreach ($node->vars as $var) {
1982: $result = $this->issetCheck($var, static function (Type $type): ?bool {
1983: $isNull = $type->isNull();
1984: if ($isNull->maybe()) {
1985: return null;
1986: }
1987:
1988: return !$isNull->yes();
1989: });
1990: if ($result !== null) {
1991: if (!$result) {
1992: return new ConstantBooleanType($result);
1993: }
1994:
1995: continue;
1996: }
1997:
1998: $issetResult = $result;
1999: }
2000:
2001: if ($issetResult === null) {
2002: return new BooleanType();
2003: }
2004:
2005: return new ConstantBooleanType($issetResult);
2006: }
2007:
2008: if ($node instanceof Expr\AssignOp\Coalesce) {
2009: return $this->getType(new BinaryOp\Coalesce($node->var, $node->expr, $node->getAttributes()));
2010: }
2011:
2012: if ($node instanceof Expr\BinaryOp\Coalesce) {
2013: $issetLeftExpr = new Expr\Isset_([$node->left]);
2014: $leftType = $this->filterByTruthyValue($issetLeftExpr)->getType($node->left);
2015:
2016: $result = $this->issetCheck($node->left, static function (Type $type): ?bool {
2017: $isNull = $type->isNull();
2018: if ($isNull->maybe()) {
2019: return null;
2020: }
2021:
2022: return !$isNull->yes();
2023: });
2024:
2025: if ($result !== null && $result !== false) {
2026: return TypeCombinator::removeNull($leftType);
2027: }
2028:
2029: $rightType = $this->filterByFalseyValue($issetLeftExpr)->getType($node->right);
2030:
2031: if ($result === null) {
2032: return TypeCombinator::union(
2033: TypeCombinator::removeNull($leftType),
2034: $rightType,
2035: );
2036: }
2037:
2038: return $rightType;
2039: }
2040:
2041: if ($node instanceof ConstFetch) {
2042: $constName = (string) $node->name;
2043: $loweredConstName = strtolower($constName);
2044: if ($loweredConstName === 'true') {
2045: return new ConstantBooleanType(true);
2046: } elseif ($loweredConstName === 'false') {
2047: return new ConstantBooleanType(false);
2048: } elseif ($loweredConstName === 'null') {
2049: return new NullType();
2050: }
2051:
2052: $namespacedName = null;
2053: if (!$node->name->isFullyQualified() && $this->getNamespace() !== null) {
2054: $namespacedName = new FullyQualified([$this->getNamespace(), $node->name->toString()]);
2055: }
2056: $globalName = new FullyQualified($node->name->toString());
2057:
2058: foreach ([$namespacedName, $globalName] as $name) {
2059: if ($name === null) {
2060: continue;
2061: }
2062: $constFetch = new ConstFetch($name);
2063: if ($this->hasExpressionType($constFetch)->yes()) {
2064: return $this->constantResolver->resolveConstantType(
2065: $name->toString(),
2066: $this->expressionTypes[$this->getNodeKey($constFetch)]->getType(),
2067: );
2068: }
2069: }
2070:
2071: $constantType = $this->constantResolver->resolveConstant($node->name, $this);
2072: if ($constantType !== null) {
2073: return $constantType;
2074: }
2075:
2076: return new ErrorType();
2077: } elseif ($node instanceof Node\Expr\ClassConstFetch && $node->name instanceof Node\Identifier) {
2078: if ($this->hasExpressionType($node)->yes()) {
2079: return $this->expressionTypes[$exprString]->getType();
2080: }
2081: return $this->initializerExprTypeResolver->getClassConstFetchTypeByReflection(
2082: $node->class,
2083: $node->name->name,
2084: $this->isInClass() ? $this->getClassReflection() : null,
2085: fn (Expr $expr): Type => $this->getType($expr),
2086: );
2087: }
2088:
2089: if ($node instanceof Expr\Ternary) {
2090: $noopCallback = static function (): void {
2091: };
2092: $condResult = $this->nodeScopeResolver->processExprNode(new Node\Stmt\Expression($node->cond), $node->cond, $this, $noopCallback, ExpressionContext::createDeep());
2093: if ($node->if === null) {
2094: $conditionType = $this->getType($node->cond);
2095: $booleanConditionType = $conditionType->toBoolean();
2096: if ($booleanConditionType->isTrue()->yes()) {
2097: return $condResult->getTruthyScope()->getType($node->cond);
2098: }
2099:
2100: if ($booleanConditionType->isFalse()->yes()) {
2101: return $condResult->getFalseyScope()->getType($node->else);
2102: }
2103:
2104: return TypeCombinator::union(
2105: TypeCombinator::removeFalsey($condResult->getTruthyScope()->getType($node->cond)),
2106: $condResult->getFalseyScope()->getType($node->else),
2107: );
2108: }
2109:
2110: $booleanConditionType = $this->getType($node->cond)->toBoolean();
2111: if ($booleanConditionType->isTrue()->yes()) {
2112: return $condResult->getTruthyScope()->getType($node->if);
2113: }
2114:
2115: if ($booleanConditionType->isFalse()->yes()) {
2116: return $condResult->getFalseyScope()->getType($node->else);
2117: }
2118:
2119: return TypeCombinator::union(
2120: $condResult->getTruthyScope()->getType($node->if),
2121: $condResult->getFalseyScope()->getType($node->else),
2122: );
2123: }
2124:
2125: if ($node instanceof Variable) {
2126: if (is_string($node->name)) {
2127: if ($this->hasVariableType($node->name)->no()) {
2128: return new ErrorType();
2129: }
2130:
2131: return $this->getVariableType($node->name);
2132: }
2133:
2134: $nameType = $this->getType($node->name);
2135: if (count($nameType->getConstantStrings()) > 0) {
2136: $types = [];
2137: foreach ($nameType->getConstantStrings() as $constantString) {
2138: $variableScope = $this
2139: ->filterByTruthyValue(
2140: new BinaryOp\Identical($node->name, new String_($constantString->getValue())),
2141: );
2142: if ($variableScope->hasVariableType($constantString->getValue())->no()) {
2143: $types[] = new ErrorType();
2144: continue;
2145: }
2146:
2147: $types[] = $variableScope->getVariableType($constantString->getValue());
2148: }
2149:
2150: return TypeCombinator::union(...$types);
2151: }
2152: }
2153:
2154: if ($node instanceof Expr\ArrayDimFetch && $node->dim !== null) {
2155: return $this->getNullsafeShortCircuitingType(
2156: $node->var,
2157: $this->getTypeFromArrayDimFetch(
2158: $node,
2159: $this->getType($node->dim),
2160: $this->getType($node->var),
2161: ),
2162: );
2163: }
2164:
2165: if ($node instanceof MethodCall) {
2166: if ($node->name instanceof Node\Identifier) {
2167: if ($this->nativeTypesPromoted) {
2168: $methodReflection = $this->getMethodReflection(
2169: $this->getNativeType($node->var),
2170: $node->name->name,
2171: );
2172: if ($methodReflection === null) {
2173: $returnType = new ErrorType();
2174: } else {
2175: $returnType = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType();
2176: }
2177:
2178: return $this->getNullsafeShortCircuitingType($node->var, $returnType);
2179: }
2180:
2181: $returnType = $this->methodCallReturnType(
2182: $this->getType($node->var),
2183: $node->name->name,
2184: $node,
2185: );
2186: if ($returnType === null) {
2187: $returnType = new ErrorType();
2188: }
2189: return $this->getNullsafeShortCircuitingType($node->var, $returnType);
2190: }
2191:
2192: $nameType = $this->getType($node->name);
2193: if (count($nameType->getConstantStrings()) > 0) {
2194: return TypeCombinator::union(
2195: ...array_map(fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $this
2196: ->filterByTruthyValue(new BinaryOp\Identical($node->name, new String_($constantString->getValue())))
2197: ->getType(new MethodCall($node->var, new Identifier($constantString->getValue()), $node->args)), $nameType->getConstantStrings()),
2198: );
2199: }
2200: }
2201:
2202: if ($node instanceof Expr\NullsafeMethodCall) {
2203: $varType = $this->getType($node->var);
2204: if ($varType->isNull()->yes()) {
2205: return new NullType();
2206: }
2207: if (!TypeCombinator::containsNull($varType)) {
2208: return $this->getType(new MethodCall($node->var, $node->name, $node->args));
2209: }
2210:
2211: return TypeCombinator::union(
2212: $this->filterByTruthyValue(new BinaryOp\NotIdentical($node->var, new ConstFetch(new Name('null'))))
2213: ->getType(new MethodCall($node->var, $node->name, $node->args)),
2214: new NullType(),
2215: );
2216: }
2217:
2218: if ($node instanceof Expr\StaticCall) {
2219: if ($node->name instanceof Node\Identifier) {
2220: if ($this->nativeTypesPromoted) {
2221: if ($node->class instanceof Name) {
2222: $staticMethodCalledOnType = $this->resolveTypeByNameWithLateStaticBinding($node->class, $node->name);
2223: } else {
2224: $staticMethodCalledOnType = $this->getNativeType($node->class);
2225: }
2226: $methodReflection = $this->getMethodReflection(
2227: $staticMethodCalledOnType,
2228: $node->name->name,
2229: );
2230: if ($methodReflection === null) {
2231: $callType = new ErrorType();
2232: } else {
2233: $callType = ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants())->getNativeReturnType();
2234: }
2235:
2236: if ($node->class instanceof Expr) {
2237: return $this->getNullsafeShortCircuitingType($node->class, $callType);
2238: }
2239:
2240: return $callType;
2241: }
2242:
2243: if ($node->class instanceof Name) {
2244: $staticMethodCalledOnType = $this->resolveTypeByNameWithLateStaticBinding($node->class, $node->name);
2245: } else {
2246: $staticMethodCalledOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType();
2247: }
2248:
2249: $callType = $this->methodCallReturnType(
2250: $staticMethodCalledOnType,
2251: $node->name->toString(),
2252: $node,
2253: );
2254: if ($callType === null) {
2255: $callType = new ErrorType();
2256: }
2257:
2258: if ($node->class instanceof Expr) {
2259: return $this->getNullsafeShortCircuitingType($node->class, $callType);
2260: }
2261:
2262: return $callType;
2263: }
2264:
2265: $nameType = $this->getType($node->name);
2266: if (count($nameType->getConstantStrings()) > 0) {
2267: return TypeCombinator::union(
2268: ...array_map(fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $this
2269: ->filterByTruthyValue(new BinaryOp\Identical($node->name, new String_($constantString->getValue())))
2270: ->getType(new Expr\StaticCall($node->class, new Identifier($constantString->getValue()), $node->args)), $nameType->getConstantStrings()),
2271: );
2272: }
2273: }
2274:
2275: if ($node instanceof PropertyFetch) {
2276: if ($node->name instanceof Node\Identifier) {
2277: if ($this->nativeTypesPromoted) {
2278: $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node, $this);
2279: if ($propertyReflection === null) {
2280: return new ErrorType();
2281: }
2282:
2283: if (!$propertyReflection->hasNativeType()) {
2284: return new MixedType();
2285: }
2286:
2287: $nativeType = $propertyReflection->getNativeType();
2288:
2289: return $this->getNullsafeShortCircuitingType($node->var, $nativeType);
2290: }
2291:
2292: $returnType = $this->propertyFetchType(
2293: $this->getType($node->var),
2294: $node->name->name,
2295: $node,
2296: );
2297: if ($returnType === null) {
2298: $returnType = new ErrorType();
2299: }
2300:
2301: return $this->getNullsafeShortCircuitingType($node->var, $returnType);
2302: }
2303:
2304: $nameType = $this->getType($node->name);
2305: if (count($nameType->getConstantStrings()) > 0) {
2306: return TypeCombinator::union(
2307: ...array_map(fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $this
2308: ->filterByTruthyValue(new BinaryOp\Identical($node->name, new String_($constantString->getValue())))
2309: ->getType(
2310: new PropertyFetch($node->var, new Identifier($constantString->getValue())),
2311: ), $nameType->getConstantStrings()),
2312: );
2313: }
2314: }
2315:
2316: if ($node instanceof Expr\NullsafePropertyFetch) {
2317: $varType = $this->getType($node->var);
2318: if ($varType->isNull()->yes()) {
2319: return new NullType();
2320: }
2321: if (!TypeCombinator::containsNull($varType)) {
2322: return $this->getType(new PropertyFetch($node->var, $node->name));
2323: }
2324:
2325: return TypeCombinator::union(
2326: $this->filterByTruthyValue(new BinaryOp\NotIdentical($node->var, new ConstFetch(new Name('null'))))
2327: ->getType(new PropertyFetch($node->var, $node->name)),
2328: new NullType(),
2329: );
2330: }
2331:
2332: if ($node instanceof Expr\StaticPropertyFetch) {
2333: if ($node->name instanceof Node\VarLikeIdentifier) {
2334: if ($this->nativeTypesPromoted) {
2335: $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($node, $this);
2336: if ($propertyReflection === null) {
2337: return new ErrorType();
2338: }
2339: if (!$propertyReflection->hasNativeType()) {
2340: return new MixedType();
2341: }
2342:
2343: $nativeType = $propertyReflection->getNativeType();
2344:
2345: if ($node->class instanceof Expr) {
2346: return $this->getNullsafeShortCircuitingType($node->class, $nativeType);
2347: }
2348:
2349: return $nativeType;
2350: }
2351:
2352: if ($node->class instanceof Name) {
2353: $staticPropertyFetchedOnType = $this->resolveTypeByName($node->class);
2354: } else {
2355: $staticPropertyFetchedOnType = TypeCombinator::removeNull($this->getType($node->class))->getObjectTypeOrClassStringObjectType();
2356: }
2357:
2358: $fetchType = $this->propertyFetchType(
2359: $staticPropertyFetchedOnType,
2360: $node->name->toString(),
2361: $node,
2362: );
2363: if ($fetchType === null) {
2364: $fetchType = new ErrorType();
2365: }
2366:
2367: if ($node->class instanceof Expr) {
2368: return $this->getNullsafeShortCircuitingType($node->class, $fetchType);
2369: }
2370:
2371: return $fetchType;
2372: }
2373:
2374: $nameType = $this->getType($node->name);
2375: if (count($nameType->getConstantStrings()) > 0) {
2376: return TypeCombinator::union(
2377: ...array_map(fn ($constantString) => $constantString->getValue() === '' ? new ErrorType() : $this
2378: ->filterByTruthyValue(new BinaryOp\Identical($node->name, new String_($constantString->getValue())))
2379: ->getType(new Expr\StaticPropertyFetch($node->class, new Node\VarLikeIdentifier($constantString->getValue()))), $nameType->getConstantStrings()),
2380: );
2381: }
2382: }
2383:
2384: if ($node instanceof FuncCall) {
2385: if ($node->name instanceof Expr) {
2386: $calledOnType = $this->getType($node->name);
2387: if ($calledOnType->isCallable()->no()) {
2388: return new ErrorType();
2389: }
2390:
2391: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
2392: $this,
2393: $node->getArgs(),
2394: $calledOnType->getCallableParametersAcceptors($this),
2395: null,
2396: );
2397:
2398: $functionName = null;
2399: if ($node->name instanceof String_) {
2400: /** @var non-empty-string $name */
2401: $name = $node->name->value;
2402: $functionName = new Name($name);
2403: } elseif (
2404: $node->name instanceof FuncCall
2405: && $node->name->name instanceof Name
2406: && $node->name->isFirstClassCallable()
2407: ) {
2408: $functionName = $node->name->name;
2409: }
2410:
2411: if ($functionName !== null && $this->reflectionProvider->hasFunction($functionName, $this)) {
2412: $functionReflection = $this->reflectionProvider->getFunction($functionName, $this);
2413: $resolvedType = $this->getDynamicFunctionReturnType($parametersAcceptor, $node, $functionReflection);
2414: if ($resolvedType !== null) {
2415: return $resolvedType;
2416: }
2417: }
2418:
2419: return $parametersAcceptor->getReturnType();
2420: }
2421:
2422: if (!$this->reflectionProvider->hasFunction($node->name, $this)) {
2423: return new ErrorType();
2424: }
2425:
2426: $functionReflection = $this->reflectionProvider->getFunction($node->name, $this);
2427: if ($this->nativeTypesPromoted) {
2428: return ParametersAcceptorSelector::combineAcceptors($functionReflection->getVariants())->getNativeReturnType();
2429: }
2430:
2431: if ($functionReflection->getName() === 'call_user_func') {
2432: $result = ArgumentsNormalizer::reorderCallUserFuncArguments($node, $this);
2433: if ($result !== null) {
2434: [, $innerFuncCall] = $result;
2435:
2436: return $this->getType($innerFuncCall);
2437: }
2438: }
2439:
2440: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
2441: $this,
2442: $node->getArgs(),
2443: $functionReflection->getVariants(),
2444: $functionReflection->getNamedArgumentsVariants(),
2445: );
2446: $normalizedNode = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node);
2447: if ($normalizedNode !== null) {
2448: $resolvedType = $this->getDynamicFunctionReturnType($parametersAcceptor, $normalizedNode, $functionReflection);
2449: if ($resolvedType !== null) {
2450: return $resolvedType;
2451: }
2452: }
2453:
2454: return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $node);
2455: }
2456:
2457: return new MixedType();
2458: }
2459:
2460: private function getDynamicFunctionReturnType(ParametersAcceptor $parametersAcceptor, FuncCall $node, FunctionReflection $functionReflection): ?Type
2461: {
2462: $normalizedNode = ArgumentsNormalizer::reorderFuncArguments($parametersAcceptor, $node);
2463: if ($normalizedNode !== null) {
2464: foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicFunctionReturnTypeExtensions() as $dynamicFunctionReturnTypeExtension) {
2465: if (!$dynamicFunctionReturnTypeExtension->isFunctionSupported($functionReflection)) {
2466: continue;
2467: }
2468:
2469: $resolvedType = $dynamicFunctionReturnTypeExtension->getTypeFromFunctionCall(
2470: $functionReflection,
2471: $node,
2472: $this,
2473: );
2474: if ($resolvedType !== null) {
2475: return $resolvedType;
2476: }
2477: }
2478: }
2479:
2480: return null;
2481: }
2482:
2483: private function getNullsafeShortCircuitingType(Expr $expr, Type $type): Type
2484: {
2485: if ($expr instanceof Expr\NullsafePropertyFetch || $expr instanceof Expr\NullsafeMethodCall) {
2486: $varType = $this->getType($expr->var);
2487: if (TypeCombinator::containsNull($varType)) {
2488: return TypeCombinator::addNull($type);
2489: }
2490:
2491: return $type;
2492: }
2493:
2494: if ($expr instanceof Expr\ArrayDimFetch) {
2495: return $this->getNullsafeShortCircuitingType($expr->var, $type);
2496: }
2497:
2498: if ($expr instanceof PropertyFetch) {
2499: return $this->getNullsafeShortCircuitingType($expr->var, $type);
2500: }
2501:
2502: if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) {
2503: return $this->getNullsafeShortCircuitingType($expr->class, $type);
2504: }
2505:
2506: if ($expr instanceof MethodCall) {
2507: return $this->getNullsafeShortCircuitingType($expr->var, $type);
2508: }
2509:
2510: if ($expr instanceof Expr\StaticCall && $expr->class instanceof Expr) {
2511: return $this->getNullsafeShortCircuitingType($expr->class, $type);
2512: }
2513:
2514: return $type;
2515: }
2516:
2517: private function transformVoidToNull(Type $type, Node $node): Type
2518: {
2519: if ($node->getAttribute(self::KEEP_VOID_ATTRIBUTE_NAME) === true) {
2520: return $type;
2521: }
2522:
2523: return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type {
2524: if ($type instanceof UnionType || $type instanceof IntersectionType) {
2525: return $traverse($type);
2526: }
2527:
2528: if ($type->isVoid()->yes()) {
2529: return new NullType();
2530: }
2531:
2532: return $type;
2533: });
2534: }
2535:
2536: /**
2537: * @param callable(Type): ?bool $typeCallback
2538: */
2539: public function issetCheck(Expr $expr, callable $typeCallback, ?bool $result = null): ?bool
2540: {
2541: // mirrored in PHPStan\Rules\IssetCheck
2542: if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) {
2543: $hasVariable = $this->hasVariableType($expr->name);
2544: if ($hasVariable->maybe()) {
2545: return null;
2546: }
2547:
2548: if ($result === null) {
2549: if ($hasVariable->yes()) {
2550: if ($expr->name === '_SESSION') {
2551: return null;
2552: }
2553:
2554: return $typeCallback($this->getVariableType($expr->name));
2555: }
2556:
2557: return false;
2558: }
2559:
2560: return $result;
2561: } elseif ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) {
2562: $type = $this->getType($expr->var);
2563: if (!$type->isOffsetAccessible()->yes()) {
2564: return $result ?? $this->issetCheckUndefined($expr->var);
2565: }
2566:
2567: $dimType = $this->getType($expr->dim);
2568: $hasOffsetValue = $type->hasOffsetValueType($dimType);
2569: if ($hasOffsetValue->no()) {
2570: return false;
2571: }
2572:
2573: // If offset cannot be null, store this error message and see if one of the earlier offsets is.
2574: // E.g. $array['a']['b']['c'] ?? null; is a valid coalesce if a OR b or C might be null.
2575: if ($hasOffsetValue->yes()) {
2576: $result = $typeCallback($type->getOffsetValueType($dimType));
2577:
2578: if ($result !== null) {
2579: return $this->issetCheck($expr->var, $typeCallback, $result);
2580: }
2581: }
2582:
2583: // Has offset, it is nullable
2584: return null;
2585:
2586: } elseif ($expr instanceof Node\Expr\PropertyFetch || $expr instanceof Node\Expr\StaticPropertyFetch) {
2587:
2588: $propertyReflection = $this->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this);
2589:
2590: if ($propertyReflection === null) {
2591: if ($expr instanceof Node\Expr\PropertyFetch) {
2592: return $this->issetCheckUndefined($expr->var);
2593: }
2594:
2595: if ($expr->class instanceof Expr) {
2596: return $this->issetCheckUndefined($expr->class);
2597: }
2598:
2599: return null;
2600: }
2601:
2602: if (!$propertyReflection->isNative()) {
2603: if ($expr instanceof Node\Expr\PropertyFetch) {
2604: return $this->issetCheckUndefined($expr->var);
2605: }
2606:
2607: if ($expr->class instanceof Expr) {
2608: return $this->issetCheckUndefined($expr->class);
2609: }
2610:
2611: return null;
2612: }
2613:
2614: if ($propertyReflection->hasNativeType() && !$propertyReflection->isVirtual()->yes()) {
2615: if (!$this->hasExpressionType($expr)->yes()) {
2616: if ($expr instanceof Node\Expr\PropertyFetch) {
2617: return $this->issetCheckUndefined($expr->var);
2618: }
2619:
2620: if ($expr->class instanceof Expr) {
2621: return $this->issetCheckUndefined($expr->class);
2622: }
2623:
2624: return null;
2625: }
2626: }
2627:
2628: if ($result !== null) {
2629: return $result;
2630: }
2631:
2632: $result = $typeCallback($propertyReflection->getWritableType());
2633: if ($result !== null) {
2634: if ($expr instanceof Node\Expr\PropertyFetch) {
2635: return $this->issetCheck($expr->var, $typeCallback, $result);
2636: }
2637:
2638: if ($expr->class instanceof Expr) {
2639: return $this->issetCheck($expr->class, $typeCallback, $result);
2640: }
2641: }
2642:
2643: return $result;
2644: }
2645:
2646: if ($result !== null) {
2647: return $result;
2648: }
2649:
2650: return $typeCallback($this->getType($expr));
2651: }
2652:
2653: private function issetCheckUndefined(Expr $expr): ?bool
2654: {
2655: if ($expr instanceof Node\Expr\Variable && is_string($expr->name)) {
2656: $hasVariable = $this->hasVariableType($expr->name);
2657: if (!$hasVariable->no()) {
2658: return null;
2659: }
2660:
2661: return false;
2662: }
2663:
2664: if ($expr instanceof Node\Expr\ArrayDimFetch && $expr->dim !== null) {
2665: $type = $this->getType($expr->var);
2666: if (!$type->isOffsetAccessible()->yes()) {
2667: return $this->issetCheckUndefined($expr->var);
2668: }
2669:
2670: $dimType = $this->getType($expr->dim);
2671: $hasOffsetValue = $type->hasOffsetValueType($dimType);
2672:
2673: if (!$hasOffsetValue->no()) {
2674: return $this->issetCheckUndefined($expr->var);
2675: }
2676:
2677: return false;
2678: }
2679:
2680: if ($expr instanceof Expr\PropertyFetch) {
2681: return $this->issetCheckUndefined($expr->var);
2682: }
2683:
2684: if ($expr instanceof Expr\StaticPropertyFetch && $expr->class instanceof Expr) {
2685: return $this->issetCheckUndefined($expr->class);
2686: }
2687:
2688: return null;
2689: }
2690:
2691: /**
2692: * @param ParametersAcceptor[] $variants
2693: */
2694: private function createFirstClassCallable(
2695: FunctionReflection|ExtendedMethodReflection|null $function,
2696: array $variants,
2697: ): Type
2698: {
2699: $closureTypes = [];
2700:
2701: foreach ($variants as $variant) {
2702: $returnType = $variant->getReturnType();
2703: if ($variant instanceof ExtendedParametersAcceptor) {
2704: $returnType = $this->nativeTypesPromoted ? $variant->getNativeReturnType() : $returnType;
2705: }
2706:
2707: $templateTags = [];
2708: foreach ($variant->getTemplateTypeMap()->getTypes() as $templateType) {
2709: if (!$templateType instanceof TemplateType) {
2710: continue;
2711: }
2712: $templateTags[$templateType->getName()] = new TemplateTag(
2713: $templateType->getName(),
2714: $templateType->getBound(),
2715: $templateType->getDefault(),
2716: $templateType->getVariance(),
2717: );
2718: }
2719:
2720: $throwPoints = [];
2721: $impurePoints = [];
2722: $acceptsNamedArguments = TrinaryLogic::createYes();
2723: if ($variant instanceof CallableParametersAcceptor) {
2724: $throwPoints = $variant->getThrowPoints();
2725: $impurePoints = $variant->getImpurePoints();
2726: $acceptsNamedArguments = $variant->acceptsNamedArguments();
2727: } elseif ($function !== null) {
2728: $returnTypeForThrow = $variant->getReturnType();
2729: $throwType = $function->getThrowType();
2730: if ($throwType === null) {
2731: if ($returnTypeForThrow instanceof NeverType && $returnTypeForThrow->isExplicit()) {
2732: $throwType = new ObjectType(Throwable::class);
2733: }
2734: }
2735:
2736: if ($throwType !== null) {
2737: if (!$throwType->isVoid()->yes()) {
2738: $throwPoints[] = SimpleThrowPoint::createExplicit($throwType, true);
2739: }
2740: } else {
2741: if (!(new ObjectType(Throwable::class))->isSuperTypeOf($returnTypeForThrow)->yes()) {
2742: $throwPoints[] = SimpleThrowPoint::createImplicit();
2743: }
2744: }
2745:
2746: $impurePoint = SimpleImpurePoint::createFromVariant($function, $variant);
2747: if ($impurePoint !== null) {
2748: $impurePoints[] = $impurePoint;
2749: }
2750:
2751: $acceptsNamedArguments = $function->acceptsNamedArguments();
2752: }
2753:
2754: $parameters = $variant->getParameters();
2755: $closureTypes[] = new ClosureType(
2756: $parameters,
2757: $returnType,
2758: $variant->isVariadic(),
2759: $variant->getTemplateTypeMap(),
2760: $variant->getResolvedTemplateTypeMap(),
2761: $variant instanceof ExtendedParametersAcceptor ? $variant->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
2762: $templateTags,
2763: $throwPoints,
2764: $impurePoints,
2765: acceptsNamedArguments: $acceptsNamedArguments,
2766: );
2767: }
2768:
2769: return TypeCombinator::union(...$closureTypes);
2770: }
2771:
2772: /** @api */
2773: public function getNativeType(Expr $expr): Type
2774: {
2775: return $this->promoteNativeTypes()->getType($expr);
2776: }
2777:
2778: public function getKeepVoidType(Expr $node): Type
2779: {
2780: $clonedNode = clone $node;
2781: $clonedNode->setAttribute(self::KEEP_VOID_ATTRIBUTE_NAME, true);
2782:
2783: return $this->getType($clonedNode);
2784: }
2785:
2786: public function doNotTreatPhpDocTypesAsCertain(): Scope
2787: {
2788: return $this->promoteNativeTypes();
2789: }
2790:
2791: private function promoteNativeTypes(): self
2792: {
2793: if ($this->nativeTypesPromoted) {
2794: return $this;
2795: }
2796:
2797: if ($this->scopeWithPromotedNativeTypes !== null) {
2798: return $this->scopeWithPromotedNativeTypes;
2799: }
2800:
2801: return $this->scopeWithPromotedNativeTypes = $this->scopeFactory->create(
2802: $this->context,
2803: $this->declareStrictTypes,
2804: $this->function,
2805: $this->namespace,
2806: $this->nativeExpressionTypes,
2807: [],
2808: [],
2809: $this->inClosureBindScopeClasses,
2810: $this->anonymousFunctionReflection,
2811: $this->inFirstLevelStatement,
2812: $this->currentlyAssignedExpressions,
2813: $this->currentlyAllowedUndefinedExpressions,
2814: $this->inFunctionCallsStack,
2815: $this->afterExtractCall,
2816: $this->parentScope,
2817: true,
2818: );
2819: }
2820:
2821: private function getTypeFromArrayDimFetch(
2822: Expr\ArrayDimFetch $arrayDimFetch,
2823: Type $offsetType,
2824: Type $offsetAccessibleType,
2825: ): Type
2826: {
2827: if ($arrayDimFetch->dim === null) {
2828: throw new ShouldNotHappenException();
2829: }
2830:
2831: if (!$offsetAccessibleType->isArray()->yes() && (new ObjectType(ArrayAccess::class))->isSuperTypeOf($offsetAccessibleType)->yes()) {
2832: return $this->getType(
2833: new MethodCall(
2834: $arrayDimFetch->var,
2835: new Node\Identifier('offsetGet'),
2836: [
2837: new Node\Arg($arrayDimFetch->dim),
2838: ],
2839: ),
2840: );
2841: }
2842:
2843: return $offsetAccessibleType->getOffsetValueType($offsetType);
2844: }
2845:
2846: private function resolveExactName(string $name): ?string
2847: {
2848: switch (strtolower($name)) {
2849: case 'self':
2850: if (!$this->isInClass()) {
2851: return null;
2852: }
2853: return $this->getClassReflection()->getName();
2854: case 'parent':
2855: if (!$this->isInClass()) {
2856: return null;
2857: }
2858: $currentClassReflection = $this->getClassReflection();
2859: if ($currentClassReflection->getParentClass() !== null) {
2860: return $currentClassReflection->getParentClass()->getName();
2861: }
2862: return null;
2863: case 'static':
2864: return null;
2865: }
2866:
2867: return $name;
2868: }
2869:
2870: /** @api */
2871: public function resolveName(Name $name): string
2872: {
2873: $originalClass = (string) $name;
2874: if ($this->isInClass()) {
2875: $lowerClass = strtolower($originalClass);
2876: if (in_array($lowerClass, [
2877: 'self',
2878: 'static',
2879: ], true)) {
2880: if ($this->inClosureBindScopeClasses !== [] && $this->inClosureBindScopeClasses !== ['static']) {
2881: return $this->inClosureBindScopeClasses[0];
2882: }
2883: return $this->getClassReflection()->getName();
2884: } elseif ($lowerClass === 'parent') {
2885: $currentClassReflection = $this->getClassReflection();
2886: if ($currentClassReflection->getParentClass() !== null) {
2887: return $currentClassReflection->getParentClass()->getName();
2888: }
2889: }
2890: }
2891:
2892: return $originalClass;
2893: }
2894:
2895: /** @api */
2896: public function resolveTypeByName(Name $name): TypeWithClassName
2897: {
2898: if ($name->toLowerString() === 'static' && $this->isInClass()) {
2899: if ($this->inClosureBindScopeClasses !== [] && $this->inClosureBindScopeClasses !== ['static']) {
2900: if ($this->reflectionProvider->hasClass($this->inClosureBindScopeClasses[0])) {
2901: return new StaticType($this->reflectionProvider->getClass($this->inClosureBindScopeClasses[0]));
2902: }
2903: }
2904:
2905: return new StaticType($this->getClassReflection());
2906: }
2907:
2908: $originalClass = $this->resolveName($name);
2909: if ($this->isInClass()) {
2910: if ($this->inClosureBindScopeClasses === [$originalClass]) {
2911: if ($this->reflectionProvider->hasClass($originalClass)) {
2912: return new ThisType($this->reflectionProvider->getClass($originalClass));
2913: }
2914: return new ObjectType($originalClass);
2915: }
2916:
2917: $thisType = new ThisType($this->getClassReflection());
2918: $ancestor = $thisType->getAncestorWithClassName($originalClass);
2919: if ($ancestor !== null) {
2920: return $ancestor;
2921: }
2922: }
2923:
2924: return new ObjectType($originalClass);
2925: }
2926:
2927: private function resolveTypeByNameWithLateStaticBinding(Name $class, Node\Identifier $name): TypeWithClassName
2928: {
2929: $classType = $this->resolveTypeByName($class);
2930:
2931: if (
2932: $classType instanceof StaticType
2933: && !in_array($class->toLowerString(), ['self', 'static', 'parent'], true)
2934: ) {
2935: $methodReflectionCandidate = $this->getMethodReflection(
2936: $classType,
2937: $name->name,
2938: );
2939: if ($methodReflectionCandidate !== null && $methodReflectionCandidate->isStatic()) {
2940: $classType = $classType->getStaticObjectType();
2941: }
2942: }
2943:
2944: return $classType;
2945: }
2946:
2947: /**
2948: * @api
2949: * @param mixed $value
2950: */
2951: public function getTypeFromValue($value): Type
2952: {
2953: return ConstantTypeHelper::getTypeFromValue($value);
2954: }
2955:
2956: /** @api */
2957: public function hasExpressionType(Expr $node): TrinaryLogic
2958: {
2959: if ($node instanceof Variable && is_string($node->name)) {
2960: return $this->hasVariableType($node->name);
2961: }
2962:
2963: $exprString = $this->getNodeKey($node);
2964: if (!isset($this->expressionTypes[$exprString])) {
2965: return TrinaryLogic::createNo();
2966: }
2967: return $this->expressionTypes[$exprString]->getCertainty();
2968: }
2969:
2970: /**
2971: * @param MethodReflection|FunctionReflection|null $reflection
2972: */
2973: public function pushInFunctionCall($reflection, ?ParameterReflection $parameter): self
2974: {
2975: $stack = $this->inFunctionCallsStack;
2976: $stack[] = [$reflection, $parameter];
2977:
2978: return $this->scopeFactory->create(
2979: $this->context,
2980: $this->isDeclareStrictTypes(),
2981: $this->getFunction(),
2982: $this->getNamespace(),
2983: $this->expressionTypes,
2984: $this->nativeExpressionTypes,
2985: $this->conditionalExpressions,
2986: $this->inClosureBindScopeClasses,
2987: $this->anonymousFunctionReflection,
2988: $this->isInFirstLevelStatement(),
2989: $this->currentlyAssignedExpressions,
2990: $this->currentlyAllowedUndefinedExpressions,
2991: $stack,
2992: $this->afterExtractCall,
2993: $this->parentScope,
2994: $this->nativeTypesPromoted,
2995: );
2996: }
2997:
2998: public function popInFunctionCall(): self
2999: {
3000: $stack = $this->inFunctionCallsStack;
3001: array_pop($stack);
3002:
3003: return $this->scopeFactory->create(
3004: $this->context,
3005: $this->isDeclareStrictTypes(),
3006: $this->getFunction(),
3007: $this->getNamespace(),
3008: $this->expressionTypes,
3009: $this->nativeExpressionTypes,
3010: $this->conditionalExpressions,
3011: $this->inClosureBindScopeClasses,
3012: $this->anonymousFunctionReflection,
3013: $this->isInFirstLevelStatement(),
3014: $this->currentlyAssignedExpressions,
3015: $this->currentlyAllowedUndefinedExpressions,
3016: $stack,
3017: $this->afterExtractCall,
3018: $this->parentScope,
3019: $this->nativeTypesPromoted,
3020: );
3021: }
3022:
3023: /** @api */
3024: public function isInClassExists(string $className): bool
3025: {
3026: foreach ($this->inFunctionCallsStack as [$inFunctionCall]) {
3027: if (!$inFunctionCall instanceof FunctionReflection) {
3028: continue;
3029: }
3030:
3031: if (in_array($inFunctionCall->getName(), [
3032: 'class_exists',
3033: 'interface_exists',
3034: 'trait_exists',
3035: ], true)) {
3036: return true;
3037: }
3038: }
3039: $expr = new FuncCall(new FullyQualified('class_exists'), [
3040: new Arg(new String_(ltrim($className, '\\'))),
3041: ]);
3042:
3043: return $this->getType($expr)->isTrue()->yes();
3044: }
3045:
3046: public function getFunctionCallStack(): array
3047: {
3048: return array_values(array_filter(
3049: array_map(static fn ($values) => $values[0], $this->inFunctionCallsStack),
3050: static fn (FunctionReflection|MethodReflection|null $reflection) => $reflection !== null,
3051: ));
3052: }
3053:
3054: public function getFunctionCallStackWithParameters(): array
3055: {
3056: return array_values(array_filter(
3057: $this->inFunctionCallsStack,
3058: static fn ($item) => $item[0] !== null,
3059: ));
3060: }
3061:
3062: /** @api */
3063: public function isInFunctionExists(string $functionName): bool
3064: {
3065: $expr = new FuncCall(new FullyQualified('function_exists'), [
3066: new Arg(new String_(ltrim($functionName, '\\'))),
3067: ]);
3068:
3069: return $this->getType($expr)->isTrue()->yes();
3070: }
3071:
3072: /** @api */
3073: public function enterClass(ClassReflection $classReflection): self
3074: {
3075: $thisHolder = ExpressionTypeHolder::createYes(new Variable('this'), new ThisType($classReflection));
3076: $constantTypes = $this->getConstantTypes();
3077: $constantTypes['$this'] = $thisHolder;
3078: $nativeConstantTypes = $this->getNativeConstantTypes();
3079: $nativeConstantTypes['$this'] = $thisHolder;
3080:
3081: return $this->scopeFactory->create(
3082: $this->context->enterClass($classReflection),
3083: $this->isDeclareStrictTypes(),
3084: null,
3085: $this->getNamespace(),
3086: $constantTypes,
3087: $nativeConstantTypes,
3088: [],
3089: [],
3090: null,
3091: true,
3092: [],
3093: [],
3094: [],
3095: false,
3096: $classReflection->isAnonymous() ? $this : null,
3097: );
3098: }
3099:
3100: public function enterTrait(ClassReflection $traitReflection): self
3101: {
3102: $namespace = null;
3103: $traitName = $traitReflection->getName();
3104: $traitNameParts = explode('\\', $traitName);
3105: if (count($traitNameParts) > 1) {
3106: $namespace = implode('\\', array_slice($traitNameParts, 0, -1));
3107: }
3108: return $this->scopeFactory->create(
3109: $this->context->enterTrait($traitReflection),
3110: $this->isDeclareStrictTypes(),
3111: $this->getFunction(),
3112: $namespace,
3113: $this->expressionTypes,
3114: $this->nativeExpressionTypes,
3115: [],
3116: $this->inClosureBindScopeClasses,
3117: $this->anonymousFunctionReflection,
3118: );
3119: }
3120:
3121: /**
3122: * @api
3123: * @param Type[] $phpDocParameterTypes
3124: * @param Type[] $parameterOutTypes
3125: * @param array<string, bool> $immediatelyInvokedCallableParameters
3126: * @param array<string, Type> $phpDocClosureThisTypeParameters
3127: */
3128: public function enterClassMethod(
3129: Node\Stmt\ClassMethod $classMethod,
3130: TemplateTypeMap $templateTypeMap,
3131: array $phpDocParameterTypes,
3132: ?Type $phpDocReturnType,
3133: ?Type $throwType,
3134: ?string $deprecatedDescription,
3135: bool $isDeprecated,
3136: bool $isInternal,
3137: bool $isFinal,
3138: ?bool $isPure = null,
3139: bool $acceptsNamedArguments = true,
3140: ?Assertions $asserts = null,
3141: ?Type $selfOutType = null,
3142: ?string $phpDocComment = null,
3143: array $parameterOutTypes = [],
3144: array $immediatelyInvokedCallableParameters = [],
3145: array $phpDocClosureThisTypeParameters = [],
3146: bool $isConstructor = false,
3147: ): self
3148: {
3149: if (!$this->isInClass()) {
3150: throw new ShouldNotHappenException();
3151: }
3152:
3153: return $this->enterFunctionLike(
3154: new PhpMethodFromParserNodeReflection(
3155: $this->getClassReflection(),
3156: $classMethod,
3157: null,
3158: $this->getFile(),
3159: $templateTypeMap,
3160: $this->getRealParameterTypes($classMethod),
3161: array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocParameterTypes),
3162: $this->getRealParameterDefaultValues($classMethod),
3163: $this->getParameterAttributes($classMethod),
3164: $this->transformStaticType($this->getFunctionType($classMethod->returnType, false, false)),
3165: $phpDocReturnType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocReturnType)) : null,
3166: $throwType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($throwType)) : null,
3167: $deprecatedDescription,
3168: $isDeprecated,
3169: $isInternal,
3170: $isFinal,
3171: $isPure,
3172: $acceptsNamedArguments,
3173: $asserts ?? Assertions::createEmpty(),
3174: $selfOutType,
3175: $phpDocComment,
3176: array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $parameterOutTypes),
3177: $immediatelyInvokedCallableParameters,
3178: array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocClosureThisTypeParameters),
3179: $isConstructor,
3180: $this->attributeReflectionFactory->fromAttrGroups($classMethod->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $classMethod)),
3181: ),
3182: !$classMethod->isStatic(),
3183: );
3184: }
3185:
3186: /**
3187: * @param Type[] $phpDocParameterTypes
3188: */
3189: public function enterPropertyHook(
3190: Node\PropertyHook $hook,
3191: string $propertyName,
3192: Identifier|Name|ComplexType|null $nativePropertyTypeNode,
3193: ?Type $phpDocPropertyType,
3194: array $phpDocParameterTypes,
3195: ?Type $throwType,
3196: ?string $deprecatedDescription,
3197: bool $isDeprecated,
3198: ?string $phpDocComment,
3199: ): self
3200: {
3201: if (!$this->isInClass()) {
3202: throw new ShouldNotHappenException();
3203: }
3204:
3205: $phpDocParameterTypes = array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocParameterTypes);
3206:
3207: $hookName = $hook->name->toLowerString();
3208: if ($hookName === 'set') {
3209: if ($hook->params === []) {
3210: $hook = clone $hook;
3211: $hook->params = [
3212: new Node\Param(new Variable('value'), type: $nativePropertyTypeNode),
3213: ];
3214: }
3215:
3216: $firstParam = $hook->params[0] ?? null;
3217: if (
3218: $firstParam !== null
3219: && $phpDocPropertyType !== null
3220: && $firstParam->var instanceof Variable
3221: && is_string($firstParam->var->name)
3222: ) {
3223: $valueParamPhpDocType = $phpDocParameterTypes[$firstParam->var->name] ?? null;
3224: if ($valueParamPhpDocType === null) {
3225: $phpDocParameterTypes[$firstParam->var->name] = $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocPropertyType));
3226: }
3227: }
3228:
3229: $realReturnType = new VoidType();
3230: $phpDocReturnType = null;
3231: } elseif ($hookName === 'get') {
3232: $realReturnType = $this->getFunctionType($nativePropertyTypeNode, false, false);
3233: $phpDocReturnType = $phpDocPropertyType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocPropertyType)) : null;
3234: } else {
3235: throw new ShouldNotHappenException();
3236: }
3237:
3238: $realParameterTypes = $this->getRealParameterTypes($hook);
3239:
3240: return $this->enterFunctionLike(
3241: new PhpMethodFromParserNodeReflection(
3242: $this->getClassReflection(),
3243: $hook,
3244: $propertyName,
3245: $this->getFile(),
3246: TemplateTypeMap::createEmpty(),
3247: $realParameterTypes,
3248: $phpDocParameterTypes,
3249: [],
3250: $this->getParameterAttributes($hook),
3251: $realReturnType,
3252: $phpDocReturnType,
3253: $throwType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($throwType)) : null,
3254: $deprecatedDescription,
3255: $isDeprecated,
3256: false,
3257: false,
3258: false,
3259: true,
3260: Assertions::createEmpty(),
3261: null,
3262: $phpDocComment,
3263: [],
3264: [],
3265: [],
3266: false,
3267: $this->attributeReflectionFactory->fromAttrGroups($hook->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $hook)),
3268: ),
3269: true,
3270: );
3271: }
3272:
3273: private function transformStaticType(Type $type): Type
3274: {
3275: return TypeTraverser::map($type, function (Type $type, callable $traverse): Type {
3276: if (!$this->isInClass()) {
3277: return $type;
3278: }
3279: if ($type instanceof StaticType) {
3280: $classReflection = $this->getClassReflection();
3281: $changedType = $type->changeBaseClass($classReflection);
3282: if ($classReflection->isFinal() && !$type instanceof ThisType) {
3283: $changedType = $changedType->getStaticObjectType();
3284: }
3285: return $traverse($changedType);
3286: }
3287:
3288: return $traverse($type);
3289: });
3290: }
3291:
3292: /**
3293: * @return Type[]
3294: */
3295: private function getRealParameterTypes(Node\FunctionLike $functionLike): array
3296: {
3297: $realParameterTypes = [];
3298: foreach ($functionLike->getParams() as $parameter) {
3299: if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
3300: throw new ShouldNotHappenException();
3301: }
3302: $realParameterTypes[$parameter->var->name] = $this->getFunctionType(
3303: $parameter->type,
3304: $this->isParameterValueNullable($parameter) && $parameter->flags === 0,
3305: false,
3306: );
3307: }
3308:
3309: return $realParameterTypes;
3310: }
3311:
3312: /**
3313: * @return Type[]
3314: */
3315: private function getRealParameterDefaultValues(Node\FunctionLike $functionLike): array
3316: {
3317: $realParameterDefaultValues = [];
3318: foreach ($functionLike->getParams() as $parameter) {
3319: if ($parameter->default === null) {
3320: continue;
3321: }
3322: if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
3323: throw new ShouldNotHappenException();
3324: }
3325: $realParameterDefaultValues[$parameter->var->name] = $this->getType($parameter->default);
3326: }
3327:
3328: return $realParameterDefaultValues;
3329: }
3330:
3331: /**
3332: * @return array<string, list<AttributeReflection>>
3333: */
3334: private function getParameterAttributes(ClassMethod|Function_|PropertyHook $functionLike): array
3335: {
3336: $parameterAttributes = [];
3337: $className = null;
3338: if ($this->isInClass()) {
3339: $className = $this->getClassReflection()->getName();
3340: }
3341: foreach ($functionLike->getParams() as $parameter) {
3342: if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
3343: throw new ShouldNotHappenException();
3344: }
3345:
3346: $parameterAttributes[$parameter->var->name] = $this->attributeReflectionFactory->fromAttrGroups($parameter->attrGroups, InitializerExprContext::fromStubParameter($className, $this->getFile(), $functionLike));
3347: }
3348:
3349: return $parameterAttributes;
3350: }
3351:
3352: /**
3353: * @api
3354: * @param Type[] $phpDocParameterTypes
3355: * @param Type[] $parameterOutTypes
3356: * @param array<string, bool> $immediatelyInvokedCallableParameters
3357: * @param array<string, Type> $phpDocClosureThisTypeParameters
3358: */
3359: public function enterFunction(
3360: Node\Stmt\Function_ $function,
3361: TemplateTypeMap $templateTypeMap,
3362: array $phpDocParameterTypes,
3363: ?Type $phpDocReturnType,
3364: ?Type $throwType,
3365: ?string $deprecatedDescription,
3366: bool $isDeprecated,
3367: bool $isInternal,
3368: ?bool $isPure = null,
3369: bool $acceptsNamedArguments = true,
3370: ?Assertions $asserts = null,
3371: ?string $phpDocComment = null,
3372: array $parameterOutTypes = [],
3373: array $immediatelyInvokedCallableParameters = [],
3374: array $phpDocClosureThisTypeParameters = [],
3375: ): self
3376: {
3377: return $this->enterFunctionLike(
3378: new PhpFunctionFromParserNodeReflection(
3379: $function,
3380: $this->getFile(),
3381: $templateTypeMap,
3382: $this->getRealParameterTypes($function),
3383: array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $phpDocParameterTypes),
3384: $this->getRealParameterDefaultValues($function),
3385: $this->getParameterAttributes($function),
3386: $this->getFunctionType($function->returnType, $function->returnType === null, false),
3387: $phpDocReturnType !== null ? TemplateTypeHelper::toArgument($phpDocReturnType) : null,
3388: $throwType,
3389: $deprecatedDescription,
3390: $isDeprecated,
3391: $isInternal,
3392: $isPure,
3393: $acceptsNamedArguments,
3394: $asserts ?? Assertions::createEmpty(),
3395: $phpDocComment,
3396: array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $parameterOutTypes),
3397: $immediatelyInvokedCallableParameters,
3398: $phpDocClosureThisTypeParameters,
3399: $this->attributeReflectionFactory->fromAttrGroups($function->attrGroups, InitializerExprContext::fromStubParameter(null, $this->getFile(), $function)),
3400: ),
3401: false,
3402: );
3403: }
3404:
3405: private function enterFunctionLike(
3406: PhpFunctionFromParserNodeReflection $functionReflection,
3407: bool $preserveConstructorScope,
3408: ): self
3409: {
3410: $parametersByName = [];
3411:
3412: foreach ($functionReflection->getParameters() as $parameter) {
3413: $parametersByName[$parameter->getName()] = $parameter;
3414: }
3415:
3416: $expressionTypes = [];
3417: $nativeExpressionTypes = [];
3418: $conditionalTypes = [];
3419:
3420: if ($preserveConstructorScope) {
3421: $expressionTypes = $this->rememberConstructorExpressions($this->expressionTypes);
3422: $nativeExpressionTypes = $this->rememberConstructorExpressions($this->nativeExpressionTypes);
3423: }
3424:
3425: foreach ($functionReflection->getParameters() as $parameter) {
3426: $parameterType = $parameter->getType();
3427:
3428: if ($parameterType instanceof ConditionalTypeForParameter) {
3429: $targetParameterName = substr($parameterType->getParameterName(), 1);
3430: if (array_key_exists($targetParameterName, $parametersByName)) {
3431: $targetParameter = $parametersByName[$targetParameterName];
3432:
3433: $ifType = $parameterType->isNegated() ? $parameterType->getElse() : $parameterType->getIf();
3434: $elseType = $parameterType->isNegated() ? $parameterType->getIf() : $parameterType->getElse();
3435:
3436: $holder = new ConditionalExpressionHolder([
3437: $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::intersect($targetParameter->getType(), $parameterType->getTarget())),
3438: ], new ExpressionTypeHolder(new Variable($parameter->getName()), $ifType, TrinaryLogic::createYes()));
3439: $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder;
3440:
3441: $holder = new ConditionalExpressionHolder([
3442: $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::remove($targetParameter->getType(), $parameterType->getTarget())),
3443: ], new ExpressionTypeHolder(new Variable($parameter->getName()), $elseType, TrinaryLogic::createYes()));
3444: $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder;
3445: }
3446: }
3447:
3448: $paramExprString = '$' . $parameter->getName();
3449: if ($parameter->isVariadic()) {
3450: if (!$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) {
3451: $parameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $parameterType);
3452: } else {
3453: $parameterType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $parameterType), new AccessoryArrayListType());
3454: }
3455: }
3456: $parameterNode = new Variable($parameter->getName());
3457: $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameterNode, $parameterType);
3458:
3459: $parameterOriginalValueExpr = new ParameterVariableOriginalValueExpr($parameter->getName());
3460: $parameterOriginalValueExprString = $this->getNodeKey($parameterOriginalValueExpr);
3461: $expressionTypes[$parameterOriginalValueExprString] = ExpressionTypeHolder::createYes($parameterOriginalValueExpr, $parameterType);
3462:
3463: $nativeParameterType = $parameter->getNativeType();
3464: if ($parameter->isVariadic()) {
3465: if (!$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) {
3466: $nativeParameterType = new ArrayType(new UnionType([new IntegerType(), new StringType()]), $nativeParameterType);
3467: } else {
3468: $nativeParameterType = TypeCombinator::intersect(new ArrayType(new IntegerType(), $nativeParameterType), new AccessoryArrayListType());
3469: }
3470: }
3471: $nativeExpressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameterNode, $nativeParameterType);
3472: $nativeExpressionTypes[$parameterOriginalValueExprString] = ExpressionTypeHolder::createYes($parameterOriginalValueExpr, $nativeParameterType);
3473: }
3474:
3475: return $this->scopeFactory->create(
3476: $this->context,
3477: $this->isDeclareStrictTypes(),
3478: $functionReflection,
3479: $this->getNamespace(),
3480: array_merge($this->getConstantTypes(), $expressionTypes),
3481: array_merge($this->getNativeConstantTypes(), $nativeExpressionTypes),
3482: $conditionalTypes,
3483: );
3484: }
3485:
3486: /** @api */
3487: public function enterNamespace(string $namespaceName): self
3488: {
3489: return $this->scopeFactory->create(
3490: $this->context->beginFile(),
3491: $this->isDeclareStrictTypes(),
3492: null,
3493: $namespaceName,
3494: );
3495: }
3496:
3497: /**
3498: * @param list<string> $scopeClasses
3499: */
3500: public function enterClosureBind(?Type $thisType, ?Type $nativeThisType, array $scopeClasses): self
3501: {
3502: $expressionTypes = $this->expressionTypes;
3503: if ($thisType !== null) {
3504: $expressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $thisType);
3505: } else {
3506: unset($expressionTypes['$this']);
3507: }
3508:
3509: $nativeExpressionTypes = $this->nativeExpressionTypes;
3510: if ($nativeThisType !== null) {
3511: $nativeExpressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $nativeThisType);
3512: } else {
3513: unset($nativeExpressionTypes['$this']);
3514: }
3515:
3516: if ($scopeClasses === ['static'] && $this->isInClass()) {
3517: $scopeClasses = [$this->getClassReflection()->getName()];
3518: }
3519:
3520: return $this->scopeFactory->create(
3521: $this->context,
3522: $this->isDeclareStrictTypes(),
3523: $this->getFunction(),
3524: $this->getNamespace(),
3525: $expressionTypes,
3526: $nativeExpressionTypes,
3527: $this->conditionalExpressions,
3528: $scopeClasses,
3529: $this->anonymousFunctionReflection,
3530: );
3531: }
3532:
3533: public function restoreOriginalScopeAfterClosureBind(self $originalScope): self
3534: {
3535: $expressionTypes = $this->expressionTypes;
3536: if (isset($originalScope->expressionTypes['$this'])) {
3537: $expressionTypes['$this'] = $originalScope->expressionTypes['$this'];
3538: } else {
3539: unset($expressionTypes['$this']);
3540: }
3541:
3542: $nativeExpressionTypes = $this->nativeExpressionTypes;
3543: if (isset($originalScope->nativeExpressionTypes['$this'])) {
3544: $nativeExpressionTypes['$this'] = $originalScope->nativeExpressionTypes['$this'];
3545: } else {
3546: unset($nativeExpressionTypes['$this']);
3547: }
3548:
3549: return $this->scopeFactory->create(
3550: $this->context,
3551: $this->isDeclareStrictTypes(),
3552: $this->getFunction(),
3553: $this->getNamespace(),
3554: $expressionTypes,
3555: $nativeExpressionTypes,
3556: $this->conditionalExpressions,
3557: $originalScope->inClosureBindScopeClasses,
3558: $this->anonymousFunctionReflection,
3559: );
3560: }
3561:
3562: public function restoreThis(self $restoreThisScope): self
3563: {
3564: $expressionTypes = $this->expressionTypes;
3565: $nativeExpressionTypes = $this->nativeExpressionTypes;
3566:
3567: if ($restoreThisScope->isInClass()) {
3568: $nodeFinder = new NodeFinder();
3569: $cb = static fn ($expr) => $expr instanceof Variable && $expr->name === 'this';
3570: foreach ($restoreThisScope->expressionTypes as $exprString => $expressionTypeHolder) {
3571: $expr = $expressionTypeHolder->getExpr();
3572: $thisExpr = $nodeFinder->findFirst([$expr], $cb);
3573: if ($thisExpr === null) {
3574: continue;
3575: }
3576:
3577: $expressionTypes[$exprString] = $expressionTypeHolder;
3578: }
3579:
3580: foreach ($restoreThisScope->nativeExpressionTypes as $exprString => $expressionTypeHolder) {
3581: $expr = $expressionTypeHolder->getExpr();
3582: $thisExpr = $nodeFinder->findFirst([$expr], $cb);
3583: if ($thisExpr === null) {
3584: continue;
3585: }
3586:
3587: $nativeExpressionTypes[$exprString] = $expressionTypeHolder;
3588: }
3589: } else {
3590: unset($expressionTypes['$this']);
3591: unset($nativeExpressionTypes['$this']);
3592: }
3593:
3594: return $this->scopeFactory->create(
3595: $this->context,
3596: $this->isDeclareStrictTypes(),
3597: $this->getFunction(),
3598: $this->getNamespace(),
3599: $expressionTypes,
3600: $nativeExpressionTypes,
3601: $this->conditionalExpressions,
3602: $this->inClosureBindScopeClasses,
3603: $this->anonymousFunctionReflection,
3604: $this->inFirstLevelStatement,
3605: [],
3606: [],
3607: $this->inFunctionCallsStack,
3608: $this->afterExtractCall,
3609: $this->parentScope,
3610: $this->nativeTypesPromoted,
3611: );
3612: }
3613:
3614: public function enterClosureCall(Type $thisType, Type $nativeThisType): self
3615: {
3616: $expressionTypes = $this->expressionTypes;
3617: $expressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $thisType);
3618:
3619: $nativeExpressionTypes = $this->nativeExpressionTypes;
3620: $nativeExpressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $nativeThisType);
3621:
3622: return $this->scopeFactory->create(
3623: $this->context,
3624: $this->isDeclareStrictTypes(),
3625: $this->getFunction(),
3626: $this->getNamespace(),
3627: $expressionTypes,
3628: $nativeExpressionTypes,
3629: $this->conditionalExpressions,
3630: $thisType->getObjectClassNames(),
3631: $this->anonymousFunctionReflection,
3632: );
3633: }
3634:
3635: /** @api */
3636: public function isInClosureBind(): bool
3637: {
3638: return $this->inClosureBindScopeClasses !== [];
3639: }
3640:
3641: /**
3642: * @api
3643: * @param ParameterReflection[]|null $callableParameters
3644: */
3645: public function enterAnonymousFunction(
3646: Expr\Closure $closure,
3647: ?array $callableParameters,
3648: ): self
3649: {
3650: $anonymousFunctionReflection = $this->getType($closure);
3651: if (!$anonymousFunctionReflection instanceof ClosureType) {
3652: throw new ShouldNotHappenException();
3653: }
3654:
3655: $scope = $this->enterAnonymousFunctionWithoutReflection($closure, $callableParameters);
3656:
3657: return $this->scopeFactory->create(
3658: $scope->context,
3659: $scope->isDeclareStrictTypes(),
3660: $scope->getFunction(),
3661: $scope->getNamespace(),
3662: $scope->expressionTypes,
3663: $scope->nativeExpressionTypes,
3664: [],
3665: $scope->inClosureBindScopeClasses,
3666: $anonymousFunctionReflection,
3667: true,
3668: [],
3669: [],
3670: $this->inFunctionCallsStack,
3671: false,
3672: $this,
3673: $this->nativeTypesPromoted,
3674: );
3675: }
3676:
3677: /**
3678: * @param ParameterReflection[]|null $callableParameters
3679: */
3680: private function enterAnonymousFunctionWithoutReflection(
3681: Expr\Closure $closure,
3682: ?array $callableParameters,
3683: ): self
3684: {
3685: $expressionTypes = [];
3686: $nativeTypes = [];
3687: foreach ($closure->params as $i => $parameter) {
3688: if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
3689: throw new ShouldNotHappenException();
3690: }
3691: $paramExprString = sprintf('$%s', $parameter->var->name);
3692: $isNullable = $this->isParameterValueNullable($parameter);
3693: $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
3694: if ($callableParameters !== null) {
3695: if (isset($callableParameters[$i])) {
3696: $parameterType = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType());
3697: } elseif (count($callableParameters) > 0) {
3698: $lastParameter = $callableParameters[count($callableParameters) - 1];
3699: if ($lastParameter->isVariadic()) {
3700: $parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType());
3701: } else {
3702: $parameterType = self::intersectButNotNever($parameterType, new MixedType());
3703: }
3704: } else {
3705: $parameterType = self::intersectButNotNever($parameterType, new MixedType());
3706: }
3707: }
3708: $holder = ExpressionTypeHolder::createYes($parameter->var, $parameterType);
3709: $expressionTypes[$paramExprString] = $holder;
3710: $nativeTypes[$paramExprString] = $holder;
3711: }
3712:
3713: $nonRefVariableNames = [];
3714: foreach ($closure->uses as $use) {
3715: if (!is_string($use->var->name)) {
3716: throw new ShouldNotHappenException();
3717: }
3718: $variableName = $use->var->name;
3719: $paramExprString = '$' . $use->var->name;
3720: if ($use->byRef) {
3721: $holder = ExpressionTypeHolder::createYes($use->var, new MixedType());
3722: $expressionTypes[$paramExprString] = $holder;
3723: $nativeTypes[$paramExprString] = $holder;
3724: continue;
3725: }
3726: $nonRefVariableNames[$variableName] = true;
3727: if ($this->hasVariableType($variableName)->no()) {
3728: $variableType = new ErrorType();
3729: $variableNativeType = new ErrorType();
3730: } else {
3731: $variableType = $this->getVariableType($variableName);
3732: $variableNativeType = $this->getNativeType($use->var);
3733: }
3734: $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($use->var, $variableType);
3735: $nativeTypes[$paramExprString] = ExpressionTypeHolder::createYes($use->var, $variableNativeType);
3736: }
3737:
3738: $nonStaticExpressions = $this->invalidateStaticExpressions($this->expressionTypes);
3739: foreach ($nonStaticExpressions as $exprString => $typeHolder) {
3740: $expr = $typeHolder->getExpr();
3741:
3742: if ($expr instanceof Variable) {
3743: continue;
3744: }
3745:
3746: $variables = (new NodeFinder())->findInstanceOf([$expr], Variable::class);
3747: if ($variables === [] && !$this->expressionTypeIsUnchangeable($typeHolder)) {
3748: continue;
3749: }
3750:
3751: foreach ($variables as $variable) {
3752: if (!is_string($variable->name)) {
3753: continue 2;
3754: }
3755: if (!array_key_exists($variable->name, $nonRefVariableNames)) {
3756: continue 2;
3757: }
3758: }
3759:
3760: $expressionTypes[$exprString] = $typeHolder;
3761: }
3762:
3763: if ($this->hasVariableType('this')->yes() && !$closure->static) {
3764: $node = new Variable('this');
3765: $expressionTypes['$this'] = ExpressionTypeHolder::createYes($node, $this->getType($node));
3766: $nativeTypes['$this'] = ExpressionTypeHolder::createYes($node, $this->getNativeType($node));
3767:
3768: if ($this->phpVersion->supportsReadOnlyProperties()) {
3769: foreach ($nonStaticExpressions as $exprString => $typeHolder) {
3770: $expr = $typeHolder->getExpr();
3771:
3772: if (!$expr instanceof PropertyFetch) {
3773: continue;
3774: }
3775:
3776: if (!$this->isReadonlyPropertyFetch($expr, true)) {
3777: continue;
3778: }
3779:
3780: $expressionTypes[$exprString] = $typeHolder;
3781: }
3782: }
3783: }
3784:
3785: return $this->scopeFactory->create(
3786: $this->context,
3787: $this->isDeclareStrictTypes(),
3788: $this->getFunction(),
3789: $this->getNamespace(),
3790: array_merge($this->getConstantTypes(), $expressionTypes),
3791: array_merge($this->getNativeConstantTypes(), $nativeTypes),
3792: [],
3793: $this->inClosureBindScopeClasses,
3794: new TrivialParametersAcceptor(),
3795: true,
3796: [],
3797: [],
3798: [],
3799: false,
3800: $this,
3801: $this->nativeTypesPromoted,
3802: );
3803: }
3804:
3805: private function expressionTypeIsUnchangeable(ExpressionTypeHolder $typeHolder): bool
3806: {
3807: $expr = $typeHolder->getExpr();
3808: $type = $typeHolder->getType();
3809:
3810: return $expr instanceof FuncCall
3811: && !$expr->isFirstClassCallable()
3812: && $expr->name instanceof FullyQualified
3813: && $expr->name->toLowerString() === 'function_exists'
3814: && isset($expr->getArgs()[0])
3815: && count($this->getType($expr->getArgs()[0]->value)->getConstantStrings()) === 1
3816: && $type->isTrue()->yes();
3817: }
3818:
3819: /**
3820: * @param array<string, ExpressionTypeHolder> $expressionTypes
3821: * @return array<string, ExpressionTypeHolder>
3822: */
3823: private function invalidateStaticExpressions(array $expressionTypes): array
3824: {
3825: $filteredExpressionTypes = [];
3826: $nodeFinder = new NodeFinder();
3827: foreach ($expressionTypes as $exprString => $expressionType) {
3828: $staticExpression = $nodeFinder->findFirst(
3829: [$expressionType->getExpr()],
3830: static fn ($node) => $node instanceof Expr\StaticCall || $node instanceof Expr\StaticPropertyFetch,
3831: );
3832: if ($staticExpression !== null) {
3833: continue;
3834: }
3835: $filteredExpressionTypes[$exprString] = $expressionType;
3836: }
3837: return $filteredExpressionTypes;
3838: }
3839:
3840: /**
3841: * @api
3842: * @param ParameterReflection[]|null $callableParameters
3843: */
3844: public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $callableParameters): self
3845: {
3846: $anonymousFunctionReflection = $this->getType($arrowFunction);
3847: if (!$anonymousFunctionReflection instanceof ClosureType) {
3848: throw new ShouldNotHappenException();
3849: }
3850:
3851: $scope = $this->enterArrowFunctionWithoutReflection($arrowFunction, $callableParameters);
3852:
3853: return $this->scopeFactory->create(
3854: $scope->context,
3855: $scope->isDeclareStrictTypes(),
3856: $scope->getFunction(),
3857: $scope->getNamespace(),
3858: $scope->expressionTypes,
3859: $scope->nativeExpressionTypes,
3860: $scope->conditionalExpressions,
3861: $scope->inClosureBindScopeClasses,
3862: $anonymousFunctionReflection,
3863: true,
3864: [],
3865: [],
3866: $this->inFunctionCallsStack,
3867: $scope->afterExtractCall,
3868: $scope->parentScope,
3869: $this->nativeTypesPromoted,
3870: );
3871: }
3872:
3873: /**
3874: * @param ParameterReflection[]|null $callableParameters
3875: */
3876: private function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFunction, ?array $callableParameters): self
3877: {
3878: $arrowFunctionScope = $this;
3879: foreach ($arrowFunction->params as $i => $parameter) {
3880: if ($parameter->type === null) {
3881: $parameterType = new MixedType();
3882: } else {
3883: $isNullable = $this->isParameterValueNullable($parameter);
3884: $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
3885: }
3886:
3887: if ($callableParameters !== null) {
3888: if (isset($callableParameters[$i])) {
3889: $parameterType = self::intersectButNotNever($parameterType, $callableParameters[$i]->getType());
3890: } elseif (count($callableParameters) > 0) {
3891: $lastParameter = $callableParameters[count($callableParameters) - 1];
3892: if ($lastParameter->isVariadic()) {
3893: $parameterType = self::intersectButNotNever($parameterType, $lastParameter->getType());
3894: } else {
3895: $parameterType = self::intersectButNotNever($parameterType, new MixedType());
3896: }
3897: } else {
3898: $parameterType = self::intersectButNotNever($parameterType, new MixedType());
3899: }
3900: }
3901:
3902: if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
3903: throw new ShouldNotHappenException();
3904: }
3905: $arrowFunctionScope = $arrowFunctionScope->assignVariable($parameter->var->name, $parameterType, $parameterType, TrinaryLogic::createYes());
3906: }
3907:
3908: if ($arrowFunction->static) {
3909: $arrowFunctionScope = $arrowFunctionScope->invalidateExpression(new Variable('this'));
3910: }
3911:
3912: return $this->scopeFactory->create(
3913: $arrowFunctionScope->context,
3914: $this->isDeclareStrictTypes(),
3915: $arrowFunctionScope->getFunction(),
3916: $arrowFunctionScope->getNamespace(),
3917: $this->invalidateStaticExpressions($arrowFunctionScope->expressionTypes),
3918: $arrowFunctionScope->nativeExpressionTypes,
3919: $arrowFunctionScope->conditionalExpressions,
3920: $arrowFunctionScope->inClosureBindScopeClasses,
3921: new TrivialParametersAcceptor(),
3922: true,
3923: [],
3924: [],
3925: [],
3926: $arrowFunctionScope->afterExtractCall,
3927: $arrowFunctionScope->parentScope,
3928: $this->nativeTypesPromoted,
3929: );
3930: }
3931:
3932: public function isParameterValueNullable(Node\Param $parameter): bool
3933: {
3934: if ($parameter->default instanceof ConstFetch) {
3935: return strtolower((string) $parameter->default->name) === 'null';
3936: }
3937:
3938: return false;
3939: }
3940:
3941: /**
3942: * @api
3943: * @param Node\Name|Node\Identifier|Node\ComplexType|null $type
3944: */
3945: public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type
3946: {
3947: if ($isNullable) {
3948: return TypeCombinator::addNull(
3949: $this->getFunctionType($type, false, $isVariadic),
3950: );
3951: }
3952: if ($isVariadic) {
3953: if (!$this->getPhpVersion()->supportsNamedArguments()->no()) {
3954: return new ArrayType(new UnionType([new IntegerType(), new StringType()]), $this->getFunctionType(
3955: $type,
3956: false,
3957: false,
3958: ));
3959: }
3960:
3961: return TypeCombinator::intersect(new ArrayType(new IntegerType(), $this->getFunctionType(
3962: $type,
3963: false,
3964: false,
3965: )), new AccessoryArrayListType());
3966: }
3967:
3968: if ($type instanceof Name) {
3969: $className = (string) $type;
3970: $lowercasedClassName = strtolower($className);
3971: if ($lowercasedClassName === 'parent') {
3972: if ($this->isInClass() && $this->getClassReflection()->getParentClass() !== null) {
3973: return new ObjectType($this->getClassReflection()->getParentClass()->getName());
3974: }
3975:
3976: return new NonexistentParentClassType();
3977: }
3978: }
3979:
3980: return ParserNodeTypeToPHPStanType::resolve($type, $this->isInClass() ? $this->getClassReflection() : null);
3981: }
3982:
3983: private static function intersectButNotNever(Type $nativeType, Type $inferredType): Type
3984: {
3985: if ($nativeType->isSuperTypeOf($inferredType)->no()) {
3986: return $nativeType;
3987: }
3988:
3989: $result = TypeCombinator::intersect($nativeType, $inferredType);
3990: if (TypeCombinator::containsNull($nativeType)) {
3991: return TypeCombinator::addNull($result);
3992: }
3993:
3994: return $result;
3995: }
3996:
3997: public function enterMatch(Expr\Match_ $expr): self
3998: {
3999: if ($expr->cond instanceof Variable) {
4000: return $this;
4001: }
4002: if ($expr->cond instanceof AlwaysRememberedExpr) {
4003: $cond = $expr->cond->expr;
4004: } else {
4005: $cond = $expr->cond;
4006: }
4007:
4008: $type = $this->getType($cond);
4009: $nativeType = $this->getNativeType($cond);
4010: $condExpr = new AlwaysRememberedExpr($cond, $type, $nativeType);
4011: $expr->cond = $condExpr;
4012:
4013: return $this->assignExpression($condExpr, $type, $nativeType);
4014: }
4015:
4016: public function enterForeach(self $originalScope, Expr $iteratee, string $valueName, ?string $keyName): self
4017: {
4018: $iterateeType = $originalScope->getType($iteratee);
4019: $nativeIterateeType = $originalScope->getNativeType($iteratee);
4020: $scope = $this->assignVariable(
4021: $valueName,
4022: $originalScope->getIterableValueType($iterateeType),
4023: $originalScope->getIterableValueType($nativeIterateeType),
4024: TrinaryLogic::createYes(),
4025: );
4026: if ($keyName !== null) {
4027: $scope = $scope->enterForeachKey($originalScope, $iteratee, $keyName);
4028: }
4029:
4030: return $scope;
4031: }
4032:
4033: public function enterForeachKey(self $originalScope, Expr $iteratee, string $keyName): self
4034: {
4035: $iterateeType = $originalScope->getType($iteratee);
4036: $nativeIterateeType = $originalScope->getNativeType($iteratee);
4037: $scope = $this->assignVariable(
4038: $keyName,
4039: $originalScope->getIterableKeyType($iterateeType),
4040: $originalScope->getIterableKeyType($nativeIterateeType),
4041: TrinaryLogic::createYes(),
4042: );
4043:
4044: if ($iterateeType->isArray()->yes()) {
4045: $scope = $scope->assignExpression(
4046: new Expr\ArrayDimFetch($iteratee, new Variable($keyName)),
4047: $originalScope->getIterableValueType($iterateeType),
4048: $originalScope->getIterableValueType($nativeIterateeType),
4049: );
4050: }
4051:
4052: return $scope;
4053: }
4054:
4055: public function enterCatchType(Type $catchType, ?string $variableName): self
4056: {
4057: if ($variableName === null) {
4058: return $this;
4059: }
4060:
4061: return $this->assignVariable(
4062: $variableName,
4063: TypeCombinator::intersect($catchType, new ObjectType(Throwable::class)),
4064: TypeCombinator::intersect($catchType, new ObjectType(Throwable::class)),
4065: TrinaryLogic::createYes(),
4066: );
4067: }
4068:
4069: public function enterExpressionAssign(Expr $expr): self
4070: {
4071: $exprString = $this->getNodeKey($expr);
4072: $currentlyAssignedExpressions = $this->currentlyAssignedExpressions;
4073: $currentlyAssignedExpressions[$exprString] = true;
4074:
4075: $scope = $this->scopeFactory->create(
4076: $this->context,
4077: $this->isDeclareStrictTypes(),
4078: $this->getFunction(),
4079: $this->getNamespace(),
4080: $this->expressionTypes,
4081: $this->nativeExpressionTypes,
4082: $this->conditionalExpressions,
4083: $this->inClosureBindScopeClasses,
4084: $this->anonymousFunctionReflection,
4085: $this->isInFirstLevelStatement(),
4086: $currentlyAssignedExpressions,
4087: $this->currentlyAllowedUndefinedExpressions,
4088: [],
4089: $this->afterExtractCall,
4090: $this->parentScope,
4091: $this->nativeTypesPromoted,
4092: );
4093: $scope->resolvedTypes = $this->resolvedTypes;
4094: $scope->truthyScopes = $this->truthyScopes;
4095: $scope->falseyScopes = $this->falseyScopes;
4096:
4097: return $scope;
4098: }
4099:
4100: public function exitExpressionAssign(Expr $expr): self
4101: {
4102: $exprString = $this->getNodeKey($expr);
4103: $currentlyAssignedExpressions = $this->currentlyAssignedExpressions;
4104: unset($currentlyAssignedExpressions[$exprString]);
4105:
4106: $scope = $this->scopeFactory->create(
4107: $this->context,
4108: $this->isDeclareStrictTypes(),
4109: $this->getFunction(),
4110: $this->getNamespace(),
4111: $this->expressionTypes,
4112: $this->nativeExpressionTypes,
4113: $this->conditionalExpressions,
4114: $this->inClosureBindScopeClasses,
4115: $this->anonymousFunctionReflection,
4116: $this->isInFirstLevelStatement(),
4117: $currentlyAssignedExpressions,
4118: $this->currentlyAllowedUndefinedExpressions,
4119: [],
4120: $this->afterExtractCall,
4121: $this->parentScope,
4122: $this->nativeTypesPromoted,
4123: );
4124: $scope->resolvedTypes = $this->resolvedTypes;
4125: $scope->truthyScopes = $this->truthyScopes;
4126: $scope->falseyScopes = $this->falseyScopes;
4127:
4128: return $scope;
4129: }
4130:
4131: /** @api */
4132: public function isInExpressionAssign(Expr $expr): bool
4133: {
4134: $exprString = $this->getNodeKey($expr);
4135: return array_key_exists($exprString, $this->currentlyAssignedExpressions);
4136: }
4137:
4138: public function setAllowedUndefinedExpression(Expr $expr): self
4139: {
4140: if ($expr instanceof Expr\StaticPropertyFetch) {
4141: return $this;
4142: }
4143:
4144: $exprString = $this->getNodeKey($expr);
4145: $currentlyAllowedUndefinedExpressions = $this->currentlyAllowedUndefinedExpressions;
4146: $currentlyAllowedUndefinedExpressions[$exprString] = true;
4147:
4148: $scope = $this->scopeFactory->create(
4149: $this->context,
4150: $this->isDeclareStrictTypes(),
4151: $this->getFunction(),
4152: $this->getNamespace(),
4153: $this->expressionTypes,
4154: $this->nativeExpressionTypes,
4155: $this->conditionalExpressions,
4156: $this->inClosureBindScopeClasses,
4157: $this->anonymousFunctionReflection,
4158: $this->isInFirstLevelStatement(),
4159: $this->currentlyAssignedExpressions,
4160: $currentlyAllowedUndefinedExpressions,
4161: [],
4162: $this->afterExtractCall,
4163: $this->parentScope,
4164: $this->nativeTypesPromoted,
4165: );
4166: $scope->resolvedTypes = $this->resolvedTypes;
4167: $scope->truthyScopes = $this->truthyScopes;
4168: $scope->falseyScopes = $this->falseyScopes;
4169:
4170: return $scope;
4171: }
4172:
4173: public function unsetAllowedUndefinedExpression(Expr $expr): self
4174: {
4175: $exprString = $this->getNodeKey($expr);
4176: $currentlyAllowedUndefinedExpressions = $this->currentlyAllowedUndefinedExpressions;
4177: unset($currentlyAllowedUndefinedExpressions[$exprString]);
4178:
4179: $scope = $this->scopeFactory->create(
4180: $this->context,
4181: $this->isDeclareStrictTypes(),
4182: $this->getFunction(),
4183: $this->getNamespace(),
4184: $this->expressionTypes,
4185: $this->nativeExpressionTypes,
4186: $this->conditionalExpressions,
4187: $this->inClosureBindScopeClasses,
4188: $this->anonymousFunctionReflection,
4189: $this->isInFirstLevelStatement(),
4190: $this->currentlyAssignedExpressions,
4191: $currentlyAllowedUndefinedExpressions,
4192: [],
4193: $this->afterExtractCall,
4194: $this->parentScope,
4195: $this->nativeTypesPromoted,
4196: );
4197: $scope->resolvedTypes = $this->resolvedTypes;
4198: $scope->truthyScopes = $this->truthyScopes;
4199: $scope->falseyScopes = $this->falseyScopes;
4200:
4201: return $scope;
4202: }
4203:
4204: /** @api */
4205: public function isUndefinedExpressionAllowed(Expr $expr): bool
4206: {
4207: $exprString = $this->getNodeKey($expr);
4208: return array_key_exists($exprString, $this->currentlyAllowedUndefinedExpressions);
4209: }
4210:
4211: public function assignVariable(string $variableName, Type $type, Type $nativeType, TrinaryLogic $certainty): self
4212: {
4213: $node = new Variable($variableName);
4214: $scope = $this->assignExpression($node, $type, $nativeType);
4215: if ($certainty->no()) {
4216: throw new ShouldNotHappenException();
4217: } elseif (!$certainty->yes()) {
4218: $exprString = '$' . $variableName;
4219: $scope->expressionTypes[$exprString] = new ExpressionTypeHolder($node, $type, $certainty);
4220: $scope->nativeExpressionTypes[$exprString] = new ExpressionTypeHolder($node, $nativeType, $certainty);
4221: }
4222:
4223: $parameterOriginalValueExprString = $this->getNodeKey(new ParameterVariableOriginalValueExpr($variableName));
4224: unset($scope->expressionTypes[$parameterOriginalValueExprString]);
4225: unset($scope->nativeExpressionTypes[$parameterOriginalValueExprString]);
4226:
4227: return $scope;
4228: }
4229:
4230: public function unsetExpression(Expr $expr): self
4231: {
4232: $scope = $this;
4233: if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) {
4234: $exprVarType = $scope->getType($expr->var);
4235: $dimType = $scope->getType($expr->dim);
4236: $unsetType = $exprVarType->unsetOffset($dimType);
4237: $exprVarNativeType = $scope->getNativeType($expr->var);
4238: $dimNativeType = $scope->getNativeType($expr->dim);
4239: $unsetNativeType = $exprVarNativeType->unsetOffset($dimNativeType);
4240: $scope = $scope->assignExpression($expr->var, $unsetType, $unsetNativeType)->invalidateExpression(
4241: new FuncCall(new FullyQualified('count'), [new Arg($expr->var)]),
4242: )->invalidateExpression(
4243: new FuncCall(new FullyQualified('sizeof'), [new Arg($expr->var)]),
4244: )->invalidateExpression(
4245: new FuncCall(new Name('count'), [new Arg($expr->var)]),
4246: )->invalidateExpression(
4247: new FuncCall(new Name('sizeof'), [new Arg($expr->var)]),
4248: );
4249:
4250: if ($expr->var instanceof Expr\ArrayDimFetch && $expr->var->dim !== null) {
4251: $scope = $scope->assignExpression(
4252: $expr->var->var,
4253: $this->getType($expr->var->var)->setOffsetValueType(
4254: $scope->getType($expr->var->dim),
4255: $scope->getType($expr->var),
4256: ),
4257: $this->getNativeType($expr->var->var)->setOffsetValueType(
4258: $scope->getNativeType($expr->var->dim),
4259: $scope->getNativeType($expr->var),
4260: ),
4261: );
4262: }
4263: }
4264:
4265: return $scope->invalidateExpression($expr);
4266: }
4267:
4268: public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, TrinaryLogic $certainty): self
4269: {
4270: if ($expr instanceof ConstFetch) {
4271: $loweredConstName = strtolower($expr->name->toString());
4272: if (in_array($loweredConstName, ['true', 'false', 'null'], true)) {
4273: return $this;
4274: }
4275: }
4276:
4277: if ($expr instanceof FuncCall && $expr->name instanceof Name && $type->isFalse()->yes()) {
4278: $functionName = $this->reflectionProvider->resolveFunctionName($expr->name, $this);
4279: if ($functionName !== null && in_array(strtolower($functionName), [
4280: 'is_dir',
4281: 'is_file',
4282: 'file_exists',
4283: ], true)) {
4284: return $this;
4285: }
4286: }
4287:
4288: $scope = $this;
4289: if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) {
4290: $dimType = $scope->getType($expr->dim)->toArrayKey();
4291: if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) {
4292: $exprVarType = $scope->getType($expr->var);
4293: if (!$exprVarType instanceof MixedType && !$exprVarType->isArray()->no()) {
4294: $types = [
4295: new ArrayType(new MixedType(), new MixedType()),
4296: new ObjectType(ArrayAccess::class),
4297: new NullType(),
4298: ];
4299: if ($dimType instanceof ConstantIntegerType) {
4300: $types[] = new StringType();
4301: }
4302:
4303: $scope = $scope->specifyExpressionType(
4304: $expr->var,
4305: TypeCombinator::intersect(
4306: TypeCombinator::intersect($exprVarType, TypeCombinator::union(...$types)),
4307: new HasOffsetValueType($dimType, $type),
4308: ),
4309: $scope->getNativeType($expr->var),
4310: $certainty,
4311: );
4312: }
4313: }
4314: }
4315:
4316: if ($certainty->no()) {
4317: throw new ShouldNotHappenException();
4318: }
4319:
4320: $exprString = $this->getNodeKey($expr);
4321: $expressionTypes = $scope->expressionTypes;
4322: $expressionTypes[$exprString] = new ExpressionTypeHolder($expr, $type, $certainty);
4323: $nativeTypes = $scope->nativeExpressionTypes;
4324: $nativeTypes[$exprString] = new ExpressionTypeHolder($expr, $nativeType, $certainty);
4325:
4326: $scope = $this->scopeFactory->create(
4327: $this->context,
4328: $this->isDeclareStrictTypes(),
4329: $this->getFunction(),
4330: $this->getNamespace(),
4331: $expressionTypes,
4332: $nativeTypes,
4333: $this->conditionalExpressions,
4334: $this->inClosureBindScopeClasses,
4335: $this->anonymousFunctionReflection,
4336: $this->inFirstLevelStatement,
4337: $this->currentlyAssignedExpressions,
4338: $this->currentlyAllowedUndefinedExpressions,
4339: $this->inFunctionCallsStack,
4340: $this->afterExtractCall,
4341: $this->parentScope,
4342: $this->nativeTypesPromoted,
4343: );
4344:
4345: if ($expr instanceof AlwaysRememberedExpr) {
4346: return $scope->specifyExpressionType($expr->expr, $type, $nativeType, $certainty);
4347: }
4348:
4349: return $scope;
4350: }
4351:
4352: public function assignExpression(Expr $expr, Type $type, Type $nativeType): self
4353: {
4354: $scope = $this;
4355: if ($expr instanceof PropertyFetch) {
4356: $scope = $this->invalidateExpression($expr)
4357: ->invalidateMethodsOnExpression($expr->var);
4358: } elseif ($expr instanceof Expr\StaticPropertyFetch) {
4359: $scope = $this->invalidateExpression($expr);
4360: } elseif ($expr instanceof Variable) {
4361: $scope = $this->invalidateExpression($expr);
4362: }
4363:
4364: return $scope->specifyExpressionType($expr, $type, $nativeType, TrinaryLogic::createYes());
4365: }
4366:
4367: public function assignInitializedProperty(Type $fetchedOnType, string $propertyName): self
4368: {
4369: if (!$this->isInClass()) {
4370: return $this;
4371: }
4372:
4373: if (TypeUtils::findThisType($fetchedOnType) === null) {
4374: return $this;
4375: }
4376:
4377: $propertyReflection = $this->getPropertyReflection($fetchedOnType, $propertyName);
4378: if ($propertyReflection === null) {
4379: return $this;
4380: }
4381: $declaringClass = $propertyReflection->getDeclaringClass();
4382: if ($this->getClassReflection()->getName() !== $declaringClass->getName()) {
4383: return $this;
4384: }
4385: if (!$declaringClass->hasNativeProperty($propertyName)) {
4386: return $this;
4387: }
4388:
4389: return $this->assignExpression(new PropertyInitializationExpr($propertyName), new MixedType(), new MixedType());
4390: }
4391:
4392: public function invalidateExpression(Expr $expressionToInvalidate, bool $requireMoreCharacters = false): self
4393: {
4394: $expressionTypes = $this->expressionTypes;
4395: $nativeExpressionTypes = $this->nativeExpressionTypes;
4396: $invalidated = false;
4397: $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate);
4398:
4399: foreach ($expressionTypes as $exprString => $exprTypeHolder) {
4400: $exprExpr = $exprTypeHolder->getExpr();
4401: if (!$this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $exprExpr, $requireMoreCharacters)) {
4402: continue;
4403: }
4404:
4405: unset($expressionTypes[$exprString]);
4406: unset($nativeExpressionTypes[$exprString]);
4407: $invalidated = true;
4408: }
4409:
4410: $newConditionalExpressions = [];
4411: foreach ($this->conditionalExpressions as $conditionalExprString => $holders) {
4412: if (count($holders) === 0) {
4413: continue;
4414: }
4415: if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $holders[array_key_first($holders)]->getTypeHolder()->getExpr())) {
4416: $invalidated = true;
4417: continue;
4418: }
4419: foreach ($holders as $holder) {
4420: $conditionalTypeHolders = $holder->getConditionExpressionTypeHolders();
4421: foreach ($conditionalTypeHolders as $conditionalTypeHolder) {
4422: if ($this->shouldInvalidateExpression($exprStringToInvalidate, $expressionToInvalidate, $conditionalTypeHolder->getExpr())) {
4423: $invalidated = true;
4424: continue 3;
4425: }
4426: }
4427: }
4428: $newConditionalExpressions[$conditionalExprString] = $holders;
4429: }
4430:
4431: if (!$invalidated) {
4432: return $this;
4433: }
4434:
4435: return $this->scopeFactory->create(
4436: $this->context,
4437: $this->isDeclareStrictTypes(),
4438: $this->getFunction(),
4439: $this->getNamespace(),
4440: $expressionTypes,
4441: $nativeExpressionTypes,
4442: $newConditionalExpressions,
4443: $this->inClosureBindScopeClasses,
4444: $this->anonymousFunctionReflection,
4445: $this->inFirstLevelStatement,
4446: $this->currentlyAssignedExpressions,
4447: $this->currentlyAllowedUndefinedExpressions,
4448: [],
4449: $this->afterExtractCall,
4450: $this->parentScope,
4451: $this->nativeTypesPromoted,
4452: );
4453: }
4454:
4455: private function shouldInvalidateExpression(string $exprStringToInvalidate, Expr $exprToInvalidate, Expr $expr, bool $requireMoreCharacters = false): bool
4456: {
4457: if ($requireMoreCharacters && $exprStringToInvalidate === $this->getNodeKey($expr)) {
4458: return false;
4459: }
4460:
4461: // Variables will not contain traversable expressions. skip the NodeFinder overhead
4462: if ($expr instanceof Variable && is_string($expr->name) && !$requireMoreCharacters) {
4463: return $exprStringToInvalidate === $this->getNodeKey($expr);
4464: }
4465:
4466: $nodeFinder = new NodeFinder();
4467: $expressionToInvalidateClass = get_class($exprToInvalidate);
4468: $found = $nodeFinder->findFirst([$expr], function (Node $node) use ($expressionToInvalidateClass, $exprStringToInvalidate): bool {
4469: if (
4470: $exprStringToInvalidate === '$this'
4471: && $node instanceof Name
4472: && (
4473: in_array($node->toLowerString(), ['self', 'static', 'parent'], true)
4474: || ($this->getClassReflection() !== null && $this->getClassReflection()->is($this->resolveName($node)))
4475: )
4476: ) {
4477: return true;
4478: }
4479:
4480: if (!$node instanceof $expressionToInvalidateClass) {
4481: return false;
4482: }
4483:
4484: $nodeString = $this->getNodeKey($node);
4485:
4486: return $nodeString === $exprStringToInvalidate;
4487: });
4488:
4489: if ($found === null) {
4490: return false;
4491: }
4492:
4493: if (
4494: $expr instanceof PropertyFetch
4495: && $requireMoreCharacters
4496: && $this->isReadonlyPropertyFetch($expr, false)
4497: ) {
4498: return false;
4499: }
4500:
4501: return true;
4502: }
4503:
4504: private function invalidateMethodsOnExpression(Expr $expressionToInvalidate): self
4505: {
4506: $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate);
4507: $expressionTypes = $this->expressionTypes;
4508: $nativeExpressionTypes = $this->nativeExpressionTypes;
4509: $invalidated = false;
4510: $nodeFinder = new NodeFinder();
4511: foreach ($expressionTypes as $exprString => $exprTypeHolder) {
4512: $expr = $exprTypeHolder->getExpr();
4513: $found = $nodeFinder->findFirst([$expr], function (Node $node) use ($exprStringToInvalidate): bool {
4514: if (!$node instanceof MethodCall) {
4515: return false;
4516: }
4517:
4518: return $this->getNodeKey($node->var) === $exprStringToInvalidate;
4519: });
4520: if ($found === null) {
4521: continue;
4522: }
4523:
4524: unset($expressionTypes[$exprString]);
4525: unset($nativeExpressionTypes[$exprString]);
4526: $invalidated = true;
4527: }
4528:
4529: if (!$invalidated) {
4530: return $this;
4531: }
4532:
4533: return $this->scopeFactory->create(
4534: $this->context,
4535: $this->isDeclareStrictTypes(),
4536: $this->getFunction(),
4537: $this->getNamespace(),
4538: $expressionTypes,
4539: $nativeExpressionTypes,
4540: $this->conditionalExpressions,
4541: $this->inClosureBindScopeClasses,
4542: $this->anonymousFunctionReflection,
4543: $this->inFirstLevelStatement,
4544: $this->currentlyAssignedExpressions,
4545: $this->currentlyAllowedUndefinedExpressions,
4546: [],
4547: $this->afterExtractCall,
4548: $this->parentScope,
4549: $this->nativeTypesPromoted,
4550: );
4551: }
4552:
4553: private function setExpressionCertainty(Expr $expr, TrinaryLogic $certainty): self
4554: {
4555: if ($this->hasExpressionType($expr)->no()) {
4556: throw new ShouldNotHappenException();
4557: }
4558:
4559: $originalExprType = $this->getType($expr);
4560: $nativeType = $this->getNativeType($expr);
4561:
4562: return $this->specifyExpressionType(
4563: $expr,
4564: $originalExprType,
4565: $nativeType,
4566: $certainty,
4567: );
4568: }
4569:
4570: public function addTypeToExpression(Expr $expr, Type $type): self
4571: {
4572: $originalExprType = $this->getType($expr);
4573: $nativeType = $this->getNativeType($expr);
4574:
4575: if ($originalExprType->equals($nativeType)) {
4576: $newType = TypeCombinator::intersect($type, $originalExprType);
4577: if ($newType->isConstantScalarValue()->yes() && $newType->equals($originalExprType)) {
4578: // don't add the same type over and over again to improve performance
4579: return $this;
4580: }
4581: return $this->specifyExpressionType($expr, $newType, $newType, TrinaryLogic::createYes());
4582: }
4583:
4584: return $this->specifyExpressionType(
4585: $expr,
4586: TypeCombinator::intersect($type, $originalExprType),
4587: TypeCombinator::intersect($type, $nativeType),
4588: TrinaryLogic::createYes(),
4589: );
4590: }
4591:
4592: public function removeTypeFromExpression(Expr $expr, Type $typeToRemove): self
4593: {
4594: $exprType = $this->getType($expr);
4595: if (
4596: $exprType instanceof NeverType ||
4597: $typeToRemove instanceof NeverType
4598: ) {
4599: return $this;
4600: }
4601: return $this->specifyExpressionType(
4602: $expr,
4603: TypeCombinator::remove($exprType, $typeToRemove),
4604: TypeCombinator::remove($this->getNativeType($expr), $typeToRemove),
4605: TrinaryLogic::createYes(),
4606: );
4607: }
4608:
4609: /**
4610: * @api
4611: * @return MutatingScope
4612: */
4613: public function filterByTruthyValue(Expr $expr): Scope
4614: {
4615: $exprString = $this->getNodeKey($expr);
4616: if (array_key_exists($exprString, $this->truthyScopes)) {
4617: return $this->truthyScopes[$exprString];
4618: }
4619:
4620: $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createTruthy());
4621: $scope = $this->filterBySpecifiedTypes($specifiedTypes);
4622: $this->truthyScopes[$exprString] = $scope;
4623:
4624: return $scope;
4625: }
4626:
4627: /**
4628: * @api
4629: * @return MutatingScope
4630: */
4631: public function filterByFalseyValue(Expr $expr): Scope
4632: {
4633: $exprString = $this->getNodeKey($expr);
4634: if (array_key_exists($exprString, $this->falseyScopes)) {
4635: return $this->falseyScopes[$exprString];
4636: }
4637:
4638: $specifiedTypes = $this->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createFalsey());
4639: $scope = $this->filterBySpecifiedTypes($specifiedTypes);
4640: $this->falseyScopes[$exprString] = $scope;
4641:
4642: return $scope;
4643: }
4644:
4645: public function filterBySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
4646: {
4647: $typeSpecifications = [];
4648: foreach ($specifiedTypes->getSureTypes() as $exprString => [$expr, $type]) {
4649: if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) {
4650: continue;
4651: }
4652: $typeSpecifications[] = [
4653: 'sure' => true,
4654: 'exprString' => (string) $exprString,
4655: 'expr' => $expr,
4656: 'type' => $type,
4657: ];
4658: }
4659: foreach ($specifiedTypes->getSureNotTypes() as $exprString => [$expr, $type]) {
4660: if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) {
4661: continue;
4662: }
4663: $typeSpecifications[] = [
4664: 'sure' => false,
4665: 'exprString' => (string) $exprString,
4666: 'expr' => $expr,
4667: 'type' => $type,
4668: ];
4669: }
4670:
4671: usort($typeSpecifications, static function (array $a, array $b): int {
4672: $length = strlen($a['exprString']) - strlen($b['exprString']);
4673: if ($length !== 0) {
4674: return $length;
4675: }
4676:
4677: return $b['sure'] - $a['sure']; // @phpstan-ignore minus.leftNonNumeric, minus.rightNonNumeric
4678: });
4679:
4680: $scope = $this;
4681: $specifiedExpressions = [];
4682: foreach ($typeSpecifications as $typeSpecification) {
4683: $expr = $typeSpecification['expr'];
4684: $type = $typeSpecification['type'];
4685:
4686: if ($expr instanceof IssetExpr) {
4687: $issetExpr = $expr;
4688: $expr = $issetExpr->getExpr();
4689:
4690: if ($typeSpecification['sure']) {
4691: $scope = $scope->setExpressionCertainty(
4692: $expr,
4693: TrinaryLogic::createMaybe(),
4694: );
4695: } else {
4696: $scope = $scope->unsetExpression($expr);
4697: }
4698:
4699: continue;
4700: }
4701:
4702: if ($typeSpecification['sure']) {
4703: if ($specifiedTypes->shouldOverwrite()) {
4704: $scope = $scope->assignExpression($expr, $type, $type);
4705: } else {
4706: $scope = $scope->addTypeToExpression($expr, $type);
4707: }
4708: } else {
4709: $scope = $scope->removeTypeFromExpression($expr, $type);
4710: }
4711: $specifiedExpressions[$this->getNodeKey($expr)] = ExpressionTypeHolder::createYes($expr, $scope->getType($expr));
4712: }
4713:
4714: $conditions = [];
4715: foreach ($scope->conditionalExpressions as $conditionalExprString => $conditionalExpressions) {
4716: foreach ($conditionalExpressions as $conditionalExpression) {
4717: foreach ($conditionalExpression->getConditionExpressionTypeHolders() as $holderExprString => $conditionalTypeHolder) {
4718: if (!array_key_exists($holderExprString, $specifiedExpressions) || !$specifiedExpressions[$holderExprString]->equals($conditionalTypeHolder)) {
4719: continue 2;
4720: }
4721: }
4722:
4723: $conditions[$conditionalExprString][] = $conditionalExpression;
4724: $specifiedExpressions[$conditionalExprString] = $conditionalExpression->getTypeHolder();
4725: }
4726: }
4727:
4728: foreach ($conditions as $conditionalExprString => $expressions) {
4729: $certainty = TrinaryLogic::lazyExtremeIdentity($expressions, static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getCertainty());
4730: if ($certainty->no()) {
4731: unset($scope->expressionTypes[$conditionalExprString]);
4732: } else {
4733: $type = TypeCombinator::intersect(...array_map(static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getType(), $expressions));
4734:
4735: $scope->expressionTypes[$conditionalExprString] = array_key_exists($conditionalExprString, $scope->expressionTypes)
4736: ? new ExpressionTypeHolder(
4737: $scope->expressionTypes[$conditionalExprString]->getExpr(),
4738: TypeCombinator::intersect($scope->expressionTypes[$conditionalExprString]->getType(), $type),
4739: TrinaryLogic::maxMin($scope->expressionTypes[$conditionalExprString]->getCertainty(), $certainty),
4740: )
4741: : $expressions[0]->getTypeHolder();
4742: }
4743: }
4744:
4745: return $scope->scopeFactory->create(
4746: $scope->context,
4747: $scope->isDeclareStrictTypes(),
4748: $scope->getFunction(),
4749: $scope->getNamespace(),
4750: $scope->expressionTypes,
4751: $scope->nativeExpressionTypes,
4752: array_merge($specifiedTypes->getNewConditionalExpressionHolders(), $scope->conditionalExpressions),
4753: $scope->inClosureBindScopeClasses,
4754: $scope->anonymousFunctionReflection,
4755: $scope->inFirstLevelStatement,
4756: $scope->currentlyAssignedExpressions,
4757: $scope->currentlyAllowedUndefinedExpressions,
4758: $scope->inFunctionCallsStack,
4759: $scope->afterExtractCall,
4760: $scope->parentScope,
4761: $scope->nativeTypesPromoted,
4762: );
4763: }
4764:
4765: /**
4766: * @param ConditionalExpressionHolder[] $conditionalExpressionHolders
4767: */
4768: public function addConditionalExpressions(string $exprString, array $conditionalExpressionHolders): self
4769: {
4770: $conditionalExpressions = $this->conditionalExpressions;
4771: $conditionalExpressions[$exprString] = $conditionalExpressionHolders;
4772: return $this->scopeFactory->create(
4773: $this->context,
4774: $this->isDeclareStrictTypes(),
4775: $this->getFunction(),
4776: $this->getNamespace(),
4777: $this->expressionTypes,
4778: $this->nativeExpressionTypes,
4779: $conditionalExpressions,
4780: $this->inClosureBindScopeClasses,
4781: $this->anonymousFunctionReflection,
4782: $this->inFirstLevelStatement,
4783: $this->currentlyAssignedExpressions,
4784: $this->currentlyAllowedUndefinedExpressions,
4785: $this->inFunctionCallsStack,
4786: $this->afterExtractCall,
4787: $this->parentScope,
4788: $this->nativeTypesPromoted,
4789: );
4790: }
4791:
4792: public function exitFirstLevelStatements(): self
4793: {
4794: if (!$this->inFirstLevelStatement) {
4795: return $this;
4796: }
4797:
4798: if ($this->scopeOutOfFirstLevelStatement !== null) {
4799: return $this->scopeOutOfFirstLevelStatement;
4800: }
4801:
4802: $scope = $this->scopeFactory->create(
4803: $this->context,
4804: $this->isDeclareStrictTypes(),
4805: $this->getFunction(),
4806: $this->getNamespace(),
4807: $this->expressionTypes,
4808: $this->nativeExpressionTypes,
4809: $this->conditionalExpressions,
4810: $this->inClosureBindScopeClasses,
4811: $this->anonymousFunctionReflection,
4812: false,
4813: $this->currentlyAssignedExpressions,
4814: $this->currentlyAllowedUndefinedExpressions,
4815: $this->inFunctionCallsStack,
4816: $this->afterExtractCall,
4817: $this->parentScope,
4818: $this->nativeTypesPromoted,
4819: );
4820: $scope->resolvedTypes = $this->resolvedTypes;
4821: $scope->truthyScopes = $this->truthyScopes;
4822: $scope->falseyScopes = $this->falseyScopes;
4823: $this->scopeOutOfFirstLevelStatement = $scope;
4824:
4825: return $scope;
4826: }
4827:
4828: /** @api */
4829: public function isInFirstLevelStatement(): bool
4830: {
4831: return $this->inFirstLevelStatement;
4832: }
4833:
4834: public function mergeWith(?self $otherScope): self
4835: {
4836: if ($otherScope === null) {
4837: return $this;
4838: }
4839: $ourExpressionTypes = $this->expressionTypes;
4840: $theirExpressionTypes = $otherScope->expressionTypes;
4841:
4842: $mergedExpressionTypes = $this->mergeVariableHolders($ourExpressionTypes, $theirExpressionTypes);
4843: $conditionalExpressions = $this->intersectConditionalExpressions($otherScope->conditionalExpressions);
4844: $conditionalExpressions = $this->createConditionalExpressions(
4845: $conditionalExpressions,
4846: $ourExpressionTypes,
4847: $theirExpressionTypes,
4848: $mergedExpressionTypes,
4849: );
4850: $conditionalExpressions = $this->createConditionalExpressions(
4851: $conditionalExpressions,
4852: $theirExpressionTypes,
4853: $ourExpressionTypes,
4854: $mergedExpressionTypes,
4855: );
4856: return $this->scopeFactory->create(
4857: $this->context,
4858: $this->isDeclareStrictTypes(),
4859: $this->getFunction(),
4860: $this->getNamespace(),
4861: $mergedExpressionTypes,
4862: $this->mergeVariableHolders($this->nativeExpressionTypes, $otherScope->nativeExpressionTypes),
4863: $conditionalExpressions,
4864: $this->inClosureBindScopeClasses,
4865: $this->anonymousFunctionReflection,
4866: $this->inFirstLevelStatement,
4867: [],
4868: [],
4869: [],
4870: $this->afterExtractCall && $otherScope->afterExtractCall,
4871: $this->parentScope,
4872: $this->nativeTypesPromoted,
4873: );
4874: }
4875:
4876: /**
4877: * @param array<string, ConditionalExpressionHolder[]> $otherConditionalExpressions
4878: * @return array<string, ConditionalExpressionHolder[]>
4879: */
4880: private function intersectConditionalExpressions(array $otherConditionalExpressions): array
4881: {
4882: $newConditionalExpressions = [];
4883: foreach ($this->conditionalExpressions as $exprString => $holders) {
4884: if (!array_key_exists($exprString, $otherConditionalExpressions)) {
4885: continue;
4886: }
4887:
4888: $otherHolders = $otherConditionalExpressions[$exprString];
4889: foreach (array_keys($holders) as $key) {
4890: if (!array_key_exists($key, $otherHolders)) {
4891: continue 2;
4892: }
4893: }
4894:
4895: $newConditionalExpressions[$exprString] = $holders;
4896: }
4897:
4898: return $newConditionalExpressions;
4899: }
4900:
4901: /**
4902: * @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
4903: * @param array<string, ExpressionTypeHolder> $ourExpressionTypes
4904: * @param array<string, ExpressionTypeHolder> $theirExpressionTypes
4905: * @param array<string, ExpressionTypeHolder> $mergedExpressionTypes
4906: * @return array<string, ConditionalExpressionHolder[]>
4907: */
4908: private function createConditionalExpressions(
4909: array $conditionalExpressions,
4910: array $ourExpressionTypes,
4911: array $theirExpressionTypes,
4912: array $mergedExpressionTypes,
4913: ): array
4914: {
4915: $newVariableTypes = $ourExpressionTypes;
4916: foreach ($theirExpressionTypes as $exprString => $holder) {
4917: if (!array_key_exists($exprString, $mergedExpressionTypes)) {
4918: continue;
4919: }
4920:
4921: if (!$mergedExpressionTypes[$exprString]->getType()->equals($holder->getType())) {
4922: continue;
4923: }
4924:
4925: unset($newVariableTypes[$exprString]);
4926: }
4927:
4928: $typeGuards = [];
4929: foreach ($newVariableTypes as $exprString => $holder) {
4930: if (!$holder->getCertainty()->yes()) {
4931: continue;
4932: }
4933: if (!array_key_exists($exprString, $mergedExpressionTypes)) {
4934: continue;
4935: }
4936: if ($mergedExpressionTypes[$exprString]->getType()->equals($holder->getType())) {
4937: continue;
4938: }
4939:
4940: $typeGuards[$exprString] = $holder;
4941: }
4942:
4943: if (count($typeGuards) === 0) {
4944: return $conditionalExpressions;
4945: }
4946:
4947: foreach ($newVariableTypes as $exprString => $holder) {
4948: if (
4949: array_key_exists($exprString, $mergedExpressionTypes)
4950: && $mergedExpressionTypes[$exprString]->equals($holder)
4951: ) {
4952: continue;
4953: }
4954:
4955: $variableTypeGuards = $typeGuards;
4956: unset($variableTypeGuards[$exprString]);
4957:
4958: if (count($variableTypeGuards) === 0) {
4959: continue;
4960: }
4961:
4962: $conditionalExpression = new ConditionalExpressionHolder($variableTypeGuards, $holder);
4963: $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression;
4964: }
4965:
4966: foreach ($mergedExpressionTypes as $exprString => $mergedExprTypeHolder) {
4967: if (array_key_exists($exprString, $ourExpressionTypes)) {
4968: continue;
4969: }
4970:
4971: $conditionalExpression = new ConditionalExpressionHolder($typeGuards, new ExpressionTypeHolder($mergedExprTypeHolder->getExpr(), new ErrorType(), TrinaryLogic::createNo()));
4972: $conditionalExpressions[$exprString][$conditionalExpression->getKey()] = $conditionalExpression;
4973: }
4974:
4975: return $conditionalExpressions;
4976: }
4977:
4978: /**
4979: * @param array<string, ExpressionTypeHolder> $ourVariableTypeHolders
4980: * @param array<string, ExpressionTypeHolder> $theirVariableTypeHolders
4981: * @return array<string, ExpressionTypeHolder>
4982: */
4983: private function mergeVariableHolders(array $ourVariableTypeHolders, array $theirVariableTypeHolders): array
4984: {
4985: $intersectedVariableTypeHolders = [];
4986: $globalVariableCallback = fn (Node $node) => $node instanceof Variable && is_string($node->name) && $this->isGlobalVariable($node->name);
4987: $nodeFinder = new NodeFinder();
4988: foreach ($ourVariableTypeHolders as $exprString => $variableTypeHolder) {
4989: if (isset($theirVariableTypeHolders[$exprString])) {
4990: if ($variableTypeHolder === $theirVariableTypeHolders[$exprString]) {
4991: $intersectedVariableTypeHolders[$exprString] = $variableTypeHolder;
4992: continue;
4993: }
4994:
4995: $intersectedVariableTypeHolders[$exprString] = $variableTypeHolder->and($theirVariableTypeHolders[$exprString]);
4996: } else {
4997: $expr = $variableTypeHolder->getExpr();
4998: if ($nodeFinder->findFirst($expr, $globalVariableCallback) !== null) {
4999: continue;
5000: }
5001:
5002: $intersectedVariableTypeHolders[$exprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType());
5003: }
5004: }
5005:
5006: foreach ($theirVariableTypeHolders as $exprString => $variableTypeHolder) {
5007: if (isset($intersectedVariableTypeHolders[$exprString])) {
5008: continue;
5009: }
5010:
5011: $expr = $variableTypeHolder->getExpr();
5012: if ($nodeFinder->findFirst($expr, $globalVariableCallback) !== null) {
5013: continue;
5014: }
5015:
5016: $intersectedVariableTypeHolders[$exprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType());
5017: }
5018:
5019: return $intersectedVariableTypeHolders;
5020: }
5021:
5022: public function mergeInitializedProperties(self $calledMethodScope): self
5023: {
5024: $scope = $this;
5025: foreach ($calledMethodScope->expressionTypes as $exprString => $typeHolder) {
5026: $exprString = (string) $exprString;
5027: if (!str_starts_with($exprString, '__phpstanPropertyInitialization(')) {
5028: continue;
5029: }
5030: $propertyName = substr($exprString, strlen('__phpstanPropertyInitialization('), -1);
5031: $propertyExpr = new PropertyInitializationExpr($propertyName);
5032: if (!array_key_exists($exprString, $scope->expressionTypes)) {
5033: $scope = $scope->assignExpression($propertyExpr, new MixedType(), new MixedType());
5034: $scope->expressionTypes[$exprString] = $typeHolder;
5035: continue;
5036: }
5037:
5038: $certainty = $scope->expressionTypes[$exprString]->getCertainty();
5039: $scope = $scope->assignExpression($propertyExpr, new MixedType(), new MixedType());
5040: $scope->expressionTypes[$exprString] = new ExpressionTypeHolder(
5041: $typeHolder->getExpr(),
5042: $typeHolder->getType(),
5043: $typeHolder->getCertainty()->or($certainty),
5044: );
5045: }
5046:
5047: return $scope;
5048: }
5049:
5050: public function processFinallyScope(self $finallyScope, self $originalFinallyScope): self
5051: {
5052: return $this->scopeFactory->create(
5053: $this->context,
5054: $this->isDeclareStrictTypes(),
5055: $this->getFunction(),
5056: $this->getNamespace(),
5057: $this->processFinallyScopeVariableTypeHolders(
5058: $this->expressionTypes,
5059: $finallyScope->expressionTypes,
5060: $originalFinallyScope->expressionTypes,
5061: ),
5062: $this->processFinallyScopeVariableTypeHolders(
5063: $this->nativeExpressionTypes,
5064: $finallyScope->nativeExpressionTypes,
5065: $originalFinallyScope->nativeExpressionTypes,
5066: ),
5067: $this->conditionalExpressions,
5068: $this->inClosureBindScopeClasses,
5069: $this->anonymousFunctionReflection,
5070: $this->inFirstLevelStatement,
5071: [],
5072: [],
5073: [],
5074: $this->afterExtractCall,
5075: $this->parentScope,
5076: $this->nativeTypesPromoted,
5077: );
5078: }
5079:
5080: /**
5081: * @param array<string, ExpressionTypeHolder> $ourVariableTypeHolders
5082: * @param array<string, ExpressionTypeHolder> $finallyVariableTypeHolders
5083: * @param array<string, ExpressionTypeHolder> $originalVariableTypeHolders
5084: * @return array<string, ExpressionTypeHolder>
5085: */
5086: private function processFinallyScopeVariableTypeHolders(
5087: array $ourVariableTypeHolders,
5088: array $finallyVariableTypeHolders,
5089: array $originalVariableTypeHolders,
5090: ): array
5091: {
5092: foreach ($finallyVariableTypeHolders as $exprString => $variableTypeHolder) {
5093: if (
5094: isset($originalVariableTypeHolders[$exprString])
5095: && !$originalVariableTypeHolders[$exprString]->getType()->equals($variableTypeHolder->getType())
5096: ) {
5097: $ourVariableTypeHolders[$exprString] = $variableTypeHolder;
5098: continue;
5099: }
5100:
5101: if (isset($originalVariableTypeHolders[$exprString])) {
5102: continue;
5103: }
5104:
5105: $ourVariableTypeHolders[$exprString] = $variableTypeHolder;
5106: }
5107:
5108: return $ourVariableTypeHolders;
5109: }
5110:
5111: /**
5112: * @param Node\ClosureUse[] $byRefUses
5113: */
5114: public function processClosureScope(
5115: self $closureScope,
5116: ?self $prevScope,
5117: array $byRefUses,
5118: ): self
5119: {
5120: $nativeExpressionTypes = $this->nativeExpressionTypes;
5121: $expressionTypes = $this->expressionTypes;
5122: if (count($byRefUses) === 0) {
5123: return $this;
5124: }
5125:
5126: foreach ($byRefUses as $use) {
5127: if (!is_string($use->var->name)) {
5128: throw new ShouldNotHappenException();
5129: }
5130:
5131: $variableName = $use->var->name;
5132: $variableExprString = '$' . $variableName;
5133:
5134: if (!$closureScope->hasVariableType($variableName)->yes()) {
5135: $holder = ExpressionTypeHolder::createYes($use->var, new NullType());
5136: $expressionTypes[$variableExprString] = $holder;
5137: $nativeExpressionTypes[$variableExprString] = $holder;
5138: continue;
5139: }
5140:
5141: $variableType = $closureScope->getVariableType($variableName);
5142:
5143: if ($prevScope !== null) {
5144: $prevVariableType = $prevScope->getVariableType($variableName);
5145: if (!$variableType->equals($prevVariableType)) {
5146: $variableType = TypeCombinator::union($variableType, $prevVariableType);
5147: $variableType = $this->generalizeType($variableType, $prevVariableType, 0);
5148: }
5149: }
5150:
5151: $expressionTypes[$variableExprString] = ExpressionTypeHolder::createYes($use->var, $variableType);
5152: $nativeExpressionTypes[$variableExprString] = ExpressionTypeHolder::createYes($use->var, $variableType);
5153: }
5154:
5155: return $this->scopeFactory->create(
5156: $this->context,
5157: $this->isDeclareStrictTypes(),
5158: $this->getFunction(),
5159: $this->getNamespace(),
5160: $expressionTypes,
5161: $nativeExpressionTypes,
5162: $this->conditionalExpressions,
5163: $this->inClosureBindScopeClasses,
5164: $this->anonymousFunctionReflection,
5165: $this->inFirstLevelStatement,
5166: [],
5167: [],
5168: $this->inFunctionCallsStack,
5169: $this->afterExtractCall,
5170: $this->parentScope,
5171: $this->nativeTypesPromoted,
5172: );
5173: }
5174:
5175: public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope): self
5176: {
5177: $expressionTypes = $this->expressionTypes;
5178: foreach ($finalScope->expressionTypes as $variableExprString => $variableTypeHolder) {
5179: if (!isset($expressionTypes[$variableExprString])) {
5180: $expressionTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType());
5181: continue;
5182: }
5183:
5184: $expressionTypes[$variableExprString] = new ExpressionTypeHolder(
5185: $variableTypeHolder->getExpr(),
5186: $variableTypeHolder->getType(),
5187: $variableTypeHolder->getCertainty()->and($expressionTypes[$variableExprString]->getCertainty()),
5188: );
5189: }
5190: $nativeTypes = $this->nativeExpressionTypes;
5191: foreach ($finalScope->nativeExpressionTypes as $variableExprString => $variableTypeHolder) {
5192: if (!isset($nativeTypes[$variableExprString])) {
5193: $nativeTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType());
5194: continue;
5195: }
5196:
5197: $nativeTypes[$variableExprString] = new ExpressionTypeHolder(
5198: $variableTypeHolder->getExpr(),
5199: $variableTypeHolder->getType(),
5200: $variableTypeHolder->getCertainty()->and($nativeTypes[$variableExprString]->getCertainty()),
5201: );
5202: }
5203:
5204: return $this->scopeFactory->create(
5205: $this->context,
5206: $this->isDeclareStrictTypes(),
5207: $this->getFunction(),
5208: $this->getNamespace(),
5209: $expressionTypes,
5210: $nativeTypes,
5211: $this->conditionalExpressions,
5212: $this->inClosureBindScopeClasses,
5213: $this->anonymousFunctionReflection,
5214: $this->inFirstLevelStatement,
5215: [],
5216: [],
5217: [],
5218: $this->afterExtractCall,
5219: $this->parentScope,
5220: $this->nativeTypesPromoted,
5221: );
5222: }
5223:
5224: public function generalizeWith(self $otherScope): self
5225: {
5226: $variableTypeHolders = $this->generalizeVariableTypeHolders(
5227: $this->expressionTypes,
5228: $otherScope->expressionTypes,
5229: );
5230: $nativeTypes = $this->generalizeVariableTypeHolders(
5231: $this->nativeExpressionTypes,
5232: $otherScope->nativeExpressionTypes,
5233: );
5234:
5235: return $this->scopeFactory->create(
5236: $this->context,
5237: $this->isDeclareStrictTypes(),
5238: $this->getFunction(),
5239: $this->getNamespace(),
5240: $variableTypeHolders,
5241: $nativeTypes,
5242: $this->conditionalExpressions,
5243: $this->inClosureBindScopeClasses,
5244: $this->anonymousFunctionReflection,
5245: $this->inFirstLevelStatement,
5246: [],
5247: [],
5248: [],
5249: $this->afterExtractCall,
5250: $this->parentScope,
5251: $this->nativeTypesPromoted,
5252: );
5253: }
5254:
5255: /**
5256: * @param array<string, ExpressionTypeHolder> $variableTypeHolders
5257: * @param array<string, ExpressionTypeHolder> $otherVariableTypeHolders
5258: * @return array<string, ExpressionTypeHolder>
5259: */
5260: private function generalizeVariableTypeHolders(
5261: array $variableTypeHolders,
5262: array $otherVariableTypeHolders,
5263: ): array
5264: {
5265: foreach ($variableTypeHolders as $variableExprString => $variableTypeHolder) {
5266: if (!isset($otherVariableTypeHolders[$variableExprString])) {
5267: continue;
5268: }
5269:
5270: $variableTypeHolders[$variableExprString] = new ExpressionTypeHolder(
5271: $variableTypeHolder->getExpr(),
5272: $this->generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$variableExprString]->getType(), 0),
5273: $variableTypeHolder->getCertainty(),
5274: );
5275: }
5276:
5277: return $variableTypeHolders;
5278: }
5279:
5280: private function generalizeType(Type $a, Type $b, int $depth): Type
5281: {
5282: if ($a->equals($b)) {
5283: return $a;
5284: }
5285:
5286: $constantIntegers = ['a' => [], 'b' => []];
5287: $constantFloats = ['a' => [], 'b' => []];
5288: $constantBooleans = ['a' => [], 'b' => []];
5289: $constantStrings = ['a' => [], 'b' => []];
5290: $constantArrays = ['a' => [], 'b' => []];
5291: $generalArrays = ['a' => [], 'b' => []];
5292: $integerRanges = ['a' => [], 'b' => []];
5293: $otherTypes = [];
5294:
5295: foreach ([
5296: 'a' => TypeUtils::flattenTypes($a),
5297: 'b' => TypeUtils::flattenTypes($b),
5298: ] as $key => $types) {
5299: foreach ($types as $type) {
5300: if ($type instanceof ConstantIntegerType) {
5301: $constantIntegers[$key][] = $type;
5302: continue;
5303: }
5304: if ($type instanceof ConstantFloatType) {
5305: $constantFloats[$key][] = $type;
5306: continue;
5307: }
5308: if ($type instanceof ConstantBooleanType) {
5309: $constantBooleans[$key][] = $type;
5310: continue;
5311: }
5312: if ($type instanceof ConstantStringType) {
5313: $constantStrings[$key][] = $type;
5314: continue;
5315: }
5316: if ($type->isConstantArray()->yes()) {
5317: $constantArrays[$key][] = $type;
5318: continue;
5319: }
5320: if ($type->isArray()->yes()) {
5321: $generalArrays[$key][] = $type;
5322: continue;
5323: }
5324: if ($type instanceof IntegerRangeType) {
5325: $integerRanges[$key][] = $type;
5326: continue;
5327: }
5328:
5329: $otherTypes[] = $type;
5330: }
5331: }
5332:
5333: $resultTypes = [];
5334: foreach ([
5335: $constantFloats,
5336: $constantBooleans,
5337: $constantStrings,
5338: ] as $constantTypes) {
5339: if (count($constantTypes['a']) === 0) {
5340: if (count($constantTypes['b']) > 0) {
5341: $resultTypes[] = TypeCombinator::union(...$constantTypes['b']);
5342: }
5343: continue;
5344: } elseif (count($constantTypes['b']) === 0) {
5345: $resultTypes[] = TypeCombinator::union(...$constantTypes['a']);
5346: continue;
5347: }
5348:
5349: $aTypes = TypeCombinator::union(...$constantTypes['a']);
5350: $bTypes = TypeCombinator::union(...$constantTypes['b']);
5351: if ($aTypes->equals($bTypes)) {
5352: $resultTypes[] = $aTypes;
5353: continue;
5354: }
5355:
5356: $resultTypes[] = TypeCombinator::union(...$constantTypes['a'], ...$constantTypes['b'])->generalize(GeneralizePrecision::moreSpecific());
5357: }
5358:
5359: if (count($constantArrays['a']) > 0) {
5360: if (count($constantArrays['b']) === 0) {
5361: $resultTypes[] = TypeCombinator::union(...$constantArrays['a']);
5362: } else {
5363: $constantArraysA = TypeCombinator::union(...$constantArrays['a']);
5364: $constantArraysB = TypeCombinator::union(...$constantArrays['b']);
5365: if (
5366: $constantArraysA->getIterableKeyType()->equals($constantArraysB->getIterableKeyType())
5367: && $constantArraysA->getArraySize()->getGreaterOrEqualType($this->phpVersion)->isSuperTypeOf($constantArraysB->getArraySize())->yes()
5368: ) {
5369: $resultArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
5370: foreach (TypeUtils::flattenTypes($constantArraysA->getIterableKeyType()) as $keyType) {
5371: $resultArrayBuilder->setOffsetValueType(
5372: $keyType,
5373: $this->generalizeType(
5374: $constantArraysA->getOffsetValueType($keyType),
5375: $constantArraysB->getOffsetValueType($keyType),
5376: $depth + 1,
5377: ),
5378: !$constantArraysA->hasOffsetValueType($keyType)->and($constantArraysB->hasOffsetValueType($keyType))->negate()->no(),
5379: );
5380: }
5381:
5382: $resultTypes[] = $resultArrayBuilder->getArray();
5383: } else {
5384: $resultType = new ArrayType(
5385: TypeCombinator::union($this->generalizeType($constantArraysA->getIterableKeyType(), $constantArraysB->getIterableKeyType(), $depth + 1)),
5386: TypeCombinator::union($this->generalizeType($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType(), $depth + 1)),
5387: );
5388: if (
5389: $constantArraysA->isIterableAtLeastOnce()->yes()
5390: && $constantArraysB->isIterableAtLeastOnce()->yes()
5391: && $constantArraysA->getArraySize()->getGreaterOrEqualType($this->phpVersion)->isSuperTypeOf($constantArraysB->getArraySize())->yes()
5392: ) {
5393: $resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType());
5394: }
5395: if ($constantArraysA->isList()->yes() && $constantArraysB->isList()->yes()) {
5396: $resultType = TypeCombinator::intersect($resultType, new AccessoryArrayListType());
5397: }
5398: $resultTypes[] = $resultType;
5399: }
5400: }
5401: } elseif (count($constantArrays['b']) > 0) {
5402: $resultTypes[] = TypeCombinator::union(...$constantArrays['b']);
5403: }
5404:
5405: if (count($generalArrays['a']) > 0) {
5406: if (count($generalArrays['b']) === 0) {
5407: $resultTypes[] = TypeCombinator::union(...$generalArrays['a']);
5408: } else {
5409: $generalArraysA = TypeCombinator::union(...$generalArrays['a']);
5410: $generalArraysB = TypeCombinator::union(...$generalArrays['b']);
5411:
5412: $aValueType = $generalArraysA->getIterableValueType();
5413: $bValueType = $generalArraysB->getIterableValueType();
5414: if (
5415: $aValueType->isArray()->yes()
5416: && $aValueType->isConstantArray()->no()
5417: && $bValueType->isArray()->yes()
5418: && $bValueType->isConstantArray()->no()
5419: ) {
5420: $aDepth = self::getArrayDepth($aValueType) + $depth;
5421: $bDepth = self::getArrayDepth($bValueType) + $depth;
5422: if (
5423: ($aDepth > 2 || $bDepth > 2)
5424: && abs($aDepth - $bDepth) > 0
5425: ) {
5426: $aValueType = new MixedType();
5427: $bValueType = new MixedType();
5428: }
5429: }
5430:
5431: $resultType = new ArrayType(
5432: TypeCombinator::union($this->generalizeType($generalArraysA->getIterableKeyType(), $generalArraysB->getIterableKeyType(), $depth + 1)),
5433: TypeCombinator::union($this->generalizeType($aValueType, $bValueType, $depth + 1)),
5434: );
5435: if ($generalArraysA->isIterableAtLeastOnce()->yes() && $generalArraysB->isIterableAtLeastOnce()->yes()) {
5436: $resultType = TypeCombinator::intersect($resultType, new NonEmptyArrayType());
5437: }
5438: if ($generalArraysA->isList()->yes() && $generalArraysB->isList()->yes()) {
5439: $resultType = TypeCombinator::intersect($resultType, new AccessoryArrayListType());
5440: }
5441: if ($generalArraysA->isOversizedArray()->yes() && $generalArraysB->isOversizedArray()->yes()) {
5442: $resultType = TypeCombinator::intersect($resultType, new OversizedArrayType());
5443: }
5444: $resultTypes[] = $resultType;
5445: }
5446: } elseif (count($generalArrays['b']) > 0) {
5447: $resultTypes[] = TypeCombinator::union(...$generalArrays['b']);
5448: }
5449:
5450: if (count($constantIntegers['a']) > 0) {
5451: if (count($constantIntegers['b']) === 0) {
5452: $resultTypes[] = TypeCombinator::union(...$constantIntegers['a']);
5453: } else {
5454: $constantIntegersA = TypeCombinator::union(...$constantIntegers['a']);
5455: $constantIntegersB = TypeCombinator::union(...$constantIntegers['b']);
5456:
5457: if ($constantIntegersA->equals($constantIntegersB)) {
5458: $resultTypes[] = $constantIntegersA;
5459: } else {
5460: $min = null;
5461: $max = null;
5462: foreach ($constantIntegers['a'] as $int) {
5463: if ($min === null || $int->getValue() < $min) {
5464: $min = $int->getValue();
5465: }
5466: if ($max !== null && $int->getValue() <= $max) {
5467: continue;
5468: }
5469:
5470: $max = $int->getValue();
5471: }
5472:
5473: $gotGreater = false;
5474: $gotSmaller = false;
5475: foreach ($constantIntegers['b'] as $int) {
5476: if ($int->getValue() > $max) {
5477: $gotGreater = true;
5478: }
5479: if ($int->getValue() >= $min) {
5480: continue;
5481: }
5482:
5483: $gotSmaller = true;
5484: }
5485:
5486: if ($gotGreater && $gotSmaller) {
5487: $resultTypes[] = new IntegerType();
5488: } elseif ($gotGreater) {
5489: $resultTypes[] = IntegerRangeType::fromInterval($min, null);
5490: } elseif ($gotSmaller) {
5491: $resultTypes[] = IntegerRangeType::fromInterval(null, $max);
5492: } else {
5493: $resultTypes[] = TypeCombinator::union($constantIntegersA, $constantIntegersB);
5494: }
5495: }
5496: }
5497: } elseif (count($constantIntegers['b']) > 0) {
5498: $resultTypes[] = TypeCombinator::union(...$constantIntegers['b']);
5499: }
5500:
5501: if (count($integerRanges['a']) > 0) {
5502: if (count($integerRanges['b']) === 0) {
5503: $resultTypes[] = TypeCombinator::union(...$integerRanges['a']);
5504: } else {
5505: $integerRangesA = TypeCombinator::union(...$integerRanges['a']);
5506: $integerRangesB = TypeCombinator::union(...$integerRanges['b']);
5507:
5508: if ($integerRangesA->equals($integerRangesB)) {
5509: $resultTypes[] = $integerRangesA;
5510: } else {
5511: $min = null;
5512: $max = null;
5513: foreach ($integerRanges['a'] as $range) {
5514: if ($range->getMin() === null) {
5515: $rangeMin = PHP_INT_MIN;
5516: } else {
5517: $rangeMin = $range->getMin();
5518: }
5519: if ($range->getMax() === null) {
5520: $rangeMax = PHP_INT_MAX;
5521: } else {
5522: $rangeMax = $range->getMax();
5523: }
5524:
5525: if ($min === null || $rangeMin < $min) {
5526: $min = $rangeMin;
5527: }
5528: if ($max !== null && $rangeMax <= $max) {
5529: continue;
5530: }
5531:
5532: $max = $rangeMax;
5533: }
5534:
5535: $gotGreater = false;
5536: $gotSmaller = false;
5537: foreach ($integerRanges['b'] as $range) {
5538: if ($range->getMin() === null) {
5539: $rangeMin = PHP_INT_MIN;
5540: } else {
5541: $rangeMin = $range->getMin();
5542: }
5543: if ($range->getMax() === null) {
5544: $rangeMax = PHP_INT_MAX;
5545: } else {
5546: $rangeMax = $range->getMax();
5547: }
5548:
5549: if ($rangeMax > $max) {
5550: $gotGreater = true;
5551: }
5552: if ($rangeMin >= $min) {
5553: continue;
5554: }
5555:
5556: $gotSmaller = true;
5557: }
5558:
5559: if ($min === PHP_INT_MIN) {
5560: $min = null;
5561: }
5562: if ($max === PHP_INT_MAX) {
5563: $max = null;
5564: }
5565:
5566: if ($gotGreater && $gotSmaller) {
5567: $resultTypes[] = new IntegerType();
5568: } elseif ($gotGreater) {
5569: $resultTypes[] = IntegerRangeType::fromInterval($min, null);
5570: } elseif ($gotSmaller) {
5571: $resultTypes[] = IntegerRangeType::fromInterval(null, $max);
5572: } else {
5573: $resultTypes[] = TypeCombinator::union($integerRangesA, $integerRangesB);
5574: }
5575: }
5576: }
5577: } elseif (count($integerRanges['b']) > 0) {
5578: $resultTypes[] = TypeCombinator::union(...$integerRanges['b']);
5579: }
5580:
5581: $accessoryTypes = array_map(
5582: static fn (Type $type): Type => $type->generalize(GeneralizePrecision::moreSpecific()),
5583: TypeUtils::getAccessoryTypes($a),
5584: );
5585:
5586: return TypeCombinator::union(TypeCombinator::intersect(
5587: TypeCombinator::union(...$resultTypes, ...$otherTypes),
5588: ...$accessoryTypes,
5589: ), ...$otherTypes);
5590: }
5591:
5592: private static function getArrayDepth(Type $type): int
5593: {
5594: $depth = 0;
5595: $arrays = TypeUtils::toBenevolentUnion($type)->getArrays();
5596: while (count($arrays) > 0) {
5597: $temp = $type->getIterableValueType();
5598: $type = $temp;
5599: $arrays = TypeUtils::toBenevolentUnion($type)->getArrays();
5600: $depth++;
5601: }
5602:
5603: return $depth;
5604: }
5605:
5606: public function equals(self $otherScope): bool
5607: {
5608: if (!$this->context->equals($otherScope->context)) {
5609: return false;
5610: }
5611:
5612: if (!$this->compareVariableTypeHolders($this->expressionTypes, $otherScope->expressionTypes)) {
5613: return false;
5614: }
5615: return $this->compareVariableTypeHolders($this->nativeExpressionTypes, $otherScope->nativeExpressionTypes);
5616: }
5617:
5618: /**
5619: * @param array<string, ExpressionTypeHolder> $variableTypeHolders
5620: * @param array<string, ExpressionTypeHolder> $otherVariableTypeHolders
5621: */
5622: private function compareVariableTypeHolders(array $variableTypeHolders, array $otherVariableTypeHolders): bool
5623: {
5624: if (count($variableTypeHolders) !== count($otherVariableTypeHolders)) {
5625: return false;
5626: }
5627: foreach ($variableTypeHolders as $variableExprString => $variableTypeHolder) {
5628: if (!isset($otherVariableTypeHolders[$variableExprString])) {
5629: return false;
5630: }
5631:
5632: if (!$variableTypeHolder->getCertainty()->equals($otherVariableTypeHolders[$variableExprString]->getCertainty())) {
5633: return false;
5634: }
5635:
5636: if (!$variableTypeHolder->getType()->equals($otherVariableTypeHolders[$variableExprString]->getType())) {
5637: return false;
5638: }
5639:
5640: unset($otherVariableTypeHolders[$variableExprString]);
5641: }
5642:
5643: return true;
5644: }
5645:
5646: private function getBooleanExpressionDepth(Expr $expr, int $depth = 0): int
5647: {
5648: while (
5649: $expr instanceof BinaryOp\BooleanOr
5650: || $expr instanceof BinaryOp\LogicalOr
5651: || $expr instanceof BinaryOp\BooleanAnd
5652: || $expr instanceof BinaryOp\LogicalAnd
5653: ) {
5654: return $this->getBooleanExpressionDepth($expr->left, $depth + 1);
5655: }
5656:
5657: return $depth;
5658: }
5659:
5660: /**
5661: * @api
5662: * @deprecated Use canReadProperty() or canWriteProperty()
5663: */
5664: public function canAccessProperty(PropertyReflection $propertyReflection): bool
5665: {
5666: return $this->canAccessClassMember($propertyReflection);
5667: }
5668:
5669: /** @api */
5670: public function canReadProperty(ExtendedPropertyReflection $propertyReflection): bool
5671: {
5672: return $this->canAccessClassMember($propertyReflection);
5673: }
5674:
5675: /** @api */
5676: public function canWriteProperty(ExtendedPropertyReflection $propertyReflection): bool
5677: {
5678: if (!$propertyReflection->isPrivateSet() && !$propertyReflection->isProtectedSet()) {
5679: return $this->canAccessClassMember($propertyReflection);
5680: }
5681:
5682: if (!$this->phpVersion->supportsAsymmetricVisibility()) {
5683: return $this->canAccessClassMember($propertyReflection);
5684: }
5685:
5686: $propertyDeclaringClass = $propertyReflection->getDeclaringClass();
5687: $canAccessClassMember = static function (ClassReflection $classReflection) use ($propertyReflection, $propertyDeclaringClass) {
5688: if ($propertyReflection->isPrivateSet()) {
5689: return $classReflection->getName() === $propertyDeclaringClass->getName();
5690: }
5691:
5692: // protected set
5693:
5694: if (
5695: $classReflection->getName() === $propertyDeclaringClass->getName()
5696: || $classReflection->isSubclassOfClass($propertyDeclaringClass->removeFinalKeywordOverride())
5697: ) {
5698: return true;
5699: }
5700:
5701: return $propertyReflection->getDeclaringClass()->isSubclassOfClass($classReflection);
5702: };
5703:
5704: foreach ($this->inClosureBindScopeClasses as $inClosureBindScopeClass) {
5705: if (!$this->reflectionProvider->hasClass($inClosureBindScopeClass)) {
5706: continue;
5707: }
5708:
5709: if ($canAccessClassMember($this->reflectionProvider->getClass($inClosureBindScopeClass))) {
5710: return true;
5711: }
5712: }
5713:
5714: if ($this->isInClass()) {
5715: return $canAccessClassMember($this->getClassReflection());
5716: }
5717:
5718: return false;
5719: }
5720:
5721: /** @api */
5722: public function canCallMethod(MethodReflection $methodReflection): bool
5723: {
5724: if ($this->canAccessClassMember($methodReflection)) {
5725: return true;
5726: }
5727:
5728: return $this->canAccessClassMember($methodReflection->getPrototype());
5729: }
5730:
5731: /** @api */
5732: public function canAccessConstant(ClassConstantReflection $constantReflection): bool
5733: {
5734: return $this->canAccessClassMember($constantReflection);
5735: }
5736:
5737: private function canAccessClassMember(ClassMemberReflection $classMemberReflection): bool
5738: {
5739: if ($classMemberReflection->isPublic()) {
5740: return true;
5741: }
5742:
5743: $classMemberDeclaringClass = $classMemberReflection->getDeclaringClass();
5744: $canAccessClassMember = static function (ClassReflection $classReflection) use ($classMemberReflection, $classMemberDeclaringClass) {
5745: if ($classMemberReflection->isPrivate()) {
5746: return $classReflection->getName() === $classMemberDeclaringClass->getName();
5747: }
5748:
5749: // protected
5750:
5751: if (
5752: $classReflection->getName() === $classMemberDeclaringClass->getName()
5753: || $classReflection->isSubclassOfClass($classMemberDeclaringClass->removeFinalKeywordOverride())
5754: ) {
5755: return true;
5756: }
5757:
5758: return $classMemberReflection->getDeclaringClass()->isSubclassOfClass($classReflection);
5759: };
5760:
5761: foreach ($this->inClosureBindScopeClasses as $inClosureBindScopeClass) {
5762: if (!$this->reflectionProvider->hasClass($inClosureBindScopeClass)) {
5763: continue;
5764: }
5765:
5766: if ($canAccessClassMember($this->reflectionProvider->getClass($inClosureBindScopeClass))) {
5767: return true;
5768: }
5769: }
5770:
5771: if ($this->isInClass()) {
5772: return $canAccessClassMember($this->getClassReflection());
5773: }
5774:
5775: return false;
5776: }
5777:
5778: /**
5779: * @return string[]
5780: */
5781: public function debug(): array
5782: {
5783: $descriptions = [];
5784: foreach ($this->expressionTypes as $name => $variableTypeHolder) {
5785: $key = sprintf('%s (%s)', $name, $variableTypeHolder->getCertainty()->describe());
5786: $descriptions[$key] = $variableTypeHolder->getType()->describe(VerbosityLevel::precise());
5787: }
5788: foreach ($this->nativeExpressionTypes as $exprString => $nativeTypeHolder) {
5789: $key = sprintf('native %s (%s)', $exprString, $nativeTypeHolder->getCertainty()->describe());
5790: $descriptions[$key] = $nativeTypeHolder->getType()->describe(VerbosityLevel::precise());
5791: }
5792:
5793: foreach ($this->conditionalExpressions as $exprString => $holders) {
5794: foreach (array_values($holders) as $i => $holder) {
5795: $key = sprintf('condition about %s #%d', $exprString, $i + 1);
5796: $parts = [];
5797: foreach ($holder->getConditionExpressionTypeHolders() as $conditionalExprString => $expressionTypeHolder) {
5798: $parts[] = $conditionalExprString . '=' . $expressionTypeHolder->getType()->describe(VerbosityLevel::precise());
5799: }
5800: $condition = implode(' && ', $parts);
5801: $descriptions[$key] = sprintf(
5802: 'if %s then %s is %s (%s)',
5803: $condition,
5804: $exprString,
5805: $holder->getTypeHolder()->getType()->describe(VerbosityLevel::precise()),
5806: $holder->getTypeHolder()->getCertainty()->describe(),
5807: );
5808: }
5809: }
5810:
5811: return $descriptions;
5812: }
5813:
5814: /**
5815: * @param non-empty-string $className
5816: */
5817: private function exactInstantiation(New_ $node, string $className): ?Type
5818: {
5819: $resolvedClassName = $this->resolveExactName($className);
5820: $isStatic = false;
5821: if ($resolvedClassName === null) {
5822: if (strtolower($className) !== 'static') {
5823: return null;
5824: }
5825:
5826: if (!$this->isInClass()) {
5827: return null;
5828: }
5829: $resolvedClassName = $this->getClassReflection()->getName();
5830: $isStatic = true;
5831: }
5832:
5833: if (!$this->reflectionProvider->hasClass($resolvedClassName)) {
5834: return null;
5835: }
5836:
5837: $classReflection = $this->reflectionProvider->getClass($resolvedClassName);
5838: $nonFinalClassReflection = $classReflection;
5839: if (!$isStatic) {
5840: $classReflection = $classReflection->asFinal();
5841: }
5842: if ($classReflection->hasConstructor()) {
5843: $constructorMethod = $classReflection->getConstructor();
5844: } else {
5845: $constructorMethod = new DummyConstructorReflection($classReflection);
5846: }
5847:
5848: if ($constructorMethod->getName() === '') {
5849: throw new ShouldNotHappenException();
5850: }
5851:
5852: $resolvedTypes = [];
5853: $methodCall = new Expr\StaticCall(
5854: new Name($resolvedClassName),
5855: new Node\Identifier($constructorMethod->getName()),
5856: $node->getArgs(),
5857: );
5858:
5859: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
5860: $this,
5861: $methodCall->getArgs(),
5862: $constructorMethod->getVariants(),
5863: $constructorMethod->getNamedArgumentsVariants(),
5864: );
5865: $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall);
5866:
5867: if ($normalizedMethodCall !== null) {
5868: foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicStaticMethodReturnTypeExtensionsForClass($classReflection->getName()) as $dynamicStaticMethodReturnTypeExtension) {
5869: if (!$dynamicStaticMethodReturnTypeExtension->isStaticMethodSupported($constructorMethod)) {
5870: continue;
5871: }
5872:
5873: $resolvedType = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall(
5874: $constructorMethod,
5875: $normalizedMethodCall,
5876: $this,
5877: );
5878: if ($resolvedType === null) {
5879: continue;
5880: }
5881:
5882: $resolvedTypes[] = $resolvedType;
5883: }
5884: }
5885:
5886: if (count($resolvedTypes) > 0) {
5887: return TypeCombinator::union(...$resolvedTypes);
5888: }
5889:
5890: $methodResult = $this->getType($methodCall);
5891: if ($methodResult instanceof NeverType && $methodResult->isExplicit()) {
5892: return $methodResult;
5893: }
5894:
5895: $objectType = $isStatic ? new StaticType($classReflection) : new ObjectType($resolvedClassName, classReflection: $classReflection);
5896: if (!$classReflection->isGeneric()) {
5897: return $objectType;
5898: }
5899:
5900: $assignedToProperty = $node->getAttribute(NewAssignedToPropertyVisitor::ATTRIBUTE_NAME);
5901: if ($assignedToProperty !== null) {
5902: $constructorVariants = $constructorMethod->getVariants();
5903: if (count($constructorVariants) === 1) {
5904: $constructorVariant = $constructorVariants[0];
5905: $classTemplateTypes = $classReflection->getTemplateTypeMap()->getTypes();
5906: $originalClassTemplateTypes = $classTemplateTypes;
5907: foreach ($constructorVariant->getParameters() as $parameter) {
5908: TypeTraverser::map($parameter->getType(), static function (Type $type, callable $traverse) use (&$classTemplateTypes): Type {
5909: if ($type instanceof TemplateType && array_key_exists($type->getName(), $classTemplateTypes)) {
5910: $classTemplateType = $classTemplateTypes[$type->getName()];
5911: if ($classTemplateType instanceof TemplateType && $classTemplateType->getScope()->equals($type->getScope())) {
5912: unset($classTemplateTypes[$type->getName()]);
5913: }
5914: return $type;
5915: }
5916:
5917: return $traverse($type);
5918: });
5919: }
5920:
5921: if (count($classTemplateTypes) === count($originalClassTemplateTypes)) {
5922: $propertyType = TypeCombinator::removeNull($this->getType($assignedToProperty));
5923: $nonFinalObjectType = $isStatic ? new StaticType($nonFinalClassReflection) : new ObjectType($resolvedClassName, classReflection: $nonFinalClassReflection);
5924: if ($nonFinalObjectType->isSuperTypeOf($propertyType)->yes()) {
5925: return $propertyType;
5926: }
5927: }
5928: }
5929: }
5930:
5931: if ($constructorMethod instanceof DummyConstructorReflection) {
5932: if ($isStatic) {
5933: return new GenericStaticType(
5934: $classReflection,
5935: $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()),
5936: null,
5937: [],
5938: );
5939: }
5940:
5941: $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds());
5942: return new GenericObjectType(
5943: $resolvedClassName,
5944: $types,
5945: classReflection: $classReflection->withTypes($types)->asFinal(),
5946: );
5947: }
5948:
5949: if ($constructorMethod->getDeclaringClass()->getName() !== $classReflection->getName()) {
5950: if (!$constructorMethod->getDeclaringClass()->isGeneric()) {
5951: if ($isStatic) {
5952: return new GenericStaticType(
5953: $classReflection,
5954: $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()),
5955: null,
5956: [],
5957: );
5958: }
5959:
5960: $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds());
5961: return new GenericObjectType(
5962: $resolvedClassName,
5963: $types,
5964: classReflection: $classReflection->withTypes($types)->asFinal(),
5965: );
5966: }
5967: $newType = new GenericObjectType($resolvedClassName, $classReflection->typeMapToList($classReflection->getTemplateTypeMap()));
5968: $ancestorType = $newType->getAncestorWithClassName($constructorMethod->getDeclaringClass()->getName());
5969: if ($ancestorType === null) {
5970: if ($isStatic) {
5971: return new GenericStaticType(
5972: $classReflection,
5973: $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()),
5974: null,
5975: [],
5976: );
5977: }
5978:
5979: $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds());
5980: return new GenericObjectType(
5981: $resolvedClassName,
5982: $types,
5983: classReflection: $classReflection->withTypes($types)->asFinal(),
5984: );
5985: }
5986: $ancestorClassReflections = $ancestorType->getObjectClassReflections();
5987: if (count($ancestorClassReflections) !== 1) {
5988: if ($isStatic) {
5989: return new GenericStaticType(
5990: $classReflection,
5991: $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()),
5992: null,
5993: [],
5994: );
5995: }
5996:
5997: $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds());
5998: return new GenericObjectType(
5999: $resolvedClassName,
6000: $types,
6001: classReflection: $classReflection->withTypes($types)->asFinal(),
6002: );
6003: }
6004:
6005: $newParentNode = new New_(new Name($constructorMethod->getDeclaringClass()->getName()), $node->args);
6006: $newParentType = $this->getType($newParentNode);
6007: $newParentTypeClassReflections = $newParentType->getObjectClassReflections();
6008: if (count($newParentTypeClassReflections) !== 1) {
6009: if ($isStatic) {
6010: return new GenericStaticType(
6011: $classReflection,
6012: $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds()),
6013: null,
6014: [],
6015: );
6016: }
6017:
6018: $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap()->resolveToBounds());
6019: return new GenericObjectType(
6020: $resolvedClassName,
6021: $types,
6022: classReflection: $classReflection->withTypes($types)->asFinal(),
6023: );
6024: }
6025: $newParentTypeClassReflection = $newParentTypeClassReflections[0];
6026:
6027: $ancestorClassReflection = $ancestorClassReflections[0];
6028: $ancestorMapping = [];
6029: foreach ($ancestorClassReflection->getActiveTemplateTypeMap()->getTypes() as $typeName => $templateType) {
6030: if (!$templateType instanceof TemplateType) {
6031: continue;
6032: }
6033:
6034: $ancestorMapping[$typeName] = $templateType;
6035: }
6036:
6037: $resolvedTypeMap = [];
6038: foreach ($newParentTypeClassReflection->getActiveTemplateTypeMap()->getTypes() as $typeName => $type) {
6039: if (!array_key_exists($typeName, $ancestorMapping)) {
6040: continue;
6041: }
6042:
6043: $ancestorType = $ancestorMapping[$typeName];
6044: if (!$ancestorType->getBound()->isSuperTypeOf($type)->yes()) {
6045: continue;
6046: }
6047:
6048: if (!array_key_exists($ancestorType->getName(), $resolvedTypeMap)) {
6049: $resolvedTypeMap[$ancestorType->getName()] = $type;
6050: continue;
6051: }
6052:
6053: $resolvedTypeMap[$ancestorType->getName()] = TypeCombinator::union($resolvedTypeMap[$ancestorType->getName()], $type);
6054: }
6055:
6056: if ($isStatic) {
6057: return new GenericStaticType(
6058: $classReflection,
6059: $classReflection->typeMapToList(new TemplateTypeMap($resolvedTypeMap)),
6060: null,
6061: [],
6062: );
6063: }
6064:
6065: $types = $classReflection->typeMapToList(new TemplateTypeMap($resolvedTypeMap));
6066: return new GenericObjectType(
6067: $resolvedClassName,
6068: $types,
6069: classReflection: $classReflection->withTypes($types)->asFinal(),
6070: );
6071: }
6072:
6073: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
6074: $this,
6075: $methodCall->getArgs(),
6076: $constructorMethod->getVariants(),
6077: $constructorMethod->getNamedArgumentsVariants(),
6078: );
6079:
6080: $resolvedTemplateTypeMap = $parametersAcceptor->getResolvedTemplateTypeMap();
6081: $types = $classReflection->typeMapToList($classReflection->getTemplateTypeMap());
6082: $newGenericType = new GenericObjectType(
6083: $resolvedClassName,
6084: $types,
6085: classReflection: $classReflection->withTypes($types)->asFinal(),
6086: );
6087: if ($isStatic) {
6088: $newGenericType = new GenericStaticType(
6089: $classReflection,
6090: $types,
6091: null,
6092: [],
6093: );
6094: }
6095: return TypeTraverser::map($newGenericType, static function (Type $type, callable $traverse) use ($resolvedTemplateTypeMap): Type {
6096: if ($type instanceof TemplateType && !$type->isArgument()) {
6097: $newType = $resolvedTemplateTypeMap->getType($type->getName());
6098: if ($newType === null || $newType instanceof ErrorType) {
6099: return $type->getDefault() ?? $type->getBound();
6100: }
6101:
6102: return TemplateTypeHelper::generalizeInferredTemplateType($type, $newType);
6103: }
6104:
6105: return $traverse($type);
6106: });
6107: }
6108:
6109: private function filterTypeWithMethod(Type $typeWithMethod, string $methodName): ?Type
6110: {
6111: if ($typeWithMethod instanceof UnionType) {
6112: $typeWithMethod = $typeWithMethod->filterTypes(static fn (Type $innerType) => $innerType->hasMethod($methodName)->yes());
6113: }
6114:
6115: if (!$typeWithMethod->hasMethod($methodName)->yes()) {
6116: return null;
6117: }
6118:
6119: return $typeWithMethod;
6120: }
6121:
6122: /** @api */
6123: public function getMethodReflection(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection
6124: {
6125: $type = $this->filterTypeWithMethod($typeWithMethod, $methodName);
6126: if ($type === null) {
6127: return null;
6128: }
6129:
6130: return $type->getMethod($methodName, $this);
6131: }
6132:
6133: public function getNakedMethod(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection
6134: {
6135: $type = $this->filterTypeWithMethod($typeWithMethod, $methodName);
6136: if ($type === null) {
6137: return null;
6138: }
6139:
6140: return $type->getUnresolvedMethodPrototype($methodName, $this)->getNakedMethod();
6141: }
6142:
6143: /**
6144: * @param MethodCall|Node\Expr\StaticCall $methodCall
6145: */
6146: private function methodCallReturnType(Type $typeWithMethod, string $methodName, Expr $methodCall): ?Type
6147: {
6148: $typeWithMethod = $this->filterTypeWithMethod($typeWithMethod, $methodName);
6149: if ($typeWithMethod === null) {
6150: return null;
6151: }
6152:
6153: $methodReflection = $typeWithMethod->getMethod($methodName, $this);
6154: $parametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
6155: $this,
6156: $methodCall->getArgs(),
6157: $methodReflection->getVariants(),
6158: $methodReflection->getNamedArgumentsVariants(),
6159: );
6160: if ($methodCall instanceof MethodCall) {
6161: $normalizedMethodCall = ArgumentsNormalizer::reorderMethodArguments($parametersAcceptor, $methodCall);
6162: } else {
6163: $normalizedMethodCall = ArgumentsNormalizer::reorderStaticCallArguments($parametersAcceptor, $methodCall);
6164: }
6165: if ($normalizedMethodCall === null) {
6166: return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $methodCall);
6167: }
6168:
6169: $resolvedTypes = [];
6170: foreach ($typeWithMethod->getObjectClassNames() as $className) {
6171: if ($normalizedMethodCall instanceof MethodCall) {
6172: foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicMethodReturnTypeExtensionsForClass($className) as $dynamicMethodReturnTypeExtension) {
6173: if (!$dynamicMethodReturnTypeExtension->isMethodSupported($methodReflection)) {
6174: continue;
6175: }
6176:
6177: $resolvedType = $dynamicMethodReturnTypeExtension->getTypeFromMethodCall($methodReflection, $normalizedMethodCall, $this);
6178: if ($resolvedType === null) {
6179: continue;
6180: }
6181:
6182: $resolvedTypes[] = $resolvedType;
6183: }
6184: } else {
6185: foreach ($this->dynamicReturnTypeExtensionRegistry->getDynamicStaticMethodReturnTypeExtensionsForClass($className) as $dynamicStaticMethodReturnTypeExtension) {
6186: if (!$dynamicStaticMethodReturnTypeExtension->isStaticMethodSupported($methodReflection)) {
6187: continue;
6188: }
6189:
6190: $resolvedType = $dynamicStaticMethodReturnTypeExtension->getTypeFromStaticMethodCall(
6191: $methodReflection,
6192: $normalizedMethodCall,
6193: $this,
6194: );
6195: if ($resolvedType === null) {
6196: continue;
6197: }
6198:
6199: $resolvedTypes[] = $resolvedType;
6200: }
6201: }
6202: }
6203:
6204: if (count($resolvedTypes) > 0) {
6205: return $this->transformVoidToNull(TypeCombinator::union(...$resolvedTypes), $methodCall);
6206: }
6207:
6208: return $this->transformVoidToNull($parametersAcceptor->getReturnType(), $methodCall);
6209: }
6210:
6211: /** @api */
6212: public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection
6213: {
6214: if ($typeWithProperty instanceof UnionType) {
6215: $typeWithProperty = $typeWithProperty->filterTypes(static fn (Type $innerType) => $innerType->hasProperty($propertyName)->yes());
6216: }
6217: if (!$typeWithProperty->hasProperty($propertyName)->yes()) {
6218: return null;
6219: }
6220:
6221: return $typeWithProperty->getProperty($propertyName, $this);
6222: }
6223:
6224: /**
6225: * @param PropertyFetch|Node\Expr\StaticPropertyFetch $propertyFetch
6226: */
6227: private function propertyFetchType(Type $fetchedOnType, string $propertyName, Expr $propertyFetch): ?Type
6228: {
6229: $propertyReflection = $this->getPropertyReflection($fetchedOnType, $propertyName);
6230: if ($propertyReflection === null) {
6231: return null;
6232: }
6233:
6234: if ($this->isInExpressionAssign($propertyFetch)) {
6235: return $propertyReflection->getWritableType();
6236: }
6237:
6238: return $propertyReflection->getReadableType();
6239: }
6240:
6241: public function getConstantReflection(Type $typeWithConstant, string $constantName): ?ClassConstantReflection
6242: {
6243: if ($typeWithConstant instanceof UnionType) {
6244: $typeWithConstant = $typeWithConstant->filterTypes(static fn (Type $innerType) => $innerType->hasConstant($constantName)->yes());
6245: }
6246: if (!$typeWithConstant->hasConstant($constantName)->yes()) {
6247: return null;
6248: }
6249:
6250: return $typeWithConstant->getConstant($constantName);
6251: }
6252:
6253: public function getConstantExplicitTypeFromConfig(string $constantName, Type $constantType): Type
6254: {
6255: return $this->constantResolver->resolveConstantType($constantName, $constantType);
6256: }
6257:
6258: /**
6259: * @return array<string, ExpressionTypeHolder>
6260: */
6261: private function getConstantTypes(): array
6262: {
6263: $constantTypes = [];
6264: foreach ($this->expressionTypes as $exprString => $typeHolder) {
6265: $expr = $typeHolder->getExpr();
6266: if (!$expr instanceof ConstFetch) {
6267: continue;
6268: }
6269: $constantTypes[$exprString] = $typeHolder;
6270: }
6271: return $constantTypes;
6272: }
6273:
6274: private function getGlobalConstantType(Name $name): ?Type
6275: {
6276: $fetches = [];
6277: if (!$name->isFullyQualified() && $this->getNamespace() !== null) {
6278: $fetches[] = new ConstFetch(new FullyQualified([$this->getNamespace(), $name->toString()]));
6279: }
6280:
6281: $fetches[] = new ConstFetch(new FullyQualified($name->toString()));
6282: $fetches[] = new ConstFetch($name);
6283:
6284: foreach ($fetches as $constFetch) {
6285: if ($this->hasExpressionType($constFetch)->yes()) {
6286: return $this->getType($constFetch);
6287: }
6288: }
6289:
6290: return null;
6291: }
6292:
6293: /**
6294: * @return array<string, ExpressionTypeHolder>
6295: */
6296: private function getNativeConstantTypes(): array
6297: {
6298: $constantTypes = [];
6299: foreach ($this->nativeExpressionTypes as $exprString => $typeHolder) {
6300: $expr = $typeHolder->getExpr();
6301: if (!$expr instanceof ConstFetch) {
6302: continue;
6303: }
6304: $constantTypes[$exprString] = $typeHolder;
6305: }
6306: return $constantTypes;
6307: }
6308:
6309: public function getIterableKeyType(Type $iteratee): Type
6310: {
6311: if ($iteratee instanceof UnionType) {
6312: $filtered = $iteratee->filterTypes(static fn (Type $innerType) => $innerType->isIterable()->yes());
6313: if (!$filtered instanceof NeverType) {
6314: $iteratee = $filtered;
6315: }
6316: }
6317:
6318: return $iteratee->getIterableKeyType();
6319: }
6320:
6321: public function getIterableValueType(Type $iteratee): Type
6322: {
6323: if ($iteratee instanceof UnionType) {
6324: $filtered = $iteratee->filterTypes(static fn (Type $innerType) => $innerType->isIterable()->yes());
6325: if (!$filtered instanceof NeverType) {
6326: $iteratee = $filtered;
6327: }
6328: }
6329:
6330: return $iteratee->getIterableValueType();
6331: }
6332:
6333: public function getPhpVersion(): PhpVersions
6334: {
6335: $constType = $this->getGlobalConstantType(new Name('PHP_VERSION_ID'));
6336:
6337: $isOverallPhpVersionRange = false;
6338: if (
6339: $constType instanceof IntegerRangeType
6340: && $constType->getMin() === ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID
6341: && ($constType->getMax() === null || $constType->getMax() === PhpVersionFactory::MAX_PHP_VERSION)
6342: ) {
6343: $isOverallPhpVersionRange = true;
6344: }
6345:
6346: if ($constType !== null && !$isOverallPhpVersionRange) {
6347: return new PhpVersions($constType);
6348: }
6349:
6350: if (is_array($this->configPhpVersion)) {
6351: return new PhpVersions(IntegerRangeType::fromInterval($this->configPhpVersion['min'], $this->configPhpVersion['max']));
6352: }
6353: return new PhpVersions(new ConstantIntegerType($this->phpVersion->getVersionId()));
6354: }
6355:
6356: }
6357: