fix(generator): 解决源代码生成器中的换行符兼容性和集合类型候选检测问题

- 在测试中添加换行符标准化功能,确保跨平台测试一致性
- 修复ContextGetGenerator中集合类型候选检测逻辑,跳过无效类型参数
- 添加针对可空服务字段的单元测试用例
- 优化生成器对不同系统换行符的处理机制
This commit is contained in:
GeWuYou 2026-03-28 19:45:14 +08:00
parent 4fb1da2da6
commit 535743e824
4 changed files with 127 additions and 25 deletions

View File

@ -1,7 +1,4 @@
using Microsoft.CodeAnalysis.CSharp.Testing; namespace GFramework.Godot.SourceGenerators.Tests.Core;
using Microsoft.CodeAnalysis.Testing;
namespace GFramework.Godot.SourceGenerators.Tests.Core;
/// <summary> /// <summary>
/// 提供源代码生成器测试的通用功能。 /// 提供源代码生成器测试的通用功能。
@ -30,8 +27,21 @@ public static class GeneratorTest<TGenerator>
foreach (var (filename, content) in generatedSources) foreach (var (filename, content) in generatedSources)
test.TestState.GeneratedSources.Add( test.TestState.GeneratedSources.Add(
(typeof(TGenerator), filename, content)); (typeof(TGenerator), filename, NormalizeLineEndings(content)));
await test.RunAsync(); await test.RunAsync();
} }
/// <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);
}
} }

View File

@ -1,7 +1,4 @@
using Microsoft.CodeAnalysis.CSharp.Testing; namespace GFramework.SourceGenerators.Tests.Core;
using Microsoft.CodeAnalysis.Testing;
namespace GFramework.SourceGenerators.Tests.Core;
/// <summary> /// <summary>
/// 提供源代码生成器测试的通用功能 /// 提供源代码生成器测试的通用功能
@ -32,8 +29,21 @@ public static class GeneratorTest<TGenerator>
// 添加期望的生成源文件到测试状态中 // 添加期望的生成源文件到测试状态中
foreach (var (filename, content) in generatedSources) foreach (var (filename, content) in generatedSources)
test.TestState.GeneratedSources.Add( test.TestState.GeneratedSources.Add(
(typeof(TGenerator), filename, content)); (typeof(TGenerator), filename, NormalizeLineEndings(content)));
await test.RunAsync(); await test.RunAsync();
} }
/// <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);
}
} }

View File

@ -1,9 +1,5 @@
using GFramework.SourceGenerators.Rule; using GFramework.SourceGenerators.Rule;
using GFramework.SourceGenerators.Tests.Core; using GFramework.SourceGenerators.Tests.Core;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;
using NUnit.Framework;
namespace GFramework.SourceGenerators.Tests.Rule; namespace GFramework.SourceGenerators.Tests.Rule;
@ -381,6 +377,98 @@ public class ContextGetGeneratorTests
Assert.Pass(); Assert.Pass();
} }
[Test]
public async Task Skips_Nullable_Service_Like_Field_For_ContextAware_GetAll_Class()
{
var source = """
using System;
using GFramework.SourceGenerators.Abstractions.Rule;
namespace GFramework.SourceGenerators.Abstractions.Rule
{
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class ContextAwareAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class GetAllAttribute : Attribute { }
}
namespace GFramework.Core.Abstractions.Rule
{
public interface IContextAware { }
}
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 GFramework.Core.Extensions
{
public static class ContextAwareServiceExtensions
{
public static T GetModel<T>(this object contextAware) => default!;
public static T GetSystem<T>(this object contextAware) => default!;
}
}
namespace Godot
{
public class Control { }
}
namespace TestApp
{
public interface IGridModel : GFramework.Core.Abstractions.Model.IModel { }
public interface IRunLoopSystem : GFramework.Core.Abstractions.Systems.ISystem { }
public interface IUiPageBehavior { }
[ContextAware]
[GetAll]
public partial class GameplayHud : Godot.Control
{
private IGridModel _gridModel = null!;
private IUiPageBehavior? _page;
private IRunLoopSystem _runLoopSystem = null!;
}
}
""";
const string expected = """
// <auto-generated />
#nullable enable
using GFramework.Core.Extensions;
namespace TestApp;
partial class GameplayHud
{
private void __InjectContextBindings_Generated()
{
_gridModel = this.GetModel<global::TestApp.IGridModel>();
_runLoopSystem = this.GetSystem<global::TestApp.IRunLoopSystem>();
}
}
""";
await GeneratorTest<ContextGetGenerator>.RunAsync(
source,
("TestApp_GameplayHud.ContextGet.g.cs", expected));
Assert.Pass();
}
[Test] [Test]
public async Task Generates_Bindings_For_IContextAware_Class() public async Task Generates_Bindings_For_IContextAware_Class()
{ {

View File

@ -1,13 +1,7 @@
using System.Collections.Immutable;
using System.Text;
using GFramework.SourceGenerators.Common.Constants; using GFramework.SourceGenerators.Common.Constants;
using GFramework.SourceGenerators.Common.Diagnostics; using GFramework.SourceGenerators.Common.Diagnostics;
using GFramework.SourceGenerators.Common.Extensions;
using GFramework.SourceGenerators.Common.Info; using GFramework.SourceGenerators.Common.Info;
using GFramework.SourceGenerators.Diagnostics; using GFramework.SourceGenerators.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace GFramework.SourceGenerators.Rule; namespace GFramework.SourceGenerators.Rule;
@ -711,11 +705,12 @@ public sealed class ContextGetGenerator : IIncrementalGenerator
if (readOnlyList is null || fieldType is not INamedTypeSymbol targetType) if (readOnlyList is null || fieldType is not INamedTypeSymbol targetType)
return false; return false;
var allTypeCandidates = EnumerateCollectionTypeCandidates(targetType) foreach (var candidateType in EnumerateCollectionTypeCandidates(targetType))
.SelectMany(candidateType => candidateType.TypeArguments);
foreach (var candidateElementType in allTypeCandidates)
{ {
if (candidateType.TypeArguments.Length != 1)
continue;
var candidateElementType = candidateType.TypeArguments[0];
var expectedSourceType = readOnlyList.Construct(candidateElementType); var expectedSourceType = readOnlyList.Construct(candidateElementType);
if (!expectedSourceType.IsAssignableTo(targetType)) if (!expectedSourceType.IsAssignableTo(targetType))
continue; continue;
@ -724,7 +719,6 @@ public sealed class ContextGetGenerator : IIncrementalGenerator
return true; return true;
} }
return false; return false;
} }