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