R-AL: restore ReferenceMembers' IsReference guard and fix D10's blind spot
CultNetSelectionEvaluator.ReferenceMembers dropped the `!member.IsReference` guard on the premise that a data member's role string could never coincide with a reference member's alias - false. CultCache's D10 only rejects a collision between two *aliased* members; an unaliased data member's bare name was never checked against another member's alias. A data member named `Owner` plus a reference member aliased `Owner` registered fine, then faulted every hop through it: ReferenceMembers yielded the data member, ReferencesOf matched it to the reference member's edges by role string, and `member.TargetType!` was null. Two independent fixes, both regression-tested: - Restore the guard, delete the false comment. Pinned directly by hand- building the colliding descriptor via reflection (bypassing the registry, since D10b below now refuses that shape at the only production door). - D10b: registration now refuses a plain member whose bare name collides with a reference member's alias in the reference-role namespace (IndexAlias ?? MemberName), not just alias-vs-alias. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
fc51e9a408
commit
375b6177ae
2 changed files with 46 additions and 6 deletions
|
|
@ -1096,6 +1096,25 @@ namespace GameCult.Caching
|
|||
rejections.Add(DuplicateIndexAliasMessage(type.Name, Qualified(pair[0].Entry.Member), Qualified(pair[1].Entry.Member), group.Key!));
|
||||
}
|
||||
|
||||
// D10b: the reference-role namespace is (IndexAlias ?? MemberName) for every member, not
|
||||
// only the aliased ones D10 groups above. A member with no [CultIndex] still contributes
|
||||
// its bare name to that namespace, and a reference member declaring the same string as its
|
||||
// alias collides with it - the collision D10 cannot see because an unaliased member's Alias
|
||||
// is null.
|
||||
foreach (var group in keyed
|
||||
.Select(entry => (
|
||||
Entry: entry,
|
||||
Role: ResolveIndexAlias(entry.Member) ?? entry.Member.Name,
|
||||
IsReference: PersistedMember.IsReferenceCandidate(entry.Member, entry.MemberType)))
|
||||
.GroupBy(pair => pair.Role, StringComparer.Ordinal)
|
||||
.Where(group => group.Select(pair => pair.Entry.Member.Name).Distinct().Count() > 1
|
||||
&& group.Any(pair => pair.IsReference))
|
||||
.OrderBy(group => group.Key, StringComparer.Ordinal))
|
||||
{
|
||||
var pair = group.Take(2).ToArray();
|
||||
rejections.Add(DuplicateReferenceRoleMessage(type.Name, Qualified(pair[0].Entry.Member), Qualified(pair[1].Entry.Member), group.Key));
|
||||
}
|
||||
|
||||
// D11: a member declared as a reference (by [CultReference] or a bare CultRecordRef<T> type)
|
||||
// whose CLR shape the cache cannot walk. many:false needs CultRecordRef<T> itself; many:true
|
||||
// needs IEnumerable<CultRecordRef<T>> or IDictionary<CultRecordRef<T>, V>. A declaration the
|
||||
|
|
@ -1355,6 +1374,14 @@ namespace GameCult.Caching
|
|||
private static string DuplicateIndexAliasMessage(string documentTypeName, string first, string second, string alias) =>
|
||||
$"Cult document {documentTypeName} members {first} and {second} both declare index alias \"{alias}\"; an index alias must name one member.";
|
||||
|
||||
// D10b (R-AL): a plain member's bare name colliding with a reference member's index alias.
|
||||
// CultDocumentRegistry.ReferencesOf and CultNetSelectionEvaluator.ReferenceMembers both key
|
||||
// reference lookups off (IndexAlias ?? MemberName); D10 above only groups members that
|
||||
// declare an alias, so an unaliased member's bare name sharing that string with a reference
|
||||
// member's alias registered silently and faulted every hop through it at resolution time.
|
||||
private static string DuplicateReferenceRoleMessage(string documentTypeName, string first, string second, string role) =>
|
||||
$"Cult document {documentTypeName} members {first} and {second} both resolve to reference role \"{role}\" (index alias, or bare member name when unaliased); a reference role must name one member.";
|
||||
|
||||
private static string UnsupportedReferenceShapeMessage(string documentTypeName, string member) =>
|
||||
$"Cult document {documentTypeName} member {member} declares a reference the cache cannot walk; many:false needs CultRecordRef<T>, many:true needs IEnumerable<CultRecordRef<T>> or IDictionary<CultRecordRef<T>, V>.";
|
||||
|
||||
|
|
@ -1525,6 +1552,15 @@ namespace GameCult.Caching
|
|||
};
|
||||
}
|
||||
|
||||
// Mirrors the isReference computation FromMember uses below, callable from DiscoverMembers
|
||||
// (D10b) before a slot is turned into a full PersistedMember.
|
||||
internal static bool IsReferenceCandidate(MemberInfo member, Type memberType)
|
||||
{
|
||||
var referenceAttribute = member.GetCustomAttribute<CultReferenceAttribute>();
|
||||
var targetType = ResolveReferenceTarget(memberType, referenceAttribute?.TargetType);
|
||||
return targetType != null || referenceAttribute != null;
|
||||
}
|
||||
|
||||
private static Type? ResolveReferenceTarget(Type memberType, Type? explicitTarget)
|
||||
{
|
||||
if (explicitTarget != null)
|
||||
|
|
|
|||
|
|
@ -448,16 +448,20 @@ namespace GameCult.Networking
|
|||
return incoming;
|
||||
}
|
||||
|
||||
// R-AK: no `!member.IsReference` guard here - CultCache.ReferencesOf (CultCache.cs:262-278)
|
||||
// already searches only its own RichMembers' reference members for a role match and
|
||||
// returns empty for anything else, including a data member's own alias (index aliases are
|
||||
// unique per descriptor, so a data member's role string can never coincide with a
|
||||
// reference member's). A second copy of the same exclusion here was duplicate authority,
|
||||
// not a second decision - CultCache owns "is this member a reference" once.
|
||||
// R-AL: the `!member.IsReference` guard is restored. R-AK's removal argued a data member's
|
||||
// role string could never coincide with a reference member's because index aliases are
|
||||
// unique per descriptor - false. CultCache's D10 only rejects a collision between two
|
||||
// *aliased* members (CultCache.cs:1085-1097); a plain data member with no [CultIndex]
|
||||
// contributes its bare MemberName to the same role namespace unchecked (fixed for new
|
||||
// registrations by D10b, CultCache.cs ~1099, but this guard is the caller-side backstop for
|
||||
// whatever is already registered). Without it, a data member sharing a reference member's
|
||||
// role is yielded here, ReferencesOf then matches it to the *reference* member's edges by
|
||||
// role string, and `member.TargetType!` is null - ArgumentNullException on every hop.
|
||||
private static IEnumerable<CultDocumentMemberView> ReferenceMembers(CultDocumentDescriptor descriptor, string? role)
|
||||
{
|
||||
foreach (var member in descriptor.DeclaredMembers)
|
||||
{
|
||||
if (!member.IsReference) continue;
|
||||
if (role != null && (member.IndexAlias ?? member.MemberName) != role) continue;
|
||||
yield return member;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue