初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
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)});";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user