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