CSharpReplaceMethodWithPropertyService.cs 13.3 KB
Newer Older
C
Cyrus Najmabadi 已提交
1 2 3 4
// Copyright (c) Microsoft.  All Rights Reserved.  Licensed under the Apache License, Version 2.0.  See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
5
using System.Composition;
C
Cyrus Najmabadi 已提交
6
using System.Linq;
7 8 9 10
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
C
Cyrus Najmabadi 已提交
11
using Microsoft.CodeAnalysis.Formatting;
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.ReplaceMethodWithProperty;

namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.ReplaceMethodWithProperty
{
    [ExportLanguageService(typeof(IReplaceMethodWithPropertyService), LanguageNames.CSharp), Shared]
    internal class CSharpReplaceMethodWithPropertyService : IReplaceMethodWithPropertyService
    {
        public string GetMethodName(SyntaxNode methodNode)
        {
            return ((MethodDeclarationSyntax)methodNode).Identifier.ValueText;
        }

        public SyntaxNode GetMethodDeclaration(SyntaxToken token)
        {
            var containingMethod = token.Parent.FirstAncestorOrSelf<MethodDeclarationSyntax>();
            if (containingMethod == null)
            {
                return null;
            }

            var start = containingMethod.AttributeLists.Count > 0
                ? containingMethod.AttributeLists.Last().GetLastToken().GetNextToken().SpanStart
                : containingMethod.SpanStart;

            // Offer this refactoring anywhere in the signature of the method.
            var position = token.SpanStart;
            if (position < start || position > containingMethod.ParameterList.Span.End)
            {
                return null;
            }

            return containingMethod;
        }

47 48 49 50 51 52 53
        public void RemoveSetMethod(SyntaxEditor editor, SyntaxNode setMethodDeclaration)
        {
            editor.RemoveNode(setMethodDeclaration);
        }

        public void ReplaceGetMethodWithProperty(
            SyntaxEditor editor,
54
            SemanticModel semanticModel,
55
            GetAndSetMethods getAndSetMethods,
56
            string propertyName, bool nameChanged)
57
        {
58 59
            var getMethodDeclaration = getAndSetMethods.GetMethodDeclaration as MethodDeclarationSyntax;
            if (getMethodDeclaration == null)
60
            {
61
                return;
62 63
            }

64 65 66 67 68 69 70 71 72 73
            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;
74 75 76 77
            var getAccessor = CreateGetAccessor(getAndSetMethods);
            var setAccessor = CreateSetAccessor(semanticModel, generator, getAndSetMethods);

            var property = SyntaxFactory.PropertyDeclaration(
78 79
                getMethodDeclaration.AttributeLists, getMethodDeclaration.Modifiers,
                getMethodDeclaration.ReturnType, getMethodDeclaration.ExplicitInterfaceSpecifier,
80
                GetPropertyName(getMethodDeclaration.Identifier, propertyName, nameChanged), accessorList: null);
81

C
Cyrus Najmabadi 已提交
82 83 84 85 86 87
            IEnumerable<SyntaxTrivia> trivia = getMethodDeclaration.GetLeadingTrivia();
            var setMethodDeclaration = getAndSetMethods.SetMethodDeclaration;
            if (setMethodDeclaration != null)
            {
                trivia = trivia.Concat(setMethodDeclaration.GetLeadingTrivia());
            }
C
Cyrus Najmabadi 已提交
88
            property = property.WithLeadingTrivia(trivia.Where(t => !t.IsDirective));
C
Cyrus Najmabadi 已提交
89 90

            if (getMethodDeclaration.ExpressionBody != null && setMethodDeclaration == null)
91
            {
92 93
                property = property.WithExpressionBody(getMethodDeclaration.ExpressionBody);
                property = property.WithSemicolonToken(getMethodDeclaration.SemicolonToken);
94
            }
95 96 97 98 99 100 101 102 103 104
            else
            {
                var accessorList = SyntaxFactory.AccessorList(SyntaxFactory.SingletonList(getAccessor));
                if (setAccessor != null)
                {
                    accessorList = accessorList.AddAccessors(new[] { setAccessor });
                }

                property = property.WithAccessorList(accessorList);
            }
105

C
Cyrus Najmabadi 已提交
106
            return property.WithAdditionalAnnotations(Formatter.Annotation);
107 108
        }

109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
        private SyntaxToken GetPropertyName(SyntaxToken identifier, string propertyName, bool nameChanged)
        {
            return nameChanged
                ? SyntaxFactory.Identifier(propertyName)
                : identifier;
        }

        private static AccessorDeclarationSyntax CreateGetAccessor(GetAndSetMethods getAndSetMethods)
        {
            var getMethodDeclaration = getAndSetMethods.GetMethodDeclaration as MethodDeclarationSyntax;

            var accessor = SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration);
            if (getMethodDeclaration.ExpressionBody != null && getAndSetMethods.SetMethodDeclaration != null)
            {
                var statement = SyntaxFactory.ReturnStatement(getMethodDeclaration.ExpressionBody.Expression)
                                             .WithSemicolonToken(getMethodDeclaration.SemicolonToken);
                return accessor.WithBody(SyntaxFactory.Block(statement));
            }

            if (getMethodDeclaration.SemicolonToken.Kind() != SyntaxKind.None)
            {
                return accessor.WithSemicolonToken(getMethodDeclaration.SemicolonToken);
            }

            if (getMethodDeclaration.Body != null)
            {
C
Cyrus Najmabadi 已提交
135
                return accessor.WithBody(getMethodDeclaration.Body.WithAdditionalAnnotations(Formatter.Annotation));
136 137 138 139 140 141 142 143 144 145
            }

            return accessor;
        }

        private static AccessorDeclarationSyntax CreateSetAccessor(
            SemanticModel semanticModel, SyntaxGenerator generator, GetAndSetMethods getAndSetMethods)
        {
            var setMethodDeclaration = getAndSetMethods.SetMethodDeclaration as MethodDeclarationSyntax;
            var setMethod = getAndSetMethods.SetMethod;
C
Cyrus Najmabadi 已提交
146
            if (setMethodDeclaration == null || setMethod?.Parameters.Length != 1)
147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174
            {
                return null;
            }

            var getMethod = getAndSetMethods.GetMethod;
            var accessor = SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration);

            if (getMethod.DeclaredAccessibility != setMethod.DeclaredAccessibility)
            {
                accessor = (AccessorDeclarationSyntax)generator.WithAccessibility(accessor, setMethod.DeclaredAccessibility);
            }

            if (setMethodDeclaration.ExpressionBody != null)
            {
                var expression = ReplaceReferencesToParameterWithValue(semanticModel, setMethod.Parameters[0], setMethodDeclaration.ExpressionBody.Expression);
                var statement = SyntaxFactory.ExpressionStatement(expression)
                                             .WithSemicolonToken(setMethodDeclaration.SemicolonToken);
                return accessor.WithBody(SyntaxFactory.Block(statement));
            }

            if (setMethodDeclaration.SemicolonToken.Kind() != SyntaxKind.None)
            {
                return accessor.WithSemicolonToken(setMethodDeclaration.SemicolonToken);
            }

            if (setMethodDeclaration.Body != null)
            {
                var body = ReplaceReferencesToParameterWithValue(semanticModel, setMethod.Parameters[0], setMethodDeclaration.Body);
C
Cyrus Najmabadi 已提交
175
                return accessor.WithBody(body.WithAdditionalAnnotations(Formatter.Annotation));
176 177 178 179 180 181 182 183 184 185 186 187 188
            }

            return accessor;
        }

        private static TNode ReplaceReferencesToParameterWithValue<TNode>(SemanticModel semanticModel, IParameterSymbol parameter, TNode node)
            where TNode : SyntaxNode
        {
            var rewriter = new Rewriter(semanticModel, parameter);
            return (TNode)rewriter.Visit(node);
        }

        private class Rewriter : CSharpSyntaxRewriter
189
        {
190 191 192
            private readonly SemanticModel _semanticModel;
            private readonly IParameterSymbol _parameter;

193
            public Rewriter(SemanticModel semanticModel, IParameterSymbol parameter)
194
            {
195 196 197
                _semanticModel = semanticModel;
                _parameter = parameter;
            }
198

199 200 201
            public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node)
            {
                if (_parameter.Equals(_semanticModel.GetSymbolInfo(node).Symbol))
202
                {
203
                    return SyntaxFactory.IdentifierName("value").WithTriviaFrom(node);
204
                }
205 206 207 208 209

                return node;
            }
        }

210
        private static Action<SyntaxEditor, InvocationExpressionSyntax, SimpleNameSyntax, SimpleNameSyntax> s_replaceGetReferenceInvocation =
211 212
            (editor, invocation, nameNode, newName) => editor.ReplaceNode(invocation, invocation.Expression.ReplaceNode(nameNode, newName));

213
        private static Action<SyntaxEditor, InvocationExpressionSyntax, SimpleNameSyntax, SimpleNameSyntax> s_replaceSetReferenceInvocation =
214 215 216
            (editor, invocation, nameNode, newName) =>
            {
                if (invocation.ArgumentList?.Arguments.Count != 1)
217
                {
218
                    var annotation = ConflictAnnotation.Create(FeaturesResources.OnlyMethodsWithASingleArgumentCanBeReplacedWithAProperty);
219
                    editor.ReplaceNode(nameNode, newName.WithIdentifier(newName.Identifier.WithAdditionalAnnotations(annotation)));
220
                    return;
221
                }
222 223

                // We use the callback form if "ReplaceNode" here because we want to see the
224
                // invocation expression after any rewrites we already did when rewriting the
225 226 227 228 229 230 231 232 233 234 235 236
                // 'get' references.
                editor.ReplaceNode(invocation, (i, g) =>
                {
                    var currentInvocation = (InvocationExpressionSyntax)i;
                    // looks like   a.b.Foo(arg)   =>     a.b.NewName = arg
                    nameNode = currentInvocation.Expression.GetRightmostName();
                    currentInvocation = (InvocationExpressionSyntax)g.ReplaceNode(currentInvocation, nameNode, newName);

                    // Wrap the argument in parentheses (in order to not introduce any precedence problems).
                    // But also add a simplification annotation so we can remove the parens if possible.
                    var argumentExpression = currentInvocation.ArgumentList.Arguments[0].Expression.Parenthesize();

237
                    var expression = SyntaxFactory.ValueAssignmentExpression(
238 239 240 241 242 243 244 245 246
                        SyntaxKind.SimpleAssignmentExpression, currentInvocation.Expression, argumentExpression);

                    return expression.Parenthesize();
                });
            };


        public void ReplaceGetReference(SyntaxEditor editor, SyntaxToken nameToken, string propertyName, bool nameChanged)
        {
247
            ReplaceInvocation(editor, nameToken, propertyName, nameChanged, s_replaceGetReferenceInvocation);
248 249 250 251
        }

        public void ReplaceSetReference(SyntaxEditor editor, SyntaxToken nameToken, string propertyName, bool nameChanged)
        {
252
            ReplaceInvocation(editor, nameToken, propertyName, nameChanged, s_replaceSetReferenceInvocation);
253 254 255 256 257 258 259 260
        }

        public void ReplaceInvocation(SyntaxEditor editor, SyntaxToken nameToken, string propertyName, bool nameChanged,
            Action<SyntaxEditor, InvocationExpressionSyntax, SimpleNameSyntax, SimpleNameSyntax> replace)
        {
            if (nameToken.Kind() != SyntaxKind.IdentifierToken)
            {
                return;
261
            }
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277

            var nameNode = nameToken.Parent as IdentifierNameSyntax;
            if (nameNode == null)
            {
                return;
            }

            var newName = nameChanged
                ? SyntaxFactory.IdentifierName(SyntaxFactory.Identifier(propertyName).WithTriviaFrom(nameToken))
                : nameNode;

            var invocation = nameNode?.FirstAncestorOrSelf<InvocationExpressionSyntax>();
            var invocationExpression = invocation?.Expression;
            if (!IsInvocationName(nameNode, invocationExpression))
            {
                // Wasn't invoked.  Change the name, but report a conflict.
278
                var annotation = ConflictAnnotation.Create(FeaturesResources.NonInvokedMethodCannotBeReplacedWithProperty);
279 280 281 282 283 284
                editor.ReplaceNode(nameNode, newName.WithIdentifier(newName.Identifier.WithAdditionalAnnotations(annotation)));
                return;
            }

            // It was invoked.  Remove the invocation, and also change the name if necessary.
            replace(editor, invocation, nameNode, newName);
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
        }

        private static bool IsInvocationName(IdentifierNameSyntax nameNode, ExpressionSyntax invocationExpression)
        {
            if (invocationExpression == nameNode)
            {
                return true;
            }

            if (nameNode.IsAnyMemberAccessExpressionName() && nameNode.Parent == invocationExpression)
            {
                return true;
            }

            return false;
        }
    }
}