Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions GFramework.Godot.SourceGenerators.Tests/Core/GeneratorTest.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;

namespace GFramework.Godot.SourceGenerators.Tests.Core;
namespace GFramework.Godot.SourceGenerators.Tests.Core;

/// <summary>
/// 提供源代码生成器测试的通用功能。
Expand Down Expand Up @@ -30,8 +27,21 @@ public static async Task RunAsync(

foreach (var (filename, content) in generatedSources)
test.TestState.GeneratedSources.Add(
(typeof(TGenerator), filename, content));
(typeof(TGenerator), filename, NormalizeLineEndings(content)));

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);
}
}
6 changes: 5 additions & 1 deletion GFramework.Godot.SourceGenerators.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
global using System.Threading.Tasks;
global using Microsoft.CodeAnalysis;
global using Microsoft.CodeAnalysis.CSharp.Testing;
global using Microsoft.CodeAnalysis.Testing;
global using NUnit.Framework;
20 changes: 15 additions & 5 deletions GFramework.SourceGenerators.Tests/Core/GeneratorTest.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
using Microsoft.CodeAnalysis.CSharp.Testing;
using Microsoft.CodeAnalysis.Testing;

namespace GFramework.SourceGenerators.Tests.Core;
namespace GFramework.SourceGenerators.Tests.Core;

/// <summary>
/// 提供源代码生成器测试的通用功能
Expand Down Expand Up @@ -32,8 +29,21 @@ public static async Task RunAsync(
// 添加期望的生成源文件到测试状态中
foreach (var (filename, content) in generatedSources)
test.TestState.GeneratedSources.Add(
(typeof(TGenerator), filename, content));
(typeof(TGenerator), filename, NormalizeLineEndings(content)));

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);
}
}
6 changes: 5 additions & 1 deletion GFramework.SourceGenerators.Tests/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
global using System.Threading.Tasks;
global using Microsoft.CodeAnalysis;
global using Microsoft.CodeAnalysis.CSharp.Testing;
global using Microsoft.CodeAnalysis.Testing;
global using NUnit.Framework;
188 changes: 183 additions & 5 deletions GFramework.SourceGenerators.Tests/Rule/ContextGetGeneratorTests.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
using GFramework.SourceGenerators.Rule;
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;

Expand Down Expand Up @@ -234,7 +230,6 @@ public static class ContextAwareServiceExtensions
public static IReadOnlyList<T> GetModels<T>(this object contextAware) => default!;
public static T GetSystem<T>(this object contextAware) => default!;
public static T GetUtility<T>(this object contextAware) => default!;
public static T GetService<T>(this object contextAware) => default!;
}
}

Expand All @@ -258,6 +253,7 @@ public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase
private ICombatSystem _system = null!;
private IUiUtility _utility = null!;
private IStrategy _service = null!;
private IReadOnlyList<IStrategy> _services = null!;
private Godot.Node _node = null!;
}
}
Expand All @@ -279,6 +275,96 @@ private void __InjectContextBindings_Generated()
_models = this.GetModels<global::TestApp.IInventoryModel>();
_system = this.GetSystem<global::TestApp.ICombatSystem>();
_utility = this.GetUtility<global::TestApp.IUiUtility>();
}
}

""";

await GeneratorTest<ContextGetGenerator>.RunAsync(
source,
("TestApp_BattlePanel.ContextGet.g.cs", expected));
Assert.Pass();
}

[Test]
public async Task Generates_Explicit_Service_Binding_For_GetAll_Class()
{
var source = """
using System;
using GFramework.SourceGenerators.Abstractions.Rule;

namespace GFramework.SourceGenerators.Abstractions.Rule
{
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public sealed class GetAllAttribute : Attribute { }

[AttributeUsage(AttributeTargets.Field, Inherited = false)]
public sealed class GetServiceAttribute : Attribute { }
}

namespace GFramework.Core.Abstractions.Rule
{
public interface IContextAware { }
}

namespace GFramework.Core.Rule
{
public abstract class ContextAwareBase : GFramework.Core.Abstractions.Rule.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 GetService<T>(this object contextAware) => default!;
}
}

namespace TestApp
{
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
public interface IStrategy { }

[GetAll]
public partial class BattlePanel : GFramework.Core.Rule.ContextAwareBase
{
private IInventoryModel _model = null!;

[GetService]
private IStrategy _service = null!;
}
}
""";

const string expected = """
// <auto-generated />
#nullable enable

using GFramework.Core.Extensions;

namespace TestApp;

partial class BattlePanel
{
private void __InjectContextBindings_Generated()
{
_model = this.GetModel<global::TestApp.IInventoryModel>();
_service = this.GetService<global::TestApp.IStrategy>();
}
}
Expand All @@ -291,6 +377,98 @@ await GeneratorTest<ContextGetGenerator>.RunAsync(
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]
public async Task Generates_Bindings_For_IContextAware_Class()
{
Expand Down
4 changes: 3 additions & 1 deletion GFramework.SourceGenerators/GlobalUsings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
global using System.Threading.Tasks;
global using Microsoft.CodeAnalysis;
global using Microsoft.CodeAnalysis.CSharp.Syntax;
5 changes: 4 additions & 1 deletion GFramework.SourceGenerators/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,7 @@ public partial class InventoryPanel
}
```

`[GetAll]` 作用于类本身,会自动扫描字段并推断对应的 `GetX` 调用;已显式标记字段的优先级更高。
`[GetAll]` 作用于类本身,会自动扫描字段并推断 `Model`、`System`、`Utility` 相关的 `GetX` 调用;已显式标记字段的优先级更高。

`Service` 和 `Services` 绑定不会在 `[GetAll]` 下自动推断。对于普通引用类型字段,请显式使用 `[GetService]` 或
`[GetServices]`,避免将非上下文服务字段误判为服务依赖。
17 changes: 2 additions & 15 deletions GFramework.SourceGenerators/Rule/ContextGetGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@
using GFramework.SourceGenerators.Common.Extensions;
using GFramework.SourceGenerators.Common.Info;
using GFramework.SourceGenerators.Diagnostics;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

namespace GFramework.SourceGenerators.Rule;

Expand Down Expand Up @@ -582,12 +579,7 @@ private static bool TryCreateInferredBinding(
return true;
}

if (elementType.IsReferenceType)
{
binding = new BindingInfo(fieldSymbol, BindingKind.Services, elementType);
return true;
}

// Service collections stay opt-in for the same reason as single services.
return false;
}

Expand All @@ -609,12 +601,7 @@ private static bool TryCreateInferredBinding(
return true;
}

if (fieldSymbol.Type.IsReferenceType)
{
binding = new BindingInfo(fieldSymbol, BindingKind.Service, fieldSymbol.Type);
return true;
}

// Service bindings stay opt-in because arbitrary reference types are too ambiguous to infer safely.
return false;
}

Expand Down
Loading