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 support for references and improve UnusedVariable checks (fixes #7254). #7339

Merged
merged 9 commits into from Jan 26, 2022

Conversation

AndrolGenhald
Copy link
Collaborator

Adds support for references by setting Context::$vars_in_scope['$reference'] as a reference to Context::$vars_in_scope['$target'].
Adding the reference to $vars_in_scope basically fixed everything by itself, except for the Unused* checks, which required quite a lot of additional work.

$expr_type->parent_nodes = [
$source->id => $source,
];
$expr_type->parent_nodes[$source->id] = $source;
Copy link
Collaborator Author

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.

Copy link
Collaborator

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

Copy link
Collaborator Author

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'`

Copy link
Collaborator

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

Copy link
Collaborator Author

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.
Copy link
Collaborator Author

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);
Copy link
Collaborator Author

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).

Copy link
Collaborator

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.
Copy link
Collaborator Author

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.

src/Psalm/Context.php Outdated Show resolved Hide resolved
@@ -348,14 +348,27 @@ function foo() : void {
'<?php
function foo() : void {
$a = 5;
$b = &$a;
Copy link
Collaborator

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...

Copy link
Collaborator Author

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.

Copy link
Collaborator

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

Copy link
Collaborator Author

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.

tests/ReferenceTest.php Outdated Show resolved Hide resolved
@orklah
Copy link
Collaborator

orklah commented Jan 10, 2022

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?

@AndrolGenhald
Copy link
Collaborator Author

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 Context::$vars_from_global, but that's another thing to think about.

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.

@AndrolGenhald AndrolGenhald marked this pull request as draft January 10, 2022 21:38
@AndrolGenhald AndrolGenhald marked this pull request as ready for review January 10, 2022 23:53
@@ -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
Copy link
Collaborator Author

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.

@AndrolGenhald
Copy link
Collaborator Author

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
Comment on lines 118 to 125
- To remove a variable from the context, Context::remove(). Calling
`unset($context->vars_in_scope[$var_id])` can cause problems when using references.
Copy link
Collaborator Author

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.

@orklah
Copy link
Collaborator

orklah commented Jan 20, 2022

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

@AndrolGenhald
Copy link
Collaborator Author

@orklah That's already in there.

@orklah
Copy link
Collaborator

orklah commented Jan 20, 2022

Sorry :( I did not look back at the changes.

I'd like to merge that soon, can you do a rebase?

@orklah orklah added release:feature The PR will be included in 'Features' section of the release notes PR: Need review labels Jan 20, 2022
@orklah
Copy link
Collaborator

orklah commented Jan 25, 2022

Looks like merging the constant one created a lot more conflict. Please ping me after rebase and I'll merge this

@AndrolGenhald
Copy link
Collaborator Author

@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 'code' key in tests.

@orklah
Copy link
Collaborator

orklah commented Jan 26, 2022

Yep, sorry for that, but it's good news, the project is alive :)

@orklah orklah merged commit f3f3fde into vimeo:master Jan 26, 2022
@orklah
Copy link
Collaborator

orklah commented Jan 26, 2022

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
PR: Need review release:feature The PR will be included in 'Features' section of the release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants