GeWuYou 81897ce2ac docs(source-generators): 添加源码生成器文档
- 新增 AutoRegisterExportedCollections 生成器文档
- 新增 AutoScene 生成器文档
- 新增 AutoUiPage 生成器文档
- 新增完整的源码生成器索引文档
- 详细介绍各生成器的使用方法和参数说明
- 提供生成代码示例和诊断信息说明
- 包含性能优势和使用示例章节
2026-04-15 17:00:49 +08:00

341 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using GFramework.Godot.SourceGenerators.Diagnostics;
using GFramework.SourceGenerators.Common.Constants;
using GFramework.SourceGenerators.Common.Diagnostics;
using GFramework.SourceGenerators.Common.Extensions;
namespace GFramework.Godot.SourceGenerators.Behavior;
/// <summary>
/// 为标记了 <c>[AutoScene]</c> 的 Godot 节点生成场景行为样板。
/// </summary>
/// <remarks>
/// 该生成器会为兼容的非嵌套 <c>partial</c> Godot 节点类型生成 <c>SceneKeyStr</c> 与 <c>GetScene</c>
/// 以便通过 <c>SceneBehaviorFactory</c> 延迟创建并缓存场景行为实例。
/// 生成管线仅处理显式标记了 <c>AutoSceneAttribute</c> 的类,并在类型不满足基类、<c>partial</c>、
/// 成员冲突或属性参数约束时通过诊断停止生成,而不是静默回退到不完整输出。
/// </remarks>
[Generator]
public sealed class AutoSceneGenerator : IIncrementalGenerator
{
private const string AutoSceneAttributeMetadataName =
$"{PathContests.GodotSourceGeneratorsAbstractionsPath}.UI.AutoSceneAttribute";
private static readonly string[] GeneratedMemberNames =
[
"SceneKeyStr",
"__autoSceneBehavior_Generated"
];
/// <summary>
/// 配置 <c>AutoScene</c> 的增量生成管线。
/// </summary>
/// <param name="context">用于注册语法筛选、语义转换和源输出阶段的增量生成上下文。</param>
/// <remarks>
/// 管线首先通过语法节点名称快速筛选潜在候选,再结合语义模型确认类型符号。
/// 最终输出阶段仅在 <c>AutoSceneAttribute</c>、<c>Godot.Node</c> 等依赖可解析且目标类型满足生成约束时产出源码;
/// 否则会报告对应诊断,或在宿主依赖缺失时直接跳过生成。
/// </remarks>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var candidates = context.SyntaxProvider.CreateSyntaxProvider(
static (node, _) => IsCandidate(node),
static (syntaxContext, _) => Transform(syntaxContext))
.Where(static candidate => candidate is not null);
var compilationAndCandidates = context.CompilationProvider.Combine(candidates.Collect());
context.RegisterSourceOutput(compilationAndCandidates,
static (spc, pair) => Execute(spc, pair.Left, pair.Right));
}
private static bool IsCandidate(SyntaxNode node)
{
return node is ClassDeclarationSyntax classDeclaration &&
classDeclaration.AttributeLists
.SelectMany(static list => list.Attributes)
.Any(static attribute => attribute.Name.ToString().Contains("AutoScene", StringComparison.Ordinal));
}
private static TypeCandidate? Transform(GeneratorSyntaxContext context)
{
if (context.Node is not ClassDeclarationSyntax classDeclaration)
return null;
if (context.SemanticModel.GetDeclaredSymbol(classDeclaration) is not INamedTypeSymbol typeSymbol)
return null;
return new TypeCandidate(classDeclaration, typeSymbol);
}
private static void Execute(
SourceProductionContext context,
Compilation compilation,
ImmutableArray<TypeCandidate?> candidates)
{
if (candidates.IsDefaultOrEmpty)
return;
var autoSceneAttribute = compilation.GetTypeByMetadataName(AutoSceneAttributeMetadataName);
var godotNodeType = compilation.GetTypeByMetadataName("Godot.Node");
if (autoSceneAttribute is null || godotNodeType is null)
return;
foreach (var candidate in candidates.Where(static candidate => candidate is not null)
.Select(static candidate => candidate!))
{
var attribute = candidate.TypeSymbol.GetAttributes()
.FirstOrDefault(attr => SymbolEqualityComparer.Default.Equals(attr.AttributeClass, autoSceneAttribute));
if (attribute is null)
continue;
if (!CanGenerateForType(context, candidate, godotNodeType))
continue;
if (candidate.TypeSymbol.ReportGeneratedMethodConflicts(
context,
candidate.ClassDeclaration.Identifier.GetLocation(),
"GetScene"))
{
continue;
}
if (ReportGeneratedMemberConflicts(
context,
candidate.TypeSymbol,
candidate.ClassDeclaration.Identifier.GetLocation(),
GeneratedMemberNames))
{
continue;
}
if (!TryGetSceneKey(context, candidate.TypeSymbol, attribute, out var key))
continue;
context.AddSource(GetHintName(candidate.TypeSymbol), GenerateSource(candidate.TypeSymbol, key));
}
}
private static bool CanGenerateForType(
SourceProductionContext context,
TypeCandidate candidate,
INamedTypeSymbol requiredBaseType)
{
if (candidate.TypeSymbol.ContainingType is not null)
{
context.ReportDiagnostic(Diagnostic.Create(
AutoBehaviorDiagnostics.NestedClassNotSupported,
candidate.ClassDeclaration.Identifier.GetLocation(),
"AutoScene",
candidate.TypeSymbol.Name));
return false;
}
if (!IsPartial(candidate.TypeSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(
CommonDiagnostics.ClassMustBePartial,
candidate.ClassDeclaration.Identifier.GetLocation(),
candidate.TypeSymbol.Name));
return false;
}
if (candidate.TypeSymbol.IsAssignableTo(requiredBaseType))
return true;
context.ReportDiagnostic(Diagnostic.Create(
AutoBehaviorDiagnostics.MissingBaseType,
candidate.ClassDeclaration.Identifier.GetLocation(),
candidate.TypeSymbol.Name,
requiredBaseType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
"AutoScene"));
return false;
}
private static bool TryGetSceneKey(
SourceProductionContext context,
INamedTypeSymbol typeSymbol,
AttributeData attribute,
out string key)
{
key = string.Empty;
if (attribute.ConstructorArguments.Length == 1 &&
attribute.ConstructorArguments[0].Value is string sceneKey)
{
key = sceneKey;
return true;
}
context.ReportDiagnostic(Diagnostic.Create(
AutoBehaviorDiagnostics.InvalidAttributeArguments,
attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ??
typeSymbol.Locations.FirstOrDefault() ??
Location.None,
"AutoSceneAttribute",
typeSymbol.Name,
"a single string scene key argument"));
return false;
}
private static string GenerateSource(INamedTypeSymbol typeSymbol, string key)
{
var builder = new StringBuilder();
builder.AppendLine("// <auto-generated />");
builder.AppendLine("#nullable enable");
builder.AppendLine();
var ns = typeSymbol.ContainingNamespace.IsGlobalNamespace
? null
: typeSymbol.ContainingNamespace.ToDisplayString();
if (ns is not null)
{
builder.AppendLine($"namespace {ns};");
builder.AppendLine();
}
builder.AppendLine($"{GetTypeDeclarationKeyword(typeSymbol)} {GetTypeDeclarationName(typeSymbol)}");
AppendTypeConstraints(builder, typeSymbol);
builder.AppendLine("{");
builder.AppendLine(
" private global::GFramework.Game.Abstractions.Scene.ISceneBehavior? __autoSceneBehavior_Generated;");
builder.AppendLine();
builder.Append(" public static string SceneKeyStr => ");
builder.Append(SymbolDisplay.FormatLiteral(key, true));
builder.AppendLine(";");
builder.AppendLine();
builder.AppendLine(" public global::GFramework.Game.Abstractions.Scene.ISceneBehavior GetScene()");
builder.AppendLine(" {");
builder.AppendLine(
" return __autoSceneBehavior_Generated ??= global::GFramework.Godot.Scene.SceneBehaviorFactory.Create(this, SceneKeyStr);");
builder.AppendLine(" }");
builder.AppendLine("}");
return builder.ToString();
}
private static bool IsPartial(INamedTypeSymbol typeSymbol)
{
return typeSymbol.DeclaringSyntaxReferences
.Select(static reference => reference.GetSyntax())
.OfType<ClassDeclarationSyntax>()
.All(static declaration =>
declaration.Modifiers.Any(static modifier => modifier.IsKind(SyntaxKind.PartialKeyword)));
}
private static string GetHintName(INamedTypeSymbol typeSymbol)
{
var prefix = typeSymbol.ContainingNamespace.IsGlobalNamespace
? typeSymbol.Name
: $"{typeSymbol.ContainingNamespace.ToDisplayString()}.{typeSymbol.Name}";
return prefix.Replace('.', '_') + ".AutoScene.g.cs";
}
private static string GetTypeDeclarationKeyword(INamedTypeSymbol typeSymbol)
{
return typeSymbol.IsRecord
? typeSymbol.TypeKind == TypeKind.Struct ? "partial record struct" : "partial record"
: typeSymbol.TypeKind == TypeKind.Struct
? "partial struct"
: "partial class";
}
private static string GetTypeDeclarationName(INamedTypeSymbol typeSymbol)
{
if (typeSymbol.TypeParameters.Length == 0)
return typeSymbol.Name;
return
$"{typeSymbol.Name}<{string.Join(", ", typeSymbol.TypeParameters.Select(static parameter => parameter.Name))}>";
}
private static void AppendTypeConstraints(StringBuilder builder, INamedTypeSymbol typeSymbol)
{
foreach (var typeParameter in typeSymbol.TypeParameters)
{
var constraints = new List<string>();
if (typeParameter.HasReferenceTypeConstraint)
{
constraints.Add(
typeParameter.ReferenceTypeConstraintNullableAnnotation == NullableAnnotation.Annotated
? "class?"
: "class");
}
if (typeParameter.HasNotNullConstraint)
constraints.Add("notnull");
// unmanaged implies the value-type constraint and must replace struct in generated constraints.
if (typeParameter.HasUnmanagedTypeConstraint)
constraints.Add("unmanaged");
else if (typeParameter.HasValueTypeConstraint)
constraints.Add("struct");
constraints.AddRange(typeParameter.ConstraintTypes.Select(static constraint =>
constraint.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)));
if (typeParameter.HasConstructorConstraint)
constraints.Add("new()");
if (constraints.Count == 0)
continue;
builder.Append(" where ");
builder.Append(typeParameter.Name);
builder.Append(" : ");
builder.AppendLine(string.Join(", ", constraints));
}
}
/// <summary>
/// 报告与生成器保留成员名冲突的字段或属性,避免生成代码出现重复成员编译错误。
/// </summary>
/// <param name="context">用于上报诊断的源代码生成上下文。</param>
/// <param name="typeSymbol">当前待生成的类型符号。</param>
/// <param name="fallbackLocation">冲突成员无定位信息时的后备位置。</param>
/// <param name="memberNames">需要校验的生成器保留成员名集合。</param>
/// <returns>存在任意冲突时返回 <c>true</c>。</returns>
private static bool ReportGeneratedMemberConflicts(
SourceProductionContext context,
INamedTypeSymbol typeSymbol,
Location fallbackLocation,
string[] memberNames)
{
var hasConflict = false;
foreach (var memberName in memberNames)
{
var conflict = typeSymbol.GetMembers(memberName)
.FirstOrDefault(member =>
!member.IsImplicitlyDeclared &&
member is IPropertySymbol or IFieldSymbol);
if (conflict is null)
continue;
context.ReportDiagnostic(Diagnostic.Create(
CommonDiagnostics.GeneratedMethodNameConflict,
conflict.Locations.FirstOrDefault() ?? fallbackLocation,
typeSymbol.Name,
memberName));
hasConflict = true;
}
return hasConflict;
}
private sealed class TypeCandidate
{
public TypeCandidate(ClassDeclarationSyntax classDeclaration, INamedTypeSymbol typeSymbol)
{
ClassDeclaration = classDeclaration;
TypeSymbol = typeSymbol;
}
public ClassDeclarationSyntax ClassDeclaration { get; }
public INamedTypeSymbol TypeSymbol { get; }
}
}