Skip to content

Go To Definition works with different Ast types #706

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

Merged
merged 5 commits into from
Jul 25, 2018

Conversation

TylerLeonhardt
Copy link
Member

@TylerLeonhardt TylerLeonhardt commented Jul 18, 2018

fixes PowerShell/vscode-powershell#1439

The issue was that when you hit F12 (aka go to definition) on any of the following scenarios except for the first one, they would fail to find the definition.

$var1 = "string"
$var1

[string] $var2 = "string"
$var2

[NJsonSchema.Annotations.NotNull()] $var3 = "string"
$var3

$a, $var4 = "string", "sss"
$var4

$a, [string] $var5 = "string", "sss"
$var5

All these scenarios now work with this PR.

We needed to recurse the AST to find the VariableExpressionAst because it could be nested in another Ast type.

Copy link
Collaborator

@SeeminglyScience SeeminglyScience left a comment

Choose a reason for hiding this comment

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

This is great! Awesome work 😄

Just a couple optional suggestions for optimizing.

List<VariableExpressionAst> asts = FindMatchingAsts(assignmentStatementAst.Left, variableName);
if (asts.Count > 0)
{
FoundDeclaration = new SymbolReference(SymbolType.Variable, asts.FirstOrDefault().Extent);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Will this only check the first variable when the lhs is an array literal? If so we should enumerate until we find a match or the list is completed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

@SeeminglyScience that will grab the first in an array literal, I guess. But we probably want the LAST hit from an array literal.

$a, [string] $b = "asdf", 5, $b = 26, 5
$b

FindMatchingAsts for $b will give the 2 hits in a list...

we want $b to jump to $b = 26 not [string] $b = "asdf" so I'll have it grab the last instead of the first - good catch.

@rjmholt looks like they went iterative. I like that, but it's a little extra code when we could just continue to recurse (since we're already doing it). Thanks for the reference though :) let me know if you feel strongly otherwise

Copy link
Contributor

Choose a reason for hiding this comment

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

I prefer the clarity of recursion actually. But thought it was worth noting that an implementation already exists

Copy link
Collaborator

Choose a reason for hiding this comment

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

@tylerl0706 Initially I thought the method was just returning all VariableExpressionAst's, so my comment doesn't make a lot of sense :P

That said, I would stick with getting the first assignment.

Take this example

$a, [string] $b, $b = 1, 2, 3

Here we know with certainty what the value of the RHS is. So we can assume that the value of $b is going to be 3. However, the declaration is still the middle variable. If you run that snippet, $b is typed as a string even though the value is 3. We also won't know in most cases what the value or length of the RHS will be, so it's not a safe assumption that it will have any value.

It'll also save a little bit of processing time in some (very) fringe cases (assuming the switch to the yield style generalized method)

variableName,
StringComparison.OrdinalIgnoreCase))
{
return new List<VariableExpressionAst>{ variableExprAst };
Copy link
Collaborator

@SeeminglyScience SeeminglyScience Jul 18, 2018

Choose a reason for hiding this comment

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

Neat! I didn't know you could omit the parenthesis 😃


// VaraibleExpressionAst case - aka base case. This will return a list with the variableExpressionAst in it or an empty list if it's not the right one.
var variableExprAst = ast as VariableExpressionAst;
if (variableExprAst != null)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since the variable will only be used within the if block, an is assignment could be used here instead. (e.g. if (ast is VariableExpressionAst variableExprAst) {})

Or possibly an assignment switch

switch (ast)
{
    case ConvertExpressionAst convertExpression:
    {
        FindMatchingAsts(convertExpressionAst);
        break;
    }
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I reckon a type switch is probably clearest here -- it's probably how the PowerShell compiler itself would work if it were written today.

Copy link
Member Author

Choose a reason for hiding this comment

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

I saw a bunch of articles that said this wasn't possible hmm - I'll give it a go

Copy link
Contributor

Choose a reason for hiding this comment

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

The MS Docs seem to call it pattern matching. (Which is guess is approximately true...)

Copy link
Contributor

Choose a reason for hiding this comment

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

Ohhhh, but this may not be possible in all the .NET versions we support

Copy link
Contributor

Choose a reason for hiding this comment

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

(Historical note: We think this should work)

{
return AstVisitAction.Continue;
return (arrayLiteralAst.Elements.SelectMany(e => FindMatchingAsts(e, variableName)).ToList());
Copy link
Collaborator

Choose a reason for hiding this comment

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

This isn't super important but this method might be a lot more efficient in assignments with a lot of decoration if it used yield.

If you change the return type to IEnumerable<VariableExpressionAst> and change the other return statements to something like

foreach (var foundVariableAst in FindMatchingAsts(convertExprAst.Child))
{
    yield return foundVariableAst;
}

yield break;

Then you could remove the variable name parameter and make this a more general utility function. You could then do the actual test in the caller while still iterating only as much as you need to.

List<VariableExpressionAst> asts = FindMatchingAsts(assignmentStatementAst.Left, variableName);
if (asts.Count > 0)
{
FoundDeclaration = new SymbolReference(SymbolType.Variable, asts.FirstOrDefault().Extent);
Copy link
Contributor

Choose a reason for hiding this comment

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


// VaraibleExpressionAst case - aka base case. This will return a list with the variableExpressionAst in it or an empty list if it's not the right one.
var variableExprAst = ast as VariableExpressionAst;
if (variableExprAst != null)
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I reckon a type switch is probably clearest here -- it's probably how the PowerShell compiler itself would work if it were written today.

/// <param name="ast">An ExpressionAst</param>
/// <param name="variableName">The name of the variable we are trying to find</param>
/// <returns>A list of ExpressionAsts that match the variable name provided</returns>
private static List<VariableExpressionAst> FindMatchingAsts (ExpressionAst ast, string variableName) {
Copy link
Contributor

Choose a reason for hiding this comment

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

May want to remove the space between the method name and the opening paren here

return AstVisitAction.StopVisit;
// The ConvertExpressionAst (static casting for example `[string]$foo = "asdf"`) could contain a VariableExpressionAst so we recurse down
var convertExprAst = ast as ConvertExpressionAst;
if (convertExprAst != null && convertExprAst.Child != null)
Copy link
Contributor

Choose a reason for hiding this comment

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

Ideally we're moving to the switch, but the null check here could be compressed to convertExprAst?.Child != null

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think we can actually skip the null on Child, it shouldn't be null.

Incidentally, I didn't realize ConvertExpressionAst inherited AttributedExpressionAst, so @tylerl0706 you probably only need to check AttributedExpressionAst and ArrayLiteralAst

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh but apparently this works

($a, $b) = 1, 2

So replace it with ParenExpressionAst :P

@TylerLeonhardt
Copy link
Member Author

TylerLeonhardt commented Jul 18, 2018

@rjmholt + @SeeminglyScience pretty big refactor in that last commit - can you both re-review? I switched to using an AstVisitor (Thanks Patrick for helping me! :) )

FoundDeclaration = new SymbolReference(SymbolType.Variable, variableExprAst.Extent);
return AstVisitAction.StopVisit;
// The AssignmentStatementAst could contain either of the following Ast types:
// VariableExpressionAst, ArrayLiteralAst, ConvertExpressionAst, AttributedExpressionAst
Copy link
Member Author

Choose a reason for hiding this comment

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

gah comment is wrong

{
if(variableExpressionAst.VariablePath.UserPath.Equals(variableName, StringComparison.OrdinalIgnoreCase))
{
// TODO also find instances of set-variable
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

nice link - we'd pretty much need that entire class and rework it to do what we do. I've grabbed a few quick wins from it and applied them.

Copy link
Member Author

@TylerLeonhardt TylerLeonhardt Jul 18, 2018

Choose a reason for hiding this comment

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

Set-Variable is particularly annoying because it's not an assignmentstatementast same with dot sourcing.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah exactly. I think we'll solve 99% with this, no reason to overcomplicate it

Copy link
Collaborator

@SeeminglyScience SeeminglyScience left a comment

Choose a reason for hiding this comment

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

Awesome job! This is much cleaner :)

One possible optimization and one nit pick.

return AstVisitAction.StopVisit;
// We want to check VariableExpressionAsts from within this AssignmentStatementAst so we visit it.
FindDeclarationVariableExpressionVisitor visitor = new FindDeclarationVariableExpressionVisitor(symbolRef);
assignmentStatementAst.Visit(visitor);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Is it possible to make this assignmentStatementAst.Left.Visit(visitor) and drop the other methods in FindDeclarationVariableExpressionVisitor?

Copy link
Collaborator

@SeeminglyScience SeeminglyScience Jul 19, 2018

Choose a reason for hiding this comment

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

Also if you go this route, add an override for VisitMemberExpression and have it return skip children. If I remember correctly that should be the only type of expression that can be on the lhs and also contain a child variable expression that would be invalid.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Actually also VisitIndexExpression

/// or a decision to continue if it wasn't found</returns>
public override AstVisitAction VisitVariableExpression(VariableExpressionAst variableExpressionAst)
{
if(variableExpressionAst.VariablePath.UserPath.Equals(variableName, StringComparison.OrdinalIgnoreCase))
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Space after if

Copy link
Contributor

@rjmholt rjmholt left a comment

Choose a reason for hiding this comment

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

LGTM!

/// </summary>
/// <param name="variableExpressionAst">A VariableExpressionAst</param>
/// <returns>A decision to stop searching if the right VariableExpressionAst was found,
/// or a decision to continue if it wasn't found</returns>
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this just GH's diff viewer or does the comment above not align with the method below?

Copy link
Contributor

@rkeithhill rkeithhill left a comment

Choose a reason for hiding this comment

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

LGTM other than comment about "comment" alignment.

@TylerLeonhardt TylerLeonhardt merged commit 5717ad0 into PowerShell:master Jul 25, 2018
@TylerLeonhardt TylerLeonhardt deleted the better-goto branch July 25, 2018 14:54
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.

VSCode Peek / Go to Definition functions do not work when variable is cast as specific type
4 participants