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) {