Skip to content

Conversation

@rik-smeets
Copy link
Contributor

@rik-smeets rik-smeets commented May 20, 2018

Customer scenario

Customer generates a variable or method from within:

  • an expression bodied getter
  • an expression bodied local function/
  • a block bodied location function inside a lambda expression

and notices type inferences does not work. See the bug below for a code example, or refer to the newly added unit tests.

Bugs this fixes

#26993

Workarounds, if any

Manually fix the type of the generated variable / the return type of the generated method.

Risk

Risk is limited: this fix is basically an extension to the way similar scenarios (like expression bodies properties) are handled in the same file.

Performance impact

Low, there's been some minor changes when inferring types in arrow expression clauses and return statements.

Is this a regression from a previous update?

Not that I know of.

Root cause analysis

These situations were not covered by unit tests yet. I added these tests now, including some extra unit tests validating situations which were already working as expected to prevent possible regressions in the future.

How was the bug found?

Customer reported, see #26993.

Test documentation updated?

No impact on test documentation.

@rik-smeets rik-smeets requested a review from a team as a code owner May 20, 2018 11:39
Copy link
Member

Choose a reason for hiding this comment

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

could you add a test with a getter with different accessibility? Thanks!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added a test where the getter has a different accessibility.

Copy link
Member

Choose a reason for hiding this comment

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

nit: consider rewriting all three of these 'ifs' as a switch.
nit: just use pattern matching for all three checks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I updated it to use pattern matching, thanks for the suggestion!

Copy link
Contributor

Choose a reason for hiding this comment

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

since you know this has to be an accessor, use a cast

Copy link
Contributor

@Neme12 Neme12 May 20, 2018

Choose a reason for hiding this comment

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

or pattern matching - this should work for any accessor, not just a getter

Copy link
Contributor Author

@rik-smeets rik-smeets May 20, 2018

Choose a reason for hiding this comment

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

Pattern matching is now used, both for getters and setters.

Copy link
Contributor

@Neme12 Neme12 May 20, 2018

Choose a reason for hiding this comment

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

this method is same as above, consider removing duplication

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I extracted the duplication to a separate method.

Copy link
Member

@CyrusNajmabadi CyrusNajmabadi May 20, 2018

Choose a reason for hiding this comment

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

We should also handle "SetAccessors" (and probably add/remove as well). In these cases, the inferred type would be 'void'. You should be able to validate that by doing something like this: class C { int P { set => Foo(value); } } and generating a method for 'Foo'. We'd want it to infer 'void' as the return type. Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that should work if we handle them all here:
if (arrowClause.Parent is AccessorDeclarationSyntax accessor)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I now added some tests in GenerateMethodTests.cs as well for both the getter and the setter situation. It indeed worked as expected after applying pattern matching.

Copy link
Contributor

Choose a reason for hiding this comment

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

is there a case for local functions somewhere?

Copy link
Contributor

@Neme12 Neme12 May 20, 2018

Choose a reason for hiding this comment

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

I just checked and they always generate object, both with expression body or block with a return statement. Please fix that too.

@Neme12
Copy link
Contributor

Neme12 commented May 20, 2018

note: the issue number is wrong

Customer reported, see #26933.

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: consider using a switch statement

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How can I make this work inside a switch statement?

Copy link
Contributor

Choose a reason for hiding this comment

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

switch (arrowClause.Parent)
{
    case PropertyDeclarationSyntax propertyDeclaration:
        return InferTypeInPropertyDeclaration(propertyDeclaration);
    case BaseMethodDeclarationSyntax baseMethodDeclaration:
        return InferTypeInBaseMethodDeclaration(baseMethodDeclaration);
    case AccessorDeclarationSyntax accessorDeclaration:
        return InferTypeInAccessorDeclaration(accessorDeclaration);
    default:
        return SpecializedCollections.EmptyEnumerable<TypeInferenceInfo>();
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! Didn't know that was possible too, nice to learn something new :). I've changed it, running tests now, after that I will push to server again.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, pattern matching is nice isn't it 😄 Glad you learned something new. You can use a pattern in an is expression or a switch case, regardless of what kind of pattern it is. In the future, this should be even shorter when we get the switch expression.

Copy link
Contributor

Choose a reason for hiding this comment

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

My idea was actually to replace both InferTypeInBaseMethodDeclaration and InferTypeInAccessorDeclaration with one method taking a SyntaxNode. I don't know what it would be called though. InferTypeInMethodLikeDeclaration? MethodSymbolDeclaration?

Copy link
Contributor

@Neme12 Neme12 May 20, 2018

Choose a reason for hiding this comment

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

Actually, the best approach might be to call GetDeclaredSymbol regardless of what we found (eliminating the switch statement) and then check if the symbol is a method symbol or a property symbol and get its type either way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried that, but I can't get it more readable / with less code that way. Getting the method symbol requires either the BaseMethodDeclarationSyntax or AccessorDeclarationSyntax to be passed to GetDeclaredSymbol. They don't share a base class. If you have a good idea, please let me know.

Copy link
Contributor

Choose a reason for hiding this comment

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

ok, I thought there was an overload that takes a SyntaxNode

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is, and it returns an ISymbol. But I don't know of a way to get the (necessary) method symbol based on that alone… If someone does know, I'd love to hear it.

Copy link
Contributor

@Neme12 Neme12 May 20, 2018

Choose a reason for hiding this comment

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

You'd have to check whether the symbol is an IMethodSymbol or an IPropertySymbol and then get its type. The upside is that you'd handle both methods, accessors and properties this way and could get rid of all 3 methods and the switch statement.

Copy link
Contributor

Choose a reason for hiding this comment

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

Actually, you could just use this extension method:

public static ITypeSymbol GetSymbolType(this ISymbol symbol)

Copy link
Contributor

Choose a reason for hiding this comment

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

Wait, no... it doesn't handle methods.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Of course, pattern matching to the rescue again! See my newest commit, hope this is to your liking.

Copy link
Contributor

@Neme12 Neme12 May 20, 2018

Choose a reason for hiding this comment

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

I would just extract a method (or local function if you like) to get the type from the symbol:

private static ITypeSymbol GetSymbolReturnType(ISymbol symbol)
{
    switch (symbol)
    {
        case IPropertySymbol property: return property.Type;
        case IMethodSymbol method: return method.ReturnType;
        default: return null;
    }
}

which you could use and then only do the type != null check once here and avoid all the duplication.

Copy link
Contributor

Choose a reason for hiding this comment

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

but you don't have to listen to me anymore 😄 i'm just giving suggestions

Copy link
Contributor

Choose a reason for hiding this comment

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

Note that this last change should fix local functions too, but we still need a test for that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well it's another good suggestion, so I did that. I also added unit tests for local functions, where type inference now also works as expected.

@jcouv jcouv added the Area-IDE label May 21, 2018
@rik-smeets rik-smeets changed the title Infer type in get accessor declaration Infer type in get accessor declaration and expression bodied local functions May 21, 2018
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: ?. is unnecessary, if parentSymbol is null, none of the patterns will match

Copy link
Contributor

@Neme12 Neme12 May 21, 2018

Choose a reason for hiding this comment

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

we should probably pass in CancellationToken

Copy link
Contributor

@Neme12 Neme12 May 21, 2018

Choose a reason for hiding this comment

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

I think this isn't fixed. If you change the return type of Y to void, this test will fail. It's still working incorrectly by taking the return type of the containing method, not the local function. You only fixed expression bodies. Block bodied local functions are still working incorrectly.

Copy link
Contributor

Choose a reason for hiding this comment

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

Just try this simple case:

class C
{
    void M()
    {
        int F()
        {
            return y;
        }
    }
}

the generated field will be of type object.

Copy link
Contributor

@Neme12 Neme12 May 21, 2018

Choose a reason for hiding this comment

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

Also, I think we shouldn't use Foo as a name for some reason, but I can't tell you why.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right regarding the block bodied types. I think I'll include that too in this pull request, might take me up to a week though because I'm busy for a while. I'll tag you once it's done.

@etbyrd etbyrd added the Community The pull request was submitted by a contributor who is not a Microsoft employee. label May 21, 2018
Copy link
Member

Choose a reason for hiding this comment

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

1 [](start = 7, length = 1)

I know other tests already do this, but it would be nicer to define a couple of constants for these indices. It's not immediately clear what these mean.

Copy link
Member

Choose a reason for hiding this comment

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

Since this function is only called once I'd prefer writign it more directly:

ISymbol symbolReturnType;
switch (parentSymbol)
 {
       case IPropertySymbol property: 
          symbolReturnType = property.Type;
          break;
       case IMethodSymbol method: 
          symbolReturnType = method.ReturnType;
          break;
       default: 
          return null;
}


Copy link
Contributor

@Neme12 Neme12 May 25, 2018

Choose a reason for hiding this comment

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

Sorry, it was my suggestion to use a function... I think it's more concise, readable and avoids duplication (of the assignment).

Copy link
Contributor

Choose a reason for hiding this comment

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

It also separates the logic, which I think is nice regardless of how many times it is called.

Copy link
Contributor

@Neme12 Neme12 May 25, 2018

Choose a reason for hiding this comment

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

If this was a method it could be reused by the code that deals with block bodies and return statements. There still has to be some modification to that because of the bug with local functions. Reusing this might be a nice solution.

Copy link
Member

Choose a reason for hiding this comment

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

Making it a method would be fine. As it is it it actually does not separate the logic and can't be reused since it's using parentSymbol directly.

Copy link
Contributor

@Neme12 Neme12 May 25, 2018

Choose a reason for hiding this comment

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

Yes I would prefer either a method or non-capturing local function instead personally

Copy link
Member

Choose a reason for hiding this comment

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

Please use another term than "Foo" in all the added tests. Thanks
Method(), local() or property would be fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Using property is not possible unfortunately, no code actions are available then. Which might actually be a bug, since it's not a reserved keyword as far as I know. Creating the property manually with the identifier property is just fine. Do you agree?

I'll just call it prop for now (and I'm working on all the other review comments now as well, I hope to finish it later today!).

Copy link
Contributor

Choose a reason for hiding this comment

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

Just call it P or whatever, it doesn't matter at all. It's not that Foo is discouraged because it's not descriptive. It's just a bad word for some reason and doesn't pass some kind of MS internal check.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I know, however, since we now have the opportunity to make the identifiers more descriptive as well, why not? I already did @jcouv's other rename suggestions too.

@jcouv jcouv added this to the 15.8 milestone May 25, 2018
Copy link
Contributor

@Neme12 Neme12 May 27, 2018

Choose a reason for hiding this comment

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

I don't think this is correct. If you have a local function inside a lambda, the previous checks will win (find the outer lambda) and this code will never reach. What we really want is the first ancestor that is either a lambda or a local function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'll create and add some tests, which should fail if your analysis is correct (good thinking either way!). I'll get to it later today.

Copy link
Contributor

Choose a reason for hiding this comment

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

nit: static

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Turns out there already is a private method for this purpose (and yes, that one IS static :-)). To prevent duplication, I'll use that one.

@rik-smeets rik-smeets changed the title Infer type in get accessor declaration and expression bodied local functions Infer type in get accessor declarations, expression bodied local functions and local functions inside lambda expressions May 27, 2018
Copy link
Contributor

@Neme12 Neme12 May 27, 2018

Choose a reason for hiding this comment

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

you can simplify this to e is AnonymousFunctionExpressionSyntax|| e is LocalFunctionStatementSyntax

Copy link
Contributor

@Neme12 Neme12 May 27, 2018

Choose a reason for hiding this comment

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

ParenthesizedLambdaExpressionSyntax and SimpleLambdaExpressionSyntax derive from LambdaExpressionSyntax which together with AnonymousMethodExpressionSyntax derive from AnonymousFunctionExpressionSyntax

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good one, I have updated it.

Copy link
Contributor

@Neme12 Neme12 May 27, 2018

Choose a reason for hiding this comment

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

note: SimpleLambdaExpressionSyntax can be async too, so the fact that isAsync = ... isn't here must be a bug (it probably doesn't unwrap the return type from Task)

Copy link
Contributor

Choose a reason for hiding this comment

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

you can remove this else if and handle both above if you check for is LambdaExpressionSyntax instead

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I fixed this too. Also did some minor improvements later in the method (pattern matching once more, yay!).

Copy link
Contributor

@Neme12 Neme12 May 27, 2018

Choose a reason for hiding this comment

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

This could likely be simplified too, because lambdas and anon. methods share all this info in AnonymousFunctionExpressionSyntax, but meh... this whole mess needs a rewrite 😄 you don't need to do that

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree, I'll leave that to you ;).

Copy link
Contributor

Choose a reason for hiding this comment

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

Not related to your change but I'd be really interested to know how we can return into a field.

@tmat
Copy link
Member

tmat commented May 30, 2018

test windows_release_vs-integration_prtes please

Copy link
Member

@jcouv jcouv left a comment

Choose a reason for hiding this comment

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

LGTM Thanks

@jinujoseph
Copy link
Contributor

Approved to merge for 15.8.Preview3

@jinujoseph jinujoseph merged commit 5a65f66 into dotnet:master Jun 4, 2018
@rik-smeets rik-smeets deleted the infer-type-in-expression-bodied-getter branch June 4, 2018 16:25
@Neme12
Copy link
Contributor

Neme12 commented Jun 4, 2018

@rik-smeets

Using property is not possible unfortunately, no code actions are available then. Which might actually be a bug, since it's not a reserved keyword as far as I know.

Did you open an issue for that? If not, please do.

@Neme12
Copy link
Contributor

Neme12 commented Jun 4, 2018

Customer generates a variable or method from within:

an expression bodied getter
an expression bodied local function/
a block bodied location function inside a lambda expression

Note: you actually fixed even more cases than that.
For example generating from an anonymous method inside a lambda
or generating from an async simple lambda - it didn't use to unwrap the task, now it does (in the code it looked like someone thought they cannot be async)

One thing that still isn't fixed though is unwrapping the Task from an expression bodied async method/function:

using System.Threading.Tasks;

class C
{
    async Task<int> M1()
    {
        return foo; // correctly generates int
    }

    async Task<int> M2() => foo; // generates Task<int>
}

Please create an issue for that too. Thanks.

@Neme12
Copy link
Contributor

Neme12 commented Jun 8, 2018

I filed both issues: #27646 #27647

@rik-smeets
Copy link
Contributor Author

Thanks for doing that @Neme12, I didn't have time for it lately.

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

Labels

Approved to merge Area-IDE Community The pull request was submitted by a contributor who is not a Microsoft employee.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants