-
Notifications
You must be signed in to change notification settings - Fork 439
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 !isset() with ArrayDimFetch #2657
Conversation
I can see in the issue bot, that the change as is would resolve a lot of open issues. I have no idea yet, how to express the "nullability" in the !isset() case. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So if we're going to handle isset
in falsy context, we need to understand what isset
does. With !isset($foo)
it means:
- The variable might not exist
- The variable might be
null
We need to test these scenarios:
- Variable with certainty maybe and nullable
- Variable with certainty maybe and not nullable
- Variable with certainty yes and nullable
- Variable with certainty yes and not nullable
For !isset($foo['bar'])
, it means:
- The key does not exist
- The key exists but it's null
So various scenarious with array{foo: int}
and array{foo: int|null}
and also array{foo?: int}
and array{foo?: int|null}
need to be tested.
Plus for !isset($foo['bar'])
it might also mean that anything in the chain might also not exist so the first 4) points about !isset($foo)
apply to !isset($foo['bar'])
.
I think the main hurdle here is that SpecifiedTypes
currently cannot express a command to unset a variable. Because for $foo
with certainty maybe and not-nullable, the only !isset($foo)
is that the variable does not exist, so the certainty needs to be no
.
But we can introduce some kind of UnsetExpr
virtual node that would be in SpecifiedTypes and would instruct MutatingScope::filterBySpecifiedTypes
to unset the variable from scope. That might or might not be the way to go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also please write regression tests for all the issues the issue bot found (after the first original iteration), to make sure we don't break them during the review.
src/Analyser/MutatingScope.php
Outdated
$specifiedExpressions = []; | ||
foreach ($typeSpecifications as $typeSpecification) { | ||
$expr = $typeSpecification['expr']; | ||
$type = $typeSpecification['type']; | ||
|
||
if ($expr instanceof NotIssetExpr) { | ||
$unsetExpr = $expr->getExpr(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do this only for 'sure'=true.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moving it into the 'sure'=true
branch regresses some tests. I think what you want to achieve is already taken care of in the TypeSpecifier
side of this PR, as NotIssetExpr
is only passed thru in the !$context->true()
-case
This pull request has been marked as ready for review. |
I am bit a lost atm .. will take a break and look again at it later. |
the only remaining errors are 2 test expectations, which I am not sure wheter my expectations are wrong, or there is a open todo:
|
src/Analyser/TypeSpecifier.php
Outdated
$newType = TypeTraverser::map($newType, static function (Type $type, callable $traverse) use ($dimType): Type { | ||
if ($type instanceof UnionType || $type instanceof IntersectionType) { | ||
return $traverse($type); | ||
} | ||
|
||
if ($type instanceof ConstantArrayType) { | ||
$builder = ConstantArrayTypeBuilder::createFromConstantArray( | ||
$type, | ||
); | ||
$builder->setOffsetValueType( | ||
$dimType, | ||
new NullType(), | ||
true, | ||
); | ||
return $builder->getArray(); | ||
} | ||
|
||
return $type->setOffsetValueType($dimType, new NullType()); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of this TypeTraverser
block, we could add a optional
new arg to Type->setOffsetValueType()
src/Analyser/TypeSpecifier.php
Outdated
$var->var, | ||
$newType, | ||
$context->negate(), | ||
true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there is no way arround override=true
here, because we do not intersect/union the previous known array-shape, but just update a single offset of it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please solve not only ArrayDimFetch
but also standalone variables that I talk about here #2657 (review)
src/Analyser/MutatingScope.php
Outdated
$exprString = $this->getNodeKey($unsetExpr->var); | ||
|
||
unset($scope->expressionTypes[$exprString]); | ||
unset($scope->nativeExpressionTypes[$exprString]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand this logic at all. Here you're saying that if we unset($var['foo'])
and the $var
is an empty array, you're going to unset $var
!?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is the implementation part of the tests which fail atm
the idea is to implement what was described in
Because for $foo with certainty maybe and not-nullable, the only !isset($foo) is that the variable does not exist, so the certainty needs to be no.
let me fix it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am doing this array stuff here, because I think:
a array shape which only has a single offset, which gets checked with isset() on said offset will never have the variable defined in the falsey-branch
$a = ['foo' => 'abc];
if (isset($a['foo'])) {
} else {
// $a is always undefined
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since we don't want to go from YES to NO certainty, I think my above case is not possible and I should not try to support it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
$a = ['foo' => 'abc'];
if (isset($a['foo'])) {
} else {
// $a should be NeverType here
}
just pushed the implementation for variables and a bunch of tests. main problem is still the certainty stuff, and I think most - if not all - remaining test-failures feel related to certainty and mooooore regression tests |
I have an idea how to fix the remaining problems, but just need a bit more time. |
@ondrejmirtes I did a lot of debugging locally and have some changes planned, but I think the main problem I am facing atm is, that when the TypeSpecifier is invoked for the the scope in the falsey-branch already thinks example: use function PHPStan\Testing\assertVariableCertainty;
function maybeCertainNull(): void
{
if (rand() % 2) {
$a = ['bar' => null];
if (rand() % 3) {
$a = ['bar' => 'hello'];
}
}
assertVariableCertainty(TrinaryLogic::createMaybe(), $a);
if (isset($a['bar'])) {
assertVariableCertainty(TrinaryLogic::createYes(), $a);
} else {
assertVariableCertainty(TrinaryLogic::createMaybe(), $a); // Expected variable certainty Maybe, actual: Yes
}
assertVariableCertainty(TrinaryLogic::createMaybe(), $a);
} thats a problem we have already before this PR, and I don't know whether fixing it here with all the other changes needed would be a good thing as this PR will be already pretty large without this topic. do you have an idea how to fix that problem? I don't have time right now to also dive into this new side-quest. |
after a break and a short walk, I realized how it could be fixed: see #2666 |
e32d61d
to
139dbc2
Compare
I feel I am close to something useful :-) one TypeSpecifierTest is failling, because when merging left/right types the |
I''d say that when you're creating new IssetExpr in TypeSpecifier, you also need to say at the same time, what it means for the expression, and specify its type too in the same operation. |
Maybe start a new PR instead of reviving this ancient one :) |
closes phpstan/phpstan#9908
closes phpstan/phpstan#8724
closes phpstan/phpstan#8659
closes phpstan/phpstan#8190
closes phpstan/phpstan#5128
closes phpstan/phpstan#9426
closes phpstan/phpstan#9580