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