|
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.Editing;
using Microsoft.CodeAnalysis.Shared.Extensions;
using Microsoft.CodeAnalysis.Simplification;
namespace Microsoft.CodeAnalysis.AddDebuggerDisplay;
internal abstract class AbstractAddDebuggerDisplayCodeRefactoringProvider<
TTypeDeclarationSyntax,
TMethodDeclarationSyntax> : CodeRefactoringProvider
where TTypeDeclarationSyntax : SyntaxNode
where TMethodDeclarationSyntax : SyntaxNode
{
private const string DebuggerDisplayPrefix = "{";
private const string DebuggerDisplayMethodName = "GetDebuggerDisplay";
private const string DebuggerDisplaySuffix = "(),nq}";
protected abstract bool CanNameofAccessNonPublicMembersFromAttributeArgument { get; }
protected abstract bool SupportsConstantInterpolatedStrings(Document document);
public override async Task ComputeRefactoringsAsync(CodeRefactoringContext context)
{
var (document, _, cancellationToken) = context;
var typeAndPriority =
await GetRelevantTypeFromHeaderAsync(context).ConfigureAwait(false) ??
await GetRelevantTypeFromMethodAsync(context).ConfigureAwait(false);
if (typeAndPriority == null)
return;
var (type, priority) = typeAndPriority.Value;
var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var compilation = semanticModel.Compilation;
var debuggerAttributeTypeSymbol = compilation.GetTypeByMetadataName("System.Diagnostics.DebuggerDisplayAttribute");
if (debuggerAttributeTypeSymbol is null)
return;
var typeSymbol = (INamedTypeSymbol)semanticModel.GetRequiredDeclaredSymbol(type, cancellationToken);
if (typeSymbol.IsStatic || !IsClassOrStruct(typeSymbol))
return;
if (HasDebuggerDisplayAttribute(typeSymbol, compilation))
return;
context.RegisterRefactoring(CodeAction.Create(
FeaturesResources.Add_DebuggerDisplay_attribute,
c => ApplyAsync(document, type, debuggerAttributeTypeSymbol, c),
nameof(FeaturesResources.Add_DebuggerDisplay_attribute),
priority));
}
private static async Task<(TTypeDeclarationSyntax type, CodeActionPriority priority)?> GetRelevantTypeFromHeaderAsync(CodeRefactoringContext context)
{
var type = await context.TryGetRelevantNodeAsync<TTypeDeclarationSyntax>().ConfigureAwait(false);
if (type is null)
return null;
return (type, CodeActionPriority.Low);
}
private static async Task<(TTypeDeclarationSyntax type, CodeActionPriority priority)?> GetRelevantTypeFromMethodAsync(CodeRefactoringContext context)
{
var (document, _, cancellationToken) = context;
var method = await context.TryGetRelevantNodeAsync<TMethodDeclarationSyntax>().ConfigureAwait(false);
if (method == null)
return null;
var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var methodSymbol = (IMethodSymbol)semanticModel.GetRequiredDeclaredSymbol(method, cancellationToken);
var isDebuggerDisplayMethod = IsDebuggerDisplayMethod(methodSymbol);
if (!isDebuggerDisplayMethod && !IsToStringMethod(methodSymbol))
return null;
// Show the feature if we're on a ToString or GetDebuggerDisplay method. For the former,
// have this be low-pri so we don't override more important light-bulb options.
var typeDecl = method.FirstAncestorOrSelf<TTypeDeclarationSyntax>();
if (typeDecl == null)
return null;
var priority = isDebuggerDisplayMethod ? CodeActionPriority.Default : CodeActionPriority.Low;
return (typeDecl, priority);
}
private static bool IsToStringMethod(IMethodSymbol methodSymbol)
=> methodSymbol is { Name: nameof(ToString), Arity: 0, Parameters.IsEmpty: true };
private static bool IsDebuggerDisplayMethod(IMethodSymbol methodSymbol)
=> methodSymbol is { Name: DebuggerDisplayMethodName, Arity: 0, Parameters.IsEmpty: true };
private static bool IsClassOrStruct(ITypeSymbol typeSymbol)
=> typeSymbol.TypeKind is TypeKind.Class or TypeKind.Struct;
private static bool HasDebuggerDisplayAttribute(ITypeSymbol typeSymbol, Compilation compilation)
=> typeSymbol.GetAttributes()
.Select(data => data.AttributeClass)
.Contains(compilation.GetTypeByMetadataName("System.Diagnostics.DebuggerDisplayAttribute"));
private async Task<Document> ApplyAsync(Document document, TTypeDeclarationSyntax type, INamedTypeSymbol debuggerAttributeTypeSymbol, CancellationToken cancellationToken)
{
var syntaxRoot = await document.GetRequiredSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
var semanticModel = await document.GetRequiredSemanticModelAsync(cancellationToken).ConfigureAwait(false);
var editor = new SyntaxEditor(syntaxRoot, document.Project.Solution.Services);
var generator = editor.Generator;
SyntaxNode attributeArgument;
if (CanNameofAccessNonPublicMembersFromAttributeArgument)
{
if (SupportsConstantInterpolatedStrings(document))
{
attributeArgument = generator.InterpolatedStringExpression(
generator.CreateInterpolatedStringStartToken(isVerbatim: false),
[
generator.InterpolatedStringText(generator.InterpolatedStringTextToken("{{", "{{")),
generator.Interpolation(generator.NameOfExpression(generator.IdentifierName(DebuggerDisplayMethodName))),
generator.InterpolatedStringText(generator.InterpolatedStringTextToken("(),nq}}", "(),nq}}")),
],
generator.CreateInterpolatedStringEndToken());
}
else
{
attributeArgument = generator.AddExpression(
generator.AddExpression(
generator.LiteralExpression(DebuggerDisplayPrefix),
generator.NameOfExpression(generator.IdentifierName(DebuggerDisplayMethodName))),
generator.LiteralExpression(DebuggerDisplaySuffix));
}
}
else
{
attributeArgument = generator.LiteralExpression(
DebuggerDisplayPrefix + DebuggerDisplayMethodName + DebuggerDisplaySuffix);
}
var newAttribute = generator
.Attribute(generator.TypeExpression(debuggerAttributeTypeSymbol), [attributeArgument])
.WithAdditionalAnnotations(
Simplifier.Annotation,
Simplifier.AddImportsAnnotation);
editor.AddAttribute(type, newAttribute);
var typeSymbol = (INamedTypeSymbol)semanticModel.GetRequiredDeclaredSymbol(type, cancellationToken);
if (!typeSymbol.GetMembers().OfType<IMethodSymbol>().Any(IsDebuggerDisplayMethod))
{
editor.AddMember(type,
generator.MethodDeclaration(
DebuggerDisplayMethodName,
returnType: generator.TypeExpression(SpecialType.System_String),
accessibility: Accessibility.Private,
statements:
[
generator.ReturnStatement(generator.InvocationExpression(
generator.MemberAccessExpression(
generator.ThisExpression(),
generator.IdentifierName("ToString"))))
]));
}
return document.WithSyntaxRoot(editor.GetChangedRoot());
}
}
|