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

Properly implement type inference for C# member access expressions. #12110

Merged
merged 11 commits into from
Jun 21, 2016
Original file line number Diff line number Diff line change
Expand Up @@ -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 ( ) ; } } ");
Copy link
Contributor

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?

}

Expand All @@ -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 ; } ) ;
}
} ",
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above

Copy link
Member

Choose a reason for hiding this comment

The 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")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Copy link
Member

Choose a reason for hiding this comment

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

Why can't we test position anymore?

Copy link
Member Author

Choose a reason for hiding this comment

The 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)]
Expand All @@ -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);
Copy link
Member

Choose a reason for hiding this comment

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

This seems worse.

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. It is correct. There is no reason for us to have inferred Task<bool> in this case. Indeed, it could have been any type there that had a ContinueWith method on it. The old code 'worked' because it simply assumed "hey, i'm bein assigned to a bool, ergo i must be task of bool. But that's just specious. it's like saying:

string s = foo().bar and inferring that "foo()" must return a string. It just has no relation

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

The 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 List<T> when the user types "foo.Count". I think it could be hugely useful. But it needs to be done properly. i.e. we should actually be finding types that contain that member and not just saying "i'm somewhere deep in an await ergo, my type is the same as the contextual type the await would find".

To be clear:

string s = await Foo().ContinueWith(list => list.Count.ToString())

It is simply wrong for us to say that "Foo()" is a Task<string>. The only reason this was ok in the test was because the test was contrived. It happened to be ok in this one case. But htat's like saying: "string s = x.y.z.Florb()" and saying that x, y, or z should have the "string" type. It's just taking a type we see and drawing a very erroneous conclusion about it.

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)]
Expand Down
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
Expand Down Expand Up @@ -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")
Copy link
Member

Choose a reason for hiding this comment

The 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")>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The 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)>
Expand All @@ -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)
Copy link
Member

Choose a reason for hiding this comment

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

Why not?

End Function

<Fact, Trait(Traits.Feature, Traits.Features.TypeInferenceService)>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
Copy link
Member

Choose a reason for hiding this comment

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

What expression is this?

Copy link
Member Author

Choose a reason for hiding this comment

The 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)
Copy link
Member

Choose a reason for hiding this comment

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

What is this in relation to? What is the current token?

{
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)
Expand Down
Loading