Skip to content

MakeMethodAsync: don't rename entry point methods#26316

Merged
jcouv merged 7 commits intodotnet:masterfrom
jcouv:async-main
Apr 26, 2018
Merged

MakeMethodAsync: don't rename entry point methods#26316
jcouv merged 7 commits intodotnet:masterfrom
jcouv:async-main

Conversation

@jcouv
Copy link
Copy Markdown
Member

@jcouv jcouv commented Apr 21, 2018

Customer scenario

Invoked MakeMethodAsync fixer on Main method. The method should be left as Main rather than renamed to MainAync.

Bugs this fixes

Fixes #26312

Workarounds, if any

Edit the name back.

Risk

Performance impact

Low. The fix is limited to the MakeMethodAsync fixer.

Is this a regression from a previous update?

No.

How was the bug found?

Reported by customer.

@jcouv jcouv added the Area-IDE label Apr 21, 2018
@jcouv jcouv added this to the 15.8 milestone Apr 21, 2018
@jcouv jcouv requested a review from a team as a code owner April 21, 2018 23:41
class Program
{
public static async void Main()
{
Copy link
Copy Markdown
Contributor

@Neme12 Neme12 Apr 22, 2018

Choose a reason for hiding this comment

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

please add another test for the version of this feature that changes the return type to Task
image
by removing the "index: 1"

thanks

Copy link
Copy Markdown
Contributor

@Neme12 Neme12 Apr 22, 2018

Choose a reason for hiding this comment

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

also, i would personally consider disabling the "stay void" version entirely here because the resulting code doesn't make sense

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good idea. Thanks

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

also, i would personally consider disabling the "stay void" version entirely here because the resulting code doesn't make sense

Sorry, can you explain that? Why does "stay void" need to be disabled . We added "stay void" because we got a bunch of user feedback from people saying they wanted "fire and forget" async methods. and they didn't like being forced to make the return type 'Task'. 'async void Main' seems like a prime candidate for something people would want ot keep 'void'.

IN other words, i don't see a reason for 'async Main' to behave differently from the rules we have for all other methods. Without a strong good reason, i would not special case it here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@CyrusNajmabadi (resolved in a comment below) async void Main is a compiler error: error CS4009: A void or int returning entry point cannot be async

new MyCodeAction(GetMakeAsyncTaskFunctionResource(), c => FixNodeAsync(
context.Document, diagnostic, keepVoid: false, cancellationToken: c)),
context.Document, diagnostic, keepVoid: false, isEntryPoint, cancellationToken: c)),
context.Diagnostics);
Copy link
Copy Markdown
Contributor

@Neme12 Neme12 Apr 22, 2018

Choose a reason for hiding this comment

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

nit: it looks to me like this code is exactly the same as in the other branch. could this be simplified to offer this fix unconditionally and then have a check if (isOrdinaryOrLocalFunction && symbol.ReturnsVoid && !isEntryPoint) to additionally offer the "keep void" fix?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks!


var symbol = semanticModel.GetDeclaredSymbol(node, cancellationToken) as IMethodSymbol;
var entryPoint = compilation.GetEntryPoint(cancellationToken);
bool isEntryPoint = entryPoint?.Equals(symbol) == true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why not remove hte "as IMethodSybol" and then just do: var isEntryPoint = symbol.Equals(entryPoint)?

Also, what's the perf of "GetEntryPoint"? wouldn't it have to scan the entire compilation? Should we instead compute this during the fix itself?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"var isEntryPoint" plz :)

Copy link
Copy Markdown
Contributor

@CyrusNajmabadi CyrusNajmabadi Apr 22, 2018

Choose a reason for hiding this comment

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

Ok, i looked at GetEntryPoint. CUrrently it's very expensive. It realizes every namespace, namedtype and member symbol in the compilation.

Note: it could be made much faster through use of hte Decl table. The decl table already stores the names of children in the SingleTypeDeclaration node. So it would be possible to only examine types that actually included a member called 'Main' in it.

With such a change i would be ok with teh expensive GetEntryPoint being called here. Otherwise, i don't think ti's a good idea to do something so expensive.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Note: we already wrote a function to find symbols efficiently by name in the compilation by using:

GetSymbolsWithName. Unfortunately, because it takes a predicate and not a string, it needs to realize a type to get all the member symbols. That code could be augmented to just take a string, in which case it could efficiently check if it even needed to check the members of a type or not.

// If it's a void returning method (and not an entry point), also offer to keep the void return type
bool isOrdinaryOrLocalFunction = symbol.IsOrdinaryMethodOrLocalFunction();
if (isOrdinaryOrLocalFunction && symbol.ReturnsVoid)
if (isOrdinaryOrLocalFunction && symbol.ReturnsVoid && !isEntryPoint)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why would we gate this on isEntryPoint. Can you have "async Task Main"? the only rule is that it is still named Main and not MainAsync right? it can be 'async void' or 'async Task'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@CyrusNajmabadi You can't have async void Main, it is a compiler error (at first I thought it would just get treated as if the method isn't awaited and the program would exit prematurely, which would have been even worse because it could trip people up - that's probably why it's explicitly disallowed)

Copy link
Copy Markdown
Contributor

@Neme12 Neme12 Apr 22, 2018

Choose a reason for hiding this comment

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

With async Task Main on the other hand, most people will get "async main isn't supported in 7.0, please upgrade to 7.1..." so at least we're making people upgrade and use new language features, which sounds good to me.

If they do not want to, they will have to come up with a solution such as .GetAwaiter().GetResult() themselves. We don't currently generate that in any case, and it's probably not worth addign that, especially when there is a better way.

Copy link
Copy Markdown
Contributor

@Neme12 Neme12 Apr 22, 2018

Choose a reason for hiding this comment

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

We could of course have a separate feature that would replace await with .GetAwaiter().GetResult() instead of making the method async. I'm not sure if we should encourage people to do that though.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

That's fair. That said, i would still not want thsi PR as is with the expensive check during analysis. I can see about making a faster version of GetEntryPoint that would make it ok to check during analysis.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@CyrusNajmabadi Yes, the check is probably more costly than most, but it's in a fixer, not an analyzer, so I don't think it would have much impact.
Alternatively, as simple enough heuristic is to check if the method is called Main (and maybe check that it is public and static too). I'm happy to do that (that was my initial approach).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jcouv Main doesn't have to be public

Copy link
Copy Markdown
Contributor

@Neme12 Neme12 Apr 22, 2018

Choose a reason for hiding this comment

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

I would prefer if we created an actual helper that would check if the method is named "Main", is static, the return type is either void, int, Task or Task<int> and it takes either no parameters or one string[] so that we could reuse this helper in other places. But I guess that can be done later, so it's up to you.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

but it's in a fixer, not an analyzer, so I don't think it would have much impact.

Fixers have a large impact :) both for computing if we should have a lightbulb and for expandin ghte lightbulb. It's the actual fix that can be a bit slower (as it will have an actual dialog telling the user it's doing the update).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yes. I would also be ok with an approach where we first checked if this looked like 'Main' and then did the expensive check then.

That said, i have a PR to make GetEntryPoint fast over here: #26325

{
return await RenameThenAddAsyncTokenAsync(
keepVoid, document, node, methodSymbolOpt, cancellationToken).ConfigureAwait(false);
keepVoid, isEntryPoint, document, node, methodSymbolOpt, cancellationToken).ConfigureAwait(false);
Copy link
Copy Markdown
Contributor

@CyrusNajmabadi CyrusNajmabadi Apr 22, 2018

Choose a reason for hiding this comment

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

instead of passing this in here, wouldn't you just want to not even do the rename if this is the entrypoint? i.e. hoist !isEntryPoint into the if-check.

If you do that, you'll just fall below to where the async token is added, which is waht you want, right?

var symbol = semanticModel.GetDeclaredSymbol(node, cancellationToken) as IMethodSymbol;
var entryPoint = compilation.GetEntryPoint(cancellationToken);
bool isEntryPoint = entryPoint?.Equals(symbol) == true;
bool isEntryPoint = symbol != null && symbol.IsStatic && string.Equals(symbol.Name, "Main", StringComparison.Ordinal);
Copy link
Copy Markdown
Contributor

@Neme12 Neme12 Apr 23, 2018

Choose a reason for hiding this comment

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

StringComparison.Ordinal does this work for VB which should be case insensitive?

}

var symbol = semanticModel.GetDeclaredSymbol(node, cancellationToken) as IMethodSymbol;
bool isEntryPoint = symbol != null && symbol.IsStatic && string.Equals(symbol.Name, "Main", StringComparison.Ordinal);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Considering the large conversation below about performance, should this at least do a quick check to see if CompilationOptions.MainTypeName is set, and if so, bring that into consideration here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think you are proposing:

  1. if MainTypeName is set, then do expensive check
  2. otherwise, use heuristic (cheap check on method to see if it looks like Main)

I'm ok to rely purely on heuristic (only doing (2) above), since using an entry point other than Main seems uncommon.

=> node.IsAsyncSupportingFunctionSyntax();

protected override bool IsLikelyEntryPointName(string methodName)
=> string.Equals(methodName, "Main", StringComparison.Ordinal);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

general IDE pattern is to get teh ISyntaxFactsService off of the Document you're starting with, then do syntaxFacts.StringComparer.Equals(...). This does the right thing for VB and C# and needs no specific delegation like you have here.

}

var symbol = semanticModel.GetDeclaredSymbol(node, cancellationToken) as IMethodSymbol;
bool isEntryPoint = symbol != null && symbol.IsStatic && IsLikelyEntryPointName(symbol.Name);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

var

Copy link
Copy Markdown
Contributor

@CyrusNajmabadi CyrusNajmabadi left a comment

Choose a reason for hiding this comment

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

LGTM with the change i recommended.

element.SetAttributeValue(CheckOverflowAttributeName, true);
}

if (options.OutputKind != OutputKind.DynamicallyLinkedLibrary)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is there reason why you set this only when kind is not dynamically linked library? you could always set it? even though default is set to dynamicallylinked library?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

No strong reason. Since the default OutputKind is DynamicallyLinkedLibrary, I figured it's not useful to do more work in the common case.

}

var symbol = semanticModel.GetDeclaredSymbol(node, cancellationToken) as IMethodSymbol;
var isEntryPoint = symbol != null && symbol.IsStatic && IsLikelyEntryPointName(symbol.Name, context.Document);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

very minor nit: since this is a heuristic, it might be worthwhile to mention taht it's not totally accurate, but we're ok with the unlikely false positive for this feature.

var syntaxPath = new SyntaxPath(node);

// Rename the method to add the 'Async' suffix, then add the 'async' keyword.
// Rename the method to add the 'Async' suffix (except if it's the entry point), then add the 'async' keyword.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

comment stale.

@CyrusNajmabadi
Copy link
Copy Markdown
Contributor

Only nits about the comments. don't care if they get fixed.

@jcouv
Copy link
Copy Markdown
Member Author

jcouv commented Apr 26, 2018

@jinujoseph Please approve for 15.8. Thanks

@jcouv jcouv self-assigned this Apr 26, 2018
@jcouv
Copy link
Copy Markdown
Member Author

jcouv commented Apr 26, 2018

test windows_debug_unit64_prtest please

@jcouv jcouv merged commit bab0f7e into dotnet:master Apr 26, 2018
@jcouv jcouv deleted the async-main branch April 26, 2018 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactoring "Make method async" should not add suffix "Async" to main method

6 participants