Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,283 @@ await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
}

[Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Inherited_Interface()
{
const string source = """
#nullable enable
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions;

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

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}

namespace TestApp
{
public interface IKeyValue<TKey, TValue>
{
}

public interface IRegistry<TKey, TValue>
{
void Registry(IKeyValue<TKey, TValue> mapping);
}

public interface IAssetRegistry<TValue> : IRegistry<string, TValue>
{
}

public sealed class IntConfig : IKeyValue<string, int>
{
}

[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly IAssetRegistry<int>? _registry = null;

[RegisterExportedCollection(nameof(_registry), "Registry")]
public List<IntConfig>? Values { get; } = new();
}
}
""";

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

namespace TestApp;

partial class Bootstrapper
{
private void __RegisterExportedCollections_Generated()
{
if (this.Values is not null && this._registry is not null)
{
foreach (var __generatedItem in this.Values)
{
this._registry.Registry(__generatedItem);
}
}
}
}

""";

await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
}
Comment thread
GeWuYou marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.

[Test]
public async Task Reports_Diagnostic_When_Register_Method_Is_Only_Explicitly_Implemented_Interface_Member()
{
const string source = """
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions;

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

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}

namespace TestApp
{
public interface IRegistry
{
void Register(int value);
}

public sealed class ExplicitRegistry : IRegistry
{
void IRegistry.Register(int value) { }
}

[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly ExplicitRegistry _registry = new();

[RegisterExportedCollection(nameof(_registry), "Register")]
public List<int> {|#0:Values|} { get; } = new();
}
}
""";

var test = new CSharpSourceGeneratorTest<AutoRegisterExportedCollectionsGenerator, DefaultVerifier>
{
TestState =
{
Sources = { source }
},
DisabledDiagnostics = { "GF_Common_Trace_001" },
TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

test.ExpectedDiagnostics.Add(new DiagnosticResult("GF_AutoExport_003", DiagnosticSeverity.Error)
.WithLocation(0)
.WithArguments("Register", "_registry", "Values"));

await test.RunAsync();
}

[Test]
public async Task Generates_Batch_Registration_Method_When_Register_Method_Comes_From_Base_Class()
{
const string source = """
#nullable enable
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions;

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

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}

namespace TestApp
{
public class BaseRegistry
{
public void Register(int value) { }
}

public sealed class DerivedRegistry : BaseRegistry
{
}

[AutoRegisterExportedCollections]
public partial class Bootstrapper
{
private readonly DerivedRegistry? _registry = new();

[RegisterExportedCollection(nameof(_registry), nameof(BaseRegistry.Register))]
public List<int>? Values { get; } = new();
}
}
""";

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

namespace TestApp;

partial class Bootstrapper
{
private void __RegisterExportedCollections_Generated()
{
if (this.Values is not null && this._registry is not null)
{
foreach (var __generatedItem in this.Values)
{
this._registry.Register(__generatedItem);
}
}
}
}

""";

await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
}

[Test]
public async Task Generates_Batch_Registration_Method_When_Registry_Member_Comes_From_Base_Class()
{
const string source = """
#nullable enable
using System;
using System.Collections.Generic;
using GFramework.Godot.SourceGenerators.Abstractions;

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

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, Inherited = false, AllowMultiple = false)]
public sealed class RegisterExportedCollectionAttribute : Attribute
{
public RegisterExportedCollectionAttribute(string registryMemberName, string registerMethodName) { }
}
}

namespace TestApp
{
public sealed class IntRegistry
{
public void Register(int value) { }
}

public abstract class BootstrapperBase
{
protected readonly IntRegistry? _registry = new();
}

[AutoRegisterExportedCollections]
public partial class Bootstrapper : BootstrapperBase
{
[RegisterExportedCollection(nameof(_registry), nameof(IntRegistry.Register))]
public List<int>? Values { get; } = new();
}
}
""";

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

namespace TestApp;

partial class Bootstrapper
{
private void __RegisterExportedCollections_Generated()
{
if (this.Values is not null && this._registry is not null)
{
foreach (var __generatedItem in this.Values)
{
this._registry.Register(__generatedItem);
}
}
}
}

""";

await GeneratorTest<AutoRegisterExportedCollectionsGenerator>.RunAsync(
source,
("TestApp_Bootstrapper.AutoRegisterExportedCollections.g.cs", expected));
}

[Test]
public async Task Reports_Diagnostic_When_Collection_Member_Is_Not_Instance_Readable()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,7 @@ private static bool TryCreateRegistration(
out var registerMethodName))
return false;

var registryMember = ownerType.GetMembers(registryMemberName)
.FirstOrDefault(member => member is IFieldSymbol or IPropertySymbol);
var registryMember = FindRegistryMember(ownerType, registryMemberName);

if (registryMember is null)
{
Expand All @@ -236,7 +235,8 @@ private static bool TryCreateRegistration(
return false;
}

if (!IsInstanceReadableMember(registryMember))
if (!IsInstanceReadableMember(registryMember) ||
!compilation.IsSymbolAccessibleWithin(registryMember, ownerType))
{
context.ReportDiagnostic(Diagnostic.Create(
AutoRegisterExportedCollectionsDiagnostics.RegistryMemberMustBeInstanceReadable,
Expand Down Expand Up @@ -268,8 +268,7 @@ private static bool TryCreateRegistration(
return false;
}

var hasCompatibleMethod = registryType.GetMembers(registerMethodName)
.OfType<IMethodSymbol>()
var hasCompatibleMethod = EnumerateCandidateMethods(registryType, registerMethodName)
.Any(method =>
!method.IsStatic &&
method.Parameters.Length == 1 &&
Expand Down Expand Up @@ -319,6 +318,64 @@ private static bool CanAcceptElementType(
return compilation.ClassifyConversion(elementType, parameterType).IsImplicit;
}

private static ISymbol? FindRegistryMember(
INamedTypeSymbol ownerType,
string registryMemberName)
{
for (var currentType = ownerType; currentType is not null; currentType = currentType.BaseType)
{
// Search the owner hierarchy one level at a time so the generator follows the same
// name-hiding order as `this.<member>` in generated code.
var candidateMember = currentType.GetMembers(registryMemberName)
.FirstOrDefault(static member => member is IFieldSymbol or IPropertySymbol);

if (candidateMember is not null)
return candidateMember;
}

return null;
}

/// <summary>
/// 枚举给定注册表类型上可能承载批量注册入口的候选实例方法。
/// </summary>
/// <param name="registryType">声明注册表成员的静态类型。</param>
/// <param name="registerMethodName">特性参数中声明的注册方法名称。</param>
/// <returns>
/// 按“当前类型 -> 基类链 -> 接口继承链(仅当静态类型本身是接口)”顺序返回所有同名方法,
/// 供后续签名和可访问性筛选使用。
/// </returns>
/// <remarks>
/// 生成器需要沿当前类型和基类链查找方法,因为用户代码可能通过派生类字段引用基类实现;
/// 当注册表成员本身声明为接口类型时,还要继续沿接口继承链查找由父接口声明的契约方法。
/// 对类或结构体不遍历 <see cref="INamedTypeSymbol.AllInterfaces"/>,避免把仅能通过接口调用的显式实现
/// 误判为可由 <c>this.&lt;registry&gt;.&lt;method&gt;(...)</c> 直接访问的方法。
/// 这里故意不做去重:同一个语义方法可能同时经由覆盖链、接口继承或显式声明被枚举多次,但当前调用方只使用
/// <c>Any</c> 判断“是否存在至少一个可用候选”,因此重复项只会带来额外的符号检查成本,不会改变生成结果或诊断边界。
/// </remarks>
private static IEnumerable<IMethodSymbol> EnumerateCandidateMethods(
INamedTypeSymbol registryType,
string registerMethodName)
{
foreach (var method in registryType.GetMembers(registerMethodName).OfType<IMethodSymbol>())
yield return method;

for (var baseType = registryType.BaseType; baseType is not null; baseType = baseType.BaseType)
{
foreach (var method in baseType.GetMembers(registerMethodName).OfType<IMethodSymbol>())
yield return method;
}

if (registryType.TypeKind != TypeKind.Interface)
yield break;

foreach (var interfaceType in registryType.AllInterfaces)
{
foreach (var method in interfaceType.GetMembers(registerMethodName).OfType<IMethodSymbol>())
yield return method;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Comment thread
GeWuYou marked this conversation as resolved.

private static bool TryGetRegistrationAttributeArguments(
SourceProductionContext context,
ISymbol collectionMember,
Expand Down Expand Up @@ -434,11 +491,15 @@ private static string GetHintName(INamedTypeSymbol typeSymbol)

private static string GetTypeDeclarationKeyword(INamedTypeSymbol typeSymbol)
{
return typeSymbol.IsRecord
? typeSymbol.TypeKind == TypeKind.Struct ? "partial record struct" : "partial record"
: typeSymbol.TypeKind == TypeKind.Struct
? "partial struct"
: "partial class";
return typeSymbol switch
{
{ IsRecord: true, TypeKind: TypeKind.Struct } => "partial record struct",
{ IsRecord: true } => "partial record",
{ TypeKind: TypeKind.Struct } => "partial struct",
{ TypeKind: TypeKind.Class } => "partial class",
{ TypeKind: TypeKind.Interface } => "partial interface",
_ => throw new NotSupportedException($"Unsupported type: {typeSymbol.TypeKind}")
};
}

private static string GetTypeDeclarationName(INamedTypeSymbol typeSymbol)
Expand Down
Loading