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

XFEExtension.NetCore.ServerInteractive

[DLL] Server interaction extension, including user identity verification and querying in conjunction with AutoConfig

公开
关注 0 Fork 0 Star 0
UTF-8
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using XFEExtension.NetCore.ServerInteractive.SourceGenerator.Models;

namespace XFEExtension.NetCore.ServerInteractive.SourceGenerator;

/// <summary>
/// 次级入口点增量生成器
/// 用于自动生成IServerCoreStandardService的入口点字典
/// </summary>
[Generator]
public class EntryPointGenerator : IIncrementalGenerator
{
    private static readonly DiagnosticDescriptor NonPartialClassRule = new(
        id: "XFE0003",
        title: "包含EntryPoint方法的类必须为partial",
        messageFormat: "类'{0}'必须声明为partial以便增量生成器可以生成入口点代码",
        category: "XFEServerInteractive",
        defaultSeverity: DiagnosticSeverity.Error,
        helpLinkUri: "https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0003",
        isEnabledByDefault: true);

    private static readonly DiagnosticDescriptor MethodMustBeParameterlessRule = new(
        id: "XFE0004",
        title: "EntryPoint方法不能有参数",
        messageFormat: "方法'{0}'标记了[EntryPoint]但包含参数,入口点方法必须是无参数的",
        category: "XFEServerInteractive",
        defaultSeverity: DiagnosticSeverity.Error,
        helpLinkUri: "https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0004",
        isEnabledByDefault: true);

    private static readonly DiagnosticDescriptor InvalidReturnTypeRule = new(
        id: "XFE0005",
        title: "EntryPoint方法返回类型无效",
        messageFormat: "方法'{0}'的返回类型'{1}'无效,入口点方法必须返回void或Task",
        category: "XFEServerInteractive",
        defaultSeverity: DiagnosticSeverity.Error,
        helpLinkUri: "https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0005",
        isEnabledByDefault: true);

    private static readonly DiagnosticDescriptor InvalidPathCharactersRule = new(
        id: "XFE0006",
        title: "EntryPoint路径包含无效字符",
        messageFormat: "入口点路径'{0}'包含无效字符(引号或反斜杠),这些字符不允许在路径中使用",
        category: "XFEServerInteractive",
        defaultSeverity: DiagnosticSeverity.Error,
        helpLinkUri: "https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0006",
        isEnabledByDefault: true);

    private static readonly DiagnosticDescriptor DuplicatePathRule = new(
        id: "XFE0012",
        title: "EntryPoint路径重复注册",
        messageFormat: "入口点路径'{0}'在类'{1}'中重复注册,每个路径只能对应一个处理方法",
        category: "XFEServerInteractive",
        defaultSeverity: DiagnosticSeverity.Error,
        helpLinkUri: "https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0012",
        isEnabledByDefault: true);

    private static readonly DiagnosticDescriptor InvalidWildcardUsageRule = new(
        id: "XFE0013",
        title: "EntryPoint通配符使用无效",
        messageFormat: "入口点路径'{0}'中的通配符'*'必须作为完整的路径段使用(例如:v1/*/test),不能与其他字符混合(例如:v1/a*b)",
        category: "XFEServerInteractive",
        defaultSeverity: DiagnosticSeverity.Error,
        helpLinkUri: "https://docs.xfegzs.com/View/Errors%2FServerInteractive%2FXFE0013",
        isEnabledByDefault: true);

    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        // 找到所有标记了EntryPointAttribute的方法
        var methodDeclarations = context.SyntaxProvider
            .CreateSyntaxProvider(
                predicate: static (s, _) => IsCandidateMethod(s),
                transform: static (ctx, _) => GetMethodsForGeneration(ctx))
            .Where(static m => m is { IsDefault: false, Length: > 0 })
            .SelectMany(static (m, _) => m);

        // 按类分组
        var compilationAndMethods = context.CompilationProvider.Combine(methodDeclarations.Collect());

        // 生成源代码
        context.RegisterSourceOutput(compilationAndMethods,
            static (spc, source) => Execute(source.Left, source.Right, spc));
    }

    private static bool IsCandidateMethod(SyntaxNode node) => node is MethodDeclarationSyntax { AttributeLists.Count: > 0 };

    private static ImmutableArray<MethodCandidate> GetMethodsForGeneration(GeneratorSyntaxContext context)
    {
        var methodDeclaration = (MethodDeclarationSyntax)context.Node;
        var methodSymbol = context.SemanticModel.GetDeclaredSymbol(methodDeclaration);

        if (methodSymbol is null)
            return default;

        // 获取所有EntryPointAttribute
        var entryPointAttributes = methodSymbol.GetAttributes()
            .Where(a => a.AttributeClass?.Name == "EntryPointAttribute")
            .ToList();

        if (entryPointAttributes.Count == 0)
            return default;

        // 检查返回类型:根据返回类型(而非async关键字)判断同步/异步
        // 注意:Task和Task<T>都是有效的异步返回类型(Task<T>可通过协变赋值给Func<Task>)
        var returnType = methodSymbol.ReturnType;
        var isVoid = returnType.SpecialType == SpecialType.System_Void;
        var isTaskLike = returnType.Name == "Task" &&
                         returnType.ContainingNamespace?.ToDisplayString() == "System.Threading.Tasks";
        var hasValidReturnType = isVoid || isTaskLike;
        var isAsync = isTaskLike;

        // 检查包含类型是否为partial
        var classDeclaration = methodDeclaration.Parent as ClassDeclarationSyntax;
        var isContainingTypePartial = classDeclaration?.Modifiers.Any(SyntaxKind.PartialKeyword) ?? false;

        // 获取泛型类型参数和约束(从语法节点获取以保留原始文本,并去除多余空白)
        var typeParameters = classDeclaration?.TypeParameterList?.ToString().Trim() ?? "";
        var typeConstraints = classDeclaration?.ConstraintClauses.ToString().Trim() ?? "";

        var containingType = methodSymbol.ContainingType;

        // 获取位置信息(使用轻量结构避免在增量缓存中持有SyntaxTree引用)
        var methodLocation = LocationInfo.From(methodDeclaration.GetLocation());
        var classLocation = classDeclaration is not null
            ? LocationInfo.From(classDeclaration.Identifier.GetLocation())
            : methodLocation;

        var results = (from attr in entryPointAttributes select attr.ConstructorArguments.FirstOrDefault().Value?.ToString() into rawPath select string.IsNullOrEmpty(rawPath) ? "*" : rawPath!.Trim('/') into path select new MethodCandidate(containingType.ContainingNamespace.ToDisplayString(), containingType.Name, methodSymbol.Name, path, isAsync, isContainingTypePartial, methodSymbol.Parameters.Length, hasValidReturnType, returnType.ToDisplayString(), methodLocation, classLocation, typeParameters, typeConstraints)).ToList();

        return results.Count == 0 ? default : [..results];
    }

    private static void Execute(Compilation compilation, ImmutableArray<MethodCandidate> methods, SourceProductionContext context)
    {
        if (methods.IsDefaultOrEmpty)
            return;

        var validMethods = new List<MethodCandidate>();

        // 校验并报告诊断信息
        foreach (var method in methods)
        {
            var isValid = true;

            // 校验:包含类型必须为partial
            if (!method.IsContainingTypePartial)
            {
                context.ReportDiagnostic(Diagnostic.Create(
                    NonPartialClassRule,
                    method.ClassLocation.ToLocation(),
                    method.ClassName));
                isValid = false;
            }

            // 校验:方法不能有参数
            if (method.ParameterCount > 0)
            {
                context.ReportDiagnostic(Diagnostic.Create(
                    MethodMustBeParameterlessRule,
                    method.MethodLocation.ToLocation(),
                    method.MethodName));
                isValid = false;
            }

            // 校验:返回类型必须为void或Task
            if (!method.HasValidReturnType)
            {
                context.ReportDiagnostic(Diagnostic.Create(
                    InvalidReturnTypeRule,
                    method.MethodLocation.ToLocation(),
                    method.MethodName,
                    method.ReturnTypeName));
                isValid = false;
            }

            // 校验:路径不能包含引号或反斜杠
            if (method.Path.Contains("\"") || method.Path.Contains("\\"))
            {
                context.ReportDiagnostic(Diagnostic.Create(
                    InvalidPathCharactersRule,
                    method.MethodLocation.ToLocation(),
                    method.Path));
                isValid = false;
            }

            // 校验:通配符 '*' 必须作为完整路径段使用
            if (method.Path.Contains("*") && method.Path != "*")
            {
                var segments = method.Path.Split('/');
                if (segments.Any(segment => segment.Contains("*") && segment != "*"))
                {
                    context.ReportDiagnostic(Diagnostic.Create(
                        InvalidWildcardUsageRule,
                        method.MethodLocation.ToLocation(),
                        method.Path));
                    isValid = false;
                }
            }

            if (isValid)
            {
                validMethods.Add(method);
            }
        }

        if (validMethods.Count == 0)
            return;

        // 按类分组并生成代码
        var methodsByClass = validMethods.GroupBy(m => (m.Namespace, m.ClassName, m.TypeParameters, m.TypeConstraints));

        foreach (var group in methodsByClass)
        {
            var (namespaceName, className, typeParameters, typeConstraints) = group.Key;
            var methodInfos = group.ToList();
            var constraintsSuffix = string.IsNullOrEmpty(typeConstraints) ? "" : $" {typeConstraints}";

            // 检查重复路径(路径相同的任意两个方法都报错,无论同步/异步)
            var hasDuplicateError = false;
            var pathToMethods = new Dictionary<string, List<MethodCandidate>>();
            foreach (var method in methodInfos)
            {
                if (!pathToMethods.TryGetValue(method.Path, out var list))
                {
                    list = [];
                    pathToMethods[method.Path] = list;
                }
                list.Add(method);
            }

            foreach (var kvp in pathToMethods.Where(kvp => kvp.Value.Count > 1))
            {
                hasDuplicateError = true;
                foreach (var dup in kvp.Value)
                {
                    context.ReportDiagnostic(Diagnostic.Create(
                        DuplicatePathRule,
                        dup.MethodLocation.ToLocation(),
                        kvp.Key,
                        className));
                }
            }

            if (hasDuplicateError)
                continue;

            var sourceBuilder = new StringBuilder();
            sourceBuilder.AppendLine($@"// <auto-generated/>
#nullable enable

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace {namespaceName}
{{
    /// <summary>
    /// {className}的自动生成入口点字典部分类
    /// </summary>
    public partial class {className}{typeParameters}{constraintsSuffix}
    {{
        /// <summary>
        /// 本类型的入口点路径列表(覆盖基类的空列表)
        /// </summary>
        public override List<string> EntryPointList {{ get; }} = new()
        {{");
            // 添加所有入口点到静态列表
            foreach (var method in methodInfos)
            {
                sourceBuilder.AppendLine($"            \"{EscapeStringLiteral(method.Path)}\",");
            }
            sourceBuilder.AppendLine($@"        }};

        private Dictionary<string, Action>? _generatedSyncEntryPoints;
        /// <inheritdoc/>
        public override Dictionary<string, Action> SyncEntryPoints
        {{
            get => _generatedSyncEntryPoints ??= new Dictionary<string, Action>()
            {{");
            // 添加同步入口点
            foreach (var method in methodInfos.Where(m => !m.IsAsync))
            {
                sourceBuilder.AppendLine($"                {{ \"{EscapeStringLiteral(method.Path)}\", {method.MethodName} }},");
            }
            sourceBuilder.AppendLine($@"            }};
        }}

        private Dictionary<string, Func<Task>>? _generatedAsyncEntryPoints;
        /// <inheritdoc/>
        public override Dictionary<string, Func<Task>> AsyncEntryPoints
        {{
            get => _generatedAsyncEntryPoints ??= new Dictionary<string, Func<Task>>()
            {{");

            // 添加异步入口点
            foreach (var method in methodInfos.Where(m => m.IsAsync))
            {
                sourceBuilder.AppendLine($"                {{ \"{EscapeStringLiteral(method.Path)}\", {method.MethodName} }},");
            }

            sourceBuilder.AppendLine(@"            };
        }
    }
}");

            context.AddSource($"{className}.EntryPoints.g.cs", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
        }
    }

    /// <summary>
    /// 转义字符串字面量中的特殊字符
    /// </summary>
    private static string EscapeStringLiteral(string value)
    {
        return value.Replace("\\", @"\\").Replace("\"", "\\\"");
    }
}