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

Fix usage of context->truthy #2230

Merged
merged 4 commits into from
Apr 5, 2023

Conversation

VincentLanglet
Copy link
Contributor

@VincentLanglet VincentLanglet commented Feb 9, 2023

Closes phpstan/phpstan#3013
Closes phpstan/phpstan#7686

I tried to work on this issue to understand more TypeSpecifyingExtension,
I discovered like explained in phpstan/phpstan#3013 (comment) that we don't get the same behavior with

if (in_array($bar, $foo, true) === true)

than with

if (in_array($bar, $foo, true))

I started a PR to solve this, it seems like replacing $context->truthy() by $context->true() solve the issue.
It's because we get

if (in_array($bar, $foo, true)) (
     $context->true();
     $context->truthy();
} else {
     $context->false();
     $context->falsey();
}

but

if (in_array($bar, $foo, true) === true) (
     $context->true();
     $context->truthy();
} else {
     $context->truthy();
     $context->false();
     $context->falsey();
}

Then I started to look at another type specifying extensions I worked on, the array_key_exists one, and I was able to reproduce the same issue by using array_key_exists($foo, $bar) === true). And again I solved it by changing $context->truthy() by $context->true().

There is 8 others TypeSpecifyingExtensions with $context->truthy() about method which can only return false or true:

  • ClassExistsFunctionTypeSpecifyingExtension
  • DefinedConstantTypeSpecifyingExtension
  • FunctionExistsFunctionTypeSpecifyingExtension
  • IsAFunctionTypeSpecifyingExtension
  • IsSubclassOfFunctionTypeSpecifyingExtension
  • MethodExistsTypeSpecifyingExtension
  • PropertyExistsTypeSpecifyingExtension
  • StrContainingTypeSpecifyingExtension

And I wonder if there is not the same issue for them.
(Also there is 7 truthy calls (and 3 falsey calls) in the TypeSpecifier and I don't know if similar issues can occur in it.)

Since I don't really know if I'm going in the right direction, I'd like your opinion @ondrejmirtes
(cc @staabm if you can help me)

@herndlm
Copy link
Contributor

herndlm commented Feb 9, 2023

This feels weird, shouldn't truthy include the true context in such cases? What I mean: truthy is kind of like a super type of true and should the fix be maybe somewhere else?

@staabm
Copy link
Contributor

staabm commented Feb 9, 2023

I have the same feeling as @herndlm

@ondrejmirtes
Copy link
Member

I've long suspected it's not intuitive and that it's actually switched. Would be nice to have some unit tests for TypeSpecifierContext to verify that.

@VincentLanglet
Copy link
Contributor Author

VincentLanglet commented Feb 9, 2023

This feels weird, shouldn't truthy include the true context in such cases? What I mean: truthy is kind of like a super type of true

Indeed, truthy is a super type type of true @herndlm.

But, let's say in_array could return true, false, 1 or 0.
We have

if (in_array($bar, $foo, true)) (
     // `true` possible
     // `1` possible
     // so context is 0011, therefore true and truthy
} else {
     // `false` possible
     // `0` possible
     // so context is 1100, therefore false and falsey
}

and

if (in_array($bar, $foo, true) === true) (
     // `true` possible
     // so context is 0001, therefore true
} else {
     // `1` possible
     // `false` possible
     // `0` possible
     // so context is 1110 therefore truthy, false and falsey
}

or

if (in_array($bar, $foo, true) === false) (
     // `false` possible
     // so context is 0100, therefore false
} else {
     // `1` possible
     // `true` possible
     // `0` possible
     // so context is 1011 therefore falsey, true and truthy
}

and should the fix be maybe somewhere else?

This would be a great way, but I'm not sure how.

The fact is context->truthy and context->falsey doesn't make a lot of sens for method which only returns true or false values and not truthy/falsey values. So maybe PHPStan should have a way to filter out those context.

Would be nice to have some unit tests for TypeSpecifierContext to verify that.

Sure, but is there a way to write php code and assert the context inside ?
Is it possible to implement a assertContext method in the same way that

if ($function->getName() === 'PHPStan\\Testing\\assertType') {
     return $this->processAssertType($node->getArgs(), $scope);
}

for instance ?

@@ -15,14 +15,14 @@ public function createRedirectRequest(string $redirectUri): ?string
return null;
}

assertType('array{scheme?: string, host?: string, port?: int<0, 65535>, user?: string, pass?: string, path?: string, query?: string, fragment?: string}', $redirectUrlParts);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is a return null if true === array_key_exists('host', $redirectUrlParts) so the key shouldn't exist anymore.

@VincentLanglet
Copy link
Contributor Author

I edited all my example, because I made a little mistake. The error is in the fact that when doing

=== true

check, both path (if and else) are truthy.

@VincentLanglet
Copy link
Contributor Author

VincentLanglet commented Feb 9, 2023

Thinking more about this, I'm hesitating between two point of view about the context:

  1. The current context behavior is valid:
if (in_array($bar, $foo, true) === true) (
     // `true` possible
     // so context is 0001, therefore true
} else {
     // `false` possible
     // `truthy not true` possible
     // `falsy` possible
     // so context is 1110, therefore truthy, false and falsey
}

Because it reports the context without knowing anything about the return values of the function.

Therefore, it the type specifying method to care about. For example changing

public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool
	{
		return strtolower($functionReflection->getName()) === 'in_array'
			&& !$context->null();
	}

to

public function isFunctionSupported(FunctionReflection $functionReflection, FuncCall $node, TypeSpecifierContext $context): bool
	{
		return strtolower($functionReflection->getName()) === 'in_array'
			&& ($context->false() || $context->true());
	}
  1. Le context should at sometime care about the return type values of the function, so we'll get
if (in_array($bar, $foo, true) === true) (
     // `true` possible
     // so context is 0001, therefore true
} else {
     // `false` possible
     // so context is 0100, therefore false
}
// truthy not true and falsey not false are impossible

at some point when working on the context.

The more I think about it and the more I think the (1) solution is the right one, it's just that the right context check must be used. So if you agree with me @ondrejmirtes, I'll keep doing this kind of fix for all the extensions

@VincentLanglet VincentLanglet marked this pull request as ready for review February 9, 2023 19:45
@phpstan-bot
Copy link
Collaborator

This pull request has been marked as ready for review.

@VincentLanglet VincentLanglet changed the title [WIP] Fix usage of context->truthy Fix usage of context->truthy Feb 9, 2023
@VincentLanglet
Copy link
Contributor Author

I have made all the truthy->true changes in typeSpecifyingFunction about function which only returns boolean.

I tried to see if it fix some bugs and if so, I added test. So far it fixes for

  • in_array
  • array_key_exists
  • defined

I tried to reproduce the same potential bugs for others functions with type specifying function but wasn't able to find one for them (like is_a, is_array, method_exists, ...). I don't know why such bug existed for some functions and not others.

With my level of understanding of context/typeSpecifier, I'm not sure to be able to do more.

Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

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

It might have also fixed phpstan/phpstan#7686, please look into that. Thank you.

src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php Outdated Show resolved Hide resolved
src/Type/Php/InArrayFunctionTypeSpecifyingExtension.php Outdated Show resolved Hide resolved
@VincentLanglet
Copy link
Contributor Author

It might have also fixed phpstan/phpstan#7686, please look into that. Thank you.

Indeed, it's also solved. I added a non regression test.

@staabm
Copy link
Contributor

staabm commented Feb 10, 2023

Awesome 👏

@VincentLanglet VincentLanglet force-pushed the fixContextUsage branch 2 times, most recently from 8cf8e27 to d0eef23 Compare February 20, 2023 23:16
@VincentLanglet VincentLanglet changed the base branch from 1.9.x to 1.10.x February 21, 2023 17:41
@VincentLanglet VincentLanglet force-pushed the fixContextUsage branch 2 times, most recently from adac589 to 515f46a Compare February 25, 2023 15:59
@ondrejmirtes
Copy link
Member

I'll keep doing this kind of fix for all the extensions

Yes, please do, I'd really appreciate it!

@ondrejmirtes ondrejmirtes merged commit 711c66b into phpstan:1.10.x Apr 5, 2023
@ondrejmirtes
Copy link
Member

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants