From 7248e2cf2d1978364519eb47364738f2ebcdfa5c Mon Sep 17 00:00:00 2001 From: Andy Gocke Date: Thu, 17 Nov 2016 22:11:03 -0800 Subject: [PATCH] Implement async and void keyword completion for local functions (#15211) This PR implements completion for the simple cases of async and void in a local function context. What it misses cases where parsing incorrectly considers bare modifiers to be local variable declaration statements rather than local function declaration statements. This is tracked by bug #14525. Fixes #8616 Fixes #8617 --- .../AsyncKeywordRecommenderTests.cs | 114 ++++++++++++++++ .../VoidKeywordRecommenderTests.cs | 125 +++++++++++++++++- .../AsyncKeywordRecommender.cs | 9 +- .../VoidKeywordRecommender.cs | 3 +- .../ContextQuery/SyntaxTreeExtensions.cs | 40 ++++++ .../Extensions/SyntaxTreeExtensions.cs | 13 +- .../Portable/Utilities/SyntaxKindSet.cs | 6 + 7 files changed, 299 insertions(+), 11 deletions(-) diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/AsyncKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/AsyncKeywordRecommenderTests.cs index 8c112bb88e4..5a321ba27fb 100644 --- a/src/EditorFeatures/CSharpTest2/Recommendations/AsyncKeywordRecommenderTests.cs +++ b/src/EditorFeatures/CSharpTest2/Recommendations/AsyncKeywordRecommenderTests.cs @@ -128,6 +128,120 @@ public async Task TestNotAfterPartialInClass() class Foo { partial $$ +}"); + } + + [Fact] + [WorkItem(8616, "https://github.com/dotnet/roslyn/issues/8616")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + public async Task TestLocalFunction() + { + await VerifyKeywordAsync(@" +class Foo +{ + public void M() + { + $$ + } +}"); + } + + [Fact] + [WorkItem(14525, "https://github.com/dotnet/roslyn/issues/14525")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction2() + { + await VerifyKeywordAsync(@" +class Foo +{ + public void M() + { + unsafe $$ + } +}"); + } + + [Fact] + [WorkItem(14525, "https://github.com/dotnet/roslyn/issues/14525")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction3() + { + await VerifyKeywordAsync(@" +class Foo +{ + public void M() + { + unsafe $$ void L() { } + } +}"); + } + + [Fact] + [WorkItem(8616, "https://github.com/dotnet/roslyn/issues/8616")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction4() + { + await VerifyKeywordAsync(@" +class Foo +{ + public void M() + { + $$ void L() { } + } +}"); + } + + [Fact] + [WorkItem(8616, "https://github.com/dotnet/roslyn/issues/8616")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction5() + { + await VerifyKeywordAsync(@" +class Foo +{ + public void M(Action a) + { + M(async () => + { + $$ + }); + } +}"); + } + + [Fact] + [WorkItem(8616, "https://github.com/dotnet/roslyn/issues/8616")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction6() + { + await VerifyAbsenceAsync(@" +class Foo +{ + public void M() + { + int $$ + } +}"); + } + + [Fact] + [WorkItem(8616, "https://github.com/dotnet/roslyn/issues/8616")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction7() + { + await VerifyAbsenceAsync(@" +class Foo +{ + public void M() + { + static $$ + } }"); } } diff --git a/src/EditorFeatures/CSharpTest2/Recommendations/VoidKeywordRecommenderTests.cs b/src/EditorFeatures/CSharpTest2/Recommendations/VoidKeywordRecommenderTests.cs index 0f96afeb59f..e521a19b618 100644 --- a/src/EditorFeatures/CSharpTest2/Recommendations/VoidKeywordRecommenderTests.cs +++ b/src/EditorFeatures/CSharpTest2/Recommendations/VoidKeywordRecommenderTests.cs @@ -83,13 +83,6 @@ public async Task TestNotInCastType2() @"var str = (($$)items) as string;")); } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] - public async Task TestNotInEmptyStatement() - { - await VerifyAbsenceAsync(AddInsideMethod( -@"$$")); - } - [Fact, Trait(Traits.Feature, Traits.Features.KeywordRecommending)] public async Task TestInTypeOf() { @@ -642,5 +635,123 @@ public async Task TestNotAfterAsyncAsType() { await VerifyAbsenceAsync(@"class c { async async $$ }"); } + + [Fact] + [WorkItem(8617, "https://github.com/dotnet/roslyn/issues/8617")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction() + { + await VerifyKeywordAsync(@" +class C +{ + void M() + { + $$ + } +}"); + } + + [Fact] + [WorkItem(8617, "https://github.com/dotnet/roslyn/issues/8617")] + [WorkItem(14525, "https://github.com/dotnet/roslyn/issues/14525")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction2() + { + await VerifyKeywordAsync(@" +class C +{ + void M() + { + async $$ + } +}"); + } + + [Fact] + [WorkItem(8617, "https://github.com/dotnet/roslyn/issues/8617")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction3() + { + await VerifyAbsenceAsync(@" +class C +{ + void M() + { + async async $$ + } +}"); + } + + [Fact] + [WorkItem(8617, "https://github.com/dotnet/roslyn/issues/8617")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction4() + { + await VerifyAbsenceAsync(@" +class C +{ + void M() + { + var async $$ + } +}"); + } + + [Fact] + [WorkItem(8617, "https://github.com/dotnet/roslyn/issues/8617")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction5() + { + await VerifyAbsenceAsync(@" +using System; +class C +{ + void M(Action a) + { + M(async $$ () => + } +}"); + } + + [Fact] + [WorkItem(8617, "https://github.com/dotnet/roslyn/issues/8617")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction6() + { + await VerifyKeywordAsync(@" +class C +{ + void M() + { + unsafe async $$ + } +}"); + } + + [Fact] + [WorkItem(8617, "https://github.com/dotnet/roslyn/issues/8617")] + [Test.Utilities.CompilerTrait(Test.Utilities.CompilerFeature.LocalFunctions)] + [Trait(Traits.Feature, Traits.Features.KeywordRecommending)] + public async Task TestLocalFunction7() + { + await VerifyKeywordAsync(@" +using System; +class C +{ + void M(Action a) + { + M(async () => + { + async $$ + }) + } +}"); + } } } diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AsyncKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AsyncKeywordRecommender.cs index 7b20dcdf7e9..cd4179ae33f 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AsyncKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/AsyncKeywordRecommender.cs @@ -21,8 +21,13 @@ protected override bool IsValidContext(int position, CSharpSyntaxContext context return true; } - return !context.TargetToken.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword) - && InMemberDeclarationContext(position, context, cancellationToken); + if (context.TargetToken.IsKindOrHasMatchingText(SyntaxKind.PartialKeyword)) + { + return false; + } + + return InMemberDeclarationContext(position, context, cancellationToken) + || context.SyntaxTree.IsLocalFunctionDeclarationContext(position, cancellationToken); } private static bool InMemberDeclarationContext(int position, CSharpSyntaxContext context, CancellationToken cancellationToken) diff --git a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VoidKeywordRecommender.cs b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VoidKeywordRecommender.cs index a78636245a1..079c0cd9ffc 100644 --- a/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VoidKeywordRecommender.cs +++ b/src/Features/CSharp/Portable/Completion/KeywordRecommenders/VoidKeywordRecommender.cs @@ -45,7 +45,8 @@ protected override bool IsValidContext(int position, CSharpSyntaxContext context IsUnsafeParameterTypeContext(context) || IsUnsafeCastTypeContext(context) || IsUnsafeDefaultExpressionContext(context, cancellationToken) || - context.IsFixedVariableDeclarationContext; + context.IsFixedVariableDeclarationContext || + context.SyntaxTree.IsLocalFunctionDeclarationContext(position, cancellationToken); } private bool IsUnsafeDefaultExpressionContext(CSharpSyntaxContext context, CancellationToken cancellationToken) diff --git a/src/Workspaces/CSharp/Portable/Extensions/ContextQuery/SyntaxTreeExtensions.cs b/src/Workspaces/CSharp/Portable/Extensions/ContextQuery/SyntaxTreeExtensions.cs index b53c2a9ca55..bb2317df6ea 100644 --- a/src/Workspaces/CSharp/Portable/Extensions/ContextQuery/SyntaxTreeExtensions.cs +++ b/src/Workspaces/CSharp/Portable/Extensions/ContextQuery/SyntaxTreeExtensions.cs @@ -292,6 +292,46 @@ public static bool IsAttributeNameContext(this SyntaxTree syntaxTree, int positi return false; } + public static bool IsLocalFunctionDeclarationContext( + this SyntaxTree syntaxTree, + int position, + CancellationToken cancellationToken) + { + var leftToken = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken); + var token = leftToken.GetPreviousTokenIfTouchingWord(position); + + // Local functions are always valid in a statement context + if (syntaxTree.IsStatementContext(position, leftToken, cancellationToken)) + { + return true; + } + + // Also valid after certain modifiers + var validModifiers = SyntaxKindSet.LocalFunctionModifiers; + + var modifierTokens = syntaxTree.GetPrecedingModifiers( + position, token, out int beforeModifiersPosition); + + if (modifierTokens.IsSubsetOf(validModifiers)) + { + if (token.HasMatchingText(SyntaxKind.AsyncKeyword)) + { + // second appearance of "async" not followed by modifier: treat as type + if (syntaxTree.GetPrecedingModifiers(token.SpanStart, token, cancellationToken) + .Contains(SyntaxKind.AsyncKeyword)) + { + return false; + } + } + + leftToken = syntaxTree.FindTokenOnLeftOfPosition(beforeModifiersPosition, cancellationToken); + token = leftToken.GetPreviousTokenIfTouchingWord(beforeModifiersPosition); + return syntaxTree.IsStatementContext(beforeModifiersPosition, token, cancellationToken); + } + + return false; + } + public static bool IsTypeDeclarationContext( this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition, CancellationToken cancellationToken) { diff --git a/src/Workspaces/CSharp/Portable/Extensions/SyntaxTreeExtensions.cs b/src/Workspaces/CSharp/Portable/Extensions/SyntaxTreeExtensions.cs index d7b5bbf0fc2..3b18d667263 100644 --- a/src/Workspaces/CSharp/Portable/Extensions/SyntaxTreeExtensions.cs +++ b/src/Workspaces/CSharp/Portable/Extensions/SyntaxTreeExtensions.cs @@ -16,7 +16,17 @@ namespace Microsoft.CodeAnalysis.CSharp.Extensions internal static partial class SyntaxTreeExtensions { public static ISet GetPrecedingModifiers( - this SyntaxTree syntaxTree, int position, SyntaxToken tokenOnLeftOfPosition, CancellationToken cancellationToken) + this SyntaxTree syntaxTree, + int position, + SyntaxToken tokenOnLeftOfPosition, + CancellationToken cancellationToken) + => syntaxTree.GetPrecedingModifiers(position, tokenOnLeftOfPosition, out var _); + + public static ISet GetPrecedingModifiers( + this SyntaxTree syntaxTree, + int position, + SyntaxToken tokenOnLeftOfPosition, + out int positionBeforeModifiers) { var token = tokenOnLeftOfPosition; token = token.GetPreviousTokenIfTouchingWord(position); @@ -58,6 +68,7 @@ internal static partial class SyntaxTreeExtensions break; } + positionBeforeModifiers = token.FullSpan.End; return result; } diff --git a/src/Workspaces/CSharp/Portable/Utilities/SyntaxKindSet.cs b/src/Workspaces/CSharp/Portable/Utilities/SyntaxKindSet.cs index 65049712827..4207b933c72 100644 --- a/src/Workspaces/CSharp/Portable/Utilities/SyntaxKindSet.cs +++ b/src/Workspaces/CSharp/Portable/Utilities/SyntaxKindSet.cs @@ -52,6 +52,12 @@ internal class SyntaxKindSet SyntaxKind.VolatileKeyword, }; + public static readonly ISet LocalFunctionModifiers = new HashSet(SyntaxFacts.EqualityComparer) + { + SyntaxKind.AsyncKeyword, + SyntaxKind.UnsafeKeyword + }; + public static readonly ISet AllTypeDeclarations = new HashSet(SyntaxFacts.EqualityComparer) { SyntaxKind.InterfaceDeclaration, -- GitLab