mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-05-07 00:39:00 +08:00
feat(generator): 改进自动注册模块生成器的跨文件顺序稳定性
当partial类分布在多个文件中时,确保生成器使用稳定的跨文件顺序来生成注册代码。 添加了对语法树排序的支持,使相同声明上的注册特性能够按照源码中的书写顺序生成安装代码。 同时修复了测试快照换行符问题,确保跨平台兼容性。
This commit is contained in:
parent
62d448354c
commit
6898866b97
@ -6,6 +6,9 @@ namespace GFramework.SourceGenerators.Tests.Architectures;
|
||||
[TestFixture]
|
||||
public class AutoRegisterModuleGeneratorTests
|
||||
{
|
||||
/// <summary>
|
||||
/// 验证同一声明上的注册特性会按照源码中的书写顺序生成安装代码。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Generates_Module_Install_Method_In_Attribute_Order()
|
||||
{
|
||||
@ -106,6 +109,156 @@ public class AutoRegisterModuleGeneratorTests
|
||||
("TestApp_GameplayModule.AutoRegisterModule.g.cs", expected));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证 partial 声明分布在多个文件时,生成器仍然会使用稳定的跨文件顺序生成注册代码。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Generates_Module_Install_Method_In_Deterministic_Order_Across_Partial_Declarations()
|
||||
{
|
||||
const string commonSource = """
|
||||
using System;
|
||||
|
||||
namespace GFramework.SourceGenerators.Abstractions.Architectures
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class AutoRegisterModuleAttribute : Attribute { }
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterModelAttribute : Attribute
|
||||
{
|
||||
public RegisterModelAttribute(Type modelType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterSystemAttribute : Attribute
|
||||
{
|
||||
public RegisterSystemAttribute(Type systemType) { }
|
||||
}
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
|
||||
public sealed class RegisterUtilityAttribute : Attribute
|
||||
{
|
||||
public RegisterUtilityAttribute(Type utilityType) { }
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Architectures
|
||||
{
|
||||
public interface IArchitecture
|
||||
{
|
||||
T RegisterModel<T>(T model) where T : GFramework.Core.Abstractions.Model.IModel;
|
||||
T RegisterSystem<T>(T system) where T : GFramework.Core.Abstractions.Systems.ISystem;
|
||||
T RegisterUtility<T>(T utility) where T : GFramework.Core.Abstractions.Utility.IUtility;
|
||||
}
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Model
|
||||
{
|
||||
public interface IModel { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Systems
|
||||
{
|
||||
public interface ISystem { }
|
||||
}
|
||||
|
||||
namespace GFramework.Core.Abstractions.Utility
|
||||
{
|
||||
public interface IUtility { }
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.Core.Abstractions.Model;
|
||||
using GFramework.Core.Abstractions.Systems;
|
||||
using GFramework.Core.Abstractions.Utility;
|
||||
|
||||
public sealed class PlayerModel : IModel { }
|
||||
public sealed class CombatSystem : ISystem { }
|
||||
public sealed class AudioUtility : IUtility { }
|
||||
}
|
||||
""";
|
||||
|
||||
const string partASource = """
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.SourceGenerators.Abstractions.Architectures;
|
||||
|
||||
// Padding ensures this attribute lives later in the file than the attributes in PartB.
|
||||
// The generator should still place it first because PartA sorts before PartB.
|
||||
// padding 01
|
||||
// padding 02
|
||||
// padding 03
|
||||
// padding 04
|
||||
// padding 05
|
||||
// padding 06
|
||||
// padding 07
|
||||
// padding 08
|
||||
// padding 09
|
||||
// padding 10
|
||||
[AutoRegisterModule]
|
||||
[RegisterUtility(typeof(AudioUtility))]
|
||||
public partial class GameplayModule
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
const string partBSource = """
|
||||
namespace TestApp
|
||||
{
|
||||
using GFramework.SourceGenerators.Abstractions.Architectures;
|
||||
|
||||
[RegisterSystem(typeof(CombatSystem))]
|
||||
[RegisterModel(typeof(PlayerModel))]
|
||||
public partial class GameplayModule
|
||||
{
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
const string expected = """
|
||||
// <auto-generated />
|
||||
#nullable enable
|
||||
|
||||
namespace TestApp;
|
||||
|
||||
partial class GameplayModule
|
||||
{
|
||||
public void Install(global::GFramework.Core.Abstractions.Architectures.IArchitecture architecture)
|
||||
{
|
||||
architecture.RegisterUtility(new global::TestApp.AudioUtility());
|
||||
architecture.RegisterSystem(new global::TestApp.CombatSystem());
|
||||
architecture.RegisterModel(new global::TestApp.PlayerModel());
|
||||
}
|
||||
}
|
||||
|
||||
""";
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<AutoRegisterModuleGenerator, DefaultVerifier>
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources =
|
||||
{
|
||||
("Common.cs", commonSource),
|
||||
("GameplayModule.PartA.cs", partASource),
|
||||
("GameplayModule.PartB.cs", partBSource)
|
||||
},
|
||||
GeneratedSources =
|
||||
{
|
||||
(typeof(AutoRegisterModuleGenerator), "TestApp_GameplayModule.AutoRegisterModule.g.cs", NormalizeLineEndings(expected))
|
||||
}
|
||||
},
|
||||
DisabledDiagnostics = { "GF_Common_Trace_001" }
|
||||
};
|
||||
|
||||
await test.RunAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 验证生成器会保留可空引用、notnull 与 unmanaged 约束。
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Generates_Type_Constraints_For_NullableReference_NotNull_And_Unmanaged()
|
||||
{
|
||||
@ -204,4 +357,17 @@ public class AutoRegisterModuleGeneratorTests
|
||||
source,
|
||||
("TestApp_GameplayModule.AutoRegisterModule.g.cs", expected));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将测试快照统一为当前平台换行符,避免不同系统上的源生成输出比较出现伪差异。
|
||||
/// </summary>
|
||||
/// <param name="content">原始快照内容。</param>
|
||||
/// <returns>使用当前平台换行符的快照内容。</returns>
|
||||
private static string NormalizeLineEndings(string content)
|
||||
{
|
||||
return content
|
||||
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||
.Replace("\r", "\n", StringComparison.Ordinal)
|
||||
.Replace("\n", Environment.NewLine, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,7 +179,12 @@ public sealed class AutoRegisterModuleGenerator : IIncrementalGenerator
|
||||
{
|
||||
var registrations = new List<RegistrationSpec>();
|
||||
|
||||
foreach (var attribute in typeSymbol.GetAttributes().OrderBy(GetAttributeOrder))
|
||||
foreach (var attribute in typeSymbol.GetAttributes()
|
||||
// Roslyn 会把 partial 类型上的属性合并到同一个集合中。
|
||||
// 先按语法树标识排序,才能让每个文件内的 Span.Start 成为可比较的稳定顺序键。
|
||||
.OrderBy(GetAttributeSyntaxTreeOrderKey, StringComparer.Ordinal)
|
||||
.ThenBy(GetAttributeOrder)
|
||||
.ThenBy(GetAttributeTypeOrderKey, StringComparer.Ordinal))
|
||||
{
|
||||
if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, registerModelAttribute))
|
||||
{
|
||||
@ -352,6 +357,27 @@ public sealed class AutoRegisterModuleGenerator : IIncrementalGenerator
|
||||
return attribute.ApplicationSyntaxReference?.Span.Start ?? int.MaxValue;
|
||||
}
|
||||
|
||||
private static string GetAttributeSyntaxTreeOrderKey(AttributeData attribute)
|
||||
{
|
||||
var syntaxTree = attribute.ApplicationSyntaxReference?.SyntaxTree;
|
||||
if (syntaxTree is null)
|
||||
return string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(syntaxTree.FilePath))
|
||||
return syntaxTree.FilePath;
|
||||
|
||||
// In-memory compilations may not assign file paths. Fall back to the syntax tree text so
|
||||
// attributes from different partial declarations still get a deterministic cross-file order.
|
||||
return syntaxTree.ToString();
|
||||
}
|
||||
|
||||
private static string GetAttributeTypeOrderKey(AttributeData attribute)
|
||||
{
|
||||
return attribute.ConstructorArguments.FirstOrDefault().Value is INamedTypeSymbol componentType
|
||||
? componentType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
private static Location GetAttributeLocation(AttributeData attribute)
|
||||
{
|
||||
return attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ?? Location.None;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user