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

QuickInfo should show the captures for lambdas and local functions #23970

Merged
merged 12 commits into from
Jan 27, 2018

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Dec 29, 2017

Customer scenario

Performance-sensitive code should beware of captured variables when using lambdas. But it's not always easy to tell what variables are captured and will cause a closure to be created.
This feature adds a "Captures" section the QuickInfo bubble:

image

image

Bugs this fixes

Fixes #23307

Workarounds, if any

Figures out the captures mentally.

Risk

Performance impact

Is this a regression from a previous update?

No.

Note for the record: this was also implemented in monodevelop (mono/monodevelop#4569)

@jcouv jcouv added the Area-IDE label Dec 29, 2017
@jcouv jcouv added this to the 15.6 milestone Dec 29, 2017
@jcouv jcouv self-assigned this Dec 29, 2017
@jcouv jcouv requested a review from a team as a code owner December 29, 2017 18:21
@jcouv jcouv added the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Dec 29, 2017
@alrz
Copy link
Member

alrz commented Dec 29, 2017

This is awesome. just two suggestions: it would be nice if we underline function name and lambda arrow as a visual clue if there is any capture (so you don't need to hover over any function/lambda to make sure it's not capturing, for example). also, it might make sense to group parameters and locals (if that's helpful at all). #WontFix

@jcouv
Copy link
Member Author

jcouv commented Dec 30, 2017

I think I'll need Ali's PR (#23428) to get scenarios with multiple capture sets to work (N(() =$$> { M(); }, () => { i++; });).
I'm also having a few tests fail on trying to do the region analysis on a MethodBodySemanticModel (throws "not supported" exception). Still investigating.

Update:
Rebased to 15.7 branch and using the newly added CapturedInside API.
Still have an issue with MethodBodySemanticModel. #Resolved

@jcouv jcouv changed the base branch from master to features/compiler December 30, 2017 19:38
@sharwell sharwell changed the title QuickInfo should show the captures for lambdas and local functions [WIP] QuickInfo should show the captures for lambdas and local functions Dec 30, 2017
@sharwell
Copy link
Member

sharwell commented Dec 30, 2017

@jcouv I'm a big fan of this and a few related items. I've marked this as a work in progress (in a way the bot understands) and will follow up in the original issue. #Resolved

@@ -587,6 +587,11 @@ public bool IsBindableToken(SyntaxToken token)
return true;
}

if (token.IsKind(SyntaxKind.EqualsGreaterThanToken))
Copy link
Contributor

@MaStr11 MaStr11 Dec 31, 2017

Choose a reason for hiding this comment

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

I did something comparable while adding support for QuickInfo for Linq query syntax here in PR #23049. I was wondering whether this might have unwanted side effects (In my case it is even worse, because it is a CommaToken). If you experience something unexpected it would be nice to get me informed. #Resolved

Copy link
Member

@jasonmalinowski jasonmalinowski Jan 17, 2018

Choose a reason for hiding this comment

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

I'd say @MaStr11's concerns are valid here, but maybe it's fine...? #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

@[email protected] Can you clarify the concern?
The method is only used in GetSemanticInfoAtPositionAsync which now supports finding a symbol when the position is in =>.
GetSemanticInfoAtPositionAsync itself is used in GoToDefinition, Peek and SymbolFinder (which is used in numerous places, like FindUsages, FindReferences, ...).


In reply to: 161932149 [](ancestors = 161932149)

@jasonmalinowski
Copy link
Member

jasonmalinowski commented Jan 3, 2018

@jcouv I'll ignore the actual code for review until the WIP is removed, but this should probably target master and not be carried along with features/compiler. This is an IDE-only feature that can be shipped independent of new language features. #Resolved

@jcouv
Copy link
Member Author

jcouv commented Jan 3, 2018

@jasonmalinowski This cannot go into master, as it depends on a new compiler API (CapturesInside) which was recently added to features/compiler for 15.7.

There are two test regressions, which I'm still investigating. When I ask for a semantic model for the some lambda syntaxes, I get back a MethodBodySemanticModel, which does not support flow analysis. I'm not sure why I'm getting that semantic model.

Also, @sharwell asked for IDE team to discuss this. It's probably better to hold-off review until design is approved. #Resolved

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Jan 3, 2018

captures @this? Why the @? #Resolved

void local() { i++; this.M(); }
}
}",
Captures($"\r\n{WorkspacesResources.Captures_colon} @this, i"));
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jan 3, 2018

Choose a reason for hiding this comment

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

@this? #Closed

AssertTextAndClassifications(expectedText, expectedClassifications: null, actualContent: ((QuickInfoDisplayDeferredContent)content).CapturesText);
};
}

Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jan 3, 2018

Choose a reason for hiding this comment

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

consider removing all the curlies here. Catures() => content => ... #Closed

Copy link
Member Author

@jcouv jcouv Jan 5, 2018

Choose a reason for hiding this comment

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

Felt harder to visually parse. Also, it's inconsistent with neighboring code. #Closed

Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jan 6, 2018

Choose a reason for hiding this comment

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

Also, it's inconsistent with neighboring code.

Fix the neighboring code :)

(But if you feel like it's actually harder to parse, that's a totally legitimate reason to not do it). #Resolved

parts.AddRange(ToMinimalDisplayParts(captured, s_formatForCaptures));
first = false;
}
AddToGroup(SymbolDescriptionGroups.Captures, parts);
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jan 3, 2018

Choose a reason for hiding this comment

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

newlines after } (above as well). #Resolved

public bool IdentifiesLambda(SyntaxToken token)
{
return token.IsKind(SyntaxKind.EqualsGreaterThanToken);
}
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jan 3, 2018

Choose a reason for hiding this comment

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

nit: use => #Closed

@@ -341,6 +341,8 @@ internal interface ISyntaxFactsService : ILanguageService
Location GetDeconstructionReferenceLocation(SyntaxNode node);

SyntaxToken? GetDeclarationIdentifierIfOverride(SyntaxToken token);

bool IdentifiesLambda(SyntaxToken token);
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jan 3, 2018

Choose a reason for hiding this comment

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

on the fence if this is the right location for this method. #Resolved

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'm planning to leave as-is unless I hear some better suggestion ;-)


In reply to: 159457900 [](ancestors = 159457900)

allSymbols = semanticModel.GetSymbolInfo(bindableParent, cancellationToken)
.GetBestOrAllSymbols()
.WhereAsArray(s => !s.Equals(declaredSymbol))
.SelectAsArray(s => MapSymbol(s, type));
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jan 3, 2018

Choose a reason for hiding this comment

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

align dots. #Closed

Copy link
Member

@CyrusNajmabadi CyrusNajmabadi left a comment

Choose a reason for hiding this comment

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

LGTM (with changes + vb support).

@@ -85,7 +85,7 @@ private static bool IsEscapable(SymbolDisplayPartKind kind)
private static string EscapeIdentifier(string identifier)
{
var kind = SyntaxFacts.GetKeywordKind(identifier);
return kind == SyntaxKind.None
return kind == SyntaxKind.None || kind == SyntaxKind.ThisKeyword
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jan 6, 2018

Choose a reason for hiding this comment

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

This looks wrong. this means we would not properly escape things in something like Foo(int @this) anymore. #Resolved

@@ -4934,18 +4954,18 @@ public static void Main()
Assert.Empty(flowAnalysis.Captured);
Assert.Empty(flowAnalysis.CapturedInside);
Assert.Empty(flowAnalysis.CapturedOutside);
Assert.Equal("MyClass @this", flowAnalysis.DataFlowsIn.Single().ToTestDisplayString());
Assert.Equal("MyClass this", flowAnalysis.DataFlowsIn.Single().ToTestDisplayString());
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jan 6, 2018

Choose a reason for hiding this comment

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

yup. these are all bad changes. #Resolved

Copy link
Member Author

Choose a reason for hiding this comment

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

No, I think those changes are correct. Added some explicit tests with "this" and "@this" to illustrate the point.


In reply to: 160014222 [](ancestors = 160014222)

Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jan 19, 2018

Choose a reason for hiding this comment

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

Gotcha! #Closed

var syntaxFacts = document.Project.LanguageServices.GetService<ISyntaxFactsService>();

ImmutableArray<ISymbol> symbols;
if (syntaxFacts.IdentifiesLambda(token))
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jan 19, 2018

Choose a reason for hiding this comment

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

yeah. i would just have IdentifiesLambda just pulled into this type as an abstract method. #Resolved

Copy link
Member Author

@jcouv jcouv Jan 20, 2018

Choose a reason for hiding this comment

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

Done :-) #Resolved

@@ -92,6 +92,13 @@ Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.QuickInfo
Return Await MyBase.BuildContentAsync(document, token, cancellationToken).ConfigureAwait(False)
End Function

''' <summary>
''' Given a Sub or Function token for a lambda, returns the syntax for the whole lambda
''' </summary>
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jan 19, 2018

Choose a reason for hiding this comment

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

do we have tests for single/multiline sub/function lambdas? #Resolved

Copy link
Member Author

@jcouv jcouv Jan 20, 2018

Choose a reason for hiding this comment

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

Yes, I have all four cases #Resolved

@jcouv jcouv added the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Jan 22, 2018
@jcouv jcouv removed the PR For Personal Review Only The PR doesn’t require anyone other than the developer to review it. label Jan 23, 2018
@jcouv
Copy link
Member Author

jcouv commented Jan 23, 2018

Fixed a couple of test regressions.
One case required a small code change: when invoking symbol completion on a local, we will build a description for that local using speculation, but we'll only display the "main description" portion of that description (not the captures).

@dotnet/roslyn-ide for review. Thanks #Resolved

@jcouv
Copy link
Member Author

jcouv commented Jan 24, 2018

@dotnet/roslyn-ide for review. All the feedback so far was addressed. Thanks #Resolved

&& token.Parent.IsKind(SyntaxKind.ParenthesizedLambdaExpression, SyntaxKind.SimpleLambdaExpression))
{
// () =>
}
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jan 24, 2018

Choose a reason for hiding this comment

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

not sure how i feel about this style of no-body then falling out to common code :) #Resolved

''' If the token is a 'Sub' or 'Function' in a lambda, returns the syntax for the whole lambda
''' </summary>
Protected Overrides Function GetBindableNodeForTokenIndicatingLambda(token As SyntaxToken, <Out> ByRef found As SyntaxNode) As Boolean
If token.IsKind(SyntaxKind.SubKeyword, SyntaxKind.FunctionKeyword) AndAlso token.Parent.IsKind(SyntaxKind.SubLambdaHeader, SyntaxKind.FunctionLambdaHeader) Then
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jan 24, 2018

Choose a reason for hiding this comment

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

to verify: this works for single and multi-line VB lambdas? #Resolved

Copy link
Member Author

@jcouv jcouv Jan 25, 2018

Choose a reason for hiding this comment

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

Yes. Here's what the syntax tree looks like (for Sub):

Multi-line Sub
image

Single-line Sub

image

Also, I have tests for both multi-line and single-line:

        <Fact, Trait(Traits.Feature, Traits.Features.QuickInfo)>
        <WorkItem(23307, "https://github.com/dotnet/roslyn/issues/23307")>
        Public Async Function TestQuickInfoCaptures() As Task
            Await TestAsync("
Class C
    Sub M(x As Integer)
        Dim a As System.Action = Sub$$()
            x = x + 1
        End Sub
    End Sub
End Class
",
                Captures($"{vbCrLf}{WorkspacesResources.Variables_captured_colon} x"))
        End Function

        <Fact, Trait(Traits.Feature, Traits.Features.QuickInfo)>
        <WorkItem(23307, "https://github.com/dotnet/roslyn/issues/23307")>
        Public Async Function TestQuickInfoCaptures2() As Task
            Await TestAsync("
Class C
    Sub M(x As Integer)
        Dim a As System.Action = S$$ub() x = x + 1
    End Sub
End Class
",
                Captures($"{vbCrLf}{WorkspacesResources.Variables_captured_colon} x"))
        End Function
``` #Resolved

}
}

private async Task TestWithOptionsAsync(TestWorkspace workspace, params Action<object>[] expectedResults)
private async Task TestWithOptionsAsync(TestWorkspace workspace, bool skipSpeculative, params Action<object>[] expectedResults)
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jan 24, 2018

Choose a reason for hiding this comment

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

i'm a little confused as to why we would still need this. Since quick-info is no longer using any speculative semantic models, there should be no effect of trying with/without speculation. Indeed, i would be fine if the CanUseSpeculativeSemanticModelAsync part of the test was removed entirely (though keeping it would be fine.) #Resolved

Copy link
Member Author

@jcouv jcouv Jan 25, 2018

Choose a reason for hiding this comment

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

Sorry about that. It was a left-over I should have cleaned up. Thanks #Resolved

var semanticModel = GetSemanticModel(syntax.SyntaxTree);
if(semanticModel.IsSpeculativeSemanticModel)
{
// In the context of symbol completion, it is possible we'll make a description for a symbol while speculating (but it won't be displayed)
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Jan 26, 2018

Choose a reason for hiding this comment

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

lolwut? we make a description, but don't display it?

Also, if that's the case, why add the group at all? why not just insta bail?

Finally, if you are going to check IsSpeculativeSemanticModel, it's worth calling that that's because data flow doens't work with SpeculativeSemanticModels, so you need to avoid the code below this check. #Resolved

Copy link
Member Author

@jcouv jcouv Jan 26, 2018

Choose a reason for hiding this comment

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

I'll clarify the comment. When you complete a local symbol, we make a description (with many parts: main description, exceptions, captures, etc), but only the main description seems to be displayed.
The reason I put captures here anyways, is in case I'm wrong and missed some scenario. Captures listing "?" is less ambiguous/misleading than displaying no captures at all.

Thanks! #Pending

Copy link
Member

@jasonmalinowski jasonmalinowski left a comment

Choose a reason for hiding this comment

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

Looks good, but I'd expect that the this/Me captures in quick info are tagged as keywords, not parameters to be consistent with how the IDE would classify it. I'd expect that to be a one-line fix...somewhere?

@@ -266,16 +273,28 @@ internal abstract partial class AbstractSemanticQuickInfoProvider : AbstractQuic
return CreateDocumentationCommentDeferredContent(null);
}

protected abstract bool GetBindableNodeForTokenIndicatingLambda(SyntaxToken token, out SyntaxNode found);
Copy link
Member

Choose a reason for hiding this comment

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

To check: this isn't being used by TypeScript or F# that this is a breaking change to derivers? I don't see how they could , but want to confirm.

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 checked with F# folks (Will and Brett) and they do not reference AbstractSemanticQuickInfoProvider from Roslyn (they only depend on some interfaces for quick info provider.

Same thing with TypeScript (Mine Starks), which only depends on interfaces, but not this abstract type.

}

parts.AddRange(Space(count: 1));
parts.AddRange(ToMinimalDisplayParts(captured, s_formatForCaptures));
Copy link
Member

Choose a reason for hiding this comment

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

I guess I'd expect the magic this parameter to be called a keyword, not a parameter. Our symbol model indeed calls it parameter; users don't.

@jcouv
Copy link
Member Author

jcouv commented Jan 26, 2018

@jasonmalinowski Good catch. I fixed the compiler tests to check part kinds too. this and Me are now keywords.
This does result in nicer UI, and the code is also simpler that way.
image
Thanks!

@dotnet/roslyn-compiler for review. The compiler portion of this PR is a small change with how we display parts for this versus @this (and Me versus [Me]).

Copy link
Contributor

@AlekseyTs AlekseyTs left a comment

Choose a reason for hiding this comment

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

Compiler changes LGTM (iteration 12)

Copy link
Member

@cston cston left a comment

Choose a reason for hiding this comment

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

Compiler changes LGTM.

@jasonmalinowski
Copy link
Member

This does result in nicer UI, and the code is also simpler that way.

Love it when that happens!

@jcouv
Copy link
Member Author

jcouv commented Jan 27, 2018

@VSadov raised a good question with nested scopes. Here's what I observe with nesting:

The inner local looks good:
image

But the outer local says that it captures j, which is weird... Vlad said that sounds right.
image

Copy link
Member

@VSadov VSadov left a comment

Choose a reason for hiding this comment

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

LGTM

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

Successfully merging this pull request may close these issues.

10 participants