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