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,48 +425,73 @@ 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>();
|
||||||
|
|
||||||
int fieldNumber = 1;
|
// Check if this type is a collection (implements IList<T>, ICollection<T>, etc.)
|
||||||
foreach (var prop in properties)
|
var collectionElementType = ProtoFileTypeMapper.GetCollectionElementTypeByInterface(type);
|
||||||
|
if (collectionElementType != null)
|
||||||
{
|
{
|
||||||
if (ProtoFileTypeMapper.IsUnsupportedType(prop.Type))
|
// This type is a collection - generate a single repeated field for items
|
||||||
{
|
var protoElementType = ProtoFileTypeMapper.MapType(collectionElementType, out var needsImport, out var importPath);
|
||||||
_messagesBuilder.AppendLine($" // Skipped: {prop.Name} - unsupported type {prop.Type.Name}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var protoType = ProtoFileTypeMapper.MapType(prop.Type, out var needsImport, out var importPath);
|
|
||||||
if (needsImport && importPath != null)
|
if (needsImport && importPath != null)
|
||||||
{
|
{
|
||||||
_requiredImports.Add(importPath);
|
_requiredImports.Add(importPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
var fieldName = ProtoFileTypeMapper.ToSnakeCase(prop.Name);
|
_messagesBuilder.AppendLine($" repeated {protoElementType} items = 1;");
|
||||||
_messagesBuilder.AppendLine($" {protoType} {fieldName} = {fieldNumber};");
|
|
||||||
|
|
||||||
// Track enums for later generation
|
// Track the element type for nested generation
|
||||||
var enumType = ProtoFileTypeMapper.GetEnumType(prop.Type);
|
if (IsComplexType(collectionElementType) && collectionElementType is INamedTypeSymbol elementNamedType)
|
||||||
if (enumType != null)
|
|
||||||
{
|
{
|
||||||
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
|
int fieldNumber = 1;
|
||||||
// Use GetElementOrUnderlyingType to extract element type from collections
|
foreach (var prop in properties)
|
||||||
var underlyingType = ProtoFileTypeMapper.GetElementOrUnderlyingType(prop.Type);
|
|
||||||
if (IsComplexType(underlyingType) && underlyingType is INamedTypeSymbol namedType)
|
|
||||||
{
|
{
|
||||||
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("}");
|
_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