From 5de49ebc60447e81d9ebdbee584739ed6ec6f341 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 14 Sep 2026 13:59:15 +0200 Subject: [PATCH] Resolve hidden inspector members to the most-derived declaration A keyed type that hides a base member with `new` offered two candidates for one name. The inspector now keeps the most-derived declaration, after dropping [IgnoreMember] ones, as the registry's own-member scan does. Co-Authored-By: Claude Opus 5 --- src/GameCult.Caching/CultInspectorModel.cs | 7 ++++++- .../CultInspectorModelTests.cs | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/GameCult.Caching/CultInspectorModel.cs b/src/GameCult.Caching/CultInspectorModel.cs index 90e4cb25..dd7e5e1c 100644 --- a/src/GameCult.Caching/CultInspectorModel.cs +++ b/src/GameCult.Caching/CultInspectorModel.cs @@ -520,7 +520,10 @@ namespace GameCult.Caching var candidates = type.GetMembers(Public) .Where(member => member is FieldInfo field && !field.IsLiteral || member is PropertyInfo property && property.GetGetMethod() != null && property.GetIndexParameters().Length == 0) - .Where(member => !member.IsDefined(typeof(IgnoreMemberAttribute), true)); + .Where(member => !member.IsDefined(typeof(IgnoreMemberAttribute), true)) + // A member hidden with `new` is its most-derived declaration, as the registry's own-member scan reads it. + .GroupBy(member => member.Name) + .Select(hides => hides.OrderByDescending(member => Ancestry(member.DeclaringType)).First()); IEnumerable<(MemberInfo Member, int Slot)> slotted; if (type.IsDefined(typeof(CultDocumentAttribute), false)) { @@ -575,6 +578,8 @@ namespace GameCult.Caching private static CultInspectorShape Unsupported(Type type, string reason) => new CultInspectorShape(type, CultInspectorValueKind.Unsupported) { Reason = reason }; + private static int Ancestry(Type? type) => type == null ? 0 : 1 + Ancestry(type.BaseType); + private static Type TypeOf(MemberInfo member) => member is FieldInfo field ? field.FieldType : ((PropertyInfo)member).PropertyType; private static bool Assignable(MemberInfo member, bool allowPrivate) diff --git a/tests/GameCult.Caching.Tests/CultInspectorModelTests.cs b/tests/GameCult.Caching.Tests/CultInspectorModelTests.cs index 97d09c56..4929f448 100644 --- a/tests/GameCult.Caching.Tests/CultInspectorModelTests.cs +++ b/tests/GameCult.Caching.Tests/CultInspectorModelTests.cs @@ -39,6 +39,10 @@ namespace GameCult.Caching.Tests var id = Model().MembersOf(typeof(InspectFixed)).Single(); Assert.That(id.IsAssignable, Is.False, "a get-only member its constructor fills cannot be assigned"); Assert.That(id.IsReadOnly, Is.True); + + var hidden = Model().MembersOf(typeof(InspectHider)).Single(); + Assert.That(hidden.Member.DeclaringType, Is.EqualTo(typeof(InspectHider)), "a `new` member is its most-derived declaration"); + Assert.That(hidden.ValueType, Is.EqualTo(typeof(string))); } [Test] @@ -242,6 +246,20 @@ namespace GameCult.Caching.Tests public float Side; } + [MessagePackObject] + public class InspectHidden + { + [Key(0)] + public int Value; + } + + [MessagePackObject] + public sealed class InspectHider : InspectHidden + { + [Key(0)] + public new string Value = string.Empty; + } + public readonly record struct InspectPair(float x, float y); public interface IFakeDrawer