From c2b655f830146fada913d2960d79f2fe0d44c0f3 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Wed, 4 Nov 2026 08:20:30 +0000 Subject: [PATCH] Narrow loose comparisons by the values each side can loosely equal Type gets getPossiblyLooselyEqualType() and getAlwaysLooselyEqualType(), which LooseComparisonHelper computes for the configured PHP version: the values `$this == $x` can be true for, and the values it is always true for. The true branch of `==` intersects the other side with the first, the false branch removes the second, and resolveEqualType() answers false or true when the other side misses the first or lies within the second. This replaces the `== 0` and `== ''` branches that hedged between PHP 7 and PHP 8. Objects equal 1 and true; only Stringable and GMP objects equal strings and only GMP, BcMath\Number and SimpleXMLElement objects equal other numbers or false. NAN equals only true. Two numeric strings compare as numbers, so `==` and a loose in_array() no longer narrow such a pair as `===`, and an enum compared with a possible true is no longer strict in in_array(). A loose in_array() now narrows the needle as well. Cast truthiness gets its own value sets instead of `== 0` and `== ''`, which (int) 'abc', (int) 0.5 and (string) 0 do not follow. Closes https://github.com/phpstan/phpstan/issues/8224 --- build/phpstan.neon | 2 +- src/Analyser/ExprHandler/BinaryOpHandler.php | 2 +- src/Analyser/ExprHandler/CastHandler.php | 54 ++- .../ExprHandler/CastStringHandler.php | 47 +- .../Helper/IdenticalNarrowingHelper.php | 64 +-- .../InitializerExprTypeResolver.php | 27 +- ...possibleInArrayHaystackFiniteTypesRule.php | 10 +- src/Rules/Comparison/SwitchConditionRule.php | 11 +- src/Type/LooseComparisonHelper.php | 456 ++++++++++++++++++ ...InArrayFunctionTypeSpecifyingExtension.php | 42 +- src/Type/Traits/OperatorTypeTrait.php | 11 + src/Type/Type.php | 12 + .../Analyser/LooseConstComparisonPhp7Test.php | 1 + ...hpVersionRangeReturnTypeExtensionsTest.php | 1 + tests/PHPStan/Analyser/TypeSpecifierTest.php | 4 +- .../data/loose-equality-narrowing-php7.php | 41 ++ .../data/loose-equality-php-version-range.php | 76 +++ tests/PHPStan/Analyser/nsrt/bcmath-number.php | 8 +- tests/PHPStan/Analyser/nsrt/bug-12312.php | 12 +- tests/PHPStan/Analyser/nsrt/bug-14047.php | 4 +- .../PHPStan/Analyser/nsrt/bug-9662-enums.php | 4 +- tests/PHPStan/Analyser/nsrt/bug-9662.php | 4 +- tests/PHPStan/Analyser/nsrt/equal-narrow.php | 16 +- tests/PHPStan/Analyser/nsrt/equal.php | 2 +- .../nsrt/equality-narrowing-new-world.php | 4 +- tests/PHPStan/Analyser/nsrt/in-array-enum.php | 4 +- .../PHPStan/Analyser/nsrt/in_array_loose.php | 12 +- .../Analyser/nsrt/loose-comparisons.php | 6 +- .../nsrt/loose-equality-narrowing.php | 206 ++++++++ tests/PHPStan/Analyser/nsrt/narrow-cast.php | 10 +- .../Analyser/nsrt/node-callback-scope.php | 6 +- .../ConstantLooseComparisonRuleTest.php | 3 - ...mpossibleCheckTypeFunctionCallRuleTest.php | 40 -- ...onCallRuleWithUncertainPhpDocTypesTest.php | 40 -- ...sibleCheckTypeMethodCallRuleEqualsTest.php | 4 + .../Comparison/SwitchConditionRuleTest.php | 2 - turbo-ext/src/BinaryOpHandler.cpp | 10 +- turbo-ext/src/CastHandler.cpp | 41 +- turbo-ext/src/CastStringHandler.cpp | 67 +-- turbo-ext/src/IdenticalNarrowingHelper.cpp | 135 +++--- turbo-ext/src/IetrSupport.h | 2 + turbo-ext/src/InitializerExprTypeResolver.cpp | 86 +++- turbo-ext/src/SimpleExprHandlers.h | 44 ++ turbo-ext/src/TypeTraits.cpp | 14 + turbo-ext/src/generated/CastStringHandler.h | 67 +-- .../src/generated/IdenticalNarrowingHelper.h | 87 ++-- .../generated/InitializerExprTypeResolver.h | 130 ++--- turbo-ext/src/generated/OperatorTypeTrait.h | 16 +- turbo-ext/src/support.h | 2 +- turbo-ext/tests/type-family.php | 21 + 50 files changed, 1498 insertions(+), 472 deletions(-) create mode 100644 tests/PHPStan/Analyser/data/loose-equality-narrowing-php7.php create mode 100644 tests/PHPStan/Analyser/data/loose-equality-php-version-range.php create mode 100644 tests/PHPStan/Analyser/nsrt/loose-equality-narrowing.php diff --git a/build/phpstan.neon b/build/phpstan.neon index 29eae01570c..7bde486ad9f 100644 --- a/build/phpstan.neon +++ b/build/phpstan.neon @@ -144,7 +144,7 @@ parameters: path: ../src/Diagnose/PHPStanDiagnoseExtension.php - identifier: ternary.shortNotAllowed - - # operators take the configured PHP version, like InitializerExprTypeResolver does; + # operators and loose comparisons take the configured PHP version, like InitializerExprTypeResolver does; # not ignored inline because PHP 7.4 reads the class attribute as a comment, # so the error is on a different line there identifier: phpstan.phpVersionInjection diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index 5f8e8beb96c..4aa6fac7a8d 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -891,7 +891,7 @@ private function resolveEqualType(MutatingScope $scope, BinaryOp\Equal $expr, Ex $leftType = $leftResult->getTypeOnScope($scope, $scope->nativeTypesPromoted); $rightType = $rightResult->getTypeOnScope($scope, $scope->nativeTypesPromoted); - return $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType)->type; + return $this->initializerExprTypeResolver->resolveEqualType($leftType, $rightType, $scope->getPhpVersion())->type; } private function createRangeTypes(?Expr $rootExpr, Expr $expr, Type $type, TypeSpecifierContext $context): SpecifiedTypes diff --git a/src/Analyser/ExprHandler/CastHandler.php b/src/Analyser/ExprHandler/CastHandler.php index 7a4165c7535..491c301a112 100644 --- a/src/Analyser/ExprHandler/CastHandler.php +++ b/src/Analyser/ExprHandler/CastHandler.php @@ -26,8 +26,17 @@ use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\ShouldNotHappenException; use PHPStan\Turbo\ShadowedByTurboExtension; +use PHPStan\Type\Constant\ConstantArrayType; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\FloatType; use PHPStan\Type\NullType; +use PHPStan\Type\ObjectWithoutClassType; +use PHPStan\Type\StringType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; /** * @implements ExprHandler @@ -86,25 +95,44 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // a cast's truthiness is a loose comparison of the inner // expression - composed from its result; the fabricated // literal is only printed into entries, never walked - if (($expr instanceof Cast\Bool_ || $expr instanceof Cast\Int_ || $expr instanceof Cast\Double) && !$context->null()) { - if ($expr instanceof Cast\Bool_) { - $literal = new ConstFetch(new FullyQualified('true')); - $equalContext = $context; - } elseif ($expr instanceof Cast\Int_) { - $literal = new Int_(0); - $equalContext = $context->negate(); - } else { - $literal = new Float_(0.0); - $equalContext = $context->negate(); - } - + if ($expr instanceof Cast\Bool_ && !$context->null()) { // the literal side never reads its stand-in result - $types = $this->identicalNarrowingHelper->specifyEqual($nodeScopeResolver, $expr->expr, $literal, $exprResult, $exprResult, $equalContext, $evaluationScope, $subjectArgResult, null); + $types = $this->identicalNarrowingHelper->specifyEqual($nodeScopeResolver, $expr->expr, new ConstFetch(new FullyQualified('true')), $exprResult, $exprResult, $context, $evaluationScope, $subjectArgResult, null); if ($types !== null) { return $types->setRootExpr($expr); } } + // a number cast is zero for more values than those loosely equal + // to zero: (int) 'abc', (int) 0.5 and (int) INF are 0 too + if (($expr instanceof Cast\Int_ || $expr instanceof Cast\Double) && !$context->null()) { + $zeroContext = $context->negate(); + if ($zeroContext->true()) { + $zeroType = TypeCombinator::union( + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + $expr instanceof Cast\Int_ ? new FloatType() : new ConstantFloatType(0.0), + new StringType(), + new ConstantArrayType([], []), + // GMP casts to its number + new ObjectWithoutClassType(), + ); + } else { + $zeroType = TypeCombinator::union( + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantFloatType(0.0), + new ConstantStringType(''), + new ConstantStringType('0'), + new ConstantArrayType([], []), + ); + } + + return $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $expr->expr, $exprResult, $zeroType, $zeroContext)->setRootExpr($expr); + } + if ($expr instanceof Cast\Bool_) { return $evaluationScope->obtainResultForNode(new Equal($expr->expr, new ConstFetch(new FullyQualified('true'))))->getSpecifiedTypes($context, $nativeTypesPromoted)->setRootExpr($expr); } diff --git a/src/Analyser/ExprHandler/CastStringHandler.php b/src/Analyser/ExprHandler/CastStringHandler.php index 0eefea4cea8..f0af56c4ae1 100644 --- a/src/Analyser/ExprHandler/CastStringHandler.php +++ b/src/Analyser/ExprHandler/CastStringHandler.php @@ -3,15 +3,14 @@ namespace PHPStan\Analyser\ExprHandler; use PhpParser\Node\Expr; -use PhpParser\Node\Expr\BinaryOp\NotEqual; use PhpParser\Node\Expr\Cast; -use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt; use PHPStan\Analyser\ExpressionContext; use PHPStan\Analyser\ExpressionResult; use PHPStan\Analyser\ExpressionResultFactory; use PHPStan\Analyser\ExpressionResultStorage; use PHPStan\Analyser\ExprHandler; +use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper; use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper; use PHPStan\Analyser\MutatingScope; use PHPStan\Analyser\NodeScopeResolver; @@ -23,7 +22,14 @@ use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\ShouldNotHappenException; use PHPStan\Turbo\ShadowedByTurboExtension; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; +use PHPStan\Type\NullType; +use PHPStan\Type\ObjectWithoutClassType; use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function array_merge; /** @@ -38,6 +44,7 @@ public function __construct( private InitializerExprTypeResolver $initializerExprTypeResolver, private ImplicitToStringCallHelper $implicitToStringCallHelper, private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, ) { } @@ -76,9 +83,39 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throw new ShouldNotHappenException(); }), - specifyTypesCallback: static fn (TypeSpecifierContext $context, bool $nativeTypesPromoted): SpecifiedTypes => ($nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope)->obtainResultForNode( - new NotEqual($expr->expr, new String_('')), - )->getSpecifiedTypes($context, $nativeTypesPromoted)->setRootExpr($expr), + specifyTypesCallback: function (TypeSpecifierContext $context, bool $nativeTypesPromoted) use ($expr, $exprResult, $beforeScope): SpecifiedTypes { + if ($context->null()) { + return new SpecifiedTypes([], []); + } + + // the cast is falsy when it is '' or '0', not only for the values loosely equal to '' + $falsyContext = $context->negate(); + if ($falsyContext->true()) { + $falsyType = TypeCombinator::union( + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + // -0.0 casts to '-0' + new ConstantFloatType(0.0), + new ConstantStringType(''), + new ConstantStringType('0'), + // a Stringable object casts to its string + new ObjectWithoutClassType(), + ); + } else { + $falsyType = TypeCombinator::union( + new NullType(), + new ConstantBooleanType(false), + new ConstantIntegerType(0), + new ConstantStringType(''), + new ConstantStringType('0'), + ); + } + + $evaluationScope = $nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope; + + return $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $expr->expr, $exprResult, $falsyType, $falsyContext)->setRootExpr($expr); + }, dependencies: $exprResult->getDependencies(), ); } diff --git a/src/Analyser/ExprHandler/Helper/IdenticalNarrowingHelper.php b/src/Analyser/ExprHandler/Helper/IdenticalNarrowingHelper.php index 1c2d71ca200..2d974c9877d 100644 --- a/src/Analyser/ExprHandler/Helper/IdenticalNarrowingHelper.php +++ b/src/Analyser/ExprHandler/Helper/IdenticalNarrowingHelper.php @@ -16,6 +16,7 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\AlwaysRememberedExpr; use PHPStan\Node\Printer\ExprPrinter; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\ReflectionProvider; use PHPStan\Turbo\ShadowedByTurboExtension; use PHPStan\Type\Accessory\AccessoryLowercaseStringType; @@ -35,6 +36,7 @@ use PHPStan\Type\IntegerRangeType; use PHPStan\Type\IntegerType; use PHPStan\Type\IntersectionType; +use PHPStan\Type\LooseComparisonHelper; use PHPStan\Type\MixedType; use PHPStan\Type\NeverType; use PHPStan\Type\NullType; @@ -581,12 +583,12 @@ public function specifyEqual( $leftScalarValues = $leftType->getConstantScalarValues(); $rightScalarValues = $rightType->getConstantScalarValues(); if (count($leftScalarValues) === 1 && !$unwrappedRight instanceof Expr\ConstFetch) { - $constantSideTypes = $this->specifyEqualAgainstConstantSide($nodeScopeResolver, $left, $right, $leftResult, $rightResult, $right, $rightResult, $leftScalarValues[0], $leftType, $rightType, $context, $evaluationScope, $leftArgResult, $rightArgResult, $identicalTypeCallback); + $constantSideTypes = $this->specifyEqualAgainstConstantSide($nodeScopeResolver, $left, $right, $leftResult, $rightResult, $right, $rightResult, $leftScalarValues[0], $leftType, $context, $evaluationScope, $leftArgResult, $rightArgResult, $identicalTypeCallback); if ($constantSideTypes !== false) { return $constantSideTypes; } } elseif (count($rightScalarValues) === 1 && !$unwrappedLeft instanceof Expr\ConstFetch) { - $constantSideTypes = $this->specifyEqualAgainstConstantSide($nodeScopeResolver, $left, $right, $leftResult, $rightResult, $left, $leftResult, $rightScalarValues[0], $rightType, $leftType, $context, $evaluationScope, $leftArgResult, $rightArgResult, $identicalTypeCallback); + $constantSideTypes = $this->specifyEqualAgainstConstantSide($nodeScopeResolver, $left, $right, $leftResult, $rightResult, $left, $leftResult, $rightScalarValues[0], $rightType, $context, $evaluationScope, $leftArgResult, $rightArgResult, $identicalTypeCallback); if ($constantSideTypes !== false) { return $constantSideTypes; } @@ -612,9 +614,10 @@ public function specifyEqual( return $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $left, $leftResult, new NonEmptyArrayType(), $context->negate()); } - // same-type sides cannot coerce - loose equals strict + // same-type sides cannot coerce - loose equals strict, except for + // two numeric strings, which compare as numbers if ( - ($leftType->isString()->yes() && $rightType->isString()->yes()) + ($leftType->isString()->yes() && $rightType->isString()->yes() && ($leftType->isNumericString()->no() || $rightType->isNumericString()->no())) || ($leftType->isInteger()->yes() && $rightType->isInteger()->yes()) || ($leftType->isFloat()->yes() && $rightType->isFloat()->yes()) || ($leftType->isEnum()->yes() && $rightType->isEnum()->yes()) @@ -628,14 +631,38 @@ public function specifyEqual( if (!$left instanceof Expr\Variable || !$right instanceof Expr\Variable) { return new SpecifiedTypes([], []); } + + // a variable equals itself unless it holds NAN + if ($context->false() && $leftType->isFloat()->no() && $leftType->isArray()->no()) { + return $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $left, $leftResult, $leftType, $context); + } } - $leftTypes = $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $left, $leftResult, $leftType, $context); - $rightTypes = $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $right, $rightResult, $rightType, $context); + // each side is one of the values the other side can equal, and in + // the false branch none of the values the other side always equals + $phpVersions = LooseComparisonHelper::getRepresentativePhpVersions($evaluationScope->getPhpVersion()); - return $context->true() - ? $leftTypes->unionWith($rightTypes) - : $this->defaultNarrowingHelper->toSureTypes($leftTypes, $evaluationScope)->intersectWith($this->defaultNarrowingHelper->toSureTypes($rightTypes, $evaluationScope)); + return $this->createLooselyEqualTypes($evaluationScope, $left, $leftResult, $leftType, $rightType, $phpVersions, $context) + ->unionWith($this->createLooselyEqualTypes($evaluationScope, $right, $rightResult, $rightType, $leftType, $phpVersions, $context)); + } + + /** + * @param non-empty-list $phpVersions + */ + private function createLooselyEqualTypes(MutatingScope $evaluationScope, Expr $subject, ExpressionResult $subjectResult, Type $subjectType, Type $otherType, array $phpVersions, TypeSpecifierContext $context): SpecifiedTypes + { + $type = $context->true() + ? LooseComparisonHelper::getPossiblyLooselyEqualTypeInVersions($otherType, $phpVersions) + : LooseComparisonHelper::getAlwaysLooselyEqualTypeInVersions($otherType, $phpVersions); + // nothing to intersect with, or nothing to remove + if ($type->isSuperTypeOf(new MixedType())->yes() || ($context->false() && $type instanceof NeverType)) { + return new SpecifiedTypes([], []); + } + if ($context->true() && $subjectType instanceof MixedType && LooseComparisonHelper::keepsMixedUnnarrowed($type)) { + return new SpecifiedTypes([], []); + } + + return $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $subject, $subjectResult, $type, $context); } /** @@ -738,7 +765,6 @@ private function specifyEqualAgainstConstantSide( ExpressionResult $subjectResult, mixed $value, Type $constantType, - Type $otherType, TypeSpecifierContext $context, MutatingScope $evaluationScope, ?ExpressionResult $leftArgResult, @@ -773,24 +799,6 @@ private function specifyEqualAgainstConstantSide( ); } - /* There is a difference between php 7.x and 8.x on the equality - * behavior between zero and the empty string, so to be conservative - * we leave it untouched regardless of the language version */ - if ($value === 0 && !$otherType->isInteger()->yes() && !$otherType->isBoolean()->yes()) { - $trueTypes = $context->true() - ? [new NullType(), new ConstantBooleanType(false), new ConstantIntegerType(0), new ConstantFloatType(0.0), new StringType()] - : [new NullType(), new ConstantBooleanType(false), new ConstantIntegerType(0), new ConstantFloatType(0.0), new ConstantStringType('0')]; - - return $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $subject, $subjectResult, new UnionType($trueTypes), $context); - } - if ($value === '') { - $trueTypes = $context->true() - ? [new NullType(), new ConstantBooleanType(false), new ConstantIntegerType(0), new ConstantFloatType(0.0), new ConstantStringType('')] - : [new NullType(), new ConstantBooleanType(false), new ConstantStringType('')]; - - return $this->defaultNarrowingHelper->createSubjectTypes($evaluationScope, $subject, $subjectResult, new UnionType($trueTypes), $context); - } - // loose equals strict for these call results and class names if ( $unwrappedSubject instanceof Expr\FuncCall diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index d23878a5a8d..2118c1f7d2e 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -33,6 +33,7 @@ use PHPStan\DependencyInjection\AutowiredService; use PHPStan\Node\Expr\TypeExpr; use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\PhpDoc\Tag\TemplateTag; use PHPStan\Reflection\Callables\CallableParametersAcceptor; use PHPStan\Reflection\Callables\SimpleImpurePoint; @@ -1164,7 +1165,7 @@ public function resolveIdenticalType(Type $leftType, Type $rightType): TypeResul /** * @return TypeResult */ - public function resolveEqualType(Type $leftType, Type $rightType): TypeResult + public function resolveEqualType(Type $leftType, Type $rightType, ?PhpVersions $phpVersions = null): TypeResult { if ($leftType instanceof NeverType || $rightType instanceof NeverType) { return new TypeResult(new ConstantBooleanType(false), []); @@ -1178,10 +1179,30 @@ public function resolveEqualType(Type $leftType, Type $rightType): TypeResult } if ($leftType instanceof ConstantArrayType && $rightType instanceof ConstantArrayType) { - return $this->resolveConstantArrayTypeComparison($leftType, $rightType, fn ($leftValueType, $rightValueType): TypeResult => $this->resolveEqualType($leftValueType, $rightValueType)); + return $this->resolveConstantArrayTypeComparison($leftType, $rightType, fn ($leftValueType, $rightValueType): TypeResult => $this->resolveEqualType($leftValueType, $rightValueType, $phpVersions)); } - return new TypeResult($leftType->looseCompare($rightType, $this->phpVersion), []); + $versions = $phpVersions !== null ? LooseComparisonHelper::getRepresentativePhpVersions($phpVersions) : [$this->phpVersion]; + $result = LooseComparisonHelper::looseCompareInVersions($leftType, $rightType, $versions); + if ($result->isTrue()->yes() || $result->isFalse()->yes()) { + return new TypeResult($result, []); + } + + if ( + LooseComparisonHelper::getPossiblyLooselyEqualTypeInVersions($leftType, $versions)->isSuperTypeOf($rightType)->no() + || LooseComparisonHelper::getPossiblyLooselyEqualTypeInVersions($rightType, $versions)->isSuperTypeOf($leftType)->no() + ) { + return new TypeResult(new ConstantBooleanType(false), []); + } + + if ( + LooseComparisonHelper::getAlwaysLooselyEqualTypeInVersions($leftType, $versions)->isSuperTypeOf($rightType)->yes() + || LooseComparisonHelper::getAlwaysLooselyEqualTypeInVersions($rightType, $versions)->isSuperTypeOf($leftType)->yes() + ) { + return new TypeResult(new ConstantBooleanType(true), []); + } + + return new TypeResult($result, []); } /** diff --git a/src/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRule.php b/src/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRule.php index 227d2dde608..0c8423926fa 100644 --- a/src/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRule.php +++ b/src/Rules/Comparison/ImpossibleInArrayHaystackFiniteTypesRule.php @@ -8,6 +8,7 @@ use PHPStan\DependencyInjection\AutowiredParameter; use PHPStan\DependencyInjection\Conditional; use PHPStan\DependencyInjection\RegisteredRule; +use PHPStan\Php\PhpVersions; use PHPStan\Reflection\InitializerExprTypeResolver; use PHPStan\Rules\IdentifierRuleError; use PHPStan\Rules\Rule; @@ -94,9 +95,10 @@ public function processNode(Node $node, Scope $scope): array /** @var array, bool}> $deadValueTypes */ $deadValueTypes = []; $anyPossibleMatch = false; + $phpVersions = $scope->getPhpVersion(); foreach ($constantArrays as $constantArray) { foreach ($constantArray->getValueTypes() as $valueType) { - $canNeverMatchReasons = $this->findCanNeverMatchReasons($needleType, $valueType, $isStrict); + $canNeverMatchReasons = $this->findCanNeverMatchReasons($needleType, $valueType, $isStrict, $phpVersions); if ($canNeverMatchReasons === null) { $anyPossibleMatch = true; continue; @@ -151,7 +153,7 @@ private function isAnotherValueOfNeedleType(Type $needleType, Type $valueType): /** * @return list|null Reasons for the impossibility when the value can never match, null when it can match. */ - private function findCanNeverMatchReasons(Type $needleType, Type $valueType, TrinaryLogic $isStrict): ?array + private function findCanNeverMatchReasons(Type $needleType, Type $valueType, TrinaryLogic $isStrict, PhpVersions $phpVersions): ?array { if ($isStrict->yes()) { $result = $this->initializerExprTypeResolver->resolveIdenticalType($needleType, $valueType); @@ -160,7 +162,7 @@ private function findCanNeverMatchReasons(Type $needleType, Type $valueType, Tri } if ($isStrict->no()) { - $result = $this->initializerExprTypeResolver->resolveEqualType($needleType, $valueType); + $result = $this->initializerExprTypeResolver->resolveEqualType($needleType, $valueType, $phpVersions); return $result->type->isFalse()->yes() ? $result->reasons : null; } @@ -170,7 +172,7 @@ private function findCanNeverMatchReasons(Type $needleType, Type $valueType, Tri return null; } - $equalResult = $this->initializerExprTypeResolver->resolveEqualType($needleType, $valueType); + $equalResult = $this->initializerExprTypeResolver->resolveEqualType($needleType, $valueType, $phpVersions); if (!$equalResult->type->isFalse()->yes()) { return null; } diff --git a/src/Rules/Comparison/SwitchConditionRule.php b/src/Rules/Comparison/SwitchConditionRule.php index c4f54822c4f..0f66f11ca9b 100644 --- a/src/Rules/Comparison/SwitchConditionRule.php +++ b/src/Rules/Comparison/SwitchConditionRule.php @@ -15,6 +15,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; +use PHPStan\Type\LooseComparisonHelper; use PHPStan\Type\Type; use PHPStan\Type\VerbosityLevel; use function count; @@ -34,7 +35,6 @@ public function __construct( private ConstantConditionRuleHelper $constantConditionRuleHelper, private ConstantConditionInTraitHelper $constantConditionInTraitHelper, private ExprPrinter $exprPrinter, - private PhpVersion $phpVersion, #[AutowiredParameter] private bool $treatPhpDocTypesAsCertain, ) @@ -53,6 +53,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE $nextCaseIsDeadForType = false; $nextCaseIsDeadForNativeType = false; $seenCases = []; + $phpVersions = LooseComparisonHelper::getRepresentativePhpVersions($scope->getPhpVersion()); foreach ($node->getArms() as $arm) { if ( @@ -71,7 +72,7 @@ public function processNode(Node $node, Scope&NodeCallbackInvoker&CollectedDataE $caseValueType = $finiteTypes[0]; $firstSeen = null; foreach ($seenCases as $seenCase) { - if ($this->isDuplicateCase($seenCase['type'], $caseValueType)) { + if ($this->isDuplicateCase($seenCase['type'], $caseValueType, $phpVersions)) { $firstSeen = $seenCase; break; } @@ -172,14 +173,16 @@ private function isConstantBoolean(Type $type): bool * same set of subject values. Besides identical values, `switch` compares * with loose `==`, so two numerically-equal constants (e.g. 1, '1' and 1.0) * are duplicates too - they cannot be told apart by a `switch`. + * + * @param non-empty-list $phpVersions */ - private function isDuplicateCase(Type $seenType, Type $caseValueType): bool + private function isDuplicateCase(Type $seenType, Type $caseValueType, array $phpVersions): bool { if ($seenType->equals($caseValueType)) { return true; } - return $seenType->looseCompare($caseValueType, $this->phpVersion)->isTrue()->yes(); + return LooseComparisonHelper::looseCompareInVersions($seenType, $caseValueType, $phpVersions)->isTrue()->yes(); } } diff --git a/src/Type/LooseComparisonHelper.php b/src/Type/LooseComparisonHelper.php index 6a5b7da3179..2ad5ef58ee0 100644 --- a/src/Type/LooseComparisonHelper.php +++ b/src/Type/LooseComparisonHelper.php @@ -2,18 +2,37 @@ namespace PHPStan\Type; +use PHPStan\Analyser\ConstantResolver; use PHPStan\Php\PhpVersion; +use PHPStan\Php\PhpVersions; use PHPStan\ShouldNotHappenException; use PHPStan\Turbo\ReferencedByTurboExtension; +use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\NonEmptyArrayType; +use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantFloatType; +use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\Constant\ConstantStringType; +use function abs; +use function array_slice; +use function ceil; +use function count; +use function floor; use function is_bool; +use function is_finite; use function is_float; use function is_infinite; +use function is_int; use function is_nan; use function is_numeric; use function is_string; use function preg_match; use function strcmp; +use const INF; +use const NAN; +use const PHP_INT_MAX; +use const PHP_INT_MIN; /** * Evaluates PHP's comparison of two scalar values (`==`, `<`, `<=`, `<=>`) the way @@ -23,11 +42,448 @@ final class LooseComparisonHelper { + private const LOOSE_COMPARISON_PHP_VERSION_BOUNDARIES = [80000, 80400]; + private const WHITESPACE = " \t\n\r\v\f"; // the integer digits are captured only for a number without a fraction, the exponent separately private const NUMBER = '([+-]?(?:\d+\.\d*|\.\d+|(\d+))([eE][+-]?\d+)?)'; + // a float with a larger magnitude is not exact enough to tell its neighbouring integers apart + private const EXACT_FLOAT_INTEGER_LIMIT = 9007199254740992; + + /** + * The values $type can loosely equal: `$type == $x` is false for every $x outside of it. + */ + public static function getPossiblyLooselyEqualType(Type $type, PhpVersion $phpVersion): Type + { + if ($type instanceof UnionType) { + $types = []; + foreach ($type->getTypes() as $innerType) { + $types[] = self::getPossiblyLooselyEqualType($innerType, $phpVersion); + } + + return TypeCombinator::union(...$types); + } + + if ($type->isNull()->yes()) { + return self::getAlwaysLooselyEqualType($type, $phpVersion); + } + if ($type->isTrue()->yes()) { + return StaticTypeFactory::truthy(); + } + if ($type->isFalse()->yes()) { + return TypeCombinator::union(StaticTypeFactory::falsey(), self::createNumberLikeObjectType($phpVersion)); + } + + if ($type->isInteger()->yes()) { + $integerType = $type instanceof IntegerRangeType || $type instanceof ConstantIntegerType ? $type : new IntegerType(); + [$min, $max] = $type instanceof IntegerRangeType + ? [$type->getMin() ?? PHP_INT_MIN, $type->getMax() ?? PHP_INT_MAX] + : ($type instanceof ConstantIntegerType ? [$type->getValue(), $type->getValue()] : [PHP_INT_MIN, PHP_INT_MAX]); + + return TypeCombinator::union( + self::getNumberPartnerType($integerType, (float) $min, (float) $max, $phpVersion), + self::getBooleanAndNullPartnerType($min <= 0 && $max >= 0, $min !== 0 || $max !== 0), + ); + } + + if ($type->isFloat()->yes()) { + if ($type instanceof FloatRangeType) { + [$min, $max, $nan] = [$type->getMin(), $type->getMax(), false]; + } elseif ($type instanceof ConstantFloatType) { + $value = $type->getValue(); + $nan = is_nan($value); + [$min, $max] = $nan ? [INF, -INF] : [$value, $value]; + } else { + [$min, $max, $nan] = [-INF, INF, true]; + } + + return TypeCombinator::union( + self::getNumberPartnerType(self::getIntegerTypeInInterval($min, $max), $min, $max, $phpVersion), + self::getBooleanAndNullPartnerType($min <= 0.0 && $max >= 0.0, $nan || $min !== 0.0 || $max !== 0.0), + ); + } + + if ($type->isString()->yes()) { + $constantStrings = $type->getConstantStrings(); + if (count($constantStrings) === 1) { + return self::getConstantStringPartnerType($constantStrings[0]->getValue(), $phpVersion); + } + + // a string compares with null as with '', and with a bool by its truthiness + $booleanAndNull = [new ConstantBooleanType(true)]; + if (!$type->isNonEmptyString()->yes()) { + $booleanAndNull[] = new NullType(); + } + if (!$type->isNonFalsyString()->yes()) { + $booleanAndNull[] = new ConstantBooleanType(false); + } + $booleanAndNull = TypeCombinator::union(...$booleanAndNull); + + if ($type->isNumericString()->yes()) { + // a numeric string compares with a non-numeric one byte by byte, so they differ + return TypeCombinator::union( + new IntegerType(), + FloatRangeType::fromInterval(-INF, INF), + self::createNumericStringType(), + self::createStringableObjectType($phpVersion), + new ResourceType(), + $booleanAndNull, + ); + } + + // a non-numeric string equals only itself + return TypeCombinator::union( + $type, + $type->isNumericString()->no() ? new NeverType() : self::createNumericStringType(), + new IntegerType(), + new FloatType(), + self::createStringableObjectType($phpVersion), + new ResourceType(), + $booleanAndNull, + ); + } + + if ($type->isEnum()->yes() && count($type->getEnumCases()) === 1) { + return self::getAlwaysLooselyEqualType($type, $phpVersion); + } + + if ($type->isArray()->yes()) { + $isNonEmpty = $type->isIterableAtLeastOnce(); + if ($isNonEmpty->no()) { + return self::getAlwaysLooselyEqualType($type, $phpVersion); + } + + $arrayType = new ArrayType(new MixedType(), new MixedType()); + if ($isNonEmpty->yes()) { + return TypeCombinator::union(new ConstantBooleanType(true), TypeCombinator::intersect($arrayType, new NonEmptyArrayType())); + } + + return TypeCombinator::union($arrayType, new BooleanType(), new NullType()); + } + + return new MixedType(); + } + + /** + * The values $type always loosely equals: `$type == $x` is true for every $x of it. + */ + public static function getAlwaysLooselyEqualType(Type $type, PhpVersion $phpVersion): Type + { + if ($type instanceof UnionType) { + // one member at a time: intersecting all the unions at once multiplies them out + $alwaysEqualType = null; + foreach ($type->getTypes() as $innerType) { + $innerAlwaysEqualType = self::getAlwaysLooselyEqualType($innerType, $phpVersion); + $alwaysEqualType = $alwaysEqualType === null ? $innerAlwaysEqualType : TypeCombinator::intersect($alwaysEqualType, $innerAlwaysEqualType); + if ($alwaysEqualType instanceof NeverType) { + break; + } + } + + return $alwaysEqualType ?? new NeverType(); + } + + if ($type->isTrue()->yes()) { + return new MixedType(subtractedType: TypeCombinator::union(StaticTypeFactory::falsey(), self::createNumberLikeObjectType($phpVersion))); + } + if ($type->isFalse()->yes()) { + return StaticTypeFactory::falsey(); + } + + $values = $type->getConstantScalarValues(); + if (count($values) === 1) { + $value = $values[0]; + $candidates = [null, false, true, 0, 0.0, '', '0', $value]; + if (is_int($value)) { + $candidates[] = (float) $value; + $candidates[] = (string) $value; + } elseif (is_float($value)) { + $candidates[] = 'NAN'; + $candidates[] = 'INF'; + $candidates[] = '-INF'; + if (is_finite($value) && floor($value) === $value && abs($value) <= self::EXACT_FLOAT_INTEGER_LIMIT) { + $candidates[] = (int) $value; + } + } elseif (is_string($value)) { + $candidates[] = NAN; + $candidates[] = INF; + $candidates[] = -INF; + $number = self::parseNumericString($value, $phpVersion); + $number = $number !== null ? $number[0] : self::parseNumericPrefix($value) ?? 0; + $candidates[] = (float) $number; + if (is_int($number)) { + $candidates[] = $number; + } elseif (is_finite($number) && floor($number) === $number && abs($number) <= self::EXACT_FLOAT_INTEGER_LIMIT) { + $candidates[] = (int) $number; + } + } + + // null compares with an array by its emptiness + $types = $value === null ? [new ConstantArrayType([], [])] : []; + foreach ($candidates as $candidate) { + if (self::compareValues($value, $candidate, $phpVersion) !== 0) { + continue; + } + + $types[] = self::createValueType($candidate); + } + + return TypeCombinator::union(...$types); + } + + if ($type->isEnum()->yes() && count($type->getEnumCases()) === 1) { + return TypeCombinator::union($type->getEnumCases()[0], new ConstantBooleanType(true)); + } + + if ($type->isArray()->yes() && $type->isIterableAtLeastOnce()->no()) { + return TypeCombinator::union(new NullType(), new ConstantBooleanType(false), new ConstantArrayType([], [])); + } + + return new NeverType(); + } + + /** + * Whether a mixed subject that loosely equals a value of $possiblyLooselyEqualType + * should stay mixed: narrowing it would add objects of classes like Stringable or GMP + * that say nothing about the value. Enum cases are precise enough to narrow to. + */ + public static function keepsMixedUnnarrowed(Type $possiblyLooselyEqualType): bool + { + if ($possiblyLooselyEqualType->isObject()->no()) { + return false; + } + + return !TypeCombinator::intersect($possiblyLooselyEqualType, new ObjectWithoutClassType())->isEnum()->yes(); + } + + /** + * One PHP version for each range of $phpVersions that loosely compares the same way. + * The rules change in PHP 8.0 (string to number comparison) and 8.4 (BcMath\Number). + * + * @return non-empty-list + */ + public static function getRepresentativePhpVersions(PhpVersions $phpVersions): array + { + $versionType = $phpVersions->getType(); + $versionIds = $versionType->getConstantScalarValues(); + if (count($versionIds) === 1 && is_int($versionIds[0])) { + return [new PhpVersion($versionIds[0])]; + } + + $mins = [ConstantResolver::PHP_MIN_ANALYZABLE_VERSION_ID, ...self::LOOSE_COMPARISON_PHP_VERSION_BOUNDARIES]; + $versions = []; + foreach ($mins as $i => $min) { + $segment = IntegerRangeType::fromInterval($min, isset($mins[$i + 1]) ? $mins[$i + 1] - 1 : null); + if ($segment->isSuperTypeOf($versionType)->no()) { + continue; + } + + $versions[] = new PhpVersion($min); + } + if ($versions === []) { + // no PHP version reaches this code, any of them will do + return [new PhpVersion($mins[0])]; + } + + return $versions; + } + + /** + * The values $type can loosely equal on any of $phpVersions. + * + * @param non-empty-list $phpVersions + */ + public static function getPossiblyLooselyEqualTypeInVersions(Type $type, array $phpVersions): Type + { + $types = []; + foreach ($phpVersions as $phpVersion) { + $types[] = $type->getPossiblyLooselyEqualType($phpVersion); + } + + return TypeCombinator::union(...$types); + } + + /** + * The values $type loosely equals on every one of $phpVersions. + * + * @param non-empty-list $phpVersions + */ + public static function getAlwaysLooselyEqualTypeInVersions(Type $type, array $phpVersions): Type + { + $types = []; + foreach ($phpVersions as $phpVersion) { + $types[] = $type->getAlwaysLooselyEqualType($phpVersion); + } + + return TypeCombinator::intersect(...$types); + } + + /** + * `$leftType == $rightType` on $phpVersions, constant only when they all agree. + * + * @param non-empty-list $phpVersions + */ + public static function looseCompareInVersions(Type $leftType, Type $rightType, array $phpVersions): BooleanType + { + $result = $leftType->looseCompare($rightType, $phpVersions[0]); + foreach (array_slice($phpVersions, 1) as $phpVersion) { + if (!$result->equals($leftType->looseCompare($rightType, $phpVersion))) { + return new BooleanType(); + } + } + + return $result; + } + + /** + * The numbers, numeric strings, objects and resources that can equal a number of the + * float interval, whose integers are $integerType. Booleans and null are left out, + * NAN equals none of them. + */ + private static function getNumberPartnerType(Type $integerType, float $min, float $max, PhpVersion $phpVersion): Type + { + $types = [$integerType]; + if ($min <= $max) { + $types[] = FloatRangeType::fromInterval($min, $max); + $types[] = self::createNumericStringType(); + // an object compares as 1, unless it is number-like + $types[] = $min <= 1.0 && $max >= 1.0 ? new ObjectWithoutClassType() : self::createNumberLikeObjectType($phpVersion); + if ($max >= 1.0 && !$integerType->isInteger()->no()) { + // as its integer ID + $types[] = new ResourceType(); + } + if (!$phpVersion->castsNumbersToStringsOnLooseComparison()) { + // as the number the string starts with + $types[] = new StringType(); + } else { + if ($max === INF) { + $types[] = new ConstantStringType('INF'); + } + if ($min === -INF) { + $types[] = new ConstantStringType('-INF'); + } + } + } + + return TypeCombinator::union(...$types); + } + + private static function getBooleanAndNullPartnerType(bool $canBeZero, bool $canBeNonZero): Type + { + $types = []; + if ($canBeZero) { + $types[] = new NullType(); + $types[] = new ConstantBooleanType(false); + } + if ($canBeNonZero) { + $types[] = new ConstantBooleanType(true); + } + + return TypeCombinator::union(...$types); + } + + private static function getConstantStringPartnerType(string $value, PhpVersion $phpVersion): Type + { + $types = [ + new ConstantStringType($value), + self::createStringableObjectType($phpVersion), + self::getBooleanAndNullPartnerType(false, $value !== '' && $value !== '0'), + self::getAlwaysLooselyEqualType(new ConstantStringType($value), $phpVersion), + ]; + + $number = self::parseNumericString($value, $phpVersion); + if ($number !== null) { + $number = $number[0]; + $types[] = self::createNumericStringType(); + $types[] = self::createNumberType($number); + if ($number >= 1 && floor($number) === (float) $number) { + $types[] = new ResourceType(); + } + } elseif (!$phpVersion->castsNumbersToStringsOnLooseComparison()) { + $number = self::parseNumericPrefix($value) ?? 0; + $types[] = self::createNumberType($number); + if ($number >= 1 && floor($number) === (float) $number) { + $types[] = new ResourceType(); + } + } + + return TypeCombinator::union(...$types); + } + + /** The integers and floats equal to $number. */ + private static function createNumberType(int|float $number): Type + { + $float = (float) $number; + $integerType = is_int($number) ? new ConstantIntegerType($number) : self::getIntegerTypeInInterval($float, $float); + + return TypeCombinator::union($integerType, FloatRangeType::fromInterval($float, $float)); + } + + /** The integers that compare equal to a float of the interval. */ + private static function getIntegerTypeInInterval(float $min, float $max): Type + { + if ($min > $max || $min > (float) PHP_INT_MAX || $max < (float) PHP_INT_MIN) { + return new NeverType(); + } + + if ($min < -self::EXACT_FLOAT_INTEGER_LIMIT) { + $integerMin = null; + } elseif ($min > self::EXACT_FLOAT_INTEGER_LIMIT) { + $integerMin = self::EXACT_FLOAT_INTEGER_LIMIT; + } else { + $integerMin = (int) ceil($min); + } + + if ($max > self::EXACT_FLOAT_INTEGER_LIMIT) { + $integerMax = null; + } elseif ($max < -self::EXACT_FLOAT_INTEGER_LIMIT) { + $integerMax = -self::EXACT_FLOAT_INTEGER_LIMIT; + } else { + $integerMax = (int) floor($max); + } + + return IntegerRangeType::fromInterval($integerMin, $integerMax); + } + + /** GMP, BcMath\Number (since PHP 8.4) and SimpleXMLElement objects compare as their number, and can be falsy. */ + private static function createNumberLikeObjectType(PhpVersion $phpVersion): Type + { + $types = [new ObjectType('GMP'), new ObjectType('SimpleXMLElement')]; + if ($phpVersion->getVersionId() >= 80400) { + $types[] = new ObjectType('BcMath\\Number'); + } + + return TypeCombinator::union(...$types); + } + + /** The objects that compare with a string as their string. */ + private static function createStringableObjectType(PhpVersion $phpVersion): Type + { + // a class with __toString() implements Stringable since PHP 8.0 + if ($phpVersion->getVersionId() < 80000) { + return new ObjectWithoutClassType(); + } + + return TypeCombinator::union(new ObjectType('Stringable'), new ObjectType('GMP')); + } + + private static function createNumericStringType(): Type + { + return new IntersectionType([new StringType(), new AccessoryNumericStringType()]); + } + + private static function createValueType(bool|float|int|string|null $value): Type + { + if (is_float($value) && !is_nan($value)) { + // both zeros + return FloatRangeType::fromInterval($value, $value); + } + + return ConstantTypeHelper::getTypeFromValue($value); + } + public static function compareConstantScalars(ConstantScalarType $leftType, ConstantScalarType $rightType, PhpVersion $phpVersion): BooleanType { return new ConstantBooleanType(self::compareValues($leftType->getValue(), $rightType->getValue(), $phpVersion) === 0); diff --git a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php index ad857c7086f..83f31464b61 100644 --- a/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php +++ b/src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php @@ -18,6 +18,7 @@ use PHPStan\Type\ArrayType; use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\FunctionTypeSpecifyingExtension; +use PHPStan\Type\LooseComparisonHelper; use PHPStan\Type\MixedType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; @@ -62,10 +63,12 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $arrayType = $scope->getType($arrayExpr); $arrayValueType = $arrayType->getIterableValueType(); + // loose comparison is strict for these: an enum case loosely equals only + // itself and true, a string pair compares as numbers when both are numeric $isStrictComparison = $isStrictComparison - || $needleType->isEnum()->yes() - || $arrayValueType->isEnum()->yes() - || ($needleType->isString()->yes() && $arrayValueType->isString()->yes()) + || ($needleType->isEnum()->yes() && $arrayValueType->isTrue()->no()) + || ($arrayValueType->isEnum()->yes() && $needleType->isTrue()->no()) + || ($needleType->isString()->yes() && $arrayValueType->isString()->yes() && ($needleType->isNumericString()->no() || $arrayValueType->isNumericString()->no())) || ($needleType->isInteger()->yes() && $arrayValueType->isInteger()->yes()) || ($needleType->isFloat()->yes() && $arrayValueType->isFloat()->yes()) || ($needleType->isBoolean()->yes() && $arrayValueType->isBoolean()->yes()); @@ -110,20 +113,31 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n } if (!$isStrictComparison) { - if ( - $context->true() - && $arrayType->isArray()->yes() - && $arrayType->getIterableValueType()->isSuperTypeOf($needleType)->yes() - ) { - return $this->typeSpecifier->create( - $args[1]->value, - TypeCombinator::intersect($arrayType, new NonEmptyArrayType()), - $context, - $scope, + $specifiedTypes = new SpecifiedTypes(); + if ($context->true()) { + // the needle is one of the values an item can loosely equal + $possiblyEqualType = LooseComparisonHelper::getPossiblyLooselyEqualTypeInVersions( + $arrayValueType, + LooseComparisonHelper::getRepresentativePhpVersions($scope->getPhpVersion()), ); + if ( + !$possiblyEqualType->isSuperTypeOf(new MixedType())->yes() + && (!$needleType instanceof MixedType || !LooseComparisonHelper::keepsMixedUnnarrowed($possiblyEqualType)) + ) { + $specifiedTypes = $this->typeSpecifier->create($needleExpr, $possiblyEqualType, $context, $scope); + } + + if ($arrayType->isArray()->yes()) { + $specifiedTypes = $specifiedTypes->unionWith($this->typeSpecifier->create( + $args[1]->value, + new NonEmptyArrayType(), + $context, + $scope, + )); + } } - return new SpecifiedTypes(); + return $specifiedTypes; } $specifiedTypes = new SpecifiedTypes(); diff --git a/src/Type/Traits/OperatorTypeTrait.php b/src/Type/Traits/OperatorTypeTrait.php index 7806358458c..193f60549fd 100644 --- a/src/Type/Traits/OperatorTypeTrait.php +++ b/src/Type/Traits/OperatorTypeTrait.php @@ -4,6 +4,7 @@ use PHPStan\Php\PhpVersion; use PHPStan\Php\PhpVersionFactory; +use PHPStan\Type\LooseComparisonHelper; use PHPStan\Type\OperatorTypeHelper; use PHPStan\Type\Type; @@ -75,6 +76,16 @@ public function bitwiseShiftRight(Type $right, PhpVersion $phpVersion): Type return OperatorTypeHelper::shiftRight($this, $right, $phpVersion); } + public function getPossiblyLooselyEqualType(PhpVersion $phpVersion): Type + { + return LooseComparisonHelper::getPossiblyLooselyEqualType($this, $phpVersion); + } + + public function getAlwaysLooselyEqualType(PhpVersion $phpVersion): Type + { + return LooseComparisonHelper::getAlwaysLooselyEqualType($this, $phpVersion); + } + public function stringConcatenate(Type $right, PhpVersion $phpVersion): Type { return OperatorTypeHelper::concat($this, $right, $phpVersion); diff --git a/src/Type/Type.php b/src/Type/Type.php index 5a22fcce1b5..aba74d884fe 100644 --- a/src/Type/Type.php +++ b/src/Type/Type.php @@ -605,6 +605,18 @@ public function isScalar(): TrinaryLogic; public function looseCompare(Type $type, PhpVersion $phpVersion): BooleanType; + /** + * The values this type can be loosely equal to: `$this == $x` is false for any $x outside of it. + * The true branch of a `==` narrows the other side by intersecting with it. + */ + public function getPossiblyLooselyEqualType(PhpVersion $phpVersion): Type; + + /** + * The values this type is always loosely equal to: `$this == $x` is true for any $x of it. + * The false branch of a `==` narrows the other side by removing it. + */ + public function getAlwaysLooselyEqualType(PhpVersion $phpVersion): Type; + /** * Type narrowing methods for comparison operators. * For example, for ConstantIntegerType(5), getSmallerType() returns int. diff --git a/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php b/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php index 86b1f1b4bc9..dd17d275393 100644 --- a/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php +++ b/tests/PHPStan/Analyser/LooseConstComparisonPhp7Test.php @@ -14,6 +14,7 @@ public static function dataFileAsserts(): iterable // _NOT_ the current php runtime version yield from self::gatherAssertTypes(__DIR__ . '/data/loose-const-comparison-php7.php'); yield from self::gatherAssertTypes(__DIR__ . '/data/bug-11732-php7.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/loose-equality-narrowing-php7.php'); } /** diff --git a/tests/PHPStan/Analyser/ScopePhpVersionRangeReturnTypeExtensionsTest.php b/tests/PHPStan/Analyser/ScopePhpVersionRangeReturnTypeExtensionsTest.php index d16c74f2b39..9861e04fac4 100644 --- a/tests/PHPStan/Analyser/ScopePhpVersionRangeReturnTypeExtensionsTest.php +++ b/tests/PHPStan/Analyser/ScopePhpVersionRangeReturnTypeExtensionsTest.php @@ -11,6 +11,7 @@ class ScopePhpVersionRangeReturnTypeExtensionsTest extends TypeInferenceTestCase public static function dataFileAsserts(): iterable { yield from self::gatherAssertTypes(__DIR__ . '/data/scope-php-version-range-return-type-extensions.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/loose-equality-php-version-range.php'); } /** diff --git a/tests/PHPStan/Analyser/TypeSpecifierTest.php b/tests/PHPStan/Analyser/TypeSpecifierTest.php index 71e8216d9ac..e440c2e78bf 100644 --- a/tests/PHPStan/Analyser/TypeSpecifierTest.php +++ b/tests/PHPStan/Analyser/TypeSpecifierTest.php @@ -1320,8 +1320,8 @@ public static function dataCondition(): iterable ), ), [ - '$float' => 'float', - '(int) $float' => 'int', + '$float' => 'float<-9.223372036854776E+18, 9.223372036854776E+18>|int|numeric-string', + '(int) $float' => '\'-INF\'|\'INF\'|bool|float|int|object|resource|numeric-string|null', ], [], ], diff --git a/tests/PHPStan/Analyser/data/loose-equality-narrowing-php7.php b/tests/PHPStan/Analyser/data/loose-equality-narrowing-php7.php new file mode 100644 index 00000000000..b3fed80357d --- /dev/null +++ b/tests/PHPStan/Analyser/data/loose-equality-narrowing-php7.php @@ -0,0 +1,41 @@ +|int<2, max>', $int); + } + + if ($string == 0) { + assertType('string', $string); + } + + if ($float == 'INF') { + assertType('0.0', $float); + } + + if ($mixed == 'abc') { + assertType('mixed', $mixed); + } + + if ($mixed == NAN) { + assertType('true', $mixed); + } + + assertType('bool', $int == 'abc'); + assertType('false', $int == 1.5); +} diff --git a/tests/PHPStan/Analyser/data/loose-equality-php-version-range.php b/tests/PHPStan/Analyser/data/loose-equality-php-version-range.php new file mode 100644 index 00000000000..e9eed78840a --- /dev/null +++ b/tests/PHPStan/Analyser/data/loose-equality-php-version-range.php @@ -0,0 +1,76 @@ + $config + */ +function doFoo($key, int $int, float $float, string $string, array $config): void +{ + // 0 == 'abc' on PHP 7, not on PHP 8 + assertType('bool', 0 == 'abc'); + assertType('bool', $int == 'abc'); + // INF == 'INF' on PHP 8, not on PHP 7 + assertType('bool', INF == 'INF'); + assertType('true', 1 == '1'); + assertType('false', 1 == 'abc'); + + if ($key == 'abc') { + assertType("0|'abc'", $key); + } else { + // 0 is in the false branch on PHP 8 + assertType('int|string', $key); + } + + if ($int == 'abc') { + assertType('0', $int); + } else { + assertType('int', $int); + } + + if ($float == 'INF') { + assertType('0.0|INF', $float); + } + + if (in_array($key, ['a', 'b'])) { + assertType("0|'a'|'b'", $key); + } + + foreach ($config as $type => $args) { + switch ($type) { + case 'options': + break; + case 'widgets': + assertType("0|'widgets'", $type); + break; + } + } +} + +/** + * @param int|string $key + */ +function narrowedByGuard($key, int $int): void +{ + if (PHP_VERSION_ID >= 80000) { + assertType('false', 0 == 'abc'); + assertType('true', INF == 'INF'); + if ($key == 'abc') { + assertType("'abc'", $key); + } + if ($int == 'abc') { + assertType('*NEVER*', $int); + } + } else { + assertType('true', 0 == 'abc'); + assertType('false', INF == 'INF'); + if ($key == 'abc') { + assertType("0|'abc'", $key); + } else { + assertType('int|int<1, max>|string', $key); + } + } +} diff --git a/tests/PHPStan/Analyser/nsrt/bcmath-number.php b/tests/PHPStan/Analyser/nsrt/bcmath-number.php index a3e1860ae20..adcff1851fa 100644 --- a/tests/PHPStan/Analyser/nsrt/bcmath-number.php +++ b/tests/PHPStan/Analyser/nsrt/bcmath-number.php @@ -195,8 +195,8 @@ public function bcVsNull(Number $a): void assertType('true', $a > $b); assertType('true', $a >= $b); assertType('int<-1, 1>', $a <=> $b); - assertType('bool', $a == $b); - assertType('bool', $a != $b); + assertType('false', $a == $b); + assertType('true', $a != $b); assertType('*ERROR*', $a & $b); assertType('*ERROR*', $a ^ $b); assertType('*ERROR*', $a | $b); @@ -223,8 +223,8 @@ public function bcVsArray(Number $a, array $b): void assertType('bool', $a > $b); assertType('bool', $a >= $b); assertType('int<-1, 1>', $a <=> $b); - assertType('bool', $a == $b); - assertType('bool', $a != $b); + assertType('false', $a == $b); + assertType('true', $a != $b); assertType('*ERROR*', $a & $b); assertType('*ERROR*', $a ^ $b); assertType('*ERROR*', $a | $b); diff --git a/tests/PHPStan/Analyser/nsrt/bug-12312.php b/tests/PHPStan/Analyser/nsrt/bug-12312.php index f67d1aa3cb3..8b88a298929 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-12312.php +++ b/tests/PHPStan/Analyser/nsrt/bug-12312.php @@ -36,7 +36,7 @@ public function sayLowercase3(string $s): void if ($s != '0') { assertType('lowercase-string&non-falsy-string', $s); } - assertType('lowercase-string&non-empty-string', $s); + assertType('(lowercase-string&non-empty-string&numeric-string)|(lowercase-string&non-falsy-string)', $s); } /** @@ -47,7 +47,7 @@ public function sayLowercase4(string $s): void if ('0' != $s) { assertType('lowercase-string&non-falsy-string', $s); } - assertType('lowercase-string&non-empty-string', $s); + assertType('(lowercase-string&non-empty-string&numeric-string)|(lowercase-string&non-falsy-string)', $s); } /** @@ -80,7 +80,7 @@ public function sayUppercase3(string $s): void if ($s != '0') { assertType('non-falsy-string&uppercase-string', $s); } - assertType('non-empty-string&uppercase-string', $s); + assertType('(non-empty-string&numeric-string&uppercase-string)|(non-falsy-string&uppercase-string)', $s); } /** @@ -91,7 +91,7 @@ public function sayUppercase4(string $s): void if ('0' != $s) { assertType('non-falsy-string&uppercase-string', $s); } - assertType('non-empty-string&uppercase-string', $s); + assertType('(non-empty-string&numeric-string&uppercase-string)|(non-falsy-string&uppercase-string)', $s); } /** @@ -124,7 +124,7 @@ public function sayBoth3(string $s): void if ($s != '0') { assertType('lowercase-string&non-falsy-string&uppercase-string', $s); } - assertType('lowercase-string&non-empty-string&uppercase-string', $s); + assertType('(lowercase-string&non-empty-string&numeric-string&uppercase-string)|(lowercase-string&non-falsy-string&uppercase-string)', $s); } /** @@ -135,6 +135,6 @@ public function sayBoth4(string $s): void if ('0' != $s) { assertType('lowercase-string&non-falsy-string&uppercase-string', $s); } - assertType('lowercase-string&non-empty-string&uppercase-string', $s); + assertType('(lowercase-string&non-empty-string&numeric-string&uppercase-string)|(lowercase-string&non-falsy-string&uppercase-string)', $s); } } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14047.php b/tests/PHPStan/Analyser/nsrt/bug-14047.php index 361e40b61f7..e30dea53ad5 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14047.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14047.php @@ -14,7 +14,7 @@ function test_strings(string $a, string $b): void if ($a !== '' && strtolower($a) == $b) { assertType('non-empty-string', $a); - assertType('lowercase-string&non-empty-string', $b); + assertType('(lowercase-string&non-empty-string)|numeric-string', $b); } if ($a !== '' && strtolower($a) === $b) { @@ -24,7 +24,7 @@ function test_strings(string $a, string $b): void if ($a !== '' && strtolower($a) == $b && $b !== '0') { assertType('non-empty-string', $a); - assertType('lowercase-string&non-falsy-string', $b); + assertType('(lowercase-string&non-falsy-string)|(non-falsy-string&numeric-string)', $b); } if ($a !== '' && strtolower($a) === $a) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-9662-enums.php b/tests/PHPStan/Analyser/nsrt/bug-9662-enums.php index 13a26b9582b..69ac5fa6c0a 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9662-enums.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9662-enums.php @@ -32,7 +32,7 @@ function doEnum(array $suite, array $arr) { if (in_array(Suit::Hearts, $arr) === false) { - assertType('array', $arr); + assertType('array', $arr); } else { assertType("non-empty-array", $arr); } @@ -97,7 +97,7 @@ function doBackedEnum(array $suite, array $arr, string $s, int $i, $mixed) { if (in_array(StringBackedSuit::Hearts, $arr) === false) { - assertType('array', $arr); + assertType('array', $arr); } else { assertType("non-empty-array", $arr); } diff --git a/tests/PHPStan/Analyser/nsrt/bug-9662.php b/tests/PHPStan/Analyser/nsrt/bug-9662.php index d88555a8632..955f2d2dace 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-9662.php +++ b/tests/PHPStan/Analyser/nsrt/bug-9662.php @@ -88,14 +88,14 @@ function doFoo(string $s, $a, $strings, $mixed) { assertType('array', $strings); if (in_array($mixed, $strings, false)) { - assertType('array', $strings); + assertType('non-empty-array', $strings); } else { assertType("array", $strings); } assertType('array', $strings); if (in_array($mixed, $strings)) { - assertType('array', $strings); + assertType('non-empty-array', $strings); } else { assertType("array", $strings); } diff --git a/tests/PHPStan/Analyser/nsrt/equal-narrow.php b/tests/PHPStan/Analyser/nsrt/equal-narrow.php index 774377f400f..df0aafc7a46 100644 --- a/tests/PHPStan/Analyser/nsrt/equal-narrow.php +++ b/tests/PHPStan/Analyser/nsrt/equal-narrow.php @@ -124,24 +124,24 @@ function doZero($x, $y, $z): void { // PHP 7.x/8.x compatibility: Keep zero in both cases if ($x == 0) { - assertType("0|0.0|''|'0'|'x'|false|null", $x); + assertType("0|0.0|'0'|BcMath\Number|GMP|SimpleXMLElement|false|null", $x); } else { assertType("1|''|'x'|array{}|object|true", $x); } if (0 != $x) { assertType("1|''|'x'|array{}|object|true", $x); } else { - assertType("0|0.0|''|'0'|'x'|false|null", $x); + assertType("0|0.0|'0'|BcMath\Number|GMP|SimpleXMLElement|false|null", $x); } if ($y == 0) { - assertType("0|string|null", $y); + assertType("0|numeric-string|null", $y); } else { assertType("int|int<1, max>|string", $y); } if ($z == 0) { - assertType("0|0.0|string|false|null", $z); + assertType('mixed', $z); } else { assertType("mixed~(0|0.0|'0'|false|null)", $z); } @@ -156,24 +156,24 @@ function doEmptyString($x, $y, $z): void { // PHP 7.x/8.x compatibility: Keep zero in both cases if ($x == '') { - assertType("0|0.0|''|false|null", $x); + assertType("''|GMP|Stringable|false|null", $x); } else { assertType("0|0.0|1|'0'|'x'|array{}|object|true", $x); } if ('' != $x) { assertType("0|0.0|1|'0'|'x'|array{}|object|true", $x); } else { - assertType("0|0.0|''|false|null", $x); + assertType("''|GMP|Stringable|false|null", $x); } if ($y == '') { - assertType("0|''|null", $y); + assertType("''|null", $y); } else { assertType("int|non-empty-string", $y); } if ($z == '') { - assertType("0|0.0|''|false|null", $z); + assertType('mixed', $z); } else { assertType("mixed~(''|false|null)", $z); } diff --git a/tests/PHPStan/Analyser/nsrt/equal.php b/tests/PHPStan/Analyser/nsrt/equal.php index e91a274257b..d9f9f407b42 100644 --- a/tests/PHPStan/Analyser/nsrt/equal.php +++ b/tests/PHPStan/Analyser/nsrt/equal.php @@ -156,7 +156,7 @@ public function doFoo(string $a, float $c): void assertType('true', 1 == 1); assertType('false', 1 == 0); - assertType('bool', $c == 'a'); + assertType('false', $c == 'a'); assertType('bool', $c == 1); assertType('bool', $c == 1.2); assertType('true', 1.2 == 1.2); diff --git a/tests/PHPStan/Analyser/nsrt/equality-narrowing-new-world.php b/tests/PHPStan/Analyser/nsrt/equality-narrowing-new-world.php index 29914710ad1..7fac601ee1e 100644 --- a/tests/PHPStan/Analyser/nsrt/equality-narrowing-new-world.php +++ b/tests/PHPStan/Analyser/nsrt/equality-narrowing-new-world.php @@ -402,12 +402,12 @@ public function moreLooseEquality(?bool $nb, $is, string $s, array $arr, string assertType('false|null', $nb); } if ($is == 0) { - assertType('0|string', $is); + assertType('0|numeric-string', $is); } else { assertType('int|int<1, max>|string', $is); } if ($is == '') { - assertType("0|''", $is); + assertType("''", $is); } else { assertType('int|non-empty-string', $is); } diff --git a/tests/PHPStan/Analyser/nsrt/in-array-enum.php b/tests/PHPStan/Analyser/nsrt/in-array-enum.php index 66ae5799800..d3bbbe6a352 100644 --- a/tests/PHPStan/Analyser/nsrt/in-array-enum.php +++ b/tests/PHPStan/Analyser/nsrt/in-array-enum.php @@ -37,10 +37,10 @@ public function nonConstantValues(FooUnitEnum $a, array $strings, array $ints): public function looseCheckEnumSpecifyNeedle(mixed $v): void { if (in_array($v, FooUnitEnum::cases())) { - assertType('InArrayEnum\FooUnitEnum::A|InArrayEnum\FooUnitEnum::B', $v); + assertType('InArrayEnum\FooUnitEnum::A|InArrayEnum\FooUnitEnum::B|true', $v); if (in_array($v, ['A', null, FooUnitEnum::B])) { - assertType('InArrayEnum\FooUnitEnum::B', $v); + assertType('InArrayEnum\FooUnitEnum::B|true', $v); } } diff --git a/tests/PHPStan/Analyser/nsrt/in_array_loose.php b/tests/PHPStan/Analyser/nsrt/in_array_loose.php index 78d2899b8cb..6b85bde84fd 100644 --- a/tests/PHPStan/Analyser/nsrt/in_array_loose.php +++ b/tests/PHPStan/Analyser/nsrt/in_array_loose.php @@ -15,16 +15,16 @@ public function looseComparison( string|null $stringOrNull, ): void { if (in_array($string, ['1', 'a'])) { - assertType("'1'|'a'", $string); + assertType("'a'|numeric-string", $string); } if (in_array($string, [1, 'a'])) { - assertType("string", $string); // could be '1'|'a' + assertType("'a'|numeric-string", $string); } if (in_array($int, [1, 2])) { assertType('1|2', $int); } if (in_array($int, ['1', 2])) { - assertType('int', $int); // could be 1|2 + assertType('1|2', $int); } if (in_array($bool, [true])) { assertType('true', $bool); @@ -36,13 +36,13 @@ public function looseComparison( assertType('1.0|2.0', $float); } if (in_array($float, ['1', 2.0])) { - assertType('float', $float); // could be 1.0|2.0 + assertType('1.0|2.0', $float); } if (in_array($stringOrInt, ['1', '2'])) { - assertType('int|string', $stringOrInt); // could be '1'|'2'|1|2 + assertType('1|2|numeric-string', $stringOrInt); } if (in_array($stringOrNull, ['1', 'a'])) { - assertType('string|null', $stringOrNull); // could be '1'|'a' + assertType("'a'|numeric-string", $stringOrNull); } } } diff --git a/tests/PHPStan/Analyser/nsrt/loose-comparisons.php b/tests/PHPStan/Analyser/nsrt/loose-comparisons.php index b860adc9c02..266d14f145b 100644 --- a/tests/PHPStan/Analyser/nsrt/loose-comparisons.php +++ b/tests/PHPStan/Analyser/nsrt/loose-comparisons.php @@ -558,7 +558,7 @@ public function sayNonFalsyStr( assertType('true', $phpStr == $phpStr); assertType('false', $phpStr == $emptyStr); - assertType('bool', $nonFalsyString == $true); + assertType('true', $nonFalsyString == $true); assertType('false', $nonFalsyString == $false); assertType('bool', $nonFalsyString == $one); assertType('bool', $nonFalsyString == $zero); // e.g. '0.0' == 0 is true (non-falsy numeric string compared numerically) @@ -884,8 +884,8 @@ public function sayConstUnion( assertType('bool', $emptyArr == $looseZero); assertType('bool', $arrShape == $looseOne); - assertType('bool', $arrShape == $constMix); - assertType('bool', $arrShape == $looseZero); + assertType('false', $arrShape == $constMix); + assertType('false', $arrShape == $looseZero); } /** diff --git a/tests/PHPStan/Analyser/nsrt/loose-equality-narrowing.php b/tests/PHPStan/Analyser/nsrt/loose-equality-narrowing.php new file mode 100644 index 00000000000..1aeefaf412e --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/loose-equality-narrowing.php @@ -0,0 +1,206 @@ += 8.4 + +namespace LooseEqualityNarrowing; + +use function PHPStan\Testing\assertType; + +class Foo +{ +} + +enum Suit +{ + case Hearts; + case Spades; +} + +class Bar +{ + + /** + * @param int<0, 10>|string $intOrString + * @param 1|2|'a'|null $constants + * @param int|float|string|bool|null $scalar + * @param float<1, 3> $floatRange + * @param numeric-string $numericString + * @param int|string|array $intStringOrArray + * @param array $array + */ + public function doFoo(mixed $mixed, int $int, float $float, string $string, bool $bool, $intOrString, $constants, Foo|int $fooOrInt, $scalar, float $floatRange, string $numericString, $intStringOrArray, array $array, Suit $suit): void + { + // mixed would only become a union with objects it says nothing about + if ($mixed == 'bar') { + assertType('mixed', $mixed); + } else { + assertType("mixed~('bar'|true)", $mixed); + } + + if ($mixed == 5) { + assertType('mixed', $mixed); + } + + if ($mixed == NAN) { + assertType('true', $mixed); + } + + if ($int == '5') { + assertType('5', $int); + } else { + assertType('int|int<6, max>', $int); + } + + if ($int == 1.5) { + assertType('*NEVER*', $int); + } + + if ($float == 1) { + assertType('1.0', $float); + } + + if ($string == 5) { + assertType('numeric-string', $string); + } + + if ($bool == 'abc') { + assertType('true', $bool); + } + + if ($intOrString == 'abc') { + assertType("'abc'", $intOrString); + } else { + assertType('int<0, 10>|string', $intOrString); + } + + if ($intOrString == 3) { + assertType('3|numeric-string', $intOrString); + } else { + assertType('int<0, 2>|int<4, 10>|string', $intOrString); + } + + if ($constants == 'a') { + assertType("'a'", $constants); + } else { + assertType('1|2|null', $constants); + } + + if ($constants == 0) { + assertType('null', $constants); + } else { + assertType("1|2|'a'", $constants); + } + + if ($fooOrInt == 1) { + assertType('1|LooseEqualityNarrowing\Foo', $fooOrInt); + } + + // only GMP, BcMath\Number and SimpleXMLElement objects compare as their number + if ($fooOrInt == 2) { + assertType('2', $fooOrInt); + } + + if ($scalar == 'x') { + assertType("'x'|true", $scalar); + } + + if ($int == $floatRange) { + assertType('int<1, 3>', $int); + } + + if ($int == $float) { + assertType('float<-9.223372036854776E+18, 9.223372036854776E+18>', $float); + } + + if ($numericString == $intStringOrArray) { + assertType('int|numeric-string', $intStringOrArray); + } + + if ($suit == Suit::Hearts) { + assertType('LooseEqualityNarrowing\Suit::Hearts', $suit); + } else { + assertType('LooseEqualityNarrowing\Suit::Spades', $suit); + } + + if (in_array($bool, [Suit::Hearts])) { + assertType('true', $bool); + } + + if (in_array($string, ['1', 'a'])) { + assertType("'a'|numeric-string", $string); + } + + if ($numericString == '1') { + assertType('numeric-string', $numericString); + } else { + assertType('numeric-string', $numericString); + } + + if ((int) $string) { + assertType('non-falsy-string', $string); + } else { + assertType('string', $string); + } + + if ((string) $int) { + assertType('int|int<1, max>', $int); + } else { + assertType('0', $int); + } + + if ((int) $float) { + assertType('float', $float); + } else { + assertType('float', $float); + } + + assertType('false', $float == 'a'); + assertType('false', $array == 'abc'); + assertType('false', $int == 'abc'); + assertType('bool', $fooOrInt == 'abc'); + assertType('false', $float == NAN); + assertType('false', $constants == 'b'); + assertType('true', $constants == true || $constants == false || $constants == null); + } + +} + +/** + * @template T + * @param T $t + * @param non-empty-mixed $nonEmptyMixed + * @param array $array + * @param string|bool|null $stringBoolOrNull + * @param object|string $objectOrString + */ +function mixedStaysMixed($t, $nonEmptyMixed, array $array, $stringBoolOrNull, $objectOrString): void +{ + if ($t == 'a') { + assertType('T (function LooseEqualityNarrowing\\mixedStaysMixed(), argument)', $t); + } + if ($nonEmptyMixed == 'a') { + assertType("mixed~(0|0.0|''|'0'|array{}|false|null)", $nonEmptyMixed); + } + if ($array['x'] == 'a') { + assertType('mixed', $array['x']); + } + if (in_array($array['y'], ['a', 'b'])) { + assertType('mixed', $array['y']); + } + switch ($array['z']) { + case 'a': + assertType('mixed', $array['z']); + break; + } + + // a set without objects still narrows mixed + if ($array['n'] == null) { + assertType("0|0.0|''|array{}|false|null", $array['n']); + } + + // other types are narrowed, objects to the ones that can equal the value + if ($stringBoolOrNull == 'a') { + assertType("'a'|true", $stringBoolOrNull); + } + if ($objectOrString == 'a') { + assertType("'a'|GMP|Stringable", $objectOrString); + } +} diff --git a/tests/PHPStan/Analyser/nsrt/narrow-cast.php b/tests/PHPStan/Analyser/nsrt/narrow-cast.php index b86b3781b2b..d63387923f1 100644 --- a/tests/PHPStan/Analyser/nsrt/narrow-cast.php +++ b/tests/PHPStan/Analyser/nsrt/narrow-cast.php @@ -31,7 +31,7 @@ function doFoo(string $x, array $arr): void { /** @param int<-5, 5> $x */ function castString($x, string $s, bool $b) { if ((string) $x) { - assertType('int<-5, 5>', $x); + assertType('int<-5, -1>|int<1, 5>', $x); } else { assertType('0', $x); } @@ -64,7 +64,7 @@ function castInt($x, string $s, bool $b) { } if ((int) $s) { - assertType('string', $s); + assertType('non-falsy-string', $s); } else { assertType('string', $s); } @@ -79,9 +79,9 @@ function castInt($x, string $s, bool $b) { /** @param int<-5, 5> $x */ function castFloat($x, string $s, bool $b) { if ((float) $x) { - assertType('int<-5, 5>', $x); + assertType('int<-5, -1>|int<1, 5>', $x); } else { - assertType('int<-5, 5>', $x); + assertType('0', $x); } if ((float) $b) { @@ -91,7 +91,7 @@ function castFloat($x, string $s, bool $b) { } if ((float) $s) { - assertType('string', $s); + assertType('non-falsy-string', $s); } else { assertType("string", $s); } diff --git a/tests/PHPStan/Analyser/nsrt/node-callback-scope.php b/tests/PHPStan/Analyser/nsrt/node-callback-scope.php index d603084976a..bcd93e475ce 100644 --- a/tests/PHPStan/Analyser/nsrt/node-callback-scope.php +++ b/tests/PHPStan/Analyser/nsrt/node-callback-scope.php @@ -534,7 +534,7 @@ function (int $i) { function (mixed $m) { if ($m == 0) { - assertType('0|0.0|string|false|null', $m); + assertType('mixed', $m); } else { assertType("mixed~(0|0.0|'0'|false|null)", $m); } @@ -546,7 +546,7 @@ function (mixed $m) { if ($m != 0) { assertType("mixed~(0|0.0|'0'|false|null)", $m); } else { - assertType('0|0.0|string|false|null', $m); + assertType('mixed', $m); } assertType('mixed', $m); @@ -554,7 +554,7 @@ function (mixed $m) { function (mixed $m) { if ($m == '') { - assertType("0|0.0|''|false|null", $m); + assertType('mixed', $m); } else { assertType("mixed~(''|false|null)", $m); } diff --git a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php index adf633d0cd0..8b245dc8de6 100644 --- a/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ConstantLooseComparisonRuleTest.php @@ -177,7 +177,6 @@ public function testBug11694(): void [ 'Loose comparison using == between int<10, 20> and null will always evaluate to false.', 27, - 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], ]; @@ -203,7 +202,6 @@ public function testBug11694(): void [ 'Loose comparison using == between int<10, 20> and \' 3\' will always evaluate to false.', 33, - 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Loose comparison using == between \' 23\' and int<10, 20> will always evaluate to false.', @@ -213,7 +211,6 @@ public function testBug11694(): void [ 'Loose comparison using == between int<10, 20> and \' 23\' will always evaluate to false.', 39, - 'Because the type is coming from a PHPDoc, you can turn off this check by setting treatPhpDocTypesAsCertain: false in your %configurationFile%.', ], [ 'Loose comparison using == between true and int<10, 20> will always evaluate to true.', diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php index e2c130bf185..bbc893fe612 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleTest.php @@ -785,46 +785,6 @@ private static function getLooseComparisonAgainsEnumsIssues(): array 'Call to function in_array() with arguments \'A\', array{LooseComparisonAgainstEnums\\FooBackedEnum|LooseComparisonAgainstEnums\\FooUnitEnum} and false will always evaluate to false.', 51, ], - [ - 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum and array{bool} will always evaluate to false.', - 57, - ], - [ - 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum, array{bool} and false will always evaluate to false.', - 60, - ], - [ - 'Call to function in_array() with LooseComparisonAgainstEnums\FooBackedEnum and array{bool} will always evaluate to false.', - 63, - ], - [ - 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooBackedEnum, array{bool} and false will always evaluate to false.', - 66, - ], - [ - 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooBackedEnum|LooseComparisonAgainstEnums\FooUnitEnum, array{bool} and false will always evaluate to false.', - 69, - ], - [ - 'Call to function in_array() with bool and array{LooseComparisonAgainstEnums\FooUnitEnum} will always evaluate to false.', - 75, - ], - [ - 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooUnitEnum} and false will always evaluate to false.', - 78, - ], - [ - 'Call to function in_array() with bool and array{LooseComparisonAgainstEnums\FooBackedEnum} will always evaluate to false.', - 81, - ], - [ - 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooBackedEnum} and false will always evaluate to false.', - 84, - ], - [ - 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooBackedEnum|LooseComparisonAgainstEnums\FooUnitEnum} and false will always evaluate to false.', - 87, - ], [ 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum and array{null} will always evaluate to false.', 93, diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest.php index 3fff9077bce..65ee0e951cc 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeFunctionCallRuleWithUncertainPhpDocTypesTest.php @@ -132,46 +132,6 @@ private static function getLooseComparisonAgainsEnumsIssues(): array 'Call to function in_array() with arguments \'A\', array{LooseComparisonAgainstEnums\\FooBackedEnum|LooseComparisonAgainstEnums\\FooUnitEnum} and false will always evaluate to false.', 51, ], - [ - 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum and array{bool} will always evaluate to false.', - 57, - ], - [ - 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooUnitEnum, array{bool} and false will always evaluate to false.', - 60, - ], - [ - 'Call to function in_array() with LooseComparisonAgainstEnums\FooBackedEnum and array{bool} will always evaluate to false.', - 63, - ], - [ - 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooBackedEnum, array{bool} and false will always evaluate to false.', - 66, - ], - [ - 'Call to function in_array() with arguments LooseComparisonAgainstEnums\FooBackedEnum|LooseComparisonAgainstEnums\FooUnitEnum, array{bool} and false will always evaluate to false.', - 69, - ], - [ - 'Call to function in_array() with bool and array{LooseComparisonAgainstEnums\FooUnitEnum} will always evaluate to false.', - 75, - ], - [ - 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooUnitEnum} and false will always evaluate to false.', - 78, - ], - [ - 'Call to function in_array() with bool and array{LooseComparisonAgainstEnums\FooBackedEnum} will always evaluate to false.', - 81, - ], - [ - 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooBackedEnum} and false will always evaluate to false.', - 84, - ], - [ - 'Call to function in_array() with arguments bool, array{LooseComparisonAgainstEnums\FooBackedEnum|LooseComparisonAgainstEnums\FooUnitEnum} and false will always evaluate to false.', - 87, - ], [ 'Call to function in_array() with LooseComparisonAgainstEnums\FooUnitEnum and array{null} will always evaluate to false.', 93, diff --git a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php index 8ddaf48e513..48b1eb768d6 100644 --- a/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php +++ b/tests/PHPStan/Rules/Comparison/ImpossibleCheckTypeMethodCallRuleEqualsTest.php @@ -97,6 +97,10 @@ public function testRule(): void 'Call to method ImpossibleMethodCall\Foo::isNotSame() with array{1, 3} and array{1, 3} will always evaluate to false.', 122, ], + [ + 'Call to method ImpossibleMethodCall\Foo::isSame() with \'1\' and stdClass will always evaluate to false.', + 133, + ], [ 'Call to method ImpossibleMethodCall\Foo::isSame() with array{\'a\', \'b\'} and array{1, 2} will always evaluate to false.', 139, diff --git a/tests/PHPStan/Rules/Comparison/SwitchConditionRuleTest.php b/tests/PHPStan/Rules/Comparison/SwitchConditionRuleTest.php index 6c064f96132..ef832e6f62f 100644 --- a/tests/PHPStan/Rules/Comparison/SwitchConditionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/SwitchConditionRuleTest.php @@ -3,7 +3,6 @@ namespace PHPStan\Rules\Comparison; use PHPStan\Node\Printer\ExprPrinter; -use PHPStan\Php\PhpVersion; use PHPStan\Rules\Rule; use PHPStan\Testing\CompositeRule; use PHPStan\Testing\RuleTestCase; @@ -30,7 +29,6 @@ protected function getRule(): Rule ), self::getContainer()->getByType(ConstantConditionInTraitHelper::class), self::getContainer()->getByType(ExprPrinter::class), - self::getContainer()->getByType(PhpVersion::class), $this->treatPhpDocTypesAsCertain, ), new ConstantConditionInTraitRule(), diff --git a/turbo-ext/src/BinaryOpHandler.cpp b/turbo-ext/src/BinaryOpHandler.cpp index 16cc33813c4..be34bec1c95 100644 --- a/turbo-ext/src/BinaryOpHandler.cpp +++ b/turbo-ext/src/BinaryOpHandler.cpp @@ -157,10 +157,10 @@ zv::Val resolveConcatType(zval *resolver, zval *left, zval *right) return pt_initializer_expr_type_resolver_resolve_concat_type(resolver, left, right); } -/* $initializerExprTypeResolver->resolveEqualType($leftType, $rightType) */ -zv::Val resolverEqualType(zval *resolver, zval *leftType, zval *rightType) +/* $initializerExprTypeResolver->resolveEqualType($leftType, $rightType, $phpVersions) */ +zv::Val resolverEqualType(zval *resolver, zval *leftType, zval *rightType, zval *phpVersions) { - return pt_initializer_expr_type_resolver_resolve_equal_type(resolver, leftType, rightType); + return pt_initializer_expr_type_resolver_resolve_equal_type(resolver, leftType, rightType, phpVersions); } /* $initializerExprTypeResolver->getType($left, $right, $getType) of @@ -648,7 +648,9 @@ class BinaryOpHandler zv::Val rightType = pt_expression_result_get_type_on_scope(rightResult, scope, nativeTypesPromoted); if (UNEXPECTED(rightType.isUndef())) return zv::Val(); - zv::Val typeResult = resolverEqualType(OBJ_PROP_NUM(self, slots::initializerExprTypeResolver), leftType.raw(), rightType.raw()); + zv::Val phpVersions = pt_type_call(Z_OBJ_P(scope), PT_LC("getphpversion"), 0, NULL); + if (UNEXPECTED(phpVersions.isUndef())) return zv::Val(); + zv::Val typeResult = resolverEqualType(OBJ_PROP_NUM(self, slots::initializerExprTypeResolver), leftType.raw(), rightType.raw(), phpVersions.raw()); if (UNEXPECTED(typeResult.isUndef())) return zv::Val(); return typeResultType(typeResult.raw()); } diff --git a/turbo-ext/src/CastHandler.cpp b/turbo-ext/src/CastHandler.cpp index faffa317ed7..33b9df2518a 100644 --- a/turbo-ext/src/CastHandler.cpp +++ b/turbo-ext/src/CastHandler.cpp @@ -254,32 +254,45 @@ class CastHandler // a cast's truthiness is a loose comparison of the inner // expression - composed from its result; the fabricated // literal is only printed into entries, never walked - if (kind == CAST_BOOL || kind == CAST_INT || kind == CAST_DOUBLE) { + if (kind == CAST_BOOL) { bool contextNull; if (UNEXPECTED(!pt_type_specifier_context_null(Z_OBJ_P(context), contextNull))) return zv::Val(); if (!contextNull) { - zv::Val literal; - zv::Val equalContext; - if (kind == CAST_BOOL) { - literal = trueConstFetch(); - if (UNEXPECTED(literal.isUndef())) return zv::Val(); - equalContext = zv::Val::copyOf(zv::Ref(context)); - } else { - literal = kind == CAST_INT ? zeroInt() : zeroFloat(); - if (UNEXPECTED(literal.isUndef())) return zv::Val(); - equalContext = pt_type_specifier_context_negate(Z_OBJ_P(context)); - if (UNEXPECTED(equalContext.isUndef())) return zv::Val(); - } + zv::Val literal = trueConstFetch(); + if (UNEXPECTED(literal.isUndef())) return zv::Val(); // the literal side never reads its stand-in result zval *inner = ptoh::operand(pt_ch_cast_expr, expr); if (UNEXPECTED(inner == NULL)) return zv::Val(); - zv::Val types = pt_identical_narrowing_helper_specify_equal(OBJ_PROP_NUM(handler, slots::identicalNarrowingHelper), nodeScopeResolver, inner, literal.raw(), exprResult, exprResult, equalContext.raw(), evaluationScope.raw(), subjectArgResult, NULL); + zv::Val types = pt_identical_narrowing_helper_specify_equal(OBJ_PROP_NUM(handler, slots::identicalNarrowingHelper), nodeScopeResolver, inner, literal.raw(), exprResult, exprResult, context, evaluationScope.raw(), subjectArgResult, NULL); if (UNEXPECTED(types.isUndef())) return zv::Val(); if (!types.isNull()) return setRootExpr(types, expr); } } + // a number cast is zero for more values than those loosely equal + // to zero: (int) 'abc', (int) 0.5 and (int) INF are 0 too + if (kind == CAST_INT || kind == CAST_DOUBLE) { + bool contextNull; + if (UNEXPECTED(!pt_type_specifier_context_null(Z_OBJ_P(context), contextNull))) return zv::Val(); + if (!contextNull) { + zv::Val zeroContext = pt_type_specifier_context_negate(Z_OBJ_P(context)); + if (UNEXPECTED(zeroContext.isUndef())) return zv::Val(); + bool zeroContextTrue; + if (UNEXPECTED(!pt_type_specifier_context_true(Z_OBJ_P(zeroContext.raw()), zeroContextTrue))) return zv::Val(); + using ptse::CastValue; + zv::Val zeroType = zeroContextTrue + ? ptse::castValueUnion({ CastValue::Null, CastValue::False, CastValue::ZeroInt, kind == CAST_INT ? CastValue::Float : CastValue::ZeroFloat, CastValue::String, CastValue::EmptyArray, CastValue::Object }) + : ptse::castValueUnion({ CastValue::Null, CastValue::False, CastValue::ZeroInt, CastValue::ZeroFloat, CastValue::EmptyString, CastValue::ZeroString, CastValue::EmptyArray }); + if (UNEXPECTED(zeroType.isUndef())) return zv::Val(); + zval *inner = ptoh::operand(pt_ch_cast_expr, expr); + if (UNEXPECTED(inner == NULL)) return zv::Val(); + zv::Val types = pt_default_narrowing_helper_create_subject_types(OBJ_PROP_NUM(handler, slots::defaultNarrowingHelper), evaluationScope.raw(), inner, exprResult, zeroType.raw(), zeroContext.raw()); + if (UNEXPECTED(types.isUndef())) return zv::Val(); + return setRootExpr(types, expr); + } + } + if (kind == CAST_BOOL || kind == CAST_INT || kind == CAST_DOUBLE) { zval *inner = ptoh::operand(pt_ch_cast_expr, expr); if (UNEXPECTED(inner == NULL)) return zv::Val(); diff --git a/turbo-ext/src/CastStringHandler.cpp b/turbo-ext/src/CastStringHandler.cpp index e90ee3dba23..ea5539296e0 100644 --- a/turbo-ext/src/CastStringHandler.cpp +++ b/turbo-ext/src/CastStringHandler.cpp @@ -8,11 +8,12 @@ * what the PHP closures capture: the typeCallback ($this, $expr, $exprResult * — InitializerExprTypeResolver::getCastType() of a string cast is its * operand's toString(), spelled out, so its $getTypeCallback is the operand - * read in place) and the specifyTypesCallback ($beforeScope, $expr). + * read in place) and the specifyTypesCallback ($this, $beforeScope, $expr, + * $exprResult). * * NodeScopeResolver, ExpressionResult, ExpressionContext, MutatingScope, - * ImplicitToStringCallHelper, VariableFlow(Builder), SpecifiedTypes and the - * Type kernel are called through their direct entries. + * ImplicitToStringCallHelper, DefaultNarrowingHelper, VariableFlow(Builder), + * SpecifiedTypes and the Type kernel are called through their direct entries. */ #include "support.h" @@ -44,11 +45,12 @@ class CastStringHandler explicit CastStringHandler(zend_object *self) : self(self) {} /* the constructor body: the promoted properties */ - void construct(zval *initializerExprTypeResolver, zval *implicitToStringCallHelper, zval *expressionResultFactory) const + void construct(zval *initializerExprTypeResolver, zval *implicitToStringCallHelper, zval *expressionResultFactory, zval *defaultNarrowingHelper) const { pt_write_slot(self, slots::initializerExprTypeResolver, initializerExprTypeResolver); pt_write_slot(self, slots::implicitToStringCallHelper, implicitToStringCallHelper); pt_write_slot(self, slots::expressionResultFactory, expressionResultFactory); + pt_write_slot(self, slots::defaultNarrowingHelper, defaultNarrowingHelper); } /* Mirrors supports(); false = pending exception */ @@ -107,7 +109,7 @@ class CastStringHandler if (UNEXPECTED(!pt_expression_result_is_always_terminating(exprResult.raw(), isAlwaysTerminating))) return zv::Val(); zv::Val typeCallback = pt_native_closure(&typeCallbackBody, self, expr, exprResult.raw()); - zv::Val specifyTypesCallback = pt_native_closure(&specifyTypesCallbackBody, beforeScope, expr); + zv::Val specifyTypesCallback = pt_native_closure(&specifyTypesCallbackBody, self, beforeScope, expr, exprResult.raw()); zv::Val dependenciesHold; zval *dependencies = pt_expression_result_dependencies(exprResult.raw(), dependenciesHold); if (UNEXPECTED(dependencies == NULL)) return zv::Val(); @@ -147,12 +149,11 @@ class CastStringHandler type.intoReturnValue(return_value); } - /* static fn (TypeSpecifierContext $context, bool $nativeTypesPromoted): - * SpecifiedTypes => ($nativeTypesPromoted ? - * $beforeScope->doNotTreatPhpDocTypesAsCertain() : - * $beforeScope)->obtainResultForNode(new NotEqual($expr->expr, new - * String_('')))->getSpecifiedTypes($context, - * $nativeTypesPromoted)->setRootExpr($expr) — captures: $beforeScope, $expr */ + /* function (TypeSpecifierContext $context, bool $nativeTypesPromoted): + * SpecifiedTypes — the cast is falsy when it is '' or '0': the subject is + * narrowed by the values whose cast can be (true context) or always is + * (false context) falsy — captures: $this, $beforeScope, $expr, + * $exprResult */ static void specifyTypesCallbackBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(!ptse::requireArgs(argc, 2, closureName))) return; @@ -160,26 +161,32 @@ class CastStringHandler bool nativeTypesPromoted = zend_is_true(&argv[1]); zv::Val types; pt_engine_with_stack([&]() { - zval *beforeScope = &captures[0]; - zval *expr = &captures[1]; + zend_object *handler = Z_OBJ(captures[0]); + zval *beforeScope = &captures[1]; + zval *expr = &captures[2]; + zval *exprResult = &captures[3]; + bool contextNull; + if (UNEXPECTED(!pt_type_specifier_context_null(Z_OBJ_P(context), contextNull))) return; + if (contextNull) { + types = pt_specified_types_new(); + return; + } + + zv::Val falsyContext = pt_type_specifier_context_negate(Z_OBJ_P(context)); + if (UNEXPECTED(falsyContext.isUndef())) return; + bool falsyContextTrue; + if (UNEXPECTED(!pt_type_specifier_context_true(Z_OBJ_P(falsyContext.raw()), falsyContextTrue))) return; + using ptse::CastValue; + zv::Val falsyType = falsyContextTrue + ? ptse::castValueUnion({ CastValue::Null, CastValue::False, CastValue::ZeroInt, CastValue::ZeroFloat, CastValue::EmptyString, CastValue::ZeroString, CastValue::Object }) + : ptse::castValueUnion({ CastValue::Null, CastValue::False, CastValue::ZeroInt, CastValue::EmptyString, CastValue::ZeroString }); + if (UNEXPECTED(falsyType.isUndef())) return; + zv::Val evaluationScope = nativeTypesPromoted ? pt_mutating_scope_do_not_treat_phpdoc_types_as_certain(Z_OBJ_P(beforeScope)) : zv::Val::copyOf(zv::Ref(beforeScope)); if (UNEXPECTED(evaluationScope.isUndef())) return; zval *inner = ptoh::operand(pt_csh_cast_expr, expr); if (UNEXPECTED(inner == NULL)) return; - zval empty; - ZVAL_EMPTY_STRING(&empty); - zv::Val emptyString = pt_type_new(PT_CLASS_SCALAR_STRING, 1, &empty); - if (UNEXPECTED(emptyString.isUndef())) return; - zv::Args comparisonArgs{inner, emptyString.raw()}; - zv::Val comparison = pt_type_new(PT_CLASS_NOT_EQUAL_EXPR, 2, comparisonArgs); - if (UNEXPECTED(comparison.isUndef())) return; - zv::Val result = pt_mutating_scope_obtain_result_for_node(Z_OBJ_P(evaluationScope.raw()), Z_OBJ_P(comparison.raw())); - if (UNEXPECTED(result.isUndef())) return; - if (UNEXPECTED(Z_TYPE_P(result.raw()) != IS_OBJECT)) { - zend_throw_error(NULL, "Call to a member function getSpecifiedTypes() on %s", zend_zval_value_name(result.raw())); - return; - } - zv::Val specified = pt_expression_result_get_specified_types(result.raw(), context, nativeTypesPromoted); + zv::Val specified = pt_default_narrowing_helper_create_subject_types(OBJ_PROP_NUM(handler, slots::defaultNarrowingHelper), evaluationScope.raw(), inner, exprResult, falsyType.raw(), falsyContext.raw()); if (UNEXPECTED(specified.isUndef())) return; if (UNEXPECTED(Z_TYPE_P(specified.raw()) != IS_OBJECT)) { zend_throw_error(NULL, "Call to a member function setRootExpr() on %s", zend_zval_value_name(specified.raw())); @@ -209,9 +216,9 @@ PT_MINIT_REGISTRATION(pt_register_cast_string_handler) /* the real parameter class names: the DI container autowires the * service by reflecting the constructor */ cls.method(sigs::__construct, [](INTERNAL_FUNCTION_PARAMETERS) { - zval *initializerExprTypeResolver, *implicitToStringCallHelper, *expressionResultFactory; - if (!zp::parse(execute_data, initializerExprTypeResolver, implicitToStringCallHelper, expressionResultFactory)) RETURN_THROWS(); - CastStringHandler(Z_OBJ_P(ZEND_THIS)).construct(initializerExprTypeResolver, implicitToStringCallHelper, expressionResultFactory); + zval *initializerExprTypeResolver, *implicitToStringCallHelper, *expressionResultFactory, *defaultNarrowingHelper; + if (!zp::parse(execute_data, initializerExprTypeResolver, implicitToStringCallHelper, expressionResultFactory, defaultNarrowingHelper)) RETURN_THROWS(); + CastStringHandler(Z_OBJ_P(ZEND_THIS)).construct(initializerExprTypeResolver, implicitToStringCallHelper, expressionResultFactory, defaultNarrowingHelper); }); cls.method(sigs::supports, [](INTERNAL_FUNCTION_PARAMETERS) { diff --git a/turbo-ext/src/IdenticalNarrowingHelper.cpp b/turbo-ext/src/IdenticalNarrowingHelper.cpp index 40d230d5598..2ce32db755c 100644 --- a/turbo-ext/src/IdenticalNarrowingHelper.cpp +++ b/turbo-ext/src/IdenticalNarrowingHelper.cpp @@ -222,6 +222,7 @@ inline zv::Val adoptOr(bool ok, zval &value) inline zv::Val newNullType() { zval v; return adoptOr(pt_null_type_new(&v), v); } inline zv::Val newNeverType() { zval v; return adoptOr(pt_never_type_new(&v), v); } +inline zv::Val newMixedType() { zval v; return adoptOr(pt_mixed_type_new(&v), v); } inline zv::Val newConstantBoolean(bool value) { zval v; return adoptOr(pt_constant_boolean_type_new(&v, value), v); } inline zv::Val newConstantInteger(zend_long value) { zval v; return adoptOr(pt_constant_integer_type_new(&v, value), v); } inline zv::Val newConstantFloat(double value) { zval v; return adoptOr(pt_constant_float_type_new(&v, value), v); } @@ -533,11 +534,11 @@ class IdenticalNarrowingHelper zv::Val rightScalarValues = pt_type_op(Z_OBJ_P(rightType.raw()), PT_OP_GET_CONSTANT_SCALAR_VALUES, 0, NULL); if (UNEXPECTED(rightScalarValues.isUndef())) return zv::Val(); if (countOf(leftScalarValues.raw()) == 1 && !isA(unwrappedRight, PT_CLASS_CONST_FETCH)) { - zv::Val constantSideTypes = specifyEqualAgainstConstantSide(nodeScopeResolver, left, right, leftResult, rightResult, right, rightResult, firstOf(leftScalarValues.raw()), leftType.raw(), rightType.raw(), context, evaluationScope, leftArgResult, rightArgResult, identicalTypeCallback.raw()); + zv::Val constantSideTypes = specifyEqualAgainstConstantSide(nodeScopeResolver, left, right, leftResult, rightResult, right, rightResult, firstOf(leftScalarValues.raw()), leftType.raw(), context, evaluationScope, leftArgResult, rightArgResult, identicalTypeCallback.raw()); if (UNEXPECTED(constantSideTypes.isUndef())) return zv::Val(); if (!isFalseOutcome(constantSideTypes)) return constantSideTypes; } else if (countOf(rightScalarValues.raw()) == 1 && !isA(unwrappedLeft, PT_CLASS_CONST_FETCH)) { - zv::Val constantSideTypes = specifyEqualAgainstConstantSide(nodeScopeResolver, left, right, leftResult, rightResult, left, leftResult, firstOf(rightScalarValues.raw()), rightType.raw(), leftType.raw(), context, evaluationScope, leftArgResult, rightArgResult, identicalTypeCallback.raw()); + zv::Val constantSideTypes = specifyEqualAgainstConstantSide(nodeScopeResolver, left, right, leftResult, rightResult, left, leftResult, firstOf(rightScalarValues.raw()), rightType.raw(), context, evaluationScope, leftArgResult, rightArgResult, identicalTypeCallback.raw()); if (UNEXPECTED(constantSideTypes.isUndef())) return zv::Val(); if (!isFalseOutcome(constantSideTypes)) return constantSideTypes; } @@ -600,7 +601,8 @@ class IdenticalNarrowingHelper : createSubjectTypes(evaluationScope, left, leftResult, nonEmptyArray.raw(), negated.raw()); } - // same-type sides cannot coerce - loose equals strict + // same-type sides cannot coerce - loose equals strict, except for + // two numeric strings, which compare as numbers bool sameKind; if (UNEXPECTED(!bothAre(leftType.raw(), rightType.raw(), sameKind))) return zv::Val(); if (sameKind) return specifyIdentical(nodeScopeResolver, left, right, leftResult, rightResult, context, evaluationScope, leftArgResult, rightArgResult, identicalTypeCallback.raw()); @@ -611,20 +613,61 @@ class IdenticalNarrowingHelper if (UNEXPECTED(rightExprString.isNull())) return zv::Val(); if (zend_string_equals(leftExprString.get(), rightExprString.get())) { if (!isA(left, PT_CLASS_VARIABLE) || !isA(right, PT_CLASS_VARIABLE)) return pt_specified_types_new(); + + // a variable equals itself unless it holds NAN + bool isFalse; + if (UNEXPECTED(!ctxFalse(context, isFalse))) return zv::Val(); + if (isFalse) { + zend_long isFloat = trinaryOp(leftType.raw(), PT_OP_IS_FLOAT, "isFloat"); + if (UNEXPECTED(isFloat < 0)) return zv::Val(); + if (isFloat == PT_TRI_NO) { + zend_long isArray = trinaryOp(leftType.raw(), PT_OP_IS_ARRAY, "isArray"); + if (UNEXPECTED(isArray < 0)) return zv::Val(); + if (isArray == PT_TRI_NO) return createSubjectTypes(evaluationScope, left, leftResult, leftType.raw(), context); + } + } } - zv::Val leftTypes = createSubjectTypes(evaluationScope, left, leftResult, leftType.raw(), context); + // each side is one of the values the other side can equal, and in + // the false branch none of the values the other side always equals + zv::Val scopePhpVersions = pt_type_call(Z_OBJ_P(evaluationScope), PT_LC("getphpversion"), 0, NULL); + if (UNEXPECTED(scopePhpVersions.isUndef())) return zv::Val(); + zv::Val phpVersions = pt_type_call_static(PT_CLASS_LOOSE_COMPARISON_HELPER, PT_LC("getrepresentativephpversions"), 1, scopePhpVersions.raw()); + if (UNEXPECTED(phpVersions.isUndef())) return zv::Val(); + zv::Val leftTypes = createLooselyEqualTypes(evaluationScope, left, leftResult, leftType.raw(), rightType.raw(), phpVersions.raw(), context); if (UNEXPECTED(leftTypes.isUndef())) return zv::Val(); - zv::Val rightTypes = createSubjectTypes(evaluationScope, right, rightResult, rightType.raw(), context); + zv::Val rightTypes = createLooselyEqualTypes(evaluationScope, right, rightResult, rightType.raw(), leftType.raw(), phpVersions.raw(), context); if (UNEXPECTED(rightTypes.isUndef())) return zv::Val(); + return unionWith(std::move(leftTypes), std::move(rightTypes)); + } + /* Mirrors createLooselyEqualTypes(). */ + zv::Val createLooselyEqualTypes(zval *evaluationScope, zval *subject, zval *subjectResult, zval *subjectType, zval *otherType, zval *phpVersions, zval *context) const + { bool isTrue; if (UNEXPECTED(!ctxTrue(context, isTrue))) return zv::Val(); - if (isTrue) return unionWith(std::move(leftTypes), std::move(rightTypes)); - zv::Val leftSure = toSureTypes(leftTypes.raw(), evaluationScope); - if (UNEXPECTED(leftSure.isUndef())) return zv::Val(); - zv::Val rightSure = toSureTypes(rightTypes.raw(), evaluationScope); - return intersectWith(std::move(leftSure), std::move(rightSure)); + zv::Args args{otherType, phpVersions}; + zv::Val type = isTrue + ? pt_type_call_static(PT_CLASS_LOOSE_COMPARISON_HELPER, PT_LC("getpossiblylooselyequaltypeinversions"), 2, args) + : pt_type_call_static(PT_CLASS_LOOSE_COMPARISON_HELPER, PT_LC("getalwayslooselyequaltypeinversions"), 2, args); + if (UNEXPECTED(type.isUndef())) return zv::Val(); + + // nothing to intersect with, or nothing to remove + zv::Val mixed = newMixedType(); + if (UNEXPECTED(mixed.isUndef())) return zv::Val(); + bool coversMixed; + if (UNEXPECTED(!isSuperTypeOfYes(type.raw(), mixed.raw(), coversMixed))) return zv::Val(); + if (coversMixed) return pt_specified_types_new(); + bool isFalse; + if (UNEXPECTED(!ctxFalse(context, isFalse))) return zv::Val(); + if (isFalse && instanceof_function(Z_OBJCE_P(type.raw()), pt_ce_never_type)) return pt_specified_types_new(); + if (isTrue && Z_TYPE_P(subjectType) == IS_OBJECT && instanceof_function(Z_OBJCE_P(subjectType), pt_ce_mixed_type)) { + zv::Val keepsMixed = pt_type_call_static(PT_CLASS_LOOSE_COMPARISON_HELPER, PT_LC("keepsmixedunnarrowed"), 1, type.raw()); + if (UNEXPECTED(keepsMixed.isUndef())) return zv::Val(); + if (Z_TYPE_P(keepsMixed.raw()) == IS_TRUE) return pt_specified_types_new(); + } + + return createSubjectTypes(evaluationScope, subject, subjectResult, type.raw(), context); } /* Mirrors specifyIdenticalAgainstType(). */ @@ -794,8 +837,8 @@ class IdenticalNarrowingHelper return pt_type_new(PT_CLASS_CONST_FETCH, 1, name.raw()); } - /* the four same-kind conjuncts of specifyEqual(): both strings, integers, - * floats or enums; false = pending exception */ + /* the four same-kind conjuncts of specifyEqual(): both strings (not both + * possibly numeric), integers, floats or enums; false = pending exception */ [[nodiscard]] static bool bothAre(zval *leftType, zval *rightType, bool &out) { static const pt_type_op_id ops[] = { PT_OP_IS_STRING, PT_OP_IS_INTEGER, PT_OP_IS_FLOAT }; @@ -806,10 +849,19 @@ class IdenticalNarrowingHelper if (leftValue != PT_TRI_YES) continue; zend_long rightValue = trinaryOp(rightType, ops[i], names[i]); if (UNEXPECTED(rightValue < 0)) return false; - if (rightValue == PT_TRI_YES) { - out = true; - return true; + if (rightValue != PT_TRI_YES) continue; + if (i == 0) { + // two numeric strings compare as numbers + zend_long leftNumeric = trinaryByName(leftType, "isNumericString", PT_LC("isnumericstring")); + if (UNEXPECTED(leftNumeric < 0)) return false; + if (leftNumeric != PT_TRI_NO) { + zend_long rightNumeric = trinaryByName(rightType, "isNumericString", PT_LC("isnumericstring")); + if (UNEXPECTED(rightNumeric < 0)) return false; + if (rightNumeric != PT_TRI_NO) continue; + } } + out = true; + return true; } zend_long leftEnum = trinaryByName(leftType, "isEnum", PT_LC("isenum")); if (UNEXPECTED(leftEnum < 0)) return false; @@ -1252,7 +1304,7 @@ class IdenticalNarrowingHelper } /* Mirrors specifyEqualAgainstConstantSide(): a SpecifiedTypes, false or null */ - zv::Val specifyEqualAgainstConstantSide(zval *nodeScopeResolver, zval *left, zval *right, zval *leftResult, zval *rightResult, zval *subject, zval *subjectResult, zval *value, zval *constantType, zval *otherType, zval *context, zval *evaluationScope, zval *leftArgResult, zval *rightArgResult, zval *identicalTypeCallback) const + zv::Val specifyEqualAgainstConstantSide(zval *nodeScopeResolver, zval *left, zval *right, zval *leftResult, zval *rightResult, zval *subject, zval *subjectResult, zval *value, zval *constantType, zval *context, zval *evaluationScope, zval *leftArgResult, zval *rightArgResult, zval *identicalTypeCallback) const { zval *unwrappedSubject = unwrap(subject); @@ -1280,57 +1332,6 @@ class IdenticalNarrowingHelper return pt_expression_result_get_specified_types_for_scope(subjectResult, evaluationScope, boolContext.raw()); } - /* There is a difference between php 7.x and 8.x on the equality - * behavior between zero and the empty string, so to be conservative - * we leave it untouched regardless of the language version */ - if (Z_TYPE_P(value) == IS_LONG && Z_LVAL_P(value) == 0) { - zend_long otherInteger = trinaryOp(otherType, PT_OP_IS_INTEGER, "isInteger"); - if (UNEXPECTED(otherInteger < 0)) return zv::Val(); - zend_long otherBoolean = otherInteger == PT_TRI_YES ? PT_TRI_YES : trinaryOp(otherType, PT_OP_IS_BOOLEAN, "isBoolean"); - if (UNEXPECTED(otherBoolean < 0)) return zv::Val(); - if (otherInteger != PT_TRI_YES && otherBoolean != PT_TRI_YES) { - bool isTrue; - if (UNEXPECTED(!ctxTrue(context, isTrue))) return zv::Val(); - zv::Arr members = zv::Arr::create(5); - if (UNEXPECTED(!pushFalsyScalars(members, true, false))) return zv::Val(); - if (isTrue) { - zv::Val stringType = newStringType(); - if (UNEXPECTED(stringType.isUndef())) return zv::Val(); - members.push(std::move(stringType)); - } else { - zend_string *zero = zend_string_init(PT_LC("0"), 0); - zv::Val zeroString = newConstantString(zero); - zend_string_release(zero); - if (UNEXPECTED(zeroString.isUndef())) return zv::Val(); - members.push(std::move(zeroString)); - } - zv::Val union_ = newUnion(members); - if (UNEXPECTED(union_.isUndef())) return zv::Val(); - return createSubjectTypes(evaluationScope, subject, subjectResult, union_.raw(), context); - } - } - if (Z_TYPE_P(value) == IS_STRING && Z_STRLEN_P(value) == 0) { - bool isTrue; - if (UNEXPECTED(!ctxTrue(context, isTrue))) return zv::Val(); - zv::Arr members = zv::Arr::create(5); - if (isTrue) { - if (UNEXPECTED(!pushFalsyScalars(members, true, false))) return zv::Val(); - } else { - zv::Val nullType = newNullType(); - if (UNEXPECTED(nullType.isUndef())) return zv::Val(); - members.push(std::move(nullType)); - zv::Val falseType = newConstantBoolean(false); - if (UNEXPECTED(falseType.isUndef())) return zv::Val(); - members.push(std::move(falseType)); - } - zv::Val emptyString = newConstantString(ZSTR_EMPTY_ALLOC()); - if (UNEXPECTED(emptyString.isUndef())) return zv::Val(); - members.push(std::move(emptyString)); - zv::Val union_ = newUnion(members); - if (UNEXPECTED(union_.isUndef())) return zv::Val(); - return createSubjectTypes(evaluationScope, subject, subjectResult, union_.raw(), context); - } - // loose equals strict for these call results and class names if (isA(unwrappedSubject, PT_CLASS_FUNC_CALL) && isA(nameOf(unwrappedSubject), PT_CLASS_NAME)) { bool hasArg; diff --git a/turbo-ext/src/IetrSupport.h b/turbo-ext/src/IetrSupport.h index dfddc48b34a..788d5c6c9f5 100644 --- a/turbo-ext/src/IetrSupport.h +++ b/turbo-ext/src/IetrSupport.h @@ -122,6 +122,8 @@ inline constexpr Method mHasOffsetValueType{"hasOffsetValueType", PT_OP_HAS_OFFS inline constexpr Method mGetOffsetValueType{"getOffsetValueType", PT_OP_GET_OFFSET_VALUE_TYPE}; inline constexpr Method mAppend{"append"}; inline constexpr Method mLooseCompare{"looseCompare"}; +inline constexpr Method mGetPossiblyLooselyEqualType{"getPossiblyLooselyEqualType"}; +inline constexpr Method mGetAlwaysLooselyEqualType{"getAlwaysLooselyEqualType"}; inline constexpr Method mGetReasons{"getReasons"}; inline constexpr Method mIsSmallerThan{"isSmallerThan"}; inline constexpr Method mIsSmallerThanOrEqual{"isSmallerThanOrEqual"}; diff --git a/turbo-ext/src/InitializerExprTypeResolver.cpp b/turbo-ext/src/InitializerExprTypeResolver.cpp index 0194a974abc..9077960d4cb 100644 --- a/turbo-ext/src/InitializerExprTypeResolver.cpp +++ b/turbo-ext/src/InitializerExprTypeResolver.cpp @@ -761,8 +761,8 @@ class InitializerExprTypeResolver return newTypeResult(newBooleanType()); } - /* Mirrors resolveEqualType(). */ - zv::Val resolveEqualType(zval *leftType, zval *rightType) const + /* Mirrors resolveEqualType(); phpVersions NULL stands for null. */ + zv::Val resolveEqualType(zval *leftType, zval *rightType, zval *phpVersions = NULL) const { if (isA(leftType, pt_ce_never_type) || isA(rightType, pt_ce_never_type)) { return newTypeResult(newConstantBooleanType(false)); @@ -788,17 +788,73 @@ class InitializerExprTypeResolver } if (isA(leftType, pt_ce_constant_array_type) && isA(rightType, pt_ce_constant_array_type)) { - return resolveConstantArrayTypeComparison(leftType, rightType, false); + return resolveConstantArrayTypeComparison(leftType, rightType, false, NULL, phpVersions); + } + + // LooseComparisonHelper::get*InVersions() over a single version is + // that version's Type method call + zval *version = prop(slots::phpVersion); + zv::Val versions; + if (phpVersions != NULL) { + IETR_VAL(representative, pt_type_call_static(PT_CLASS_LOOSE_COMPARISON_HELPER, PT_LC("getrepresentativephpversions"), 1, phpVersions)); + if (UNEXPECTED(!requireArray(representative.raw()))) return zv::Val(); + if (zend_hash_num_elements(Z_ARRVAL_P(representative.raw())) == 1) { + version = arrayIndexForRead(representative.raw(), 0); + if (UNEXPECTED(version == NULL)) return zv::Val(); + } + versions = std::move(representative); + } + bool singleVersion = versions.isUndef() || zend_hash_num_elements(Z_ARRVAL_P(versions.raw())) == 1; + auto possiblyEqual = [&](zval *type) -> zv::Val { + if (singleVersion) return callOn(type, mGetPossiblyLooselyEqualType, 1, version); + zv::Args args{type, versions.raw()}; + return pt_type_call_static(PT_CLASS_LOOSE_COMPARISON_HELPER, PT_LC("getpossiblylooselyequaltypeinversions"), 2, args); + }; + auto alwaysEqual = [&](zval *type) -> zv::Val { + if (singleVersion) return callOn(type, mGetAlwaysLooselyEqualType, 1, version); + zv::Args args{type, versions.raw()}; + return pt_type_call_static(PT_CLASS_LOOSE_COMPARISON_HELPER, PT_LC("getalwayslooselyequaltypeinversions"), 2, args); + }; + + zv::Val compared; + if (singleVersion) { + zv::Args argv{rightType, version}; + compared = callOn(leftType, mLooseCompare, 2, argv); + } else { + zv::Args argv{leftType, rightType, versions.raw()}; + compared = pt_type_call_static(PT_CLASS_LOOSE_COMPARISON_HELPER, PT_LC("loosecompareinversions"), 3, argv); } + IETR_VAL(result, std::move(compared)); + IETR_TRI(resultIsTrue, triOn(result.raw(), mIsTrue)); + if (resultIsTrue == PT_TRI_YES) return newTypeResult(std::move(result)); + IETR_TRI(resultIsFalse, triOn(result.raw(), mIsFalse)); + if (resultIsFalse == PT_TRI_YES) return newTypeResult(std::move(result)); - zv::Args argv{rightType, prop(slots::phpVersion)}; - return newTypeResult(callOn(leftType, mLooseCompare, 2, argv)); + // either side's possibly loosely equal type rules the other side out + for (int side = 0; side < 2; side++) { + zval *type = side == 0 ? leftType : rightType; + zval *other = side == 0 ? rightType : leftType; + IETR_VAL(possible, possiblyEqual(type)); + IETR_TRI(covers, triOn(possible.raw(), mIsSuperTypeOf, 1, other)); + if (covers == PT_TRI_NO) return newTypeResult(newConstantBooleanType(false)); + } + + // either side's always loosely equal type holds the other side + for (int side = 0; side < 2; side++) { + zval *type = side == 0 ? leftType : rightType; + zval *other = side == 0 ? rightType : leftType; + IETR_VAL(always, alwaysEqual(type)); + IETR_TRI(covers, triOn(always.raw(), mIsSuperTypeOf, 1, other)); + if (covers == PT_TRI_YES) return newTypeResult(newConstantBooleanType(true)); + } + + return newTypeResult(std::move(result)); } /* Mirrors resolveConstantArrayTypeComparison() with the twin's value * comparison callbacks: resolveIdenticalType() (identical) or * resolveEqualType() */ - zv::Val resolveConstantArrayTypeComparison(zval *leftType, zval *rightType, bool identical, zval *valueComparisonCallback = NULL) const + zv::Val resolveConstantArrayTypeComparison(zval *leftType, zval *rightType, bool identical, zval *valueComparisonCallback = NULL, zval *phpVersions = NULL) const { IETR_VAL(leftKeyTypes, callOn(leftType, mGetKeyTypes)); IETR_VAL(rightKeyTypesHold, callOn(rightType, mGetKeyTypes)); @@ -890,7 +946,7 @@ class InitializerExprTypeResolver zv::Args argv{leftValue, rightValue}; leftIdenticalToRightResult = pt_type_call_callable(valueComparisonCallback, 2, argv); } else { - leftIdenticalToRightResult = identical ? resolveIdenticalType(leftValue, rightValue) : resolveEqualType(leftValue, rightValue); + leftIdenticalToRightResult = identical ? resolveIdenticalType(leftValue, rightValue) : resolveEqualType(leftValue, rightValue, phpVersions); } if (UNEXPECTED(leftIdenticalToRightResult.isUndef())) return zv::Val(); IETR_VAL(leftIdenticalToRight, typeResultType(leftIdenticalToRightResult.raw())); @@ -3168,12 +3224,14 @@ zv::Val pt_initializer_expr_type_resolver_resolve_identical_type(zval *resolver, return callResolverMethod(site, resolver, PT_LC("resolveidenticaltype"), 2, argv); } -zv::Val pt_initializer_expr_type_resolver_resolve_equal_type(zval *resolver, zval *leftType, zval *rightType) +zv::Val pt_initializer_expr_type_resolver_resolve_equal_type(zval *resolver, zval *leftType, zval *rightType, zval *phpVersions) { - if (EXPECTED(isNativeResolver(resolver))) return InitializerExprTypeResolver(Z_OBJ_P(resolver)).resolveEqualType(leftType, rightType); + if (EXPECTED(isNativeResolver(resolver))) return InitializerExprTypeResolver(Z_OBJ_P(resolver)).resolveEqualType(leftType, rightType, phpVersions); static pt_method_site site; - zv::Args argv{leftType, rightType}; - return callResolverMethod(site, resolver, PT_LC("resolveequaltype"), 2, argv); + zval nullVersions; + ZVAL_NULL(&nullVersions); + zv::Args argv{leftType, rightType, phpVersions != NULL ? phpVersions : &nullVersions}; + return callResolverMethod(site, resolver, PT_LC("resolveequaltype"), 3, argv); } zv::Val pt_initializer_expr_type_resolver_get_array_type(zval *resolver, zval *expr, const pt_ietr_get_type &getTypeCallback) @@ -3468,10 +3526,10 @@ PT_MINIT_REGISTRATION(pt_register_initializer_expr_type_resolver) }); cls.method(sigs::resolveEqualType, [](INTERNAL_FUNCTION_PARAMETERS) { - zval *leftType, *rightType; - if (!zp::parse(execute_data, leftType, rightType)) RETURN_THROWS(); + zval *leftType, *rightType, *phpVersions = NULL; + if (!zp::parse>(execute_data, leftType, rightType, phpVersions)) RETURN_THROWS(); if (UNEXPECTED(!ietrArgType(leftType, 1) || !ietrArgType(rightType, 2))) RETURN_THROWS(); - PT_RETURN_VAL(IETR_THIS.resolveEqualType(leftType, rightType)); + PT_RETURN_VAL(IETR_THIS.resolveEqualType(leftType, rightType, phpVersions)); }); cls.method(sigs::resolveConstantArrayTypeComparison, [](INTERNAL_FUNCTION_PARAMETERS) { diff --git a/turbo-ext/src/SimpleExprHandlers.h b/turbo-ext/src/SimpleExprHandlers.h index 8187b935699..167f3b0c96a 100644 --- a/turbo-ext/src/SimpleExprHandlers.h +++ b/turbo-ext/src/SimpleExprHandlers.h @@ -18,6 +18,8 @@ #include "OperatorHandlers.h" #include "CallHandlerSupport.h" +#include + namespace ptse { /* {{{ a processed child's result */ @@ -225,6 +227,48 @@ void nonAcceptingNeverTypeBody(zval *captures, uint32_t argc, zval *argv, zval * /* }}} */ +/* {{{ the cast handlers' zero / falsy value sets */ + +enum class CastValue { Null, False, ZeroInt, ZeroFloat, Float, String, EmptyString, ZeroString, EmptyArray, Object }; + +/* TypeCombinator::union() of the listed types, in order; UNDEF = pending + * exception */ +inline zv::Val castValueUnion(std::initializer_list values) +{ + zv::Val members[10]; + zval argv[10]; + uint32_t count = 0; + for (CastValue value : values) { + zval out; + bool ok; + switch (value) { + case CastValue::Null: ok = pt_null_type_new(&out); break; + case CastValue::False: ok = pt_constant_boolean_type_new(&out, false); break; + case CastValue::ZeroInt: ok = pt_constant_integer_type_new(&out, 0); break; + case CastValue::ZeroFloat: ok = pt_constant_float_type_new(&out, 0.0); break; + case CastValue::Float: ok = pt_float_type_new(&out); break; + case CastValue::String: ok = pt_string_type_new(&out); break; + case CastValue::EmptyString: ok = pt_constant_string_type_new(&out, ZSTR_EMPTY_ALLOC()); break; + case CastValue::ZeroString: ok = pt_constant_string_type_new(&out, ZSTR_CHAR('0')); break; + case CastValue::EmptyArray: { + zval empty; + ZVAL_EMPTY_ARRAY(&empty); + ok = pt_constant_array_type_new(&out, &empty, &empty); + break; + } + default: ok = pt_object_without_class_type_new(&out); break; + } + if (UNEXPECTED(!ok)) return zv::Val(); + members[count] = zv::Val::adopt(out); + ZVAL_COPY_VALUE(&argv[count], members[count].raw()); + count++; + } + + return pt_type_combinator_union(count, argv); +} + +/* }}} */ + } // namespace ptse #endif /* PHPSTANTURBO_SIMPLE_EXPR_HANDLERS_H */ diff --git a/turbo-ext/src/TypeTraits.cpp b/turbo-ext/src/TypeTraits.cpp index 06216501d4b..b9c83a23be4 100644 --- a/turbo-ext/src/TypeTraits.cpp +++ b/turbo-ext/src/TypeTraits.cpp @@ -5475,6 +5475,20 @@ void pt_type_trait_operator(reg::Class &cls) cls.traitMethod(sigs::bitwiseShiftRight, operatorBinary); cls.traitMethod(sigs::stringConcatenate, operatorBinary); cls.traitMethod(sigs::arrayUnion, operatorBinary); + /* LooseComparisonHelper::getPossiblyLooselyEqualType($this, $phpVersion) */ + cls.traitMethod(sigs::getPossiblyLooselyEqualType, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *phpVersion; + if (!zp::parse(execute_data, phpVersion)) RETURN_THROWS(); + zv::Args args{ZEND_THIS, phpVersion}; + PT_RETURN_VAL(pt_type_call_static(PT_CLASS_LOOSE_COMPARISON_HELPER, PT_LC("getpossiblylooselyequaltype"), 2, args)); + }); + /* LooseComparisonHelper::getAlwaysLooselyEqualType($this, $phpVersion) */ + cls.traitMethod(sigs::getAlwaysLooselyEqualType, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *phpVersion; + if (!zp::parse(execute_data, phpVersion)) RETURN_THROWS(); + zv::Args args{ZEND_THIS, phpVersion}; + PT_RETURN_VAL(pt_type_call_static(PT_CLASS_LOOSE_COMPARISON_HELPER, PT_LC("getalwayslooselyequaltype"), 2, args)); + }); } /* }}} */ diff --git a/turbo-ext/src/generated/CastStringHandler.h b/turbo-ext/src/generated/CastStringHandler.h index fd717a6f5d3..37d4807fe7c 100644 --- a/turbo-ext/src/generated/CastStringHandler.h +++ b/turbo-ext/src/generated/CastStringHandler.h @@ -13,6 +13,7 @@ namespace slot { inline constexpr uint32_t initializerExprTypeResolver = 0; inline constexpr uint32_t implicitToStringCallHelper = 1; inline constexpr uint32_t expressionResultFactory = 2; +inline constexpr uint32_t defaultNarrowingHelper = 3; } // namespace slot inline void declareClass(reg::Class &cls) @@ -27,6 +28,7 @@ inline void declareProperties(reg::Class &cls) cls.property("initializerExprTypeResolver", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Reflection\\InitializerExprTypeResolver"); cls.property("implicitToStringCallHelper", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ExprHandler\\Helper\\ImplicitToStringCallHelper"); cls.property("expressionResultFactory", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ExpressionResultFactory"); + cls.property("defaultNarrowingHelper", ZEND_ACC_PRIVATE, reg::PropertyKind::Typed, 0, "PHPStan\\Analyser\\ExprHandler\\Helper\\DefaultNarrowingHelper"); } /* the string and parameter tables the signatures below index into (see reg::Sig) */ @@ -38,47 +40,50 @@ inline constexpr char strings[] = "PHPStan\\Analyser\\ExprHandler\\Helper\\ImplicitToStringCallHelper\0" /* 102 */ "expressionResultFactory\0" /* 165 */ "PHPStan\\Analyser\\ExpressionResultFactory\0" /* 189 */ - "__construct\0" /* 230 */ - "expr\0" /* 242 */ - "PhpParser\\Node\\Expr\0" /* 247 */ - "\0" /* 267 */ - "supports\0" /* 268 */ - "nodeScopeResolver\0" /* 277 */ - "PHPStan\\Analyser\\NodeScopeResolver\0" /* 295 */ - "stmt\0" /* 330 */ - "PhpParser\\Node\\Stmt\0" /* 335 */ - "scope\0" /* 355 */ - "PHPStan\\Analyser\\MutatingScope\0" /* 361 */ - "storage\0" /* 392 */ - "PHPStan\\Analyser\\ExpressionResultStorage\0" /* 400 */ - "nodeCallback\0" /* 441 */ - "context\0" /* 454 */ - "PHPStan\\Analyser\\ExpressionContext\0" /* 462 */ - "PHPStan\\Analyser\\ExpressionResult\0" /* 497 */ - "processExpr"; /* 531 */ + "defaultNarrowingHelper\0" /* 230 */ + "PHPStan\\Analyser\\ExprHandler\\Helper\\DefaultNarrowingHelper\0" /* 253 */ + "__construct\0" /* 312 */ + "expr\0" /* 324 */ + "PhpParser\\Node\\Expr\0" /* 329 */ + "\0" /* 349 */ + "supports\0" /* 350 */ + "nodeScopeResolver\0" /* 359 */ + "PHPStan\\Analyser\\NodeScopeResolver\0" /* 377 */ + "stmt\0" /* 412 */ + "PhpParser\\Node\\Stmt\0" /* 417 */ + "scope\0" /* 437 */ + "PHPStan\\Analyser\\MutatingScope\0" /* 443 */ + "storage\0" /* 474 */ + "PHPStan\\Analyser\\ExpressionResultStorage\0" /* 482 */ + "nodeCallback\0" /* 523 */ + "context\0" /* 536 */ + "PHPStan\\Analyser\\ExpressionContext\0" /* 544 */ + "PHPStan\\Analyser\\ExpressionResult\0" /* 579 */ + "processExpr"; /* 613 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 28), /* __construct $initializerExprTypeResolver */ reg::packed(75, 0, 102), /* __construct $implicitToStringCallHelper */ reg::packed(165, 0, 189), /* __construct $expressionResultFactory */ - reg::packed(242, 0, 247), /* supports $expr */ - reg::packed(267, MAY_BE_BOOL), /* supports return */ - reg::packed(277, 0, 295), /* processExpr $nodeScopeResolver */ - reg::packed(330, 0, 335), /* processExpr $stmt */ - reg::packed(242, 0, 247), /* processExpr $expr */ - reg::packed(355, 0, 361), /* processExpr $scope */ - reg::packed(392, 0, 400), /* processExpr $storage */ - reg::packed(441, MAY_BE_CALLABLE), /* processExpr $nodeCallback */ - reg::packed(454, 0, 462), /* processExpr $context */ - reg::packed(267, 0, 497), /* processExpr return */ + reg::packed(230, 0, 253), /* __construct $defaultNarrowingHelper */ + reg::packed(324, 0, 329), /* supports $expr */ + reg::packed(349, MAY_BE_BOOL), /* supports return */ + reg::packed(359, 0, 377), /* processExpr $nodeScopeResolver */ + reg::packed(412, 0, 417), /* processExpr $stmt */ + reg::packed(324, 0, 329), /* processExpr $expr */ + reg::packed(437, 0, 443), /* processExpr $scope */ + reg::packed(474, 0, 482), /* processExpr $storage */ + reg::packed(523, MAY_BE_CALLABLE), /* processExpr $nodeCallback */ + reg::packed(536, 0, 544), /* processExpr $context */ + reg::packed(349, 0, 579), /* processExpr return */ }; using Sig = reg::Sig; } // namespace sigtab /* the signatures of the methods the class declares itself (a used trait's are in the trait's header) */ namespace sig { -inline constexpr sigtab::Sig __construct = { { 230 /* __construct */, 3, 0, 3, reg::NoArg, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig supports = { { 268 /* supports */, 1, 3, 1, 4, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig processExpr = { { 531 /* processExpr */, 7, 5, 7, 12, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig __construct = { { 312 /* __construct */, 4, 0, 4, reg::NoArg, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig supports = { { 350 /* supports */, 1, 4, 1, 5, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig processExpr = { { 613 /* processExpr */, 7, 6, 7, 13, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::CastStringHandler diff --git a/turbo-ext/src/generated/IdenticalNarrowingHelper.h b/turbo-ext/src/generated/IdenticalNarrowingHelper.h index a4e419ab84f..5271c7958bd 100644 --- a/turbo-ext/src/generated/IdenticalNarrowingHelper.h +++ b/turbo-ext/src/generated/IdenticalNarrowingHelper.h @@ -74,24 +74,27 @@ inline constexpr char strings[] = "getTypeFromGettypeStringValue\0" /* 794 */ "specifyGeneral\0" /* 824 */ "specifyEqual\0" /* 839 */ - "constantExpr\0" /* 852 */ - "constantType\0" /* 865 */ - "subjectArgResult\0" /* 878 */ - "specifyIdenticalAgainstType\0" /* 895 */ - "otherType\0" /* 923 */ - "specifyEqualAgainstConstantSide\0" /* 933 */ - "call\0" /* 965 */ - "PhpParser\\Node\\Expr\\FuncCall\0" /* 970 */ - "argResult\0" /* 999 */ - "specifyFuncCallFamilies\0" /* 1009 */ - "side\0" /* 1033 */ - "storage\0" /* 1038 */ - "PHPStan\\Analyser\\ExpressionResultStorage\0" /* 1046 */ - "captureFirstArgResult\0" /* 1087 */ - "expr\0" /* 1109 */ - "literalType\0" /* 1114 */ - "isScalarLiteral\0" /* 1126 */ - "isSubjectCoveredAgainstConstant"; /* 1142 */ + "subjectType\0" /* 852 */ + "otherType\0" /* 864 */ + "phpVersions\0" /* 874 */ + "createLooselyEqualTypes\0" /* 886 */ + "constantExpr\0" /* 910 */ + "constantType\0" /* 923 */ + "subjectArgResult\0" /* 936 */ + "specifyIdenticalAgainstType\0" /* 953 */ + "specifyEqualAgainstConstantSide\0" /* 981 */ + "call\0" /* 1013 */ + "PhpParser\\Node\\Expr\\FuncCall\0" /* 1018 */ + "argResult\0" /* 1047 */ + "specifyFuncCallFamilies\0" /* 1057 */ + "side\0" /* 1081 */ + "storage\0" /* 1086 */ + "PHPStan\\Analyser\\ExpressionResultStorage\0" /* 1094 */ + "captureFirstArgResult\0" /* 1135 */ + "expr\0" /* 1157 */ + "literalType\0" /* 1162 */ + "isScalarLiteral\0" /* 1174 */ + "isSubjectCoveredAgainstConstant"; /* 1190 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 23), /* __construct $defaultNarrowingHelper */ reg::packed(82, 0, 101), /* __construct $reflectionProvider */ @@ -155,13 +158,21 @@ inline constexpr reg::PackedArg args[] = { reg::packed(575, MAY_BE_NULL, 436), /* specifyEqual $leftArgResult */ reg::packed(589, MAY_BE_NULL, 436), /* specifyEqual $rightArgResult */ reg::packed(626, MAY_BE_NULL, 627), /* specifyEqual return */ + reg::packed(528, 0, 544), /* createLooselyEqualTypes $evaluationScope */ + reg::packed(676, 0, 399), /* createLooselyEqualTypes $subject */ + reg::packed(684, 0, 436), /* createLooselyEqualTypes $subjectResult */ + reg::packed(852, 0, 776), /* createLooselyEqualTypes $subjectType */ + reg::packed(864, 0, 776), /* createLooselyEqualTypes $otherType */ + reg::packed(874, MAY_BE_ARRAY), /* createLooselyEqualTypes $phpVersions */ + reg::packed(482, 0, 490), /* createLooselyEqualTypes $context */ + reg::packed(626, 0, 627), /* createLooselyEqualTypes return */ reg::packed(676, 0, 399), /* specifyIdenticalAgainstType $subject */ reg::packed(684, 0, 436), /* specifyIdenticalAgainstType $subjectResult */ - reg::packed(852, 0, 399), /* specifyIdenticalAgainstType $constantExpr */ - reg::packed(865, 0, 776), /* specifyIdenticalAgainstType $constantType */ + reg::packed(910, 0, 399), /* specifyIdenticalAgainstType $constantExpr */ + reg::packed(923, 0, 776), /* specifyIdenticalAgainstType $constantType */ reg::packed(482, 0, 490), /* specifyIdenticalAgainstType $context */ reg::packed(528, 0, 544), /* specifyIdenticalAgainstType $evaluationScope */ - reg::packed(878, MAY_BE_NULL, 436), /* specifyIdenticalAgainstType $subjectArgResult */ + reg::packed(936, MAY_BE_NULL, 436), /* specifyIdenticalAgainstType $subjectArgResult */ reg::packed(604, MAY_BE_CALLABLE), /* specifyIdenticalAgainstType $identicalTypeCallback */ reg::packed(626, MAY_BE_NULL, 627), /* specifyIdenticalAgainstType return */ reg::packed(341, 0, 359), /* specifyEqualAgainstConstantSide $nodeScopeResolver */ @@ -172,8 +183,7 @@ inline constexpr reg::PackedArg args[] = { reg::packed(676, 0, 399), /* specifyEqualAgainstConstantSide $subject */ reg::packed(684, 0, 436), /* specifyEqualAgainstConstantSide $subjectResult */ reg::packed(698, MAY_BE_ANY), /* specifyEqualAgainstConstantSide $value */ - reg::packed(865, 0, 776), /* specifyEqualAgainstConstantSide $constantType */ - reg::packed(923, 0, 776), /* specifyEqualAgainstConstantSide $otherType */ + reg::packed(923, 0, 776), /* specifyEqualAgainstConstantSide $constantType */ reg::packed(482, 0, 490), /* specifyEqualAgainstConstantSide $context */ reg::packed(528, 0, 544), /* specifyEqualAgainstConstantSide $evaluationScope */ reg::packed(575, MAY_BE_NULL, 436), /* specifyEqualAgainstConstantSide $leftArgResult */ @@ -182,19 +192,19 @@ inline constexpr reg::PackedArg args[] = { reg::packed(626, MAY_BE_NULL | MAY_BE_FALSE, 627), /* specifyEqualAgainstConstantSide return */ reg::packed(676, 0, 399), /* specifyFuncCallFamilies $subject */ reg::packed(684, 0, 436), /* specifyFuncCallFamilies $subjectResult */ - reg::packed(965, 0, 970), /* specifyFuncCallFamilies $call */ - reg::packed(852, 0, 399), /* specifyFuncCallFamilies $constantExpr */ - reg::packed(865, 0, 776), /* specifyFuncCallFamilies $constantType */ + reg::packed(1013, 0, 1018), /* specifyFuncCallFamilies $call */ + reg::packed(910, 0, 399), /* specifyFuncCallFamilies $constantExpr */ + reg::packed(923, 0, 776), /* specifyFuncCallFamilies $constantType */ reg::packed(482, 0, 490), /* specifyFuncCallFamilies $context */ reg::packed(528, 0, 544), /* specifyFuncCallFamilies $evaluationScope */ - reg::packed(999, MAY_BE_NULL, 436), /* specifyFuncCallFamilies $argResult */ + reg::packed(1047, MAY_BE_NULL, 436), /* specifyFuncCallFamilies $argResult */ reg::packed(626, MAY_BE_NULL | MAY_BE_FALSE, 627), /* specifyFuncCallFamilies return */ - reg::packed(1033, 0, 399), /* captureFirstArgResult $side */ - reg::packed(1038, 0, 1046), /* captureFirstArgResult $storage */ + reg::packed(1081, 0, 399), /* captureFirstArgResult $side */ + reg::packed(1086, 0, 1094), /* captureFirstArgResult $storage */ reg::packed(626, MAY_BE_NULL, 436), /* captureFirstArgResult return */ - reg::packed(1109, 0, 399), /* literalType $expr */ + reg::packed(1157, 0, 399), /* literalType $expr */ reg::packed(626, MAY_BE_NULL, 776), /* literalType return */ - reg::packed(1109, 0, 399), /* isScalarLiteral $expr */ + reg::packed(1157, 0, 399), /* isScalarLiteral $expr */ reg::packed(626, MAY_BE_BOOL), /* isScalarLiteral return */ reg::packed(676, 0, 399), /* isSubjectCoveredAgainstConstant $subject */ reg::packed(626, MAY_BE_BOOL), /* isSubjectCoveredAgainstConstant return */ @@ -212,13 +222,14 @@ inline constexpr sigtab::Sig specifyDecidedComparison = { { 751 /* specifyDecide inline constexpr sigtab::Sig getTypeFromGettypeStringValue = { { 794 /* getTypeFromGettypeStringValue */, 1, 40, 1, 41, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig specifyGeneral = { { 824 /* specifyGeneral */, 9, 42, 9, 51, ZEND_ACC_PRIVATE } }; inline constexpr sigtab::Sig specifyEqual = { { 839 /* specifyEqual */, 9, 52, 9, 61, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig specifyIdenticalAgainstType = { { 895 /* specifyIdenticalAgainstType */, 8, 62, 8, 70, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig specifyEqualAgainstConstantSide = { { 933 /* specifyEqualAgainstConstantSide */, 15, 71, 15, 86, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig specifyFuncCallFamilies = { { 1009 /* specifyFuncCallFamilies */, 8, 87, 8, 95, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig captureFirstArgResult = { { 1087 /* captureFirstArgResult */, 2, 96, 2, 98, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig literalType = { { 1114 /* literalType */, 1, 99, 1, 100, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isScalarLiteral = { { 1126 /* isScalarLiteral */, 1, 101, 1, 102, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig isSubjectCoveredAgainstConstant = { { 1142 /* isSubjectCoveredAgainstConstant */, 1, 103, 1, 104, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig createLooselyEqualTypes = { { 886 /* createLooselyEqualTypes */, 7, 62, 7, 69, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig specifyIdenticalAgainstType = { { 953 /* specifyIdenticalAgainstType */, 8, 70, 8, 78, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig specifyEqualAgainstConstantSide = { { 981 /* specifyEqualAgainstConstantSide */, 14, 79, 14, 93, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig specifyFuncCallFamilies = { { 1057 /* specifyFuncCallFamilies */, 8, 94, 8, 102, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig captureFirstArgResult = { { 1135 /* captureFirstArgResult */, 2, 103, 2, 105, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig literalType = { { 1162 /* literalType */, 1, 106, 1, 107, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isScalarLiteral = { { 1174 /* isScalarLiteral */, 1, 108, 1, 109, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig isSubjectCoveredAgainstConstant = { { 1190 /* isSubjectCoveredAgainstConstant */, 1, 110, 1, 111, ZEND_ACC_PRIVATE } }; } // namespace sig } // namespace ptdecl::IdenticalNarrowingHelper diff --git a/turbo-ext/src/generated/InitializerExprTypeResolver.h b/turbo-ext/src/generated/InitializerExprTypeResolver.h index 4092c022288..14e68151c77 100644 --- a/turbo-ext/src/generated/InitializerExprTypeResolver.h +++ b/turbo-ext/src/generated/InitializerExprTypeResolver.h @@ -109,36 +109,39 @@ inline constexpr char strings[] = "rightType\0" /* 1281 */ "PHPStan\\Type\\TypeResult\0" /* 1291 */ "resolveIdenticalType\0" /* 1315 */ - "resolveEqualType\0" /* 1336 */ - "PHPStan\\Type\\Constant\\ConstantArrayType\0" /* 1353 */ - "valueComparisonCallback\0" /* 1393 */ - "resolveConstantArrayTypeComparison\0" /* 1417 */ - "class\0" /* 1452 */ - "PhpParser\\Node\\Name|PhpParser\\Node\\Expr\0" /* 1458 */ - "constantName\0" /* 1498 */ - "classReflection\0" /* 1511 */ - "PHPStan\\Reflection\\ClassReflection\0" /* 1527 */ - "false\0" /* 1562 */ - "getClassConstFetchTypeByReflection\0" /* 1568 */ - "fetchedOnType\0" /* 1603 */ - "transformStaticType\0" /* 1617 */ - "getClassConstFetchType\0" /* 1637 */ - "getUnaryPlusType\0" /* 1660 */ - "getUnaryMinusType\0" /* 1677 */ - "getBitwiseNotType\0" /* 1695 */ - "getBitwiseNotTypeFromType\0" /* 1713 */ - "name\0" /* 1739 */ - "PhpParser\\Node\\Name\0" /* 1744 */ - "resolveName\0" /* 1764 */ - "PHPStan\\Type\\TypeWithClassName\0" /* 1776 */ - "resolveTypeByName\0" /* 1807 */ - "classType\0" /* 1825 */ - "methodReflectionCandidate\0" /* 1835 */ - "PHPStan\\Reflection\\MethodReflection\0" /* 1861 */ - "resolveTypeByNameWithLateStaticBinding\0" /* 1897 */ - "PHPStan\\Reflection\\ReflectionProvider\0" /* 1936 */ - "getReflectionProvider\0" /* 1974 */ - "getNeverType"; /* 1996 */ + "phpVersions\0" /* 1336 */ + "PHPStan\\Php\\PhpVersions\0" /* 1348 */ + "null\0" /* 1372 */ + "resolveEqualType\0" /* 1377 */ + "PHPStan\\Type\\Constant\\ConstantArrayType\0" /* 1394 */ + "valueComparisonCallback\0" /* 1434 */ + "resolveConstantArrayTypeComparison\0" /* 1458 */ + "class\0" /* 1493 */ + "PhpParser\\Node\\Name|PhpParser\\Node\\Expr\0" /* 1499 */ + "constantName\0" /* 1539 */ + "classReflection\0" /* 1552 */ + "PHPStan\\Reflection\\ClassReflection\0" /* 1568 */ + "false\0" /* 1603 */ + "getClassConstFetchTypeByReflection\0" /* 1609 */ + "fetchedOnType\0" /* 1644 */ + "transformStaticType\0" /* 1658 */ + "getClassConstFetchType\0" /* 1678 */ + "getUnaryPlusType\0" /* 1701 */ + "getUnaryMinusType\0" /* 1718 */ + "getBitwiseNotType\0" /* 1736 */ + "getBitwiseNotTypeFromType\0" /* 1754 */ + "name\0" /* 1780 */ + "PhpParser\\Node\\Name\0" /* 1785 */ + "resolveName\0" /* 1805 */ + "PHPStan\\Type\\TypeWithClassName\0" /* 1817 */ + "resolveTypeByName\0" /* 1848 */ + "classType\0" /* 1866 */ + "methodReflectionCandidate\0" /* 1876 */ + "PHPStan\\Reflection\\MethodReflection\0" /* 1902 */ + "resolveTypeByNameWithLateStaticBinding\0" /* 1938 */ + "PHPStan\\Reflection\\ReflectionProvider\0" /* 1977 */ + "getReflectionProvider\0" /* 2015 */ + "getNeverType"; /* 2037 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 17), /* __construct $constantResolver */ reg::packed(51, 0, 78), /* __construct $reflectionProviderProvider */ @@ -238,22 +241,23 @@ inline constexpr reg::PackedArg args[] = { reg::packed(559, 0, 1291), /* resolveIdenticalType return */ reg::packed(1272, 0, 560), /* resolveEqualType $leftType */ reg::packed(1281, 0, 560), /* resolveEqualType $rightType */ + reg::packed(1336, MAY_BE_NULL, 1348, false, false, 1372), /* resolveEqualType $phpVersions */ reg::packed(559, 0, 1291), /* resolveEqualType return */ - reg::packed(1272, 0, 1353), /* resolveConstantArrayTypeComparison $leftType */ - reg::packed(1281, 0, 1353), /* resolveConstantArrayTypeComparison $rightType */ - reg::packed(1393, MAY_BE_CALLABLE), /* resolveConstantArrayTypeComparison $valueComparisonCallback */ + reg::packed(1272, 0, 1394), /* resolveConstantArrayTypeComparison $leftType */ + reg::packed(1281, 0, 1394), /* resolveConstantArrayTypeComparison $rightType */ + reg::packed(1434, MAY_BE_CALLABLE), /* resolveConstantArrayTypeComparison $valueComparisonCallback */ reg::packed(559, 0, 1291), /* resolveConstantArrayTypeComparison return */ - reg::packed(1452, 0, 1458), /* getClassConstFetchTypeByReflection $class */ - reg::packed(1498, MAY_BE_STRING), /* getClassConstFetchTypeByReflection $constantName */ - reg::packed(1511, MAY_BE_NULL, 1527), /* getClassConstFetchTypeByReflection $classReflection */ + reg::packed(1493, 0, 1499), /* getClassConstFetchTypeByReflection $class */ + reg::packed(1539, MAY_BE_STRING), /* getClassConstFetchTypeByReflection $constantName */ + reg::packed(1552, MAY_BE_NULL, 1568), /* getClassConstFetchTypeByReflection $classReflection */ reg::packed(647, MAY_BE_CALLABLE), /* getClassConstFetchTypeByReflection $getTypeCallback */ - reg::packed(927, MAY_BE_BOOL, reg::NoString, false, false, 1562), /* getClassConstFetchTypeByReflection $nativeTypesPromoted */ + reg::packed(927, MAY_BE_BOOL, reg::NoString, false, false, 1603), /* getClassConstFetchTypeByReflection $nativeTypesPromoted */ reg::packed(559, 0, 560), /* getClassConstFetchTypeByReflection return */ reg::packed(799, 0, 560), /* transformStaticType $type */ - reg::packed(1603, 0, 560), /* transformStaticType $fetchedOnType */ + reg::packed(1644, 0, 560), /* transformStaticType $fetchedOnType */ reg::packed(559, 0, 560), /* transformStaticType return */ - reg::packed(1452, 0, 1458), /* getClassConstFetchType $class */ - reg::packed(1498, MAY_BE_STRING), /* getClassConstFetchType $constantName */ + reg::packed(1493, 0, 1499), /* getClassConstFetchType $class */ + reg::packed(1539, MAY_BE_STRING), /* getClassConstFetchType $constantName */ reg::packed(586, MAY_BE_NULL | MAY_BE_STRING), /* getClassConstFetchType $className */ reg::packed(647, MAY_BE_CALLABLE), /* getClassConstFetchType $getTypeCallback */ reg::packed(559, 0, 560), /* getClassConstFetchType return */ @@ -268,17 +272,17 @@ inline constexpr reg::PackedArg args[] = { reg::packed(559, 0, 560), /* getBitwiseNotType return */ reg::packed(772, 0, 560), /* getBitwiseNotTypeFromType $exprType */ reg::packed(559, 0, 560), /* getBitwiseNotTypeFromType return */ - reg::packed(1739, 0, 1744), /* resolveName $name */ - reg::packed(1511, MAY_BE_NULL, 1527), /* resolveName $classReflection */ + reg::packed(1780, 0, 1785), /* resolveName $name */ + reg::packed(1552, MAY_BE_NULL, 1568), /* resolveName $classReflection */ reg::packed(559, MAY_BE_STRING), /* resolveName return */ - reg::packed(1739, 0, 1744), /* resolveTypeByName $name */ - reg::packed(1511, MAY_BE_NULL, 1527), /* resolveTypeByName $classReflection */ - reg::packed(559, 0, 1776), /* resolveTypeByName return */ - reg::packed(1452, 0, 1744), /* resolveTypeByNameWithLateStaticBinding $class */ - reg::packed(1825, 0, 560), /* resolveTypeByNameWithLateStaticBinding $classType */ - reg::packed(1835, 0, 1861), /* resolveTypeByNameWithLateStaticBinding $methodReflectionCandidate */ + reg::packed(1780, 0, 1785), /* resolveTypeByName $name */ + reg::packed(1552, MAY_BE_NULL, 1568), /* resolveTypeByName $classReflection */ + reg::packed(559, 0, 1817), /* resolveTypeByName return */ + reg::packed(1493, 0, 1785), /* resolveTypeByNameWithLateStaticBinding $class */ + reg::packed(1866, 0, 560), /* resolveTypeByNameWithLateStaticBinding $classType */ + reg::packed(1876, 0, 1902), /* resolveTypeByNameWithLateStaticBinding $methodReflectionCandidate */ reg::packed(559, 0, 560), /* resolveTypeByNameWithLateStaticBinding return */ - reg::packed(559, 0, 1936), /* getReflectionProvider return */ + reg::packed(559, 0, 1977), /* getReflectionProvider return */ reg::packed(1272, 0, 560), /* getNeverType $leftType */ reg::packed(1281, 0, 560), /* getNeverType $rightType */ reg::packed(559, 0, 560), /* getNeverType return */ @@ -313,20 +317,20 @@ inline constexpr sigtab::Sig getPowType = { { 1226 /* getPowType */, 3, 81, 3, 8 inline constexpr sigtab::Sig getShiftLeftType = { { 1237 /* getShiftLeftType */, 3, 85, 3, 88, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig getShiftRightType = { { 1254 /* getShiftRightType */, 3, 89, 3, 92, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig resolveIdenticalType = { { 1315 /* resolveIdenticalType */, 2, 93, 2, 95, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig resolveEqualType = { { 1336 /* resolveEqualType */, 2, 96, 2, 98, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig resolveConstantArrayTypeComparison = { { 1417 /* resolveConstantArrayTypeComparison */, 3, 99, 3, 102, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getClassConstFetchTypeByReflection = { { 1568 /* getClassConstFetchTypeByReflection */, 4, 103, 5, 108, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig transformStaticType = { { 1617 /* transformStaticType */, 2, 109, 2, 111, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getClassConstFetchType = { { 1637 /* getClassConstFetchType */, 4, 112, 4, 116, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getUnaryPlusType = { { 1660 /* getUnaryPlusType */, 2, 117, 2, 119, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getUnaryMinusType = { { 1677 /* getUnaryMinusType */, 2, 120, 2, 122, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getBitwiseNotType = { { 1695 /* getBitwiseNotType */, 2, 123, 2, 125, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig getBitwiseNotTypeFromType = { { 1713 /* getBitwiseNotTypeFromType */, 1, 126, 1, 127, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig resolveName = { { 1764 /* resolveName */, 2, 128, 2, 130, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig resolveTypeByName = { { 1807 /* resolveTypeByName */, 2, 131, 2, 133, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig resolveTypeByNameWithLateStaticBinding = { { 1897 /* resolveTypeByNameWithLateStaticBinding */, 3, 134, 3, 137, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getReflectionProvider = { { 1974 /* getReflectionProvider */, 0, 138, 0, 138, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig getNeverType = { { 1996 /* getNeverType */, 2, 139, 2, 141, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveEqualType = { { 1377 /* resolveEqualType */, 2, 96, 3, 99, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig resolveConstantArrayTypeComparison = { { 1458 /* resolveConstantArrayTypeComparison */, 3, 100, 3, 103, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getClassConstFetchTypeByReflection = { { 1609 /* getClassConstFetchTypeByReflection */, 4, 104, 5, 109, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig transformStaticType = { { 1658 /* transformStaticType */, 2, 110, 2, 112, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getClassConstFetchType = { { 1678 /* getClassConstFetchType */, 4, 113, 4, 117, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getUnaryPlusType = { { 1701 /* getUnaryPlusType */, 2, 118, 2, 120, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getUnaryMinusType = { { 1718 /* getUnaryMinusType */, 2, 121, 2, 123, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getBitwiseNotType = { { 1736 /* getBitwiseNotType */, 2, 124, 2, 126, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getBitwiseNotTypeFromType = { { 1754 /* getBitwiseNotTypeFromType */, 1, 127, 1, 128, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig resolveName = { { 1805 /* resolveName */, 2, 129, 2, 131, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveTypeByName = { { 1848 /* resolveTypeByName */, 2, 132, 2, 134, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveTypeByNameWithLateStaticBinding = { { 1938 /* resolveTypeByNameWithLateStaticBinding */, 3, 135, 3, 138, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getReflectionProvider = { { 2015 /* getReflectionProvider */, 0, 139, 0, 139, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getNeverType = { { 2037 /* getNeverType */, 2, 140, 2, 142, ZEND_ACC_PRIVATE } }; } // namespace sig } // namespace ptdecl::InitializerExprTypeResolver diff --git a/turbo-ext/src/generated/OperatorTypeTrait.h b/turbo-ext/src/generated/OperatorTypeTrait.h index d37cdd269bd..a88c5594a35 100644 --- a/turbo-ext/src/generated/OperatorTypeTrait.h +++ b/turbo-ext/src/generated/OperatorTypeTrait.h @@ -30,8 +30,10 @@ inline constexpr char strings[] = "bitwiseXor\0" /* 225 */ "bitwiseShiftLeft\0" /* 236 */ "bitwiseShiftRight\0" /* 253 */ - "stringConcatenate\0" /* 271 */ - "arrayUnion"; /* 289 */ + "getPossiblyLooselyEqualType\0" /* 271 */ + "getAlwaysLooselyEqualType\0" /* 299 */ + "stringConcatenate\0" /* 325 */ + "arrayUnion"; /* 343 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 9), /* exponentiate $exponent */ reg::packed(27, 0, 9), /* exponentiate return */ @@ -70,6 +72,10 @@ inline constexpr reg::PackedArg args[] = { reg::packed(41, 0, 9), /* bitwiseShiftRight $right */ reg::packed(47, 0, 58), /* bitwiseShiftRight $phpVersion */ reg::packed(27, 0, 9), /* bitwiseShiftRight return */ + reg::packed(47, 0, 58), /* getPossiblyLooselyEqualType $phpVersion */ + reg::packed(27, 0, 9), /* getPossiblyLooselyEqualType return */ + reg::packed(47, 0, 58), /* getAlwaysLooselyEqualType $phpVersion */ + reg::packed(27, 0, 9), /* getAlwaysLooselyEqualType return */ reg::packed(41, 0, 9), /* stringConcatenate $right */ reg::packed(47, 0, 58), /* stringConcatenate $phpVersion */ reg::packed(27, 0, 9), /* stringConcatenate return */ @@ -95,8 +101,10 @@ inline constexpr sigtab::Sig bitwiseOr = { { 215 /* bitwiseOr */, 2, 25, 2, 27, inline constexpr sigtab::Sig bitwiseXor = { { 225 /* bitwiseXor */, 2, 28, 2, 30, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig bitwiseShiftLeft = { { 236 /* bitwiseShiftLeft */, 2, 31, 2, 33, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig bitwiseShiftRight = { { 253 /* bitwiseShiftRight */, 2, 34, 2, 36, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig stringConcatenate = { { 271 /* stringConcatenate */, 2, 37, 2, 39, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig arrayUnion = { { 289 /* arrayUnion */, 2, 40, 2, 42, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getPossiblyLooselyEqualType = { { 271 /* getPossiblyLooselyEqualType */, 1, 37, 1, 38, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig getAlwaysLooselyEqualType = { { 299 /* getAlwaysLooselyEqualType */, 1, 39, 1, 40, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig stringConcatenate = { { 325 /* stringConcatenate */, 2, 41, 2, 43, ZEND_ACC_PUBLIC } }; +inline constexpr sigtab::Sig arrayUnion = { { 343 /* arrayUnion */, 2, 44, 2, 46, ZEND_ACC_PUBLIC } }; } // namespace sig } // namespace ptdecl::OperatorTypeTrait diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 5a7d02df62f..c1cf08062be 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -4076,7 +4076,7 @@ zv::Val pt_initializer_expr_type_resolver_get_enum_case_value_type(zval *resolve zv::Val pt_initializer_expr_type_resolver_get_binary_op_type(zval *resolver, pt_ietr_binary_operator op, zval *left, zval *right, const pt_ietr_get_type &getTypeCallback); zv::Val pt_initializer_expr_type_resolver_resolve_concat_type(zval *resolver, zval *left, zval *right); zv::Val pt_initializer_expr_type_resolver_resolve_identical_type(zval *resolver, zval *leftType, zval *rightType); -zv::Val pt_initializer_expr_type_resolver_resolve_equal_type(zval *resolver, zval *leftType, zval *rightType); +zv::Val pt_initializer_expr_type_resolver_resolve_equal_type(zval *resolver, zval *leftType, zval *rightType, zval *phpVersions = NULL); zv::Val pt_initializer_expr_type_resolver_get_array_type(zval *resolver, zval *expr, const pt_ietr_get_type &getTypeCallback); zv::Val pt_initializer_expr_type_resolver_get_cast_type(zval *resolver, zval *expr, const pt_ietr_get_type &getTypeCallback); zv::Val pt_initializer_expr_type_resolver_get_cast_object_type(zval *resolver, zval *exprType); diff --git a/turbo-ext/tests/type-family.php b/turbo-ext/tests/type-family.php index 77ffb272095..a5a670cf654 100644 --- a/turbo-ext/tests/type-family.php +++ b/turbo-ext/tests/type-family.php @@ -1019,6 +1019,27 @@ public function getValue() 'benevolent' => new \PHPStan\Type\BenevolentUnionType([$floatRangeClass::fromInterval(1.0, 2.0), new \PHPStan\Type\IntegerType()]), 'string' => new \PHPStan\Type\StringType(), ]; + // the loose equality sets of LooseComparisonHelper, reached through OperatorTypeTrait + $looseEqualityOperands = $arithmeticOperands + [ + 'null' => new \PHPStan\Type\NullType(), + 'true' => new \PHPStan\Type\Constant\ConstantBooleanType(true), + 'false' => new \PHPStan\Type\Constant\ConstantBooleanType(false), + 'emptyString' => new \PHPStan\Type\Constant\ConstantStringType(''), + 'abc' => new \PHPStan\Type\Constant\ConstantStringType('abc'), + 'numeric1e3' => new \PHPStan\Type\Constant\ConstantStringType('1e3'), + 'INF string' => new \PHPStan\Type\Constant\ConstantStringType('INF'), + 'numericString' => new \PHPStan\Type\IntersectionType([new \PHPStan\Type\StringType(), new \PHPStan\Type\Accessory\AccessoryNumericStringType()]), + 'emptyArray' => new \PHPStan\Type\Constant\ConstantArrayType([], []), + 'array' => new \PHPStan\Type\ArrayType(new \PHPStan\Type\MixedType(), new \PHPStan\Type\MixedType()), + 'mixed' => new \PHPStan\Type\MixedType(), + ]; + foreach ($looseEqualityOperands as $name => $operand) { + foreach ($floatPhpVersions as $vi => $phpVersion) { + $r["$name getPossiblyLooselyEqualType $vi"] = $view($operand->getPossiblyLooselyEqualType($phpVersion)); + $r["$name getAlwaysLooselyEqualType $vi"] = $view($operand->getAlwaysLooselyEqualType($phpVersion)); + } + } + foreach ($arithmeticOperands as $leftName => $left) { foreach ($arithmeticOperands as $rightName => $right) { foreach (['arithmeticAdd', 'arithmeticSubtract', 'arithmeticMultiply', 'arithmeticDivide'] as $method) {