diff --git a/.gitignore b/.gitignore index d2845b1..9fbf6b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .vs bin/ obj/ -*.csproj.user \ No newline at end of file +*.user +.idea/ \ No newline at end of file diff --git a/src/Publicizer/AssemblyEditor.cs b/src/Publicizer/AssemblyEditor.cs index 4bbc802..8d9eba9 100644 --- a/src/Publicizer/AssemblyEditor.cs +++ b/src/Publicizer/AssemblyEditor.cs @@ -1,13 +1,46 @@ +using System; +using System.Linq; using dnlib.DotNet; +using dnlib.DotNet.Emit; +using FieldAttributes = dnlib.DotNet.FieldAttributes; +using MethodAttributes = dnlib.DotNet.MethodAttributes; +using TypeAttributes = dnlib.DotNet.TypeAttributes; namespace Publicizer; /// /// Class for making edits to assemblies and related types. /// -internal static class AssemblyEditor +internal class AssemblyEditor { - internal static bool PublicizeType(TypeDef type) + private readonly ModuleDef _module; + private readonly TypeDef? _accessAttributeType; + private readonly MethodDef? _accessAttributeConstructor; + internal AssemblyEditor(ModuleDef module, bool addOriginalAccessModifierAttribute) + { + _module = module; + + // workaround for custom attributes not being supported in, at least some, core libs + bool isCoreLib = module.IsCoreLibraryModule ?? false; + if (!isCoreLib && addOriginalAccessModifierAttribute) + { + _accessAttributeType = GetOrCreateAccessAttributeType(module); + _accessAttributeConstructor = _accessAttributeType.FindConstructors().First(); + } + } + + internal bool IsAccessAttribute(ITypeDefOrRef typeRef) + { + return typeRef == _accessAttributeType; + } + + internal bool publicizedAnyMemberInAssembly; + internal int publicizedTypesCount; + internal int publicizedPropertiesCount; + internal int publicizedMethodsCount; + internal int publicizedFieldsCount; + + internal bool PublicizeType(TypeDef type) { TypeAttributes oldAttributes = type.Attributes; type.Attributes &= ~TypeAttributes.VisibilityMask; @@ -20,10 +53,17 @@ internal static bool PublicizeType(TypeDef type) { type.Attributes |= TypeAttributes.Public; } - return type.Attributes != oldAttributes; + + if (type.Attributes != oldAttributes) + { + AddOriginalAccessModifierAttribute(type, ConvertAttributes(oldAttributes)); + publicizedTypesCount++; + return true; + } + return false; } - internal static bool PublicizeProperty(PropertyDef property, bool includeVirtual = true) + internal bool PublicizeProperty(PropertyDef property, bool includeVirtual = true) { bool publicized = false; @@ -37,26 +77,153 @@ internal static bool PublicizeProperty(PropertyDef property, bool includeVirtual publicized |= PublicizeMethod(setMethod, includeVirtual); } + if (publicized) + { + publicizedPropertiesCount++; + } return publicized; } - internal static bool PublicizeMethod(MethodDef method, bool includeVirtual = true) + internal bool PublicizeMethod(MethodDef method, bool includeVirtual = true) { if (includeVirtual || !method.IsVirtual) { MethodAttributes oldAttributes = method.Attributes; method.Attributes &= ~MethodAttributes.MemberAccessMask; method.Attributes |= MethodAttributes.Public; - return method.Attributes != oldAttributes; + if (method.Attributes != oldAttributes) + { + AddOriginalAccessModifierAttribute(method, ConvertAttributes(oldAttributes)); + publicizedAnyMemberInAssembly = true; + publicizedMethodsCount++; + return true; + } } return false; } - internal static bool PublicizeField(FieldDef field) + internal bool PublicizeField(FieldDef field) { FieldAttributes oldAttributes = field.Attributes; field.Attributes &= ~FieldAttributes.FieldAccessMask; field.Attributes |= FieldAttributes.Public; - return field.Attributes != oldAttributes; + if (field.Attributes != oldAttributes) + { + AddOriginalAccessModifierAttribute(field, ConvertAttributes(oldAttributes)); + publicizedAnyMemberInAssembly = true; + publicizedFieldsCount++; + return true; + } + return false; + } + + private void AddOriginalAccessModifierAttribute(IHasCustomAttribute item, AccessModifier original) + { + if (_accessAttributeConstructor == null) + { + return; + } + var attribute = new CustomAttribute(_accessAttributeConstructor); + var caArgument = new CAArgument(_module.CorLibTypes.String, AccessModifierToString(original)); + attribute.ConstructorArguments.Add(caArgument); + item.CustomAttributes.Add(attribute); + } + + private static readonly string s_accessAttributeNamespace = new UTF8String(nameof(Publicizer)); + private static readonly string s_accessAttributeName = new UTF8String("OriginalAccessModifierAttribute"); + private static TypeDef GetOrCreateAccessAttributeType(ModuleDef module) + { + TypeDef? attributeTypeDef = module.Types.FirstOrDefault(t => t.Namespace == s_accessAttributeNamespace && t.Name == s_accessAttributeName); + if (attributeTypeDef == null) + { + TypeRef attributeBaseTypeRef = module.CorLibTypes.GetTypeRef("System", "Attribute"); + MemberRef attributeBaseConstructorMethodRef = new MemberRefUser( + module, + ".ctor", + MethodSig.CreateInstance(module.CorLibTypes.Void), + attributeBaseTypeRef + ); + attributeTypeDef = new TypeDefUser(s_accessAttributeNamespace, s_accessAttributeName, attributeBaseTypeRef); + attributeTypeDef.Attributes = TypeAttributes.NestedAssembly | TypeAttributes.Sealed; + + var methodSig = MethodSig.CreateInstance( + module.CorLibTypes.Void, + module.CorLibTypes.String + ); + var methodDef = new MethodDefUser( + ".ctor", + methodSig, + MethodAttributes.Assembly | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName | MethodAttributes.HideBySig + ); + + CilBody body = methodDef.Body = new CilBody(); + body.Instructions.Add(new Instruction(OpCodes.Ldarg_0)); + body.Instructions.Add(new Instruction(OpCodes.Call, attributeBaseConstructorMethodRef)); + body.Instructions.Add(new Instruction(OpCodes.Ret)); + + attributeTypeDef.Methods.Add(methodDef); + + module.Types.Add(attributeTypeDef); + } + return attributeTypeDef; + } + + private static AccessModifier ConvertAttributes(TypeAttributes attributes) + { + attributes &= ~TypeAttributes.VisibilityMask; + return attributes switch + { + TypeAttributes.NotPublic => AccessModifier.Private, + TypeAttributes.Public => AccessModifier.Public, + TypeAttributes.NestedPublic => AccessModifier.Public, + TypeAttributes.NestedPrivate => AccessModifier.Private, + TypeAttributes.NestedFamily => AccessModifier.Protected, + TypeAttributes.NestedAssembly => AccessModifier.Internal, + TypeAttributes.NestedFamANDAssem => AccessModifier.PrivateProtected, + TypeAttributes.NestedFamORAssem => AccessModifier.ProtectedInternal, + _ => AccessModifier.File + }; + } + private static AccessModifier ConvertAttributes(MethodAttributes attributes) + { + // methods and fields share the same visibility mask order and values + return ConvertAttributes((FieldAttributes)attributes); + } + private static AccessModifier ConvertAttributes(FieldAttributes attributes) + { + attributes &= ~FieldAttributes.FieldAccessMask; + return attributes switch + { + FieldAttributes.PrivateScope => AccessModifier.CompilerControlled, + FieldAttributes.Private => AccessModifier.Private, + FieldAttributes.FamANDAssem => AccessModifier.PrivateProtected, + FieldAttributes.Assembly => AccessModifier.Internal, + FieldAttributes.Family => AccessModifier.Protected, + FieldAttributes.FamORAssem => AccessModifier.ProtectedInternal, + FieldAttributes.Public => AccessModifier.Public, + _ => AccessModifier.File + }; + } + // makes it more readable + // corresponds to official documentation + // https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/access-modifiers + private enum AccessModifier + { + Public, Private, Protected, Internal, ProtectedInternal, PrivateProtected, File, CompilerControlled + } + private static string AccessModifierToString(AccessModifier modifier) + { + return modifier switch + { + AccessModifier.Public => "public", + AccessModifier.Private => "private", + AccessModifier.Protected => "protected", + AccessModifier.Internal => "internal", + AccessModifier.ProtectedInternal => "protected internal", + AccessModifier.PrivateProtected => "private protected", + AccessModifier.File => "file", + AccessModifier.CompilerControlled => "[CompilerControlled|PrivateScope]", + _ => throw new ArgumentOutOfRangeException(nameof(modifier), modifier, null) + }; } } diff --git a/src/Publicizer/PublicizeAssemblies.cs b/src/Publicizer/PublicizeAssemblies.cs index 06a58dc..8bc7f72 100644 --- a/src/Publicizer/PublicizeAssemblies.cs +++ b/src/Publicizer/PublicizeAssemblies.cs @@ -221,17 +221,20 @@ private static Dictionary GetPublicizerAssemb private static bool PublicizeAssembly(ModuleDef module, PublicizerAssemblyContext assemblyContext, ITaskLogger logger) { - bool publicizedAnyMemberInAssembly = false; var doNotPublicizePropertyMethods = new HashSet(); - int publicizedTypesCount = 0; - int publicizedPropertiesCount = 0; - int publicizedMethodsCount = 0; - int publicizedFieldsCount = 0; + var assemblyEditor = new AssemblyEditor(module, assemblyContext.AddOriginalAccessModifierAttribute); // TYPES - foreach (TypeDef? typeDef in module.GetTypes()) + foreach (TypeDef typeDef in module.GetTypes()) { + if (assemblyEditor.IsAccessAttribute(typeDef)) + { + // do not publicize the access attribute itself, keep it internal + // also avoids the attribute attributing itself + continue; + } + doNotPublicizePropertyMethods.Clear(); bool publicizedAnyMemberInType = false; @@ -262,11 +265,9 @@ private static bool PublicizeAssembly(ModuleDef module, PublicizerAssemblyContex bool explicitlyPublicizeProperty = assemblyContext.PublicizeMemberPatterns.Contains(propertyName); if (explicitlyPublicizeProperty) { - if (AssemblyEditor.PublicizeProperty(propertyDef)) + if (assemblyEditor.PublicizeProperty(propertyDef)) { publicizedAnyMemberInType = true; - publicizedAnyMemberInAssembly = true; - publicizedPropertiesCount++; logger.Verbose($"Explicitly publicizing property: {propertyName}"); } continue; @@ -296,11 +297,9 @@ private static bool PublicizeAssembly(ModuleDef module, PublicizerAssemblyContex continue; } - if (AssemblyEditor.PublicizeProperty(propertyDef, assemblyContext.IncludeVirtualMembers)) + if (assemblyEditor.PublicizeProperty(propertyDef, assemblyContext.IncludeVirtualMembers)) { publicizedAnyMemberInType = true; - publicizedAnyMemberInAssembly = true; - publicizedPropertiesCount++; } } } @@ -326,11 +325,9 @@ private static bool PublicizeAssembly(ModuleDef module, PublicizerAssemblyContex bool explicitlyPublicizeMethod = assemblyContext.PublicizeMemberPatterns.Contains(methodName); if (explicitlyPublicizeMethod) { - if (AssemblyEditor.PublicizeMethod(methodDef)) + if (assemblyEditor.PublicizeMethod(methodDef)) { publicizedAnyMemberInType = true; - publicizedAnyMemberInAssembly = true; - publicizedMethodsCount++; logger.Verbose($"Explicitly publicizing method: {methodName}"); } continue; @@ -360,11 +357,9 @@ private static bool PublicizeAssembly(ModuleDef module, PublicizerAssemblyContex continue; } - if (AssemblyEditor.PublicizeMethod(methodDef, assemblyContext.IncludeVirtualMembers)) + if (assemblyEditor.PublicizeMethod(methodDef, assemblyContext.IncludeVirtualMembers)) { publicizedAnyMemberInType = true; - publicizedAnyMemberInAssembly = true; - publicizedMethodsCount++; } } } @@ -384,11 +379,9 @@ private static bool PublicizeAssembly(ModuleDef module, PublicizerAssemblyContex bool explicitlyPublicizeField = assemblyContext.PublicizeMemberPatterns.Contains(fieldName); if (explicitlyPublicizeField) { - if (AssemblyEditor.PublicizeField(fieldDef)) + if (assemblyEditor.PublicizeField(fieldDef)) { publicizedAnyMemberInType = true; - publicizedAnyMemberInAssembly = true; - publicizedFieldsCount++; logger.Verbose($"Explicitly publicizing field: {fieldName}"); } continue; @@ -418,22 +411,16 @@ private static bool PublicizeAssembly(ModuleDef module, PublicizerAssemblyContex continue; } - if (AssemblyEditor.PublicizeField(fieldDef)) + if (assemblyEditor.PublicizeField(fieldDef)) { publicizedAnyMemberInType = true; - publicizedAnyMemberInAssembly = true; - publicizedFieldsCount++; } } } if (publicizedAnyMemberInType) { - if (AssemblyEditor.PublicizeType(typeDef)) - { - publicizedAnyMemberInAssembly = true; - publicizedTypesCount++; - } + assemblyEditor.PublicizeType(typeDef); continue; } @@ -446,10 +433,8 @@ private static bool PublicizeAssembly(ModuleDef module, PublicizerAssemblyContex bool explicitlyPublicizeType = assemblyContext.PublicizeMemberPatterns.Contains(typeName); if (explicitlyPublicizeType) { - if (AssemblyEditor.PublicizeType(typeDef)) + if (assemblyEditor.PublicizeType(typeDef)) { - publicizedAnyMemberInAssembly = true; - publicizedTypesCount++; logger.Verbose($"Explicitly publicizing type: {typeName}"); } continue; @@ -474,20 +459,16 @@ private static bool PublicizeAssembly(ModuleDef module, PublicizerAssemblyContex continue; } - if (AssemblyEditor.PublicizeType(typeDef)) - { - publicizedAnyMemberInAssembly = true; - publicizedTypesCount++; - } + assemblyEditor.PublicizeType(typeDef); } } - logger.Info("Publicized types: " + publicizedTypesCount); - logger.Info("Publicized properties: " + publicizedPropertiesCount); - logger.Info("Publicized methods: " + publicizedMethodsCount); - logger.Info("Publicized fields: " + publicizedFieldsCount); + logger.Info("Publicized types: " + assemblyEditor.publicizedTypesCount); + logger.Info("Publicized properties: " + assemblyEditor.publicizedPropertiesCount); + logger.Info("Publicized methods: " + assemblyEditor.publicizedMethodsCount); + logger.Info("Publicized fields: " + assemblyEditor.publicizedFieldsCount); - return publicizedAnyMemberInAssembly; + return assemblyEditor.publicizedAnyMemberInAssembly; } private static bool IsCompilerGenerated(IHasCustomAttribute memberDef) diff --git a/src/Publicizer/PublicizerAssemblyContext.cs b/src/Publicizer/PublicizerAssemblyContext.cs index 948ff4e..ae8c371 100644 --- a/src/Publicizer/PublicizerAssemblyContext.cs +++ b/src/Publicizer/PublicizerAssemblyContext.cs @@ -14,6 +14,7 @@ internal PublicizerAssemblyContext(string assemblyName) internal bool ExplicitlyPublicizeAssembly { get; set; } = false; internal bool IncludeCompilerGeneratedMembers { get; set; } = true; internal bool IncludeVirtualMembers { get; set; } = true; + internal bool AddOriginalAccessModifierAttribute { get; set; } = true; internal bool ExplicitlyDoNotPublicizeAssembly { get; set; } = false; internal HashSet PublicizeMemberPatterns { get; } = new HashSet(); internal Regex? PublicizeMemberRegexPattern { get; set; }