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