/
githubmirror
/
roslyn
Обзор
Документация
Войти
/
githubmirror
/
roslyn
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/Features/CSharp/Portable/ReplaceMethodWithProperty/CSharpReplaceMethodWithPropertyService.cs
406 строк
18 KB
Cyrus Najmabadi
Make sealed
26 мар 2025, 22:21
26 мар 2025, 22:21
d99b771
Код
Авторство
О чём код?
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. #nullable disable using System; using System.Composition; using System.Threading; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeGeneration; using Microsoft.CodeAnalysis.CodeStyle; using Microsoft.CodeAnalysis.CSharp.CodeGeneration; using Microsoft.CodeAnalysis.CSharp.Extensions; using Microsoft.CodeAnalysis.CSharp.LanguageService; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Editing; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Host.Mef; using Microsoft.CodeAnalysis.ReplaceMethodWithProperty; namespace Microsoft.CodeAnalysis.CSharp.CodeRefactorings.ReplaceMethodWithProperty; using static CSharpSyntaxTokens; using static SyntaxFactory; [ExportLanguageService(typeof(IReplaceMethodWithPropertyService), LanguageNames.CSharp), Shared] internal sealed class CSharpReplaceMethodWithPropertyService : AbstractReplaceMethodWithPropertyService<MethodDeclarationSyntax>, IReplaceMethodWithPropertyService { [ImportingConstructor] [Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] public CSharpReplaceMethodWithPropertyService() { } public void RemoveSetMethod(SyntaxEditor editor, SyntaxNode setMethodDeclaration) => editor.RemoveNode(setMethodDeclaration); public void ReplaceGetMethodWithProperty( CodeGenerationOptions options, ParseOptions parseOptions, SyntaxEditor editor, SemanticModel semanticModel, GetAndSetMethods getAndSetMethods, string propertyName, bool nameChanged, CancellationToken cancellationToken) { if (getAndSetMethods.GetMethodDeclaration is not MethodDeclarationSyntax getMethodDeclaration) { return; } var languageVersion = parseOptions.LanguageVersion(); var newProperty = ConvertMethodsToProperty( (CSharpCodeGenerationOptions)options, languageVersion, semanticModel, editor.Generator, getAndSetMethods, propertyName, nameChanged, cancellationToken); editor.ReplaceNode(getMethodDeclaration, newProperty); } public static SyntaxNode ConvertMethodsToProperty( CSharpCodeGenerationOptions options, LanguageVersion languageVersion, SemanticModel semanticModel, SyntaxGenerator generator, GetAndSetMethods getAndSetMethods, string propertyName, bool nameChanged, CancellationToken cancellationToken) { var propertyDeclaration = ConvertMethodsToPropertyWorker( options, languageVersion, semanticModel, generator, getAndSetMethods, propertyName, nameChanged, cancellationToken); var expressionBodyPreference = options.PreferExpressionBodiedProperties.Value; if (expressionBodyPreference != ExpressionBodyPreference.Never) { if (propertyDeclaration.AccessorList is { Accessors: [(kind: SyntaxKind.GetAccessorDeclaration) getAccessor] }) { if (getAccessor.ExpressionBody != null) { return propertyDeclaration.WithExpressionBody(getAccessor.ExpressionBody) .WithSemicolonToken(getAccessor.SemicolonToken) .WithAccessorList(null); } else if (getAccessor.Body != null && getAccessor.Body.TryConvertToArrowExpressionBody( propertyDeclaration.Kind(), languageVersion, expressionBodyPreference, cancellationToken, out var arrowExpression, out var semicolonToken)) { return propertyDeclaration.WithExpressionBody(arrowExpression) .WithSemicolonToken(semicolonToken) .WithAccessorList(null); } } } else { if (propertyDeclaration.ExpressionBody != null && propertyDeclaration.ExpressionBody.TryConvertToBlock( propertyDeclaration.SemicolonToken, createReturnStatementForExpression: true, block: out var block)) { var accessor = AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithBody(block); var accessorList = AccessorList([accessor]); return propertyDeclaration.WithAccessorList(accessorList) .WithExpressionBody(null) .WithSemicolonToken(default); } } return propertyDeclaration; } public static PropertyDeclarationSyntax ConvertMethodsToPropertyWorker( CSharpCodeGenerationOptions options, LanguageVersion languageVersion, SemanticModel semanticModel, SyntaxGenerator generator, GetAndSetMethods getAndSetMethods, string propertyName, bool nameChanged, CancellationToken cancellationToken) { var getMethodDeclaration = (MethodDeclarationSyntax)getAndSetMethods.GetMethodDeclaration; var setMethodDeclaration = getAndSetMethods.SetMethodDeclaration as MethodDeclarationSyntax; var getAccessor = CreateGetAccessor(getAndSetMethods, options, languageVersion, cancellationToken); var setAccessor = CreateSetAccessor(semanticModel, generator, getAndSetMethods, options, languageVersion, cancellationToken); var nameToken = GetPropertyName(getMethodDeclaration.Identifier, propertyName, nameChanged); var warning = GetWarning(getAndSetMethods); if (warning != null) { nameToken = nameToken.WithAdditionalAnnotations(WarningAnnotation.Create(warning)); } var property = PropertyDeclaration( getMethodDeclaration.AttributeLists, getMethodDeclaration.Modifiers, getMethodDeclaration.ReturnType, getMethodDeclaration.ExplicitInterfaceSpecifier, nameToken, accessorList: null); // copy 'unsafe' from the set method, if it hasn't been already copied from the get method if (setMethodDeclaration?.Modifiers.Any(SyntaxKind.UnsafeKeyword) == true && !property.Modifiers.Any(SyntaxKind.UnsafeKeyword)) { property = property.AddModifiers(UnsafeKeyword); } property = SetLeadingTrivia( CSharpSyntaxFacts.Instance, getAndSetMethods, property); var accessorList = AccessorList([getAccessor]); if (setAccessor != null) { accessorList = accessorList.AddAccessors(setAccessor); } property = property.WithAccessorList(accessorList); return property.WithAdditionalAnnotations(Formatter.Annotation); } private static SyntaxToken GetPropertyName(SyntaxToken identifier, string propertyName, bool nameChanged) { return nameChanged ? Identifier(propertyName) : identifier; } private static AccessorDeclarationSyntax CreateGetAccessor( GetAndSetMethods getAndSetMethods, CSharpCodeGenerationOptions options, LanguageVersion languageVersion, CancellationToken cancellationToken) { var accessorDeclaration = CreateGetAccessorWorker(getAndSetMethods); return UseExpressionOrBlockBodyIfDesired( options, languageVersion, accessorDeclaration, cancellationToken); } private static AccessorDeclarationSyntax UseExpressionOrBlockBodyIfDesired( CSharpCodeGenerationOptions options, LanguageVersion languageVersion, AccessorDeclarationSyntax accessorDeclaration, CancellationToken cancellationToken) { var expressionBodyPreference = options.PreferExpressionBodiedAccessors.Value; if (accessorDeclaration?.Body != null && expressionBodyPreference != ExpressionBodyPreference.Never) { if (accessorDeclaration.Body.TryConvertToArrowExpressionBody( accessorDeclaration.Kind(), languageVersion, expressionBodyPreference, cancellationToken, out var arrowExpression, out var semicolonToken)) { return accessorDeclaration.WithBody(null) .WithExpressionBody(arrowExpression) .WithSemicolonToken(semicolonToken) .WithTrailingTrivia(accessorDeclaration.Body.GetTrailingTrivia()) .WithAdditionalAnnotations(Formatter.Annotation); } } else if (accessorDeclaration?.ExpressionBody != null && expressionBodyPreference == ExpressionBodyPreference.Never) { if (accessorDeclaration.ExpressionBody.TryConvertToBlock( accessorDeclaration.SemicolonToken, createReturnStatementForExpression: accessorDeclaration.Kind() == SyntaxKind.GetAccessorDeclaration, block: out var block)) { return accessorDeclaration.WithExpressionBody(null) .WithSemicolonToken(default) .WithBody(block) .WithAdditionalAnnotations(Formatter.Annotation); } } return accessorDeclaration; } private static AccessorDeclarationSyntax CreateGetAccessorWorker(GetAndSetMethods getAndSetMethods) { var getMethodDeclaration = getAndSetMethods.GetMethodDeclaration as MethodDeclarationSyntax; var accessor = AccessorDeclaration(SyntaxKind.GetAccessorDeclaration); if (getMethodDeclaration.ExpressionBody != null) { return accessor.WithExpressionBody(getMethodDeclaration.ExpressionBody) .WithSemicolonToken(getMethodDeclaration.SemicolonToken); } if (getMethodDeclaration.SemicolonToken.Kind() != SyntaxKind.None) { return accessor.WithSemicolonToken(getMethodDeclaration.SemicolonToken); } if (getMethodDeclaration.Body != null) { return accessor.WithBody(getMethodDeclaration.Body.WithAdditionalAnnotations(Formatter.Annotation)); } return accessor; } private static AccessorDeclarationSyntax CreateSetAccessor( SemanticModel semanticModel, SyntaxGenerator generator, GetAndSetMethods getAndSetMethods, CSharpCodeGenerationOptions options, LanguageVersion languageVersion, CancellationToken cancellationToken) { var accessorDeclaration = CreateSetAccessorWorker(semanticModel, generator, getAndSetMethods); return UseExpressionOrBlockBodyIfDesired(options, languageVersion, accessorDeclaration, cancellationToken); } private static AccessorDeclarationSyntax CreateSetAccessorWorker( SemanticModel semanticModel, SyntaxGenerator generator, GetAndSetMethods getAndSetMethods) { var setMethod = getAndSetMethods.SetMethod; if (getAndSetMethods.SetMethodDeclaration is not MethodDeclarationSyntax setMethodDeclaration || setMethod?.Parameters.Length != 1) { return null; } var getMethod = getAndSetMethods.GetMethod; var accessor = AccessorDeclaration(SyntaxKind.SetAccessorDeclaration); if (getMethod.DeclaredAccessibility != setMethod.DeclaredAccessibility) { accessor = (AccessorDeclarationSyntax)generator.WithAccessibility(accessor, setMethod.DeclaredAccessibility); } if (setMethodDeclaration.ExpressionBody != null) { var oldExpressionBody = setMethodDeclaration.ExpressionBody; var expression = ReplaceReferencesToParameterWithValue( semanticModel, setMethod.Parameters[0], oldExpressionBody.Expression); return accessor.WithExpressionBody(oldExpressionBody.WithExpression(expression)) .WithSemicolonToken(setMethodDeclaration.SemicolonToken); } if (setMethodDeclaration.SemicolonToken.Kind() != SyntaxKind.None) { return accessor.WithSemicolonToken(setMethodDeclaration.SemicolonToken); } if (setMethodDeclaration.Body != null) { var body = ReplaceReferencesToParameterWithValue(semanticModel, setMethod.Parameters[0], setMethodDeclaration.Body); return accessor.WithBody(body.WithAdditionalAnnotations(Formatter.Annotation)); } 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 sealed class Rewriter(SemanticModel semanticModel, IParameterSymbol parameter) : CSharpSyntaxRewriter { private readonly SemanticModel _semanticModel = semanticModel; private readonly IParameterSymbol _parameter = parameter; public override SyntaxNode VisitIdentifierName(IdentifierNameSyntax node) { if (_parameter.Equals(_semanticModel.GetSymbolInfo(node).Symbol)) { return IdentifierName("value").WithTriviaFrom(node); } return node; } } // We use the callback form if "ReplaceNode" here because we want to see the // invocation expression after any rewrites we already did when rewriting previous // 'get' references. private static readonly Action<SyntaxEditor, InvocationExpressionSyntax, SimpleNameSyntax, SimpleNameSyntax> s_replaceGetReferenceInvocation = (editor, invocation, nameNode, newName) => editor.ReplaceNode(invocation, (i, g) => { var currentInvocation = (InvocationExpressionSyntax)i; var currentName = currentInvocation.Expression.GetRightmostName(); return currentInvocation.Expression.ReplaceNode(currentName, newName); }); private static readonly Action<SyntaxEditor, InvocationExpressionSyntax, SimpleNameSyntax, SimpleNameSyntax> s_replaceSetReferenceInvocation = (editor, invocation, nameNode, newName) => { if (invocation.ArgumentList?.Arguments.Count != 1 || invocation.ArgumentList.Arguments[0].Expression.Kind() == SyntaxKind.DeclarationExpression) { var annotation = ConflictAnnotation.Create(FeaturesResources.Only_methods_with_a_single_argument_which_is_not_an_out_variable_declaration_can_be_replaced_with_a_property); editor.ReplaceNode(nameNode, newName.WithIdentifier(newName.Identifier.WithAdditionalAnnotations(annotation))); return; } // We use the callback form if "ReplaceNode" here because we want to see the // invocation expression after any rewrites we already did when rewriting the // 'get' references. editor.ReplaceNode(invocation, (i, g) => { var currentInvocation = (InvocationExpressionSyntax)i; // looks like a.b.Goo(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(); var expression = AssignmentExpression( SyntaxKind.SimpleAssignmentExpression, currentInvocation.Expression, argumentExpression); return expression.Parenthesize(); }); }; public void ReplaceGetReference(SyntaxEditor editor, SyntaxToken nameToken, string propertyName, bool nameChanged) => ReplaceInvocation(editor, nameToken, propertyName, nameChanged, s_replaceGetReferenceInvocation); public void ReplaceSetReference(SyntaxEditor editor, SyntaxToken nameToken, string propertyName, bool nameChanged) => ReplaceInvocation(editor, nameToken, propertyName, nameChanged, s_replaceSetReferenceInvocation); public static void ReplaceInvocation(SyntaxEditor editor, SyntaxToken nameToken, string propertyName, bool nameChanged, Action<SyntaxEditor, InvocationExpressionSyntax, SimpleNameSyntax, SimpleNameSyntax> replace) { if (nameToken.Kind() != SyntaxKind.IdentifierToken) { return; } if (nameToken.Parent is not IdentifierNameSyntax nameNode) { return; } var invocation = nameNode?.FirstAncestorOrSelf<InvocationExpressionSyntax>(); var newName = nameNode; if (nameChanged) { newName = IdentifierName(Identifier(propertyName)); } newName = newName.WithTriviaFrom(invocation is null ? nameToken.Parent : invocation); var invocationExpression = invocation?.Expression; if (!IsInvocationName(nameNode, invocationExpression)) { // Wasn't invoked. Change the name, but report a conflict. var annotation = ConflictAnnotation.Create(FeaturesResources.Non_invoked_method_cannot_be_replaced_with_property); 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); } private static bool IsInvocationName(IdentifierNameSyntax nameNode, ExpressionSyntax invocationExpression) { if (invocationExpression == nameNode) { return true; } if (nameNode.IsAnyMemberAccessExpressionName() && nameNode.Parent == invocationExpression) { return true; } return false; } }