|
// 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.
using System;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.CodeGeneration;
using Microsoft.CodeAnalysis.CSharp.Extensions;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.ReplacePropertyWithMethods;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Roslyn.Utilities;
namespace Microsoft.CodeAnalysis.CSharp.ReplacePropertyWithMethods;
using static CSharpSyntaxTokens;
using static SyntaxFactory;
[ExportLanguageService(typeof(IReplacePropertyWithMethodsService), LanguageNames.CSharp), Shared]
internal sealed partial class CSharpReplacePropertyWithMethodsService :
AbstractReplacePropertyWithMethodsService<IdentifierNameSyntax, ExpressionSyntax, NameMemberCrefSyntax, StatementSyntax, PropertyDeclarationSyntax>
{
[ImportingConstructor]
[Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
public CSharpReplacePropertyWithMethodsService()
{
}
public override async Task<ImmutableArray<SyntaxNode>> GetReplacementMembersAsync(
Document document,
IPropertySymbol property,
SyntaxNode propertyDeclarationNode,
IFieldSymbol propertyBackingField,
string desiredGetMethodName,
string desiredSetMethodName,
CancellationToken cancellationToken)
{
if (propertyDeclarationNode is not PropertyDeclarationSyntax propertyDeclaration)
return [];
var options = (CSharpCodeGenerationOptions)await document.GetCodeGenerationOptionsAsync(cancellationToken).ConfigureAwait(false);
var syntaxTree = await document.GetRequiredSyntaxTreeAsync(cancellationToken).ConfigureAwait(false);
var languageVersion = syntaxTree.Options.LanguageVersion();
return ConvertPropertyToMembers(
languageVersion,
SyntaxGenerator.GetGenerator(document), property,
propertyDeclaration, propertyBackingField,
options.PreferExpressionBodiedMethods.Value, desiredGetMethodName, desiredSetMethodName,
cancellationToken);
}
private static ImmutableArray<SyntaxNode> ConvertPropertyToMembers(
LanguageVersion languageVersion,
SyntaxGenerator generator,
IPropertySymbol property,
PropertyDeclarationSyntax propertyDeclaration,
IFieldSymbol? propertyBackingField,
ExpressionBodyPreference expressionBodyPreference,
string desiredGetMethodName,
string desiredSetMethodName,
CancellationToken cancellationToken)
{
using var result = TemporaryArray<SyntaxNode>.Empty;
if (propertyBackingField != null)
{
var initializer = propertyDeclaration.Initializer?.Value;
result.Add(generator.FieldDeclaration(propertyBackingField, initializer));
}
var getMethod = property.GetMethod;
if (getMethod != null)
{
result.Add(GetGetMethod(
languageVersion,
generator, propertyDeclaration, propertyBackingField,
getMethod, desiredGetMethodName, expressionBodyPreference,
cancellationToken));
}
var setMethod = property.SetMethod;
if (setMethod != null)
{
result.Add(GetSetMethod(
languageVersion,
generator, propertyDeclaration, propertyBackingField,
setMethod, desiredSetMethodName, expressionBodyPreference,
cancellationToken));
}
return result.ToImmutableAndClear();
}
private static SyntaxNode GetSetMethod(
LanguageVersion languageVersion,
SyntaxGenerator generator,
PropertyDeclarationSyntax propertyDeclaration,
IFieldSymbol? propertyBackingField,
IMethodSymbol setMethod,
string desiredSetMethodName,
ExpressionBodyPreference expressionBodyPreference,
CancellationToken cancellationToken)
{
var methodDeclaration = GetSetMethodWorker();
// The analyzer doesn't report diagnostics when the trivia contains preprocessor directives, so it's safe
// to copy the complete leading trivia to both generated methods.
methodDeclaration = CopyLeadingTrivia(propertyDeclaration, methodDeclaration, ConvertValueToParamRewriter.Instance);
return UseExpressionOrBlockBodyIfDesired(
languageVersion, methodDeclaration, expressionBodyPreference,
createReturnStatementForExpression: false, cancellationToken);
MethodDeclarationSyntax GetSetMethodWorker()
{
var setAccessorDeclaration = (AccessorDeclarationSyntax)setMethod.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken);
var methodDeclaration = (MethodDeclarationSyntax)generator.MethodDeclaration(setMethod, desiredSetMethodName);
// property has unsafe, but generator didn't add it to the method, so we have to add it here
if (propertyDeclaration.Modifiers.Any(SyntaxKind.UnsafeKeyword)
&& !methodDeclaration.Modifiers.Any(SyntaxKind.UnsafeKeyword))
{
methodDeclaration = methodDeclaration.AddModifiers(UnsafeKeyword);
}
methodDeclaration = methodDeclaration.WithAttributeLists(setAccessorDeclaration.AttributeLists);
if (setAccessorDeclaration.Body != null)
{
return methodDeclaration.WithBody(setAccessorDeclaration.Body)
.WithAdditionalAnnotations(Formatter.Annotation);
}
else if (setAccessorDeclaration.ExpressionBody != null)
{
return methodDeclaration.WithBody(null)
.WithExpressionBody(setAccessorDeclaration.ExpressionBody)
.WithSemicolonToken(setAccessorDeclaration.SemicolonToken);
}
else if (propertyBackingField != null)
{
return methodDeclaration.WithBody(Block(
(StatementSyntax)generator.ExpressionStatement(
generator.AssignmentStatement(
GetFieldReference(generator, propertyBackingField),
generator.IdentifierName("value")))));
}
return methodDeclaration;
}
}
private static SyntaxNode GetGetMethod(
LanguageVersion languageVersion,
SyntaxGenerator generator,
PropertyDeclarationSyntax propertyDeclaration,
IFieldSymbol? propertyBackingField,
IMethodSymbol getMethod,
string desiredGetMethodName,
ExpressionBodyPreference expressionBodyPreference,
CancellationToken cancellationToken)
{
var methodDeclaration = GetGetMethodWorker();
methodDeclaration = CopyLeadingTrivia(propertyDeclaration, methodDeclaration, ConvertValueToReturnsRewriter.Instance);
return UseExpressionOrBlockBodyIfDesired(
languageVersion, methodDeclaration, expressionBodyPreference,
createReturnStatementForExpression: true, cancellationToken);
MethodDeclarationSyntax GetGetMethodWorker()
{
var methodDeclaration = (MethodDeclarationSyntax)generator.MethodDeclaration(getMethod, desiredGetMethodName);
// property has unsafe, but generator didn't add it to the method, so we have to add it here
if (propertyDeclaration.Modifiers.Any(SyntaxKind.UnsafeKeyword)
&& !methodDeclaration.Modifiers.Any(SyntaxKind.UnsafeKeyword))
{
methodDeclaration = methodDeclaration.AddModifiers(UnsafeKeyword);
}
if (propertyDeclaration.ExpressionBody != null)
{
return methodDeclaration.WithBody(null)
.WithExpressionBody(propertyDeclaration.ExpressionBody)
.WithSemicolonToken(propertyDeclaration.SemicolonToken);
}
else
{
var getAccessorDeclaration = (AccessorDeclarationSyntax)getMethod.DeclaringSyntaxReferences[0].GetSyntax(cancellationToken);
methodDeclaration = methodDeclaration.WithAttributeLists(getAccessorDeclaration.AttributeLists);
if (getAccessorDeclaration?.ExpressionBody != null)
{
return methodDeclaration.WithBody(null)
.WithExpressionBody(getAccessorDeclaration.ExpressionBody)
.WithSemicolonToken(getAccessorDeclaration.SemicolonToken);
}
else if (getAccessorDeclaration?.Body != null)
{
return methodDeclaration.WithBody(getAccessorDeclaration.Body)
.WithAdditionalAnnotations(Formatter.Annotation);
}
else if (propertyBackingField != null)
{
var fieldReference = GetFieldReference(generator, propertyBackingField);
return methodDeclaration.WithBody(
Block(
(StatementSyntax)generator.ReturnStatement(fieldReference)));
}
}
return methodDeclaration;
}
}
private static MethodDeclarationSyntax CopyLeadingTrivia(
PropertyDeclarationSyntax propertyDeclaration,
MethodDeclarationSyntax methodDeclaration,
CSharpSyntaxRewriter documentationCommentRewriter)
{
var leadingTrivia = propertyDeclaration.GetLeadingTrivia();
return methodDeclaration.WithLeadingTrivia(leadingTrivia.Select(trivia => ConvertTrivia(trivia, documentationCommentRewriter)));
}
private static SyntaxTrivia ConvertTrivia(SyntaxTrivia trivia, CSharpSyntaxRewriter rewriter)
{
if (trivia.Kind() is SyntaxKind.MultiLineDocumentationCommentTrivia or
SyntaxKind.SingleLineDocumentationCommentTrivia)
{
return ConvertDocumentationComment(trivia, rewriter);
}
return trivia;
}
private static SyntaxTrivia ConvertDocumentationComment(SyntaxTrivia trivia, CSharpSyntaxRewriter rewriter)
{
var structure = trivia.GetStructure();
var rewritten = rewriter.Visit(structure);
Contract.ThrowIfNull(rewritten);
return Trivia((StructuredTriviaSyntax)rewritten);
}
private static SyntaxNode UseExpressionOrBlockBodyIfDesired(
LanguageVersion languageVersion,
MethodDeclarationSyntax methodDeclaration,
ExpressionBodyPreference expressionBodyPreference,
bool createReturnStatementForExpression,
CancellationToken cancellationToken)
{
if (methodDeclaration.Body != null && expressionBodyPreference != ExpressionBodyPreference.Never)
{
if (methodDeclaration.Body.TryConvertToArrowExpressionBody(
methodDeclaration.Kind(), languageVersion, expressionBodyPreference, cancellationToken,
out var arrowExpression, out var semicolonToken))
{
return methodDeclaration.WithBody(null)
.WithExpressionBody(arrowExpression)
.WithSemicolonToken(semicolonToken)
.WithAdditionalAnnotations(Formatter.Annotation);
}
}
else if (methodDeclaration.ExpressionBody != null && expressionBodyPreference == ExpressionBodyPreference.Never)
{
if (methodDeclaration.ExpressionBody.TryConvertToBlock(
methodDeclaration.SemicolonToken, createReturnStatementForExpression, out var block))
{
return methodDeclaration.WithExpressionBody(null)
.WithSemicolonToken(default)
.WithBody(block)
.WithAdditionalAnnotations(Formatter.Annotation);
}
}
return methodDeclaration;
}
/// <summary>
/// Used by the documentation comment rewriters to identify top-level <c><value></c> nodes.
/// </summary>
private static bool IsValueName(XmlNameSyntax name)
=> name.Prefix == null &&
name.LocalName.ValueText == "value";
public override SyntaxNode GetPropertyNodeToReplace(SyntaxNode propertyDeclaration)
{
// For C# we'll have the property declaration that we want to replace.
return propertyDeclaration;
}
protected override NameMemberCrefSyntax? TryGetCrefSyntax(IdentifierNameSyntax identifierName)
=> identifierName.Parent as NameMemberCrefSyntax;
protected override NameMemberCrefSyntax CreateCrefSyntax(NameMemberCrefSyntax originalCref, SyntaxToken identifierToken, SyntaxNode? parameterType)
{
CrefParameterListSyntax parameterList;
if (parameterType is TypeSyntax typeSyntax)
{
var parameter = CrefParameter(typeSyntax);
parameterList = CrefParameterList([parameter]);
}
else
{
parameterList = CrefParameterList();
}
// XmlCrefAttribute replaces <T> with {T}, which is required for C# documentation comments
var crefAttribute = XmlCrefAttribute(
NameMemberCref(IdentifierName(identifierToken), parameterList));
return (NameMemberCrefSyntax)crefAttribute.Cref;
}
protected override ExpressionSyntax UnwrapCompoundAssignment(
SyntaxNode compoundAssignment, ExpressionSyntax readExpression)
{
var parent = (AssignmentExpressionSyntax)compoundAssignment;
var operatorKind = parent.Kind() switch
{
SyntaxKind.AddAssignmentExpression => SyntaxKind.AddExpression,
SyntaxKind.AndAssignmentExpression => SyntaxKind.BitwiseAndExpression,
SyntaxKind.CoalesceAssignmentExpression => SyntaxKind.CoalesceExpression,
SyntaxKind.DivideAssignmentExpression => SyntaxKind.DivideExpression,
SyntaxKind.ExclusiveOrAssignmentExpression => SyntaxKind.ExclusiveOrExpression,
SyntaxKind.LeftShiftAssignmentExpression => SyntaxKind.LeftShiftExpression,
SyntaxKind.ModuloAssignmentExpression => SyntaxKind.ModuloExpression,
SyntaxKind.MultiplyAssignmentExpression => SyntaxKind.MultiplyExpression,
SyntaxKind.OrAssignmentExpression => SyntaxKind.BitwiseOrExpression,
SyntaxKind.RightShiftAssignmentExpression => SyntaxKind.RightShiftExpression,
SyntaxKind.SubtractAssignmentExpression => SyntaxKind.SubtractExpression,
SyntaxKind.UnsignedRightShiftAssignmentExpression => SyntaxKind.UnsignedRightShiftExpression,
_ => SyntaxKind.None,
};
if (operatorKind is SyntaxKind.None)
return parent;
return BinaryExpression(operatorKind, readExpression, parent.Right.Parenthesize());
}
}
|