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.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using System;
using System.Collections.Immutable;
using System.Linq;
using System.Text.RegularExpressions;

namespace XFEExtension.NetCore.AutoConfig.Diagnostics;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class AutoConfigDiagnostics : DiagnosticAnalyzer
{
    private const string AddGetAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAddGetAttribute";
    private const string AddSetAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAddSetAttribute";
    private const string ProfilePropertyAttributeName = "XFEExtension.NetCore.AutoConfig.ProfilePropertyAttribute";
    private const string GeneratedProfilePropertyAttributeName = "XFEExtension.NetCore.AutoConfig.ProfileFieldAutoGenerateAttribute";
    private const string ProfileBaseTypeName = "XFEExtension.NetCore.AutoConfig.XFEProfile";
    private const string TargetFrameworkAttributeName = "System.Runtime.Versioning.TargetFrameworkAttribute";

    public const string AddGetNoResultErrorId = "XFE0002";
    public const string FieldCanUsePartialPropertyId = "XFE0003";
    public const string AddSetNoSetResultWarningId = "XFW0001";

    public static readonly DiagnosticDescriptor AddGetNoResultError = new(
        AddGetNoResultErrorId,
        "Get方法没有返回值",
        "设置了自定义的Get方法但是没有返回值:'{0}'",
        "XFEExtension.NetCore.AutoConfig.Diagnostics",
        DiagnosticSeverity.Error,
        true,
        "设置了自定义的Get方法但是没有返回值.",
        "https://www.xfegzs.com/Docs/View/Errors%2FAutoConfig%2FXFE0002");

    public static readonly DiagnosticDescriptor AddSetNoSetResultWarning = new(
        AddSetNoSetResultWarningId,
        "Set方法没有设置值",
        "设置了自定义的Set方法但是没有对实际字段进行操作:'{0}'",
        "XFEExtension.NetCore.AutoConfig.Diagnostics",
        DiagnosticSeverity.Warning,
        true,
        "设置了自定义的Set方法但是没有对实际字段进行操作.",
        "https://www.xfegzs.com/Docs/View/Errors%2FAutoConfig%2FXFW0001");

    public static readonly DiagnosticDescriptor FieldCanUsePartialProperty = new(
        FieldCanUsePartialPropertyId,
        "配置字段可升级为部分属性",
        "配置字段“{0}”可升级为 .NET 10 静态部分属性“{1}”,并保留现有静态调用方式",
        "XFEExtension.NetCore.AutoConfig.Diagnostics",
        DiagnosticSeverity.Info,
        true,
        "目标项目为 .NET 10 或更高版本时,可使用 C# 14 static field-backed partial property,并通过代码修复完成迁移.");

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [AddGetNoResultError, AddSetNoSetResultWarning, FieldCanUsePartialProperty];

    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        context.RegisterSyntaxNodeAction(AnalyzeFieldDeclaration, SyntaxKind.FieldDeclaration);
        context.RegisterSyntaxNodeAction(AnalyzePropertyDeclaration, SyntaxKind.PropertyDeclaration);
    }

    private static void AnalyzeFieldDeclaration(SyntaxNodeAnalysisContext context)
    {
        var declaration = (FieldDeclarationSyntax)context.Node;
        foreach (var variable in declaration.Declaration.Variables)
        {
            if (context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken) is not IFieldSymbol field)
                continue;

            var getAttributes = field.GetAttributes().Where(static attribute => attribute.AttributeClass?.ToDisplayString() == AddGetAttributeName).ToArray();
            if (getAttributes.Length > 0 && !getAttributes.Select(GetCode).Any(static code => code?.Contains("return") == true))
            {
                context.ReportDiagnostic(Diagnostic.Create(AddGetNoResultError, GetAttributeLocation(getAttributes[getAttributes.Length - 1], variable), field.Name));
            }

            var setAttributes = field.GetAttributes().Where(static attribute => attribute.AttributeClass?.ToDisplayString() == AddSetAttributeName).ToArray();
            if (setAttributes.Length == 0)
            {
                ReportPartialPropertySuggestion(context, declaration, variable, field);
                continue;
            }
            var assignmentPattern = $@"\b{Regex.Escape(field.Name)}\s*=\s*value\b";
            if (!setAttributes.Select(GetCode).Any(code => code is not null && Regex.IsMatch(code, assignmentPattern)))
                context.ReportDiagnostic(Diagnostic.Create(AddSetNoSetResultWarning, GetAttributeLocation(setAttributes[setAttributes.Length - 1], variable), field.Name));
            ReportPartialPropertySuggestion(context, declaration, variable, field);
        }
    }

    private static void AnalyzePropertyDeclaration(SyntaxNodeAnalysisContext context)
    {
        var declaration = (PropertyDeclarationSyntax)context.Node;
        if (context.SemanticModel.GetDeclaredSymbol(declaration, context.CancellationToken) is not IPropertySymbol property
            || !property.GetAttributes().Any(static attribute => attribute.AttributeClass?.ToDisplayString() == ProfilePropertyAttributeName))
            return;

        var getAttributes = property.GetAttributes().Where(static attribute => attribute.AttributeClass?.ToDisplayString() == AddGetAttributeName).ToArray();
        if (getAttributes.Length > 0 && !getAttributes.Select(GetCode).Any(static code => code?.Contains("return") == true))
            context.ReportDiagnostic(Diagnostic.Create(AddGetNoResultError, GetAttributeLocation(getAttributes[getAttributes.Length - 1], declaration), property.Name));

        var setAttributes = property.GetAttributes().Where(static attribute => attribute.AttributeClass?.ToDisplayString() == AddSetAttributeName).ToArray();
        if (setAttributes.Length == 0)
            return;
        var assignmentPattern = @"\bfield\s*=\s*value\b";
        if (!setAttributes.Select(GetCode).Any(code => code is not null && Regex.IsMatch(code, assignmentPattern)))
            context.ReportDiagnostic(Diagnostic.Create(AddSetNoSetResultWarning, GetAttributeLocation(setAttributes[setAttributes.Length - 1], declaration), property.Name));
    }

    private static void ReportPartialPropertySuggestion(SyntaxNodeAnalysisContext context, FieldDeclarationSyntax declaration, VariableDeclaratorSyntax variable, IFieldSymbol field)
    {
        if (declaration.Declaration.Variables.Count != 1
            || field.IsStatic
            || field.IsConst
            || field.IsReadOnly
            || field.IsFixedSizeBuffer
            || field.Type is IPointerTypeSymbol
            || field.Type is IFunctionPointerTypeSymbol
            || declaration.Modifiers.Any(SyntaxKind.VolatileKeyword)
            || declaration.Modifiers.Any(SyntaxKind.UnsafeKeyword)
            || !field.GetAttributes().Any(static attribute => attribute.AttributeClass?.ToDisplayString() == ProfilePropertyAttributeName)
            || !DerivesFromXfeProfile(field.ContainingType)
            || !SupportsNet10PartialProperties(context))
            return;

        var propertyName = GetGeneratedPropertyName(field);
        if (string.IsNullOrWhiteSpace(propertyName)
            || !SyntaxFacts.IsValidIdentifier(propertyName)
            || SyntaxFacts.GetKeywordKind(propertyName) != SyntaxKind.None
            || SyntaxFacts.GetContextualKeywordKind(propertyName) != SyntaxKind.None)
            return;

        if (HasMemberConflict(field, propertyName))
            return;

        var properties = ImmutableDictionary<string, string?>.Empty
            .Add("PropertyName", propertyName);
        context.ReportDiagnostic(Diagnostic.Create(FieldCanUsePartialProperty, variable.Identifier.GetLocation(), properties, field.Name, propertyName));
    }

    private static bool HasMemberConflict(IFieldSymbol field, string memberName) => field.ContainingType
        .GetMembers(memberName)
        .Any(member => !SymbolEqualityComparer.Default.Equals(member, field)
            && !member.GetAttributes().Any(static attribute => attribute.AttributeClass?.ToDisplayString() == GeneratedProfilePropertyAttributeName));

    private static bool SupportsNet10PartialProperties(SyntaxNodeAnalysisContext context)
    {
        if (context.Node.SyntaxTree.Options is not CSharpParseOptions parseOptions || parseOptions.LanguageVersion < LanguageVersion.CSharp14)
            return false;

        if (context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.TargetFramework", out var targetFramework))
            return IsNet10OrGreater(targetFramework);

        var targetFrameworkAttribute = context.Compilation.Assembly.GetAttributes().FirstOrDefault(static attribute => attribute.AttributeClass?.ToDisplayString() == TargetFrameworkAttributeName);
        return targetFrameworkAttribute is not null
            && targetFrameworkAttribute.ConstructorArguments.Length > 0
            && targetFrameworkAttribute.ConstructorArguments[0].Value is string frameworkName
            && IsNet10OrGreater(frameworkName);
    }

    private static bool IsNet10OrGreater(string targetFramework)
    {
        if (targetFramework.StartsWith("net", StringComparison.OrdinalIgnoreCase)
            && !targetFramework.StartsWith("netstandard", StringComparison.OrdinalIgnoreCase)
            && !targetFramework.StartsWith("netcoreapp", StringComparison.OrdinalIgnoreCase))
        {
            var versionText = targetFramework.Substring(3).Split('-')[0];
            return versionText.Contains(".") && Version.TryParse(versionText, out var version) && version.Major >= 10;
        }

        const string corePrefix = ".NETCoreApp,Version=v";
        const string netPrefix = ".NET,Version=v";
        var prefix = targetFramework.StartsWith(corePrefix, StringComparison.OrdinalIgnoreCase)
            ? corePrefix
            : targetFramework.StartsWith(netPrefix, StringComparison.OrdinalIgnoreCase) ? netPrefix : null;
        return prefix is not null
            && Version.TryParse(targetFramework.Substring(prefix.Length), out var frameworkVersion)
            && frameworkVersion.Major >= 10;
    }

    private static string GetGeneratedPropertyName(IFieldSymbol field)
    {
        var attribute = field.GetAttributes().FirstOrDefault(static candidate => candidate.AttributeClass?.ToDisplayString() == ProfilePropertyAttributeName);
        var explicitName = attribute?.ConstructorArguments.Length > 0 ? attribute.ConstructorArguments[0].Value as string : null;
        if (string.IsNullOrWhiteSpace(explicitName) && attribute is not null)
        {
            foreach (var namedArgument in attribute.NamedArguments)
                if (namedArgument.Key == "PropertyName" && namedArgument.Value.Value is string namedValue)
                    explicitName = namedValue;
        }
        if (!string.IsNullOrWhiteSpace(explicitName))
            return explicitName!;
        var fieldName = field.Name.StartsWith("_", StringComparison.Ordinal) ? field.Name.Substring(1) : field.Name;
        return fieldName.Length == 0 ? fieldName : char.ToUpperInvariant(fieldName[0]) + fieldName.Substring(1);
    }

    private static bool DerivesFromXfeProfile(INamedTypeSymbol? type)
    {
        for (var current = type?.BaseType; current is not null; current = current.BaseType)
            if (current.ToDisplayString() == ProfileBaseTypeName)
                return true;
        return false;
    }

    private static string? GetCode(AttributeData attribute) => attribute.ConstructorArguments.Length > 0 ? attribute.ConstructorArguments[0].Value as string : null;

    private static Location GetAttributeLocation(AttributeData attribute, VariableDeclaratorSyntax fallback) => attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ?? fallback.GetLocation();

    private static Location GetAttributeLocation(AttributeData attribute, PropertyDeclarationSyntax fallback) => attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ?? fallback.GetLocation();
}