1: <?php declare(strict_types = 1);
2:
3: namespace PHPStan\Reflection;
4:
5: use Closure;
6: use Nette\Utils\Strings;
7: use PhpParser\Node\Arg;
8: use PhpParser\Node\ComplexType;
9: use PhpParser\Node\Expr;
10: use PhpParser\Node\Expr\BinaryOp;
11: use PhpParser\Node\Expr\Cast\Array_;
12: use PhpParser\Node\Expr\Cast\Bool_;
13: use PhpParser\Node\Expr\Cast\Double;
14: use PhpParser\Node\Expr\Cast\Object_;
15: use PhpParser\Node\Expr\ClassConstFetch;
16: use PhpParser\Node\Expr\ConstFetch;
17: use PhpParser\Node\Expr\FuncCall;
18: use PhpParser\Node\Expr\New_;
19: use PhpParser\Node\Expr\PropertyFetch;
20: use PhpParser\Node\Expr\Variable;
21: use PhpParser\Node\Identifier;
22: use PhpParser\Node\Name;
23: use PhpParser\Node\Param;
24: use PhpParser\Node\Scalar\Float_;
25: use PhpParser\Node\Scalar\Int_;
26: use PhpParser\Node\Scalar\MagicConst;
27: use PhpParser\Node\Scalar\MagicConst\Dir;
28: use PhpParser\Node\Scalar\MagicConst\File;
29: use PhpParser\Node\Scalar\MagicConst\Line;
30: use PhpParser\Node\Scalar\String_;
31: use PHPStan\Analyser\ConstantResolver;
32: use PHPStan\Analyser\OutOfClassScope;
33: use PHPStan\DependencyInjection\AutowiredParameter;
34: use PHPStan\DependencyInjection\AutowiredService;
35: use PHPStan\Node\Expr\TypeExpr;
36: use PHPStan\Php\PhpVersion;
37: use PHPStan\PhpDoc\Tag\TemplateTag;
38: use PHPStan\Reflection\Callables\CallableParametersAcceptor;
39: use PHPStan\Reflection\Callables\SimpleImpurePoint;
40: use PHPStan\Reflection\Callables\SimpleThrowPoint;
41: use PHPStan\Reflection\Native\NativeParameterReflection;
42: use PHPStan\Reflection\ReflectionProvider\ReflectionProviderProvider;
43: use PHPStan\ShouldNotHappenException;
44: use PHPStan\TrinaryLogic;
45: use PHPStan\Turbo\ShadowedByTurboExtension;
46: use PHPStan\Type\Accessory\AccessoryArrayListType;
47: use PHPStan\Type\Accessory\AccessoryLiteralStringType;
48: use PHPStan\Type\Accessory\AccessoryLowercaseStringType;
49: use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
50: use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
51: use PHPStan\Type\Accessory\AccessoryNumericStringType;
52: use PHPStan\Type\Accessory\AccessoryUppercaseStringType;
53: use PHPStan\Type\Accessory\HasOffsetValueType;
54: use PHPStan\Type\Accessory\NonEmptyArrayType;
55: use PHPStan\Type\ArrayType;
56: use PHPStan\Type\BenevolentUnionType;
57: use PHPStan\Type\BooleanType;
58: use PHPStan\Type\CallableAssertionsHelper;
59: use PHPStan\Type\ClassStringType;
60: use PHPStan\Type\ClosureType;
61: use PHPStan\Type\Constant\ConstantArrayType;
62: use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
63: use PHPStan\Type\Constant\ConstantBooleanType;
64: use PHPStan\Type\Constant\ConstantFloatType;
65: use PHPStan\Type\Constant\ConstantIntegerType;
66: use PHPStan\Type\Constant\ConstantStringType;
67: use PHPStan\Type\Constant\OversizedArrayBuilder;
68: use PHPStan\Type\ConstantScalarType;
69: use PHPStan\Type\ConstantTypeHelper;
70: use PHPStan\Type\Enum\EnumCaseObjectType;
71: use PHPStan\Type\ErrorType;
72: use PHPStan\Type\FloatType;
73: use PHPStan\Type\GeneralizePrecision;
74: use PHPStan\Type\Generic\GenericClassStringType;
75: use PHPStan\Type\Generic\TemplateType;
76: use PHPStan\Type\Generic\TemplateTypeMap;
77: use PHPStan\Type\Generic\TemplateTypeVarianceMap;
78: use PHPStan\Type\IntegerRangeType;
79: use PHPStan\Type\IntegerType;
80: use PHPStan\Type\IntersectionType;
81: use PHPStan\Type\MixedType;
82: use PHPStan\Type\NeverType;
83: use PHPStan\Type\NonexistentParentClassType;
84: use PHPStan\Type\NullType;
85: use PHPStan\Type\ObjectShapeType;
86: use PHPStan\Type\ObjectType;
87: use PHPStan\Type\ObjectWithoutClassType;
88: use PHPStan\Type\OperatorTypeSpecifyingExtensionRegistry;
89: use PHPStan\Type\ParserNodeTypeToPHPStanType;
90: use PHPStan\Type\StaticType;
91: use PHPStan\Type\StringType;
92: use PHPStan\Type\ThisType;
93: use PHPStan\Type\Type;
94: use PHPStan\Type\TypeCombinator;
95: use PHPStan\Type\TypehintHelper;
96: use PHPStan\Type\TypeResult;
97: use PHPStan\Type\TypeUtils;
98: use PHPStan\Type\TypeWithClassName;
99: use PHPStan\Type\UnaryOperatorTypeSpecifyingExtensionRegistry;
100: use PHPStan\Type\UnionType;
101: use stdClass;
102: use Throwable;
103: use function abs;
104: use function array_key_exists;
105: use function array_keys;
106: use function array_map;
107: use function array_merge;
108: use function array_values;
109: use function assert;
110: use function ceil;
111: use function count;
112: use function dirname;
113: use function floor;
114: use function in_array;
115: use function intval;
116: use function is_finite;
117: use function is_float;
118: use function is_int;
119: use function is_numeric;
120: use function is_string;
121: use function max;
122: use function min;
123: use function sprintf;
124: use function str_starts_with;
125: use function strtolower;
126: use const INF;
127: use const PHP_INT_MAX;
128: use const PHP_INT_MIN;
129:
130: #[AutowiredService]
131: #[ShadowedByTurboExtension(implementation: __DIR__ . '/../../turbo-ext/src/InitializerExprTypeResolver.cpp')]
132: final class InitializerExprTypeResolver
133: {
134:
135: public const CALCULATE_SCALARS_LIMIT = 128;
136:
137: /** @var array<string, true> */
138: private array $currentlyResolvingClassConstant = [];
139:
140: /** @var array<string, Type> */
141: private array $classConstantValueTypeCache = [];
142:
143: public function __construct(
144: private ConstantResolver $constantResolver,
145: private ReflectionProviderProvider $reflectionProviderProvider,
146: private PhpVersion $phpVersion,
147: private OperatorTypeSpecifyingExtensionRegistry $operatorTypeSpecifyingExtensionRegistry,
148: private UnaryOperatorTypeSpecifyingExtensionRegistry $unaryOperatorTypeSpecifyingExtensionRegistry,
149: private OversizedArrayBuilder $oversizedArrayBuilder,
150: #[AutowiredParameter]
151: private bool $usePathConstantsAsConstantString,
152: )
153: {
154: }
155:
156: /** @api */
157: public function getType(Expr $expr, InitializerExprContext $context): Type
158: {
159: if ($expr instanceof TypeExpr) {
160: return $expr->getExprType();
161: }
162: if ($expr instanceof Int_) {
163: return new ConstantIntegerType($expr->value);
164: }
165: if ($expr instanceof Float_) {
166: return new ConstantFloatType($expr->value);
167: }
168: if ($expr instanceof String_) {
169: return new ConstantStringType($expr->value);
170: }
171: if ($expr instanceof ConstFetch) {
172: $constName = (string) $expr->name;
173: $loweredConstName = strtolower($constName);
174: if ($loweredConstName === 'true') {
175: return new ConstantBooleanType(true);
176: } elseif ($loweredConstName === 'false') {
177: return new ConstantBooleanType(false);
178: } elseif ($loweredConstName === 'null') {
179: return new NullType();
180: }
181:
182: $constant = $this->constantResolver->resolveConstant($expr->name, $context);
183: if ($constant !== null) {
184: return $constant;
185: }
186:
187: return new ErrorType();
188: }
189: if ($expr instanceof File) {
190: $file = $context->getFile();
191: if ($file === null) {
192: return new StringType();
193: }
194: $stringType = new ConstantStringType($file);
195: return $this->usePathConstantsAsConstantString ? $stringType : $stringType->generalize(GeneralizePrecision::moreSpecific());
196: }
197: if ($expr instanceof Dir) {
198: $file = $context->getFile();
199: if ($file === null) {
200: return new StringType();
201: }
202: $stringType = new ConstantStringType(dirname($file));
203: return $this->usePathConstantsAsConstantString ? $stringType : $stringType->generalize(GeneralizePrecision::moreSpecific());
204: }
205: if ($expr instanceof Line) {
206: return new ConstantIntegerType($expr->getStartLine());
207: }
208: if ($expr instanceof Expr\New_) {
209: if ($expr->class instanceof Name) {
210: return new ObjectType((string) $expr->class);
211: }
212:
213: return new ObjectWithoutClassType();
214: }
215: if ($expr instanceof Expr\Array_) {
216: return $this->getArrayType($expr, fn (Expr $expr): Type => $this->getType($expr, $context));
217: }
218: if ($expr instanceof Expr\Cast) {
219: return $this->getCastType($expr, fn (Expr $expr): Type => $this->getType($expr, $context));
220: }
221: if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) {
222: return $this->getFirstClassCallableType($expr, $context, false);
223: }
224: if ($expr instanceof Expr\Closure && $expr->static) {
225: $parameters = [];
226: $isVariadic = false;
227: $firstOptionalParameterIndex = null;
228: foreach ($expr->params as $i => $param) {
229: $isOptionalCandidate = $param->default !== null || $param->variadic;
230:
231: if ($isOptionalCandidate) {
232: if ($firstOptionalParameterIndex === null) {
233: $firstOptionalParameterIndex = $i;
234: }
235: } else {
236: $firstOptionalParameterIndex = null;
237: }
238: }
239:
240: foreach ($expr->params as $i => $param) {
241: if ($param->variadic) {
242: $isVariadic = true;
243: }
244: if (!$param->var instanceof Variable || !is_string($param->var->name)) {
245: throw new ShouldNotHappenException();
246: }
247: $parameters[] = new NativeParameterReflection(
248: $param->var->name,
249: $firstOptionalParameterIndex !== null && $i >= $firstOptionalParameterIndex,
250: $this->getFunctionType($param->type, $this->isParameterValueNullable($param), false, $context),
251: $param->byRef
252: ? PassedByReference::createCreatesNewVariable()
253: : PassedByReference::createNo(),
254: $param->variadic,
255: $param->default !== null ? $this->getType($param->default, $context) : null,
256: );
257: }
258:
259: $returnType = new MixedType(false);
260: if ($expr->returnType !== null) {
261: $returnType = $this->getFunctionType($expr->returnType, false, false, $context);
262: }
263:
264: return new ClosureType(
265: $parameters,
266: $returnType,
267: $isVariadic,
268: TemplateTypeMap::createEmpty(),
269: TemplateTypeMap::createEmpty(),
270: TemplateTypeVarianceMap::createEmpty(),
271: acceptsNamedArguments: TrinaryLogic::createYes(),
272: isStatic: TrinaryLogic::createYes(),
273: );
274: }
275: if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) {
276: $var = $this->getType($expr->var, $context);
277: $dim = $this->getType($expr->dim, $context);
278: return $var->getOffsetValueType($dim);
279: }
280: if ($expr instanceof ClassConstFetch && $expr->name instanceof Identifier) {
281: return $this->getClassConstFetchType($expr->class, $expr->name->toString(), $context->getClassName(), fn (Expr $expr): Type => $this->getType($expr, $context));
282: }
283: if ($expr instanceof Expr\UnaryPlus) {
284: return $this->getUnaryPlusType($expr->expr, fn (Expr $expr): Type => $this->getType($expr, $context));
285: }
286: if ($expr instanceof Expr\UnaryMinus) {
287: return $this->getUnaryMinusType($expr->expr, fn (Expr $expr): Type => $this->getType($expr, $context));
288: }
289: if ($expr instanceof Expr\BinaryOp\Coalesce) {
290: $leftType = $this->getType($expr->left, $context);
291: $rightType = $this->getType($expr->right, $context);
292:
293: return TypeCombinator::union(TypeCombinator::removeNull($leftType), $rightType);
294: }
295:
296: if ($expr instanceof Expr\Ternary) {
297: $elseType = $this->getType($expr->else, $context);
298: if ($expr->if === null) {
299: $condType = $this->getType($expr->cond, $context);
300:
301: return TypeCombinator::union(
302: TypeCombinator::removeFalsey($condType),
303: $elseType,
304: );
305: }
306:
307: $ifType = $this->getType($expr->if, $context);
308:
309: return TypeCombinator::union(
310: TypeCombinator::removeFalsey($ifType),
311: $elseType,
312: );
313: }
314:
315: if ($expr instanceof Expr\FuncCall && $expr->name instanceof Name && $expr->name->toLowerString() === 'constant') {
316: $firstArg = $expr->args[0] ?? null;
317: if ($firstArg instanceof Arg && $firstArg->value instanceof String_) {
318: $constant = $this->constantResolver->resolvePredefinedConstant($firstArg->value->value);
319: if ($constant !== null) {
320: return $constant;
321: }
322: }
323: }
324:
325: if ($expr instanceof Expr\BooleanNot) {
326: $exprBooleanType = $this->getType($expr->expr, $context)->toBoolean();
327:
328: if ($exprBooleanType instanceof ConstantBooleanType) {
329: return new ConstantBooleanType(!$exprBooleanType->getValue());
330: }
331:
332: return new BooleanType();
333: }
334:
335: if ($expr instanceof Expr\BitwiseNot) {
336: return $this->getBitwiseNotType($expr->expr, fn (Expr $expr): Type => $this->getType($expr, $context));
337: }
338:
339: if ($expr instanceof Expr\BinaryOp\Concat) {
340: return $this->getConcatType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context));
341: }
342:
343: if ($expr instanceof Expr\BinaryOp\BitwiseAnd) {
344: return $this->getBitwiseAndType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context));
345: }
346:
347: if ($expr instanceof Expr\BinaryOp\BitwiseOr) {
348: return $this->getBitwiseOrType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context));
349: }
350:
351: if ($expr instanceof Expr\BinaryOp\BitwiseXor) {
352: return $this->getBitwiseXorType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context));
353: }
354:
355: if ($expr instanceof Expr\BinaryOp\Spaceship) {
356: return $this->getSpaceshipType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context));
357: }
358:
359: if (
360: $expr instanceof Expr\BinaryOp\BooleanAnd
361: || $expr instanceof Expr\BinaryOp\LogicalAnd
362: || $expr instanceof Expr\BinaryOp\BooleanOr
363: || $expr instanceof Expr\BinaryOp\LogicalOr
364: ) {
365: return new BooleanType();
366: }
367:
368: if ($expr instanceof Expr\BinaryOp\Div) {
369: return $this->getDivType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context));
370: }
371:
372: if ($expr instanceof Expr\BinaryOp\Mod) {
373: return $this->getModType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context));
374: }
375:
376: if ($expr instanceof Expr\BinaryOp\Plus) {
377: return $this->getPlusType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context));
378: }
379:
380: if ($expr instanceof Expr\BinaryOp\Minus) {
381: return $this->getMinusType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context));
382: }
383:
384: if ($expr instanceof Expr\BinaryOp\Mul) {
385: return $this->getMulType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context));
386: }
387:
388: if ($expr instanceof Expr\BinaryOp\Pow) {
389: return $this->getPowType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context));
390: }
391:
392: if ($expr instanceof Expr\BinaryOp\ShiftLeft) {
393: return $this->getShiftLeftType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context));
394: }
395:
396: if ($expr instanceof Expr\BinaryOp\ShiftRight) {
397: return $this->getShiftRightType($expr->left, $expr->right, fn (Expr $expr): Type => $this->getType($expr, $context));
398: }
399:
400: if ($expr instanceof BinaryOp\Identical) {
401: return $this->resolveIdenticalType(
402: $this->getType($expr->left, $context),
403: $this->getType($expr->right, $context),
404: )->type;
405: }
406:
407: if ($expr instanceof BinaryOp\NotIdentical) {
408: return $this->getType(new Expr\BooleanNot(new BinaryOp\Identical($expr->left, $expr->right)), $context);
409: }
410:
411: if ($expr instanceof BinaryOp\Equal) {
412: return $this->resolveEqualType(
413: $this->getType($expr->left, $context),
414: $this->getType($expr->right, $context),
415: )->type;
416: }
417:
418: if ($expr instanceof BinaryOp\NotEqual) {
419: return $this->getType(new Expr\BooleanNot(new BinaryOp\Equal($expr->left, $expr->right)), $context);
420: }
421:
422: if ($expr instanceof Expr\BinaryOp\Smaller) {
423: return $this->getType($expr->left, $context)->isSmallerThan($this->getType($expr->right, $context), $this->phpVersion)->toBooleanType();
424: }
425:
426: if ($expr instanceof Expr\BinaryOp\SmallerOrEqual) {
427: return $this->getType($expr->left, $context)->isSmallerThanOrEqual($this->getType($expr->right, $context), $this->phpVersion)->toBooleanType();
428: }
429:
430: if ($expr instanceof Expr\BinaryOp\Greater) {
431: return $this->getType($expr->right, $context)->isSmallerThan($this->getType($expr->left, $context), $this->phpVersion)->toBooleanType();
432: }
433:
434: if ($expr instanceof Expr\BinaryOp\GreaterOrEqual) {
435: return $this->getType($expr->right, $context)->isSmallerThanOrEqual($this->getType($expr->left, $context), $this->phpVersion)->toBooleanType();
436: }
437:
438: if ($expr instanceof Expr\BinaryOp\LogicalXor) {
439: $leftBooleanType = $this->getType($expr->left, $context)->toBoolean();
440: $rightBooleanType = $this->getType($expr->right, $context)->toBoolean();
441:
442: if (
443: $leftBooleanType instanceof ConstantBooleanType
444: && $rightBooleanType instanceof ConstantBooleanType
445: ) {
446: return new ConstantBooleanType(
447: $leftBooleanType->getValue() xor $rightBooleanType->getValue(),
448: );
449: }
450:
451: return new BooleanType();
452: }
453:
454: if ($expr instanceof MagicConst\Class_) {
455: if ($context->getTraitName() !== null) {
456: return new IntersectionType([
457: new ClassStringType(),
458: new AccessoryLiteralStringType(),
459: ]);
460: }
461:
462: if ($context->getClassName() === null) {
463: return new ConstantStringType('');
464: }
465:
466: return new ConstantStringType($context->getClassName(), true);
467: }
468:
469: if ($expr instanceof MagicConst\Namespace_) {
470: if ($context->getTraitName() !== null) {
471: return new IntersectionType([
472: new StringType(),
473: new AccessoryLiteralStringType(),
474: ]);
475: }
476:
477: return new ConstantStringType($context->getNamespace() ?? '');
478: }
479:
480: if ($expr instanceof MagicConst\Method) {
481: return new ConstantStringType($context->getMethod() ?? '');
482: }
483:
484: if ($expr instanceof MagicConst\Function_) {
485: return new ConstantStringType($context->getFunction() ?? '');
486: }
487:
488: if ($expr instanceof MagicConst\Trait_) {
489: if ($context->getTraitName() === null) {
490: return new ConstantStringType('');
491: }
492:
493: return new ConstantStringType($context->getTraitName(), true);
494: }
495:
496: if ($expr instanceof MagicConst\Property) {
497: $contextProperty = $context->getProperty();
498: if ($contextProperty === null) {
499: return new ConstantStringType('');
500: }
501:
502: return new ConstantStringType($contextProperty);
503: }
504:
505: if ($expr instanceof PropertyFetch && $expr->name instanceof Identifier) {
506: $fetchedOnType = $this->getType($expr->var, $context);
507: if (!$fetchedOnType->hasInstanceProperty($expr->name->name)->yes()) {
508: return new ErrorType();
509: }
510:
511: return $fetchedOnType->getInstanceProperty($expr->name->name, new OutOfClassScope())->getReadableType();
512: }
513:
514: return new MixedType();
515: }
516:
517: /**
518: * @param callable(Expr): Type $getTypeCallback
519: */
520: public function getConcatType(Expr $left, Expr $right, callable $getTypeCallback): Type
521: {
522: $leftType = $getTypeCallback($left);
523: $rightType = $getTypeCallback($right);
524:
525: return $this->resolveConcatType($leftType, $rightType);
526: }
527:
528: public function resolveConcatType(Type $left, Type $right): Type
529: {
530: $leftStringType = $left->toString();
531: $rightStringType = $right->toString();
532: if (TypeCombinator::union(
533: $leftStringType,
534: $rightStringType,
535: ) instanceof ErrorType) {
536: return new ErrorType();
537: }
538:
539: if ($leftStringType instanceof ConstantStringType && $leftStringType->getValue() === '') {
540: return $rightStringType;
541: }
542:
543: if ($rightStringType instanceof ConstantStringType && $rightStringType->getValue() === '') {
544: return $leftStringType;
545: }
546:
547: if ($leftStringType instanceof ConstantStringType && $rightStringType instanceof ConstantStringType) {
548: return $leftStringType->append($rightStringType);
549: }
550:
551: $leftConstantStrings = $leftStringType->getConstantStrings();
552: $rightConstantStrings = $rightStringType->getConstantStrings();
553: $combinedConstantStringsCount = count($leftConstantStrings) * count($rightConstantStrings);
554:
555: // we limit the number of union-types for performance reasons
556: if ($combinedConstantStringsCount > 0 && $combinedConstantStringsCount <= self::CALCULATE_SCALARS_LIMIT) {
557: $strings = [];
558:
559: foreach ($leftConstantStrings as $leftConstantString) {
560: if ($leftConstantString->getValue() === '') {
561: $strings = array_merge($strings, $rightConstantStrings);
562:
563: continue;
564: }
565:
566: foreach ($rightConstantStrings as $rightConstantString) {
567: if ($rightConstantString->getValue() === '') {
568: $strings[] = $leftConstantString;
569:
570: continue;
571: }
572:
573: $strings[] = $leftConstantString->append($rightConstantString);
574: }
575: }
576:
577: if (count($strings) > 0) {
578: return TypeCombinator::union(...$strings);
579: }
580: }
581:
582: $accessoryTypes = [];
583: if ($leftStringType->isNonEmptyString()->and($rightStringType->isNonEmptyString())->yes()) {
584: $accessoryTypes[] = new AccessoryNonFalsyStringType();
585: } elseif ($leftStringType->isNonFalsyString()->or($rightStringType->isNonFalsyString())->yes()) {
586: $accessoryTypes[] = new AccessoryNonFalsyStringType();
587: } elseif ($leftStringType->isNonEmptyString()->or($rightStringType->isNonEmptyString())->yes()) {
588: $accessoryTypes[] = new AccessoryNonEmptyStringType();
589: }
590:
591: if ($leftStringType->isLiteralString()->and($rightStringType->isLiteralString())->yes()) {
592: $accessoryTypes[] = new AccessoryLiteralStringType();
593: }
594:
595: if ($leftStringType->isLowercaseString()->and($rightStringType->isLowercaseString())->yes()) {
596: $accessoryTypes[] = new AccessoryLowercaseStringType();
597: }
598:
599: if ($leftStringType->isUppercaseString()->and($rightStringType->isUppercaseString())->yes()) {
600: $accessoryTypes[] = new AccessoryUppercaseStringType();
601: }
602:
603: $leftNumericStringNonEmpty = TypeCombinator::remove($leftStringType, new ConstantStringType(''));
604: if ($leftNumericStringNonEmpty->isNumericString()->yes()) {
605: $validationCallback = $left->isInteger()->yes()
606: ? static fn (string $value): bool => !str_starts_with($value, '-')
607: : static fn (string $value): bool => Strings::match($value, '#^\d+$#') !== null;
608:
609: $allRightConstantsZeroOrMore = false;
610: foreach ($rightConstantStrings as $rightConstantString) {
611: if ($rightConstantString->getValue() === '') {
612: continue;
613: }
614:
615: if (
616: !is_numeric($rightConstantString->getValue())
617: || !$validationCallback($rightConstantString->getValue())
618: ) {
619: $allRightConstantsZeroOrMore = false;
620: break;
621: }
622:
623: $allRightConstantsZeroOrMore = true;
624: }
625:
626: $zeroOrMoreInteger = IntegerRangeType::fromInterval(0, null);
627: $nonNegativeRight = $allRightConstantsZeroOrMore || $zeroOrMoreInteger->isSuperTypeOf($right)->yes();
628: if ($nonNegativeRight) {
629: $accessoryTypes[] = new AccessoryNumericStringType();
630: }
631: }
632:
633: if (count($accessoryTypes) > 0) {
634: $accessoryTypes[] = new StringType();
635: return new IntersectionType($accessoryTypes);
636: }
637:
638: return new StringType();
639: }
640:
641: /**
642: * @param callable(Expr): Type $getTypeCallback
643: */
644: public function getArrayType(Expr\Array_ $expr, callable $getTypeCallback): Type
645: {
646: if (count($expr->items) > ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT) {
647: return $this->oversizedArrayBuilder->build($expr, $getTypeCallback);
648: }
649:
650: $arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
651: $isList = null;
652: $hasOffsetValueTypes = [];
653: foreach ($expr->items as $arrayItem) {
654: $valueType = $getTypeCallback($arrayItem->value);
655: if ($arrayItem->unpack) {
656: $constantArrays = $valueType->getConstantArrays();
657: if (count($constantArrays) > 0) {
658: $keepStringKeys = $this->phpVersion->supportsArrayUnpackingWithStringKeys();
659: $totalArrays = count($constantArrays);
660:
661: // Unpacking merges string keys by name, while integer keys are always
662: // renumbered, so they're merged by their position among integer keys.
663: // A slot missing from some of the unpacked arrays becomes optional.
664: /** @var array<string, array{keyType: ConstantStringType|null, valueTypes: list<Type>, presentCount: int, anyOptional: bool}> $slots */
665: $slots = [];
666: /** @var list<string> $slotOrder */
667: $slotOrder = [];
668:
669: foreach ($constantArrays as $constantArrayType) {
670: $nextIntegerSlot = 0;
671: foreach ($constantArrayType->getKeyTypes() as $i => $keyType) {
672: if ($keepStringKeys && $keyType->isString()->yes()) {
673: $slotKey = 's' . $keyType->getValue();
674: $slotKeyType = $keyType;
675: } else {
676: $slotKey = 'i' . $nextIntegerSlot;
677: $slotKeyType = null;
678: $nextIntegerSlot++;
679: }
680:
681: if (!isset($slots[$slotKey])) {
682: $slots[$slotKey] = [
683: 'keyType' => $slotKeyType,
684: 'valueTypes' => [],
685: 'presentCount' => 0,
686: 'anyOptional' => false,
687: ];
688: $slotOrder[] = $slotKey;
689: }
690:
691: $slots[$slotKey]['valueTypes'][] = $constantArrayType->getValueTypes()[$i];
692: $slots[$slotKey]['presentCount']++;
693: if (!$constantArrayType->isOptionalKey($i)) {
694: continue;
695: }
696:
697: $slots[$slotKey]['anyOptional'] = true;
698: }
699: }
700:
701: foreach ($slotOrder as $slotKey) {
702: $slot = $slots[$slotKey];
703: $mergedValueType = TypeCombinator::union(...$slot['valueTypes']);
704: $isOptional = $slot['anyOptional'] || $slot['presentCount'] < $totalArrays;
705: $slotKeyType = $slot['keyType'];
706: $arrayBuilder->setOffsetValueType($slotKeyType, $mergedValueType, $isOptional);
707:
708: if ($slotKeyType === null) {
709: continue;
710: }
711:
712: $keyValue = $slotKeyType->getValue();
713: if (isset($hasOffsetValueTypes[$keyValue])) {
714: $hasOffsetValueTypes[$keyValue] = new HasOffsetValueType(
715: $slotKeyType,
716: $isOptional
717: ? TypeCombinator::union($hasOffsetValueTypes[$keyValue]->getValueType(), $mergedValueType)
718: : $mergedValueType,
719: );
720: } elseif (!$isOptional) {
721: $hasOffsetValueTypes[$keyValue] = new HasOffsetValueType($slotKeyType, $mergedValueType);
722: }
723: }
724: } else {
725: $arrayBuilder->degradeToGeneralArray();
726:
727: if ($this->phpVersion->supportsArrayUnpackingWithStringKeys() && !$valueType->getIterableKeyType()->isString()->no()) {
728: $isList = false;
729: $offsetType = $valueType->getIterableKeyType();
730:
731: foreach ($hasOffsetValueTypes as $key => $hasOffsetValueType) {
732: if (!$offsetType->isSuperTypeOf($hasOffsetValueType->getOffsetType())->yes()) {
733: continue;
734: }
735:
736: unset($hasOffsetValueTypes[$key]);
737: }
738: } else {
739: $isList ??= $arrayBuilder->isList();
740: $offsetType = new IntegerType();
741: }
742:
743: $arrayBuilder->setOffsetValueType($offsetType, $valueType->getIterableValueType(), !$valueType->isIterableAtLeastOnce()->yes());
744: }
745: } else {
746: $arrayBuilder->setOffsetValueType(
747: $arrayItem->key !== null ? $getTypeCallback($arrayItem->key) : null,
748: $valueType,
749: );
750: }
751: }
752:
753: $arrayType = $arrayBuilder->getArray();
754: if ($isList === true) {
755: $arrayType = TypeCombinator::intersect($arrayType, new AccessoryArrayListType());
756: }
757:
758: if (count($hasOffsetValueTypes) > 0 && !$arrayType->isConstantArray()->yes()) {
759: $arrayType = TypeCombinator::intersect($arrayType, ...array_values($hasOffsetValueTypes));
760: }
761:
762: return $arrayType;
763: }
764:
765: /**
766: * @param callable(Expr): Type $getTypeCallback
767: */
768: public function getCastType(Expr\Cast $expr, callable $getTypeCallback): Type
769: {
770: if ($expr instanceof \PhpParser\Node\Expr\Cast\Int_) {
771: return $getTypeCallback($expr->expr)->toInteger();
772: }
773: if ($expr instanceof Bool_) {
774: return $getTypeCallback($expr->expr)->toBoolean();
775: }
776: if ($expr instanceof Double) {
777: return $getTypeCallback($expr->expr)->toFloat();
778: }
779: if ($expr instanceof \PhpParser\Node\Expr\Cast\String_) {
780: return $getTypeCallback($expr->expr)->toString();
781: }
782: if ($expr instanceof Array_) {
783: return $getTypeCallback($expr->expr)->toArray();
784: }
785: if ($expr instanceof Object_) {
786: return $this->getCastObjectType($getTypeCallback($expr->expr));
787: }
788:
789: return new MixedType();
790: }
791:
792: public function getCastObjectType(Type $exprType): Type
793: {
794: $castToObject = static function (Type $type): Type {
795: $constantArrays = $type->getConstantArrays();
796: if (count($constantArrays) > 0) {
797: $objects = [];
798: foreach ($constantArrays as $constantArray) {
799: $properties = [];
800: $optionalProperties = [];
801: foreach ($constantArray->getKeyTypes() as $i => $keyType) {
802: $valueType = $constantArray->getValueTypes()[$i];
803: $optional = $constantArray->isOptionalKey($i);
804: if ($optional) {
805: $optionalProperties[] = $keyType->getValue();
806: }
807: $properties[$keyType->getValue()] = $valueType;
808: }
809:
810: $objects[] = new IntersectionType([new ObjectShapeType($properties, $optionalProperties), new ObjectType(stdClass::class)]);
811: }
812:
813: return TypeCombinator::union(...$objects);
814: }
815: if ($type->isObject()->yes()) {
816: return $type;
817: }
818:
819: return new ObjectType('stdClass');
820: };
821:
822: if ($exprType instanceof UnionType) {
823: return TypeCombinator::union(...array_map($castToObject, $exprType->getTypes()));
824: }
825:
826: return $castToObject($exprType);
827: }
828:
829: /**
830: * @param Name|Identifier|ComplexType|null $type
831: */
832: public function getFunctionType($type, bool $isNullable, bool $isVariadic, InitializerExprContext $context): Type
833: {
834: if ($isNullable) {
835: return TypeCombinator::addNull(
836: $this->getFunctionType($type, false, $isVariadic, $context),
837: );
838: }
839: if ($isVariadic) {
840: if ($this->phpVersion->supportsNamedArguments()) {
841: return new ArrayType(new UnionType([IntegerRangeType::createAllGreaterThanOrEqualTo(0), new StringType()]), $this->getFunctionType(
842: $type,
843: false,
844: false,
845: $context,
846: ));
847: }
848:
849: return new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $this->getFunctionType(
850: $type,
851: false,
852: false,
853: $context,
854: )), new AccessoryArrayListType()]);
855: }
856:
857: if ($type instanceof Name) {
858: $className = (string) $type;
859: $lowercasedClassName = strtolower($className);
860: if ($lowercasedClassName === 'parent') {
861: $classReflection = null;
862: if ($context->getClassName() !== null && $this->getReflectionProvider()->hasClass($context->getClassName())) {
863: $classReflection = $this->getReflectionProvider()->getClass($context->getClassName());
864: }
865: if ($classReflection !== null && $classReflection->getParentClass() !== null) {
866: return new ObjectType($classReflection->getParentClass()->getName());
867: }
868:
869: return new NonexistentParentClassType();
870: }
871: }
872:
873: $classReflection = null;
874: if ($context->getClassName() !== null && $this->getReflectionProvider()->hasClass($context->getClassName())) {
875: $classReflection = $this->getReflectionProvider()->getClass($context->getClassName());
876: }
877:
878: return ParserNodeTypeToPHPStanType::resolve($type, $classReflection);
879: }
880:
881: private function isParameterValueNullable(Param $parameter): bool
882: {
883: if ($parameter->default instanceof ConstFetch) {
884: return strtolower((string) $parameter->default->name) === 'null';
885: }
886:
887: return false;
888: }
889:
890: public function getFirstClassCallableType(Expr\CallLike $expr, InitializerExprContext $context, bool $nativeTypesPromoted): Type
891: {
892: if ($expr instanceof FuncCall) {
893: if ($expr->name instanceof Name) {
894: if ($this->getReflectionProvider()->hasFunction($expr->name, $context)) {
895: $function = $this->getReflectionProvider()->getFunction($expr->name, $context);
896: return $this->createFirstClassCallable(
897: $function,
898: $function->getVariants(),
899: $nativeTypesPromoted,
900: );
901: }
902:
903: return new ObjectType(Closure::class);
904: }
905: }
906:
907: if ($expr instanceof Expr\StaticCall) {
908: if (!$expr->class instanceof Name) {
909: return new ObjectType(Closure::class);
910: }
911:
912: if (!$expr->name instanceof Identifier) {
913: return new ObjectType(Closure::class);
914: }
915:
916: $classReflection = null;
917: if ($context->getClassName() !== null && $this->getReflectionProvider()->hasClass($context->getClassName())) {
918: $classReflection = $this->getReflectionProvider()->getClass($context->getClassName());
919: }
920:
921: $classType = $this->resolveTypeByName($expr->class, $classReflection);
922: $methodName = $expr->name->toString();
923: if (!$classType->hasMethod($methodName)->yes()) {
924: return new ObjectType(Closure::class);
925: }
926:
927: $method = $classType->getMethod($methodName, new OutOfClassScope());
928: $classType = $this->resolveTypeByNameWithLateStaticBinding($expr->class, $classType, $method);
929: if (!$classType->hasMethod($methodName)->yes()) {
930: return new ObjectType(Closure::class);
931: }
932: $method = $classType->getMethod($methodName, new OutOfClassScope());
933:
934: return $this->createFirstClassCallable(
935: $method,
936: $method->getVariants(),
937: $nativeTypesPromoted,
938: );
939: }
940:
941: if ($expr instanceof New_) {
942: return new ErrorType();
943: }
944:
945: throw new ShouldNotHappenException();
946: }
947:
948: /**
949: * @param ParametersAcceptor[] $variants
950: */
951: public function createFirstClassCallable(
952: FunctionReflection|ExtendedMethodReflection|null $function,
953: array $variants,
954: bool $nativeTypesPromoted,
955: ): Type
956: {
957: $closureTypes = [];
958:
959: foreach ($variants as $variant) {
960: $returnType = $variant->getReturnType();
961: if ($variant instanceof ExtendedParametersAcceptor) {
962: $returnType = $nativeTypesPromoted ? $variant->getNativeReturnType() : $returnType;
963: }
964:
965: $templateTags = [];
966: foreach ($variant->getTemplateTypeMap()->getTypes() as $templateType) {
967: if (!$templateType instanceof TemplateType) {
968: continue;
969: }
970: $templateTags[$templateType->getName()] = new TemplateTag(
971: $templateType->getName(),
972: $templateType->getBound(),
973: $templateType->getDefault(),
974: $templateType->getVariance(),
975: );
976: }
977:
978: $throwPoints = [];
979: $impurePoints = [];
980: $acceptsNamedArguments = TrinaryLogic::createYes();
981: $mustUseReturnValue = TrinaryLogic::createMaybe();
982: $isStaticClosure = TrinaryLogic::createMaybe();
983: if ($variant instanceof CallableParametersAcceptor) {
984: $throwPoints = $variant->getThrowPoints();
985: $impurePoints = $variant->getImpurePoints();
986: $acceptsNamedArguments = $variant->acceptsNamedArguments();
987: $mustUseReturnValue = $variant->mustUseReturnValue();
988: $isStaticClosure = $variant->isStaticClosure();
989: } elseif ($function !== null) {
990: $returnTypeForThrow = $variant->getReturnType();
991: $throwType = $function->getThrowType();
992: if ($throwType === null) {
993: if ($returnTypeForThrow instanceof NeverType && $returnTypeForThrow->isExplicit()) {
994: $throwType = new ObjectType(Throwable::class);
995: }
996: }
997:
998: if ($throwType !== null) {
999: if (!$throwType->isVoid()->yes()) {
1000: $throwPoints[] = SimpleThrowPoint::createExplicit($throwType, true);
1001: }
1002: } else {
1003: if (!(new ObjectType(Throwable::class))->isSuperTypeOf($returnTypeForThrow)->yes()) {
1004: $throwPoints[] = SimpleThrowPoint::createImplicit();
1005: }
1006: }
1007:
1008: $impurePoint = SimpleImpurePoint::createFromVariant($function, $variant);
1009: if ($impurePoint !== null) {
1010: $impurePoints[] = $impurePoint;
1011: }
1012:
1013: $acceptsNamedArguments = $function->acceptsNamedArguments();
1014: $mustUseReturnValue = $function->mustUseReturnValue();
1015: }
1016:
1017: $parameters = $variant->getParameters();
1018: if ($function !== null) {
1019: $assertions = $function->getAsserts();
1020: } elseif ($variant instanceof CallableParametersAcceptor) {
1021: $assertions = $variant->getAsserts();
1022: } else {
1023: $assertions = Assertions::createEmpty();
1024: }
1025:
1026: // a conditional return type referencing the function's own parameter,
1027: // like @return ($value is int ? true : false) on is_int(),
1028: // also becomes a type predicate of the resulting Closure
1029: $assertions = CallableAssertionsHelper::withConditionalReturnPredicate($assertions, $variant);
1030: $closureTypes[] = new ClosureType(
1031: $parameters,
1032: $returnType,
1033: $variant->isVariadic(),
1034: $variant->getTemplateTypeMap(),
1035: $variant->getResolvedTemplateTypeMap(),
1036: $variant instanceof ExtendedParametersAcceptor ? $variant->getCallSiteVarianceMap() : TemplateTypeVarianceMap::createEmpty(),
1037: $templateTags,
1038: $throwPoints,
1039: $impurePoints,
1040: acceptsNamedArguments: $acceptsNamedArguments,
1041: mustUseReturnValue: $mustUseReturnValue,
1042: assertions: $assertions,
1043: isStatic: $isStaticClosure,
1044: );
1045: }
1046:
1047: return TypeCombinator::union(...$closureTypes);
1048: }
1049:
1050: /**
1051: * @param callable(Expr): Type $getTypeCallback
1052: */
1053: public function getBitwiseAndType(Expr $left, Expr $right, callable $getTypeCallback): Type
1054: {
1055: $leftType = $getTypeCallback($left);
1056: $rightType = $getTypeCallback($right);
1057:
1058: $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistry
1059: ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseAnd($left, $right), $leftType, $rightType);
1060: if ($specifiedTypes !== null) {
1061: return $specifiedTypes;
1062: }
1063:
1064: if ($leftType instanceof NeverType || $rightType instanceof NeverType) {
1065: return $this->getNeverType($leftType, $rightType);
1066: }
1067:
1068: $result = $this->getFiniteOrConstantScalarTypes($leftType, $rightType, static fn ($a, $b) => $a & $b);
1069: if ($result instanceof Type) {
1070: return $result;
1071: }
1072:
1073: // computed before optimizeScalarType() below widens integer ranges to int
1074: $leftNumberType = $leftType->toNumber();
1075: $rightNumberType = $rightType->toNumber();
1076:
1077: if ($result === self::IS_SCALAR_TYPE) {
1078: $leftType = $this->optimizeScalarType($leftType);
1079: $rightType = $this->optimizeScalarType($rightType);
1080: }
1081:
1082: if ($leftType instanceof MixedType && $rightType instanceof MixedType) {
1083: return new BenevolentUnionType([new IntegerType(), new StringType()]);
1084: }
1085:
1086: $leftIsString = $leftType->isString();
1087: $rightIsString = $rightType->isString();
1088: if (
1089: ($leftIsString->yes() || $leftType instanceof MixedType)
1090: && ($rightIsString->yes() || $rightType instanceof MixedType)
1091: ) {
1092: return new StringType();
1093: }
1094: if ($leftIsString->maybe() && $rightIsString->maybe()) {
1095: return new ErrorType();
1096: }
1097:
1098: if ($leftType->toNumber() instanceof ErrorType || $rightType->toNumber() instanceof ErrorType) {
1099: return new ErrorType();
1100: }
1101:
1102: $bitwiseAndRange = $this->computeBitwiseAndRange($leftNumberType, $rightNumberType);
1103: if ($bitwiseAndRange !== null) {
1104: return $bitwiseAndRange;
1105: }
1106:
1107: return new IntegerType();
1108: }
1109:
1110: /**
1111: * @param callable(Expr): Type $getTypeCallback
1112: */
1113: public function getBitwiseOrType(Expr $left, Expr $right, callable $getTypeCallback): Type
1114: {
1115: $leftType = $getTypeCallback($left);
1116: $rightType = $getTypeCallback($right);
1117:
1118: $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistry
1119: ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseOr($left, $right), $leftType, $rightType);
1120: if ($specifiedTypes !== null) {
1121: return $specifiedTypes;
1122: }
1123:
1124: if ($leftType instanceof NeverType || $rightType instanceof NeverType) {
1125: return $this->getNeverType($leftType, $rightType);
1126: }
1127:
1128: $result = $this->getFiniteOrConstantScalarTypes($leftType, $rightType, static fn ($a, $b) => $a | $b);
1129: if ($result instanceof Type) {
1130: return $result;
1131: }
1132:
1133: // computed before optimizeScalarType() below widens integer ranges to int
1134: $leftNumberType = $leftType->toNumber();
1135: $rightNumberType = $rightType->toNumber();
1136:
1137: if ($result === self::IS_SCALAR_TYPE) {
1138: $leftType = $this->optimizeScalarType($leftType);
1139: $rightType = $this->optimizeScalarType($rightType);
1140: }
1141:
1142: if ($leftType instanceof MixedType && $rightType instanceof MixedType) {
1143: return new BenevolentUnionType([new IntegerType(), new StringType()]);
1144: }
1145:
1146: $leftIsString = $leftType->isString();
1147: $rightIsString = $rightType->isString();
1148: if (
1149: ($leftIsString->yes() || $leftType instanceof MixedType)
1150: && ($rightIsString->yes() || $rightType instanceof MixedType)
1151: ) {
1152: return new StringType();
1153: }
1154: if ($leftIsString->maybe() && $rightIsString->maybe()) {
1155: return new ErrorType();
1156: }
1157:
1158: if ($leftType->toNumber() instanceof ErrorType || $rightType->toNumber() instanceof ErrorType) {
1159: return new ErrorType();
1160: }
1161:
1162: $bitwiseOrRange = $this->computeBitwiseOrXorRange($leftNumberType, $rightNumberType);
1163: if ($bitwiseOrRange !== null) {
1164: return $bitwiseOrRange;
1165: }
1166:
1167: return new IntegerType();
1168: }
1169:
1170: /**
1171: * @param callable(Expr): Type $getTypeCallback
1172: */
1173: public function getBitwiseXorType(Expr $left, Expr $right, callable $getTypeCallback): Type
1174: {
1175: $leftType = $getTypeCallback($left);
1176: $rightType = $getTypeCallback($right);
1177:
1178: $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistry
1179: ->callOperatorTypeSpecifyingExtensions(new BinaryOp\BitwiseXor($left, $right), $leftType, $rightType);
1180: if ($specifiedTypes !== null) {
1181: return $specifiedTypes;
1182: }
1183:
1184: if ($leftType instanceof NeverType || $rightType instanceof NeverType) {
1185: return $this->getNeverType($leftType, $rightType);
1186: }
1187:
1188: $result = $this->getFiniteOrConstantScalarTypes($leftType, $rightType, static fn ($a, $b) => $a ^ $b);
1189: if ($result instanceof Type) {
1190: return $result;
1191: }
1192:
1193: // computed before optimizeScalarType() below widens integer ranges to int
1194: $leftNumberType = $leftType->toNumber();
1195: $rightNumberType = $rightType->toNumber();
1196:
1197: if ($result === self::IS_SCALAR_TYPE) {
1198: $leftType = $this->optimizeScalarType($leftType);
1199: $rightType = $this->optimizeScalarType($rightType);
1200: }
1201:
1202: if ($leftType instanceof MixedType && $rightType instanceof MixedType) {
1203: return new BenevolentUnionType([new IntegerType(), new StringType()]);
1204: }
1205:
1206: $leftIsString = $leftType->isString();
1207: $rightIsString = $rightType->isString();
1208: if (
1209: ($leftIsString->yes() || $leftType instanceof MixedType)
1210: && ($rightIsString->yes() || $rightType instanceof MixedType)
1211: ) {
1212: return new StringType();
1213: }
1214: if ($leftIsString->maybe() && $rightIsString->maybe()) {
1215: return new ErrorType();
1216: }
1217:
1218: if ($leftType->toNumber() instanceof ErrorType || $rightType->toNumber() instanceof ErrorType) {
1219: return new ErrorType();
1220: }
1221:
1222: $bitwiseXorRange = $this->computeBitwiseOrXorRange($leftNumberType, $rightNumberType);
1223: if ($bitwiseXorRange !== null) {
1224: return $bitwiseXorRange;
1225: }
1226:
1227: return new IntegerType();
1228: }
1229:
1230: private const IS_SCALAR_TYPE = 1;
1231: private const IS_UNKNOWN = 2;
1232:
1233: /**
1234: * @param callable(bool|float|int|string|null, bool|float|int|string|null):string $operationCallable
1235: *
1236: * @return self::IS_UNKNOWN|self::IS_SCALAR_TYPE|Type
1237: */
1238: private function getFiniteOrConstantScalarTypes(Type $leftType, Type $rightType, callable $operationCallable): int|Type
1239: {
1240: if ($leftType instanceof IntegerRangeType) {
1241: $leftTypes = $leftType->getFiniteTypes();
1242: } else {
1243: $leftTypes = $leftType->getConstantScalarTypes();
1244: }
1245: if ($rightType instanceof IntegerRangeType) {
1246: $rightTypes = $rightType->getFiniteTypes();
1247: } else {
1248: $rightTypes = $rightType->getConstantScalarTypes();
1249: }
1250:
1251: $leftTypesCount = count($leftTypes);
1252: $rightTypesCount = count($rightTypes);
1253:
1254: if ($leftTypesCount === 0 || $rightTypesCount === 0) {
1255: return self::IS_UNKNOWN;
1256: }
1257:
1258: $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT;
1259: if ($generalize) {
1260: return self::IS_SCALAR_TYPE;
1261: }
1262:
1263: $resultTypes = [];
1264: foreach ($leftTypes as $leftTypeInner) {
1265: foreach ($rightTypes as $rightTypeInner) {
1266: if ($leftTypeInner instanceof ConstantStringType && $rightTypeInner instanceof ConstantStringType) {
1267: $resultValue = $operationCallable($leftTypeInner->getValue(), $rightTypeInner->getValue());
1268: $resultType = $this->getTypeFromValue($resultValue);
1269: } else {
1270: $leftNumberType = $leftTypeInner->toNumber();
1271: $rightNumberType = $rightTypeInner->toNumber();
1272:
1273: if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) {
1274: return new ErrorType();
1275: }
1276:
1277: if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) {
1278: throw new ShouldNotHappenException();
1279: }
1280:
1281: $resultValue = $operationCallable($leftNumberType->getValue(), $rightNumberType->getValue());
1282: $resultType = $this->getTypeFromValue($resultValue);
1283: }
1284: $resultTypes[] = $resultType;
1285: }
1286: }
1287: return TypeCombinator::union(...$resultTypes);
1288: }
1289:
1290: /**
1291: * @param callable(Expr): Type $getTypeCallback
1292: */
1293: public function getSpaceshipType(Expr $left, Expr $right, callable $getTypeCallback): Type
1294: {
1295: $leftTypes = $getTypeCallback($left);
1296: $rightTypes = $getTypeCallback($right);
1297:
1298: if ($leftTypes instanceof NeverType || $rightTypes instanceof NeverType) {
1299: return $this->getNeverType($leftTypes, $rightTypes);
1300: }
1301:
1302: $leftValues = $leftTypes->getConstantScalarValues();
1303: $rightValues = $rightTypes->getConstantScalarValues();
1304:
1305: $leftValuesCount = count($leftValues);
1306: $rightValuesCount = count($rightValues);
1307: if ($leftValuesCount > 0 && $rightValuesCount > 0 && $leftValuesCount * $rightValuesCount <= self::CALCULATE_SCALARS_LIMIT) {
1308: $resultTypes = [];
1309: foreach ($leftValues as $leftValue) {
1310: foreach ($rightValues as $rightValue) {
1311: $resultType = $this->getTypeFromValue($leftValue <=> $rightValue);
1312: $resultTypes[] = $resultType;
1313: }
1314: }
1315: return TypeCombinator::union(...$resultTypes);
1316: }
1317:
1318: return IntegerRangeType::fromInterval(-1, 1);
1319: }
1320:
1321: /**
1322: * @param callable(Expr): Type $getTypeCallback
1323: */
1324: public function getDivType(Expr $left, Expr $right, callable $getTypeCallback): Type
1325: {
1326: $leftType = $getTypeCallback($left);
1327: $rightType = $getTypeCallback($right);
1328:
1329: $result = $this->getDivTypeFromTypes($left, $right, $leftType, $rightType);
1330:
1331: if ($leftType->isInteger()->yes() && $rightType->isInteger()->yes()) {
1332: $modType = $getTypeCallback(new BinaryOp\Mod($left, $right));
1333: if ($modType->isInteger()->yes() && (new ConstantIntegerType(0))->isSuperTypeOf($modType)->yes()) {
1334: $withoutFloat = TypeCombinator::remove($result, new FloatType());
1335:
1336: // PHP_INT_MIN / -1 divides without a remainder but still overflows to a float
1337: if (!$withoutFloat instanceof NeverType) {
1338: return $withoutFloat;
1339: }
1340: }
1341: }
1342:
1343: return $result;
1344: }
1345:
1346: private function getDivTypeFromTypes(Expr $left, Expr $right, Type $leftType, Type $rightType): Type
1347: {
1348: $leftTypes = $leftType->getConstantScalarTypes();
1349: $rightTypes = $rightType->getConstantScalarTypes();
1350: $leftTypesCount = count($leftTypes);
1351: $rightTypesCount = count($rightTypes);
1352: if ($leftTypesCount > 0 && $rightTypesCount > 0) {
1353: $resultTypes = [];
1354: $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT;
1355: if (!$generalize) {
1356: foreach ($leftTypes as $leftTypeInner) {
1357: foreach ($rightTypes as $rightTypeInner) {
1358: $leftNumberType = $leftTypeInner->toNumber();
1359: $rightNumberType = $rightTypeInner->toNumber();
1360:
1361: if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) {
1362: return new ErrorType();
1363: }
1364:
1365: if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) {
1366: throw new ShouldNotHappenException();
1367: }
1368:
1369: if (in_array($rightNumberType->getValue(), [0, 0.0], true)) {
1370: return new ErrorType();
1371: }
1372:
1373: $resultType = $this->getTypeFromValue($leftNumberType->getValue() / $rightNumberType->getValue()); // @phpstan-ignore binaryOp.invalid
1374: $resultTypes[] = $resultType;
1375: }
1376: }
1377: return TypeCombinator::union(...$resultTypes);
1378: }
1379:
1380: $leftType = $this->optimizeScalarType($leftType);
1381: $rightType = $this->optimizeScalarType($rightType);
1382: }
1383:
1384: $rightScalarValues = $rightType->toNumber()->getConstantScalarValues();
1385: foreach ($rightScalarValues as $scalarValue) {
1386: if (in_array($scalarValue, [0, 0.0], true)) {
1387: return new ErrorType();
1388: }
1389: }
1390:
1391: return $this->resolveCommonMath(new BinaryOp\Div($left, $right), $leftType, $rightType);
1392: }
1393:
1394: /**
1395: * @param callable(Expr): Type $getTypeCallback
1396: */
1397: public function getModType(Expr $left, Expr $right, callable $getTypeCallback): Type
1398: {
1399: $leftType = $getTypeCallback($left);
1400: $rightType = $getTypeCallback($right);
1401:
1402: if ($leftType instanceof NeverType || $rightType instanceof NeverType) {
1403: return $this->getNeverType($leftType, $rightType);
1404: }
1405:
1406: $extensionSpecified = $this->operatorTypeSpecifyingExtensionRegistry
1407: ->callOperatorTypeSpecifyingExtensions(new BinaryOp\Mod($left, $right), $leftType, $rightType);
1408: if ($extensionSpecified !== null) {
1409: return $extensionSpecified;
1410: }
1411:
1412: if ($leftType->toNumber() instanceof ErrorType || $rightType->toNumber() instanceof ErrorType) {
1413: return new ErrorType();
1414: }
1415:
1416: $leftTypes = $leftType->getConstantScalarTypes();
1417: $rightTypes = $rightType->getConstantScalarTypes();
1418: $leftTypesCount = count($leftTypes);
1419: $rightTypesCount = count($rightTypes);
1420: if ($leftTypesCount > 0 && $rightTypesCount > 0) {
1421: $resultTypes = [];
1422: $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT;
1423: if (!$generalize) {
1424: foreach ($leftTypes as $leftTypeInner) {
1425: foreach ($rightTypes as $rightTypeInner) {
1426: $leftNumberType = $leftTypeInner->toNumber();
1427: $rightNumberType = $rightTypeInner->toNumber();
1428:
1429: if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) {
1430: return new ErrorType();
1431: }
1432:
1433: if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) {
1434: throw new ShouldNotHappenException();
1435: }
1436:
1437: $rightIntegerValue = (int) $rightNumberType->getValue();
1438: if ($rightIntegerValue === 0) {
1439: return new ErrorType();
1440: }
1441:
1442: $resultType = $this->getTypeFromValue((int) $leftNumberType->getValue() % $rightIntegerValue);
1443: $resultTypes[] = $resultType;
1444: }
1445: }
1446: return TypeCombinator::union(...$resultTypes);
1447: }
1448:
1449: $leftType = $this->optimizeScalarType($leftType);
1450: $rightType = $this->optimizeScalarType($rightType);
1451: }
1452:
1453: $integerType = $rightType->toInteger();
1454: if ($integerType instanceof ConstantIntegerType && $integerType->getValue() === 1) {
1455: return new ConstantIntegerType(0);
1456: }
1457:
1458: $rightScalarValues = $rightType->toNumber()->getConstantScalarValues();
1459: foreach ($rightScalarValues as $scalarValue) {
1460:
1461: if (in_array($scalarValue, [0, 0.0], true)) {
1462: return new ErrorType();
1463: }
1464: }
1465:
1466: $maxMagnitude = null;
1467: $divisorBounds = $this->getIntegerBounds($rightType->toInteger());
1468: if ($divisorBounds !== null) {
1469: $maxMagnitude = self::getMaxModuloMagnitude($divisorBounds[0], $divisorBounds[1]);
1470: }
1471:
1472: [$leftMin, $leftMax] = $this->getIntegerBounds($leftType->toInteger()) ?? [null, null];
1473: if ($leftMin === null && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes()) {
1474: $leftMin = 0;
1475: }
1476: if ($leftMax === null && IntegerRangeType::fromInterval(null, 0)->isSuperTypeOf($leftType)->yes()) {
1477: $leftMax = 0;
1478: }
1479:
1480: // The result has the sign of the dividend and is never bigger in magnitude than either
1481: // the dividend or the largest magnitude the divisor can have.
1482: if ($leftMax !== null && $leftMax <= 0) {
1483: $rangeMax = 0;
1484: } elseif ($maxMagnitude === null) {
1485: $rangeMax = $leftMax;
1486: } elseif ($leftMax === null) {
1487: $rangeMax = $maxMagnitude;
1488: } else {
1489: $rangeMax = min($leftMax, $maxMagnitude);
1490: }
1491:
1492: if ($leftMin !== null && $leftMin >= 0) {
1493: $rangeMin = 0;
1494: } elseif ($maxMagnitude === null) {
1495: $rangeMin = $leftMin;
1496: } elseif ($leftMin === null) {
1497: $rangeMin = -$maxMagnitude;
1498: } else {
1499: $rangeMin = max($leftMin, -$maxMagnitude);
1500: }
1501:
1502: return IntegerRangeType::fromInterval($rangeMin, $rangeMax);
1503: }
1504:
1505: /**
1506: * Highest possible absolute value of `$x % $divisor`, which is one less than the largest
1507: * possible absolute value of the divisor. Null when there is no such bound.
1508: *
1509: * A divisor reaching PHP_INT_MIN is reported as unbounded too: `abs(PHP_INT_MIN)` does not
1510: * fit into an integer, and the bound it stands for is PHP_INT_MAX, which is all an unbounded
1511: * result can hold anyway.
1512: */
1513: private static function getMaxModuloMagnitude(?int $divisorMin, ?int $divisorMax): ?int
1514: {
1515: if ($divisorMin === null || $divisorMax === null || $divisorMin === PHP_INT_MIN) {
1516: return null;
1517: }
1518:
1519: $magnitude = max(abs($divisorMin), abs($divisorMax));
1520: if ($magnitude === 0) {
1521: return null;
1522: }
1523:
1524: return $magnitude - 1;
1525: }
1526:
1527: /**
1528: * @return array{int|null, int|null}|null Lowest and highest value the type can hold, with null
1529: * for an unbounded side. Null when the type is not built from integer ranges and constants.
1530: */
1531: private function getIntegerBounds(Type $type): ?array
1532: {
1533: $innerTypes = $type instanceof UnionType ? $type->getTypes() : [$type];
1534: if ($innerTypes === []) {
1535: return null;
1536: }
1537:
1538: $min = null;
1539: $max = null;
1540: $unboundedMin = false;
1541: $unboundedMax = false;
1542: foreach ($innerTypes as $innerType) {
1543: if ($innerType instanceof IntegerRangeType) {
1544: $innerMin = $innerType->getMin();
1545: $innerMax = $innerType->getMax();
1546: } elseif ($innerType instanceof ConstantIntegerType) {
1547: $innerMin = $innerType->getValue();
1548: $innerMax = $innerMin;
1549: } else {
1550: return null;
1551: }
1552:
1553: if ($innerMin === null) {
1554: $unboundedMin = true;
1555: } elseif ($min === null || $innerMin < $min) {
1556: $min = $innerMin;
1557: }
1558:
1559: if ($innerMax === null) {
1560: $unboundedMax = true;
1561: } elseif ($max === null || $innerMax > $max) {
1562: $max = $innerMax;
1563: }
1564: }
1565:
1566: return [
1567: $unboundedMin ? null : $min,
1568: $unboundedMax ? null : $max,
1569: ];
1570: }
1571:
1572: /**
1573: * @param callable(Expr): Type $getTypeCallback
1574: */
1575: public function getPlusType(Expr $left, Expr $right, callable $getTypeCallback): Type
1576: {
1577: $leftType = $getTypeCallback($left);
1578: $rightType = $getTypeCallback($right);
1579:
1580: if ($leftType instanceof NeverType || $rightType instanceof NeverType) {
1581: return $this->getNeverType($leftType, $rightType);
1582: }
1583:
1584: $leftTypes = $leftType->getConstantScalarTypes();
1585: $rightTypes = $rightType->getConstantScalarTypes();
1586: $leftTypesCount = count($leftTypes);
1587: $rightTypesCount = count($rightTypes);
1588: if ($leftTypesCount > 0 && $rightTypesCount > 0) {
1589: $resultTypes = [];
1590: $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT;
1591: if (!$generalize) {
1592: foreach ($leftTypes as $leftTypeInner) {
1593: foreach ($rightTypes as $rightTypeInner) {
1594: $leftNumberType = $leftTypeInner->toNumber();
1595: $rightNumberType = $rightTypeInner->toNumber();
1596:
1597: if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) {
1598: return new ErrorType();
1599: }
1600:
1601: if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) {
1602: throw new ShouldNotHappenException();
1603: }
1604:
1605: $resultType = $this->getTypeFromValue($leftNumberType->getValue() + $rightNumberType->getValue());
1606: $resultTypes[] = $resultType;
1607: }
1608: }
1609:
1610: return TypeCombinator::union(...$resultTypes);
1611: }
1612:
1613: $leftType = $this->optimizeScalarType($leftType);
1614: $rightType = $this->optimizeScalarType($rightType);
1615: }
1616:
1617: $leftConstantArrays = $leftType->getConstantArrays();
1618: $rightConstantArrays = $rightType->getConstantArrays();
1619:
1620: $leftCount = count($leftConstantArrays);
1621: $rightCount = count($rightConstantArrays);
1622: if ($leftCount > 0 && $rightCount > 0
1623: && ($leftCount + $rightCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT)) {
1624: $resultTypes = [];
1625: foreach ($rightConstantArrays as $rightConstantArray) {
1626: foreach ($leftConstantArrays as $leftConstantArray) {
1627: $newArrayBuilder = ConstantArrayTypeBuilder::createFromConstantArray($rightConstantArray);
1628: foreach ($leftConstantArray->getKeyTypes() as $i => $leftKeyType) {
1629: $optional = $leftConstantArray->isOptionalKey($i);
1630: $valueType = $leftConstantArray->getOffsetValueType($leftKeyType);
1631: if (!$optional) {
1632: if ($rightConstantArray->hasOffsetValueType($leftKeyType)->maybe()) {
1633: $valueType = TypeCombinator::union($valueType, $rightConstantArray->getOffsetValueType($leftKeyType));
1634: }
1635: }
1636: $newArrayBuilder->setOffsetValueType(
1637: $leftKeyType,
1638: $valueType,
1639: $optional,
1640: );
1641: }
1642: $resultTypes[] = $newArrayBuilder->getArray();
1643: }
1644: }
1645: return TypeCombinator::union(...$resultTypes);
1646: }
1647:
1648: // `array{...} + array<TKey, TValue>` keeps the known keyed prefix and
1649: // folds the right-hand side into an open `...<TKey, TValue>` tail. This
1650: // only applies when the left arrays' sealedness is known (bleeding edge);
1651: // otherwise the legacy behavior below is kept for backward compatibility.
1652: if ($leftCount > 0 && $rightCount === 0
1653: && $leftCount < ConstantArrayTypeBuilder::ARRAY_COUNT_LIMIT
1654: && $rightType->isArray()->yes()) {
1655: $allSealednessKnown = true;
1656: foreach ($leftConstantArrays as $leftConstantArray) {
1657: if ($leftConstantArray->isUnsealed()->maybe()) {
1658: $allSealednessKnown = false;
1659: break;
1660: }
1661: }
1662:
1663: if ($allSealednessKnown) {
1664: $rightKeyType = $rightType->getIterableKeyType();
1665: $rightValueType = $rightType->getIterableValueType();
1666: $resultTypes = [];
1667: foreach ($leftConstantArrays as $leftConstantArray) {
1668: $newArrayBuilder = ConstantArrayTypeBuilder::createFromConstantArray($leftConstantArray);
1669: $existingUnsealed = $leftConstantArray->getUnsealedTypes();
1670: if ($leftConstantArray->isUnsealed()->yes() && $existingUnsealed !== null) {
1671: $newArrayBuilder->makeUnsealed(
1672: TypeCombinator::union($existingUnsealed[0], $rightKeyType),
1673: TypeCombinator::union($existingUnsealed[1], $rightValueType),
1674: );
1675: } else {
1676: $newArrayBuilder->makeUnsealed($rightKeyType, $rightValueType);
1677: }
1678: $resultTypes[] = $newArrayBuilder->getArray();
1679: }
1680: return TypeCombinator::union(...$resultTypes);
1681: }
1682: }
1683:
1684: $leftIsArray = $leftType->isArray();
1685: $rightIsArray = $rightType->isArray();
1686: if ($leftIsArray->yes() && $rightIsArray->yes()) {
1687: if ($leftType->getIterableKeyType()->equals($rightType->getIterableKeyType())) {
1688: // to preserve BenevolentUnionType
1689: $keyType = $leftType->getIterableKeyType();
1690: } else {
1691: $keyTypes = [];
1692: foreach ([
1693: $leftType->getIterableKeyType(),
1694: $rightType->getIterableKeyType(),
1695: ] as $keyType) {
1696: $keyTypes[] = $keyType;
1697: }
1698: $keyType = TypeCombinator::union(...$keyTypes);
1699: }
1700:
1701: $leftIterableValueType = $leftType->getIterableValueType();
1702: $arrayType = new ArrayType(
1703: $keyType,
1704: TypeCombinator::union($leftIterableValueType, $rightType->getIterableValueType()),
1705: );
1706:
1707: $accessories = [];
1708: if ($leftCount > 0) {
1709: // Use the first constant array as a reference to list potential offsets.
1710: // We only need to check the first array because we're looking for offsets that exist in ALL arrays.
1711: $constantArray = $leftConstantArrays[0];
1712: foreach ($constantArray->getKeyTypes() as $offsetType) {
1713: if (!$leftType->hasOffsetValueType($offsetType)->yes()) {
1714: continue;
1715: }
1716:
1717: $valueType = $leftType->getOffsetValueType($offsetType);
1718: $accessories[] = new HasOffsetValueType($offsetType, $valueType);
1719: }
1720: }
1721:
1722: if ($rightCount > 0) {
1723: // Use the first constant array as a reference to list potential offsets.
1724: // We only need to check the first array because we're looking for offsets that exist in ALL arrays.
1725: $constantArray = $rightConstantArrays[0];
1726: foreach ($constantArray->getKeyTypes() as $offsetType) {
1727: if (!$rightType->hasOffsetValueType($offsetType)->yes()) {
1728: continue;
1729: }
1730:
1731: $valueType = TypeCombinator::union($leftIterableValueType, $rightType->getOffsetValueType($offsetType));
1732: $accessories[] = new HasOffsetValueType($offsetType, $valueType);
1733: }
1734: }
1735:
1736: if ($leftType->isIterableAtLeastOnce()->yes() || $rightType->isIterableAtLeastOnce()->yes()) {
1737: $accessories[] = new NonEmptyArrayType();
1738: }
1739: if ($leftType->isList()->yes() && $rightType->isList()->yes()) {
1740: $accessories[] = new AccessoryArrayListType();
1741: }
1742:
1743: if (count($accessories) > 0) {
1744: $arrayType = TypeCombinator::intersect($arrayType, ...$accessories);
1745: }
1746:
1747: return $arrayType;
1748: }
1749:
1750: if ($leftType instanceof MixedType && $rightType instanceof MixedType) {
1751: if ($leftIsArray->no() && $rightIsArray->no()) {
1752: return new BenevolentUnionType([
1753: new FloatType(),
1754: new IntegerType(),
1755: ]);
1756: }
1757: return new BenevolentUnionType([
1758: new FloatType(),
1759: new IntegerType(),
1760: new ArrayType(new MixedType(), new MixedType()),
1761: ]);
1762: }
1763:
1764: if (
1765: ($leftIsArray->yes() && $rightIsArray->no())
1766: || ($leftIsArray->no() && $rightIsArray->yes())
1767: ) {
1768: return new ErrorType();
1769: }
1770:
1771: if (
1772: ($leftIsArray->yes() && $rightIsArray->maybe())
1773: || ($leftIsArray->maybe() && $rightIsArray->yes())
1774: ) {
1775: $resultType = new ArrayType(new MixedType(), new MixedType());
1776: if ($leftType->isIterableAtLeastOnce()->yes() || $rightType->isIterableAtLeastOnce()->yes()) {
1777: return TypeCombinator::intersect($resultType, new NonEmptyArrayType());
1778: }
1779:
1780: return $resultType;
1781: }
1782:
1783: if ($leftIsArray->maybe() && $rightIsArray->maybe()) {
1784: $plusable = new UnionType([
1785: new StringType(),
1786: new FloatType(),
1787: new IntegerType(),
1788: new ArrayType(new MixedType(), new MixedType()),
1789: new BooleanType(),
1790: ]);
1791:
1792: $plusableSuperTypeOfLeft = $plusable->isSuperTypeOf($leftType)->yes();
1793: $plusableSuperTypeOfRight = $plusable->isSuperTypeOf($rightType)->yes();
1794: if ($plusableSuperTypeOfLeft && $plusableSuperTypeOfRight) {
1795: return TypeCombinator::union($leftType, $rightType);
1796: }
1797: if ($plusableSuperTypeOfLeft && $rightType instanceof MixedType) {
1798: return $leftType;
1799: }
1800: if ($plusableSuperTypeOfRight && $leftType instanceof MixedType) {
1801: return $rightType;
1802: }
1803: }
1804:
1805: return $this->resolveCommonMath(new BinaryOp\Plus($left, $right), $leftType, $rightType);
1806: }
1807:
1808: /**
1809: * @param callable(Expr): Type $getTypeCallback
1810: */
1811: public function getMinusType(Expr $left, Expr $right, callable $getTypeCallback): Type
1812: {
1813: $leftType = $getTypeCallback($left);
1814: $rightType = $getTypeCallback($right);
1815:
1816: $leftTypes = $leftType->getConstantScalarTypes();
1817: $rightTypes = $rightType->getConstantScalarTypes();
1818: $leftTypesCount = count($leftTypes);
1819: $rightTypesCount = count($rightTypes);
1820: if ($leftTypesCount > 0 && $rightTypesCount > 0) {
1821: $resultTypes = [];
1822: $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT;
1823: if (!$generalize) {
1824: foreach ($leftTypes as $leftTypeInner) {
1825: foreach ($rightTypes as $rightTypeInner) {
1826: $leftNumberType = $leftTypeInner->toNumber();
1827: $rightNumberType = $rightTypeInner->toNumber();
1828:
1829: if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) {
1830: return new ErrorType();
1831: }
1832:
1833: if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) {
1834: throw new ShouldNotHappenException();
1835: }
1836:
1837: $resultType = $this->getTypeFromValue($leftNumberType->getValue() - $rightNumberType->getValue());
1838: $resultTypes[] = $resultType;
1839: }
1840: }
1841:
1842: return TypeCombinator::union(...$resultTypes);
1843: }
1844:
1845: $leftType = $this->optimizeScalarType($leftType);
1846: $rightType = $this->optimizeScalarType($rightType);
1847: }
1848:
1849: return $this->resolveCommonMath(new BinaryOp\Minus($left, $right), $leftType, $rightType);
1850: }
1851:
1852: /**
1853: * @param callable(Expr): Type $getTypeCallback
1854: */
1855: public function getMulType(Expr $left, Expr $right, callable $getTypeCallback): Type
1856: {
1857: $leftType = $getTypeCallback($left);
1858: $rightType = $getTypeCallback($right);
1859:
1860: $leftTypes = $leftType->getConstantScalarTypes();
1861: $rightTypes = $rightType->getConstantScalarTypes();
1862: $leftTypesCount = count($leftTypes);
1863: $rightTypesCount = count($rightTypes);
1864: if ($leftTypesCount > 0 && $rightTypesCount > 0) {
1865: $resultTypes = [];
1866: $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT;
1867: if (!$generalize) {
1868: foreach ($leftTypes as $leftTypeInner) {
1869: foreach ($rightTypes as $rightTypeInner) {
1870: $leftNumberType = $leftTypeInner->toNumber();
1871: $rightNumberType = $rightTypeInner->toNumber();
1872:
1873: if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) {
1874: return new ErrorType();
1875: }
1876:
1877: if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) {
1878: throw new ShouldNotHappenException();
1879: }
1880:
1881: $resultType = $this->getTypeFromValue($leftNumberType->getValue() * $rightNumberType->getValue());
1882: $resultTypes[] = $resultType;
1883: }
1884: }
1885:
1886: return TypeCombinator::union(...$resultTypes);
1887: }
1888:
1889: $leftType = $this->optimizeScalarType($leftType);
1890: $rightType = $this->optimizeScalarType($rightType);
1891: }
1892:
1893: $leftNumberType = $leftType->toNumber();
1894: if ($leftNumberType instanceof ConstantIntegerType && $leftNumberType->getValue() === 0) {
1895: if ($rightType->isFloat()->yes()) {
1896: return new ConstantFloatType(0.0);
1897: }
1898: return new ConstantIntegerType(0);
1899: }
1900: $rightNumberType = $rightType->toNumber();
1901: if ($rightNumberType instanceof ConstantIntegerType && $rightNumberType->getValue() === 0) {
1902: if ($leftType->isFloat()->yes()) {
1903: return new ConstantFloatType(0.0);
1904: }
1905: return new ConstantIntegerType(0);
1906: }
1907:
1908: return $this->resolveCommonMath(new BinaryOp\Mul($left, $right), $leftType, $rightType);
1909: }
1910:
1911: /**
1912: * @param callable(Expr): Type $getTypeCallback
1913: */
1914: public function getPowType(Expr $left, Expr $right, callable $getTypeCallback): Type
1915: {
1916: $leftType = $getTypeCallback($left);
1917: $rightType = $getTypeCallback($right);
1918:
1919: $extensionSpecified = $this->operatorTypeSpecifyingExtensionRegistry
1920: ->callOperatorTypeSpecifyingExtensions(new BinaryOp\Pow($left, $right), $leftType, $rightType);
1921: if ($extensionSpecified !== null) {
1922: return $extensionSpecified;
1923: }
1924:
1925: $exponentiatedTyped = $leftType->exponentiate($rightType);
1926: if (!$exponentiatedTyped instanceof ErrorType) {
1927: return $exponentiatedTyped;
1928: }
1929:
1930: return new ErrorType();
1931: }
1932:
1933: /**
1934: * @param callable(Expr): Type $getTypeCallback
1935: */
1936: public function getShiftLeftType(Expr $left, Expr $right, callable $getTypeCallback): Type
1937: {
1938: $leftType = $getTypeCallback($left);
1939: $rightType = $getTypeCallback($right);
1940:
1941: $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistry
1942: ->callOperatorTypeSpecifyingExtensions(new BinaryOp\ShiftLeft($left, $right), $leftType, $rightType);
1943: if ($specifiedTypes !== null) {
1944: return $specifiedTypes;
1945: }
1946:
1947: if ($leftType instanceof NeverType || $rightType instanceof NeverType) {
1948: return $this->getNeverType($leftType, $rightType);
1949: }
1950:
1951: $leftTypes = $leftType->getConstantScalarTypes();
1952: $rightTypes = $rightType->getConstantScalarTypes();
1953: $leftTypesCount = count($leftTypes);
1954: $rightTypesCount = count($rightTypes);
1955: if ($leftTypesCount > 0 && $rightTypesCount > 0) {
1956: $resultTypes = [];
1957: $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT;
1958: if (!$generalize) {
1959: foreach ($leftTypes as $leftTypeInner) {
1960: foreach ($rightTypes as $rightTypeInner) {
1961: $leftNumberType = $leftTypeInner->toNumber();
1962: $rightNumberType = $rightTypeInner->toNumber();
1963:
1964: if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) {
1965: return new ErrorType();
1966: }
1967:
1968: if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) {
1969: throw new ShouldNotHappenException();
1970: }
1971:
1972: if ($rightNumberType->getValue() < 0) {
1973: return new ErrorType();
1974: }
1975:
1976: $resultType = $this->getTypeFromValue(intval($leftNumberType->getValue()) << intval($rightNumberType->getValue()));
1977: $resultTypes[] = $resultType;
1978: }
1979: }
1980:
1981: return TypeCombinator::union(...$resultTypes);
1982: }
1983:
1984: $leftType = $this->optimizeScalarType($leftType);
1985: $rightType = $this->optimizeScalarType($rightType);
1986: }
1987:
1988: $leftNumberType = $leftType->toNumber();
1989: $rightNumberType = $rightType->toNumber();
1990:
1991: if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) {
1992: return new ErrorType();
1993: }
1994:
1995: return $this->resolveCommonMath(new Expr\BinaryOp\ShiftLeft($left, $right), $leftType, $rightType);
1996: }
1997:
1998: /**
1999: * @param callable(Expr): Type $getTypeCallback
2000: */
2001: public function getShiftRightType(Expr $left, Expr $right, callable $getTypeCallback): Type
2002: {
2003: $leftType = $getTypeCallback($left);
2004: $rightType = $getTypeCallback($right);
2005:
2006: $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistry
2007: ->callOperatorTypeSpecifyingExtensions(new BinaryOp\ShiftRight($left, $right), $leftType, $rightType);
2008: if ($specifiedTypes !== null) {
2009: return $specifiedTypes;
2010: }
2011:
2012: if ($leftType instanceof NeverType || $rightType instanceof NeverType) {
2013: return $this->getNeverType($leftType, $rightType);
2014: }
2015:
2016: $leftTypes = $leftType->getConstantScalarTypes();
2017: $rightTypes = $rightType->getConstantScalarTypes();
2018: $leftTypesCount = count($leftTypes);
2019: $rightTypesCount = count($rightTypes);
2020: if ($leftTypesCount > 0 && $rightTypesCount > 0) {
2021: $resultTypes = [];
2022: $generalize = $leftTypesCount * $rightTypesCount > self::CALCULATE_SCALARS_LIMIT;
2023: if (!$generalize) {
2024: foreach ($leftTypes as $leftTypeInner) {
2025: foreach ($rightTypes as $rightTypeInner) {
2026: $leftNumberType = $leftTypeInner->toNumber();
2027: $rightNumberType = $rightTypeInner->toNumber();
2028:
2029: if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) {
2030: return new ErrorType();
2031: }
2032:
2033: if (!$leftNumberType instanceof ConstantScalarType || !$rightNumberType instanceof ConstantScalarType) {
2034: throw new ShouldNotHappenException();
2035: }
2036:
2037: if ($rightNumberType->getValue() < 0) {
2038: return new ErrorType();
2039: }
2040:
2041: $resultType = $this->getTypeFromValue(intval($leftNumberType->getValue()) >> intval($rightNumberType->getValue()));
2042: $resultTypes[] = $resultType;
2043: }
2044: }
2045:
2046: return TypeCombinator::union(...$resultTypes);
2047: }
2048:
2049: $leftType = $this->optimizeScalarType($leftType);
2050: $rightType = $this->optimizeScalarType($rightType);
2051: }
2052:
2053: $leftNumberType = $leftType->toNumber();
2054: $rightNumberType = $rightType->toNumber();
2055:
2056: if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) {
2057: return new ErrorType();
2058: }
2059:
2060: return $this->resolveCommonMath(new Expr\BinaryOp\ShiftRight($left, $right), $leftType, $rightType);
2061: }
2062:
2063: private function optimizeScalarType(Type $type): Type
2064: {
2065: $types = [];
2066: if ($type->isInteger()->yes()) {
2067: $types[] = new IntegerType();
2068: }
2069: if ($type->isString()->yes()) {
2070: $types[] = new StringType();
2071: }
2072: if ($type->isFloat()->yes()) {
2073: $types[] = new FloatType();
2074: }
2075: if ($type->isNull()->yes()) {
2076: $types[] = new NullType();
2077: }
2078:
2079: if (count($types) === 0) {
2080: return new ErrorType();
2081: }
2082:
2083: if (count($types) === 1) {
2084: return $types[0];
2085: }
2086:
2087: return new UnionType($types);
2088: }
2089:
2090: /**
2091: * @return array{int, int|null}|null [min, max] of a type known to be a non-negative integer,
2092: * with a null max when it has no finite upper bound. Null when the type is not known
2093: * to be a non-negative integer.
2094: */
2095: private function getNonNegativeIntegerBounds(Type $type): ?array
2096: {
2097: if ($type instanceof IntegerRangeType) {
2098: $min = $type->getMin();
2099: if ($min !== null && $min >= 0) {
2100: return [$min, $type->getMax()];
2101: }
2102: return null;
2103: }
2104: if ($type instanceof ConstantIntegerType && $type->getValue() >= 0) {
2105: return [$type->getValue(), $type->getValue()];
2106: }
2107: return null;
2108: }
2109:
2110: /**
2111: * Expects the operands already converted with toNumber().
2112: *
2113: * A single non-negative operand is enough to bound the result: the result's bits
2114: * are a subset of that operand's bits, so it stays within int<0, thatMax>.
2115: * With no finite max on any non-negative side the result is still int<0, max>.
2116: */
2117: private function computeBitwiseAndRange(Type $leftNumberType, Type $rightNumberType): ?Type
2118: {
2119: $leftBounds = $this->getNonNegativeIntegerBounds($leftNumberType);
2120: $rightBounds = $this->getNonNegativeIntegerBounds($rightNumberType);
2121: if ($leftBounds === null && $rightBounds === null) {
2122: return null;
2123: }
2124:
2125: $maxValues = [];
2126: if ($leftBounds !== null && $leftBounds[1] !== null) {
2127: $maxValues[] = $leftBounds[1];
2128: }
2129: if ($rightBounds !== null && $rightBounds[1] !== null) {
2130: $maxValues[] = $rightBounds[1];
2131: }
2132:
2133: return IntegerRangeType::fromInterval(0, $maxValues === [] ? null : min($maxValues));
2134: }
2135:
2136: /**
2137: * Expects the operands already converted with toNumber().
2138: *
2139: * Both operands have to be non-negative: they keep the sign bit of the result clear.
2140: * Without a finite max on either side the result is still int<0, max>.
2141: */
2142: private function computeBitwiseOrXorRange(Type $leftNumberType, Type $rightNumberType): ?Type
2143: {
2144: $leftBounds = $this->getNonNegativeIntegerBounds($leftNumberType);
2145: $rightBounds = $this->getNonNegativeIntegerBounds($rightNumberType);
2146: if ($leftBounds === null || $rightBounds === null) {
2147: return null;
2148: }
2149: if ($leftBounds[1] === null || $rightBounds[1] === null) {
2150: return IntegerRangeType::fromInterval(0, null);
2151: }
2152:
2153: return IntegerRangeType::fromInterval(0, self::allBitsMask(max($leftBounds[1], $rightBounds[1])));
2154: }
2155:
2156: /**
2157: * Propagates the highest set bit of a non-negative value into all lower bits:
2158: * 200 becomes 255, 10 becomes 15.
2159: */
2160: private static function allBitsMask(int $value): int
2161: {
2162: $value |= $value >> 1;
2163: $value |= $value >> 2;
2164: $value |= $value >> 4;
2165: $value |= $value >> 8;
2166: $value |= $value >> 16;
2167: $value |= $value >> 32;
2168:
2169: return $value;
2170: }
2171:
2172: /**
2173: * @return TypeResult<BooleanType>
2174: */
2175: public function resolveIdenticalType(Type $leftType, Type $rightType): TypeResult
2176: {
2177: if ($leftType instanceof NeverType || $rightType instanceof NeverType) {
2178: return new TypeResult(new ConstantBooleanType(false), []);
2179: }
2180:
2181: if ($leftType instanceof ConstantScalarType && $rightType instanceof ConstantScalarType) {
2182: return new TypeResult(new ConstantBooleanType($leftType->getValue() === $rightType->getValue()), []);
2183: }
2184:
2185: $leftTypeFiniteTypes = $leftType->getFiniteTypes();
2186: $rightTypeFiniteType = $rightType->getFiniteTypes();
2187: if (count($leftTypeFiniteTypes) === 1 && count($rightTypeFiniteType) === 1) {
2188: return new TypeResult(new ConstantBooleanType($leftTypeFiniteTypes[0]->equals($rightTypeFiniteType[0])), []);
2189: }
2190:
2191: $leftIsSuperTypeOfRight = $leftType->isSuperTypeOf($rightType);
2192: $rightIsSuperTypeOfLeft = $rightType->isSuperTypeOf($leftType);
2193: if ($leftIsSuperTypeOfRight->no() && $rightIsSuperTypeOfLeft->no()) {
2194: return new TypeResult(new ConstantBooleanType(false), array_merge($leftIsSuperTypeOfRight->getReasons(), $rightIsSuperTypeOfLeft->getReasons()));
2195: }
2196:
2197: if ($leftType instanceof ConstantArrayType && $rightType instanceof ConstantArrayType) {
2198: return $this->resolveConstantArrayTypeComparison($leftType, $rightType, fn ($leftValueType, $rightValueType): TypeResult => $this->resolveIdenticalType($leftValueType, $rightValueType));
2199: }
2200:
2201: return new TypeResult(new BooleanType(), []);
2202: }
2203:
2204: /**
2205: * @return TypeResult<BooleanType>
2206: */
2207: public function resolveEqualType(Type $leftType, Type $rightType): TypeResult
2208: {
2209: if ($leftType instanceof NeverType || $rightType instanceof NeverType) {
2210: return new TypeResult(new ConstantBooleanType(false), []);
2211: }
2212:
2213: if (
2214: ($leftType->isEnum()->yes() && $rightType->isTrue()->no())
2215: || ($rightType->isEnum()->yes() && $leftType->isTrue()->no())
2216: ) {
2217: return $this->resolveIdenticalType($leftType, $rightType);
2218: }
2219:
2220: if ($leftType instanceof ConstantArrayType && $rightType instanceof ConstantArrayType) {
2221: return $this->resolveConstantArrayTypeComparison($leftType, $rightType, fn ($leftValueType, $rightValueType): TypeResult => $this->resolveEqualType($leftValueType, $rightValueType));
2222: }
2223:
2224: return new TypeResult($leftType->looseCompare($rightType, $this->phpVersion), []);
2225: }
2226:
2227: /**
2228: * @param callable(Type, Type): TypeResult<BooleanType> $valueComparisonCallback
2229: * @return TypeResult<BooleanType>
2230: */
2231: private function resolveConstantArrayTypeComparison(ConstantArrayType $leftType, ConstantArrayType $rightType, callable $valueComparisonCallback): TypeResult
2232: {
2233: $leftKeyTypes = $leftType->getKeyTypes();
2234: $rightKeyTypes = $rightType->getKeyTypes();
2235: $leftValueTypes = $leftType->getValueTypes();
2236: $rightValueTypes = $rightType->getValueTypes();
2237:
2238: $resultType = new ConstantBooleanType(true);
2239:
2240: foreach ($leftKeyTypes as $i => $leftKeyType) {
2241: $leftOptional = $leftType->isOptionalKey($i);
2242: if ($leftOptional) {
2243: $resultType = new BooleanType();
2244: }
2245:
2246: if (count($rightKeyTypes) === 0) {
2247: if (!$leftOptional) {
2248: return new TypeResult(new ConstantBooleanType(false), []);
2249: }
2250: continue;
2251: }
2252:
2253: $found = false;
2254: foreach ($rightKeyTypes as $j => $rightKeyType) {
2255: unset($rightKeyTypes[$j]);
2256:
2257: if ($leftKeyType->equals($rightKeyType)) {
2258: $found = true;
2259: break;
2260: } elseif (!$rightType->isOptionalKey($j)) {
2261: return new TypeResult(new ConstantBooleanType(false), []);
2262: }
2263: }
2264:
2265: if (!$found) {
2266: if (!$leftOptional) {
2267: return new TypeResult(new ConstantBooleanType(false), []);
2268: }
2269: continue;
2270: }
2271:
2272: if (!isset($j)) {
2273: throw new ShouldNotHappenException();
2274: }
2275:
2276: $rightOptional = $rightType->isOptionalKey($j);
2277: if ($rightOptional) {
2278: $resultType = new BooleanType();
2279: if ($leftOptional) {
2280: continue;
2281: }
2282: }
2283:
2284: $leftIdenticalToRightResult = $valueComparisonCallback($leftValueTypes[$i], $rightValueTypes[$j]);
2285: $leftIdenticalToRight = $leftIdenticalToRightResult->type;
2286: if ($leftIdenticalToRight->isFalse()->yes()) {
2287: return $leftIdenticalToRightResult;
2288: }
2289: $resultType = TypeCombinator::union($resultType, $leftIdenticalToRight);
2290: }
2291:
2292: foreach (array_keys($rightKeyTypes) as $j) {
2293: if (!$rightType->isOptionalKey($j)) {
2294: return new TypeResult(new ConstantBooleanType(false), []);
2295: }
2296: $resultType = new BooleanType();
2297: }
2298:
2299: return new TypeResult($resultType->toBoolean(), []);
2300: }
2301:
2302: /**
2303: * @param BinaryOp\Plus|BinaryOp\Minus|BinaryOp\Mul|BinaryOp\Div|BinaryOp\ShiftLeft|BinaryOp\ShiftRight $expr
2304: */
2305: private function resolveCommonMath(Expr\BinaryOp $expr, Type $leftType, Type $rightType): Type
2306: {
2307: $specifiedTypes = $this->operatorTypeSpecifyingExtensionRegistry
2308: ->callOperatorTypeSpecifyingExtensions($expr, $leftType, $rightType);
2309: if ($specifiedTypes !== null) {
2310: return $specifiedTypes;
2311: }
2312:
2313: $types = TypeCombinator::union($leftType, $rightType);
2314: $leftNumberType = $leftType->toNumber();
2315: $rightNumberType = $rightType->toNumber();
2316:
2317: if (
2318: !$types instanceof MixedType
2319: && (
2320: $rightNumberType instanceof IntegerRangeType
2321: || $rightNumberType instanceof ConstantIntegerType
2322: || $rightNumberType instanceof UnionType
2323: )
2324: ) {
2325: if ($leftNumberType instanceof IntegerRangeType || $leftNumberType instanceof ConstantIntegerType) {
2326: return $this->integerRangeMath(
2327: $leftNumberType,
2328: $expr,
2329: $rightNumberType,
2330: );
2331: } elseif ($leftNumberType instanceof UnionType) {
2332: $unionParts = [];
2333:
2334: foreach ($leftNumberType->getTypes() as $type) {
2335: $numberType = $type->toNumber();
2336: if ($numberType instanceof IntegerRangeType || $numberType instanceof ConstantIntegerType) {
2337: $unionParts[] = $this->integerRangeMath($numberType, $expr, $rightNumberType);
2338: } else {
2339: $unionParts[] = $numberType;
2340: }
2341: }
2342:
2343: $union = TypeCombinator::union(...$unionParts);
2344: if ($leftNumberType instanceof BenevolentUnionType) {
2345: return TypeUtils::toBenevolentUnion($union)->toNumber();
2346: }
2347:
2348: return $union->toNumber();
2349: }
2350: }
2351:
2352: if (
2353: $leftType->isArray()->yes()
2354: || $rightType->isArray()->yes()
2355: || $types->isArray()->yes()
2356: ) {
2357: return new ErrorType();
2358: }
2359:
2360: if ($leftNumberType instanceof ErrorType || $rightNumberType instanceof ErrorType) {
2361: return new ErrorType();
2362: }
2363: if ($leftNumberType instanceof NeverType || $rightNumberType instanceof NeverType) {
2364: return $this->getNeverType($leftNumberType, $rightNumberType);
2365: }
2366:
2367: if (
2368: $leftNumberType->isFloat()->yes()
2369: || $rightNumberType->isFloat()->yes()
2370: ) {
2371: if ($expr instanceof Expr\BinaryOp\ShiftLeft || $expr instanceof Expr\BinaryOp\ShiftRight) {
2372: return new IntegerType();
2373: }
2374: return new FloatType();
2375: }
2376:
2377: $resultType = TypeCombinator::union($leftNumberType, $rightNumberType);
2378: if ($expr instanceof Expr\BinaryOp\Div) {
2379: if ($types instanceof MixedType || $resultType->isInteger()->yes()) {
2380: return new BenevolentUnionType([new IntegerType(), new FloatType()]);
2381: }
2382:
2383: return new UnionType([new IntegerType(), new FloatType()]);
2384: }
2385:
2386: if ($types instanceof MixedType
2387: || $leftType instanceof BenevolentUnionType
2388: || $rightType instanceof BenevolentUnionType
2389: ) {
2390: return TypeUtils::toBenevolentUnion($resultType);
2391: }
2392:
2393: return $resultType;
2394: }
2395:
2396: /**
2397: * @param ConstantIntegerType|IntegerRangeType $range
2398: * @param BinaryOp\Div|BinaryOp\Minus|BinaryOp\Mul|BinaryOp\Plus|BinaryOp\ShiftLeft|BinaryOp\ShiftRight $node
2399: */
2400: private function integerRangeMath(Type $range, BinaryOp $node, Type $operand): Type
2401: {
2402: if ($range instanceof IntegerRangeType) {
2403: $rangeMin = $range->getMin();
2404: $rangeMax = $range->getMax();
2405: } else {
2406: $rangeMin = $range->getValue();
2407: $rangeMax = $rangeMin;
2408: }
2409:
2410: if ($operand instanceof UnionType) {
2411:
2412: $unionParts = [];
2413:
2414: foreach ($operand->getTypes() as $type) {
2415: $numberType = $type->toNumber();
2416: if ($numberType instanceof IntegerRangeType || $numberType instanceof ConstantIntegerType) {
2417: $unionParts[] = $this->integerRangeMath($range, $node, $numberType);
2418: } else {
2419: $unionParts[] = $type->toNumber();
2420: }
2421: }
2422:
2423: $union = TypeCombinator::union(...$unionParts);
2424: if ($operand instanceof BenevolentUnionType) {
2425: return TypeUtils::toBenevolentUnion($union)->toNumber();
2426: }
2427:
2428: return $union->toNumber();
2429: }
2430:
2431: $operand = $operand->toNumber();
2432: if ($operand instanceof IntegerRangeType) {
2433: $operandMin = $operand->getMin();
2434: $operandMax = $operand->getMax();
2435: } elseif ($operand instanceof ConstantIntegerType) {
2436: $operandMin = $operand->getValue();
2437: $operandMax = $operand->getValue();
2438: } else {
2439: return $operand;
2440: }
2441:
2442: if ($node instanceof BinaryOp\Plus) {
2443: if ($operand instanceof ConstantIntegerType) {
2444: /** @var int|float|null $min */
2445: $min = $rangeMin !== null ? $rangeMin + $operand->getValue() : null;
2446:
2447: /** @var int|float|null $max */
2448: $max = $rangeMax !== null ? $rangeMax + $operand->getValue() : null;
2449: } else {
2450: /** @var int|float|null $min */
2451: $min = $rangeMin !== null && $operand->getMin() !== null ? $rangeMin + $operand->getMin() : null;
2452:
2453: /** @var int|float|null $max */
2454: $max = $rangeMax !== null && $operand->getMax() !== null ? $rangeMax + $operand->getMax() : null;
2455: }
2456: } elseif ($node instanceof BinaryOp\Minus) {
2457: if ($operand instanceof ConstantIntegerType) {
2458: /** @var int|float|null $min */
2459: $min = $rangeMin !== null ? $rangeMin - $operand->getValue() : null;
2460:
2461: /** @var int|float|null $max */
2462: $max = $rangeMax !== null ? $rangeMax - $operand->getValue() : null;
2463: } else {
2464: if ($rangeMin === $rangeMax && $rangeMin !== null
2465: && ($operand->getMin() === null || $operand->getMax() === null)) {
2466: $min = null;
2467: $max = $rangeMin;
2468: } else {
2469: if ($operand->getMin() === null) {
2470: $min = null;
2471: } elseif ($rangeMin !== null) {
2472: if ($operand->getMax() !== null) {
2473: /** @var int|float $min */
2474: $min = $rangeMin - $operand->getMax();
2475: } else {
2476: /** @var int|float $min */
2477: $min = $rangeMin - $operand->getMin();
2478: }
2479: } else {
2480: $min = null;
2481: }
2482:
2483: if ($operand->getMax() === null) {
2484: $min = null;
2485: $max = null;
2486: } elseif ($rangeMax !== null) {
2487: if ($rangeMin !== null && $operand->getMin() === null) {
2488: /** @var int|float $min */
2489: $min = $rangeMin - $operand->getMax();
2490: $max = null;
2491: } elseif ($operand->getMin() !== null) {
2492: /** @var int|float $max */
2493: $max = $rangeMax - $operand->getMin();
2494: } else {
2495: $max = null;
2496: }
2497: } else {
2498: $max = null;
2499: }
2500:
2501: if ($min !== null && $max !== null && $min > $max) {
2502: [$min, $max] = [$max, $min];
2503: }
2504: }
2505: }
2506: } elseif ($node instanceof Expr\BinaryOp\Mul) {
2507: $min1 = $rangeMin === 0 || $operandMin === 0 ? 0 : ($rangeMin ?? -INF) * ($operandMin ?? -INF);
2508: $min2 = $rangeMin === 0 || $operandMax === 0 ? 0 : ($rangeMin ?? -INF) * ($operandMax ?? INF);
2509: $max1 = $rangeMax === 0 || $operandMin === 0 ? 0 : ($rangeMax ?? INF) * ($operandMin ?? -INF);
2510: $max2 = $rangeMax === 0 || $operandMax === 0 ? 0 : ($rangeMax ?? INF) * ($operandMax ?? INF);
2511:
2512: $min = min($min1, $min2, $max1, $max2);
2513: $max = max($min1, $min2, $max1, $max2);
2514:
2515: if (!is_finite($min)) {
2516: $min = null;
2517: }
2518: if (!is_finite($max)) {
2519: $max = null;
2520: }
2521: } elseif ($node instanceof Expr\BinaryOp\Div) {
2522: if ($operand instanceof ConstantIntegerType) {
2523: $min = $rangeMin !== null && $operand->getValue() !== 0 ? $rangeMin / $operand->getValue() : null;
2524: $max = $rangeMax !== null && $operand->getValue() !== 0 ? $rangeMax / $operand->getValue() : null;
2525: } else {
2526: // Avoid division by zero when looking for the min and the max by using the closest int
2527: $operandMin = $operandMin !== 0 ? $operandMin : 1;
2528: $operandMax = $operandMax !== 0 ? $operandMax : -1;
2529:
2530: if (
2531: ($operandMin < 0 || $operandMin === null)
2532: && ($operandMax > 0 || $operandMax === null)
2533: ) {
2534: $negativeOperand = IntegerRangeType::fromInterval($operandMin, 0);
2535: assert($negativeOperand instanceof IntegerRangeType);
2536: $positiveOperand = IntegerRangeType::fromInterval(0, $operandMax);
2537: assert($positiveOperand instanceof IntegerRangeType);
2538:
2539: $result = TypeCombinator::union(
2540: $this->integerRangeMath($range, $node, $negativeOperand),
2541: $this->integerRangeMath($range, $node, $positiveOperand),
2542: )->toNumber();
2543:
2544: if ($result->equals(new UnionType([new IntegerType(), new FloatType()]))) {
2545: return new BenevolentUnionType([new IntegerType(), new FloatType()]);
2546: }
2547:
2548: return $result;
2549: }
2550: if (
2551: ($rangeMin < 0 || $rangeMin === null)
2552: && ($rangeMax > 0 || $rangeMax === null)
2553: ) {
2554: $negativeRange = IntegerRangeType::fromInterval($rangeMin, 0);
2555: assert($negativeRange instanceof IntegerRangeType);
2556: $positiveRange = IntegerRangeType::fromInterval(0, $rangeMax);
2557: assert($positiveRange instanceof IntegerRangeType);
2558:
2559: $result = TypeCombinator::union(
2560: $this->integerRangeMath($negativeRange, $node, $operand),
2561: $this->integerRangeMath($positiveRange, $node, $operand),
2562: )->toNumber();
2563:
2564: if ($result->equals(new UnionType([new IntegerType(), new FloatType()]))) {
2565: return new BenevolentUnionType([new IntegerType(), new FloatType()]);
2566: }
2567:
2568: return $result;
2569: }
2570:
2571: $rangeMinSign = ($rangeMin ?? -INF) <=> 0;
2572: $rangeMaxSign = ($rangeMax ?? INF) <=> 0;
2573:
2574: $min1 = $operandMin !== null ? ($rangeMin ?? -INF) / $operandMin : $rangeMinSign * -0.1;
2575: $min2 = $operandMax !== null ? ($rangeMin ?? -INF) / $operandMax : $rangeMinSign * 0.1;
2576: $max1 = $operandMin !== null ? ($rangeMax ?? INF) / $operandMin : $rangeMaxSign * -0.1;
2577: $max2 = $operandMax !== null ? ($rangeMax ?? INF) / $operandMax : $rangeMaxSign * 0.1;
2578:
2579: $min = min($min1, $min2, $max1, $max2);
2580: $max = max($min1, $min2, $max1, $max2);
2581:
2582: if ($min === -INF) {
2583: $min = null;
2584: }
2585: if ($max === INF) {
2586: $max = null;
2587: }
2588: }
2589:
2590: if ($min !== null && $max !== null && $min > $max) {
2591: [$min, $max] = [$max, $min];
2592: }
2593:
2594: if (is_float($min)) {
2595: $min = self::toIntBound(ceil($min));
2596: }
2597: if (is_float($max)) {
2598: $max = self::toIntBound(floor($max));
2599: }
2600:
2601: // invert maximas on division with negative constants
2602: if ((($range instanceof ConstantIntegerType && $range->getValue() < 0)
2603: || ($operand instanceof ConstantIntegerType && $operand->getValue() < 0))
2604: && ($min === null || $max === null)) {
2605: [$min, $max] = [$max, $min];
2606: }
2607:
2608: if ($min === null && $max === null) {
2609: return new BenevolentUnionType([new IntegerType(), new FloatType()]);
2610: }
2611:
2612: return TypeCombinator::union(IntegerRangeType::fromInterval($min, $max), new FloatType());
2613: } elseif ($node instanceof Expr\BinaryOp\ShiftLeft) {
2614: if (!$operand instanceof ConstantIntegerType) {
2615: return new IntegerType();
2616: }
2617: if ($operand->getValue() < 0) {
2618: return new ErrorType();
2619: }
2620: // an overflowing shift wraps around, which breaks the monotonicity the bounds rely on
2621: if (
2622: ($rangeMin !== null && self::shiftLeftOverflows(intval($rangeMin), $operand->getValue()))
2623: || ($rangeMax !== null && self::shiftLeftOverflows(intval($rangeMax), $operand->getValue()))
2624: ) {
2625: return new IntegerType();
2626: }
2627:
2628: $min = $rangeMin !== null ? intval($rangeMin) << $operand->getValue() : null;
2629: $max = $rangeMax !== null ? intval($rangeMax) << $operand->getValue() : null;
2630: } elseif ($node instanceof Expr\BinaryOp\ShiftRight) {
2631: if (!$operand instanceof ConstantIntegerType) {
2632: return new IntegerType();
2633: }
2634: if ($operand->getValue() < 0) {
2635: return new ErrorType();
2636: }
2637: $min = $rangeMin !== null ? intval($rangeMin) >> $operand->getValue() : null;
2638: $max = $rangeMax !== null ? intval($rangeMax) >> $operand->getValue() : null;
2639: } else {
2640: throw new ShouldNotHappenException();
2641: }
2642:
2643: if (is_float($min)) {
2644: $min = null;
2645: }
2646: if (is_float($max)) {
2647: $max = null;
2648: }
2649:
2650: return IntegerRangeType::fromInterval($min, $max);
2651: }
2652:
2653: /**
2654: * A left shift that does not fit into an integer wraps around instead of turning
2655: * into a float, so the shifted value no longer preserves the ordering of its operand.
2656: */
2657: private static function shiftLeftOverflows(int $value, int $shift): bool
2658: {
2659: return ($value << $shift) >> $shift !== $value;
2660: }
2661:
2662: /**
2663: * @param callable(Expr): Type $getTypeCallback
2664: */
2665: public function getClassConstFetchTypeByReflection(Name|Expr $class, string $constantName, ?ClassReflection $classReflection, callable $getTypeCallback): Type
2666: {
2667: $isObject = false;
2668: if ($class instanceof Name) {
2669: $constantClass = (string) $class;
2670: $constantClassType = new ObjectType($constantClass);
2671: $namesToResolve = [
2672: 'self',
2673: 'parent',
2674: ];
2675: if ($classReflection !== null) {
2676: if ($classReflection->isFinal()) {
2677: $namesToResolve[] = 'static';
2678: } elseif (strtolower($constantClass) === 'static') {
2679: if (strtolower($constantName) === 'class') {
2680: return new GenericClassStringType(new StaticType($classReflection));
2681: }
2682:
2683: $namesToResolve[] = 'static';
2684: $isObject = true;
2685: }
2686: }
2687: if (in_array(strtolower($constantClass), $namesToResolve, true)) {
2688: $resolvedName = $this->resolveName($class, $classReflection);
2689: if (strtolower($resolvedName) === 'parent' && strtolower($constantName) === 'class') {
2690: return new ClassStringType();
2691: }
2692: $constantClassType = $this->resolveTypeByName($class, $classReflection);
2693: }
2694:
2695: if (strtolower($constantName) === 'class') {
2696: return new ConstantStringType($constantClassType->getClassName(), true);
2697: }
2698: } elseif ($class instanceof String_ && strtolower($constantName) === 'class') {
2699: return new ConstantStringType($class->value, true);
2700: } else {
2701: $constantClassType = $getTypeCallback($class);
2702: $isObject = true;
2703: }
2704:
2705: if (strtolower($constantName) === 'class') {
2706: return $constantClassType->toClassConstantType($this->getReflectionProvider());
2707: }
2708:
2709: if ($constantClassType->isClassString()->yes()) {
2710: if ($constantClassType->isConstantScalarValue()->yes()) {
2711: $isObject = false;
2712: }
2713: $constantClassType = $constantClassType->getClassStringObjectType();
2714: }
2715:
2716: $types = [];
2717: foreach ($constantClassType->getObjectClassNames() as $referencedClass) {
2718: if (!$this->getReflectionProvider()->hasClass($referencedClass)) {
2719: continue;
2720: }
2721:
2722: $constantClassReflection = $this->getReflectionProvider()->getClass($referencedClass);
2723: if (!$constantClassReflection->hasConstant($constantName)) {
2724: if ($constantClassReflection->getName() === 'Attribute' && $constantName === 'TARGET_CONSTANT') {
2725: return new ConstantIntegerType(1 << 16);
2726: }
2727: continue;
2728: }
2729:
2730: if ($constantClassReflection->isEnum() && $constantClassReflection->hasEnumCase($constantName)) {
2731: $types[] = new EnumCaseObjectType($constantClassReflection->getName(), $constantName);
2732: continue;
2733: }
2734:
2735: $resolvingName = sprintf('%s::%s', $constantClassReflection->getName(), $constantName);
2736: if (array_key_exists($resolvingName, $this->currentlyResolvingClassConstant)) {
2737: $types[] = new MixedType();
2738: continue;
2739: }
2740:
2741: if (!$isObject && array_key_exists($resolvingName, $this->classConstantValueTypeCache)) {
2742: $types[] = $this->classConstantValueTypeCache[$resolvingName];
2743: continue;
2744: }
2745:
2746: $this->currentlyResolvingClassConstant[$resolvingName] = true;
2747:
2748: if (!$isObject) {
2749: $reflectionConstant = $constantClassReflection->getNativeReflection()->getReflectionConstant($constantName);
2750: if ($reflectionConstant === false) {
2751: unset($this->currentlyResolvingClassConstant[$resolvingName]);
2752: continue;
2753: }
2754: $reflectionConstantDeclaringClass = $reflectionConstant->getDeclaringClass();
2755: $constantType = $this->getType($reflectionConstant->getValueExpression(), InitializerExprContext::fromClass($reflectionConstantDeclaringClass->getName(), $reflectionConstantDeclaringClass->getFileName() ?: null));
2756: $nativeType = null;
2757: if ($reflectionConstant->getType() !== null) {
2758: $nativeType = TypehintHelper::decideTypeFromReflection($reflectionConstant->getType(), selfClass: $constantClassReflection);
2759: }
2760: $resolvedType = $this->constantResolver->resolveClassConstantType(
2761: $constantClassReflection->getName(),
2762: $constantName,
2763: $constantType,
2764: $nativeType,
2765: $constantClassReflection->getConstantPhpDocType($constantName),
2766: );
2767: $this->classConstantValueTypeCache[$resolvingName] = $resolvedType;
2768: $types[] = $resolvedType;
2769: unset($this->currentlyResolvingClassConstant[$resolvingName]);
2770: continue;
2771: }
2772:
2773: $constantReflection = $constantClassReflection->getConstant($constantName);
2774: if (
2775: !$constantClassReflection->isFinal()
2776: && !$constantReflection->isFinal()
2777: && !$constantReflection->hasPhpDocType()
2778: && !$constantReflection->hasNativeType()
2779: ) {
2780: unset($this->currentlyResolvingClassConstant[$resolvingName]);
2781: return new MixedType();
2782: }
2783:
2784: if (!$constantClassReflection->isFinal()) {
2785: $constantType = $constantReflection->getValueType();
2786: } else {
2787: $constantType = $this->getType($constantReflection->getValueExpr(), InitializerExprContext::fromClassReflection($constantReflection->getDeclaringClass()));
2788: }
2789:
2790: $nativeType = $constantReflection->getNativeType();
2791: $constantType = $this->constantResolver->resolveClassConstantType(
2792: $constantClassReflection->getName(),
2793: $constantName,
2794: $constantType,
2795: $nativeType,
2796: $constantClassReflection->getConstantPhpDocType($constantName),
2797: );
2798: unset($this->currentlyResolvingClassConstant[$resolvingName]);
2799: $types[] = $constantType;
2800: }
2801:
2802: if (count($types) > 0) {
2803: return TypeCombinator::union(...$types);
2804: }
2805:
2806: if (!$constantClassType->hasConstant($constantName)->yes()) {
2807: return new ErrorType();
2808: }
2809:
2810: return $constantClassType->getConstant($constantName)->getValueType();
2811: }
2812:
2813: /**
2814: * @param callable(Expr): Type $getTypeCallback
2815: */
2816: public function getClassConstFetchType(Name|Expr $class, string $constantName, ?string $className, callable $getTypeCallback): Type
2817: {
2818: $classReflection = null;
2819: if ($className !== null && $this->getReflectionProvider()->hasClass($className)) {
2820: $classReflection = $this->getReflectionProvider()->getClass($className);
2821: }
2822:
2823: return $this->getClassConstFetchTypeByReflection($class, $constantName, $classReflection, $getTypeCallback);
2824: }
2825:
2826: /**
2827: * @param callable(Expr): Type $getTypeCallback
2828: */
2829: public function getUnaryPlusType(Expr $expr, callable $getTypeCallback): Type
2830: {
2831: $type = $getTypeCallback($expr);
2832:
2833: $specifiedTypes = $this->unaryOperatorTypeSpecifyingExtensionRegistry
2834: ->callUnaryOperatorTypeSpecifyingExtensions('+', $type);
2835: if ($specifiedTypes !== null) {
2836: return $specifiedTypes;
2837: }
2838:
2839: return $type->toNumber();
2840: }
2841:
2842: /**
2843: * @param callable(Expr): Type $getTypeCallback
2844: */
2845: public function getUnaryMinusType(Expr $expr, callable $getTypeCallback): Type
2846: {
2847: $type = $getTypeCallback($expr);
2848:
2849: $specifiedTypes = $this->unaryOperatorTypeSpecifyingExtensionRegistry
2850: ->callUnaryOperatorTypeSpecifyingExtensions('-', $type);
2851: if ($specifiedTypes !== null) {
2852: return $specifiedTypes;
2853: }
2854:
2855: $type = $this->getUnaryMinusTypeFromType($expr, $type);
2856: if ($type instanceof IntegerRangeType) {
2857: return $getTypeCallback(new Expr\BinaryOp\Mul($expr, new Int_(-1)));
2858: }
2859:
2860: return $type;
2861: }
2862:
2863: public function getUnaryMinusTypeFromType(Expr $expr, Type $type): Type
2864: {
2865: $type = $type->toNumber();
2866: $scalarValues = $type->getConstantScalarValues();
2867:
2868: if (count($scalarValues) > 0) {
2869: $newTypes = [];
2870: foreach ($scalarValues as $scalarValue) {
2871: if (is_int($scalarValue)) {
2872: /** @var int|float $newValue */
2873: $newValue = -$scalarValue;
2874: if (!is_int($newValue)) {
2875: // Negating the smallest integer overflows into a float.
2876: $newTypes[] = new ConstantFloatType($newValue);
2877: continue;
2878: }
2879: $newTypes[] = new ConstantIntegerType($newValue);
2880: } elseif (is_float($scalarValue)) {
2881: $newTypes[] = new ConstantFloatType(-$scalarValue);
2882: }
2883: }
2884:
2885: return TypeCombinator::union(...$newTypes);
2886: }
2887:
2888: return $type;
2889: }
2890:
2891: /**
2892: * @param callable(Expr): Type $getTypeCallback
2893: */
2894: public function getBitwiseNotType(Expr $expr, callable $getTypeCallback): Type
2895: {
2896: $exprType = $getTypeCallback($expr);
2897:
2898: $specifiedTypes = $this->unaryOperatorTypeSpecifyingExtensionRegistry
2899: ->callUnaryOperatorTypeSpecifyingExtensions('~', $exprType);
2900: if ($specifiedTypes !== null) {
2901: return $specifiedTypes;
2902: }
2903:
2904: return $this->getBitwiseNotTypeFromType($exprType);
2905: }
2906:
2907: public function getBitwiseNotTypeFromType(Type $exprType): Type
2908: {
2909: return $exprType->toBitwiseNotType();
2910: }
2911:
2912: private function resolveName(Name $name, ?ClassReflection $classReflection): string
2913: {
2914: $originalClass = (string) $name;
2915: if ($classReflection !== null) {
2916: $lowerClass = strtolower($originalClass);
2917:
2918: if (in_array($lowerClass, [
2919: 'self',
2920: 'static',
2921: ], true)) {
2922: return $classReflection->getName();
2923: } elseif ($lowerClass === 'parent') {
2924: if ($classReflection->getParentClass() !== null) {
2925: return $classReflection->getParentClass()->getName();
2926: }
2927: }
2928: }
2929:
2930: return $originalClass;
2931: }
2932:
2933: private function resolveTypeByName(Name $name, ?ClassReflection $classReflection): TypeWithClassName
2934: {
2935: if ($name->toLowerString() === 'static' && $classReflection !== null) {
2936: return new StaticType($classReflection);
2937: }
2938:
2939: $originalClass = $this->resolveName($name, $classReflection);
2940: if ($classReflection !== null) {
2941: $thisType = new ThisType($classReflection);
2942: $ancestor = $thisType->getAncestorWithClassName($originalClass);
2943: if ($ancestor !== null) {
2944: return $ancestor;
2945: }
2946: }
2947:
2948: return new ObjectType($originalClass);
2949: }
2950:
2951: private function resolveTypeByNameWithLateStaticBinding(Name $class, Type $classType, MethodReflection $methodReflectionCandidate): Type
2952: {
2953: if (
2954: $classType instanceof StaticType
2955: && !in_array($class->toLowerString(), ['self', 'static', 'parent'], true)
2956: ) {
2957: if ($methodReflectionCandidate->isStatic()) {
2958: $classType = $classType->getStaticObjectType();
2959: }
2960: }
2961:
2962: return $classType;
2963: }
2964:
2965: /**
2966: * The bound an overflowing float stands for: an int operation whose result
2967: * leaves the int range yields a float there, so the integer part of the
2968: * result reaches no further than the int range does. Casting the float
2969: * instead would wrap the bound around (and warns since PHP 8.5): note that
2970: * PHP_INT_MAX is not representable as a float, so the first float past the
2971: * int range is (float) PHP_INT_MAX itself.
2972: */
2973: private static function toIntBound(float $value): ?int
2974: {
2975: if (!is_finite($value)) {
2976: return null;
2977: }
2978: if ($value >= (float) PHP_INT_MAX) {
2979: return PHP_INT_MAX;
2980: }
2981: if ($value <= (float) PHP_INT_MIN) {
2982: return PHP_INT_MIN;
2983: }
2984:
2985: return (int) $value;
2986: }
2987:
2988: /**
2989: * @param mixed $value
2990: */
2991: private function getTypeFromValue($value): Type
2992: {
2993: return ConstantTypeHelper::getTypeFromValue($value);
2994: }
2995:
2996: private function getReflectionProvider(): ReflectionProvider
2997: {
2998: return $this->reflectionProviderProvider->getReflectionProvider();
2999: }
3000:
3001: private function getNeverType(Type $leftType, Type $rightType): Type
3002: {
3003: // make sure we don't lose the explicit flag in the process
3004: if ($leftType instanceof NeverType && $leftType->isExplicit()) {
3005: return $leftType;
3006: }
3007: if ($rightType instanceof NeverType && $rightType->isExplicit()) {
3008: return $rightType;
3009: }
3010: return new NeverType();
3011: }
3012:
3013: }
3014: