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

XFEExtension.NetCore.XUnit

【DLL】提供方便快捷的测试,无需编写Main方法,可直接添加特性在类或方法上进行测试

公开
关注 0 Fork 0 Star 0
UTF-8
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using System.Collections.Immutable;
using System.Linq;

namespace XFEExtension.NetCore.XUnit.Analyzer.Diagnostics;

/// <summary>
/// 对 XFE 测试和基准特性执行语义分析,诊断不可等待的异步方法、无效生命周期签名和 3.x 旧 API。
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class XUnitCodeAnalyzer : DiagnosticAnalyzer
{
    private const string AttributeNamespace = "XFEExtension.NetCore.XUnit.Attributes";

    /// <summary>
    /// 获取“测试或基准使用 <see langword="async void"/>”诊断的稳定标识符。
    /// </summary>
    public const string AsyncVoidId = "XFE0002";

    /// <summary>
    /// 获取“生命周期方法签名无效”诊断的稳定标识符。
    /// </summary>
    public const string LifecycleId = "XFE0003";

    /// <summary>
    /// 获取“使用 3.x 旧测试 API”迁移诊断的稳定标识符。
    /// </summary>
    public const string LegacyId = "XFE0100";

    /// <summary>
    /// 获取拒绝不可等待 <see langword="async void"/> 测试和基准的错误规则。
    /// </summary>
    public static readonly DiagnosticDescriptor AsyncVoidRule = new(
        AsyncVoidId, "async void tests cannot be awaited",
        "Method '{0}' is async void; return Task or ValueTask so the runner can await completion",
        "XFEExtension.NetCore.XUnit", DiagnosticSeverity.Error, true);

    /// <summary>
    /// 获取验证生命周期方法静态性和参数列表的错误规则。
    /// </summary>
    public static readonly DiagnosticDescriptor LifecycleRule = new(
        LifecycleId, "Invalid lifecycle method",
        "Lifecycle method '{0}' must be parameterless{1}",
        "XFEExtension.NetCore.XUnit", DiagnosticSeverity.Error, true);

    /// <summary>
    /// 获取提示迁移 3.x 特性的警告规则。
    /// </summary>
    public static readonly DiagnosticDescriptor LegacyRule = new(
        LegacyId, "Legacy XFE test API",
        "'{0}' is obsolete and will be removed in XUnit 5.0; migrate to '{1}'",
        "XFEExtension.NetCore.XUnit", DiagnosticSeverity.Warning, true);

    /// <summary>
    /// 获取该分析器能够产生的全部诊断描述符。
    /// </summary>
    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => [AsyncVoidRule, LifecycleRule, LegacyRule];

    /// <summary>
    /// 注册并发符号分析操作,并排除生成代码。
    /// </summary>
    /// <param name="context">Roslyn 分析器初始化上下文。</param>
    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();
        context.RegisterSymbolAction(AnalyzeMethod, SymbolKind.Method);
        context.RegisterSymbolAction(AnalyzeType, SymbolKind.NamedType);
    }

    private static void AnalyzeMethod(SymbolAnalysisContext context)
    {
        var method = (IMethodSymbol)context.Symbol;
        var attributes = method.GetAttributes().Where(IsXfeAttribute).ToArray();
        if (attributes.Length == 0)
            return;
        if (method.IsAsync && method.ReturnsVoid && attributes.Any(static attribute => IsExecutionAttribute(attribute.AttributeClass)))
            context.ReportDiagnostic(Diagnostic.Create(AsyncVoidRule, method.Locations.FirstOrDefault(), method.Name));

        foreach (var attribute in attributes)
        {
            var name = attribute.AttributeClass?.Name;
            if (name is "BeforeAllAttribute" or "AfterAllAttribute" && (!method.IsStatic || method.Parameters.Length != 0))
                context.ReportDiagnostic(Diagnostic.Create(LifecycleRule, method.Locations.FirstOrDefault(), method.Name, " and static"));
            else if (name is "BeforeEachAttribute" or "AfterEachAttribute" or "GlobalSetupAttribute" or "GlobalCleanupAttribute" or "IterationSetupAttribute" or "IterationCleanupAttribute" && method.Parameters.Length != 0)
                context.ReportDiagnostic(Diagnostic.Create(LifecycleRule, method.Locations.FirstOrDefault(), method.Name, string.Empty));
            ReportLegacy(context, attribute);
        }
    }

    private static void AnalyzeType(SymbolAnalysisContext context)
    {
        foreach (var attribute in ((INamedTypeSymbol)context.Symbol).GetAttributes().Where(IsXfeAttribute))
            ReportLegacy(context, attribute);
    }

    private static void ReportLegacy(SymbolAnalysisContext context, AttributeData attribute)
    {
        var name = attribute.AttributeClass?.Name;
        var replacement = name switch
        {
            "CTestAttribute" or "CNTestAttribute" => "TestFixtureAttribute",
            "MTestAttribute" or "MNTestAttribute" or "MRTestAttribute" or "MNRTestAttribute" => "TestCaseAttribute",
            "SMTestAttribute" or "SMNTestAttribute" or "SMRTestAttribute" or "SMNRTestAttribute" => "TestCaseAttribute",
            "SetUpAttribute" => "BeforeEachAttribute",
            _ => null
        };
        if (replacement is null)
            return;
        var location = attribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken).GetLocation() ?? context.Symbol.Locations.FirstOrDefault();
        var properties = ImmutableDictionary<string, string?>.Empty.Add("Replacement", replacement.Replace("Attribute", string.Empty));
        context.ReportDiagnostic(Diagnostic.Create(LegacyRule, location, properties, name!.Replace("Attribute", string.Empty), replacement.Replace("Attribute", string.Empty)));
    }

    private static bool IsXfeAttribute(AttributeData attribute) => attribute.AttributeClass?.ContainingNamespace.ToDisplayString() == AttributeNamespace;

    private static bool IsExecutionAttribute(INamedTypeSymbol? type)
    {
        while (type is not null)
        {
            if (type.ContainingNamespace.ToDisplayString() == AttributeNamespace && type.Name is "TestAttribute" or "TestCaseAttribute" or "MTestAttribute" or "SMTestAttribute" or "BenchmarkAttribute")
                return true;
            type = type.BaseType;
        }
        return false;
    }
}