From 4ebf96580878fb35ef962b70fb22a05af8756ae8 Mon Sep 17 00:00:00 2001 From: Cyrus Najmabadi Date: Wed, 16 Sep 2015 21:45:32 -0700 Subject: [PATCH] Add VB side of the ReplaceMethodWithProperty refactoring. --- .../BasicEditorServicesTest.vbproj | 1 + .../ReplaceMethodWithPropertyTests.vb | 276 ++++++++++++++++++ .../CSharpFeaturesResources.Designer.cs | 18 -- .../Portable/CSharpFeaturesResources.resx | 6 - .../CSharpReplaceMethodWithPropertyService.cs | 26 +- .../Portable/FeaturesResources.Designer.cs | 20 +- .../Core/Portable/FeaturesResources.resx | 7 +- .../IReplaceMethodWithPropertyService.cs | 4 +- ...thodWithPropertyCodeRefactoringProvider.cs | 7 +- .../VisualBasic/Portable/BasicFeatures.vbproj | 3 + ...alBasicReplaceMethodWithPropertyService.vb | 233 +++++++++++++++ 11 files changed, 564 insertions(+), 37 deletions(-) create mode 100644 src/EditorFeatures/VisualBasicTest/CodeActions/ReplaceMethodWithProperty/ReplaceMethodWithPropertyTests.vb create mode 100644 src/Features/VisualBasic/Portable/ReplaceMethodWithProperty/VisualBasicReplaceMethodWithPropertyService.vb diff --git a/src/EditorFeatures/VisualBasicTest/BasicEditorServicesTest.vbproj b/src/EditorFeatures/VisualBasicTest/BasicEditorServicesTest.vbproj index 34e8c3bdb72..25206c95466 100644 --- a/src/EditorFeatures/VisualBasicTest/BasicEditorServicesTest.vbproj +++ b/src/EditorFeatures/VisualBasicTest/BasicEditorServicesTest.vbproj @@ -614,6 +614,7 @@ PreserveNewest + PreserveNewest diff --git a/src/EditorFeatures/VisualBasicTest/CodeActions/ReplaceMethodWithProperty/ReplaceMethodWithPropertyTests.vb b/src/EditorFeatures/VisualBasicTest/CodeActions/ReplaceMethodWithProperty/ReplaceMethodWithPropertyTests.vb new file mode 100644 index 00000000000..5cb8e732148 --- /dev/null +++ b/src/EditorFeatures/VisualBasicTest/CodeActions/ReplaceMethodWithProperty/ReplaceMethodWithPropertyTests.vb @@ -0,0 +1,276 @@ +' 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.Editor.VisualBasic.UnitTests.CodeRefactorings +Imports Microsoft.CodeAnalysis.ReplaceMethodWithProperty +Imports Roslyn.Test.Utilities +Imports Xunit + +Namespace Microsoft.CodeAnalysis.Editor.VisualBasic.UnitTests.CodeActions.ReplaceMethodWithProperty + Public Class ReplaceMethodWithPropertyTests + Inherits AbstractVisualBasicCodeActionTest + + Protected Overrides Function CreateCodeRefactoringProvider(workspace As Workspace) As Object + Return New ReplaceMethodWithPropertyCodeRefactoringProvider() + End Function + + + Public Sub TestMethodWithGetName() + Test( +NewLines("class C \n function [||]GetFoo() as integer \n End function \n End class"), +NewLines("class C \n ReadOnly Property Foo as integer \n Get \n End Get \n End Property \n End class")) + End Sub + + + Public Sub TestMethodWithoutGetName() + Test( +NewLines("class C \n function [||]Foo() as integer \n End function \n End class"), +NewLines("class C \n ReadOnly Property Foo as integer \n Get \n End Get \n End Property \n End class")) + End Sub + + + Public Sub TestMethodWithoutBody() + Test( +NewLines("mustinherit class C \n MustOverride function [||]GetFoo() as integer \n End class"), +NewLines("mustinherit class C \n MustOverride ReadOnly Property Foo as integer \n End class")) + End Sub + + + Public Sub TestMethodWithModifiers() + Test( +NewLines("class C \n public shared function [||]GetFoo() as integer \n End function \n End class"), +NewLines("class C \n public shared ReadOnly Property Foo as integer \n Get \n End Get \n End Property \n End class")) + End Sub + + + Public Sub TestMethodWithAttributes() + Test( +NewLines("class C \n function [||]GetFoo() as integer \n End function \n End class"), +NewLines("class C \n ReadOnly Property Foo as integer \n Get \n End Get \n End Property \n End class")) + End Sub + + + Public Sub TestMethodWithTrivia_1() + Test( +"class C + ' Foo + function [||]GetFoo() as integer + End function +End class", +"class C + ' Foo + ReadOnly Property Foo as integer + Get + End Get + End Property +End class", +compareTokens:=False) + End Sub + + + Public Sub TestIfDefMethod() + Test( +"class C +#if true + function [||]GetFoo() as integer + End function +#End if +End class", +"class C +#if true + ReadOnly Property Foo as integer + Get + End Get + End Property +#End if +End class") + End Sub + + + Public Sub TestMethodWithTrivia_2() + Test( +"class C + ' Foo + function [||]GetFoo() as integer + End function + ' SetFoo + sub SetFoo(i as integer) + End sub +End class", +"class C + ' Foo + ' SetFoo + Property Foo as integer + Get + End Get + Set(i as integer) + End Set + End Property +End class", +index:=1, +compareTokens:=False) + End Sub + + + Public Sub TestExplicitInterfaceMethod_2() + Test( +NewLines("interface I \n function GetFoo() as integer \n End interface \n class C \n implements I \n function [||]GetFoo() as integer implements I.GetFoo \n End function \n End class"), +NewLines("interface I \n ReadOnly Property Foo as integer \n End interface \n class C \n implements I \n ReadOnly Property Foo as integer implements I.Foo \n Get \n End Get \n End Property \n End class")) + End Sub + + + Public Sub TestExplicitInterfaceMethod_3() + Test( +NewLines("interface I \n function [||]GetFoo() as integer \n End interface \n class C \n implements I \n function GetFoo() as integer implements I.GetFoo \n End function \n End class"), +NewLines("interface I \n ReadOnly Property Foo as integer \n End interface \n class C \n implements I \n ReadOnly Property Foo as integer implements I.Foo \n Get \n End Get \n End Property \n End class")) + End Sub + + + Public Sub TestInAttribute() + TestMissing( +NewLines("class C \n function GetFoo() as integer \n End function \n End class")) + End Sub + + + Public Sub TestInMethod() + TestMissing( +NewLines("class C \n function GetFoo() as integer \n [||] \n End function \n End class")) + End Sub + + + Public Sub TestSubMethod() + TestMissing( +NewLines("class C \n sub [||]GetFoo() \n End sub \n End class")) + End Sub + + + Public Sub TestAsyncMethod() + TestMissing( +NewLines("class C \n async function [||]GetFoo() as Task \n End function \n End class")) + End Sub + + + Public Sub TestGenericMethod() + TestMissing( +NewLines("class C \n function [||]GetFoo(of T)() as integer \n End function \n End class")) + End Sub + + + + Public Sub TestExtensionMethod() + TestMissing( +NewLines("module C \n function [||]GetFoo(i as integer) as integer \n End function \n End module")) + End Sub + + + Public Sub TestMethodWithParameters_1() + TestMissing( +NewLines("class C \n function [||]GetFoo(i as integer) as integer \n End function \n End class")) + End Sub + + + Public Sub TestUpdateGetReferenceNotInMethod() + Test( +NewLines("class C \n function [||]GetFoo() as integer \n End function \n sub Bar() \n dim x = GetFoo() \n End sub \n End class"), +NewLines("class C \n ReadOnly Property Foo as integer \n Get \n End Get \n End Property \n sub Bar() \n dim x = Foo \n End sub \n End class")) + End Sub + + + Public Sub TestUpdateGetReferenceMemberAccessInvocation() + Test( +NewLines("class C \n function [||]GetFoo() as integer \n End function \n sub Bar() \n dim x = me.GetFoo() \n End sub \n End class"), +NewLines("class C \n ReadOnly Property Foo as integer \n Get \n End Get \n End Property \n sub Bar() \n dim x = me.Foo \n End sub \n End class")) + End Sub + + + Public Sub TestUpdateGetReferenceBindingMemberInvocation() + Test( +NewLines("class C \n function [||]GetFoo() as integer \n End function \n sub Bar() \n dim x as C \n dim v = x?.GetFoo() \n End sub \n End class"), +NewLines("class C \n ReadOnly Property Foo as integer \n Get \n End Get \n End Property \n sub Bar() \n dim x as C \n dim v = x?.Foo \n End sub \n End class")) + End Sub + + + Public Sub TestUpdateGetReferenceInMethod() + Test( +NewLines("class C \n function [||]GetFoo() as integer \n return GetFoo() \n End function \n End class"), +NewLines("class C \n ReadOnly Property Foo as integer \n Get \n return Foo \n End Get \n End Property \n End class")) + End Sub + + + Public Sub TestOverride() + Test( +NewLines("class C \n public overridable function [||]GetFoo() as integer \n End function \n End class \n class D \n inherits C \n public overrides function GetFoo() as integer \n End function \n End class"), +NewLines("class C \n public overridable ReadOnly Property Foo as integer \n Get \n End Get \n End Property \n End class \n class D \n inherits C \n public overrides ReadOnly Property Foo as integer \n Get \n End Get \n End Property \n End class")) + End Sub + + + Public Sub TestUpdateGetReference_NonInvoked() + Test( +NewLines("class C \n function [||]GetFoo() as integer \n End function \n sub Bar() \n dim i = GetFoo \n End sub \n End class"), +NewLines("class C \n ReadOnly Property Foo as integer \n Get \n End Get \n End Property \n sub Bar() \n dim i = Foo \n End sub \n End class")) + End Sub + + + Public Sub TestUpdateGetSet() + Test( +NewLines("class C \n function [||]GetFoo() as integer \n End function \n sub SetFoo(i as integer) \n End sub \n End class"), +NewLines("class C \n Property Foo as integer \n Get \n End Get \n Set(i as integer) \n End Set \n End Property \n End class"), +index:=1) + End Sub + + + Public Sub TestUpdateGetSetReference_NonInvoked() + Test( +NewLines("Imports System \n class C \n function [||]GetFoo() as integer \n End function \n sub SetFoo(i as integer) \n End sub \n sub Bar() \n dim i as Action(of integer) = addressof SetFoo \n End sub \n End class"), +NewLines("Imports System \n class C \n Property Foo as integer \n Get \n End Get \n Set(i as integer) \n End Set \n End Property \n sub Bar() \n dim i as Action(of integer) = addressof {|Conflict:Foo|} \n End sub \n End class"), +index:=1) + End Sub + + + Public Sub TestUpdateGetSet_SetterAccessibility() + Test( +NewLines("class C \n public function [||]GetFoo() as integer \n End function \n private sub SetFoo(i as integer) \n End sub \n End class"), +NewLines("class C \n public Property Foo as integer \n Get End Get \n Private Set(i as integer) \n End Set \n End Property \n End class"), +index:=1) + End Sub + + + Public Sub TestUpdateGetSet_GetInSetReference() + Test( +NewLines("class C \n function [||]GetFoo() as integer \n End function \n sub SetFoo(i as integer) \n End sub \n sub Bar() \n SetFoo(GetFoo() + 1) \n End sub \n End class"), +NewLines("class C \n Property Foo as integer \n Get \n End Get \n Set(i as integer) \n End Set \n End Property \n sub Bar() \n Foo = Foo + 1 \n End sub \n End class"), +index:=1) + End Sub + + + Public Sub TestUpdateGetSet_SetReferenceInSetter() + Test( +NewLines("class C \n function [||]GetFoo() as integer \n End function \n sub SetFoo(i as integer) \n SetFoo(i - 1) \n End sub \n End class"), +NewLines("class C \n Property Foo as integer \n Get \n End Get \n Set(i as integer) \n Foo = i - 1 \n End Set \n End Property \n End class"), +index:=1) + End Sub + + + Public Sub TestVirtualGetWithOverride_1() + Test( +NewLines("class C \n protected overridable function [||]GetFoo() as integer \n End function \n End class \n class D \n inherits C \n protected overrides function GetFoo() as integer \n End function \n End class"), +NewLines("class C \n protected overridable ReadOnly Property Foo as integer \n Get \n End Get \n End Property \n End class \n class D \n inherits C \n protected overrides ReadOnly Property Foo as integer \n Get \n End Get \n End Property \n End class"), +index:=0) + End Sub + + + Public Sub TestVirtualGetWithOverride_2() + Test( +NewLines("class C \n protected overridable function [||]GetFoo() as integer \n End function \n End class \n class D \n inherits C \n protected overrides function GetFoo() as integer \n return mybase.GetFoo() \n End function \n End class"), +NewLines("class C \n protected overridable ReadOnly Property Foo as integer \n Get \n End Get \n End Property \n End class \n class D \n inherits C \n protected overrides ReadOnly Property Foo as integer \n Get \n return mybase.Foo \n End Get \n End Property \n End class"), +index:=0) + End Sub + + + Public Sub TestWithPartialClasses() + Test( +NewLines("partial class C \n function [||]GetFoo() as integer \n End function \n End class \n partial class C \n sub SetFoo(i as integer) \n End sub \n End class"), +NewLines("partial class C \n Property Foo as integer \n Get \n End Get \n Set(i as integer) \n End Set \n End Property \n End class \n partial class C \n End class"), +index:=1) + End Sub + End Class +End Namespace \ No newline at end of file diff --git a/src/Features/CSharp/Portable/CSharpFeaturesResources.Designer.cs b/src/Features/CSharp/Portable/CSharpFeaturesResources.Designer.cs index e83483f889c..6795361f436 100644 --- a/src/Features/CSharp/Portable/CSharpFeaturesResources.Designer.cs +++ b/src/Features/CSharp/Portable/CSharpFeaturesResources.Designer.cs @@ -565,15 +565,6 @@ internal class CSharpFeaturesResources { } } - /// - /// Looks up a localized string similar to Non-invoked method cannot be replaced with property.. - /// - internal static string NonInvokedMethodCannotBeReplacedWithProperty { - get { - return ResourceManager.GetString("NonInvokedMethodCannotBeReplacedWithProperty", resourceCulture); - } - } - /// /// Looks up a localized string similar to Not all code paths return. /// @@ -601,15 +592,6 @@ internal class CSharpFeaturesResources { } } - /// - /// Looks up a localized string similar to Only methods with a single argument can be replaced with a property.. - /// - internal static string OnlyMethodsWithASingleArgumentCanBeReplacedWithAProperty { - get { - return ResourceManager.GetString("OnlyMethodsWithASingleArgumentCanBeReplacedWithAProperty", resourceCulture); - } - } - /// /// Looks up a localized string similar to orderby clause. /// diff --git a/src/Features/CSharp/Portable/CSharpFeaturesResources.resx b/src/Features/CSharp/Portable/CSharpFeaturesResources.resx index 0215b179cc6..c323779c201 100644 --- a/src/Features/CSharp/Portable/CSharpFeaturesResources.resx +++ b/src/Features/CSharp/Portable/CSharpFeaturesResources.resx @@ -434,10 +434,4 @@ The name '{0}' does not exist in the current context. - - Non-invoked method cannot be replaced with property. - - - Only methods with a single argument can be replaced with a property. - \ No newline at end of file diff --git a/src/Features/CSharp/Portable/ReplaceMethodWithProperty/CSharpReplaceMethodWithPropertyService.cs b/src/Features/CSharp/Portable/ReplaceMethodWithProperty/CSharpReplaceMethodWithPropertyService.cs index 346eb79b883..f97222f7928 100644 --- a/src/Features/CSharp/Portable/ReplaceMethodWithProperty/CSharpReplaceMethodWithPropertyService.cs +++ b/src/Features/CSharp/Portable/ReplaceMethodWithProperty/CSharpReplaceMethodWithPropertyService.cs @@ -44,17 +44,33 @@ public SyntaxNode GetMethodDeclaration(SyntaxToken token) return containingMethod; } - public SyntaxNode ConvertMethodsToProperty( + public void RemoveSetMethod(SyntaxEditor editor, SyntaxNode setMethodDeclaration) + { + editor.RemoveNode(setMethodDeclaration); + } + + public void ReplaceGetMethodWithProperty( + SyntaxEditor editor, SemanticModel semanticModel, - SyntaxGenerator generator, GetAndSetMethods getAndSetMethods, + GetAndSetMethods getAndSetMethods, string propertyName, bool nameChanged) { var getMethodDeclaration = getAndSetMethods.GetMethodDeclaration as MethodDeclarationSyntax; if (getMethodDeclaration == null) { - return getAndSetMethods.GetMethodDeclaration; + return; } + editor.ReplaceNode(getMethodDeclaration, + ConvertMethodsToProperty(semanticModel, editor.Generator, getAndSetMethods, propertyName, nameChanged)); + } + + public SyntaxNode ConvertMethodsToProperty( + SemanticModel semanticModel, + SyntaxGenerator generator, GetAndSetMethods getAndSetMethods, + string propertyName, bool nameChanged) + { + var getMethodDeclaration = getAndSetMethods.GetMethodDeclaration as MethodDeclarationSyntax; var getAccessor = CreateGetAccessor(getAndSetMethods); var setAccessor = CreateSetAccessor(semanticModel, generator, getAndSetMethods); @@ -195,7 +211,7 @@ public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node) { if (invocation.ArgumentList?.Arguments.Count != 1) { - var annotation = ConflictAnnotation.Create(CSharpFeaturesResources.OnlyMethodsWithASingleArgumentCanBeReplacedWithAProperty); + var annotation = ConflictAnnotation.Create(FeaturesResources.OnlyMethodsWithASingleArgumentCanBeReplacedWithAProperty); editor.ReplaceNode(nameNode, newName.WithIdentifier(newName.Identifier.WithAdditionalAnnotations(annotation))); return; } @@ -255,7 +271,7 @@ public void ReplaceSetReference(SyntaxEditor editor, SyntaxToken nameToken, stri if (!IsInvocationName(nameNode, invocationExpression)) { // Wasn't invoked. Change the name, but report a conflict. - var annotation = ConflictAnnotation.Create(CSharpFeaturesResources.NonInvokedMethodCannotBeReplacedWithProperty); + var annotation = ConflictAnnotation.Create(FeaturesResources.NonInvokedMethodCannotBeReplacedWithProperty); editor.ReplaceNode(nameNode, newName.WithIdentifier(newName.Identifier.WithAdditionalAnnotations(annotation))); return; } diff --git a/src/Features/Core/Portable/FeaturesResources.Designer.cs b/src/Features/Core/Portable/FeaturesResources.Designer.cs index 487b7939f31..f9c3584b86c 100644 --- a/src/Features/Core/Portable/FeaturesResources.Designer.cs +++ b/src/Features/Core/Portable/FeaturesResources.Designer.cs @@ -422,7 +422,7 @@ internal class FeaturesResources { } /// - /// Looks up a localized string similar to >>>>>>> master. + /// Looks up a localized string similar to Change '{0}' to '{1}'.. /// internal static string ChangeTo { get { @@ -1501,6 +1501,15 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Non-invoked method cannot be replaced with property.. + /// + internal static string NonInvokedMethodCannotBeReplacedWithProperty { + get { + return ResourceManager.GetString("NonInvokedMethodCannotBeReplacedWithProperty", resourceCulture); + } + } + /// /// Looks up a localized string similar to Ceasing to access captured variable '{0}' in {1} will prevent the debug session from continuing.. /// @@ -1537,6 +1546,15 @@ internal class FeaturesResources { } } + /// + /// Looks up a localized string similar to Only methods with a single argument can be replaced with a property.. + /// + internal static string OnlyMethodsWithASingleArgumentCanBeReplacedWithAProperty { + get { + return ResourceManager.GetString("OnlyMethodsWithASingleArgumentCanBeReplacedWithAProperty", resourceCulture); + } + } + /// /// Looks up a localized string similar to operator. /// diff --git a/src/Features/Core/Portable/FeaturesResources.resx b/src/Features/Core/Portable/FeaturesResources.resx index 4cc165517ff..33cb88d710d 100644 --- a/src/Features/Core/Portable/FeaturesResources.resx +++ b/src/Features/Core/Portable/FeaturesResources.resx @@ -851,6 +851,11 @@ Do you want to continue? Change '{0}' to '{1}'. ->>>>>>> master + + + Non-invoked method cannot be replaced with property. + + + Only methods with a single argument can be replaced with a property. \ No newline at end of file diff --git a/src/Features/Core/Portable/ReplaceMethodWithProperty/IReplaceMethodWithPropertyService.cs b/src/Features/Core/Portable/ReplaceMethodWithProperty/IReplaceMethodWithPropertyService.cs index 5e96b21bac7..2af45e8efc6 100644 --- a/src/Features/Core/Portable/ReplaceMethodWithProperty/IReplaceMethodWithPropertyService.cs +++ b/src/Features/Core/Portable/ReplaceMethodWithProperty/IReplaceMethodWithPropertyService.cs @@ -8,10 +8,12 @@ namespace Microsoft.CodeAnalysis.ReplaceMethodWithProperty interface IReplaceMethodWithPropertyService : ILanguageService { SyntaxNode GetMethodDeclaration(SyntaxToken token); - SyntaxNode ConvertMethodsToProperty(SemanticModel semanticModel, SyntaxGenerator generator, GetAndSetMethods getAndSetMethods, string propertyName, bool nameChanged); string GetMethodName(SyntaxNode methodDeclaration); void ReplaceGetReference(SyntaxEditor editor, SyntaxToken nameToken, string propertyName, bool nameChanged); void ReplaceSetReference(SyntaxEditor editor, SyntaxToken nameToken, string propertyName, bool nameChanged); + + void ReplaceGetMethodWithProperty(SyntaxEditor editor, SemanticModel semanticModel, GetAndSetMethods getAndSetMethods, string propertyName, bool nameChanged); + void RemoveSetMethod(SyntaxEditor editor, SyntaxNode setMethodDeclaration); } internal struct GetAndSetMethods diff --git a/src/Features/Core/Portable/ReplaceMethodWithProperty/ReplaceMethodWithPropertyCodeRefactoringProvider.cs b/src/Features/Core/Portable/ReplaceMethodWithProperty/ReplaceMethodWithPropertyCodeRefactoringProvider.cs index d8f0eb15492..b5ec421eb48 100644 --- a/src/Features/Core/Portable/ReplaceMethodWithProperty/ReplaceMethodWithPropertyCodeRefactoringProvider.cs +++ b/src/Features/Core/Portable/ReplaceMethodWithProperty/ReplaceMethodWithPropertyCodeRefactoringProvider.cs @@ -321,16 +321,13 @@ private async Task UpdateReferencesAsync(Solution updatedSolution, str var root = await updatedDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var editor = new SyntaxEditor(root, updatedSolution.Workspace); - var generator = SyntaxGenerator.GetGenerator(updatedDocument); // First replace all the get methods with properties. foreach (var getSetPair in getSetPairs) { cancellationToken.ThrowIfCancellationRequested(); - var getMethod = getSetPair.GetMethodDeclaration; - editor.ReplaceNode(getMethod, service.ConvertMethodsToProperty( - semanticModel, generator, getSetPair, propertyName, nameChanged)); + service.ReplaceGetMethodWithProperty(editor, semanticModel, getSetPair, propertyName, nameChanged); } // Then remove all the set methods. @@ -344,7 +341,7 @@ private async Task UpdateReferencesAsync(Solution updatedSolution, str var setMethodDocument = updatedSolution.GetDocument(setMethodDeclaration?.SyntaxTree); if (setMethodDocument?.Id == documentId) { - editor.RemoveNode(setMethodDeclaration); + service.RemoveSetMethod(editor, setMethodDeclaration); } } diff --git a/src/Features/VisualBasic/Portable/BasicFeatures.vbproj b/src/Features/VisualBasic/Portable/BasicFeatures.vbproj index 0c212078b95..ca05d422d14 100644 --- a/src/Features/VisualBasic/Portable/BasicFeatures.vbproj +++ b/src/Features/VisualBasic/Portable/BasicFeatures.vbproj @@ -401,6 +401,9 @@ + + + diff --git a/src/Features/VisualBasic/Portable/ReplaceMethodWithProperty/VisualBasicReplaceMethodWithPropertyService.vb b/src/Features/VisualBasic/Portable/ReplaceMethodWithProperty/VisualBasicReplaceMethodWithPropertyService.vb new file mode 100644 index 00000000000..6a7dfad798f --- /dev/null +++ b/src/Features/VisualBasic/Portable/ReplaceMethodWithProperty/VisualBasicReplaceMethodWithPropertyService.vb @@ -0,0 +1,233 @@ +' 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 System +Imports System.Collections.Generic +Imports System.Composition +Imports System.Linq +Imports Microsoft.CodeAnalysis.CodeActions +Imports Microsoft.CodeAnalysis.VisualBasic.Extensions +Imports Microsoft.CodeAnalysis.VisualBasic.Syntax +Imports Microsoft.CodeAnalysis.Editing +Imports Microsoft.CodeAnalysis.Formatting +Imports Microsoft.CodeAnalysis.Host.Mef +Imports Microsoft.CodeAnalysis.ReplaceMethodWithProperty + +Namespace Microsoft.CodeAnalysis.VisualBasic.CodeRefactorings.ReplaceMethodWithProperty + + Friend Class VisualBasicReplaceMethodWithPropertyService + Implements IReplaceMethodWithPropertyService + + Public Function GetMethodName(methodNode As SyntaxNode) As String Implements IReplaceMethodWithPropertyService.GetMethodName + Return DirectCast(methodNode, MethodStatementSyntax).Identifier.ValueText + End Function + + Public Function GetMethodDeclaration(token As SyntaxToken) As SyntaxNode Implements IReplaceMethodWithPropertyService.GetMethodDeclaration + Dim containingMethod = token.Parent.FirstAncestorOrSelf(Of MethodStatementSyntax) + If containingMethod Is Nothing Then + Return Nothing + End If + + Dim start = If(containingMethod.AttributeLists.Count > 0, + containingMethod.AttributeLists.Last().GetLastToken().GetNextToken().SpanStart, + containingMethod.SpanStart) + + ' Offer this refactoring anywhere in the signature of the method. + Dim position = token.SpanStart + If position < start OrElse position > containingMethod.ParameterList.Span.End Then + Return Nothing + End If + + Return containingMethod + End Function + + Public Sub RemoveSetMethod(editor As SyntaxEditor, setMethodDeclaration As SyntaxNode) Implements IReplaceMethodWithPropertyService.RemoveSetMethod + Dim setMethodStatement = TryCast(setMethodDeclaration, MethodStatementSyntax) + If setMethodStatement Is Nothing Then + Return + End If + + Dim methodOrBlock = GetParentIfBlock(setMethodStatement) + editor.RemoveNode(methodOrBlock) + End Sub + + Public Sub ReplaceGetMethodWithProperty( + editor As SyntaxEditor, + semanticModel As SemanticModel, + getAndSetMethods As GetAndSetMethods, + propertyName As String, nameChanged As Boolean) Implements IReplaceMethodWithPropertyService.ReplaceGetMethodWithProperty + + Dim getMethodDeclaration = TryCast(getAndSetMethods.GetMethodDeclaration, MethodStatementSyntax) + If getMethodDeclaration Is Nothing Then + Return + End If + + Dim methodBlockOrStatement = GetParentIfBlock(getMethodDeclaration) + editor.ReplaceNode(methodBlockOrStatement, + ConvertMethodsToProperty(editor, semanticModel, getAndSetMethods, propertyName, nameChanged)) + End Sub + + Private Function GetParentIfBlock(declaration As MethodStatementSyntax) As DeclarationStatementSyntax + If declaration.IsParentKind(SyntaxKind.FunctionBlock) OrElse declaration.IsParentKind(SyntaxKind.SubBlock) Then + Return DirectCast(declaration.Parent, DeclarationStatementSyntax) + End If + + Return declaration + End Function + + Private Function ConvertMethodsToProperty( + editor As SyntaxEditor, + semanticModel As SemanticModel, + getAndSetMethods As GetAndSetMethods, + propertyName As String, nameChanged As Boolean) As DeclarationStatementSyntax + + Dim generator = editor.Generator + + Dim getMethodStatement = DirectCast(getAndSetMethods.GetMethodDeclaration, MethodStatementSyntax) + Dim setMethodStatement = TryCast(getAndSetMethods.SetMethodDeclaration, MethodStatementSyntax) + + Dim propertyNameToken = GetPropertyName(getMethodStatement.Identifier, propertyName, nameChanged) + + Dim newPropertyDeclaration As DeclarationStatementSyntax + If getAndSetMethods.SetMethod Is Nothing Then + Dim modifiers = getMethodStatement.Modifiers.Add(SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword)) + Dim propertyStatement = SyntaxFactory.PropertyStatement( + getMethodStatement.AttributeLists, modifiers, propertyNameToken, Nothing, + getMethodStatement.AsClause, initializer:=Nothing, implementsClause:=getMethodStatement.ImplementsClause) + + If getAndSetMethods.GetMethodDeclaration.IsParentKind(SyntaxKind.FunctionBlock) Then + ' Get method has no body, and we have no setter. Just make a readonly property block + Dim accessor = SyntaxFactory.GetAccessorBlock(SyntaxFactory.GetAccessorStatement(), + DirectCast(getAndSetMethods.GetMethodDeclaration.Parent, MethodBlockBaseSyntax).Statements) + Dim accessors = SyntaxFactory.SingletonList(accessor) + newPropertyDeclaration = SyntaxFactory.PropertyBlock(propertyStatement, accessors) + Else + ' Get method has no body, and we have no setter. Just make a readonly property statement + newPropertyDeclaration = propertyStatement + End If + Else + Dim propertyStatement = SyntaxFactory.PropertyStatement( + getMethodStatement.AttributeLists, getMethodStatement.Modifiers, propertyNameToken, Nothing, + getMethodStatement.AsClause, initializer:=Nothing, implementsClause:=getMethodStatement.ImplementsClause) + + If getAndSetMethods.GetMethodDeclaration.IsParentKind(SyntaxKind.FunctionBlock) AndAlso + getAndSetMethods.SetMethodDeclaration.IsParentKind(SyntaxKind.SubBlock) Then + + Dim getAccessor = SyntaxFactory.GetAccessorBlock(SyntaxFactory.GetAccessorStatement(), + DirectCast(getAndSetMethods.GetMethodDeclaration.Parent, MethodBlockBaseSyntax).Statements) + + Dim setAccessorStatement = SyntaxFactory.SetAccessorStatement() + setAccessorStatement = setAccessorStatement.WithParameterList(setMethodStatement?.ParameterList) + + If getAndSetMethods.GetMethod.DeclaredAccessibility <> getAndSetMethods.SetMethod.DeclaredAccessibility Then + setAccessorStatement = DirectCast(generator.WithAccessibility(setAccessorStatement, getAndSetMethods.SetMethod.DeclaredAccessibility), AccessorStatementSyntax) + End If + + Dim setAccessor = SyntaxFactory.SetAccessorBlock(setAccessorStatement, + DirectCast(getAndSetMethods.SetMethodDeclaration.Parent, MethodBlockBaseSyntax).Statements) + + Dim accessors = SyntaxFactory.List({getAccessor, setAccessor}) + newPropertyDeclaration = SyntaxFactory.PropertyBlock(propertyStatement, accessors) + Else + ' Methods don't have bodies. Just make a property statement + newPropertyDeclaration = propertyStatement + End If + End If + + Dim trivia As IEnumerable(Of SyntaxTrivia) = getMethodStatement.GetLeadingTrivia() + If setMethodStatement IsNot Nothing Then + trivia = trivia.Concat(setMethodStatement.GetLeadingTrivia()) + End If + + newPropertyDeclaration = newPropertyDeclaration.WithLeadingTrivia(trivia) + + Return newPropertyDeclaration.WithAdditionalAnnotations(Formatter.Annotation) + End Function + + Private Function GetPropertyName(identifier As SyntaxToken, propertyName As String, nameChanged As Boolean) As SyntaxToken + Return If(nameChanged, SyntaxFactory.Identifier(propertyName), identifier) + End Function + + Public Sub ReplaceGetReference(editor As SyntaxEditor, nameToken As SyntaxToken, propertyName As String, nameChanged As Boolean) Implements IReplaceMethodWithPropertyService.ReplaceGetReference + If nameToken.Kind() <> SyntaxKind.IdentifierToken Then + Return + End If + + Dim nameNode = TryCast(nameToken.Parent, IdentifierNameSyntax) + If nameNode Is Nothing Then + Return + End If + + Dim newName = If(nameChanged, + SyntaxFactory.IdentifierName(SyntaxFactory.Identifier(propertyName).WithTriviaFrom(nameToken)), + nameNode) + + Dim parentExpression = If(nameNode.IsRightSideOfDot(), DirectCast(nameNode.Parent, ExpressionSyntax), nameNode) + Dim root = If(parentExpression.IsParentKind(SyntaxKind.InvocationExpression), parentExpression.Parent, parentExpression) + + editor.ReplaceNode(root, parentExpression.ReplaceNode(nameNode, newName)) + End Sub + + Public Sub ReplaceSetReference(editor As SyntaxEditor, nameToken As SyntaxToken, propertyName As String, nameChanged As Boolean) Implements IReplaceMethodWithPropertyService.ReplaceSetReference + If nameToken.Kind() <> SyntaxKind.IdentifierToken Then + Return + End If + + Dim nameNode = TryCast(nameToken.Parent, IdentifierNameSyntax) + If nameNode Is Nothing Then + Return + End If + + Dim newName = If(nameChanged, + SyntaxFactory.IdentifierName(SyntaxFactory.Identifier(propertyName).WithTriviaFrom(nameToken)), + nameNode) + + Dim parentExpression = If(nameNode.IsRightSideOfDot(), DirectCast(nameNode.Parent, ExpressionSyntax), nameNode) + If Not parentExpression.IsParentKind(SyntaxKind.InvocationExpression) OrElse + Not parentExpression.Parent.IsParentKind(SyntaxKind.ExpressionStatement) Then + + + ' Wasn't invoked. Change the name, but report a conflict. + Dim annotation = ConflictAnnotation.Create(FeaturesResources.NonInvokedMethodCannotBeReplacedWithProperty) + editor.ReplaceNode(nameNode, Function(n, g) newName.WithIdentifier(newName.Identifier.WithAdditionalAnnotations(annotation))) + Return + End If + + editor.ReplaceNode( + parentExpression.Parent.Parent, + Function(statement, generator) + Dim expressionStatement = DirectCast(statement, ExpressionStatementSyntax) + Dim invocationExpression = DirectCast(expressionStatement.Expression, InvocationExpressionSyntax) + Dim expression = invocationExpression.Expression + Dim name = If(expression.Kind() = SyntaxKind.SimpleMemberAccessExpression, + DirectCast(expression, MemberAccessExpressionSyntax).Name, + If(expression.Kind() = SyntaxKind.IdentifierName, DirectCast(expression, IdentifierNameSyntax), Nothing)) + + If name Is Nothing Then + Return statement + End If + + If invocationExpression.ArgumentList?.Arguments.Count <> 1 Then + Return statement + End If + + Dim result As SyntaxNode = SyntaxFactory.SimpleAssignmentStatement( + expression.ReplaceNode(name, newName), + invocationExpression.ArgumentList.Arguments(0).GetExpression()) + + Return result + End Function) + End Sub + + Private Shared Function IsInvocationName(nameNode As IdentifierNameSyntax, invocationExpression As ExpressionSyntax) As Boolean + If invocationExpression Is nameNode Then + Return True + End If + + If nameNode.IsAnyMemberAccessExpressionName() AndAlso nameNode.Parent Is invocationExpression Then + Return True + End If + + Return False + End Function + End Class +End Namespace \ No newline at end of file -- GitLab