1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Analyser;
4:
5: use PhpParser\Node;
6: use PhpParser\Node\Arg;
7: use PhpParser\Node\ComplexType;
8: use PhpParser\Node\Expr;
9: use PhpParser\Node\Expr\Array_;
10: use PhpParser\Node\Expr\ClassConstFetch;
11: use PhpParser\Node\Expr\ConstFetch;
12: use PhpParser\Node\Expr\FuncCall;
13: use PhpParser\Node\Expr\Match_;
14: use PhpParser\Node\Expr\MethodCall;
15: use PhpParser\Node\Expr\PropertyFetch;
16: use PhpParser\Node\Expr\Variable;
17: use PhpParser\Node\Identifier;
18: use PhpParser\Node\Name;
19: use PhpParser\Node\Name\FullyQualified;
20: use PhpParser\Node\PropertyHook;
21: use PhpParser\Node\Scalar;
22: use PhpParser\Node\Scalar\String_;
23: use PhpParser\Node\Stmt\ClassMethod;
24: use PhpParser\Node\Stmt\Function_;
25: use PhpParser\NodeFinder;
26: use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver;
27: use PHPStan\Analyser\Generics\ClosureSignatureInference;
28: use PHPStan\Analyser\Generics\TemplateArgumentConstraints;
29: use PHPStan\Analyser\Generics\TemplateArgumentFrame;
30: use PHPStan\Analyser\Traverser\TransformStaticTypeTraverser;
31: use PHPStan\Collectors\Collector;
32: use PHPStan\Node\EmitCollectedDataNode;
33: use PHPStan\Node\Expr\AlwaysRememberedExpr;
34: use PHPStan\Node\Expr\CloneReinitializationExpr;
35: use PHPStan\Node\Expr\IntertwinedVariableByReferenceWithExpr;
36: use PHPStan\Node\Expr\NativeTypeExpr;
37: use PHPStan\Node\Expr\OriginalForeachKeyExpr;
38: use PHPStan\Node\Expr\OriginalForeachValueExpr;
39: use PHPStan\Node\Expr\ParameterVariableOriginalValueExpr;
40: use PHPStan\Node\Expr\PropertyInitializationExpr;
41: use PHPStan\Node\Expr\SetExistingOffsetValueTypeExpr;
42: use PHPStan\Node\IssetExpr;
43: use PHPStan\Node\Printer\ExprPrinter;
44: use PHPStan\Node\VirtualNode;
45: use PHPStan\Php\PhpVersionFactory;
46: use PHPStan\Php\PhpVersions;
47: use PHPStan\PhpDoc\ResolvedPhpDocBlock;
48: use PHPStan\Reflection\Assertions;
49: use PHPStan\Reflection\AttributeReflection;
50: use PHPStan\Reflection\ClassConstantReflection;
51: use PHPStan\Reflection\ClassMemberReflection;
52: use PHPStan\Reflection\ClassReflection;
53: use PHPStan\Reflection\ExtendedMethodReflection;
54: use PHPStan\Reflection\ExtendedParametersAcceptor;
55: use PHPStan\Reflection\ExtendedPropertyReflection;
56: use PHPStan\Reflection\FunctionReflection;
57: use PHPStan\Reflection\InitializerExprContext;
58: use PHPStan\Reflection\MethodReflection;
59: use PHPStan\Reflection\ParameterReflection;
60: use PHPStan\Reflection\ParametersAcceptorSelector;
61: use PHPStan\Reflection\Php\PhpFunctionFromParserNodeReflection;
62: use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection;
63: use PHPStan\Reflection\PropertyReflection;
64: use PHPStan\ShouldNotHappenException;
65: use PHPStan\TrinaryLogic;
66: use PHPStan\Turbo\ShadowedByTurboExtension;
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\GeneralizePrecision;
83: use PHPStan\Type\Generic\TemplateTypeHelper;
84: use PHPStan\Type\Generic\TemplateTypeMap;
85: use PHPStan\Type\IntegerRangeType;
86: use PHPStan\Type\IntegerType;
87: use PHPStan\Type\IntersectionType;
88: use PHPStan\Type\MixedType;
89: use PHPStan\Type\NeverType;
90: use PHPStan\Type\NullType;
91: use PHPStan\Type\ObjectType;
92: use PHPStan\Type\StaticType;
93: use PHPStan\Type\StaticTypeFactory;
94: use PHPStan\Type\StringType;
95: use PHPStan\Type\ThisType;
96: use PHPStan\Type\Type;
97: use PHPStan\Type\TypeCombinator;
98: use PHPStan\Type\TypeTraverser;
99: use PHPStan\Type\TypeUtils;
100: use PHPStan\Type\TypeWithClassName;
101: use PHPStan\Type\UnionType;
102: use PHPStan\Type\VerbosityLevel;
103: use PHPStan\Type\VoidType;
104: use Serializable;
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_values;
117: use function count;
118: use function ctype_alnum;
119: use function explode;
120: use function get_class;
121: use function implode;
122: use function in_array;
123: use function is_string;
124: use function ltrim;
125: use function md5;
126: use function min;
127: use function preg_match;
128: use function preg_match_all;
129: use function spl_object_id;
130: use function sprintf;
131: use function str_starts_with;
132: use function strlen;
133: use function strtolower;
134: use function substr;
135: use function uksort;
136: use function usort;
137: use const PHP_INT_MAX;
138: use const PHP_INT_MIN;
139:
140: #[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/MutatingScope.cpp')]
141: class MutatingScope implements Scope, NodeCallbackInvoker, CollectedDataEmitter, DependencyTracker
142: {
143:
144: private const COMPLEX_UNION_TYPE_MEMBER_LIMIT = 8;
145:
146: /** Distinct name/namespace combinations getGlobalConstantType() remembers the expression keys of. */
147: private const GLOBAL_CONSTANT_FETCH_KEYS_LIMIT = 8192;
148:
149: /** Magic methods that let the author decide which properties survive a serialize()/unserialize() round trip. */
150: private const CUSTOM_SERIALIZATION_METHODS = ['__sleep', '__serialize', '__unserialize'];
151:
152: /**
153: * Expression keys the constant-fetch nodes of a global constant lookup print
154: * as, by the name class, name and namespace they are built from. The nodes
155: * exist only to produce those keys, and a freshly built node can never hit
156: * the printer's node-attribute cache - so without this every lookup printed
157: * two or three of them again. PHP_VERSION_ID alone accounted for ~30% of all
158: * expression-key prints in a self-analysis run.
159: *
160: * @var array<string, list<string>>
161: */
162: private static array $globalConstantFetchKeys = [];
163:
164: /**
165: * @internal accessed by ScopeOps (native and PHP implementations)
166: * @var array<string, Type>
167: */
168: public array $resolvedTypes = [];
169:
170: private ?self $nodeCallbackScope = null;
171:
172: /** @var non-empty-string|null */
173: private ?string $namespace;
174:
175: private ?self $scopeOutOfFirstLevelStatement = null;
176:
177: private ?self $scopeWithPromotedNativeTypes = null;
178:
179: /**
180: * @param array<string, ExpressionTypeHolder> $expressionTypes
181: * @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
182: * @param list<non-empty-string> $inClosureBindScopeClasses
183: * @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
184: * @param array<string, true> $currentlyAllowedUndefinedExpressions
185: * @param array<string, ExpressionTypeHolder> $nativeExpressionTypes
186: * @param list<array{MethodReflection|FunctionReflection|null, ParameterReflection|null}> $inFunctionCallsStack
187: */
188: public function __construct(
189: private ScopeServices $services,
190: protected InternalScopeFactory $scopeFactory,
191: protected ScopeContext $context,
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: protected ?TemplateArgumentFrame $templateArgumentFrame = null,
208: protected ?TemplateArgumentConstraints $templateArgumentConstraints = null,
209: )
210: {
211: if ($namespace === '') {
212: $namespace = null;
213: }
214:
215: $this->namespace = $namespace;
216: }
217:
218: public function toNodeCallbackScope(): self
219: {
220: if ($this->nodeCallbackScope !== null) {
221: return $this->nodeCallbackScope;
222: }
223:
224: $nodeCallbackScope = $this->scopeFactory->toNodeCallbackScopeFactory()->create(
225: $this->context,
226: $this->isDeclareStrictTypes(),
227: $this->getFunction(),
228: $this->getNamespace(),
229: $this->expressionTypes,
230: $this->nativeExpressionTypes,
231: $this->conditionalExpressions,
232: $this->inClosureBindScopeClasses,
233: $this->anonymousFunctionReflection,
234: $this->isInFirstLevelStatement(),
235: $this->currentlyAssignedExpressions,
236: $this->currentlyAllowedUndefinedExpressions,
237: $this->inFunctionCallsStack,
238: $this->afterExtractCall,
239: $this->parentScope,
240: $this->nativeTypesPromoted,
241: $this->templateArgumentFrame,
242: $this->templateArgumentConstraints,
243: );
244: if ($nodeCallbackScope instanceof NodeCallbackScope) {
245: $nodeCallbackScope->seedWalkScope($this);
246: }
247:
248: return $this->nodeCallbackScope = $nodeCallbackScope;
249: }
250:
251: public function toWalkScope(): self
252: {
253: return $this;
254: }
255:
256: /** @deprecated */
257: public function toMutatingScope(): self
258: {
259: return $this;
260: }
261:
262: /** @api */
263: public function getFile(): string
264: {
265: return $this->context->getFile();
266: }
267:
268: /** @api */
269: public function getFileDescription(): string
270: {
271: if ($this->context->getTraitReflection() === null) {
272: return $this->getFile();
273: }
274:
275: /** @var ClassReflection $classReflection */
276: $classReflection = $this->context->getClassReflection();
277:
278: $className = $classReflection->getDisplayName();
279: if (!$classReflection->isAnonymous()) {
280: $className = sprintf('class %s', $className);
281: }
282:
283: $traitReflection = $this->context->getTraitReflection();
284: if ($traitReflection->getFileName() === null) {
285: throw new ShouldNotHappenException();
286: }
287:
288: return sprintf(
289: '%s (in context of %s)',
290: $traitReflection->getFileName(),
291: $className,
292: );
293: }
294:
295: /** @api */
296: public function isDeclareStrictTypes(): bool
297: {
298: return $this->declareStrictTypes;
299: }
300:
301: public function enterDeclareStrictTypes(): self
302: {
303: return $this->scopeFactory->create(
304: $this->context,
305: true,
306: null,
307: null,
308: $this->expressionTypes,
309: $this->nativeExpressionTypes,
310: templateArgumentFrame: $this->templateArgumentFrame,
311: templateArgumentConstraints: $this->templateArgumentConstraints,
312: );
313: }
314:
315: /**
316: * @param array<string, ExpressionTypeHolder> $currentExpressionTypes
317: * @return array<string, ExpressionTypeHolder>
318: */
319: private function rememberConstructorExpressions(array $currentExpressionTypes): array
320: {
321: $rememberPropertyState = !$this->classHasCustomSerialization();
322: $expressionTypes = [];
323: foreach ($currentExpressionTypes as $exprString => $expressionTypeHolder) {
324: $expr = $expressionTypeHolder->getExpr();
325: if ($expr instanceof FuncCall) {
326: if (
327: !$expr->name instanceof Name
328: // interface_exists() etc. imply class_exists() therefore not listed here
329: || !in_array($expr->name->name, ['class_exists', 'function_exists'], true)
330: ) {
331: continue;
332: }
333: } elseif ($expr instanceof PropertyFetch) {
334: if (!$rememberPropertyState || !$this->isReadonlyPropertyFetch($expr, true)) {
335: continue;
336: }
337: } elseif ($expr instanceof PropertyInitializationExpr) {
338: if (!$rememberPropertyState) {
339: continue;
340: }
341: } elseif (!$expr instanceof ConstFetch) {
342: continue;
343: }
344:
345: $expressionTypes[$exprString] = $expressionTypeHolder;
346: }
347:
348: if (array_key_exists('$this', $currentExpressionTypes)) {
349: $expressionTypes['$this'] = $currentExpressionTypes['$this'];
350: }
351:
352: return $expressionTypes;
353: }
354:
355: /**
356: * A class with custom serialization logic can be rebuilt by unserialize()
357: * without the constructor ever running, and the author decides which properties
358: * make the round trip - so nothing the constructor established can be relied upon
359: * in the other methods.
360: */
361: private function classHasCustomSerialization(): bool
362: {
363: if (!$this->isInClass()) {
364: return false;
365: }
366:
367: $classReflection = $this->getClassReflection();
368: foreach (self::CUSTOM_SERIALIZATION_METHODS as $methodName) {
369: if ($classReflection->hasNativeMethod($methodName)) {
370: return true;
371: }
372: }
373:
374: return $classReflection->implementsInterface(Serializable::class)
375: && $classReflection->hasNativeMethod('unserialize');
376: }
377:
378: public function rememberConstructorScope(): self
379: {
380: return $this->scopeFactory->create(
381: $this->context,
382: $this->isDeclareStrictTypes(),
383: null,
384: $this->getNamespace(),
385: $this->rememberConstructorExpressions($this->expressionTypes),
386: $this->rememberConstructorExpressions($this->nativeExpressionTypes),
387: $this->conditionalExpressions,
388: $this->inClosureBindScopeClasses,
389: $this->anonymousFunctionReflection,
390: $this->inFirstLevelStatement,
391: [],
392: [],
393: $this->inFunctionCallsStack,
394: $this->afterExtractCall,
395: $this->parentScope,
396: $this->nativeTypesPromoted,
397: $this->templateArgumentFrame,
398: $this->templateArgumentConstraints,
399: );
400: }
401:
402: /** @internal called by ScopeOps */
403: public function isReadonlyPropertyFetch(PropertyFetch $expr, bool $allowOnlyOnThis): bool
404: {
405: if (!$this->services->phpVersion->supportsReadOnlyProperties()) {
406: return false;
407: }
408:
409: while ($expr instanceof PropertyFetch) {
410: if ($expr->var instanceof Variable) {
411: if (
412: $allowOnlyOnThis
413: && (
414: ! $expr->name instanceof Node\Identifier
415: || !is_string($expr->var->name)
416: || $expr->var->name !== 'this'
417: )
418: ) {
419: return false;
420: }
421: } elseif (!$expr->var instanceof PropertyFetch) {
422: return false;
423: }
424:
425: $propertyReflection = $this->services->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this);
426: if ($propertyReflection === null) {
427: return false;
428: }
429:
430: $nativePropertyReflection = $propertyReflection->getNativeReflection();
431: if ($nativePropertyReflection === null || !$nativePropertyReflection->isReadOnly()) {
432: return false;
433: }
434:
435: $expr = $expr->var;
436: }
437:
438: return true;
439: }
440:
441: /** @api */
442: public function isInClass(): bool
443: {
444: return $this->context->getClassReflection() !== null;
445: }
446:
447: /** @api */
448: public function isInTrait(): bool
449: {
450: return $this->context->getTraitReflection() !== null;
451: }
452:
453: /** @api */
454: public function getClassReflection(): ?ClassReflection
455: {
456: return $this->context->getClassReflection();
457: }
458:
459: /** @api */
460: public function getTraitReflection(): ?ClassReflection
461: {
462: return $this->context->getTraitReflection();
463: }
464:
465: /**
466: * @api
467: */
468: public function getFunction(): ?PhpFunctionFromParserNodeReflection
469: {
470: return $this->function;
471: }
472:
473: /** @api */
474: public function getFunctionName(): ?string
475: {
476: return $this->function !== null ? $this->function->getName() : null;
477: }
478:
479: /** @api */
480: public function getNamespace(): ?string
481: {
482: return $this->namespace;
483: }
484:
485: /** @api */
486: public function getParentScope(): ?self
487: {
488: return $this->parentScope;
489: }
490:
491: /** @api */
492: public function canAnyVariableExist(): bool
493: {
494: return ($this->function === null && !$this->isInAnonymousFunction()) || $this->afterExtractCall;
495: }
496:
497: public function afterExtractCall(): self
498: {
499: return $this->scopeFactory->create(
500: $this->context,
501: $this->isDeclareStrictTypes(),
502: $this->getFunction(),
503: $this->getNamespace(),
504: $this->expressionTypes,
505: $this->nativeExpressionTypes,
506: [],
507: $this->inClosureBindScopeClasses,
508: $this->anonymousFunctionReflection,
509: $this->isInFirstLevelStatement(),
510: $this->currentlyAssignedExpressions,
511: $this->currentlyAllowedUndefinedExpressions,
512: $this->inFunctionCallsStack,
513: true,
514: $this->parentScope,
515: $this->nativeTypesPromoted,
516: $this->templateArgumentFrame,
517: $this->templateArgumentConstraints,
518: );
519: }
520:
521: public function afterClearstatcacheCall(): self
522: {
523: $changed = false;
524:
525: $expressionTypes = $this->expressionTypes;
526: $nativeExpressionTypes = $this->nativeExpressionTypes;
527: foreach (array_keys($expressionTypes) as $exprString) {
528: // list from https://www.php.net/manual/en/function.clearstatcache.php
529:
530: // 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().
531: foreach ([
532: 'stat',
533: 'lstat',
534: 'file_exists',
535: 'is_writable',
536: 'is_writeable',
537: 'is_readable',
538: 'is_executable',
539: 'is_file',
540: 'is_dir',
541: 'is_link',
542: 'filectime',
543: 'fileatime',
544: 'filemtime',
545: 'fileinode',
546: 'filegroup',
547: 'fileowner',
548: 'filesize',
549: 'filetype',
550: 'fileperms',
551: ] as $functionName) {
552: if (!str_starts_with($exprString, $functionName . '(') && !str_starts_with($exprString, '\\' . $functionName . '(')) {
553: continue;
554: }
555:
556: unset($expressionTypes[$exprString]);
557: unset($nativeExpressionTypes[$exprString]);
558: $changed = true;
559: continue 2;
560: }
561: }
562:
563: if (!$changed) {
564: return $this;
565: }
566:
567: return $this->scopeFactory->create(
568: $this->context,
569: $this->isDeclareStrictTypes(),
570: $this->getFunction(),
571: $this->getNamespace(),
572: $expressionTypes,
573: $nativeExpressionTypes,
574: $this->conditionalExpressions,
575: $this->inClosureBindScopeClasses,
576: $this->anonymousFunctionReflection,
577: $this->isInFirstLevelStatement(),
578: $this->currentlyAssignedExpressions,
579: $this->currentlyAllowedUndefinedExpressions,
580: $this->inFunctionCallsStack,
581: $this->afterExtractCall,
582: $this->parentScope,
583: $this->nativeTypesPromoted,
584: $this->templateArgumentFrame,
585: $this->templateArgumentConstraints,
586: );
587: }
588:
589: public function afterOpenSslCall(string $openSslFunctionName): self
590: {
591: $expressionTypes = $this->expressionTypes;
592: $nativeExpressionTypes = $this->nativeExpressionTypes;
593:
594: $errorStringFunction = '\openssl_error_string()';
595: if (
596: !array_key_exists($errorStringFunction, $expressionTypes)
597: && !array_key_exists($errorStringFunction, $nativeExpressionTypes)
598: ) {
599: return $this;
600: }
601:
602: $changed = false;
603: if (in_array($openSslFunctionName, [
604: 'openssl_cipher_iv_length',
605: 'openssl_cms_decrypt',
606: 'openssl_cms_encrypt',
607: 'openssl_cms_read',
608: 'openssl_cms_sign',
609: 'openssl_cms_verify',
610: 'openssl_csr_export_to_file',
611: 'openssl_csr_export',
612: 'openssl_csr_get_public_key',
613: 'openssl_csr_get_subject',
614: 'openssl_csr_new',
615: 'openssl_csr_sign',
616: 'openssl_decrypt',
617: 'openssl_dh_compute_key',
618: 'openssl_digest',
619: 'openssl_encrypt',
620: 'openssl_get_curve_names',
621: 'openssl_get_privatekey',
622: 'openssl_get_publickey',
623: 'openssl_open',
624: 'openssl_pbkdf2',
625: 'openssl_pkcs12_export_to_file',
626: 'openssl_pkcs12_export',
627: 'openssl_pkcs12_read',
628: 'openssl_pkcs7_decrypt',
629: 'openssl_pkcs7_encrypt',
630: 'openssl_pkcs7_read',
631: 'openssl_pkcs7_sign',
632: 'openssl_pkcs7_verify',
633: 'openssl_pkey_derive',
634: 'openssl_pkey_export_to_file',
635: 'openssl_pkey_export',
636: 'openssl_pkey_get_private',
637: 'openssl_pkey_get_public',
638: 'openssl_pkey_new',
639: 'openssl_private_decrypt',
640: 'openssl_private_encrypt',
641: 'openssl_public_decrypt',
642: 'openssl_public_encrypt',
643: 'openssl_random_pseudo_bytes',
644: 'openssl_seal',
645: 'openssl_sign',
646: 'openssl_spki_export_challenge',
647: 'openssl_spki_export',
648: 'openssl_spki_new',
649: 'openssl_spki_verify',
650: 'openssl_verify',
651: 'openssl_x509_checkpurpose',
652: 'openssl_x509_export_to_file',
653: 'openssl_x509_export',
654: 'openssl_x509_fingerprint',
655: 'openssl_x509_read',
656: 'openssl_x509_verify',
657: ], true)) {
658: unset($expressionTypes[$errorStringFunction]);
659: unset($nativeExpressionTypes[$errorStringFunction]);
660: $changed = true;
661: }
662:
663: if (!$changed) {
664: return $this;
665: }
666:
667: return $this->scopeFactory->create(
668: $this->context,
669: $this->isDeclareStrictTypes(),
670: $this->getFunction(),
671: $this->getNamespace(),
672: $expressionTypes,
673: $nativeExpressionTypes,
674: $this->conditionalExpressions,
675: $this->inClosureBindScopeClasses,
676: $this->anonymousFunctionReflection,
677: $this->isInFirstLevelStatement(),
678: $this->currentlyAssignedExpressions,
679: $this->currentlyAllowedUndefinedExpressions,
680: $this->inFunctionCallsStack,
681: $this->afterExtractCall,
682: $this->parentScope,
683: $this->nativeTypesPromoted,
684: $this->templateArgumentFrame,
685: $this->templateArgumentConstraints,
686: );
687: }
688:
689: /**
690: * Forgets every tracked volatile global-state expression: argument-less
691: * function-call expressions whose value reflects mutable global/output-buffer
692: * state rather than just their arguments, superglobal variables and their offsets
693: * and negative results of existence checks (function_exists(), class_exists(), ...)
694: * because the invalidating code may define the missing function/class/etc.
695: */
696: public function invalidateVolatileExpressions(): self
697: {
698: $expressionTypes = $this->expressionTypes;
699: $nativeExpressionTypes = $this->nativeExpressionTypes;
700:
701: $changed = VolatileExpressionHelper::invalidateVolatileFunctionCalls($expressionTypes, $nativeExpressionTypes);
702: $changed = VolatileExpressionHelper::invalidateSuperglobals($expressionTypes, $nativeExpressionTypes) || $changed;
703: $changed = VolatileExpressionHelper::invalidateNegativeExistenceChecks($this, $expressionTypes, $nativeExpressionTypes) || $changed;
704:
705: if (!$changed) {
706: return $this;
707: }
708:
709: return $this->scopeFactory->create(
710: $this->context,
711: $this->isDeclareStrictTypes(),
712: $this->getFunction(),
713: $this->getNamespace(),
714: $expressionTypes,
715: $nativeExpressionTypes,
716: $this->conditionalExpressions,
717: $this->inClosureBindScopeClasses,
718: $this->anonymousFunctionReflection,
719: $this->isInFirstLevelStatement(),
720: $this->currentlyAssignedExpressions,
721: $this->currentlyAllowedUndefinedExpressions,
722: $this->inFunctionCallsStack,
723: $this->afterExtractCall,
724: $this->parentScope,
725: $this->nativeTypesPromoted,
726: $this->templateArgumentFrame,
727: $this->templateArgumentConstraints,
728: );
729: }
730:
731: /**
732: * Forgets negative results of the given existence checks (function_exists(),
733: * class_exists(), ...) because declaring a symbol may define the previously-missing one.
734: *
735: * @param list<'class_exists'|'interface_exists'|'trait_exists'|'enum_exists'|'function_exists'> $functionNames existence-check function names to forget
736: */
737: public function invalidateExistenceCheckExpressions(array $functionNames, ?string $declaredSymbolName): self
738: {
739: $expressionTypes = $this->expressionTypes;
740: $nativeExpressionTypes = $this->nativeExpressionTypes;
741:
742: if (!VolatileExpressionHelper::invalidateNegativeExistenceChecks($this, $expressionTypes, $nativeExpressionTypes, $functionNames, $declaredSymbolName)) {
743: return $this;
744: }
745:
746: return $this->scopeFactory->create(
747: $this->context,
748: $this->isDeclareStrictTypes(),
749: $this->getFunction(),
750: $this->getNamespace(),
751: $expressionTypes,
752: $nativeExpressionTypes,
753: $this->conditionalExpressions,
754: $this->inClosureBindScopeClasses,
755: $this->anonymousFunctionReflection,
756: $this->isInFirstLevelStatement(),
757: $this->currentlyAssignedExpressions,
758: $this->currentlyAllowedUndefinedExpressions,
759: $this->inFunctionCallsStack,
760: $this->afterExtractCall,
761: $this->parentScope,
762: $this->nativeTypesPromoted,
763: $this->templateArgumentFrame,
764: $this->templateArgumentConstraints,
765: );
766: }
767:
768: /** @api */
769: public function hasVariableType(string $variableName): TrinaryLogic
770: {
771: return ScopeOps::hasVariableType($this, $variableName);
772: }
773:
774: /** @api */
775: public function getVariableType(string $variableName): Type
776: {
777: $hasVariableType = $this->hasVariableType($variableName);
778:
779: if ($hasVariableType->maybe()) {
780: if ($variableName === 'argc') {
781: return StaticTypeFactory::argc();
782: }
783: if ($variableName === 'argv') {
784: return StaticTypeFactory::argv();
785: }
786: if ($this->canAnyVariableExist()) {
787: return new MixedType();
788: }
789: }
790:
791: if ($hasVariableType->no()) {
792: throw new UndefinedVariableException($this, $variableName);
793: }
794:
795: $varExprString = '$' . $variableName;
796: if (!array_key_exists($varExprString, $this->expressionTypes)) {
797: if ($this->isGlobalVariable($variableName)) {
798: return new ArrayType(new BenevolentUnionType([new IntegerType(), new StringType()]), new MixedType(true));
799: }
800: return new MixedType();
801: }
802:
803: return $this->expressionTypes[$varExprString]->getType();
804: }
805:
806: /**
807: * @api
808: * @return list<string>
809: */
810: public function getDefinedVariables(): array
811: {
812: $variables = [];
813: foreach ($this->expressionTypes as $exprString => $holder) {
814: if (!$holder->getExpr() instanceof Variable) {
815: continue;
816: }
817: if (!$holder->getCertainty()->yes()) {
818: continue;
819: }
820:
821: $variables[] = substr($exprString, 1);
822: }
823:
824: return $variables;
825: }
826:
827: /**
828: * @api
829: * @return list<string>
830: */
831: public function getMaybeDefinedVariables(): array
832: {
833: $variables = [];
834: foreach ($this->expressionTypes as $exprString => $holder) {
835: if (!$holder->getExpr() instanceof Variable) {
836: continue;
837: }
838: if (!$holder->getCertainty()->maybe()) {
839: continue;
840: }
841:
842: $variables[] = substr($exprString, 1);
843: }
844:
845: return $variables;
846: }
847:
848: private function isGlobalVariable(string $variableName): bool
849: {
850: return in_array($variableName, self::SUPERGLOBAL_VARIABLES, true);
851: }
852:
853: /** @api */
854: public function hasConstant(Name $name): bool
855: {
856: $isCompilerHaltOffset = $name->toString() === '__COMPILER_HALT_OFFSET__';
857: if ($isCompilerHaltOffset) {
858: return $this->fileHasCompilerHaltStatementCalls();
859: }
860:
861: if ($this->getGlobalConstantType($name) !== null) {
862: return true;
863: }
864:
865: return $this->services->reflectionProvider->hasConstant($name, $this);
866: }
867:
868: private function fileHasCompilerHaltStatementCalls(): bool
869: {
870: $nodes = $this->services->parser->parseFile($this->getFile());
871: foreach ($nodes as $node) {
872: if ($node instanceof Node\Stmt\HaltCompiler) {
873: return true;
874: }
875: }
876:
877: return false;
878: }
879:
880: /** @api */
881: public function isInAnonymousFunction(): bool
882: {
883: return $this->anonymousFunctionReflection !== null;
884: }
885:
886: /** @api */
887: public function getAnonymousFunctionReflection(): ?ClosureType
888: {
889: return $this->anonymousFunctionReflection;
890: }
891:
892: /** @api */
893: public function getAnonymousFunctionReturnType(): ?Type
894: {
895: if ($this->anonymousFunctionReflection === null) {
896: return null;
897: }
898:
899: return $this->anonymousFunctionReflection->getReturnType();
900: }
901:
902: /**
903: * Returns a scope identical to this one but with the anonymous function
904: * reflection replaced. The scope entered at a closure/arrow carries only a
905: * shallow reflection (parameters + declared return); once the single body
906: * walk has gathered the returns, the engine builds the refined ClosureType and
907: * swaps it in here so the closure/arrow return-type node and its rules see the
908: * refined expected return.
909: */
910: public function withAnonymousFunctionReflection(ClosureType $anonymousFunctionReflection): self
911: {
912: return $this->scopeFactory->create(
913: $this->context,
914: $this->isDeclareStrictTypes(),
915: $this->getFunction(),
916: $this->getNamespace(),
917: $this->expressionTypes,
918: $this->nativeExpressionTypes,
919: $this->conditionalExpressions,
920: $this->inClosureBindScopeClasses,
921: $anonymousFunctionReflection,
922: $this->isInFirstLevelStatement(),
923: $this->currentlyAssignedExpressions,
924: $this->currentlyAllowedUndefinedExpressions,
925: $this->inFunctionCallsStack,
926: $this->afterExtractCall,
927: $this->parentScope,
928: $this->nativeTypesPromoted,
929: $this->templateArgumentFrame,
930: $this->templateArgumentConstraints,
931: );
932: }
933:
934: /** @api */
935: public function getType(Expr $node): Type
936: {
937: // a variable read is scope state and a literal is a constant - neither
938: // needs the node walked, so asking about them ahead of the walk is not
939: // the on-demand pricing the guard exists to catch
940: if (
941: NodeScopeResolver::$guardNewWorld
942: && isset(NodeScopeResolver::$guardRealExprIds[spl_object_id($node)])
943: && !isset(NodeScopeResolver::$guardProcessedExprIds[spl_object_id($node)])
944: && !($node instanceof Variable && is_string($node->name))
945: && !$node instanceof Node\Scalar\String_
946: && !$node instanceof Node\Scalar\Int_
947: && !$node instanceof Node\Scalar\Float_
948: ) {
949: throw new ShouldNotHappenException(sprintf(
950: 'getType() asked about non-synthetic %s on line %d before it was processed by processExprNode() - it should consume the node\'s ExpressionResult instead.',
951: get_class($node),
952: $node->getStartLine(),
953: ));
954: }
955:
956: $type = ScopeOps::getTypeFromCache($this, $node, $key);
957: if ($type !== null) {
958: return $type;
959: }
960:
961: return $this->resolvedTypes[$key] = TypeUtils::resolveLateResolvableTypes($this->resolveType($key, $node));
962: }
963:
964: public function getScopeType(Expr $expr): Type
965: {
966: return $this->getType($expr);
967: }
968:
969: public function getScopeNativeType(Expr $expr): Type
970: {
971: return $this->getNativeType($expr);
972: }
973:
974: public function getNodeKey(Expr $node): string
975: {
976: return ScopeOps::nodeKey($node, $this->services->exprPrinter);
977: }
978:
979: /** @internal */
980: public function getExprPrinter(): ExprPrinter
981: {
982: return $this->services->exprPrinter;
983: }
984:
985: /**
986: * Creates a copy of this scope with the given expression tables and flags
987: * replaced, keeping context, function, namespace and everything else.
988: *
989: * @internal called by ScopeOps
990: * @param array<string, ExpressionTypeHolder> $expressionTypes
991: * @param array<string, ExpressionTypeHolder> $nativeExpressionTypes
992: * @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
993: * @param array<string, bool> $currentlyAssignedExpressions
994: * @param array<string, true> $currentlyAllowedUndefinedExpressions
995: * @param list<array{FunctionReflection|MethodReflection|null, ParameterReflection|null}> $inFunctionCallsStack
996: */
997: public function duplicateWith(
998: array $expressionTypes,
999: array $nativeExpressionTypes,
1000: array $conditionalExpressions,
1001: array $currentlyAssignedExpressions,
1002: array $currentlyAllowedUndefinedExpressions,
1003: array $inFunctionCallsStack,
1004: bool $inFirstLevelStatement,
1005: bool $afterExtractCall,
1006: ): self
1007: {
1008: return $this->scopeFactory->create(
1009: $this->context,
1010: $this->isDeclareStrictTypes(),
1011: $this->getFunction(),
1012: $this->getNamespace(),
1013: $expressionTypes,
1014: $nativeExpressionTypes,
1015: $conditionalExpressions,
1016: $this->inClosureBindScopeClasses,
1017: $this->anonymousFunctionReflection,
1018: $inFirstLevelStatement,
1019: $currentlyAssignedExpressions,
1020: $currentlyAllowedUndefinedExpressions,
1021: $inFunctionCallsStack,
1022: $afterExtractCall,
1023: $this->parentScope,
1024: $this->nativeTypesPromoted,
1025: $this->templateArgumentFrame,
1026: $this->templateArgumentConstraints,
1027: );
1028: }
1029:
1030: /**
1031: * A cache key of the scope state a closure's type can depend on. With
1032: * $relevantRoots (the closure's free variables, '$this' included) only
1033: * the expression types rooted in them contribute - narrow enough that
1034: * loop-local churn does not invalidate the closure's cached type. Null
1035: * means everything contributes (dynamic variable access in the body).
1036: *
1037: * @param list<string>|null $relevantRoots
1038: */
1039: public function getClosureScopeCacheKey(?array $relevantRoots = null): string
1040: {
1041: $parts = [];
1042: foreach ($this->expressionTypes as $exprString => $expressionTypeHolder) {
1043: if ($expressionTypeHolder->getExpr() instanceof VirtualNode) {
1044: continue;
1045: }
1046: if ($relevantRoots !== null && !self::exprStringIsRootedIn($exprString, $relevantRoots)) {
1047: continue;
1048: }
1049: $parts[] = sprintf('%s::%s', $exprString, $expressionTypeHolder->getType()->describe(VerbosityLevel::cache()));
1050: }
1051: $parts[] = '---';
1052:
1053: $parts[] = sprintf(':%d', count($this->inFunctionCallsStack));
1054: foreach ($this->inFunctionCallsStack as [, $parameter]) {
1055: if ($parameter === null) {
1056: $parts[] = ',null';
1057: continue;
1058: }
1059:
1060: $parts[] = sprintf(',%s', $parameter->getType()->describe(VerbosityLevel::cache()));
1061: }
1062:
1063: return md5(implode("\n", $parts));
1064: }
1065:
1066: /** @param list<string> $roots */
1067: private static function exprStringIsRootedIn(string $exprString, array $roots): bool
1068: {
1069: foreach ($roots as $root) {
1070: if ($exprString === $root) {
1071: return true;
1072: }
1073: if (!str_starts_with($exprString, $root)) {
1074: continue;
1075: }
1076:
1077: $next = $exprString[strlen($root)];
1078: if ($next !== '_' && !ctype_alnum($next)) {
1079: return true;
1080: }
1081: }
1082:
1083: return false;
1084: }
1085:
1086: private function resolveType(string $exprString, Expr $node): Type
1087: {
1088: foreach ($this->services->expressionTypeResolverExtensions->getAll() as $extension) {
1089: $type = $extension->getType($node, $this);
1090: if ($type !== null) {
1091: return $type;
1092: }
1093: }
1094:
1095: $expressionType = ScopeOps::expressionTypeByKey($this, $node, $exprString);
1096: if ($expressionType !== null) {
1097: return $expressionType;
1098: }
1099:
1100: // NodeScopeResolver intercepts a first-class callable CallLike before the
1101: // ExprHandler dispatch - no handler supports the original node, its closure
1102: // type lives on the stored result's typeCallback (see the *CallableNode
1103: // handlers), mirroring TypeSpecifier::specifyTypesInCondition().
1104: if ($node instanceof Expr\CallLike && $node->isFirstClassCallable()) {
1105: return $this->resolveTypeOfNewWorldHandlerNode($node);
1106: }
1107:
1108: $exprHandler = ExprHandlerRegistry::resolve($node, $this->services->container);
1109: if ($exprHandler !== null) {
1110: return $this->resolveTypeOfNewWorldHandlerNode($node);
1111: }
1112:
1113: return new MixedType();
1114: }
1115:
1116: /**
1117: * Resolves the type of a node whose ExprHandler produced an ExpressionResult.
1118: * The answer comes from the ExpressionResult stored during the analysis
1119: * currently in progress (its eager type or typeCallback), or from processing
1120: * the node on demand (synthetic nodes, or no analysis in progress at all).
1121: *
1122: * The scope deliberately does not reference the storage - that would create
1123: * a reference cycle that never gets collected (see ExpressionResultStorageStack).
1124: */
1125: private function resolveTypeOfNewWorldHandlerNode(Expr $node): Type
1126: {
1127: // the hooks are the boundary between the rule-facing world and the
1128: // engine - a rule's NodeCallbackScope must not flow into result
1129: // callbacks or on-demand processing
1130: $scope = $this->toWalkScope();
1131: $storage = $this->services->expressionResultStorageStack->getCurrent();
1132: $counterfactualAsk = false;
1133: if ($storage !== null) {
1134: $result = $storage->findExpressionResult($node);
1135: if ($result !== null && $result->canResolveOwnType()) {
1136: // a counterfactual ask (the asking scope re-binds a variable the
1137: // expression reads, e.g. array_filter pricing its callback body
1138: // per constant element) must re-price the node on that scope -
1139: // the memoized walk-position type answers a different question
1140: $counterfactualAsk = !$result->askScopeVariableStateMatches($scope, $scope->nativeTypesPromoted);
1141: if (!$counterfactualAsk) {
1142: return $result->getTypeOnScope($scope, $scope->nativeTypesPromoted);
1143: }
1144: }
1145: }
1146:
1147: // A closure/arrow function type is computed directly (as
1148: // resolveCallableTypeForScope() also does) - never by processing it on
1149: // demand, which would re-enter ClosureHandler::processExpr() endlessly.
1150: // This answers both a closure whose result is not stored yet (its own
1151: // body walk asks for its type, and a callable parameter is derived from
1152: // it while it is being processed) and a closure passed as a call argument,
1153: // whose result NodeScopeResolver stores without an eager type.
1154: // getClosureType()'s own depth guard answers the self-by-ref ask.
1155: if ($node instanceof Expr\Closure || $node instanceof Expr\ArrowFunction) {
1156: return $this->services->container->getByType(ClosureTypeResolver::class)->getClosureType($scope, $node, false, $storage);
1157: }
1158:
1159: if (!$counterfactualAsk && $storage !== null && $storage->findExpressionResult($node) !== null) {
1160: throw new ShouldNotHappenException(sprintf(
1161: 'ExpressionResult of %s cannot resolve its own type (no eager type, no typeCallback).',
1162: get_class($node),
1163: ));
1164: }
1165:
1166: // a synthetic node, or no analysis in progress
1167: $onDemandResult = $this->services->container->getByType(NodeScopeResolver::class)->processExprOnDemand(
1168: $node,
1169: $scope,
1170: $storage !== null ? $storage->duplicate() : new ExpressionResultStorage(),
1171: );
1172:
1173: return $onDemandResult->getTypeOnScope($scope, $scope->nativeTypesPromoted);
1174: }
1175:
1176: /**
1177: * Prices the current (phpdoc, native) type pair of an expression that
1178: * applySpecifiedTypes() needs to intersect with or subtract from but that
1179: * is not tracked in the scope. Old-world filterBySpecifiedTypes() asked
1180: * Scope::getType() here; pricing from the stored ExpressionResult answers
1181: * through the typeCallback for converted handlers. A synthetic node the
1182: * analysis never processed - e.g. the plain-chain variant a nullsafe
1183: * narrowing emits ($a->b() alongside $a?->b()) - is priced on demand,
1184: * mirroring resolveTypeOfNewWorldHandlerNode(); its real subnodes answer
1185: * from stored results so the on-demand walk terminates. Returns null only
1186: * when there is no analysis in progress to price against.
1187: *
1188: * @return array{Type, Type}|null
1189: */
1190: private function getCurrentTypesOfSpecifiedExpr(Expr $expr): ?array
1191: {
1192: $storage = $this->services->expressionResultStorageStack->getCurrent();
1193: if ($storage === null) {
1194: return null;
1195: }
1196:
1197: // a narrowable expression's scope-view type is derived from tracked
1198: // state - the application-point semantics this method exists for. The
1199: // stored result must NOT win here: a narrowing entry's node sits inside
1200: // the condition (the \$a of `'' !== \$a`, walked on a truthy branch), so
1201: // its walk-position type carries branch narrowing that would poison the
1202: // base the narrowing is applied to. A nullsafe-containing chain is the
1203: // exception: deriving it from tracked state re-prices its links on
1204: // receiver state an active isset/?? ensure devices non-null, losing the
1205: // short-circuit's null - its stored result keeps it.
1206: $exprResult = $storage->findExpressionResult($expr);
1207: if (
1208: (
1209: ($expr instanceof Expr\Variable && is_string($expr->name))
1210: || $expr instanceof PropertyFetch
1211: || $expr instanceof Expr\ArrayDimFetch
1212: || $expr instanceof Expr\StaticPropertyFetch
1213: // argument-less instance calls: the shape @phpstan-assert subjects
1214: // take (synthetic per-build nodes, never stored - a walk per
1215: // application otherwise)
1216: || ($expr instanceof Expr\MethodCall && $expr->name instanceof Identifier && !$expr->isFirstClassCallable() && $expr->getArgs() === [])
1217: )
1218: && ($exprResult === null || !$exprResult->containsNullsafe())
1219: ) {
1220: return [
1221: $this->resolveScopeStateType($expr, $this->nativeTypesPromoted),
1222: $this->resolveScopeStateType($expr, true),
1223: ];
1224: }
1225:
1226: $result = $exprResult;
1227: if ($result === null) {
1228: // a call subject (or a synthetic plain-chain variant) is priced on
1229: // demand: one walk answers both flavours. Not memoized - a census
1230: // showed repeat asks for the same unstored subject on one scope
1231: // never happen (0 hits across corpora)
1232: $scope = $this->toWalkScope();
1233: $result = $this->services->container->getByType(NodeScopeResolver::class)->processExprOnDemand(
1234: $expr,
1235: $scope,
1236: $storage->duplicate(),
1237: );
1238:
1239: return [
1240: $result->getTypeOnScope($scope, $scope->nativeTypesPromoted),
1241: $result->getTypeOnScope($scope, true),
1242: ];
1243: }
1244:
1245: // a type tracked for the whole expression on the asking scope wins over
1246: // the stored result's own type: a handler (e.g. isset/empty via
1247: // NonNullabilityHelper) may have processed the inner expression on a
1248: // scope that strips null, so the result's type would be stale for the
1249: // narrowing the caller is applying
1250: return [
1251: $result->getTypeOnScope($this, $this->nativeTypesPromoted),
1252: $result->getTypeOnScope($this, true),
1253: ];
1254: }
1255:
1256: /**
1257: * Narrowing counterpart of resolveTypeOfNewWorldHandlerNode() - the old-world
1258: * TypeSpecifier dispatcher asks here for a node's narrowing. Returns null when
1259: * the ExpressionResult carries no specifyTypesCallback - the dispatcher falls
1260: * back to default truthy/falsey narrowing.
1261: *
1262: * @internal
1263: */
1264: public function specifyTypesOfNewWorldHandlerNode(Expr $node, TypeSpecifierContext $context): SpecifiedTypes
1265: {
1266: return $this->obtainResultForNode($node)->getSpecifiedTypesForScope($this->toWalkScope(), $context);
1267: }
1268:
1269: /**
1270: * Obtains the ExpressionResult of a node so its narrowing/type can be asked
1271: * (getSpecifiedTypesForScope()/getTypeOnScope()): the stored result of an
1272: * already-processed node, or - for a synthetic node (or with no analysis in
1273: * progress) - the result of processing it on demand against a duplicate of
1274: * the current storage, so the throwaway walk never pollutes the live one.
1275: */
1276: public function obtainResultForNode(Expr $node): ExpressionResult
1277: {
1278: $storage = $this->services->expressionResultStorageStack->getCurrent();
1279: if ($storage !== null) {
1280: $result = $storage->findExpressionResult($node);
1281: if ($result !== null) {
1282: return $result;
1283: }
1284: }
1285:
1286: if (
1287: NodeScopeResolver::$guardNewWorld
1288: && isset(NodeScopeResolver::$guardRealExprIds[spl_object_id($node)])
1289: && !isset(NodeScopeResolver::$guardProcessedExprIds[spl_object_id($node)])
1290: ) {
1291: throw new ShouldNotHappenException(sprintf(
1292: 'obtainResultForNode() asked about non-synthetic %s on line %d before it was processed by processExprNode() - it should consume the node\'s ExpressionResult instead.',
1293: get_class($node),
1294: $node->getStartLine(),
1295: ));
1296: }
1297:
1298: // see resolveTypeOfNewWorldHandlerNode() - rules ask the dispatcher
1299: // with their NodeCallbackScope (e.g. ImpossibleCheckTypeHelper), the engine
1300: // side of the boundary works with the mutating flavor
1301: $scope = $this->toWalkScope();
1302:
1303: // a synthetic node, or no analysis in progress
1304: return $this->services->container->getByType(NodeScopeResolver::class)->processExprOnDemand(
1305: $node,
1306: $scope,
1307: $storage !== null ? $storage->duplicate() : new ExpressionResultStorage(),
1308: );
1309: }
1310:
1311: /**
1312: * Makes the storage answer type questions asked on this scope (and every
1313: * scope sharing its ExpressionResultStorageStack) for the duration of an
1314: * analysis. The caller must pop in a finally block.
1315: */
1316: public function pushExpressionResultStorage(ExpressionResultStorage $storage): void
1317: {
1318: $this->services->expressionResultStorageStack->push($storage);
1319: }
1320:
1321: public function popExpressionResultStorage(): void
1322: {
1323: $this->services->expressionResultStorageStack->pop();
1324: }
1325:
1326: /**
1327: * The ExpressionResultStorage of the analysis currently in progress, the one
1328: * resolveTypeOfNewWorldHandlerNode() prices synthetic nodes against. A handler
1329: * pricing a synthetic node from a lazily-invoked typeCallback must use this
1330: * (not a storage captured at processExpr() time): a later re-evaluation
1331: * (e.g. findEarlyTerminatingExpr()) runs under a different current storage,
1332: * and the captured one would resolve the synthetic node's real subnodes from
1333: * stale stored results.
1334: *
1335: * @internal
1336: */
1337: /** The settled stored result of the current storage - NodeCallbackScope's no-switch fast path. */
1338: protected function findSettledStoredResult(Expr $node): ?ExpressionResult
1339: {
1340: $storage = $this->services->expressionResultStorageStack->getCurrent();
1341: if ($storage === null) {
1342: return null;
1343: }
1344:
1345: return $storage->findExpressionResult($node);
1346: }
1347:
1348: public function getCurrentExpressionResultStorage(): ?ExpressionResultStorage
1349: {
1350: return $this->services->expressionResultStorageStack->getCurrent();
1351: }
1352:
1353: public function withTemplateArgumentFrame(?TemplateArgumentFrame $frame): self
1354: {
1355: $scope = $this->withoutMemoizedTypes();
1356: $scope->templateArgumentFrame = $frame;
1357: return $scope;
1358: }
1359:
1360: public function getCurrentTemplateArgumentFrame(): ?TemplateArgumentFrame
1361: {
1362: return $this->templateArgumentFrame;
1363: }
1364:
1365: public function getTemplateArgumentConstraints(): ?TemplateArgumentConstraints
1366: {
1367: return $this->templateArgumentConstraints;
1368: }
1369:
1370: public function withTemplateArgumentConstraints(?TemplateArgumentConstraints $constraints): self
1371: {
1372: if ($constraints === $this->templateArgumentConstraints) {
1373: return $this;
1374: }
1375: $scope = clone $this;
1376: $scope->nodeCallbackScope = null;
1377: $scope->scopeOutOfFirstLevelStatement = null;
1378: $scope->scopeWithPromotedNativeTypes = null;
1379: $scope->templateArgumentConstraints = $constraints;
1380: return $scope;
1381: }
1382:
1383: /** Inference facts join independently of variable-state convergence and branch termination. */
1384: public function addTemplateArgumentConstraints(?TemplateArgumentConstraints $constraints): self
1385: {
1386: if ($constraints === null || $constraints->isEmpty()) {
1387: return $this;
1388: }
1389:
1390: return $this->withTemplateArgumentConstraints($this->templateArgumentConstraints === null ? $constraints : $this->templateArgumentConstraints->merge($constraints));
1391: }
1392:
1393: /**
1394: * A copy of this scope without its memoized type answers: the recorded
1395: * entry scope of a statement the second pass re-walks answered questions
1396: * during the observation pass with unresolved template arguments in them.
1397: */
1398: public function withoutMemoizedTypes(): self
1399: {
1400: return $this->duplicateWith(
1401: $this->expressionTypes,
1402: $this->nativeExpressionTypes,
1403: $this->conditionalExpressions,
1404: $this->currentlyAssignedExpressions,
1405: $this->currentlyAllowedUndefinedExpressions,
1406: $this->inFunctionCallsStack,
1407: $this->inFirstLevelStatement,
1408: $this->afterExtractCall,
1409: );
1410: }
1411:
1412: /**
1413: * The variables spelled by the tracked expressions whose state differs
1414: * between this scope and $other - a statement mentioning none of them walks
1415: * the same on both - or null when a differing entry has no variable root (a
1416: * static property, a class constant fetch). Every variable of an entry counts,
1417: * not just its root: assigning $b invalidates $a->get($b).
1418: *
1419: * @return list<string>|null
1420: */
1421: public function getDifferingVariableRoots(self $other): ?array
1422: {
1423: $roots = [];
1424: $tables = [
1425: [$this->expressionTypes, $other->expressionTypes],
1426: [$this->nativeExpressionTypes, $other->nativeExpressionTypes],
1427: ];
1428: foreach ($tables as [$ours, $theirs]) {
1429: foreach ($ours as $key => $holder) {
1430: $theirHolder = $theirs[$key] ?? null;
1431: if ($theirHolder !== null && ($theirHolder === $holder || $holder->equals($theirHolder))) {
1432: continue;
1433: }
1434: $variables = self::getVariablesOfExpressionKey($key);
1435: if ($variables === null) {
1436: return null;
1437: }
1438: foreach ($variables as $variable) {
1439: $roots[$variable] = true;
1440: }
1441: }
1442: foreach ($theirs as $key => $holder) {
1443: if (isset($ours[$key])) {
1444: continue;
1445: }
1446: $variables = self::getVariablesOfExpressionKey($key);
1447: if ($variables === null) {
1448: return null;
1449: }
1450: foreach ($variables as $variable) {
1451: $roots[$variable] = true;
1452: }
1453: }
1454: }
1455: $conditionalTables = [
1456: [$this->conditionalExpressions, $other->conditionalExpressions],
1457: [$other->conditionalExpressions, $this->conditionalExpressions],
1458: ];
1459: foreach ($conditionalTables as [$ours, $theirs]) {
1460: foreach ($ours as $key => $holders) {
1461: if (isset($theirs[$key]) && $theirs[$key] === $holders) {
1462: continue;
1463: }
1464: $variables = self::getVariablesOfExpressionKey($key);
1465: if ($variables === null) {
1466: return null;
1467: }
1468: foreach ($variables as $variable) {
1469: $roots[$variable] = true;
1470: }
1471: }
1472: }
1473:
1474: return array_keys($roots);
1475: }
1476:
1477: /**
1478: * The variables an expression key spells, or null when the key is not
1479: * rooted in a variable.
1480: *
1481: * @return list<string>|null
1482: */
1483: private static function getVariablesOfExpressionKey(string $key): ?array
1484: {
1485: if (preg_match('/^\$[a-zA-Z_\x80-\xff]/', $key) !== 1) {
1486: return null;
1487: }
1488:
1489: preg_match_all('/\$([a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)/', $key, $matches);
1490:
1491: return $matches[1];
1492: }
1493:
1494: /**
1495: * This scope after a statement whose recorded walk stands: the entries the
1496: * statement changed or removed between its recorded entry and exit scopes
1497: * are carried over, everything else keeps this scope's state.
1498: */
1499: public function withRecordedStatementDelta(self $recordedEntry, self $recordedExit): self
1500: {
1501: $conditionalExpressions = $this->conditionalExpressions;
1502: foreach ($recordedExit->conditionalExpressions as $key => $holders) {
1503: if (isset($recordedEntry->conditionalExpressions[$key]) && $recordedEntry->conditionalExpressions[$key] === $holders) {
1504: continue;
1505: }
1506: $conditionalExpressions[$key] = $holders;
1507: }
1508: foreach (array_keys($recordedEntry->conditionalExpressions) as $key) {
1509: if (isset($recordedExit->conditionalExpressions[$key])) {
1510: continue;
1511: }
1512: unset($conditionalExpressions[$key]);
1513: }
1514:
1515: return $this->duplicateWith(
1516: self::applyRecordedHolderDelta($this->expressionTypes, $recordedEntry->expressionTypes, $recordedExit->expressionTypes),
1517: self::applyRecordedHolderDelta($this->nativeExpressionTypes, $recordedEntry->nativeExpressionTypes, $recordedExit->nativeExpressionTypes),
1518: $conditionalExpressions,
1519: [],
1520: [],
1521: [],
1522: $this->inFirstLevelStatement,
1523: $recordedExit->afterExtractCall,
1524: );
1525: }
1526:
1527: /**
1528: * @param array<string, ExpressionTypeHolder> $current
1529: * @param array<string, ExpressionTypeHolder> $recordedEntry
1530: * @param array<string, ExpressionTypeHolder> $recordedExit
1531: * @return array<string, ExpressionTypeHolder>
1532: */
1533: private static function applyRecordedHolderDelta(array $current, array $recordedEntry, array $recordedExit): array
1534: {
1535: foreach ($recordedExit as $key => $holder) {
1536: $entryHolder = $recordedEntry[$key] ?? null;
1537: if ($entryHolder !== null && ($entryHolder === $holder || $entryHolder->equals($holder))) {
1538: continue;
1539: }
1540: $current[$key] = $holder;
1541: }
1542: foreach (array_keys($recordedEntry) as $key) {
1543: if (isset($recordedExit[$key])) {
1544: continue;
1545: }
1546: unset($current[$key]);
1547: }
1548:
1549: return $current;
1550: }
1551:
1552: /** @api */
1553: public function getNativeType(Expr $expr): Type
1554: {
1555: return $this->promoteNativeTypes()->getType($expr);
1556: }
1557:
1558: public function getKeepVoidType(Expr $node): Type
1559: {
1560: if (
1561: !$node instanceof Match_
1562: && !$node instanceof Expr\Yield_
1563: && !$node instanceof Expr\YieldFrom
1564: && (
1565: (
1566: !$node instanceof FuncCall
1567: && !$node instanceof MethodCall
1568: && !$node instanceof Expr\NullsafeMethodCall
1569: && !$node instanceof Expr\StaticCall
1570: ) || $node->isFirstClassCallable()
1571: )
1572: ) {
1573: return $this->getScopeStateType($node);
1574: }
1575:
1576: $originalType = $this->getScopeStateType($node);
1577: if (!TypeCombinator::containsNull($originalType)) {
1578: return $originalType;
1579: }
1580:
1581: // the null may be a projected void: read the call's/match's raw
1582: // (void-kept) own type. A result already stored in the current frame is
1583: // read directly; a node evaluated on a different scope - e.g. an arrow
1584: // body typed on the closure scope - is processed on demand there, its
1585: // raw own type keeping void without any keep-void marker on the node.
1586: $storage = $this->services->expressionResultStorageStack->getCurrent();
1587: $result = $storage !== null ? $storage->findExpressionResult($node) : null;
1588: if ($result === null) {
1589: $result = $this->services->container->getByType(NodeScopeResolver::class)->processExprOnDemand(
1590: $node,
1591: $this->toWalkScope(),
1592: $storage !== null ? $storage->duplicate() : new ExpressionResultStorage(),
1593: );
1594: }
1595:
1596: return $result->getKeepVoidType($this->nativeTypesPromoted);
1597: }
1598:
1599: public function doNotTreatPhpDocTypesAsCertain(): self
1600: {
1601: return $this->promoteNativeTypes();
1602: }
1603:
1604: private function promoteNativeTypes(): self
1605: {
1606: if ($this->nativeTypesPromoted) {
1607: return $this;
1608: }
1609:
1610: if ($this->scopeWithPromotedNativeTypes !== null) {
1611: return $this->scopeWithPromotedNativeTypes;
1612: }
1613:
1614: return $this->scopeWithPromotedNativeTypes = $this->scopeFactory->create(
1615: $this->context,
1616: $this->declareStrictTypes,
1617: $this->function,
1618: $this->namespace,
1619: $this->nativeExpressionTypes,
1620: [],
1621: [],
1622: $this->inClosureBindScopeClasses,
1623: $this->anonymousFunctionReflection,
1624: $this->inFirstLevelStatement,
1625: $this->currentlyAssignedExpressions,
1626: $this->currentlyAllowedUndefinedExpressions,
1627: $this->inFunctionCallsStack,
1628: $this->afterExtractCall,
1629: $this->parentScope,
1630: true,
1631: templateArgumentFrame: $this->templateArgumentFrame,
1632: templateArgumentConstraints: $this->templateArgumentConstraints,
1633: );
1634: }
1635:
1636: /** @api */
1637: public function resolveName(Name $name): string
1638: {
1639: $originalClass = (string) $name;
1640: if ($this->isInClass()) {
1641: $lowerClass = strtolower($originalClass);
1642: if (in_array($lowerClass, [
1643: 'self',
1644: 'static',
1645: ], true)) {
1646: if ($this->inClosureBindScopeClasses !== [] && $this->inClosureBindScopeClasses !== ['static']) {
1647: return $this->inClosureBindScopeClasses[0];
1648: }
1649: return $this->getClassReflection()->getName();
1650: } elseif ($lowerClass === 'parent') {
1651: $currentClassReflection = $this->getClassReflection();
1652: if ($currentClassReflection->getParentClass() !== null) {
1653: return $currentClassReflection->getParentClass()->getName();
1654: }
1655: }
1656: }
1657:
1658: return $originalClass;
1659: }
1660:
1661: /** @api */
1662: public function resolveTypeByName(Name $name): TypeWithClassName
1663: {
1664: if ($name->toLowerString() === 'static' && $this->isInClass()) {
1665: if ($this->inClosureBindScopeClasses !== [] && $this->inClosureBindScopeClasses !== ['static']) {
1666: if ($this->services->reflectionProvider->hasClass($this->inClosureBindScopeClasses[0])) {
1667: return new StaticType($this->services->reflectionProvider->getClass($this->inClosureBindScopeClasses[0]));
1668: }
1669: }
1670:
1671: return new StaticType($this->getClassReflection());
1672: }
1673:
1674: $originalClass = $this->resolveName($name);
1675: if ($this->isInClass()) {
1676: if ($this->inClosureBindScopeClasses === [$originalClass]) {
1677: if ($this->services->reflectionProvider->hasClass($originalClass)) {
1678: return new ThisType($this->services->reflectionProvider->getClass($originalClass));
1679: }
1680: return new ObjectType($originalClass);
1681: }
1682:
1683: $thisType = new ThisType($this->getClassReflection());
1684: $ancestor = $thisType->getAncestorWithClassName($originalClass);
1685: if ($ancestor !== null) {
1686: return $ancestor;
1687: }
1688: }
1689:
1690: return new ObjectType($originalClass);
1691: }
1692:
1693: /**
1694: * @api
1695: * @param mixed $value
1696: */
1697: public function getTypeFromValue($value): Type
1698: {
1699: return ConstantTypeHelper::getTypeFromValue($value);
1700: }
1701:
1702: /** @api */
1703: public function hasExpressionType(Expr $node): TrinaryLogic
1704: {
1705: return ScopeOps::hasExpressionType($this, $node, $this->services->exprPrinter);
1706: }
1707:
1708: /**
1709: * Reads the type tracked for an expression straight from its holder, skipping
1710: * the extension/dispatch/cache machinery that getType() runs. Only valid when
1711: * hasExpressionType($node) is yes - mirrors resolveType()'s tracked-holder
1712: * early return and is what ExpressionResult uses on its tracked-holder path.
1713: *
1714: * @internal
1715: */
1716: public function getTrackedExpressionType(Expr $node): Type
1717: {
1718: return $this->expressionTypes[$this->getNodeKey($node)]->getType();
1719: }
1720:
1721: /**
1722: * @param MethodReflection|FunctionReflection|null $reflection
1723: */
1724: public function pushInFunctionCall($reflection, ?ParameterReflection $parameter, bool $rememberTypes): self
1725: {
1726: $stack = $this->inFunctionCallsStack;
1727: $stack[] = [$reflection, $parameter];
1728:
1729: $functionScope = $this->scopeFactory->create(
1730: $this->context,
1731: $this->isDeclareStrictTypes(),
1732: $this->getFunction(),
1733: $this->getNamespace(),
1734: $this->expressionTypes,
1735: $this->nativeExpressionTypes,
1736: $this->conditionalExpressions,
1737: $this->inClosureBindScopeClasses,
1738: $this->anonymousFunctionReflection,
1739: $this->isInFirstLevelStatement(),
1740: $this->currentlyAssignedExpressions,
1741: $this->currentlyAllowedUndefinedExpressions,
1742: $stack,
1743: $this->afterExtractCall,
1744: $this->parentScope,
1745: $this->nativeTypesPromoted,
1746: $this->templateArgumentFrame,
1747: $this->templateArgumentConstraints,
1748: );
1749:
1750: if ($rememberTypes) {
1751: $functionScope->resolvedTypes = $this->resolvedTypes;
1752: }
1753:
1754: return $functionScope;
1755: }
1756:
1757: public function popInFunctionCall(): self
1758: {
1759: $stack = $this->inFunctionCallsStack;
1760: array_pop($stack);
1761:
1762: $parentScope = $this->scopeFactory->create(
1763: $this->context,
1764: $this->isDeclareStrictTypes(),
1765: $this->getFunction(),
1766: $this->getNamespace(),
1767: $this->expressionTypes,
1768: $this->nativeExpressionTypes,
1769: $this->conditionalExpressions,
1770: $this->inClosureBindScopeClasses,
1771: $this->anonymousFunctionReflection,
1772: $this->isInFirstLevelStatement(),
1773: $this->currentlyAssignedExpressions,
1774: $this->currentlyAllowedUndefinedExpressions,
1775: $stack,
1776: $this->afterExtractCall,
1777: $this->parentScope,
1778: $this->nativeTypesPromoted,
1779: $this->templateArgumentFrame,
1780: $this->templateArgumentConstraints,
1781: );
1782:
1783: $parentScope->resolvedTypes = $this->resolvedTypes;
1784:
1785: return $parentScope;
1786: }
1787:
1788: /** @api */
1789: public function isInClassExists(string $className): bool
1790: {
1791: foreach ($this->inFunctionCallsStack as [$inFunctionCall]) {
1792: if (!$inFunctionCall instanceof FunctionReflection) {
1793: continue;
1794: }
1795:
1796: if (in_array($inFunctionCall->getName(), [
1797: 'class_exists',
1798: 'interface_exists',
1799: 'trait_exists',
1800: 'enum_exists',
1801: ], true)) {
1802: return true;
1803: }
1804: }
1805:
1806: // interface_exists() etc. imply class_exists() therefore not listed here
1807: $expr = new FuncCall(new FullyQualified('class_exists'), [
1808: new Arg(new String_(ltrim($className, '\\'))),
1809: ]);
1810:
1811: return $this->getType($expr)->isTrue()->yes();
1812: }
1813:
1814: public function getFunctionCallStack(): array
1815: {
1816: return array_values(array_filter(
1817: array_map(static fn ($values) => $values[0], $this->inFunctionCallsStack),
1818: static fn (FunctionReflection|MethodReflection|null $reflection) => $reflection !== null,
1819: ));
1820: }
1821:
1822: public function getFunctionCallStackWithParameters(): array
1823: {
1824: return array_values(array_filter(
1825: $this->inFunctionCallsStack,
1826: static fn ($item) => $item[0] !== null,
1827: ));
1828: }
1829:
1830: /** @api */
1831: public function isInFunctionExists(string $functionName): bool
1832: {
1833: $expr = new FuncCall(new FullyQualified('function_exists'), [
1834: new Arg(new String_(ltrim($functionName, '\\'))),
1835: ]);
1836:
1837: return $this->getType($expr)->isTrue()->yes();
1838: }
1839:
1840: /** @api */
1841: public function enterClass(ClassReflection $classReflection): self
1842: {
1843: $thisHolder = ExpressionTypeHolder::createYes(new Variable('this'), new ThisType($classReflection));
1844: $constantTypes = $this->getConstantTypes();
1845: $constantTypes['$this'] = $thisHolder;
1846: $nativeConstantTypes = $this->getNativeConstantTypes();
1847: $nativeConstantTypes['$this'] = $thisHolder;
1848:
1849: return $this->scopeFactory->create(
1850: $this->context->enterClass($classReflection),
1851: $this->isDeclareStrictTypes(),
1852: null,
1853: $this->getNamespace(),
1854: $constantTypes,
1855: $nativeConstantTypes,
1856: [],
1857: [],
1858: null,
1859: true,
1860: [],
1861: [],
1862: [],
1863: false,
1864: $classReflection->isAnonymous() ? $this : null,
1865: templateArgumentFrame: $this->templateArgumentFrame,
1866: templateArgumentConstraints: $this->templateArgumentConstraints,
1867: );
1868: }
1869:
1870: public function enterTrait(ClassReflection $traitReflection): self
1871: {
1872: $namespace = null;
1873: $traitName = $traitReflection->getName();
1874: $traitNameParts = explode('\\', $traitName);
1875: if (count($traitNameParts) > 1) {
1876: $namespace = implode('\\', array_slice($traitNameParts, 0, -1));
1877: }
1878: return $this->scopeFactory->create(
1879: $this->context->enterTrait($traitReflection),
1880: $this->isDeclareStrictTypes(),
1881: $this->getFunction(),
1882: $namespace,
1883: $this->expressionTypes,
1884: $this->nativeExpressionTypes,
1885: [],
1886: $this->inClosureBindScopeClasses,
1887: $this->anonymousFunctionReflection,
1888: templateArgumentFrame: $this->templateArgumentFrame,
1889: templateArgumentConstraints: $this->templateArgumentConstraints,
1890: );
1891: }
1892:
1893: /**
1894: * @api
1895: * @param Type[] $phpDocParameterTypes
1896: * @param Type[] $parameterOutTypes
1897: * @param array<string, bool> $immediatelyInvokedCallableParameters
1898: * @param array<string, Type> $phpDocClosureThisTypeParameters
1899: * @param array<string, bool> $phpDocPureUnlessCallableIsImpureParameters
1900: * @param array<string, bool> $phpDocPureUnlessParameterPassedParameters
1901: */
1902: public function enterClassMethod(
1903: Node\Stmt\ClassMethod $classMethod,
1904: TemplateTypeMap $templateTypeMap,
1905: array $phpDocParameterTypes,
1906: ?Type $phpDocReturnType,
1907: ?Type $throwType,
1908: ?string $deprecatedDescription,
1909: bool $isDeprecated,
1910: bool $isInternal,
1911: bool $isFinal,
1912: ?bool $isPure = null,
1913: bool $acceptsNamedArguments = true,
1914: ?Assertions $asserts = null,
1915: ?Type $selfOutType = null,
1916: ?string $phpDocComment = null,
1917: array $parameterOutTypes = [],
1918: array $immediatelyInvokedCallableParameters = [],
1919: array $phpDocClosureThisTypeParameters = [],
1920: bool $isConstructor = false,
1921: ?ResolvedPhpDocBlock $resolvedPhpDocBlock = null,
1922: array $phpDocPureUnlessCallableIsImpureParameters = [],
1923: array $phpDocPureUnlessParameterPassedParameters = [],
1924: ): self
1925: {
1926: if (!$this->isInClass()) {
1927: throw new ShouldNotHappenException();
1928: }
1929:
1930: return $this->enterFunctionLike(
1931: new PhpMethodFromParserNodeReflection(
1932: $this->getClassReflection(),
1933: $classMethod,
1934: null,
1935: $this->getFile(),
1936: $templateTypeMap,
1937: $this->getRealParameterTypes($classMethod),
1938: array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocParameterTypes),
1939: $this->getRealParameterDefaultValues($classMethod),
1940: $this->getParameterAttributes($classMethod),
1941: $this->transformStaticType($this->getFunctionType($classMethod->returnType, false, false)),
1942: $phpDocReturnType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocReturnType)) : null,
1943: $throwType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($throwType)) : null,
1944: $deprecatedDescription,
1945: $isDeprecated,
1946: $isInternal,
1947: $isFinal,
1948: $isPure,
1949: $acceptsNamedArguments,
1950: $asserts ?? Assertions::createEmpty(),
1951: $selfOutType,
1952: $phpDocComment,
1953: $resolvedPhpDocBlock,
1954: array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $parameterOutTypes),
1955: $immediatelyInvokedCallableParameters,
1956: array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocClosureThisTypeParameters),
1957: $isConstructor,
1958: $this->services->attributeReflectionFactory->fromAttrGroups($classMethod->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $classMethod)),
1959: $phpDocPureUnlessCallableIsImpureParameters,
1960: $phpDocPureUnlessParameterPassedParameters,
1961: ),
1962: !$classMethod->isStatic(),
1963: );
1964: }
1965:
1966: /**
1967: * @param Type[] $phpDocParameterTypes
1968: */
1969: public function enterPropertyHook(
1970: Node\PropertyHook $hook,
1971: string $propertyName,
1972: Identifier|Name|ComplexType|null $nativePropertyTypeNode,
1973: ?Type $phpDocPropertyType,
1974: array $phpDocParameterTypes,
1975: ?Type $throwType,
1976: ?string $deprecatedDescription,
1977: bool $isDeprecated,
1978: ?bool $isPure,
1979: ?string $phpDocComment,
1980: ?ResolvedPhpDocBlock $resolvedPhpDocBlock = null,
1981: ): self
1982: {
1983: if (!$this->isInClass()) {
1984: throw new ShouldNotHappenException();
1985: }
1986:
1987: $phpDocParameterTypes = array_map(fn (Type $type): Type => $this->transformStaticType(TemplateTypeHelper::toArgument($type)), $phpDocParameterTypes);
1988:
1989: $hookName = $hook->name->toLowerString();
1990: if ($hookName === 'set') {
1991: if ($hook->params === []) {
1992: $hook = clone $hook;
1993: $hook->params = [
1994: new Node\Param(new Variable('value'), type: $nativePropertyTypeNode),
1995: ];
1996: }
1997:
1998: $firstParam = $hook->params[0] ?? null;
1999: if (
2000: $firstParam !== null
2001: && $phpDocPropertyType !== null
2002: && $firstParam->var instanceof Variable
2003: && is_string($firstParam->var->name)
2004: ) {
2005: $valueParamPhpDocType = $phpDocParameterTypes[$firstParam->var->name] ?? null;
2006: if ($valueParamPhpDocType === null) {
2007: $phpDocParameterTypes[$firstParam->var->name] = $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocPropertyType));
2008: }
2009: }
2010:
2011: $realReturnType = new VoidType();
2012: $phpDocReturnType = null;
2013: } elseif ($hookName === 'get') {
2014: $realReturnType = $this->getFunctionType($nativePropertyTypeNode, false, false);
2015: $phpDocReturnType = $phpDocPropertyType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($phpDocPropertyType)) : null;
2016: } else {
2017: throw new ShouldNotHappenException();
2018: }
2019:
2020: $realParameterTypes = $this->getRealParameterTypes($hook);
2021:
2022: return $this->enterFunctionLike(
2023: new PhpMethodFromParserNodeReflection(
2024: $this->getClassReflection(),
2025: $hook,
2026: $propertyName,
2027: $this->getFile(),
2028: TemplateTypeMap::createEmpty(),
2029: $realParameterTypes,
2030: $phpDocParameterTypes,
2031: [],
2032: $this->getParameterAttributes($hook),
2033: $realReturnType,
2034: $phpDocReturnType,
2035: $throwType !== null ? $this->transformStaticType(TemplateTypeHelper::toArgument($throwType)) : null,
2036: $deprecatedDescription,
2037: $isDeprecated,
2038: false,
2039: false,
2040: $isPure,
2041: true,
2042: Assertions::createEmpty(),
2043: null,
2044: $phpDocComment,
2045: $resolvedPhpDocBlock,
2046: [],
2047: [],
2048: [],
2049: false,
2050: $this->services->attributeReflectionFactory->fromAttrGroups($hook->attrGroups, InitializerExprContext::fromStubParameter($this->getClassReflection()->getName(), $this->getFile(), $hook)),
2051: [],
2052: [],
2053: ),
2054: true,
2055: );
2056: }
2057:
2058: private function transformStaticType(Type $type): Type
2059: {
2060: return TypeTraverser::mapMemoized($type, new TransformStaticTypeTraverser($this));
2061: }
2062:
2063: /**
2064: * @return Type[]
2065: */
2066: private function getRealParameterTypes(Node\FunctionLike $functionLike): array
2067: {
2068: $realParameterTypes = [];
2069: foreach ($functionLike->getParams() as $parameter) {
2070: if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
2071: throw new ShouldNotHappenException();
2072: }
2073: $realParameterTypes[$parameter->var->name] = $this->getFunctionType(
2074: $parameter->type,
2075: $this->isParameterValueNullable($parameter) && $parameter->flags === 0,
2076: false,
2077: );
2078: }
2079:
2080: return $realParameterTypes;
2081: }
2082:
2083: /**
2084: * @return Type[]
2085: */
2086: private function getRealParameterDefaultValues(Node\FunctionLike $functionLike): array
2087: {
2088: $realParameterDefaultValues = [];
2089: foreach ($functionLike->getParams() as $parameter) {
2090: if ($parameter->default === null) {
2091: continue;
2092: }
2093: if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
2094: throw new ShouldNotHappenException();
2095: }
2096: $realParameterDefaultValues[$parameter->var->name] = $this->services->initializerExprTypeResolver->getType($parameter->default, InitializerExprContext::fromScope($this));
2097: }
2098:
2099: return $realParameterDefaultValues;
2100: }
2101:
2102: /**
2103: * @return array<string, list<AttributeReflection>>
2104: */
2105: private function getParameterAttributes(ClassMethod|Function_|PropertyHook $functionLike): array
2106: {
2107: $parameterAttributes = [];
2108: $className = null;
2109: if ($this->isInClass()) {
2110: $className = $this->getClassReflection()->getName();
2111: }
2112: foreach ($functionLike->getParams() as $parameter) {
2113: if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
2114: throw new ShouldNotHappenException();
2115: }
2116:
2117: $parameterAttributes[$parameter->var->name] = $this->services->attributeReflectionFactory->fromAttrGroups($parameter->attrGroups, InitializerExprContext::fromStubParameter($className, $this->getFile(), $functionLike));
2118: }
2119:
2120: return $parameterAttributes;
2121: }
2122:
2123: /**
2124: * @api
2125: * @param Type[] $phpDocParameterTypes
2126: * @param Type[] $parameterOutTypes
2127: * @param array<string, bool> $immediatelyInvokedCallableParameters
2128: * @param array<string, Type> $phpDocClosureThisTypeParameters
2129: * @param array<string, bool> $pureUnlessCallableIsImpureParameters
2130: * @param array<string, bool> $pureUnlessParameterPassedParameters
2131: */
2132: public function enterFunction(
2133: Node\Stmt\Function_ $function,
2134: TemplateTypeMap $templateTypeMap,
2135: array $phpDocParameterTypes,
2136: ?Type $phpDocReturnType,
2137: ?Type $throwType,
2138: ?string $deprecatedDescription,
2139: bool $isDeprecated,
2140: bool $isInternal,
2141: ?bool $isPure = null,
2142: bool $acceptsNamedArguments = true,
2143: ?Assertions $asserts = null,
2144: ?string $phpDocComment = null,
2145: array $parameterOutTypes = [],
2146: array $immediatelyInvokedCallableParameters = [],
2147: array $phpDocClosureThisTypeParameters = [],
2148: array $pureUnlessCallableIsImpureParameters = [],
2149: array $pureUnlessParameterPassedParameters = [],
2150: ): self
2151: {
2152: return $this->enterFunctionLike(
2153: new PhpFunctionFromParserNodeReflection(
2154: $function,
2155: $this->getFile(),
2156: $templateTypeMap,
2157: $this->getRealParameterTypes($function),
2158: array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $phpDocParameterTypes),
2159: $this->getRealParameterDefaultValues($function),
2160: $this->getParameterAttributes($function),
2161: $this->getFunctionType($function->returnType, $function->returnType === null, false),
2162: $phpDocReturnType !== null ? TemplateTypeHelper::toArgument($phpDocReturnType) : null,
2163: $throwType,
2164: $deprecatedDescription,
2165: $isDeprecated,
2166: $isInternal,
2167: $isPure,
2168: $acceptsNamedArguments,
2169: $asserts ?? Assertions::createEmpty(),
2170: $phpDocComment,
2171: array_map(static fn (Type $type): Type => TemplateTypeHelper::toArgument($type), $parameterOutTypes),
2172: $immediatelyInvokedCallableParameters,
2173: $phpDocClosureThisTypeParameters,
2174: $this->services->attributeReflectionFactory->fromAttrGroups($function->attrGroups, InitializerExprContext::fromStubParameter(null, $this->getFile(), $function)),
2175: $pureUnlessCallableIsImpureParameters,
2176: $pureUnlessParameterPassedParameters,
2177: ),
2178: false,
2179: );
2180: }
2181:
2182: private function enterFunctionLike(
2183: PhpFunctionFromParserNodeReflection $functionReflection,
2184: bool $preserveConstructorScope,
2185: ): self
2186: {
2187: $parametersByName = [];
2188:
2189: $functionParameters = $functionReflection->getParameters();
2190: foreach ($functionParameters as $parameter) {
2191: $parametersByName[$parameter->getName()] = $parameter;
2192: }
2193:
2194: $expressionTypes = [];
2195: $nativeExpressionTypes = [];
2196: $conditionalTypes = [];
2197:
2198: if ($preserveConstructorScope) {
2199: $expressionTypes = $this->expressionTypes;
2200: $nativeExpressionTypes = $this->nativeExpressionTypes;
2201: }
2202:
2203: foreach ($functionParameters as $parameter) {
2204: $parameterType = $parameter->getType();
2205:
2206: if ($parameterType instanceof ConditionalTypeForParameter) {
2207: $targetParameterName = substr($parameterType->getParameterName(), 1);
2208: if (array_key_exists($targetParameterName, $parametersByName)) {
2209: $targetParameter = $parametersByName[$targetParameterName];
2210:
2211: $ifType = $parameterType->isNegated() ? $parameterType->getElse() : $parameterType->getIf();
2212: $elseType = $parameterType->isNegated() ? $parameterType->getIf() : $parameterType->getElse();
2213:
2214: $holder = new ConditionalExpressionHolder([
2215: $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::intersect($targetParameter->getType(), $parameterType->getTarget())),
2216: ], ExpressionTypeHolder::createYes(new Variable($parameter->getName()), $ifType));
2217: $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder;
2218:
2219: $holder = new ConditionalExpressionHolder([
2220: $parameterType->getParameterName() => ExpressionTypeHolder::createYes(new Variable($targetParameterName), TypeCombinator::remove($targetParameter->getType(), $parameterType->getTarget())),
2221: ], ExpressionTypeHolder::createYes(new Variable($parameter->getName()), $elseType));
2222: $conditionalTypes['$' . $parameter->getName()][$holder->getKey()] = $holder;
2223: }
2224: }
2225:
2226: $paramExprString = '$' . $parameter->getName();
2227: if ($parameter->isVariadic()) {
2228: if (!$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) {
2229: $parameterType = new ArrayType(new UnionType([IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()]), $parameterType);
2230: } else {
2231: $parameterType = new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $parameterType), new AccessoryArrayListType()]);
2232: }
2233: }
2234: $parameterNode = new Variable($parameter->getName());
2235: $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameterNode, $parameterType);
2236:
2237: $parameterOriginalValueExpr = new ParameterVariableOriginalValueExpr($parameter->getName());
2238: $parameterOriginalValueExprString = $this->getNodeKey($parameterOriginalValueExpr);
2239: $expressionTypes[$parameterOriginalValueExprString] = ExpressionTypeHolder::createYes($parameterOriginalValueExpr, $parameterType);
2240:
2241: $nativeParameterType = $parameter->getNativeType();
2242: if ($parameter->isVariadic()) {
2243: if (!$this->getPhpVersion()->supportsNamedArguments()->no() && $functionReflection->acceptsNamedArguments()->yes()) {
2244: $nativeParameterType = new ArrayType(new UnionType([IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()]), $nativeParameterType);
2245: } else {
2246: $nativeParameterType = new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $nativeParameterType), new AccessoryArrayListType()]);
2247: }
2248: }
2249: $nativeExpressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameterNode, $nativeParameterType);
2250: $nativeExpressionTypes[$parameterOriginalValueExprString] = ExpressionTypeHolder::createYes($parameterOriginalValueExpr, $nativeParameterType);
2251: }
2252:
2253: return $this->scopeFactory->create(
2254: $this->context,
2255: $this->isDeclareStrictTypes(),
2256: $functionReflection,
2257: $this->getNamespace(),
2258: array_merge($this->getConstantTypes(), $expressionTypes),
2259: array_merge($this->getNativeConstantTypes(), $nativeExpressionTypes),
2260: $conditionalTypes,
2261: templateArgumentFrame: $this->templateArgumentFrame,
2262: templateArgumentConstraints: $this->templateArgumentConstraints,
2263: );
2264: }
2265:
2266: /** @api */
2267: public function enterNamespace(string $namespaceName): self
2268: {
2269: return $this->scopeFactory->create(
2270: $this->context->beginFile(),
2271: $this->isDeclareStrictTypes(),
2272: null,
2273: $namespaceName,
2274: templateArgumentFrame: $this->templateArgumentFrame,
2275: templateArgumentConstraints: $this->templateArgumentConstraints,
2276: );
2277: }
2278:
2279: /**
2280: * @param list<non-empty-string> $scopeClasses
2281: */
2282: public function enterClosureBind(?Type $thisType, ?Type $nativeThisType, array $scopeClasses): self
2283: {
2284: $expressionTypes = $this->expressionTypes;
2285: if ($thisType !== null) {
2286: $expressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $thisType);
2287: } else {
2288: unset($expressionTypes['$this']);
2289: }
2290:
2291: $nativeExpressionTypes = $this->nativeExpressionTypes;
2292: if ($nativeThisType !== null) {
2293: $nativeExpressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $nativeThisType);
2294: } else {
2295: unset($nativeExpressionTypes['$this']);
2296: }
2297:
2298: if ($scopeClasses === ['static'] && $this->isInClass()) {
2299: $scopeClasses = [$this->getClassReflection()->getName()];
2300: }
2301:
2302: return $this->scopeFactory->create(
2303: $this->context,
2304: $this->isDeclareStrictTypes(),
2305: $this->getFunction(),
2306: $this->getNamespace(),
2307: $expressionTypes,
2308: $nativeExpressionTypes,
2309: $this->conditionalExpressions,
2310: $scopeClasses,
2311: $this->anonymousFunctionReflection,
2312: templateArgumentFrame: $this->templateArgumentFrame,
2313: templateArgumentConstraints: $this->templateArgumentConstraints,
2314: );
2315: }
2316:
2317: public function restoreOriginalScopeAfterClosureBind(self $originalScope): self
2318: {
2319: $expressionTypes = $this->expressionTypes;
2320: if (isset($originalScope->expressionTypes['$this'])) {
2321: $expressionTypes['$this'] = $originalScope->expressionTypes['$this'];
2322: } else {
2323: unset($expressionTypes['$this']);
2324: }
2325:
2326: $nativeExpressionTypes = $this->nativeExpressionTypes;
2327: if (isset($originalScope->nativeExpressionTypes['$this'])) {
2328: $nativeExpressionTypes['$this'] = $originalScope->nativeExpressionTypes['$this'];
2329: } else {
2330: unset($nativeExpressionTypes['$this']);
2331: }
2332:
2333: return $this->scopeFactory->create(
2334: $this->context,
2335: $this->isDeclareStrictTypes(),
2336: $this->getFunction(),
2337: $this->getNamespace(),
2338: $expressionTypes,
2339: $nativeExpressionTypes,
2340: $this->conditionalExpressions,
2341: $originalScope->inClosureBindScopeClasses,
2342: $this->anonymousFunctionReflection,
2343: templateArgumentFrame: $this->templateArgumentFrame,
2344: templateArgumentConstraints: $this->templateArgumentConstraints,
2345: );
2346: }
2347:
2348: public function restoreThis(self $restoreThisScope): self
2349: {
2350: $expressionTypes = $this->expressionTypes;
2351: $nativeExpressionTypes = $this->nativeExpressionTypes;
2352:
2353: if ($restoreThisScope->isInClass()) {
2354: foreach ($restoreThisScope->expressionTypes as $exprString => $expressionTypeHolder) {
2355: if (!str_starts_with($exprString, '$this')) {
2356: continue;
2357: }
2358:
2359: $expressionTypes[$exprString] = $expressionTypeHolder;
2360: }
2361:
2362: foreach ($restoreThisScope->nativeExpressionTypes as $exprString => $expressionTypeHolder) {
2363: if (!str_starts_with($exprString, '$this')) {
2364: continue;
2365: }
2366:
2367: $nativeExpressionTypes[$exprString] = $expressionTypeHolder;
2368: }
2369: } else {
2370: unset($expressionTypes['$this']);
2371: unset($nativeExpressionTypes['$this']);
2372: }
2373:
2374: return $this->scopeFactory->create(
2375: $this->context,
2376: $this->isDeclareStrictTypes(),
2377: $this->getFunction(),
2378: $this->getNamespace(),
2379: $expressionTypes,
2380: $nativeExpressionTypes,
2381: $this->conditionalExpressions,
2382: $restoreThisScope->inClosureBindScopeClasses,
2383: $this->anonymousFunctionReflection,
2384: $this->inFirstLevelStatement,
2385: [],
2386: [],
2387: $this->inFunctionCallsStack,
2388: $this->afterExtractCall,
2389: $this->parentScope,
2390: $this->nativeTypesPromoted,
2391: $this->templateArgumentFrame,
2392: $this->templateArgumentConstraints,
2393: );
2394: }
2395:
2396: public function enterClosureCall(Type $thisType, Type $nativeThisType): self
2397: {
2398: $expressionTypes = $this->expressionTypes;
2399: $expressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $thisType);
2400:
2401: $nativeExpressionTypes = $this->nativeExpressionTypes;
2402: $nativeExpressionTypes['$this'] = ExpressionTypeHolder::createYes(new Variable('this'), $nativeThisType);
2403:
2404: return $this->scopeFactory->create(
2405: $this->context,
2406: $this->isDeclareStrictTypes(),
2407: $this->getFunction(),
2408: $this->getNamespace(),
2409: $expressionTypes,
2410: $nativeExpressionTypes,
2411: $this->conditionalExpressions,
2412: $thisType->getObjectClassNames(),
2413: $this->anonymousFunctionReflection,
2414: templateArgumentFrame: $this->templateArgumentFrame,
2415: templateArgumentConstraints: $this->templateArgumentConstraints,
2416: );
2417: }
2418:
2419: /** @api */
2420: public function isInClosureBind(): bool
2421: {
2422: return $this->inClosureBindScopeClasses !== [];
2423: }
2424:
2425: /**
2426: * @param list<non-empty-string> $scopeClasses
2427: */
2428: public function withClosureBindScopeClasses(array $scopeClasses): self
2429: {
2430: return $this->scopeFactory->create(
2431: $this->context,
2432: $this->isDeclareStrictTypes(),
2433: $this->getFunction(),
2434: $this->getNamespace(),
2435: $this->expressionTypes,
2436: $this->nativeExpressionTypes,
2437: $this->conditionalExpressions,
2438: $scopeClasses,
2439: $this->anonymousFunctionReflection,
2440: $this->isInFirstLevelStatement(),
2441: $this->currentlyAssignedExpressions,
2442: $this->currentlyAllowedUndefinedExpressions,
2443: $this->inFunctionCallsStack,
2444: $this->afterExtractCall,
2445: $this->parentScope,
2446: $this->nativeTypesPromoted,
2447: $this->templateArgumentFrame,
2448: $this->templateArgumentConstraints,
2449: );
2450: }
2451:
2452: /**
2453: * @api
2454: * @param ParameterReflection[]|null $callableParameters
2455: * @param ParameterReflection[]|null $nativeCallableParameters
2456: */
2457: public function enterAnonymousFunction(
2458: Expr\Closure $closure,
2459: ?array $callableParameters,
2460: ?array $nativeCallableParameters = null,
2461: ): self
2462: {
2463: $anonymousFunctionReflection = $this->services->container->getByType(ClosureTypeResolver::class)->getClosureType($this, $closure, true, $this->getCurrentExpressionResultStorage());
2464:
2465: $scope = $this->enterAnonymousFunctionWithoutReflection($closure, $callableParameters, $nativeCallableParameters);
2466:
2467: return $this->scopeFactory->create(
2468: $scope->context,
2469: $scope->isDeclareStrictTypes(),
2470: $scope->getFunction(),
2471: $scope->getNamespace(),
2472: $scope->expressionTypes,
2473: $scope->nativeExpressionTypes,
2474: $scope->conditionalExpressions,
2475: $scope->inClosureBindScopeClasses,
2476: $anonymousFunctionReflection,
2477: true,
2478: [],
2479: [],
2480: $this->inFunctionCallsStack,
2481: false,
2482: $this,
2483: $this->nativeTypesPromoted,
2484: $this->templateArgumentFrame,
2485: $this->templateArgumentConstraints,
2486: );
2487: }
2488:
2489: /**
2490: * @param ParameterReflection[]|null $callableParameters
2491: * @param ParameterReflection[]|null $nativeCallableParameters
2492: */
2493: public function enterAnonymousFunctionWithoutReflection(
2494: Expr\Closure $closure,
2495: ?array $callableParameters,
2496: ?array $nativeCallableParameters,
2497: ): self
2498: {
2499: $expressionTypes = [];
2500: $nativeTypes = [];
2501: foreach ($closure->params as $i => $parameter) {
2502: if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
2503: throw new ShouldNotHappenException();
2504: }
2505: $paramExprString = sprintf('$%s', $parameter->var->name);
2506: $isNullable = $this->isParameterValueNullable($parameter);
2507: $nativeParameterType = $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
2508: if ($callableParameters !== null) {
2509: $parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($parameter, $callableParameters, $i));
2510: }
2511: if ($nativeCallableParameters !== null) {
2512: $nativeParameterType = self::intersectButNotNever($nativeParameterType, $this->getCallableParameterType($parameter, $nativeCallableParameters, $i));
2513: }
2514: $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameter->var, $parameterType);
2515: $nativeTypes[$paramExprString] = ExpressionTypeHolder::createYes($parameter->var, $nativeParameterType);
2516: }
2517:
2518: $nonRefVariableNames = [];
2519: $useVariableNames = [];
2520: foreach ($closure->uses as $use) {
2521: if (!is_string($use->var->name)) {
2522: throw new ShouldNotHappenException();
2523: }
2524: $variableName = $use->var->name;
2525: $paramExprString = '$' . $use->var->name;
2526: $useVariableNames[$paramExprString] = true;
2527: if ($use->byRef) {
2528: $holder = ExpressionTypeHolder::createYes($use->var, new MixedType());
2529: $expressionTypes[$paramExprString] = $holder;
2530: $nativeTypes[$paramExprString] = $holder;
2531: continue;
2532: }
2533: $nonRefVariableNames[$variableName] = true;
2534: if ($this->hasVariableType($variableName)->no()) {
2535: $variableType = new ErrorType();
2536: $variableNativeType = new ErrorType();
2537: } else {
2538: $variableType = $this->getVariableType($variableName);
2539: // a plain variable read is scope state - never priced via the
2540: // node, which may not have been processed yet
2541: $nativeScope = $this->doNotTreatPhpDocTypesAsCertain();
2542: $variableNativeType = $nativeScope->hasVariableType($variableName)->no() ? new ErrorType() : $nativeScope->getVariableType($variableName);
2543: }
2544: $expressionTypes[$paramExprString] = ExpressionTypeHolder::createYes($use->var, $variableType);
2545: $nativeTypes[$paramExprString] = ExpressionTypeHolder::createYes($use->var, $variableNativeType);
2546: }
2547:
2548: $nonStaticExpressions = $this->invalidateStaticExpressions($this->expressionTypes);
2549: foreach ($nonStaticExpressions as $exprString => $typeHolder) {
2550: $expr = $typeHolder->getExpr();
2551:
2552: if ($expr instanceof Variable) {
2553: continue;
2554: }
2555:
2556: $variables = (new NodeFinder())->findInstanceOf([$expr], Variable::class);
2557: if ($variables === [] && !$this->expressionTypeIsUnchangeable($typeHolder)) {
2558: continue;
2559: }
2560:
2561: foreach ($variables as $variable) {
2562: if (!is_string($variable->name)) {
2563: continue 2;
2564: }
2565: if (!array_key_exists($variable->name, $nonRefVariableNames)) {
2566: continue 2;
2567: }
2568: }
2569:
2570: $expressionTypes[$exprString] = $typeHolder;
2571: }
2572:
2573: if ($this->hasVariableType('this')->yes() && !$closure->static) {
2574: $node = new Variable('this');
2575: $expressionTypes['$this'] = ExpressionTypeHolder::createYes($node, $this->getType($node));
2576: $nativeTypes['$this'] = ExpressionTypeHolder::createYes($node, $this->getNativeType($node));
2577:
2578: if ($this->services->phpVersion->supportsReadOnlyProperties()) {
2579: foreach ($nonStaticExpressions as $exprString => $typeHolder) {
2580: $expr = $typeHolder->getExpr();
2581:
2582: if (!$expr instanceof PropertyFetch) {
2583: continue;
2584: }
2585:
2586: if (!$this->isReadonlyPropertyFetch($expr, true)) {
2587: continue;
2588: }
2589:
2590: $expressionTypes[$exprString] = $typeHolder;
2591: }
2592: }
2593: }
2594:
2595: $filteredConditionalExpressions = [];
2596: foreach ($this->conditionalExpressions as $conditionalExprString => $holders) {
2597: if (!array_key_exists($conditionalExprString, $useVariableNames)) {
2598: continue;
2599: }
2600: $filteredHolders = [];
2601: foreach ($holders as $key => $holder) {
2602: foreach (array_keys($holder->getConditionExpressionTypeHolders()) as $holderExprString) {
2603: if (!array_key_exists($holderExprString, $useVariableNames)) {
2604: continue 2;
2605: }
2606: }
2607: $filteredHolders[$key] = $holder;
2608: }
2609: if ($filteredHolders === []) {
2610: continue;
2611: }
2612:
2613: $filteredConditionalExpressions[$conditionalExprString] = $filteredHolders;
2614: }
2615:
2616: return $this->scopeFactory->create(
2617: $this->context,
2618: $this->isDeclareStrictTypes(),
2619: $this->getFunction(),
2620: $this->getNamespace(),
2621: array_merge($this->getConstantTypes(), $expressionTypes),
2622: array_merge($this->getNativeConstantTypes(), $nativeTypes),
2623: $filteredConditionalExpressions,
2624: $this->inClosureBindScopeClasses,
2625: new ClosureType(),
2626: true,
2627: [],
2628: [],
2629: [],
2630: false,
2631: $this,
2632: $this->nativeTypesPromoted,
2633: $this->templateArgumentFrame,
2634: $this->templateArgumentConstraints,
2635: );
2636: }
2637:
2638: private function expressionTypeIsUnchangeable(ExpressionTypeHolder $typeHolder): bool
2639: {
2640: $expr = $typeHolder->getExpr();
2641: $type = $typeHolder->getType();
2642:
2643: return $expr instanceof FuncCall
2644: && !$expr->isFirstClassCallable()
2645: && $expr->name instanceof FullyQualified
2646: && in_array(
2647: $expr->name->toLowerString(),
2648: [
2649: 'class_exists',
2650: 'interface_exists',
2651: 'trait_exists',
2652: 'enum_exists',
2653: 'function_exists',
2654: ],
2655: true,
2656: )
2657: && isset($expr->getArgs()[0])
2658: && count($this->getScopeStateType($expr->getArgs()[0]->value)->getConstantStrings()) === 1
2659: && $type->isTrue()->yes();
2660: }
2661:
2662: /**
2663: * @param array<string, ExpressionTypeHolder> $expressionTypes
2664: * @return array<string, ExpressionTypeHolder>
2665: */
2666: private function invalidateStaticExpressions(array $expressionTypes): array
2667: {
2668: $filteredExpressionTypes = [];
2669: $nodeFinder = new NodeFinder();
2670: foreach ($expressionTypes as $exprString => $expressionType) {
2671: $staticExpression = $nodeFinder->findFirst(
2672: [$expressionType->getExpr()],
2673: static fn ($node) => $node instanceof Expr\StaticCall || $node instanceof Expr\StaticPropertyFetch,
2674: );
2675: if ($staticExpression !== null) {
2676: continue;
2677: }
2678: $filteredExpressionTypes[$exprString] = $expressionType;
2679: }
2680: return $filteredExpressionTypes;
2681: }
2682:
2683: /**
2684: * @api
2685: * @param ParameterReflection[]|null $callableParameters
2686: * @param ParameterReflection[]|null $nativeCallableParameters
2687: */
2688: public function enterArrowFunction(Expr\ArrowFunction $arrowFunction, ?array $callableParameters, ?array $nativeCallableParameters = null): self
2689: {
2690: $anonymousFunctionReflection = $this->services->container->getByType(ClosureTypeResolver::class)->getClosureType($this, $arrowFunction, true, $this->getCurrentExpressionResultStorage());
2691:
2692: $scope = $this->enterArrowFunctionWithoutReflection($arrowFunction, $callableParameters, $nativeCallableParameters);
2693:
2694: return $this->scopeFactory->create(
2695: $scope->context,
2696: $scope->isDeclareStrictTypes(),
2697: $scope->getFunction(),
2698: $scope->getNamespace(),
2699: $scope->expressionTypes,
2700: $scope->nativeExpressionTypes,
2701: $scope->conditionalExpressions,
2702: $scope->inClosureBindScopeClasses,
2703: $anonymousFunctionReflection,
2704: true,
2705: [],
2706: [],
2707: $this->inFunctionCallsStack,
2708: $scope->afterExtractCall,
2709: $scope->parentScope,
2710: $this->nativeTypesPromoted,
2711: $this->templateArgumentFrame,
2712: $this->templateArgumentConstraints,
2713: );
2714: }
2715:
2716: /**
2717: * @param ParameterReflection[]|null $callableParameters
2718: * @param ParameterReflection[]|null $nativeCallableParameters
2719: */
2720: public function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFunction, ?array $callableParameters, ?array $nativeCallableParameters): self
2721: {
2722: $arrowFunctionScope = $this;
2723: foreach ($arrowFunction->params as $i => $parameter) {
2724: $isNullable = $this->isParameterValueNullable($parameter);
2725: $nativeParameterType = $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic);
2726: if ($callableParameters !== null) {
2727: $parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($parameter, $callableParameters, $i));
2728: }
2729: if ($nativeCallableParameters !== null) {
2730: $nativeParameterType = self::intersectButNotNever($nativeParameterType, $this->getCallableParameterType($parameter, $nativeCallableParameters, $i));
2731: }
2732:
2733: if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) {
2734: throw new ShouldNotHappenException();
2735: }
2736: $arrowFunctionScope = $arrowFunctionScope->assignVariable($parameter->var->name, $parameterType, $nativeParameterType, TrinaryLogic::createYes());
2737: }
2738:
2739: if ($arrowFunction->static) {
2740: $arrowFunctionScope = $arrowFunctionScope->invalidateExpression(new Variable('this'));
2741: }
2742:
2743: return $this->scopeFactory->create(
2744: $arrowFunctionScope->context,
2745: $this->isDeclareStrictTypes(),
2746: $arrowFunctionScope->getFunction(),
2747: $arrowFunctionScope->getNamespace(),
2748: $this->invalidateStaticExpressions($arrowFunctionScope->expressionTypes),
2749: $arrowFunctionScope->nativeExpressionTypes,
2750: $arrowFunctionScope->conditionalExpressions,
2751: $arrowFunctionScope->inClosureBindScopeClasses,
2752: new ClosureType(),
2753: true,
2754: [],
2755: [],
2756: [],
2757: $arrowFunctionScope->afterExtractCall,
2758: $arrowFunctionScope->parentScope,
2759: $this->nativeTypesPromoted,
2760: $this->templateArgumentFrame,
2761: $this->templateArgumentConstraints,
2762: );
2763: }
2764:
2765: public function isParameterValueNullable(Node\Param $parameter): bool
2766: {
2767: if ($parameter->default instanceof ConstFetch) {
2768: return strtolower((string) $parameter->default->name) === 'null';
2769: }
2770:
2771: return false;
2772: }
2773:
2774: /**
2775: * @api
2776: * @param Node\Name|Node\Identifier|Node\ComplexType|null $type
2777: */
2778: public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type
2779: {
2780: if ($isVariadic) {
2781: if (!$this->getPhpVersion()->supportsNamedArguments()->no()) {
2782: return new ArrayType(new UnionType([IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()]), $this->getFunctionType(
2783: $type,
2784: $isNullable,
2785: false,
2786: ));
2787: }
2788:
2789: return new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $this->getFunctionType(
2790: $type,
2791: $isNullable,
2792: false,
2793: )), new AccessoryArrayListType()]);
2794: }
2795: if (
2796: $type instanceof Name
2797: && $this->inClosureBindScopeClasses !== []
2798: && $this->inClosureBindScopeClasses !== ['static']
2799: && in_array($type->toLowerString(), ['static', 'self', 'parent'], true)
2800: && $this->services->reflectionProvider->hasClass($this->inClosureBindScopeClasses[0])
2801: ) {
2802: return $this->services->initializerExprTypeResolver->getFunctionType(
2803: $type,
2804: $isNullable,
2805: false,
2806: InitializerExprContext::fromClassReflection(
2807: $this->services->reflectionProvider->getClass($this->inClosureBindScopeClasses[0]),
2808: ),
2809: );
2810: }
2811:
2812: return $this->services->initializerExprTypeResolver->getFunctionType($type, $isNullable, false, InitializerExprContext::fromScope($this));
2813: }
2814:
2815: /**
2816: * @param ParameterReflection[] $callableParameters
2817: */
2818: private function getCallableParameterType(Node\Param $parameter, array $callableParameters, int $index): Type
2819: {
2820: if ($parameter->variadic) {
2821: return $this->buildVariadicArrayTypeFromCallableParameters($callableParameters, $index);
2822: }
2823:
2824: if (isset($callableParameters[$index])) {
2825: return $callableParameters[$index]->getType();
2826: }
2827:
2828: if (count($callableParameters) === 0) {
2829: return new MixedType();
2830: }
2831:
2832: $lastParameter = array_last($callableParameters);
2833: if ($lastParameter->isVariadic()) {
2834: return $lastParameter->getType();
2835: }
2836:
2837: return new MixedType();
2838: }
2839:
2840: /**
2841: * @param array<ParameterReflection> $callableParameters
2842: */
2843: private function buildVariadicArrayTypeFromCallableParameters(array $callableParameters, int $startIndex): Type
2844: {
2845: $elementTypes = [];
2846: $callableParametersCount = count($callableParameters);
2847: for ($j = $startIndex; $j < $callableParametersCount; $j++) {
2848: $elementTypes[] = $callableParameters[$j]->getType();
2849: if ($callableParameters[$j]->isVariadic()) {
2850: break;
2851: }
2852: }
2853:
2854: if ($elementTypes === [] && $callableParametersCount > 0) {
2855: $lastParameter = array_last($callableParameters);
2856: if ($lastParameter->isVariadic()) {
2857: $elementTypes[] = $lastParameter->getType();
2858: }
2859: }
2860:
2861: if ($elementTypes === []) {
2862: return new MixedType();
2863: }
2864:
2865: $elementType = TypeCombinator::union(...$elementTypes);
2866:
2867: if (!$this->getPhpVersion()->supportsNamedArguments()->no()) {
2868: return new ArrayType(new UnionType([IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()]), $elementType);
2869: }
2870:
2871: return new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $elementType), new AccessoryArrayListType()]);
2872: }
2873:
2874: public static function intersectButNotNever(Type $nativeType, Type $inferredType): Type
2875: {
2876: if ($nativeType->isSuperTypeOf($inferredType)->no()) {
2877: return $nativeType;
2878: }
2879:
2880: $result = TypeCombinator::intersect($nativeType, $inferredType);
2881: if ($result instanceof NeverType) {
2882: // the inferred type says no value is ever produced - the native
2883: // type's nullability must not resurrect one
2884: return $result;
2885: }
2886:
2887: if (TypeCombinator::containsNull($nativeType)) {
2888: return TypeCombinator::addNull($result);
2889: }
2890:
2891: return $result;
2892: }
2893:
2894: public function enterMatch(Expr\Match_ $expr, Type $condType, Type $condNativeType): self
2895: {
2896: if ($expr->cond instanceof Variable) {
2897: return $this;
2898: }
2899: if ($expr->cond instanceof AlwaysRememberedExpr) {
2900: $cond = $expr->cond->expr;
2901: } else {
2902: $cond = $expr->cond;
2903: }
2904: if ($cond instanceof Scalar) {
2905: return $this;
2906: }
2907:
2908: $type = $condType;
2909: $nativeType = $condNativeType;
2910: $condExpr = new AlwaysRememberedExpr($cond, $type, $nativeType);
2911: $expr->cond = $condExpr;
2912:
2913: return $this->assignExpression($condExpr, $type, $nativeType);
2914: }
2915:
2916: public function enterForeach(self $originalScope, Expr $iteratee, Type $iterateeType, Type $nativeIterateeType, string $valueName, ?string $keyName, bool $valueByRef): self
2917: {
2918: $valueType = $originalScope->getIterableValueType($iterateeType);
2919: $nativeValueType = $originalScope->getIterableValueType($nativeIterateeType);
2920: $scope = $this->assignVariable(
2921: $valueName,
2922: $valueType,
2923: $nativeValueType,
2924: TrinaryLogic::createYes(),
2925: );
2926: // Track the original foreach value so narrowings applied to the value
2927: // variable (e.g. is_string($type)) can later be projected back onto the
2928: // corresponding array dim fetch without being confused by a reassignment
2929: // ($type = 'foo' invalidates this expression, same as OriginalForeachKeyExpr).
2930: $scope = $scope->assignExpression(new OriginalForeachValueExpr($valueName), $valueType, $nativeValueType);
2931: if ($valueByRef && $iterateeType->isArray()->yes() && $iterateeType->isConstantArray()->no()) {
2932: // the write-through rebuilds the iteratee AT FOREACH ENTRY with the
2933: // value variable's latest type - captured here, not read live: a
2934: // live read would union the transient mid-iteration value states
2935: // into the array (the loop convergence owns cross-iteration merging)
2936: $scope = $scope->assignExpression(
2937: new IntertwinedVariableByReferenceWithExpr($valueName, $iteratee, new SetExistingOffsetValueTypeExpr(
2938: new NativeTypeExpr($iterateeType, $nativeIterateeType),
2939: new NativeTypeExpr(
2940: $originalScope->getIterableKeyType($iterateeType),
2941: $originalScope->getIterableKeyType($nativeIterateeType),
2942: ),
2943: new Variable($valueName),
2944: )),
2945: $valueType,
2946: $nativeValueType,
2947: );
2948: }
2949: if ($keyName !== null) {
2950: $scope = $scope->enterForeachKey($originalScope, $iteratee, $iterateeType, $nativeIterateeType, $keyName);
2951:
2952: if ($valueByRef && $iterateeType->isArray()->yes() && $iterateeType->isConstantArray()->no()) {
2953: $scope = $scope->assignExpression(
2954: new IntertwinedVariableByReferenceWithExpr($valueName, new Expr\ArrayDimFetch($iteratee, new Variable($keyName)), new Variable($valueName)),
2955: $valueType,
2956: $nativeValueType,
2957: );
2958: }
2959: }
2960:
2961: return $scope;
2962: }
2963:
2964: public function enterForeachKey(self $originalScope, Expr $iteratee, Type $iterateeType, Type $nativeIterateeType, string $keyName): self
2965: {
2966: $keyType = $originalScope->getIterableKeyType($iterateeType);
2967: $nativeKeyType = $originalScope->getIterableKeyType($nativeIterateeType);
2968:
2969: $scope = $this->assignVariable(
2970: $keyName,
2971: $keyType,
2972: $nativeKeyType,
2973: TrinaryLogic::createYes(),
2974: );
2975:
2976: $originalForeachKeyExpr = new OriginalForeachKeyExpr($keyName);
2977: $scope = $scope->assignExpression($originalForeachKeyExpr, $keyType, $nativeKeyType);
2978: if ($iterateeType->isArray()->yes()) {
2979: $scope = $scope->assignExpression(
2980: new Expr\ArrayDimFetch($iteratee, new Variable($keyName)),
2981: $originalScope->getIterableValueType($iterateeType),
2982: $originalScope->getIterableValueType($nativeIterateeType),
2983: );
2984: }
2985:
2986: return $scope;
2987: }
2988:
2989: public function enterCatchType(Type $catchType, ?string $variableName): self
2990: {
2991: if ($variableName === null) {
2992: return $this;
2993: }
2994:
2995: return $this->assignVariable(
2996: $variableName,
2997: TypeCombinator::intersect($catchType, new ObjectType(Throwable::class)),
2998: TypeCombinator::intersect($catchType, new ObjectType(Throwable::class)),
2999: TrinaryLogic::createYes(),
3000: );
3001: }
3002:
3003: public function enterExpressionAssign(Expr $expr, bool $isPlainWrite = true): self
3004: {
3005: $exprString = $this->getNodeKey($expr);
3006: $currentlyAssignedExpressions = $this->currentlyAssignedExpressions;
3007: $currentlyAssignedExpressions[$exprString] = $isPlainWrite;
3008:
3009: $scope = $this->scopeFactory->create(
3010: $this->context,
3011: $this->isDeclareStrictTypes(),
3012: $this->getFunction(),
3013: $this->getNamespace(),
3014: $this->expressionTypes,
3015: $this->nativeExpressionTypes,
3016: $this->conditionalExpressions,
3017: $this->inClosureBindScopeClasses,
3018: $this->anonymousFunctionReflection,
3019: $this->isInFirstLevelStatement(),
3020: $currentlyAssignedExpressions,
3021: $this->currentlyAllowedUndefinedExpressions,
3022: [],
3023: $this->afterExtractCall,
3024: $this->parentScope,
3025: $this->nativeTypesPromoted,
3026: $this->templateArgumentFrame,
3027: $this->templateArgumentConstraints,
3028: );
3029: $scope->resolvedTypes = $this->resolvedTypes;
3030:
3031: return $scope;
3032: }
3033:
3034: public function exitExpressionAssign(Expr $expr): self
3035: {
3036: $exprString = $this->getNodeKey($expr);
3037: $currentlyAssignedExpressions = $this->currentlyAssignedExpressions;
3038: unset($currentlyAssignedExpressions[$exprString]);
3039:
3040: $scope = $this->scopeFactory->create(
3041: $this->context,
3042: $this->isDeclareStrictTypes(),
3043: $this->getFunction(),
3044: $this->getNamespace(),
3045: $this->expressionTypes,
3046: $this->nativeExpressionTypes,
3047: $this->conditionalExpressions,
3048: $this->inClosureBindScopeClasses,
3049: $this->anonymousFunctionReflection,
3050: $this->isInFirstLevelStatement(),
3051: $currentlyAssignedExpressions,
3052: $this->currentlyAllowedUndefinedExpressions,
3053: [],
3054: $this->afterExtractCall,
3055: $this->parentScope,
3056: $this->nativeTypesPromoted,
3057: $this->templateArgumentFrame,
3058: $this->templateArgumentConstraints,
3059: );
3060: $scope->resolvedTypes = $this->resolvedTypes;
3061:
3062: return $scope;
3063: }
3064:
3065: /** @api */
3066: public function isInExpressionAssign(Expr $expr): bool
3067: {
3068: if (count($this->currentlyAssignedExpressions) === 0) {
3069: return false;
3070: }
3071:
3072: $exprString = $this->getNodeKey($expr);
3073: return array_key_exists($exprString, $this->currentlyAssignedExpressions);
3074: }
3075:
3076: /**
3077: * Whether the expression is a plain write target of an assignment, as opposed to being
3078: * read-modified in place (e.g. the base of `$prop[] = ...`). Used to decide whether a
3079: * property fetch resolves to its writable or readable type.
3080: */
3081: public function isInWriteExpressionAssign(Expr $expr): bool
3082: {
3083: if (count($this->currentlyAssignedExpressions) === 0) {
3084: return false;
3085: }
3086:
3087: $exprString = $this->getNodeKey($expr);
3088: return ($this->currentlyAssignedExpressions[$exprString] ?? false) === true;
3089: }
3090:
3091: public function setAllowedUndefinedExpression(Expr $expr): self
3092: {
3093: if ($expr instanceof Expr\StaticPropertyFetch) {
3094: return $this;
3095: }
3096:
3097: $exprString = $this->getNodeKey($expr);
3098: $currentlyAllowedUndefinedExpressions = $this->currentlyAllowedUndefinedExpressions;
3099: $currentlyAllowedUndefinedExpressions[$exprString] = true;
3100:
3101: $scope = $this->scopeFactory->create(
3102: $this->context,
3103: $this->isDeclareStrictTypes(),
3104: $this->getFunction(),
3105: $this->getNamespace(),
3106: $this->expressionTypes,
3107: $this->nativeExpressionTypes,
3108: $this->conditionalExpressions,
3109: $this->inClosureBindScopeClasses,
3110: $this->anonymousFunctionReflection,
3111: $this->isInFirstLevelStatement(),
3112: $this->currentlyAssignedExpressions,
3113: $currentlyAllowedUndefinedExpressions,
3114: [],
3115: $this->afterExtractCall,
3116: $this->parentScope,
3117: $this->nativeTypesPromoted,
3118: $this->templateArgumentFrame,
3119: $this->templateArgumentConstraints,
3120: );
3121: $scope->resolvedTypes = $this->resolvedTypes;
3122:
3123: return $scope;
3124: }
3125:
3126: public function unsetAllowedUndefinedExpression(Expr $expr): self
3127: {
3128: $exprString = $this->getNodeKey($expr);
3129: $currentlyAllowedUndefinedExpressions = $this->currentlyAllowedUndefinedExpressions;
3130: unset($currentlyAllowedUndefinedExpressions[$exprString]);
3131:
3132: $scope = $this->scopeFactory->create(
3133: $this->context,
3134: $this->isDeclareStrictTypes(),
3135: $this->getFunction(),
3136: $this->getNamespace(),
3137: $this->expressionTypes,
3138: $this->nativeExpressionTypes,
3139: $this->conditionalExpressions,
3140: $this->inClosureBindScopeClasses,
3141: $this->anonymousFunctionReflection,
3142: $this->isInFirstLevelStatement(),
3143: $this->currentlyAssignedExpressions,
3144: $currentlyAllowedUndefinedExpressions,
3145: [],
3146: $this->afterExtractCall,
3147: $this->parentScope,
3148: $this->nativeTypesPromoted,
3149: $this->templateArgumentFrame,
3150: $this->templateArgumentConstraints,
3151: );
3152: $scope->resolvedTypes = $this->resolvedTypes;
3153:
3154: return $scope;
3155: }
3156:
3157: /** @api */
3158: public function isUndefinedExpressionAllowed(Expr $expr): bool
3159: {
3160: if (count($this->currentlyAllowedUndefinedExpressions) === 0) {
3161: return false;
3162: }
3163: $exprString = $this->getNodeKey($expr);
3164: return array_key_exists($exprString, $this->currentlyAllowedUndefinedExpressions);
3165: }
3166:
3167: /**
3168: * @param list<string> $intertwinedPropagatedFrom
3169: */
3170: public function assignVariable(string $variableName, Type $type, Type $nativeType, TrinaryLogic $certainty, array $intertwinedPropagatedFrom = []): self
3171: {
3172: $node = new Variable($variableName);
3173: $scope = $this->assignExpression($node, $type, $nativeType);
3174: if ($certainty->no()) {
3175: throw new ShouldNotHappenException();
3176: } elseif (!$certainty->yes()) {
3177: $exprString = '$' . $variableName;
3178: $scope->expressionTypes[$exprString] = new ExpressionTypeHolder($node, $type, $certainty);
3179: $scope->nativeExpressionTypes[$exprString] = new ExpressionTypeHolder($node, $nativeType, $certainty);
3180: }
3181:
3182: foreach ($scope->expressionTypes as $exprString => $expressionType) {
3183: if (!$expressionType->getExpr() instanceof IntertwinedVariableByReferenceWithExpr) {
3184: continue;
3185: }
3186: if (!$expressionType->getCertainty()->yes()) {
3187: continue;
3188: }
3189: if ($expressionType->getExpr()->getVariableName() !== $variableName) {
3190: continue;
3191: }
3192:
3193: $assignedExpr = $expressionType->getExpr()->getAssignedExpr();
3194: if (
3195: $assignedExpr instanceof Expr\ArrayDimFetch
3196: && !$this->isDimFetchPathReachable($scope, $assignedExpr)
3197: ) {
3198: unset($scope->expressionTypes[$exprString]);
3199: unset($scope->nativeExpressionTypes[$exprString]);
3200: continue;
3201: }
3202:
3203: // When the byref's dim is non-constant AND not enumerable as a
3204: // finite set of scalars (e.g. general `int` or `mixed`), the just-
3205: // performed write to $array might or might not have hit the byref's
3206: // slot. Union the new $array[dim] read with the byref's previous
3207: // type and the pre-write $array[dim] so values that could still be
3208: // at the slot (unmodified or shadowed by an explicit-key overwrite)
3209: // survive. For finitely-enumerable dims (e.g. `bool`, `int<0, 5>`)
3210: // the array literal builder enumerates all possibilities, so the
3211: // new $array[dim] read already covers every reachable slot.
3212: $unionWithOld = false;
3213: if ($assignedExpr instanceof Expr\ArrayDimFetch && $assignedExpr->dim !== null) {
3214: $dimType = $scope->getType($assignedExpr->dim);
3215: if (count($dimType->getConstantScalarValues()) !== 1 && count($dimType->getFiniteTypes()) === 0) {
3216: $unionWithOld = true;
3217: }
3218: }
3219:
3220: // Resolve the byref slot's new value directly from the just-assigned
3221: // root variable's type, instead of re-evaluating the (stale) $assignedExpr
3222: // node via Scope::getType(): the stored ArrayDimFetch result captured the
3223: // array variable before it existed, so re-reading it would only resolve
3224: // through the asking scope. We already hold the authoritative value here.
3225: $assignedType = $this->resolveIntertwinedAssignedType($scope, $type, $assignedExpr, $variableName, false);
3226: $assignedNativeType = $this->resolveIntertwinedAssignedType($scope, $nativeType, $assignedExpr, $variableName, true);
3227:
3228: $has = $scope->hasExpressionType($expressionType->getExpr()->getExpr());
3229: if (
3230: $expressionType->getExpr()->getExpr() instanceof Variable
3231: && is_string($expressionType->getExpr()->getExpr()->name)
3232: && !$has->no()
3233: ) {
3234: $targetVarName = $expressionType->getExpr()->getExpr()->name;
3235: if (in_array($targetVarName, $intertwinedPropagatedFrom, true)) {
3236: continue;
3237: }
3238: if ($unionWithOld) {
3239: $targetVarNode = new Variable($targetVarName);
3240: $rootVarNode = new Variable($variableName);
3241: $assignedType = TypeCombinator::union(
3242: $assignedType,
3243: $this->resolveIntertwinedAssignedType($this, $this->getType($rootVarNode), $assignedExpr, $variableName, false),
3244: $scope->getType($targetVarNode),
3245: );
3246: $assignedNativeType = TypeCombinator::union(
3247: $assignedNativeType,
3248: $this->resolveIntertwinedAssignedType($this, $this->getNativeType($rootVarNode), $assignedExpr, $variableName, true),
3249: $scope->getNativeType($targetVarNode),
3250: );
3251: }
3252: $scope = $scope->assignVariable(
3253: $targetVarName,
3254: $assignedType,
3255: $assignedNativeType,
3256: $has,
3257: array_merge($intertwinedPropagatedFrom, [$variableName]),
3258: );
3259: } else {
3260: $targetRootVar = ScopeOps::getIntertwinedRefRootVariableName($expressionType->getExpr()->getExpr());
3261: if ($targetRootVar !== null && in_array($targetRootVar, $intertwinedPropagatedFrom, true)) {
3262: continue;
3263: }
3264: $scope = $scope->overwriteExpression(
3265: $expressionType->getExpr()->getExpr(),
3266: $assignedType,
3267: $assignedNativeType,
3268: );
3269: }
3270: }
3271:
3272: return $scope;
3273: }
3274:
3275: /**
3276: * assignExpression() for a value that overwrites what an already existing
3277: * offset holds - a byref alias write or a setAlwaysOverwriteTypes()
3278: * specification.
3279: *
3280: * For an `ArrayDimFetch` the new value has to be *written* into the containing
3281: * array. assignExpression() would instead narrow it: specifyExpressionType()
3282: * intersects the parent with HasOffsetValueType(dim, newValue), which
3283: * contradicts - and collapses to `never` - a parent still holding the offset's
3284: * previous (constant) value.
3285: */
3286: private function overwriteExpression(Expr $expr, Type $type, Type $nativeType): self
3287: {
3288: if (!$expr instanceof Expr\ArrayDimFetch || $expr->dim === null) {
3289: return $this->assignExpression($expr, $type, $nativeType);
3290: }
3291:
3292: $dimType = $this->getType($expr->dim);
3293: $scope = $this->overwriteExpression(
3294: $expr->var,
3295: $this->getType($expr->var)->setExistingOffsetValueType($dimType, $type),
3296: $this->getNativeType($expr->var)->setExistingOffsetValueType($dimType, $nativeType),
3297: );
3298:
3299: return $scope->specifyExpressionType($expr, $type, $nativeType, TrinaryLogic::createYes());
3300: }
3301:
3302: /**
3303: * Resolves the type of a byref slot expression (rooted at $rootVariableName)
3304: * from $rootType - the type just assigned to that root variable - by walking
3305: * the offsets, without re-evaluating the stored $assignedExpr node via
3306: * Scope::getType().
3307: */
3308: private function resolveIntertwinedAssignedType(self $scope, Type $rootType, Expr $assignedExpr, string $rootVariableName, bool $native): Type
3309: {
3310: if ($assignedExpr instanceof Variable && is_string($assignedExpr->name) && $assignedExpr->name === $rootVariableName) {
3311: return $rootType;
3312: }
3313:
3314: if ($assignedExpr instanceof Expr\ArrayDimFetch && $assignedExpr->dim !== null) {
3315: return $this->resolveIntertwinedAssignedType($scope, $rootType, $assignedExpr->var, $rootVariableName, $native)
3316: ->getOffsetValueType($native ? $scope->getNativeType($assignedExpr->dim) : $scope->getType($assignedExpr->dim));
3317: }
3318:
3319: if ($assignedExpr instanceof SetExistingOffsetValueTypeExpr) {
3320: // foreach-byref slot: the iteratee with its key offset set to the value
3321: // variable's new type ($rootType is exactly that value - the expr's
3322: // getValue()).
3323: $iterateeType = $native
3324: ? $scope->getNativeType($assignedExpr->getVar())
3325: : $scope->getType($assignedExpr->getVar());
3326:
3327: $dimType = $native
3328: ? $scope->getNativeType($assignedExpr->getDim())
3329: : $scope->getType($assignedExpr->getDim());
3330:
3331: return $iterateeType->setExistingOffsetValueType($dimType, $rootType);
3332: }
3333:
3334: throw new ShouldNotHappenException();
3335: }
3336:
3337: private function isDimFetchPathReachable(self $scope, Expr\ArrayDimFetch $dimFetch): bool
3338: {
3339: if ($dimFetch->dim === null) {
3340: return false;
3341: }
3342:
3343: if (!$dimFetch->var instanceof Expr\ArrayDimFetch) {
3344: return true;
3345: }
3346:
3347: $varType = $scope->getType($dimFetch->var);
3348: $dimType = $scope->getType($dimFetch->dim);
3349:
3350: if (!$varType->hasOffsetValueType($dimType)->yes()) {
3351: return false;
3352: }
3353:
3354: return $this->isDimFetchPathReachable($scope, $dimFetch->var);
3355: }
3356:
3357: private function unsetExpression(Expr $expr): self
3358: {
3359: $scope = $this;
3360: if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) {
3361: $exprVarType = $scope->getScopeStateType($expr->var);
3362: $dimType = $scope->getType($expr->dim);
3363: $unsetType = $exprVarType->unsetOffset($dimType);
3364: $exprVarNativeType = $scope->getScopeStateNativeType($expr->var);
3365: $dimNativeType = $scope->getNativeType($expr->dim);
3366: $unsetNativeType = $exprVarNativeType->unsetOffset($dimNativeType);
3367: $scope = $scope->assignExpression($expr->var, $unsetType, $unsetNativeType)->invalidateExpression(
3368: new FuncCall(new FullyQualified('count'), [new Arg($expr->var)]),
3369: )->invalidateExpression(
3370: new FuncCall(new FullyQualified('sizeof'), [new Arg($expr->var)]),
3371: )->invalidateExpression(
3372: new FuncCall(new Name('count'), [new Arg($expr->var)]),
3373: )->invalidateExpression(
3374: new FuncCall(new Name('sizeof'), [new Arg($expr->var)]),
3375: );
3376:
3377: if ($expr->var instanceof Expr\ArrayDimFetch && $expr->var->dim !== null) {
3378: $scope = $scope->assignExpression(
3379: $expr->var->var,
3380: $this->getType($expr->var->var)->setOffsetValueType(
3381: $scope->getType($expr->var->dim),
3382: $scope->getScopeStateType($expr->var),
3383: ),
3384: $this->getNativeType($expr->var->var)->setOffsetValueType(
3385: $scope->getNativeType($expr->var->dim),
3386: $scope->getScopeStateNativeType($expr->var),
3387: ),
3388: );
3389: }
3390: }
3391:
3392: return $scope->invalidateExpression($expr);
3393: }
3394:
3395: /**
3396: * A narrowable expression's current type as this scope sees it, derived
3397: * from tracked state (recursing into operands via reflection/offset reads)
3398: * - never by processing the node. The flavour follows the scope: a
3399: * native-promoted scope answers native types. Non-narrowable expressions
3400: * (calls, constants) fall back to getType().
3401: */
3402: public function getStateType(Expr $expr): Type
3403: {
3404: return $this->resolveScopeStateType($expr, $this->nativeTypesPromoted);
3405: }
3406:
3407: private function getScopeStateType(Expr $expr): Type
3408: {
3409: return $this->resolveScopeStateType($expr, false);
3410: }
3411:
3412: private function getScopeStateNativeType(Expr $expr): Type
3413: {
3414: return $this->resolveScopeStateType($expr, true);
3415: }
3416:
3417: /**
3418: * Reads a narrowable expression's current type from the scope's tracked
3419: * state (recursing into its operands), instead of routing through the stored
3420: * ExpressionResult callbacks - so it reflects narrowings and assignments
3421: * applied to this scope rather than the expression's original evaluation
3422: * point (where Variable callbacks would read their captured beforeScope).
3423: */
3424: private function resolveScopeStateType(Expr $expr, bool $native): Type
3425: {
3426: if (!$expr instanceof Variable && $this->hasExpressionType($expr)->yes()) {
3427: // mirror resolveType()'s tracked-holder lookup without pricing the
3428: // node - the tracked type IS scope state (the extension hook is
3429: // deliberately skipped, like the getVariableType() read below)
3430: $askScope = $native ? $this->doNotTreatPhpDocTypesAsCertain() : $this;
3431: $trackedType = ScopeOps::expressionTypeByKey($askScope, $expr, $askScope->getNodeKey($expr));
3432: if ($trackedType !== null) {
3433: return TypeUtils::resolveLateResolvableTypes($trackedType);
3434: }
3435:
3436: return $native ? $this->getNativeType($expr) : $this->getType($expr);
3437: }
3438:
3439: if ($expr instanceof Variable && is_string($expr->name)) {
3440: $scope = $native ? $this->doNotTreatPhpDocTypesAsCertain() : $this;
3441:
3442: return $scope->hasVariableType($expr->name)->no() ? new ErrorType() : $scope->getVariableType($expr->name);
3443: }
3444:
3445: if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) {
3446: $varStateType = $this->resolveScopeStateType($expr->var, $native);
3447: if ($varStateType instanceof NeverType) {
3448: // real pricing of an offset read on never yields ErrorType (a
3449: // benevolent mixed), never NeverType - mirror it, or a narrowing
3450: // applied in a dead branch intersects its type against never and
3451: // loses it (e.g. is_object($x[0]) no longer tracks $x[0] as object,
3452: // silencing rules that read the narrowed type)
3453: return new ErrorType();
3454: }
3455:
3456: return $varStateType->getOffsetValueType($this->resolveScopeStateType($expr->dim, $native));
3457: }
3458:
3459: if ($expr instanceof PropertyFetch && $expr->name instanceof Identifier) {
3460: $propertyReflection = $this->getInstancePropertyReflection(
3461: $this->resolveScopeStateType($expr->var, $native),
3462: $expr->name->toString(),
3463: );
3464: if ($propertyReflection === null) {
3465: return new ErrorType();
3466: }
3467:
3468: if ($native) {
3469: return $propertyReflection->hasNativeType() ? $propertyReflection->getNativeType() : new MixedType();
3470: }
3471:
3472: return $propertyReflection->getReadableType();
3473: }
3474:
3475: if ($expr instanceof Expr\StaticPropertyFetch && $expr->name instanceof Node\VarLikeIdentifier) {
3476: $fetchedOnType = $expr->class instanceof Name
3477: ? $this->resolveTypeByName($expr->class)
3478: : TypeCombinator::removeNull($this->resolveScopeStateType($expr->class, $native))->getObjectTypeOrClassStringObjectType();
3479: $propertyReflection = $this->getStaticPropertyReflection($fetchedOnType, $expr->name->toString());
3480: if ($propertyReflection === null) {
3481: return new ErrorType();
3482: }
3483:
3484: if ($native) {
3485: return $propertyReflection->hasNativeType() ? $propertyReflection->getNativeType() : new MixedType();
3486: }
3487:
3488: return $propertyReflection->getReadableType();
3489: }
3490:
3491: // a nullsafe link of a chain being ensured non-null ahead of its walk
3492: // (isset()/empty()/?? over `$a?->b()?->c`): its state is the plain
3493: // link's state on the receiver's state, plus the short-circuit null -
3494: // the same reflection-derived read as the plain fetch/call arms below,
3495: // never a walk of the still-unprocessed node
3496: if ($expr instanceof Expr\NullsafePropertyFetch && $expr->name instanceof Identifier) {
3497: return TypeCombinator::addNull($this->resolveScopeStateType(new PropertyFetch($expr->var, $expr->name), $native));
3498: }
3499: if (
3500: $expr instanceof Expr\NullsafeMethodCall
3501: && $expr->name instanceof Identifier
3502: && !$expr->isFirstClassCallable()
3503: && $expr->getArgs() === []
3504: ) {
3505: $attributes = $expr->getAttributes();
3506: unset($attributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]);
3507: return TypeCombinator::addNull($this->resolveScopeStateType(new Expr\MethodCall($expr->var, $expr->name, attributes: $attributes), $native));
3508: }
3509:
3510: // an argument-less instance call - the shape @phpstan-assert subjects
3511: // take (synthetic nodes built fresh from the assert tag, never stored):
3512: // its declared return type on the receiver's state is the narrowing
3513: // base, derived from reflection instead of walking the synthetic node.
3514: // A call the walk did store answers from that result: the declared
3515: // return type lacks what the walk resolved against the arguments (a
3516: // conditional return type, a template, a dynamic return type extension)
3517: if (
3518: $expr instanceof Expr\MethodCall
3519: && $expr->name instanceof Identifier
3520: && !$expr->isFirstClassCallable()
3521: && $expr->getArgs() === []
3522: ) {
3523: $storage = $this->services->expressionResultStorageStack->getCurrent();
3524: if ($storage !== null && $storage->findExpressionResult($expr) !== null) {
3525: return $native ? $this->getNativeType($expr) : $this->getType($expr);
3526: }
3527:
3528: $methodReflection = $this->getMethodReflection(
3529: $this->resolveScopeStateType($expr->var, $native),
3530: $expr->name->toString(),
3531: );
3532: if ($methodReflection === null) {
3533: return new ErrorType();
3534: }
3535:
3536: // resolved against the (empty) argument list so a template inferred
3537: // from an omitted parameter's default resolves the way a walk
3538: // resolves it (Collection::first()'s TFirstDefault -> null)
3539: $variant = ParametersAcceptorSelector::selectFromArgs($this, [], $methodReflection->getVariants(), $methodReflection->getNamedArgumentsVariants());
3540:
3541: return $native && $variant instanceof ExtendedParametersAcceptor ? $variant->getNativeReturnType() : TemplateArgumentFrame::returnTypeOfCall($variant, $this, $expr, true);
3542: }
3543:
3544: // position-independent constant expressions (isset()/?? dimensions and
3545: // narrowing subjects like self::KEY) are priced without walking the node
3546: if (
3547: $expr instanceof Node\Scalar\String_
3548: || $expr instanceof Node\Scalar\Int_
3549: || $expr instanceof Node\Scalar\Float_
3550: || ($expr instanceof Expr\ClassConstFetch && $expr->class instanceof Name && $expr->name instanceof Identifier)
3551: || $expr instanceof ConstFetch
3552: ) {
3553: return $this->services->initializerExprTypeResolver->getType($expr, InitializerExprContext::fromScope($this));
3554: }
3555:
3556: // genuinely non-narrowed expressions (calls, ...) have no
3557: // variable-callback hazard, so read them normally.
3558: return $native ? $this->getNativeType($expr) : $this->getType($expr);
3559: }
3560:
3561: public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType, TrinaryLogic $certainty): self
3562: {
3563: if ($this->isSpecifyExpressionTypeNoop($expr, $type)) {
3564: return $this;
3565: }
3566:
3567: $scope = $this->openSpecificationScope();
3568: $scope->specifyExpressionTypeInPlace($expr, $type, $nativeType, $certainty);
3569:
3570: return $scope;
3571: }
3572:
3573: /** An unpublished copy of this scope that in-place specification may mutate. */
3574: private function openSpecificationScope(): self
3575: {
3576: return $this->scopeFactory->create(
3577: $this->context,
3578: $this->isDeclareStrictTypes(),
3579: $this->getFunction(),
3580: $this->getNamespace(),
3581: $this->expressionTypes,
3582: $this->nativeExpressionTypes,
3583: $this->conditionalExpressions,
3584: $this->inClosureBindScopeClasses,
3585: $this->anonymousFunctionReflection,
3586: $this->inFirstLevelStatement,
3587: $this->currentlyAssignedExpressions,
3588: $this->currentlyAllowedUndefinedExpressions,
3589: $this->inFunctionCallsStack,
3590: $this->afterExtractCall,
3591: $this->parentScope,
3592: $this->nativeTypesPromoted,
3593: $this->templateArgumentFrame,
3594: $this->templateArgumentConstraints,
3595: );
3596: }
3597:
3598: private function isSpecifyExpressionTypeNoop(Expr $expr, Type $type): bool
3599: {
3600: if ($expr instanceof Scalar) {
3601: return true;
3602: }
3603:
3604: if ($expr instanceof ConstFetch) {
3605: $loweredConstName = strtolower($expr->name->toString());
3606: if (in_array($loweredConstName, ['true', 'false', 'null'], true)) {
3607: return true;
3608: }
3609: }
3610:
3611: if ($expr instanceof FuncCall && $expr->name instanceof Name && $type->isFalse()->yes()) {
3612: $functionName = $this->services->reflectionProvider->resolveFunctionName($expr->name, $this);
3613: if ($functionName !== null && in_array(strtolower($functionName), [
3614: 'is_dir',
3615: 'is_file',
3616: 'file_exists',
3617: ], true)) {
3618: return true;
3619: }
3620: }
3621:
3622: return false;
3623: }
3624:
3625: /**
3626: * The body of specifyExpressionType() writing straight into this scope's
3627: * holder maps - only to be called on an unpublished scope (see
3628: * openSpecificationScope()). Batching callers avoid one whole-map copy and
3629: * scope construction per specification (and per array-dim level).
3630: */
3631: private function specifyExpressionTypeInPlace(Expr $expr, Type $type, Type $nativeType, TrinaryLogic $certainty): void
3632: {
3633: if ($this->isSpecifyExpressionTypeNoop($expr, $type)) {
3634: return;
3635: }
3636:
3637: if (
3638: $expr instanceof Expr\ArrayDimFetch
3639: && $expr->dim !== null
3640: && !$expr->dim instanceof Expr\PreInc
3641: && !$expr->dim instanceof Expr\PreDec
3642: && !$expr->dim instanceof Expr\PostDec
3643: && !$expr->dim instanceof Expr\PostInc
3644: ) {
3645: $dimType = $this->getScopeStateType($expr->dim)->toArrayKey();
3646: if ($dimType->isInteger()->yes() || $dimType->isString()->yes()) {
3647: $exprVarType = $this->getScopeStateType($expr->var);
3648: $isArray = $exprVarType->isArray();
3649: if (!$exprVarType instanceof MixedType && !$isArray->no()) {
3650: $varType = $exprVarType;
3651: if (!$isArray->yes()) {
3652: if ($dimType->isInteger()->yes()) {
3653: $varType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::intOffsetAccessibleType());
3654: } else {
3655: $varType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::generalOffsetAccessibleType());
3656: }
3657: }
3658:
3659: if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) {
3660: if (!$this->isComplexUnionType($varType)) {
3661: $varType = TypeCombinator::intersect(
3662: $varType,
3663: new HasOffsetValueType($dimType, $type),
3664: );
3665: }
3666: }
3667:
3668: $this->specifyExpressionTypeInPlace(
3669: $expr->var,
3670: $varType,
3671: $this->getScopeStateNativeType($expr->var),
3672: $certainty,
3673: );
3674: }
3675: }
3676: }
3677:
3678: if ($certainty->no()) {
3679: throw new ShouldNotHappenException();
3680: }
3681:
3682: $exprString = $this->getNodeKey($expr);
3683: $this->expressionTypes[$exprString] = new ExpressionTypeHolder($expr, $type, $certainty);
3684: $this->nativeExpressionTypes[$exprString] = new ExpressionTypeHolder($expr, $nativeType, $certainty);
3685:
3686: if (!($expr instanceof AlwaysRememberedExpr)) {
3687: return;
3688: }
3689:
3690: $this->specifyExpressionTypeInPlace($expr->expr, $type, $nativeType, $certainty);
3691: }
3692:
3693: public function assignExpression(Expr $expr, Type $type, Type $nativeType): self
3694: {
3695: $scope = $this;
3696: if ($expr instanceof PropertyFetch) {
3697: $scope = $this->invalidateExpression($expr)
3698: ->invalidateMethodsOnExpression($expr->var);
3699: } elseif ($expr instanceof Expr\StaticPropertyFetch) {
3700: $scope = $this->invalidateExpression($expr);
3701: } elseif ($expr instanceof Variable) {
3702: $scope = $this->invalidateExpression($expr);
3703: }
3704:
3705: return $scope->specifyExpressionType($expr, $type, $nativeType, TrinaryLogic::createYes());
3706: }
3707:
3708: public function assignInitializedProperty(Type $fetchedOnType, string $propertyName): self
3709: {
3710: if (!$this->isInClass()) {
3711: return $this;
3712: }
3713:
3714: if (TypeUtils::findThisType($fetchedOnType) === null) {
3715: return $this;
3716: }
3717:
3718: $propertyReflection = $this->getInstancePropertyReflection($fetchedOnType, $propertyName);
3719: if ($propertyReflection === null) {
3720: return $this;
3721: }
3722: $declaringClass = $propertyReflection->getDeclaringClass();
3723: if ($this->getClassReflection()->getName() !== $declaringClass->getName()) {
3724: return $this;
3725: }
3726: if (!$declaringClass->hasNativeProperty($propertyName)) {
3727: return $this;
3728: }
3729:
3730: $scope = $this->assignExpression(new PropertyInitializationExpr($propertyName), new MixedType(), new MixedType());
3731:
3732: $function = $scope->getFunction();
3733: if (
3734: $function instanceof MethodReflection
3735: && strtolower($function->getName()) === '__clone'
3736: && $scope->services->phpVersion->supportsReadonlyPropertyReinitializationOnClone()
3737: ) {
3738: $scope = $scope->assignExpression(new CloneReinitializationExpr($propertyName), new MixedType(), new MixedType());
3739: }
3740:
3741: return $scope;
3742: }
3743:
3744: /**
3745: * @param bool $keepPropertyFetches Keeps property fetches on the invalidated expression
3746: * (like '$this->foo') - for callees that never receive
3747: * the object, like static methods and static closures.
3748: */
3749: public function invalidateExpression(Expr $expressionToInvalidate, bool $requireMoreCharacters = false, ?ClassReflection $invalidatingClass = null, bool $keepPropertyFetches = false): self
3750: {
3751: $exprStringToInvalidate = $this->getNodeKey($expressionToInvalidate);
3752:
3753: $result = ScopeOps::invalidateExpressionEntries(
3754: $this,
3755: $this->services->exprPrinter,
3756: $exprStringToInvalidate,
3757: $expressionToInvalidate,
3758: $requireMoreCharacters,
3759: $invalidatingClass,
3760: $this->expressionTypes,
3761: $this->nativeExpressionTypes,
3762: $this->conditionalExpressions,
3763: $keepPropertyFetches,
3764: );
3765: if ($result === null) {
3766: return $this;
3767: }
3768:
3769: /** @var static */
3770: return ScopeOps::scopeWith(
3771: $this,
3772: $result[0],
3773: $result[1],
3774: $result[2],
3775: $this->currentlyAssignedExpressions,
3776: $this->currentlyAllowedUndefinedExpressions,
3777: [],
3778: $this->inFirstLevelStatement,
3779: $this->afterExtractCall,
3780: );
3781: }
3782:
3783: /** @internal called by ScopeOps */
3784: public function isPrivatePropertyOfDifferentClass(Expr $expr, ClassReflection $invalidatingClass): bool
3785: {
3786: if ($expr instanceof Expr\StaticPropertyFetch || $expr instanceof PropertyFetch) {
3787: $propertyReflection = $this->services->propertyReflectionFinder->findPropertyReflectionFromNode($expr, $this);
3788: if ($propertyReflection === null) {
3789: return false;
3790: }
3791: if (!$propertyReflection->isPrivate()) {
3792: return false;
3793: }
3794: return $propertyReflection->getDeclaringClass()->getName() !== $invalidatingClass->getName();
3795: }
3796:
3797: return false;
3798: }
3799:
3800: private function invalidateMethodsOnExpression(Expr $expressionToInvalidate): self
3801: {
3802: $result = ScopeOps::invalidateMethodsOnExpression(
3803: $this->services->exprPrinter,
3804: $this->getNodeKey($expressionToInvalidate),
3805: $this->expressionTypes,
3806: $this->nativeExpressionTypes,
3807: );
3808: if ($result === null) {
3809: return $this;
3810: }
3811:
3812: /** @var static */
3813: return ScopeOps::scopeWith(
3814: $this,
3815: $result[0],
3816: $result[1],
3817: $this->conditionalExpressions,
3818: $this->currentlyAssignedExpressions,
3819: $this->currentlyAllowedUndefinedExpressions,
3820: [],
3821: $this->inFirstLevelStatement,
3822: $this->afterExtractCall,
3823: );
3824: }
3825:
3826: /**
3827: * Certainty change for applySpecifiedTypes():
3828: * it keeps the type already held for the expression instead of re-reading it
3829: * via getType(). getType() only reports the type of Yes-certainty holders, so
3830: * for a maybe-defined variable it broadens to the original type - which would
3831: * overwrite a co-applied narrowing (e.g. isset's $a -> null in the else branch).
3832: */
3833: private function setExpressionCertaintyKeepingType(Expr $expr, TrinaryLogic $certainty): self
3834: {
3835: $exprString = $this->getNodeKey($expr);
3836: if (!array_key_exists($exprString, $this->expressionTypes)) {
3837: throw new ShouldNotHappenException();
3838: }
3839:
3840: $exprType = $this->expressionTypes[$exprString]->getType();
3841: $nativeType = array_key_exists($exprString, $this->nativeExpressionTypes)
3842: ? $this->nativeExpressionTypes[$exprString]->getType()
3843: : $exprType;
3844:
3845: return $this->specifyExpressionType(
3846: $expr,
3847: $exprType,
3848: $nativeType,
3849: $certainty,
3850: );
3851: }
3852:
3853: /**
3854: * Returns true when the type is a large union with intersection
3855: * members that carry HasOffsetValueType — a sign of combinatorial
3856: * growth from successive array|object offset access patterns.
3857: * Operating on such types is expensive and should be skipped.
3858: */
3859: private function isComplexUnionType(Type $type): bool
3860: {
3861: if (!$type instanceof UnionType) {
3862: return false;
3863: }
3864: $types = $type->getTypes();
3865: if (count($types) <= self::COMPLEX_UNION_TYPE_MEMBER_LIMIT) {
3866: return false;
3867: }
3868: foreach ($types as $member) {
3869: if (!$member instanceof IntersectionType) {
3870: continue;
3871: }
3872: foreach ($member->getTypes() as $innerType) {
3873: if ($innerType instanceof HasOffsetValueType) {
3874: return true;
3875: }
3876: }
3877: }
3878: return false;
3879: }
3880:
3881: public function addTypeToExpression(Expr $expr, Type $type): self
3882: {
3883: $originalExprType = $this->getScopeStateType($expr);
3884: if ($this->isComplexUnionType($originalExprType)) {
3885: return $this;
3886: }
3887:
3888: $nativeType = $this->getScopeStateNativeType($expr);
3889:
3890: if ($originalExprType->equals($nativeType)) {
3891: $newType = TypeCombinator::intersect($type, $originalExprType);
3892: return $this->specifyExpressionType($expr, $newType, $newType, TrinaryLogic::createYes());
3893: }
3894:
3895: return $this->specifyExpressionType(
3896: $expr,
3897: TypeCombinator::intersect($type, $originalExprType),
3898: TypeCombinator::intersect($type, $nativeType),
3899: TrinaryLogic::createYes(),
3900: );
3901: }
3902:
3903: public function removeTypeFromExpression(Expr $expr, Type $typeToRemove): self
3904: {
3905: if ($typeToRemove instanceof NeverType) {
3906: return $this;
3907: }
3908:
3909: $exprType = $this->getScopeStateType($expr);
3910: if ($exprType instanceof NeverType) {
3911: return $this;
3912: }
3913:
3914: if ($this->isComplexUnionType($exprType)) {
3915: return $this;
3916: }
3917:
3918: return $this->specifyExpressionType(
3919: $expr,
3920: TypeCombinator::remove($exprType, $typeToRemove),
3921: TypeCombinator::remove($this->getScopeStateNativeType($expr), $typeToRemove),
3922: TrinaryLogic::createYes(),
3923: );
3924: }
3925:
3926: /**
3927: * @api
3928: */
3929: public function filterByTruthyValue(Expr $expr): self
3930: {
3931: $specifiedTypes = $this->services->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createTruthy());
3932: if ($specifiedTypes->isEquality() && $this->getType($expr)->isBoolean()->yes()) {
3933: $specifiedTypes = $specifiedTypes->unionWith(
3934: $this->services->typeSpecifier->create($expr, new ConstantBooleanType(true), TypeSpecifierContext::createTrue(), $this),
3935: );
3936: }
3937:
3938: return $this->applySpecifiedTypes($specifiedTypes);
3939: }
3940:
3941: /**
3942: * @api
3943: */
3944: public function filterByFalseyValue(Expr $expr): self
3945: {
3946: $specifiedTypes = $this->services->typeSpecifier->specifyTypesInCondition($this, $expr, TypeSpecifierContext::createFalsey());
3947: if ($specifiedTypes->isEquality() && $this->getType($expr)->isBoolean()->yes()) {
3948: $specifiedTypes = $specifiedTypes->unionWith(
3949: $this->services->typeSpecifier->create($expr, new ConstantBooleanType(false), TypeSpecifierContext::createTrue(), $this),
3950: );
3951: }
3952:
3953: return $this->applySpecifiedTypes($specifiedTypes);
3954: }
3955:
3956: /**
3957: * Applies computed narrowing to this scope.
3958: *
3959: * The types inside SpecifiedTypes were already computed from ExpressionResults
3960: * by the specifyTypesCallback of an ExprHandler. This method must never call
3961: * Scope::getType() - it only combines the given types with already-tracked
3962: * expression type holders.
3963: *
3964: * @return static
3965: */
3966: public function applySpecifiedTypes(SpecifiedTypes $specifiedTypes): self
3967: {
3968: // deferred augments see this scope's pre-application state - the
3969: // application point of the narrowing; their entries join this batch
3970: $pendingAugments = $specifiedTypes->getDeferredAugments();
3971: while ($pendingAugments !== []) {
3972: $augment = array_shift($pendingAugments);
3973: $augmentTypes = $augment->evaluate($this);
3974: if ($augmentTypes === null) {
3975: continue;
3976: }
3977:
3978: foreach ($augmentTypes->getDeferredAugments() as $nestedAugment) {
3979: $pendingAugments[] = $nestedAugment;
3980: }
3981: $specifiedTypes = $specifiedTypes->unionWith($augmentTypes);
3982: }
3983:
3984: $typeSpecifications = [];
3985: foreach ($specifiedTypes->getSureTypes() as $exprString => [$expr, $type]) {
3986: if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) {
3987: continue;
3988: }
3989: $typeSpecifications[] = [
3990: 'sure' => true,
3991: 'exprString' => (string) $exprString,
3992: 'expr' => $expr,
3993: 'type' => $type,
3994: ];
3995: }
3996: foreach ($specifiedTypes->getSureNotTypes() as $exprString => [$expr, $type]) {
3997: if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) {
3998: continue;
3999: }
4000: $typeSpecifications[] = [
4001: 'sure' => false,
4002: 'exprString' => (string) $exprString,
4003: 'expr' => $expr,
4004: 'type' => $type,
4005: ];
4006: }
4007: foreach ($specifiedTypes->getAlternativeTypes() as $exprString => [$expr, $terms]) {
4008: if ($expr instanceof Node\Scalar || $expr instanceof Array_ || $expr instanceof Expr\UnaryMinus && $expr->expr instanceof Node\Scalar) {
4009: continue;
4010: }
4011: $typeSpecifications[] = [
4012: 'sure' => true,
4013: 'exprString' => (string) $exprString,
4014: 'expr' => $expr,
4015: 'terms' => $terms,
4016: ];
4017: }
4018:
4019: usort($typeSpecifications, static function (array $a, array $b): int {
4020: $length = strlen($a['exprString']) - strlen($b['exprString']);
4021: if ($length !== 0) {
4022: return $length;
4023: }
4024:
4025: return $b['sure'] - $a['sure']; // @phpstan-ignore minus.leftNonNumeric, minus.rightNonNumeric
4026: });
4027:
4028: $scope = $this;
4029: // one unpublished working copy takes all in-place specifications of the
4030: // batch; operations that go through other scope derivations publish it
4031: // and a fresh copy opens on the next specification
4032: $scopeIsWorkingCopy = false;
4033: $specifiedExpressions = [];
4034: foreach ($typeSpecifications as $typeSpecification) {
4035: $expr = $typeSpecification['expr'];
4036: $exprString = $typeSpecification['exprString'];
4037:
4038: if ($expr instanceof IssetExpr) {
4039: $issetExpr = $expr;
4040: $expr = $issetExpr->getExpr();
4041:
4042: if ($typeSpecification['sure']) {
4043: $scope = $scope->setExpressionCertaintyKeepingType(
4044: $expr,
4045: TrinaryLogic::createMaybe(),
4046: );
4047: } else {
4048: $scope = $scope->unsetExpression($expr);
4049: }
4050: $scopeIsWorkingCopy = false;
4051:
4052: continue;
4053: }
4054:
4055: if (
4056: !$typeSpecification['sure']
4057: && $expr instanceof Variable && is_string($expr->name)
4058: && $scope->hasVariableType($expr->name)->no()
4059: ) {
4060: // removing type from a certainly-undefined variable cannot make
4061: // it defined; a sure specification (e.g. is_string($a)) still can -
4062: // the condition can only hold for a defined variable
4063: continue;
4064: }
4065:
4066: // only Yes-certainty holders hold the current type of the expression -
4067: // a Maybe-certainty holder holds the when-defined type (e.g. after
4068: // merging a branch where the expression was never assigned), which
4069: // the certainty-aware Scope::getType() of the old world never returned
4070: $trackedType = null;
4071: $trackedNativeType = null;
4072: if (
4073: array_key_exists($exprString, $scope->expressionTypes)
4074: && $scope->expressionTypes[$exprString]->getCertainty()->yes()
4075: ) {
4076: $trackedType = $scope->expressionTypes[$exprString]->getType();
4077: }
4078: if (
4079: array_key_exists($exprString, $scope->nativeExpressionTypes)
4080: && $scope->nativeExpressionTypes[$exprString]->getCertainty()->yes()
4081: ) {
4082: $trackedNativeType = $scope->nativeExpressionTypes[$exprString]->getType();
4083: }
4084: if ($trackedType === null) {
4085: $currentTypes = $scope->getCurrentTypesOfSpecifiedExpr($expr);
4086: if ($currentTypes !== null) {
4087: if ($scope->isComplexUnionType($currentTypes[0])) {
4088: continue;
4089: }
4090:
4091: $trackedType = $currentTypes[0];
4092: $trackedNativeType ??= $currentTypes[1];
4093: }
4094: } elseif (!$specifiedTypes->shouldOverwrite() && $scope->isComplexUnionType($trackedType)) {
4095: // mirrors addTypeToExpression()/removeTypeFromExpression(): narrowing
4096: // a combinatorially-grown offset union doubles it with every isset()-
4097: // style check and gets skipped (overwrites assign, they never narrow)
4098: continue;
4099: }
4100:
4101: if (isset($typeSpecification['terms'])) {
4102: // an alternative-form entry: the union over its terms of
4103: // `(sure ?? current) minus subtract`, evaluated here at the
4104: // application point - the deferred descendant of the old
4105: // SpecifiedTypes::normalize()
4106: $evaluate = static function (?Type $current) use ($typeSpecification): ?Type {
4107: $parts = [];
4108: foreach ($typeSpecification['terms'] as [$sure, $subtract]) {
4109: $base = $sure ?? $current;
4110: if ($base === null) {
4111: return null;
4112: }
4113: $parts[] = $subtract !== null ? TypeCombinator::remove($base, $subtract) : $base;
4114: }
4115:
4116: return TypeCombinator::union(...$parts);
4117: };
4118: $evaluated = $evaluate($trackedType);
4119: if ($evaluated === null) {
4120: // a current-type-dependent term with no known current type -
4121: // nothing sound to specify (mirrors the sure-not behaviour)
4122: continue;
4123: }
4124: $evaluatedNative = $evaluate($trackedNativeType ?? $trackedType) ?? $evaluated;
4125:
4126: $newType = $trackedType !== null ? TypeCombinator::intersect($evaluated, $trackedType) : $evaluated;
4127: $newNativeType = $trackedNativeType !== null ? TypeCombinator::intersect($evaluatedNative, $trackedNativeType) : $evaluatedNative;
4128: if (!$this->isSpecifyExpressionTypeNoop($expr, $newType)) {
4129: if (!$scopeIsWorkingCopy) {
4130: $scope = $scope->openSpecificationScope();
4131: $scopeIsWorkingCopy = true;
4132: }
4133: $scope->specifyExpressionTypeInPlace($expr, $newType, $newNativeType, TrinaryLogic::createYes());
4134: }
4135:
4136: $holderType = array_key_exists($exprString, $scope->expressionTypes)
4137: ? $scope->expressionTypes[$exprString]->getType()
4138: : $newType;
4139: $specifiedExpressions[$exprString] = ExpressionTypeHolder::createYes($expr, $holderType);
4140: continue;
4141: }
4142:
4143: $type = $typeSpecification['type'];
4144: if ($typeSpecification['sure']) {
4145: if ($specifiedTypes->shouldOverwrite()) {
4146: $scope = $scope->overwriteExpression($expr, $type, $type);
4147: $scopeIsWorkingCopy = false;
4148: } else {
4149: $newType = $trackedType !== null ? TypeCombinator::intersect($type, $trackedType) : $type;
4150: $newNativeType = $trackedNativeType !== null ? TypeCombinator::intersect($type, $trackedNativeType) : $type;
4151: if (!$this->isSpecifyExpressionTypeNoop($expr, $newType)) {
4152: if (!$scopeIsWorkingCopy) {
4153: $scope = $scope->openSpecificationScope();
4154: $scopeIsWorkingCopy = true;
4155: }
4156: $scope->specifyExpressionTypeInPlace($expr, $newType, $newNativeType, TrinaryLogic::createYes());
4157: }
4158: }
4159: } else {
4160: if ($type instanceof NeverType || $trackedType instanceof NeverType) {
4161: continue;
4162: }
4163: $newType = $trackedType !== null ? TypeCombinator::remove($trackedType, $type) : null;
4164: if ($newType === null) {
4165: // the expression is not tracked - there is nothing to subtract from
4166: continue;
4167: }
4168: $newNativeType = $trackedNativeType !== null ? TypeCombinator::remove($trackedNativeType, $type) : $newType;
4169: if (!$this->isSpecifyExpressionTypeNoop($expr, $newType)) {
4170: if (!$scopeIsWorkingCopy) {
4171: $scope = $scope->openSpecificationScope();
4172: $scopeIsWorkingCopy = true;
4173: }
4174: $scope->specifyExpressionTypeInPlace($expr, $newType, $newNativeType, TrinaryLogic::createYes());
4175: }
4176: }
4177:
4178: $holderType = array_key_exists($exprString, $scope->expressionTypes)
4179: ? $scope->expressionTypes[$exprString]->getType()
4180: : $type;
4181: $specifiedExpressions[$exprString] = ExpressionTypeHolder::createYes($expr, $holderType);
4182: }
4183:
4184: $scope = $scope->processConditionalExpressionsAfterSpecifying($specifiedExpressions);
4185:
4186: $newConditionalExpressionHolders = $specifiedTypes->getNewConditionalExpressionHolders();
4187: foreach ($specifiedTypes->getConditionalExpressionHolderRecipes() as $recipe) {
4188: // the recipes' state-dependent math runs here, against this scope's
4189: // pre-application state - the application point of the narrowing
4190: foreach ($recipe->evaluate($this) as $exprString => $recipeHolders) {
4191: foreach ($recipeHolders as $key => $holder) {
4192: $newConditionalExpressionHolders[$exprString][$key] = $holder;
4193: }
4194: }
4195: }
4196:
4197: /** @var static */
4198: return $scope->scopeFactory->create(
4199: $scope->context,
4200: $scope->isDeclareStrictTypes(),
4201: $scope->getFunction(),
4202: $scope->getNamespace(),
4203: $scope->expressionTypes,
4204: $scope->nativeExpressionTypes,
4205: $this->mergeConditionalExpressions($newConditionalExpressionHolders, $scope->conditionalExpressions),
4206: $scope->inClosureBindScopeClasses,
4207: $scope->anonymousFunctionReflection,
4208: $scope->inFirstLevelStatement,
4209: $scope->currentlyAssignedExpressions,
4210: $scope->currentlyAllowedUndefinedExpressions,
4211: $scope->inFunctionCallsStack,
4212: $scope->afterExtractCall,
4213: $scope->parentScope,
4214: $scope->nativeTypesPromoted,
4215: $scope->templateArgumentFrame,
4216: $scope->templateArgumentConstraints,
4217: );
4218: }
4219:
4220: /**
4221: * Matches already-registered conditional expressions against the just-specified
4222: * expression type holders and applies the matching consequences.
4223: *
4224: * Mutates and returns $this - only to be called on an intermediate scope
4225: * that is about to be rebuilt through the scope factory.
4226: *
4227: * @param array<string, ExpressionTypeHolder> $specifiedExpressions
4228: */
4229: private function processConditionalExpressionsAfterSpecifying(array $specifiedExpressions): self
4230: {
4231: $scope = $this;
4232: [$conditions] = ScopeOps::matchConditionalExpressions($scope->conditionalExpressions, $specifiedExpressions);
4233:
4234: foreach ($conditions as $conditionalExprString => $expressions) {
4235: $certainty = TrinaryLogic::lazyExtremeIdentity($expressions, static fn (ConditionalExpressionHolder $holder) => $holder->getTypeHolder()->getCertainty());
4236: if ($certainty->no()) {
4237: unset($scope->expressionTypes[$conditionalExprString]);
4238: } else {
4239: if (array_key_exists($conditionalExprString, $scope->expressionTypes)) {
4240: $type = $expressions[0]->getTypeHolder()->getType();
4241: for ($i = 1, $count = count($expressions); $i < $count; $i++) {
4242: $type = TypeCombinator::intersect($type, $expressions[$i]->getTypeHolder()->getType());
4243: }
4244:
4245: $scope->expressionTypes[$conditionalExprString] = new ExpressionTypeHolder(
4246: $scope->expressionTypes[$conditionalExprString]->getExpr(),
4247: TypeCombinator::intersect($scope->expressionTypes[$conditionalExprString]->getType(), $type),
4248: TrinaryLogic::maxMin($scope->expressionTypes[$conditionalExprString]->getCertainty(), $certainty),
4249: );
4250: } else {
4251: $scope->expressionTypes[$conditionalExprString] = $expressions[0]->getTypeHolder();
4252: }
4253: }
4254: }
4255:
4256: return $scope;
4257: }
4258:
4259: /**
4260: * @return array<string, ConditionalExpressionHolder[]>
4261: */
4262: public function getConditionalExpressions(): array
4263: {
4264: return $this->conditionalExpressions;
4265: }
4266:
4267: /**
4268: * @param ConditionalExpressionHolder[] $conditionalExpressionHolders
4269: */
4270: public function addConditionalExpressions(string $exprString, array $conditionalExpressionHolders): self
4271: {
4272: $conditionalExpressions = $this->conditionalExpressions;
4273: // Merge rather than overwrite: multiple independent holders can target the same
4274: // expression (e.g. `$xIsA = $x instanceof A && $y instanceof A` stores a holder
4275: // for `$x` keyed on `$xIsA`; later `$yIsA = $y instanceof A && $x instanceof A`
4276: // stores another holder for the same target `$x` keyed on `$yIsA`). Replacing
4277: // the existing entry here would throw away the earlier binding, breaking
4278: // narrowing inside later `if ($xIsA) { … }` inside `if ($xIsA || $yIsA)`.
4279: // Holder keys (`getKey()`) disambiguate identical entries so we still dedupe.
4280: $existing = $conditionalExpressions[$exprString] ?? [];
4281: foreach ($conditionalExpressionHolders as $holder) {
4282: $existing[$holder->getKey()] = $holder;
4283: }
4284: $conditionalExpressions[$exprString] = $existing;
4285:
4286: /** @var static */
4287: return ScopeOps::scopeWith(
4288: $this,
4289: $this->expressionTypes,
4290: $this->nativeExpressionTypes,
4291: $conditionalExpressions,
4292: $this->currentlyAssignedExpressions,
4293: $this->currentlyAllowedUndefinedExpressions,
4294: $this->inFunctionCallsStack,
4295: $this->inFirstLevelStatement,
4296: $this->afterExtractCall,
4297: );
4298: }
4299:
4300: public function exitFirstLevelStatements(): self
4301: {
4302: if (!$this->inFirstLevelStatement) {
4303: return $this;
4304: }
4305:
4306: if ($this->scopeOutOfFirstLevelStatement !== null) {
4307: return $this->scopeOutOfFirstLevelStatement;
4308: }
4309:
4310: /** @var static $scope */
4311: $scope = ScopeOps::scopeWith(
4312: $this,
4313: $this->expressionTypes,
4314: $this->nativeExpressionTypes,
4315: $this->conditionalExpressions,
4316: $this->currentlyAssignedExpressions,
4317: $this->currentlyAllowedUndefinedExpressions,
4318: $this->inFunctionCallsStack,
4319: false,
4320: $this->afterExtractCall,
4321: );
4322: $scope->resolvedTypes = $this->resolvedTypes;
4323: $this->scopeOutOfFirstLevelStatement = $scope;
4324:
4325: return $scope;
4326: }
4327:
4328: /** @api */
4329: public function isInFirstLevelStatement(): bool
4330: {
4331: return $this->inFirstLevelStatement;
4332: }
4333:
4334: public function mergeWith(?self $otherScope, bool $preserveVacuousConditionals = false): self
4335: {
4336: $merged = $this->mergeWithVariableState($otherScope, $preserveVacuousConditionals)->addTemplateArgumentConstraints($otherScope !== null ? $otherScope->getTemplateArgumentConstraints() : null);
4337: if (
4338: $otherScope === null
4339: || $merged->templateArgumentConstraints === null
4340: || $merged->templateArgumentFrame === null
4341: || !$merged->templateArgumentFrame->isObservingClosures()
4342: ) {
4343: return $merged;
4344: }
4345:
4346: return $merged->addTemplateArgumentConstraints($merged->collectAbsorbedClosures($this, $otherScope));
4347: }
4348:
4349: /**
4350: * The closures a variable held on one side of a merge and the merged
4351: * variable lost - see ClosureSignatureInference::collectAbsorbed().
4352: */
4353: private function collectAbsorbedClosures(self $ours, self $theirs): ?TemplateArgumentConstraints
4354: {
4355: $constraints = null;
4356: foreach ($this->expressionTypes as $key => $holder) {
4357: foreach ([$ours->expressionTypes[$key] ?? null, $theirs->expressionTypes[$key] ?? null] as $sideHolder) {
4358: if ($sideHolder === null || $sideHolder === $holder || $sideHolder->getType() === $holder->getType()) {
4359: continue;
4360: }
4361: $absorbed = ClosureSignatureInference::collectAbsorbed($sideHolder->getType(), $holder->getType());
4362: if ($absorbed->isEmpty()) {
4363: continue;
4364: }
4365: $constraints = $constraints === null ? $absorbed : $constraints->merge($absorbed);
4366: }
4367: }
4368:
4369: return $constraints;
4370: }
4371:
4372: private function mergeWithVariableState(?self $otherScope, bool $preserveVacuousConditionals = false): self
4373: {
4374: if ($otherScope === null || $this === $otherScope) {
4375: return $this;
4376: }
4377: $ourExpressionTypes = $this->expressionTypes;
4378: $theirExpressionTypes = $otherScope->expressionTypes;
4379:
4380: $differingExpressionKeys = [];
4381: $mergedExpressionTypes = ScopeOps::mergeVariableHolders($ourExpressionTypes, $theirExpressionTypes, $differingExpressionKeys);
4382: $differingExpressionKeys = $this->withoutPreciseClassConstantFetches($differingExpressionKeys, $ourExpressionTypes, $theirExpressionTypes);
4383: $conditionalExpressions = ScopeOps::intersectConditionalExpressions($this->conditionalExpressions, $otherScope->conditionalExpressions);
4384: if ($preserveVacuousConditionals) {
4385: $conditionalExpressions = $this->preserveVacuousConditionalExpressions(
4386: $conditionalExpressions,
4387: $this->conditionalExpressions,
4388: $theirExpressionTypes,
4389: );
4390: $conditionalExpressions = $this->preserveVacuousConditionalExpressions(
4391: $conditionalExpressions,
4392: $otherScope->conditionalExpressions,
4393: $ourExpressionTypes,
4394: );
4395: }
4396: $conditionalExpressions = $this->mergeSameGuardConditionalExpressions(
4397: $conditionalExpressions,
4398: $this->conditionalExpressions,
4399: $otherScope->conditionalExpressions,
4400: );
4401: $conditionalExpressions = ScopeOps::createConditionalExpressions(
4402: $conditionalExpressions,
4403: $ourExpressionTypes,
4404: $theirExpressionTypes,
4405: $mergedExpressionTypes,
4406: $differingExpressionKeys,
4407: );
4408: $conditionalExpressions = ScopeOps::createConditionalExpressions(
4409: $conditionalExpressions,
4410: $theirExpressionTypes,
4411: $ourExpressionTypes,
4412: $mergedExpressionTypes,
4413: $differingExpressionKeys,
4414: );
4415:
4416: [$mergedExpressionTypes, $mergedNativeTypes] = ScopeOps::finishMerge(
4417: $mergedExpressionTypes,
4418: $ourExpressionTypes,
4419: $theirExpressionTypes,
4420: $this->nativeExpressionTypes,
4421: $otherScope->nativeExpressionTypes,
4422: );
4423:
4424: /** @var static */
4425: return ScopeOps::scopeWith(
4426: $this,
4427: $mergedExpressionTypes,
4428: $mergedNativeTypes,
4429: $conditionalExpressions,
4430: [],
4431: [],
4432: [],
4433: $this->inFirstLevelStatement,
4434: $this->afterExtractCall && $otherScope->afterExtractCall,
4435: );
4436: }
4437:
4438: /**
4439: * Drops the keys of class-constant fetches that resolve to their declared value.
4440: *
4441: * A conditional expression records "when the guard holds, this expression had
4442: * that type in the branch the guard selects". Such a record can only ever pay
4443: * off when the expression resolves to something less precise on its own - and
4444: * a class-constant fetch with a statically known class resolves to the exact
4445: * declared value, unless the constant is configured as dynamic. So the record
4446: * is bookkeeping that can never narrow anything, and an expensive one: creating
4447: * it compares the guard against every member of the (potentially very wide)
4448: * merged guard type.
4449: *
4450: * @param array<string, true> $differingExpressionKeys
4451: * @param array<string, ExpressionTypeHolder> $ourExpressionTypes
4452: * @param array<string, ExpressionTypeHolder> $theirExpressionTypes
4453: * @return array<string, true>
4454: */
4455: private function withoutPreciseClassConstantFetches(
4456: array $differingExpressionKeys,
4457: array $ourExpressionTypes,
4458: array $theirExpressionTypes,
4459: ): array
4460: {
4461: foreach (array_keys($differingExpressionKeys) as $exprString) {
4462: $holder = $ourExpressionTypes[$exprString] ?? $theirExpressionTypes[$exprString] ?? null;
4463: if ($holder === null) {
4464: continue;
4465: }
4466:
4467: $expr = $holder->getExpr();
4468: if (
4469: !$expr instanceof ClassConstFetch
4470: || !$expr->class instanceof Name
4471: || !$expr->name instanceof Identifier
4472: ) {
4473: continue;
4474: }
4475:
4476: // static::CONST is late-bound, so which class - and therefore which
4477: // declared value - it resolves to is not known here.
4478: if ($expr->class->toLowerString() === 'static') {
4479: continue;
4480: }
4481:
4482: if ($this->services->constantResolver->isDynamicClassConstant($this->resolveName($expr->class), $expr->name->toString())) {
4483: continue;
4484: }
4485:
4486: unset($differingExpressionKeys[$exprString]);
4487: }
4488:
4489: return $differingExpressionKeys;
4490: }
4491:
4492: /**
4493: * Rescues one-sided conditional holders across an if-merge.
4494: *
4495: * A holder missing from the intersection survives when either some guard's
4496: * recorded type is impossible in the other branch (the holder is vacuously
4497: * true there), or the other branch's flat state for the holder's target
4498: * already satisfies the holder's consequent (subtype with at-least-as-strong
4499: * certainty) - the consequent then holds on both paths under the guard.
4500: *
4501: * @param array<string, ConditionalExpressionHolder[]> $currentConditionalExpressions
4502: * @param array<string, ConditionalExpressionHolder[]> $sourceConditionalExpressions
4503: * @param array<string, ExpressionTypeHolder> $otherExpressionTypes
4504: * @return array<string, ConditionalExpressionHolder[]>
4505: */
4506: private function preserveVacuousConditionalExpressions(
4507: array $currentConditionalExpressions,
4508: array $sourceConditionalExpressions,
4509: array $otherExpressionTypes,
4510: ): array
4511: {
4512: foreach ($sourceConditionalExpressions as $exprString => $holders) {
4513: foreach ($holders as $key => $holder) {
4514: if (isset($currentConditionalExpressions[$exprString][$key])) {
4515: continue;
4516: }
4517:
4518: $typeHolder = $holder->getTypeHolder();
4519: if ($typeHolder->getCertainty()->no() && !$typeHolder->getExpr() instanceof Variable) {
4520: continue;
4521: }
4522:
4523: foreach ($holder->getConditionExpressionTypeHolders() as $guardExprString => $guardTypeHolder) {
4524: if (!array_key_exists($guardExprString, $otherExpressionTypes)) {
4525: continue;
4526: }
4527:
4528: $otherType = $otherExpressionTypes[$guardExprString]->getType();
4529: $guardType = $guardTypeHolder->getType();
4530:
4531: if ($otherType->isSuperTypeOf($guardType)->no()) {
4532: $currentConditionalExpressions[$exprString][$key] = $holder;
4533: continue 2;
4534: }
4535: }
4536:
4537: if ($typeHolder->getCertainty()->no() || !array_key_exists($exprString, $otherExpressionTypes)) {
4538: continue;
4539: }
4540:
4541: $otherTargetHolder = $otherExpressionTypes[$exprString];
4542: $otherTargetCertainty = $otherTargetHolder->getCertainty();
4543: if (!$otherTargetCertainty->yes() && !$otherTargetCertainty->equals($typeHolder->getCertainty())) {
4544: continue;
4545: }
4546:
4547: // An unresolved expression (e.g. an access to an undefined property) is held
4548: // as ErrorType. ErrorType is a subtype of everything, so a consequent or an
4549: // other-branch state of ErrorType would look "already satisfied" and preserve
4550: // a holder that tracks the expression and hides the underlying error. Such a
4551: // holder carries no real type relationship, so skip it.
4552: if ($typeHolder->getType() instanceof ErrorType || $otherTargetHolder->getType() instanceof ErrorType) {
4553: continue;
4554: }
4555:
4556: if (!$typeHolder->getType()->isSuperTypeOf($otherTargetHolder->getType())->yes()) {
4557: continue;
4558: }
4559:
4560: $currentConditionalExpressions[$exprString][$key] = $holder;
4561: }
4562: }
4563:
4564: return $currentConditionalExpressions;
4565: }
4566:
4567: /**
4568: * Merges one-sided holders that share a target and an identical guard set:
4569: * whichever branch a merged path came from, the guard matching later implies
4570: * one of the recorded consequents, so the union of the consequent types
4571: * (under their shared certainty) holds on every merged path.
4572: *
4573: * @param array<string, ConditionalExpressionHolder[]> $currentConditionalExpressions
4574: * @param array<string, ConditionalExpressionHolder[]> $ourConditionalExpressions
4575: * @param array<string, ConditionalExpressionHolder[]> $theirConditionalExpressions
4576: * @return array<string, ConditionalExpressionHolder[]>
4577: */
4578: private function mergeSameGuardConditionalExpressions(
4579: array $currentConditionalExpressions,
4580: array $ourConditionalExpressions,
4581: array $theirConditionalExpressions,
4582: ): array
4583: {
4584: foreach ($ourConditionalExpressions as $exprString => $ourHolders) {
4585: if (!array_key_exists($exprString, $theirConditionalExpressions)) {
4586: continue;
4587: }
4588:
4589: $theirHolders = $theirConditionalExpressions[$exprString];
4590: foreach ($ourHolders as $ourKey => $ourHolder) {
4591: if (isset($currentConditionalExpressions[$exprString][$ourKey])) {
4592: continue;
4593: }
4594:
4595: $ourTypeHolder = $ourHolder->getTypeHolder();
4596: if ($ourTypeHolder->getCertainty()->no()) {
4597: continue;
4598: }
4599:
4600: foreach ($theirHolders as $theirKey => $theirHolder) {
4601: if (isset($currentConditionalExpressions[$exprString][$theirKey])) {
4602: continue;
4603: }
4604:
4605: $theirTypeHolder = $theirHolder->getTypeHolder();
4606: if (!$theirTypeHolder->getCertainty()->equals($ourTypeHolder->getCertainty())) {
4607: continue;
4608: }
4609:
4610: $ourGuards = $ourHolder->getConditionExpressionTypeHolders();
4611: $theirGuards = $theirHolder->getConditionExpressionTypeHolders();
4612: if (count($ourGuards) !== count($theirGuards)) {
4613: continue;
4614: }
4615:
4616: foreach ($ourGuards as $guardExprString => $ourGuardHolder) {
4617: if (
4618: !array_key_exists($guardExprString, $theirGuards)
4619: || !$ourGuardHolder->equals($theirGuards[$guardExprString])
4620: ) {
4621: continue 2;
4622: }
4623: }
4624:
4625: $unionHolder = new ConditionalExpressionHolder(
4626: $ourGuards,
4627: new ExpressionTypeHolder(
4628: $ourTypeHolder->getExpr(),
4629: TypeCombinator::union($ourTypeHolder->getType(), $theirTypeHolder->getType()),
4630: $ourTypeHolder->getCertainty(),
4631: ),
4632: );
4633: $currentConditionalExpressions[$exprString][$unionHolder->getKey()] = $unionHolder;
4634: }
4635: }
4636: }
4637:
4638: return $currentConditionalExpressions;
4639: }
4640:
4641: /**
4642: * @param array<string, ConditionalExpressionHolder[]> $newConditionalExpressions
4643: * @param array<string, ConditionalExpressionHolder[]> $existingConditionalExpressions
4644: * @return array<string, ConditionalExpressionHolder[]>
4645: */
4646: private function mergeConditionalExpressions(array $newConditionalExpressions, array $existingConditionalExpressions): array
4647: {
4648: $result = $existingConditionalExpressions;
4649: foreach ($newConditionalExpressions as $exprString => $holders) {
4650: if (!array_key_exists($exprString, $result)) {
4651: $result[$exprString] = $holders;
4652: } else {
4653: $result[$exprString] = array_merge($result[$exprString], $holders);
4654: }
4655: }
4656:
4657: return $result;
4658: }
4659:
4660: public function mergeInitializedProperties(self $calledMethodScope): self
4661: {
4662: $scope = $this;
4663: foreach ($calledMethodScope->expressionTypes as $exprString => $typeHolder) {
4664: $exprString = (string) $exprString;
4665: if (!str_starts_with($exprString, '__phpstanPropertyInitialization(')) {
4666: continue;
4667: }
4668: $propertyName = substr($exprString, strlen('__phpstanPropertyInitialization('), -1);
4669: $propertyExpr = new PropertyInitializationExpr($propertyName);
4670: if (!array_key_exists($exprString, $scope->expressionTypes)) {
4671: $scope = $scope->assignExpression($propertyExpr, new MixedType(), new MixedType());
4672: $scope->expressionTypes[$exprString] = $typeHolder;
4673: continue;
4674: }
4675:
4676: $certainty = $scope->expressionTypes[$exprString]->getCertainty();
4677: $scope = $scope->assignExpression($propertyExpr, new MixedType(), new MixedType());
4678: $scope->expressionTypes[$exprString] = new ExpressionTypeHolder(
4679: $typeHolder->getExpr(),
4680: $typeHolder->getType(),
4681: $typeHolder->getCertainty()->or($certainty),
4682: );
4683: }
4684:
4685: return $scope;
4686: }
4687:
4688: public function processFinallyScope(self $finallyScope, self $originalFinallyScope): self
4689: {
4690: return $this->scopeFactory->create(
4691: $this->context,
4692: $this->isDeclareStrictTypes(),
4693: $this->getFunction(),
4694: $this->getNamespace(),
4695: $this->processFinallyScopeVariableTypeHolders(
4696: $this->expressionTypes,
4697: $finallyScope->expressionTypes,
4698: $originalFinallyScope->expressionTypes,
4699: ),
4700: $this->processFinallyScopeVariableTypeHolders(
4701: $this->nativeExpressionTypes,
4702: $finallyScope->nativeExpressionTypes,
4703: $originalFinallyScope->nativeExpressionTypes,
4704: ),
4705: ScopeOps::intersectConditionalExpressions($this->conditionalExpressions, $finallyScope->conditionalExpressions),
4706: $this->inClosureBindScopeClasses,
4707: $this->anonymousFunctionReflection,
4708: $this->inFirstLevelStatement,
4709: [],
4710: [],
4711: [],
4712: $this->afterExtractCall,
4713: $this->parentScope,
4714: $this->nativeTypesPromoted,
4715: $this->templateArgumentFrame,
4716: $this->templateArgumentConstraints,
4717: );
4718: }
4719:
4720: /**
4721: * @param array<string, ExpressionTypeHolder> $ourVariableTypeHolders
4722: * @param array<string, ExpressionTypeHolder> $finallyVariableTypeHolders
4723: * @param array<string, ExpressionTypeHolder> $originalVariableTypeHolders
4724: * @return array<string, ExpressionTypeHolder>
4725: */
4726: private function processFinallyScopeVariableTypeHolders(
4727: array $ourVariableTypeHolders,
4728: array $finallyVariableTypeHolders,
4729: array $originalVariableTypeHolders,
4730: ): array
4731: {
4732: foreach ($finallyVariableTypeHolders as $exprString => $variableTypeHolder) {
4733: if (
4734: isset($originalVariableTypeHolders[$exprString])
4735: && !$originalVariableTypeHolders[$exprString]->equalTypes($variableTypeHolder)
4736: ) {
4737: $ourVariableTypeHolders[$exprString] = $variableTypeHolder;
4738: continue;
4739: }
4740:
4741: if (isset($originalVariableTypeHolders[$exprString])) {
4742: continue;
4743: }
4744:
4745: $ourVariableTypeHolders[$exprString] = $variableTypeHolder;
4746: }
4747:
4748: return $ourVariableTypeHolders;
4749: }
4750:
4751: /**
4752: * @param Node\ClosureUse[] $byRefUses
4753: */
4754: public function processClosureScope(
4755: self $closureScope,
4756: ?self $prevScope,
4757: array $byRefUses,
4758: ): self
4759: {
4760: $nativeExpressionTypes = $this->nativeExpressionTypes;
4761: $expressionTypes = $this->expressionTypes;
4762: if (count($byRefUses) === 0) {
4763: return $this;
4764: }
4765:
4766: foreach ($byRefUses as $use) {
4767: if (!is_string($use->var->name)) {
4768: throw new ShouldNotHappenException();
4769: }
4770:
4771: $variableName = $use->var->name;
4772: $variableExprString = '$' . $variableName;
4773:
4774: if (!$closureScope->hasVariableType($variableName)->yes()) {
4775: $holder = ExpressionTypeHolder::createYes($use->var, new NullType());
4776: $expressionTypes[$variableExprString] = $holder;
4777: $nativeExpressionTypes[$variableExprString] = $holder;
4778: continue;
4779: }
4780:
4781: $variableType = $closureScope->getVariableType($variableName);
4782:
4783: if ($prevScope !== null) {
4784: $prevVariableType = $prevScope->getVariableType($variableName);
4785: if (!$variableType->equals($prevVariableType)) {
4786: $variableType = TypeCombinator::union($variableType, $prevVariableType);
4787: $variableType = $this->generalizeType($variableType, $prevVariableType, 0);
4788: }
4789: }
4790:
4791: $holder = ExpressionTypeHolder::createYes($use->var, $variableType);
4792: $expressionTypes[$variableExprString] = $holder;
4793: $nativeExpressionTypes[$variableExprString] = $holder;
4794: }
4795:
4796: return $this->scopeFactory->create(
4797: $this->context,
4798: $this->isDeclareStrictTypes(),
4799: $this->getFunction(),
4800: $this->getNamespace(),
4801: $expressionTypes,
4802: $nativeExpressionTypes,
4803: $this->conditionalExpressions,
4804: $this->inClosureBindScopeClasses,
4805: $this->anonymousFunctionReflection,
4806: $this->inFirstLevelStatement,
4807: [],
4808: [],
4809: $this->inFunctionCallsStack,
4810: $this->afterExtractCall,
4811: $this->parentScope,
4812: $this->nativeTypesPromoted,
4813: $this->templateArgumentFrame,
4814: $this->templateArgumentConstraints,
4815: );
4816: }
4817:
4818: public function processAlwaysIterableForeachScopeWithoutPollute(self $finalScope): self
4819: {
4820: $expressionTypes = $this->expressionTypes;
4821: foreach ($finalScope->expressionTypes as $variableExprString => $variableTypeHolder) {
4822: if (!isset($expressionTypes[$variableExprString])) {
4823: $expressionTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType());
4824: continue;
4825: }
4826:
4827: $expressionTypes[$variableExprString] = new ExpressionTypeHolder(
4828: $variableTypeHolder->getExpr(),
4829: $variableTypeHolder->getType(),
4830: $variableTypeHolder->getCertainty()->and($expressionTypes[$variableExprString]->getCertainty()),
4831: );
4832: }
4833: $nativeTypes = $this->nativeExpressionTypes;
4834: foreach ($finalScope->nativeExpressionTypes as $variableExprString => $variableTypeHolder) {
4835: if (!isset($nativeTypes[$variableExprString])) {
4836: $nativeTypes[$variableExprString] = ExpressionTypeHolder::createMaybe($variableTypeHolder->getExpr(), $variableTypeHolder->getType());
4837: continue;
4838: }
4839:
4840: $nativeTypes[$variableExprString] = new ExpressionTypeHolder(
4841: $variableTypeHolder->getExpr(),
4842: $variableTypeHolder->getType(),
4843: $variableTypeHolder->getCertainty()->and($nativeTypes[$variableExprString]->getCertainty()),
4844: );
4845: }
4846:
4847: return $this->scopeFactory->create(
4848: $this->context,
4849: $this->isDeclareStrictTypes(),
4850: $this->getFunction(),
4851: $this->getNamespace(),
4852: $expressionTypes,
4853: $nativeTypes,
4854: ScopeOps::intersectConditionalExpressions($this->conditionalExpressions, $finalScope->conditionalExpressions),
4855: $this->inClosureBindScopeClasses,
4856: $this->anonymousFunctionReflection,
4857: $this->inFirstLevelStatement,
4858: [],
4859: [],
4860: [],
4861: $this->afterExtractCall,
4862: $this->parentScope,
4863: $this->nativeTypesPromoted,
4864: $this->templateArgumentFrame,
4865: $this->templateArgumentConstraints,
4866: );
4867: }
4868:
4869: /**
4870: * @param array<string, true>|null $writableVariableNames variables the loop can write, null when unknown
4871: */
4872: public function generalizeWith(self $otherScope, ?array $writableVariableNames = null): self
4873: {
4874: return $this->generalizeWithVariableState($otherScope, $writableVariableNames)->addTemplateArgumentConstraints($otherScope->getTemplateArgumentConstraints());
4875: }
4876:
4877: /**
4878: * @param array<string, true>|null $writableVariableNames
4879: */
4880: private function generalizeWithVariableState(self $otherScope, ?array $writableVariableNames): self
4881: {
4882: if ($writableVariableNames !== null) {
4883: // a reference created before the loop lets the loop write a variable it does not name
4884: foreach ([$this->expressionTypes, $otherScope->expressionTypes] as $expressionTypes) {
4885: foreach ($expressionTypes as $expressionTypeHolder) {
4886: $intertwinedExpr = $expressionTypeHolder->getExpr();
4887: if (!$intertwinedExpr instanceof IntertwinedVariableByReferenceWithExpr) {
4888: continue;
4889: }
4890: $writableVariableNames[$intertwinedExpr->getVariableName()] = true;
4891: foreach ([$intertwinedExpr->getExpr(), $intertwinedExpr->getAssignedExpr()] as $aliasedExpr) {
4892: $aliasedVariableName = ScopeOps::getIntertwinedRefRootVariableName($aliasedExpr);
4893: if ($aliasedVariableName === null) {
4894: continue;
4895: }
4896: $writableVariableNames[$aliasedVariableName] = true;
4897: }
4898: }
4899: }
4900: }
4901: $variableTypeHolders = $this->generalizeVariableTypeHolders(
4902: $this->expressionTypes,
4903: $otherScope->expressionTypes,
4904: $writableVariableNames,
4905: );
4906: $nativeTypes = $this->generalizeVariableTypeHolders(
4907: $this->nativeExpressionTypes,
4908: $otherScope->nativeExpressionTypes,
4909: $writableVariableNames,
4910: );
4911:
4912: return $this->scopeFactory->create(
4913: $this->context,
4914: $this->isDeclareStrictTypes(),
4915: $this->getFunction(),
4916: $this->getNamespace(),
4917: $variableTypeHolders,
4918: $nativeTypes,
4919: $this->conditionalExpressions,
4920: $this->inClosureBindScopeClasses,
4921: $this->anonymousFunctionReflection,
4922: $this->inFirstLevelStatement,
4923: [],
4924: [],
4925: [],
4926: $this->afterExtractCall,
4927: $this->parentScope,
4928: $this->nativeTypesPromoted,
4929: $this->templateArgumentFrame,
4930: $this->templateArgumentConstraints,
4931: );
4932: }
4933:
4934: /**
4935: * @param array<string, ExpressionTypeHolder> $variableTypeHolders
4936: * @param array<string, ExpressionTypeHolder> $otherVariableTypeHolders
4937: * @param array<string, true>|null $writableVariableNames
4938: * @return array<string, ExpressionTypeHolder>
4939: */
4940: private function generalizeVariableTypeHolders(
4941: array $variableTypeHolders,
4942: array $otherVariableTypeHolders,
4943: ?array $writableVariableNames,
4944: ): array
4945: {
4946: uksort($variableTypeHolders, static fn (string $exprA, string $exprB): int => strlen($exprA) <=> strlen($exprB));
4947:
4948: $generalizedExpressions = [];
4949: $newVariableTypeHolders = [];
4950: foreach ($variableTypeHolders as $variableExprString => $variableTypeHolder) {
4951: foreach ($generalizedExpressions as $generalizedExprString => $generalizedExpr) {
4952: if (!ScopeOps::shouldInvalidateExpression($this, $this->services->exprPrinter, $generalizedExprString, $generalizedExpr, $variableTypeHolder->getExpr(), $variableExprString)) {
4953: continue;
4954: }
4955:
4956: continue 2;
4957: }
4958: if (!isset($otherVariableTypeHolders[$variableExprString])) {
4959: $newVariableTypeHolders[$variableExprString] = $variableTypeHolder;
4960: continue;
4961: }
4962:
4963: $variableExpr = $variableTypeHolder->getExpr();
4964: if (
4965: $writableVariableNames !== null
4966: && $variableExpr instanceof Variable
4967: && is_string($variableExpr->name)
4968: && !isset($writableVariableNames[$variableExpr->name])
4969: ) {
4970: // the loop does not write this variable, its types differ between passes only by narrowing
4971: $generalizedType = TypeCombinator::union($variableTypeHolder->getType(), $otherVariableTypeHolders[$variableExprString]->getType());
4972: } else {
4973: $generalizedType = $this->generalizeType($variableTypeHolder->getType(), $otherVariableTypeHolders[$variableExprString]->getType(), 0);
4974: }
4975: if (
4976: !$generalizedType->equals($variableTypeHolder->getType())
4977: ) {
4978: $generalizedExpressions[$variableExprString] = $variableTypeHolder->getExpr();
4979: }
4980: $newVariableTypeHolders[$variableExprString] = new ExpressionTypeHolder(
4981: $variableTypeHolder->getExpr(),
4982: $generalizedType,
4983: $variableTypeHolder->getCertainty(),
4984: );
4985: }
4986:
4987: return $newVariableTypeHolders;
4988: }
4989:
4990: /**
4991: * TypeUtils::flattenTypes() expands a shape with optional keys into every
4992: * concrete variant (2^N of them, four lossy representatives above ten),
4993: * only for the union below to merge them back into the very same shape -
4994: * quadratic in the number of variants. The per-key widening reads the
4995: * shape's keys, values and optionality directly, so only unions are split.
4996: *
4997: * @return list<Type>
4998: */
4999: private function flattenUnionForGeneralization(Type $type): array
5000: {
5001: if (!$type instanceof UnionType) {
5002: return [$type];
5003: }
5004:
5005: $types = [];
5006: foreach ($type->getTypes() as $innerType) {
5007: foreach ($this->flattenUnionForGeneralization($innerType) as $flattenedType) {
5008: $types[] = $flattenedType;
5009: }
5010: }
5011:
5012: return $types;
5013: }
5014:
5015: private function generalizeType(Type $a, Type $b, int $depth): Type
5016: {
5017: if ($a->equals($b)) {
5018: return $a;
5019: }
5020:
5021: // Track whether either input carries a BenevolentUnion so the result
5022: // can be re-wrapped at the end. `flattenTypes` below drops the
5023: // BenevolentUnion wrapper, which would silently downgrade e.g.
5024: // `(float|int)` (numeric-accepting) to a strict `float|int`. Inside a
5025: // loop's fixed-point this propagates into the iterable value type of
5026: // an array and turns `return [..., $int]` checks into false positives
5027: // when the iteration body's `+ 1` arithmetic was originally produced
5028: // by an `ErrorType`-derived `int|float` benevolent union (the typical
5029: // case for reads of literally-missing keys inside the body).
5030: $wrapBenevolent = $a instanceof BenevolentUnionType || $b instanceof BenevolentUnionType;
5031:
5032: $constantIntegers = ['a' => [], 'b' => []];
5033: $constantFloats = ['a' => [], 'b' => []];
5034: $constantBooleans = ['a' => [], 'b' => []];
5035: $constantStrings = ['a' => [], 'b' => []];
5036: $constantArrays = ['a' => [], 'b' => []];
5037: $generalArrays = ['a' => [], 'b' => []];
5038: $integerRanges = ['a' => [], 'b' => []];
5039: $otherTypes = [];
5040:
5041: foreach ([
5042: 'a' => $this->flattenUnionForGeneralization($a),
5043: 'b' => $this->flattenUnionForGeneralization($b),
5044: ] as $key => $types) {
5045: foreach ($types as $type) {
5046: if ($type instanceof ConstantIntegerType) {
5047: $constantIntegers[$key][] = $type;
5048: continue;
5049: }
5050: if ($type instanceof ConstantFloatType) {
5051: $constantFloats[$key][] = $type;
5052: continue;
5053: }
5054: if ($type instanceof ConstantBooleanType) {
5055: $constantBooleans[$key][] = $type;
5056: continue;
5057: }
5058: if ($type instanceof ConstantStringType) {
5059: $constantStrings[$key][] = $type;
5060: continue;
5061: }
5062: if ($type->isConstantArray()->yes()) {
5063: $constantArrays[$key][] = $type;
5064: continue;
5065: }
5066: if ($type->isArray()->yes()) {
5067: $generalArrays[$key][] = $type;
5068: continue;
5069: }
5070: if ($type instanceof IntegerRangeType) {
5071: $integerRanges[$key][] = $type;
5072: continue;
5073: }
5074:
5075: $otherTypes[] = $type;
5076: }
5077: }
5078:
5079: $resultTypes = [];
5080: foreach ([
5081: $constantFloats,
5082: $constantBooleans,
5083: $constantStrings,
5084: ] as $constantTypes) {
5085: if (count($constantTypes['a']) === 0) {
5086: if (count($constantTypes['b']) > 0) {
5087: $resultTypes[] = TypeCombinator::union(...$constantTypes['b']);
5088: }
5089: continue;
5090: } elseif (count($constantTypes['b']) === 0) {
5091: $resultTypes[] = TypeCombinator::union(...$constantTypes['a']);
5092: continue;
5093: }
5094:
5095: $aTypes = TypeCombinator::union(...$constantTypes['a']);
5096: $bTypes = TypeCombinator::union(...$constantTypes['b']);
5097: if ($aTypes->equals($bTypes)) {
5098: $resultTypes[] = $aTypes;
5099: continue;
5100: }
5101:
5102: $resultTypes[] = TypeCombinator::union(...$constantTypes['a'], ...$constantTypes['b'])->generalize(GeneralizePrecision::moreSpecific());
5103: }
5104:
5105: if (count($constantArrays['a']) > 0) {
5106: if (count($constantArrays['b']) === 0) {
5107: $resultTypes[] = TypeCombinator::union(...$constantArrays['a']);
5108: } else {
5109: $constantArraysA = TypeCombinator::union(...$constantArrays['a']);
5110: $constantArraysB = TypeCombinator::union(...$constantArrays['b']);
5111: if (
5112: $constantArraysA->getIterableKeyType()->equals($constantArraysB->getIterableKeyType())
5113: && $constantArraysA->getArraySize()->getGreaterOrEqualType($this->services->phpVersion)->isSuperTypeOf($constantArraysB->getArraySize())->yes()
5114: ) {
5115: $resultArrayBuilder = ConstantArrayTypeBuilder::createEmpty();
5116: foreach (TypeUtils::flattenTypes($constantArraysA->getIterableKeyType()) as $keyType) {
5117: $resultArrayBuilder->setOffsetValueType(
5118: $keyType,
5119: $this->generalizeType(
5120: $constantArraysA->getOffsetValueType($keyType),
5121: $constantArraysB->getOffsetValueType($keyType),
5122: $depth + 1,
5123: ),
5124: !$constantArraysA->hasOffsetValueType($keyType)->and($constantArraysB->hasOffsetValueType($keyType))->negate()->no(),
5125: );
5126: }
5127:
5128: $resultTypes[] = $resultArrayBuilder->getArray();
5129: } else {
5130: // Both inputs are sealed constant array shapes — their key
5131: // sets are finite by construction. On the fall-through
5132: // ArrayType path, recursing into `generalizeType` would
5133: // widen e.g. `0|1` to `int<0, max>` — for both the keys and
5134: // the values — losing the loop's per-iteration precision.
5135: // Keep the literal union instead so the loop's bounds stay
5136: // visible. (Scoped to sealed shapes so the general
5137: // `generalize()` widening contract for legacy arrays — see
5138: // ScopeTest::testGeneralize — is unaffected.)
5139: $bothSealed = true;
5140: foreach ([...$constantArrays['a'], ...$constantArrays['b']] as $constantArrayCheck) {
5141: foreach ($constantArrayCheck->getConstantArrays() as $constantArrayInstance) {
5142: if (!$constantArrayInstance->isSealed()->yes()) {
5143: $bothSealed = false;
5144: break 2;
5145: }
5146: }
5147: }
5148: if ($bothSealed) {
5149: $resultKeyType = TypeCombinator::union($constantArraysA->getIterableKeyType(), $constantArraysB->getIterableKeyType());
5150: $resultValueType = TypeCombinator::union($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType());
5151: if ($resultValueType->isOversizedArray()->yes()) {
5152: // The literal value union outgrew the shape limit (a
5153: // deeply/widely nested value): fall back to generalizing
5154: // it into a bounded range-keyed array rather than
5155: // keeping an oversized literal shape.
5156: $resultValueType = TypeCombinator::union($this->generalizeType($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType(), $depth + 1));
5157: }
5158: } else {
5159: $resultKeyType = TypeCombinator::union($this->generalizeType($constantArraysA->getIterableKeyType(), $constantArraysB->getIterableKeyType(), $depth + 1));
5160: $resultValueType = TypeCombinator::union($this->generalizeType($constantArraysA->getIterableValueType(), $constantArraysB->getIterableValueType(), $depth + 1));
5161: }
5162: $resultType = new ArrayType(
5163: $resultKeyType,
5164: $resultValueType,
5165: );
5166: $accessories = [];
5167: if (
5168: $constantArraysA->isIterableAtLeastOnce()->yes()
5169: && $constantArraysB->isIterableAtLeastOnce()->yes()
5170: && $constantArraysA->getArraySize()->getGreaterOrEqualType($this->services->phpVersion)->isSuperTypeOf($constantArraysB->getArraySize())->yes()
5171: ) {
5172: $accessories[] = new NonEmptyArrayType();
5173: }
5174: if ($constantArraysA->isList()->yes() && $constantArraysB->isList()->yes()) {
5175: $accessories[] = new AccessoryArrayListType();
5176: }
5177:
5178: if (count($accessories) === 0) {
5179: $resultTypes[] = $resultType;
5180: } else {
5181: $resultTypes[] = TypeCombinator::intersect($resultType, ...$accessories);
5182: }
5183: }
5184: }
5185: } elseif (count($constantArrays['b']) > 0) {
5186: $resultTypes[] = TypeCombinator::union(...$constantArrays['b']);
5187: }
5188:
5189: if (count($generalArrays['a']) > 0) {
5190: if (count($generalArrays['b']) === 0) {
5191: $resultTypes[] = TypeCombinator::union(...$generalArrays['a']);
5192: } else {
5193: $generalArraysA = TypeCombinator::union(...$generalArrays['a']);
5194: $generalArraysB = TypeCombinator::union(...$generalArrays['b']);
5195:
5196: $aValueType = $generalArraysA->getIterableValueType();
5197: $bValueType = $generalArraysB->getIterableValueType();
5198: if (
5199: $aValueType->isArray()->yes()
5200: && $aValueType->isConstantArray()->no()
5201: && $bValueType->isArray()->yes()
5202: && $bValueType->isConstantArray()->no()
5203: ) {
5204: $aDepth = self::getArrayDepth($aValueType) + $depth;
5205: $bDepth = self::getArrayDepth($bValueType) + $depth;
5206: if (
5207: ($aDepth > 2 || $bDepth > 2)
5208: && abs($aDepth - $bDepth) > 0
5209: ) {
5210: $aValueType = new MixedType();
5211: $bValueType = new MixedType();
5212: }
5213: }
5214:
5215: $resultType = new ArrayType(
5216: TypeCombinator::union($this->generalizeType($generalArraysA->getIterableKeyType(), $generalArraysB->getIterableKeyType(), $depth + 1)),
5217: TypeCombinator::union($this->generalizeType($aValueType, $bValueType, $depth + 1)),
5218: );
5219:
5220: $accessories = [];
5221: if ($generalArraysA->isIterableAtLeastOnce()->yes() && $generalArraysB->isIterableAtLeastOnce()->yes()) {
5222: $accessories[] = new NonEmptyArrayType();
5223: }
5224: if ($generalArraysA->isList()->yes() && $generalArraysB->isList()->yes()) {
5225: $accessories[] = new AccessoryArrayListType();
5226: }
5227: if ($generalArraysA->isOversizedArray()->yes() && $generalArraysB->isOversizedArray()->yes()) {
5228: $accessories[] = new OversizedArrayType();
5229: }
5230:
5231: if (count($accessories) === 0) {
5232: $resultTypes[] = $resultType;
5233: } else {
5234: $resultTypes[] = TypeCombinator::intersect($resultType, ...$accessories);
5235: }
5236: }
5237: } elseif (count($generalArrays['b']) > 0) {
5238: $resultTypes[] = TypeCombinator::union(...$generalArrays['b']);
5239: }
5240:
5241: if (count($constantIntegers['a']) > 0) {
5242: if (count($constantIntegers['b']) === 0) {
5243: $resultTypes[] = TypeCombinator::union(...$constantIntegers['a']);
5244: } else {
5245: $constantIntegersA = TypeCombinator::union(...$constantIntegers['a']);
5246: $constantIntegersB = TypeCombinator::union(...$constantIntegers['b']);
5247:
5248: if ($constantIntegersA->equals($constantIntegersB)) {
5249: $resultTypes[] = $constantIntegersA;
5250: } else {
5251: $min = null;
5252: $max = null;
5253: foreach ($constantIntegers['a'] as $int) {
5254: if ($min === null || $int->getValue() < $min) {
5255: $min = $int->getValue();
5256: }
5257: if ($max !== null && $int->getValue() <= $max) {
5258: continue;
5259: }
5260:
5261: $max = $int->getValue();
5262: }
5263:
5264: $newMin = $min;
5265: $newMax = $max;
5266: foreach ($constantIntegers['b'] as $int) {
5267: if ($int->getValue() > $newMax) {
5268: $newMax = $int->getValue();
5269: }
5270: if ($int->getValue() >= $newMin) {
5271: continue;
5272: }
5273:
5274: $newMin = $int->getValue();
5275: }
5276:
5277: if ($newMax > $max && $newMin < $min) {
5278: $resultTypes[] = IntegerRangeType::fromInterval($newMin, $newMax);
5279: } elseif ($newMax > $max) {
5280: $resultTypes[] = IntegerRangeType::fromInterval($min, null);
5281: } elseif ($newMin < $min) {
5282: $resultTypes[] = IntegerRangeType::fromInterval(null, $max);
5283: } else {
5284: $resultTypes[] = TypeCombinator::union($constantIntegersA, $constantIntegersB);
5285: }
5286: }
5287: }
5288: } elseif (count($constantIntegers['b']) > 0) {
5289: $resultTypes[] = TypeCombinator::union(...$constantIntegers['b']);
5290: }
5291:
5292: if (count($integerRanges['a']) > 0) {
5293: if (count($integerRanges['b']) === 0) {
5294: $resultTypes[] = TypeCombinator::union(...$integerRanges['a']);
5295: } else {
5296: $integerRangesA = TypeCombinator::union(...$integerRanges['a']);
5297: $integerRangesB = TypeCombinator::union(...$integerRanges['b']);
5298:
5299: if ($integerRangesA->equals($integerRangesB)) {
5300: $resultTypes[] = $integerRangesA;
5301: } else {
5302: $min = null;
5303: $max = null;
5304: foreach ($integerRanges['a'] as $range) {
5305: if ($range->getMin() === null) {
5306: $rangeMin = PHP_INT_MIN;
5307: } else {
5308: $rangeMin = $range->getMin();
5309: }
5310: if ($range->getMax() === null) {
5311: $rangeMax = PHP_INT_MAX;
5312: } else {
5313: $rangeMax = $range->getMax();
5314: }
5315:
5316: if ($min === null || $rangeMin < $min) {
5317: $min = $rangeMin;
5318: }
5319: if ($max !== null && $rangeMax <= $max) {
5320: continue;
5321: }
5322:
5323: $max = $rangeMax;
5324: }
5325:
5326: $newMin = $min;
5327: $newMax = $max;
5328: foreach ($integerRanges['b'] as $range) {
5329: if ($range->getMin() === null) {
5330: $rangeMin = PHP_INT_MIN;
5331: } else {
5332: $rangeMin = $range->getMin();
5333: }
5334: if ($range->getMax() === null) {
5335: $rangeMax = PHP_INT_MAX;
5336: } else {
5337: $rangeMax = $range->getMax();
5338: }
5339:
5340: if ($rangeMax > $newMax) {
5341: $newMax = $rangeMax;
5342: }
5343: if ($rangeMin >= $newMin) {
5344: continue;
5345: }
5346:
5347: $newMin = $rangeMin;
5348: }
5349:
5350: $gotGreater = $newMax > $max;
5351: $gotSmaller = $newMin < $min;
5352:
5353: if ($min === PHP_INT_MIN) {
5354: $min = null;
5355: }
5356: if ($max === PHP_INT_MAX) {
5357: $max = null;
5358: }
5359: if ($newMin === PHP_INT_MIN) {
5360: $newMin = null;
5361: }
5362: if ($newMax === PHP_INT_MAX) {
5363: $newMax = null;
5364: }
5365:
5366: if ($gotGreater && $gotSmaller) {
5367: $resultTypes[] = IntegerRangeType::fromInterval($newMin, $newMax);
5368: } elseif ($gotGreater) {
5369: $resultTypes[] = IntegerRangeType::fromInterval($min, null);
5370: } elseif ($gotSmaller) {
5371: $resultTypes[] = IntegerRangeType::fromInterval(null, $max);
5372: } else {
5373: $resultTypes[] = TypeCombinator::union($integerRangesA, $integerRangesB);
5374: }
5375: }
5376: }
5377: } elseif (count($integerRanges['b']) > 0) {
5378: $resultTypes[] = TypeCombinator::union(...$integerRanges['b']);
5379: }
5380:
5381: $accessoryTypes = array_map(
5382: static fn (Type $type): Type => $type->generalize(GeneralizePrecision::moreSpecific()),
5383: TypeUtils::getAccessoryTypes($a),
5384: );
5385:
5386: $combined = TypeCombinator::union(...$resultTypes, ...$otherTypes);
5387: if ($wrapBenevolent) {
5388: $combined = TypeUtils::toBenevolentUnion($combined);
5389: }
5390:
5391: return TypeCombinator::union(TypeCombinator::intersect(
5392: $combined,
5393: ...$accessoryTypes,
5394: ), ...$otherTypes);
5395: }
5396:
5397: private static function getArrayDepth(Type $type): int
5398: {
5399: $depth = 0;
5400: $arrays = TypeUtils::toBenevolentUnion($type)->getArrays();
5401: while (count($arrays) > 0) {
5402: $temp = $type->getIterableValueType();
5403: $type = $temp;
5404: $arrays = TypeUtils::toBenevolentUnion($type)->getArrays();
5405: $depth++;
5406: }
5407:
5408: return $depth;
5409: }
5410:
5411: public function equals(self $otherScope): bool
5412: {
5413: if (!$this->context->equals($otherScope->context)) {
5414: return false;
5415: }
5416:
5417: if (!$this->compareVariableTypeHolders($this->expressionTypes, $otherScope->expressionTypes)) {
5418: return false;
5419: }
5420: if (!$this->compareVariableTypeHolders($this->nativeExpressionTypes, $otherScope->nativeExpressionTypes)) {
5421: return false;
5422: }
5423:
5424: return $this->compareConditionalExpressions($this->conditionalExpressions, $otherScope->conditionalExpressions);
5425: }
5426:
5427: /**
5428: * @param array<string, ConditionalExpressionHolder[]> $conditionalExpressions
5429: * @param array<string, ConditionalExpressionHolder[]> $otherConditionalExpressions
5430: */
5431: private function compareConditionalExpressions(array $conditionalExpressions, array $otherConditionalExpressions): bool
5432: {
5433: if (count($conditionalExpressions) !== count($otherConditionalExpressions)) {
5434: return false;
5435: }
5436: foreach ($conditionalExpressions as $exprString => $holders) {
5437: if (!isset($otherConditionalExpressions[$exprString])) {
5438: return false;
5439: }
5440: $otherHolders = $otherConditionalExpressions[$exprString];
5441: if (count($holders) !== count($otherHolders)) {
5442: return false;
5443: }
5444: foreach ($holders as $key => $holder) {
5445: if (!isset($otherHolders[$key])) {
5446: return false;
5447: }
5448: $otherHolder = $otherHolders[$key];
5449: if (!$holder->getTypeHolder()->equals($otherHolder->getTypeHolder())) {
5450: return false;
5451: }
5452: $conditions = $holder->getConditionExpressionTypeHolders();
5453: $otherConditions = $otherHolder->getConditionExpressionTypeHolders();
5454: if (count($conditions) !== count($otherConditions)) {
5455: return false;
5456: }
5457: foreach ($conditions as $conditionExprString => $conditionHolder) {
5458: if (!isset($otherConditions[$conditionExprString])) {
5459: return false;
5460: }
5461: if (!$conditionHolder->equals($otherConditions[$conditionExprString])) {
5462: return false;
5463: }
5464: }
5465: }
5466: }
5467:
5468: return true;
5469: }
5470:
5471: /**
5472: * @param array<string, ExpressionTypeHolder> $variableTypeHolders
5473: * @param array<string, ExpressionTypeHolder> $otherVariableTypeHolders
5474: */
5475: private function compareVariableTypeHolders(array $variableTypeHolders, array $otherVariableTypeHolders): bool
5476: {
5477: if (count($variableTypeHolders) !== count($otherVariableTypeHolders)) {
5478: return false;
5479: }
5480: foreach ($variableTypeHolders as $variableExprString => $variableTypeHolder) {
5481: if (!isset($otherVariableTypeHolders[$variableExprString])) {
5482: return false;
5483: }
5484:
5485: if (!$variableTypeHolder->getCertainty()->equals($otherVariableTypeHolders[$variableExprString]->getCertainty())) {
5486: return false;
5487: }
5488:
5489: if (!$variableTypeHolder->equalTypes($otherVariableTypeHolders[$variableExprString])) {
5490: return false;
5491: }
5492: }
5493:
5494: return true;
5495: }
5496:
5497: /**
5498: * @api
5499: * @deprecated Use canReadProperty() or canWriteProperty()
5500: */
5501: public function canAccessProperty(PropertyReflection $propertyReflection): bool
5502: {
5503: return $this->canAccessClassMember($propertyReflection);
5504: }
5505:
5506: /** @api */
5507: public function canReadProperty(ExtendedPropertyReflection $propertyReflection): bool
5508: {
5509: return $this->canAccessClassMember($propertyReflection);
5510: }
5511:
5512: /** @api */
5513: public function canWriteProperty(ExtendedPropertyReflection $propertyReflection): bool
5514: {
5515: if (!$propertyReflection->isPrivateSet() && !$propertyReflection->isProtectedSet()) {
5516: return $this->canAccessClassMember($propertyReflection);
5517: }
5518:
5519: if (!$this->services->phpVersion->supportsAsymmetricVisibility()) {
5520: return $this->canAccessClassMember($propertyReflection);
5521: }
5522:
5523: $propertyDeclaringClass = $propertyReflection->getDeclaringClass();
5524: $canAccessClassMember = static function (ClassReflection $classReflection) use ($propertyReflection, $propertyDeclaringClass) {
5525: if ($propertyReflection->isPrivateSet()) {
5526: return $classReflection->getName() === $propertyDeclaringClass->getName();
5527: }
5528:
5529: // protected set
5530:
5531: if (
5532: $classReflection->getName() === $propertyDeclaringClass->getName()
5533: || $classReflection->isSubclassOfClass($propertyDeclaringClass->removeFinalKeywordOverride())
5534: ) {
5535: return true;
5536: }
5537:
5538: return $propertyReflection->getDeclaringClass()->isSubclassOfClass($classReflection);
5539: };
5540:
5541: foreach ($this->inClosureBindScopeClasses as $inClosureBindScopeClass) {
5542: if (!$this->services->reflectionProvider->hasClass($inClosureBindScopeClass)) {
5543: continue;
5544: }
5545:
5546: if ($canAccessClassMember($this->services->reflectionProvider->getClass($inClosureBindScopeClass))) {
5547: return true;
5548: }
5549: }
5550:
5551: if ($this->isInClass()) {
5552: return $canAccessClassMember($this->getClassReflection());
5553: }
5554:
5555: return false;
5556: }
5557:
5558: /** @api */
5559: public function canCallMethod(MethodReflection $methodReflection): bool
5560: {
5561: if ($this->canAccessClassMember($methodReflection)) {
5562: return true;
5563: }
5564:
5565: return $this->canAccessClassMember($methodReflection->getPrototype());
5566: }
5567:
5568: /** @api */
5569: public function canAccessConstant(ClassConstantReflection $constantReflection): bool
5570: {
5571: return $this->canAccessClassMember($constantReflection);
5572: }
5573:
5574: private function canAccessClassMember(ClassMemberReflection $classMemberReflection): bool
5575: {
5576: if ($classMemberReflection->isPublic()) {
5577: return true;
5578: }
5579:
5580: $classMemberDeclaringClass = $classMemberReflection->getDeclaringClass();
5581: $canAccessClassMember = static function (ClassReflection $classReflection) use ($classMemberReflection, $classMemberDeclaringClass) {
5582: if ($classMemberReflection->isPrivate()) {
5583: return $classReflection->getName() === $classMemberDeclaringClass->getName();
5584: }
5585:
5586: // protected
5587:
5588: if (
5589: $classReflection->getName() === $classMemberDeclaringClass->getName()
5590: || $classReflection->isSubclassOfClass($classMemberDeclaringClass->removeFinalKeywordOverride())
5591: ) {
5592: return true;
5593: }
5594:
5595: return $classMemberReflection->getDeclaringClass()->isSubclassOfClass($classReflection);
5596: };
5597:
5598: foreach ($this->inClosureBindScopeClasses as $inClosureBindScopeClass) {
5599: if (!$this->services->reflectionProvider->hasClass($inClosureBindScopeClass)) {
5600: continue;
5601: }
5602:
5603: if ($canAccessClassMember($this->services->reflectionProvider->getClass($inClosureBindScopeClass))) {
5604: return true;
5605: }
5606: }
5607:
5608: if ($this->isInClass()) {
5609: return $canAccessClassMember($this->getClassReflection());
5610: }
5611:
5612: return false;
5613: }
5614:
5615: /**
5616: * @return string[]
5617: */
5618: public function debug(): array
5619: {
5620: $descriptions = [];
5621: foreach ($this->expressionTypes as $name => $variableTypeHolder) {
5622: $key = sprintf('%s (%s)', $name, $variableTypeHolder->getCertainty()->describe());
5623: $descriptions[$key] = $variableTypeHolder->getType()->describe(VerbosityLevel::precise());
5624: }
5625: foreach ($this->nativeExpressionTypes as $exprString => $nativeTypeHolder) {
5626: $key = sprintf('native %s (%s)', $exprString, $nativeTypeHolder->getCertainty()->describe());
5627: $descriptions[$key] = $nativeTypeHolder->getType()->describe(VerbosityLevel::precise());
5628: }
5629:
5630: foreach (array_keys($this->currentlyAssignedExpressions) as $exprString) {
5631: $descriptions[sprintf('currently assigned %s', $exprString)] = 'true';
5632: }
5633:
5634: foreach (array_keys($this->currentlyAllowedUndefinedExpressions) as $exprString) {
5635: $descriptions[sprintf('currently allowed undefined %s', $exprString)] = 'true';
5636: }
5637:
5638: foreach ($this->conditionalExpressions as $exprString => $holders) {
5639: foreach (array_values($holders) as $i => $holder) {
5640: $key = sprintf('condition about %s #%d', $exprString, $i + 1);
5641: $parts = [];
5642: foreach ($holder->getConditionExpressionTypeHolders() as $conditionalExprString => $expressionTypeHolder) {
5643: $parts[] = $conditionalExprString . '=' . $expressionTypeHolder->getType()->describe(VerbosityLevel::precise());
5644: }
5645: $condition = implode(' && ', $parts);
5646: $descriptions[$key] = sprintf(
5647: 'if %s then %s is %s (%s)',
5648: $condition,
5649: $exprString,
5650: $holder->getTypeHolder()->getType()->describe(VerbosityLevel::precise()),
5651: $holder->getTypeHolder()->getCertainty()->describe(),
5652: );
5653: }
5654: }
5655:
5656: return $descriptions;
5657: }
5658:
5659: public function filterTypeWithMethod(Type $typeWithMethod, string $methodName): ?Type
5660: {
5661: if ($typeWithMethod instanceof UnionType) {
5662: $typeWithMethod = $typeWithMethod->filterTypes(static fn (Type $innerType) => $innerType->hasMethod($methodName)->yes());
5663: if ($typeWithMethod instanceof NeverType) {
5664: return null;
5665: }
5666: } elseif (!$typeWithMethod->hasMethod($methodName)->yes()) {
5667: return null;
5668: }
5669:
5670: return $typeWithMethod;
5671: }
5672:
5673: /** @api */
5674: public function getMethodReflection(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection
5675: {
5676: $type = $this->filterTypeWithMethod($typeWithMethod, $methodName);
5677: if ($type === null) {
5678: return null;
5679: }
5680:
5681: return $type->getMethod($methodName, $this);
5682: }
5683:
5684: public function getNakedMethod(Type $typeWithMethod, string $methodName): ?ExtendedMethodReflection
5685: {
5686: $type = $this->filterTypeWithMethod($typeWithMethod, $methodName);
5687: if ($type === null) {
5688: return null;
5689: }
5690:
5691: return $type->getUnresolvedMethodPrototype($methodName, $this)->getNakedMethod();
5692: }
5693:
5694: /**
5695: * @api
5696: * @deprecated Use getInstancePropertyReflection or getStaticPropertyReflection instead
5697: */
5698: public function getPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection
5699: {
5700: if ($typeWithProperty instanceof UnionType) {
5701: $typeWithProperty = $typeWithProperty->filterTypes(static fn (Type $innerType) => $innerType->hasProperty($propertyName)->yes());
5702: if ($typeWithProperty instanceof NeverType) {
5703: return null;
5704: }
5705: } elseif (!$typeWithProperty->hasProperty($propertyName)->yes()) {
5706: return null;
5707: }
5708:
5709: return $typeWithProperty->getProperty($propertyName, $this);
5710: }
5711:
5712: /** @api */
5713: public function getInstancePropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection
5714: {
5715: if ($typeWithProperty instanceof UnionType) {
5716: $typeWithProperty = $typeWithProperty->filterTypes(static fn (Type $innerType) => $innerType->hasInstanceProperty($propertyName)->yes());
5717: if ($typeWithProperty instanceof NeverType) {
5718: return null;
5719: }
5720: }
5721: if (!$typeWithProperty->hasInstanceProperty($propertyName)->yes()) {
5722: return null;
5723: }
5724:
5725: return $typeWithProperty->getInstanceProperty($propertyName, $this);
5726: }
5727:
5728: /** @api */
5729: public function getStaticPropertyReflection(Type $typeWithProperty, string $propertyName): ?ExtendedPropertyReflection
5730: {
5731: if ($typeWithProperty instanceof UnionType) {
5732: $typeWithProperty = $typeWithProperty->filterTypes(static fn (Type $innerType) => $innerType->hasStaticProperty($propertyName)->yes());
5733: if ($typeWithProperty instanceof NeverType) {
5734: return null;
5735: }
5736: }
5737: if (!$typeWithProperty->hasStaticProperty($propertyName)->yes()) {
5738: return null;
5739: }
5740:
5741: return $typeWithProperty->getStaticProperty($propertyName, $this);
5742: }
5743:
5744: public function getConstantReflection(Type $typeWithConstant, string $constantName): ?ClassConstantReflection
5745: {
5746: if ($typeWithConstant instanceof UnionType) {
5747: $typeWithConstant = $typeWithConstant->filterTypes(static fn (Type $innerType) => $innerType->hasConstant($constantName)->yes());
5748:
5749: if ($typeWithConstant instanceof NeverType) {
5750: return null;
5751: }
5752: } elseif (!$typeWithConstant->hasConstant($constantName)->yes()) {
5753: return null;
5754: }
5755:
5756: return $typeWithConstant->getConstant($constantName);
5757: }
5758:
5759: public function getConstantExplicitTypeFromConfig(string $constantName, Type $constantType): Type
5760: {
5761: return $this->services->constantResolver->resolveConstantType($constantName, $constantType);
5762: }
5763:
5764: /**
5765: * @return array<string, ExpressionTypeHolder>
5766: */
5767: private function getConstantTypes(): array
5768: {
5769: $constantTypes = [];
5770: foreach ($this->expressionTypes as $exprString => $typeHolder) {
5771: $expr = $typeHolder->getExpr();
5772: if (!$expr instanceof ConstFetch) {
5773: continue;
5774: }
5775: $constantTypes[$exprString] = $typeHolder;
5776: }
5777: return $constantTypes;
5778: }
5779:
5780: private function getGlobalConstantType(Name $name): ?Type
5781: {
5782: // the namespace only takes part for a name that is not already fully qualified
5783: $namespace = $name->isFullyQualified() ? null : $this->getNamespace();
5784: $nameString = $name->toString();
5785: $cacheKey = get_class($name) . "\0" . $nameString . "\0" . ($namespace ?? "\0");
5786:
5787: $exprStrings = self::$globalConstantFetchKeys[$cacheKey] ?? null;
5788: if ($exprStrings === null) {
5789: $exprStrings = [];
5790: foreach (self::createGlobalConstantFetches($name, $nameString, $namespace) as $constFetch) {
5791: $exprStrings[] = $this->getNodeKey($constFetch);
5792: }
5793:
5794: if (count(self::$globalConstantFetchKeys) < self::GLOBAL_CONSTANT_FETCH_KEYS_LIMIT) {
5795: self::$globalConstantFetchKeys[$cacheKey] = $exprStrings;
5796: }
5797: }
5798:
5799: foreach ($exprStrings as $i => $exprString) {
5800: // what hasExpressionType() does for a node that is not a Variable: the
5801: // key is all it looks at, and the key is exactly what is memoized above
5802: $typeHolder = $this->expressionTypes[$exprString] ?? null;
5803: if ($typeHolder === null || !$typeHolder->getCertainty()->yes()) {
5804: continue;
5805: }
5806:
5807: return $this->getType(self::createGlobalConstantFetches($name, $nameString, $namespace)[$i]);
5808: }
5809:
5810: return null;
5811: }
5812:
5813: /**
5814: * The nodes a global constant name is looked up as, in priority order: the
5815: * current namespace's constant, the global one, then the name as written.
5816: *
5817: * Fresh nodes on every call by design - everything that keys on node identity
5818: * (ExpressionResultStorage, NodeScopeResolver's processed-node guards) must
5819: * keep seeing a node of its own; only the keys they print as are memoized.
5820: *
5821: * @param non-empty-string $nameString
5822: * @return list<ConstFetch>
5823: */
5824: private static function createGlobalConstantFetches(Name $name, string $nameString, ?string $namespace): array
5825: {
5826: $fetches = [];
5827: if ($namespace !== null) {
5828: $fetches[] = new ConstFetch(new FullyQualified([$namespace, $nameString]));
5829: }
5830:
5831: $fetches[] = new ConstFetch(new FullyQualified($nameString));
5832: $fetches[] = new ConstFetch($name);
5833:
5834: return $fetches;
5835: }
5836:
5837: /**
5838: * @return array<string, ExpressionTypeHolder>
5839: */
5840: private function getNativeConstantTypes(): array
5841: {
5842: $constantTypes = [];
5843: foreach ($this->nativeExpressionTypes as $exprString => $typeHolder) {
5844: $expr = $typeHolder->getExpr();
5845: if (!$expr instanceof ConstFetch) {
5846: continue;
5847: }
5848: $constantTypes[$exprString] = $typeHolder;
5849: }
5850: return $constantTypes;
5851: }
5852:
5853: public function getIterableKeyType(Type $iteratee): Type
5854: {
5855: if ($iteratee instanceof UnionType) {
5856: $filtered = $iteratee->filterTypes(static fn (Type $innerType) => $innerType->isIterable()->yes());
5857: if (!$filtered instanceof NeverType) {
5858: $iteratee = $filtered;
5859: }
5860: }
5861:
5862: return $iteratee->getIterableKeyType();
5863: }
5864:
5865: public function getIterableValueType(Type $iteratee): Type
5866: {
5867: if ($iteratee instanceof UnionType) {
5868: $filtered = $iteratee->filterTypes(static fn (Type $innerType) => $innerType->isIterable()->yes());
5869: if (!$filtered instanceof NeverType) {
5870: $iteratee = $filtered;
5871: }
5872: }
5873:
5874: return $iteratee->getIterableValueType();
5875: }
5876:
5877: public function getPhpVersion(): PhpVersions
5878: {
5879: $constType = $this->getGlobalConstantType(new Name('PHP_VERSION_ID'));
5880: if ($constType !== null && !$this->isOverallPhpVersionRange($constType)) {
5881: if (
5882: $constType instanceof IntegerRangeType
5883: && $constType->getMin() === ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID
5884: && $constType->getMax() !== null
5885: && $this->getConfiguredPhpVersionRange() === null
5886: ) {
5887: // A guard like `PHP_VERSION_ID < 80400` only gives an upper bound. Without
5888: // a configured version range, the lower bound is merely the lowest version
5889: // PHPStan knows, while outside the guard the single analysed PHP version
5890: // applies. Keep the lower bound at that version, or as close to it as
5891: // the guard allows.
5892: $maxVersionId = $constType->getMax();
5893: return new PhpVersions(IntegerRangeType::fromInterval(
5894: min($this->services->phpVersion->getVersionId(), $maxVersionId),
5895: $maxVersionId,
5896: ));
5897: }
5898:
5899: return new PhpVersions($constType);
5900: }
5901:
5902: $configuredRange = $this->getConfiguredPhpVersionRange();
5903: if ($configuredRange !== null) {
5904: return new PhpVersions($configuredRange);
5905: }
5906:
5907: return new PhpVersions(new ConstantIntegerType($this->services->phpVersion->getVersionId()));
5908: }
5909:
5910: /**
5911: * The analysed PHP version range comes either from the NEON phpVersion min/max
5912: * config or from the composer.json "require.php" constraint - the very same
5913: * source ConstantResolver narrows PHP_VERSION_ID with, so that
5914: * Scope::getPhpVersion() never contradicts the PHP_VERSION_ID constant.
5915: * Null when neither is set and a single PHP version is analysed.
5916: */
5917: private function getConfiguredPhpVersionRange(): ?Type
5918: {
5919: [$minPhpVersion, $maxPhpVersion] = $this->services->configuredPhpVersionRangeHelper->getVersionRange();
5920: if (
5921: $minPhpVersion === null
5922: && ($maxPhpVersion === null || $maxPhpVersion->getVersionId() === PhpVersionFactory::MAX_PHP_VERSION)
5923: ) {
5924: return null;
5925: }
5926:
5927: return IntegerRangeType::fromInterval(
5928: $minPhpVersion !== null ? $minPhpVersion->getVersionId() : ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID,
5929: $maxPhpVersion !== null ? $maxPhpVersion->getVersionId() : null,
5930: );
5931: }
5932:
5933: /**
5934: * Whether the type carries no information about the analysed PHP version,
5935: * i.e. it spans everything PHPStan is able to analyse.
5936: */
5937: private function isOverallPhpVersionRange(Type $type): bool
5938: {
5939: return $type instanceof IntegerRangeType
5940: && $type->getMin() === ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID
5941: && ($type->getMax() === null || $type->getMax() === PhpVersionFactory::MAX_PHP_VERSION);
5942: }
5943:
5944: public function invokeNodeCallback(Node $node): void
5945: {
5946: $nodeCallback = $this->services->nodeCallback;
5947: if ($nodeCallback === null) {
5948: throw new ShouldNotHappenException('Node callback is not present in this scope');
5949: }
5950:
5951: $nodeCallback($node, $this);
5952: }
5953:
5954: /**
5955: * @template TNodeType of Node
5956: * @template TValue
5957: * @param class-string<Collector<TNodeType, TValue>> $collectorType
5958: * @param TValue $data
5959: */
5960: public function emitCollectedData(string $collectorType, mixed $data): void
5961: {
5962: $nodeCallback = $this->services->nodeCallback;
5963: if ($nodeCallback === null) {
5964: throw new ShouldNotHappenException('Node callback is not present in this scope');
5965: }
5966:
5967: $nodeCallback(new EmitCollectedDataNode($collectorType, $data), $this);
5968: }
5969:
5970: public function trackValueDependency(string $extensionClass, string $key): void
5971: {
5972: // a scope without a node callback is outside the walk of the analysed file - see ValueDependencyCollector
5973: $this->services->container->getByType(ValueDependencyCollector::class)->record($extensionClass, $key, $this, $this->services->nodeCallback !== null);
5974: }
5975:
5976: public function trackFileDependency(string $file): void
5977: {
5978: $this->services->container->getByType(ValueDependencyCollector::class)->recordFile($file, $this, $this->services->nodeCallback !== null);
5979: }
5980:
5981: public function trackDirectoryDependency(string $directory, string $pattern = '*'): void
5982: {
5983: $this->services->container->getByType(ValueDependencyCollector::class)->recordDirectory($directory, $pattern, $this, $this->services->nodeCallback !== null);
5984: }
5985:
5986: public function trackClassDependency(string $className): void
5987: {
5988: $this->services->container->getByType(ValueDependencyCollector::class)->recordClass($className, $this, $this->services->nodeCallback !== null);
5989: }
5990:
5991: }
5992: