747 lines
28 KiB
C#
747 lines
28 KiB
C#
using System;
|
|||
|
|
using System.Collections.Generic;
|
||
|
|
using System.Collections.Immutable;
|
||
|
|
using System.Linq;
|
||
|
|
using System.Text;
|
||
|
|
using Microsoft.CodeAnalysis;
|
||
|
|
using Microsoft.CodeAnalysis.CSharp;
|
||
|
|
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; }
|
||
|
|
}
|
||
|
|
}
|