From 69a7bb4becd4333e5f465e92c9a44d53688bf72a Mon Sep 17 00:00:00 2001 From: ds5678 <49847914+ds5678@users.noreply.github.com> Date: Mon, 6 Jul 2026 20:46:31 -0700 Subject: [PATCH 1/5] Improve Diffable-CS keyword handling --- Cpp2IL.Core.Tests/CsFileUtilsTests.cs | 22 ++ .../Model/Contexts/EventAnalysisContext.cs | 42 +++ .../Model/Contexts/MethodAnalysisContext.cs | 2 + .../Model/Contexts/PropertyAnalysisContext.cs | 41 +++ .../Model/Contexts/TypeAnalysisContext.cs | 1 + .../OutputFormats/DiffableCsOutputFormat.cs | 14 +- Cpp2IL.Core/Utils/CsFileUtils.cs | 288 +++++++++--------- 7 files changed, 264 insertions(+), 146 deletions(-) create mode 100644 Cpp2IL.Core.Tests/CsFileUtilsTests.cs diff --git a/Cpp2IL.Core.Tests/CsFileUtilsTests.cs b/Cpp2IL.Core.Tests/CsFileUtilsTests.cs new file mode 100644 index 00000000..e0463876 --- /dev/null +++ b/Cpp2IL.Core.Tests/CsFileUtilsTests.cs @@ -0,0 +1,22 @@ +using Cpp2IL.Core.Utils; + +namespace Cpp2IL.Core.Tests; + +public class CsFileUtilsTests +{ + [Test] + public void TypesShouldHaveCorrectKeywords() + { + var appContext = TestGameLoader.LoadSimple2019Game(); + var mscorlib = appContext.SystemTypes.SystemObjectType.DeclaringAssembly; + + using (Assert.EnterMultipleScope()) + { + Assert.That(CsFileUtils.GetKeyWordsForType(appContext.SystemTypes.SystemObjectType), Is.EqualTo("public class")); + Assert.That(CsFileUtils.GetKeyWordsForType(appContext.SystemTypes.SystemInt32Type), Is.EqualTo("public struct")); + Assert.That(CsFileUtils.GetKeyWordsForType(mscorlib.GetTypeByFullName("System.Array")!), Is.EqualTo("public abstract class")); + Assert.That(CsFileUtils.GetKeyWordsForType(mscorlib.GetTypeByFullName("System.EmptyArray`1")!), Is.EqualTo("internal static class")); + Assert.That(CsFileUtils.GetKeyWordsForType(mscorlib.GetTypeByFullName("System.IDisposable")!), Is.EqualTo("public interface")); + } + } +} diff --git a/Cpp2IL.Core/Model/Contexts/EventAnalysisContext.cs b/Cpp2IL.Core/Model/Contexts/EventAnalysisContext.cs index 8aa788c4..add85526 100644 --- a/Cpp2IL.Core/Model/Contexts/EventAnalysisContext.cs +++ b/Cpp2IL.Core/Model/Contexts/EventAnalysisContext.cs @@ -40,8 +40,50 @@ public TypeAnalysisContext EventType set => OverrideEventType = value; } + public MethodAttributes Visibility + { + get + { + // Determine the most permissive visibility among the adder, remover, and invoker + var adderVisibility = Adder?.Visibility ?? MethodAttributes.PrivateScope; + var removerVisibility = Remover?.Visibility ?? MethodAttributes.PrivateScope; + var invokerVisibility = Invoker?.Visibility ?? MethodAttributes.PrivateScope; + + // public + if (adderVisibility == MethodAttributes.Public || removerVisibility == MethodAttributes.Public || invokerVisibility == MethodAttributes.Public) + return MethodAttributes.Public; + + // protected internal + if (adderVisibility == MethodAttributes.FamORAssem || removerVisibility == MethodAttributes.FamORAssem || invokerVisibility == MethodAttributes.FamORAssem) + return MethodAttributes.FamORAssem; + + // internal + if (adderVisibility == MethodAttributes.Assembly || removerVisibility == MethodAttributes.Assembly || invokerVisibility == MethodAttributes.Assembly) + return MethodAttributes.Assembly; + + // protected + if (adderVisibility == MethodAttributes.Family || removerVisibility == MethodAttributes.Family || invokerVisibility == MethodAttributes.Family) + return MethodAttributes.Family; + + // private protected + if (adderVisibility == MethodAttributes.FamANDAssem || removerVisibility == MethodAttributes.FamANDAssem || invokerVisibility == MethodAttributes.FamANDAssem) + return MethodAttributes.FamANDAssem; + + // private + return MethodAttributes.Private; + } + } + public virtual bool IsStatic => Definition?.IsStatic ?? throw new($"Subclasses must override {nameof(IsStatic)}."); + public bool IsAbstract => Adder?.IsAbstract ?? Remover?.IsAbstract ?? Invoker?.IsAbstract ?? false; + + public bool IsVirtual => Adder?.IsVirtual ?? Remover?.IsVirtual ?? Invoker?.IsVirtual ?? false; + + public bool IsNewSlot => Adder?.IsNewSlot ?? Remover?.IsNewSlot ?? Invoker?.IsNewSlot ?? false; + + public bool IsFinal => Adder?.IsFinal ?? Remover?.IsFinal ?? Invoker?.IsFinal ?? false; + public EventAnalysisContext(Il2CppEventDefinition definition, TypeAnalysisContext parent) : base(definition.token, parent.AppContext) { Definition = definition; diff --git a/Cpp2IL.Core/Model/Contexts/MethodAnalysisContext.cs b/Cpp2IL.Core/Model/Contexts/MethodAnalysisContext.cs index 18b6bbe2..91969a7b 100644 --- a/Cpp2IL.Core/Model/Contexts/MethodAnalysisContext.cs +++ b/Cpp2IL.Core/Model/Contexts/MethodAnalysisContext.cs @@ -89,6 +89,8 @@ public class MethodAnalysisContext : HasGenericParameters, IMethodInfoProvider public bool IsNewSlot => (Attributes & MethodAttributes.NewSlot) != 0; + public bool IsFinal => (Attributes & MethodAttributes.Final) != 0; + protected override int CustomAttributeIndex => Definition?.customAttributeIndex ?? throw new("Subclasses of MethodAnalysisContext should override CustomAttributeIndex if they have custom attributes"); public override AssemblyAnalysisContext CustomAttributeAssembly => DeclaringType?.DeclaringAssembly ?? throw new("Subclasses of MethodAnalysisContext should override CustomAttributeAssembly if they have custom attributes"); diff --git a/Cpp2IL.Core/Model/Contexts/PropertyAnalysisContext.cs b/Cpp2IL.Core/Model/Contexts/PropertyAnalysisContext.cs index 0de88120..a3c1b664 100644 --- a/Cpp2IL.Core/Model/Contexts/PropertyAnalysisContext.cs +++ b/Cpp2IL.Core/Model/Contexts/PropertyAnalysisContext.cs @@ -22,6 +22,14 @@ public class PropertyAnalysisContext : HasCustomAttributesAndName, IPropertyInfo public virtual bool IsStatic => Definition?.IsStatic ?? throw new($"Subclasses must override {nameof(IsStatic)}."); + public bool IsAbstract => Getter?.IsAbstract ?? Setter?.IsAbstract ?? false; + + public bool IsVirtual => Getter?.IsVirtual ?? Setter?.IsVirtual ?? false; + + public bool IsNewSlot => Getter?.IsNewSlot ?? Setter?.IsNewSlot ?? false; + + public bool IsFinal => Getter?.IsFinal ?? Setter?.IsFinal ?? false; + public virtual PropertyAttributes DefaultAttributes => (PropertyAttributes?)Definition?.attrs ?? throw new($"Subclasses must override {nameof(DefaultAttributes)}."); public PropertyAttributes? OverrideAttributes { get; set; } @@ -43,6 +51,39 @@ public TypeAnalysisContext PropertyType set => OverridePropertyType = value; } + public MethodAttributes Visibility + { + get + { + // Determine the most permissive visibility among the getter and setter + var getterVisibility = Getter?.Visibility ?? MethodAttributes.PrivateScope; + var setterVisibility = Setter?.Visibility ?? MethodAttributes.PrivateScope; + + // public + if (getterVisibility == MethodAttributes.Public || setterVisibility == MethodAttributes.Public) + return MethodAttributes.Public; + + // protected internal + if (getterVisibility == MethodAttributes.FamORAssem || setterVisibility == MethodAttributes.FamORAssem) + return MethodAttributes.FamORAssem; + + // internal + if (getterVisibility == MethodAttributes.Assembly || setterVisibility == MethodAttributes.Assembly) + return MethodAttributes.Assembly; + + // protected + if (getterVisibility == MethodAttributes.Family || setterVisibility == MethodAttributes.Family) + return MethodAttributes.Family; + + // private protected + if (getterVisibility == MethodAttributes.FamANDAssem || setterVisibility == MethodAttributes.FamANDAssem) + return MethodAttributes.FamANDAssem; + + // private + return MethodAttributes.Private; + } + } + public PropertyAnalysisContext(Il2CppPropertyDefinition definition, TypeAnalysisContext parent) : base(definition.token, parent.AppContext) { DeclaringType = parent; diff --git a/Cpp2IL.Core/Model/Contexts/TypeAnalysisContext.cs b/Cpp2IL.Core/Model/Contexts/TypeAnalysisContext.cs index 9218c3ca..a3173e81 100644 --- a/Cpp2IL.Core/Model/Contexts/TypeAnalysisContext.cs +++ b/Cpp2IL.Core/Model/Contexts/TypeAnalysisContext.cs @@ -174,6 +174,7 @@ public TypeAttributes Visibility public bool IsAbstract => (Attributes & TypeAttributes.Abstract) != default; public bool IsSealed => (Attributes & TypeAttributes.Sealed) != default; public bool IsStatic => IsAbstract && IsSealed; + public bool IsDelegate => BaseType is not ReferencedTypeAnalysisContext and { Namespace: "System", Name: "MulticastDelegate" }; /// /// Returns the namespace of this type expressed as a folder hierarchy, with each sub-namespace becoming a sub-directory. diff --git a/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs b/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs index 023f6b82..9a815cb0 100644 --- a/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs +++ b/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs @@ -303,11 +303,11 @@ private static void AppendEvent(StringBuilder sb, EventAnalysisContext evt, int //Add/Remove/Invoke indent++; if (evt.Adder != null) - AppendAccessor(sb, evt.Adder, "add", indent); + AppendAccessor(sb, evt.Adder, "add", indent, evt.Visibility); if (evt.Remover != null) - AppendAccessor(sb, evt.Remover, "remove", indent); + AppendAccessor(sb, evt.Remover, "remove", indent, evt.Visibility); if (evt.Invoker != null) - AppendAccessor(sb, evt.Invoker, "fire", indent); + AppendAccessor(sb, evt.Invoker, "fire", indent, evt.Visibility); indent--; sb.Append('\t', indent); @@ -335,9 +335,9 @@ private static void AppendProperty(StringBuilder sb, PropertyAnalysisContext pro //Get/Set indent++; if (prop.Getter != null) - AppendAccessor(sb, prop.Getter, "get", indent); + AppendAccessor(sb, prop.Getter, "get", indent, prop.Visibility); if (prop.Setter != null) - AppendAccessor(sb, prop.Setter, "set", indent); + AppendAccessor(sb, prop.Setter, "set", indent, prop.Visibility); indent--; sb.Append('\t', indent); @@ -383,13 +383,13 @@ private static void AppendMethod(StringBuilder sb, MethodAnalysisContext method, } //get/set/add/remove/raise - private static void AppendAccessor(StringBuilder sb, MethodAnalysisContext accessor, string accessorType, int indent) + private static void AppendAccessor(StringBuilder sb, MethodAnalysisContext accessor, string accessorType, int indent, MethodAttributes parentVisibility) { //Custom attributes for accessor. Includes a trailing newline AppendCustomAttributes(sb, accessor, indent); sb.Append('\t', indent); - sb.Append(CsFileUtils.GetKeyWordsForMethod(accessor, true, true)); + sb.Append(CsFileUtils.GetKeyWordsForMethod(accessor, parentVisibility)); sb.Append(' '); sb.Append(accessorType); sb.Append(" { } //Length: "); diff --git a/Cpp2IL.Core/Utils/CsFileUtils.cs b/Cpp2IL.Core/Utils/CsFileUtils.cs index afb0b4b1..0c7fc35c 100644 --- a/Cpp2IL.Core/Utils/CsFileUtils.cs +++ b/Cpp2IL.Core/Utils/CsFileUtils.cs @@ -18,19 +18,73 @@ public static class CsFileUtils /// A properly-formatted parameter string as described above. public static string GetMethodParameterString(MethodAnalysisContext method) { - var sb = new StringBuilder(); - var first = true; - foreach (var paramData in method!.Parameters!) - { - if (!first) - sb.Append(", "); + // ToString on the ParameterData will do the right thing. + return string.Join(", ", method.Parameters); + } - first = false; + public static string GetAccessModifiers(TypeAnalysisContext type) => type.Visibility switch + { + TypeAttributes.Public => "public", + TypeAttributes.NotPublic => "internal", + TypeAttributes.NestedPublic => "public", + TypeAttributes.NestedAssembly => "internal", + TypeAttributes.NestedPrivate => "private", + TypeAttributes.NestedFamily => "protected", + TypeAttributes.NestedFamORAssem => "protected internal", + TypeAttributes.NestedFamANDAssem => "private protected", + _ => throw new ArgumentOutOfRangeException($"Unknown visibility for type {type.FullName}: {type.Visibility}") + }; + + public static string GetAccessModifiers(FieldAnalysisContext field) => field.Visibility switch + { + FieldAttributes.Public => "public", + FieldAttributes.Private => "private", + FieldAttributes.Family => "protected", + FieldAttributes.Assembly => "internal", + FieldAttributes.FamORAssem => "protected internal", + FieldAttributes.FamANDAssem => "private protected", + _ => throw new ArgumentOutOfRangeException($"Unknown visibility for field {field.DeclaringType.FullName}.{field.Name}: {field.Visibility}") + }; - sb.Append(paramData); //ToString on the ParameterData will do the right thing. - } + public static string GetAccessModifiers(MethodAnalysisContext method) => GetAccessModifiers(method.Visibility); - return sb.ToString(); + public static string GetAccessModifiers(PropertyAnalysisContext property) => GetAccessModifiers(property.Visibility); + + public static string GetAccessModifiers(EventAnalysisContext evt) => GetAccessModifiers(evt.Visibility); + + private static string GetAccessModifiers(MethodAttributes visibility) => visibility switch + { + MethodAttributes.Public => "public", + MethodAttributes.Private or MethodAttributes.PrivateScope => "private", + MethodAttributes.Family => "protected", + MethodAttributes.Assembly => "internal", + MethodAttributes.FamORAssem => "protected internal", + MethodAttributes.FamANDAssem => "private protected", + _ => throw new ArgumentOutOfRangeException($"Unknown visibility: {visibility}") + }; + + public static string GetTypeDeclarationKeyword(TypeAnalysisContext type) + { + if (type.IsEnumType) + return "enum"; + if (type.IsValueType) + return "struct"; + if (type.IsInterface) + return "interface"; + if (type.IsDelegate) + return "delegate"; + return "class"; + } + + public static string? GetClassInheritanceKeyword(TypeAnalysisContext type) + { + if (type.IsStatic) + return "static"; + if (type.IsAbstract && !type.IsInterface) + return "abstract"; + if (type.IsSealed && !type.IsValueType && !type.IsDelegate) + return "sealed"; + return null; } /// @@ -40,36 +94,10 @@ public static string GetMethodParameterString(MethodAnalysisContext method) /// The type to generate the keywords for public static string GetKeyWordsForType(TypeAnalysisContext type) { - var sb = new StringBuilder(); - var attributes = type.Definition!.Attributes; - - if (attributes.HasFlag(TypeAttributes.NestedPrivate)) - sb.Append("private "); - else if (attributes.HasFlag(TypeAttributes.Public)) - sb.Append("public "); - else - sb.Append("internal "); //private top-level classes don't exist, for obvious reasons - - if (type.IsEnumType) - sb.Append("enum "); - else if (type.IsValueType) - sb.Append("struct "); - else if (attributes.HasFlag(TypeAttributes.Interface)) - sb.Append("interface "); - else - { - if (attributes.HasFlag(TypeAttributes.Abstract) && attributes.HasFlag(TypeAttributes.Sealed)) - //Abstract Sealed => Static - sb.Append("static "); - else if (attributes.HasFlag(TypeAttributes.Abstract)) - sb.Append("abstract "); - else if (attributes.HasFlag(TypeAttributes.Sealed)) - sb.Append("sealed "); - - sb.Append("class "); - } - - return sb.ToString().Trim(); + var visibility = GetAccessModifiers(type); + var inheritance = GetClassInheritanceKeyword(type); + var declaration = GetTypeDeclarationKeyword(type); + return inheritance is null ? $"{visibility} {declaration}" : $"{visibility} {inheritance} {declaration}"; } /// @@ -80,16 +108,9 @@ public static string GetKeyWordsForType(TypeAnalysisContext type) public static string GetKeyWordsForField(FieldAnalysisContext field) { var sb = new StringBuilder(); - var attributes = field.BackingData!.Attributes; + var attributes = field.Attributes; - if (attributes.HasFlag(FieldAttributes.Public)) - sb.Append("public "); - else if (attributes.HasFlag(FieldAttributes.Family)) - sb.Append("protected "); - if (attributes.HasFlag(FieldAttributes.Assembly)) - sb.Append("internal "); - else if (attributes.HasFlag(FieldAttributes.Private)) - sb.Append("private "); + sb.Append(GetAccessModifiers(field)).Append(' '); if (attributes.HasFlag(FieldAttributes.Literal)) sb.Append("const "); @@ -102,7 +123,52 @@ public static string GetKeyWordsForField(FieldAnalysisContext field) sb.Append("readonly "); } - return sb.ToString().Trim(); + return sb.ToString().TrimEnd(); + } + + private static string? GetVirtualLookupKeyword(bool isInterfaceMember, bool isStatic, bool isAbstract, bool isVirtual, bool isNewSlot, bool isFinal) + { + // slot-related modifiers like abstract, virtual, override, sealed + + if (isInterfaceMember) + { + if (isAbstract) + return isStatic ? "abstract" : null; + else if (isVirtual) + return isStatic ? "virtual" : null; + else + return isStatic ? null : "sealed"; + } + else if (isAbstract) + { + return "abstract"; + } + else if (isVirtual) + { + if (isNewSlot) + return isFinal ? null : "virtual"; // final, virtual, newslot means an interface implementation + else + return isFinal ? "sealed override" : "override"; + } + else + { + return null; + } + } + + public static string? GetVirtualLookupKeyword(MethodAnalysisContext method) + { + return GetVirtualLookupKeyword(method.DeclaringType?.IsInterface ?? false, method.IsStatic, method.IsAbstract, method.IsVirtual, method.IsNewSlot, method.IsFinal); + } + + public static string? GetVirtualLookupKeyword(PropertyAnalysisContext property) + { + return GetVirtualLookupKeyword(property.DeclaringType.IsInterface, property.IsStatic, property.IsAbstract, property.IsVirtual, property.IsNewSlot, property.IsFinal); + } + + public static string? GetVirtualLookupKeyword(EventAnalysisContext evt) + { + return GetVirtualLookupKeyword(evt.DeclaringType.IsInterface, evt.IsStatic, evt.IsAbstract, evt.IsVirtual, evt.IsNewSlot, evt.IsFinal); } /// @@ -110,42 +176,25 @@ public static string GetKeyWordsForField(FieldAnalysisContext field) /// Does not include the return type, name, or parameters. /// /// The method to generate keywords for - /// Skip slot-related modifiers like abstract, virtual, override - /// Skip the public and static keywords, as those aren't valid for property accessors - public static string GetKeyWordsForMethod(MethodAnalysisContext method, bool skipSlotRelated = false, bool skipKeywordsInvalidForAccessors = false) + /// The visibility of the parent, used to determine if the method's visibility should be included + public static string GetKeyWordsForMethod(MethodAnalysisContext method, MethodAttributes? parentVisibility = null) { var sb = new StringBuilder(); - var attributes = method.Definition!.Attributes; - - if (!skipKeywordsInvalidForAccessors) - { - if (attributes.HasFlag(MethodAttributes.Public)) - sb.Append("public "); - else if (attributes.HasFlag(MethodAttributes.Family)) - sb.Append("protected "); - } - if (attributes.HasFlag(MethodAttributes.Assembly)) - sb.Append("internal "); - else if (attributes.HasFlag(MethodAttributes.Private)) - sb.Append("private "); + if (method.Visibility != parentVisibility) + sb.Append(GetAccessModifiers(method)).Append(' '); - if (!skipKeywordsInvalidForAccessors && attributes.HasFlag(MethodAttributes.Static)) - sb.Append("static "); - - if (method.DeclaringType!.Definition!.Attributes.HasFlag(TypeAttributes.Interface) || skipSlotRelated) + if (parentVisibility is null) { - //Deliberate no-op to avoid unnecessarily marking interface methods as abstract - } - else if (attributes.HasFlag(MethodAttributes.Abstract)) - sb.Append("abstract "); - else if (attributes.HasFlag(MethodAttributes.NewSlot)) - sb.Append("override "); - else if (attributes.HasFlag(MethodAttributes.Virtual)) - sb.Append("virtual "); + if (method.IsStatic) + sb.Append("static "); + var slotKeyword = GetVirtualLookupKeyword(method); + if (slotKeyword != null) + sb.Append(slotKeyword); + } - return sb.ToString().Trim(); + return sb.ToString().TrimEnd(); } /// @@ -157,80 +206,39 @@ public static string GetKeyWordsForEvent(EventAnalysisContext evt) { var sb = new StringBuilder(); - var addAttrs = evt.Adder?.Attributes ?? 0; - var removeAttrs = evt.Remover?.Attributes ?? 0; - var raiseAttrs = evt.Invoker?.Attributes ?? 0; + sb.Append(GetAccessModifiers(evt)).Append(' '); - var all = addAttrs | removeAttrs | raiseAttrs; - - //Accessibility must be that of the most accessible method - if (addAttrs.HasFlag(MethodAttributes.Public) || removeAttrs.HasFlag(MethodAttributes.Public) || raiseAttrs.HasFlag(MethodAttributes.Public)) - sb.Append("public "); - else if (all.HasFlag(MethodAttributes.Family)) //Family is only one bit so we can use the OR'd attributes - sb.Append("protected "); - if (addAttrs.HasFlag(MethodAttributes.Assembly) || removeAttrs.HasFlag(MethodAttributes.Assembly) || raiseAttrs.HasFlag(MethodAttributes.Assembly)) - sb.Append("internal "); - else if (all.HasFlag(MethodAttributes.Private)) - sb.Append("private "); - - if (all.HasFlag(MethodAttributes.Static)) + if (evt.IsStatic) sb.Append("static "); - if (evt.DeclaringType!.Definition!.Attributes.HasFlag(TypeAttributes.Interface)) - { - //Deliberate no-op to avoid unnecessarily marking interface methods as abstract - } - else if (all.HasFlag(MethodAttributes.Abstract)) - sb.Append("abstract "); - else if (all.HasFlag(MethodAttributes.NewSlot)) - sb.Append("override "); - else if (all.HasFlag(MethodAttributes.Virtual)) - sb.Append("virtual "); + var slotKeyword = GetVirtualLookupKeyword(evt); + if (slotKeyword != null) + sb.Append(slotKeyword).Append(' '); - sb.Append("event "); + sb.Append("event"); - return sb.ToString().Trim(); + return sb.ToString(); } /// - /// Returns all the keywords that would be present in the c# source file to generate this event, i.e. access modifiers, static/abstract/etc. - /// Does not include the event type or name + /// Returns all the keywords that would be present in the c# source file to generate this property, i.e. access modifiers, static/abstract/etc. + /// Does not include the property type or name /// - /// The event to generate keywords for + /// The property to generate keywords for public static string GetKeyWordsForProperty(PropertyAnalysisContext prop) { var sb = new StringBuilder(); - var getterAttributes = prop.Getter?.Attributes ?? 0; - var setterAttributes = prop.Setter?.Attributes ?? 0; - - var all = getterAttributes | setterAttributes; + sb.Append(GetAccessModifiers(prop)).Append(' '); - //Accessibility must be that of the most accessible method - if (getterAttributes.HasFlag(MethodAttributes.Public) || setterAttributes.HasFlag(MethodAttributes.Public)) - sb.Append("public "); - else if (all.HasFlag(MethodAttributes.Family)) //Family is only one bit so we can use the OR'd attributes - sb.Append("protected "); - if (getterAttributes.HasFlag(MethodAttributes.Assembly) || setterAttributes.HasFlag(MethodAttributes.Assembly)) - sb.Append("internal "); - else if (all.HasFlag(MethodAttributes.Private)) - sb.Append("private "); - - if (all.HasFlag(MethodAttributes.Static)) + if (prop.IsStatic) sb.Append("static "); - if (prop.DeclaringType!.Definition!.Attributes.HasFlag(TypeAttributes.Interface)) - { - //Deliberate no-op to avoid unnecessarily marking interface methods as abstract - } - else if (all.HasFlag(MethodAttributes.Abstract)) - sb.Append("abstract "); - else if (all.HasFlag(MethodAttributes.NewSlot)) - sb.Append("override "); - else if (all.HasFlag(MethodAttributes.Virtual)) - sb.Append("virtual "); - - return sb.ToString().Trim(); + var slotKeyword = GetVirtualLookupKeyword(prop); + if (slotKeyword != null) + sb.Append(slotKeyword); + + return sb.ToString().TrimEnd(); } /// @@ -326,6 +334,8 @@ public static string GetTypeName(TypeAnalysisContext type) "UInt64" => "ulong", "Int16" => "short", "UInt16" => "ushort", + "IntPtr" => "nint", + "UIntPtr" => "nuint", "String" => "string", "Object" => "object", _ => type.Name, @@ -339,14 +349,14 @@ public static string GetTypeName(TypeAnalysisContext type) /// /// Appends inheritance data (base class and interfaces) for the given type to the given string builder. - /// If the base class is System.Object, System.ValueType, or System.Enum, it will be ignored + /// If the base class is System.Object, System.ValueType, System.Enum, or System.MulticastDelegate, it will be ignored /// /// /// public static void AppendInheritanceInfo(TypeAnalysisContext type, StringBuilder sb) { var baseType = type.BaseType; - var needsBaseClass = baseType is { FullName: not "System.Object" and not "System.ValueType" and not "System.Enum" }; + var needsBaseClass = baseType is not ReferencedTypeAnalysisContext and ({ Namespace: not "System" } or { Name: not "Object" and not "ValueType" and not "Enum" and not "MulticastDelegate" }); if (needsBaseClass) sb.Append(" : ").Append(GetTypeName(baseType!)); From 67a7500d0a533bb3708f83a0a6b1017c54f4c8b2 Mon Sep 17 00:00:00 2001 From: ds5678 <49847914+ds5678@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:27:00 -0700 Subject: [PATCH 2/5] Initial port of StringBuilder -> IndentedTextWriter --- .../OutputFormats/DiffableCsOutputFormat.cs | 291 ++++++++++-------- Cpp2IL.Core/Utils/CsFileUtils.cs | 29 ++ 2 files changed, 191 insertions(+), 129 deletions(-) diff --git a/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs b/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs index 9a815cb0..0b75c86b 100644 --- a/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs +++ b/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs @@ -1,12 +1,12 @@ using System; using System.Buffers.Binary; +using System.CodeDom.Compiler; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; -using System.Text; using Cpp2IL.Core.Api; using Cpp2IL.Core.Extensions; using Cpp2IL.Core.Logging; @@ -51,9 +51,9 @@ public override void DoOutput(ApplicationAnalysisContext context, string outputR } } - private static Dictionary BuildOutput(ApplicationAnalysisContext context, string outputRoot) + private static Dictionary BuildOutput(ApplicationAnalysisContext context, string outputRoot) { - var ret = new Dictionary(); + var ret = new Dictionary(); foreach (var assembly in context.Assemblies) { @@ -68,43 +68,50 @@ private static Dictionary BuildOutput(ApplicationAnalysis var path = Path.Combine(asmPath, type.NamespaceAsSubdirs, MiscUtils.CleanPathElement(type.Name + ".cs")); Directory.CreateDirectory(Path.GetDirectoryName(path)!); - var sb = new StringBuilder(); + var stringWriter = new StringWriter(); + var writer = new IndentedTextWriter(stringWriter, "\t"); //Namespace at top of file if (!string.IsNullOrEmpty(type.Namespace)) - sb.AppendLine($"namespace {type.Namespace};").AppendLine(); + { + writer.WriteLine($"namespace {type.Namespace};"); + writer.WriteLine(); + } else - sb.AppendLine("//Type is in global namespace").AppendLine(); + { + writer.WriteLine("//Type is in global namespace"); + writer.WriteLine(); + } - AppendType(sb, type); + AppendType(writer, type); - ret[path] = sb; + ret[path] = stringWriter; } } return ret; } - private static void AppendType(StringBuilder sb, TypeAnalysisContext type, int indent = 0) + private static void AppendType(IndentedTextWriter writer, TypeAnalysisContext type, int indent = 0) { // if (type.IsCompilerGeneratedBasedOnCustomAttributes) //Do not output compiler-generated types // return; //Custom attributes for type. Includes a trailing newline - AppendCustomAttributes(sb, type, indent); + AppendCustomAttributes(writer, type, indent); //Type declaration line - sb.Append('\t', indent); + writer.Write(new string('\t', indent)); - sb.Append(CsFileUtils.GetKeyWordsForType(type)); - sb.Append(' '); - sb.Append(CsFileUtils.GetTypeName(type)); - CsFileUtils.AppendInheritanceInfo(type, sb); - sb.AppendLine(); - sb.Append('\t', indent); - sb.Append('{'); - sb.AppendLine(); + writer.Write(CsFileUtils.GetKeyWordsForType(type)); + writer.Write(' '); + writer.Write(CsFileUtils.GetTypeName(type)); + CsFileUtils.WriteInheritanceInfo(type, writer); + writer.WriteLine(); + writer.Write(new string('\t', indent)); + writer.Write('{'); + writer.WriteLine(); //Type declaration done, increase indent indent++; @@ -115,12 +122,12 @@ private static void AppendType(StringBuilder sb, TypeAnalysisContext type, int i enumValues.SortByExtractedKey(e => e.Token); //Not as good as sorting by value but it'll do foreach (var enumValue in enumValues) { - sb.Append('\t', indent); - sb.Append(enumValue.Name); - sb.Append(" = "); - sb.Append(InvariantValue(enumValue.BackingData!.DefaultValue)); - sb.Append(','); - sb.AppendLine(); + writer.Write(new string('\t', indent)); + writer.Write(enumValue.Name); + writer.Write(" = "); + writer.Write(InvariantValue(enumValue.BackingData!.DefaultValue)); + writer.Write(','); + writer.WriteLine(); } } else @@ -129,128 +136,150 @@ private static void AppendType(StringBuilder sb, TypeAnalysisContext type, int i var nestedTypes = type.NestedTypes.Clone(); nestedTypes.SortByExtractedKey(t => t.Name); foreach (var nested in nestedTypes) - AppendType(sb, nested, indent); + AppendType(writer, nested, indent); //Fields, offset order, static first var fields = type.Fields.Clone(); fields.SortByExtractedKey(f => f.IsStatic ? f.Offset : f.Offset + 0x1000); foreach (var field in fields) - AppendField(sb, field, indent); + AppendField(writer, field, indent); - sb.AppendLine(); + writer.WriteLine(); //Events, alphabetical order var events = type.Events.Clone(); events.SortByExtractedKey(e => e.Name); foreach (var evt in events) - AppendEvent(sb, evt, indent); + AppendEvent(writer, evt, indent); //Properties, alphabetical order var properties = type.Properties.Clone(); properties.SortByExtractedKey(p => p.Name); foreach (var prop in properties) - AppendProperty(sb, prop, indent); + AppendProperty(writer, prop, indent); //Methods, alphabetical order var methods = type.Methods.Clone(); methods.SortByExtractedKey(m => m.Name); foreach (var method in methods) - AppendMethod(sb, method, indent); + AppendMethod(writer, method, indent); } //Decrease indent, close brace indent--; - sb.Append('\t', indent); - sb.Append('}'); - sb.AppendLine().AppendLine(); + writer.Write(new string('\t', indent)); + writer.Write('}'); + writer.WriteLine(); + writer.WriteLine(); } - private static void AppendField(StringBuilder sb, FieldAnalysisContext field, int indent) + private static void AppendField(IndentedTextWriter writer, FieldAnalysisContext field, int indent) { if (field is InjectedFieldAnalysisContext) return; //Custom attributes for field. Includes a trailing newline - AppendCustomAttributes(sb, field, indent); + AppendCustomAttributes(writer, field, indent); //Field declaration line - sb.Append('\t', indent); - sb.Append(CsFileUtils.GetKeyWordsForField(field)); - sb.Append(' '); - sb.Append(CsFileUtils.GetTypeName(field.FieldType)); - sb.Append(' '); - sb.Append(field.Name); + writer.Write(new string('\t', indent)); + writer.Write(CsFileUtils.GetKeyWordsForField(field)); + writer.Write(' '); + writer.Write(CsFileUtils.GetTypeName(field.FieldType)); + writer.Write(' '); + writer.Write(field.Name); if ((field.Attributes & FieldAttributes.HasFieldRVA) != 0) { var fieldRva = field.StaticArrayInitialValue; if (fieldRva.Length > 0) { - AppendFieldRvaInitializer(sb, field, fieldRva, indent); + AppendFieldRvaInitializer(writer, field, fieldRva, indent); return; } } if (field.BackingData?.DefaultValue is { } defaultValue) { - sb.Append(" = "); + writer.Write(" = "); if (defaultValue is string stringDefaultValue) - sb.Append('"').Append(stringDefaultValue).Append('"'); + { + writer.Write('"'); + writer.Write(stringDefaultValue); + writer.Write('"'); + } else if (defaultValue is char charDefaultValue) - sb.Append("'\\u").Append(((int)charDefaultValue).ToString("X")).Append("'"); + { + writer.Write("'\\u"); + writer.Write(((int)charDefaultValue).ToString("X")); + writer.Write("'"); + } else - sb.Append(InvariantValue(defaultValue)); + writer.Write(InvariantValue(defaultValue)); } - sb.Append("; //Field offset: 0x"); - sb.Append(field.Offset.ToString("X")); + writer.Write("; //Field offset: 0x"); + writer.Write(field.Offset.ToString("X")); if ((field.Attributes & FieldAttributes.HasFieldRVA) != 0) - sb.Append(" || Has Field RVA (address hidden for diffability)"); + writer.Write(" || Has Field RVA (address hidden for diffability)"); - sb.AppendLine(); + writer.WriteLine(); } - private static void AppendFieldRvaInitializer(StringBuilder sb, FieldAnalysisContext field, byte[] data, int indent) + private static void AppendFieldRvaInitializer(IndentedTextWriter writer, FieldAnalysisContext field, byte[] data, int indent) { var tail = $" //Field offset: 0x{field.Offset.ToString("X")} || Has Field RVA (address hidden for diffability)"; if (TryAscendingInt32Array(data, out var ints)) { - sb.Append(" = new int[]").Append(tail).AppendLine(); - sb.Append('\t', indent).Append('{').AppendLine(); + writer.Write(" = new int[]"); + writer.Write(tail); + writer.WriteLine(); + writer.Write(new string('\t', indent)); + writer.Write('{'); + writer.WriteLine(); for (var i = 0; i < ints.Length; i += 12) { var n = Math.Min(12, ints.Length - i); - sb.Append('\t', indent + 1); + writer.Write(new string('\t', indent + 1)); for (var j = 0; j < n; j++) { - if (j > 0) sb.Append(", "); - sb.Append(ints[i + j]); + if (j > 0) writer.Write(", "); + writer.Write(ints[i + j]); } - if (i + n < ints.Length) sb.Append(','); - sb.AppendLine(); + if (i + n < ints.Length) writer.Write(','); + writer.WriteLine(); } - sb.Append('\t', indent).Append("};").AppendLine(); + writer.Write(new string('\t', indent)); + writer.Write("};"); + writer.WriteLine(); return; } - sb.Append(" = new byte[]").Append(tail).AppendLine(); - sb.Append('\t', indent).Append('{').AppendLine(); + writer.Write(" = new byte[]"); + writer.Write(tail); + writer.WriteLine(); + writer.Write(new string('\t', indent)); + writer.Write('{'); + writer.WriteLine(); for (var i = 0; i < data.Length; i += 16) { var n = Math.Min(16, data.Length - i); - sb.Append('\t', indent + 1); + writer.Write(new string('\t', indent + 1)); for (var j = 0; j < n; j++) { - if (j > 0) sb.Append(", "); - sb.Append("0x").Append(data[i + j].ToString("X2")); + if (j > 0) writer.Write(", "); + writer.Write("0x"); + writer.Write(data[i + j].ToString("X2")); } - if (i + n < data.Length) sb.Append(','); - sb.AppendLine(); + if (i + n < data.Length) writer.Write(','); + writer.WriteLine(); } - sb.Append('\t', indent).Append("};").AppendLine(); + writer.Write(new string('\t', indent)); + writer.Write("};"); + writer.WriteLine(); } //blobs that decode as 0 followed by strictly ascending little-endian int32s are (probably) offset tables, @@ -284,121 +313,125 @@ private static bool TryAscendingInt32Array(byte[] b, [NotNullWhen(true)] out int return true; } - private static void AppendEvent(StringBuilder sb, EventAnalysisContext evt, int indent) + private static void AppendEvent(IndentedTextWriter writer, EventAnalysisContext evt, int indent) { //Custom attributes for event. Includes a trailing newline - AppendCustomAttributes(sb, evt, indent); + AppendCustomAttributes(writer, evt, indent); //Event declaration line - sb.Append('\t', indent); - sb.Append(CsFileUtils.GetKeyWordsForEvent(evt)); - sb.Append(' '); - sb.Append(CsFileUtils.GetTypeName(evt.EventType)); - sb.Append(' '); - sb.Append(evt.Name).AppendLine(); - sb.Append('\t', indent); - sb.Append('{'); - sb.AppendLine(); + writer.Write(new string('\t', indent)); + writer.Write(CsFileUtils.GetKeyWordsForEvent(evt)); + writer.Write(' '); + writer.Write(CsFileUtils.GetTypeName(evt.EventType)); + writer.Write(' '); + writer.Write(evt.Name); + writer.WriteLine(); + writer.Write(new string('\t', indent)); + writer.Write('{'); + writer.WriteLine(); //Add/Remove/Invoke indent++; if (evt.Adder != null) - AppendAccessor(sb, evt.Adder, "add", indent, evt.Visibility); + AppendAccessor(writer, evt.Adder, "add", indent, evt.Visibility); if (evt.Remover != null) - AppendAccessor(sb, evt.Remover, "remove", indent, evt.Visibility); + AppendAccessor(writer, evt.Remover, "remove", indent, evt.Visibility); if (evt.Invoker != null) - AppendAccessor(sb, evt.Invoker, "fire", indent, evt.Visibility); + AppendAccessor(writer, evt.Invoker, "fire", indent, evt.Visibility); indent--; - sb.Append('\t', indent); - sb.Append('}'); - sb.AppendLine().AppendLine(); + writer.Write(new string('\t', indent)); + writer.Write('}'); + writer.WriteLine(); + writer.WriteLine(); } - private static void AppendProperty(StringBuilder sb, PropertyAnalysisContext prop, int indent) + private static void AppendProperty(IndentedTextWriter writer, PropertyAnalysisContext prop, int indent) { //Custom attributes for property. Includes a trailing newline - AppendCustomAttributes(sb, prop, indent); + AppendCustomAttributes(writer, prop, indent); //Property declaration line - sb.Append('\t', indent); - sb.Append(CsFileUtils.GetKeyWordsForProperty(prop)); - sb.Append(' '); - sb.Append(CsFileUtils.GetTypeName(prop.PropertyType)); - sb.Append(' '); - sb.Append(prop.Name); - sb.AppendLine(); - sb.Append('\t', indent); - sb.Append('{'); - sb.AppendLine(); + writer.Write(new string('\t', indent)); + writer.Write(CsFileUtils.GetKeyWordsForProperty(prop)); + writer.Write(' '); + writer.Write(CsFileUtils.GetTypeName(prop.PropertyType)); + writer.Write(' '); + writer.Write(prop.Name); + writer.WriteLine(); + writer.Write(new string('\t', indent)); + writer.Write('{'); + writer.WriteLine(); //Get/Set indent++; if (prop.Getter != null) - AppendAccessor(sb, prop.Getter, "get", indent, prop.Visibility); + AppendAccessor(writer, prop.Getter, "get", indent, prop.Visibility); if (prop.Setter != null) - AppendAccessor(sb, prop.Setter, "set", indent, prop.Visibility); + AppendAccessor(writer, prop.Setter, "set", indent, prop.Visibility); indent--; - sb.Append('\t', indent); - sb.Append('}'); - sb.AppendLine().AppendLine(); + writer.Write(new string('\t', indent)); + writer.Write('}'); + writer.WriteLine(); + writer.WriteLine(); } - private static void AppendMethod(StringBuilder sb, MethodAnalysisContext method, int indent) + private static void AppendMethod(IndentedTextWriter writer, MethodAnalysisContext method, int indent) { if (method is InjectedMethodAnalysisContext) return; //Custom attributes for method. Includes a trailing newline - AppendCustomAttributes(sb, method, indent); + AppendCustomAttributes(writer, method, indent); //Method declaration line - sb.Append('\t', indent); - sb.Append(CsFileUtils.GetKeyWordsForMethod(method)); - sb.Append(' '); + writer.Write(new string('\t', indent)); + writer.Write(CsFileUtils.GetKeyWordsForMethod(method)); + writer.Write(' '); if (method.Name is not ".ctor" and not ".cctor") { - sb.Append(CsFileUtils.GetTypeName(method.ReturnType)); - sb.Append(' '); - sb.Append(method.Name); + writer.Write(CsFileUtils.GetTypeName(method.ReturnType)); + writer.Write(' '); + writer.Write(method.Name); } else { //Constructor - sb.Append(CsFileUtils.GetTypeName(method.DeclaringType!)); + writer.Write(CsFileUtils.GetTypeName(method.DeclaringType!)); } - sb.Append('('); - sb.Append(CsFileUtils.GetMethodParameterString(method)); - sb.Append(") { }"); + writer.Write('('); + writer.Write(CsFileUtils.GetMethodParameterString(method)); + writer.Write(") { }"); if (IncludeMethodLength) { - sb.Append(" //Length: "); - sb.Append(method.RawBytes.Length); + writer.Write(" //Length: "); + writer.Write(method.RawBytes.Length); } - sb.AppendLine().AppendLine(); + writer.WriteLine(); + writer.WriteLine(); } //get/set/add/remove/raise - private static void AppendAccessor(StringBuilder sb, MethodAnalysisContext accessor, string accessorType, int indent, MethodAttributes parentVisibility) + private static void AppendAccessor(IndentedTextWriter writer, MethodAnalysisContext accessor, string accessorType, int indent, MethodAttributes parentVisibility) { //Custom attributes for accessor. Includes a trailing newline - AppendCustomAttributes(sb, accessor, indent); - - sb.Append('\t', indent); - sb.Append(CsFileUtils.GetKeyWordsForMethod(accessor, parentVisibility)); - sb.Append(' '); - sb.Append(accessorType); - sb.Append(" { } //Length: "); - sb.Append(accessor.RawBytes.Length); - sb.AppendLine(); + AppendCustomAttributes(writer, accessor, indent); + + writer.Write(new string('\t', indent)); + writer.Write(CsFileUtils.GetKeyWordsForMethod(accessor, parentVisibility)); + writer.Write(' '); + writer.Write(accessorType); + writer.Write(" { } //Length: "); + writer.Write(accessor.RawBytes.Length); + writer.WriteLine(); } - private static void AppendCustomAttributes(StringBuilder sb, HasCustomAttributes owner, int indent) - => sb.Append(CsFileUtils.GetCustomAttributeStrings(owner, indent, true, true)); + private static void AppendCustomAttributes(IndentedTextWriter writer, HasCustomAttributes owner, int indent) + => writer.Write(CsFileUtils.GetCustomAttributeStrings(owner, indent, true, true)); private static string InvariantValue(object? value) => value is null ? "" : value is IFormattable f ? f.ToString(null, CultureInfo.InvariantCulture) : value.ToString() ?? ""; diff --git a/Cpp2IL.Core/Utils/CsFileUtils.cs b/Cpp2IL.Core/Utils/CsFileUtils.cs index 0c7fc35c..555d5b5a 100644 --- a/Cpp2IL.Core/Utils/CsFileUtils.cs +++ b/Cpp2IL.Core/Utils/CsFileUtils.cs @@ -1,4 +1,5 @@ using System; +using System.CodeDom.Compiler; using System.Reflection; using System.Text; using Cpp2IL.Core.Logging; @@ -378,4 +379,32 @@ public static void AppendInheritanceInfo(TypeAnalysisContext type, StringBuilder sb.Append(GetTypeName(iface)); } } + public static void WriteInheritanceInfo(TypeAnalysisContext type, IndentedTextWriter writer) + { + var baseType = type.BaseType; + var needsBaseClass = baseType is not ReferencedTypeAnalysisContext and ({ Namespace: not "System" } or { Name: not "Object" and not "ValueType" and not "Enum" and not "MulticastDelegate" }); + if (needsBaseClass) + { + writer.Write(" : "); + writer.Write(GetTypeName(baseType!)); + } + + //Interfaces + if (type.InterfaceContexts.Count <= 0) + return; + + if (!needsBaseClass) + writer.Write(" : "); + + var addComma = needsBaseClass; + foreach (var iface in type.InterfaceContexts) + { + if (addComma) + writer.Write(", "); + + addComma = true; + + writer.Write(GetTypeName(iface)); + } + } } From 4257b918730d424df5a026ca3a6386c2524fc4d3 Mon Sep 17 00:00:00 2001 From: ds5678 <49847914+ds5678@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:32:48 -0700 Subject: [PATCH 3/5] Fully use IndentedTextWriter handling of indentation --- .../OutputFormats/DiffableCsOutputFormat.cs | 135 +++++++----------- Cpp2IL.Core/Utils/CsFileUtils.cs | 49 ++----- 2 files changed, 63 insertions(+), 121 deletions(-) diff --git a/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs b/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs index 0b75c86b..cf957823 100644 --- a/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs +++ b/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs @@ -75,12 +75,12 @@ private static Dictionary BuildOutput(ApplicationAnalysisC if (!string.IsNullOrEmpty(type.Namespace)) { writer.WriteLine($"namespace {type.Namespace};"); - writer.WriteLine(); + writer.WriteLineNoTabs(string.Empty); } else { writer.WriteLine("//Type is in global namespace"); - writer.WriteLine(); + writer.WriteLineNoTabs(string.Empty); } AppendType(writer, type); @@ -92,29 +92,25 @@ private static Dictionary BuildOutput(ApplicationAnalysisC return ret; } - private static void AppendType(IndentedTextWriter writer, TypeAnalysisContext type, int indent = 0) + private static void AppendType(IndentedTextWriter writer, TypeAnalysisContext type) { // if (type.IsCompilerGeneratedBasedOnCustomAttributes) //Do not output compiler-generated types // return; //Custom attributes for type. Includes a trailing newline - AppendCustomAttributes(writer, type, indent); + AppendCustomAttributes(writer, type); //Type declaration line - writer.Write(new string('\t', indent)); - writer.Write(CsFileUtils.GetKeyWordsForType(type)); writer.Write(' '); writer.Write(CsFileUtils.GetTypeName(type)); CsFileUtils.WriteInheritanceInfo(type, writer); writer.WriteLine(); - writer.Write(new string('\t', indent)); - writer.Write('{'); - writer.WriteLine(); + writer.WriteLine('{'); //Type declaration done, increase indent - indent++; + writer.Indent++; if (type.IsEnumType) { @@ -122,12 +118,10 @@ private static void AppendType(IndentedTextWriter writer, TypeAnalysisContext ty enumValues.SortByExtractedKey(e => e.Token); //Not as good as sorting by value but it'll do foreach (var enumValue in enumValues) { - writer.Write(new string('\t', indent)); writer.Write(enumValue.Name); writer.Write(" = "); writer.Write(InvariantValue(enumValue.BackingData!.DefaultValue)); - writer.Write(','); - writer.WriteLine(); + writer.WriteLine(','); } } else @@ -136,53 +130,50 @@ private static void AppendType(IndentedTextWriter writer, TypeAnalysisContext ty var nestedTypes = type.NestedTypes.Clone(); nestedTypes.SortByExtractedKey(t => t.Name); foreach (var nested in nestedTypes) - AppendType(writer, nested, indent); + AppendType(writer, nested); //Fields, offset order, static first var fields = type.Fields.Clone(); fields.SortByExtractedKey(f => f.IsStatic ? f.Offset : f.Offset + 0x1000); foreach (var field in fields) - AppendField(writer, field, indent); + AppendField(writer, field); - writer.WriteLine(); + writer.WriteLineNoTabs(string.Empty); //Events, alphabetical order var events = type.Events.Clone(); events.SortByExtractedKey(e => e.Name); foreach (var evt in events) - AppendEvent(writer, evt, indent); + AppendEvent(writer, evt); //Properties, alphabetical order var properties = type.Properties.Clone(); properties.SortByExtractedKey(p => p.Name); foreach (var prop in properties) - AppendProperty(writer, prop, indent); + AppendProperty(writer, prop); //Methods, alphabetical order var methods = type.Methods.Clone(); methods.SortByExtractedKey(m => m.Name); foreach (var method in methods) - AppendMethod(writer, method, indent); + AppendMethod(writer, method); } //Decrease indent, close brace - indent--; - writer.Write(new string('\t', indent)); - writer.Write('}'); - writer.WriteLine(); - writer.WriteLine(); + writer.Indent--; + writer.WriteLine('}'); + writer.WriteLineNoTabs(string.Empty); } - private static void AppendField(IndentedTextWriter writer, FieldAnalysisContext field, int indent) + private static void AppendField(IndentedTextWriter writer, FieldAnalysisContext field) { if (field is InjectedFieldAnalysisContext) return; //Custom attributes for field. Includes a trailing newline - AppendCustomAttributes(writer, field, indent); + AppendCustomAttributes(writer, field); //Field declaration line - writer.Write(new string('\t', indent)); writer.Write(CsFileUtils.GetKeyWordsForField(field)); writer.Write(' '); writer.Write(CsFileUtils.GetTypeName(field.FieldType)); @@ -194,7 +185,7 @@ private static void AppendField(IndentedTextWriter writer, FieldAnalysisContext var fieldRva = field.StaticArrayInitialValue; if (fieldRva.Length > 0) { - AppendFieldRvaInitializer(writer, field, fieldRva, indent); + AppendFieldRvaInitializer(writer, field, fieldRva); return; } } @@ -228,7 +219,7 @@ private static void AppendField(IndentedTextWriter writer, FieldAnalysisContext writer.WriteLine(); } - private static void AppendFieldRvaInitializer(IndentedTextWriter writer, FieldAnalysisContext field, byte[] data, int indent) + private static void AppendFieldRvaInitializer(IndentedTextWriter writer, FieldAnalysisContext field, byte[] data) { var tail = $" //Field offset: 0x{field.Offset.ToString("X")} || Has Field RVA (address hidden for diffability)"; @@ -237,13 +228,11 @@ private static void AppendFieldRvaInitializer(IndentedTextWriter writer, FieldAn writer.Write(" = new int[]"); writer.Write(tail); writer.WriteLine(); - writer.Write(new string('\t', indent)); - writer.Write('{'); - writer.WriteLine(); + writer.WriteLine('{'); + writer.Indent++; for (var i = 0; i < ints.Length; i += 12) { var n = Math.Min(12, ints.Length - i); - writer.Write(new string('\t', indent + 1)); for (var j = 0; j < n; j++) { if (j > 0) writer.Write(", "); @@ -252,22 +241,19 @@ private static void AppendFieldRvaInitializer(IndentedTextWriter writer, FieldAn if (i + n < ints.Length) writer.Write(','); writer.WriteLine(); } - writer.Write(new string('\t', indent)); - writer.Write("};"); - writer.WriteLine(); + writer.Indent--; + writer.WriteLine("};"); return; } writer.Write(" = new byte[]"); writer.Write(tail); writer.WriteLine(); - writer.Write(new string('\t', indent)); - writer.Write('{'); - writer.WriteLine(); + writer.WriteLine('{'); + writer.Indent++; for (var i = 0; i < data.Length; i += 16) { var n = Math.Min(16, data.Length - i); - writer.Write(new string('\t', indent + 1)); for (var j = 0; j < n; j++) { if (j > 0) writer.Write(", "); @@ -277,9 +263,8 @@ private static void AppendFieldRvaInitializer(IndentedTextWriter writer, FieldAn if (i + n < data.Length) writer.Write(','); writer.WriteLine(); } - writer.Write(new string('\t', indent)); - writer.Write("};"); - writer.WriteLine(); + writer.Indent--; + writer.WriteLine("};"); } //blobs that decode as 0 followed by strictly ascending little-endian int32s are (probably) offset tables, @@ -313,80 +298,69 @@ private static bool TryAscendingInt32Array(byte[] b, [NotNullWhen(true)] out int return true; } - private static void AppendEvent(IndentedTextWriter writer, EventAnalysisContext evt, int indent) + private static void AppendEvent(IndentedTextWriter writer, EventAnalysisContext evt) { //Custom attributes for event. Includes a trailing newline - AppendCustomAttributes(writer, evt, indent); + AppendCustomAttributes(writer, evt); //Event declaration line - writer.Write(new string('\t', indent)); writer.Write(CsFileUtils.GetKeyWordsForEvent(evt)); writer.Write(' '); writer.Write(CsFileUtils.GetTypeName(evt.EventType)); writer.Write(' '); writer.Write(evt.Name); writer.WriteLine(); - writer.Write(new string('\t', indent)); - writer.Write('{'); - writer.WriteLine(); + writer.WriteLine('{'); //Add/Remove/Invoke - indent++; + writer.Indent++; if (evt.Adder != null) - AppendAccessor(writer, evt.Adder, "add", indent, evt.Visibility); + AppendAccessor(writer, evt.Adder, "add", evt.Visibility); if (evt.Remover != null) - AppendAccessor(writer, evt.Remover, "remove", indent, evt.Visibility); + AppendAccessor(writer, evt.Remover, "remove", evt.Visibility); if (evt.Invoker != null) - AppendAccessor(writer, evt.Invoker, "fire", indent, evt.Visibility); - indent--; + AppendAccessor(writer, evt.Invoker, "fire", evt.Visibility); + writer.Indent--; - writer.Write(new string('\t', indent)); - writer.Write('}'); - writer.WriteLine(); - writer.WriteLine(); + writer.WriteLine('}'); + writer.WriteLineNoTabs(string.Empty); } - private static void AppendProperty(IndentedTextWriter writer, PropertyAnalysisContext prop, int indent) + private static void AppendProperty(IndentedTextWriter writer, PropertyAnalysisContext prop) { //Custom attributes for property. Includes a trailing newline - AppendCustomAttributes(writer, prop, indent); + AppendCustomAttributes(writer, prop); //Property declaration line - writer.Write(new string('\t', indent)); writer.Write(CsFileUtils.GetKeyWordsForProperty(prop)); writer.Write(' '); writer.Write(CsFileUtils.GetTypeName(prop.PropertyType)); writer.Write(' '); writer.Write(prop.Name); writer.WriteLine(); - writer.Write(new string('\t', indent)); - writer.Write('{'); - writer.WriteLine(); + writer.WriteLine('{'); //Get/Set - indent++; + writer.Indent++; if (prop.Getter != null) - AppendAccessor(writer, prop.Getter, "get", indent, prop.Visibility); + AppendAccessor(writer, prop.Getter, "get", prop.Visibility); if (prop.Setter != null) - AppendAccessor(writer, prop.Setter, "set", indent, prop.Visibility); - indent--; + AppendAccessor(writer, prop.Setter, "set", prop.Visibility); + writer.Indent--; - writer.Write(new string('\t', indent)); - writer.Write('}'); - writer.WriteLine(); - writer.WriteLine(); + writer.WriteLine('}'); + writer.WriteLineNoTabs(string.Empty); } - private static void AppendMethod(IndentedTextWriter writer, MethodAnalysisContext method, int indent) + private static void AppendMethod(IndentedTextWriter writer, MethodAnalysisContext method) { if (method is InjectedMethodAnalysisContext) return; //Custom attributes for method. Includes a trailing newline - AppendCustomAttributes(writer, method, indent); + AppendCustomAttributes(writer, method); //Method declaration line - writer.Write(new string('\t', indent)); writer.Write(CsFileUtils.GetKeyWordsForMethod(method)); writer.Write(' '); if (method.Name is not ".ctor" and not ".cctor") @@ -412,16 +386,15 @@ private static void AppendMethod(IndentedTextWriter writer, MethodAnalysisContex } writer.WriteLine(); - writer.WriteLine(); + writer.WriteLineNoTabs(string.Empty); } //get/set/add/remove/raise - private static void AppendAccessor(IndentedTextWriter writer, MethodAnalysisContext accessor, string accessorType, int indent, MethodAttributes parentVisibility) + private static void AppendAccessor(IndentedTextWriter writer, MethodAnalysisContext accessor, string accessorType, MethodAttributes parentVisibility) { //Custom attributes for accessor. Includes a trailing newline - AppendCustomAttributes(writer, accessor, indent); + AppendCustomAttributes(writer, accessor); - writer.Write(new string('\t', indent)); writer.Write(CsFileUtils.GetKeyWordsForMethod(accessor, parentVisibility)); writer.Write(' '); writer.Write(accessorType); @@ -430,8 +403,8 @@ private static void AppendAccessor(IndentedTextWriter writer, MethodAnalysisCont writer.WriteLine(); } - private static void AppendCustomAttributes(IndentedTextWriter writer, HasCustomAttributes owner, int indent) - => writer.Write(CsFileUtils.GetCustomAttributeStrings(owner, indent, true, true)); + private static void AppendCustomAttributes(IndentedTextWriter writer, HasCustomAttributes owner) + => CsFileUtils.WriteCustomAttributeStrings(owner, writer, true, true); private static string InvariantValue(object? value) => value is null ? "" : value is IFormattable f ? f.ToString(null, CultureInfo.InvariantCulture) : value.ToString() ?? ""; diff --git a/Cpp2IL.Core/Utils/CsFileUtils.cs b/Cpp2IL.Core/Utils/CsFileUtils.cs index 555d5b5a..d8aa171f 100644 --- a/Cpp2IL.Core/Utils/CsFileUtils.cs +++ b/Cpp2IL.Core/Utils/CsFileUtils.cs @@ -243,16 +243,15 @@ public static string GetKeyWordsForProperty(PropertyAnalysisContext prop) } /// - /// Returns all the custom attributes for the given entity, as they would appear in a C# source file (i.e. properly wrapped in square brackets, with params if known) + /// Writes all the custom attributes for the given entity to the given writer, as they would appear in a C# source file (i.e. properly wrapped in square brackets, with params if known). + /// Each attribute is written on its own line, indented according to the writer's current level. /// - /// The entity to generate custom attribute strings for - /// The number of tab characters to emit at the start of each line + /// The entity to write custom attribute strings for + /// The writer to write the custom attribute strings to /// True to call before generating. /// True to emit custom attributes even if they have required parameters that aren't known - public static string GetCustomAttributeStrings(HasCustomAttributes context, int indentCount, bool analyze = true, bool includeIncomplete = true) + public static void WriteCustomAttributeStrings(HasCustomAttributes context, IndentedTextWriter writer, bool analyze = true, bool includeIncomplete = true) { - var sb = new StringBuilder(); - if (analyze) context.AnalyzeCustomAttributeData(); @@ -264,21 +263,16 @@ public static string GetCustomAttributeStrings(HasCustomAttributes context, int if (!includeIncomplete && !analyzedCustomAttribute.IsSuitableForEmission) continue; - if (indentCount > 0) - sb.Append('\t', indentCount); - try { - sb.AppendLine(analyzedCustomAttribute.ToString()); + writer.WriteLine(analyzedCustomAttribute.ToString()); } catch (Exception e) { Logger.WarnNewline("Exception printing/formatting custom attribute: " + e, "C# Generator"); - sb.Append("/*Cpp2IL: Exception outputting custom attribute of type ").Append(analyzedCustomAttribute.Constructor.DeclaringType?.Name ?? "").AppendLine("*/"); + writer.WriteLine($"/*Cpp2IL: Exception outputting custom attribute of type {analyzedCustomAttribute.Constructor.DeclaringType?.Name ?? ""}*/"); } } - - return sb.ToString(); } @@ -349,36 +343,11 @@ public static string GetTypeName(TypeAnalysisContext type) } /// - /// Appends inheritance data (base class and interfaces) for the given type to the given string builder. + /// Writes inheritance data (base class and interfaces) for the given type to the given writer. /// If the base class is System.Object, System.ValueType, System.Enum, or System.MulticastDelegate, it will be ignored /// /// - /// - public static void AppendInheritanceInfo(TypeAnalysisContext type, StringBuilder sb) - { - var baseType = type.BaseType; - var needsBaseClass = baseType is not ReferencedTypeAnalysisContext and ({ Namespace: not "System" } or { Name: not "Object" and not "ValueType" and not "Enum" and not "MulticastDelegate" }); - if (needsBaseClass) - sb.Append(" : ").Append(GetTypeName(baseType!)); - - //Interfaces - if (type.InterfaceContexts.Count <= 0) - return; - - if (!needsBaseClass) - sb.Append(" : "); - - var addComma = needsBaseClass; - foreach (var iface in type.InterfaceContexts) - { - if (addComma) - sb.Append(", "); - - addComma = true; - - sb.Append(GetTypeName(iface)); - } - } + /// public static void WriteInheritanceInfo(TypeAnalysisContext type, IndentedTextWriter writer) { var baseType = type.BaseType; From 70efb76ac289fd312e8c251459876fb60681555e Mon Sep 17 00:00:00 2001 From: ds5678 <49847914+ds5678@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:48:11 -0700 Subject: [PATCH 4/5] Append -> Write --- .../OutputFormats/DiffableCsOutputFormat.cs | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs b/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs index cf957823..81b7af88 100644 --- a/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs +++ b/Cpp2IL.Core/OutputFormats/DiffableCsOutputFormat.cs @@ -83,7 +83,7 @@ private static Dictionary BuildOutput(ApplicationAnalysisC writer.WriteLineNoTabs(string.Empty); } - AppendType(writer, type); + WriteType(writer, type); ret[path] = stringWriter; } @@ -92,14 +92,14 @@ private static Dictionary BuildOutput(ApplicationAnalysisC return ret; } - private static void AppendType(IndentedTextWriter writer, TypeAnalysisContext type) + private static void WriteType(IndentedTextWriter writer, TypeAnalysisContext type) { // if (type.IsCompilerGeneratedBasedOnCustomAttributes) //Do not output compiler-generated types // return; //Custom attributes for type. Includes a trailing newline - AppendCustomAttributes(writer, type); + WriteCustomAttributes(writer, type); //Type declaration line writer.Write(CsFileUtils.GetKeyWordsForType(type)); @@ -130,13 +130,13 @@ private static void AppendType(IndentedTextWriter writer, TypeAnalysisContext ty var nestedTypes = type.NestedTypes.Clone(); nestedTypes.SortByExtractedKey(t => t.Name); foreach (var nested in nestedTypes) - AppendType(writer, nested); + WriteType(writer, nested); //Fields, offset order, static first var fields = type.Fields.Clone(); fields.SortByExtractedKey(f => f.IsStatic ? f.Offset : f.Offset + 0x1000); foreach (var field in fields) - AppendField(writer, field); + WriteField(writer, field); writer.WriteLineNoTabs(string.Empty); @@ -144,19 +144,19 @@ private static void AppendType(IndentedTextWriter writer, TypeAnalysisContext ty var events = type.Events.Clone(); events.SortByExtractedKey(e => e.Name); foreach (var evt in events) - AppendEvent(writer, evt); + WriteEvent(writer, evt); //Properties, alphabetical order var properties = type.Properties.Clone(); properties.SortByExtractedKey(p => p.Name); foreach (var prop in properties) - AppendProperty(writer, prop); + WriteProperty(writer, prop); //Methods, alphabetical order var methods = type.Methods.Clone(); methods.SortByExtractedKey(m => m.Name); foreach (var method in methods) - AppendMethod(writer, method); + WriteMethod(writer, method); } //Decrease indent, close brace @@ -165,13 +165,13 @@ private static void AppendType(IndentedTextWriter writer, TypeAnalysisContext ty writer.WriteLineNoTabs(string.Empty); } - private static void AppendField(IndentedTextWriter writer, FieldAnalysisContext field) + private static void WriteField(IndentedTextWriter writer, FieldAnalysisContext field) { if (field is InjectedFieldAnalysisContext) return; //Custom attributes for field. Includes a trailing newline - AppendCustomAttributes(writer, field); + WriteCustomAttributes(writer, field); //Field declaration line writer.Write(CsFileUtils.GetKeyWordsForField(field)); @@ -185,7 +185,7 @@ private static void AppendField(IndentedTextWriter writer, FieldAnalysisContext var fieldRva = field.StaticArrayInitialValue; if (fieldRva.Length > 0) { - AppendFieldRvaInitializer(writer, field, fieldRva); + WriteFieldRvaInitializer(writer, field, fieldRva); return; } } @@ -219,7 +219,7 @@ private static void AppendField(IndentedTextWriter writer, FieldAnalysisContext writer.WriteLine(); } - private static void AppendFieldRvaInitializer(IndentedTextWriter writer, FieldAnalysisContext field, byte[] data) + private static void WriteFieldRvaInitializer(IndentedTextWriter writer, FieldAnalysisContext field, byte[] data) { var tail = $" //Field offset: 0x{field.Offset.ToString("X")} || Has Field RVA (address hidden for diffability)"; @@ -298,10 +298,10 @@ private static bool TryAscendingInt32Array(byte[] b, [NotNullWhen(true)] out int return true; } - private static void AppendEvent(IndentedTextWriter writer, EventAnalysisContext evt) + private static void WriteEvent(IndentedTextWriter writer, EventAnalysisContext evt) { //Custom attributes for event. Includes a trailing newline - AppendCustomAttributes(writer, evt); + WriteCustomAttributes(writer, evt); //Event declaration line writer.Write(CsFileUtils.GetKeyWordsForEvent(evt)); @@ -315,21 +315,21 @@ private static void AppendEvent(IndentedTextWriter writer, EventAnalysisContext //Add/Remove/Invoke writer.Indent++; if (evt.Adder != null) - AppendAccessor(writer, evt.Adder, "add", evt.Visibility); + WriteAccessor(writer, evt.Adder, "add", evt.Visibility); if (evt.Remover != null) - AppendAccessor(writer, evt.Remover, "remove", evt.Visibility); + WriteAccessor(writer, evt.Remover, "remove", evt.Visibility); if (evt.Invoker != null) - AppendAccessor(writer, evt.Invoker, "fire", evt.Visibility); + WriteAccessor(writer, evt.Invoker, "fire", evt.Visibility); writer.Indent--; writer.WriteLine('}'); writer.WriteLineNoTabs(string.Empty); } - private static void AppendProperty(IndentedTextWriter writer, PropertyAnalysisContext prop) + private static void WriteProperty(IndentedTextWriter writer, PropertyAnalysisContext prop) { //Custom attributes for property. Includes a trailing newline - AppendCustomAttributes(writer, prop); + WriteCustomAttributes(writer, prop); //Property declaration line writer.Write(CsFileUtils.GetKeyWordsForProperty(prop)); @@ -343,22 +343,22 @@ private static void AppendProperty(IndentedTextWriter writer, PropertyAnalysisCo //Get/Set writer.Indent++; if (prop.Getter != null) - AppendAccessor(writer, prop.Getter, "get", prop.Visibility); + WriteAccessor(writer, prop.Getter, "get", prop.Visibility); if (prop.Setter != null) - AppendAccessor(writer, prop.Setter, "set", prop.Visibility); + WriteAccessor(writer, prop.Setter, "set", prop.Visibility); writer.Indent--; writer.WriteLine('}'); writer.WriteLineNoTabs(string.Empty); } - private static void AppendMethod(IndentedTextWriter writer, MethodAnalysisContext method) + private static void WriteMethod(IndentedTextWriter writer, MethodAnalysisContext method) { if (method is InjectedMethodAnalysisContext) return; //Custom attributes for method. Includes a trailing newline - AppendCustomAttributes(writer, method); + WriteCustomAttributes(writer, method); //Method declaration line writer.Write(CsFileUtils.GetKeyWordsForMethod(method)); @@ -390,10 +390,10 @@ private static void AppendMethod(IndentedTextWriter writer, MethodAnalysisContex } //get/set/add/remove/raise - private static void AppendAccessor(IndentedTextWriter writer, MethodAnalysisContext accessor, string accessorType, MethodAttributes parentVisibility) + private static void WriteAccessor(IndentedTextWriter writer, MethodAnalysisContext accessor, string accessorType, MethodAttributes parentVisibility) { //Custom attributes for accessor. Includes a trailing newline - AppendCustomAttributes(writer, accessor); + WriteCustomAttributes(writer, accessor); writer.Write(CsFileUtils.GetKeyWordsForMethod(accessor, parentVisibility)); writer.Write(' '); @@ -403,7 +403,7 @@ private static void AppendAccessor(IndentedTextWriter writer, MethodAnalysisCont writer.WriteLine(); } - private static void AppendCustomAttributes(IndentedTextWriter writer, HasCustomAttributes owner) + private static void WriteCustomAttributes(IndentedTextWriter writer, HasCustomAttributes owner) => CsFileUtils.WriteCustomAttributeStrings(owner, writer, true, true); private static string InvariantValue(object? value) From 999a690c7f3a86e8e50bde2f31e8fd16ad7efe1e Mon Sep 17 00:00:00 2001 From: ds5678 <49847914+ds5678@users.noreply.github.com> Date: Mon, 6 Jul 2026 21:59:33 -0700 Subject: [PATCH 5/5] Add missing documentation to CsFileUtils --- Cpp2IL.Core/Utils/CsFileUtils.cs | 68 ++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/Cpp2IL.Core/Utils/CsFileUtils.cs b/Cpp2IL.Core/Utils/CsFileUtils.cs index d8aa171f..907d584d 100644 --- a/Cpp2IL.Core/Utils/CsFileUtils.cs +++ b/Cpp2IL.Core/Utils/CsFileUtils.cs @@ -23,6 +23,12 @@ public static string GetMethodParameterString(MethodAnalysisContext method) return string.Join(", ", method.Parameters); } + /// + /// Gets the C# access modifier string for the given type analysis context. + /// Examples: "public", "internal", "private", "protected", "protected internal", "private protected". + /// + /// The type analysis context to inspect. + /// The access modifier keyword appropriate for the type. public static string GetAccessModifiers(TypeAnalysisContext type) => type.Visibility switch { TypeAttributes.Public => "public", @@ -36,6 +42,12 @@ public static string GetMethodParameterString(MethodAnalysisContext method) _ => throw new ArgumentOutOfRangeException($"Unknown visibility for type {type.FullName}: {type.Visibility}") }; + /// + /// Gets the C# access modifier string for the given field analysis context. + /// Examples: "public", "private", "protected", "internal", "protected internal", "private protected". + /// + /// The field analysis context to inspect. + /// The access modifier keyword appropriate for the field. public static string GetAccessModifiers(FieldAnalysisContext field) => field.Visibility switch { FieldAttributes.Public => "public", @@ -47,10 +59,25 @@ public static string GetMethodParameterString(MethodAnalysisContext method) _ => throw new ArgumentOutOfRangeException($"Unknown visibility for field {field.DeclaringType.FullName}.{field.Name}: {field.Visibility}") }; + /// + /// Gets the C# access modifier string for the given method analysis context. + /// + /// The method analysis context to inspect. + /// The access modifier keyword appropriate for the method. public static string GetAccessModifiers(MethodAnalysisContext method) => GetAccessModifiers(method.Visibility); + /// + /// Gets the C# access modifier string for the given property analysis context. + /// + /// The property analysis context to inspect. + /// The access modifier keyword appropriate for the property. public static string GetAccessModifiers(PropertyAnalysisContext property) => GetAccessModifiers(property.Visibility); + /// + /// Gets the C# access modifier string for the given event analysis context. + /// + /// The event analysis context to inspect. + /// The access modifier keyword appropriate for the event. public static string GetAccessModifiers(EventAnalysisContext evt) => GetAccessModifiers(evt.Visibility); private static string GetAccessModifiers(MethodAttributes visibility) => visibility switch @@ -64,6 +91,12 @@ public static string GetMethodParameterString(MethodAnalysisContext method) _ => throw new ArgumentOutOfRangeException($"Unknown visibility: {visibility}") }; + /// + /// Returns the C# keyword that declares the kind of type represented by the context. + /// Examples: "class", "struct", "enum", "interface", "delegate". + /// + /// The type analysis context to evaluate. + /// The declaration keyword for the type. public static string GetTypeDeclarationKeyword(TypeAnalysisContext type) { if (type.IsEnumType) @@ -77,6 +110,12 @@ public static string GetTypeDeclarationKeyword(TypeAnalysisContext type) return "class"; } + /// + /// Returns a class-level inheritance modifier for the given type, if applicable. + /// Examples: "static", "abstract", "sealed", or null when no modifier should be emitted. + /// + /// The type analysis context to inspect. + /// The inheritance modifier keyword or null. public static string? GetClassInheritanceKeyword(TypeAnalysisContext type) { if (type.IsStatic) @@ -102,7 +141,7 @@ public static string GetKeyWordsForType(TypeAnalysisContext type) } /// - /// Returns all the keywords that would be present in the c# source file to generate this method, i.e. access modifiers, static/const/etc. + /// Returns all the keywords that would be present in the c# source file to generate this field, i.e. access modifiers, static/const/etc. /// Does not include the type of the field or its name. /// /// The field to generate keywords for @@ -157,16 +196,32 @@ public static string GetKeyWordsForField(FieldAnalysisContext field) } } + /// + /// Determines the appropriate virtual/slot-related keyword for a method. + /// Examples: "abstract", "virtual", "override", "sealed override", or null when no slot keyword applies. + /// + /// The method analysis context to inspect. + /// The slot-related keyword or null if none should be emitted. public static string? GetVirtualLookupKeyword(MethodAnalysisContext method) { return GetVirtualLookupKeyword(method.DeclaringType?.IsInterface ?? false, method.IsStatic, method.IsAbstract, method.IsVirtual, method.IsNewSlot, method.IsFinal); } + /// + /// Determines the appropriate virtual/slot-related keyword for a property. + /// + /// The property analysis context to inspect. + /// The slot-related keyword or null if none should be emitted. public static string? GetVirtualLookupKeyword(PropertyAnalysisContext property) { return GetVirtualLookupKeyword(property.DeclaringType.IsInterface, property.IsStatic, property.IsAbstract, property.IsVirtual, property.IsNewSlot, property.IsFinal); } + /// + /// Determines the appropriate virtual/slot-related keyword for an event. + /// + /// The event analysis context to inspect. + /// The slot-related keyword or null if none should be emitted. public static string? GetVirtualLookupKeyword(EventAnalysisContext evt) { return GetVirtualLookupKeyword(evt.DeclaringType.IsInterface, evt.IsStatic, evt.IsAbstract, evt.IsVirtual, evt.IsNewSlot, evt.IsFinal); @@ -275,7 +330,12 @@ public static void WriteCustomAttributeStrings(HasCustomAttributes context, Inde } } - + /// + /// Returns the C#-style name for the given type analysis context. + /// Handles built-in System type aliases (e.g. System.Int32 -> int), arrays, pointers, by-ref and generic instances. + /// + /// The type analysis context to convert to a C# type name. + /// The C# type name as it should appear in source. public static string GetTypeName(TypeAnalysisContext type) { if (type is WrappedTypeAnalysisContext wrapped) @@ -346,8 +406,8 @@ public static string GetTypeName(TypeAnalysisContext type) /// Writes inheritance data (base class and interfaces) for the given type to the given writer. /// If the base class is System.Object, System.ValueType, System.Enum, or System.MulticastDelegate, it will be ignored /// - /// - /// + /// The type analysis context whose inheritance is to be written. + /// The writer to which the inheritance information will be written. public static void WriteInheritanceInfo(TypeAnalysisContext type, IndentedTextWriter writer) { var baseType = type.BaseType;