using System; using Microsoft.CodeAnalysis; namespace Svrnty.CQRS.Grpc.Generators; /// /// Maps C# types to Protocol Buffer types for proto file generation /// internal static class ProtoFileTypeMapper { public static string MapType(ITypeSymbol typeSymbol, out bool needsImport, out string? importPath) { needsImport = false; importPath = null; // Handle special name (fully qualified name) var fullTypeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); var typeName = typeSymbol.Name; // Note: NullableAnnotation.Annotated is for reference type nullability (List?, string?, etc.) // We don't unwrap these - just use the underlying type. Nullable value types are handled later. // Handle Nullable value types (e.g., int?, decimal?, enum?) FIRST if (typeSymbol is INamedTypeSymbol nullableType && nullableType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T && nullableType.TypeArguments.Length == 1) { // Unwrap the nullable and map the inner type return MapType(nullableType.TypeArguments[0], out needsImport, out importPath); } // Handle collections BEFORE basic type checks (to avoid matching List as Guid) if (typeSymbol is INamedTypeSymbol collectionType) { // List, IEnumerable, Array, ICollection etc. (but not Nullable) var collectionTypeName = collectionType.Name; if (collectionType.TypeArguments.Length == 1 && (collectionTypeName.Contains("List") || collectionTypeName.Contains("Collection") || collectionTypeName.Contains("Enumerable") || collectionTypeName.Contains("Array") || collectionTypeName.Contains("Set") || collectionTypeName.Contains("IList") || collectionTypeName.Contains("ICollection") || collectionTypeName.Contains("IEnumerable"))) { var elementType = collectionType.TypeArguments[0]; var protoElementType = MapType(elementType, out needsImport, out importPath); return $"repeated {protoElementType}"; } // Dictionary if (collectionType.TypeArguments.Length == 2 && (collectionTypeName.Contains("Dictionary") || collectionTypeName.Contains("IDictionary"))) { var keyType = MapType(collectionType.TypeArguments[0], out var keyNeedsImport, out var keyImportPath); var valueType = MapType(collectionType.TypeArguments[1], out var valueNeedsImport, out var valueImportPath); // Set import flags if either key or value needs imports if (keyNeedsImport) { needsImport = true; importPath = keyImportPath; } if (valueNeedsImport) { needsImport = true; importPath = valueImportPath; // Note: This only captures last import, may need improvement } return $"map<{keyType}, {valueType}>"; } } // Handle byte[] array type (check before switch since it's an array) if (typeSymbol is IArrayTypeSymbol arrayType && arrayType.ElementType.SpecialType == SpecialType.System_Byte) { return "bytes"; } // Handle Stream types -> bytes if (fullTypeName.Contains("System.IO.Stream") || fullTypeName.Contains("System.IO.MemoryStream") || fullTypeName.Contains("System.IO.FileStream")) { return "bytes"; } // Basic types switch (typeName) { case "String": return "string"; case "Int32": return "int32"; case "UInt32": return "uint32"; case "Int64": return "int64"; case "UInt64": return "uint64"; case "Int16": return "int32"; // Proto has no int16 case "UInt16": return "uint32"; // Proto has no uint16 case "Byte": return "uint32"; // Proto has no byte case "SByte": return "int32"; // Proto has no sbyte case "Boolean": return "bool"; case "Single": return "float"; case "Double": return "double"; case "Byte[]": return "bytes"; case "Stream": case "MemoryStream": case "FileStream": return "bytes"; case "Guid": // Guid serialized as string return "string"; case "Decimal": // Decimal serialized as string (no native decimal in proto) return "string"; case "DateTime": case "DateTimeOffset": needsImport = true; importPath = "google/protobuf/timestamp.proto"; return "google.protobuf.Timestamp"; case "DateOnly": // DateOnly serialized as string (YYYY-MM-DD format) return "string"; case "TimeOnly": // TimeOnly serialized as string (HH:mm:ss format) return "string"; case "TimeSpan": needsImport = true; importPath = "google/protobuf/duration.proto"; return "google.protobuf.Duration"; case "JsonElement": needsImport = true; importPath = "google/protobuf/struct.proto"; return "google.protobuf.Struct"; } // Enums if (typeSymbol.TypeKind == TypeKind.Enum) { return typeName; // Use the enum name directly } // Complex types (classes/records) become message types if (typeSymbol.TypeKind == TypeKind.Class || typeSymbol.TypeKind == TypeKind.Struct) { return typeName; // Reference the message type by name } // Fallback return "string"; // Default to string for unknown types } /// /// Converts C# PascalCase property name to proto snake_case field name /// public static string ToSnakeCase(string pascalCase) { if (string.IsNullOrEmpty(pascalCase)) return pascalCase; var result = new System.Text.StringBuilder(); result.Append(char.ToLowerInvariant(pascalCase[0])); for (int i = 1; i < pascalCase.Length; i++) { var c = pascalCase[i]; if (char.IsUpper(c)) { // Handle sequences of uppercase letters (e.g., "APIKey" -> "api_key") if (i + 1 < pascalCase.Length && char.IsUpper(pascalCase[i + 1])) { result.Append(char.ToLowerInvariant(c)); } else { result.Append('_'); result.Append(char.ToLowerInvariant(c)); } } else { result.Append(c); } } return result.ToString(); } /// /// Checks if a type should be skipped/ignored for proto generation /// public static bool IsUnsupportedType(ITypeSymbol typeSymbol) { var fullTypeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); // Skip these types - they should trigger a warning/error // Note: Stream types are now supported (mapped to bytes) if (fullTypeName.Contains("System.Threading.CancellationToken") || fullTypeName.Contains("System.Threading.Tasks.Task") || fullTypeName.Contains("System.Collections.Generic.IAsyncEnumerable") || fullTypeName.Contains("System.Func") || fullTypeName.Contains("System.Action") || fullTypeName.Contains("System.Delegate")) { return true; } return false; } /// /// Checks if a type is a Stream or byte array type (for special ByteString handling) /// public static bool IsBinaryType(ITypeSymbol typeSymbol) { var fullTypeName = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); // Check for byte[] if (typeSymbol is IArrayTypeSymbol arrayType && arrayType.ElementType.SpecialType == SpecialType.System_Byte) { return true; } // Check for Stream types if (fullTypeName.Contains("System.IO.Stream") || fullTypeName.Contains("System.IO.MemoryStream") || fullTypeName.Contains("System.IO.FileStream")) { return true; } var typeName = typeSymbol.Name; return typeName == "Stream" || typeName == "MemoryStream" || typeName == "FileStream"; } /// /// Gets the element type from a collection type, or returns the type itself if not a collection. /// Also unwraps Nullable types. /// public static ITypeSymbol GetElementOrUnderlyingType(ITypeSymbol typeSymbol) { // Unwrap Nullable if (typeSymbol is INamedTypeSymbol nullableType && nullableType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T && nullableType.TypeArguments.Length == 1) { return GetElementOrUnderlyingType(nullableType.TypeArguments[0]); } // Extract element type from collections if (typeSymbol is INamedTypeSymbol collectionType && collectionType.TypeArguments.Length == 1) { var typeName = collectionType.Name; if (typeName.Contains("List") || typeName.Contains("Collection") || typeName.Contains("Enumerable") || typeName.Contains("Array") || typeName.Contains("Set") || typeName.Contains("IList") || typeName.Contains("ICollection") || typeName.Contains("IEnumerable")) { return GetElementOrUnderlyingType(collectionType.TypeArguments[0]); } } return typeSymbol; } /// /// Checks if the type is an enum (including nullable enums) /// public static bool IsEnumType(ITypeSymbol typeSymbol) { var underlying = GetElementOrUnderlyingType(typeSymbol); return underlying.TypeKind == TypeKind.Enum; } /// /// Gets the enum type symbol if this is an enum or nullable enum, otherwise null /// public static INamedTypeSymbol? GetEnumType(ITypeSymbol typeSymbol) { var underlying = GetElementOrUnderlyingType(typeSymbol); if (underlying.TypeKind == TypeKind.Enum && underlying is INamedTypeSymbol enumType) { return enumType; } return null; } /// /// 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" /// public static bool IsCollectionTypeByInterface(ITypeSymbol typeSymbol) { if (typeSymbol is not INamedTypeSymbol namedType) return false; // Skip string (implements IEnumerable) if (namedType.SpecialType == SpecialType.System_String) return false; // Check all interfaces for IList, ICollection, or IEnumerable foreach (var iface in namedType.AllInterfaces) { if (iface.IsGenericType && iface.TypeArguments.Length == 1) { var ifaceName = iface.OriginalDefinition.ToDisplayString(); if (ifaceName == "System.Collections.Generic.IList" || ifaceName == "System.Collections.Generic.ICollection" || ifaceName == "System.Collections.Generic.IEnumerable" || ifaceName == "System.Collections.Generic.IReadOnlyList" || ifaceName == "System.Collections.Generic.IReadOnlyCollection") { return true; } } } return false; } /// /// 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 /// 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 over ICollection over IEnumerable 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" || ifaceName == "System.Collections.Generic.IReadOnlyList") currentPriority = 3; else if (ifaceName == "System.Collections.Generic.ICollection" || ifaceName == "System.Collections.Generic.IReadOnlyCollection") currentPriority = 2; else if (ifaceName == "System.Collections.Generic.IEnumerable") currentPriority = 1; if (currentPriority > priority) { priority = currentPriority; elementType = iface.TypeArguments[0]; } } } return elementType; } /// /// Collection-internal properties that should be skipped when generating proto messages /// private static readonly System.Collections.Generic.HashSet CollectionInternalProperties = new() { "Count", "Capacity", "IsReadOnly", "IsSynchronized", "SyncRoot", "Keys", "Values" }; /// /// Checks if a property name is a collection-internal property that should be skipped /// public static bool IsCollectionInternalProperty(string propertyName) { return CollectionInternalProperties.Contains(propertyName); } }