From e94fecce6615230af8abf667e60f623b0249342e Mon Sep 17 00:00:00 2001 From: Xpl0itR Date: Thu, 11 Jul 2024 05:39:04 +0100 Subject: [PATCH] Rewrite some more stuff and implement logging --- .github/workflows/release.yml | 4 +- README.md | 1 + src/LibProtodec/LibProtodec.csproj | 5 +- src/LibProtodec/Loaders/CilAssemblyLoader.cs | 34 ++ src/LibProtodec/Loaders/ClrAssemblyLoader.cs | 75 +-- src/LibProtodec/Loaders/ICilAssemblyLoader.cs | 23 - src/LibProtodec/LoggerMessages.cs | 86 ++++ .../Models/Cil/Clr/ClrAttribute.cs | 25 +- src/LibProtodec/Models/Cil/Clr/ClrMember.cs | 23 +- src/LibProtodec/Models/Cil/Clr/ClrMethod.cs | 9 +- src/LibProtodec/Models/Cil/Clr/ClrProperty.cs | 38 +- src/LibProtodec/Models/Cil/Clr/ClrType.cs | 22 +- src/LibProtodec/Models/Cil/ICilAttribute.cs | 2 +- src/LibProtodec/Models/Cil/ICilField.cs | 4 +- src/LibProtodec/Models/Cil/ICilMethod.cs | 11 +- src/LibProtodec/Models/Cil/ICilProperty.cs | 2 +- src/LibProtodec/Models/Cil/ICilType.cs | 2 +- .../Models/Protobuf/Fields/MessageField.cs | 7 +- .../Models/Protobuf/Fields/ServiceMethod.cs | 10 +- src/LibProtodec/Models/Protobuf/Protobuf.cs | 22 +- .../Models/Protobuf/TopLevels/Enum.cs | 4 +- .../Models/Protobuf/TopLevels/Message.cs | 4 +- .../Models/Protobuf/TopLevels/Service.cs | 2 +- .../Models/Protobuf/TopLevels/TopLevel.cs | 14 +- .../Models/Protobuf/Types/IProtobufType.cs | 6 - .../Models/Protobuf/Types/Scalar.cs | 35 +- .../Models/Protobuf/Types/WellKnown.cs | 64 +-- src/LibProtodec/ProtodecContext.cs | 469 +++++++++--------- src/protodec/Program.cs | 30 +- src/protodec/protodec.csproj | 1 + 30 files changed, 570 insertions(+), 464 deletions(-) create mode 100644 src/LibProtodec/Loaders/CilAssemblyLoader.cs delete mode 100644 src/LibProtodec/Loaders/ICilAssemblyLoader.cs create mode 100644 src/LibProtodec/LoggerMessages.cs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a7b78cc..539faac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '8.0.x' - name: Build protodec - run: dotnet publish --configuration Release --runtime ${{ matrix.runtime }} /p:VersionPrefix=${{ github.ref_name }} /p:ContinuousIntegrationBuild=true + run: dotnet publish --configuration Release --runtime ${{ matrix.runtime }} /p:Version=${{ github.ref_name }} /p:ContinuousIntegrationBuild=true - name: Pack LibProtodec if: matrix.os == 'ubuntu-latest' - run: dotnet pack --configuration Release /p:VersionPrefix=${{ github.ref_name }} /p:ContinuousIntegrationBuild=true + run: dotnet pack --configuration Release /p:Version=${{ github.ref_name }} /p:ContinuousIntegrationBuild=true - name: Release uses: softprops/action-gh-release@v1 diff --git a/README.md b/README.md index 283af37..fb55d3a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ Arguments: target_assembly_path Either the path to the target assembly or a directory of assemblies, all of which be parsed. out_path An existing directory to output into individual files, otherwise output to a single file. Options: + --debug Drops the minimum log level to Debug. --parse_service_servers Parses gRPC service definitions from server classes. --parse_service_clients Parses gRPC service definitions from client classes. --skip_enums Skip parsing enums and replace references to them with int32. diff --git a/src/LibProtodec/LibProtodec.csproj b/src/LibProtodec/LibProtodec.csproj index 8120bb0..87a204e 100644 --- a/src/LibProtodec/LibProtodec.csproj +++ b/src/LibProtodec/LibProtodec.csproj @@ -18,9 +18,10 @@ + - - + + \ No newline at end of file diff --git a/src/LibProtodec/Loaders/CilAssemblyLoader.cs b/src/LibProtodec/Loaders/CilAssemblyLoader.cs new file mode 100644 index 0000000..ab803b2 --- /dev/null +++ b/src/LibProtodec/Loaders/CilAssemblyLoader.cs @@ -0,0 +1,34 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System; +using System.Collections.Generic; +using LibProtodec.Models.Cil; + +namespace LibProtodec.Loaders; + +public abstract class CilAssemblyLoader : IDisposable +{ + private ICilType? _iMessage; + private ICilType? _clientBase; + private ICilType? _bindServiceMethodAttribute; + + // ReSharper disable once InconsistentNaming + public ICilType IMessage => + _iMessage ??= FindType("Google.Protobuf.IMessage", "Google.Protobuf"); + + public ICilType ClientBase => + _clientBase ??= FindType("Grpc.Core.ClientBase", "Grpc.Core.Api"); + + public ICilType BindServiceMethodAttribute => + _bindServiceMethodAttribute ??= FindType("Grpc.Core.BindServiceMethodAttribute", "Grpc.Core.Api"); + + public IReadOnlyList LoadedTypes { get; protected init; } + + public virtual void Dispose() { } + + protected abstract ICilType FindType(string typeFullName, string assemblySimpleName); +} \ No newline at end of file diff --git a/src/LibProtodec/Loaders/ClrAssemblyLoader.cs b/src/LibProtodec/Loaders/ClrAssemblyLoader.cs index 01abb22..2aaf143 100644 --- a/src/LibProtodec/Loaders/ClrAssemblyLoader.cs +++ b/src/LibProtodec/Loaders/ClrAssemblyLoader.cs @@ -12,14 +12,15 @@ using System.Reflection; using CommunityToolkit.Diagnostics; using LibProtodec.Models.Cil; using LibProtodec.Models.Cil.Clr; +using Microsoft.Extensions.Logging; namespace LibProtodec.Loaders; -public sealed class ClrAssemblyLoader : ICilAssemblyLoader +public sealed class ClrAssemblyLoader : CilAssemblyLoader { public readonly MetadataLoadContext LoadContext; - public ClrAssemblyLoader(string assemblyPath) + public ClrAssemblyLoader(string assemblyPath, ILogger? logger = null) { bool isFile = File.Exists(assemblyPath); string assemblyDir = isFile @@ -28,67 +29,33 @@ public sealed class ClrAssemblyLoader : ICilAssemblyLoader PermissiveAssemblyResolver assemblyResolver = new( Directory.EnumerateFiles(assemblyDir, searchPattern: "*.dll")); - LoadContext = new MetadataLoadContext(assemblyResolver); - LoadedTypes = isFile - ? LoadContext.LoadFromAssemblyPath(assemblyPath) - .GetTypes() - .Select(ClrType.GetOrCreate) - .ToList() + + IEnumerable allTypes = isFile + ? LoadContext.LoadFromAssemblyPath(assemblyPath).GetTypes() : assemblyResolver.AssemblyPathLookup.Values - .SelectMany(path => LoadContext.LoadFromAssemblyPath(path).GetTypes()) - .Select(ClrType.GetOrCreate) - .ToList(); + .SelectMany(path => LoadContext.LoadFromAssemblyPath(path).GetTypes()); + + this.LoadedTypes = allTypes.Where(static type => type.GenericTypeArguments.Length == 0) + .Select(ClrType.GetOrCreate) + .ToList(); + + logger?.LogLoadedTypeAndAssemblyCount(this.LoadedTypes.Count, LoadContext.GetAssemblies().Count()); } - public IReadOnlyList LoadedTypes { get; } - - public ICilType IMessage + protected override ICilType FindType(string typeFullName, string assemblySimpleName) { - get - { - ICilType? iMessage = LoadedTypes.SingleOrDefault(static type => type?.FullName == "Google.Protobuf.IMessage", null); - if (iMessage is not null) - return iMessage; + ICilType? type = this.LoadedTypes.SingleOrDefault(type => type?.FullName == typeFullName, null); + if (type is not null) + return type; - Type? iMessageType = LoadContext.LoadFromAssemblyName("Google.Protobuf").GetType("Google.Protobuf.IMessage"); - Guard.IsNotNull(iMessageType); + Type? clrType = LoadContext.LoadFromAssemblyName(assemblySimpleName).GetType(typeFullName); + Guard.IsNotNull(clrType); - return ClrType.GetOrCreate(iMessageType); - } + return ClrType.GetOrCreate(clrType); } - public ICilType ClientBase - { - get - { - ICilType? clientBase = LoadedTypes.SingleOrDefault(static type => type?.FullName == "Grpc.Core.ClientBase", null); - if (clientBase is not null) - return clientBase; - - Type? clientBaseType = LoadContext.LoadFromAssemblyName("Grpc.Core.Api").GetType("Grpc.Core.ClientBase"); - Guard.IsNotNull(clientBaseType); - - return ClrType.GetOrCreate(clientBaseType); - } - } - - public ICilType BindServiceMethodAttribute - { - get - { - ICilType? attribute = LoadedTypes.SingleOrDefault(static type => type?.FullName == "Grpc.Core.BindServiceMethodAttribute", null); - if (attribute is not null) - return attribute; - - Type? attributeType = LoadContext.LoadFromAssemblyName("Grpc.Core.Api").GetType("Grpc.Core.BindServiceMethodAttribute"); - Guard.IsNotNull(attributeType); - - return ClrType.GetOrCreate(attributeType); - } - } - - public void Dispose() => + public override void Dispose() => LoadContext.Dispose(); /// diff --git a/src/LibProtodec/Loaders/ICilAssemblyLoader.cs b/src/LibProtodec/Loaders/ICilAssemblyLoader.cs deleted file mode 100644 index 5069e4c..0000000 --- a/src/LibProtodec/Loaders/ICilAssemblyLoader.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright © 2024 Xpl0itR -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -using System; -using System.Collections.Generic; -using LibProtodec.Models.Cil; - -namespace LibProtodec.Loaders; - -public interface ICilAssemblyLoader : IDisposable -{ - IReadOnlyList LoadedTypes { get; } - - // ReSharper disable once InconsistentNaming - ICilType IMessage { get; } - - ICilType ClientBase { get; } - - ICilType BindServiceMethodAttribute { get; } -} \ No newline at end of file diff --git a/src/LibProtodec/LoggerMessages.cs b/src/LibProtodec/LoggerMessages.cs new file mode 100644 index 0000000..d69d10a --- /dev/null +++ b/src/LibProtodec/LoggerMessages.cs @@ -0,0 +1,86 @@ +// Copyright © 2024 Xpl0itR +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +using System; +using Microsoft.Extensions.Logging; + +namespace LibProtodec; + +// ReSharper disable InconsistentNaming, StringLiteralTypo +internal static partial class LoggerMessages +{ + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to locate corresponding id field; likely stripped or otherwise obfuscated.")] + internal static partial void LogFailedToLocateIdField(this ILogger logger); + + [LoggerMessage(Level = LogLevel.Information, Message = "Loaded {typeCount} types from {assemblyCount} assemblies for parsing.")] + internal static partial void LogLoadedTypeAndAssemblyCount(this ILogger logger, int typeCount, int assemblyCount); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Parsed as enum \"{name}\".")] + internal static partial void LogParsedEnum(this ILogger logger, string name); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Parsed as field \"{name}\" with id \"{id}\".")] + internal static partial void LogParsedField(this ILogger logger, string name, int id); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Parsed as field \"{name}\" with id \"{id}\" of type \"{typeName}\".")] + internal static partial void LogParsedField(this ILogger logger, string name, int id, string typeName); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Parsed as message \"{name}\".")] + internal static partial void LogParsedMessage(this ILogger logger, string name); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Parsed as method \"{name}\" with request type \"{reqType}\" and response type \"{resType}\".")] + internal static partial void LogParsedMethod(this ILogger logger, string name, string reqType, string resType); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Parsed as oneof field \"{name}\".")] + internal static partial void LogParsedOneOfField(this ILogger logger, string name); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Parsed as service \"{name}\".")] + internal static partial void LogParsedService(this ILogger logger, string name); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Skipping duplicate method.")] + internal static partial void LogSkippingDuplicateMethod(this ILogger logger); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Skipping property without required NonUserCodeAttribute.")] + internal static partial void LogSkippingPropertyWithoutNonUserCodeAttribute(this ILogger logger); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Skipping method without required GeneratedCodeAttribute.")] + internal static partial void LogSkippingMethodWithoutGeneratedCodeAttribute(this ILogger logger); + + internal static IDisposable? BeginScopeParsingEnum(this ILogger logger, string typeName) => + __BeginScopeParsingEnumCallback(logger, typeName); + + internal static IDisposable? BeginScopeParsingField(this ILogger logger, string name) => + __BeginScopeParsingFieldCallback(logger, name); + + internal static IDisposable? BeginScopeParsingMessage(this ILogger logger, string name) => + __BeginScopeParsingMessageCallback(logger, name); + + internal static IDisposable? BeginScopeParsingMethod(this ILogger logger, string name) => + __BeginScopeParsingMethodCallback(logger, name); + + internal static IDisposable? BeginScopeParsingProperty(this ILogger logger, string name, string typeName) => + __BeginScopeParsingPropertyCallback(logger, name, typeName); + + internal static IDisposable? BeginScopeParsingService(this ILogger logger, string typeName) => + __BeginScopeParsingServiceCallback(logger, typeName); + + private static readonly Func __BeginScopeParsingEnumCallback = + LoggerMessage.DefineScope("Parsing enum from type \"{typeName}\""); + + private static readonly Func __BeginScopeParsingFieldCallback = + LoggerMessage.DefineScope("Parsing field \"{name}\""); + + private static readonly Func __BeginScopeParsingMessageCallback = + LoggerMessage.DefineScope("Parsing message from type \"{name}\""); + + private static readonly Func __BeginScopeParsingMethodCallback = + LoggerMessage.DefineScope("Parsing method \"{name}\""); + + private static readonly Func __BeginScopeParsingPropertyCallback = + LoggerMessage.DefineScope("Parsing property \"{name}\" of type \"{typeName}\""); + + private static readonly Func __BeginScopeParsingServiceCallback = + LoggerMessage.DefineScope("Parsing service from type \"{typeName}\""); +} \ No newline at end of file diff --git a/src/LibProtodec/Models/Cil/Clr/ClrAttribute.cs b/src/LibProtodec/Models/Cil/Clr/ClrAttribute.cs index 3b3ef1a..493ae88 100644 --- a/src/LibProtodec/Models/Cil/Clr/ClrAttribute.cs +++ b/src/LibProtodec/Models/Cil/Clr/ClrAttribute.cs @@ -6,37 +6,38 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reflection; namespace LibProtodec.Models.Cil.Clr; public sealed class ClrAttribute(CustomAttributeData clrAttribute) : ICilAttribute { - private IList? _constructorArguments; + private ICilType? _type; + private object?[]? _constructorArgumentValues; public ICilType Type => - ClrType.GetOrCreate(clrAttribute.AttributeType); + _type ??= ClrType.GetOrCreate( + clrAttribute.AttributeType); - public IList ConstructorArguments + public IList ConstructorArgumentValues { get { - if (_constructorArguments is null) + if (_constructorArgumentValues is null) { IList args = clrAttribute.ConstructorArguments; - if (args.Count < 1) + _constructorArgumentValues = args.Count < 1 + ? Array.Empty() + : new object[args.Count]; + + for (int i = 0; i < args.Count; i++) { - _constructorArguments = Array.Empty(); - } - else - { - _constructorArguments = args.Select(static arg => arg.Value).ToList(); + _constructorArgumentValues[i] = args[i].Value; } } - return _constructorArguments; + return _constructorArgumentValues; } } } \ No newline at end of file diff --git a/src/LibProtodec/Models/Cil/Clr/ClrMember.cs b/src/LibProtodec/Models/Cil/Clr/ClrMember.cs index 289e01b..7c5cc31 100644 --- a/src/LibProtodec/Models/Cil/Clr/ClrMember.cs +++ b/src/LibProtodec/Models/Cil/Clr/ClrMember.cs @@ -4,6 +4,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. +using System; using System.Collections.Generic; using System.Reflection; @@ -11,6 +12,8 @@ namespace LibProtodec.Models.Cil.Clr; public abstract class ClrMember(MemberInfo clrMember) { + private ICilAttribute[]? _customAttributes; + public string Name => clrMember.Name; @@ -23,11 +26,25 @@ public abstract class ClrMember(MemberInfo clrMember) : ClrType.GetOrCreate( clrMember.DeclaringType); - public IEnumerable GetCustomAttributes() + public IList CustomAttributes { - foreach (CustomAttributeData attribute in clrMember.GetCustomAttributesData()) + get { - yield return new ClrAttribute(attribute); + if (_customAttributes is null) + { + IList attributes = clrMember.GetCustomAttributesData(); + + _customAttributes = attributes.Count < 1 + ? Array.Empty() + : new ICilAttribute[attributes.Count]; + + for (int i = 0; i < attributes.Count; i++) + { + _customAttributes[i] = new ClrAttribute(attributes[i]); + } + } + + return _customAttributes; } } } \ No newline at end of file diff --git a/src/LibProtodec/Models/Cil/Clr/ClrMethod.cs b/src/LibProtodec/Models/Cil/Clr/ClrMethod.cs index 0dcbfd5..f29e480 100644 --- a/src/LibProtodec/Models/Cil/Clr/ClrMethod.cs +++ b/src/LibProtodec/Models/Cil/Clr/ClrMethod.cs @@ -11,12 +11,12 @@ namespace LibProtodec.Models.Cil.Clr; public sealed class ClrMethod(MethodInfo clrMethod) : ClrMember(clrMethod), ICilMethod { + public bool IsConstructor => + clrMethod.IsConstructor; + public bool IsPublic => clrMethod.IsPublic; - public bool IsNonPublic => - (clrMethod.Attributes & MethodAttributes.Public) == 0; - public bool IsStatic => clrMethod.IsStatic; @@ -24,7 +24,8 @@ public sealed class ClrMethod(MethodInfo clrMethod) : ClrMember(clrMethod), ICil clrMethod.IsVirtual; public ICilType ReturnType => - ClrType.GetOrCreate(clrMethod.ReturnType); + ClrType.GetOrCreate( + clrMethod.ReturnType); public IEnumerable GetParameterTypes() { diff --git a/src/LibProtodec/Models/Cil/Clr/ClrProperty.cs b/src/LibProtodec/Models/Cil/Clr/ClrProperty.cs index c249792..8dfe64e 100644 --- a/src/LibProtodec/Models/Cil/Clr/ClrProperty.cs +++ b/src/LibProtodec/Models/Cil/Clr/ClrProperty.cs @@ -10,23 +10,29 @@ namespace LibProtodec.Models.Cil.Clr; public sealed class ClrProperty(PropertyInfo clrProperty) : ClrMember(clrProperty), ICilProperty { + private readonly MethodInfo? _getterInfo = clrProperty.GetMethod; + private readonly MethodInfo? _setterInfo = clrProperty.SetMethod; + + private ClrMethod? _getter; + private ClrMethod? _setter; + + public bool CanRead => + _getterInfo is not null; + + public bool CanWrite => + _setterInfo is not null; + + public ICilMethod? Getter => + _getterInfo is null + ? null + : _getter ??= new ClrMethod(_getterInfo); + + public ICilMethod? Setter => + _setterInfo is null + ? null + : _setter ??= new ClrMethod(_setterInfo); + public ICilType Type => ClrType.GetOrCreate( clrProperty.PropertyType); - - public bool CanRead => - clrProperty.CanRead; - - public bool CanWrite => - clrProperty.CanWrite; - - public ICilMethod? Getter => - CanRead - ? new ClrMethod(clrProperty.GetMethod!) - : null; - - public ICilMethod? Setter => - CanWrite - ? new ClrMethod(clrProperty.SetMethod!) - : null; } \ No newline at end of file diff --git a/src/LibProtodec/Models/Cil/Clr/ClrType.cs b/src/LibProtodec/Models/Cil/Clr/ClrType.cs index 53dac65..62fd807 100644 --- a/src/LibProtodec/Models/Cil/Clr/ClrType.cs +++ b/src/LibProtodec/Models/Cil/Clr/ClrType.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Linq; using System.Reflection; using CommunityToolkit.Diagnostics; @@ -18,7 +17,7 @@ public sealed class ClrType : ClrMember, ICilType private const BindingFlags Everything = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance; private readonly Type _clrType; - private IList? _genericTypeArguments; + private ICilType[]? _genericTypeArguments; private ClrType(Type clrType) : base(clrType) => _clrType = clrType; @@ -30,14 +29,13 @@ public sealed class ClrType : ClrMember, ICilType _clrType.Namespace; public string DeclaringAssemblyName => - _clrType.Assembly.FullName - ?? ThrowHelper.ThrowArgumentNullException( - nameof(_clrType.Assembly.FullName)); + _clrType.Assembly.FullName!; public ICilType? BaseType => _clrType.BaseType is null ? null - : GetOrCreate(_clrType.BaseType); + : GetOrCreate( + _clrType.BaseType); public bool IsAbstract => _clrType.IsAbstract; @@ -62,13 +60,13 @@ public sealed class ClrType : ClrMember, ICilType { Type[] args = _clrType.GenericTypeArguments; - if (args.Length < 1) + _genericTypeArguments = args.Length < 1 + ? Array.Empty() + : new ICilType[args.Length]; + + for (int i = 0; i < args.Length; i++) { - _genericTypeArguments = Array.Empty(); - } - else - { - _genericTypeArguments = args.Select(GetOrCreate).ToList(); + _genericTypeArguments[i] = GetOrCreate(args[i]); } } diff --git a/src/LibProtodec/Models/Cil/ICilAttribute.cs b/src/LibProtodec/Models/Cil/ICilAttribute.cs index 76d17e1..4bbdaac 100644 --- a/src/LibProtodec/Models/Cil/ICilAttribute.cs +++ b/src/LibProtodec/Models/Cil/ICilAttribute.cs @@ -12,5 +12,5 @@ public interface ICilAttribute { ICilType Type { get; } - IList ConstructorArguments { get; } + IList ConstructorArgumentValues { get; } } \ No newline at end of file diff --git a/src/LibProtodec/Models/Cil/ICilField.cs b/src/LibProtodec/Models/Cil/ICilField.cs index 5b511e7..061ab2f 100644 --- a/src/LibProtodec/Models/Cil/ICilField.cs +++ b/src/LibProtodec/Models/Cil/ICilField.cs @@ -14,9 +14,9 @@ public interface ICilField object? ConstantValue { get; } + IList CustomAttributes { get; } + bool IsLiteral { get; } bool IsPublic { get; } bool IsStatic { get; } - - IEnumerable GetCustomAttributes(); } \ No newline at end of file diff --git a/src/LibProtodec/Models/Cil/ICilMethod.cs b/src/LibProtodec/Models/Cil/ICilMethod.cs index 7c4454c..01dc2fe 100644 --- a/src/LibProtodec/Models/Cil/ICilMethod.cs +++ b/src/LibProtodec/Models/Cil/ICilMethod.cs @@ -12,14 +12,15 @@ public interface ICilMethod { string Name { get; } - bool IsInherited { get; } - bool IsPublic { get; } - bool IsStatic { get; } - bool IsVirtual { get; } + bool IsConstructor { get; } + bool IsInherited { get; } + bool IsPublic { get; } + bool IsStatic { get; } + bool IsVirtual { get; } ICilType ReturnType { get; } - IEnumerable GetCustomAttributes(); + IList CustomAttributes { get; } IEnumerable GetParameterTypes(); } \ No newline at end of file diff --git a/src/LibProtodec/Models/Cil/ICilProperty.cs b/src/LibProtodec/Models/Cil/ICilProperty.cs index 15b8a20..f3fb079 100644 --- a/src/LibProtodec/Models/Cil/ICilProperty.cs +++ b/src/LibProtodec/Models/Cil/ICilProperty.cs @@ -20,5 +20,5 @@ public interface ICilProperty ICilMethod? Getter { get; } ICilMethod? Setter { get; } - IEnumerable GetCustomAttributes(); + IList CustomAttributes { get; } } \ No newline at end of file diff --git a/src/LibProtodec/Models/Cil/ICilType.cs b/src/LibProtodec/Models/Cil/ICilType.cs index 8c94bfb..a2a490b 100644 --- a/src/LibProtodec/Models/Cil/ICilType.cs +++ b/src/LibProtodec/Models/Cil/ICilType.cs @@ -26,7 +26,7 @@ public interface ICilType IList GenericTypeArguments { get; } - IEnumerable GetCustomAttributes(); + IList CustomAttributes { get; } IEnumerable GetFields(); diff --git a/src/LibProtodec/Models/Protobuf/Fields/MessageField.cs b/src/LibProtodec/Models/Protobuf/Fields/MessageField.cs index 96c2cbe..7c96f4f 100644 --- a/src/LibProtodec/Models/Protobuf/Fields/MessageField.cs +++ b/src/LibProtodec/Models/Protobuf/Fields/MessageField.cs @@ -10,7 +10,7 @@ using LibProtodec.Models.Protobuf.Types; namespace LibProtodec.Models.Protobuf.Fields; -public sealed class MessageField +public sealed class MessageField(Message declaringMessage) { public required IProtobufType Type { get; init; } public required string Name { get; init; } @@ -19,14 +19,15 @@ public sealed class MessageField public bool IsObsolete { get; init; } public bool HasHasProp { get; init; } - public void WriteTo(TextWriter writer, TopLevel topLevel, bool isOneOf) + public void WriteTo(TextWriter writer, bool isOneOf) { if (HasHasProp && !isOneOf && Type is not Repeated) { writer.Write("optional "); } - Protobuf.WriteTypeNameTo(writer, Type, topLevel); + writer.Write( + declaringMessage.QualifyTypeName(Type)); writer.Write(' '); writer.Write(Name); writer.Write(" = "); diff --git a/src/LibProtodec/Models/Protobuf/Fields/ServiceMethod.cs b/src/LibProtodec/Models/Protobuf/Fields/ServiceMethod.cs index 48cbca2..1f436ad 100644 --- a/src/LibProtodec/Models/Protobuf/Fields/ServiceMethod.cs +++ b/src/LibProtodec/Models/Protobuf/Fields/ServiceMethod.cs @@ -10,7 +10,7 @@ using LibProtodec.Models.Protobuf.Types; namespace LibProtodec.Models.Protobuf.Fields; -public sealed class ServiceMethod +public sealed class ServiceMethod(Service declaringService) { public required string Name { get; init; } public required IProtobufType RequestType { get; init; } @@ -20,7 +20,7 @@ public sealed class ServiceMethod public bool IsResponseStreamed { get; init; } public bool IsObsolete { get; init; } - public void WriteTo(IndentedTextWriter writer, TopLevel topLevel) + public void WriteTo(IndentedTextWriter writer) { writer.Write("rpc "); writer.Write(Name); @@ -31,7 +31,8 @@ public sealed class ServiceMethod writer.Write("stream "); } - Protobuf.WriteTypeNameTo(writer, RequestType, topLevel); + writer.Write( + declaringService.QualifyTypeName(RequestType)); writer.Write(") returns ("); if (IsResponseStreamed) @@ -39,7 +40,8 @@ public sealed class ServiceMethod writer.Write("stream "); } - Protobuf.WriteTypeNameTo(writer, ResponseType, topLevel); + writer.Write( + declaringService.QualifyTypeName(ResponseType)); writer.Write(')'); if (IsObsolete) diff --git a/src/LibProtodec/Models/Protobuf/Protobuf.cs b/src/LibProtodec/Models/Protobuf/Protobuf.cs index 37ee6c8..02607d8 100644 --- a/src/LibProtodec/Models/Protobuf/Protobuf.cs +++ b/src/LibProtodec/Models/Protobuf/Protobuf.cs @@ -9,15 +9,13 @@ using System.Collections.Generic; using System.IO; using System.Linq; using LibProtodec.Models.Protobuf.TopLevels; -using LibProtodec.Models.Protobuf.Types; namespace LibProtodec.Models.Protobuf; public sealed class Protobuf { + private string? _fileName; private HashSet? _imports; - public HashSet Imports => - _imports ??= []; public readonly List TopLevels = []; @@ -26,7 +24,10 @@ public sealed class Protobuf public string? Namespace { get; init; } public string FileName => - $"{TopLevels.FirstOrDefault()?.Name}.proto"; + _fileName ??= $"{string.Join('_', TopLevels.Select(static topLevel => topLevel.Name))}.proto"; + + public HashSet Imports => + _imports ??= []; public void WriteTo(IndentedTextWriter writer) { @@ -89,17 +90,4 @@ public sealed class Protobuf writer.WriteLine(';'); } - - public static void WriteTypeNameTo(TextWriter writer, IProtobufType type, TopLevel topLevel) - { - if (type is TopLevel { Parent: not null } typeTopLevel && typeTopLevel.Parent != topLevel) - { - writer.Write( - typeTopLevel.QualifyName(topLevel)); - } - else - { - writer.Write(type.Name); - } - } } \ No newline at end of file diff --git a/src/LibProtodec/Models/Protobuf/TopLevels/Enum.cs b/src/LibProtodec/Models/Protobuf/TopLevels/Enum.cs index dc8b84e..d538bc7 100644 --- a/src/LibProtodec/Models/Protobuf/TopLevels/Enum.cs +++ b/src/LibProtodec/Models/Protobuf/TopLevels/Enum.cs @@ -23,7 +23,7 @@ public sealed class Enum : TopLevel, INestableType writer.WriteLine(" {"); writer.Indent++; - if (ContainsDuplicateField) + if (ContainsDuplicateFieldId) { Protobuf.WriteOptionTo(writer, "allow_alias", "true"); } @@ -49,7 +49,7 @@ public sealed class Enum : TopLevel, INestableType public bool IsClosed { get; set; } - private bool ContainsDuplicateField + private bool ContainsDuplicateFieldId { get { diff --git a/src/LibProtodec/Models/Protobuf/TopLevels/Message.cs b/src/LibProtodec/Models/Protobuf/TopLevels/Message.cs index a2081b2..458e780 100644 --- a/src/LibProtodec/Models/Protobuf/TopLevels/Message.cs +++ b/src/LibProtodec/Models/Protobuf/TopLevels/Message.cs @@ -37,7 +37,7 @@ public sealed class Message : TopLevel, INestableType if (oneOfs.Contains(field.Id)) continue; - field.WriteTo(writer, this, isOneOf: false); + field.WriteTo(writer, isOneOf: false); } foreach ((string name, List fieldIds) in OneOfs) @@ -50,7 +50,7 @@ public sealed class Message : TopLevel, INestableType foreach (int fieldId in fieldIds) { - Fields[fieldId].WriteTo(writer, this, isOneOf: true); + Fields[fieldId].WriteTo(writer, isOneOf: true); } writer.Indent--; diff --git a/src/LibProtodec/Models/Protobuf/TopLevels/Service.cs b/src/LibProtodec/Models/Protobuf/TopLevels/Service.cs index 6f42874..42fa5bc 100644 --- a/src/LibProtodec/Models/Protobuf/TopLevels/Service.cs +++ b/src/LibProtodec/Models/Protobuf/TopLevels/Service.cs @@ -28,7 +28,7 @@ public sealed class Service : TopLevel foreach (ServiceMethod method in Methods) { - method.WriteTo(writer, this); + method.WriteTo(writer); } writer.Indent--; diff --git a/src/LibProtodec/Models/Protobuf/TopLevels/TopLevel.cs b/src/LibProtodec/Models/Protobuf/TopLevels/TopLevel.cs index 4bab6ed..755560d 100644 --- a/src/LibProtodec/Models/Protobuf/TopLevels/TopLevel.cs +++ b/src/LibProtodec/Models/Protobuf/TopLevels/TopLevel.cs @@ -6,22 +6,28 @@ using System.CodeDom.Compiler; using System.Collections.Generic; +using LibProtodec.Models.Protobuf.Types; namespace LibProtodec.Models.Protobuf.TopLevels; public abstract class TopLevel { public required string Name { get; init; } + public bool IsObsolete { get; init; } public Protobuf? Protobuf { get; set; } public TopLevel? Parent { get; set; } - public string QualifyName(TopLevel topLevel) + public string QualifyTypeName(IProtobufType type) { - List names = [Name]; + if (type is not TopLevel { Parent: not null } typeTopLevel + || typeTopLevel.Parent == this) + return type.Name; - TopLevel? parent = Parent; - while (parent is not null && parent != topLevel) + List names = [typeTopLevel.Name]; + + TopLevel? parent = typeTopLevel.Parent; + while (parent is not null && parent != this) { names.Add(parent.Name); parent = parent.Parent; diff --git a/src/LibProtodec/Models/Protobuf/Types/IProtobufType.cs b/src/LibProtodec/Models/Protobuf/Types/IProtobufType.cs index a272204..78618c1 100644 --- a/src/LibProtodec/Models/Protobuf/Types/IProtobufType.cs +++ b/src/LibProtodec/Models/Protobuf/Types/IProtobufType.cs @@ -9,10 +9,4 @@ namespace LibProtodec.Models.Protobuf.Types; public interface IProtobufType { string Name { get; } -} - -public sealed class External(string typeName) : IProtobufType -{ - public string Name => - typeName; } \ No newline at end of file diff --git a/src/LibProtodec/Models/Protobuf/Types/Scalar.cs b/src/LibProtodec/Models/Protobuf/Types/Scalar.cs index 2feec68..e187062 100644 --- a/src/LibProtodec/Models/Protobuf/Types/Scalar.cs +++ b/src/LibProtodec/Models/Protobuf/Types/Scalar.cs @@ -7,21 +7,24 @@ namespace LibProtodec.Models.Protobuf.Types; // ReSharper disable StringLiteralTypo -public static class Scalar +public sealed class Scalar(string typeName) : IProtobufType { - public static readonly IProtobufType Bool = new External("bool"); - public static readonly IProtobufType Bytes = new External("bytes"); - public static readonly IProtobufType Double = new External("double"); - public static readonly IProtobufType Fixed32 = new External("fixed32"); - public static readonly IProtobufType Fixed64 = new External("fixed64"); - public static readonly IProtobufType Float = new External("float"); - public static readonly IProtobufType Int32 = new External("int32"); - public static readonly IProtobufType Int64 = new External("int64"); - public static readonly IProtobufType SFixed32 = new External("sfixed32"); - public static readonly IProtobufType SFixed64 = new External("sfixed64"); - public static readonly IProtobufType SInt32 = new External("sint32"); - public static readonly IProtobufType SInt64 = new External("sint64"); - public static readonly IProtobufType String = new External("string"); - public static readonly IProtobufType UInt32 = new External("uint32"); - public static readonly IProtobufType UInt64 = new External("uint64"); + public string Name => + typeName; + + public static readonly IProtobufType Bool = new Scalar("bool"); + public static readonly IProtobufType Bytes = new Scalar("bytes"); + public static readonly IProtobufType Double = new Scalar("double"); + public static readonly IProtobufType Fixed32 = new Scalar("fixed32"); + public static readonly IProtobufType Fixed64 = new Scalar("fixed64"); + public static readonly IProtobufType Float = new Scalar("float"); + public static readonly IProtobufType Int32 = new Scalar("int32"); + public static readonly IProtobufType Int64 = new Scalar("int64"); + public static readonly IProtobufType SFixed32 = new Scalar("sfixed32"); + public static readonly IProtobufType SFixed64 = new Scalar("sfixed64"); + public static readonly IProtobufType SInt32 = new Scalar("sint32"); + public static readonly IProtobufType SInt64 = new Scalar("sint64"); + public static readonly IProtobufType String = new Scalar("string"); + public static readonly IProtobufType UInt32 = new Scalar("uint32"); + public static readonly IProtobufType UInt64 = new Scalar("uint64"); } \ No newline at end of file diff --git a/src/LibProtodec/Models/Protobuf/Types/WellKnown.cs b/src/LibProtodec/Models/Protobuf/Types/WellKnown.cs index 488ac4a..e84b8f9 100644 --- a/src/LibProtodec/Models/Protobuf/Types/WellKnown.cs +++ b/src/LibProtodec/Models/Protobuf/Types/WellKnown.cs @@ -6,34 +6,40 @@ namespace LibProtodec.Models.Protobuf.Types; -public static class WellKnown +public sealed class WellKnown(string typeName, string fileName) : IProtobufType { - public static readonly IProtobufType Any = new External("google.protobuf.Any"); - public static readonly IProtobufType Api = new External("google.protobuf.Api"); - public static readonly IProtobufType BoolValue = new External("google.protobuf.BoolValue"); - public static readonly IProtobufType BytesValue = new External("google.protobuf.BytesValue"); - public static readonly IProtobufType DoubleValue = new External("google.protobuf.DoubleValue"); - public static readonly IProtobufType Duration = new External("google.protobuf.Duration"); - public static readonly IProtobufType Empty = new External("google.protobuf.Empty"); - public static readonly IProtobufType Enum = new External("google.protobuf.Enum"); - public static readonly IProtobufType EnumValue = new External("google.protobuf.EnumValue"); - public static readonly IProtobufType Field = new External("google.protobuf.Field"); - public static readonly IProtobufType FieldMask = new External("google.protobuf.FieldMask"); - public static readonly IProtobufType FloatValue = new External("google.protobuf.FloatValue"); - public static readonly IProtobufType Int32Value = new External("google.protobuf.Int32Value"); - public static readonly IProtobufType Int64Value = new External("google.protobuf.Int64Value"); - public static readonly IProtobufType ListValue = new External("google.protobuf.ListValue"); - public static readonly IProtobufType Method = new External("google.protobuf.Method"); - public static readonly IProtobufType Mixin = new External("google.protobuf.Mixin"); - public static readonly IProtobufType NullValue = new External("google.protobuf.NullValue"); - public static readonly IProtobufType Option = new External("google.protobuf.Option"); - public static readonly IProtobufType SourceContext = new External("google.protobuf.SourceContext"); - public static readonly IProtobufType StringValue = new External("google.protobuf.StringValue"); - public static readonly IProtobufType Struct = new External("google.protobuf.Struct"); - public static readonly IProtobufType Syntax = new External("google.protobuf.Syntax"); - public static readonly IProtobufType Timestamp = new External("google.protobuf.Timestamp"); - public static readonly IProtobufType Type = new External("google.protobuf.Type"); - public static readonly IProtobufType UInt32Value = new External("google.protobuf.UInt32Value"); - public static readonly IProtobufType UInt64Value = new External("google.protobuf.UInt64Value"); - public static readonly IProtobufType Value = new External("google.protobuf.Value"); + public string Name => + typeName; + + public string FileName => + fileName; + + public static readonly IProtobufType Any = new WellKnown("google.protobuf.Any", "google/protobuf/any.proto"); + public static readonly IProtobufType Api = new WellKnown("google.protobuf.Api", "google/protobuf/api.proto"); + public static readonly IProtobufType BoolValue = new WellKnown("google.protobuf.BoolValue", "google/protobuf/wrappers.proto"); + public static readonly IProtobufType BytesValue = new WellKnown("google.protobuf.BytesValue", "google/protobuf/wrappers.proto"); + public static readonly IProtobufType DoubleValue = new WellKnown("google.protobuf.DoubleValue", "google/protobuf/wrappers.proto"); + public static readonly IProtobufType Duration = new WellKnown("google.protobuf.Duration", "google/protobuf/duration.proto"); + public static readonly IProtobufType Empty = new WellKnown("google.protobuf.Empty", "google/protobuf/empty.proto"); + public static readonly IProtobufType Enum = new WellKnown("google.protobuf.Enum", "google/protobuf/type.proto"); + public static readonly IProtobufType EnumValue = new WellKnown("google.protobuf.EnumValue", "google/protobuf/type.proto"); + public static readonly IProtobufType Field = new WellKnown("google.protobuf.Field", "google/protobuf/type.proto"); + public static readonly IProtobufType FieldMask = new WellKnown("google.protobuf.FieldMask", "google/protobuf/field_mask.proto"); + public static readonly IProtobufType FloatValue = new WellKnown("google.protobuf.FloatValue", "google/protobuf/wrappers.proto"); + public static readonly IProtobufType Int32Value = new WellKnown("google.protobuf.Int32Value", "google/protobuf/wrappers.proto"); + public static readonly IProtobufType Int64Value = new WellKnown("google.protobuf.Int64Value", "google/protobuf/wrappers.proto"); + public static readonly IProtobufType ListValue = new WellKnown("google.protobuf.ListValue", "google/protobuf/struct.proto"); + public static readonly IProtobufType Method = new WellKnown("google.protobuf.Method", "google/protobuf/api.proto"); + public static readonly IProtobufType Mixin = new WellKnown("google.protobuf.Mixin", "google/protobuf/api.proto"); + public static readonly IProtobufType NullValue = new WellKnown("google.protobuf.NullValue", "google/protobuf/struct.proto"); + public static readonly IProtobufType Option = new WellKnown("google.protobuf.Option", "google/protobuf/type.proto"); + public static readonly IProtobufType SourceContext = new WellKnown("google.protobuf.SourceContext", "google/protobuf/source_context.proto"); + public static readonly IProtobufType StringValue = new WellKnown("google.protobuf.StringValue", "google/protobuf/wrappers.proto"); + public static readonly IProtobufType Struct = new WellKnown("google.protobuf.Struct", "google/protobuf/struct.proto"); + public static readonly IProtobufType Syntax = new WellKnown("google.protobuf.Syntax", "google/protobuf/type.proto"); + public static readonly IProtobufType Timestamp = new WellKnown("google.protobuf.Timestamp", "google/protobuf/timestamp.proto"); + public static readonly IProtobufType Type = new WellKnown("google.protobuf.Type", "google/protobuf/type.proto"); + public static readonly IProtobufType UInt32Value = new WellKnown("google.protobuf.UInt32Value", "google/protobuf/wrappers.proto"); + public static readonly IProtobufType UInt64Value = new WellKnown("google.protobuf.UInt64Value", "google/protobuf/wrappers.proto"); + public static readonly IProtobufType Value = new WellKnown("google.protobuf.Value", "google/protobuf/struct.proto"); } \ No newline at end of file diff --git a/src/LibProtodec/ProtodecContext.cs b/src/LibProtodec/ProtodecContext.cs index a6939d5..5aa905e 100644 --- a/src/LibProtodec/ProtodecContext.cs +++ b/src/LibProtodec/ProtodecContext.cs @@ -17,22 +17,22 @@ using LibProtodec.Models.Protobuf; using LibProtodec.Models.Protobuf.Fields; using LibProtodec.Models.Protobuf.TopLevels; using LibProtodec.Models.Protobuf.Types; +using Microsoft.Extensions.Logging; namespace LibProtodec; -public delegate bool TypeLookupFunc(ICilType cilType, [NotNullWhen(true)] out IProtobufType? protobufType, out string? import); public delegate bool NameLookupFunc(string name, [MaybeNullWhen(false)] out string translatedName); -public sealed class ProtodecContext +// ReSharper disable ClassWithVirtualMembersNeverInherited.Global, MemberCanBePrivate.Global, MemberCanBeProtected.Global +public class ProtodecContext { private readonly Dictionary _parsed = []; public readonly List Protobufs = []; - public NameLookupFunc? NameLookup { get; set; } + public ILogger? Logger { get; set; } - public TypeLookupFunc TypeLookup { get; set; } = - LookupScalarAndWellKnownTypes; + public NameLookupFunc? NameLookup { get; set; } public void WriteAllTo(IndentedTextWriter writer) { @@ -49,19 +49,21 @@ public sealed class ProtodecContext } } - public Message ParseMessage(ICilType messageClass, ParserOptions options = ParserOptions.None) + public virtual Message ParseMessage(ICilType messageClass, ParserOptions options = ParserOptions.None) { Guard.IsTrue(messageClass is { IsClass: true, IsSealed: true }); + using IDisposable? _ = Logger?.BeginScopeParsingMessage(messageClass.FullName); if (_parsed.TryGetValue(messageClass.FullName, out TopLevel? parsedMessage)) { + Logger?.LogParsedMessage(parsedMessage.Name); return (Message)parsedMessage; } Message message = new() { Name = TranslateTypeName(messageClass), - IsObsolete = HasObsoleteAttribute(messageClass.GetCustomAttributes()) + IsObsolete = HasObsoleteAttribute(messageClass.CustomAttributes) }; _parsed.Add(messageClass.FullName, message); @@ -77,20 +79,23 @@ public sealed class ProtodecContext for (int pi = 0, fi = 0; pi < properties.Count; pi++) { - ICilProperty property = properties[pi]; - List attributes = property.GetCustomAttributes().ToList(); + ICilProperty property = properties[pi]; + ICilType propertyType = property.Type; - if (((options & ParserOptions.IncludePropertiesWithoutNonUserCodeAttribute) == 0 && !HasNonUserCodeAttribute(attributes))) + using IDisposable? __ = Logger?.BeginScopeParsingProperty(property.Name, propertyType.FullName); + + if (((options & ParserOptions.IncludePropertiesWithoutNonUserCodeAttribute) == 0 && !HasNonUserCodeAttribute(property.CustomAttributes))) { + Logger?.LogSkippingPropertyWithoutNonUserCodeAttribute(); continue; } - ICilType propertyType = property.Type; - // only OneOf enums are defined nested directly in the message class if (propertyType.IsEnum && propertyType.DeclaringType?.Name == messageClass.Name) { string oneOfName = TranslateOneOfPropName(property.Name); + Logger?.LogParsedOneOfField(oneOfName); + List oneOfProtoFieldIds = propertyType.GetFields() .Where(static field => field.IsLiteral) .Select(static field => (int)field.ConstantValue!) @@ -108,51 +113,63 @@ public sealed class ProtodecContext pi++; } - MessageField field = new() + if (idFields.Count <= fi) + { + Logger?.LogFailedToLocateIdField(); + continue; + } + + MessageField field = new(message) { Type = ParseFieldType(propertyType, options, protobuf), Name = TranslateMessageFieldName(property.Name), Id = (int)idFields[fi].ConstantValue!, - IsObsolete = HasObsoleteAttribute(attributes), + IsObsolete = HasObsoleteAttribute(property.CustomAttributes), HasHasProp = msgFieldHasHasProp }; + Logger?.LogParsedField(field.Name, field.Id, field.Type.Name); message.Fields.Add(field.Id, field); fi++; } + Logger?.LogParsedMessage(message.Name); return message; } - public Enum ParseEnum(ICilType enumEnum, ParserOptions options = ParserOptions.None) + public virtual Enum ParseEnum(ICilType enumEnum, ParserOptions options = ParserOptions.None) { Guard.IsTrue(enumEnum.IsEnum); + using IDisposable? _ = Logger?.BeginScopeParsingEnum(enumEnum.FullName); if (_parsed.TryGetValue(enumEnum.FullName, out TopLevel? parsedEnum)) { + Logger?.LogParsedEnum(parsedEnum.Name); return (Enum)parsedEnum; } Enum @enum = new() { Name = TranslateTypeName(enumEnum), - IsObsolete = HasObsoleteAttribute(enumEnum.GetCustomAttributes()) + IsObsolete = HasObsoleteAttribute(enumEnum.CustomAttributes) }; _parsed.Add(enumEnum.FullName, @enum); Protobuf protobuf = GetProtobuf(enumEnum, @enum, options); - foreach (ICilField field in enumEnum.GetFields().Where(static field => field.IsLiteral)) + foreach (ICilField enumField in enumEnum.GetFields().Where(static field => field.IsLiteral)) { - List attributes = field.GetCustomAttributes().ToList(); + using IDisposable? __ = Logger?.BeginScopeParsingField(enumField.Name); - @enum.Fields.Add( - new EnumField - { - Id = (int)field.ConstantValue!, - Name = TranslateEnumFieldName(attributes, field.Name, @enum.Name), - IsObsolete = HasObsoleteAttribute(attributes) - }); + EnumField field = new() + { + Id = (int)enumField.ConstantValue!, + Name = TranslateEnumFieldName(enumField.CustomAttributes, enumField.Name, @enum.Name), + IsObsolete = HasObsoleteAttribute(enumField.CustomAttributes) + }; + + Logger?.LogParsedField(field.Name, field.Id); + @enum.Fields.Add(field); } if (@enum.Fields.All(static field => field.Id != 0)) @@ -161,10 +178,11 @@ public sealed class ProtodecContext @enum.IsClosed = true; } + Logger?.LogParsedEnum(@enum.Name); return @enum; } - public Service ParseService(ICilType serviceClass, ParserOptions options = ParserOptions.None) + public virtual Service ParseService(ICilType serviceClass, ParserOptions options = ParserOptions.None) { Guard.IsTrue(serviceClass.IsClass); @@ -190,31 +208,35 @@ public sealed class ProtodecContext } Guard.IsNotNull(isClientClass); + using IDisposable? _ = Logger?.BeginScopeParsingService(serviceClass.DeclaringType!.FullName); if (_parsed.TryGetValue(serviceClass.DeclaringType!.FullName, out TopLevel? parsedService)) { + Logger?.LogParsedService(parsedService.Name); return (Service)parsedService; } Service service = new() { Name = TranslateTypeName(serviceClass.DeclaringType), - IsObsolete = HasObsoleteAttribute(serviceClass.GetCustomAttributes()) + IsObsolete = HasObsoleteAttribute(serviceClass.CustomAttributes) }; _parsed.Add(serviceClass.DeclaringType!.FullName, service); Protobuf protobuf = NewProtobuf(serviceClass, service); - foreach (ICilMethod method in serviceClass.GetMethods().Where(static method => method is { IsInherited: false, IsPublic: true, IsStatic: false })) + foreach (ICilMethod cilMethod in serviceClass.GetMethods().Where(static method => method is { IsInherited: false, IsPublic: true, IsStatic: false, IsConstructor: false })) { - List attributes = method.GetCustomAttributes().ToList(); + using IDisposable? __ = Logger?.BeginScopeParsingMethod(cilMethod.Name); + if ((options & ParserOptions.IncludeServiceMethodsWithoutGeneratedCodeAttribute) == 0 - && !HasGeneratedCodeAttribute(attributes, "grpc_csharp_plugin")) + && !HasGeneratedCodeAttribute(cilMethod.CustomAttributes, "grpc_csharp_plugin")) { + Logger?.LogSkippingMethodWithoutGeneratedCodeAttribute(); continue; } - ICilType requestType, responseType, returnType = method.ReturnType; + ICilType requestType, responseType, returnType = cilMethod.ReturnType; bool streamReq, streamRes; if (isClientClass.Value) @@ -222,12 +244,14 @@ public sealed class ProtodecContext string returnTypeName = TranslateTypeName(returnType); if (returnTypeName == "AsyncUnaryCall`1") { + Logger?.LogSkippingDuplicateMethod(); continue; } - List parameters = method.GetParameterTypes().ToList(); + List parameters = cilMethod.GetParameterTypes().ToList(); if (parameters.Count > 2) { + Logger?.LogSkippingDuplicateMethod(); continue; } @@ -255,7 +279,7 @@ public sealed class ProtodecContext } else { - List parameters = method.GetParameterTypes().ToList(); + List parameters = cilMethod.GetParameterTypes().ToList(); if (parameters[0].GenericTypeArguments.Count == 1) { @@ -280,22 +304,25 @@ public sealed class ProtodecContext } } - service.Methods.Add( - new ServiceMethod - { - Name = TranslateMethodName(method.Name), - IsObsolete = HasObsoleteAttribute(attributes), - RequestType = ParseFieldType(requestType, options, protobuf), - ResponseType = ParseFieldType(responseType, options, protobuf), - IsRequestStreamed = streamReq, - IsResponseStreamed = streamRes - }); + ServiceMethod method = new(service) + { + Name = TranslateMethodName(cilMethod.Name), + IsObsolete = HasObsoleteAttribute(cilMethod.CustomAttributes), + RequestType = ParseFieldType(requestType, options, protobuf), + ResponseType = ParseFieldType(responseType, options, protobuf), + IsRequestStreamed = streamReq, + IsResponseStreamed = streamRes + }; + + Logger?.LogParsedMethod(method.Name, method.RequestType.Name, method.ResponseType.Name); + service.Methods.Add(method); } + Logger?.LogParsedService(service.Name); return service; } - private IProtobufType ParseFieldType(ICilType type, ParserOptions options, Protobuf referencingProtobuf) + protected IProtobufType ParseFieldType(ICilType type, ParserOptions options, Protobuf referencingProtobuf) { switch (type.GenericTypeArguments.Count) { @@ -308,40 +335,166 @@ public sealed class ProtodecContext ParseFieldType(type.GenericTypeArguments[1], options, referencingProtobuf)); } - if (TypeLookup(type, out IProtobufType? fieldType, out string? import)) + if (!LookupType(type, out IProtobufType? fieldType)) { - if (import is not null) + if (type.IsEnum) { - referencingProtobuf.Imports.Add(import); + if ((options & ParserOptions.SkipEnums) > 0) + { + return Scalar.Int32; + } + + fieldType = ParseEnum(type, options); } - - return fieldType; - } - - if (type.IsEnum) - { - if ((options & ParserOptions.SkipEnums) > 0) + else { - return Scalar.Int32; + fieldType = ParseMessage(type, options); } - - fieldType = ParseEnum(type, options); - } - else - { - fieldType = ParseMessage(type, options); } - Protobuf protobuf = ((INestableType)fieldType).Protobuf!; - if (referencingProtobuf != protobuf) + switch (fieldType) { - referencingProtobuf.Imports.Add(protobuf.FileName); + case WellKnown wellKnown: + referencingProtobuf.Imports.Add( + wellKnown.FileName); + break; + case INestableType nestableType: + Protobuf protobuf = nestableType.Protobuf!; + if (referencingProtobuf != protobuf) + referencingProtobuf.Imports.Add( + protobuf.FileName); + break; } return fieldType; } - private Protobuf NewProtobuf(ICilType topLevelType, TopLevel topLevel) + protected virtual bool LookupType(ICilType cilType, [NotNullWhen(true)] out IProtobufType? protobufType) + { + switch (cilType.FullName) + { + case "System.String": + protobufType = Scalar.String; + break; + case "System.Boolean": + protobufType = Scalar.Bool; + break; + case "System.Double": + protobufType = Scalar.Double; + break; + case "System.UInt32": + protobufType = Scalar.UInt32; + break; + case "System.UInt64": + protobufType = Scalar.UInt64; + break; + case "System.Int32": + protobufType = Scalar.Int32; + break; + case "System.Int64": + protobufType = Scalar.Int64; + break; + case "System.Single": + protobufType = Scalar.Float; + break; + case "Google.Protobuf.ByteString": + protobufType = Scalar.Bytes; + break; + + case "Google.Protobuf.WellKnownTypes.Any": + protobufType = WellKnown.Any; + break; + case "Google.Protobuf.WellKnownTypes.Api": + protobufType = WellKnown.Api; + break; + case "Google.Protobuf.WellKnownTypes.BoolValue": + protobufType = WellKnown.BoolValue; + break; + case "Google.Protobuf.WellKnownTypes.BytesValue": + protobufType = WellKnown.BytesValue; + break; + case "Google.Protobuf.WellKnownTypes.DoubleValue": + protobufType = WellKnown.DoubleValue; + break; + case "Google.Protobuf.WellKnownTypes.Duration": + protobufType = WellKnown.Duration; + break; + case "Google.Protobuf.WellKnownTypes.Empty": + protobufType = WellKnown.Empty; + break; + case "Google.Protobuf.WellKnownTypes.Enum": + protobufType = WellKnown.Enum; + break; + case "Google.Protobuf.WellKnownTypes.EnumValue": + protobufType = WellKnown.EnumValue; + break; + case "Google.Protobuf.WellKnownTypes.Field": + protobufType = WellKnown.Field; + break; + case "Google.Protobuf.WellKnownTypes.FieldMask": + protobufType = WellKnown.FieldMask; + break; + case "Google.Protobuf.WellKnownTypes.FloatValue": + protobufType = WellKnown.FloatValue; + break; + case "Google.Protobuf.WellKnownTypes.Int32Value": + protobufType = WellKnown.Int32Value; + break; + case "Google.Protobuf.WellKnownTypes.Int64Value": + protobufType = WellKnown.Int64Value; + break; + case "Google.Protobuf.WellKnownTypes.ListValue": + protobufType = WellKnown.ListValue; + break; + case "Google.Protobuf.WellKnownTypes.Method": + protobufType = WellKnown.Method; + break; + case "Google.Protobuf.WellKnownTypes.Mixin": + protobufType = WellKnown.Mixin; + break; + case "Google.Protobuf.WellKnownTypes.NullValue": + protobufType = WellKnown.NullValue; + break; + case "Google.Protobuf.WellKnownTypes.Option": + protobufType = WellKnown.Option; + break; + case "Google.Protobuf.WellKnownTypes.SourceContext": + protobufType = WellKnown.SourceContext; + break; + case "Google.Protobuf.WellKnownTypes.StringValue": + protobufType = WellKnown.StringValue; + break; + case "Google.Protobuf.WellKnownTypes.Struct": + protobufType = WellKnown.Struct; + break; + case "Google.Protobuf.WellKnownTypes.Syntax": + protobufType = WellKnown.Syntax; + break; + case "Google.Protobuf.WellKnownTypes.Timestamp": + protobufType = WellKnown.Timestamp; + break; + case "Google.Protobuf.WellKnownTypes.Type": + protobufType = WellKnown.Type; + break; + case "Google.Protobuf.WellKnownTypes.UInt32Value": + protobufType = WellKnown.UInt32Value; + break; + case "Google.Protobuf.WellKnownTypes.UInt64Value": + protobufType = WellKnown.UInt64Value; + break; + case "Google.Protobuf.WellKnownTypes.Value": + protobufType = WellKnown.Value; + break; + + default: + protobufType = null; + return false; + } + + return true; + } + + protected Protobuf NewProtobuf(ICilType topLevelType, TopLevel topLevel) { Protobuf protobuf = new() { @@ -356,7 +509,7 @@ public sealed class ProtodecContext return protobuf; } - private Protobuf GetProtobuf(ICilType topLevelType, T topLevel, ParserOptions options) + protected Protobuf GetProtobuf(ICilType topLevelType, T topLevel, ParserOptions options) where T : TopLevel, INestableType { Protobuf protobuf; @@ -382,12 +535,12 @@ public sealed class ProtodecContext return protobuf; } - private string TranslateMethodName(string methodName) => + protected string TranslateMethodName(string methodName) => NameLookup?.Invoke(methodName, out string? translatedName) == true ? translatedName : methodName; - private string TranslateOneOfPropName(string oneOfPropName) + protected string TranslateOneOfPropName(string oneOfPropName) { if (NameLookup?.Invoke(oneOfPropName, out string? translatedName) != true) { @@ -402,7 +555,7 @@ public sealed class ProtodecContext return translatedName!.TrimEnd("Case").ToSnakeCaseLower(); } - private string TranslateMessageFieldName(string fieldName) + protected string TranslateMessageFieldName(string fieldName) { if (NameLookup?.Invoke(fieldName, out string? translatedName) != true) { @@ -417,10 +570,10 @@ public sealed class ProtodecContext return translatedName!.ToSnakeCaseLower(); } - private string TranslateEnumFieldName(IEnumerable attributes, string fieldName, string enumName) + protected string TranslateEnumFieldName(IEnumerable attributes, string fieldName, string enumName) { if (attributes.SingleOrDefault(static attr => attr.Type.Name == "OriginalNameAttribute") - ?.ConstructorArguments[0] is string originalName) + ?.ConstructorArgumentValues[0] is string originalName) { return originalName; } @@ -443,7 +596,7 @@ public sealed class ProtodecContext return enumName + '_' + fieldName; } - private string TranslateTypeName(ICilType type) + protected string TranslateTypeName(ICilType type) { if (NameLookup is null) return type.Name; @@ -469,177 +622,17 @@ public sealed class ProtodecContext return translatedName; } - public static bool LookupScalarAndWellKnownTypes(ICilType cilType, [NotNullWhen(true)] out IProtobufType? protobufType, out string? import) - { - switch (cilType.FullName) - { - case "System.String": - import = null; - protobufType = Scalar.String; - return true; - case "System.Boolean": - import = null; - protobufType = Scalar.Bool; - return true; - case "System.Double": - import = null; - protobufType = Scalar.Double; - return true; - case "System.UInt32": - import = null; - protobufType = Scalar.UInt32; - return true; - case "System.UInt64": - import = null; - protobufType = Scalar.UInt64; - return true; - case "System.Int32": - import = null; - protobufType = Scalar.Int32; - return true; - case "System.Int64": - import = null; - protobufType = Scalar.Int64; - return true; - case "System.Single": - import = null; - protobufType = Scalar.Float; - return true; - case "Google.Protobuf.ByteString": - import = null; - protobufType = Scalar.Bytes; - return true; - case "Google.Protobuf.WellKnownTypes.Any": - import = "google/protobuf/any.proto"; - protobufType = WellKnown.Any; - return true; - case "Google.Protobuf.WellKnownTypes.Api": - import = "google/protobuf/api.proto"; - protobufType = WellKnown.Api; - return true; - case "Google.Protobuf.WellKnownTypes.BoolValue": - import = "google/protobuf/wrappers.proto"; - protobufType = WellKnown.BoolValue; - return true; - case "Google.Protobuf.WellKnownTypes.BytesValue": - import = "google/protobuf/wrappers.proto"; - protobufType = WellKnown.BytesValue; - return true; - case "Google.Protobuf.WellKnownTypes.DoubleValue": - import = "google/protobuf/wrappers.proto"; - protobufType = WellKnown.DoubleValue; - return true; - case "Google.Protobuf.WellKnownTypes.Duration": - import = "google/protobuf/duration.proto"; - protobufType = WellKnown.Duration; - return true; - case "Google.Protobuf.WellKnownTypes.Empty": - import = "google/protobuf/empty.proto"; - protobufType = WellKnown.Empty; - return true; - case "Google.Protobuf.WellKnownTypes.Enum": - import = "google/protobuf/type.proto"; - protobufType = WellKnown.Enum; - return true; - case "Google.Protobuf.WellKnownTypes.EnumValue": - import = "google/protobuf/type.proto"; - protobufType = WellKnown.EnumValue; - return true; - case "Google.Protobuf.WellKnownTypes.Field": - import = "google/protobuf/type.proto"; - protobufType = WellKnown.Field; - return true; - case "Google.Protobuf.WellKnownTypes.FieldMask": - import = "google/protobuf/field_mask.proto"; - protobufType = WellKnown.FieldMask; - return true; - case "Google.Protobuf.WellKnownTypes.FloatValue": - import = "google/protobuf/wrappers.proto"; - protobufType = WellKnown.FloatValue; - return true; - case "Google.Protobuf.WellKnownTypes.Int32Value": - import = "google/protobuf/wrappers.proto"; - protobufType = WellKnown.Int32Value; - return true; - case "Google.Protobuf.WellKnownTypes.Int64Value": - import = "google/protobuf/wrappers.proto"; - protobufType = WellKnown.Int64Value; - return true; - case "Google.Protobuf.WellKnownTypes.ListValue": - import = "google/protobuf/struct.proto"; - protobufType = WellKnown.ListValue; - return true; - case "Google.Protobuf.WellKnownTypes.Method": - import = "google/protobuf/api.proto"; - protobufType = WellKnown.Method; - return true; - case "Google.Protobuf.WellKnownTypes.Mixin": - import = "google/protobuf/api.proto"; - protobufType = WellKnown.Mixin; - return true; - case "Google.Protobuf.WellKnownTypes.NullValue": - import = "google/protobuf/struct.proto"; - protobufType = WellKnown.NullValue; - return true; - case "Google.Protobuf.WellKnownTypes.Option": - import = "google/protobuf/type.proto"; - protobufType = WellKnown.Option; - return true; - case "Google.Protobuf.WellKnownTypes.SourceContext": - import = "google/protobuf/source_context.proto"; - protobufType = WellKnown.SourceContext; - return true; - case "Google.Protobuf.WellKnownTypes.StringValue": - import = "google/protobuf/wrappers.proto"; - protobufType = WellKnown.StringValue; - return true; - case "Google.Protobuf.WellKnownTypes.Struct": - import = "google/protobuf/struct.proto"; - protobufType = WellKnown.Struct; - return true; - case "Google.Protobuf.WellKnownTypes.Syntax": - import = "google/protobuf/type.proto"; - protobufType = WellKnown.Syntax; - return true; - case "Google.Protobuf.WellKnownTypes.Timestamp": - import = "google/protobuf/timestamp.proto"; - protobufType = WellKnown.Timestamp; - return true; - case "Google.Protobuf.WellKnownTypes.Type": - import = "google/protobuf/type.proto"; - protobufType = WellKnown.Type; - return true; - case "Google.Protobuf.WellKnownTypes.UInt32Value": - import = "google/protobuf/wrappers.proto"; - protobufType = WellKnown.UInt32Value; - return true; - case "Google.Protobuf.WellKnownTypes.UInt64Value": - import = "google/protobuf/wrappers.proto"; - protobufType = WellKnown.UInt64Value; - return true; - case "Google.Protobuf.WellKnownTypes.Value": - import = "google/protobuf/struct.proto"; - protobufType = WellKnown.Value; - return true; - - default: - import = null; - protobufType = null; - return false; - } - } - // ReSharper disable once IdentifierTypo - private static bool IsBeebyted(string name) => + protected static bool IsBeebyted(string name) => name.Length == 11 && name.CountUpper() == 11; - private static bool HasGeneratedCodeAttribute(IEnumerable attributes, string tool) => - attributes.Any(attr => attr.Type.Name == nameof(GeneratedCodeAttribute) - && attr.ConstructorArguments[0] as string == tool); + protected static bool HasGeneratedCodeAttribute(IEnumerable attributes, string tool) => + attributes.Any(attr => attr.Type.Name == nameof(GeneratedCodeAttribute) + && attr.ConstructorArgumentValues[0] as string == tool); - private static bool HasNonUserCodeAttribute(IEnumerable attributes) => + protected static bool HasNonUserCodeAttribute(IEnumerable attributes) => attributes.Any(static attr => attr.Type.Name == nameof(DebuggerNonUserCodeAttribute)); - private static bool HasObsoleteAttribute(IEnumerable attributes) => + protected static bool HasObsoleteAttribute(IEnumerable attributes) => attributes.Any(static attr => attr.Type.Name == nameof(ObsoleteAttribute)); } \ No newline at end of file diff --git a/src/protodec/Program.cs b/src/protodec/Program.cs index e99321b..cd6040a 100644 --- a/src/protodec/Program.cs +++ b/src/protodec/Program.cs @@ -13,6 +13,7 @@ using LibProtodec; using LibProtodec.Loaders; using LibProtodec.Models.Cil; using LibProtodec.Models.Protobuf; +using Microsoft.Extensions.Logging; const string indent = " "; const string help = """ @@ -21,6 +22,7 @@ const string help = """ target_assembly_path Either the path to the target assembly or a directory of assemblies, all of which be parsed. out_path An existing directory to output into individual files, otherwise output to a single file. Options: + --debug Drops the minimum log level to Debug. --parse_service_servers Parses gRPC service definitions from server classes. --parse_service_clients Parses gRPC service definitions from client classes. --skip_enums Skip parsing enums and replace references to them with int32. @@ -37,6 +39,9 @@ if (args.Length < 2) string assembly = args[0]; string outPath = Path.GetFullPath(args[1]); ParserOptions options = ParserOptions.None; +LogLevel logLevel = args.Contains("--debug") + ? LogLevel.Debug + : LogLevel.Information; if (args.Contains("--skip_enums")) options |= ParserOptions.SkipEnums; @@ -47,9 +52,21 @@ if (args.Contains("--include_properties_without_non_user_code_attribute")) if (args.Contains("--include_service_methods_without_generated_code_attribute")) options |= ParserOptions.IncludeServiceMethodsWithoutGeneratedCodeAttribute; -using ICilAssemblyLoader loader = new ClrAssemblyLoader(assembly); -ProtodecContext ctx = new(); +using ILoggerFactory loggerFactory = LoggerFactory.Create( + builder => builder.AddSimpleConsole(static console => console.IncludeScopes = true) + .SetMinimumLevel(logLevel)); +ILogger logger = loggerFactory.CreateLogger("protodec"); +logger.LogInformation("Loading target assemblies..."); +using CilAssemblyLoader loader = new ClrAssemblyLoader( + assembly, loggerFactory.CreateLogger()); + +ProtodecContext ctx = new() +{ + Logger = loggerFactory.CreateLogger() +}; + +logger.LogInformation("Parsing Protobuf message types..."); foreach (ICilType message in GetProtobufMessageTypes()) { ctx.ParseMessage(message, options); @@ -57,6 +74,7 @@ foreach (ICilType message in GetProtobufMessageTypes()) if (args.Contains("--parse_service_servers")) { + logger.LogInformation("Parsing Protobuf service server types..."); foreach (ICilType service in GetProtobufServiceServerTypes()) { ctx.ParseService(service, options); @@ -65,6 +83,7 @@ if (args.Contains("--parse_service_servers")) if (args.Contains("--parse_service_clients")) { + logger.LogInformation("Parsing Protobuf service client types..."); foreach (ICilType service in GetProtobufServiceClientTypes()) { ctx.ParseService(service, options); @@ -73,8 +92,9 @@ if (args.Contains("--parse_service_clients")) if (Directory.Exists(outPath)) { - HashSet writtenFiles = []; + logger.LogInformation("Writing {count} Protobuf files to \"{path}\"...", ctx.Protobufs.Count, outPath); + HashSet writtenFiles = []; foreach (Protobuf protobuf in ctx.Protobufs) { // This workaround stops files from being overwritten in the case of a naming conflict, @@ -95,6 +115,8 @@ if (Directory.Exists(outPath)) } else { + logger.LogInformation("Writing Protobufs as a single file to \"{path}\"...", outPath); + using StreamWriter streamWriter = new(outPath); using IndentedTextWriter indentWriter = new(streamWriter, indent); @@ -115,4 +137,4 @@ IEnumerable GetProtobufServiceClientTypes() => IEnumerable GetProtobufServiceServerTypes() => loader.LoadedTypes.Where( type => type is { IsNested: true, IsAbstract: true, DeclaringType: { IsNested: false, IsSealed: true, IsAbstract: true } } - && type.GetCustomAttributes().Any(attribute => attribute.Type == loader.BindServiceMethodAttribute)); \ No newline at end of file + && type.CustomAttributes.Any(attribute => attribute.Type == loader.BindServiceMethodAttribute)); \ No newline at end of file diff --git a/src/protodec/protodec.csproj b/src/protodec/protodec.csproj index 998145b..9a5da0a 100644 --- a/src/protodec/protodec.csproj +++ b/src/protodec/protodec.csproj @@ -12,6 +12,7 @@ + \ No newline at end of file