Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add basic type narrowing in the loose equality with the empty string #3304

Open
wants to merge 2 commits into
base: 1.11.x
Choose a base branch
from

Conversation

thg2k
Copy link
Contributor

@thg2k thg2k commented Aug 8, 2024

As promised a long time ago, I submit my first real attempt to narrow the type after if ($a != "") ...

This is by no means a complete analysis of the loose equality operator, but it should be sound. The discrepancy between php 7.x and 8.x are handled by preserving the two offending types (integer and float zero) in the type for both the true and false context.

I have the feeling that the definitive solution would be to replace Type::looseCompare() with Type::looseIntersectWith(), which should solve all the loose comparison problems. Do you think it would be something worth pursuing?

If in the meanwhile you think this PR is acceptable, it would solve a lot of problems in my codebase.

@phpstan-bot
Copy link
Collaborator

You've opened the pull request against the latest branch 1.12.x. If your code is relevant on 1.11.x and you want it to be released sooner, please rebase your pull request and change its target to 1.11.x.

@thg2k thg2k changed the base branch from 1.12.x to 1.11.x August 8, 2024 11:51
@ondrejmirtes
Copy link
Member

ondrejmirtes commented Aug 8, 2024

We need to solve two things in regard to == and !=.

  1. Whether the result of the comparison is true or false. This is currently achieved with Type::looseCompare().
  2. How types are narrowed in == and != conditions.

I'm in favour of adding a new method (or methods) on Type that will solve both of these needs at once.

I'd like the implementations to easily follow the loose comparison table from https://www.php.net/manual/en/types.comparisons.php.

The new method on Type could return a union of all types it loosely compares to with true. If we take ConstantStringType with '-1', the method should return a union type of:

  • true
  • -1
  • -1 ($this)

To resolve the result of == and !=, we could run $leftType->isSuperTypeOf($rightType->looseComparison()).

For type narrowing, we can run TypeCombinator::intersect($leftType->looseComparison(), $rightType).

I'm not saying this will work for all cases, maybe we'll have to adjust, but it's a start.

@thg2k
Copy link
Contributor Author

thg2k commented Aug 8, 2024

I'm a bit lost on this approach, but I'd be happy to investigate it. Should I open a dedicated issue for it? This PR is about getting one subset of the problem handled.

@ondrejmirtes
Copy link
Member

I'm sorry, I don't want a narrow partial solution if we can do a proper one.

@thg2k
Copy link
Contributor Author

thg2k commented Aug 8, 2024

I still don't understand the approach of returning the types, are you sure it shouldn't be an looseIntersectWith($type, $version)? How would you tackle things that don't involve basic constants, like '15foo' == 15 (true < 8.0, false >= 8.0)?

Anyway I managed to convert it to a plugin by leveraging the nette DI, I had to override also the TypeSpecifierFactory, as there is an explicit reference to the class name there.

@thg2k
Copy link
Contributor Author

thg2k commented Aug 9, 2024

Another example that comes to mind is: if ($a == 15) ...

/** @var mixed $a */
if ($a == 15) { assertType('15|numeric-string', $a); }

In this case $a can be '15', '+15', '015', '0015' and so on.

So a proper solution is really not easy. I think the best solution is to follow the approach in this PR and just handle all the cases in the table there, I basically handlded the empty string case but it can easily be duplicated for the other cases. Would you accept the PR if I complete the table in the docs?

@thg2k
Copy link
Contributor Author

thg2k commented Aug 9, 2024

I'm working on an updated PR to handle more cases. This also fixes a problem I found, see this:

https://phpstan.org/r/62751f1c-596f-4d7c-8a82-ff027b0515d5

Note that the evaluation of the expression actually gets it right, it's the type narrowing that gets it wrong.. is there no possibility to get this done in one method without duplicating the logic?
saying that '$a == $b' is true/false/bool should be a subset of the problem of narrowing types for $a and $b, but I get lost in the thought process...

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.

3 participants