Fix proto generation for collection types (NpgsqlPolygon, etc.)
- Add IsCollectionTypeByInterface() to detect types implementing IList<T>, ICollection<T>, IEnumerable<T> - Add GetCollectionElementTypeByInterface() to extract element type from collection interfaces - Add IsCollectionInternalProperty() to filter out Count, Capacity, IsReadOnly, etc. - Update GenerateComplexTypeMessage to generate `repeated T items` for collection types - Filter out indexers (!p.IsIndexer) and collection-internal properties from all property extraction This fixes the invalid proto syntax where C# indexers (this[]) were being generated as proto fields, causing proto compilation errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f76dbb1a97
commit
99aebcf314
@ -320,7 +320,9 @@ internal class ProtoFileGenerator
|
|||||||
|
|
||||||
var properties = type.GetMembers()
|
var properties = type.GetMembers()
|
||||||
.OfType<IPropertySymbol>()
|
.OfType<IPropertySymbol>()
|
||||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public)
|
.Where(p => p.DeclaredAccessibility == Accessibility.Public &&
|
||||||
|
!p.IsIndexer &&
|
||||||
|
!ProtoFileTypeMapper.IsCollectionInternalProperty(p.Name))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
// Collect nested complex types to generate after closing this message
|
// Collect nested complex types to generate after closing this message
|
||||||
@ -423,14 +425,38 @@ internal class ProtoFileGenerator
|
|||||||
_messagesBuilder.AppendLine($"// {type.Name} entity");
|
_messagesBuilder.AppendLine($"// {type.Name} entity");
|
||||||
_messagesBuilder.AppendLine($"message {type.Name} {{");
|
_messagesBuilder.AppendLine($"message {type.Name} {{");
|
||||||
|
|
||||||
var properties = type.GetMembers()
|
|
||||||
.OfType<IPropertySymbol>()
|
|
||||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
// Collect nested complex types to generate after closing this message
|
// Collect nested complex types to generate after closing this message
|
||||||
var nestedComplexTypes = new List<INamedTypeSymbol>();
|
var nestedComplexTypes = new List<INamedTypeSymbol>();
|
||||||
|
|
||||||
|
// Check if this type is a collection (implements IList<T>, ICollection<T>, etc.)
|
||||||
|
var collectionElementType = ProtoFileTypeMapper.GetCollectionElementTypeByInterface(type);
|
||||||
|
if (collectionElementType != null)
|
||||||
|
{
|
||||||
|
// This type is a collection - generate a single repeated field for items
|
||||||
|
var protoElementType = ProtoFileTypeMapper.MapType(collectionElementType, out var needsImport, out var importPath);
|
||||||
|
if (needsImport && importPath != null)
|
||||||
|
{
|
||||||
|
_requiredImports.Add(importPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
_messagesBuilder.AppendLine($" repeated {protoElementType} items = 1;");
|
||||||
|
|
||||||
|
// Track the element type for nested generation
|
||||||
|
if (IsComplexType(collectionElementType) && collectionElementType is INamedTypeSymbol elementNamedType)
|
||||||
|
{
|
||||||
|
nestedComplexTypes.Add(elementNamedType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Not a collection - generate properties as usual
|
||||||
|
var properties = type.GetMembers()
|
||||||
|
.OfType<IPropertySymbol>()
|
||||||
|
.Where(p => p.DeclaredAccessibility == Accessibility.Public &&
|
||||||
|
!p.IsIndexer &&
|
||||||
|
!ProtoFileTypeMapper.IsCollectionInternalProperty(p.Name))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
int fieldNumber = 1;
|
int fieldNumber = 1;
|
||||||
foreach (var prop in properties)
|
foreach (var prop in properties)
|
||||||
{
|
{
|
||||||
@ -466,6 +492,7 @@ internal class ProtoFileGenerator
|
|||||||
|
|
||||||
fieldNumber++;
|
fieldNumber++;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_messagesBuilder.AppendLine("}");
|
_messagesBuilder.AppendLine("}");
|
||||||
_messagesBuilder.AppendLine();
|
_messagesBuilder.AppendLine();
|
||||||
@ -755,7 +782,9 @@ internal class ProtoFileGenerator
|
|||||||
int fieldNumber = 1;
|
int fieldNumber = 1;
|
||||||
|
|
||||||
foreach (var prop in type.GetMembers().OfType<IPropertySymbol>()
|
foreach (var prop in type.GetMembers().OfType<IPropertySymbol>()
|
||||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public))
|
.Where(p => p.DeclaredAccessibility == Accessibility.Public &&
|
||||||
|
!p.IsIndexer &&
|
||||||
|
!ProtoFileTypeMapper.IsCollectionInternalProperty(p.Name)))
|
||||||
{
|
{
|
||||||
if (ProtoFileTypeMapper.IsUnsupportedType(prop.Type))
|
if (ProtoFileTypeMapper.IsUnsupportedType(prop.Type))
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@ -243,4 +243,97 @@ internal static class ProtoFileTypeMapper
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a type is a collection by checking if it implements IList{T}, ICollection{T}, or IEnumerable{T}
|
||||||
|
/// This handles types like NpgsqlPolygon that implement IList{NpgsqlPoint} but aren't named "List"
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsCollectionTypeByInterface(ITypeSymbol typeSymbol)
|
||||||
|
{
|
||||||
|
if (typeSymbol is not INamedTypeSymbol namedType)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Skip string (implements IEnumerable<char>)
|
||||||
|
if (namedType.SpecialType == SpecialType.System_String)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Check all interfaces for IList<T>, ICollection<T>, or IEnumerable<T>
|
||||||
|
foreach (var iface in namedType.AllInterfaces)
|
||||||
|
{
|
||||||
|
if (iface.IsGenericType && iface.TypeArguments.Length == 1)
|
||||||
|
{
|
||||||
|
var ifaceName = iface.OriginalDefinition.ToDisplayString();
|
||||||
|
if (ifaceName == "System.Collections.Generic.IList<T>" ||
|
||||||
|
ifaceName == "System.Collections.Generic.ICollection<T>" ||
|
||||||
|
ifaceName == "System.Collections.Generic.IEnumerable<T>" ||
|
||||||
|
ifaceName == "System.Collections.Generic.IReadOnlyList<T>" ||
|
||||||
|
ifaceName == "System.Collections.Generic.IReadOnlyCollection<T>")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the element type from a collection that implements IList{T}, ICollection{T}, or IEnumerable{T}
|
||||||
|
/// Returns null if the type is not a collection
|
||||||
|
/// </summary>
|
||||||
|
public static ITypeSymbol? GetCollectionElementTypeByInterface(ITypeSymbol typeSymbol)
|
||||||
|
{
|
||||||
|
if (typeSymbol is not INamedTypeSymbol namedType)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Skip string
|
||||||
|
if (namedType.SpecialType == SpecialType.System_String)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
// Prefer IList<T> over ICollection<T> over IEnumerable<T>
|
||||||
|
ITypeSymbol? elementType = null;
|
||||||
|
int priority = 0;
|
||||||
|
|
||||||
|
foreach (var iface in namedType.AllInterfaces)
|
||||||
|
{
|
||||||
|
if (iface.IsGenericType && iface.TypeArguments.Length == 1)
|
||||||
|
{
|
||||||
|
var ifaceName = iface.OriginalDefinition.ToDisplayString();
|
||||||
|
int currentPriority = 0;
|
||||||
|
|
||||||
|
if (ifaceName == "System.Collections.Generic.IList<T>" ||
|
||||||
|
ifaceName == "System.Collections.Generic.IReadOnlyList<T>")
|
||||||
|
currentPriority = 3;
|
||||||
|
else if (ifaceName == "System.Collections.Generic.ICollection<T>" ||
|
||||||
|
ifaceName == "System.Collections.Generic.IReadOnlyCollection<T>")
|
||||||
|
currentPriority = 2;
|
||||||
|
else if (ifaceName == "System.Collections.Generic.IEnumerable<T>")
|
||||||
|
currentPriority = 1;
|
||||||
|
|
||||||
|
if (currentPriority > priority)
|
||||||
|
{
|
||||||
|
priority = currentPriority;
|
||||||
|
elementType = iface.TypeArguments[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return elementType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Collection-internal properties that should be skipped when generating proto messages
|
||||||
|
/// </summary>
|
||||||
|
private static readonly System.Collections.Generic.HashSet<string> CollectionInternalProperties = new()
|
||||||
|
{
|
||||||
|
"Count", "Capacity", "IsReadOnly", "IsSynchronized", "SyncRoot", "Keys", "Values"
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a property name is a collection-internal property that should be skipped
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsCollectionInternalProperty(string propertyName)
|
||||||
|
{
|
||||||
|
return CollectionInternalProperties.Contains(propertyName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user