diff --git a/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj b/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj index b361cba5f691130a0471db5dbee1aa10523bd820..28924a4bd65d30ae417d6c40c6d0db177deb4df8 100644 --- a/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj +++ b/src/EditorFeatures/CSharpTest/CSharpEditorServicesTest.csproj @@ -93,8 +93,10 @@ Roslyn.Services.Editor.CSharp.UnitTests UnitTest - - + + + + @@ -278,6 +280,8 @@ + + diff --git a/src/EditorFeatures/CSharpTest/UseCoalesceExpression/UseCoalesceExpressionForNullableTests.cs b/src/EditorFeatures/CSharpTest/UseCoalesceExpression/UseCoalesceExpressionForNullableTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..1e91d9c552fbb1775192b38c17c2376662f9b39e --- /dev/null +++ b/src/EditorFeatures/CSharpTest/UseCoalesceExpression/UseCoalesceExpressionForNullableTests.cs @@ -0,0 +1,201 @@ +// 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.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.UseCoalesceExpression; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics; +using Microsoft.CodeAnalysis.UseCoalesceExpression; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.UseCoalesceExpression +{ + public class UseCoalesceExpressionForNullableTests : AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest + { + internal override Tuple CreateDiagnosticProviderAndFixer(Workspace workspace) + { + return new Tuple( + new CSharpUseCoalesceExpressionForNullableDiagnosticAnalyzer(), + new UseCoalesceExpressionForNullableCodeFixProvider()); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestOnLeft_Equals() + { + await TestAsync( +@" +using System; + +class C +{ + void M(int? x, int? y) + { + var z = [||]!x.HasValue ? y : x.Value; + } +}", +@"using System; + +class C +{ + void M(int? x, int? y) + { + var z = x ?? y; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestOnLeft_NotEquals() + { + await TestAsync( +@" +using System; + +class C +{ + void M(int? x, int? y) + { + var z = [||]x.HasValue ? x.Value : y; + } +}", +@"using System; + +class C +{ + void M(int? x, int? y) + { + var z = x ?? y; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestComplexExpression() + { + await TestAsync( +@" +using System; + +class C +{ + void M(int? x, int? y) + { + var z = [||]!(x + y).HasValue ? y : (x + y).Value; + } +}", +@"using System; + +class C +{ + void M(int? x, int? y) + { + var z = (x + y) ?? y; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestParens1() + { + await TestAsync( +@" +using System; + +class C +{ + void M(int? x, int? y) + { + var z = [||](x.HasValue) ? x.Value : y; + } +}", +@"using System; + +class C +{ + void M(int? x, int? y) + { + var z = x ?? y; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestFixAll1() + { + await TestAsync( +@" +using System; + +class C +{ + void M(int? x, int? y) + { + var z1 = {|FixAllInDocument:x|}.HasValue ? x.Value : y; + var z2 = !x.HasValue ? y : x.Value; + } +}", +@"using System; + +class C +{ + void M(int? x, int? y) + { + var z1 = x ?? y; + var z2 = x ?? y; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestFixAll2() + { + await TestAsync( +@" +using System; + +class C +{ + void M(int? x, int? y, int? z) + { + var w = {|FixAllInDocument:x|}.HasValue ? x.Value : y.ToString(z.HasValue ? z.Value : y); + } +}", +@"using System; + +class C +{ + void M(int? x, int? y, int? z) + { + var w = x ?? y.ToString(z ?? y); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestFixAll3() + { + await TestAsync( +@" +using System; + +class C +{ + void M(int? x, int? y, int? z) + { + var w = {|FixAllInDocument:x|}.HasValue ? x.Value : y.HasValue ? y.Value : z; + } +}", +@"using System; + +class C +{ + void M(int? x, int? y, int? z) + { + var w = x ?? y ?? z; + } +}"); + } + } +} \ No newline at end of file diff --git a/src/EditorFeatures/CSharpTest/UseCoalesceExpression/UseCoalesceExpressionTests.cs b/src/EditorFeatures/CSharpTest/UseCoalesceExpression/UseCoalesceExpressionTests.cs new file mode 100644 index 0000000000000000000000000000000000000000..64ab66eea2b73829a7ef2d6029e8d82b31b38bf3 --- /dev/null +++ b/src/EditorFeatures/CSharpTest/UseCoalesceExpression/UseCoalesceExpressionTests.cs @@ -0,0 +1,326 @@ +// 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.Threading.Tasks; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp.UseCoalesceExpression; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Diagnostics; +using Microsoft.CodeAnalysis.UseCoalesceExpression; +using Roslyn.Test.Utilities; +using Xunit; + +namespace Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.UseCoalesceExpression +{ + public class UseCoalesceExpressionTests : AbstractCSharpDiagnosticProviderBasedUserDiagnosticTest + { + internal override Tuple CreateDiagnosticProviderAndFixer(Workspace workspace) + { + return new Tuple( + new CSharpUseCoalesceExpressionDiagnosticAnalyzer(), + new UseCoalesceExpressionCodeFixProvider()); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestOnLeft_Equals() + { + await TestAsync( +@" +using System; + +class C +{ + void M(string x, string y) + { + var z = [||]x == null ? y : x; + } +}", +@"using System; + +class C +{ + void M(string x, string y) + { + var z = x ?? y; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestOnLeft_NotEquals() + { + await TestAsync( +@" +using System; + +class C +{ + void M(string x, string y) + { + var z = [||]x != null ? x : y; + } +}", +@"using System; + +class C +{ + void M(string x, string y) + { + var z = x ?? y; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestOnRight_Equals() + { + await TestAsync( +@" +using System; + +class C +{ + void M(string x, string y) + { + var z = [||]null == x ? y : x; + } +}", +@"using System; + +class C +{ + void M(string x, string y) + { + var z = x ?? y; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestOnRight_NotEquals() + { + await TestAsync( +@" +using System; + +class C +{ + void M(string x, string y) + { + var z = [||]null != x ? x : y; + } +}", +@"using System; + +class C +{ + void M(string x, string y) + { + var z = x ?? y; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestComplexExpression() + { + await TestAsync( +@" +using System; + +class C +{ + void M(string x, string y) + { + var z = [||]x.ToString() == null ? y : x.ToString(); + } +}", +@"using System; + +class C +{ + void M(string x, string y) + { + var z = x.ToString() ?? y; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestParens1() + { + await TestAsync( +@" +using System; + +class C +{ + void M(string x, string y) + { + var z = [||](x == null) ? y : x; + } +}", +@"using System; + +class C +{ + void M(string x, string y) + { + var z = x ?? y; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestParens2() + { + await TestAsync( +@" +using System; + +class C +{ + void M(string x, string y) + { + var z = [||](x) == null ? y : x; + } +}", +@"using System; + +class C +{ + void M(string x, string y) + { + var z = x ?? y; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestParens3() + { + await TestAsync( +@" +using System; + +class C +{ + void M(string x, string y) + { + var z = [||]x == null ? y : (x); + } +}", +@"using System; + +class C +{ + void M(string x, string y) + { + var z = x ?? y; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestParens4() + { + await TestAsync( +@" +using System; + +class C +{ + void M(string x, string y) + { + var z = [||]x == null ? (y) : x; + } +}", +@"using System; + +class C +{ + void M(string x, string y) + { + var z = x ?? y; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestFixAll1() + { + await TestAsync( +@" +using System; + +class C +{ + void M(string x, string y) + { + var z1 = {|FixAllInDocument:x|} == null ? y : x; + var z2 = x != null ? x : y; + } +}", +@"using System; + +class C +{ + void M(string x, string y) + { + var z1 = x ?? y; + var z2 = x ?? y; + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestFixAll2() + { + await TestAsync( +@" +using System; + +class C +{ + void M(string x, string y, string z) + { + var w = {|FixAllInDocument:x|} != null ? x : y.ToString(z != null ? z : y); + } +}", +@"using System; + +class C +{ + void M(string x, string y, string z) + { + var w = x ?? y.ToString(z ?? y); + } +}"); + } + + [Fact, Trait(Traits.Feature, Traits.Features.CodeActionsUseCoalesceExpression)] + public async Task TestFixAll3() + { + await TestAsync( +@" +using System; + +class C +{ + void M(string x, string y, string z) + { + var w = {|FixAllInDocument:x|} != null ? x : y != null ? y : z; + } +}", +@"using System; + +class C +{ + void M(string x, string y, string z) + { + var w = x ?? y ?? z; + } +}"); + } + } +} \ No newline at end of file diff --git a/src/EditorFeatures/TestUtilities/Traits.cs b/src/EditorFeatures/TestUtilities/Traits.cs index eb782d4f9855f5991bef685fad0040acdecf967b..473ac72329c76612a39438c2db46eec70e86be04 100644 --- a/src/EditorFeatures/TestUtilities/Traits.cs +++ b/src/EditorFeatures/TestUtilities/Traits.cs @@ -84,6 +84,7 @@ public static class Features public const string CodeActionsSpellcheck = "CodeActions.Spellcheck"; public const string CodeActionsSuppression = "CodeActions.Suppression"; public const string CodeActionsUseAutoProperty = "CodeActions.UseAutoProperty"; + public const string CodeActionsUseCoalesceExpression = "CodeActions.UseCoalesceExpression"; public const string CodeActionsUseExpressionBody = "CodeActions.UseExpressionBody"; public const string CodeActionsUseImplicitType = "CodeActions.UseImplicitType"; public const string CodeActionsUseExplicitType = "CodeActions.UseExplicitType"; diff --git a/src/EditorFeatures/VisualBasicTest/BasicEditorServicesTest.vbproj b/src/EditorFeatures/VisualBasicTest/BasicEditorServicesTest.vbproj index 6188dbeac11414c50feeaccae8e24d2f5025600e..e7b3ce1c284fd1cdeab079221409f03b2dce1b33 100644 --- a/src/EditorFeatures/VisualBasicTest/BasicEditorServicesTest.vbproj +++ b/src/EditorFeatures/VisualBasicTest/BasicEditorServicesTest.vbproj @@ -504,6 +504,7 @@ + @@ -601,6 +602,7 @@ + PreserveNewest Designer diff --git a/src/EditorFeatures/VisualBasicTest/UseCoalesceExpression/UseCoalesceExpressionForNullableTests.vb b/src/EditorFeatures/VisualBasicTest/UseCoalesceExpression/UseCoalesceExpressionForNullableTests.vb new file mode 100644 index 0000000000000000000000000000000000000000..e35a21e8a66bfcff94ee595a0d07cec409f93584 --- /dev/null +++ b/src/EditorFeatures/VisualBasicTest/UseCoalesceExpression/UseCoalesceExpressionForNullableTests.vb @@ -0,0 +1,141 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports Microsoft.CodeAnalysis.CodeFixes +Imports Microsoft.CodeAnalysis.Diagnostics +Imports Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.Diagnostics +Imports Microsoft.CodeAnalysis.UseCoalesceExpression +Imports Microsoft.CodeAnalysis.VisualBasic.UseCoalesceExpression + +Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.UseCoalesceExpression + Public Class UseCoalesceExpressionForNullableTests + Inherits AbstractVisualBasicDiagnosticProviderBasedUserDiagnosticTest + + Friend Overrides Function CreateDiagnosticProviderAndFixer(workspace As Workspace) As Tuple(Of DiagnosticAnalyzer, CodeFixProvider) + Return New Tuple(Of DiagnosticAnalyzer, CodeFixProvider)( + New VisualBasicUseCoalesceExpressionForNullableDiagnosticAnalyzer(), + New UseCoalesceExpressionForNullableCodeFixProvider()) + End Function + + + Public Async Function TestOnLeft_Equals() As Task + Await TestAsync( +" +Imports System + +Class C + Sub M(x as Integer?, y as Integer?) + Dim z = [||]If (Not x.HasValue, y, x.Value) + End Sub +End Class", +"Imports System + +Class C + Sub M(x as Integer?, y as Integer?) + Dim z = If(x, y) + End Sub +End Class") + End Function + + + Public Async Function TestOnLeft_NotEquals() As Task + Await TestAsync( +" +Imports System + +Class C + Sub M(x as Integer?, y as Integer?) + Dim z = [||]If(x.HasValue, x.Value, y) + End Sub +End Class", +"Imports System + +Class C + Sub M(x as Integer?, y as Integer?) + Dim z = If(x, y) + End Sub +End Class") + End Function + + + Public Async Function TestComplexExpression() As Task + Await TestAsync( +" +Imports System + +Class C + Sub M(x as Integer?, y as Integer?) + Dim z = [||]If (Not (x + y).HasValue, y, (x + y).Value) + End Sub +End Class", +"Imports System + +Class C + Sub M(x as Integer?, y as Integer?) + Dim z = If((x + y), y) + End Sub +End Class") + End Function + + + Public Async Function TestParens1() As Task + Await TestAsync( +" +Imports System + +Class C + Sub M(x as Integer?, y as Integer?) + Dim z = [||]If ((Not x.HasValue), y, x.Value) + End Sub +End Class", +"Imports System + +Class C + Sub M(x as Integer?, y as Integer?) + Dim z = If(x, y) + End Sub +End Class") + End Function + + + Public Async Function TestFixAll1() As Task + Await TestAsync( +" +Imports System + +Class C + Sub M(x as Integer?, y as Integer?) + Dim z1 = {|FixAllInDocument:If|} (Not x.HasValue, y, x.Value) + Dim z2 = If(x.HasValue, x.Value, y) + End Sub +End Class", +"Imports System + +Class C + Sub M(x as Integer?, y as Integer?) + Dim z1 = If(x, y) + Dim z2 = If(x, y) + End Sub +End Class") + End Function + + + Public Async Function TestFixAll2() As Task + Await TestAsync( +" +Imports System + +Class C + Sub M(x as Integer?, y as Integer?, z as Integer?) + dim w = {|FixAllInDocument:If|} (x.HasValue, x.Value, If(y.HasValue, y.Value, z)) + End Sub +End Class", +"Imports System + +Class C + Sub M(x as Integer?, y as Integer?, z as Integer?) + dim w = If(x, If(y, z)) + End Sub +End Class") + End Function + End Class +End Namespace \ No newline at end of file diff --git a/src/EditorFeatures/VisualBasicTest/UseCoalesceExpression/UseCoalesceExpressionTests.vb b/src/EditorFeatures/VisualBasicTest/UseCoalesceExpression/UseCoalesceExpressionTests.vb new file mode 100644 index 0000000000000000000000000000000000000000..d702742b1feb94e9e0f51fb202e960adaabe6d18 --- /dev/null +++ b/src/EditorFeatures/VisualBasicTest/UseCoalesceExpression/UseCoalesceExpressionTests.vb @@ -0,0 +1,241 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports Microsoft.CodeAnalysis.CodeFixes +Imports Microsoft.CodeAnalysis.Diagnostics +Imports Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.Diagnostics +Imports Microsoft.CodeAnalysis.UseCoalesceExpression +Imports Microsoft.CodeAnalysis.VisualBasic.UseCoalesceExpression + +Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.UseCoalesceExpression + Public Class UseCoalesceExpressionTests + Inherits AbstractVisualBasicDiagnosticProviderBasedUserDiagnosticTest + + Friend Overrides Function CreateDiagnosticProviderAndFixer(workspace As Workspace) As Tuple(Of DiagnosticAnalyzer, CodeFixProvider) + Return New Tuple(Of DiagnosticAnalyzer, CodeFixProvider)( + New VisualBasicUseCoalesceExpressionDiagnosticAnalyzer(), + New UseCoalesceExpressionCodeFixProvider()) + End Function + + + Public Async Function TestOnLeft_Equals() As Task + Await TestAsync( +" +Imports System + +Class C + Sub M(x as string, y as string) + Dim z = [||]If (x Is Nothing, y, x) + End Sub +End Class", +"Imports System + +Class C + Sub M(x as string, y as string) + Dim z = If(x, y) + End Sub +End Class") + End Function + + + Public Async Function TestOnLeft_NotEquals() As Task + Await TestAsync( +" +Imports System + +Class C + Sub M(x as string, y as string) + Dim z = [||]If(x IsNot Nothing, x, y) + End Sub +End Class", +"Imports System + +Class C + Sub M(x as string, y as string) + Dim z = If(x, y) + End Sub +End Class") + End Function + + + Public Async Function TestOnRight_Equals() As Task + Await TestAsync( +" +Imports System + +Class C + Sub M(x as string, y as string) + Dim z = [||]If(Nothing Is x, y, x) + End Sub +End Class", +"Imports System + +Class C + Sub M(x as string, y as string) + Dim z = If(x, y) + End Sub +End Class") + End Function + + + Public Async Function TestOnRight_NotEquals() As Task + Await TestAsync( +" +Imports System + +Class C + Sub M(x as string, y as string) + Dim z = [||]If(Nothing IsNot x, x, y) + End Sub +End Class", +"Imports System + +Class C + Sub M(x as string, y as string) + Dim z = If(x, y) + End Sub +End Class") + End Function + + + Public Async Function TestComplexExpression() As Task + Await TestAsync( +" +Imports System + +Class C + Sub M(x as string, y as string) + Dim z = [||]If (x.ToString() is Nothing, y, x.ToString()) + End Sub +End Class", +"Imports System + +Class C + Sub M(x as string, y as string) + Dim z = If(x.ToString(), y) + End Sub +End Class") + End Function + + + Public Async Function TestParens1() As Task + Await TestAsync( +" +Imports System + +Class C + Sub M(x as string, y as string) + Dim z = [||]If ((x Is Nothing), y, x) + End Sub +End Class", +"Imports System + +Class C + Sub M(x as string, y as string) + Dim z = If(x, y) + End Sub +End Class") + End Function + + + Public Async Function TestParens2() As Task + Await TestAsync( +" +Imports System + +Class C + Sub M(x as string, y as string) + Dim z = [||]If ((x) Is Nothing, y, x) + End Sub +End Class", +"Imports System + +Class C + Sub M(x as string, y as string) + Dim z = If(x, y) + End Sub +End Class") + End Function + + + Public Async Function TestParens3() As Task + Await TestAsync( +" +Imports System + +Class C + Sub M(x as string, y as string) + Dim z = [||]If (x Is Nothing, y, (x)) + End Sub +End Class", +"Imports System + +Class C + Sub M(x as string, y as string) + Dim z = If(x, y) + End Sub +End Class") + End Function + + + Public Async Function TestParens4() As Task + Await TestAsync( +" +Imports System + +Class C + Sub M(x as string, y as string) + Dim z = [||]If (x Is Nothing, (y), x) + End Sub +End Class", +"Imports System + +Class C + Sub M(x as string, y as string) + Dim z = If(x, y) + End Sub +End Class") + End Function + + + Public Async Function TestFixAll1() As Task + Await TestAsync( +" +Imports System + +Class C + Sub M(x as string, y as string) + Dim z1 = {|FixAllInDocument:If|} (x is Nothing, y, x) + Dim z2 = If(x IsNot Nothing, x, y) + End Sub +End Class", +"Imports System + +Class C + Sub M(x as string, y as string) + Dim z1 = If(x, y) + Dim z2 = If(x, y) + End Sub +End Class") + End Function + + + Public Async Function TestFixAll2() As Task + Await TestAsync( +" +Imports System + +Class C + Sub M(x as string, y as string, z as string) + dim w = {|FixAllInDocument:If|} (x isnot Nothing, x, If(y isnot Nothing, y, z)) + End Sub +End Class", +"Imports System + +Class C + Sub M(x as string, y as string, z as string) + dim w = If(x, If(y, z)) + End Sub +End Class") + End Function + End Class +End Namespace \ No newline at end of file diff --git a/src/Features/CSharp/Portable/CSharpFeatures.csproj b/src/Features/CSharp/Portable/CSharpFeatures.csproj index 34cd473fbf1f5c6cd87778ed451802c6b522b95b..eb0650f5e3cbcee4312133ced08d2fa374a6ad6a 100644 --- a/src/Features/CSharp/Portable/CSharpFeatures.csproj +++ b/src/Features/CSharp/Portable/CSharpFeatures.csproj @@ -55,6 +55,8 @@ InternalUtilities\LambdaUtilities.cs + + diff --git a/src/Features/CSharp/Portable/UseCoalesceExpression/CSharpUseCoalesceExpressionDiagnosticAnalyzer.cs b/src/Features/CSharp/Portable/UseCoalesceExpression/CSharpUseCoalesceExpressionDiagnosticAnalyzer.cs new file mode 100644 index 0000000000000000000000000000000000000000..0001415e930da104dba5738fc053006b5033119c --- /dev/null +++ b/src/Features/CSharp/Portable/UseCoalesceExpression/CSharpUseCoalesceExpressionDiagnosticAnalyzer.cs @@ -0,0 +1,30 @@ +// 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 Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.UseCoalesceExpression; + +namespace Microsoft.CodeAnalysis.CSharp.UseCoalesceExpression +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal class CSharpUseCoalesceExpressionDiagnosticAnalyzer : + AbstractUseCoalesceExpressionDiagnosticAnalyzer< + SyntaxKind, + ExpressionSyntax, + ConditionalExpressionSyntax, + BinaryExpressionSyntax> + { + protected override ISyntaxFactsService GetSyntaxFactsService() + => CSharpSyntaxFactsService.Instance; + + protected override SyntaxKind GetSyntaxKindToAnalyze() + => SyntaxKind.ConditionalExpression; + + protected override bool IsEquals(BinaryExpressionSyntax condition) + => condition.Kind() == SyntaxKind.EqualsExpression; + + protected override bool IsNotEquals(BinaryExpressionSyntax condition) + => condition.Kind() == SyntaxKind.NotEqualsExpression; + } +} \ No newline at end of file diff --git a/src/Features/CSharp/Portable/UseCoalesceExpression/CSharpUseCoalesceExpressionForNullableDiagnosticAnalyzer.cs b/src/Features/CSharp/Portable/UseCoalesceExpression/CSharpUseCoalesceExpressionForNullableDiagnosticAnalyzer.cs new file mode 100644 index 0000000000000000000000000000000000000000..b3a22312cd001ac2282a81da28d024f26a6ca5b8 --- /dev/null +++ b/src/Features/CSharp/Portable/UseCoalesceExpression/CSharpUseCoalesceExpressionForNullableDiagnosticAnalyzer.cs @@ -0,0 +1,26 @@ +// 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 Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.UseCoalesceExpression; + +namespace Microsoft.CodeAnalysis.CSharp.UseCoalesceExpression +{ + [DiagnosticAnalyzer(LanguageNames.CSharp)] + internal class CSharpUseCoalesceExpressionForNullableDiagnosticAnalyzer : + AbstractUseCoalesceExpressionForNullableDiagnosticAnalyzer< + SyntaxKind, + ExpressionSyntax, + ConditionalExpressionSyntax, + BinaryExpressionSyntax, + MemberAccessExpressionSyntax, + PrefixUnaryExpressionSyntax> + { + protected override ISyntaxFactsService GetSyntaxFactsService() + => CSharpSyntaxFactsService.Instance; + + protected override SyntaxKind GetSyntaxKindToAnalyze() + => SyntaxKind.ConditionalExpression; + } +} \ No newline at end of file diff --git a/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs b/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs index ddc1c436a5cda6b3e7f2af0a231d8d9eb3b54eb3..89a7877b2ba14e04a03d58c6789fcfa7bb54270c 100644 --- a/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs +++ b/src/Features/Core/Portable/Diagnostics/Analyzers/IDEDiagnosticIds.cs @@ -33,6 +33,9 @@ internal static class IDEDiagnosticIds public const string UseExpressionBodyForIndexersDiagnosticId = "IDE0025"; public const string UseExpressionBodyForAccessorsDiagnosticId = "IDE0026"; + public const string UseCoalesceExpressionDiagnosticId = "IDE0028"; + public const string UseCoalesceExpressionForNullableDiagnosticId = "IDE0029"; + // Analyzer error Ids public const string AnalyzerChangedId = "IDE1001"; public const string AnalyzerDependencyConflictId = "IDE1002"; diff --git a/src/Features/Core/Portable/Features.csproj b/src/Features/Core/Portable/Features.csproj index 28dd45dac9751c8ec1977411af27688d990358b0..0a79e11b1152e44c06ef71f5805ab0c816281ade 100644 --- a/src/Features/Core/Portable/Features.csproj +++ b/src/Features/Core/Portable/Features.csproj @@ -121,6 +121,10 @@ + + + + diff --git a/src/Features/Core/Portable/FeaturesResources.Designer.cs b/src/Features/Core/Portable/FeaturesResources.Designer.cs index 77b5dfa07e6be161d3e8e3c0e131841f3443f076..3678dacbda45cb06b6d3938635cd890d3fbf2d95 100644 --- a/src/Features/Core/Portable/FeaturesResources.Designer.cs +++ b/src/Features/Core/Portable/FeaturesResources.Designer.cs @@ -3037,6 +3037,15 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Use coalesce expression. + /// + internal static string Use_coalesce_expression { + get { + return ResourceManager.GetString("Use_coalesce_expression", resourceCulture); + } + } + /// /// Looks up a localized string similar to Use expression body for accessors. /// diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index 83a61dac4da1ebe9184feaca4d87c65d935f922c..a1d7bc43102d73220dcc06a7dfec8c3e791bff13 100644 --- a/src/Features/Core/Portable/FeaturesResources.resx +++ b/src/Features/Core/Portable/FeaturesResources.resx @@ -1151,4 +1151,7 @@ This version used in: {2} Pascal Case + + Use coalesce expression + \ No newline at end of file diff --git a/src/Features/Core/Portable/UseCoalesceExpression/AbstractUseCoalesceExpressionDiagnosticAnalyzer.cs b/src/Features/Core/Portable/UseCoalesceExpression/AbstractUseCoalesceExpressionDiagnosticAnalyzer.cs new file mode 100644 index 0000000000000000000000000000000000000000..e06cf2cca4d793a69422d9f9e6feb10b89d4f2ab --- /dev/null +++ b/src/Features/Core/Portable/UseCoalesceExpression/AbstractUseCoalesceExpressionDiagnosticAnalyzer.cs @@ -0,0 +1,111 @@ +// 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 Microsoft.CodeAnalysis.CodeStyle; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServices; + +namespace Microsoft.CodeAnalysis.UseCoalesceExpression +{ + internal abstract class AbstractUseCoalesceExpressionDiagnosticAnalyzer< + TSyntaxKind, + TExpressionSyntax, + TConditionalExpressionSyntax, + TBinaryExpressionSyntax> : AbstractCodeStyleDiagnosticAnalyzer + where TSyntaxKind : struct + where TExpressionSyntax : SyntaxNode + where TConditionalExpressionSyntax : TExpressionSyntax + where TBinaryExpressionSyntax : TExpressionSyntax + { + protected AbstractUseCoalesceExpressionDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseCoalesceExpressionDiagnosticId, + new LocalizableResourceString(nameof(FeaturesResources.Use_coalesce_expression), FeaturesResources.ResourceManager, typeof(FeaturesResources))) + { + } + + protected abstract TSyntaxKind GetSyntaxKindToAnalyze(); + protected abstract ISyntaxFactsService GetSyntaxFactsService(); + protected abstract bool IsEquals(TBinaryExpressionSyntax condition); + protected abstract bool IsNotEquals(TBinaryExpressionSyntax condition); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeSyntax, GetSyntaxKindToAnalyze()); + } + + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + var conditionalExpression = (TConditionalExpressionSyntax)context.Node; + + var optionSet = context.Options.GetOptionSet(); + var option = optionSet.GetOption(CodeStyleOptions.PreferCoalesceExpression, conditionalExpression.Language); + if (!option.Value) + { + return; + } + + var syntaxFacts = this.GetSyntaxFactsService(); + + SyntaxNode conditionNode, whenTrueNodeHigh, whenFalseNodeHigh; + syntaxFacts.GetPartsOfConditionalExpression( + conditionalExpression, out conditionNode, out whenTrueNodeHigh, out whenFalseNodeHigh); + + conditionNode = syntaxFacts.WalkDownParentheses(conditionNode); + var whenTrueNodeLow = syntaxFacts.WalkDownParentheses(whenTrueNodeHigh); + var whenFalseNodeLow = syntaxFacts.WalkDownParentheses(whenFalseNodeHigh); + + var condition = conditionNode as TBinaryExpressionSyntax; + if (condition == null) + { + return; + } + + var isEquals = IsEquals(condition); + var isNotEquals = IsNotEquals(condition); + if (!isEquals && !isNotEquals) + { + return; + } + + SyntaxNode conditionLeftHigh; + SyntaxNode conditionRightHigh; + syntaxFacts.GetPartsOfBinaryExpression(condition, out conditionLeftHigh, out conditionRightHigh); + + var conditionLeftLow = syntaxFacts.WalkDownParentheses(conditionLeftHigh); + var conditionRightLow = syntaxFacts.WalkDownParentheses(conditionRightHigh); + + var conditionLeftIsNull = syntaxFacts.IsNullLiteralExpression(conditionLeftLow); + var conditionRightIsNull = syntaxFacts.IsNullLiteralExpression(conditionRightLow); + + if (conditionRightIsNull && conditionLeftIsNull) + { + // null == null nothing to do here. + return; + } + + if (!conditionRightIsNull && !conditionLeftIsNull) + { + return; + } + + if (!syntaxFacts.AreEquivalent( + conditionRightIsNull ? conditionLeftLow : conditionRightLow, + isEquals ? whenFalseNodeLow : whenTrueNodeLow)) + { + return; + } + + var conditionPartToCheck = conditionRightIsNull ? conditionLeftHigh : conditionRightHigh; + var whenPartToKeep = isEquals ? whenTrueNodeHigh : whenFalseNodeHigh; + var locations = ImmutableArray.Create( + conditionalExpression.GetLocation(), + conditionPartToCheck.GetLocation(), + whenPartToKeep.GetLocation()); + + context.ReportDiagnostic(Diagnostic.Create( + this.CreateDescriptor(this.DescriptorId, option.Notification.Value), + conditionalExpression.GetLocation(), + locations)); + } + } +} \ No newline at end of file diff --git a/src/Features/Core/Portable/UseCoalesceExpression/AbstractUseCoalesceExpressionForNullableDiagnosticAnalyzer.cs b/src/Features/Core/Portable/UseCoalesceExpression/AbstractUseCoalesceExpressionForNullableDiagnosticAnalyzer.cs new file mode 100644 index 0000000000000000000000000000000000000000..e8898d53b2b9a62dc62845d26b4b5743b618c61b --- /dev/null +++ b/src/Features/Core/Portable/UseCoalesceExpression/AbstractUseCoalesceExpressionForNullableDiagnosticAnalyzer.cs @@ -0,0 +1,137 @@ +// 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.Immutable; +using Microsoft.CodeAnalysis.CodeStyle; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.LanguageServices; + +namespace Microsoft.CodeAnalysis.UseCoalesceExpression +{ + internal abstract class AbstractUseCoalesceExpressionForNullableDiagnosticAnalyzer< + TSyntaxKind, + TExpressionSyntax, + TConditionalExpressionSyntax, + TBinaryExpressionSyntax, + TMemberAccessExpression, + TPrefixUnaryExpressionSyntax> : AbstractCodeStyleDiagnosticAnalyzer + where TSyntaxKind : struct + where TExpressionSyntax : SyntaxNode + where TConditionalExpressionSyntax : TExpressionSyntax + where TBinaryExpressionSyntax : TExpressionSyntax + where TMemberAccessExpression : TExpressionSyntax + where TPrefixUnaryExpressionSyntax : TExpressionSyntax + { + protected AbstractUseCoalesceExpressionForNullableDiagnosticAnalyzer() + : base(IDEDiagnosticIds.UseCoalesceExpressionForNullableDiagnosticId, + new LocalizableResourceString(nameof(FeaturesResources.Use_coalesce_expression), FeaturesResources.ResourceManager, typeof(FeaturesResources))) + { + } + + protected abstract TSyntaxKind GetSyntaxKindToAnalyze(); + protected abstract ISyntaxFactsService GetSyntaxFactsService(); + + public override void Initialize(AnalysisContext context) + { + context.RegisterSyntaxNodeAction(AnalyzeSyntax, GetSyntaxKindToAnalyze()); + } + + private void AnalyzeSyntax(SyntaxNodeAnalysisContext context) + { + var conditionalExpression = (TConditionalExpressionSyntax)context.Node; + + var optionSet = context.Options.GetOptionSet(); + var option = optionSet.GetOption(CodeStyleOptions.PreferCoalesceExpression, conditionalExpression.Language); + if (!option.Value) + { + return; + } + + var syntaxFacts = this.GetSyntaxFactsService(); + + SyntaxNode conditionNode, whenTrueNodeHigh, whenFalseNodeHigh; + syntaxFacts.GetPartsOfConditionalExpression( + conditionalExpression, out conditionNode, out whenTrueNodeHigh, out whenFalseNodeHigh); + + conditionNode = syntaxFacts.WalkDownParentheses(conditionNode); + var whenTrueNodeLow = syntaxFacts.WalkDownParentheses(whenTrueNodeHigh); + var whenFalseNodeLow = syntaxFacts.WalkDownParentheses(whenFalseNodeHigh); + + var notHasValueExpression = false; + if (syntaxFacts.IsLogicalNotExpression(conditionNode)) + { + notHasValueExpression = true; + conditionNode = syntaxFacts.GetOperandOfPrefixUnaryExpression(conditionNode); + } + + var conditionMemberAccess = conditionNode as TMemberAccessExpression; + if (conditionMemberAccess == null) + { + return; + } + + SyntaxNode conditionExpression, conditionSimpleName; + syntaxFacts.GetPartsOfMemberAccessExpression(conditionMemberAccess, out conditionExpression, out conditionSimpleName); + + string conditionName; int unused; + syntaxFacts.GetNameAndArityOfSimpleName(conditionSimpleName, out conditionName, out unused); + + if (conditionName != nameof(Nullable.HasValue)) + { + return; + } + + var whenPartToCheck = notHasValueExpression ? whenFalseNodeLow : whenTrueNodeLow; + var whenPartMemberAccess = whenPartToCheck as TMemberAccessExpression; + if (whenPartMemberAccess == null) + { + return; + } + + SyntaxNode whenPartExpression, whenPartSimpleName; + syntaxFacts.GetPartsOfMemberAccessExpression(whenPartMemberAccess, out whenPartExpression, out whenPartSimpleName); + + string whenPartName; + syntaxFacts.GetNameAndArityOfSimpleName(whenPartSimpleName, out whenPartName, out unused); + + if (whenPartName != nameof(Nullable.Value)) + { + return; + } + + if (!syntaxFacts.AreEquivalent(conditionExpression, whenPartExpression)) + { + return; + } + + // Syntactically this looks like something we can simplify. Make sure we're + // actually looking at something Nullable (and not some type that uses a similar + // syntactic pattern). + var semanticModel = context.SemanticModel; + var nullableType = semanticModel.Compilation.GetTypeByMetadataName("System.Nullable`1"); + if (nullableType == null) + { + return; + } + + var cancellationToken = context.CancellationToken; + var type = semanticModel.GetTypeInfo(conditionExpression, cancellationToken); + + if (!nullableType.Equals(type.Type?.OriginalDefinition)) + { + return; + } + + var whenPartToKeep = notHasValueExpression ? whenTrueNodeHigh : whenFalseNodeHigh; + var locations = ImmutableArray.Create( + conditionalExpression.GetLocation(), + conditionExpression.GetLocation(), + whenPartToKeep.GetLocation()); + + context.ReportDiagnostic(Diagnostic.Create( + this.CreateDescriptor(this.DescriptorId, option.Notification.Value), + conditionalExpression.GetLocation(), + locations)); + } + } +} \ No newline at end of file diff --git a/src/Features/Core/Portable/UseCoalesceExpression/UseCoalesceExpressionCodeFixProvider.cs b/src/Features/Core/Portable/UseCoalesceExpression/UseCoalesceExpressionCodeFixProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..6f18b3cfa1997bd56ca94bfcbc0ce0d37a6513b4 --- /dev/null +++ b/src/Features/Core/Portable/UseCoalesceExpression/UseCoalesceExpressionCodeFixProvider.cs @@ -0,0 +1,109 @@ +// 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.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.CodeFixes.FixAllOccurrences; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.UseCoalesceExpression +{ + [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared] + internal class UseCoalesceExpressionCodeFixProvider : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds + => ImmutableArray.Create(IDEDiagnosticIds.UseCoalesceExpressionDiagnosticId); + + public override FixAllProvider GetFixAllProvider() => new UseCoalesceExpressionFixAllProvider(this); + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + context.RegisterCodeFix(new MyCodeAction( + c => FixAsync(context.Document, context.Diagnostics[0], c)), + context.Diagnostics); + return SpecializedTasks.EmptyTask; + } + + private Task FixAsync( + Document document, + Diagnostic diagnostic, + CancellationToken cancellationToken) + { + return FixAllAsync(document, ImmutableArray.Create(diagnostic), cancellationToken); + } + + private async Task FixAllAsync( + Document document, + ImmutableArray diagnostics, + CancellationToken cancellationToken) + { + var syntaxFacts = document.GetLanguageService(); + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var editor = new SyntaxEditor(root, document.Project.Solution.Workspace); + var generator = editor.Generator; + + foreach (var diagnostic in diagnostics) + { + var conditionalExpression = root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan, getInnermostNodeForTie: true); + var conditionalPartHigh = root.FindNode(diagnostic.AdditionalLocations[1].SourceSpan); + var whenPart = root.FindNode(diagnostic.AdditionalLocations[2].SourceSpan); + + SyntaxNode condition, whenTrue, whenFalse; + syntaxFacts.GetPartsOfConditionalExpression( + conditionalExpression, out condition, out whenTrue, out whenFalse); + + var conditionalPartLow = syntaxFacts.WalkDownParentheses(conditionalPartHigh); + editor.ReplaceNode(conditionalExpression, + (c, g) => { + SyntaxNode currentCondition, currentWhenTrue, currentWhenFalse; + syntaxFacts.GetPartsOfConditionalExpression( + c, out currentCondition, out currentWhenTrue, out currentWhenFalse); + + return whenPart == whenTrue + ? g.CoalesceExpression(conditionalPartLow, syntaxFacts.WalkDownParentheses(currentWhenTrue)) + : g.CoalesceExpression(conditionalPartLow, syntaxFacts.WalkDownParentheses(currentWhenFalse)); + }); + } + + var newRoot = editor.GetChangedRoot(); + return document.WithSyntaxRoot(newRoot); + } + + private class UseCoalesceExpressionFixAllProvider : DocumentBasedFixAllProvider + { + private readonly UseCoalesceExpressionCodeFixProvider _provider; + + public UseCoalesceExpressionFixAllProvider(UseCoalesceExpressionCodeFixProvider provider) + { + _provider = provider; + } + + protected override Task FixDocumentAsync( + Document document, ImmutableArray diagnostics, CancellationToken cancellationToken) + { + var filteredDiagnostics = diagnostics.WhereAsArray( + d => !d.Descriptor.CustomTags.Contains(WellKnownDiagnosticTags.Unnecessary)); + return _provider.FixAllAsync(document, filteredDiagnostics, cancellationToken); + } + } + + private class MyCodeAction : CodeAction.DocumentChangeAction + { + public MyCodeAction(Func> createChangedDocument) + : base(FeaturesResources.Use_coalesce_expression, createChangedDocument) + { + + } + } + } +} \ No newline at end of file diff --git a/src/Features/Core/Portable/UseCoalesceExpression/UseCoalesceExpressionForNullableCodeFixProvider.cs b/src/Features/Core/Portable/UseCoalesceExpression/UseCoalesceExpressionForNullableCodeFixProvider.cs new file mode 100644 index 0000000000000000000000000000000000000000..8dec7b90ad9e46fbd11d402c030974d328efbbfd --- /dev/null +++ b/src/Features/Core/Portable/UseCoalesceExpression/UseCoalesceExpressionForNullableCodeFixProvider.cs @@ -0,0 +1,108 @@ +// 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.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.CodeFixes.FixAllOccurrences; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Editing; +using Microsoft.CodeAnalysis.LanguageServices; +using Microsoft.CodeAnalysis.Shared.Extensions; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.UseCoalesceExpression +{ + [ExportCodeFixProvider(LanguageNames.CSharp, LanguageNames.VisualBasic), Shared] + internal class UseCoalesceExpressionForNullableCodeFixProvider : CodeFixProvider + { + public override ImmutableArray FixableDiagnosticIds + => ImmutableArray.Create(IDEDiagnosticIds.UseCoalesceExpressionForNullableDiagnosticId); + + public override FixAllProvider GetFixAllProvider() => new UseCoalesceExpressionForNullableFixAllProvider(this); + + public override Task RegisterCodeFixesAsync(CodeFixContext context) + { + context.RegisterCodeFix(new MyCodeAction( + c => FixAsync(context.Document, context.Diagnostics[0], c)), + context.Diagnostics); + return SpecializedTasks.EmptyTask; + } + + private Task FixAsync( + Document document, + Diagnostic diagnostic, + CancellationToken cancellationToken) + { + return FixAllAsync(document, ImmutableArray.Create(diagnostic), cancellationToken); + } + + private async Task FixAllAsync( + Document document, + ImmutableArray diagnostics, + CancellationToken cancellationToken) + { + var syntaxFacts = document.GetLanguageService(); + var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); + + var editor = new SyntaxEditor(root, document.Project.Solution.Workspace); + var generator = editor.Generator; + + foreach (var diagnostic in diagnostics) + { + var conditionalExpression = root.FindNode(diagnostic.AdditionalLocations[0].SourceSpan, getInnermostNodeForTie: true); + var conditionExpression = root.FindNode(diagnostic.AdditionalLocations[1].SourceSpan); + var whenPart = root.FindNode(diagnostic.AdditionalLocations[2].SourceSpan); + + SyntaxNode condition, whenTrue, whenFalse; + syntaxFacts.GetPartsOfConditionalExpression( + conditionalExpression, out condition, out whenTrue, out whenFalse); + + editor.ReplaceNode(conditionalExpression, + (c, g) => { + SyntaxNode currentCondition, currentWhenTrue, currentWhenFalse; + syntaxFacts.GetPartsOfConditionalExpression( + c, out currentCondition, out currentWhenTrue, out currentWhenFalse); + + return whenPart == whenTrue + ? g.CoalesceExpression(conditionExpression, syntaxFacts.WalkDownParentheses(currentWhenTrue)) + : g.CoalesceExpression(conditionExpression, syntaxFacts.WalkDownParentheses(currentWhenFalse)); + }); + } + + var newRoot = editor.GetChangedRoot(); + return document.WithSyntaxRoot(newRoot); + } + + private class UseCoalesceExpressionForNullableFixAllProvider : DocumentBasedFixAllProvider + { + private readonly UseCoalesceExpressionForNullableCodeFixProvider _provider; + + public UseCoalesceExpressionForNullableFixAllProvider(UseCoalesceExpressionForNullableCodeFixProvider provider) + { + _provider = provider; + } + + protected override Task FixDocumentAsync( + Document document, ImmutableArray diagnostics, CancellationToken cancellationToken) + { + var filteredDiagnostics = diagnostics.WhereAsArray( + d => !d.Descriptor.CustomTags.Contains(WellKnownDiagnosticTags.Unnecessary)); + return _provider.FixAllAsync(document, filteredDiagnostics, cancellationToken); + } + } + + private class MyCodeAction : CodeAction.DocumentChangeAction + { + public MyCodeAction(Func> createChangedDocument) + : base(FeaturesResources.Use_coalesce_expression, createChangedDocument) + { + + } + } + } +} \ No newline at end of file diff --git a/src/Features/VisualBasic/Portable/BasicFeatures.vbproj b/src/Features/VisualBasic/Portable/BasicFeatures.vbproj index bea3fd1fcb933ed572b0758582a61467911715ef..113ac52862a87cc0aa5452d19b754f24d36c80e2 100644 --- a/src/Features/VisualBasic/Portable/BasicFeatures.vbproj +++ b/src/Features/VisualBasic/Portable/BasicFeatures.vbproj @@ -5,7 +5,8 @@ AnyCPU {A1BCD0CE-6C2F-4F8C-9A48-D9D93928E26D} Library - + + Microsoft.CodeAnalysis.VisualBasic.Features .NETPortable v5.0 @@ -429,6 +430,8 @@ + + diff --git a/src/Features/VisualBasic/Portable/UseCoalesceExpression/VisualBasicUseCoalesceExpressionDiagnosticAnalyzer.vb b/src/Features/VisualBasic/Portable/UseCoalesceExpression/VisualBasicUseCoalesceExpressionDiagnosticAnalyzer.vb new file mode 100644 index 0000000000000000000000000000000000000000..f8f65b8d5af09e3add0b752325fd8b1c0d3a1fd5 --- /dev/null +++ b/src/Features/VisualBasic/Portable/UseCoalesceExpression/VisualBasicUseCoalesceExpressionDiagnosticAnalyzer.vb @@ -0,0 +1,33 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports Microsoft.CodeAnalysis.Diagnostics +Imports Microsoft.CodeAnalysis.LanguageServices +Imports Microsoft.CodeAnalysis.UseCoalesceExpression +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax + +Namespace Microsoft.CodeAnalysis.VisualBasic.UseCoalesceExpression + + Friend Class VisualBasicUseCoalesceExpressionDiagnosticAnalyzer + Inherits AbstractUseCoalesceExpressionDiagnosticAnalyzer(Of + SyntaxKind, + ExpressionSyntax, + TernaryConditionalExpressionSyntax, + BinaryExpressionSyntax) + + Protected Overrides Function GetSyntaxFactsService() As ISyntaxFactsService + Return VisualBasicSyntaxFactsService.Instance + End Function + + Protected Overrides Function GetSyntaxKindToAnalyze() As SyntaxKind + Return SyntaxKind.TernaryConditionalExpression + End Function + + Protected Overrides Function IsEquals(condition As BinaryExpressionSyntax) As Boolean + Return condition.Kind() = SyntaxKind.IsExpression + End Function + + Protected Overrides Function IsNotEquals(condition As BinaryExpressionSyntax) As Boolean + Return condition.Kind() = SyntaxKind.IsNotExpression + End Function + End Class +End Namespace \ No newline at end of file diff --git a/src/Features/VisualBasic/Portable/UseCoalesceExpression/VisualBasicUseCoalesceExpressionForNullableDiagnosticAnalyzer.vb b/src/Features/VisualBasic/Portable/UseCoalesceExpression/VisualBasicUseCoalesceExpressionForNullableDiagnosticAnalyzer.vb new file mode 100644 index 0000000000000000000000000000000000000000..16dc451b5daf41772b081774d17b574a9e3784a9 --- /dev/null +++ b/src/Features/VisualBasic/Portable/UseCoalesceExpression/VisualBasicUseCoalesceExpressionForNullableDiagnosticAnalyzer.vb @@ -0,0 +1,27 @@ +' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +Imports Microsoft.CodeAnalysis.Diagnostics +Imports Microsoft.CodeAnalysis.LanguageServices +Imports Microsoft.CodeAnalysis.UseCoalesceExpression +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax + +Namespace Microsoft.CodeAnalysis.VisualBasic.UseCoalesceExpression + + Friend Class VisualBasicUseCoalesceExpressionForNullableDiagnosticAnalyzer + Inherits AbstractUseCoalesceExpressionForNullableDiagnosticAnalyzer(Of + SyntaxKind, + ExpressionSyntax, + TernaryConditionalExpressionSyntax, + BinaryExpressionSyntax, + MemberAccessExpressionSyntax, + UnaryExpressionSyntax) + + Protected Overrides Function GetSyntaxFactsService() As ISyntaxFactsService + Return VisualBasicSyntaxFactsService.Instance + End Function + + Protected Overrides Function GetSyntaxKindToAnalyze() As SyntaxKind + Return SyntaxKind.TernaryConditionalExpression + End Function + End Class +End Namespace \ No newline at end of file diff --git a/src/VisualStudio/CSharp/Impl/Options/Formatting/StyleViewModel.cs b/src/VisualStudio/CSharp/Impl/Options/Formatting/StyleViewModel.cs index 2e88b34b02237e3b5b5cc4ac7cbc18e36d970b56..5eb995fc877ca94c10bf375a4fdb5f992052784f 100644 --- a/src/VisualStudio/CSharp/Impl/Options/Formatting/StyleViewModel.cs +++ b/src/VisualStudio/CSharp/Impl/Options/Formatting/StyleViewModel.cs @@ -249,6 +249,27 @@ public C(string s) //] } } +"; + + private static readonly string s_preferCoalesceExpression = @" +using System; + +class C +{ + private string s; + + public C(string s) + { +//[ + // Prefer: + var v = x ?? y; + + // Over: + var v = x != null ? x : y; // or + var v = x == null ? y : x; +//] + } +} "; private static readonly string s_preferConditionalDelegateCall = @" @@ -598,6 +619,7 @@ internal StyleViewModel(OptionSet optionSet, IServiceProvider serviceProvider) : // Null preferences. CodeStyleItems.Add(new SimpleCodeStyleOptionViewModel(CodeStyleOptions.PreferThrowExpression, CSharpVSResources.Prefer_throw_expression, s_preferThrowExpression, s_preferThrowExpression, this, optionSet, nullCheckingGroupTitle)); CodeStyleItems.Add(new SimpleCodeStyleOptionViewModel(CSharpCodeStyleOptions.PreferConditionalDelegateCall, CSharpVSResources.Prefer_conditional_delegate_call, s_preferConditionalDelegateCall, s_preferConditionalDelegateCall, this, optionSet, nullCheckingGroupTitle)); + CodeStyleItems.Add(new SimpleCodeStyleOptionViewModel(CodeStyleOptions.PreferCoalesceExpression, ServicesVSResources.Prefer_coalesce_expression, s_preferCoalesceExpression, s_preferCoalesceExpression, this, optionSet, nullCheckingGroupTitle)); } } } \ No newline at end of file diff --git a/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs b/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs index 4f4e8ff05a183ee68a8f25c19d691b3a23af0236..349b94b901ffa2ed4b0ce6cb8e46e72afaa8f812 100644 --- a/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs +++ b/src/VisualStudio/Core/Def/ServicesVSResources.Designer.cs @@ -1365,6 +1365,15 @@ internal class ServicesVSResources { } } + /// + /// Looks up a localized string similar to Prefer coalesce expression. + /// + internal static string Prefer_coalesce_expression { + get { + return ResourceManager.GetString("Prefer_coalesce_expression", resourceCulture); + } + } + /// /// Looks up a localized string similar to Prefer framework type. /// diff --git a/src/VisualStudio/Core/Def/ServicesVSResources.resx b/src/VisualStudio/Core/Def/ServicesVSResources.resx index 417c32725b0106199433b1d8136f3d9c6b9e4ba5..713ce6086342a8652171407a5974fa8a435f89a3 100644 --- a/src/VisualStudio/Core/Def/ServicesVSResources.resx +++ b/src/VisualStudio/Core/Def/ServicesVSResources.resx @@ -804,4 +804,7 @@ Additional information: {1} This item cannot be deleted because it is used by an existing Naming Rule. + + Prefer coalesce expression + \ No newline at end of file diff --git a/src/VisualStudio/VisualBasic/Impl/BasicVSResources.Designer.vb b/src/VisualStudio/VisualBasic/Impl/BasicVSResources.Designer.vb index 7d994bff1bb0f1297f2ac927e09af6b3277c71dc..38b1f6e14e58e3018a8ae6563456a548ce693f3c 100644 --- a/src/VisualStudio/VisualBasic/Impl/BasicVSResources.Designer.vb +++ b/src/VisualStudio/VisualBasic/Impl/BasicVSResources.Designer.vb @@ -325,6 +325,15 @@ Namespace Microsoft.VisualStudio.LanguageServices.VisualBasic End Get End Property + ''' + ''' Looks up a localized string similar to 'nothing' checking:. + ''' + Friend Shared ReadOnly Property nothing_checking_colon() As String + Get + Return ResourceManager.GetString("nothing_checking_colon", resourceCulture) + End Get + End Property + ''' ''' Looks up a localized string similar to _Only add new line on enter after end of fully typed word. ''' diff --git a/src/VisualStudio/VisualBasic/Impl/BasicVSResources.resx b/src/VisualStudio/VisualBasic/Impl/BasicVSResources.resx index bea1a34e97d96777755d747e00f8d1ddbdae1fbc..6327a685e47d6ab6e9428e032474f5d1903b9b66 100644 --- a/src/VisualStudio/VisualBasic/Impl/BasicVSResources.resx +++ b/src/VisualStudio/VisualBasic/Impl/BasicVSResources.resx @@ -267,4 +267,7 @@ _Show completion list after a character is typed + + 'nothing' checking: + \ No newline at end of file diff --git a/src/VisualStudio/VisualBasic/Impl/Options/StyleViewModel.vb b/src/VisualStudio/VisualBasic/Impl/Options/StyleViewModel.vb index fbe94d07d06f63e99cdf24620f248e204cbd85a5..0f494a3566673a09b83750f1f68041589aa83a81 100644 --- a/src/VisualStudio/VisualBasic/Impl/Options/StyleViewModel.vb +++ b/src/VisualStudio/VisualBasic/Impl/Options/StyleViewModel.vb @@ -165,6 +165,24 @@ Class Customer End Sub End Class" + Private Shared ReadOnly s_preferCoalesceExpression As String = " +Imports System + +Class Customer + Private Age As Integer + + Sub New() +//[ + ' Prefer: + Dim v = If(x, y) + + ' Over: + Dim v = If(x Is Nothing, y, x) ' or + Dim v = If(x IsNot Nothing, x, y) +//] + End Sub +End Class" + #End Region Public Sub New(optionSet As OptionSet, serviceProvider As IServiceProvider) @@ -188,7 +206,7 @@ End Class" } Dim expressionPreferencesGroupTitle = ServicesVSResources.Expression_preferences_colon - + Dim nothingPreferencesGroupTitle = BasicVSResources.nothing_checking_colon ' qualify with Me. group Me.CodeStyleItems.Add(New SimpleCodeStyleOptionViewModel(CodeStyleOptions.QualifyFieldAccess, BasicVSResources.Qualify_field_access_with_Me, s_fieldDeclarationPreviewTrue, s_fieldDeclarationPreviewFalse, Me, optionSet, qualifyGroupTitle, qualifyMemberAccessPreferences)) @@ -203,6 +221,8 @@ End Class" ' expression preferences Me.CodeStyleItems.Add(New SimpleCodeStyleOptionViewModel(CodeStyleOptions.PreferObjectInitializer, ServicesVSResources.Prefer_object_initializer, s_preferObjectInitializer, s_preferObjectInitializer, Me, optionSet, expressionPreferencesGroupTitle)) + ' nothing preferences + Me.CodeStyleItems.Add(New SimpleCodeStyleOptionViewModel(CodeStyleOptions.PreferCoalesceExpression, ServicesVSResources.Prefer_coalesce_expression, s_preferCoalesceExpression, s_preferCoalesceExpression, Me, optionSet, nothingPreferencesGroupTitle)) End Sub End Class End Namespace \ No newline at end of file diff --git a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs index e68131ea5e23e7b297275096f56e5cd88d944410..994770a89b1708d9190b9c1251be798f3946629b 100644 --- a/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs +++ b/src/Workspaces/CSharp/Portable/LanguageServices/CSharpSyntaxFactsService.cs @@ -1871,6 +1871,40 @@ public bool IsPossibleTupleContext(SyntaxTree syntaxTree, int position, Cancella return syntaxTree.IsPossibleTupleContext(token, position); } + public bool IsNullLiteralExpression(SyntaxNode node) + => node.Kind() == SyntaxKind.NullLiteralExpression; + + public void GetPartsOfBinaryExpression(SyntaxNode node, out SyntaxNode left, out SyntaxNode right) + { + var binaryExpression = (BinaryExpressionSyntax)node; + left = binaryExpression.Left; + right = binaryExpression.Right; + } + + public void GetPartsOfConditionalExpression(SyntaxNode node, out SyntaxNode condition, out SyntaxNode whenTrue, out SyntaxNode whenFalse) + { + var conditionalExpression = (ConditionalExpressionSyntax)node; + condition = conditionalExpression.Condition; + whenTrue = conditionalExpression.WhenTrue; + whenFalse = conditionalExpression.WhenFalse; + } + + public SyntaxNode WalkDownParentheses(SyntaxNode node) + => (node as ExpressionSyntax)?.WalkDownParentheses() ?? node; + + public bool IsLogicalNotExpression(SyntaxNode node) + => node.Kind() == SyntaxKind.LogicalNotExpression; + + public SyntaxNode GetOperandOfPrefixUnaryExpression(SyntaxNode node) + => ((PrefixUnaryExpressionSyntax)node).Operand; + + public void GetPartsOfMemberAccessExpression(SyntaxNode node, out SyntaxNode expression, out SyntaxNode name) + { + var memberAccessExpression = (MemberAccessExpressionSyntax)node; + expression = memberAccessExpression.Expression; + name = memberAccessExpression.Name; + } + private class AddFirstMissingCloseBaceRewriter: CSharpSyntaxRewriter { private readonly SyntaxNode _contextNode; diff --git a/src/Workspaces/Core/Portable/CodeStyle/CodeStyleOptions.cs b/src/Workspaces/Core/Portable/CodeStyle/CodeStyleOptions.cs index 9c27bb56c6d3cce8e209c87798d9fabdc304ba17..5982c0e0529bac1b2eda0d6c7db54872e616d96b 100644 --- a/src/Workspaces/Core/Portable/CodeStyle/CodeStyleOptions.cs +++ b/src/Workspaces/Core/Portable/CodeStyle/CodeStyleOptions.cs @@ -70,6 +70,12 @@ public class CodeStyleOptions defaultValue: false, storageLocations: new RoamingProfileStorageLocation("TextEditor.%LANGUAGE%.Specific.PreferObjectInitializer")); + internal static readonly PerLanguageOption> PreferCoalesceExpression = new PerLanguageOption>( + nameof(CodeStyleOptions), + nameof(PreferCoalesceExpression), + defaultValue: TrueWithSuggestionEnforcement, + storageLocations: new RoamingProfileStorageLocation("TextEditor.%LANGUAGE%.Specific.PreferCoalesceExpression")); + internal static readonly PerLanguageOption> PreferInlinedVariableDeclaration = new PerLanguageOption>( nameof(CodeStyleOptions), nameof(PreferInlinedVariableDeclaration), diff --git a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs index 47eb763a4175298a913b1e526ba7c51d2c224961..92a325cd9ce5e97b695d16ecdd5e0aa9aa0be9a5 100644 --- a/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs +++ b/src/Workspaces/Core/Portable/LanguageServices/SyntaxFactsService/ISyntaxFactsService.cs @@ -29,7 +29,7 @@ internal interface ISyntaxFactsService : ILanguageService bool IsLiteral(SyntaxToken token); bool IsStringLiteralOrInterpolatedStringLiteral(SyntaxToken token); bool IsStringLiteral(SyntaxToken token); - bool IsNumericLiteralExpression(SyntaxNode node); + bool IsTypeNamedVarInVariableOrFieldDeclaration(SyntaxToken token, SyntaxNode parent); bool IsTypeNamedDynamic(SyntaxToken token, SyntaxNode parent); bool IsDocumentationComment(SyntaxNode node); @@ -37,6 +37,9 @@ internal interface ISyntaxFactsService : ILanguageService bool IsGlobalAttribute(SyntaxNode node); bool IsDeclaration(SyntaxNode node); + bool IsNumericLiteralExpression(SyntaxNode node); + bool IsNullLiteralExpression(SyntaxNode node); + string GetText(int kind); bool IsInInactiveRegion(SyntaxTree syntaxTree, int position, CancellationToken cancellationToken); @@ -52,6 +55,9 @@ internal interface ISyntaxFactsService : ILanguageService bool IsObjectCreationExpression(SyntaxNode node); SyntaxNode GetObjectCreationInitializer(SyntaxNode objectCreationExpression); + void GetPartsOfBinaryExpression(SyntaxNode node, out SyntaxNode left, out SyntaxNode right); + void GetPartsOfConditionalExpression(SyntaxNode node, out SyntaxNode condition, out SyntaxNode whenTrue, out SyntaxNode whenFalse); + bool IsInvocationExpression(SyntaxNode node); bool IsExpressionOfInvocationExpression(SyntaxNode node); SyntaxNode GetExpressionOfInvocationExpression(SyntaxNode node); @@ -59,6 +65,9 @@ internal interface ISyntaxFactsService : ILanguageService bool IsExpressionOfAwaitExpression(SyntaxNode node); SyntaxNode GetExpressionOfAwaitExpression(SyntaxNode node); + bool IsLogicalNotExpression(SyntaxNode node); + SyntaxNode GetOperandOfPrefixUnaryExpression(SyntaxNode node); + // Left side of = assignment. bool IsLeftSideOfAssignment(SyntaxNode node); @@ -81,9 +90,10 @@ internal interface ISyntaxFactsService : ILanguageService bool IsNameOfMemberAccessExpression(SyntaxNode node); bool IsExpressionOfMemberAccessExpression(SyntaxNode node); - SyntaxNode GetNameOfMemberAccessExpression(SyntaxNode memberAccessExpression); - SyntaxNode GetExpressionOfMemberAccessExpression(SyntaxNode memberAccessExpression); - SyntaxToken GetOperatorTokenOfMemberAccessExpression(SyntaxNode memberAccessExpression); + SyntaxNode GetNameOfMemberAccessExpression(SyntaxNode node); + SyntaxNode GetExpressionOfMemberAccessExpression(SyntaxNode node); + SyntaxToken GetOperatorTokenOfMemberAccessExpression(SyntaxNode node); + void GetPartsOfMemberAccessExpression(SyntaxNode node, out SyntaxNode expression, out SyntaxNode name); bool IsSimpleMemberAccessExpression(SyntaxNode node); bool IsPointerMemberAccessExpression(SyntaxNode node); @@ -176,6 +186,7 @@ internal interface ISyntaxFactsService : ILanguageService SyntaxToken FindTokenOnRightOfPosition(SyntaxNode node, int position, bool includeSkipped = true, bool includeDirectives = false, bool includeDocumentationComments = false); SyntaxNode Parenthesize(SyntaxNode expression, bool includeElasticTrivia = true); + SyntaxNode WalkDownParentheses(SyntaxNode node); SyntaxNode ConvertToSingleLine(SyntaxNode node, bool useElasticTrivia = false); diff --git a/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb b/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb index d8978c90cbb60d8e1a5477ef4bbec47fe14943ba..82478ba05df4eb1cc4ab3063631cebf2ceaf550e 100644 --- a/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb +++ b/src/Workspaces/VisualBasic/Portable/LanguageServices/VisualBasicSyntaxFactsService.vb @@ -1546,5 +1546,41 @@ Namespace Microsoft.CodeAnalysis.VisualBasic Dim token = syntaxTree.FindTokenOnLeftOfPosition(position, cancellationToken) Return syntaxTree.IsPossibleTupleContext(token, position) End Function + + Public Function IsNullLiteralExpression(node As SyntaxNode) As Boolean Implements ISyntaxFactsService.IsNullLiteralExpression + Return node.Kind() = SyntaxKind.NothingLiteralExpression + End Function + + Public Sub GetPartsOfBinaryExpression(node As SyntaxNode, ByRef left As SyntaxNode, ByRef right As SyntaxNode) Implements ISyntaxFactsService.GetPartsOfBinaryExpression + Dim binaryExpression = DirectCast(node, BinaryExpressionSyntax) + left = binaryExpression.Left + right = binaryExpression.Right + End Sub + + Public Sub GetPartsOfConditionalExpression(node As SyntaxNode, ByRef condition As SyntaxNode, ByRef whenTrue As SyntaxNode, ByRef whenFalse As SyntaxNode) Implements ISyntaxFactsService.GetPartsOfConditionalExpression + Dim conditionalExpression = DirectCast(node, TernaryConditionalExpressionSyntax) + condition = conditionalExpression.Condition + whenTrue = conditionalExpression.WhenTrue + whenFalse = conditionalExpression.WhenFalse + End Sub + + Public Function WalkDownParentheses(node As SyntaxNode) As SyntaxNode Implements ISyntaxFactsService.WalkDownParentheses + Return If(TryCast(node, ExpressionSyntax)?.WalkDownParentheses(), node) + End Function + + Public Function IsLogicalNotExpression(node As SyntaxNode) As Boolean Implements ISyntaxFactsService.IsLogicalNotExpression + Return node.IsKind(SyntaxKind.NotExpression) + Throw New NotImplementedException() + End Function + + Public Function GetOperandOfPrefixUnaryExpression(node As SyntaxNode) As SyntaxNode Implements ISyntaxFactsService.GetOperandOfPrefixUnaryExpression + Return DirectCast(node, UnaryExpressionSyntax).Operand + End Function + + Public Sub GetPartsOfMemberAccessExpression(node As SyntaxNode, ByRef expression As SyntaxNode, ByRef name As SyntaxNode) Implements ISyntaxFactsService.GetPartsOfMemberAccessExpression + Dim memberAccess = DirectCast(node, MemberAccessExpressionSyntax) + expression = memberAccess.Expression + name = memberAccess.Name + End Sub End Class End Namespace \ No newline at end of file