Skip to content

Commit be39689

Browse files
feat: Add constructed generic candidate support for SubclassSelector
Add support for discovering and constructing closed generic candidate types (e.g. ConstantValueProvider<int>) when the field type is a closed generic interface or base class (Unity 2023.2+). Previously, only non-generic concrete classes that explicitly closed the type parameter were shown in the popup. Changes: - Relax intrinsic type policy to allow constructed generics through - Infer type arguments and construct closed types via MakeGenericType - Add generic-aware display names for popup menu and inline labels - Merge dual assembly scans into a single pass - Remove redundant filtering in TypeCandiateService - Cache attribute lookups, type paths, and nicified names - Pre-sort type array once at construction instead of on every Show
1 parent 34d7bb3 commit be39689

7 files changed

Lines changed: 214 additions & 36 deletions

File tree

Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/AdvancedTypePopup.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public static void AddTo (AdvancedDropdownItem root, IEnumerable<Type> types)
3939
};
4040
root.AddChild(nullItem);
4141

42-
Type[] typeArray = types.OrderByType().ToArray();
42+
Type[] typeArray = types as Type[] ?? types.ToArray();
4343

4444
// Single namespace if the root has one namespace and the nest is unbranched.
4545
bool isSingleNamespace = true;
@@ -111,7 +111,8 @@ public static void AddTo (AdvancedDropdownItem root, IEnumerable<Type> types)
111111
}
112112

113113
// Add type item.
114-
var item = new AdvancedTypePopupItem(type, ObjectNames.NicifyVariableName(splittedTypePath[splittedTypePath.Length - 1]))
114+
var item = new AdvancedTypePopupItem(type, TypeMenuUtility.CachedNicifyVariableName(splittedTypePath[splittedTypePath.Length - 1]))
115+
115116
{
116117
id = itemCount++
117118
};
@@ -145,7 +146,7 @@ public AdvancedTypePopup (IEnumerable<Type> types, int maxLineCount, AdvancedDro
145146

146147
public void SetTypes (IEnumerable<Type> types)
147148
{
148-
this.types = types.ToArray();
149+
this.types = types.OrderByType().ToArray();
149150
}
150151

151152
protected override AdvancedDropdownItem BuildRoot ()
@@ -163,6 +164,6 @@ protected override void ItemSelected (AdvancedDropdownItem item)
163164
OnItemSelected?.Invoke(typePopupItem);
164165
}
165166
}
166-
167+
167168
}
168169
}

Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/SubclassSelectorDrawer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,13 +207,13 @@ private GUIContent GetTypeName (SerializedProperty property)
207207
typeName = typeMenu.GetTypeNameWithoutPath();
208208
if (!string.IsNullOrWhiteSpace(typeName))
209209
{
210-
typeName = ObjectNames.NicifyVariableName(typeName);
210+
typeName = TypeMenuUtility.CachedNicifyVariableName(typeName);
211211
}
212212
}
213213

214214
if (string.IsNullOrWhiteSpace(typeName))
215215
{
216-
typeName = ObjectNames.NicifyVariableName(type.Name);
216+
typeName = TypeMenuUtility.CachedNicifyVariableName(TypeMenuUtility.GetNiceGenericName(type));
217217
}
218218

219219
GUIContent result = new GUIContent(typeName);

Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeMenuUtility.cs

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,50 +10,127 @@ public static class TypeMenuUtility
1010

1111
public const string NullDisplayName = "<null>";
1212

13+
private static readonly Dictionary<Type, string[]> splittedTypePathCache = new Dictionary<Type, string[]>();
14+
private static readonly Dictionary<Type, AddTypeMenuAttribute> attributeCache = new Dictionary<Type, AddTypeMenuAttribute>();
15+
private static readonly Dictionary<string, string> nicifyCache = new Dictionary<string, string>();
16+
17+
1318
public static AddTypeMenuAttribute GetAttribute (Type type)
1419
{
15-
return Attribute.GetCustomAttribute(type, typeof(AddTypeMenuAttribute)) as AddTypeMenuAttribute;
16-
}
20+
if (type == null)
21+
{
22+
return null;
23+
}
24+
25+
if (attributeCache.TryGetValue(type, out AddTypeMenuAttribute cached))
26+
{
27+
return cached;
28+
}
1729

18-
public static string[] GetSplittedTypePath (Type type)
30+
var result = Attribute.GetCustomAttribute(type, typeof(AddTypeMenuAttribute)) as AddTypeMenuAttribute;
31+
attributeCache.Add(type, result);
32+
return result;
33+
}
34+
35+
public static string[] GetSplittedTypePath(Type type)
1936
{
37+
if (splittedTypePathCache.TryGetValue(type, out string[] cached))
38+
{
39+
return cached;
40+
}
41+
42+
string[] result;
43+
2044
AddTypeMenuAttribute typeMenu = GetAttribute(type);
2145
if (typeMenu != null)
2246
{
23-
return typeMenu.GetSplittedMenuName();
47+
result = typeMenu.GetSplittedMenuName();
2448
}
2549
else
2650
{
27-
int splitIndex = type.FullName.LastIndexOf('.');
51+
string fullName = GetNiceGenericFullName(type);
52+
int splitIndex = fullName.LastIndexOf('.');
2853
if (splitIndex >= 0)
2954
{
30-
return new string[] { type.FullName.Substring(0, splitIndex), type.FullName.Substring(splitIndex + 1) };
55+
result = new string[] { fullName.Substring(0, splitIndex), fullName.Substring(splitIndex + 1) };
3156
}
3257
else
3358
{
34-
return new string[] { type.Name };
59+
result = new string[] { GetNiceGenericName(type) };
3560
}
3661
}
62+
63+
splittedTypePathCache.Add(type, result);
64+
return result;
3765
}
3866

39-
public static IEnumerable<Type> OrderByType (this IEnumerable<Type> source)
67+
public static IEnumerable<Type> OrderByType(this IEnumerable<Type> source)
4068
{
4169
return source.OrderBy(type =>
4270
{
4371
if (type == null)
4472
{
4573
return -999;
4674
}
75+
4776
return GetAttribute(type)?.Order ?? 0;
4877
}).ThenBy(type =>
4978
{
5079
if (type == null)
5180
{
5281
return null;
5382
}
54-
return GetAttribute(type)?.MenuName ?? type.Name;
83+
84+
return GetAttribute(type)?.MenuName ?? GetNiceGenericName(type);
5585
});
5686
}
5787

88+
public static string GetNiceGenericName(Type type)
89+
{
90+
if (!type.IsGenericType)
91+
{
92+
return type.Name;
93+
}
94+
95+
string baseName = type.Name;
96+
int backtickIndex = baseName.IndexOf('`');
97+
if (backtickIndex > 0)
98+
{
99+
baseName = baseName.Substring(0, backtickIndex);
100+
}
101+
102+
Type[] args = type.GetGenericArguments();
103+
string argsJoined = string.Join(", ", args.Select(a => GetNiceGenericName(a)));
104+
return $"{baseName}<{argsJoined}>";
105+
}
106+
107+
private static string GetNiceGenericFullName(Type type)
108+
{
109+
if (!type.IsGenericType)
110+
{
111+
return type.FullName ?? type.Name;
112+
}
113+
114+
string ns = type.Namespace;
115+
string niceName = GetNiceGenericName(type);
116+
return string.IsNullOrEmpty(ns) ? niceName : $"{ns}.{niceName}";
117+
}
118+
119+
public static string CachedNicifyVariableName (string name)
120+
{
121+
if (string.IsNullOrEmpty(name))
122+
{
123+
return name;
124+
}
125+
126+
if (nicifyCache.TryGetValue(name, out string cached))
127+
{
128+
return cached;
129+
}
130+
131+
string result = ObjectNames.NicifyVariableName(name);
132+
nicifyCache.Add(name, result);
133+
return result;
134+
}
58135
}
59136
}

Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/IntrinsicTypePolicy/DefaultIntrinsicTypePolicy.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ public bool IsAllowed (Type candiateType)
1212
return
1313
(candiateType.IsPublic || candiateType.IsNestedPublic || candiateType.IsNestedPrivate) &&
1414
!candiateType.IsAbstract &&
15-
!candiateType.IsGenericType &&
15+
!candiateType.ContainsGenericParameters &&
1616
!candiateType.IsPrimitive &&
1717
!candiateType.IsEnum &&
1818
!typeof(UnityEngine.Object).IsAssignableFrom(candiateType) &&

Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeCandiateProvider/Unity_2023_2_OrNewer_TypeCandiateProvider.cs

Lines changed: 116 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,22 +46,32 @@ private IEnumerable<Type> GetTypesWithGeneric (Type baseType)
4646

4747
result = new List<Type>();
4848

49-
IEnumerable<Type> types = EnumerateAllTypesSafely();
50-
foreach (Type type in types)
49+
// Prepare generic inference data upfront
50+
bool baseIsConstructedGeneric = baseType.IsGenericType && !baseType.IsGenericTypeDefinition && !baseType.ContainsGenericParameters;
51+
Type baseGenericDef = baseIsConstructedGeneric ? baseType.GetGenericTypeDefinition() : null;
52+
Type[] baseTypeArgs = baseIsConstructedGeneric ? baseType.GetGenericArguments() : null;
53+
54+
// Single pass over all types
55+
foreach (Type type in EnumerateAllTypesSafely())
5156
{
52-
if (!intrinsicTypePolicy.IsAllowed(type))
57+
// Existing: check closed/non-generic candidates
58+
if (intrinsicTypePolicy.IsAllowed(type) && typeCompatibilityPolicy.IsCompatible(baseType, type))
5359
{
60+
result.Add(type);
5461
continue;
5562
}
56-
if (!typeCompatibilityPolicy.IsCompatible(baseType, type))
63+
64+
// New: try to close open generic candidates
65+
if (baseIsConstructedGeneric && type.IsGenericTypeDefinition && Attribute.IsDefined(type, typeof(SerializableAttribute)))
5766
{
58-
continue;
67+
Type closedType = TryCloseGenericType(type, baseGenericDef, baseTypeArgs);
68+
if (closedType != null && intrinsicTypePolicy.IsAllowed(closedType))
69+
{
70+
result.Add(closedType);
71+
}
5972
}
60-
61-
result.Add(type);
6273
}
6374

64-
// Include the base type itself if allowed
6575
if (intrinsicTypePolicy.IsAllowed(baseType) && typeCompatibilityPolicy.IsCompatible(baseType, baseType))
6676
{
6777
result.Add(baseType);
@@ -99,6 +109,104 @@ private static IEnumerable<Type> EnumerateAllTypesSafely ()
99109
}
100110
}
101111
}
112+
private static Type TryCloseGenericType (Type openCandidateType, Type baseGenericDef, Type[] baseTypeArgs)
113+
{
114+
// openCandidateType is e.g. ConstantValueProvider<T>
115+
// baseGenericDef is e.g. IValueProvider<>
116+
// baseTypeArgs is e.g. [int]
117+
118+
Type[] candidateGenericParams = openCandidateType.GetGenericArguments();
119+
Type[] resolvedArgs = new Type[candidateGenericParams.Length];
120+
121+
// Walk the candidate's interfaces to find one matching the base generic definition
122+
foreach (Type iface in openCandidateType.GetInterfaces())
123+
{
124+
if (!iface.IsGenericType) continue;
125+
if (iface.GetGenericTypeDefinition() != baseGenericDef) continue;
126+
127+
Type[] ifaceArgs = iface.GetGenericArguments();
128+
if (ifaceArgs.Length != baseTypeArgs.Length) continue;
129+
130+
if (TryMapTypeArguments(candidateGenericParams, ifaceArgs, baseTypeArgs, resolvedArgs))
131+
{
132+
return TryMakeGenericTypeSafe(openCandidateType, resolvedArgs);
133+
}
134+
}
135+
136+
// Walk base class chain
137+
for (Type t = openCandidateType.BaseType; t != null && t != typeof(object); t = t.BaseType)
138+
{
139+
if (!t.IsGenericType) continue;
140+
if (t.GetGenericTypeDefinition() != baseGenericDef) continue;
141+
142+
Type[] tArgs = t.GetGenericArguments();
143+
if (tArgs.Length != baseTypeArgs.Length) continue;
144+
145+
if (TryMapTypeArguments(candidateGenericParams, tArgs, baseTypeArgs, resolvedArgs))
146+
{
147+
return TryMakeGenericTypeSafe(openCandidateType, resolvedArgs);
148+
}
149+
}
150+
151+
return null;
152+
}
153+
154+
private static bool TryMapTypeArguments (Type[] candidateGenericParams, Type[] ifaceArgs, Type[] baseTypeArgs, Type[] resolvedArgs)
155+
{
156+
// Reset
157+
for (int i = 0; i < resolvedArgs.Length; i++)
158+
{
159+
resolvedArgs[i] = null;
160+
}
161+
162+
// For each type argument in the interface/base, map it back to the candidate's generic parameter
163+
// e.g. IValueProvider<T> has ifaceArgs=[T], baseTypeArgs=[int]
164+
// We need to find that T is candidateGenericParams[0], so resolvedArgs[0] = int
165+
for (int i = 0; i < ifaceArgs.Length; i++)
166+
{
167+
Type ifaceArg = ifaceArgs[i];
168+
Type targetArg = baseTypeArgs[i];
169+
170+
if (ifaceArg.IsGenericParameter)
171+
{
172+
int position = ifaceArg.GenericParameterPosition;
173+
if (position < 0 || position >= resolvedArgs.Length) return false;
174+
175+
if (resolvedArgs[position] != null && resolvedArgs[position] != targetArg)
176+
{
177+
// Conflicting mapping for the same parameter
178+
return false;
179+
}
180+
resolvedArgs[position] = targetArg;
181+
}
182+
else
183+
{
184+
// The interface argument is already concrete — it must match exactly
185+
if (ifaceArg != targetArg) return false;
186+
}
187+
}
188+
189+
// Check all parameters were resolved
190+
for (int i = 0; i < resolvedArgs.Length; i++)
191+
{
192+
if (resolvedArgs[i] == null) return false;
193+
}
194+
195+
return true;
196+
}
197+
198+
private static Type TryMakeGenericTypeSafe (Type openType, Type[] typeArgs)
199+
{
200+
try
201+
{
202+
return openType.MakeGenericType(typeArgs);
203+
}
204+
catch (ArgumentException)
205+
{
206+
// Constraint violation (e.g. where T : struct but we passed a class)
207+
return null;
208+
}
209+
}
102210
}
103211
}
104212
#endif

Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeCandiateService.cs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,11 @@ public sealed class TypeCandiateService
88
{
99

1010
private readonly ITypeCandiateProvider typeCandiateProvider;
11-
private readonly IIntrinsicTypePolicy intrinsicTypePolicy;
12-
private readonly ITypeCompatibilityPolicy typeCompatibilityPolicy;
13-
1411
private readonly Dictionary<Type, Type[]> typeCache = new Dictionary<Type, Type[]>();
1512

16-
public TypeCandiateService (ITypeCandiateProvider typeCandiateProvider, IIntrinsicTypePolicy intrinsicTypePolicy, ITypeCompatibilityPolicy typeCompatibilityPolicy)
13+
public TypeCandiateService (ITypeCandiateProvider typeCandiateProvider)
1714
{
1815
this.typeCandiateProvider = typeCandiateProvider ?? throw new ArgumentNullException(nameof(typeCandiateProvider));
19-
this.intrinsicTypePolicy = intrinsicTypePolicy ?? throw new ArgumentNullException(nameof(intrinsicTypePolicy));
20-
this.typeCompatibilityPolicy = typeCompatibilityPolicy ?? throw new ArgumentNullException(nameof(typeCompatibilityPolicy));
2116
}
2217

2318
public IReadOnlyList<Type> GetDisplayableTypes (Type baseType)
@@ -30,11 +25,8 @@ public IReadOnlyList<Type> GetDisplayableTypes (Type baseType)
3025
{
3126
return cachedTypes;
3227
}
33-
34-
var candiateTypes = typeCandiateProvider.GetTypeCandidates(baseType);
35-
var result = candiateTypes
36-
.Where(intrinsicTypePolicy.IsAllowed)
37-
.Where(t => typeCompatibilityPolicy.IsCompatible(baseType, t))
28+
29+
var result = typeCandiateProvider.GetTypeCandidates(baseType)
3830
.Distinct()
3931
.ToArray();
4032

Assets/MackySoft/MackySoft.SerializeReferenceExtensions/Editor/TypeSearch/TypeSearchService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ static TypeSearchService ()
2020
TypeCandiateProvider = DefaultTypeCandiateProvider.Instance;
2121
#endif
2222

23-
TypeCandiateService = new TypeCandiateService(TypeCandiateProvider, IntrinsicTypePolicy, TypeCompatibilityPolicy);
23+
TypeCandiateService = new TypeCandiateService(TypeCandiateProvider);
2424
}
2525
}
2626
}

0 commit comments

Comments
 (0)