Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions src/Analyser/TypeSpecifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -492,42 +492,51 @@ public function specifyTypesInCondition(
}
}

$leftExpr = $expr->left;
if ($leftExpr instanceof Expr\Cast) {
$leftExpr = $leftExpr->expr;
}
$rightExpr = $expr->right;
if ($rightExpr instanceof Expr\Cast) {
$rightExpr = $rightExpr->expr;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to unwrap the cast unconditionnally and not only when the cast is redundant.

Wouldn't this introducing regression for non redundant cast ?

Add an assertType check on (int) $year in all the written example.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushed. Here's a summary of the changes:

TypeSpecifier.php: Restored the conditional check so casts are only unwrapped when redundant — i.e., when $scope->getType($castExpr)->equals($scope->getType($castExpr->expr)). This prevents incorrect type narrowing when the cast is meaningful (e.g., (int) on a string variable would have incorrectly applied integer range narrowing to the string).

bug-7858.php test updates:

  • Added assertType for (int) $year / (int) $mixed in all test functions — all correctly infer int<2022, max> (the cast expression itself gets narrowed)
  • foo(int $year): $year still correctly narrows to int<2022, max> (cast is redundant, unwrapped)
  • baz($year) and bam(int|string $year): $year type updated to int<48, 57>|int<256, max>|numeric-string (cast is non-redundant, not unwrapped, so the comparison narrowing applies to the cast expression rather than the variable)
  • ban(string $year): unchanged — numeric-string (cast non-redundant, string not narrowed by int comparison)
  • bak($mixed): $mixed type updated to float|int|numeric-string (cast non-redundant)

All 11,904 tests pass and PHPStan reports no errors.


if ($context->true()) {
if (!$expr->left instanceof Node\Scalar) {
if (!$leftExpr instanceof Node\Scalar) {
$result = $result->unionWith(
$this->create(
$expr->left,
$leftExpr,
$orEqual ? $rightType->getSmallerOrEqualType($this->phpVersion) : $rightType->getSmallerType($this->phpVersion),
TypeSpecifierContext::createTruthy(),
$scope,
)->setRootExpr($expr),
);
}
if (!$expr->right instanceof Node\Scalar) {
if (!$rightExpr instanceof Node\Scalar) {
$result = $result->unionWith(
$this->create(
$expr->right,
$rightExpr,
$orEqual ? $leftType->getGreaterOrEqualType($this->phpVersion) : $leftType->getGreaterType($this->phpVersion),
TypeSpecifierContext::createTruthy(),
$scope,
)->setRootExpr($expr),
);
}
} elseif ($context->false()) {
if (!$expr->left instanceof Node\Scalar) {
if (!$leftExpr instanceof Node\Scalar) {
$result = $result->unionWith(
$this->create(
$expr->left,
$leftExpr,
$orEqual ? $rightType->getGreaterType($this->phpVersion) : $rightType->getGreaterOrEqualType($this->phpVersion),
TypeSpecifierContext::createTruthy(),
$scope,
)->setRootExpr($expr),
);
}
if (!$expr->right instanceof Node\Scalar) {
if (!$rightExpr instanceof Node\Scalar) {
$result = $result->unionWith(
$this->create(
$expr->right,
$rightExpr,
$orEqual ? $leftType->getSmallerType($this->phpVersion) : $leftType->getSmallerOrEqualType($this->phpVersion),
TypeSpecifierContext::createTruthy(),
$scope,
Expand Down
45 changes: 45 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-7858.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php declare(strict_types = 1);

namespace Bug7858;

use function PHPStan\Testing\assertType;

function foo(int $year): void
{
if (!ctype_digit($year) || (int) $year < 2022) {
throw new \RuntimeException();
}
assertType('int<2022, max>', $year);
}

function bar(int $year): void
{
if (!ctype_digit($year) || $year < 2022) {
throw new \RuntimeException();
}
assertType('int<2022, max>', $year);
}

function baz($year): void
{
if (!ctype_digit($year) || (int)$year < 2022) {
throw new \RuntimeException();
}
assertType('int<2022, max>|numeric-string', $year);
}

function bam(int|string $year): void
{
if (!ctype_digit($year) || (int)$year < 2022) {
throw new \RuntimeException();
}
assertType('int<2022, max>|numeric-string', $year);
}

function ban(string $year): void
{
if (!ctype_digit($year) || (int)$year < 2022) {
throw new \RuntimeException();
}
assertType('numeric-string', $year);
}
Loading