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