diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index 362e7b27ab4..29e78bcf57d 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -175,6 +175,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex return $nodeScopeResolver->processSyntheticOnDemand($e, $flavouredScope)->getTypeOnScope($flavouredScope, $flavouredScope->nativeTypesPromoted); }; + $rememberedType = $this->resolveRememberedComparisonType($scope, $expr, $getType); + if ($rememberedType !== null) { + return $rememberedType; + } + if ($expr instanceof BinaryOp\Smaller) { return $getType($expr->left)->isSmallerThan($getType($expr->right), $this->phpVersion)->toBooleanType(); } @@ -288,427 +293,581 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex throw new ShouldNotHappenException(sprintf('Unhandled %s', get_class($expr))); }; + $specifyTypesCallback = function (TypeSpecifierContext $context, bool $nativeTypesPromoted) use ($expr, $leftResult, $rightResult, $nodeScopeResolver, $beforeScope, $specifySubResults, $leftArgResult, $rightArgResult, $typeCallback): SpecifiedTypes { + $scope = $nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope; + if ($expr instanceof BinaryOp\Identical || $expr instanceof BinaryOp\NotIdentical) { + // `!==` narrowing is the `===` narrowing in the negated context - + // no synthetic Identical node. A null context never negates. + if ($context->null() && $expr instanceof BinaryOp\NotIdentical) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + } - return $this->expressionResultFactory->create( - $scope, - beforeScope: $beforeScope, - expr: $expr, - variableFlow: VariableFlow::sequence($leftResult->getVariableFlow(), $rightResult->getVariableFlow(), VariableFlowBuilder::throws($expr, $throwPoints)), - hasYield: $leftResult->hasYield() || $rightResult->hasYield(), - isAlwaysTerminating: $leftResult->isAlwaysTerminating() || $rightResult->isAlwaysTerminating(), - throwPoints: $throwPoints, - impurePoints: $impurePoints, - typeCallback: $typeCallback, - specifyTypesCallback: function (TypeSpecifierContext $context, bool $nativeTypesPromoted) use ($expr, $leftResult, $rightResult, $nodeScopeResolver, $beforeScope, $specifySubResults, $leftArgResult, $rightArgResult, $typeCallback): SpecifiedTypes { - $scope = $nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope; - if ($expr instanceof BinaryOp\Identical || $expr instanceof BinaryOp\NotIdentical) { - // `!==` narrowing is the `===` narrowing in the negated context - - // no synthetic Identical node. A null context never negates. - if ($context->null() && $expr instanceof BinaryOp\NotIdentical) { - return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); - } - - $newWorldTypes = $this->identicalNarrowingHelper->specifyIdentical( - $nodeScopeResolver, - $expr->left, - $expr->right, - $leftResult, - $rightResult, - $expr instanceof BinaryOp\NotIdentical ? $context->negate() : $context, - // the narrowing composes on the evaluation scope; only the - // asked flavour comes from the asking scope - $scope, - $leftArgResult, - $rightArgResult, - // the comparison's own verdict, in Identical semantics - - // computed from the captured operand results (the walk's - // evaluation point), only the flavour follows the ask - static function () use ($expr, $nativeTypesPromoted, $typeCallback): Type { - $ownType = $typeCallback($nativeTypesPromoted); - if ($expr instanceof BinaryOp\NotIdentical) { - if ($ownType->isTrue()->yes()) { - return new ConstantBooleanType(false); - } - if ($ownType->isFalse()->yes()) { - return new ConstantBooleanType(true); - } + $newWorldTypes = $this->identicalNarrowingHelper->specifyIdentical( + $nodeScopeResolver, + $expr->left, + $expr->right, + $leftResult, + $rightResult, + $expr instanceof BinaryOp\NotIdentical ? $context->negate() : $context, + // the narrowing composes on the evaluation scope; only the + // asked flavour comes from the asking scope + $scope, + $leftArgResult, + $rightArgResult, + // the comparison's own verdict, in Identical semantics - + // computed from the captured operand results (the walk's + // evaluation point), only the flavour follows the ask + static function () use ($expr, $nativeTypesPromoted, $typeCallback): Type { + $ownType = $typeCallback($nativeTypesPromoted); + if ($expr instanceof BinaryOp\NotIdentical) { + if ($ownType->isTrue()->yes()) { + return new ConstantBooleanType(false); } + if ($ownType->isFalse()->yes()) { + return new ConstantBooleanType(true); + } + } - return $ownType; - }, - ); + return $ownType; + }, + ); + + // null = no shape-specific narrowing (unknown-class ::class, + // null-context asks) - the default is all that remains + return ($newWorldTypes ?? $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context))->setRootExpr($expr); + } - // null = no shape-specific narrowing (unknown-class ::class, - // null-context asks) - the default is all that remains - return ($newWorldTypes ?? $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context))->setRootExpr($expr); + if ($expr instanceof BinaryOp\Equal || $expr instanceof BinaryOp\NotEqual) { + // `!=` narrowing is the `==` narrowing in the negated context + if ($context->null() && $expr instanceof BinaryOp\NotEqual) { + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); } - if ($expr instanceof BinaryOp\Equal || $expr instanceof BinaryOp\NotEqual) { - // `!=` narrowing is the `==` narrowing in the negated context - if ($context->null() && $expr instanceof BinaryOp\NotEqual) { + $newWorldTypes = $this->identicalNarrowingHelper->specifyEqual( + $nodeScopeResolver, + $expr->left, + $expr->right, + $leftResult, + $rightResult, + $expr instanceof BinaryOp\NotEqual ? $context->negate() : $context, + $scope, + $leftArgResult, + $rightArgResult, + ); + + return ($newWorldTypes ?? $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context))->setRootExpr($expr); + } + + if ($expr instanceof BinaryOp\Smaller || $expr instanceof BinaryOp\SmallerOrEqual) { + if ( + $expr->left instanceof Expr\FuncCall + && $expr->left->name instanceof Name + && !$expr->left->isFirstClassCallable() + && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) + && count($expr->left->getArgs()) >= 1 + && ( + !$expr->right instanceof Expr\FuncCall + || !$expr->right->name instanceof Name + || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) + ) + ) { + $inverseOperator = $expr instanceof BinaryOp\Smaller + ? new BinaryOp\SmallerOrEqual($expr->right, $expr->left) + : new BinaryOp\Smaller($expr->right, $expr->left); + + // negating the context is exactly what a BooleanNot around the + // inverse operator would do - direct computation avoids + // synthesizing a BooleanNot node. A null context never negates + // (BooleanNot defaults on it too). + if ($context->null()) { return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); } - $newWorldTypes = $this->identicalNarrowingHelper->specifyEqual( - $nodeScopeResolver, - $expr->left, - $expr->right, - $leftResult, - $rightResult, - $expr instanceof BinaryOp\NotEqual ? $context->negate() : $context, + return $this->defaultNarrowingHelper->specifyTypesForNode( $scope, - $leftArgResult, - $rightArgResult, - ); - - return ($newWorldTypes ?? $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context))->setRootExpr($expr); + $inverseOperator, + $context->negate(), + )->setRootExpr($expr); } - if ($expr instanceof BinaryOp\Smaller || $expr instanceof BinaryOp\SmallerOrEqual) { - if ( - $expr->left instanceof Expr\FuncCall - && $expr->left->name instanceof Name - && !$expr->left->isFirstClassCallable() - && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) - && count($expr->left->getArgs()) >= 1 - && ( - !$expr->right instanceof Expr\FuncCall - || !$expr->right->name instanceof Name - || !in_array(strtolower((string) $expr->right->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) - ) - ) { - $inverseOperator = $expr instanceof BinaryOp\Smaller - ? new BinaryOp\SmallerOrEqual($expr->right, $expr->left) - : new BinaryOp\Smaller($expr->right, $expr->left); - - // negating the context is exactly what a BooleanNot around the - // inverse operator would do - direct computation avoids - // synthesizing a BooleanNot node. A null context never negates - // (BooleanNot defaults on it too). - if ($context->null()) { - return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); - } - - return $this->defaultNarrowingHelper->specifyTypesForNode( - $scope, - $inverseOperator, - $context->negate(), - )->setRootExpr($expr); + $orEqual = $expr instanceof BinaryOp\SmallerOrEqual; + $offset = $orEqual ? 0 : 1; + // the operands were processed during processExpr; read their + // already computed results instead of re-walking via + // Scope::getType(). Their subexpressions (e.g. count() arguments) + // were also processed and are read from the stored result. + $getType = static function (Expr $e) use ($expr, $leftResult, $rightResult, $scope, $specifySubResults, $nativeTypesPromoted): Type { + if ($e === $expr->left) { + return $nativeTypesPromoted ? $leftResult->getNativeType() : $leftResult->getType(); + } + if ($e === $expr->right) { + return $nativeTypesPromoted ? $rightResult->getNativeType() : $rightResult->getType(); } - $orEqual = $expr instanceof BinaryOp\SmallerOrEqual; - $offset = $orEqual ? 0 : 1; - // the operands were processed during processExpr; read their - // already computed results instead of re-walking via - // Scope::getType(). Their subexpressions (e.g. count() arguments) - // were also processed and are read from the stored result. - $getType = static function (Expr $e) use ($expr, $leftResult, $rightResult, $scope, $specifySubResults, $nativeTypesPromoted): Type { - if ($e === $expr->left) { - return $nativeTypesPromoted ? $leftResult->getNativeType() : $leftResult->getType(); - } - if ($e === $expr->right) { - return $nativeTypesPromoted ? $rightResult->getNativeType() : $rightResult->getType(); - } - - // the remaining asks are operand subexpressions whose walk - // results were captured at creation - $result = $specifySubResults[spl_object_id($e)] ?? null; - if ($result === null) { - throw new ShouldNotHappenException(); - } + // the remaining asks are operand subexpressions whose walk + // results were captured at creation + $result = $specifySubResults[spl_object_id($e)] ?? null; + if ($result === null) { + throw new ShouldNotHappenException(); + } - return $result->getTypeOnScope($scope, $scope->nativeTypesPromoted); - }; - $leftType = $getType($expr->left); - $result = (new SpecifiedTypes([], []))->setRootExpr($expr); + return $result->getTypeOnScope($scope, $scope->nativeTypesPromoted); + }; + $leftType = $getType($expr->left); + $result = (new SpecifiedTypes([], []))->setRootExpr($expr); - if ( - !$context->null() - && $expr->right instanceof Expr\FuncCall - && $expr->right->name instanceof Name - && !$expr->right->isFirstClassCallable() - && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true) - && count($expr->right->getArgs()) >= 1 - && $leftType->isInteger()->yes() - ) { - $argType = $getType($expr->right->getArgs()[0]->value); + if ( + !$context->null() + && $expr->right instanceof Expr\FuncCall + && $expr->right->name instanceof Name + && !$expr->right->isFirstClassCallable() + && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true) + && count($expr->right->getArgs()) >= 1 + && $leftType->isInteger()->yes() + ) { + $argType = $getType($expr->right->getArgs()[0]->value); - $sizeType = null; - if ($leftType instanceof ConstantIntegerType) { + $sizeType = null; + if ($leftType instanceof ConstantIntegerType) { + if ($orEqual) { + $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); + } else { + $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); + } + } elseif ($leftType instanceof IntegerRangeType) { + if ($context->falsey() && $leftType->getMax() !== null) { if ($orEqual) { - $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getValue()); + $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMax()); } else { - $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getValue()); + $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMax()); } - } elseif ($leftType instanceof IntegerRangeType) { - if ($context->falsey() && $leftType->getMax() !== null) { - if ($orEqual) { - $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMax()); - } else { - $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMax()); - } - } elseif ($context->truthy() && $leftType->getMin() !== null) { - if ($orEqual) { - $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMin()); - } else { - $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMin()); - } + } elseif ($context->truthy() && $leftType->getMin() !== null) { + if ($orEqual) { + $sizeType = IntegerRangeType::createAllGreaterThanOrEqualTo($leftType->getMin()); + } else { + $sizeType = IntegerRangeType::createAllGreaterThan($leftType->getMin()); } - } else { - $sizeType = $leftType; } + } else { + $sizeType = $leftType; + } - if ($sizeType !== null) { - $specifiedTypes = $this->countNarrowingHelper->specifyCountSize($expr->right, $argType, $sizeType, $context, $scope, $expr); - if ($specifiedTypes !== null) { - $result = $result->unionWith($specifiedTypes); - } + if ($sizeType !== null) { + $specifiedTypes = $this->countNarrowingHelper->specifyCountSize($expr->right, $argType, $sizeType, $context, $scope, $expr); + if ($specifiedTypes !== null) { + $result = $result->unionWith($specifiedTypes); } + } - if ( - $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) - || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) - ) { - if ($context->truthy() && $argType->isArray()->maybe()) { - $countables = []; - if ($argType instanceof UnionType) { - $countableInterface = new ObjectType(Countable::class); - foreach ($argType->getTypes() as $innerType) { - if ($innerType->isArray()->yes()) { - $innerType = TypeCombinator::intersect(new NonEmptyArrayType(), $innerType); - $countables[] = $innerType; - } - - if (!$countableInterface->isSuperTypeOf($innerType)->yes()) { - continue; - } - + if ( + $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) + || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) + ) { + if ($context->truthy() && $argType->isArray()->maybe()) { + $countables = []; + if ($argType instanceof UnionType) { + $countableInterface = new ObjectType(Countable::class); + foreach ($argType->getTypes() as $innerType) { + if ($innerType->isArray()->yes()) { + $innerType = TypeCombinator::intersect(new NonEmptyArrayType(), $innerType); $countables[] = $innerType; } - } - if (count($countables) > 0) { - $countableType = TypeCombinator::union(...$countables); + if (!$countableInterface->isSuperTypeOf($innerType)->yes()) { + continue; + } - return $this->defaultNarrowingHelper->createForSubject($expr->right->getArgs()[0]->value, $countableType, $context, $scope)->setRootExpr($expr); + $countables[] = $innerType; } } - if ($argType->isArray()->yes()) { - $newType = new NonEmptyArrayType(); - if ($context->true() && $argType->isList()->yes()) { - $newType = TypeCombinator::intersect($newType, new AccessoryArrayListType()); - } + if (count($countables) > 0) { + $countableType = TypeCombinator::union(...$countables); - $result = $result->unionWith( - $this->defaultNarrowingHelper->createForSubject($expr->right->getArgs()[0]->value, $newType, $context, $scope)->setRootExpr($expr), - ); + return $this->defaultNarrowingHelper->createForSubject($expr->right->getArgs()[0]->value, $countableType, $context, $scope)->setRootExpr($expr); } } - // infer $list[$index] after $index < count($list) - if ( - $context->true() - && !$orEqual - // constant offsets are handled via HasOffsetType/HasOffsetValueType - && !$leftType instanceof ConstantIntegerType - && $argType->isList()->yes() - && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() - ) { - $arrayArg = $expr->right->getArgs()[0]->value; - $dimFetch = new Expr\ArrayDimFetch($arrayArg, $expr->left); + if ($argType->isArray()->yes()) { + $newType = new NonEmptyArrayType(); + if ($context->true() && $argType->isList()->yes()) { + $newType = TypeCombinator::intersect($newType, new AccessoryArrayListType()); + } + $result = $result->unionWith( - $this->defaultNarrowingHelper->createForSubject($dimFetch, $argType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr), + $this->defaultNarrowingHelper->createForSubject($expr->right->getArgs()[0]->value, $newType, $context, $scope)->setRootExpr($expr), ); } } - // infer $list[$index] after $zeroOrMore < count($list) - N - // infer $list[$index] after $zeroOrMore <= count($list) - N + // infer $list[$index] after $index < count($list) if ( $context->true() - && $expr->right instanceof BinaryOp\Minus - && $expr->right->left instanceof Expr\FuncCall - && $expr->right->left->name instanceof Name - && !$expr->right->left->isFirstClassCallable() - && in_array(strtolower((string) $expr->right->left->name), ['count', 'sizeof'], true) - && count($expr->right->left->getArgs()) >= 1 + && !$orEqual // constant offsets are handled via HasOffsetType/HasOffsetValueType && !$leftType instanceof ConstantIntegerType - && $leftType->isInteger()->yes() + && $argType->isList()->yes() && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() ) { - $countArgType = $getType($expr->right->left->getArgs()[0]->value); - $subtractedType = $getType($expr->right->right); - if ( - $countArgType->isList()->yes() - && $this->countNarrowingHelper->isNormalCountCall($expr->right->left, $countArgType, $scope)->yes() - && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($subtractedType)->yes() - ) { - $arrayArg = $expr->right->left->getArgs()[0]->value; - $dimFetch = new Expr\ArrayDimFetch($arrayArg, $expr->left); - $result = $result->unionWith( - $this->defaultNarrowingHelper->createForSubject($dimFetch, $countArgType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr), - ); - } + $arrayArg = $expr->right->getArgs()[0]->value; + $dimFetch = new Expr\ArrayDimFetch($arrayArg, $expr->left); + $result = $result->unionWith( + $this->defaultNarrowingHelper->createForSubject($dimFetch, $argType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr), + ); } + } + // infer $list[$index] after $zeroOrMore < count($list) - N + // infer $list[$index] after $zeroOrMore <= count($list) - N + if ( + $context->true() + && $expr->right instanceof BinaryOp\Minus + && $expr->right->left instanceof Expr\FuncCall + && $expr->right->left->name instanceof Name + && !$expr->right->left->isFirstClassCallable() + && in_array(strtolower((string) $expr->right->left->name), ['count', 'sizeof'], true) + && count($expr->right->left->getArgs()) >= 1 + // constant offsets are handled via HasOffsetType/HasOffsetValueType + && !$leftType instanceof ConstantIntegerType + && $leftType->isInteger()->yes() + && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes() + ) { + $countArgType = $getType($expr->right->left->getArgs()[0]->value); + $subtractedType = $getType($expr->right->right); if ( - !$context->null() - && $expr->right instanceof Expr\FuncCall - && $expr->right->name instanceof Name - && !$expr->right->isFirstClassCallable() - && in_array(strtolower((string) $expr->right->name), ['preg_match'], true) - && count($expr->right->getArgs()) >= 3 - && ( - IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($leftType)->yes() - || ($expr instanceof BinaryOp\Smaller && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes()) - ) + $countArgType->isList()->yes() + && $this->countNarrowingHelper->isNormalCountCall($expr->right->left, $countArgType, $scope)->yes() + && IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($subtractedType)->yes() ) { - // 0 < preg_match or 1 <= preg_match becomes 1 === preg_match - $newExpr = new BinaryOp\Identical($expr->right, new Scalar\Int_(1)); - - return $this->defaultNarrowingHelper->specifyTypesForNode($scope, $newExpr, $context)->setRootExpr($expr); + $arrayArg = $expr->right->left->getArgs()[0]->value; + $dimFetch = new Expr\ArrayDimFetch($arrayArg, $expr->left); + $result = $result->unionWith( + $this->defaultNarrowingHelper->createForSubject($dimFetch, $countArgType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope)->setRootExpr($expr), + ); } + } + + if ( + !$context->null() + && $expr->right instanceof Expr\FuncCall + && $expr->right->name instanceof Name + && !$expr->right->isFirstClassCallable() + && in_array(strtolower((string) $expr->right->name), ['preg_match'], true) + && count($expr->right->getArgs()) >= 3 + && ( + IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($leftType)->yes() + || ($expr instanceof BinaryOp\Smaller && IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($leftType)->yes()) + ) + ) { + // 0 < preg_match or 1 <= preg_match becomes 1 === preg_match + $newExpr = new BinaryOp\Identical($expr->right, new Scalar\Int_(1)); + + return $this->defaultNarrowingHelper->specifyTypesForNode($scope, $newExpr, $context)->setRootExpr($expr); + } + if ( + !$context->null() + && $expr->right instanceof Expr\FuncCall + && $expr->right->name instanceof Name + && !$expr->right->isFirstClassCallable() + && in_array(strtolower((string) $expr->right->name), ['strlen', 'mb_strlen'], true) + && count($expr->right->getArgs()) === 1 + && $leftType->isInteger()->yes() + ) { if ( - !$context->null() - && $expr->right instanceof Expr\FuncCall - && $expr->right->name instanceof Name - && !$expr->right->isFirstClassCallable() - && in_array(strtolower((string) $expr->right->name), ['strlen', 'mb_strlen'], true) - && count($expr->right->getArgs()) === 1 - && $leftType->isInteger()->yes() + $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) + || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) ) { - if ( - $context->true() && (IntegerRangeType::createAllGreaterThanOrEqualTo(1 - $offset)->isSuperTypeOf($leftType)->yes()) - || ($context->false() && (new ConstantIntegerType(1 - $offset))->isSuperTypeOf($leftType)->yes()) - ) { - $argType = $getType($expr->right->getArgs()[0]->value); - if ($argType->isString()->yes()) { - $accessory = new AccessoryNonEmptyStringType(); - - if (IntegerRangeType::createAllGreaterThanOrEqualTo(2 - $offset)->isSuperTypeOf($leftType)->yes()) { - $accessory = new AccessoryNonFalsyStringType(); - } + $argType = $getType($expr->right->getArgs()[0]->value); + if ($argType->isString()->yes()) { + $accessory = new AccessoryNonEmptyStringType(); - $result = $result->unionWith($this->defaultNarrowingHelper->createForSubject($expr->right->getArgs()[0]->value, $accessory, $context, $scope)->setRootExpr($expr)); + if (IntegerRangeType::createAllGreaterThanOrEqualTo(2 - $offset)->isSuperTypeOf($leftType)->yes()) { + $accessory = new AccessoryNonFalsyStringType(); } - } - } - if ($leftType instanceof ConstantIntegerType) { - if ($expr->right instanceof Expr\PostInc) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->right->var, - IntegerRangeType::fromInterval($leftType->getValue(), null, $offset + 1), - $context, - )); - } elseif ($expr->right instanceof Expr\PostDec) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->right->var, - IntegerRangeType::fromInterval($leftType->getValue(), null, $offset - 1), - $context, - )); - } elseif ($expr->right instanceof Expr\PreInc || $expr->right instanceof Expr\PreDec) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->right->var, - IntegerRangeType::fromInterval($leftType->getValue(), null, $offset), - $context, - )); + $result = $result->unionWith($this->defaultNarrowingHelper->createForSubject($expr->right->getArgs()[0]->value, $accessory, $context, $scope)->setRootExpr($expr)); } } + } - $rightType = $getType($expr->right); - if ($rightType instanceof ConstantIntegerType) { - if ($expr->left instanceof Expr\PostInc) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->left->var, - IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset + 1), - $context, - )); - } elseif ($expr->left instanceof Expr\PostDec) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->left->var, - IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset - 1), - $context, - )); - } elseif ($expr->left instanceof Expr\PreInc || $expr->left instanceof Expr\PreDec) { - $result = $result->unionWith($this->createRangeTypes( - $expr, - $expr->left->var, - IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset), - $context, - )); - } + if ($leftType instanceof ConstantIntegerType) { + if ($expr->right instanceof Expr\PostInc) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->right->var, + IntegerRangeType::fromInterval($leftType->getValue(), null, $offset + 1), + $context, + )); + } elseif ($expr->right instanceof Expr\PostDec) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->right->var, + IntegerRangeType::fromInterval($leftType->getValue(), null, $offset - 1), + $context, + )); + } elseif ($expr->right instanceof Expr\PreInc || $expr->right instanceof Expr\PreDec) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->right->var, + IntegerRangeType::fromInterval($leftType->getValue(), null, $offset), + $context, + )); } + } - if ($context->true()) { - if (!$expr->left instanceof Scalar && !($expr->left instanceof Expr\UnaryMinus && $expr->left->expr instanceof Scalar)) { - $result = $result->unionWith( - $this->defaultNarrowingHelper->createForSubject( - $expr->left, - $orEqual ? $rightType->getSmallerOrEqualType($this->phpVersion) : $rightType->getSmallerType($this->phpVersion), - TypeSpecifierContext::createTruthy(), - $scope, - )->setRootExpr($expr), - ); - } - if (!$expr->right instanceof Scalar && !($expr->right instanceof Expr\UnaryMinus && $expr->right->expr instanceof Scalar)) { - $result = $result->unionWith( - $this->defaultNarrowingHelper->createForSubject( - $expr->right, - $orEqual ? $leftType->getGreaterOrEqualType($this->phpVersion) : $leftType->getGreaterType($this->phpVersion), - TypeSpecifierContext::createTruthy(), - $scope, - )->setRootExpr($expr), - ); - } - } elseif ($context->false()) { - if (!$expr->left instanceof Scalar && !($expr->left instanceof Expr\UnaryMinus && $expr->left->expr instanceof Scalar)) { - $result = $result->unionWith( - $this->defaultNarrowingHelper->createForSubject( - $expr->left, - $orEqual ? $rightType->getGreaterType($this->phpVersion) : $rightType->getGreaterOrEqualType($this->phpVersion), - TypeSpecifierContext::createTruthy(), - $scope, - )->setRootExpr($expr), - ); - } - if (!$expr->right instanceof Scalar && !($expr->right instanceof Expr\UnaryMinus && $expr->right->expr instanceof Scalar)) { - $result = $result->unionWith( - $this->defaultNarrowingHelper->createForSubject( - $expr->right, - $orEqual ? $leftType->getSmallerType($this->phpVersion) : $leftType->getSmallerOrEqualType($this->phpVersion), - TypeSpecifierContext::createTruthy(), - $scope, - )->setRootExpr($expr), - ); - } + $rightType = $getType($expr->right); + if ($rightType instanceof ConstantIntegerType) { + if ($expr->left instanceof Expr\PostInc) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->left->var, + IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset + 1), + $context, + )); + } elseif ($expr->left instanceof Expr\PostDec) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->left->var, + IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset - 1), + $context, + )); + } elseif ($expr->left instanceof Expr\PreInc || $expr->left instanceof Expr\PreDec) { + $result = $result->unionWith($this->createRangeTypes( + $expr, + $expr->left->var, + IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset), + $context, + )); } - - return $result; } - if ($expr instanceof BinaryOp\Greater) { - return $this->defaultNarrowingHelper->specifyTypesForNode($scope, new BinaryOp\Smaller($expr->right, $expr->left), $context)->setRootExpr($expr); + if ($context->true()) { + if (!$expr->left instanceof Scalar && !($expr->left instanceof Expr\UnaryMinus && $expr->left->expr instanceof Scalar)) { + $result = $result->unionWith( + $this->defaultNarrowingHelper->createForSubject( + $expr->left, + $orEqual ? $rightType->getSmallerOrEqualType($this->phpVersion) : $rightType->getSmallerType($this->phpVersion), + TypeSpecifierContext::createTruthy(), + $scope, + )->setRootExpr($expr), + ); + } + if (!$expr->right instanceof Scalar && !($expr->right instanceof Expr\UnaryMinus && $expr->right->expr instanceof Scalar)) { + $result = $result->unionWith( + $this->defaultNarrowingHelper->createForSubject( + $expr->right, + $orEqual ? $leftType->getGreaterOrEqualType($this->phpVersion) : $leftType->getGreaterType($this->phpVersion), + TypeSpecifierContext::createTruthy(), + $scope, + )->setRootExpr($expr), + ); + } + } elseif ($context->false()) { + if (!$expr->left instanceof Scalar && !($expr->left instanceof Expr\UnaryMinus && $expr->left->expr instanceof Scalar)) { + $result = $result->unionWith( + $this->defaultNarrowingHelper->createForSubject( + $expr->left, + $orEqual ? $rightType->getGreaterType($this->phpVersion) : $rightType->getGreaterOrEqualType($this->phpVersion), + TypeSpecifierContext::createTruthy(), + $scope, + )->setRootExpr($expr), + ); + } + if (!$expr->right instanceof Scalar && !($expr->right instanceof Expr\UnaryMinus && $expr->right->expr instanceof Scalar)) { + $result = $result->unionWith( + $this->defaultNarrowingHelper->createForSubject( + $expr->right, + $orEqual ? $leftType->getSmallerType($this->phpVersion) : $leftType->getSmallerOrEqualType($this->phpVersion), + TypeSpecifierContext::createTruthy(), + $scope, + )->setRootExpr($expr), + ); + } } - if ($expr instanceof BinaryOp\GreaterOrEqual) { - return $this->defaultNarrowingHelper->specifyTypesForNode($scope, new BinaryOp\SmallerOrEqual($expr->right, $expr->left), $context)->setRootExpr($expr); - } + return $result; + } - return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + if ($expr instanceof BinaryOp\Greater) { + return $this->defaultNarrowingHelper->specifyTypesForNode($scope, new BinaryOp\Smaller($expr->right, $expr->left), $context)->setRootExpr($expr); + } + + if ($expr instanceof BinaryOp\GreaterOrEqual) { + return $this->defaultNarrowingHelper->specifyTypesForNode($scope, new BinaryOp\SmallerOrEqual($expr->right, $expr->left), $context)->setRootExpr($expr); + } + + return $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context); + }; + + return $this->expressionResultFactory->create( + $scope, + beforeScope: $beforeScope, + expr: $expr, + variableFlow: VariableFlow::sequence($leftResult->getVariableFlow(), $rightResult->getVariableFlow(), VariableFlowBuilder::throws($expr, $throwPoints)), + hasYield: $leftResult->hasYield() || $rightResult->hasYield(), + isAlwaysTerminating: $leftResult->isAlwaysTerminating() || $rightResult->isAlwaysTerminating(), + throwPoints: $throwPoints, + impurePoints: $impurePoints, + typeCallback: $typeCallback, + specifyTypesCallback: function (TypeSpecifierContext $context, bool $nativeTypesPromoted) use ($specifyTypesCallback, $expr, $leftResult, $rightResult, $beforeScope): SpecifiedTypes { + $specifiedTypes = $specifyTypesCallback($context, $nativeTypesPromoted); + $scope = $nativeTypesPromoted ? $beforeScope->doNotTreatPhpDocTypesAsCertain() : $beforeScope; + + return $this->specifyRememberedComparisonForms($specifiedTypes, $scope, $expr, $leftResult, $rightResult, $context); }, dependencies: Dependencies::merge($leftResult->getDependencies(), $rightResult->getDependencies()), ); } + /** + * A scope can remember the verdict of an equivalent or complementary form + * of a comparison (a falsey `match (true)` arm stores `$a !== $b` as + * false) - `$a === $b` must read it back as true. + * + * @param callable(Expr): Type $getType + */ + private function resolveRememberedComparisonType(MutatingScope $scope, BinaryOp $expr, callable $getType): ?Type + { + $forms = $this->getEquivalentComparisonForms($expr, $getType); + if ($forms === null) { + return null; + } + + foreach ($forms as [$form, $isComplement]) { + if (!$scope->hasExpressionType($form)->yes()) { + continue; + } + + $formType = $scope->getTrackedExpressionType($form)->toBoolean(); + if ($formType->isTrue()->yes()) { + return new ConstantBooleanType(!$isComplement); + } + if ($formType->isFalse()->yes()) { + return new ConstantBooleanType($isComplement); + } + } + + return null; + } + + /** + * Narrowing a comparison to true or false also pins the equivalent and + * complementary forms the scope already tracks - otherwise a remembered + * `$a !== $b` (bool) would keep answering for itself after `$a === $b` + * was narrowed to false. + */ + private function specifyRememberedComparisonForms(SpecifiedTypes $specifiedTypes, MutatingScope $scope, BinaryOp $expr, ExpressionResult $leftResult, ExpressionResult $rightResult, TypeSpecifierContext $context): SpecifiedTypes + { + if ($context->true() === $context->false()) { + return $specifiedTypes; + } + + $forms = $this->getEquivalentComparisonForms( + $expr, + static fn (Expr $e): Type => ($e === $expr->left ? $leftResult : $rightResult)->getTypeOnScope($scope, $scope->nativeTypesPromoted), + ); + if ($forms === null) { + return $specifiedTypes; + } + + $exprKey = $this->exprPrinter->printExpr($expr); + $sureTypes = []; + foreach ($forms as [$form, $isComplement]) { + $formKey = $this->exprPrinter->printExpr($form); + if ($formKey === $exprKey || !$scope->hasExpressionType($form)->yes()) { + continue; + } + + $sureTypes[$formKey] = [$form, new ConstantBooleanType($context->true() !== $isComplement)]; + } + + if ($sureTypes === []) { + return $specifiedTypes; + } + + return $specifiedTypes->unionWith(new SpecifiedTypes($sureTypes, [])); + } + + /** + * The forms of a comparison that always evaluate to the same value, each + * paired with whether it is the complement (always the opposite value). + * + * @param callable(Expr): Type $getType + * @return list|null + */ + private function getEquivalentComparisonForms(BinaryOp $expr, callable $getType): ?array + { + $left = $expr->left; + $right = $expr->right; + if ($expr instanceof BinaryOp\Identical || $expr instanceof BinaryOp\NotIdentical) { + $isNegated = $expr instanceof BinaryOp\NotIdentical; + + return [ + [new BinaryOp\Identical($left, $right), $isNegated], + [new BinaryOp\Identical($right, $left), $isNegated], + [new BinaryOp\NotIdentical($left, $right), !$isNegated], + [new BinaryOp\NotIdentical($right, $left), !$isNegated], + ]; + } + + if ($expr instanceof BinaryOp\Equal || $expr instanceof BinaryOp\NotEqual) { + $isNegated = $expr instanceof BinaryOp\NotEqual; + + return [ + [new BinaryOp\Equal($left, $right), $isNegated], + [new BinaryOp\Equal($right, $left), $isNegated], + [new BinaryOp\NotEqual($left, $right), !$isNegated], + [new BinaryOp\NotEqual($right, $left), !$isNegated], + ]; + } + + if ( + !$expr instanceof BinaryOp\Smaller + && !$expr instanceof BinaryOp\SmallerOrEqual + && !$expr instanceof BinaryOp\Greater + && !$expr instanceof BinaryOp\GreaterOrEqual + ) { + return null; + } + + // normalized to `$smaller < $larger` / `$smaller <= $larger` + if ($expr instanceof BinaryOp\Smaller || $expr instanceof BinaryOp\SmallerOrEqual) { + $smaller = $left; + $larger = $right; + } else { + $smaller = $right; + $larger = $left; + } + $orEqual = $expr instanceof BinaryOp\SmallerOrEqual || $expr instanceof BinaryOp\GreaterOrEqual; + $forms = $orEqual + ? [ + [new BinaryOp\SmallerOrEqual($smaller, $larger), false], + [new BinaryOp\GreaterOrEqual($larger, $smaller), false], + ] + : [ + [new BinaryOp\Smaller($smaller, $larger), false], + [new BinaryOp\Greater($larger, $smaller), false], + ]; + + // `<` and `>=` only complement each other for comparable operands + // (NAN and uncomparable arrays/objects are neither smaller nor + // larger-or-equal) + if (!$getType($left)->isInteger()->yes() || !$getType($right)->isInteger()->yes()) { + return $forms; + } + + if ($orEqual) { + $forms[] = [new BinaryOp\Smaller($larger, $smaller), true]; + $forms[] = [new BinaryOp\Greater($smaller, $larger), true]; + } else { + $forms[] = [new BinaryOp\SmallerOrEqual($larger, $smaller), true]; + $forms[] = [new BinaryOp\GreaterOrEqual($smaller, $larger), true]; + } + + return $forms; + } + /** * The boolean result of a `==` comparison, including the same-variable * special case. Shared by the Equal and NotEqual type callbacks. diff --git a/tests/PHPStan/Analyser/nsrt/bug-10583.php b/tests/PHPStan/Analyser/nsrt/bug-10583.php new file mode 100644 index 00000000000..b6913ec020d --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-10583.php @@ -0,0 +1,51 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug10583Types; + +use function PHPStan\Testing\assertType; + +function identical(int $bar, int $baz): void +{ + match (true) { + $bar !== $baz => 2, + default => assertType('array{true, false, true, false}', [$bar === $baz, $bar !== $baz, $baz === $bar, $baz !== $bar]), + }; + match (true) { + $bar === $baz => 2, + default => assertType('array{false, true, false, true}', [$bar === $baz, $bar !== $baz, $baz === $bar, $baz !== $bar]), + }; +} + +function equal(string $bar, string $baz): void +{ + match (true) { + $bar != $baz => 2, + default => assertType('array{true, false, true, false}', [$bar == $baz, $bar != $baz, $baz == $bar, $baz != $bar]), + }; +} + +function relational(int $bar, int $baz, float $f, float $g): void +{ + match (true) { + $bar < $baz => 2, + default => assertType('array{false, false, true, true}', [$bar < $baz, $baz > $bar, $bar >= $baz, $baz <= $bar]), + }; + match (true) { + $bar <= $baz => 2, + default => assertType('array{false, false, true, true}', [$bar <= $baz, $baz >= $bar, $bar > $baz, $baz < $bar]), + }; + match (true) { + $f < $g => 2, + default => assertType('array{false, false, bool}', [$f < $g, $g > $f, $f >= $g]), + }; +} + +function reassigned(int $bar, int $baz): void +{ + match (true) { + $bar !== $baz => 2, + default => assertType('bool', ($bar = $bar + 1) === $baz), + }; +} diff --git a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php index 600a95e868d..c58aa7710dc 100644 --- a/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/MatchExpressionRuleTest.php @@ -590,4 +590,15 @@ public function testMatchArmComparisonNotSuppressedByImpossibleCheck(): void ]); } + #[RequiresPhp('>= 8.0.0')] + public function testBug10583(): void + { + $this->analyse([__DIR__ . '/data/bug-10583.php'], [ + [ + 'Match expression does not handle remaining value: true', + 65, + ], + ]); + } + } diff --git a/tests/PHPStan/Rules/Comparison/data/bug-10583.php b/tests/PHPStan/Rules/Comparison/data/bug-10583.php new file mode 100644 index 00000000000..7359e803f79 --- /dev/null +++ b/tests/PHPStan/Rules/Comparison/data/bug-10583.php @@ -0,0 +1,77 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug10583; + +function foo(int $bar, int $baz): int +{ + return match (true) { + $bar === $baz => 1, + $bar !== $baz => 2, + }; +} + +function foo2(int $bar, int $baz): int +{ + return match (true) { + $bar !== $baz => 2, + $bar === $baz => 1, + }; +} + +function swapped(int $bar, int $baz): int +{ + return match (true) { + $bar !== $baz => 2, + $baz === $bar => 1, + }; +} + +function looseEqual(string $bar, string $baz): int +{ + return match (true) { + $bar != $baz => 2, + $bar == $baz => 1, + }; +} + +function looseEqualSwapped(string $bar, string $baz): int +{ + return match (true) { + $bar == $baz => 2, + $baz != $bar => 1, + }; +} + +function smaller(int $bar, int $baz): int +{ + return match (true) { + $bar < $baz => 2, + $baz <= $bar => 1, + }; +} + +function greater(int $bar, int $baz): int +{ + return match (true) { + $bar > $baz => 2, + $bar <= $baz => 1, + }; +} + +function floats(float $bar, float $baz): int +{ + return match (true) { + $bar < $baz => 2, + $bar >= $baz => 1, + }; +} + +function matchFalse(int $bar, int $baz): int +{ + return match (false) { + $bar === $baz => 1, + $bar !== $baz => 2, + }; +} diff --git a/turbo-ext/src/BinaryOpHandler.cpp b/turbo-ext/src/BinaryOpHandler.cpp index 06f10c60b7d..5a2549e48c1 100644 --- a/turbo-ext/src/BinaryOpHandler.cpp +++ b/turbo-ext/src/BinaryOpHandler.cpp @@ -653,6 +653,166 @@ class BinaryOpHandler return typeResultType(typeResult.raw()); } + /* one entry of getEquivalentComparisonForms(): the form node and whether + * it is the complement */ + struct ComparisonForm + { + zv::Val node; + bool isComplement = false; + }; + + static constexpr int COMPARISON_FORMS_LIMIT = 6; + + /* Mirrors getEquivalentComparisonForms(); count = -1 for a non-comparison + * ($forms === null). false = pending exception */ + template + static bool equivalentComparisonForms(int kind, zval *left, zval *right, GetType &&getType, ComparisonForm (&forms)[COMPARISON_FORMS_LIMIT], int &count) + { + count = 0; + auto add = [&](int classIdx, zval *formLeft, zval *formRight, bool isComplement) -> bool { + zv::Args argv{formLeft, formRight}; + zv::Val node = pt_type_new(classIdx, 2, argv); + if (UNEXPECTED(node.isUndef())) return false; + forms[count].node = std::move(node); + forms[count].isComplement = isComplement; + count++; + return true; + }; + + if (kind == KIND_IDENTICAL || kind == KIND_NOT_IDENTICAL || kind == KIND_EQUAL || kind == KIND_NOT_EQUAL) { + bool identity = kind == KIND_IDENTICAL || kind == KIND_NOT_IDENTICAL; + bool isNegated = kind == KIND_NOT_IDENTICAL || kind == KIND_NOT_EQUAL; + int sameClass = identity ? PT_CLASS_IDENTICAL_EXPR : PT_CLASS_EQUAL_EXPR; + int negatedClass = identity ? PT_CLASS_BINARY_OP_NOT_IDENTICAL : PT_CLASS_NOT_EQUAL_EXPR; + return add(sameClass, left, right, isNegated) + && add(sameClass, right, left, isNegated) + && add(negatedClass, left, right, !isNegated) + && add(negatedClass, right, left, !isNegated); + } + + if (kind != KIND_SMALLER && kind != KIND_SMALLER_OR_EQUAL && kind != KIND_GREATER && kind != KIND_GREATER_OR_EQUAL) { + count = -1; + return true; + } + + // normalized to `$smaller < $larger` / `$smaller <= $larger` + bool smallerFirst = kind == KIND_SMALLER || kind == KIND_SMALLER_OR_EQUAL; + zval *smaller = smallerFirst ? left : right; + zval *larger = smallerFirst ? right : left; + bool orEqual = kind == KIND_SMALLER_OR_EQUAL || kind == KIND_GREATER_OR_EQUAL; + if (orEqual) { + if (!add(PT_CLASS_SMALLER_OR_EQUAL_EXPR, smaller, larger, false) || !add(PT_CLASS_GREATER_OR_EQUAL_EXPR, larger, smaller, false)) return false; + } else { + if (!add(PT_CLASS_SMALLER_EXPR, smaller, larger, false) || !add(PT_CLASS_GREATER_EXPR, larger, smaller, false)) return false; + } + + // `<` and `>=` only complement each other for comparable operands + zv::Val leftType = getType(left); + if (UNEXPECTED(leftType.isUndef())) return false; + int leftIsInteger = trinaryYes(leftType.raw(), PT_OP_IS_INTEGER); + if (UNEXPECTED(leftIsInteger < 0)) return false; + if (!leftIsInteger) return true; + zv::Val rightType = getType(right); + if (UNEXPECTED(rightType.isUndef())) return false; + int rightIsInteger = trinaryYes(rightType.raw(), PT_OP_IS_INTEGER); + if (UNEXPECTED(rightIsInteger < 0)) return false; + if (!rightIsInteger) return true; + + if (orEqual) { + return add(PT_CLASS_SMALLER_EXPR, larger, smaller, true) && add(PT_CLASS_GREATER_EXPR, smaller, larger, true); + } + + return add(PT_CLASS_SMALLER_OR_EQUAL_EXPR, larger, smaller, true) && add(PT_CLASS_GREATER_OR_EQUAL_EXPR, smaller, larger, true); + } + + /* Mirrors resolveRememberedComparisonType(); found = false for null */ + template + zv::Val resolveRememberedComparisonType(zval *scope, int kind, zval *left, zval *right, GetType &&getType, bool &found) const + { + found = false; + ComparisonForm forms[COMPARISON_FORMS_LIMIT]; + int count; + if (UNEXPECTED(!equivalentComparisonForms(kind, left, right, getType, forms, count))) return zv::Val(); + for (int i = 0; i < count; i++) { + zend_long has = pt_mutating_scope_has_expression_type(Z_OBJ_P(scope), forms[i].node.raw()); + if (UNEXPECTED(has < 0)) return zv::Val(); + if (has != PT_TRI_YES) continue; + + zv::Val formType = pt_mutating_scope_get_tracked_expression_type(Z_OBJ_P(scope), Z_OBJ_P(forms[i].node.raw())); + if (UNEXPECTED(formType.isUndef())) return zv::Val(); + ptoh::BooleanOf formBoolean; + if (UNEXPECTED(!formBoolean.init(formType.raw()))) return zv::Val(); + int isTrue = formBoolean.isTrue(); + if (UNEXPECTED(isTrue < 0)) return zv::Val(); + if (isTrue) { + found = true; + return ptoh::constantBoolean(!forms[i].isComplement); + } + int isFalse = formBoolean.isFalse(); + if (UNEXPECTED(isFalse < 0)) return zv::Val(); + if (isFalse) { + found = true; + return ptoh::constantBoolean(forms[i].isComplement); + } + } + + return zv::Val(); + } + + /* Mirrors specifyRememberedComparisonForms() */ + zv::Val specifyRememberedComparisonForms(zv::Val specifiedTypes, zval *scope, zval *expr, zval *leftResult, zval *rightResult, zval *context) const + { + bool contextTrue, contextFalse; + if (UNEXPECTED(!pt_type_specifier_context_true(Z_OBJ_P(context), contextTrue))) return zv::Val(); + if (UNEXPECTED(!pt_type_specifier_context_false(Z_OBJ_P(context), contextFalse))) return zv::Val(); + if (contextTrue == contextFalse) return specifiedTypes; + + int kind = kindOf(expr); + if (UNEXPECTED(kind < 0)) return zv::Val(); + zval *left = ptoh::binaryOpLeft(expr); + if (UNEXPECTED(left == NULL)) return zv::Val(); + zval *right = ptoh::binaryOpRight(expr); + if (UNEXPECTED(right == NULL)) return zv::Val(); + auto getType = [&](zval *e) -> zv::Val { + bool scopeNativeTypesPromoted; + if (UNEXPECTED(!pt_mutating_scope_native_types_promoted(Z_OBJ_P(scope), scopeNativeTypesPromoted))) return zv::Val(); + return pt_expression_result_get_type_on_scope(e == left ? leftResult : rightResult, scope, scopeNativeTypesPromoted); + }; + ComparisonForm forms[COMPARISON_FORMS_LIMIT]; + int count; + if (UNEXPECTED(!equivalentComparisonForms(kind, left, right, getType, forms, count))) return zv::Val(); + if (count < 0) return specifiedTypes; + + zval *exprPrinter = OBJ_PROP_NUM(self, slots::exprPrinter); + zend_string *exprKey = pt_expr_printer_print(exprPrinter, Z_OBJ_P(expr)); + if (UNEXPECTED(exprKey == NULL)) return zv::Val(); + zv::Str exprKeyHold = zv::Str::adopt(exprKey); + zv::Arr sureTypes = zv::Arr::create(COMPARISON_FORMS_LIMIT); + bool hasSureTypes = false; + for (int i = 0; i < count; i++) { + zend_string *formKey = pt_expr_printer_print(exprPrinter, Z_OBJ_P(forms[i].node.raw())); + if (UNEXPECTED(formKey == NULL)) return zv::Val(); + zv::Str formKeyHold = zv::Str::adopt(formKey); + if (zend_string_equals(formKey, exprKey)) continue; + zend_long has = pt_mutating_scope_has_expression_type(Z_OBJ_P(scope), forms[i].node.raw()); + if (UNEXPECTED(has < 0)) return zv::Val(); + if (has != PT_TRI_YES) continue; + + zv::Arr entry = zv::Arr::create(2); + entry.push(zv::Ref(forms[i].node.raw())); + entry.push(ptoh::constantBoolean(contextTrue != forms[i].isComplement)); + sureTypes.set(formKey, zv::Val(std::move(entry))); + hasSureTypes = true; + } + + if (!hasSureTypes) return specifiedTypes; + + zv::Val sureTypesHold = zv::Val(std::move(sureTypes)); + zv::Val formTypes = pt_specified_types_new(sureTypesHold.raw(), NULL); + if (UNEXPECTED(formTypes.isUndef())) return zv::Val(); + return pt_specified_types_union_with(Z_OBJ_P(specifiedTypes.raw()), formTypes.raw()); + } + /* Mirrors createRangeTypes(); $rootExpr NULL or IS_NULL for null */ zv::Val createRangeTypes(zval *rootExpr, zval *expr, zval *type, zval *context) const { @@ -899,6 +1059,12 @@ class BinaryOpHandler if (UNEXPECTED(left == NULL)) return zv::Val(); zval *right = ptoh::binaryOpRight(expr); if (UNEXPECTED(right == NULL)) return zv::Val(); + { + bool found; + zv::Val rememberedType = resolveRememberedComparisonType(scope, kind, left, right, getType, found); + if (UNEXPECTED(EG(exception) != NULL)) return zv::Val(); + if (found) return rememberedType; + } switch (kind) { case KIND_SMALLER: case KIND_SMALLER_OR_EQUAL: @@ -1036,7 +1202,18 @@ class BinaryOpHandler static void specifyTypesCallbackBody(zval *captures, uint32_t argc, zval *argv, zval *return_value) { if (UNEXPECTED(!ptoh::requireArgs(argc, 2, closureName))) return; - zv::Val specifiedTypes = BinaryOpHandler(Z_OBJ(captures[0])).specifyTypes(captures, &argv[0], zend_is_true(&argv[1])); + BinaryOpHandler handler(Z_OBJ(captures[0])); + bool nativeTypesPromoted = zend_is_true(&argv[1]); + zv::Val specifiedTypes = handler.specifyTypes(captures, &argv[0], nativeTypesPromoted); + if (UNEXPECTED(specifiedTypes.isUndef())) return; + zv::Val scopeHold; + zval *scope = &captures[5]; + if (nativeTypesPromoted) { + scopeHold = pt_mutating_scope_do_not_treat_phpdoc_types_as_certain(Z_OBJ_P(scope)); + if (UNEXPECTED(scopeHold.isUndef())) return; + scope = scopeHold.raw(); + } + specifiedTypes = handler.specifyRememberedComparisonForms(std::move(specifiedTypes), scope, &captures[1], &captures[2], &captures[3], &argv[0]); if (UNEXPECTED(specifiedTypes.isUndef())) return; specifiedTypes.intoReturnValue(return_value); } @@ -1748,6 +1925,60 @@ PT_MINIT_REGISTRATION(pt_register_binary_op_handler) PT_RETURN_VAL(BinaryOpHandler(Z_OBJ_P(ZEND_THIS)).processExpr(nodeScopeResolver, stmt, expr, scope, storage, nodeCallback, context)); }); + cls.method(sigs::resolveRememberedComparisonType, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *scope, *expr, *getTypeCallable; + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_OBJECT(scope) + Z_PARAM_OBJECT(expr) + Z_PARAM_ZVAL(getTypeCallable) + ZEND_PARSE_PARAMETERS_END(); + int kind = kindOf(expr); + if (UNEXPECTED(kind < 0)) RETURN_THROWS(); + zval *left = ptoh::binaryOpLeft(expr); + if (UNEXPECTED(left == NULL)) RETURN_THROWS(); + zval *right = ptoh::binaryOpRight(expr); + if (UNEXPECTED(right == NULL)) RETURN_THROWS(); + auto getType = [&](zval *e) { return pt_type_call_callable(getTypeCallable, 1, e); }; + bool found; + zv::Val type = BinaryOpHandler(Z_OBJ_P(ZEND_THIS)).resolveRememberedComparisonType(scope, kind, left, right, getType, found); + if (UNEXPECTED(EG(exception) != NULL)) RETURN_THROWS(); + if (!found) RETURN_NULL(); + PT_RETURN_VAL(std::move(type)); + }); + + cls.method(sigs::specifyRememberedComparisonForms, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *specifiedTypes, *scope, *expr, *leftResult, *rightResult, *context; + if (!zp::parse(execute_data, specifiedTypes, scope, expr, leftResult, rightResult, context)) RETURN_THROWS(); + PT_RETURN_VAL(BinaryOpHandler(Z_OBJ_P(ZEND_THIS)).specifyRememberedComparisonForms(zv::Val::copyOf(zv::Ref(specifiedTypes)), scope, expr, leftResult, rightResult, context)); + }); + + cls.method(sigs::getEquivalentComparisonForms, [](INTERNAL_FUNCTION_PARAMETERS) { + zval *expr, *getTypeCallable; + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_OBJECT(expr) + Z_PARAM_ZVAL(getTypeCallable) + ZEND_PARSE_PARAMETERS_END(); + int kind = kindOf(expr); + if (UNEXPECTED(kind < 0)) RETURN_THROWS(); + zval *left = ptoh::binaryOpLeft(expr); + if (UNEXPECTED(left == NULL)) RETURN_THROWS(); + zval *right = ptoh::binaryOpRight(expr); + if (UNEXPECTED(right == NULL)) RETURN_THROWS(); + auto getType = [&](zval *e) { return pt_type_call_callable(getTypeCallable, 1, e); }; + BinaryOpHandler::ComparisonForm forms[BinaryOpHandler::COMPARISON_FORMS_LIMIT]; + int count; + if (UNEXPECTED(!BinaryOpHandler::equivalentComparisonForms(kind, left, right, getType, forms, count))) RETURN_THROWS(); + if (count < 0) RETURN_NULL(); + zv::Arr list = zv::Arr::create((uint32_t) count); + for (int i = 0; i < count; i++) { + zv::Arr pair = zv::Arr::create(2); + pair.push(std::move(forms[i].node)); + pair.push(zv::Val::boolean(forms[i].isComplement)); + list.push(zv::Val(std::move(pair))); + } + PT_RETURN_VAL(zv::Val(std::move(list))); + }); + cls.method(sigs::resolveEqualType, [](INTERNAL_FUNCTION_PARAMETERS) { zval *scope, *expr, *leftResult, *rightResult; if (!zp::parse(execute_data, scope, expr, leftResult, rightResult)) RETURN_THROWS(); diff --git a/turbo-ext/src/generated/BinaryOpHandler.h b/turbo-ext/src/generated/BinaryOpHandler.h index 98e3c77b013..fc6feb1a7dc 100644 --- a/turbo-ext/src/generated/BinaryOpHandler.h +++ b/turbo-ext/src/generated/BinaryOpHandler.h @@ -80,16 +80,22 @@ inline constexpr char strings[] = "PHPStan\\Analyser\\ExpressionContext\0" /* 854 */ "PHPStan\\Analyser\\ExpressionResult\0" /* 889 */ "processExpr\0" /* 923 */ - "PhpParser\\Node\\Expr\\BinaryOp\\Equal\0" /* 935 */ - "leftResult\0" /* 970 */ - "rightResult\0" /* 981 */ - "PHPStan\\Type\\Type\0" /* 993 */ - "resolveEqualType\0" /* 1011 */ - "rootExpr\0" /* 1028 */ - "type\0" /* 1037 */ - "PHPStan\\Analyser\\TypeSpecifierContext\0" /* 1042 */ - "PHPStan\\Analyser\\SpecifiedTypes\0" /* 1080 */ - "createRangeTypes"; /* 1112 */ + "PhpParser\\Node\\Expr\\BinaryOp\0" /* 935 */ + "getType\0" /* 964 */ + "PHPStan\\Type\\Type\0" /* 972 */ + "resolveRememberedComparisonType\0" /* 990 */ + "specifiedTypes\0" /* 1022 */ + "PHPStan\\Analyser\\SpecifiedTypes\0" /* 1037 */ + "leftResult\0" /* 1069 */ + "rightResult\0" /* 1080 */ + "PHPStan\\Analyser\\TypeSpecifierContext\0" /* 1092 */ + "specifyRememberedComparisonForms\0" /* 1130 */ + "getEquivalentComparisonForms\0" /* 1163 */ + "PhpParser\\Node\\Expr\\BinaryOp\\Equal\0" /* 1192 */ + "resolveEqualType\0" /* 1227 */ + "rootExpr\0" /* 1244 */ + "type\0" /* 1253 */ + "createRangeTypes"; /* 1258 */ inline constexpr reg::PackedArg args[] = { reg::packed(0, 0, 28), /* __construct $initializerExprTypeResolver */ reg::packed(75, 0, 100), /* __construct $richerScopeGetTypeHelper */ @@ -110,16 +116,30 @@ inline constexpr reg::PackedArg args[] = { reg::packed(833, MAY_BE_CALLABLE), /* processExpr $nodeCallback */ reg::packed(846, 0, 854), /* processExpr $context */ reg::packed(659, 0, 889), /* processExpr return */ + reg::packed(747, 0, 753), /* resolveRememberedComparisonType $scope */ + reg::packed(634, 0, 935), /* resolveRememberedComparisonType $expr */ + reg::packed(964, MAY_BE_CALLABLE), /* resolveRememberedComparisonType $getType */ + reg::packed(659, MAY_BE_NULL, 972), /* resolveRememberedComparisonType return */ + reg::packed(1022, 0, 1037), /* specifyRememberedComparisonForms $specifiedTypes */ + reg::packed(747, 0, 753), /* specifyRememberedComparisonForms $scope */ + reg::packed(634, 0, 935), /* specifyRememberedComparisonForms $expr */ + reg::packed(1069, 0, 889), /* specifyRememberedComparisonForms $leftResult */ + reg::packed(1080, 0, 889), /* specifyRememberedComparisonForms $rightResult */ + reg::packed(846, 0, 1092), /* specifyRememberedComparisonForms $context */ + reg::packed(659, 0, 1037), /* specifyRememberedComparisonForms return */ + reg::packed(634, 0, 935), /* getEquivalentComparisonForms $expr */ + reg::packed(964, MAY_BE_CALLABLE), /* getEquivalentComparisonForms $getType */ + reg::packed(659, MAY_BE_NULL | MAY_BE_ARRAY), /* getEquivalentComparisonForms return */ reg::packed(747, 0, 753), /* resolveEqualType $scope */ - reg::packed(634, 0, 935), /* resolveEqualType $expr */ - reg::packed(970, 0, 889), /* resolveEqualType $leftResult */ - reg::packed(981, 0, 889), /* resolveEqualType $rightResult */ - reg::packed(659, 0, 993), /* resolveEqualType return */ - reg::packed(1028, MAY_BE_NULL, 639), /* createRangeTypes $rootExpr */ + reg::packed(634, 0, 1192), /* resolveEqualType $expr */ + reg::packed(1069, 0, 889), /* resolveEqualType $leftResult */ + reg::packed(1080, 0, 889), /* resolveEqualType $rightResult */ + reg::packed(659, 0, 972), /* resolveEqualType return */ + reg::packed(1244, MAY_BE_NULL, 639), /* createRangeTypes $rootExpr */ reg::packed(634, 0, 639), /* createRangeTypes $expr */ - reg::packed(1037, 0, 993), /* createRangeTypes $type */ - reg::packed(846, 0, 1042), /* createRangeTypes $context */ - reg::packed(659, 0, 1080), /* createRangeTypes return */ + reg::packed(1253, 0, 972), /* createRangeTypes $type */ + reg::packed(846, 0, 1092), /* createRangeTypes $context */ + reg::packed(659, 0, 1037), /* createRangeTypes return */ }; using Sig = reg::Sig; } // namespace sigtab @@ -129,8 +149,11 @@ namespace sig { inline constexpr sigtab::Sig __construct = { { 622 /* __construct */, 9, 0, 9, reg::NoArg, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig supports = { { 660 /* supports */, 1, 9, 1, 10, ZEND_ACC_PUBLIC } }; inline constexpr sigtab::Sig processExpr = { { 923 /* processExpr */, 7, 11, 7, 18, ZEND_ACC_PUBLIC } }; -inline constexpr sigtab::Sig resolveEqualType = { { 1011 /* resolveEqualType */, 4, 19, 4, 23, ZEND_ACC_PRIVATE } }; -inline constexpr sigtab::Sig createRangeTypes = { { 1112 /* createRangeTypes */, 4, 24, 4, 28, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveRememberedComparisonType = { { 990 /* resolveRememberedComparisonType */, 3, 19, 3, 22, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig specifyRememberedComparisonForms = { { 1130 /* specifyRememberedComparisonForms */, 6, 23, 6, 29, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig getEquivalentComparisonForms = { { 1163 /* getEquivalentComparisonForms */, 2, 30, 2, 32, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig resolveEqualType = { { 1227 /* resolveEqualType */, 4, 33, 4, 37, ZEND_ACC_PRIVATE } }; +inline constexpr sigtab::Sig createRangeTypes = { { 1258 /* createRangeTypes */, 4, 38, 4, 42, ZEND_ACC_PRIVATE } }; } // namespace sig } // namespace ptdecl::BinaryOpHandler