mirror of
https://github.com/GeWuYou/GFramework.git
synced 2026-03-31 18:39:00 +08:00
feat(generator): 添加 BindNodeSignal 和 GetNode 源代码生成器
- 实现 BindNodeSignalGenerator 用于生成节点信号绑定与解绑逻辑 - 实现 GetNodeGenerator 用于生成 Godot 节点获取注入逻辑 - 添加 BindNodeSignalDiagnostics 提供详细的诊断错误信息 - 集成到 AnalyzerReleases.Unshipped.md 追踪新的分析规则 - 支持 [BindNodeSignal] 属性的方法自动生成事件绑定代码 - 支持 [GetNode] 属性的字段自动生成节点获取代码 - 提供生命周期方法集成的智能提示和验证功能
This commit is contained in:
parent
2dfd6e044f
commit
6fa4580893
@ -526,11 +526,11 @@ public class BindNodeSignalGeneratorTests
|
||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
||||
};
|
||||
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_011", DiagnosticSeverity.Error)
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
|
||||
.WithLocation(0)
|
||||
.WithArguments("Hud", "__BindNodeSignals_Generated"));
|
||||
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_011", DiagnosticSeverity.Error)
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
|
||||
.WithLocation(1)
|
||||
.WithArguments("Hud", "__UnbindNodeSignals_Generated"));
|
||||
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
using GFramework.Godot.SourceGenerators.Tests.Core;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp.Testing;
|
||||
using Microsoft.CodeAnalysis.Testing;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Tests.GetNode;
|
||||
|
||||
@ -240,4 +236,71 @@ public class GetNodeGeneratorTests
|
||||
|
||||
await test.RunAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists()
|
||||
{
|
||||
const string source = """
|
||||
using System;
|
||||
using GFramework.Godot.SourceGenerators.Abstractions;
|
||||
using Godot;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators.Abstractions
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
|
||||
public sealed class GetNodeAttribute : Attribute
|
||||
{
|
||||
public GetNodeAttribute() {}
|
||||
}
|
||||
|
||||
public enum NodeLookupMode
|
||||
{
|
||||
Auto = 0
|
||||
}
|
||||
}
|
||||
|
||||
namespace Godot
|
||||
{
|
||||
public class Node
|
||||
{
|
||||
public virtual void _Ready() {}
|
||||
public T GetNode<T>(string path) where T : Node => throw new InvalidOperationException(path);
|
||||
public T? GetNodeOrNull<T>(string path) where T : Node => default;
|
||||
}
|
||||
|
||||
public class HBoxContainer : Node
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public partial class TopBar : HBoxContainer
|
||||
{
|
||||
[GetNode]
|
||||
private HBoxContainer _leftContainer = null!;
|
||||
|
||||
private void {|#0:__InjectGetNodes_Generated|}()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<GetNodeGenerator, DefaultVerifier>
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { source }
|
||||
},
|
||||
DisabledDiagnostics = { "GF_Common_Trace_001" },
|
||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
||||
};
|
||||
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
|
||||
.WithLocation(0)
|
||||
.WithArguments("TopBar", "__InjectGetNodes_Generated"));
|
||||
|
||||
await test.RunAsync();
|
||||
}
|
||||
}
|
||||
@ -21,4 +21,3 @@
|
||||
GF_Godot_BindNodeSignal_008 | GFramework.Godot | Warning | BindNodeSignalDiagnostics
|
||||
GF_Godot_BindNodeSignal_009 | GFramework.Godot | Warning | BindNodeSignalDiagnostics
|
||||
GF_Godot_BindNodeSignal_010 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
||||
GF_Godot_BindNodeSignal_011 | GFramework.Godot | Error | BindNodeSignalDiagnostics
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using GFramework.Godot.SourceGenerators.Diagnostics;
|
||||
using GFramework.SourceGenerators.Common.Constants;
|
||||
using GFramework.SourceGenerators.Common.Diagnostics;
|
||||
@ -88,7 +89,11 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
|
||||
if (!CanGenerateForType(context, group, typeSymbol))
|
||||
continue;
|
||||
|
||||
if (HasGeneratedMethodNameConflict(context, group, typeSymbol))
|
||||
if (typeSymbol.ReportGeneratedMethodConflicts(
|
||||
context,
|
||||
group.Methods[0].Method.Identifier.GetLocation(),
|
||||
BindMethodName,
|
||||
UnbindMethodName))
|
||||
continue;
|
||||
|
||||
var bindings = new List<SignalBindingInfo>();
|
||||
@ -390,36 +395,6 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
|
||||
typeSymbol.Name));
|
||||
}
|
||||
|
||||
private static bool HasGeneratedMethodNameConflict(
|
||||
SourceProductionContext context,
|
||||
TypeGroup group,
|
||||
INamedTypeSymbol typeSymbol)
|
||||
{
|
||||
var hasConflict = false;
|
||||
|
||||
foreach (var generatedMethodName in new[] { BindMethodName, UnbindMethodName })
|
||||
{
|
||||
var conflictingMethod = typeSymbol.GetMembers()
|
||||
.OfType<IMethodSymbol>()
|
||||
.FirstOrDefault(method =>
|
||||
method.Name == generatedMethodName &&
|
||||
method.Parameters.Length == 0 &&
|
||||
method.TypeParameters.Length == 0);
|
||||
|
||||
if (conflictingMethod is null)
|
||||
continue;
|
||||
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
BindNodeSignalDiagnostics.GeneratedMethodNameConflict,
|
||||
conflictingMethod.Locations.FirstOrDefault() ?? group.Methods[0].Method.Identifier.GetLocation(),
|
||||
typeSymbol.Name,
|
||||
generatedMethodName));
|
||||
hasConflict = true;
|
||||
}
|
||||
|
||||
return hasConflict;
|
||||
}
|
||||
|
||||
private static IMethodSymbol? FindLifecycleMethod(
|
||||
INamedTypeSymbol typeSymbol,
|
||||
string methodName)
|
||||
@ -627,7 +602,7 @@ public sealed class BindNodeSignalGenerator : IIncrementalGenerator
|
||||
|
||||
public int GetHashCode(MethodCandidate obj)
|
||||
{
|
||||
return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
|
||||
return RuntimeHelpers.GetHashCode(obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -126,16 +126,4 @@ public static class BindNodeSignalDiagnostics
|
||||
PathContests.GodotNamespace,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
|
||||
/// <summary>
|
||||
/// 用户代码中已存在与生成方法同名的成员。
|
||||
/// </summary>
|
||||
public static readonly DiagnosticDescriptor GeneratedMethodNameConflict =
|
||||
new(
|
||||
"GF_Godot_BindNodeSignal_011",
|
||||
"Generated method name conflicts with an existing member",
|
||||
"Class '{0}' already defines method '{1}()', which conflicts with [BindNodeSignal] generated code",
|
||||
PathContests.GodotNamespace,
|
||||
DiagnosticSeverity.Error,
|
||||
true);
|
||||
}
|
||||
@ -1,12 +1,6 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text;
|
||||
using GFramework.Godot.SourceGenerators.Diagnostics;
|
||||
using GFramework.SourceGenerators.Common.Constants;
|
||||
using GFramework.SourceGenerators.Common.Diagnostics;
|
||||
using GFramework.SourceGenerators.Common.Extensions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
|
||||
namespace GFramework.Godot.SourceGenerators;
|
||||
|
||||
@ -95,6 +89,12 @@ public sealed class GetNodeGenerator : IIncrementalGenerator
|
||||
if (!CanGenerateForType(context, group, typeSymbol))
|
||||
continue;
|
||||
|
||||
if (typeSymbol.ReportGeneratedMethodConflicts(
|
||||
context,
|
||||
group.Fields[0].Variable.Identifier.GetLocation(),
|
||||
InjectionMethodName))
|
||||
continue;
|
||||
|
||||
var bindings = new List<NodeBindingInfo>();
|
||||
|
||||
foreach (var candidate in group.Fields)
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
; Unshipped analyzer release
|
||||
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
|
||||
; https://github.com/dotnet/roslyn/blob/main/src/RoslynAnalyzers/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
|
||||
|
||||
### New Rules
|
||||
|
||||
Rule ID | Category | Severity | Notes
|
||||
---------------------|-------------------|----------|-------------------
|
||||
GF_Common_Class_001 | GFramework.Common | Error | CommonDiagnostics
|
||||
GF_Common_Class_002 | GFramework.Common | Error | CommonDiagnostics
|
||||
GF_Common_Trace_001 | GFramework.Trace | Info | CommonDiagnostics
|
||||
@ -1,6 +1,4 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace GFramework.SourceGenerators.Common.Diagnostics;
|
||||
namespace GFramework.SourceGenerators.Common.Diagnostics;
|
||||
|
||||
/// <summary>
|
||||
/// 提供通用诊断描述符的静态类
|
||||
@ -27,6 +25,23 @@ public static class CommonDiagnostics
|
||||
true
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 定义生成方法名与用户代码冲突的诊断描述符。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 该诊断用于保护生成器保留的方法名,避免用户代码手动声明了相同零参数方法时出现重复成员错误,
|
||||
/// 并使多个生成器可以复用同一条一致的冲突报告规则。
|
||||
/// </remarks>
|
||||
public static readonly DiagnosticDescriptor GeneratedMethodNameConflict =
|
||||
new(
|
||||
"GF_Common_Class_002",
|
||||
"Generated method name conflicts with an existing member",
|
||||
"Class '{0}' already defines method '{1}()', which conflicts with generated code",
|
||||
"GFramework.Common",
|
||||
DiagnosticSeverity.Error,
|
||||
true
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 定义源代码生成器跟踪信息的诊断描述符
|
||||
/// </summary>
|
||||
|
||||
@ -0,0 +1,49 @@
|
||||
using GFramework.SourceGenerators.Common.Diagnostics;
|
||||
|
||||
namespace GFramework.SourceGenerators.Common.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// 提供生成方法名冲突校验的通用扩展。
|
||||
/// </summary>
|
||||
public static class GeneratedMethodConflictExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 检查目标类型上是否已存在与生成器保留方法同名的零参数方法,并在冲突时报告统一诊断。
|
||||
/// </summary>
|
||||
/// <param name="typeSymbol">待校验的目标类型。</param>
|
||||
/// <param name="context">源代码生成上下文。</param>
|
||||
/// <param name="fallbackLocation">当冲突成员缺少源码位置时使用的后备位置。</param>
|
||||
/// <param name="generatedMethodNames">生成器将保留的零参数方法名集合。</param>
|
||||
/// <returns>若发现任一冲突则返回 <c>true</c>。</returns>
|
||||
public static bool ReportGeneratedMethodConflicts(
|
||||
this INamedTypeSymbol typeSymbol,
|
||||
SourceProductionContext context,
|
||||
Location fallbackLocation,
|
||||
params string[] generatedMethodNames)
|
||||
{
|
||||
var hasConflict = false;
|
||||
|
||||
foreach (var generatedMethodName in generatedMethodNames.Distinct(StringComparer.Ordinal))
|
||||
{
|
||||
var conflictingMethod = typeSymbol.GetMembers()
|
||||
.OfType<IMethodSymbol>()
|
||||
.FirstOrDefault(method =>
|
||||
!method.IsImplicitlyDeclared &&
|
||||
string.Equals(method.Name, generatedMethodName, StringComparison.Ordinal) &&
|
||||
method.Parameters.Length == 0 &&
|
||||
method.TypeParameters.Length == 0);
|
||||
|
||||
if (conflictingMethod is null)
|
||||
continue;
|
||||
|
||||
context.ReportDiagnostic(Diagnostic.Create(
|
||||
CommonDiagnostics.GeneratedMethodNameConflict,
|
||||
conflictingMethod.Locations.FirstOrDefault() ?? fallbackLocation,
|
||||
typeSymbol.Name,
|
||||
generatedMethodName));
|
||||
hasConflict = true;
|
||||
}
|
||||
|
||||
return hasConflict;
|
||||
}
|
||||
}
|
||||
@ -377,6 +377,84 @@ public class ContextGetGeneratorTests
|
||||
Assert.Pass();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Reports_Diagnostic_When_Generated_Injection_Method_Name_Already_Exists()
|
||||
{
|
||||
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.Field, Inherited = false)]
|
||||
public sealed class GetModelAttribute : 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!;
|
||||
}
|
||||
}
|
||||
|
||||
namespace TestApp
|
||||
{
|
||||
public interface IInventoryModel : GFramework.Core.Abstractions.Model.IModel { }
|
||||
|
||||
[ContextAware]
|
||||
public partial class InventoryPanel
|
||||
{
|
||||
[GetModel]
|
||||
private IInventoryModel _model = null!;
|
||||
|
||||
private void {|#0:__InjectContextBindings_Generated|}()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
var test = new CSharpSourceGeneratorTest<ContextGetGenerator, DefaultVerifier>
|
||||
{
|
||||
TestState =
|
||||
{
|
||||
Sources = { source }
|
||||
},
|
||||
DisabledDiagnostics = { "GF_Common_Trace_001" },
|
||||
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
|
||||
};
|
||||
|
||||
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
|
||||
.WithLocation(0)
|
||||
.WithArguments("InventoryPanel", "__InjectContextBindings_Generated"));
|
||||
|
||||
await test.RunAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Ignores_NonInferable_Const_Field_For_GetAll_Class_Without_Diagnostic()
|
||||
{
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
using System.Text;
|
||||
using GFramework.SourceGenerators.Common.Constants;
|
||||
using GFramework.SourceGenerators.Common.Diagnostics;
|
||||
using GFramework.SourceGenerators.Common.Extensions;
|
||||
using GFramework.SourceGenerators.Common.Info;
|
||||
using GFramework.SourceGenerators.Diagnostics;
|
||||
|
||||
@ -220,6 +218,12 @@ public sealed class ContextGetGenerator : IIncrementalGenerator
|
||||
if (!CanGenerateForType(context, workItem, symbols))
|
||||
continue;
|
||||
|
||||
if (workItem.TypeSymbol.ReportGeneratedMethodConflicts(
|
||||
context,
|
||||
GetTypeLocation(workItem),
|
||||
InjectionMethodName))
|
||||
continue;
|
||||
|
||||
var bindings = CollectBindings(context, workItem, descriptors, symbols);
|
||||
if (bindings.Count == 0 && workItem.GetAllDeclaration is null)
|
||||
continue;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user