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..81b7af88 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,46 +68,49 @@ 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.WriteLineNoTabs(string.Empty); + } else - sb.AppendLine("//Type is in global namespace").AppendLine(); + { + writer.WriteLine("//Type is in global namespace"); + writer.WriteLineNoTabs(string.Empty); + } - AppendType(sb, type); + WriteType(writer, type); - ret[path] = sb; + ret[path] = stringWriter; } } return ret; } - private static void AppendType(StringBuilder sb, TypeAnalysisContext type, int indent = 0) + 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(sb, type, indent); + WriteCustomAttributes(writer, type); //Type declaration line - sb.Append('\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.WriteLine('{'); //Type declaration done, increase indent - indent++; + writer.Indent++; if (type.IsEnumType) { @@ -115,12 +118,10 @@ 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(enumValue.Name); + writer.Write(" = "); + writer.Write(InvariantValue(enumValue.BackingData!.DefaultValue)); + writer.WriteLine(','); } } else @@ -129,128 +130,141 @@ 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); + 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(sb, field, indent); + WriteField(writer, field); - sb.AppendLine(); + writer.WriteLineNoTabs(string.Empty); //Events, alphabetical order var events = type.Events.Clone(); events.SortByExtractedKey(e => e.Name); foreach (var evt in events) - AppendEvent(sb, evt, indent); + WriteEvent(writer, evt); //Properties, alphabetical order var properties = type.Properties.Clone(); properties.SortByExtractedKey(p => p.Name); foreach (var prop in properties) - AppendProperty(sb, prop, indent); + WriteProperty(writer, prop); //Methods, alphabetical order var methods = type.Methods.Clone(); methods.SortByExtractedKey(m => m.Name); foreach (var method in methods) - AppendMethod(sb, method, indent); + WriteMethod(writer, method); } //Decrease indent, close brace - indent--; - sb.Append('\t', indent); - sb.Append('}'); - sb.AppendLine().AppendLine(); + writer.Indent--; + writer.WriteLine('}'); + writer.WriteLineNoTabs(string.Empty); } - private static void AppendField(StringBuilder sb, FieldAnalysisContext field, int indent) + private static void WriteField(IndentedTextWriter writer, FieldAnalysisContext field) { if (field is InjectedFieldAnalysisContext) return; //Custom attributes for field. Includes a trailing newline - AppendCustomAttributes(sb, field, indent); + WriteCustomAttributes(writer, field); //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(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); + WriteFieldRvaInitializer(writer, field, fieldRva); 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 WriteFieldRvaInitializer(IndentedTextWriter writer, FieldAnalysisContext field, byte[] data) { 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.WriteLine('{'); + writer.Indent++; for (var i = 0; i < ints.Length; i += 12) { var n = Math.Min(12, ints.Length - i); - sb.Append('\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.Indent--; + 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.WriteLine('{'); + writer.Indent++; for (var i = 0; i < data.Length; i += 16) { var n = Math.Min(16, data.Length - i); - sb.Append('\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.Indent--; + writer.WriteLine("};"); } //blobs that decode as 0 followed by strictly ascending little-endian int32s are (probably) offset tables, @@ -284,121 +298,113 @@ 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 WriteEvent(IndentedTextWriter writer, EventAnalysisContext evt) { //Custom attributes for event. Includes a trailing newline - AppendCustomAttributes(sb, evt, indent); + WriteCustomAttributes(writer, evt); //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(CsFileUtils.GetKeyWordsForEvent(evt)); + writer.Write(' '); + writer.Write(CsFileUtils.GetTypeName(evt.EventType)); + writer.Write(' '); + writer.Write(evt.Name); + writer.WriteLine(); + writer.WriteLine('{'); //Add/Remove/Invoke - indent++; + writer.Indent++; if (evt.Adder != null) - AppendAccessor(sb, evt.Adder, "add", indent); + WriteAccessor(writer, evt.Adder, "add", evt.Visibility); if (evt.Remover != null) - AppendAccessor(sb, evt.Remover, "remove", indent); + WriteAccessor(writer, evt.Remover, "remove", evt.Visibility); if (evt.Invoker != null) - AppendAccessor(sb, evt.Invoker, "fire", indent); - indent--; + WriteAccessor(writer, evt.Invoker, "fire", evt.Visibility); + writer.Indent--; - sb.Append('\t', indent); - sb.Append('}'); - sb.AppendLine().AppendLine(); + writer.WriteLine('}'); + writer.WriteLineNoTabs(string.Empty); } - private static void AppendProperty(StringBuilder sb, PropertyAnalysisContext prop, int indent) + private static void WriteProperty(IndentedTextWriter writer, PropertyAnalysisContext prop) { //Custom attributes for property. Includes a trailing newline - AppendCustomAttributes(sb, prop, indent); + WriteCustomAttributes(writer, prop); //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(CsFileUtils.GetKeyWordsForProperty(prop)); + writer.Write(' '); + writer.Write(CsFileUtils.GetTypeName(prop.PropertyType)); + writer.Write(' '); + writer.Write(prop.Name); + writer.WriteLine(); + writer.WriteLine('{'); //Get/Set - indent++; + writer.Indent++; if (prop.Getter != null) - AppendAccessor(sb, prop.Getter, "get", indent); + WriteAccessor(writer, prop.Getter, "get", prop.Visibility); if (prop.Setter != null) - AppendAccessor(sb, prop.Setter, "set", indent); - indent--; + WriteAccessor(writer, prop.Setter, "set", prop.Visibility); + writer.Indent--; - sb.Append('\t', indent); - sb.Append('}'); - sb.AppendLine().AppendLine(); + writer.WriteLine('}'); + writer.WriteLineNoTabs(string.Empty); } - private static void AppendMethod(StringBuilder sb, MethodAnalysisContext method, int indent) + private static void WriteMethod(IndentedTextWriter writer, MethodAnalysisContext method) { if (method is InjectedMethodAnalysisContext) return; //Custom attributes for method. Includes a trailing newline - AppendCustomAttributes(sb, method, indent); + WriteCustomAttributes(writer, method); //Method declaration line - sb.Append('\t', indent); - sb.Append(CsFileUtils.GetKeyWordsForMethod(method)); - sb.Append(' '); + 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.WriteLineNoTabs(string.Empty); } //get/set/add/remove/raise - private static void AppendAccessor(StringBuilder sb, MethodAnalysisContext accessor, string accessorType, int indent) + private static void WriteAccessor(IndentedTextWriter writer, MethodAnalysisContext accessor, string accessorType, 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(' '); - sb.Append(accessorType); - sb.Append(" { } //Length: "); - sb.Append(accessor.RawBytes.Length); - sb.AppendLine(); + WriteCustomAttributes(writer, accessor); + + 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 WriteCustomAttributes(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 afb0b4b1..907d584d 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; @@ -18,19 +19,112 @@ 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; + /// + /// 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", + 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}") + }; - sb.Append(paramData); //ToString on the ParameterData will do the right thing. - } + /// + /// 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", + 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}") + }; - return sb.ToString(); + /// + /// 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 + { + 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}") + }; + + /// + /// 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) + return "enum"; + if (type.IsValueType) + return "struct"; + if (type.IsInterface) + return "interface"; + if (type.IsDelegate) + return "delegate"; + 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) + return "static"; + if (type.IsAbstract && !type.IsInterface) + return "abstract"; + if (type.IsSealed && !type.IsValueType && !type.IsDelegate) + return "sealed"; + return null; } /// @@ -40,56 +134,23 @@ 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}"; } /// - /// 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 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 +163,68 @@ 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; + } + } + + /// + /// 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); } /// @@ -110,42 +232,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 (!skipKeywordsInvalidForAccessors && attributes.HasFlag(MethodAttributes.Static)) - sb.Append("static "); + if (method.Visibility != parentVisibility) + sb.Append(GetAccessModifiers(method)).Append(' '); - 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,93 +262,51 @@ 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; - - var all = addAttrs | removeAttrs | raiseAttrs; + sb.Append(GetAccessModifiers(evt)).Append(' '); - //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; + sb.Append(GetAccessModifiers(prop)).Append(' '); - var all = getterAttributes | setterAttributes; - - //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(); } /// - /// 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(); @@ -255,24 +318,24 @@ 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(); } - + /// + /// 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) @@ -326,6 +389,8 @@ public static string GetTypeName(TypeAnalysisContext type) "UInt64" => "ulong", "Int16" => "short", "UInt16" => "ushort", + "IntPtr" => "nint", + "UIntPtr" => "nuint", "String" => "string", "Object" => "object", _ => type.Name, @@ -338,34 +403,37 @@ 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 + /// 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) + /// 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; - 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!)); + { + writer.Write(" : "); + writer.Write(GetTypeName(baseType!)); + } //Interfaces if (type.InterfaceContexts.Count <= 0) return; if (!needsBaseClass) - sb.Append(" : "); + writer.Write(" : "); var addComma = needsBaseClass; foreach (var iface in type.InterfaceContexts) { if (addComma) - sb.Append(", "); + writer.Write(", "); addComma = true; - sb.Append(GetTypeName(iface)); + writer.Write(GetTypeName(iface)); } } }