-
Notifications
You must be signed in to change notification settings - Fork 4k
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
Properly implement type inference for C# member access expressions. #12110
Changes from 5 commits
29c11cd
045003e
1e4027f
91cad67
41a1d64
b35bac4
f29a6cb
6bdb5a5
e11beed
3491dd2
a5b8071
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2757,7 +2757,15 @@ await TestAsync( | |
public async Task TestGenerateMethodWithMethodChaining() | ||
{ | ||
await TestAsync( | ||
@"using System ; using System . Collections . Generic ; using System . Linq ; using System . Threading . Tasks ; class Program { static void Main ( string [ ] args ) { bool x = await [|Foo|] ( ) . ConfigureAwait ( false ) ; } } ", | ||
@"using System ; | ||
using System . Collections . Generic ; | ||
using System . Linq ; | ||
using System . Threading . Tasks ; | ||
class Program { | ||
static void Main ( string [ ] args ) { | ||
bool x = await [|Foo|] ( ) . ConfigureAwait ( false ) ; | ||
} | ||
}", | ||
@"using System ; using System . Collections . Generic ; using System . Linq ; using System . Threading . Tasks ; class Program { static void Main ( string [ ] args ) { bool x = await Foo ( ) . ConfigureAwait ( false ) ; } private static Task < bool > Foo ( ) { throw new NotImplementedException ( ) ; } } "); | ||
} | ||
|
||
|
@@ -2766,8 +2774,14 @@ await TestAsync( | |
public async Task TestGenerateMethodWithMethodChaining2() | ||
{ | ||
await TestAsync( | ||
@"using System ; using System . Threading . Tasks ; class C { static async void T ( ) { bool x = await [|M|] ( ) . ContinueWith ( a => { return true ; } ) . ContinueWith ( a => { return false ; } ) ; } } ", | ||
@"using System ; using System . Threading . Tasks ; class C { static async void T ( ) { bool x = await M ( ) . ContinueWith ( a => { return true ; } ) . ContinueWith ( a => { return false ; } ) ; } private static Task < bool > M ( ) { throw new NotImplementedException ( ) ; } } "); | ||
@"using System ; | ||
using System . Threading . Tasks ; | ||
class C { | ||
static async void T ( ) { | ||
bool x = await [|M|] ( ) . ContinueWith ( a => { return true ; } ) . ContinueWith ( a => { return false ; } ) ; | ||
} | ||
} ", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also, what actually changed here? |
||
@"using System ; using System . Threading . Tasks ; class C { static async void T ( ) { bool x = await M ( ) . ContinueWith ( a => { return true ; } ) . ContinueWith ( a => { return false ; } ) ; } private static object M ( ) { throw new NotImplementedException ( ) ; } } "); | ||
} | ||
|
||
[WorkItem(529480, "http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/529480")] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1591,7 +1591,7 @@ static async void T() | |
bool x = await [|M()|].ConfigureAwait(false); | ||
} | ||
}"; | ||
await TestAsync(text, "global::System.Threading.Tasks.Task<System.Boolean>"); | ||
await TestAsync(text, "global::System.Threading.Tasks.Task<System.Boolean>", testPosition: false); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why can't we test position anymore? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. because at the start position of that expression (which is what "testPosition" is testing) there is no reason to infer Task if you have a member access expression. |
||
} | ||
|
||
[Fact, Trait(Traits.Feature, Traits.Features.TypeInferenceService)] | ||
|
@@ -1609,7 +1609,7 @@ static async void T() | |
bool x = await [|M|].ContinueWith(a => { return true; }).ContinueWith(a => { return false; }); | ||
} | ||
}"; | ||
await TestAsync(text, "global::System.Threading.Tasks.Task<System.Boolean>"); | ||
await TestAsync(text, "System.Object", testPosition: false); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems worse. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No. It is correct. There is no reason for us to have inferred
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See the original bug at #643. I think there is some value is finding Task on some of these types. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Finding "Task" when someone writes "ContinueWith" is akin to us finding types like To be clear:
It is simply wrong for us to say that "Foo()" is a Similarly, if you had "if (a.b)" then "b" should have type 'bool', but 'a' should have nothing inferred about it**. Up until #643 was "fixed" teh type inference service operated in a very considered and careful manner. It infers types when there is a genuine connection between things. i.e. if i have "string s = a.b.c" then "c" def can be inferred to have the "string" type. similarly "string s = await foo" or "string s = await foo.ConfigureAwait()". in both these cases 'foo' can be inferred to have type string. ** in the future it would be nifty to say "try to find a type that has a 'b' on it, and pick something suitable". Until then though, i don't want this bad inference code in the type inferrer. |
||
} | ||
|
||
[Fact, Trait(Traits.Feature, Traits.Features.TypeInferenceService)] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,8 @@ | ||
' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. | ||
|
||
Option Strict Off | ||
Imports Microsoft.CodeAnalysis.CodeFixes | ||
Imports Microsoft.CodeAnalysis.VisualBasic.CodeFixes.GenerateMethod | ||
Imports Microsoft.CodeAnalysis.Diagnostics | ||
Imports System.Threading.Tasks | ||
|
||
Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.Diagnostics.GenerateMethod | ||
Public Class GenerateMethodTests | ||
|
@@ -2190,17 +2188,24 @@ index:=1) | |
<Fact(), Trait(Traits.Feature, Traits.Features.CodeActionsGenerateMethod)> | ||
Public Async Function TestGenerateMethodWithMethodChaining() As Task | ||
Await TestAsync( | ||
NewLines("Imports System \n Imports System.Linq \n Module M \n Async Sub T() \n Dim x As Boolean = Await [|F|]().ContinueWith(Function(a) True).ContinueWith(Function(a) False) \n End Sub \n End Module"), | ||
NewLines("Imports System\nImports System.Linq\nImports System.Threading.Tasks\n\nModule M\n Async Sub T()\n Dim x As Boolean = Await F().ContinueWith(Function(a) True).ContinueWith(Function(a) False)\n End Sub\n\n Private Function F() As Task(Of Boolean)\n Throw New NotImplementedException()\n End Function\nEnd Module")) | ||
End Function | ||
|
||
<WorkItem(643, "https://github.com/dotnet/roslyn/issues/643")> | ||
<Fact(), Trait(Traits.Feature, Traits.Features.CodeActionsGenerateMethod)> | ||
Public Async Function TestGenerateMethodWithMethodChaining2() As Task | ||
Await TestAsync( | ||
NewLines("Imports System \n Imports System.Linq \n Module M \n Async Sub T() \n Dim x As Boolean = Await [|F|]().ContinueWith(Function(a) True).ContinueWith(Function(a) False) \n End Sub \n End Module"), | ||
NewLines("Imports System\nImports System.Linq\nImports System.Threading.Tasks\n\nModule M\n Async Sub T()\n Dim x As Boolean = Await F().ContinueWith(Function(a) True).ContinueWith(Function(a) False)\n End Sub\n\n Private ReadOnly Property F As Task(Of Boolean)\n Get\n Throw New NotImplementedException()\n End Get\n End Property\nEnd Module"), | ||
index:=1) | ||
"Imports System | ||
Imports System.Linq | ||
Module M | ||
Async Sub T() | ||
Dim x As Boolean = Await [|F|]().ConfigureAwait(False) | ||
End Sub | ||
End Module", | ||
"Imports System | ||
Imports System.Linq | ||
Imports System.Threading.Tasks | ||
Module M | ||
Async Sub T() | ||
Dim x As Boolean = Await F().ConfigureAwait(False) | ||
End Sub | ||
Private Function F() As Task(Of Boolean) | ||
Throw New NotImplementedException() | ||
End Function | ||
End Module") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What actually changed? |
||
End Function | ||
|
||
<WorkItem(1130960, "http://vstfdevdiv:8080/DevDiv2/DevDiv/_workitems/edit/1130960")> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -705,7 +705,7 @@ Module M | |
Dim x As Boolean = Await [|F|].ContinueWith(Function(a) True).ContinueWith(Function(a) False) | ||
End Sub | ||
End Module" | ||
Await TestAsync(text, "Global.System.Threading.Tasks.Task(Of System.Boolean)", testPosition:=True) | ||
Await TestAsync(text, "System.Object", testPosition:=False) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like a regression. |
||
End Function | ||
|
||
<Fact, Trait(Traits.Feature, Traits.Features.TypeInferenceService)> | ||
|
@@ -719,7 +719,7 @@ Module M | |
Dim x As Boolean = Await [|F|].ConfigureAwait(False) | ||
End Sub | ||
End Module" | ||
Await TestAsync(text, "Global.System.Threading.Tasks.Task(Of System.Boolean)", testPosition:=True) | ||
Await TestAsync(text, "Global.System.Threading.Tasks.Task(Of System.Boolean)", testPosition:=False) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why not? |
||
End Function | ||
|
||
<Fact, Trait(Traits.Feature, Traits.Features.TypeInferenceService)> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,8 +3,10 @@ | |
using System; | ||
using System.Collections.Generic; | ||
using System.Collections.Immutable; | ||
using System.Diagnostics; | ||
using System.Linq; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using Microsoft.CodeAnalysis.CSharp.Extensions; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Shared.Extensions; | ||
|
@@ -113,7 +115,8 @@ private IEnumerable<ITypeSymbol> GetTypesSimple(ExpressionSyntax expression) | |
return SpecializedCollections.EmptyEnumerable<ITypeSymbol>(); | ||
} | ||
|
||
protected override IEnumerable<ITypeSymbol> InferTypesWorker_DoNotCallDirectly(ExpressionSyntax expression) | ||
protected override IEnumerable<ITypeSymbol> InferTypesWorker_DoNotCallDirectly( | ||
ExpressionSyntax expression) | ||
{ | ||
expression = expression.WalkUpParentheses(); | ||
var parent = expression.Parent; | ||
|
@@ -145,7 +148,7 @@ protected override IEnumerable<ITypeSymbol> InferTypesWorker_DoNotCallDirectly(E | |
(InitializerExpressionSyntax initializerExpression) => InferTypeInInitializerExpression(initializerExpression, expression), | ||
(IsPatternExpressionSyntax isPatternExpression) => InferTypeInIsPatternExpression(isPatternExpression, expression), | ||
(LockStatementSyntax lockStatement) => InferTypeInLockStatement(lockStatement), | ||
(MemberAccessExpressionSyntax memberAccessExpression) => InferTypeInMemberAccessExpression(memberAccessExpression), | ||
(MemberAccessExpressionSyntax memberAccessExpression) => InferTypeInMemberAccessExpression(memberAccessExpression, expression), | ||
(NameEqualsSyntax nameEquals) => InferTypeInNameEquals(nameEquals), | ||
(ParenthesizedLambdaExpressionSyntax parenthesizedLambdaExpression) => InferTypeInParenthesizedLambdaExpression(parenthesizedLambdaExpression), | ||
(PostfixUnaryExpressionSyntax postfixUnary) => InferTypeInPostfixUnaryExpression(postfixUnary), | ||
|
@@ -214,6 +217,7 @@ protected override IEnumerable<ITypeSymbol> InferTypesWorker_DoNotCallDirectly(i | |
(IfStatementSyntax ifStatement) => InferTypeInIfStatement(ifStatement, token), | ||
(InitializerExpressionSyntax initializerExpression) => InferTypeInInitializerExpression(initializerExpression, previousToken: token), | ||
(LockStatementSyntax lockStatement) => InferTypeInLockStatement(lockStatement, token), | ||
(MemberAccessExpressionSyntax memberAccessExpression) => InferTypeInMemberAccessExpression(memberAccessExpression, previousToken: token), | ||
(NameColonSyntax nameColon) => InferTypeInNameColon(nameColon, token), | ||
(NameEqualsSyntax nameEquals) => InferTypeInNameEquals(nameEquals, token), | ||
(ObjectCreationExpressionSyntax objectCreation) => InferTypeInObjectCreationExpression(objectCreation, token), | ||
|
@@ -1426,15 +1430,54 @@ private IEnumerable<ITypeSymbol> InferTypeInNameColon(NameColonSyntax nameColon, | |
return SpecializedCollections.EmptyEnumerable<ITypeSymbol>(); | ||
} | ||
|
||
private IEnumerable<ITypeSymbol> InferTypeInMemberAccessExpression(MemberAccessExpressionSyntax expression) | ||
private IEnumerable<ITypeSymbol> InferTypeInMemberAccessExpression( | ||
MemberAccessExpressionSyntax memberAccessExpression, | ||
ExpressionSyntax expressionOpt = null, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What expression is this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So the pattern for type inference is that we walk upwards finding a proper context that we can determine a type for. The node we're currently on is the first arg. If we walked up from a cihld, then that's the second arg. If this was a positional inference, then this is the token on the left of the cursor. Inference is used for two purposes. One is when we have an actual node and we're tring to decide what type it should have. i.e. "int i = [|Foo()|]". Here we want to say "Foo() should have type 'int'". The second is when we're just typing and we want to know what type we would infer to drive features like completion. for example if i have "Color c = $$", then i want to say "i infer Color here" so we can do enum preselection. This pattern should be consistent throughout the entire feature for C# and VB. |
||
SyntaxToken? previousToken = null) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is this in relation to? What is the |
||
{ | ||
var awaitExpression = expression.GetAncestor<AwaitExpressionSyntax>(); | ||
if (awaitExpression != null) | ||
// We need to be on the right of the dot to infer an appropriate type for | ||
// the member access expression. i.e. if we have "Foo.Bar" then we can | ||
// def infer what the type of 'Bar' should be (it's whatever type we infer | ||
// for 'Foo.Bar' itself. However, if we're on 'Foo' then we can't figure | ||
// out anything about its type. | ||
if (previousToken != null) | ||
{ | ||
return InferTypes(awaitExpression.Expression); | ||
if (previousToken.Value != memberAccessExpression.OperatorToken) | ||
{ | ||
return SpecializedCollections.EmptyEnumerable<ITypeSymbol>(); | ||
} | ||
|
||
// We're right after the dot in "Foo.Bar". The type for "Bar" should be | ||
// whatever type we'd infer for "Foo.Bar" itself. | ||
return InferTypes(memberAccessExpression); | ||
} | ||
else | ||
{ | ||
Debug.Assert(expressionOpt != null); | ||
if (expressionOpt == memberAccessExpression.Expression) | ||
{ | ||
// If we're on the left side of a dot, it's possible in a few cases | ||
// to figure out what type we should be. Specifically, if we have | ||
// | ||
// await foo.ConfigureAwait() | ||
// | ||
// then we can figure out what 'foo' should be based on teh await | ||
// context. | ||
|
||
return SpecializedCollections.EmptyEnumerable<ITypeSymbol>(); | ||
if (memberAccessExpression.Name.Identifier.Value.Equals(nameof(Task<int>.ConfigureAwait)) && | ||
memberAccessExpression.IsParentKind(SyntaxKind.InvocationExpression) && | ||
memberAccessExpression.Parent.IsParentKind(SyntaxKind.AwaitExpression)) | ||
{ | ||
return InferTypes((ExpressionSyntax)memberAccessExpression.Parent); | ||
} | ||
|
||
return SpecializedCollections.EmptyEnumerable<ITypeSymbol>(); | ||
} | ||
|
||
// We're right after the dot in "Foo.Bar". The type for "Bar" should be | ||
// whatever type we'd infer for "Foo.Bar" itself. | ||
return InferTypes(memberAccessExpression); | ||
} | ||
} | ||
|
||
private IEnumerable<ITypeSymbol> InferTypeInNameEquals(NameEqualsSyntax nameEquals, SyntaxToken? previousToken = null) | ||
|
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.
Can you change the formatting here two, so they both match?