CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
307 lines
15 KiB
C#
307 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.Immutable;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
|
|
namespace PCL.Core.SourceGenerators;
|
|
|
|
[Generator(LanguageNames.CSharp)]
|
|
public class LifecycleScopeGenerator : IIncrementalGenerator
|
|
{
|
|
private const string ScopeAttributeType = SharedConstants.LifecycleScopeAttribute;
|
|
|
|
private const string StartMethodAttributeType = SharedConstants.LifecycleStartAttribute;
|
|
private const string StopMethodAttributeType = SharedConstants.LifecycleStopAttribute;
|
|
private const string CommandHandlerMethodAttributeType = SharedConstants.LifecycleCommandHandlerAttribute;
|
|
private const string DependencyInjectionMethodAttributeType = SharedConstants.LifecycleDependencyInjectionAttribute;
|
|
private const string NewDependencyInjectionPointAttributeType = SharedConstants.DependencyInjectionPointAttribute;
|
|
|
|
private static readonly HashSet<string> _MethodAttributeTypes = [
|
|
StartMethodAttributeType, StopMethodAttributeType,
|
|
CommandHandlerMethodAttributeType, DependencyInjectionMethodAttributeType,
|
|
NewDependencyInjectionPointAttributeType
|
|
];
|
|
|
|
private record ScopeMethodModel
|
|
{
|
|
public string MethodName { get; init; } = null!;
|
|
public bool Awaitable { get; init; }
|
|
}
|
|
|
|
private record StartMethodModel : ScopeMethodModel;
|
|
|
|
private record StopMethodModel : ScopeMethodModel;
|
|
|
|
private record CommandHandlerMethodModel(
|
|
string Command,
|
|
bool HasCommandModelArg,
|
|
bool HasIsCallbackArg,
|
|
(string Name, string TypeName, bool hasDefaultValue, object? DefaultValue)[] SplitArgs
|
|
) : ScopeMethodModel;
|
|
|
|
private record DependencyInjectionMethodModel(
|
|
string Identifier,
|
|
int Targets,
|
|
string ParameterType
|
|
) : ScopeMethodModel;
|
|
|
|
private record NewDependencyInjectionPointModel(
|
|
string Identifier
|
|
) : ScopeMethodModel;
|
|
|
|
private class ScopeModel
|
|
{
|
|
public string Namespace { get; init; } = null!;
|
|
public string TypeName { get; init; } = null!;
|
|
public string QualifiedTypeName => $"{Namespace}.{TypeName}";
|
|
public string Identifier { get; init; } = null!;
|
|
public string Name { get; init; } = null!;
|
|
public bool SupportAsync { get; init; }
|
|
public List<ScopeMethodModel> Methods { get; } = [];
|
|
}
|
|
|
|
public void Initialize(IncrementalGeneratorInitializationContext context)
|
|
{
|
|
// Debugger.Launch();
|
|
var candidates = context.SyntaxProvider.ForAttributeWithMetadataName(
|
|
ScopeAttributeType,
|
|
static (node, _) => node is ClassDeclarationSyntax syntax && syntax.Modifiers.Any(m => m.ValueText == "partial"),
|
|
static (INamedTypeSymbol, ScopeModel)? (ctx, _) =>
|
|
{
|
|
if (ctx.TargetSymbol is not INamedTypeSymbol typeSymbol) return null;
|
|
var attr = ctx.Attributes[0];
|
|
var args = attr.ConstructorArguments;
|
|
var scopeIdentifier = args[0].Value!.ToString();
|
|
var scopeName = args[1].Value!.ToString();
|
|
var scopeAsyncStart = true;
|
|
if (args.Length > 2 && args[2].Value is bool v) scopeAsyncStart = v;
|
|
var ns = typeSymbol.ContainingNamespace.ToDisplayString();
|
|
var typeName = typeSymbol.Name;
|
|
return (typeSymbol, new ScopeModel
|
|
{
|
|
Namespace = ns,
|
|
TypeName = typeName,
|
|
Identifier = scopeIdentifier,
|
|
Name = scopeName,
|
|
SupportAsync = scopeAsyncStart
|
|
});
|
|
}
|
|
).Where(static i => i is not null).Select(static (i, _) => i.GetValueOrDefault());
|
|
var collected = candidates.Collect();
|
|
context.RegisterSourceOutput(collected, _CollectSources);
|
|
}
|
|
|
|
private static void _CollectSources(SourceProductionContext spc, ImmutableArray<(INamedTypeSymbol TypeSymbol, ScopeModel Model)> models)
|
|
{
|
|
foreach (var (symbol, model) in models)
|
|
{
|
|
model.Methods.Clear();
|
|
foreach (var member in symbol.GetMembers())
|
|
{
|
|
if (member is not IMethodSymbol method) continue;
|
|
var attrTypeName = string.Empty;
|
|
var attr = method.GetAttributes().FirstOrDefault(data =>
|
|
{
|
|
attrTypeName = data.AttributeClass?.GetSimplifiedTypeName();
|
|
return attrTypeName is not null && _MethodAttributeTypes.Contains(attrTypeName);
|
|
});
|
|
if (attr is null) continue;
|
|
var methodName = method.Name;
|
|
var awaitable = method.IsAwaitable();
|
|
ScopeMethodModel? methodModel = attrTypeName switch
|
|
{
|
|
StartMethodAttributeType => new StartMethodModel { MethodName = methodName, Awaitable = awaitable },
|
|
StopMethodAttributeType => new StopMethodModel { MethodName = methodName, Awaitable = awaitable },
|
|
CommandHandlerMethodAttributeType => GetCommandHandlerMethodModel(),
|
|
DependencyInjectionMethodAttributeType => GetDependencyInjectionMethodModel(),
|
|
NewDependencyInjectionPointAttributeType => GetNewDependencyInjectionPointModel(),
|
|
_ => null
|
|
};
|
|
if (methodModel is not null) model.Methods.Add(methodModel);
|
|
continue;
|
|
CommandHandlerMethodModel? GetCommandHandlerMethodModel()
|
|
{
|
|
if (awaitable) return null;
|
|
var command = attr.ConstructorArguments[0].Value!.ToString();
|
|
var paraArray = method.Parameters;
|
|
var skip = 0;
|
|
var hasCommandModelArg = paraArray.Length > 0
|
|
&& paraArray[0].Type.GetSimplifiedTypeName() == "PCL.Core.App.Cli.CommandLine";
|
|
if (hasCommandModelArg) skip++;
|
|
var hasIsCallbackArgIndex = hasCommandModelArg ? 1 : 0;
|
|
var hasIsCallbackArg = paraArray.Length > hasIsCallbackArgIndex
|
|
&& paraArray[hasIsCallbackArgIndex].Type.SpecialType == SpecialType.System_Boolean
|
|
&& paraArray[hasIsCallbackArgIndex].Name == "isCallback";
|
|
if (hasIsCallbackArg) skip++;
|
|
var splitArgs = (
|
|
from para in paraArray.Skip(skip)
|
|
let name = para.Name
|
|
let typeName = para.Type.GetFullyQualifiedName()
|
|
let hasDefaultValue = para.HasExplicitDefaultValue
|
|
select (name, typeName, hasDefaultValue, hasDefaultValue ? para.ExplicitDefaultValue : null)
|
|
).ToArray();
|
|
return new CommandHandlerMethodModel(command, hasCommandModelArg, hasIsCallbackArg, splitArgs)
|
|
{
|
|
MethodName = methodName,
|
|
Awaitable = false
|
|
};
|
|
}
|
|
DependencyInjectionMethodModel? GetDependencyInjectionMethodModel()
|
|
{
|
|
var args = attr.ConstructorArguments;
|
|
var identifier = args[0].Value!.ToString();
|
|
var targets = (int)args[1].Value!;
|
|
if (method.Parameters.FirstOrDefault() is not { } param) return null;
|
|
var paramType = param.Type.GetFullyQualifiedName();
|
|
return new DependencyInjectionMethodModel(identifier, targets, paramType)
|
|
{
|
|
MethodName = methodName,
|
|
Awaitable = awaitable
|
|
};
|
|
}
|
|
NewDependencyInjectionPointModel? GetNewDependencyInjectionPointModel()
|
|
{
|
|
var args = attr.ConstructorArguments;
|
|
if (args.Length > 1 && args[1].Value is false) return null; // lifecycleAutoInvoke is set to false
|
|
var identifier = args[0].Value!.ToString();
|
|
return new NewDependencyInjectionPointModel(identifier)
|
|
{
|
|
MethodName = methodName,
|
|
Awaitable = awaitable
|
|
};
|
|
}
|
|
}
|
|
spc.AddSource($"{model.QualifiedTypeName}.g.cs", _GenerateScopeSource(model));
|
|
}
|
|
}
|
|
|
|
private static readonly HashSet<Type> _TypesIncludingInStartMethod = [
|
|
typeof(StartMethodModel),
|
|
typeof(CommandHandlerMethodModel),
|
|
typeof(DependencyInjectionMethodModel),
|
|
typeof(CommandHandlerMethodModel),
|
|
typeof(NewDependencyInjectionPointModel),
|
|
];
|
|
|
|
private static string _GenerateScopeSource(ScopeModel model)
|
|
{
|
|
var sb = new StringBuilder();
|
|
|
|
// head
|
|
sb.AppendLine("// 此文件由 Source Generator 自动生成,请勿手动修改");
|
|
sb.AppendLine("// <auto-generated />");
|
|
sb.AppendLine();
|
|
sb.AppendLine("using System;");
|
|
sb.AppendLine("using System.Threading.Tasks;");
|
|
sb.AppendLine($"using {SharedConstants.AppNamespace};");
|
|
sb.AppendLine($"using {SharedConstants.IocNamespace};");
|
|
sb.AppendLine();
|
|
sb.AppendLine("#nullable enable");
|
|
sb.AppendLine();
|
|
sb.AppendLine($"namespace {model.Namespace};");
|
|
sb.AppendLine();
|
|
|
|
// basic structure
|
|
sb.AppendLine($"partial class {model.TypeName} : ILifecycleService");
|
|
sb.AppendLine("{");
|
|
sb.AppendLine($" public string Identifier => {model.Identifier.ToLiteral()};");
|
|
sb.AppendLine($" public string Name => {model.Name.ToLiteral()};");
|
|
sb.AppendLine($" public bool SupportAsync => {(model.SupportAsync ? "true" : "false")};");
|
|
sb.AppendLine();
|
|
sb.AppendLine(" private static LifecycleContext Context { get => field ?? throw new InvalidOperationException(\"Not initialized\"); set; } = null!;");
|
|
sb.AppendLine(" private static ILifecycleService Service => Context.ServiceInstance;");
|
|
sb.AppendLine($" public {model.TypeName}() {{ Context = Lifecycle.GetContext(this); }}");
|
|
sb.AppendLine();
|
|
|
|
// StopAsync() implementation
|
|
sb.AppendLine(" public async Task StopAsync()");
|
|
sb.AppendLine(" {");
|
|
var stopCount = AppendMethodInvokes(2, model.Methods.Where(x => x is StopMethodModel));
|
|
sb.AppendLine(" }");
|
|
sb.AppendLine();
|
|
|
|
// StartAsync() implementation
|
|
sb.AppendLine(" public async Task StartAsync()");
|
|
sb.AppendLine(" {");
|
|
AppendMethodInvokes(2, model.Methods.Where(x => _TypesIncludingInStartMethod.Contains(x.GetType())));
|
|
if (stopCount == 0) sb.AppendLine(" Context.DeclareStopped();");
|
|
sb.AppendLine(" }");
|
|
|
|
// structure tail
|
|
sb.AppendLine("}");
|
|
|
|
return sb.ToString();
|
|
|
|
// method invokes implementation
|
|
int AppendMethodInvokes(int indent, IEnumerable<ScopeMethodModel> models)
|
|
{
|
|
var count = 0;
|
|
var indentStr = new string(' ', indent * 4);
|
|
foreach (var methodModel in models)
|
|
{
|
|
count++;
|
|
sb.Append(indentStr).AppendLine("{");
|
|
foreach (var line in _EmitMethod(methodModel)) sb.Append(indentStr).Append(" ").AppendLine(line);
|
|
sb.Append(indentStr).AppendLine("}");
|
|
}
|
|
return count;
|
|
}
|
|
}
|
|
|
|
private static IEnumerable<string> _EmitMethod(ScopeMethodModel model)
|
|
{
|
|
if (model is StartMethodModel or StopMethodModel)
|
|
{
|
|
yield return MethodInvoke();
|
|
}
|
|
else if (model is CommandHandlerMethodModel argModel)
|
|
{
|
|
var actionParamModel = argModel.HasCommandModelArg ? "model" : "_";
|
|
var actionParamIsCallback = argModel.HasIsCallbackArg ? "isCallback" : "_";
|
|
yield return $"Essentials.StartupService.TryHandleCommand(" +
|
|
$"{argModel.Command.ToLiteral()}, ({actionParamModel}, {actionParamIsCallback}) => {{";
|
|
var argTexts = new List<string>();
|
|
if (argModel.HasCommandModelArg) argTexts.Add(actionParamModel);
|
|
if (argModel.HasIsCallbackArg) argTexts.Add(actionParamIsCallback);
|
|
foreach (var (name, typeName, hasDefaultValue, defaultValue) in argModel.SplitArgs)
|
|
{
|
|
var existsText = "exists_" + name;
|
|
var isTypeMatchText = "isTypeMatch_" + name;
|
|
var valueText = "value_" + name;
|
|
yield return $" var ({(hasDefaultValue ? existsText : "_")}, {isTypeMatchText}) = model.TryGetArgumentValue<{typeName}>(\"{name}\", out var {valueText});";
|
|
yield return $" if (!{isTypeMatchText}) throw new InvalidCastException(\"Argument type mismatch\");";
|
|
argTexts.Add(hasDefaultValue ? $"{existsText} ? {valueText} : {defaultValue.ToPrimitive() ?? "default"}" : valueText);
|
|
}
|
|
yield return MethodInvoke(" ", argTexts);
|
|
yield return "}, true);";
|
|
}
|
|
else if (model is DependencyInjectionMethodModel diModel)
|
|
{
|
|
var awaitable = diModel.Awaitable;
|
|
if (awaitable) yield return "await Task.Run(() => {";
|
|
var indentStr = awaitable ? " " : "";
|
|
if (awaitable) yield return $"{indentStr}Func<{diModel.ParameterType}, Task>";
|
|
else yield return $"{indentStr}Action<{diModel.ParameterType}>";
|
|
yield return $"{indentStr} action = {diModel.MethodName};";
|
|
yield return $"{indentStr}var result = DependencyGroups.InvokeInjection(action, " +
|
|
$"{diModel.Identifier.ToLiteral()}, " +
|
|
$"(AttributeTargets){diModel.Targets});";
|
|
var logStr = diModel.Identifier + "@" + diModel.Targets;
|
|
yield return $"{indentStr}if (result) Context.Trace(\"Dependency injection success: {logStr}\");";
|
|
yield return $"{indentStr}else Context.Warn(\"Dependency injection failed: {logStr}\");";
|
|
if (awaitable) yield return "});";
|
|
}
|
|
else if (model is NewDependencyInjectionPointModel newDiModel)
|
|
{
|
|
yield return $"{(model.Awaitable ? "await " : "")}" +
|
|
$"{model.MethodName}_InvokeInjection_{newDiModel.Identifier.SnakeIdToPascal()}();";
|
|
}
|
|
yield break;
|
|
string MethodInvoke(string prefix = "", params IEnumerable<string> args)
|
|
=> $"{prefix}{(model.Awaitable ? "await " : "")}{model.MethodName}({string.Join(", ", args)});";
|
|
}
|
|
}
|