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()
|
||||
.OfType<IPropertySymbol>()
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public)
|
||||
.Where(p => p.DeclaredAccessibility == Accessibility.Public &&
|
||||
!p.IsIndexer &&
|
||||
!ProtoFileTypeMapper.IsCollectionInternalProperty(p.Name))
|
||||
.ToList();
|
||||
|
||||
// Collect nested complex types to generate after closing this message
|
||||
@ -423,48 +425,73 @@ internal class ProtoFileGenerator
|
||||
_messagesBuilder.AppendLine($"// {type.Name} entity");
|
||||
_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
|
||||
var nestedComplexTypes = new List<INamedTypeSymbol>();
|
||||
|
||||
int fieldNumber = 1;
|
||||
foreach (var prop in properties)
|
||||
// Check if this type is a collection (implements IList<T>, ICollection<T>, etc.)
|
||||
var collectionElementType = ProtoFileTypeMapper.GetCollectionElementTypeByInterface(type);
|
||||
if (collectionElementType != null)
|
||||
{
|
||||
if (ProtoFileTypeMapper.IsUnsupportedType(prop.Type))
|
||||
{
|
||||
_messagesBuilder.AppendLine($" // Skipped: {prop.Name} - unsupported type {prop.Type.Name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var protoType = ProtoFileTypeMapper.MapType(prop.Type, out var needsImport, out var importPath);
|
||||
// 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);
|
||||
}
|
||||
|
||||
var fieldName = ProtoFileTypeMapper.ToSnakeCase(prop.Name);
|
||||
_messagesBuilder.AppendLine($" {protoType} {fieldName} = {fieldNumber};");
|
||||
_messagesBuilder.AppendLine($" repeated {protoElementType} items = 1;");
|
||||
|
||||
// Track enums for later generation
|
||||
var enumType = ProtoFileTypeMapper.GetEnumType(prop.Type);
|
||||
if (enumType != null)
|
||||
// Track the element type for nested generation
|
||||
if (IsComplexType(collectionElementType) && collectionElementType is INamedTypeSymbol elementNamedType)
|
||||
{
|
||||
TrackEnumType(enumType);
|
||||
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();
|
||||
|
||||
// Collect complex types to generate after this message is closed
|
||||
// Use GetElementOrUnderlyingType to extract element type from collections
|
||||
var underlyingType = ProtoFileTypeMapper.GetElementOrUnderlyingType(prop.Type);
|
||||
if (IsComplexType(underlyingType) && underlyingType is INamedTypeSymbol namedType)
|
||||
int fieldNumber = 1;
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
nestedComplexTypes.Add(namedType);
|
||||
}
|
||||
if (ProtoFileTypeMapper.IsUnsupportedType(prop.Type))
|
||||
{
|
||||
_messagesBuilder.AppendLine($" // Skipped: {prop.Name} - unsupported type {prop.Type.Name}");
|
||||
continue;
|
||||
}
|
||||
|
||||
fieldNumber++;
|
||||
var protoType = ProtoFileTypeMapper.MapType(prop.Type, out var needsImport, out var importPath);
|
||||
if (needsImport && importPath != null)
|
||||
{
|
||||
_requiredImports.Add(importPath);
|
||||
}
|
||||
|
||||
var fieldName = ProtoFileTypeMapper.ToSnakeCase(prop.Name);
|
||||
_messagesBuilder.AppendLine($" {protoType} {fieldName} = {fieldNumber};");
|
||||
|
||||
// Track enums for later generation
|
||||
var enumType = ProtoFileTypeMapper.GetEnumType(prop.Type);
|
||||
if (enumType != null)
|
||||
{
|
||||
TrackEnumType(enumType);
|
||||
}
|
||||
|
||||
// Collect complex types to generate after this message is closed
|
||||
// Use GetElementOrUnderlyingType to extract element type from collections
|
||||
var underlyingType = ProtoFileTypeMapper.GetElementOrUnderlyingType(prop.Type);
|
||||
if (IsComplexType(underlyingType) && underlyingType is INamedTypeSymbol namedType)
|
||||
{
|
||||
nestedComplexTypes.Add(namedType);
|
||||
}
|
||||
|
||||
fieldNumber++;
|
||||
}
|
||||
}
|
||||
|
||||
_messagesBuilder.AppendLine("}");
|
||||
@ -755,7 +782,9 @@ internal class ProtoFileGenerator
|
||||
int fieldNumber = 1;
|
||||
|
||||
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))
|
||||
continue;
|
||||
|
||||
@ -243,4 +243,97 @@ internal static class ProtoFileTypeMapper
|
||||
}
|
||||
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