初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,746 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace PCL.Core.SourceGenerators;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
public sealed class ConfigGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
// 收集所有可能的属性与类
|
||||
var propertyCandidates = context.SyntaxProvider.CreateSyntaxProvider(
|
||||
static (s, _) => s is PropertyDeclarationSyntax { AttributeLists.Count: > 0 },
|
||||
static (ctx, _) => _GetItemCandidate(ctx)
|
||||
).Where(static m => m is not null);
|
||||
|
||||
var groupCandidates = context.SyntaxProvider.CreateSyntaxProvider(
|
||||
static (s, _) => s is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
|
||||
static (ctx, _) => _GetGroupCandidate(ctx)
|
||||
).Where(static m => m is not null);
|
||||
|
||||
var configClassCandidates = context.SyntaxProvider.CreateSyntaxProvider(
|
||||
static (s, _) => s is ClassDeclarationSyntax,
|
||||
static (ctx, _) => _GetConfigClass(ctx)
|
||||
).Where(static m => m is not null);
|
||||
|
||||
// 新增:收集 [RegisterConfigEvent] 的 public static 属性
|
||||
var eventCandidates = context.SyntaxProvider.CreateSyntaxProvider(
|
||||
static (s, _) => s is PropertyDeclarationSyntax { AttributeLists.Count: > 0 },
|
||||
static (ctx, _) => _GetRegisterConfigEventCandidate(ctx)
|
||||
).Where(static m => m is not null);
|
||||
|
||||
var collected = propertyCandidates.Collect()
|
||||
.Combine(groupCandidates.Collect())
|
||||
.Combine(configClassCandidates.Collect());
|
||||
|
||||
context.RegisterSourceOutput(collected, static (spc, triple) =>
|
||||
{
|
||||
var items = triple.Left.Left;
|
||||
var groups = triple.Left.Right;
|
||||
var configs = triple.Right;
|
||||
|
||||
if (configs.Length == 0) return;
|
||||
|
||||
// 建立快速查找
|
||||
var itemList = items.Cast<ItemModel>().ToImmutableArray();
|
||||
var groupList = groups.Cast<GroupModel>().ToImmutableArray();
|
||||
var configList = configs.Cast<ConfigModel>().ToImmutableArray();
|
||||
|
||||
foreach (var config in configList)
|
||||
{
|
||||
try
|
||||
{
|
||||
var tree = _BuildConfigTree(config, itemList, groupList);
|
||||
|
||||
// 跳过无用生成(无顶层项和顶层组声明的类型)
|
||||
if (tree.TopItems.Count == 0 && tree.TopGroups.Count == 0) continue;
|
||||
|
||||
var source = _GenerateAdditionalSource(tree);
|
||||
var hint = _MakeHintName(config);
|
||||
spc.AddSource(hint, source);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 可添加诊断,此处直接忽略以免打断编译
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var serviceInputs = propertyCandidates.Collect()
|
||||
.Combine(groupCandidates.Collect())
|
||||
.Combine(eventCandidates.Collect());
|
||||
context.RegisterSourceOutput(serviceInputs, static (spc, tuple) =>
|
||||
{
|
||||
var items = tuple.Left.Left.Cast<ItemModel>().OrderBy(i => i.DeclOrder).ToList();
|
||||
var groups = tuple.Left.Right.Cast<GroupModel>().ToList();
|
||||
var events = tuple.Right.Cast<EventRegisterModel>().OrderBy(e => e.DeclOrder).ToList();
|
||||
|
||||
// 两者都为空则不生成
|
||||
if (items.Count == 0 && events.Count == 0) return;
|
||||
|
||||
var groupLookup = new Dictionary<INamedTypeSymbol, GroupModel>(SymbolEqualityComparer.Default);
|
||||
foreach (var group in groups)
|
||||
{
|
||||
groupLookup[group.GroupType] = group;
|
||||
}
|
||||
|
||||
var src = _GenerateServiceInitSource(items, events, groupLookup);
|
||||
spc.AddSource("ConfigService.g.cs", src);
|
||||
});
|
||||
}
|
||||
|
||||
private static object? _GetItemCandidate(GeneratorSyntaxContext ctx)
|
||||
{
|
||||
var propSyntax = (PropertyDeclarationSyntax)ctx.Node;
|
||||
var symbol = ctx.SemanticModel.GetDeclaredSymbol(propSyntax);
|
||||
if (symbol is null) return null;
|
||||
|
||||
var compilation = ctx.SemanticModel.Compilation;
|
||||
var attrDefItem = compilation.GetTypeByMetadataName("PCL.Core.App.Configuration.ConfigItemAttribute`1");
|
||||
var attrDefAny = compilation.GetTypeByMetadataName("PCL.Core.App.Configuration.AnyConfigItemAttribute`1");
|
||||
if (attrDefItem is null && attrDefAny is null) return null;
|
||||
|
||||
AttributeData? picked = null;
|
||||
var isAny = false;
|
||||
|
||||
foreach (var a in symbol.GetAttributes())
|
||||
{
|
||||
var ac = a.AttributeClass;
|
||||
if (ac is null) continue;
|
||||
if (attrDefItem is not null && SymbolEqualityComparer.Default.Equals(ac.ConstructedFrom, attrDefItem))
|
||||
{
|
||||
picked = a; isAny = false; break;
|
||||
}
|
||||
if (attrDefAny is not null && SymbolEqualityComparer.Default.Equals(ac.ConstructedFrom, attrDefAny))
|
||||
{
|
||||
picked = a; isAny = true; break;
|
||||
}
|
||||
}
|
||||
if (picked is null) return null;
|
||||
|
||||
if (picked.ConstructorArguments.Length < 1) return null;
|
||||
var key = picked.ConstructorArguments[0].Value as string;
|
||||
if (string.IsNullOrEmpty(key)) return null;
|
||||
|
||||
// 默认值与来源解析
|
||||
var defaultCode = "default";
|
||||
string? sourceCode = null;
|
||||
|
||||
var attrSyntax = (AttributeSyntax?)picked.ApplicationSyntaxReference?.GetSyntax();
|
||||
if (attrSyntax is not null)
|
||||
{
|
||||
var args = attrSyntax.ArgumentList?.Arguments;
|
||||
|
||||
if (isAny)
|
||||
{
|
||||
// AnyConfigItem:没有“默认值”参数: 替换为无参构造函数
|
||||
var tQualified = symbol.Type.GetFullyQualifiedName();
|
||||
defaultCode = "() => new " + tQualified + "()";
|
||||
|
||||
// 来源参数若存在,是第 2 个实参
|
||||
if (args is { Count: >= 2 })
|
||||
{
|
||||
sourceCode = _RenderSourceCode(ctx.SemanticModel, args.Value[1].Expression);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// ConfigItem:参数2为默认值,参数3为来源(可省略)
|
||||
if (args is { Count: >= 2 })
|
||||
{
|
||||
defaultCode = ctx.SemanticModel.RenderDefaultValueCode(args.Value[1].Expression);
|
||||
}
|
||||
if (args is { Count: >= 3 })
|
||||
{
|
||||
sourceCode = _RenderSourceCode(ctx.SemanticModel, args.Value[2].Expression);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ItemModel
|
||||
{
|
||||
Property = symbol,
|
||||
Key = key!,
|
||||
Type = symbol.Type,
|
||||
IsStatic = symbol.IsStatic,
|
||||
DeclOrder = symbol.GetDeclarationOrder(),
|
||||
DefaultValueCode = defaultCode,
|
||||
SourceCode = sourceCode
|
||||
};
|
||||
}
|
||||
|
||||
private static object? _GetGroupCandidate(GeneratorSyntaxContext ctx)
|
||||
{
|
||||
var classSyntax = (ClassDeclarationSyntax)ctx.Node;
|
||||
var symbol = ModelExtensions.GetDeclaredSymbol(ctx.SemanticModel, classSyntax) as INamedTypeSymbol;
|
||||
if (symbol is null) return null;
|
||||
|
||||
var compilation = ctx.SemanticModel.Compilation;
|
||||
var attrDef = compilation.GetTypeByMetadataName("PCL.Core.App.Configuration.ConfigGroupAttribute");
|
||||
if (attrDef is null) return null;
|
||||
|
||||
var attr = symbol.GetAttributes().FirstOrDefault(a =>
|
||||
a.AttributeClass is not null &&
|
||||
SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrDef));
|
||||
|
||||
if (attr is null) return null;
|
||||
|
||||
if (attr.ConstructorArguments.Length < 1) return null;
|
||||
var name = attr.ConstructorArguments[0].Value as string;
|
||||
if (string.IsNullOrEmpty(name)) return null;
|
||||
|
||||
var hasDeclaredSource = false;
|
||||
string? declaredSourceCode = null;
|
||||
var attrSyntax = (AttributeSyntax?)attr.ApplicationSyntaxReference?.GetSyntax();
|
||||
if (attrSyntax?.ArgumentList?.Arguments is { Count: >= 2 } arguments)
|
||||
{
|
||||
hasDeclaredSource = true;
|
||||
declaredSourceCode = _RenderSourceCode(ctx.SemanticModel, arguments[1].Expression);
|
||||
}
|
||||
|
||||
return new GroupModel
|
||||
{
|
||||
GroupType = symbol,
|
||||
GroupName = name!,
|
||||
DeclOrder = symbol.GetDeclarationOrder(),
|
||||
HasDeclaredSource = hasDeclaredSource,
|
||||
DeclaredSourceCode = declaredSourceCode
|
||||
};
|
||||
}
|
||||
|
||||
private static object? _GetRegisterConfigEventCandidate(GeneratorSyntaxContext ctx)
|
||||
{
|
||||
var propSyntax = (PropertyDeclarationSyntax)ctx.Node;
|
||||
if (ctx.SemanticModel.GetDeclaredSymbol(propSyntax) is not { } symbol) return null;
|
||||
|
||||
// 仅 public static
|
||||
if (symbol.DeclaredAccessibility != Accessibility.Public || !symbol.IsStatic) return null;
|
||||
|
||||
// 精确匹配 [RegisterConfigEvent]
|
||||
var compilation = ctx.SemanticModel.Compilation;
|
||||
var attrDef = compilation.GetTypeByMetadataName("PCL.Core.App.Configuration.RegisterConfigEventAttribute");
|
||||
if (attrDef is null) return null;
|
||||
var hasAttr = symbol.GetAttributes().Any(a =>
|
||||
a.AttributeClass is not null &&
|
||||
SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrDef));
|
||||
if (!hasAttr) return null;
|
||||
|
||||
return new EventRegisterModel
|
||||
{
|
||||
Property = symbol,
|
||||
DeclOrder = symbol.GetDeclarationOrder()
|
||||
};
|
||||
}
|
||||
|
||||
private static object? _GetConfigClass(GeneratorSyntaxContext ctx)
|
||||
{
|
||||
var classSyntax = (ClassDeclarationSyntax)ctx.Node;
|
||||
if (ModelExtensions.GetDeclaredSymbol(ctx.SemanticModel, classSyntax) is not INamedTypeSymbol symbol) return null;
|
||||
|
||||
// 限定为 partial
|
||||
if (!symbol.IsPartial()) return null;
|
||||
|
||||
// 绕过 [ConfigGroup]
|
||||
var compilation = ctx.SemanticModel.Compilation;
|
||||
var attrDef = compilation.GetTypeByMetadataName("PCL.Core.App.Configuration.ConfigGroupAttribute");
|
||||
if (attrDef is not null)
|
||||
{
|
||||
var hasAttr = symbol.GetAttributes().Any(a =>
|
||||
a.AttributeClass is not null &&
|
||||
SymbolEqualityComparer.Default.Equals(a.AttributeClass, attrDef));
|
||||
if (hasAttr) return null;
|
||||
}
|
||||
|
||||
return new ConfigModel
|
||||
{
|
||||
ConfigType = symbol,
|
||||
DeclOrder = symbol.GetDeclarationOrder()
|
||||
};
|
||||
}
|
||||
|
||||
private static ConfigTree _BuildConfigTree(ConfigModel config,
|
||||
ImmutableArray<ItemModel> items,
|
||||
ImmutableArray<GroupModel> groups)
|
||||
{
|
||||
var configType = config.ConfigType;
|
||||
|
||||
// 过滤归属于该 Config 的顶层项与组
|
||||
var topItems = items.Where(i => SymbolEqualityComparer.Default.Equals(i.Property.ContainingType, configType))
|
||||
.OrderBy(i => i.DeclOrder)
|
||||
.ToList();
|
||||
|
||||
var allGroupsForConfig = groups
|
||||
.Where(g => g.GroupType.IsNestedWithin(configType))
|
||||
.OrderBy(g => g.DeclOrder)
|
||||
.ToList();
|
||||
|
||||
// 构建组索引
|
||||
var groupMap = allGroupsForConfig.ToDictionary(g => g.GroupType, g => new GroupNode(g), SymbolEqualityComparer.Default);
|
||||
|
||||
GroupModel? GroupLookup(INamedTypeSymbol type) =>
|
||||
groupMap.TryGetValue(type, out var node) ? node.Model : null;
|
||||
|
||||
string ResolveItemSource(ItemModel item) =>
|
||||
_ResolveItemSourceCode(item, GroupLookup);
|
||||
|
||||
// 组装层级
|
||||
foreach (var node in groupMap.Values)
|
||||
{
|
||||
var parentType = node.Model.GroupType.ContainingType;
|
||||
if (parentType is not null && !SymbolEqualityComparer.Default.Equals(parentType, configType))
|
||||
{
|
||||
if (groupMap.TryGetValue(parentType, out var parentNode))
|
||||
{
|
||||
parentNode.Children.Add(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 顶层组
|
||||
var topGroups = groupMap.Values
|
||||
.Where(n => SymbolEqualityComparer.Default.Equals(n.Model.GroupType.ContainingType, configType))
|
||||
.OrderBy(n => n.Model.DeclOrder)
|
||||
.ToList();
|
||||
|
||||
// 将 Item 分配到各自的组
|
||||
foreach (var item in items.Except(topItems))
|
||||
{
|
||||
var container = item.Property.ContainingType;
|
||||
if (container is null) continue;
|
||||
if (groupMap.TryGetValue(container, out var groupNode))
|
||||
{
|
||||
groupNode.Items.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
return new ConfigTree
|
||||
{
|
||||
Namespace = configType.ContainingNamespace?.ToDisplayString() ?? "",
|
||||
ConfigType = configType,
|
||||
TopItems = topItems,
|
||||
TopGroups = topGroups,
|
||||
ResolveSourceCode = ResolveItemSource
|
||||
};
|
||||
}
|
||||
|
||||
private static string _MakeHintName(ConfigModel config)
|
||||
{
|
||||
var ns = config.ConfigType.ContainingNamespace?.ToDisplayString() ?? "Global";
|
||||
return $"{ns}.{config.ConfigType.Name}.g.cs";
|
||||
}
|
||||
|
||||
private static string _GenerateAdditionalSource(ConfigTree tree)
|
||||
{
|
||||
var sb = new StringBuilder(4096);
|
||||
|
||||
var ns = string.IsNullOrEmpty(tree.Namespace) ? null : tree.Namespace;
|
||||
var configType = tree.ConfigType;
|
||||
var configName = configType.Name;
|
||||
var resolveSource = tree.ResolveSourceCode;
|
||||
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("// 此文件由 Source Generator 自动生成,请勿手动修改");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("using System.Collections.Generic;");
|
||||
sb.AppendLine("using System.Linq;");
|
||||
sb.AppendLine("using PCL.Core.App.Configuration;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine();
|
||||
|
||||
if (!string.IsNullOrEmpty(ns))
|
||||
{
|
||||
sb.Append("namespace ").Append(ns).AppendLine(";");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.Append("partial class ").Append(configName).AppendLine();
|
||||
sb.AppendLine("{");
|
||||
|
||||
// === Config Items ===
|
||||
sb.AppendLine(" // === Config Items ===");
|
||||
sb.AppendLine();
|
||||
if (tree.TopItems.Count == 0)
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var item in tree.TopItems)
|
||||
{
|
||||
_EmitItem(sb, item, indent: 1, isTopLevel: true, resolveSource);
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
// === Config Groups ===
|
||||
sb.AppendLine(" // === Config Groups ===");
|
||||
if (tree.TopGroups.Count > 0)
|
||||
{
|
||||
foreach (var grp in tree.TopGroups)
|
||||
{
|
||||
_EmitGroupInto(sb, grp, indent: 1, isTopLevel: true, resolveSource);
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string _GenerateServiceInitSource(
|
||||
IReadOnlyList<ItemModel> items,
|
||||
IReadOnlyList<EventRegisterModel> events,
|
||||
IReadOnlyDictionary<INamedTypeSymbol, GroupModel> groupLookup)
|
||||
{
|
||||
GroupModel? Lookup(INamedTypeSymbol type) =>
|
||||
groupLookup.TryGetValue(type, out var model) ? model : null;
|
||||
|
||||
string ResolveSource(ItemModel item) =>
|
||||
_ResolveItemSourceCode(item, Lookup);
|
||||
|
||||
var sb = new StringBuilder(1024);
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("// 此文件由 Source Generator 自动生成,请勿手动修改");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("namespace PCL.Core.App.Configuration;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("public sealed partial class ConfigService");
|
||||
sb.AppendLine("{");
|
||||
|
||||
// 配置项初始化
|
||||
sb.AppendLine(" private static void _InitializeConfigItems()");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" (string, ConfigItem)[] items = [");
|
||||
|
||||
HashSet<string> keysAdded = [];
|
||||
for (var i = 0; i < items.Count; i++)
|
||||
{
|
||||
var it = items[i];
|
||||
if (!keysAdded.Add(it.Key)) continue;
|
||||
var keyLiteral = it.Key.ToLiteral();
|
||||
var typeName = it.Type.GetFullyQualifiedName().CorrectConfigTypeName(out _);
|
||||
var sourceCode = ResolveSource(it);
|
||||
sb.Append(" (")
|
||||
.Append(keyLiteral)
|
||||
.Append(", new ConfigItem<").Append(typeName).Append(">(")
|
||||
.Append(keyLiteral).Append(", ")
|
||||
.Append(it.DefaultValueCode).Append(", ").Append(sourceCode)
|
||||
.Append("))");
|
||||
if (i != items.Count - 1) sb.Append(',');
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" ];");
|
||||
sb.AppendLine(" foreach (var (key, value) in items) {");
|
||||
sb.AppendLine(" _KeySet.Add(key);");
|
||||
sb.AppendLine(" _Items[key] = value;");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine();
|
||||
|
||||
// 事件观察器初始化
|
||||
sb.AppendLine(" private static void _InitializeObservers()");
|
||||
sb.AppendLine(" {");
|
||||
sb.AppendLine(" ConfigEventRegistry[] registers = [");
|
||||
|
||||
for (var i = 0; i < events.Count; i++)
|
||||
{
|
||||
var ev = events[i];
|
||||
sb.Append(" ")
|
||||
.Append(ev.Property.GetQualifiedPropertyAccess());
|
||||
if (i != events.Count - 1) sb.Append(',');
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine(" ];");
|
||||
sb.AppendLine(" foreach (var r in registers) foreach (var scope in r.Scopes) {");
|
||||
sb.AppendLine(" RegisterObserver(scope, r.ToObserver());");
|
||||
sb.AppendLine(" }");
|
||||
sb.AppendLine(" }");
|
||||
|
||||
sb.AppendLine("}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string _ResolveItemSourceCode(ItemModel item, Func<INamedTypeSymbol, GroupModel?> groupLookup)
|
||||
{
|
||||
if (item.SourceCode is { } explicitSource)
|
||||
{
|
||||
return explicitSource;
|
||||
}
|
||||
|
||||
var container = item.Property.ContainingType;
|
||||
while (container is not null)
|
||||
{
|
||||
var group = groupLookup(container);
|
||||
if (group is { HasDeclaredSource: true, DeclaredSourceCode: { } declared })
|
||||
{
|
||||
return declared;
|
||||
}
|
||||
container = container.ContainingType;
|
||||
}
|
||||
|
||||
return "ConfigSource.Shared";
|
||||
}
|
||||
|
||||
private static Action<StringBuilder>? _EmitItem(
|
||||
StringBuilder sb,
|
||||
ItemModel item,
|
||||
int indent,
|
||||
bool isTopLevel,
|
||||
Func<ItemModel, string> resolveSource)
|
||||
{
|
||||
Action<StringBuilder>? accessorInitializer = null;
|
||||
var typeName = item.Type.GetFullyQualifiedName().CorrectConfigTypeName(out var fullTypeName);
|
||||
var propName = item.Property.Name;
|
||||
var configItemName = propName + "Config";
|
||||
var staticKeyword = item.IsStatic || isTopLevel ? "static " : string.Empty;
|
||||
var indentStr = new string(' ', indent * 4);
|
||||
var sourceCode = resolveSource(item);
|
||||
|
||||
// 注释
|
||||
sb.Append(indentStr).Append("// Item: ").Append(propName).Append(" [").Append(item.Key).AppendLine("]");
|
||||
|
||||
// 访问器
|
||||
sb.Append(indentStr)
|
||||
.Append("public ").Append(staticKeyword).Append("partial ")
|
||||
.Append(fullTypeName ?? typeName).Append(' ').Append(propName);
|
||||
|
||||
if (fullTypeName is not null)
|
||||
{
|
||||
var accessorName = "ACCESSOR_" + propName;
|
||||
sb.Append(" => ").Append(accessorName).AppendLine(";");
|
||||
// 初始化带参数访问器
|
||||
sb.Append(indentStr)
|
||||
.Append("private ").Append(staticKeyword).Append("readonly ")
|
||||
.Append(fullTypeName).Append(' ');
|
||||
// ReSharper disable once VariableHidesOuterVariable
|
||||
void AccessorInitializer(StringBuilder sb)
|
||||
{
|
||||
sb.Append(accessorName)
|
||||
.Append(" = new((arg) => ")
|
||||
.Append(configItemName)
|
||||
.Append(".GetValue(arg), (arg, value) => ")
|
||||
.Append(configItemName)
|
||||
.AppendLine(".SetValue(value, arg));");
|
||||
}
|
||||
if (item.IsStatic) AccessorInitializer(sb);
|
||||
else {
|
||||
accessorInitializer = AccessorInitializer;
|
||||
sb.Append(accessorName).AppendLine(";");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(" { get => ")
|
||||
.Append(configItemName)
|
||||
.Append(".GetValue(); set => ")
|
||||
.Append(configItemName)
|
||||
.AppendLine(".SetValue(value); }");
|
||||
}
|
||||
|
||||
// 配置项
|
||||
sb.Append(indentStr)
|
||||
.Append("public ").Append(staticKeyword)
|
||||
.Append("ConfigItem<").Append(typeName).Append("> ")
|
||||
.Append(configItemName).Append(" { get => field ??= ConfigService.GetConfigItem<")
|
||||
.Append(typeName)
|
||||
.Append(">(")
|
||||
.Append(item.Key.ToLiteral())
|
||||
.AppendLine("); } = null!;");
|
||||
|
||||
return accessorInitializer;
|
||||
}
|
||||
|
||||
private static void _EmitGroupInto(
|
||||
StringBuilder sb,
|
||||
GroupNode node,
|
||||
int indent,
|
||||
bool isTopLevel,
|
||||
Func<ItemModel, string> resolveSource)
|
||||
{
|
||||
var indentStr = new string(' ', indent * 4);
|
||||
var type = node.Model.GroupType;
|
||||
var typeName = type.Name;
|
||||
var staticKeyword = isTopLevel ? "static " : string.Empty;
|
||||
|
||||
// 组实例字段(在其父作用域中)
|
||||
sb.AppendLine();
|
||||
sb.Append(indentStr).Append("// Group: ").AppendLine(node.Model.GroupName);
|
||||
sb.Append(indentStr).Append("/// <inheritdoc cref=\"").Append(typeName).AppendLine("\" />");
|
||||
sb.Append(indentStr)
|
||||
.Append("public ")
|
||||
.Append(staticKeyword)
|
||||
.Append("readonly ")
|
||||
.Append(typeName)
|
||||
.Append(' ')
|
||||
.Append(node.Model.GroupName)
|
||||
.Append(" = ")
|
||||
.Append(typeName)
|
||||
.AppendLine(".SINGLE_INSTANCE;");
|
||||
|
||||
// 嵌套类型定义
|
||||
sb.Append(indentStr)
|
||||
.Append("public sealed partial class ")
|
||||
.Append(typeName)
|
||||
.AppendLine(" : IConfigScope");
|
||||
sb.Append(indentStr).AppendLine("{");
|
||||
|
||||
// === Config Items ===
|
||||
sb.Append(indentStr).AppendLine(" // === Config Items ===");
|
||||
sb.Append(indentStr).AppendLine();
|
||||
List<Action<StringBuilder>> accessorInitializers = [];
|
||||
foreach (var item in node.Items.OrderBy(i => i.DeclOrder))
|
||||
{
|
||||
var result = _EmitItem(sb, item, indent + 1, isTopLevel: false, resolveSource);
|
||||
if (result is not null) accessorInitializers.Add(result);
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// === Config Groups ===
|
||||
sb.Append(indentStr).AppendLine(" // === Config Groups ===");
|
||||
foreach (var child in node.Children.OrderBy(c => c.Model.DeclOrder))
|
||||
{
|
||||
_EmitGroupInto(sb, child, indent + 1, isTopLevel: false, resolveSource);
|
||||
}
|
||||
|
||||
// === Group Scope Implementation ===
|
||||
sb.AppendLine();
|
||||
sb.Append(indentStr).AppendLine(" // === Group Scope Implementation ===");
|
||||
sb.Append(indentStr).AppendLine();
|
||||
sb.Append(indentStr).AppendLine(" public static readonly " + typeName + " SINGLE_INSTANCE = new();");
|
||||
sb.Append(indentStr).AppendLine(" private readonly IConfigScope[] _InnerScopes;");
|
||||
sb.Append(indentStr).AppendLine(" private " + typeName + "()");
|
||||
sb.Append(indentStr).AppendLine(" {");
|
||||
sb.Append(indentStr).AppendLine(" _InnerScopes = [");
|
||||
|
||||
// InnerScopes: 先项再子组
|
||||
var first = true;
|
||||
foreach (var item in node.Items.SkipWhile(i => i.IsStatic).OrderBy(i => i.DeclOrder))
|
||||
{
|
||||
if (!first) sb.AppendLine(",");
|
||||
sb.Append(indentStr).Append(" ").Append(item.Property.Name).Append("Config");
|
||||
first = false;
|
||||
}
|
||||
foreach (var child in node.Children.OrderBy(c => c.Model.DeclOrder))
|
||||
{
|
||||
if (!first) sb.AppendLine(",");
|
||||
sb.Append(indentStr).Append(" ").Append(child.Model.GroupName);
|
||||
first = false;
|
||||
}
|
||||
if (!first) sb.AppendLine();
|
||||
sb.Append(indentStr).AppendLine(" ];");
|
||||
foreach (var initializer in accessorInitializers)
|
||||
{
|
||||
sb.Append(indentStr).Append(" ");
|
||||
initializer.Invoke(sb);
|
||||
}
|
||||
sb.Append(indentStr).AppendLine(" }");
|
||||
|
||||
// CheckScope
|
||||
sb.Append(indentStr).AppendLine(" public IEnumerable<string> CheckScope(IReadOnlySet<string> keys)");
|
||||
sb.Append(indentStr).AppendLine(" {");
|
||||
sb.Append(indentStr).AppendLine(" IEnumerable<string> result = [];");
|
||||
sb.Append(indentStr).AppendLine(" foreach (var scope in _InnerScopes)");
|
||||
sb.Append(indentStr).AppendLine(" {");
|
||||
sb.Append(indentStr).AppendLine(" var next = scope.CheckScope(keys);");
|
||||
sb.Append(indentStr).AppendLine(" if (next.Any()) result = result.Concat(next);");
|
||||
sb.Append(indentStr).AppendLine(" }");
|
||||
sb.Append(indentStr).AppendLine(" return result;");
|
||||
sb.Append(indentStr).AppendLine(" }");
|
||||
|
||||
// Reset
|
||||
sb.Append(indentStr).AppendLine(" public bool Reset(object? argument = null)");
|
||||
sb.Append(indentStr).AppendLine(" {");
|
||||
sb.Append(indentStr).AppendLine(" var result = true;");
|
||||
sb.Append(indentStr).AppendLine(" foreach (var scope in _InnerScopes)");
|
||||
sb.Append(indentStr).AppendLine(" {");
|
||||
sb.Append(indentStr).AppendLine(" var next = scope.Reset(argument);");
|
||||
sb.Append(indentStr).AppendLine(" if (!next) result = false;");
|
||||
sb.Append(indentStr).AppendLine(" }");
|
||||
sb.Append(indentStr).AppendLine(" return result;");
|
||||
sb.Append(indentStr).AppendLine(" }");
|
||||
|
||||
// IsDefault
|
||||
sb.Append(indentStr).AppendLine(" public bool IsDefault(object? argument = null)");
|
||||
sb.Append(indentStr).AppendLine(" {");
|
||||
sb.Append(indentStr).AppendLine(" var result = true;");
|
||||
sb.Append(indentStr).AppendLine(" foreach (var scope in _InnerScopes)");
|
||||
sb.Append(indentStr).AppendLine(" {");
|
||||
sb.Append(indentStr).AppendLine(" var next = scope.IsDefault(argument);");
|
||||
sb.Append(indentStr).AppendLine(" if (!next) result = false;");
|
||||
sb.Append(indentStr).AppendLine(" }");
|
||||
sb.Append(indentStr).AppendLine(" return result;");
|
||||
sb.Append(indentStr).AppendLine(" }");
|
||||
|
||||
sb.Append(indentStr).AppendLine("}");
|
||||
}
|
||||
|
||||
public static string _RenderSourceCode(SemanticModel sm, ExpressionSyntax expr)
|
||||
{
|
||||
var sym = sm.GetSymbolInfo(expr).Symbol;
|
||||
if (sym is IFieldSymbol fs && fs.ContainingType?.ToDisplayString() == "PCL.Core.App.Configuration.ConfigSource")
|
||||
{
|
||||
return "ConfigSource." + fs.Name;
|
||||
}
|
||||
return expr.ToString();
|
||||
}
|
||||
|
||||
// ===== Models/Trees =====
|
||||
|
||||
private sealed class ItemModel
|
||||
{
|
||||
public IPropertySymbol Property { get; set; } = null!;
|
||||
public string Key { get; set; } = "";
|
||||
public ITypeSymbol Type { get; set; } = null!;
|
||||
public bool IsStatic { get; set; }
|
||||
public int DeclOrder { get; set; }
|
||||
public string DefaultValueCode { get; set; } = "";
|
||||
public string? SourceCode { get; set; }
|
||||
}
|
||||
|
||||
private sealed class GroupModel
|
||||
{
|
||||
public INamedTypeSymbol GroupType { get; set; } = null!;
|
||||
public string GroupName { get; set; } = "";
|
||||
public int DeclOrder { get; set; }
|
||||
public bool HasDeclaredSource { get; set; }
|
||||
public string? DeclaredSourceCode { get; set; }
|
||||
}
|
||||
|
||||
private sealed class GroupNode(GroupModel model)
|
||||
{
|
||||
public GroupModel Model { get; } = model;
|
||||
public List<GroupNode> Children { get; } = [];
|
||||
public List<ItemModel> Items { get; } = [];
|
||||
}
|
||||
|
||||
private sealed class ConfigModel
|
||||
{
|
||||
public INamedTypeSymbol ConfigType { get; set; } = null!;
|
||||
// ReSharper disable once UnusedAutoPropertyAccessor.Local
|
||||
public int DeclOrder { get; set; }
|
||||
}
|
||||
|
||||
private sealed class ConfigTree
|
||||
{
|
||||
public string Namespace { get; set; } = "";
|
||||
public INamedTypeSymbol ConfigType { get; set; } = null!;
|
||||
public List<ItemModel> TopItems { get; set; } = [];
|
||||
public List<GroupNode> TopGroups { get; set; } = [];
|
||||
public Func<ItemModel, string> ResolveSourceCode { get; set; } = static _ => "ConfigSource.Shared";
|
||||
}
|
||||
|
||||
private sealed class EventRegisterModel
|
||||
{
|
||||
public IPropertySymbol Property { get; set; } = null!;
|
||||
public int DeclOrder { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace PCL.Core.SourceGenerators;
|
||||
|
||||
public readonly record struct CollectorInfo(
|
||||
INamedTypeSymbol CollectorAttrSymbol,
|
||||
ITypeSymbol DependencyType,
|
||||
string Identifier,
|
||||
AttributeTargets Targets
|
||||
);
|
||||
|
||||
public readonly record struct DependencyMatchResult(
|
||||
ISymbol Target,
|
||||
AttributeTargets TargetType,
|
||||
AttributeData CollectorAttr,
|
||||
CollectorInfo Info
|
||||
);
|
||||
|
||||
public readonly record struct InjectionPointInfo(
|
||||
IMethodSymbol Target,
|
||||
string Identifier
|
||||
);
|
||||
|
||||
public readonly record struct InjectionPointMatchResult(
|
||||
InjectionPointInfo Info,
|
||||
ImmutableArray<DependencyMatchResult> Dependencies
|
||||
);
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
public sealed class DependencyCollectorGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
const string collectorMarkupAttr = SharedConstants.DependencyCollectorAttribute;
|
||||
const string collectorMarkupAttrFull = $"{collectorMarkupAttr}`1";
|
||||
const string injectionPointAttr = SharedConstants.DependencyInjectionPointAttribute;
|
||||
|
||||
// 收集被标记为 collector 的注解
|
||||
var collectorAttrs = context.SyntaxProvider
|
||||
.ForAttributeWithMetadataName(collectorMarkupAttrFull,
|
||||
predicate: static (node, _) => node is ClassDeclarationSyntax,
|
||||
transform: static (ctx, _) =>
|
||||
{
|
||||
if (ctx.TargetSymbol is not INamedTypeSymbol attr || !attr.IsAttribute()) return default;
|
||||
var infos = new List<CollectorInfo>();
|
||||
foreach (var attrData in ctx.Attributes)
|
||||
{
|
||||
var attrClass = attrData.AttributeClass;
|
||||
if (attrClass is null || attrClass.GetSimplifiedTypeName() != collectorMarkupAttr) continue;
|
||||
// 收集注解信息
|
||||
var dependencyType = attrClass.TypeArguments.FirstOrDefault();
|
||||
if (dependencyType is null) continue;
|
||||
var ctorArgs = attrData.ConstructorArguments;
|
||||
if (ctorArgs.Length < 2
|
||||
|| ctorArgs[0].Value is not string identifier
|
||||
|| ctorArgs[1].Value is not int targets)
|
||||
continue;
|
||||
infos.Add(new CollectorInfo(attr, dependencyType, identifier, (AttributeTargets)targets));
|
||||
}
|
||||
return new KeyValuePair<INamedTypeSymbol, List<CollectorInfo>>(attr, infos);
|
||||
})
|
||||
.Where(x => x.Key is not null)
|
||||
.Collect()
|
||||
// 此处合并到 dictionary 以优化后续查找性能
|
||||
.Select(static (pairs, _) =>
|
||||
{
|
||||
var dict = new Dictionary<INamedTypeSymbol, List<CollectorInfo>>(SymbolEqualityComparer.Default);
|
||||
foreach (var pair in pairs)
|
||||
{
|
||||
if (dict.TryGetValue(pair.Key, out var list)) list.AddRange(pair.Value);
|
||||
else dict[pair.Key] = pair.Value;
|
||||
}
|
||||
return dict.ToImmutableDictionary(SymbolEqualityComparer.Default);
|
||||
});
|
||||
|
||||
// 收集所有带注解的 static member
|
||||
var potentialTargets = context.SyntaxProvider.CreateSyntaxProvider(
|
||||
predicate: static (node, _) =>
|
||||
{
|
||||
// 仅支持 class, property, method
|
||||
if (node is not MemberDeclarationSyntax { AttributeLists.Count: > 0 } member) return false;
|
||||
if (node is ClassDeclarationSyntax) return true;
|
||||
if (node is PropertyDeclarationSyntax or MethodDeclarationSyntax
|
||||
&& member.Modifiers.Any(x => x.IsKind(SyntaxKind.StaticKeyword))) return true;
|
||||
return false;
|
||||
},
|
||||
transform: static (ctx, _) => ctx);
|
||||
|
||||
// 筛选出被 collector 标记的 member
|
||||
var matches = potentialTargets.Combine(collectorAttrs)
|
||||
.SelectMany(static (pair, cancelToken) =>
|
||||
{
|
||||
var (ctx, validAttrs) = pair;
|
||||
// 从 syntax node 获取对应语义 symbol
|
||||
var symbol = ctx.SemanticModel.GetDeclaredSymbol(ctx.Node, cancelToken);
|
||||
if (symbol is null) return [];
|
||||
// 确定目标类型
|
||||
AttributeTargets targetType = default;
|
||||
if (symbol is INamedTypeSymbol) targetType = AttributeTargets.Class;
|
||||
else if (symbol is IPropertySymbol) targetType = AttributeTargets.Property;
|
||||
else if (symbol is IMethodSymbol) targetType = AttributeTargets.Method;
|
||||
// 筛选目标所有符合条件的注解
|
||||
var results = new List<DependencyMatchResult>();
|
||||
foreach (var attrData in symbol.GetAttributes())
|
||||
{
|
||||
var attr = attrData.AttributeClass;
|
||||
if (attr is null) continue;
|
||||
if (!validAttrs.TryGetValue(attr, out var infos)) continue;
|
||||
results.AddRange(
|
||||
from info in infos
|
||||
where info.Targets.HasFlag(targetType)
|
||||
select new DependencyMatchResult(symbol, targetType, attrData, info)
|
||||
);
|
||||
}
|
||||
return results;
|
||||
})
|
||||
.Collect();
|
||||
|
||||
// 收集被标记为注入点的方法
|
||||
var injectionPoints = context.SyntaxProvider
|
||||
.ForAttributeWithMetadataName(injectionPointAttr,
|
||||
predicate: static (node, _) => node is MethodDeclarationSyntax,
|
||||
transform: static (ctx, _) =>
|
||||
{
|
||||
var method = (IMethodSymbol)ctx.TargetSymbol;
|
||||
var attr = ctx.Attributes.First(x => x.AttributeClass?.GetSimplifiedTypeName() == injectionPointAttr);
|
||||
var attrArgs = attr.ConstructorArguments;
|
||||
var identifier = attrArgs[0].Value?.ToString();
|
||||
return identifier is null ? default : new InjectionPointInfo(method, identifier);
|
||||
})
|
||||
.Where(x => x != default);
|
||||
|
||||
// 将注入点与对应标记的依赖项关联
|
||||
var injectionPointMatches = injectionPoints.Combine(matches)
|
||||
.Select((item, _) =>
|
||||
{
|
||||
var point = item.Left;
|
||||
var deps = item.Right
|
||||
.Where(x => x.Info.Identifier == point.Identifier)
|
||||
.ToImmutableArray();
|
||||
return new InjectionPointMatchResult(point, deps);
|
||||
})
|
||||
.Collect();
|
||||
|
||||
// 生成注入实现
|
||||
context.RegisterSourceOutput(injectionPointMatches, _GenerateDependencyInjectionMethods);
|
||||
|
||||
// 保留旧生成模式以供旧组件兼容
|
||||
context.RegisterSourceOutput(matches, _GenerateDependencyGroup);
|
||||
}
|
||||
|
||||
private static void _GenerateDependencyGroup(SourceProductionContext spc, ImmutableArray<DependencyMatchResult> matches)
|
||||
{
|
||||
var dependencyMap = new Dictionary<CollectorInfo, Dictionary<AttributeTargets, List<DependencyMatchResult>>>();
|
||||
foreach (var dep in matches)
|
||||
{
|
||||
var info = dep.Info;
|
||||
if (!dependencyMap.TryGetValue(info, out var map))
|
||||
{
|
||||
map = new Dictionary<AttributeTargets, List<DependencyMatchResult>>
|
||||
{
|
||||
[AttributeTargets.Class] = [],
|
||||
[AttributeTargets.Method] = [],
|
||||
[AttributeTargets.Property] = []
|
||||
};
|
||||
dependencyMap[info] = map;
|
||||
}
|
||||
map[dep.TargetType].Add(dep);
|
||||
}
|
||||
|
||||
var sb = new StringBuilder(1024);
|
||||
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("// 此文件由 Source Generator 自动生成,请勿手动修改");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("using System;");
|
||||
sb.AppendLine("using System.Collections.Generic;");
|
||||
sb.AppendLine("using System.Collections.Immutable;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("namespace PCL.Core.App.IoC;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("public static partial class DependencyGroups");
|
||||
sb.AppendLine("{");
|
||||
|
||||
sb.AppendLine(" private static readonly Dictionary<string, Dictionary<AttributeTargets, DependencyGroup>> _GroupMap = new()");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
foreach (var (info, map) in dependencyMap
|
||||
.Select(x => (x.Key, x.Value)))
|
||||
{
|
||||
sb.Append(" [").Append(info.Identifier.ToLiteral()).AppendLine("] = new()");
|
||||
sb.AppendLine(" {");
|
||||
string? typeStr = null;
|
||||
string? argTypeList = null;
|
||||
foreach (var (target, deps) in map
|
||||
.Where(x => x.Value.Count > 0)
|
||||
.Select(x => (x.Key, x.Value)))
|
||||
{
|
||||
sb.Append(" [AttributeTargets.").Append(target).Append("] = new DependencyGroup<");
|
||||
typeStr ??= info.DependencyType.GetFullyQualifiedName();
|
||||
switch (target)
|
||||
{
|
||||
case AttributeTargets.Class:
|
||||
sb.Append("Action<").Append(typeStr).Append(">");
|
||||
break;
|
||||
case AttributeTargets.Method:
|
||||
sb.Append(typeStr);
|
||||
break;
|
||||
case AttributeTargets.Property:
|
||||
sb.Append("PropertyAccessor<").Append(typeStr).Append(">");
|
||||
break;
|
||||
}
|
||||
argTypeList ??= ((Func<string>)(() =>
|
||||
{
|
||||
var ctor = info.CollectorAttrSymbol.InstanceConstructors.FirstOrDefault();
|
||||
if (ctor is null) return string.Empty;
|
||||
var args = ctor.Parameters.Select(para => para.Type.GetFullyQualifiedName()).ToList();
|
||||
var cnt = args.Count;
|
||||
if (cnt == 0) return string.Empty;
|
||||
if (cnt == 1) return args[0];
|
||||
return "(" + string.Join(", ", args) + ")";
|
||||
}))();
|
||||
if (argTypeList != string.Empty) sb.Append(", ").Append(argTypeList);
|
||||
sb.AppendLine("> { Items = [");
|
||||
foreach (var dep in deps)
|
||||
{
|
||||
sb.Append(" (");
|
||||
var depRef = dep.Target.GetQualifiedSymbolName();
|
||||
switch (target)
|
||||
{
|
||||
case AttributeTargets.Class:
|
||||
sb.Append("static () => new ")
|
||||
.Append(depRef).Append("()");
|
||||
break;
|
||||
case AttributeTargets.Method:
|
||||
sb.Append(depRef);
|
||||
break;
|
||||
case AttributeTargets.Property:
|
||||
sb.Append("new(getter: ");
|
||||
var prop = (IPropertySymbol)dep.Target;
|
||||
if (prop.IsWriteOnly) sb.Append("null");
|
||||
else sb.Append("static () => ").Append(depRef);
|
||||
sb.Append(", setter: ");
|
||||
if (prop.IsReadOnly) sb.Append("null");
|
||||
else sb.Append("static value => ").Append(depRef).Append(" = value");
|
||||
sb.Append(")");
|
||||
break;
|
||||
}
|
||||
if (argTypeList != string.Empty)
|
||||
{
|
||||
sb.Append(", ");
|
||||
var args = dep.CollectorAttr.ConstructorArguments.Select(arg => arg.ToCSharpString()).ToList();
|
||||
if (args.Count == 1) sb.Append(args[0]);
|
||||
else sb.Append("(").Append(string.Join(", ", args)).Append(")");
|
||||
}
|
||||
sb.AppendLine("),");
|
||||
}
|
||||
sb.AppendLine(" ] },");
|
||||
}
|
||||
sb.AppendLine(" },");
|
||||
}
|
||||
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine("}");
|
||||
|
||||
spc.AddSource("DependencyGroups.g.cs", sb.ToString());
|
||||
}
|
||||
|
||||
private static void _GenerateDependencyInjectionMethods(SourceProductionContext spc, ImmutableArray<InjectionPointMatchResult> matches)
|
||||
{
|
||||
foreach (var match in matches)
|
||||
{
|
||||
var sb = new StringBuilder(1024);
|
||||
|
||||
// file header
|
||||
sb.AppendLine("// 此文件由 Source Generator 自动生成,请勿手动修改");
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("using System;");
|
||||
sb.AppendLine("using System.Threading.Tasks;");
|
||||
sb.AppendLine($"using {SharedConstants.IocNamespace};");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine();
|
||||
|
||||
// type header
|
||||
var targetMethod = match.Info.Target;
|
||||
var indent = targetMethod.ContainingType.GenerateTypeHeader(sb);
|
||||
|
||||
// method
|
||||
var indentStr = new string(' ', indent * 4);
|
||||
var targetMethodName = targetMethod.Name;
|
||||
var isStatic = targetMethod.IsStatic;
|
||||
var isAwaitable = targetMethod.IsAwaitable();
|
||||
sb.Append(indentStr).AppendLine("[global::System.CodeDom.Compiler.GeneratedCode(\"PCL.Core.SourceGenerators.DependencyCollectorGenerator\", \"1.0.0.0\")]");
|
||||
sb.Append(indentStr).AppendLine("[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]");
|
||||
var idCode = match.Info.Identifier.SnakeIdToPascal();
|
||||
sb.Append(indentStr).Append("private ");
|
||||
if (isStatic) sb.Append("static ");
|
||||
sb.Append(isAwaitable ? "async Task " : "void ");
|
||||
sb.Append(targetMethodName).Append("_InvokeInjection_").Append(idCode).AppendLine("()");
|
||||
sb.Append(indentStr).AppendLine("{");
|
||||
foreach (var dep in match.Dependencies)
|
||||
{
|
||||
sb.Append(indentStr).Append(" ");
|
||||
if (isAwaitable) sb.Append("await ");
|
||||
sb.Append(targetMethodName).Append("(");
|
||||
var depRef = dep.Target.GetQualifiedSymbolName();
|
||||
switch (dep.TargetType)
|
||||
{
|
||||
case AttributeTargets.Class:
|
||||
sb.Append("static () => new ").Append(depRef).Append("()");
|
||||
break;
|
||||
case AttributeTargets.Method:
|
||||
sb.Append(depRef);
|
||||
break;
|
||||
case AttributeTargets.Property:
|
||||
sb.Append("new PropertyAccessor(getter: ");
|
||||
var prop = (IPropertySymbol)dep.Target;
|
||||
if (prop.IsWriteOnly) sb.Append("null");
|
||||
else sb.Append("static () => ").Append(depRef);
|
||||
sb.Append(", setter: ");
|
||||
if (prop.IsReadOnly) sb.Append("null");
|
||||
else sb.Append("static value => ").Append(depRef).Append(" = value");
|
||||
sb.Append(")");
|
||||
break;
|
||||
}
|
||||
foreach (var arg in dep.CollectorAttr.ConstructorArguments)
|
||||
sb.Append(", ").Append(arg.ToCSharpString());
|
||||
sb.AppendLine(");");
|
||||
}
|
||||
sb.Append(indentStr).AppendLine("}");
|
||||
|
||||
// type footer
|
||||
while (indent-- > 0) sb.Append(' ', indent * 4).AppendLine("}");
|
||||
|
||||
// register source code
|
||||
spc.AddSource($"{targetMethod.GetQualifiedSymbolName()}.g.cs", sb.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
|
||||
namespace PCL.Core.SourceGenerators;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
public class EnvironmentInteropGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
var secretProvider = context.CompilationProvider.Select(static (_, _) =>
|
||||
{
|
||||
// 判断 PCL_WRITE_SECRET 是否存在并遍历转换环境变量
|
||||
// 打死也不会用 MSBuild Properties 这种非人类设计出来的垃圾
|
||||
#pragma warning disable RS1035
|
||||
var envs = Environment.GetEnvironmentVariables();
|
||||
#pragma warning restore RS1035
|
||||
var secretPairs = envs.Contains("PCL_WRITE_SECRET") ? (
|
||||
from key in (
|
||||
from key in envs.Keys.Cast<string>()
|
||||
where !string.IsNullOrWhiteSpace(key) && key.StartsWith("PCL_") && key != "PCL_WRITE_SECRET"
|
||||
select key
|
||||
)
|
||||
let value = envs[key]?.ToString()
|
||||
where !string.IsNullOrWhiteSpace(value)
|
||||
select (key.Substring(4), value)
|
||||
) : [];
|
||||
return secretPairs;
|
||||
});
|
||||
|
||||
// 注册源代码输出
|
||||
context.RegisterSourceOutput(secretProvider, static (spc, secretPairs) => _Execute(spc, secretPairs));
|
||||
}
|
||||
|
||||
private static void _Execute(SourceProductionContext context, IEnumerable<(string, string)> secretPairs)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("// 此文件由 Source Generator 自动生成,请勿手动修改");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("#nullable enable");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("namespace PCL.Core.Utils.OS;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("partial class EnvironmentInterop");
|
||||
sb.AppendLine("{");
|
||||
sb.AppendLine(" private static readonly System.Collections.Generic.Dictionary<string, string?> SecretDictionary = new()");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
foreach (var (key, value) in secretPairs)
|
||||
sb.AppendLine($" [\"{key}\"] = {_ToVerbatimString(value)},");
|
||||
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine("}");
|
||||
|
||||
context.AddSource("EnvironmentInterop.g.cs", SourceText.From(sb.ToString(), Encoding.UTF8));
|
||||
}
|
||||
|
||||
private static string _ToVerbatimString(string text)
|
||||
{
|
||||
return "@\"" + text.Replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
}
|
||||
@@ -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)});";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace PCL.Core.SourceGenerators;
|
||||
|
||||
[Generator(LanguageNames.CSharp)]
|
||||
public class LifecycleServiceTypesGenerator : IIncrementalGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
// 查找 LifecycleState.cs 文件以获取有效的枚举值
|
||||
var lifecycleStateProvider = context.AdditionalTextsProvider
|
||||
.Where(static file => file.Path.EndsWith("LifecycleState.cs"))
|
||||
.Select(static (text, cancellationToken) =>
|
||||
{
|
||||
var content = text.GetText(cancellationToken)?.ToString();
|
||||
return _GetValidLifecycleStates(content);
|
||||
})
|
||||
.Where(static states => states?.Count > 0)
|
||||
.Collect();
|
||||
|
||||
// 查找带有 LifecycleService 属性的类
|
||||
var serviceClassProvider = context.SyntaxProvider.CreateSyntaxProvider(
|
||||
predicate: static (s, _) => s is ClassDeclarationSyntax { AttributeLists.Count: > 0 },
|
||||
transform: static (ctx, _) => _GetLifecycleServiceInfo(ctx))
|
||||
.Where(static x => x is not null);
|
||||
|
||||
// 收集所有服务信息
|
||||
var servicesProvider = serviceClassProvider.Collect();
|
||||
|
||||
// 合并枚举状态和服务信息
|
||||
var combinedProvider = lifecycleStateProvider.Combine(servicesProvider);
|
||||
|
||||
// 生成代码
|
||||
context.RegisterSourceOutput(combinedProvider,
|
||||
static (spc, data) => _Execute(spc, data.Left.FirstOrDefault() ?? new List<string>(), [..data.Right.Where(x => x is not null).Select(x => x!)]));
|
||||
}
|
||||
|
||||
private static List<string>? _GetValidLifecycleStates(string? content)
|
||||
{
|
||||
if (string.IsNullOrEmpty(content))
|
||||
return null;
|
||||
|
||||
var validStates = new List<string>();
|
||||
|
||||
// 提取枚举定义块
|
||||
var enumPattern = @"(?s)public\s+enum\s+LifecycleState\s*\{(.*?)\}";
|
||||
var enumMatch = Regex.Match(content, enumPattern);
|
||||
|
||||
if (!enumMatch.Success)
|
||||
return null;
|
||||
|
||||
var enumContent = enumMatch.Groups[1].Value;
|
||||
|
||||
// 按行分割并处理每一行
|
||||
var lines = enumContent.Split('\n');
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var trimmedLine = line.Trim();
|
||||
|
||||
// 跳过空行、注释行和花括号
|
||||
if (string.IsNullOrEmpty(trimmedLine) ||
|
||||
trimmedLine.StartsWith("///") ||
|
||||
trimmedLine.StartsWith("//") ||
|
||||
trimmedLine.StartsWith("/*") ||
|
||||
trimmedLine.StartsWith("*") ||
|
||||
trimmedLine == "{" ||
|
||||
trimmedLine == "}")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// 匹配枚举成员(可能包含逗号)
|
||||
var memberMatch = Regex.Match(trimmedLine, @"^(\w+)\s*,?\s*$");
|
||||
if (memberMatch.Success)
|
||||
{
|
||||
var enumValue = memberMatch.Groups[1].Value;
|
||||
if (!string.IsNullOrEmpty(enumValue) && enumValue != "LifecycleState")
|
||||
{
|
||||
validStates.Add(enumValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return validStates.Count > 0 ? validStates : null;
|
||||
}
|
||||
|
||||
private static LifecycleServiceInfo? _GetLifecycleServiceInfo(GeneratorSyntaxContext context)
|
||||
{
|
||||
var classDeclaration = (ClassDeclarationSyntax)context.Node;
|
||||
|
||||
// 查找 LifecycleService 属性
|
||||
var lifecycleAttribute = classDeclaration.AttributeLists
|
||||
.SelectMany(al => al.Attributes)
|
||||
.FirstOrDefault(a => a.Name.ToString().Contains("LifecycleService"));
|
||||
|
||||
if (lifecycleAttribute is null)
|
||||
return null;
|
||||
|
||||
// 获取语义模型信息
|
||||
var semanticModel = context.SemanticModel;
|
||||
var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration);
|
||||
if (classSymbol is null)
|
||||
return null;
|
||||
|
||||
// 解析属性参数
|
||||
var state = "Unknown";
|
||||
var priority = 0;
|
||||
|
||||
if (lifecycleAttribute.ArgumentList?.Arguments.Count > 0)
|
||||
{
|
||||
// 解析第一个参数(状态)
|
||||
var firstArg = lifecycleAttribute.ArgumentList.Arguments[0];
|
||||
if (firstArg.Expression is MemberAccessExpressionSyntax memberAccess)
|
||||
{
|
||||
state = memberAccess.Name.Identifier.ValueText;
|
||||
}
|
||||
|
||||
// 查找 Priority 参数(支持命名参数和位置参数)
|
||||
var priorityArg = lifecycleAttribute.ArgumentList.Arguments
|
||||
.FirstOrDefault(arg => arg.NameEquals?.Name.Identifier.ValueText == "Priority");
|
||||
|
||||
// 如果没有找到命名的Priority参数,检查第二个位置参数
|
||||
if (priorityArg is null && lifecycleAttribute.ArgumentList.Arguments.Count > 1)
|
||||
{
|
||||
priorityArg = lifecycleAttribute.ArgumentList.Arguments[1];
|
||||
}
|
||||
|
||||
if (priorityArg is not null)
|
||||
{
|
||||
priority = _ParsePriorityExpression(priorityArg.Expression, semanticModel);
|
||||
}
|
||||
}
|
||||
|
||||
return new LifecycleServiceInfo(
|
||||
classSymbol.ToDisplayString(),
|
||||
classSymbol.Name,
|
||||
state,
|
||||
priority);
|
||||
}
|
||||
|
||||
private static int _ParsePriorityExpression(ExpressionSyntax expression, SemanticModel semanticModel)
|
||||
{
|
||||
switch (expression)
|
||||
{
|
||||
case LiteralExpressionSyntax literal:
|
||||
if (int.TryParse(literal.Token.ValueText, out var literalValue))
|
||||
return literalValue;
|
||||
break;
|
||||
|
||||
case MemberAccessExpressionSyntax memberAccess:
|
||||
var memberName = memberAccess.ToString();
|
||||
if (memberName == "int.MaxValue")
|
||||
return int.MaxValue;
|
||||
if (memberName == "int.MinValue")
|
||||
return int.MinValue;
|
||||
break;
|
||||
|
||||
case PrefixUnaryExpressionSyntax unary when unary.IsKind(SyntaxKind.UnaryMinusExpression):
|
||||
// 处理负数
|
||||
if (unary.Operand is LiteralExpressionSyntax negLiteral &&
|
||||
int.TryParse(negLiteral.Token.ValueText, out var negValue))
|
||||
{
|
||||
return -negValue;
|
||||
}
|
||||
break;
|
||||
|
||||
case BinaryExpressionSyntax binary:
|
||||
// 简单的数学表达式支持
|
||||
var left = _ParsePriorityExpression(binary.Left, semanticModel);
|
||||
var right = _ParsePriorityExpression(binary.Right, semanticModel);
|
||||
|
||||
return binary.OperatorToken.Kind() switch
|
||||
{
|
||||
SyntaxKind.PlusToken => left + right,
|
||||
SyntaxKind.MinusToken => left - right,
|
||||
SyntaxKind.AsteriskToken => left * right,
|
||||
SyntaxKind.SlashToken => right != 0 ? left / right : 0,
|
||||
_ => 0
|
||||
};
|
||||
}
|
||||
|
||||
// 尝试获取常量值
|
||||
var constantValue = semanticModel.GetConstantValue(expression);
|
||||
return constantValue is { HasValue: true, Value: int intValue } ? intValue : 0;
|
||||
}
|
||||
|
||||
private static void _Execute(SourceProductionContext context, List<string> validStates, ImmutableArray<LifecycleServiceInfo> services)
|
||||
{
|
||||
// 过滤服务,只保留有效状态的服务
|
||||
var filteredServices = services.Where(s => validStates.Count == 0 || validStates.Contains(s.State)).ToList();
|
||||
|
||||
// 按状态分组并排序
|
||||
var groupedServices = filteredServices
|
||||
.GroupBy(s => s.State)
|
||||
.OrderBy(g => g.Key)
|
||||
.ToList();
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("// <auto-generated />");
|
||||
sb.AppendLine("// 此文件由 Source Generator 自动生成,请勿手动修改");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("using System;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("namespace PCL.Core.App.IoC;");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("/// <summary>");
|
||||
sb.AppendLine("/// 包含所有使用 LifecycleService 注解的类型,按 StartState 分类并按 Priority 降序排序");
|
||||
sb.AppendLine("/// </summary>");
|
||||
sb.AppendLine("public static class LifecycleServiceTypes");
|
||||
sb.AppendLine("{");
|
||||
|
||||
// 为每个状态生成数组
|
||||
foreach (var group in groupedServices)
|
||||
{
|
||||
var sortedServices = group.OrderByDescending(s => s.Priority).ToList();
|
||||
|
||||
sb.AppendLine($" /// <summary>");
|
||||
sb.AppendLine($" /// {group.Key} 状态的生命周期服务类型");
|
||||
sb.AppendLine($" /// </summary>");
|
||||
sb.AppendLine($" public static readonly Type[] {group.Key} = [");
|
||||
|
||||
foreach (var service in sortedServices)
|
||||
{
|
||||
sb.AppendLine($" typeof({service.FullName}), // Priority: {service.Priority}");
|
||||
}
|
||||
|
||||
sb.AppendLine(" ];");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// 生成 GetServiceTypes 方法
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// 获取指定生命周期状态的所有服务类型");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" /// <param name=\"state\">生命周期状态</param>");
|
||||
sb.AppendLine(" /// <returns>该状态下的所有服务类型数组</returns>");
|
||||
sb.AppendLine(" public static Type[] GetServiceTypes(LifecycleState state) => state switch");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
foreach (var group in groupedServices)
|
||||
{
|
||||
sb.AppendLine($" LifecycleState.{group.Key} => {group.Key},");
|
||||
}
|
||||
|
||||
sb.AppendLine(" _ => new Type[0]");
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine();
|
||||
|
||||
// 生成 GetAllServiceTypes 方法
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// 获取所有生命周期服务类型的状态映射");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" /// <returns>状态到类型数组的字典</returns>");
|
||||
sb.AppendLine(" public static System.Collections.Generic.Dictionary<LifecycleState, Type[]> GetAllServiceTypes() => new()");
|
||||
sb.AppendLine(" {");
|
||||
|
||||
foreach (var group in groupedServices)
|
||||
{
|
||||
sb.AppendLine($" [LifecycleState.{group.Key}] = {group.Key},");
|
||||
}
|
||||
|
||||
sb.AppendLine(" };");
|
||||
sb.AppendLine();
|
||||
|
||||
// 生成统计信息方法
|
||||
sb.AppendLine(" /// <summary>");
|
||||
sb.AppendLine(" /// 获取生命周期服务的统计信息");
|
||||
sb.AppendLine(" /// </summary>");
|
||||
sb.AppendLine(" /// <returns>包含状态数量和总服务数量的统计信息</returns>");
|
||||
sb.AppendLine($" public static (int StateCount, int TotalServices) GetStatistics() => ({groupedServices.Count}, {filteredServices.Count});");
|
||||
|
||||
sb.AppendLine("}");
|
||||
|
||||
context.AddSource("LifecycleServiceTypes.g.cs", sb.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
// 破烂 .NET Standard 用不了 init 修饰没法 record,先这样吧
|
||||
public class LifecycleServiceInfo(string fullName, string className, string state, int priority)
|
||||
{
|
||||
public string FullName { get; } = fullName;
|
||||
public string ClassName { get; } = className;
|
||||
public string State { get; } = state;
|
||||
public int Priority { get; } = priority;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
<Platforms>AnyCPU;x64;ARM64</Platforms>
|
||||
<Configurations>Debug;CI;Release;Beta</Configurations>
|
||||
<!-- 确保源代码生成器在所有平台架构下都能正常工作 -->
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<Prefer32Bit>false</Prefer32Bit>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.6.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="5.6.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace PCL.Core.SourceGenerators;
|
||||
|
||||
public static class SharedConstants
|
||||
{
|
||||
public const string AppNamespace = "PCL.Core.App";
|
||||
public const string IocNamespace = $"{AppNamespace}.IoC";
|
||||
public const string DependencyCollectorAttribute = $"{IocNamespace}.DependencyCollectorAttribute";
|
||||
public const string DependencyInjectionPointAttribute = $"{IocNamespace}.DependencyInjectionPointAttribute";
|
||||
public const string LifecycleScopeAttribute = $"{IocNamespace}.LifecycleScopeAttribute";
|
||||
public const string LifecycleStartAttribute = $"{IocNamespace}.LifecycleStartAttribute";
|
||||
public const string LifecycleStopAttribute = $"{IocNamespace}.LifecycleStopAttribute";
|
||||
public const string LifecycleCommandHandlerAttribute = $"{IocNamespace}.LifecycleCommandHandlerAttribute";
|
||||
public const string LifecycleDependencyInjectionAttribute = $"{IocNamespace}.LifecycleDependencyInjectionAttribute";
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace PCL.Core.SourceGenerators;
|
||||
|
||||
public static class SharedExtensions
|
||||
{
|
||||
public static string ToLiteral(this string str) => SymbolDisplay.FormatLiteral(str, true);
|
||||
|
||||
public static string? ToPrimitive(this object? obj) => SymbolDisplay.FormatPrimitive(obj, true, false);
|
||||
|
||||
public static int GetDeclarationOrder(this ISymbol symbol)
|
||||
{
|
||||
var loc = symbol.DeclaringSyntaxReferences.FirstOrDefault()?.GetSyntax().GetLocation();
|
||||
return loc?.SourceSpan.Start ?? int.MaxValue;
|
||||
}
|
||||
|
||||
extension(INamedTypeSymbol type)
|
||||
{
|
||||
public bool IsPartial()
|
||||
{
|
||||
foreach (var decl in type.DeclaringSyntaxReferences)
|
||||
{
|
||||
if (decl.GetSyntax() is ClassDeclarationSyntax { Modifiers: { } modifiers } &&
|
||||
modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsNestedWithin(INamedTypeSymbol potentialContainer)
|
||||
{
|
||||
var t = type.ContainingType;
|
||||
while (t is not null)
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(t, potentialContainer))
|
||||
return true;
|
||||
t = t.ContainingType;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool IsAttribute()
|
||||
{
|
||||
var baseType = type.BaseType;
|
||||
while (baseType is not null)
|
||||
{
|
||||
if (baseType.ToDisplayString() == "System.Attribute") return true;
|
||||
baseType = baseType.BaseType;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public int GenerateTypeHeader(StringBuilder sb)
|
||||
{
|
||||
var ctnTypes = new Stack<INamedTypeSymbol>();
|
||||
for (var ctnType = type.ContainingType; ctnType is not null; ctnType = ctnType.ContainingType) ctnTypes.Push(ctnType);
|
||||
// namespace
|
||||
var ns = type.ContainingNamespace?.ToDisplayString();
|
||||
var indent = 0;
|
||||
if (!string.IsNullOrEmpty(ns))
|
||||
{
|
||||
sb.Append("namespace ").Append(ns).AppendLine();
|
||||
sb.AppendLine("{");
|
||||
indent++;
|
||||
}
|
||||
// outer classes
|
||||
foreach (var containingType in ctnTypes)
|
||||
{
|
||||
sb.Append(' ', indent * 4).Append("partial class ").Append(containingType.Name).AppendLine();
|
||||
sb.Append(' ', indent * 4).AppendLine("{");
|
||||
indent++;
|
||||
}
|
||||
// class
|
||||
sb.Append(' ', indent * 4).Append("partial class ").Append(type.Name).AppendLine();
|
||||
sb.Append(' ', indent * 4).AppendLine("{");
|
||||
return indent + 1;
|
||||
}
|
||||
}
|
||||
|
||||
public static string RenderDefaultValueCode(this SemanticModel sm, ExpressionSyntax expr)
|
||||
{
|
||||
if (expr is LiteralExpressionSyntax || expr.IsNegativeNumeric())
|
||||
return expr.ToString();
|
||||
|
||||
if (expr is TypeOfExpressionSyntax toe)
|
||||
{
|
||||
var type = sm.GetTypeInfo(toe.Type).Type;
|
||||
if (type is not null)
|
||||
return "typeof(" + type.GetFullyQualifiedName() + ")";
|
||||
return expr.ToString();
|
||||
}
|
||||
|
||||
if (expr is InvocationExpressionSyntax
|
||||
{
|
||||
Expression: IdentifierNameSyntax { Identifier.ValueText: "nameof" },
|
||||
ArgumentList.Arguments.Count: 1
|
||||
} inv)
|
||||
{
|
||||
var targetExpr = inv.ArgumentList.Arguments[0].Expression;
|
||||
var sym = sm.GetSymbolInfo(targetExpr).Symbol;
|
||||
if (sym is not null)
|
||||
{
|
||||
return "nameof(" + sym.GetQualifiedSymbolName() + ")";
|
||||
}
|
||||
return expr.ToString();
|
||||
}
|
||||
|
||||
var s = sm.GetSymbolInfo(expr).Symbol;
|
||||
if (s is IFieldSymbol fs)
|
||||
{
|
||||
return fs.GetQualifiedSymbolName();
|
||||
}
|
||||
|
||||
return expr.ToString();
|
||||
}
|
||||
|
||||
public static bool IsNegativeNumeric(this ExpressionSyntax expr)
|
||||
{
|
||||
return expr is PrefixUnaryExpressionSyntax p
|
||||
&& p.IsKind(SyntaxKind.UnaryMinusExpression)
|
||||
&& p.Operand is LiteralExpressionSyntax l
|
||||
&& l.IsKind(SyntaxKind.NumericLiteralExpression);
|
||||
}
|
||||
|
||||
extension(ISymbol symbol)
|
||||
{
|
||||
public string GetQualifiedSymbolName()
|
||||
{
|
||||
if (symbol is ITypeSymbol ts) return ts.GetFullyQualifiedName();
|
||||
|
||||
var parts = new Stack<string>();
|
||||
parts.Push(symbol.Name);
|
||||
var t = symbol.ContainingType;
|
||||
while (t is not null)
|
||||
{
|
||||
parts.Push(t.Name);
|
||||
t = t.ContainingType;
|
||||
}
|
||||
var ns = symbol.ContainingNamespace?.ToDisplayString();
|
||||
if (!string.IsNullOrEmpty(ns)) parts.Push(ns!);
|
||||
return string.Join(".", parts);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly SymbolDisplayFormat _SimplifiedTypeNameFormat = new(
|
||||
globalNamespaceStyle: SymbolDisplayGlobalNamespaceStyle.OmittedAsContaining,
|
||||
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
|
||||
miscellaneousOptions:
|
||||
SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers |
|
||||
SymbolDisplayMiscellaneousOptions.CollapseTupleTypes |
|
||||
SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier |
|
||||
SymbolDisplayMiscellaneousOptions.UseSpecialTypes,
|
||||
genericsOptions: SymbolDisplayGenericsOptions.None
|
||||
);
|
||||
|
||||
private static readonly SymbolDisplayFormat _FullQualifiedNameFormat = new(
|
||||
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypesAndNamespaces,
|
||||
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
|
||||
miscellaneousOptions:
|
||||
SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers |
|
||||
SymbolDisplayMiscellaneousOptions.UseSpecialTypes
|
||||
);
|
||||
|
||||
extension(ITypeSymbol type)
|
||||
{
|
||||
public string GetSimplifiedTypeName()
|
||||
{
|
||||
return type.ToDisplayString(_SimplifiedTypeNameFormat);
|
||||
}
|
||||
|
||||
public string GetFullyQualifiedName()
|
||||
{
|
||||
if (type is INamedTypeSymbol {
|
||||
OriginalDefinition.SpecialType: SpecialType.System_Nullable_T,
|
||||
TypeArguments.Length: 1 } nt)
|
||||
{
|
||||
var inner = nt.TypeArguments[0];
|
||||
return inner.GetFullyQualifiedName() + "?";
|
||||
}
|
||||
if (type.TryGetSpecialTypeKeyword(out var keyword)) return keyword;
|
||||
return type.ToDisplayString(_FullQualifiedNameFormat);
|
||||
}
|
||||
|
||||
public bool TryGetSpecialTypeKeyword(out string keyword)
|
||||
{
|
||||
switch (type.SpecialType)
|
||||
{
|
||||
case SpecialType.System_Boolean: keyword = "bool"; return true;
|
||||
case SpecialType.System_Byte: keyword = "byte"; return true;
|
||||
case SpecialType.System_SByte: keyword = "sbyte"; return true;
|
||||
case SpecialType.System_Int16: keyword = "short"; return true;
|
||||
case SpecialType.System_UInt16: keyword = "ushort"; return true;
|
||||
case SpecialType.System_Int32: keyword = "int"; return true;
|
||||
case SpecialType.System_UInt32: keyword = "uint"; return true;
|
||||
case SpecialType.System_Int64: keyword = "long"; return true;
|
||||
case SpecialType.System_UInt64: keyword = "ulong"; return true;
|
||||
case SpecialType.System_IntPtr: keyword = "nint"; return true;
|
||||
case SpecialType.System_UIntPtr: keyword = "nuint"; return true;
|
||||
case SpecialType.System_Char: keyword = "char"; return true;
|
||||
case SpecialType.System_String: keyword = "string"; return true;
|
||||
case SpecialType.System_Object: keyword = "object"; return true;
|
||||
case SpecialType.System_Single: keyword = "float"; return true;
|
||||
case SpecialType.System_Double: keyword = "double"; return true;
|
||||
case SpecialType.System_Decimal: keyword = "decimal"; return true;
|
||||
default: keyword = ""; return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static string GetQualifiedPropertyAccess(this IPropertySymbol prop)
|
||||
{
|
||||
var owner = prop.ContainingType.GetFullyQualifiedName();
|
||||
return owner + "." + prop.Name;
|
||||
}
|
||||
|
||||
public static bool IsAwaitable(this IMethodSymbol method)
|
||||
{
|
||||
// TODO this is a very naive implementation.
|
||||
return method.ReturnType.GetSimplifiedTypeName() == "System.Threading.Tasks.Task";
|
||||
}
|
||||
|
||||
public static string CorrectConfigTypeName(this string typeName, out string? fullTypeName)
|
||||
{
|
||||
var isArgConfig = typeName.StartsWith("PCL.Core.App.Configuration.ArgConfig<");
|
||||
if (isArgConfig)
|
||||
{
|
||||
fullTypeName = typeName;
|
||||
typeName = typeName.Substring(37, typeName.Length - 38);
|
||||
}
|
||||
else fullTypeName = null;
|
||||
return typeName;
|
||||
}
|
||||
|
||||
extension(string str)
|
||||
{
|
||||
public string SnakeIdToPascal()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var part in str.Split('-'))
|
||||
{
|
||||
if (part.Length == 0) continue;
|
||||
sb.Append(char.ToUpper(part[0])).Append(part.Substring(1));
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace System.Runtime.CompilerServices;
|
||||
|
||||
using ComponentModel;
|
||||
|
||||
/// <summary>
|
||||
/// Reserved to be used by the compiler for tracking metadata.
|
||||
/// This class should not be used by developers in source code.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
internal static class IsExternalInit;
|
||||
Reference in New Issue
Block a user