Skip to content

RoundFunctionReturnTypeExtension supports types with ConstantScalar - #3349

Open
ShioPy0101 wants to merge 26 commits into
phpstan:2.3.xfrom
ShioPy0101:feature/add-type-round-function
Open

ShioPy0101 wants to merge 26 commits into
phpstan:2.3.xfrom
ShioPy0101:feature/add-type-round-function

Conversation

@ShioPy0101

@ShioPy0101 ShioPy0101 commented Aug 26, 2024

Copy link
Copy Markdown

Returns a constant type when the $num argument and the relevant options of round(), ceil(), or floor() can be resolved to constant values.

Constant numeric strings are also evaluated in non-strict mode.

round() also supports the RoundingMode enum cases corresponding to the legacy PHP_ROUND_HALF_* constants.

The default return type is used when constant evaluation is not possible, for example when:

  • the precision is not a single constant integer;
  • the rounding mode is not a single supported constant;
  • the input contains unsupported constant values.

@ShioPy0101 ShioPy0101 changed the title ReturnFunctionReturnTypeExtension supports types with ConstantScalar RoundFunctionReturnTypeExtension supports types with ConstantScalar Aug 26, 2024
@ShioPy0101
ShioPy0101 force-pushed the feature/add-type-round-function branch from 465733b to 276f471 Compare August 26, 2024 05:47
@staabm

staabm commented Aug 26, 2024

Copy link
Copy Markdown
Contributor

which problem does this change solve? whats your real world use-case?

Comment thread src/Type/Php/RoundFunctionReturnTypeExtension.php Outdated
@zonuexe

zonuexe commented Aug 26, 2024

Copy link
Copy Markdown
Contributor

@staabm

I'm sure there are several use cases for this, but one I quickly spotted in my private code is the following pattern, which uses $last_page for some logic other than just printing it.

Ideally it would be even more useful if a float range type was implemented, but I feel like this is worthwhile enough on its own.

const MAX = 1000;
const PER_PAGE = 30;
$last_page = (int)floor(MAX / PER_PAGE);

https://phpstan.org/r/b583dc30-3b4e-4b1f-af60-713159f43309

@ShioPy0101
ShioPy0101 force-pushed the feature/add-type-round-function branch from 98e7404 to e4ad097 Compare August 28, 2024 01:08
@ShioPy0101
ShioPy0101 changed the base branch from 1.11.x to 1.12.x August 28, 2024 01:09
@VincentLanglet

Copy link
Copy Markdown
Contributor

Hi @ShioPy0101, are you still interested by this PR ? Could you rebase and target 2.3.x branch ?

@ShioPy0101
ShioPy0101 force-pushed the feature/add-type-round-function branch from e4ad097 to 143a1c6 Compare September 20, 2026 09:40
@ShioPy0101
ShioPy0101 changed the base branch from 1.12.x to 2.3.x September 20, 2026 09:41

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

octoscan found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

zizmor found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@ShioPy0101
ShioPy0101 force-pushed the feature/add-type-round-function branch from 143a1c6 to 77a4c85 Compare September 20, 2026 09:49
@ShioPy0101

Copy link
Copy Markdown
Author

Hi @VincentLanglet, I’ve rebased the PR onto 2.3.x and updated the target branch. Could you take another look when you have a chance?

Thank you for bringing this PR back to my attention.

Comment on lines +171 to +172
$returnValueTypes = [];
break;

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.

You could return null directly no ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, you're right. I'll simplify this to return null directly.

$returnValueTypes = [];

foreach ($constantScalarValues as $constantScalarValue) {
if (!is_int($constantScalarValue) && !is_float($constantScalarValue)) {

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.

Why this check ?

I saw in test you have the assertion

assertType('float', round($IntOrNumStr));

but can't we also understand '2.5' ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thank you for pointing this out. I agree that constant numeric strings can also be handled in non-strict mode. I’ll update the implementation and tests while preserving the strict-types behavior.

$modeType = $scope->getType($modeArg);
$mode = $modeType->getConstantScalarValues();

if (count($mode) === 1 && in_array($mode[0], [PHP_ROUND_HALF_UP, PHP_ROUND_HALF_DOWN, PHP_ROUND_HALF_EVEN, PHP_ROUND_HALF_ODD], true)) {

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for pointing it out — I’ll update the implementation for the current PHP versions.

@VincentLanglet VincentLanglet left a comment

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.

Seems good otherwise

return null;
}

switch ($enumCase->getEnumCaseName()) {

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.

Why this mapping ?

This loose some case.

assertType('float', round(9.5, 0, \RoundingMode::TowardsZero));
	assertType('float', round(9.5, 0, \RoundingMode::AwayFromZero));
	assertType('float', round(9.5, 0, \RoundingMode::NegativeInfinity));
	assertType('float', round(9.5, 0, \RoundingMode::PositiveInfinity));

You could keep the original value when the Enum exists no ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I intentionally limited the mapping to the four RoundingMode cases that have direct equivalents in the legacy PHP_ROUND_HALF_* constants. The intent was to avoid making constant evaluation depend on the runtime availability of the RoundingMode enum.

For the remaining cases, I currently fall back to the existing return-type logic because they do not have a direct legacy constant mapping.

That said, I agree that this is an implementation limitation rather than a limitation of RoundingMode itself. Preserving the enum case internally would allow all cases to be evaluated more precisely, so I think that would be a better direction.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I am still considering the best approach here. The README states that PHPStan's source code is developed on PHP 8.2, while the distributed package and PHAR are transformed to run on PHP 7.2 and higher. Given that, I would prefer to avoid making constant evaluation depend on the runtime availability of RoundingMode.

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.

Any suggestion @staabm ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants