-
Notifications
You must be signed in to change notification settings - Fork 678
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 support for references and improve UnusedVariable checks (fixes #7254). #7339
Conversation
$expr_type->parent_nodes = [ | ||
$source->id => $source, | ||
]; | ||
$expr_type->parent_nodes[$source->id] = $source; |
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 isn't really that closely related to everything else in this PR, but I'm pretty sure it's a correct change. I'm not familiar enough with the taint feature to know how to test it, but I worry that without this change some of the taint stuff might mess up the parent_nodes
for a reference.
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.
Let's fix that when we have a reported bug then. I see we have multiple instances of both ways of declaring parent_nodes and I'm not sure when to choose which
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 was leaning that way originally, but it looks like this only exists for plugins to use, and based on the name it's supposed to add a source, not remove all other sources as well.
I think the only time parent_nodes
should be set to an array rather than setting an offset is when the previous nodes are known to be wrong, such as when assigning a variable.
$foo = 'a';
$foo = 'b'; // Should no longer have a parent node for `$foo = 'a'`
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'm unclear on what can cause a variable to have multiple parents though
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'm a bit unsure about that myself. I utilized multiple parents for references, but outside of that I don't know what use it has, other than it probably has something to do with taints.
* Maps references to referenced variables for the current scope. | ||
* With `$b = &$a`, this will contain `['$b' => '$a']`. | ||
* | ||
* All keys and values in this array are guaranteed to be set in $vars_in_scope. |
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'm fairly confident I caught all the places a var_in_scope might be unset, but if this isn't true for some reason I consider that a bug to be fixed.
@@ -667,7 +753,7 @@ public function removeDescendents( | |||
|
|||
foreach ($this->vars_in_scope as $var_id => $type) { | |||
if (preg_match('/' . preg_quote($remove_var_id, '/') . '[\]\[\-]/', $var_id)) { | |||
unset($this->vars_in_scope[$var_id]); | |||
$this->remove($var_id, false); |
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.
Not sure why this wasn't unsetting $this->vars_possibly_in_scope
before, but it works fine this way. I'm guessing it was just overlooked? Or maybe $this->vars_possibly_in_scope
is guaranteed to not contain descendents? (side note, that should actually be descendant, descendent is an adjective with a subtly different meaning).
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.
well, vars_possibly_in_scope is just an array of bools. It's nothing without vars_in_scope so yeah, probably overlooked and never caused issue
// `$a = &$b` this variable is $b), we need to analyze it as an assignment to null. | ||
AssignmentAnalyzer::analyze($statements_analyzer, $stmt, null, $stmt_type, $context, null); | ||
|
||
// Stop here, we don't want it to be considered possibly undefined like the array 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.
Per #7328 I don't think the array should be PossiblyUndefined either, but I'll think about it and maybe do a separate pull request for that.
@@ -348,14 +348,27 @@ function foo() : void { | |||
'<?php | |||
function foo() : void { | |||
$a = 5; | |||
$b = &$a; |
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.
weird, this test was basically testing nothing...
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.
Yep, it was confirming incorrect behavior...
Unfortunately the actual correct behavior doesn't work yet either, but I figure I'll leave it for a future improvement.
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 hate that kind of tests when they're not flagged properly. When you break them, you just have no idea if the test is legit or not
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.
Agreed, but at least this one was obvious.
Just thinking about it, you removed every unset from $context->vars_in_scope, but we could had some. Would that be a problem? Should we target Psalm 5 after telling plugin developpers to use your new function? |
I already targeted Psalm 5 because I removed I'm going to convert this to a draft for now, I'm trying to get assertions working on references, and I discovered that PHP's reference implementation is a broken mess. |
@@ -96,6 +101,32 @@ public static function reconcileKeyedTypes( | |||
return $existing_types; | |||
} | |||
|
|||
$reference_map = []; | |||
if (!empty($existing_references)) { | |||
// PHP behaves oddly when passing an array containing references: https://bugs.php.net/bug.php?id=20993 |
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 this is fine and makes perfect sense. But then you do this, which is the exact same thing but with an array item, and everything falls apart. So what you end up with is this black magic to work around the problem.
I've come to the conclusion that references in PHP are a half-supported buggy mess, and if you use references in arrays or objects you're asking for trouble.
I think there's still quite a bit of improvement that could be done, but the basic support is there now and for references that aren't array items or object properties things more or less work as expected. I think I'll leave it here unless someone reports a bug with it, it seems to me that relying on references in PHP for anything more than basic local variable modification is a bad idea, and there are probably much more impactful things that could be worked on. |
UPGRADING.md
Outdated
- To remove a variable from the context, Context::remove(). Calling | ||
`unset($context->vars_in_scope[$var_id])` can cause problems when using references. |
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.
Kind of makes me want @psalm-readonly-external
or @psalm-readonly-allow-internal-mutation
that would be like @psalm-readonly-allow-private-mutation
but instead of restricting modifications to the class itself, it would restrict it to a namespace.
0dc5f41
to
6883665
Compare
Can you describe on https://github.com/vimeo/psalm/blob/master/UPGRADING.md that unset on vars_in_scope is now forbidden? If you see any other BC break, please add them also |
@orklah That's already in there. |
Sorry :( I did not look back at the changes. I'd like to merge that soon, can you do a rebase? |
Looks like merging the constant one created a lot more conflict. Please ping me after rebase and I'll merge this |
phpcs want `as` to have a single space before it, but it also wants the line indented. Worked around by assigning to another variable.
6883665
to
e268a05
Compare
@orklah done. That's the most painful rebase I've had to do in a while, there have been a good number of large changes recently like the |
Yep, sorry for that, but it's good news, the project is alive :) |
Thanks! |
Adds support for references by setting
Context::$vars_in_scope['$reference']
as a reference toContext::$vars_in_scope['$target']
.Adding the reference to
$vars_in_scope
basically fixed everything by itself, except for theUnused*
checks, which required quite a lot of additional work.