diff --git a/src/EditorFeatures/CSharpTest/UseLocalFunction/UseLocalFunctionTests.cs b/src/EditorFeatures/CSharpTest/UseLocalFunction/UseLocalFunctionTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..92139273ceffb6b09a0c8d0dfa7ec472ec2eb652 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/UseLocalFunction/UseLocalFunctionTests.cs @@ -0,0 +1,1148 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.UseLocalFunction; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.UseLocalFunction +{ + public partial class UseLocalFunctionTests : AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest + { + internal override (DiagnosticAnalyzer, CodeFixProvider) CreateDiagnosticProviderAndFixer(Workspace workspace) + => (new CSharpUseLocalFunctionDiagnosticAnalyzer(), new CSharpUseLocalFunctionCodeFixProvider()); + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestMissingBeforeCSharp7() + { + await TestMissingAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = v => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }; + } +}", parameters: new TestParameters(parseOptions: CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp6))); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestMissingIfWrittenAfter() + { + await TestMissingAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = v => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }; + + fibonacci = null; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestMissingIfWrittenInside() + { + await TestMissingAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = v => + { + fibonacci = null; + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestMissingForErrorType() + { + await TestMissingAsync( +@"class C +{ + void M() + { + // Func can't be bound. + Func [||]fibonacci = v => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestMissingForMultipleVariables() + { + await TestMissingAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = v => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }, fib2 = x => x; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestMissingForField() + { + await TestMissingAsync( +@"using System; + +class C +{ + Func [||]fibonacci = v => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }; +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestSimpleInitialization_SimpleLambda_Block() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = v => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }; + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + } + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestSimpleInitialization_ParenLambdaNoType_Block() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = (v) => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }; + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + } + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestSimpleInitialization_ParenLambdaWithType_Block() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = (int v) => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }; + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + } + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestSimpleInitialization_SimpleLambda_ExprBody() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = v => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2); + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2); + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestSimpleInitialization_ParenLambdaNoType_ExprBody() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = (v) => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2); + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2); + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestSimpleInitialization_ParenLambdaWithType_ExprBody() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = (int v) => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2); + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2); + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestCastInitialization_SimpleLambda_Block() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + var [||]fibonacci = (Func)(v => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }); + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + } + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestCastInitialization_SimpleLambda_Block_ExtraParens() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + var [||]fibonacci = ((Func)(v => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + })); + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + } + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestCastInitialization_ParenLambdaNoType_Block() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + var [||]fibonacci = (Func)((v) => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }); + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + } + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestCastInitialization_ParenLambdaWithType_Block() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + var [||]fibonacci = (Func)((int v) => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }); + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + } + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestCastInitialization_SimpleLambda_ExprBody() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + var [||]fibonacci = (Func)(v => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2)); + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2); + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestCastInitialization_ParenLambdaNoType_ExprBody() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + var [||]fibonacci = (Func)((v) => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2)); + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2); + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestCastInitialization_ParenLambdaWithType_ExprBody() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + var [||]fibonacci = (Func)((int v) => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2)); + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2); + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestSplitInitialization_WrongName() + { + await TestMissingAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fib = null; + fibonacci = v => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestSplitInitialization_InitializedToOtherValue() + { + await TestMissingAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = GetCallback(); + fibonacci = v => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestSplitInitialization_SimpleLambda_Block() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = null; + fibonacci = v => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }; + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + } + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestSplitInitialization_SimpleLambda_Block_DefaultLiteral() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = default; + fibonacci = v => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }; + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + } + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestSplitInitialization_SimpleLambda_Block_DefaultExpression() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = default(Func); + fibonacci = v => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }; + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + } + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestSplitInitialization_SimpleLambda_Block_DefaultExpression_var() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + var [||]fibonacci = default(Func); + fibonacci = v => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }; + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + } + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestSplitInitialization_ParenLambdaNoType_Block() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = null; + fibonacci = (v) => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }; + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + } + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestSplitInitialization_ParenLambdaWithType_Block() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = null; + fibonacci = (int v) => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }; + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + } + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestSplitInitialization_SimpleLambda_ExprBody() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = null; + fibonacci = v => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2); + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2); + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestSplitInitialization_ParenLambdaNoType_ExprBody() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = null; + fibonacci = (v) => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2); + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2); + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestSplitInitialization_ParenLambdaWithType_ExprBody() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + Func [||]fibonacci = null; + fibonacci = (int v) => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2); + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) => + v <= 1 + ? 1 + : fibonacci(v - 1, v - 2); + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestFixAll1() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + Func {|FixAllInDocument:fibonacci|} = v => + { + Func isTrue = b => b; + + return fibonacci(v - 1, v - 2); + }; + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) + { + bool isTrue(bool b) => b; + + return fibonacci(v - 1, v - 2); + } + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestFixAll2() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + Func fibonacci = v => + { + Func {|FixAllInDocument:isTrue|} = b => b; + + return fibonacci(v - 1, v - 2); + }; + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) + { + bool isTrue(bool b) => b; + + return fibonacci(v - 1, v - 2); + } + } +}", ignoreTrivia: false); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestFixAll3() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + Func fibonacci = null; + fibonacci = v => + { + Func {|FixAllInDocument:isTrue|} = null; + isTrue = b => b; + + return fibonacci(v - 1, v - 2); + }; + } +}", +@"using System; + +class C +{ + void M() + { + int fibonacci(int v) + { + bool isTrue(bool b) => b; + + return fibonacci(v - 1, v - 2); + } + } +}"); + } + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseLocalFunction)] + public async Task TestTrivia() + { + await TestInRegularAndScriptAsync( +@"using System; + +class C +{ + void M() + { + // Leading trivia + Func [||]fibonacci = v => + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + }; // Trailing trivia + } +}", +@"using System; + +class C +{ + void M() + { + // Leading trivia + int fibonacci(int v) + { + if (v <= 1) + { + return 1; + } + + return fibonacci(v - 1, v - 2); + } // Trailing trivia + } +}", ignoreTrivia: false); + } + } +} diff --git a/src/EditorFeatures/TestUtilities/Traits.cs b/src/EditorFeatures/TestUtilities/Traits.cs index af758d10db2d33796b828b044767264f68aa64ed..85f58a4c6f6aad41906dfbe8ff432bb05f8787f6 100644 --- a/src/EditorFeatures/TestUtilities/Traits.cs +++ b/src/EditorFeatures/TestUtilities/Traits.cs @@ -106,6 +106,7 @@ public static class Features public const string CodeActionsUseExplicitType = "CodeActions.UseExplicitType"; public const string CodeActionsUseExplicitTupleName = "CodeActions.UseExplicitTupleName"; public const string CodeActionsUseFrameworkType = "CodeActions.UseFrameworkType"; + public const string CodeActionsUseLocalFunction = "CodeActions.UseLocalFunction"; public const string CodeActionsUseNullPropagation = "CodeActions.UseNullPropagation"; public const string CodeActionsUseNamedArguments = "CodeActions.UseNamedArguments"; public const string CodeActionsUseObjectInitializer = "CodeActions.UseObjectInitializer"; diff --git a/src/Features/CSharp/Portable/UseLocalFunction/CSharpUseLocalFunctionCodeFixProvider.cs b/src/Features/CSharp/Portable/UseLocalFunction/CSharpUseLocalFunctionCodeFixProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..fc3026fad1bd2eb62a634c349513fd8b5f01e4c9 --- /dev/null +++ b/src/Features/CSharp/Portable/UseLocalFunction/CSharpUseLocalFunctionCodeFixProvider.cs @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Composition; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.Formatting; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.CSharp.UseLocalFunction +{ + [ExportCodeFixProvider(LanguageNames.CSharp), Shared] + internal class CSharpUseLocalFunctionCodeFixProvider : SyntaxEditorBasedCodeFixProvider + { + private static TypeSyntax s_voidType = SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.VoidKeyword)); + private static TypeSyntax s_objectType = SyntaxFactory.PredefinedType(SyntaxFactory.Token(SyntaxKind.ObjectKeyword)); + + public override ImmutableArray FixableDiagnosticIds + => ImmutableArray.Create(IDEDiagnosticIds.UseLocalFunctionDiagnosticId); + + protected override bool IncludeDiagnosticDuringFixAll(Diagnostic diagnostic) + => diagnostic.Severity != DiagnosticSeverity.Hidden; + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + context.RegisterCodeFix( + new MyCodeAction(c => FixAsync(context.Document, context.Diagnostics.First(), c)), + context.Diagnostics); + return SpecializedTasks.EmptyTask; + } + + protected override async Task FixAllAsync( + Document document, ImmutableArray diagnostics, + SyntaxEditor editor, CancellationToken cancellationToken) + { + var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); + + var localDeclarationToLambda = new Dictionary(); + var nodesToTrack = new HashSet(); + foreach (var diagnostic in diagnostics) + { + var localDeclaration = (LocalDeclarationStatementSyntax)diagnostic.AdditionalLocations[0].FindNode(cancellationToken); + var lambda = (LambdaExpressionSyntax)diagnostic.AdditionalLocations[1].FindNode(cancellationToken); + + localDeclarationToLambda[localDeclaration] = lambda; + + nodesToTrack.Add(localDeclaration); + nodesToTrack.Add(lambda); + } + + var root = editor.OriginalRoot; + var currentRoot = root.TrackNodes(nodesToTrack); + + // Process declarations in reverse order so that we see the effects of nested + // declarations befor processing the outer decls. + foreach (var (originalLocalDeclaration, originalLambda) in localDeclarationToLambda.OrderByDescending(kvp => kvp.Value.SpanStart)) + { + var delegateType = (INamedTypeSymbol)semanticModel.GetTypeInfo(originalLambda, cancellationToken).ConvertedType; + var parameterList = GenerateParameterList(semanticModel, originalLambda, cancellationToken); + + var currentLocalDeclaration = currentRoot.GetCurrentNode(originalLocalDeclaration); + var currentLambda = currentRoot.GetCurrentNode(originalLambda); + + currentRoot = ReplaceAnonymousWithLocalFunction( + document.Project.Solution.Workspace, currentRoot, + currentLocalDeclaration, currentLambda, + delegateType, parameterList, + cancellationToken); + } + + editor.ReplaceNode(root, currentRoot); + } + + private SyntaxNode ReplaceAnonymousWithLocalFunction( + Workspace workspace, SyntaxNode currentRoot, + LocalDeclarationStatementSyntax localDeclaration, LambdaExpressionSyntax lambda, + INamedTypeSymbol delegateType, ParameterListSyntax parameterList, + CancellationToken cancellationToken) + { + var newLocalFunctionStatement = CreateLocalFunctionStatement( + localDeclaration, lambda, delegateType, parameterList, cancellationToken); + + newLocalFunctionStatement = newLocalFunctionStatement.WithTriviaFrom(localDeclaration) + .WithAdditionalAnnotations(Formatter.Annotation); + + var editor = new SyntaxEditor(currentRoot, workspace); + editor.ReplaceNode(localDeclaration, newLocalFunctionStatement); + + var lambdaStatement = lambda.GetAncestor(); + if (lambdaStatement != localDeclaration) + { + // This is the split decl+init form. Remove the second statement as we're + // merging into the first one. + editor.RemoveNode(lambdaStatement); + } + + return editor.GetChangedRoot(); + } + + private LocalFunctionStatementSyntax CreateLocalFunctionStatement( + LocalDeclarationStatementSyntax localDeclaration, + LambdaExpressionSyntax lambda, + INamedTypeSymbol delegateType, + ParameterListSyntax parameterList, + CancellationToken cancellationToken) + { + var modifiers = lambda.AsyncKeyword.IsKind(SyntaxKind.AsyncKeyword) + ? new SyntaxTokenList(lambda.AsyncKeyword) + : default; + + var invokeMethod = delegateType.DelegateInvokeMethod; + + var returnType = invokeMethod.ReturnsVoid + ? s_voidType + : invokeMethod.ReturnType.GenerateTypeSyntax(); + + var identifier = localDeclaration.Declaration.Variables[0].Identifier; + var typeParameterList = default(TypeParameterListSyntax); + + var constraintClauses = default(SyntaxList); + + var body = lambda.Body.IsKind(SyntaxKind.Block) + ? (BlockSyntax)lambda.Body + : null; + + var expressionBody = lambda.Body is ExpressionSyntax expression + ? SyntaxFactory.ArrowExpressionClause(lambda.ArrowToken, expression) + : null; + + var semicolonToken = lambda.Body is ExpressionSyntax + ? localDeclaration.SemicolonToken + : default; + + return SyntaxFactory.LocalFunctionStatement( + modifiers, returnType, identifier, typeParameterList, parameterList, + constraintClauses, body, expressionBody, semicolonToken); + } + + private ParameterListSyntax GenerateParameterList( + SemanticModel semanticModel, AnonymousFunctionExpressionSyntax anonymousFunction, CancellationToken cancellationToken) + { + switch (anonymousFunction) + { + case SimpleLambdaExpressionSyntax simpleLambda: + return GenerateSimpleLambdaParameterList(semanticModel, simpleLambda, cancellationToken); + case ParenthesizedLambdaExpressionSyntax parenthesizedLambda: + return GenerateParenthesizedLambdaParameterList(semanticModel, parenthesizedLambda, cancellationToken); + default: + throw ExceptionUtilities.UnexpectedValue(anonymousFunction); + } + } + + private ParameterListSyntax GenerateSimpleLambdaParameterList( + SemanticModel semanticModel, SimpleLambdaExpressionSyntax lambdaExpression, CancellationToken cancellationToken) + { + var parameter = semanticModel.GetDeclaredSymbol(lambdaExpression.Parameter, cancellationToken); + var type = parameter?.Type.GenerateTypeSyntax() ?? s_objectType; + + return SyntaxFactory.ParameterList( + SyntaxFactory.SeparatedList().Add( + SyntaxFactory.Parameter(lambdaExpression.Parameter.Identifier).WithType(type))); + } + + private ParameterListSyntax GenerateParenthesizedLambdaParameterList( + SemanticModel semanticModel, ParenthesizedLambdaExpressionSyntax lambdaExpression, CancellationToken cancellationToken) + { + return lambdaExpression.ParameterList.ReplaceNodes( + lambdaExpression.ParameterList.Parameters, + (parameterNode, _) => + { + if (parameterNode.Type != null) + { + return parameterNode; + } + + var parameter = semanticModel.GetDeclaredSymbol(parameterNode, cancellationToken); + return parameterNode.WithType(parameter?.Type.GenerateTypeSyntax() ?? s_objectType); + }); + } + + private class MyCodeAction : CodeAction.DocumentChangeAction + { + public MyCodeAction(Func> createChangedDocument) + : base(FeaturesResources.Use_local_function, createChangedDocument, FeaturesResources.Use_local_function) + { + } + } + } +} diff --git a/src/Features/CSharp/Portable/UseLocalFunction/CSharpUseLocalFunctionDiagnosticAnalyzer.cs b/src/Features/CSharp/Portable/UseLocalFunction/CSharpUseLocalFunctionDiagnosticAnalyzer.cs new file mode 100644 index 0000000000000000000000000000000000000000..1a6c585ac7f33537a229e2f42c1a1275e063fd5c --- /dev/null +++ b/src/Features/CSharp/Portable/UseLocalFunction/CSharpUseLocalFunctionDiagnosticAnalyzer.cs @@ -0,0 +1,303 @@ +// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Immutable; +using System.Threading; +using Microsoft.CodeAnalysis.CodeStyle; +using Microsoft.CodeAnalysis.CSharp.CodeStyle; +using Microsoft.CodeAnalysis.CSharp.Extensions; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Shared.Extensions; + +namespace Microsoft.CodeAnalysis.CSharp.UseLocalFunction +{ + /// + /// Looks for code of the form: + /// + /// Func<int, int> fib = n => + /// { + /// if (n <= 2) + /// return 1 + /// + /// return fib(n - 1) + fib(n - 2); + /// } + /// + /// and converts it to: + /// + /// int fib(int n) + /// { + /// if (n <= 2) + /// return 1 + /// + /// return fib(n - 1) + fib(n - 2); + /// } + /// + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal class CSharpUseLocalFunctionDiagnosticAnalyzer : AbstractCodeStyleDiagnosticAnalyzer + { + public override bool OpenFileOnly(Workspace workspace) => false; + + public CSharpUseLocalFunctionDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseLocalFunctionDiagnosticId, + new LocalizableResourceString( + nameof(FeaturesResources.Use_local_function), FeaturesResources.ResourceManager, typeof(FeaturesResources))) + { + } + + protected override void InitializeWorker(AnalysisContext context) + => context.RegisterSyntaxNodeAction(SyntaxNodeAction, SyntaxKind.SimpleLambdaExpression, SyntaxKind.ParenthesizedLambdaExpression); + + private void SyntaxNodeAction(SyntaxNodeAnalysisContext syntaxContext) + { + var options = syntaxContext.Options; + var syntaxTree = syntaxContext.Node.SyntaxTree; + var cancellationToken = syntaxContext.CancellationToken; + var optionSet = options.GetDocumentOptionSetAsync(syntaxTree, cancellationToken).GetAwaiter().GetResult(); + if (optionSet == null) + { + return; + } + + var styleOption = optionSet.GetOption(CSharpCodeStyleOptions.PreferLocalOverAnonymousFunction); + if (!styleOption.Value) + { + // Bail immediately if the user has disabled this feature. + return; + } + + // Local functions are only available in C# 7.0 and above. Don't offer this refactoring + // in projects targetting a lesser version. + if (((CSharpParseOptions)syntaxTree.Options).LanguageVersion < LanguageVersion.CSharp7) + { + return; + } + + var severity = styleOption.Notification.Value; + var anonymousFunction = (AnonymousFunctionExpressionSyntax)syntaxContext.Node; + + var semanticModel = syntaxContext.SemanticModel; + if (!CheckForPattern(semanticModel, anonymousFunction, cancellationToken, + out var localDeclaration)) + { + return; + } + + if (localDeclaration.Declaration.Variables.Count != 1) + { + return; + } + + if (!(localDeclaration.Parent is BlockSyntax block)) + { + return; + } + + var local = semanticModel.GetDeclaredSymbol(localDeclaration.Declaration.Variables[0], cancellationToken); + if (local == null) + { + return; + } + + if (IsWrittenAfter(semanticModel, local, block, anonymousFunction, cancellationToken)) + { + return; + } + + var delegateType = semanticModel.GetTypeInfo(anonymousFunction, cancellationToken).ConvertedType as INamedTypeSymbol; + if (!delegateType.IsDelegateType() || + delegateType.DelegateInvokeMethod == null) + { + return; + } + + // Looks good! + var additionalLocations = ImmutableArray.Create( + localDeclaration.GetLocation(), + anonymousFunction.GetLocation()); + + if (severity != DiagnosticSeverity.Hidden) + { + // If the diagnostic is not hidden, then just place the user visible part + // on the local being initialized with the lambda. + syntaxContext.ReportDiagnostic(Diagnostic.Create( + GetDescriptorWithSeverity(severity), + localDeclaration.Declaration.Variables[0].Identifier.GetLocation(), + additionalLocations)); + } + else + { + // If the diagnostic is hidden, place it on the entire construct. + syntaxContext.ReportDiagnostic(Diagnostic.Create( + GetDescriptorWithSeverity(severity), + localDeclaration.GetLocation(), + additionalLocations)); + + var anonymousFunctionStatement = anonymousFunction.GetAncestor(); + if (localDeclaration != anonymousFunctionStatement) + { + syntaxContext.ReportDiagnostic(Diagnostic.Create( + GetDescriptorWithSeverity(severity), + anonymousFunctionStatement.GetLocation(), + additionalLocations)); + } + } + } + + private bool CheckForPattern( + SemanticModel semanticModel, + AnonymousFunctionExpressionSyntax anonymousFunction, + CancellationToken cancellationToken, + out LocalDeclarationStatementSyntax localDeclaration) + { + // Look for: + // + // Type t = + // var t = (Type)() + // + // Type t = null; + // t = + return CheckForSimpleLocalDeclarationPattern(semanticModel, anonymousFunction, cancellationToken, out localDeclaration) || + CheckForCastedLocalDeclarationPattern(semanticModel, anonymousFunction, cancellationToken, out localDeclaration) || + CheckForLocalDeclarationAndAssignment(semanticModel, anonymousFunction, cancellationToken, out localDeclaration); + } + + private bool CheckForSimpleLocalDeclarationPattern( + SemanticModel semanticModel, + AnonymousFunctionExpressionSyntax anonymousFunction, + CancellationToken cancellationToken, + out LocalDeclarationStatementSyntax localDeclaration) + { + // Type t = + if (anonymousFunction.IsParentKind(SyntaxKind.EqualsValueClause) && + anonymousFunction.Parent.IsParentKind(SyntaxKind.VariableDeclarator) && + anonymousFunction.Parent.Parent.IsParentKind(SyntaxKind.VariableDeclaration) && + anonymousFunction.Parent.Parent.Parent.IsParentKind(SyntaxKind.LocalDeclarationStatement)) + { + localDeclaration = (LocalDeclarationStatementSyntax)anonymousFunction.Parent.Parent.Parent.Parent; + if (!localDeclaration.Declaration.Type.IsVar) + { + return true; + } + } + + localDeclaration = null; + return false; + } + + private bool IsWrittenAfter( + SemanticModel semanticModel, ISymbol local, BlockSyntax block, + AnonymousFunctionExpressionSyntax anonymousFunction, CancellationToken cancellationToken) + { + var anonymousFunctionStart = anonymousFunction.SpanStart; + foreach (var descendentNode in block.DescendantNodes()) + { + var descendentStart = descendentNode.Span.Start; + if (descendentStart <= anonymousFunctionStart) + { + // This node is before the local declaration. Can ignore it entirely as it could + // not be an access to the local. + continue; + } + + if (descendentNode.IsKind(SyntaxKind.IdentifierName)) + { + var identifierName = (IdentifierNameSyntax)descendentNode; + if (identifierName.Identifier.ValueText == local.Name && + identifierName.IsWrittenTo() && + local.Equals(semanticModel.GetSymbolInfo(identifierName, cancellationToken).GetAnySymbol())) + { + return true; + } + } + } + + return false; + } + + private bool CheckForCastedLocalDeclarationPattern( + SemanticModel semanticModel, + AnonymousFunctionExpressionSyntax anonymousFunction, + CancellationToken cancellationToken, + out LocalDeclarationStatementSyntax localDeclaration) + { + // var t = (Type)() + var containingStatement = anonymousFunction.GetAncestor(); + if (containingStatement.IsKind(SyntaxKind.LocalDeclarationStatement)) + { + localDeclaration = (LocalDeclarationStatementSyntax)containingStatement; + if (localDeclaration.Declaration.Variables.Count == 1) + { + var variableDeclarator = localDeclaration.Declaration.Variables[0]; + if (variableDeclarator.Initializer != null) + { + var value = variableDeclarator.Initializer.Value.WalkDownParentheses(); + if (value is CastExpressionSyntax castExpression) + { + if (castExpression.Expression.WalkDownParentheses() == anonymousFunction) + { + return true; + } + } + } + } + } + + localDeclaration = null; + return false; + } + + private bool CheckForLocalDeclarationAndAssignment( + SemanticModel semanticModel, + AnonymousFunctionExpressionSyntax anonymousFunction, + CancellationToken cancellationToken, + out LocalDeclarationStatementSyntax localDeclaration) + { + // Type t = null; + // t = + if (anonymousFunction.IsParentKind(SyntaxKind.SimpleAssignmentExpression) && + anonymousFunction.Parent.IsParentKind(SyntaxKind.ExpressionStatement) && + anonymousFunction.Parent.Parent.IsParentKind(SyntaxKind.Block)) + { + var assignment = (AssignmentExpressionSyntax)anonymousFunction.Parent; + if (assignment.Left.IsKind(SyntaxKind.IdentifierName)) + { + var expressionStatement = (ExpressionStatementSyntax)assignment.Parent; + var block = (BlockSyntax)expressionStatement.Parent; + var expressionStatementIndex = block.Statements.IndexOf(expressionStatement); + if (expressionStatementIndex >= 1) + { + var previousStatement = block.Statements[expressionStatementIndex - 1]; + if (previousStatement.IsKind(SyntaxKind.LocalDeclarationStatement)) + { + localDeclaration = (LocalDeclarationStatementSyntax)previousStatement; + if (localDeclaration.Declaration.Variables.Count == 1) + { + var variableDeclarator = localDeclaration.Declaration.Variables[0]; + if (variableDeclarator.Initializer != null) + { + var value = variableDeclarator.Initializer.Value; + if (value.IsKind(SyntaxKind.NullLiteralExpression) || + value.IsKind(SyntaxKind.DefaultLiteralExpression) || + value.IsKind(SyntaxKind.DefaultExpression)) + { + var identifierName = (IdentifierNameSyntax)assignment.Left; + if (variableDeclarator.Identifier.ValueText == identifierName.Identifier.ValueText) + { + return true; + } + } + } + } + } + } + } + } + + localDeclaration = null; + return false; + } + + public override DiagnosticAnalyzerCategory GetAnalyzerCategory() + => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis; + } +} diff --git a/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs b/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs index 1101700eb3393874c5c7f557fbe377cc173a5f6b..4340daab26a0dd7ae15027d1a993c17e7bb64736 100644 --- a/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs +++ b/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs @@ -54,6 +54,7 @@ internal static class IDEDiagnosticIds public const string InlineIsTypeWithoutNameCheckId = "IDE0038"; + public const string UseLocalFunctionDiagnosticId = "IDE0039"; // Analyzer error Ids public const string AnalyzerChangedId = "IDE1001"; diff --git a/src/Features/Core/Portable/FeaturesResources.Designer.cs b/src/Features/Core/Portable/FeaturesResources.Designer.cs index 2e2ff208c7118b8deb183149ac7a700328912a0e..5dcaa83dcec86ed27aa696e9c7082db2d591a821 100644 --- a/src/Features/Core/Portable/FeaturesResources.Designer.cs +++ b/src/Features/Core/Portable/FeaturesResources.Designer.cs @@ -3453,6 +3453,15 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Use local function. + /// + internal static string Use_local_function { + get { + return ResourceManager.GetString("Use_local_function", resourceCulture); + } + } + /// /// Looks up a localized string similar to Use local version '{0}'. /// diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index 308ac6e6a9eb73c6db1de7af1859956ccf24b8bf..aa3b9329e724b469971422b3885544242282337f 100644 --- a/src/Features/Core/Portable/FeaturesResources.resx +++ b/src/Features/Core/Portable/FeaturesResources.resx @@ -1244,7 +1244,7 @@ This version used in: {2} 'default' expression can be simplified - + Use inferred member name @@ -1271,6 +1271,9 @@ This version used in: {2} in {0} (project {1}) + + Use local function + Warning: Declaration changes scope and may change meaning. diff --git a/src/VisualStudio/CSharp/Impl/Options/Formatting/StyleViewModel.cs b/src/VisualStudio/CSharp/Impl/Options/Formatting/StyleViewModel.cs index e52f086cd0d96cf8baf37dc19386dc753b92c800..81677a4e3236b6a9d14e16e18ca53032f1d84f93 100644 --- a/src/VisualStudio/CSharp/Impl/Options/Formatting/StyleViewModel.cs +++ b/src/VisualStudio/CSharp/Impl/Options/Formatting/StyleViewModel.cs @@ -523,6 +523,31 @@ public int GetAge() //] }} }} +"; + + private static readonly string s_preferLocalFunctionOverAnonymousFunction = $@" +using System; + +class Customer +{{ + public Customer(string value) + {{ +//[ + // {ServicesVSResources.Prefer_colon} + int fibonacci(int n) + {{ + return n <= 1 ? 1 : fibonacci(n - 1) + fibonacci(n - 2); + }} + + // {ServicesVSResources.Over_colon} + Func fibonacci = null; + fibonacci = (int n) => + {{ + return n <= 1 ? 1 : fibonacci(n - 1) + fibonacci(n - 2); + }}; +//] + }} +}} "; private const string s_preferExpressionBodyForMethods = @" @@ -747,6 +772,7 @@ internal StyleViewModel(OptionSet optionSet, IServiceProvider serviceProvider) : CodeStyleItems.Add(new BooleanCodeStyleOptionViewModel(CSharpCodeStyleOptions.PreferSimpleDefaultExpression, ServicesVSResources.Prefer_simple_default_expression, s_preferSimpleDefaultExpression, s_preferSimpleDefaultExpression, this, optionSet, expressionPreferencesGroupTitle)); CodeStyleItems.Add(new BooleanCodeStyleOptionViewModel(CSharpCodeStyleOptions.PreferInferredTupleNames, ServicesVSResources.Prefer_inferred_tuple_names, s_preferInferredTupleName, s_preferInferredTupleName, this, optionSet, expressionPreferencesGroupTitle)); CodeStyleItems.Add(new BooleanCodeStyleOptionViewModel(CSharpCodeStyleOptions.PreferInferredAnonymousTypeMemberNames, ServicesVSResources.Prefer_inferred_anonymous_type_member_names, s_preferInferredAnonymousTypeMemberName, s_preferInferredAnonymousTypeMemberName, this, optionSet, expressionPreferencesGroupTitle)); + CodeStyleItems.Add(new BooleanCodeStyleOptionViewModel(CSharpCodeStyleOptions.PreferLocalOverAnonymousFunction, ServicesVSResources.Prefer_local_function_over_anonymous_function, s_preferLocalFunctionOverAnonymousFunction, s_preferLocalFunctionOverAnonymousFunction, this, optionSet, expressionPreferencesGroupTitle)); AddExpressionBodyOptions(optionSet, expressionPreferencesGroupTitle); diff --git a/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs b/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs index 1419c2e4a553c0b8dbf2b84de6c04b856bd02f50..661400367b1a3cbe57875652015456acc2db8fc1 100644 --- a/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs +++ b/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs @@ -1639,6 +1639,15 @@ internal class ServicesVSResources { } } + /// + /// Looks up a localized string similar to Prefere local function over anonymous function. + /// + internal static string Prefer_local_function_over_anonymous_function { + get { + return ResourceManager.GetString("Prefer_local_function_over_anonymous_function", resourceCulture); + } + } + /// /// Looks up a localized string similar to Prefer null propagation. /// diff --git a/src/VisualStudio/Core/Def/ServicesVSResources.resx b/src/VisualStudio/Core/Def/ServicesVSResources.resx index 1b4582288aee1485f331d8006f508b8226da1903..cb67db1e584ce6ccc8f57df252a2cf45e97d146c 100644 --- a/src/VisualStudio/Core/Def/ServicesVSResources.resx +++ b/src/VisualStudio/Core/Def/ServicesVSResources.resx @@ -959,4 +959,7 @@ Additional information: {1} Changes are not allowed while code is running. + + Prefere local function over anonymous function + \ No newline at end of file diff --git a/src/Workspaces/CSharp/Portable/CodeStyle/CSharpCodeStyleOptions.cs b/src/Workspaces/CSharp/Portable/CodeStyle/CSharpCodeStyleOptions.cs index e8506d021e4156e72f3965dd3b83f00b5bfb2d05..03852254068eaf882ec23bad8f1376088aec9c5b 100644 --- a/src/Workspaces/CSharp/Portable/CodeStyle/CSharpCodeStyleOptions.cs +++ b/src/Workspaces/CSharp/Portable/CodeStyle/CSharpCodeStyleOptions.cs @@ -174,6 +174,12 @@ internal static class CSharpCodeStyleOptions EditorConfigStorageLocation.ForStringCodeStyleOption("csharp_preferred_modifier_order"), new RoamingProfileStorageLocation($"TextEditor.CSharp.Specific.{nameof(PreferredModifierOrder)}")}); + public static readonly Option> PreferLocalOverAnonymousFunction = new Option>( + nameof(CodeStyleOptions), nameof(PreferLocalOverAnonymousFunction), defaultValue: CodeStyleOptions.TrueWithSuggestionEnforcement, + storageLocations: new OptionStorageLocation[] { + EditorConfigStorageLocation.ForBoolCodeStyleOption("csharp_style_pattern_local_over_anonymous_function"), + new RoamingProfileStorageLocation($"TextEditor.CSharp.Specific.{nameof(PreferLocalOverAnonymousFunction)}")}); + public static IEnumerable>> GetCodeStyleOptions() { yield return UseImplicitTypeForIntrinsicTypes; @@ -186,6 +192,7 @@ public static IEnumerable>> GetCodeStyleOptions() yield return PreferSimpleDefaultExpression; yield return PreferInferredTupleNames; yield return PreferInferredAnonymousTypeMemberNames; + yield return PreferLocalOverAnonymousFunction; } public static IEnumerable>> GetExpressionBodyOptions() diff --git a/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxNodeExtensions.cs b/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxNodeExtensions.cs index e26005cc1dcd5b2b08732dfb5e5c44cdd36c5c52..0c6fd5b16e1760f1650293df274eaed71776f272 100644 --- a/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxNodeExtensions.cs +++ b/src/Workspaces/Core/Portable/Shared/Extensions/SyntaxNodeExtensions.cs @@ -47,7 +47,18 @@ public static IEnumerable GetAncestors(this SyntaxNode node) public static TNode GetAncestor(this SyntaxNode node) where TNode : SyntaxNode { - return node?.GetAncestors().FirstOrDefault(); + var current = node.Parent; + while (current != null) + { + if (current is TNode tNode) + { + return tNode; + } + + current = current.GetParent(); + } + + return null; } public static TNode GetAncestorOrThis(this SyntaxNode node)