XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEExtension.NetCore.AutoConfig

【DLL】自动实现配置文件的存储

公开
关注 0 Fork 0 Star 0
UTF-8
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.FindSymbols;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Composition;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using XFEExtension.NetCore.AutoConfig.Diagnostics;

namespace XFEExtension.NetCore.AutoConfig.CodeFix;

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AutoConfigCodeFixProvider)), Shared]
public sealed class AutoConfigCodeFixProvider : CodeFixProvider
{
    private const string ProfilePropertyAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAttribute";
    private const string AddGetAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAddGetAttribute";
    private const string AddSetAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAddSetAttribute";

    public override ImmutableArray<string> FixableDiagnosticIds => [AutoConfigDiagnostics.AddGetNoResultErrorId, AutoConfigDiagnostics.AddSetNoSetResultWarningId, AutoConfigDiagnostics.FieldCanUsePartialPropertyId];

    public override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;

    public override Task RegisterCodeFixesAsync(CodeFixContext context)
    {
        foreach (var diagnostic in context.Diagnostics)
        {
            if (diagnostic.Id == AutoConfigDiagnostics.AddGetNoResultErrorId)
            {
                context.RegisterCodeFix(
                    CodeAction.Create("添加返回值方法", cancellationToken => AddAttributeAsync(context.Document, diagnostic.Location.SourceSpan, "ProfilePropertyAddGet", "return Current.{0}", cancellationToken), "添加返回值"),
                    diagnostic);
            }
            else if (diagnostic.Id == AutoConfigDiagnostics.AddSetNoSetResultWarningId)
            {
                context.RegisterCodeFix(
                    CodeAction.Create("添加字段的设置方法", cancellationToken => AddAttributeAsync(context.Document, diagnostic.Location.SourceSpan, "ProfilePropertyAddSet", "Current.{0} = value", cancellationToken), "添加字段的设置方法"),
                    diagnostic);
            }
            else if (diagnostic.Id == AutoConfigDiagnostics.FieldCanUsePartialPropertyId
                && diagnostic.Properties.TryGetValue("PropertyName", out var propertyName)
                && !string.IsNullOrWhiteSpace(propertyName))
            {
                context.RegisterCodeFix(
                    CodeAction.Create(
                        "转换为 .NET 10 部分配置属性",
                        cancellationToken => ConvertToPartialPropertyAsync(context.Document, diagnostic.Location.SourceSpan, propertyName!, cancellationToken),
                        "转换为部分配置属性"),
                    diagnostic);
            }
        }
        return Task.CompletedTask;
    }

    private static async Task<Document> AddAttributeAsync(Document document, TextSpan sourceSpan, string attributeName, string codeFormat, CancellationToken cancellationToken)
    {
        var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        if (root is null)
            return document;
        var diagnosticNode = root.FindToken(sourceSpan.Start).Parent;
        var fieldDeclaration = diagnosticNode?.AncestorsAndSelf().OfType<FieldDeclarationSyntax>().FirstOrDefault();
        var propertyDeclaration = diagnosticNode?.AncestorsAndSelf().OfType<PropertyDeclarationSyntax>().FirstOrDefault();
        if (fieldDeclaration is null && propertyDeclaration is null)
            return document;
        var code = propertyDeclaration is null
            ? string.Format(System.Globalization.CultureInfo.InvariantCulture, codeFormat, fieldDeclaration!.Declaration.Variables.First().Identifier.ValueText)
            : attributeName == AddGetAttributeName || attributeName == "ProfilePropertyAddGet"
                ? "return field"
                : "field = value";
        var newAttribute = SyntaxFactory.Attribute(SyntaxFactory.ParseName(attributeName))
            .AddArgumentListArguments(SyntaxFactory.AttributeArgument(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(code))));
        var attributeList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(newAttribute));
        var newRoot = fieldDeclaration is not null
            ? root.ReplaceNode(fieldDeclaration, fieldDeclaration.AddAttributeLists(attributeList))
            : root.ReplaceNode(propertyDeclaration!, propertyDeclaration!.AddAttributeLists(attributeList));
        return document.WithSyntaxRoot(newRoot);
    }

    private static async Task<Solution> ConvertToPartialPropertyAsync(Document document, TextSpan sourceSpan, string propertyName, CancellationToken cancellationToken)
    {
        var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false);
        if (root is null || semanticModel is null)
            return document.Project.Solution;
        var variable = root.FindToken(sourceSpan.Start).Parent?.AncestorsAndSelf().OfType<VariableDeclaratorSyntax>().FirstOrDefault();
        var fieldDeclaration = variable?.Parent?.Parent as FieldDeclarationSyntax;
        if (variable is null
            || fieldDeclaration is null
            || fieldDeclaration.Declaration.Variables.Count != 1
            || semanticModel.GetDeclaredSymbol(variable, cancellationToken) is not IFieldSymbol field)
            return document.Project.Solution;

        var originalFieldName = field.Name;
        var profileAttributeNames = new HashSet<string>(StringComparer.Ordinal);
        var getHookAttributeNames = new HashSet<string>(StringComparer.Ordinal);
        var setHookAttributeNames = new HashSet<string>(StringComparer.Ordinal);
        foreach (var attribute in fieldDeclaration.AttributeLists.SelectMany(static list => list.Attributes))
        {
            var attributeTypeName = (semanticModel.GetSymbolInfo(attribute, cancellationToken).Symbol as IMethodSymbol)?.ContainingType.ToDisplayString();
            if (attributeTypeName != ProfilePropertyAttributeName && attributeTypeName != AddGetAttributeName && attributeTypeName != AddSetAttributeName)
                continue;
            var sourceName = attribute.Name.ToString();
            profileAttributeNames.Add(sourceName);
            if (attributeTypeName == AddGetAttributeName)
                getHookAttributeNames.Add(sourceName);
            else if (attributeTypeName == AddSetAttributeName)
                setHookAttributeNames.Add(sourceName);
        }
        var solution = document.Project.Solution;
        var declarationAnnotation = new SyntaxAnnotation();
        solution = solution.WithDocumentSyntaxRoot(
            document.Id,
            root.ReplaceNode(fieldDeclaration, fieldDeclaration.WithAdditionalAnnotations(declarationAnnotation)));
        var referencedSymbols = await SymbolFinder.FindReferencesAsync(field, solution, cancellationToken).ConfigureAwait(false);
        var referenceLocations = referencedSymbols
            .SelectMany(static referencedSymbol => referencedSymbol.Locations)
            .Where(static location => location.Location.IsInSource)
            .GroupBy(static location => location.Document.Id);

        foreach (var documentLocations in referenceLocations)
        {
            var referenceDocument = solution.GetDocument(documentLocations.Key);
            var referenceRoot = referenceDocument is null ? null : await referenceDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
            if (referenceDocument is null || referenceRoot is null)
                continue;
            var tokens = documentLocations
                .Select(location => referenceRoot.FindToken(location.Location.SourceSpan.Start))
                .Where(token => token.IsKind(SyntaxKind.IdentifierToken) && token.ValueText == originalFieldName)
                .Distinct()
                .ToArray();
            if (tokens.Length == 0)
                continue;
            var renamedRoot = referenceRoot.ReplaceTokens(tokens, (_, rewritten) => SyntaxFactory.Identifier(rewritten.LeadingTrivia, propertyName, rewritten.TrailingTrivia));
            solution = solution.WithDocumentSyntaxRoot(referenceDocument.Id, renamedRoot);
        }

        var declarationDocument = solution.GetDocument(document.Id);
        var declarationRoot = declarationDocument is null ? null : await declarationDocument.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false);
        if (declarationDocument is null || declarationRoot is null)
            return solution;
        var currentFieldDeclaration = declarationRoot.GetAnnotatedNodes(declarationAnnotation)
            .OfType<FieldDeclarationSyntax>()
            .Where(static candidate => candidate.Declaration.Variables.Count == 1)
            .FirstOrDefault(candidate => candidate.Declaration.Variables[0].Identifier.ValueText == originalFieldName);
        if (currentFieldDeclaration is null)
            return solution;
        var currentVariable = currentFieldDeclaration.Declaration.Variables[0];
        var attributeLists = PreparePartialPropertyAttributes(
            currentFieldDeclaration.AttributeLists,
            originalFieldName,
            "field",
            profileAttributeNames,
            getHookAttributeNames,
            setHookAttributeNames);
        var partialProperty = SyntaxFactory.PropertyDeclaration(currentFieldDeclaration.Declaration.Type.WithoutTrivia(), SyntaxFactory.Identifier(propertyName))
            .WithAttributeLists(attributeLists)
            .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword), SyntaxFactory.Token(SyntaxKind.StaticKeyword), SyntaxFactory.Token(SyntaxKind.PartialKeyword)))
            .WithAccessorList(SyntaxFactory.AccessorList(SyntaxFactory.List([
                SyntaxFactory.AccessorDeclaration(SyntaxKind.GetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken)),
                SyntaxFactory.AccessorDeclaration(SyntaxKind.SetAccessorDeclaration).WithSemicolonToken(SyntaxFactory.Token(SyntaxKind.SemicolonToken))
            ])))
            .WithInitializer(currentVariable.Initializer)
            .WithSemicolonToken(currentVariable.Initializer is null ? default : SyntaxFactory.Token(SyntaxKind.SemicolonToken))
            .WithLeadingTrivia(currentFieldDeclaration.GetLeadingTrivia())
            .WithTrailingTrivia(currentFieldDeclaration.GetTrailingTrivia())
            .WithAdditionalAnnotations(Formatter.Annotation);
        var updatedRoot = declarationRoot.ReplaceNode(currentFieldDeclaration, partialProperty);
        return solution.WithDocumentSyntaxRoot(declarationDocument.Id, updatedRoot);
    }

    private static SyntaxList<AttributeListSyntax> RewriteHookAttributeStrings(
        SyntaxList<AttributeListSyntax> attributeLists,
        string fieldName,
        string backingFieldName,
        HashSet<string> getHookAttributeNames,
        HashSet<string> setHookAttributeNames)
    {
        return SyntaxFactory.List(attributeLists.Select(attributeList => attributeList.ReplaceNodes(
            attributeList.Attributes,
            (_, attribute) =>
            {
                var attributeName = attribute.Name.ToString();
                var isGetHook = getHookAttributeNames.Contains(attributeName);
                if (!isGetHook && !setHookAttributeNames.Contains(attributeName))
                    return attribute;
                var arguments = attribute.ArgumentList?.Arguments;
                if (arguments is not { Count: > 0 }
                    || arguments.Value[0].Expression is not LiteralExpressionSyntax literal
                    || !literal.IsKind(SyntaxKind.StringLiteralExpression))
                    return attribute;
                var rewrittenCode = literal.Token.ValueText;
                var qualifiedFieldPattern = $@"(?:(?:\b@?[A-Za-z_][A-Za-z0-9_]*\s*\.\s*)*)(?:Current|this)\s*\.\s*{Regex.Escape(fieldName)}\b";
                rewrittenCode = Regex.Replace(rewrittenCode, qualifiedFieldPattern, backingFieldName);
                if (isGetHook || fieldName != "value")
                {
                    rewrittenCode = Regex.Replace(rewrittenCode, $@"\b{Regex.Escape(fieldName)}\b", backingFieldName);
                }
                var rewrittenLiteral = SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(rewrittenCode));
                return attribute.ReplaceNode(literal, rewrittenLiteral);
            })));
    }

    private static SyntaxList<AttributeListSyntax> PreparePartialPropertyAttributes(
        SyntaxList<AttributeListSyntax> attributeLists,
        string fieldName,
        string backingFieldName,
        HashSet<string> profileAttributeNames,
        HashSet<string> getHookAttributeNames,
        HashSet<string> setHookAttributeNames)
    {
        var rewrittenLists = RewriteHookAttributeStrings(attributeLists, fieldName, backingFieldName, getHookAttributeNames, setHookAttributeNames);
        var result = new List<AttributeListSyntax>();
        foreach (var attributeList in rewrittenLists)
        {
            var propertyAttributes = attributeList.Attributes.Where(attribute => profileAttributeNames.Contains(attribute.Name.ToString())).ToArray();
            var backingFieldAttributes = attributeList.Attributes.Where(attribute => !profileAttributeNames.Contains(attribute.Name.ToString())).ToArray();
            if (propertyAttributes.Length > 0)
            {
                result.Add(attributeList
                    .WithTarget(null)
                    .WithAttributes(SyntaxFactory.SeparatedList(propertyAttributes)));
            }
            if (backingFieldAttributes.Length > 0)
            {
                result.Add(attributeList
                    .WithTarget(SyntaxFactory.AttributeTargetSpecifier(SyntaxFactory.Token(SyntaxKind.FieldKeyword))
                        .WithColonToken(SyntaxFactory.Token(SyntaxKind.ColonToken)))
                    .WithAttributes(SyntaxFactory.SeparatedList(backingFieldAttributes))
                    .WithLeadingTrivia(propertyAttributes.Length == 0 ? attributeList.GetLeadingTrivia() : default));
            }
        }
        return SyntaxFactory.List(result);
    }
}