Merge pull request #152 from GeWuYou/feat/bindnodesignal-generator

Feat/bindnodesignal generator
This commit is contained in:
gewuyou 2026-03-31 12:15:54 +08:00 committed by GitHub
commit ba45171924
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 1705 additions and 22 deletions

View File

@ -0,0 +1,40 @@
#nullable enable
namespace GFramework.Godot.SourceGenerators.Abstractions;
/// <summary>
/// 标记 Godot 节点事件处理方法Source Generator 会为其生成事件绑定与解绑逻辑。
/// </summary>
/// <remarks>
/// 该特性通过节点字段名与事件名建立声明式订阅关系,适用于将
/// <c>_Ready()</c> / <c>_ExitTree()</c> 中重复的 <c>+=</c> 与 <c>-=</c> 样板代码
/// 收敛到生成器中统一维护。
/// </remarks>
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
/// <summary>
/// 初始化 <see cref="BindNodeSignalAttribute" /> 的新实例。
/// </summary>
/// <param name="nodeFieldName">目标节点字段名。</param>
/// <param name="signalName">目标节点上的 CLR 事件名。</param>
/// <exception cref="ArgumentNullException">
/// <paramref name="nodeFieldName" /> 或 <paramref name="signalName" /> 为 <see langword="null" />。
/// </exception>
public BindNodeSignalAttribute(
string nodeFieldName,
string signalName)
{
NodeFieldName = nodeFieldName ?? throw new ArgumentNullException(nameof(nodeFieldName));
SignalName = signalName ?? throw new ArgumentNullException(nameof(signalName));
}
/// <summary>
/// 获取目标节点字段名。
/// </summary>
public string NodeFieldName { get; }
/// <summary>
/// 获取目标节点上的 CLR 事件名。
/// </summary>
public string SignalName { get; }
}

View File

@ -0,0 +1,629 @@
using GFramework.Godot.SourceGenerators.Tests.Core;
namespace GFramework.Godot.SourceGenerators.Tests.BindNodeSignal;
/// <summary>
/// 验证 <see cref="BindNodeSignalGenerator" /> 的生成与诊断行为。
/// </summary>
[TestFixture]
public class BindNodeSignalGeneratorTests
{
/// <summary>
/// 验证生成器会为已有生命周期调用生成成对的绑定与解绑方法。
/// </summary>
[Test]
public async Task Generates_Bind_And_Unbind_Methods_For_Existing_Lifecycle_Hooks()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; }
public string SignalName { get; }
}
}
namespace Godot
{
public class Node
{
public virtual void _Ready() {}
public virtual void _ExitTree() {}
}
public class Button : Node
{
public event Action? Pressed
{
add {}
remove {}
}
}
public class SpinBox : Node
{
public delegate void ValueChangedEventHandler(double value);
public event ValueChangedEventHandler? ValueChanged
{
add {}
remove {}
}
}
}
namespace TestApp
{
public partial class Hud : Node
{
private Button _startButton = null!;
private SpinBox _startOreSpinBox = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed()
{
}
[BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))]
private void OnStartOreValueChanged(double value)
{
}
public override void _Ready()
{
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
}
""";
const string expected = """
// <auto-generated />
#nullable enable
namespace TestApp;
partial class Hud
{
private void __BindNodeSignals_Generated()
{
_startButton.Pressed += OnStartButtonPressed;
_startOreSpinBox.ValueChanged += OnStartOreValueChanged;
}
private void __UnbindNodeSignals_Generated()
{
_startButton.Pressed -= OnStartButtonPressed;
_startOreSpinBox.ValueChanged -= OnStartOreValueChanged;
}
}
""";
await GeneratorTest<BindNodeSignalGenerator>.RunAsync(
source,
("TestApp_Hud.BindNodeSignal.g.cs", expected));
}
/// <summary>
/// 验证一个处理方法可以通过多个特性绑定到多个节点事件,且能与 GetNode 声明共存。
/// </summary>
[Test]
public async Task Generates_Multiple_Subscriptions_For_The_Same_Handler_And_Coexists_With_GetNode()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; }
public string SignalName { get; }
}
[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public sealed class GetNodeAttribute : Attribute
{
}
}
namespace Godot
{
public class Node
{
public virtual void _Ready() {}
public virtual void _ExitTree() {}
}
public class Button : Node
{
public event Action? Pressed
{
add {}
remove {}
}
}
}
namespace TestApp
{
public partial class Hud : Node
{
[GetNode]
private Button _startButton = null!;
[GetNode]
private Button _cancelButton = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
[BindNodeSignal(nameof(_cancelButton), nameof(Button.Pressed))]
private void OnAnyButtonPressed()
{
}
}
}
""";
const string expected = """
// <auto-generated />
#nullable enable
namespace TestApp;
partial class Hud
{
private void __BindNodeSignals_Generated()
{
_startButton.Pressed += OnAnyButtonPressed;
_cancelButton.Pressed += OnAnyButtonPressed;
}
private void __UnbindNodeSignals_Generated()
{
_startButton.Pressed -= OnAnyButtonPressed;
_cancelButton.Pressed -= OnAnyButtonPressed;
}
}
""";
await GeneratorTest<BindNodeSignalGenerator>.RunAsync(
source,
("TestApp_Hud.BindNodeSignal.g.cs", expected));
}
/// <summary>
/// 验证引用不存在的事件时会报告错误。
/// </summary>
[Test]
public async Task Reports_Diagnostic_When_Signal_Does_Not_Exist()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; }
public string SignalName { get; }
}
}
namespace Godot
{
public class Node
{
}
public class Button : Node
{
public event Action? Pressed
{
add {}
remove {}
}
}
}
namespace TestApp
{
public partial class Hud : Node
{
private Button _startButton = null!;
[{|#0:BindNodeSignal(nameof(_startButton), "Released")|}]
private void OnStartButtonPressed()
{
}
}
}
""";
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_006", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("_startButton", "Released"));
await test.RunAsync();
}
/// <summary>
/// 验证方法签名与事件委托不匹配时会报告错误。
/// </summary>
[Test]
public async Task Reports_Diagnostic_When_Method_Signature_Does_Not_Match_Event()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; }
public string SignalName { get; }
}
}
namespace Godot
{
public class Node
{
}
public class SpinBox : Node
{
public delegate void ValueChangedEventHandler(double value);
public event ValueChangedEventHandler? ValueChanged
{
add {}
remove {}
}
}
}
namespace TestApp
{
public partial class Hud : Node
{
private SpinBox _startOreSpinBox = null!;
[{|#0:BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))|}]
private void OnStartOreValueChanged()
{
}
}
}
""";
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_007", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("OnStartOreValueChanged", "ValueChanged", "_startOreSpinBox"));
await test.RunAsync();
}
/// <summary>
/// 验证特性构造参数为空时会报告明确的参数无效诊断。
/// </summary>
[Test]
public async Task Reports_Diagnostic_When_Constructor_Argument_Is_Empty()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; }
public string SignalName { get; }
}
}
namespace Godot
{
public class Node
{
}
public class Button : Node
{
public event Action? Pressed
{
add {}
remove {}
}
}
}
namespace TestApp
{
public partial class Hud : Node
{
private Button _startButton = null!;
[{|#0:BindNodeSignal(nameof(_startButton), "")|}]
private void OnStartButtonPressed()
{
}
}
}
""";
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_010", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("OnStartButtonPressed", "signalName"));
await test.RunAsync();
}
/// <summary>
/// 验证当用户自定义了与生成方法同名的成员时,会报告冲突而不是生成重复成员。
/// </summary>
[Test]
public async Task Reports_Diagnostic_When_Generated_Method_Names_Already_Exist()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; }
public string SignalName { get; }
}
}
namespace Godot
{
public class Node
{
}
public class Button : Node
{
public event Action? Pressed
{
add {}
remove {}
}
}
}
namespace TestApp
{
public partial class Hud : Node
{
private Button _startButton = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed()
{
}
private void {|#0:__BindNodeSignals_Generated|}()
{
}
private void {|#1:__UnbindNodeSignals_Generated|}()
{
}
}
}
""";
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, 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("Hud", "__BindNodeSignals_Generated"));
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Common_Class_002", DiagnosticSeverity.Error)
.WithLocation(1)
.WithArguments("Hud", "__UnbindNodeSignals_Generated"));
await test.RunAsync();
}
/// <summary>
/// 验证已有生命周期方法但未调用生成方法时会报告对称的警告。
/// </summary>
[Test]
public async Task Reports_Warnings_When_Lifecycle_Methods_Do_Not_Call_Generated_Methods()
{
const string source = """
using System;
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
namespace GFramework.Godot.SourceGenerators.Abstractions
{
[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = true)]
public sealed class BindNodeSignalAttribute : Attribute
{
public BindNodeSignalAttribute(string nodeFieldName, string signalName)
{
NodeFieldName = nodeFieldName;
SignalName = signalName;
}
public string NodeFieldName { get; }
public string SignalName { get; }
}
}
namespace Godot
{
public class Node
{
public virtual void _Ready() {}
public virtual void _ExitTree() {}
}
public class Button : Node
{
public event Action? Pressed
{
add {}
remove {}
}
}
}
namespace TestApp
{
public partial class Hud : Node
{
private Button _startButton = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed()
{
}
public override void {|#0:_Ready|}()
{
}
public override void {|#1:_ExitTree|}()
{
}
}
}
""";
var test = new CSharpSourceGeneratorTest<BindNodeSignalGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_008", DiagnosticSeverity.Warning)
.WithLocation(0)
.WithArguments("Hud"));
test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_Godot_BindNodeSignal_009", DiagnosticSeverity.Warning)
.WithLocation(1)
.WithArguments("Hud"));
await test.RunAsync();
}
}

View File

@ -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();
}
}

View File

@ -4,10 +4,20 @@
### New Rules
Rule ID | Category | Severity | Notes
----------------------|------------------|----------|--------------------
-----------------------------|------------------|----------|---------------------------
GF_Godot_GetNode_001 | GFramework.Godot | Error | GetNodeDiagnostics
GF_Godot_GetNode_002 | GFramework.Godot | Error | GetNodeDiagnostics
GF_Godot_GetNode_003 | GFramework.Godot | Error | GetNodeDiagnostics
GF_Godot_GetNode_004 | GFramework.Godot | Error | GetNodeDiagnostics
GF_Godot_GetNode_005 | GFramework.Godot | Error | GetNodeDiagnostics
GF_Godot_GetNode_006 | GFramework.Godot | Warning | GetNodeDiagnostics
GF_Godot_BindNodeSignal_001 | GFramework.Godot | Error | BindNodeSignalDiagnostics
GF_Godot_BindNodeSignal_002 | GFramework.Godot | Error | BindNodeSignalDiagnostics
GF_Godot_BindNodeSignal_003 | GFramework.Godot | Error | BindNodeSignalDiagnostics
GF_Godot_BindNodeSignal_004 | GFramework.Godot | Error | BindNodeSignalDiagnostics
GF_Godot_BindNodeSignal_005 | GFramework.Godot | Error | BindNodeSignalDiagnostics
GF_Godot_BindNodeSignal_006 | GFramework.Godot | Error | BindNodeSignalDiagnostics
GF_Godot_BindNodeSignal_007 | GFramework.Godot | Error | BindNodeSignalDiagnostics
GF_Godot_BindNodeSignal_008 | GFramework.Godot | Warning | BindNodeSignalDiagnostics
GF_Godot_BindNodeSignal_009 | GFramework.Godot | Warning | BindNodeSignalDiagnostics
GF_Godot_BindNodeSignal_010 | GFramework.Godot | Error | BindNodeSignalDiagnostics

View File

@ -0,0 +1,611 @@
using System.Collections.Immutable;
using System.Runtime.CompilerServices;
using System.Text;
using GFramework.Godot.SourceGenerators.Diagnostics;
using GFramework.SourceGenerators.Common.Constants;
using GFramework.SourceGenerators.Common.Diagnostics;
using GFramework.SourceGenerators.Common.Extensions;
namespace GFramework.Godot.SourceGenerators;
/// <summary>
/// 为带有 <c>[BindNodeSignal]</c> 的方法生成 Godot 节点事件绑定与解绑逻辑。
/// </summary>
[Generator]
public sealed class BindNodeSignalGenerator : IIncrementalGenerator
{
private const string BindNodeSignalAttributeMetadataName =
$"{PathContests.GodotSourceGeneratorsAbstractionsPath}.BindNodeSignalAttribute";
private const string BindMethodName = "__BindNodeSignals_Generated";
private const string UnbindMethodName = "__UnbindNodeSignals_Generated";
/// <summary>
/// 初始化增量生成器。
/// </summary>
/// <param name="context">生成器初始化上下文。</param>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
var candidates = context.SyntaxProvider.CreateSyntaxProvider(
static (node, _) => IsCandidate(node),
static (ctx, _) => Transform(ctx))
.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)
{
if (node is not MethodDeclarationSyntax methodDeclaration)
return false;
return methodDeclaration.AttributeLists
.SelectMany(static list => list.Attributes)
.Any(static attribute => IsBindNodeSignalAttributeName(attribute.Name));
}
private static MethodCandidate? Transform(GeneratorSyntaxContext context)
{
if (context.Node is not MethodDeclarationSyntax methodDeclaration)
return null;
if (context.SemanticModel.GetDeclaredSymbol(methodDeclaration) is not IMethodSymbol methodSymbol)
return null;
return new MethodCandidate(methodDeclaration, methodSymbol);
}
private static void Execute(
SourceProductionContext context,
Compilation compilation,
ImmutableArray<MethodCandidate?> candidates)
{
if (candidates.IsDefaultOrEmpty)
return;
var bindNodeSignalAttribute = compilation.GetTypeByMetadataName(BindNodeSignalAttributeMetadataName);
var godotNodeSymbol = compilation.GetTypeByMetadataName("Godot.Node");
if (bindNodeSignalAttribute is null || godotNodeSymbol is null)
return;
// 缓存每个方法上已解析的特性,避免在筛选和生成阶段重复做语义查询。
var methodAttributes = candidates
.Where(static candidate => candidate is not null)
.Select(static candidate => candidate!)
.ToDictionary(
static candidate => candidate,
candidate => ResolveAttributes(candidate.MethodSymbol, bindNodeSignalAttribute),
ReferenceEqualityComparer.Instance);
var methodCandidates = methodAttributes
.Where(static pair => pair.Value.Count > 0)
.Select(static pair => pair.Key)
.ToList();
foreach (var group in GroupByContainingType(methodCandidates))
{
var typeSymbol = group.TypeSymbol;
if (!CanGenerateForType(context, group, typeSymbol))
continue;
if (typeSymbol.ReportGeneratedMethodConflicts(
context,
group.Methods[0].Method.Identifier.GetLocation(),
BindMethodName,
UnbindMethodName))
continue;
var bindings = new List<SignalBindingInfo>();
foreach (var candidate in group.Methods)
{
foreach (var attribute in methodAttributes[candidate])
{
if (!TryCreateBinding(context, candidate, attribute, godotNodeSymbol, out var binding))
continue;
bindings.Add(binding);
}
}
if (bindings.Count == 0)
continue;
ReportMissingLifecycleHookCall(
context,
group,
typeSymbol,
"_Ready",
BindMethodName,
BindNodeSignalDiagnostics.ManualReadyHookRequired);
ReportMissingLifecycleHookCall(
context,
group,
typeSymbol,
"_ExitTree",
UnbindMethodName,
BindNodeSignalDiagnostics.ManualExitTreeHookRequired);
context.AddSource(GetHintName(typeSymbol), GenerateSource(typeSymbol, bindings));
}
}
private static bool CanGenerateForType(
SourceProductionContext context,
TypeGroup group,
INamedTypeSymbol typeSymbol)
{
if (typeSymbol.ContainingType is not null)
{
context.ReportDiagnostic(Diagnostic.Create(
BindNodeSignalDiagnostics.NestedClassNotSupported,
group.Methods[0].Method.Identifier.GetLocation(),
typeSymbol.Name));
return false;
}
if (typeSymbol.AreAllDeclarationsPartial())
return true;
context.ReportDiagnostic(Diagnostic.Create(
CommonDiagnostics.ClassMustBePartial,
group.Methods[0].Method.Identifier.GetLocation(),
typeSymbol.Name));
return false;
}
private static bool TryCreateBinding(
SourceProductionContext context,
MethodCandidate candidate,
AttributeData attribute,
INamedTypeSymbol godotNodeSymbol,
out SignalBindingInfo binding)
{
binding = default!;
if (candidate.MethodSymbol.IsStatic)
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.StaticMethodNotSupported,
candidate,
attribute,
candidate.MethodSymbol.Name);
return false;
}
if (!TryResolveCtorString(attribute, 0, out var nodeFieldName))
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.InvalidConstructorArgument,
candidate,
attribute,
candidate.MethodSymbol.Name,
"nodeFieldName");
return false;
}
if (!TryResolveCtorString(attribute, 1, out var signalName))
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.InvalidConstructorArgument,
candidate,
attribute,
candidate.MethodSymbol.Name,
"signalName");
return false;
}
var fieldSymbol = FindField(candidate.MethodSymbol.ContainingType, nodeFieldName);
if (fieldSymbol is null)
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.NodeFieldNotFound,
candidate,
attribute,
candidate.MethodSymbol.Name,
nodeFieldName,
candidate.MethodSymbol.ContainingType.Name);
return false;
}
if (fieldSymbol.IsStatic)
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.NodeFieldMustBeInstanceField,
candidate,
attribute,
candidate.MethodSymbol.Name,
fieldSymbol.Name);
return false;
}
if (!fieldSymbol.Type.IsAssignableTo(godotNodeSymbol))
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.FieldTypeMustDeriveFromNode,
candidate,
attribute,
fieldSymbol.Name);
return false;
}
var eventSymbol = FindEvent(fieldSymbol.Type, signalName);
if (eventSymbol is null)
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.SignalNotFound,
candidate,
attribute,
fieldSymbol.Name,
signalName);
return false;
}
if (!IsMethodCompatibleWithEvent(candidate.MethodSymbol, eventSymbol))
{
ReportMethodDiagnostic(
context,
BindNodeSignalDiagnostics.MethodSignatureNotCompatible,
candidate,
attribute,
candidate.MethodSymbol.Name,
eventSymbol.Name,
fieldSymbol.Name);
return false;
}
binding = new SignalBindingInfo(fieldSymbol, eventSymbol, candidate.MethodSymbol);
return true;
}
private static void ReportMethodDiagnostic(
SourceProductionContext context,
DiagnosticDescriptor descriptor,
MethodCandidate candidate,
AttributeData attribute,
params object[] messageArgs)
{
var location = attribute.ApplicationSyntaxReference?.GetSyntax().GetLocation() ??
candidate.Method.Identifier.GetLocation();
context.ReportDiagnostic(Diagnostic.Create(descriptor, location, messageArgs));
}
private static bool TryResolveCtorString(
AttributeData attribute,
int index,
out string value)
{
value = string.Empty;
if (attribute.ConstructorArguments.Length <= index)
return false;
var ctorArgument = attribute.ConstructorArguments[index];
if (ctorArgument.Kind != TypedConstantKind.Primitive || ctorArgument.Value is not string ctorString)
return false;
if (string.IsNullOrWhiteSpace(ctorString))
return false;
value = ctorString;
return true;
}
private static IReadOnlyList<AttributeData> ResolveAttributes(
IMethodSymbol methodSymbol,
INamedTypeSymbol bindNodeSignalAttribute)
{
return methodSymbol.GetAttributes()
.Where(attribute =>
SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, bindNodeSignalAttribute))
.ToList();
}
private static IFieldSymbol? FindField(
INamedTypeSymbol typeSymbol,
string nodeFieldName)
{
return typeSymbol.GetMembers()
.OfType<IFieldSymbol>()
.FirstOrDefault(field => string.Equals(field.Name, nodeFieldName, StringComparison.Ordinal));
}
private static IEventSymbol? FindEvent(
ITypeSymbol typeSymbol,
string signalName)
{
for (var current = typeSymbol as INamedTypeSymbol; current is not null; current = current.BaseType)
{
var eventSymbol = current.GetMembers()
.OfType<IEventSymbol>()
.FirstOrDefault(evt => string.Equals(evt.Name, signalName, StringComparison.Ordinal));
if (eventSymbol is not null)
return eventSymbol;
}
return null;
}
private static bool IsMethodCompatibleWithEvent(
IMethodSymbol methodSymbol,
IEventSymbol eventSymbol)
{
if (!methodSymbol.ReturnsVoid)
return false;
if (methodSymbol.TypeParameters.Length > 0)
return false;
if (eventSymbol.Type is not INamedTypeSymbol delegateType)
return false;
var invokeMethod = delegateType.DelegateInvokeMethod;
if (invokeMethod is null || !invokeMethod.ReturnsVoid)
return false;
if (methodSymbol.Parameters.Length != invokeMethod.Parameters.Length)
return false;
// 这里采用“精确签名匹配”而不是宽松推断,确保生成代码的订阅行为可预测且诊断明确。
for (var index = 0; index < methodSymbol.Parameters.Length; index++)
{
var methodParameter = methodSymbol.Parameters[index];
var delegateParameter = invokeMethod.Parameters[index];
if (methodParameter.RefKind != delegateParameter.RefKind)
return false;
var methodParameterType = methodParameter.Type.WithNullableAnnotation(NullableAnnotation.None);
var delegateParameterType = delegateParameter.Type.WithNullableAnnotation(NullableAnnotation.None);
if (!SymbolEqualityComparer.Default.Equals(methodParameterType, delegateParameterType))
return false;
}
return true;
}
private static void ReportMissingLifecycleHookCall(
SourceProductionContext context,
TypeGroup group,
INamedTypeSymbol typeSymbol,
string lifecycleMethodName,
string generatedMethodName,
DiagnosticDescriptor descriptor)
{
var lifecycleMethod = FindLifecycleMethod(typeSymbol, lifecycleMethodName);
if (lifecycleMethod is null || CallsGeneratedMethod(lifecycleMethod, generatedMethodName))
return;
context.ReportDiagnostic(Diagnostic.Create(
descriptor,
lifecycleMethod.Locations.FirstOrDefault() ?? group.Methods[0].Method.Identifier.GetLocation(),
typeSymbol.Name));
}
private static IMethodSymbol? FindLifecycleMethod(
INamedTypeSymbol typeSymbol,
string methodName)
{
return typeSymbol.GetMembers()
.OfType<IMethodSymbol>()
.FirstOrDefault(method =>
method.Name == methodName &&
!method.IsStatic &&
method.Parameters.Length == 0 &&
method.MethodKind == MethodKind.Ordinary);
}
private static bool CallsGeneratedMethod(
IMethodSymbol methodSymbol,
string generatedMethodName)
{
foreach (var syntaxReference in methodSymbol.DeclaringSyntaxReferences)
{
if (syntaxReference.GetSyntax() is not MethodDeclarationSyntax methodSyntax)
continue;
if (methodSyntax.DescendantNodes()
.OfType<InvocationExpressionSyntax>()
.Any(invocation => IsGeneratedMethodInvocation(invocation, generatedMethodName)))
return true;
}
return false;
}
private static bool IsGeneratedMethodInvocation(
InvocationExpressionSyntax invocation,
string generatedMethodName)
{
return invocation.Expression switch
{
IdentifierNameSyntax identifierName => string.Equals(
identifierName.Identifier.ValueText,
generatedMethodName,
StringComparison.Ordinal),
MemberAccessExpressionSyntax memberAccess => string.Equals(
memberAccess.Name.Identifier.ValueText,
generatedMethodName,
StringComparison.Ordinal),
_ => false
};
}
private static bool IsBindNodeSignalAttributeName(NameSyntax attributeName)
{
var simpleName = GetAttributeSimpleName(attributeName);
return simpleName is "BindNodeSignal" or "BindNodeSignalAttribute";
}
private static string? GetAttributeSimpleName(NameSyntax attributeName)
{
return attributeName switch
{
IdentifierNameSyntax identifierName => identifierName.Identifier.ValueText,
QualifiedNameSyntax qualifiedName => GetAttributeSimpleName(qualifiedName.Right),
AliasQualifiedNameSyntax aliasQualifiedName => aliasQualifiedName.Name.Identifier.ValueText,
_ => null
};
}
private static string GenerateSource(
INamedTypeSymbol typeSymbol,
IReadOnlyList<SignalBindingInfo> bindings)
{
var namespaceName = typeSymbol.GetNamespace();
var generics = typeSymbol.ResolveGenerics();
var sb = new StringBuilder()
.AppendLine("// <auto-generated />")
.AppendLine("#nullable enable");
if (namespaceName is not null)
{
sb.AppendLine()
.AppendLine($"namespace {namespaceName};");
}
sb.AppendLine()
.AppendLine($"partial class {typeSymbol.Name}{generics.Parameters}");
foreach (var constraint in generics.Constraints)
sb.AppendLine($" {constraint}");
sb.AppendLine("{")
.AppendLine($" private void {BindMethodName}()")
.AppendLine(" {");
foreach (var binding in bindings)
sb.AppendLine(
$" {binding.FieldSymbol.Name}.{binding.EventSymbol.Name} += {binding.MethodSymbol.Name};");
sb.AppendLine(" }")
.AppendLine()
.AppendLine($" private void {UnbindMethodName}()")
.AppendLine(" {");
foreach (var binding in bindings)
sb.AppendLine(
$" {binding.FieldSymbol.Name}.{binding.EventSymbol.Name} -= {binding.MethodSymbol.Name};");
sb.AppendLine(" }")
.AppendLine("}");
return sb.ToString();
}
private static string GetHintName(INamedTypeSymbol typeSymbol)
{
return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
.Replace("global::", string.Empty)
.Replace("<", "_")
.Replace(">", "_")
.Replace(",", "_")
.Replace(" ", string.Empty)
.Replace(".", "_") + ".BindNodeSignal.g.cs";
}
private static IReadOnlyList<TypeGroup> GroupByContainingType(IEnumerable<MethodCandidate> candidates)
{
var groupMap = new Dictionary<INamedTypeSymbol, TypeGroup>(SymbolEqualityComparer.Default);
var orderedGroups = new List<TypeGroup>();
foreach (var candidate in candidates)
{
var typeSymbol = candidate.MethodSymbol.ContainingType;
if (!groupMap.TryGetValue(typeSymbol, out var group))
{
group = new TypeGroup(typeSymbol);
groupMap.Add(typeSymbol, group);
orderedGroups.Add(group);
}
group.Methods.Add(candidate);
}
return orderedGroups;
}
private sealed class MethodCandidate
{
public MethodCandidate(
MethodDeclarationSyntax method,
IMethodSymbol methodSymbol)
{
Method = method;
MethodSymbol = methodSymbol;
}
public MethodDeclarationSyntax Method { get; }
public IMethodSymbol MethodSymbol { get; }
}
private sealed class SignalBindingInfo
{
public SignalBindingInfo(
IFieldSymbol fieldSymbol,
IEventSymbol eventSymbol,
IMethodSymbol methodSymbol)
{
FieldSymbol = fieldSymbol;
EventSymbol = eventSymbol;
MethodSymbol = methodSymbol;
}
public IFieldSymbol FieldSymbol { get; }
public IEventSymbol EventSymbol { get; }
public IMethodSymbol MethodSymbol { get; }
}
private sealed class TypeGroup
{
public TypeGroup(INamedTypeSymbol typeSymbol)
{
TypeSymbol = typeSymbol;
Methods = new List<MethodCandidate>();
}
public INamedTypeSymbol TypeSymbol { get; }
public List<MethodCandidate> Methods { get; }
}
/// <summary>
/// 使用引用相等比较 MethodCandidate确保缓存字典复用同一语法候选对象。
/// </summary>
private sealed class ReferenceEqualityComparer : IEqualityComparer<MethodCandidate>
{
public static ReferenceEqualityComparer Instance { get; } = new();
public bool Equals(
MethodCandidate? x,
MethodCandidate? y)
{
return ReferenceEquals(x, y);
}
public int GetHashCode(MethodCandidate obj)
{
return RuntimeHelpers.GetHashCode(obj);
}
}
}

View File

@ -0,0 +1,129 @@
using GFramework.SourceGenerators.Common.Constants;
namespace GFramework.Godot.SourceGenerators.Diagnostics;
/// <summary>
/// BindNodeSignal 生成器相关诊断。
/// </summary>
public static class BindNodeSignalDiagnostics
{
/// <summary>
/// 嵌套类型不受支持。
/// </summary>
public static readonly DiagnosticDescriptor NestedClassNotSupported =
new(
"GF_Godot_BindNodeSignal_001",
"Nested classes are not supported",
"Class '{0}' cannot use [BindNodeSignal] inside a nested type",
PathContests.GodotNamespace,
DiagnosticSeverity.Error,
true);
/// <summary>
/// static 方法不受支持。
/// </summary>
public static readonly DiagnosticDescriptor StaticMethodNotSupported =
new(
"GF_Godot_BindNodeSignal_002",
"Static methods are not supported",
"Method '{0}' cannot be static when using [BindNodeSignal]",
PathContests.GodotNamespace,
DiagnosticSeverity.Error,
true);
/// <summary>
/// 节点字段不存在。
/// </summary>
public static readonly DiagnosticDescriptor NodeFieldNotFound =
new(
"GF_Godot_BindNodeSignal_003",
"Referenced node field was not found",
"Method '{0}' references node field '{1}', but no matching field exists on class '{2}'",
PathContests.GodotNamespace,
DiagnosticSeverity.Error,
true);
/// <summary>
/// 节点字段必须是实例字段。
/// </summary>
public static readonly DiagnosticDescriptor NodeFieldMustBeInstanceField =
new(
"GF_Godot_BindNodeSignal_004",
"Referenced node field must be an instance field",
"Method '{0}' references node field '{1}', but that field must be an instance field",
PathContests.GodotNamespace,
DiagnosticSeverity.Error,
true);
/// <summary>
/// 字段类型必须继承自 Godot.Node。
/// </summary>
public static readonly DiagnosticDescriptor FieldTypeMustDeriveFromNode =
new(
"GF_Godot_BindNodeSignal_005",
"Field type must derive from Godot.Node",
"Field '{0}' must be a Godot.Node type to use [BindNodeSignal]",
PathContests.GodotNamespace,
DiagnosticSeverity.Error,
true);
/// <summary>
/// 目标事件不存在。
/// </summary>
public static readonly DiagnosticDescriptor SignalNotFound =
new(
"GF_Godot_BindNodeSignal_006",
"Referenced event was not found",
"Field '{0}' does not contain an event named '{1}'",
PathContests.GodotNamespace,
DiagnosticSeverity.Error,
true);
/// <summary>
/// 方法签名与事件委托不兼容。
/// </summary>
public static readonly DiagnosticDescriptor MethodSignatureNotCompatible =
new(
"GF_Godot_BindNodeSignal_007",
"Method signature is not compatible with the referenced event",
"Method '{0}' is not compatible with event '{1}' on field '{2}'",
PathContests.GodotNamespace,
DiagnosticSeverity.Error,
true);
/// <summary>
/// 现有 _Ready 中未调用生成绑定逻辑。
/// </summary>
public static readonly DiagnosticDescriptor ManualReadyHookRequired =
new(
"GF_Godot_BindNodeSignal_008",
"Call generated signal binding from _Ready",
"Class '{0}' defines _Ready(); call __BindNodeSignals_Generated() there to bind [BindNodeSignal] handlers",
PathContests.GodotNamespace,
DiagnosticSeverity.Warning,
true);
/// <summary>
/// 现有 _ExitTree 中未调用生成解绑逻辑。
/// </summary>
public static readonly DiagnosticDescriptor ManualExitTreeHookRequired =
new(
"GF_Godot_BindNodeSignal_009",
"Call generated signal unbinding from _ExitTree",
"Class '{0}' defines _ExitTree(); call __UnbindNodeSignals_Generated() there to unbind [BindNodeSignal] handlers",
PathContests.GodotNamespace,
DiagnosticSeverity.Warning,
true);
/// <summary>
/// BindNodeSignalAttribute 构造参数无效。
/// </summary>
public static readonly DiagnosticDescriptor InvalidConstructorArgument =
new(
"GF_Godot_BindNodeSignal_010",
"BindNodeSignal attribute arguments are invalid",
"Method '{0}' uses [BindNodeSignal] with an invalid '{1}' constructor argument; it must be a non-empty string literal",
PathContests.GodotNamespace,
DiagnosticSeverity.Error,
true);
}

View File

@ -4,9 +4,6 @@ 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 +92,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)

View File

@ -16,3 +16,6 @@ global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
global using Microsoft.CodeAnalysis;
global using Microsoft.CodeAnalysis.CSharp;
global using Microsoft.CodeAnalysis.CSharp.Syntax;

View File

@ -7,6 +7,7 @@
- 与 Godot 场景相关的编译期生成能力
- 基于 Roslyn 的增量生成器实现
- `[GetNode]` 字段注入,减少 `_Ready()` 里的 `GetNode<T>()` 样板代码
- `[BindNodeSignal]` 方法绑定,减少 `_Ready()` / `_ExitTree()` 中重复的事件订阅样板代码
## 使用建议
@ -43,3 +44,47 @@ public partial class TopBar : HBoxContainer
- `_leftContainer` -> `%LeftContainer`
- `m_rightContainer` -> `%RightContainer`
## BindNodeSignal 用法
```csharp
using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;
public partial class Hud : Control
{
[GetNode]
private Button _startButton = null!;
[GetNode]
private SpinBox _startOreSpinBox = null!;
[BindNodeSignal(nameof(_startButton), nameof(Button.Pressed))]
private void OnStartButtonPressed()
{
}
[BindNodeSignal(nameof(_startOreSpinBox), nameof(SpinBox.ValueChanged))]
private void OnStartOreValueChanged(double value)
{
}
public override void _Ready()
{
__InjectGetNodes_Generated();
__BindNodeSignals_Generated();
}
public override void _ExitTree()
{
__UnbindNodeSignals_Generated();
}
}
```
生成器会产出两个辅助方法:
- `__BindNodeSignals_Generated()`:负责统一订阅事件
- `__UnbindNodeSignals_Generated()`:负责统一解绑事件
当前设计只处理 CLR event 形式的 Godot 事件绑定,不会自动调用 `Connect()` / `Disconnect()`

View File

@ -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

View File

@ -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>

View File

@ -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;
}
}

View File

@ -16,3 +16,4 @@ global using System.Collections.Generic;
global using System.Linq;
global using System.Threading;
global using System.Threading.Tasks;
global using Microsoft.CodeAnalysis;

View File

@ -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()
{

View File

@ -220,6 +220,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;