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

Implement FindReferences for deconstructions #22934

Merged
merged 2 commits into from
Nov 9, 2017

Conversation

jcouv
Copy link
Member

@jcouv jcouv commented Oct 31, 2017

Implements "Find All References" for Deconstruct methods. It is similar to how FAR works on methods that participate in foreach.

Fixes #18963

From the compiler side, the change is to expose information about deconstructions (assignment/declarations and foreach) via new public APIs. Since deconstructions are not conversions in the language, but they are implemented using nested conversions (like tuple conversions) with a deconstruction method, I'm using a wrapper class around deconstruction conversion objects to abstract from the implementation.
The existing DeconstructionInfo class (which is internal) was renamed to DeconstructMethodInfo.

From the IDE side, we now keep track of whether a document uses any deconstructions (just like we do for foreach) and we can scan through a document to find all the Deconstruct method symbols that are references. That's the core of the "Find All References" implementation. (thanks Cyrus for the tips)

Note: this fix was incomplete, it was supplemented by #24223

@jcouv jcouv added this to the 15.later milestone Oct 31, 2017
@jcouv jcouv self-assigned this Oct 31, 2017

foreach (var nested in deconstruction.Nested)
{
foreach (var m in getMethods(nested))
Copy link
Member Author

@jcouv jcouv Oct 31, 2017

Choose a reason for hiding this comment

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

📝 this is a bit unfortunate, but I don't expect deeply nested deconstructions. #Resolved

Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 1, 2017

Choose a reason for hiding this comment

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

I'd prefer an immutable array builder. we end up tending to have to avoid IEnumreables in pathological cases. #Resolved

@jcouv
Copy link
Member Author

jcouv commented Oct 31, 2017

@dotnet/roslyn-compiler for review. This PR provides a public API for accessing deconstruction information. See description for more details. Thanks #Resolved

@jcouv
Copy link
Member Author

jcouv commented Oct 31, 2017

@dotnet/roslyn-ide for review as well. This PR implements FindReferences for Deconstruct methods. Thanks #Resolved

@@ -227,6 +227,38 @@ public ForEachSymbols GetForEachSymbols(SemanticModel semanticModel, SyntaxNode
}
}

public ImmutableArray<IMethodSymbol> GetDeconstructionSymbols(SemanticModel semanticModel, SyntaxNode node)
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 1, 2017

Choose a reason for hiding this comment

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

maybe GetDeconstructionMethods? #Resolved

Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 5, 2017

Choose a reason for hiding this comment

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

Also... on a personal level... i don't like having a single helper method that works on all these types. I think i'd prefer a GetAssignmentDeconstructionSymbols and GetForEachStatementDeconstructionSymbols. #Resolved


foreach (var nested in deconstruction.Nested)
{
foreach (var m in getMethods(nested))
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 1, 2017

Choose a reason for hiding this comment

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

avoid single letter foreach variable names. #Closed

@@ -106,6 +106,11 @@ private bool IsForEachMethod(IMethodSymbol methodSymbol)
methodSymbol.Name == WellKnownMemberNames.MoveNextMethodName;
}

private bool IsDeconstructMethod(IMethodSymbol methodSymbol)
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 1, 2017

Choose a reason for hiding this comment

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

nit: => #Resolved

@@ -24,7 +24,8 @@ private struct ContextInfo
bool containsThisConstructorInitializer,
bool containsBaseConstructorInitializer,
bool containsElementAccessExpression,
bool containsIndexerMemberCref) :
bool containsIndexerMemberCref,
bool containsDeconstruction) :
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 1, 2017

Choose a reason for hiding this comment

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

need to update the 'version' of SyntaxTreeINdex. Otherwise, existing indices will not be recreated and it will look like no files contain deconstructions. #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.

Updated SerializationFormat number.


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

@@ -268,6 +268,68 @@ class B : C, A
Assert.Empty(expectedMatchedLines);
}

[Fact, WorkItem(18963, "https://github.com/dotnet/roslyn/issues/18963")]
public async Task FindReferences_Deconstruction()
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 1, 2017

Choose a reason for hiding this comment

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

Wrong location for these tests. Look for the more declarative FindReferencesTests written in VB. #Resolved

@jcouv
Copy link
Member Author

jcouv commented Nov 3, 2017

@dotnet/roslyn-compiler for review as well (adding public API to get information about deconstructions, for IDE scenario). Thanks #Resolved

@CyrusNajmabadi
Copy link
Member

CyrusNajmabadi commented Nov 5, 2017

(thanks Cyrus for the tips)

You're welcome :) Can't wait to have this. It'll be super useful! #Resolved

@@ -246,6 +246,16 @@ public bool IsForEachStatement(SyntaxNode node)
return node is ForEachStatementSyntax;
}

public bool IsDeconstruction(SyntaxNode node)
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 5, 2017

Choose a reason for hiding this comment

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

Same here. SyntaxFacts isn't for merging multiple nodes from the same language into the same concept. It's for providing a uniform syntactic API over VB and C#. I would prefer IsDeconstructionAssignment and IsDeconstructionForEachStatement #Resolved

@@ -91,6 +92,7 @@ internal sealed partial class SyntaxTreeIndex
containsQueryExpression = containsQueryExpression || syntaxFacts.IsQueryExpression(node);
containsElementAccess = containsElementAccess || syntaxFacts.IsElementAccessExpression(node);
containsIndexerMemberCref = containsIndexerMemberCref || syntaxFacts.IsIndexerMemberCRef(node);
containsDeconstruction = containsDeconstruction || syntaxFacts.IsDeconstruction(node);
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 5, 2017

Choose a reason for hiding this comment

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

this would then become containsDestruction = containsDeconstruction || (syntaxFacts.IsDeconstructionAssignment || syntaxFacts.IsDeconstructionForEachStatement); #Resolved

public void {|Definition:Decons$$truct|}(out int x1, out int x2) { x1 = 1; x2 = 2; }
public void M()
{
[|var (x1, x2) =|] this;
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 6, 2017

Choose a reason for hiding this comment

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

Fasscinating. This is the resultant span? I guess i'm ok with that. though i think it's strange. #Resolved

Copy link
Member

@jasonmalinowski jasonmalinowski Nov 7, 2017

Choose a reason for hiding this comment

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

Yeah, I think something a bit narrower might be wise. Consider if there are comments in the middle of that somewhere... #Resolved

public void M()
{
[|var (x1, x2) =|] this;
[|foreach (var (y1, y2)|] in new[] { this }) { }
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 6, 2017

Choose a reason for hiding this comment

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

same here.. i think i would prefer either "var (x1, x2)" to be the span, or "(x1, x2)" to be hte span. or "=" to be hte span. for the foreach case, the same (except replacing "in" for "="). #Resolved

return;
}

foreach (var nested in deconstruction.Nested)
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 6, 2017

Choose a reason for hiding this comment

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

so just to be sure. You can' thave both .Method and .Nested available at the same time? #Resolved

Copy link
Member Author

@jcouv jcouv Nov 6, 2017

Choose a reason for hiding this comment

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

Good catch. There is a bug here. Actually, if there is a .Method, there will be a .Nested. #Resolved

@@ -246,6 +246,32 @@ public bool IsForEachStatement(SyntaxNode node)
return node is ForEachStatementSyntax;
}

public bool IsDeconstructionForEachStatement(SyntaxNode node)
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 6, 2017

Choose a reason for hiding this comment

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

nit: => #Resolved

return Location.Create(tree, TextSpan.FromBounds(assignment.SpanStart, assignment.OperatorToken.Span.End));

case ForEachVariableStatementSyntax @foreach:
return Location.Create(tree, TextSpan.FromBounds(@foreach.SpanStart, @foreach.Variable.Span.End));
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 6, 2017

Choose a reason for hiding this comment

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

this location seems absolutely wacky :D #Resolved

switch (node)
{
case AssignmentExpressionSyntax assignment:
return Location.Create(tree, TextSpan.FromBounds(assignment.SpanStart, assignment.OperatorToken.Span.End));
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 6, 2017

Choose a reason for hiding this comment

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

I am... barely ok with this :D i think it would be better to just select hte =, and the 'in' token though. But i would be open for arguments otherwise. #Resolved

var deconstructMethods = semanticFacts.GetDeconstructionAssignmentMethods(semanticModel, node);
if (deconstructMethods.IsEmpty)
{
deconstructMethods = semanticFacts.GetDeconstructionForEachMethods(semanticModel, node);
Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 6, 2017

Choose a reason for hiding this comment

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

don't you always want to do this? i don't understand why you only do htis if you didn't find any deconstruction assignments. #Closed

Copy link
Member

@CyrusNajmabadi CyrusNajmabadi Nov 6, 2017

Choose a reason for hiding this comment

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

oh........... i see. yes. this is checking per node. so you couldn't have both. cute :) n/m then. #Closed

Copy link
Member

@jasonmalinowski jasonmalinowski Nov 7, 2017

Choose a reason for hiding this comment

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

Still worth adding a comment. I think what's throwing me off is the GetDeconstruction..Methods* name, since plural, implies that it might be doing recursive itself. Which it's not. #Resolved

@jcouv
Copy link
Member Author

jcouv commented Nov 7, 2017

Filed #23037 for updating ChangeSignature to handle Deconstruct methods. #Resolved

@jcouv
Copy link
Member Author

jcouv commented Nov 7, 2017

@dotnet/roslyn-compiler for review of the compiler portion. Thanks #Resolved

@jcouv jcouv requested a review from gafter November 7, 2017 16:33
if (!underlyingConversions.IsDefault)
{
result = Hash.Combine(result, Hash.CombineValues(underlyingConversions.SelectAsArray(c => c.GetHashCode())));
}
Copy link
Member

@cston cston Nov 7, 2017

Choose a reason for hiding this comment

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

Hash.CombineValues calls GetHashCode() on the ImmutableArray<T> and handles IsDefault. #Resolved

/// Its second nested node has a Method (Deconstructable1.Deconstruct), no Conversion, and two Nested nodes.
/// Those last two nested nodes have no Method, but each have a Conversion (ImplicitNumeric, from int to long).
/// </summary>
public struct DeconstructionInfo : IEquatable<DeconstructionInfo>
Copy link
Member

@cston cston Nov 7, 2017

Choose a reason for hiding this comment

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

Are IEquatable<DeconstructionInfo> and Equals needed? #Closed

Copy link
Member Author

@jcouv jcouv Nov 7, 2017

Choose a reason for hiding this comment

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

I did this for consistency with ForEachDeconstructionInfo, which begs the question.
I'll check with @gafter on public API design, and if nobody objects, I'll remove IEquatable. #Closed

@jcouv
Copy link
Member Author

jcouv commented Nov 7, 2017

From discussion with Chuck and Neal, we don't need IEquatable on DeconstructionInfo yet (we're not sure why ForEachStatementInfo needed it). Removed that portion and its tendrils. #Resolved

@@ -4403,6 +4403,18 @@ private static ImmutableArray<Symbol> CreateReducedExtensionMethodIfPossible(Bou
public abstract ForEachStatementInfo GetForEachStatementInfo(CommonForEachStatementSyntax node);

/// <summary>
/// Gets for deconstruction info.
Copy link
Member

@cston cston Nov 7, 2017

Choose a reason for hiding this comment

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

Gets assignment deconstruction info? #Resolved

public abstract DeconstructionInfo GetDeconstructionInfo(AssignmentExpressionSyntax node);

/// <summary>
/// Gets for deconstruction info.
Copy link
Member

@cston cston Nov 7, 2017

Choose a reason for hiding this comment

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

Gets foreach variable deconstruction info? #Resolved

@@ -837,6 +837,34 @@ public override ForEachStatementInfo GetForEachStatementInfo(CommonForEachStatem
enumeratorInfoOpt.CurrentConversion);
}

public override DeconstructionInfo GetDeconstructionInfo(AssignmentExpressionSyntax node)
{
var boundDeconstruction = (BoundDeconstructionAssignmentOperator)GetUpperBoundNode(node);
Copy link
Member

@cston cston Nov 7, 2017

Choose a reason for hiding this comment

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

Does this throw a cast exception if the caller passes an assignment that is not a deconstruction assignment? Is that the desired behavior? #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.

Good question. It's the same behavior as GetForEachStatementInfo, but does seem wrong.
For comparison, GetAwaitExpressionInfo returns default in such cases. I'll change foreach and deconstruction methods to do the same.


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

Copy link
Member Author

Choose a reason for hiding this comment

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

Hum, actually GetForEachStatementInfo is fine. The problem is just with assignment (which may or may not be a deconstruction).


In reply to: 149516672 [](ancestors = 149516672,149514253)

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.

@jcouv
Copy link
Member Author

jcouv commented Nov 8, 2017

Thanks Chuck!
@dotnet/roslyn-compiler for a second review of compiler portion.


/// <summary>
/// The representation of a deconstruction as a tree of Deconstruct methods and conversions.
/// Methods only appear in non-terminal nodes. All terminal nodes have a Conversion.
Copy link
Member

Choose a reason for hiding this comment

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

cref Conversion if that means it's a reference to the property.

///
/// The top-level node has no Method and no Conversion, but has two Nested nodes.
/// Its first nested node has no Method, but has a Conversion (Identity).
/// Its second nested node has a Method (Deconstructable1.Deconstruct), no Conversion, and two Nested nodes.
Copy link
Member

Choose a reason for hiding this comment

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

This comment implies that across the summation of all the nodes, there is one node that has a Method, but I would expect two since two methods are being invoked.

Copy link
Member Author

Choose a reason for hiding this comment

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

Argh, sorry about that :-(

@@ -996,5 +996,31 @@ public static ConditionalAccessExpressionSyntax GetInnerMostConditionalAccessExp
return to.WithPrependedLeadingTrivia(finalTrivia)
.WithAdditionalAnnotations(Formatter.Annotation);
}

public static SyntaxNode IsInDeconstructionLeft(this SyntaxNode node)
Copy link
Member

@jasonmalinowski jasonmalinowski Nov 8, 2017

Choose a reason for hiding this comment

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

This is an Is* method returning non-Boolean. Please revise the name. #Resolved

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.

Good other than one specific question about doc comments that look like they have a typo which makes it really confusing.

Copy link
Member

@gafter gafter left a comment

Choose a reason for hiding this comment

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

API and compiler changes look good to me.

@jcouv
Copy link
Member Author

jcouv commented Nov 9, 2017

Nice, thanks!
@jasonmalinowski I've addressed the doc comment issues. Good to go?

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.

The docs are so clear, even a IDE developer can understand it. Thanks!

@jcouv
Copy link
Member Author

jcouv commented Nov 9, 2017

You mean when the docs don't say the opposite of what they should ;-)
Awesome. I'll resolve conflicts and merge once green. Thanks all.

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

Successfully merging this pull request may close these issues.

6 participants