dotnet / roslyn

The Roslyn .NET compiler provides C# and Visual Basic languages with rich code analysis APIs.
https://docs.microsoft.com/dotnet/csharp/roslyn-sdk/
MIT License
19.11k stars 4.04k forks source link

prefer var not working as expected #29657

Open vsfeedback opened 6 years ago

vsfeedback commented 6 years ago

when setting "When variable type is apparent" to "Prefer var" then using the FormatDocument functionality, the .cs file with the following line:

List<string> listOfStrings = myString.Split(',').ToList() 

Is formatted/rewritten to:

var listOfStrings = myString.Split(',').ToList()

I would expect it to leave the type definition and not replace List<string> with var.

I have used stylecop and resharper on and off over the years, and the general rule has always been if the type is somewhere to the right of the equals, use var (meaning it is apparent). If the type is not listed, use the explicit type.

If you guys change the way apparent is defined it is going to screw up our whole codebase. I love that you are adding functionality in VisualStudio, but please do not change this definition. Is this a bug or did someone figure if Linq is used and even if the type isn't on the line it is still apparent? If the latter is the case, one could argue for everything being a var.

This issue has been moved from https://developercommunity.visualstudio.com/content/problem/322157/prefer-var-not-working-as-expected.html VSTS ticketId: 672386 These are the original issue comments: (no comments) These are the original issue solutions: (no solutions)

CyrusNajmabadi commented 6 years ago

ToList is considered a 'conversion' method. Because it's "To" + Type-Name. So if you have methods like ToDateTime it's apparent that that's it's producing a 'DateTime'.

In this case there is 'ToList' which is producing a 'List'. So the type is apparent and 'var' is used.

--

This behavior was present when the feature was added here: https://github.com/dotnet/roslyn/blob/a534500c8f7d473fbfdb72e4ede133ae99c55af8/src/Features/CSharp/Portable/Diagnostics/Analyzers/CSharpTypingStyleDiagnosticAnalyzerBase.State.cs#L191

--

I have used stylecop and resharper on and off over the years, and the general rule has always been if the type is somewhere to the right of the equals, use var (meaning it is apparent). If the type is not listed, use the explicit type.

In this case, the type is considered to be on the right of the equals (because it's in 'ToList').

sharwell commented 6 years ago

I tend to agree with the original complaint; I've never been able to make sense of the rules for "type is apparent".

MarioAtTrimble commented 6 years ago

If you really are going to argue this, please give us another option, something like 'Use var only when type is present on line'

CyrusNajmabadi commented 6 years ago

I tend to agree with the original complaint; I've never been able to make sense of the rules for "type is apparent".

The rules are that the type 'Foo' is apparent if we see:

  1. new Foo
  2. new Foo[]
  3. (Foo)...
  4. ... as Foo
  5. Foo.StaticMethodThatReturnsAFoo(...). i.e. XElement.Parse
  6. ....ToFoo(...). i.e. Convert.ToDateTime, blah.ToList(),

Those are considered 'apparent'. Having there be a doc somewhere that mentions this would probably be good.

CyrusNajmabadi commented 6 years ago

I have used stylecop and resharper on and off over the years, and the general rule has always been if the type is somewhere to the right of the equals, use var (meaning it is apparent). If the type is not listed, use the explicit type.

What would be the difference between that and:

If you really are going to argue this, please give us another option, something like 'Use var only when type is present on line'

?

In all these cases, the system thinks the type is listed, and so is 'apparent'. What would be the difference between the current 'use var when apparent' and the new 'use var only when type is present on line'?

MarioAtTrimble commented 6 years ago

The type is nowhere on this line:

var listOfStrings = myString.Split(',').ToList()

I could just as easily create my own poorly named extension method called ToCollection() that returned List<>:

var listOfStrings = myString.Split(',').ToCollection()

Or maybe I could do this:

var listOfStrings = myString.Split(',').ToListOfTypeString()

All three examples above do not have a type on the line. Yes, the type is List, but it is obscured in the method name.

The point of this post is that tools I have been using for years require the type on the line. It doesn't matter if the type is called out in a method, if the ACTUAL TYPE is not on the line, var is not used. Coloring in Visual Studio is different for types and methods. To make code as quickly readable as possible, my team has the standard that the actual type (again, colored differently than methods) is on the line once and only once. This enables quick comprehension.

If you really feel that an extension method with the type name somewhere in the method name is apparent, then please give us another option.

The way VS now works is that

List<string> listOfStrings = myString.Split(',').ToList()

is rewritten to

var listOfStrings = myString.Split(',').ToList()

I am requesting that unless there is a type on the line, var not be used, so it would stay as:

List<string> listOfStrings = myString.Split(',').ToList()

CyrusNajmabadi commented 6 years ago

The type is nowhere on this line:

var listOfStrings = myString.Split(',').ToList()

It as, as per rule '6' on the list.

I could just as easily create my own poorly named extension method called ToCollection() that returned List<>:

In this case, the type would not be apparent. Because 'Collection' and 'List' are not the same types. If your method did return a Collection though, then it would be. ToBlah is a very common pattern in the .Net ecosystem to say: i'm converting what i have to an instance of 'Blah'.

Yes, the type is List, but it is obscured in the method name.

Apparent is in the eye of the behold. You think it is not apparent, i think it is. When i see IConvertible.ToDateTime, it's totally clear to me that that's returning a DateTime and i don't need the type name there.

Similarly, someone may think an as Blah at the end of a long, complex, multi-line expression is 'obscured'. But others may not.

I am requesting that unless there is a type on the line, var not be used, so it would stay as:

Would it be potentially better to just disable this Roslyn rule and use whatever other tools you want here to control this scenario? It would keep Roslyn from having to reverse engineer whatever rules other tools have, and from having to keep those in sync as other tools change over time.

MarioAtTrimble commented 6 years ago

The rules are that the type 'Foo' is apparent if we see:

  1. new Foo
  2. new Foo[]
  3. (Foo)...
  4. ... as Foo
  5. Foo.StaticMethodThatReturnsAFoo(...). i.e. XElement.Parse
  6. ....ToFoo(...). i.e. Convert.ToDateTime, blah.ToList(),

All tooling I have used for years disagrees with 6. Please give us an option that avoids this. I am not going to argue anymore, as the type is NOT on the line, it is buried in a method. You may think it is apparent, but tooling that has been around for years does not agree. I'm just asking you give us an option. You yourself even admit the type is not present:

IConvertible.ToDateTime, it's totally clear to me that that's returning a DateTime and i don't need the type name there.

All I'm asking is that you don't break out tooling. Please give us something like 'Use var only when type is present on line'

MarioAtTrimble commented 6 years ago

I also agree with you, the rules for what is 'apparent' would be great to have somewhere.

CyrusNajmabadi commented 6 years ago

All tooling I have used for years disagrees with 6.

I guess my point is this: if you want to use that tooling, feel free :) Roslyn is not expecting you to only follow it's rules here. It's an ecosystem full of options that are open for all to use.

You may think it is apparent, but tooling that has been around for years does not agree.

It is not a goal of Roslyn to match other tools and the decisions they've made in all these different areas. Different groups have different opinions on what the right behaviors are, and the designs flow from that.

You yourself even admit the type is not present:

That's not what i was saying. When i said "and i don't need the type name there", i was saying "i can use 'var' because its redundant to write DateTime d = c.ToDateTime() as the type is clearly present".

All I'm asking is that you don't break out tooling.

It's very unclear to me how your tooling is broken. These VS features are very easy to disable. The original bug mentions that this happens with Format-Document. In that case, you can just disable the following:

image

This will make the 'var' preference not be changed during a format-document call.

CyrusNajmabadi commented 6 years ago

Please give us something like 'Use var only when type is present on line'

I'm somewhat amenable to another option here. However, my default position is to not try to overload the product with tons of options unless there is a very strong need for it. I believe this feature itself has been around about 3 years now and this is the first piece of feedback about this. Indeed, IIRC, the feedback we've gotten in the past is about 'apparent' not showing up in more places. i.e. there are patterns people want supported (especially involving generics or more complex expressions) that are not currently thought to be 'apparent' by the current system.

In general, we've opted to try to limit options as it just introduces more complexity and maintenance costs, as well as significantly constraining implementation/design choices we can make in the future.

That said, if this was something a lot of people were reporting, i might feel differently about it and would be more open to supporting.

CyrusNajmabadi commented 6 years ago

Minor clarification question. When you say 'on line' what do you mean by that? Do you mean that the type would be apparent here:

var v = x.Foo().Bar() as Customer;

But it would not be apparent here:

var v =
    x.Foo().Bar() as Customer;

Or do you mean, 'as long as the type is the initializer for that variable'? I'm asking to make sure i'm on hte same page as you as to what when type is present on line means. Thanks!

CyrusNajmabadi commented 6 years ago

@sharwell You were pretty against me having fine-grained options for the 'parentheses preferences' PR :) What are you thoughts on breaking down the "type is apparent" option into options controlling these behaviors? I don't necessarily think they'd need to be part of the UI. But they could be .editorconfig options that someone could set if they wanted this fine-grained control.

Name TBD, but something like "type_is_apparent_with_method_called_to_type_name". It would be true by default, but could be set to 'false' by @MarioAtTrimble to opt-out of this behavior.

Thoughts?

sharwell commented 6 years ago

Assuming the list above is the complete set of rules for determining if something is apparent, it seems the only problem I have is with generics. List and List<T> are not the same type, and there's no question that T is not apparent under the current rules/examples above.

The rules for "apparent" should meet the following: The static type of var can be correctly stated by a reader by looking only at the initializing expression, without access to the rest of the file. I'm not sure the rules need a new configuration option, but we should consider updating the current rules if they fail to meet this.

CyrusNajmabadi commented 6 years ago

Assuming the list above is the complete set of rules for determining if something is apparent, it seems the only problem I have is with generics. List and List<T> are not the same type, and there's no question that T is not apparent under the current rules/examples above.

I believe this was discussed at hte time and the feeling was that the type was apparent. i.e. the critical bit was knowing you had a List, not the complete instantiation of that type. It's the same reason why "ToDateTime" is clear enough, even though i technically don't know which namespace "DateTime" is referring to. I don't need the function to be "ToSystemDateTime".

but we should consider updating the current rules if they fail to meet this.

I'm definitely worried about us going that route. Because that would make different versions of Roslyn incompatible with itself. i.e. a team could have different versions of VS and now have them fighting each other. Similarly if you had this be an error, and then upgraded, we might now take a whole bunch of code and say: this is wrong, you must use an explicit type. It's unclear to me if that would upset more people than the amount of people it makes happy.

idg10 commented 6 years ago

I think this comparison is flawed:

It's the same reason why "ToDateTime" is clear enough, even though i technically don't know which namespace "DateTime" is referring to. I don't need the function to be "ToSystemDateTime".

This essentially puts 'containing namespace' and 'type argument' in the same category. That seems arbitrary and, to me at least, surprising.

A big difference with the namespace-based example you give is that it is allowable (and indeed normal) not to namespace qualify in the case where you do make the variable type explicit. E.g., you can write this:

DateTime start = startStamp.ToDateTime();

Here, the explicit type name in the variable declaration looks identical to what appears after To on the right. Developers who routinely fully-namespace-qualify DateTime might get upset, but there are at least valid (and, apparently common) coding styles in which you don't include the System. prefix.

Compare that with a List<T> example:

List<int> ns = Enumerable.Range(1, 10).ToList()

There's a very clear difference here: the type name on the right (List<int>) does not appear in the initializer expression. Only part of it (List) does.

And unlike your DateTime example, this isn't simply a matter of choice. Whereas with DateTime I have the option to leave off the System., I do not have any equivalent option to leave off the <int>. I cannot write this:

// Will not compile
List ns = Enumerable.Range(1, 10).ToList()

So the comparison seems weak. Although it's true that in both cases, the text following To is not the full typename, the critical difference is that with DateTime, it is at least a permissible representation of the type name, whereas you don't get to use List as a shorthand version of List<int>.

In any case, the whole argument seems to miss the point. Isn't the heart of the matter this: why do people want to use an explicit type in in certain cases? Without surveying developers it's impossible to say for sure, so I can only add my 2c, but as someone who wants var if and only if the type is apparent, it's because when reviewing pull requests in a web browser (which is how I normally review them) I can't mouse-over something to find out its real type.

Here's the crux for me: I don't like var because it just means "thing", and I want to know what kind of thing; exactly the same logic applies if I have a collection—if all I have is var and ToList, that just means "things" and I want to know what kind of things.

So when the analyzer replaces List<int> xs = q.ToList(); with var xs = q.ToList();, I have gone from knowing that I have some ints, to knowing only that I have some things. This feels exactly equivalent from going from int x = Foo(); to var x = Foo(); in that I've gone from knowing that I have an int to knowing only that I have a thing.

To narrow it down further: I want to know what sort of thing the code is dealing with. So when it comes to List<int> the int part of that is actually more important than the List part. So saying that ToList makes the type explicit ignores the more important of the two types.

I actually find it hard to imagine why anyone would want int x but var xs in these examples. I understand reasonable people can disagree over the merits of var vs explicit typing, but I find it hard to think of any consistent position in which you'd want explicit typing most of the time, but wouldn't want to know the types of your things if they happen to be in collections that were returned by methods, and even then only if the method was called ToList. I'm genuinely curious to know: is there any data to suggest that there are people who (like me) want int x in the singular case, but who (unlike me) want var xs in the plural case.

It feels like the "var only when type is apparent" behaviour was implemented by someone who prefers var everywhere and doesn't really understand why some people want the behaviour in question.

I'd love to see a coherent argument for why this:

int x = GetNumber();
List<int> xs = GetNumbers();
var zs = ys.ToList();

is in any way internally consistent.

CyrusNajmabadi commented 6 years ago

This essentially puts 'containing namespace' and 'type argument' in the same category. That seems arbitrary and, to me at least, surprising.

Yes. It was arbitrary, but was based off the idea that for the majority of cases, such a name wasn't important enough, and having 'var' not be offered here would be against what people wanted. This was borne out by actually checking with a large set of people about when/how they would want 'var', and seeing that 'var' was preferred in these cases and that people did think of it as 'apparent'.

The bind we're now in is that once you pick this, it cannot be changed (without some other option being added). Because if you change this behavior, it breaks existing codebases that want 'apparent' to work this way.

One way forward would be to provide configuration options here. However, i'm fairly loathe to do that myself. I would prefer instead of just adding more and more knobs over time, we settle on a behavior that is years old at this point and just consider that to be how it works.

And unlike your DateTime example, this isn't simply a matter of choice. Whereas with DateTime I have the option to leave off the System., I do not have any equivalent option to leave off the . I cannot write this:

The perspective many had was that: 'generics' provide an instantiation of some type, but that type that is being used to instantiate is apparent enough. Needing to know the exact instantiation itself isn't that relevant. This was felt to be the case because the high ordre bit for someone was: "i need to know what sort of outer type i'm dealing with. For example, is this an IEnumerable? A List? An IList? An ImmutableArray?" This was the part that would affect their decision making the most. i.e. were operations going to be O(n) or O(1). Was there boxing, or not?

List<int> ns = Enumerable.Range(1, 10).ToList()

The core problem with these examples is that they apply to cases that rarely exist. (And, frankly, even in that case, i'd say: yup, it's totally apperant to me that that's a List<int>).

What we did for 'var' was to actually look at real world code and analyze cases and ask if people felt if things were apparent or not. And, in practice, in the vast majority of cases, the type was felt to be apparent here. That's why the heuristic was designed this way. Not for some sense of categorical purity, but to provide an overall reasonable place to land with only a small number of knobs (always, never, apparent), that then handled more cases properly and in line with expectations than not.

CyrusNajmabadi commented 6 years ago

I'd love to see a coherent argument for why this:

This, to me, is not a useful way of looking at the feature. It's not intended to be some sort of "first principled" approach to the problem. The problem was: there is a group of people that do not always want 'var'. They want it some of the time, and we'd like to have as few options as possible that satisfy as many as possible by having those options reflect what is wanted. By necessity of going with just a small set of options, there would invariably be people left out.

For example, some people have not liked the behavior here around static methods and using the type the static method exists on to dictate things. Or, using just weak form where the type's name is embedded in some larger identifier. There have been people who only want this to show up if the type truly shows up only as a type on the initializer. And so on and so forth.

But the goal was not to try to make it possible for every customer to express control over every part of this heuristic. The goal was to have a reasonably small set of options, and to have that do a good-enough job for the largest amount of people.

idg10 commented 6 years ago

The problem was: there is a group of people that do not always want 'var'.

Indeed, and as a member of that group, I feel you have misunderstood what we wanted. I might be wrong of course: if it turns out that I'm the one out on a limb here, and that the majority of my fellow slightly-var-averse (or more accurately, type-concealment-averse) types are actually happy with this, then fine. What I was hoping is that you would say that you have canvassed opinion on this and found that the majority of people nagging you for this var-only-when-non-information-hiding behavior do actually prefer the behaviour that we ended up with. (And TBH, at that point it's largely a matter of intellectual curiosity for me as to why other people have a different view. It's easier to be philosophical about ending up on the losing side of a decision if you can at least comprehend the other point of view. But right now I don't even know what the other point of view is. And maybe I could learn from it—perhaps if I understood why anyone would want the current behaviour, I might come round to it. I reserve the right to get smarter.)

But based on the information I currently have, it looks like this feature has failed to understand the goals of the very group it was supposed to support. Certainly, I've not yet heard from anyone who both a) wants "var if and only if the type is evident without it" and b) feels that the current behaviour does what they want. Maybe this table will help explain why I'm sceptical:

Developer Type a) b)
var-happy No Don't care
Fully-var-averse No Don't care
Likes var only when it avoids duplication Yes No
????? Yes Yes

You address the needs of developers in the first two rows perfectly: people who want var everywhere are fully catered for, as are people who will only tolerate var in scenarios where it is completely unavoidable (anonymous types). I'm in the 3rd row, as is everyone I know who prefers the setting we're discussing. But you have designed for row 4. I am as yet unconvinced that there is anyone in row 4.

By the way, a common feature of the people I know to be in row 3 is that they do a lot of code reviews. So another way to describe the requirement is "Don't force me to do type inference in my head during code reviews."

I believe this feature itself has been around about 3 years now and this is the first piece of feedback about this.

I thought this came in with code style enforcement, which was a VS 2017 feature wasn't it? So I think it's more like one and a half years. And for whatever reason, I only discovered this rather surprising ToList behaviour recently—I complained the moment I became aware of it. So apparently it's possible to use VS for a year and a half without coming across this problem. And at my current company, there has been a relatively recent process of migrating away from Resharper because all the new analyzers that have gradually been appearing in VS (e.g. "Convert implicit type to explicit type (C#)" which only appeared in 15.7) have meant that there's less and less incentive to use Resharper. So not everyone leapt onto this feature the minute it appeared last year.

Indeed, IIRC, the feedback we've gotten in the past is about 'apparent' not showing up in more places.

I am also a member of this group. This is not mutually exclusive with thinking that the behaviour being discussed in this issue is undesirable. (In fact, it's presumably only those of us who have reasons to be selective about use of var who are making the complaints you describe: nobody in rows 1 or 2 of the table above will be having these issues.)

E.g., with something like var logger = serviceProvider.GetRequiredService<ILogger>(); I consider the type ILogger to be apparent, and I think that's a case you currently don't handle as 'apparent'?

So it would be a mistake to interpret "We are hearing feedback asking for us to report more cases as apparent" as "There are no scenarios in which we overestimate the apparentness". In an ideal world,

One possible outcome of this discussion is: there is no evidence that row 4 in that table is a non-empty set, so maybe the current design is wrong, but most people don't care about this, so it's not a priority. (Or, more pithily: Wrong, but won't fix.) In which case, can I ask: is there a way to plug your own analyzers and fixers into VS in a way that code generation uses their rules? I did actually write my own analyzer and fixer back in 2016, because as far I was able to tell, this problem didn't have a solution 3 years ago. (It is available at https://www.nuget.org/packages/Flyntax.AvoidVar/ and it not only gets this right, for my definition of right, but it also identifies a wide range of additional cases as 'apparent'.) But the problem with it is that it didn't participate in any code generation. (Although this was more of a problem with https://www.nuget.org/packages/Flyntax.StoreCtorArg/ tbh.)

The big benefit of the VS Code Style Enforcement is that all these settings get honoured by code gen (unlike anything you attempt to enforce through analyzers). Is there a way for us to write analyzers and code fixes that get involved during code gen? If there is, then I'll happily update my existing analyzer which has the benefit of enabling me to define "right" however I please.

(Ideally, I'd also like some good way of managing settings for code style analyzers. I'd love to resurrect https://resharper-plugins.jetbrains.com/packages/OrderUsings/ as a Roslyn-based tool, but it currently relies on Resharper's settings management system, something for which there is apparently no corollary for VS analyzers and code fixes. And it too needs to get hooked into code gen scenarios to be usable.)

CyrusNajmabadi commented 6 years ago

What I was hoping is that you would say that you have canvassed opinion on this and found that the majority of people nagging you for this var-only-when-non-information-hiding behavior do actually prefer the behaviour that we ended up with

The feature when originally written involved discussions with lots of people and a lot of cases. Original approaches which did not cover examples like this ended up with enough feedback that people felt these were areas where they wanted 'var' even though in general they only felt they wanted 'var' for 'obvious' types.

We discussed if this should be a granular system, where people could have control over all these knobs and choices. But, eventually decided that it was better to just pick a set of cases that was felt to be 'apparent' and keep the feature limited to that.

and b) feels that the current behaviour does what they want.

The feature does what i want. :)

I'm in the 3rd row, as is everyone I know who prefers the setting we're discussing.

That is the intent of this setting. However, tehre is an open interpretation across many about what is considered 'duplication'. It's certainly understandable that that might mean 'the type including type args' must be contained somewhere in the code. But that's not how everyone felt. For example, for some people it was enough that the type not including type args was apparent in some manner. A great case for this were things like XXXX val = ImmutableArray.Create(...). This will generate an ImmutableArray<SomeType>, but there was enough belief by people that you should be able to use var here for XXXX instead of neeidng to say ImmutableArray<ArgType> val.

E.g., with something like var logger = serviceProvider.GetRequiredService(); I consider the type ILogger to be apparent, and I think that's a case you currently don't handle as 'apparent'?

Definitely an interesting case. I can see an argument being made for that as it's similar to the sort of thing we detect when looking for apparent types. That case wasn't something we thought of, and at the time, no one we discussed with (and no one since) has brought it up. If you'd like to open an issue on that, i think it would be worthwhile to be discussed.

CyrusNajmabadi commented 6 years ago

In which case, can I ask: is there a way to plug your own analyzers and fixers into VS in a way that code generation uses their rules?

Not currently. But you could definitely open an issue asking for that :)

sharwell commented 6 years ago

I'm hoping to get to this one in Monday's review.

I believe this feature itself has been around about 3 years now and this is the first piece of feedback about this

On this specific note, the feature's current behavior has bothered me from the start. However, since the IDE analyzers weren't written with compiler enforcement, I was able to generally get by with just disabling this rule. I don't see a way to enable this rule at compile time without changing the behavior.

The bind we're now in is that once you pick this, it cannot be changed (without some other option being added). Because if you change this behavior, it breaks existing codebases that want 'apparent' to work this way.

We are not in this bind yet, because the analyzer doesn't have any enforcement option today.

CyrusNajmabadi commented 6 years ago

doesn't have any enforcement option today.

IMO, it's incorrect to view things as binding only if the analyzer enforces. We're in a bind when we change behavior because people may want the behavior as it exists today. They may be in a 'zero warnings/messages' mode for their IDE. If we change behavior on something like this, that now ends up impacting them because they are now forced to either disable this, or go fix everything up.

IMO, if you are going to change behavior here, it should be with sub-options of 'apparent'. Allow people to decide what apparent means to them so that you dont' change it unilaterally for everyone, causing problems.

For example, i would be quite unhappy if i now had to go from var tokens = ImmutableArray.Create(block.OpenBraceToken, block.CloseBraceToken) to ImmutableArray<SyntaxToken> tokens = .... Yes, ImmutableArray<SyntaxToken> was not specified in full in the initializer. However, cases like these were considered and were viewed to be apparent enough to warrant this behavior.

IMO, this is like changing a muscle memory feature. It should not be done unless there is hugely good reason to do so. Once shipped, maintaining behavior is super important. If people think changes should happen they should be made in a manner that can suit both people who are used to the previous behavior and people who want new behavior that conflicts with the old behavior.

CyrusNajmabadi commented 6 years ago

We are not in this bind yet, because the analyzer doesn't have any enforcement option today.

And what about customers who do want to turn on enforcement but want the rules as they exist today since they've made all their code follow those rules? You've now put them in a position of stating that the code changes they've made are now not correct and need to be changed again to have enforcement.

To do this responsibly, i think this has to come through configurable options in control of the user. Perhaps with a 'default' value that aligns to the current approach we ship with. That also has the benefit of allowing us to add new things in the future (like serviceProvider.GetService<ILogger>) in a compatible fashion. This alone seems super relevant to me once we do have 'enforcement' as any change in behavior is now similar to the 'breaking change' issues the compiler has to deal with.

Eirenarch commented 5 years ago

I have always been strong proponent of this type of var usage. It seems like there are very few of us and I have yet to meet someone who thinks the current behavior is preferred. Everyone who says the current behavior is preferred seems to be in the "always var" camp to begin with. This particular behavior was the reason I setup my editor config to not report these although I want it badly but this is deal breaker. I'd rather scream at my team when I see it than have lists of invisible types all over my code. If a lot of people who use the option (i.e. they are not "always var" people) disagree then as @MarioAtTrimble suggest there is need for another option.

Also this hurts "I believe this feature itself has been around about 3 years now and this is the first piece of feedback about this"... Yeah my user voice feedback from 20 months ago which even had a reasonable number of upvotes was ignored. The very same migrated user voice item linked above was submitted by me.

CyrusNajmabadi commented 5 years ago

Also this hurts "I believe this feature itself has been around about 3 years now and this is the first piece of feedback about this"... Yeah my user voice feedback from 20 months ago which even had a reasonable number of upvotes was ignored. The very same migrated user voice item linked above was submitted by me.

Can you link me to that user voice item?

CyrusNajmabadi commented 5 years ago

I have yet to meet someone who thinks the current behavior is preferred.

I prefer the current behavior. We maybe have never met. But i certainly exist :D

Eirenarch commented 5 years ago

It is the GitHub issue you yourself linked, migrated from User Voice :) https://visualstudio.uservoice.com/forums/121579/suggestions/18612712

OK noted, you exist. Are you sure you are not "always var" person? :)

CyrusNajmabadi commented 5 years ago

If a lot of people who use the option (i.e. they are not "always var" people) disagree then as @MarioAtTrimble suggest there is need for another option.

Or, it may simply be: this just isn't important enough to warrant an additional option and it may just be necessary to 'disallow var' in your codebase.

The goal of roslyn is not to provide enough flexibility to satisfy every developer out therewith features out of the box. Instead, that's what the analyzer/fixer/refactoring framework is for. If the current approach is not satisfactory, then it's definitely encouraged and suitable to go and write your own alternative that has 'var/types' operating as you would like them to behave.

CyrusNajmabadi commented 5 years ago

Please provide a fix to this behavior. In addition code like this

MessageProcessorBase messageProcessor = MessageProcessorBase.GetMessageProcessor(eventType);

Now triggers a suggestion to use var. While the type is indeed mentioned on the right I am calling a static method and it may have returned something else.

But htat static method doesn't return something else. If it did, we wouldn't offer to use 'var' here. This only offered for static methods on a type that return instances of that type. i.e. XElement.Parse, etc.

--

Note that's never been necessary for the type name to be an exact match either. For example we got strong feedback that a method like foo.ToMessageProcessorBase() was enough indication that of what the type was. This is the crux of hte issue. As we can clearly see, not everyone agrees on what 'apperant' means. For many (especially when we discussed and talked with lots of people about this initially), there were lots who felt that the important thing was not to see the type literally on the right, but instead to see enough that it was apparent.

At the time, the belief among enough people was that type-arg inference was so apparent from context, that the end type was equally apparent. Indeed, in effectively all the examples i've seen so far, that holds up.

Now, i get that that isn't your view. And i understand the argument behind it. I'm just not comfortable with any approach that changes the defaults here, for the reasons i've outlined above.

CyrusNajmabadi commented 5 years ago

No :) And i've explained my reasoning here: https://github.com/dotnet/roslyn/issues/29657#issuecomment-435113426

As mentioned already, i'm not opposed to a new option being made available. I'm even ok with someone wanting to contribute this. But i'm heavily opposed to having the existing options change meanings across Roslyn version. IMO, that would be one of the worst approaches we could take. It would mean that you would literally have people opening the same code in different versions of VS and getting a disparate set of warnings. if one person fixed them up, the other person would now see warnings. Things like fix-all would stomp over each other.

If you want a sub-option to control whether or not type-arguments have to be equally apparent, then i'd be happy to work with you to get such an option in place.

Eirenarch commented 5 years ago

Your comment about existing code is totally valid, this is why another mode for this option may be a good idea as suggested.

As for the MessageProcessor/XElement.Parse example which I consider separate and much less impactful issue that I wouldn't bother to report if it wasn't for the similar but different ToList issue here is my logic: I think of the apparent var option like this - if I am looking at the code in notepad do I 100% know the type of the var? If yes - suggest var, if not - suggest explicit type. I now know how the tool works but when I encountered it for the first time I thought it might be a bug. I still feel the need to hover with my cursor over the method to check if the type is right when I see a code like this. Who knows what editorconfig options the code uses. Also what about when reading code on GitHub? This opinion of mine is probably too extreme but it advocates for very consistent behavior with no exceptions. As I said it doesn't bother me nearly as much as the ToList issue which literally made the option unusable for me. While I thought about building an analyzer as you suggested I decided that it is not worth it because what I really want is to share this option via editorconfig.

CyrusNajmabadi commented 5 years ago

if I am looking at the code in notepad do I 100% know the type of the var? If yes - suggest var, if not - suggest explicit type.

Here's the problem. Even on that that count people disagree. For example, there's been strong feedback that the following should use var:

ILogger logger = serviceProvider.GetService<ILogger>();

Many people think: "the type is apparent. i explicitly wrote it on the right. This should be var.

Whereas other people go: "there's no reason to be certain that GetService will return the same type as its type argument. So it's not apparent!"

There's the rub. it turns out there's enough of a set of people who do not want "100%". They want something more wishy washy like "i can reasonably tell, and i'm right enough of the time that typing var is silly". That group wants "XElement.Parse" to use 'var' (even though you might not be 100% sure it returns an XElement). They want "ToInt32" to use var (even though you don't know for certain it makes an int32". They want ImmutableArray.Create(token1, token2) to use var (even though technically that might not create the type of ImmutableArray you might expect.

And so on and so forth. The goal has never been "you are 100% sure of the type". The goal was "this fits the intuition people have about where they woudl be ok with 'var' because the type is reasonably apparent based on everything they know about the language".

Eirenarch commented 5 years ago

Yeah I know people disagree but I am an extremist, if I was designing the feature for myself I would make that example suggest explicit type. However I can compromise with this. I can't compromise with ToList so I currently turn the feature off.

CyrusNajmabadi commented 5 years ago

This opinion of mine is probably too extreme but it advocates for very consistent behavior with no exceptions.

Technically, what we have now is very consistent, with no exceptions :) The behavior is just the behavior. It's just that it is consistent on a set of rules that you happen to not like. I get that, but i'm not of the opinion that the strict set of rules are particularly desirable. If we went with those rules i think that would limit things to just:

  1. default(SomeType)
  2. expr as SomeType
  3. new SomeType ...
  4. new SomeType[] ...
  5. Maybe tuples built entirely out of the above (though i'm not even sure about that one).

That's it. That's the entire list. I'm sure some people will be ok with that. But it will be a big take-back from others who think that int.Parse is so obviously apparent that you get an int back (as well as the other cases). :)

--

Honestly, at this point, i may just try to do this work to see if something can be worked out there. I woudl prefer a third part approach. But it doesn't seem as if anyone is interested in pursuing that path right now.

Eirenarch commented 5 years ago

Doesn't editor config support more options than true/false. Can we have like 3 modes for this and even more in the future?

CyrusNajmabadi commented 5 years ago

Doesn't editor config support more options than true/false. Can we have like 3 modes for this and even more in the future?

It can, but this ties into: The goal of roslyn is not to provide enough flexibility to satisfy every developer out therewith features out of the box. Instead, that's what the analyzer/fixer/refactoring framework is for. If the current approach is not satisfactory, then it's definitely encouraged and suitable to go and write your own alternative that has 'var/types' operating as you would like them to behave.

That said, i may just try to whip something up here since it would probably be less effort to provide new functionality versus argue about it more :D

Yeah I know people disagree but I am an extremist

Roslyn really is trying to be a general purpose tool to fit the needs of a large but not complete set of individuals. Extension points are the way to let others plug in their own needs if necessary (and if they're particularly "extreme").

CyrusNajmabadi commented 5 years ago

@Eirenarch @MarioAtTrimble @idg10 Have started on a PR over here: https://github.com/dotnet/roslyn/pull/33651

A few questions i had for you all (or anyone else interested):

If we added a new option about only allowing var i the type was truly on the rhs would you expect to have to write:

Case 1

(int, int) x = ((int)a, (int)b) or would it be ok to write: var x = ((int)a, (int)b);?

In this case, the question is about if it's ok to use 'var' for tuple expressions if the individual constituent elements are totally explicit as to their types.

Case 2

XElement x = XElement.Parse(...); or would be it be ok to write: var x = XElement.Parse(...);?

In this case the question is about if it's ok to consider the type explicitly stated on the right if you're accessing what is effectively a static factory method through the type itself. This only applies to static members that return the same type as their container.

Case 3

ILogger logger = service.GetComponent<ILogger>(); or would it be ok to write: var logger = service.GetComponent<ILogger>();

In this case, the question is if it's ok to consider the type explicitly stated if there's a generic call on the right of the expression whose explicit type-arg and return type are the same.

Case 4

ImmutableArray<int> x = ImmutableArray.Create<int>(y); or would it be ok to write: var x = ImmutableArray.Create<int>(y);?

in this case the question is if it's ok to use a factory that is on a non-generic, but specifies a generic type-arg, if it produces a generic type with the same name and with that type arg.

Thanks!

orthoxerox commented 5 years ago

Yes to all 4 cases, the type is obvious to me in all of them.

I would even welcome case 5, where the type name is a substring of the method being called on the rhs.

MarioAtTrimble commented 5 years ago

IMO, var is fine in each since the actual type is on the line once already. That's the only rule I have - don't repeat the actual type more than once

On Mon, Feb 25, 2019 at 1:04 AM orthoxerox notifications@github.com wrote:

Yes to all 4 cases, the type is obvious to me in all of them.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/dotnet/roslyn/issues/29657#issuecomment-466908680, or mute the thread https://github.com/notifications/unsubscribe-auth/ANTX2efVXjOu34ghBHbAnqBpOgNvzCcCks5vQ5kJgaJpZM4WYbso .

-- Mario Ricci | Software Engineer | Trimble Navigation Limited | VirtualSite Solutions | Office: +1 (303) 635-8604 / x228604

Eirenarch commented 5 years ago

Case 1 - var x = ((int)a, (int)b); is perfectly fine Case 2 - I have a very minor preference for explicit type. Case 3 - Minor preference for explicit type Case 4 - prefer explicit type.

CyrusNajmabadi commented 5 years ago

IMO, var is fine in each since the actual type is on the line once

Note: In case '4' this is sorta the case. i.e. the inferred type is ImmutableArray<int>, but it appears on the right as ImmutableArray-.Create-<int>. But if at first glance this is felt to be the right thing for you, that's good to know.

Thanks all!

case1: everyone seems ok with 'var'. good to know!

For case '2' and '3' based on the fact that several people feel that 'var' is fine, and @Eirenarch only mildly prefers 'explicit type', i think having those use 'var' seems reasonable.

Case4 is def interesting. Some people are ok with it. @Eirenarch would like the explicit type. I keep flip flopping. Will have to figure out the right approach there. I mainly think 'var' is fine here since my brain keeps saying "it's totally obvious what the type is there!" :)

CyrusNajmabadi commented 5 years ago

Here's where i'm landing on things:

"Allow var when type is explicit in initializer":

Initializer Expression Allow var Disallow Uncertain
default(SomeType) ✔️
new SomeType(...) ✔️
new SomeType[...] { ... } ✔️
expr as SomeType ✔️
(SomeType)expr ✔️
GetService<SomeType>(...) (Call has to return SomeType) ✔️
SomeType.FactoryMethod(...) (Call has to return SomeType) ✔️
((int)a, (int)b) (tuple, where all element types are explicit) ✔️
ImmutableArray.Create<int> (Name and generics are split) ✔️
.ToString() (Type name included after .To) ✔️
ImmutableArray.Create(...) (Call does not include generics) ✔️

Are there other cases people care deeply about (either that they should definitely allow var, or that they definitely should not allow var).

Eirenarch commented 5 years ago

I assume (5, "test") would be considered obvious type for a tuple and allow var?

CyrusNajmabadi commented 5 years ago

I assume (5, "test") would be considered obvious type for a tuple and allow var?

So currently literals are in the category of built-in-types. So if you say "it's ok to use 'var' for a builtin type/literal" then it's ok to use 'var' for that tuple. If you say you do not want 'var' for builtin type/literal, then we will not consider it ok here.

Note: tuples+var+builtins is definitely a bit unclear on what the desired behavior is (esp. as it relates (or doesn't!) to the other option). So I would def love to hear your thoughts here so we can implement as 'correctly' as possible and so we don't have to change this down the line.

Thanks!

Eirenarch commented 5 years ago

To be fair tuples are the one case where I had doubts of changing to var everywhere. The whole idea of constructing a type on the fly starts to lose its attractiveness when you have to annotate with types kind of like why Java's anonymous classes are practically unusable and lambdas are everywhere. I'd personally support more vars around tuples.

CyrusNajmabadi commented 5 years ago

I'd personally support more vars around tuples.

But i assume that wouldn't apply to:

... x = (florb(), bippity.boppity.Boo());

Right?

In this case, the type of the tuple would be completely unclear and not at all explicit from the RHS. So this would have to have a spelled out type right?

We're only discussing the times when at least the type of the tuple seems moderately clear from it's constructor, right?

(Just trying to make sure i'm on the same page as you. It's been made totally clear to me that my gut feelings on what is 'explicit/apparent' doesn't necessarily align with others. so i'm trying hard to not assume and to make sure i'm hearing things properly). Thanks!

Eirenarch commented 5 years ago

Yes, with this feature this case should definitely require explicit type in this case. I was just mentioning my personal doubts about the whole "explicit types" thing.

peter-dolkens commented 5 years ago

Wouldn't it be easier to simply make the 6 rules configurable in .editorconfig?

This entire discussion would have been over months ago.

Without getting into the individual rules, I can definitely see why some people disagree that unknownType.ToList() is the same as unknownType.ToList<Int32>()

It was mentioned above that:

If the current approach is not satisfactory, then it's definitely encouraged and suitable to go and write your own alternative that has 'var/types' operating as you would like them to behave.

And yet,

In which case, can I ask: is there a way to plug your own analyzers and fixers into VS in a way that code generation uses their rules?

Not currently. But you could definitely open an issue asking for that :)

Based on that, I'd say roslyn has failed in its mission if we're telling people to go write their own alternatives for a system that hasn't been put in place, instead of adding a few config flags to an .editorconfig.

CyrusNajmabadi commented 5 years ago

Wouldn't it be easier to simply make the 6 rules configurable in .editorconfig?

Not really. Now you have a confluence of how all those rules combine together. More options is not automatically a better thing.

Based on that, I'd say roslyn has failed in its mission if we're telling people to go write their own alternatives for a system that hasn't been put in place, instead of adding a few config flags to an .editorconfig.

There is no mission in Roslyn to support all use cases for all users. Never has been, never will be.

However, one of Roslyn's missions is: if you want to be able to create your own domains-specific rules, and/or you want to be able to share those with others, you should be able to. And, in that regard, you can.

It's a total non goal that you be able to tweak/change/customize every bit of all the features the Roslyn IDE (which itself just sits on top of the core Roslyn compiler APIs) provides.