├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── Publicizer.sln ├── README.md ├── icon.png └── src ├── Publicizer.Tests ├── NugetConfigMaker.cs ├── ProcessResult.cs ├── Publicizer.Tests.csproj ├── PublicizerTests.cs ├── Runner.cs └── TemporaryFolder.cs └── Publicizer ├── AssemblyEditor.cs ├── Hasher.cs ├── ITaskLogger.cs ├── IgnoresAccessChecksToAttribute.cs ├── Krafs.Publicizer.props ├── Krafs.Publicizer.targets ├── Logger.cs ├── PublicizeAssemblies.cs ├── Publicizer.csproj ├── PublicizerAssemblyContext.cs └── TaskItemExtensions.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | # Modified version of roslyn repo's editorconfig. 2 | 3 | # EditorConfig is awesome: https://EditorConfig.org 4 | 5 | # top-most EditorConfig file 6 | root = true 7 | 8 | # Don't use tabs for indentation. 9 | [*] 10 | indent_style = space 11 | 12 | # XML files 13 | [*.{csproj,props,targets}] 14 | indent_size = 2 15 | 16 | # Dotnet code style settings: 17 | [*.cs] 18 | indent_size = 4 19 | insert_final_newline = true 20 | charset = utf-8 21 | 22 | dotnet_analyzer_diagnostic.severity = warning 23 | 24 | # IDE0055: Fix formatting 25 | dotnet_diagnostic.IDE0055.severity = warning 26 | 27 | # Sort using and Import directives with System.* appearing first 28 | dotnet_sort_system_directives_first = true 29 | dotnet_separate_import_directive_groups = false 30 | 31 | # Avoid "this." and "Me." if not necessary 32 | dotnet_style_qualification_for_field = false:refactoring 33 | dotnet_style_qualification_for_property = false:refactoring 34 | dotnet_style_qualification_for_method = false:refactoring 35 | dotnet_style_qualification_for_event = false:refactoring 36 | 37 | # Use language keywords instead of framework type names for type references 38 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 39 | dotnet_style_predefined_type_for_member_access = true:suggestion 40 | 41 | # Suggest more modern language features when available 42 | dotnet_style_object_initializer = true:suggestion 43 | dotnet_style_collection_initializer = true:suggestion 44 | dotnet_style_coalesce_expression = true:suggestion 45 | dotnet_style_null_propagation = true:suggestion 46 | dotnet_style_explicit_tuple_names = true:suggestion 47 | 48 | # Whitespace options 49 | dotnet_style_allow_multiple_blank_lines_experimental = false 50 | 51 | # Non-private static fields are PascalCase 52 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion 53 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields 54 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style 55 | 56 | dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field 57 | dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected 58 | dotnet_naming_symbols.non_private_static_fields.required_modifiers = static 59 | 60 | dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case 61 | 62 | # Non-private readonly fields are PascalCase 63 | dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion 64 | dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields 65 | dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style 66 | 67 | dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field 68 | dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected 69 | dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly 70 | 71 | dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case 72 | 73 | # Constants are PascalCase 74 | dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion 75 | dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants 76 | dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style 77 | 78 | dotnet_naming_symbols.constants.applicable_kinds = field, local 79 | dotnet_naming_symbols.constants.required_modifiers = const 80 | 81 | dotnet_naming_style.constant_style.capitalization = pascal_case 82 | 83 | # Static fields are camelCase and start with s_ 84 | dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion 85 | dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields 86 | dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style 87 | 88 | dotnet_naming_symbols.static_fields.applicable_kinds = field 89 | dotnet_naming_symbols.static_fields.required_modifiers = static 90 | 91 | dotnet_naming_style.static_field_style.capitalization = camel_case 92 | dotnet_naming_style.static_field_style.required_prefix = s_ 93 | 94 | # Instance fields are camelCase and start with _ 95 | dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion 96 | dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields 97 | dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style 98 | 99 | dotnet_naming_symbols.instance_fields.applicable_kinds = field 100 | 101 | dotnet_naming_style.instance_field_style.capitalization = camel_case 102 | dotnet_naming_style.instance_field_style.required_prefix = _ 103 | 104 | # Locals and parameters are camelCase 105 | dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion 106 | dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters 107 | dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style 108 | 109 | dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local 110 | 111 | dotnet_naming_style.camel_case_style.capitalization = camel_case 112 | 113 | # Local functions are PascalCase 114 | dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion 115 | dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions 116 | dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style 117 | 118 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function 119 | 120 | dotnet_naming_style.local_function_style.capitalization = pascal_case 121 | 122 | # By default, name items with PascalCase 123 | dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion 124 | dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members 125 | dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style 126 | 127 | dotnet_naming_symbols.all_members.applicable_kinds = * 128 | 129 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 130 | 131 | # IDE0035: Remove unreachable code 132 | dotnet_diagnostic.IDE0035.severity = warning 133 | 134 | # IDE0036: Order modifiers 135 | dotnet_diagnostic.IDE0036.severity = warning 136 | 137 | # IDE0043: Format string contains invalid placeholder 138 | dotnet_diagnostic.IDE0043.severity = warning 139 | 140 | # IDE0044: Make field readonly 141 | dotnet_diagnostic.IDE0044.severity = warning 142 | 143 | # IDE0051: Remove unused private member 144 | dotnet_diagnostic.IDE0051.severity = warning 145 | 146 | # IDE0170: Prefer extended property pattern 147 | dotnet_diagnostic.IDE0170.severity = warning 148 | 149 | # Newline settings 150 | csharp_new_line_before_open_brace = all 151 | csharp_new_line_before_else = true 152 | csharp_new_line_before_catch = true 153 | csharp_new_line_before_finally = true 154 | csharp_new_line_before_members_in_object_initializers = true 155 | csharp_new_line_before_members_in_anonymous_types = true 156 | csharp_new_line_between_query_expression_clauses = true 157 | 158 | # Indentation preferences 159 | csharp_indent_block_contents = true 160 | csharp_indent_braces = false 161 | csharp_indent_case_contents = true 162 | csharp_indent_case_contents_when_block = true 163 | csharp_indent_switch_labels = true 164 | csharp_indent_labels = flush_left 165 | 166 | # Whitespace options 167 | csharp_style_allow_embedded_statements_on_same_line_experimental = false 168 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false 169 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false 170 | 171 | # Prefer method-like constructs to have a block body 172 | csharp_style_expression_bodied_methods = false:none 173 | csharp_style_expression_bodied_constructors = false:none 174 | csharp_style_expression_bodied_operators = false:none 175 | 176 | # Prefer property-like constructs to have an expression-body 177 | csharp_style_expression_bodied_properties = true:none 178 | csharp_style_expression_bodied_indexers = true:none 179 | csharp_style_expression_bodied_accessors = true:none 180 | 181 | # Suggest more modern language features when available 182 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 183 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 184 | csharp_style_inlined_variable_declaration = true:suggestion 185 | csharp_style_throw_expression = true:suggestion 186 | csharp_style_conditional_delegate_call = true:suggestion 187 | csharp_style_prefer_extended_property_pattern = true:suggestion 188 | 189 | # Space preferences 190 | csharp_space_after_cast = false 191 | csharp_space_after_colon_in_inheritance_clause = true 192 | csharp_space_after_comma = true 193 | csharp_space_after_dot = false 194 | csharp_space_after_keywords_in_control_flow_statements = true 195 | csharp_space_after_semicolon_in_for_statement = true 196 | csharp_space_around_binary_operators = before_and_after 197 | csharp_space_around_declaration_statements = do_not_ignore 198 | csharp_space_before_colon_in_inheritance_clause = true 199 | csharp_space_before_comma = false 200 | csharp_space_before_dot = false 201 | csharp_space_before_open_square_brackets = false 202 | csharp_space_before_semicolon_in_for_statement = false 203 | csharp_space_between_empty_square_brackets = false 204 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 205 | csharp_space_between_method_call_name_and_opening_parenthesis = false 206 | csharp_space_between_method_call_parameter_list_parentheses = false 207 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 208 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 209 | csharp_space_between_method_declaration_parameter_list_parentheses = false 210 | csharp_space_between_parentheses = false 211 | csharp_space_between_square_brackets = false 212 | 213 | # Blocks are allowed 214 | csharp_prefer_braces = true:warning 215 | csharp_preserve_single_line_blocks = true 216 | csharp_preserve_single_line_statements = true 217 | 218 | # IDE0160: Convert to file-scoped namespace 219 | csharp_style_namespace_declarations = file_scoped:warning 220 | 221 | # IDE0040: Add accessibility modifiers 222 | dotnet_diagnostic.IDE0040.severity = warning 223 | 224 | # IDE0052: Remove unread private member 225 | dotnet_diagnostic.IDE0052.severity = warning 226 | 227 | # IDE0059: Unnecessary assignment to a value 228 | dotnet_diagnostic.IDE0059.severity = warning 229 | 230 | # IDE0060: Remove unused parameter 231 | dotnet_diagnostic.IDE0060.severity = warning 232 | 233 | # CA1012: Abstract types should not have public constructors 234 | dotnet_diagnostic.CA1012.severity = warning 235 | 236 | # CA1822: Make member static 237 | dotnet_diagnostic.CA1822.severity = warning 238 | 239 | # Prefer "var" everywhere 240 | dotnet_diagnostic.IDE0007.severity = warning 241 | csharp_style_var_for_built_in_types = false:warning 242 | csharp_style_var_when_type_is_apparent = true:warning 243 | csharp_style_var_elsewhere = false:warning 244 | 245 | # dotnet_style_allow_multiple_blank_lines_experimental 246 | dotnet_diagnostic.IDE2000.severity = warning 247 | 248 | # csharp_style_allow_embedded_statements_on_same_line_experimental 249 | dotnet_diagnostic.IDE2001.severity = warning 250 | 251 | # csharp_style_allow_blank_lines_between_consecutive_braces_experimental 252 | dotnet_diagnostic.IDE2002.severity = warning 253 | 254 | # dotnet_style_allow_statement_immediately_after_block_experimental 255 | dotnet_diagnostic.IDE2003.severity = warning 256 | 257 | # csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental 258 | dotnet_diagnostic.IDE2004.severity = warning 259 | 260 | # CA1707: Identifiers should not contain underscores 261 | dotnet_diagnostic.CA1707.severity = none 262 | 263 | # IDE0058: Expression value is never used 264 | dotnet_diagnostic.IDE0058.severity = none 265 | 266 | # IDE0046: Convert to conditional expression 267 | dotnet_diagnostic.IDE0046.severity = silent -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "nuget" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | labels: 8 | - "enhancement" 9 | assignees: 10 | - "krafs" 11 | 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | labels: 17 | - "enhancement" 18 | assignees: 19 | - "krafs" -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main ] 7 | paths-ignore: 8 | - 'README.md' 9 | tags-ignore: 10 | - 'v*' 11 | pull_request: 12 | branches: [ main ] 13 | paths-ignore: 14 | - 'README.md' 15 | 16 | jobs: 17 | build: 18 | name: Build 19 | runs-on: ubuntu-latest 20 | steps: 21 | 22 | - uses: actions/checkout@v4 23 | 24 | - name: Set up .NET 25 | uses: actions/setup-dotnet@v4.3.1 26 | with: 27 | dotnet-version: 9.0.x 28 | env: 29 | DOTNET_NOLOGO: true 30 | DOTNET_CLI_TELEMETRY_OPTOUT: true 31 | 32 | - name: Restore 33 | run: dotnet restore 34 | 35 | - name: Build 36 | run: dotnet build -c Release --no-restore -warnaserror 37 | 38 | - name: Test 39 | run: dotnet test -c Release --no-build --no-restore -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | bin/ 3 | obj/ 4 | *.csproj.user -------------------------------------------------------------------------------- /Publicizer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32721.290 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Publicizer", "src\Publicizer\Publicizer.csproj", "{74860348-EEC2-4755-89D1-86C854EEB163}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Publicizer.Tests", "src\Publicizer.Tests\Publicizer.Tests.csproj", "{B7C5B3E2-7E53-4D8B-B324-E03E74D9628C}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E0C6F53C-4019-404B-AFB1-28D34268047B}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | .gitignore = .gitignore 14 | .github\workflows\ci.yml = .github\workflows\ci.yml 15 | .github\dependabot.yml = .github\dependabot.yml 16 | README.md = README.md 17 | EndProjectSection 18 | EndProject 19 | Global 20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 21 | Debug|Any CPU = Debug|Any CPU 22 | Release|Any CPU = Release|Any CPU 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {74860348-EEC2-4755-89D1-86C854EEB163}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {74860348-EEC2-4755-89D1-86C854EEB163}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {74860348-EEC2-4755-89D1-86C854EEB163}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {74860348-EEC2-4755-89D1-86C854EEB163}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {B7C5B3E2-7E53-4D8B-B324-E03E74D9628C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 30 | {B7C5B3E2-7E53-4D8B-B324-E03E74D9628C}.Debug|Any CPU.Build.0 = Debug|Any CPU 31 | {B7C5B3E2-7E53-4D8B-B324-E03E74D9628C}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {B7C5B3E2-7E53-4D8B-B324-E03E74D9628C}.Release|Any CPU.Build.0 = Release|Any CPU 33 | EndGlobalSection 34 | GlobalSection(SolutionProperties) = preSolution 35 | HideSolutionNode = FALSE 36 | EndGlobalSection 37 | GlobalSection(ExtensibilityGlobals) = postSolution 38 | SolutionGuid = {4631A3F3-0A19-4BB4-BC2C-E898C9F969E2} 39 | EndGlobalSection 40 | EndGlobal 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Publicizer Version Downloads 2 | 3 | Publicizer is an MSBuild plugin that allows direct access to private members in .NET-assemblies. 4 | 5 | ## Installation 6 | Use your IDE's package manager and add [Krafs.Publicizer](https://www.nuget.org/packages/Krafs.Publicizer) from nuget.org. 7 | 8 | Or add via the dotnet CLI: 9 | ```bash 10 | dotnet add package Krafs.Publicizer 11 | ``` 12 | 13 | ## Usage 14 | Publicizer needs to be told what private members you want access to. You do this by defining _Publicize_-items in your project file. 15 | 16 | ```xml 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ``` 44 | 45 | ### Regular expressions 46 | Regular expressions are supported with the `MemberPattern` attribute. 47 | ```xml 48 | 49 | 50 | 51 | 52 | ``` 53 | 54 | Notes: 55 | - Assemblies are referenced by their file name, excluding file extension. 56 | So, given an assembly called `MyAssemblyFileName.dll`, you reference it as `MyAssemblyFileName`. 57 | - There is no way to publicize a specific method overload. Specifying a method will publicize all its overloads. 58 | 59 | ### Publicize assemblies from a PackageReference 60 | PackageReferences, like other kinds of References, point towards one or more underlying assemblies. Publicizing these assemblies is just a matter of finding out the file names of the underlying assemblies, and then specify them as explained above. 61 | 62 | ### Publicize All 63 | You can use this shorthand property to publicize **all** assemblies referenced by your project: 64 | ```xml 65 | 66 | true 67 | 68 | ``` 69 | 70 | Save the project file and the changes should take effect shortly. If not, try performing a _Restore_. 71 | 72 | ### Diagnostics 73 | Publicizer logs to MSBuild. However, for convenience it is also possible to log to a custom log file by setting: 74 | ```xml 75 | 76 | path/to/logfile 77 | 78 | ``` 79 | If the file does not exist it will be created. 80 | 81 | The file is overwritten on every execution. 82 | 83 | ### Clean 84 | You can instruct Publicizer to clear its cache everytime the project is cleaned: 85 | ```xml 86 | 87 | true 88 | 89 | ``` 90 | This is mostly useful when troubleshooting Publicizer and you want logs to publicize on every rebuild instead of using the cached assemblies. 91 | 92 | ## How Publicizer works 93 | There are two obstacles with accessing private members - the compiler and the runtime. 94 | The compiler won't compile code that attempts to access private members, and even if it would - the runtime would throw a [MemberAccessException](https://docs.microsoft.com/en-us/dotnet/api/system.memberaccessexception/) during execution. 95 | 96 | Publicizer addresses the compiler issue by copying the assemblies, rewriting the access modifiers to public, and feeding those edited assemblies to the compiler instead of the real ones. This makes the compilation succeed. 97 | 98 | The runtime issue is solved by instructing the runtime to not throw MemberAccessExceptions when accessing private members. 99 | This is done differently depending on the runtime. Publicizer implements two strategies: Unsafe and IgnoresAccessChecksTo. 100 | 101 | Unsafe means that the assembly will be compiled with the [unsafe](https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/unsafe-code/) flag. 102 | 103 | IgnoresAccessChecksTo emits an [IgnoresAccessChecksToAttribute](https://www.strathweb.com/2018/10/no-internalvisibleto-no-problem-bypassing-c-visibility-rules-with-roslyn/) to your source code, which then becomes part of your assembly. 104 | 105 | Unsafe works for most versions of [Mono](https://www.mono-project.com/). IgnoresAccessChecksTo should work for most other runtimes, like CoreClr. That said - there could be exceptions. 106 | 107 | These strategies can be toggled on or off by editing the PublicizerRuntimeStrategies-property in your project file. 108 | 109 | Both strategies are enabled by default: 110 | ```xml 111 | 112 | Unsafe;IgnoresAccessChecksTo 113 | 114 | ``` 115 | However, if you e.g. know that your code runs fine with just the Unsafe strategy, you can avoid including the IgnoresAccessChecksToAttribute by telling Publicizer to only use Unsafe: 116 | ```xml 117 | 118 | Unsafe 119 | 120 | ``` 121 | 122 | ## Quirks 123 | Publicizer works by hacking the compiler and runtime, and there are a couple of quirks to be aware of. 124 | 125 | ### Overriding publicized members 126 | Overriding a publicized member will throw an error at runtime. For example, say the following class exists in a referenced assembly ExampleAssembly: 127 | ```cs 128 | namespace Example; 129 | public abstract class Person 130 | { 131 | protected abstract string Name { get; } 132 | } 133 | ``` 134 | If you publicize this assembly, then Person.Name will be changed to public. If you then create a subclass Student, it might look like this: 135 | ```cs 136 | public class Student : Person 137 | { 138 | public override string Name => "Foobar"; 139 | } 140 | ``` 141 | This compiles just fine. However, during execution the runtime is presumably loading the original assembly where Person.Name is protected. 142 | So you have a Student class with a public Name-property overriding a protected Name-property on the Person class. 143 | This will cause an access check mismatch at runtime and throw an error. 144 | 145 | You can avoid this by instructing Publicizer to not publicize Person.Name. You can use the _DoNotPublicize_-item for this: 146 | ```xml 147 | 148 | 149 | 150 | 151 | ``` 152 | 153 | However, if there are a lot of protected members you have to override, doing this for all of them can be cumbersome. 154 | For this scenario, you can instruct Publicizer to ignore all virtual members in the assembly: 155 | ```xml 156 | 157 | 158 | 159 | ``` 160 | 161 | ### Compiler-generated member name conflicts 162 | Sometimes assemblies contain members generated automatically by the compiler, like backing-fields for events. 163 | These generated members sometimes have names that conflict with other member names when they become public. 164 | 165 | You can solve this in the same ways as above - either by using individual _DoNotPublicize_-items, or by telling Publicizer to ignore all compiler-generated members in the assembly: 166 | ```xml 167 | 168 | 169 | 170 | ``` 171 | 172 | If you opt to ignore all virtual and/or compiler-generated members, you can still publicize specific ignored members by specifying them explicitly: 173 | ```xml 174 | 175 | 176 | 177 | 178 | ``` 179 | 180 | ## Acknowledgements 181 | This project builds upon rwmt's [Publicise](https://github.com/rwmt/Publicise), simplyWiri's [TaskPubliciser](https://github.com/simplyWiri/TaskPubliciser), and [this gist](https://gist.github.com/Zetrith/d86b1d84e993c8117983c09f1a5dcdcd) by Zetrith. 182 | 183 | ## License 184 | [MIT](https://choosealicense.com/licenses/mit/) 185 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krafs/Publicizer/2b9cfa4879f2c34219583fbc8483701b044a6467/icon.png -------------------------------------------------------------------------------- /src/Publicizer.Tests/NugetConfigMaker.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Reflection; 3 | 4 | namespace Publicizer.Tests; 5 | 6 | internal static class NugetConfigMaker 7 | { 8 | internal static void CreateConfigThatRestoresPublicizerLocally(string root) 9 | { 10 | // Given the built Krafs.Publicizer nuget package is located next to the Publicizer assembly. 11 | string? publicizerPackagesFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); 12 | 13 | DirectoryInfo globalPackagesFolder = Directory.CreateDirectory(Path.Combine(root, ".nuget", "packages")); 14 | 15 | string nugetConfig = $""" 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | """; 38 | 39 | string nugetConfigPath = Path.Combine(root, "nuget.config"); 40 | File.WriteAllText(nugetConfigPath, nugetConfig); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Publicizer.Tests/ProcessResult.cs: -------------------------------------------------------------------------------- 1 | namespace Publicizer.Tests; 2 | internal sealed record ProcessResult(int ExitCode, string Output, string Error); 3 | -------------------------------------------------------------------------------- /src/Publicizer.Tests/Publicizer.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net9.0 5 | enable 6 | false 7 | NU1702 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Publicizer.Tests/PublicizerTests.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using NUnit.Framework; 3 | 4 | namespace Publicizer.Tests; 5 | 6 | public class PublicizerTests 7 | { 8 | private const string TestTargetFramework = "net9.0"; 9 | 10 | [Test] 11 | public void PublicizePrivateField_CompilesAndRunsWithExitCode0AndPrintsFieldValue() 12 | { 13 | using var libraryFolder = new TemporaryFolder(); 14 | string libraryCodePath = Path.Combine(libraryFolder.Path, "PrivateClass.cs"); 15 | string libraryCode = """ 16 | namespace PrivateNamespace; 17 | class PrivateClass 18 | { 19 | private static string PrivateField = "foobar"; 20 | } 21 | """; 22 | File.WriteAllText(libraryCodePath, libraryCode); 23 | 24 | string libraryCsprojPath = Path.Combine(libraryFolder.Path, "PrivateAssembly.csproj"); 25 | string libraryCsproj = $""" 26 | 27 | 28 | 29 | {TestTargetFramework} 30 | false 31 | {libraryFolder.Path} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | """; 40 | 41 | File.WriteAllText(libraryCsprojPath, libraryCsproj); 42 | ProcessResult buildLibraryResult = Runner.Run("dotnet", "build", libraryCsprojPath); 43 | Assert.That(buildLibraryResult.ExitCode, Is.Zero, buildLibraryResult.Output); 44 | 45 | using var appFolder = new TemporaryFolder(); 46 | string appCodePath = Path.Combine(appFolder.Path, "Program.cs"); 47 | string appCode = "System.Console.Write(PrivateNamespace.PrivateClass.PrivateField);"; 48 | File.WriteAllText(appCodePath, appCode); 49 | string libraryPath = Path.Combine(libraryFolder.Path, "PrivateAssembly.dll"); 50 | 51 | string appCsproj = $""" 52 | 53 | 54 | 55 | {TestTargetFramework} 56 | false 57 | exe 58 | {appFolder.Path} 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | """; 70 | 71 | string appCsprojPath = Path.Combine(appFolder.Path, "App.csproj"); 72 | File.WriteAllText(appCsprojPath, appCsproj); 73 | string appPath = Path.Combine(appFolder.Path, "App.dll"); 74 | NugetConfigMaker.CreateConfigThatRestoresPublicizerLocally(appFolder.Path); 75 | 76 | ProcessResult buildAppProcess = Runner.Run("dotnet", "build", appCsprojPath); 77 | ProcessResult runAppProcess = Runner.Run("dotnet", appPath); 78 | 79 | Assert.That(buildAppProcess.ExitCode, Is.Zero, buildAppProcess.Output); 80 | Assert.That(runAppProcess.ExitCode, Is.Zero, runAppProcess.Output); 81 | Assert.That(runAppProcess.Output, Is.EqualTo("foobar"), runAppProcess.Output); 82 | } 83 | 84 | [Test] 85 | public void PublicizePrivateProperty_CompilesAndRunsWithExitCode0AndPrintsPropertyValue() 86 | { 87 | using var libraryFolder = new TemporaryFolder(); 88 | string libraryCodePath = Path.Combine(libraryFolder.Path, "PrivateClass.cs"); 89 | string libraryCode = """ 90 | namespace PrivateNamespace; 91 | class PrivateClass 92 | { 93 | private static string PrivateProperty => "foobar"; 94 | } 95 | """; 96 | File.WriteAllText(libraryCodePath, libraryCode); 97 | 98 | string libraryCsprojPath = Path.Combine(libraryFolder.Path, "PrivateAssembly.csproj"); 99 | string libraryCsproj = $""" 100 | 101 | 102 | 103 | {TestTargetFramework} 104 | false 105 | {libraryFolder.Path} 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | """; 114 | 115 | File.WriteAllText(libraryCsprojPath, libraryCsproj); 116 | ProcessResult buildLibraryResult = Runner.Run("dotnet", "build", libraryCsprojPath); 117 | Assert.That(buildLibraryResult.ExitCode, Is.Zero, buildLibraryResult.Output); 118 | 119 | using var appFolder = new TemporaryFolder(); 120 | string appCodePath = Path.Combine(appFolder.Path, "Program.cs"); 121 | string appCode = "System.Console.Write(PrivateNamespace.PrivateClass.PrivateProperty);"; 122 | File.WriteAllText(appCodePath, appCode); 123 | string libraryPath = Path.Combine(libraryFolder.Path, "PrivateAssembly.dll"); 124 | 125 | string appCsproj = $""" 126 | 127 | 128 | 129 | {TestTargetFramework} 130 | false 131 | exe 132 | {appFolder.Path} 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | """; 144 | 145 | string appCsprojPath = Path.Combine(appFolder.Path, "App.csproj"); 146 | File.WriteAllText(appCsprojPath, appCsproj); 147 | string appPath = Path.Combine(appFolder.Path, "App.dll"); 148 | NugetConfigMaker.CreateConfigThatRestoresPublicizerLocally(appFolder.Path); 149 | 150 | ProcessResult buildAppProcess = Runner.Run("dotnet", "build", appCsprojPath); 151 | ProcessResult runAppProcess = Runner.Run("dotnet", appPath); 152 | 153 | Assert.That(buildAppProcess.ExitCode, Is.Zero, buildAppProcess.Output); 154 | Assert.That(runAppProcess.ExitCode, Is.Zero, runAppProcess.Output); 155 | Assert.That(runAppProcess.Output, Is.EqualTo("foobar"), runAppProcess.Output); 156 | } 157 | 158 | [Test] 159 | public void PublicizePrivateMethod_CompilesAndRunsWithExitCode0AndPrintsReturnValue() 160 | { 161 | using var libraryFolder = new TemporaryFolder(); 162 | string libraryCodePath = Path.Combine(libraryFolder.Path, "PrivateClass.cs"); 163 | string libraryCode = """ 164 | namespace PrivateNamespace; 165 | class PrivateClass 166 | { 167 | private static string PrivateMethod() => "foobar"; 168 | } 169 | """; 170 | File.WriteAllText(libraryCodePath, libraryCode); 171 | 172 | string libraryCsprojPath = Path.Combine(libraryFolder.Path, "PrivateAssembly.csproj"); 173 | string libraryCsproj = $""" 174 | 175 | 176 | 177 | {TestTargetFramework} 178 | false 179 | {libraryFolder.Path} 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | """; 188 | 189 | File.WriteAllText(libraryCsprojPath, libraryCsproj); 190 | ProcessResult buildLibraryResult = Runner.Run("dotnet", "build", libraryCsprojPath); 191 | Assert.That(buildLibraryResult.ExitCode, Is.Zero, buildLibraryResult.Output); 192 | 193 | using var appFolder = new TemporaryFolder(); 194 | string appCodePath = Path.Combine(appFolder.Path, "Program.cs"); 195 | string appCode = "System.Console.Write(PrivateNamespace.PrivateClass.PrivateMethod());"; 196 | File.WriteAllText(appCodePath, appCode); 197 | string libraryPath = Path.Combine(libraryFolder.Path, "PrivateAssembly.dll"); 198 | 199 | string appCsproj = $""" 200 | 201 | 202 | 203 | {TestTargetFramework} 204 | false 205 | exe 206 | {appFolder.Path} 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | """; 218 | 219 | string appCsprojPath = Path.Combine(appFolder.Path, "App.csproj"); 220 | File.WriteAllText(appCsprojPath, appCsproj); 221 | string appPath = Path.Combine(appFolder.Path, "App.dll"); 222 | NugetConfigMaker.CreateConfigThatRestoresPublicizerLocally(appFolder.Path); 223 | 224 | ProcessResult buildAppProcess = Runner.Run("dotnet", "build", appCsprojPath); 225 | ProcessResult runAppProcess = Runner.Run("dotnet", appPath); 226 | 227 | Assert.That(buildAppProcess.ExitCode, Is.Zero, buildAppProcess.Output); 228 | Assert.That(runAppProcess.ExitCode, Is.Zero, runAppProcess.Output); 229 | Assert.That(runAppProcess.Output, Is.EqualTo("foobar"), runAppProcess.Output); 230 | } 231 | 232 | [Test] 233 | public void PublicizePrivateConstructor_CompilesAndRunsWithExitCode0() 234 | { 235 | using var libraryFolder = new TemporaryFolder(); 236 | string libraryCodePath = Path.Combine(libraryFolder.Path, "PrivateClass.cs"); 237 | string libraryCode = """ 238 | namespace PrivateNamespace; 239 | class PrivateClass 240 | { 241 | private PrivateClass() 242 | { } 243 | } 244 | """; 245 | File.WriteAllText(libraryCodePath, libraryCode); 246 | 247 | string libraryCsprojPath = Path.Combine(libraryFolder.Path, "PrivateAssembly.csproj"); 248 | string libraryCsproj = $""" 249 | 250 | 251 | 252 | {TestTargetFramework} 253 | false 254 | {libraryFolder.Path} 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | """; 263 | 264 | File.WriteAllText(libraryCsprojPath, libraryCsproj); 265 | ProcessResult buildLibraryResult = Runner.Run("dotnet", "build", libraryCsprojPath); 266 | Assert.That(buildLibraryResult.ExitCode, Is.Zero, buildLibraryResult.Output); 267 | 268 | using var appFolder = new TemporaryFolder(); 269 | string appCodePath = Path.Combine(appFolder.Path, "Program.cs"); 270 | string appCode = """ 271 | _ = new PrivateNamespace.PrivateClass(); 272 | System.Console.Write("foobar"); // Printing this means success, because failing the PrivateClass constructor above would throw. 273 | """; 274 | File.WriteAllText(appCodePath, appCode); 275 | string libraryPath = Path.Combine(libraryFolder.Path, "PrivateAssembly.dll"); 276 | 277 | string appCsproj = $""" 278 | 279 | 280 | 281 | {TestTargetFramework} 282 | false 283 | exe 284 | {appFolder.Path} 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | """; 296 | 297 | string appCsprojPath = Path.Combine(appFolder.Path, "App.csproj"); 298 | File.WriteAllText(appCsprojPath, appCsproj); 299 | string appPath = Path.Combine(appFolder.Path, "App.dll"); 300 | NugetConfigMaker.CreateConfigThatRestoresPublicizerLocally(appFolder.Path); 301 | 302 | ProcessResult buildAppProcess = Runner.Run("dotnet", "build", appCsprojPath); 303 | ProcessResult runAppProcess = Runner.Run("dotnet", appPath); 304 | 305 | Assert.That(buildAppProcess.ExitCode, Is.Zero, buildAppProcess.Output); 306 | Assert.That(runAppProcess.ExitCode, Is.Zero, runAppProcess.Output); 307 | Assert.That(runAppProcess.Output, Is.EqualTo("foobar"), runAppProcess.Output); 308 | } 309 | 310 | [Test] 311 | public void PublicizeAssembly_CompilesAndRunsWithExitCode0AndPrintsReturnValuesFromAllPrivateMembersInPrivateClass() 312 | { 313 | using var libraryFolder = new TemporaryFolder(); 314 | string libraryCodePath = Path.Combine(libraryFolder.Path, "PrivateClass.cs"); 315 | string libraryCode = """ 316 | namespace PrivateNamespace; 317 | class PrivateClass 318 | { 319 | private PrivateClass() 320 | { } 321 | 322 | private string PrivateField = "foo"; 323 | private string PrivateProperty => "ba"; 324 | private string PrivateMethod() => "r"; 325 | } 326 | """; 327 | File.WriteAllText(libraryCodePath, libraryCode); 328 | 329 | string libraryCsprojPath = Path.Combine(libraryFolder.Path, "PrivateAssembly.csproj"); 330 | string libraryCsproj = $""" 331 | 332 | 333 | 334 | {TestTargetFramework} 335 | false 336 | {libraryFolder.Path} 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | """; 345 | 346 | File.WriteAllText(libraryCsprojPath, libraryCsproj); 347 | ProcessResult buildLibraryResult = Runner.Run("dotnet", "build", libraryCsprojPath); 348 | Assert.That(buildLibraryResult.ExitCode, Is.Zero, buildLibraryResult.Output); 349 | 350 | using var appFolder = new TemporaryFolder(); 351 | string appCodePath = Path.Combine(appFolder.Path, "Program.cs"); 352 | string appCode = """ 353 | var privateClass = new PrivateNamespace.PrivateClass(); 354 | var result = privateClass.PrivateField; 355 | result += privateClass.PrivateProperty; 356 | result += privateClass.PrivateMethod(); 357 | System.Console.Write(result); 358 | """; 359 | File.WriteAllText(appCodePath, appCode); 360 | string libraryPath = Path.Combine(libraryFolder.Path, "PrivateAssembly.dll"); 361 | 362 | string appCsproj = $""" 363 | 364 | 365 | 366 | {TestTargetFramework} 367 | false 368 | exe 369 | {appFolder.Path} 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | """; 381 | 382 | string appCsprojPath = Path.Combine(appFolder.Path, "App.csproj"); 383 | File.WriteAllText(appCsprojPath, appCsproj); 384 | string appPath = Path.Combine(appFolder.Path, "App.dll"); 385 | NugetConfigMaker.CreateConfigThatRestoresPublicizerLocally(appFolder.Path); 386 | 387 | ProcessResult buildAppProcess = Runner.Run("dotnet", "build", appCsprojPath); 388 | ProcessResult runAppProcess = Runner.Run("dotnet", appPath); 389 | 390 | Assert.That(buildAppProcess.ExitCode, Is.Zero, buildAppProcess.Output); 391 | Assert.That(runAppProcess.ExitCode, Is.Zero, runAppProcess.Output); 392 | Assert.That(runAppProcess.Output, Is.EqualTo("foobar"), runAppProcess.Output); 393 | } 394 | 395 | [Test] 396 | public void PublicizeAll_CompilesAndRunsWithExitCode0AndPrintsReturnValuesFromPrivateMembersFromTwoDifferentAssemblies() 397 | { 398 | using var library1Folder = new TemporaryFolder(); 399 | string library1CodePath = Path.Combine(library1Folder.Path, "PrivateClass.cs"); 400 | string library1Code = """ 401 | namespace PrivateNamespace1; 402 | class PrivateClass 403 | { 404 | private PrivateClass() 405 | { } 406 | 407 | private string PrivateField = "foo"; 408 | private string PrivateProperty => "ba"; 409 | private string PrivateMethod() => "r"; 410 | } 411 | """; 412 | File.WriteAllText(library1CodePath, library1Code); 413 | 414 | string library1CsprojPath = Path.Combine(library1Folder.Path, "PrivateAssembly1.csproj"); 415 | string library1Csproj = $""" 416 | 417 | 418 | 419 | {TestTargetFramework} 420 | false 421 | {library1Folder.Path} 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | """; 430 | 431 | File.WriteAllText(library1CsprojPath, library1Csproj); 432 | ProcessResult buildLibrary1Result = Runner.Run("dotnet", "build", library1CsprojPath); 433 | Assert.That(buildLibrary1Result.ExitCode, Is.Zero, buildLibrary1Result.Output); 434 | 435 | using var library2Folder = new TemporaryFolder(); 436 | string library2CodePath = Path.Combine(library2Folder.Path, "PrivateClass.cs"); 437 | string library2Code = """ 438 | namespace PrivateNamespace2; 439 | class PrivateClass 440 | { 441 | private PrivateClass() 442 | { } 443 | 444 | private string PrivateField = "foo"; 445 | private string PrivateProperty => "ba"; 446 | private string PrivateMethod() => "r"; 447 | } 448 | """; 449 | File.WriteAllText(library2CodePath, library2Code); 450 | 451 | string library2CsprojPath = Path.Combine(library2Folder.Path, "PrivateAssembly2.csproj"); 452 | string library2Csproj = $""" 453 | 454 | 455 | 456 | {TestTargetFramework} 457 | false 458 | {library2Folder.Path} 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | """; 467 | 468 | File.WriteAllText(library2CsprojPath, library2Csproj); 469 | ProcessResult buildLibrary2Result = Runner.Run("dotnet", "build", library2CsprojPath); 470 | Assert.That(buildLibrary2Result.ExitCode, Is.Zero, buildLibrary2Result.Output); 471 | 472 | using var appFolder = new TemporaryFolder(); 473 | string appCodePath = Path.Combine(appFolder.Path, "Program.cs"); 474 | string appCode = """ 475 | var privateClass1 = new PrivateNamespace1.PrivateClass(); 476 | var result1 = privateClass1.PrivateField; 477 | result1 += privateClass1.PrivateProperty; 478 | result1 += privateClass1.PrivateMethod(); 479 | 480 | var privateClass2 = new PrivateNamespace2.PrivateClass(); 481 | var result2 = privateClass2.PrivateField; 482 | result2 += privateClass2.PrivateProperty; 483 | result2 += privateClass2.PrivateMethod(); 484 | 485 | System.Console.Write(result1 + result2); 486 | """; 487 | File.WriteAllText(appCodePath, appCode); 488 | string library1Path = Path.Combine(library1Folder.Path, "PrivateAssembly1.dll"); 489 | string library2Path = Path.Combine(library2Folder.Path, "PrivateAssembly2.dll"); 490 | 491 | string appCsproj = $""" 492 | 493 | 494 | 495 | {TestTargetFramework} 496 | false 497 | exe 498 | {appFolder.Path} 499 | true 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | """; 511 | 512 | string appCsprojPath = Path.Combine(appFolder.Path, "App.csproj"); 513 | File.WriteAllText(appCsprojPath, appCsproj); 514 | string appPath = Path.Combine(appFolder.Path, "App.dll"); 515 | NugetConfigMaker.CreateConfigThatRestoresPublicizerLocally(appFolder.Path); 516 | 517 | ProcessResult buildAppProcess = Runner.Run("dotnet", "build", appCsprojPath); 518 | ProcessResult runAppProcess = Runner.Run("dotnet", appPath); 519 | 520 | Assert.That(buildAppProcess.ExitCode, Is.Zero, buildAppProcess.Output); 521 | Assert.That(runAppProcess.ExitCode, Is.Zero, runAppProcess.Output); 522 | Assert.That(runAppProcess.Output, Is.EqualTo("foobarfoobar"), runAppProcess.Output); 523 | } 524 | 525 | [Test] 526 | public void PublicizeAssembly_ExceptProtectedMethod_OverridingThatMethod_CompilesAndRunsWithExitCode0() 527 | { 528 | using var libraryFolder = new TemporaryFolder(); 529 | string libraryCodePath = Path.Combine(libraryFolder.Path, "ProtectedClass.cs"); 530 | string libraryCode = """ 531 | namespace ProtectedNamespace; 532 | public abstract class ProtectedClass 533 | { 534 | protected abstract void ProtectedMethod(); 535 | } 536 | """; 537 | File.WriteAllText(libraryCodePath, libraryCode); 538 | 539 | string libraryCsprojPath = Path.Combine(libraryFolder.Path, "ProtectedAssembly.csproj"); 540 | string libraryCsproj = $""" 541 | 542 | 543 | 544 | {TestTargetFramework} 545 | false 546 | {libraryFolder.Path} 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | """; 555 | 556 | File.WriteAllText(libraryCsprojPath, libraryCsproj); 557 | ProcessResult buildLibraryResult = Runner.Run("dotnet", "build", libraryCsprojPath); 558 | Assert.That(buildLibraryResult.ExitCode, Is.Zero, buildLibraryResult.Output); 559 | 560 | using var appFolder = new TemporaryFolder(); 561 | string appCodePath = Path.Combine(appFolder.Path, "Program.cs"); 562 | string appCode = """ 563 | _ = new SubClass(); 564 | System.Console.Write("foobar"); 565 | class SubClass : ProtectedNamespace.ProtectedClass 566 | { 567 | protected override void ProtectedMethod() { } 568 | } 569 | """; 570 | File.WriteAllText(appCodePath, appCode); 571 | string libraryPath = Path.Combine(libraryFolder.Path, "ProtectedAssembly.dll"); 572 | 573 | string appCsproj = $""" 574 | 575 | 576 | 577 | {TestTargetFramework} 578 | false 579 | exe 580 | {appFolder.Path} 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | """; 593 | 594 | string appCsprojPath = Path.Combine(appFolder.Path, "App.csproj"); 595 | File.WriteAllText(appCsprojPath, appCsproj); 596 | string appPath = Path.Combine(appFolder.Path, "App.dll"); 597 | NugetConfigMaker.CreateConfigThatRestoresPublicizerLocally(appFolder.Path); 598 | 599 | ProcessResult buildAppProcess = Runner.Run("dotnet", "build", appCsprojPath); 600 | ProcessResult runAppProcess = Runner.Run("dotnet", appPath); 601 | 602 | Assert.That(buildAppProcess.ExitCode, Is.Zero, buildAppProcess.Output); 603 | Assert.That(runAppProcess.ExitCode, Is.Zero, runAppProcess.Output); 604 | Assert.That(runAppProcess.Output, Is.EqualTo("foobar"), runAppProcess.Output); 605 | } 606 | 607 | [Test] 608 | public void PublicizeAssembly_ExceptCompilerGenerated_FailsCompileAndPrintsErrorCodeCS0117ForCompilerGeneratedField() 609 | { 610 | using var libraryFolder = new TemporaryFolder(); 611 | string libraryCodePath = Path.Combine(libraryFolder.Path, "LibraryClass.cs"); 612 | string libraryCode = """ 613 | namespace LibraryNamespace; 614 | public class LibraryClass 615 | { 616 | [System.Runtime.CompilerServices.CompilerGenerated] 617 | private static string CompilerGeneratedPrivateField; 618 | private static string PrivateField; 619 | } 620 | """; 621 | File.WriteAllText(libraryCodePath, libraryCode); 622 | 623 | string libraryCsprojPath = Path.Combine(libraryFolder.Path, "LibraryAssembly.csproj"); 624 | string libraryCsproj = $""" 625 | 626 | 627 | 628 | {TestTargetFramework} 629 | false 630 | {libraryFolder.Path} 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | """; 639 | 640 | File.WriteAllText(libraryCsprojPath, libraryCsproj); 641 | ProcessResult buildLibraryResult = Runner.Run("dotnet", "build", libraryCsprojPath); 642 | Assert.That(buildLibraryResult.ExitCode, Is.Zero, buildLibraryResult.Output); 643 | 644 | using var appFolder = new TemporaryFolder(); 645 | string appCodePath = Path.Combine(appFolder.Path, "Program.cs"); 646 | string appCode = """ 647 | _ = LibraryNamespace.LibraryClass.CompilerGeneratedPrivateField; 648 | _ = LibraryNamespace.LibraryClass.PrivateField; 649 | """; 650 | File.WriteAllText(appCodePath, appCode); 651 | string libraryPath = Path.Combine(libraryFolder.Path, "LibraryAssembly.dll"); 652 | 653 | string appCsproj = $""" 654 | 655 | 656 | 657 | {TestTargetFramework} 658 | false 659 | exe 660 | {appFolder.Path} 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | """; 672 | 673 | string appCsprojPath = Path.Combine(appFolder.Path, "App.csproj"); 674 | File.WriteAllText(appCsprojPath, appCsproj); 675 | NugetConfigMaker.CreateConfigThatRestoresPublicizerLocally(appFolder.Path); 676 | 677 | ProcessResult buildAppProcess = Runner.Run("dotnet", "build", appCsprojPath); 678 | 679 | Assert.That(buildAppProcess.ExitCode, Is.Not.Zero, buildAppProcess.Output); 680 | Assert.That(buildAppProcess.Output, Does.Match("CS0117: 'LibraryClass' does not contain a definition for 'CompilerGeneratedPrivateField'")); 681 | Assert.That(buildAppProcess.Output, Does.Not.Match("CS0117: 'LibraryClass' does not contain a definition for 'PrivateField'")); 682 | } 683 | 684 | [Test] 685 | public void PublicizeAssembly_ExceptVirtual_FailsCompileAndPrintsErrorCodeCS0122ForVirtualProperty() 686 | { 687 | using var libraryFolder = new TemporaryFolder(); 688 | string libraryCodePath = Path.Combine(libraryFolder.Path, "LibraryClass.cs"); 689 | string libraryCode = """ 690 | namespace LibraryNamespace; 691 | public class LibraryClass 692 | { 693 | protected virtual string VirtualProtectedProperty => "foo"; 694 | protected string ProtectedProperty => "bar"; 695 | } 696 | """; 697 | File.WriteAllText(libraryCodePath, libraryCode); 698 | 699 | string libraryCsprojPath = Path.Combine(libraryFolder.Path, "LibraryAssembly.csproj"); 700 | string libraryCsproj = $""" 701 | 702 | 703 | 704 | {TestTargetFramework} 705 | false 706 | {libraryFolder.Path} 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | """; 715 | 716 | File.WriteAllText(libraryCsprojPath, libraryCsproj); 717 | ProcessResult buildLibraryResult = Runner.Run("dotnet", "build", libraryCsprojPath); 718 | Assert.That(buildLibraryResult.ExitCode, Is.Zero, buildLibraryResult.Output); 719 | 720 | using var appFolder = new TemporaryFolder(); 721 | string appCodePath = Path.Combine(appFolder.Path, "Program.cs"); 722 | string appCode = """ 723 | var instance = new LibraryNamespace.LibraryClass(); 724 | _ = instance.VirtualProtectedProperty; 725 | _ = instance.ProtectedProperty; 726 | """; 727 | File.WriteAllText(appCodePath, appCode); 728 | string libraryPath = Path.Combine(libraryFolder.Path, "LibraryAssembly.dll"); 729 | 730 | string appCsproj = $""" 731 | 732 | 733 | 734 | {TestTargetFramework} 735 | false 736 | exe 737 | {appFolder.Path} 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | """; 749 | 750 | string appCsprojPath = Path.Combine(appFolder.Path, "App.csproj"); 751 | File.WriteAllText(appCsprojPath, appCsproj); 752 | NugetConfigMaker.CreateConfigThatRestoresPublicizerLocally(appFolder.Path); 753 | 754 | ProcessResult buildAppProcess = Runner.Run("dotnet", "build", appCsprojPath); 755 | 756 | Assert.That(buildAppProcess.ExitCode, Is.Not.Zero, buildAppProcess.Output); 757 | Assert.That(buildAppProcess.Output, Does.Match("CS0122: 'LibraryClass.VirtualProtectedProperty' is inaccessible due to its protection level")); 758 | Assert.That(buildAppProcess.Output, Does.Not.Match("CS0122: 'LibraryClass.ProtectedProperty' is inaccessible due to its protection level")); 759 | } 760 | 761 | [Test] 762 | public void PublicizePrivateMembersWithMemberPattern_CompilesAndRunsWithExitCode0AndPrintsExpectedValues() 763 | { 764 | using var libraryFolder = new TemporaryFolder(); 765 | string libraryCodePath = Path.Combine(libraryFolder.Path, "PrivateClass.cs"); 766 | string libraryCode = """" 767 | namespace PrivateNamespace; 768 | class PrivateClass 769 | { 770 | protected string PrivateFooField = "foo"; 771 | protected string PrivateBarField = "bar"; 772 | protected string PrivateFooProperty { get; } = "foo"; 773 | } 774 | """"; 775 | File.WriteAllText(libraryCodePath, libraryCode); 776 | 777 | string libraryCsprojPath = Path.Combine(libraryFolder.Path, "PrivateAssembly.csproj"); 778 | string libraryCsproj = $""" 779 | 780 | 781 | 782 | {TestTargetFramework} 783 | false 784 | {libraryFolder.Path} 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | """; 793 | 794 | File.WriteAllText(libraryCsprojPath, libraryCsproj); 795 | ProcessResult buildLibraryResult = Runner.Run("dotnet", "build", libraryCsprojPath); 796 | Assert.That(buildLibraryResult.ExitCode, Is.Zero, buildLibraryResult.Output); 797 | 798 | using var appFolder = new TemporaryFolder(); 799 | string appCodePath = Path.Combine(appFolder.Path, "Program.cs"); 800 | string appCode = """ 801 | var instance = new PrivateNamespace.PrivateClass(); 802 | _ = instance.PrivateFooField; 803 | _ = instance.PrivateBarField; 804 | _ = instance.PrivateFooProperty; 805 | """; 806 | File.WriteAllText(appCodePath, appCode); 807 | string libraryPath = Path.Combine(libraryFolder.Path, "PrivateAssembly.dll"); 808 | 809 | string appCsproj = $""" 810 | 811 | 812 | 813 | {TestTargetFramework} 814 | false 815 | exe 816 | {appFolder.Path} 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | """; 828 | 829 | string appCsprojPath = Path.Combine(appFolder.Path, "App.csproj"); 830 | File.WriteAllText(appCsprojPath, appCsproj); 831 | string appPath = Path.Combine(appFolder.Path, "App.dll"); 832 | NugetConfigMaker.CreateConfigThatRestoresPublicizerLocally(appFolder.Path); 833 | 834 | ProcessResult buildAppProcess = Runner.Run("dotnet", "build", appCsprojPath); 835 | Assert.That(buildAppProcess.ExitCode, Is.Not.Zero, buildAppProcess.Output); 836 | Assert.That(buildAppProcess.Output, Does.Match("CS0122: 'PrivateClass.PrivateBarField' is inaccessible due to its protection level")); 837 | Assert.That(buildAppProcess.Output, Does.Not.Match("CS0122: 'PrivateClass.PrivateFooField' is inaccessible due to its protection level")); 838 | Assert.That(buildAppProcess.Output, Does.Not.Match("CS0122: 'PrivateClass.PrivateFooProperty' is inaccessible due to its protection level")); 839 | } 840 | } 841 | -------------------------------------------------------------------------------- /src/Publicizer.Tests/Runner.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Publicizer.Tests; 4 | 5 | internal static class Runner 6 | { 7 | internal static ProcessResult Run(string command, params string[] arguments) 8 | { 9 | var startInfo = new ProcessStartInfo 10 | { 11 | FileName = command, 12 | RedirectStandardOutput = true, 13 | RedirectStandardError = true 14 | }; 15 | foreach (string argument in arguments) 16 | { 17 | startInfo.ArgumentList.Add(argument); 18 | } 19 | using Process process = Process.Start(startInfo)!; 20 | process.WaitForExit(); 21 | 22 | var result = new ProcessResult( 23 | ExitCode: process.ExitCode, 24 | Output: process.StandardOutput.ReadToEnd(), 25 | Error: process.StandardError.ReadToEnd() 26 | ); 27 | 28 | process.Close(); 29 | 30 | return result; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Publicizer.Tests/TemporaryFolder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Publicizer.Tests; 5 | 6 | internal sealed class TemporaryFolder : IDisposable 7 | { 8 | private readonly DirectoryInfo _directoryInfo; 9 | internal TemporaryFolder() 10 | { 11 | string path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), Guid.NewGuid().ToString()); 12 | 13 | _directoryInfo = Directory.CreateDirectory(path); 14 | } 15 | 16 | internal string Path => _directoryInfo.FullName; 17 | 18 | public override string ToString() 19 | { 20 | return _directoryInfo.FullName; 21 | } 22 | 23 | void IDisposable.Dispose() 24 | { 25 | _directoryInfo.Delete(recursive: true); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Publicizer/AssemblyEditor.cs: -------------------------------------------------------------------------------- 1 | using dnlib.DotNet; 2 | 3 | namespace Publicizer; 4 | 5 | /// 6 | /// Class for making edits to assemblies and related types. 7 | /// 8 | internal static class AssemblyEditor 9 | { 10 | internal static bool PublicizeType(TypeDef type) 11 | { 12 | TypeAttributes oldAttributes = type.Attributes; 13 | type.Attributes &= ~TypeAttributes.VisibilityMask; 14 | 15 | if (type.IsNested) 16 | { 17 | type.Attributes |= TypeAttributes.NestedPublic; 18 | } 19 | else 20 | { 21 | type.Attributes |= TypeAttributes.Public; 22 | } 23 | return type.Attributes != oldAttributes; 24 | } 25 | 26 | internal static bool PublicizeProperty(PropertyDef property, bool includeVirtual = true) 27 | { 28 | bool publicized = false; 29 | 30 | if (property.GetMethod is MethodDef getMethod) 31 | { 32 | publicized |= PublicizeMethod(getMethod, includeVirtual); 33 | } 34 | 35 | if (property.SetMethod is MethodDef setMethod) 36 | { 37 | publicized |= PublicizeMethod(setMethod, includeVirtual); 38 | } 39 | 40 | return publicized; 41 | } 42 | 43 | internal static bool PublicizeMethod(MethodDef method, bool includeVirtual = true) 44 | { 45 | if (includeVirtual || !method.IsVirtual) 46 | { 47 | MethodAttributes oldAttributes = method.Attributes; 48 | method.Attributes &= ~MethodAttributes.MemberAccessMask; 49 | method.Attributes |= MethodAttributes.Public; 50 | return method.Attributes != oldAttributes; 51 | } 52 | return false; 53 | } 54 | 55 | internal static bool PublicizeField(FieldDef field) 56 | { 57 | FieldAttributes oldAttributes = field.Attributes; 58 | field.Attributes &= ~FieldAttributes.FieldAccessMask; 59 | field.Attributes |= FieldAttributes.Public; 60 | return field.Attributes != oldAttributes; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Publicizer/Hasher.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Linq; 3 | using System.Security.Cryptography; 4 | using System.Text; 5 | 6 | namespace Publicizer; 7 | 8 | /// 9 | /// Helper class for various hash related functions. 10 | /// 11 | internal static class Hasher 12 | { 13 | internal static string ComputeHash(string assemblyPath, PublicizerAssemblyContext assemblyContext) 14 | { 15 | var sb = new StringBuilder(); 16 | sb.Append(assemblyContext.AssemblyName); 17 | sb.Append(assemblyContext.IncludeCompilerGeneratedMembers); 18 | sb.Append(assemblyContext.IncludeVirtualMembers); 19 | sb.Append(assemblyContext.ExplicitlyPublicizeAssembly); 20 | sb.Append(assemblyContext.ExplicitlyDoNotPublicizeAssembly); 21 | foreach (string publicizePattern in assemblyContext.PublicizeMemberPatterns) 22 | { 23 | sb.Append(publicizePattern); 24 | } 25 | foreach (string doNotPublicizePattern in assemblyContext.DoNotPublicizeMemberPatterns) 26 | { 27 | sb.Append(doNotPublicizePattern); 28 | } 29 | if (assemblyContext.PublicizeMemberRegexPattern is not null) 30 | { 31 | sb.Append(assemblyContext.PublicizeMemberRegexPattern.ToString()); 32 | } 33 | 34 | byte[] patternBytes = Encoding.UTF8.GetBytes(sb.ToString()); 35 | byte[] assemblyBytes = File.ReadAllBytes(assemblyPath); 36 | byte[] allBytes = assemblyBytes.Concat(patternBytes).ToArray(); 37 | 38 | return ComputeHash(allBytes); 39 | } 40 | 41 | private static string ComputeHash(byte[] bytes) 42 | { 43 | using var algorithm = MD5.Create(); 44 | 45 | byte[] computedHash = algorithm.ComputeHash(bytes); 46 | var sb = new StringBuilder(); 47 | foreach (byte b in computedHash) 48 | { 49 | sb.Append($"{b:X2}"); 50 | } 51 | string hexadecimalHash = sb.ToString(); 52 | 53 | return hexadecimalHash; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Publicizer/ITaskLogger.cs: -------------------------------------------------------------------------------- 1 | namespace Publicizer; 2 | 3 | public interface ITaskLogger 4 | { 5 | public void Error(string message); 6 | public void Warning(string message); 7 | public void Info(string message); 8 | public void Verbose(string message); 9 | } 10 | -------------------------------------------------------------------------------- /src/Publicizer/IgnoresAccessChecksToAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace System.Runtime.CompilerServices 2 | { 3 | [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] 4 | internal sealed class IgnoresAccessChecksToAttribute : Attribute 5 | { 6 | internal IgnoresAccessChecksToAttribute(string assemblyName) 7 | { 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Publicizer/Krafs.Publicizer.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ResolveAssemblyReferences 7 | ResolveReferences 8 | Unsafe;IgnoresAccessChecksTo 9 | false 10 | false 11 | 12 | 13 | 14 | 15 | false 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Publicizer/Krafs.Publicizer.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | <_UnsafePattern>(;|\b)Unsafe(;|\b) 26 | <_IactPattern>(;|\b)IgnoresAccessChecksTo(;|\b) 27 | <_UseUnsafeStrategy>$([System.Text.RegularExpressions.Regex]::IsMatch($(PublicizerRuntimeStrategies), $(_UnsafePattern), RegexOptions.IgnoreCase)) 28 | <_UseIactStrategy>$([System.Text.RegularExpressions.Regex]::IsMatch($(PublicizerRuntimeStrategies), $(_IactPattern), RegexOptions.IgnoreCase)) 29 | 30 | 31 | 33 | 34 | 35 | 36 | <_Parameter1>%(_ReferencePathsToAdd.Filename) 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | true 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/Publicizer/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.Build.Framework; 4 | using Microsoft.Build.Utilities; 5 | 6 | namespace Publicizer; 7 | 8 | /// 9 | /// Simple logger implementation logging both to MSBuilds Build engine and an arbitrary Stream. 10 | /// 11 | internal sealed class Logger : ITaskLogger, IDisposable 12 | { 13 | private readonly StreamWriter _logFileWriter = StreamWriter.Null; 14 | private readonly TaskLoggingHelper _taskLogger; 15 | private readonly string _scope; 16 | 17 | private static string Now => DateTime.Now.ToLongTimeString(); 18 | 19 | /// 20 | /// Constructs an instance of that writes to both a Task and a Stream 21 | /// 22 | /// The logging helper of a Task 23 | /// An arbitrary stream for writing logs to 24 | internal Logger(TaskLoggingHelper taskLogger, Stream stream) 25 | { 26 | _logFileWriter = new StreamWriter(stream) 27 | { 28 | AutoFlush = true 29 | }; 30 | _taskLogger = taskLogger; 31 | _scope = string.Empty; 32 | } 33 | 34 | /// 35 | /// Constructs an instance of with a scope 36 | /// 37 | /// 38 | /// A string representing the scope of the logger. This will be written to each log entry in the log file 39 | private Logger(Logger parentLogger, string scope) 40 | { 41 | _logFileWriter = parentLogger._logFileWriter; 42 | _taskLogger = parentLogger._taskLogger; 43 | _scope = $" [{scope}]"; 44 | } 45 | 46 | public void Error(string message) 47 | { 48 | _taskLogger.LogError(message); 49 | Write("ERR", message); 50 | } 51 | 52 | public void Warning(string message) 53 | { 54 | _taskLogger.LogWarning(message); 55 | Write("WRN", message); 56 | } 57 | 58 | public void Info(string message) 59 | { 60 | _taskLogger.LogMessage(MessageImportance.Normal, message); 61 | Write("INF", message); 62 | } 63 | 64 | public void Verbose(string message) 65 | { 66 | _taskLogger.LogMessage(MessageImportance.Low, message); 67 | Write("VRB", message); 68 | } 69 | 70 | private void Write(string logLevel, string message) 71 | { 72 | _logFileWriter.WriteLine($"[{Now} {logLevel}]{_scope} {message}"); 73 | } 74 | 75 | internal ITaskLogger CreateScope(string assemblyName) 76 | { 77 | return new Logger(this, assemblyName); 78 | } 79 | 80 | public void Dispose() 81 | { 82 | _logFileWriter.Dispose(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Publicizer/PublicizeAssemblies.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using dnlib.DotNet; 6 | using dnlib.DotNet.Writer; 7 | using Microsoft.Build.Framework; 8 | using Microsoft.Build.Utilities; 9 | 10 | namespace Publicizer; 11 | public sealed class PublicizeAssemblies : Task 12 | { 13 | [Required] 14 | public string OutputDirectory { get; set; } = null!; 15 | 16 | [Required] 17 | public ITaskItem[] ReferencePaths { get; set; } = null!; 18 | 19 | public ITaskItem[]? Publicizes { get; set; } 20 | public ITaskItem[]? DoNotPublicizes { get; set; } 21 | public string? LogFilePath { get; set; } 22 | 23 | [Output] 24 | public ITaskItem[]? ReferencePathsToDelete { get; set; } 25 | 26 | [Output] 27 | public ITaskItem[]? ReferencePathsToAdd { get; set; } 28 | 29 | private Logger GetLogger() 30 | { 31 | Stream logStream = Stream.Null; 32 | if (!string.IsNullOrWhiteSpace(LogFilePath)) 33 | { 34 | try 35 | { 36 | string directory = Path.GetDirectoryName(LogFilePath); 37 | Directory.CreateDirectory(directory); 38 | logStream = File.Open(LogFilePath, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read); 39 | 40 | // Ensure log file is empty. 41 | logStream.SetLength(0); 42 | } 43 | catch (Exception e) 44 | { 45 | Log.LogError($"Error creating Publicizer log file: {e.Message}"); 46 | } 47 | } 48 | 49 | return new Logger(Log, logStream); 50 | } 51 | 52 | public override bool Execute() 53 | { 54 | using Logger logger = GetLogger(); 55 | logger.Info($"Initializing assembly publicization"); 56 | 57 | Publicizes ??= Array.Empty(); 58 | DoNotPublicizes ??= Array.Empty(); 59 | 60 | logger.Info($"Referenced assemblies: {ReferencePaths.Length}"); 61 | 62 | if (Publicizes.Length == 0) 63 | { 64 | logger.Info("No Publicizes provided. Terminating task."); 65 | return true; 66 | } 67 | 68 | try 69 | { 70 | Directory.CreateDirectory(OutputDirectory); 71 | } 72 | catch (Exception e) 73 | { 74 | logger.Error($"{nameof(OutputDirectory)} '{OutputDirectory}' is not a valid directory path: {e.Message}"); 75 | return false; 76 | } 77 | 78 | Dictionary assemblyContexts = GetPublicizerAssemblyContexts(Publicizes, DoNotPublicizes, logger); 79 | 80 | var referencePathsToDelete = new List(); 81 | var referencePathsToAdd = new List(); 82 | 83 | foreach (ITaskItem reference in ReferencePaths) 84 | { 85 | string assemblyName = reference.FileName(); 86 | if (!assemblyContexts.TryGetValue(assemblyName, out PublicizerAssemblyContext? assemblyContext)) 87 | { 88 | continue; 89 | } 90 | 91 | ITaskLogger scopedLogger = logger.CreateScope(assemblyName); 92 | scopedLogger.Info($"Assembly processing starting..."); 93 | string assemblyPath = reference.FullPath(); 94 | scopedLogger.Info($"Path: {assemblyPath}"); 95 | 96 | string hash = Hasher.ComputeHash(assemblyPath, assemblyContext); 97 | scopedLogger.Info($"Publicization hash: {hash}"); 98 | 99 | string outputAssemblyFolder = Path.Combine(OutputDirectory, $"{assemblyName}.{hash}"); 100 | Directory.CreateDirectory(outputAssemblyFolder); 101 | string outputAssemblyPath = Path.Combine(outputAssemblyFolder, assemblyName + ".dll"); 102 | 103 | if (File.Exists(outputAssemblyPath)) 104 | { 105 | scopedLogger.Info($"Assembly already publicized at {outputAssemblyPath}"); 106 | } 107 | else 108 | { 109 | using ModuleDef module = ModuleDefMD.Load(assemblyPath); 110 | scopedLogger.Info("Publicizing members..."); 111 | bool isAssemblyModified = PublicizeAssembly(module, assemblyContext, scopedLogger); 112 | if (!isAssemblyModified) 113 | { 114 | scopedLogger.Warning("Assembly is marked for publicization, but no members were publicized"); 115 | continue; 116 | } 117 | 118 | using var fileStream = new FileStream(outputAssemblyPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read); 119 | 120 | var writerOptions = new ModuleWriterOptions(module) 121 | { 122 | // Writing the module sometime fails without this flag due to how it was originally compiled. 123 | // https://github.com/krafs/Publicizer/issues/42 124 | MetadataOptions = new MetadataOptions(MetadataFlags.KeepOldMaxStack), 125 | Logger = DummyLogger.NoThrowInstance 126 | }; 127 | scopedLogger.Info($"Saving publicized assembly to {outputAssemblyPath}"); 128 | module.Write(fileStream, writerOptions); 129 | 130 | string assemblyDirectory = Path.GetDirectoryName(assemblyPath); 131 | string originalDocumentationFullPath = Path.Combine(assemblyDirectory, assemblyName + ".xml"); 132 | 133 | if (File.Exists(originalDocumentationFullPath)) 134 | { 135 | scopedLogger.Info($"Found XML documentation at {originalDocumentationFullPath}"); 136 | string newDocumentationRelativePath = Path.Combine(outputAssemblyFolder, assemblyName + ".xml"); 137 | string newDocumentationFullPath = Path.GetFullPath(newDocumentationRelativePath); 138 | scopedLogger.Info($"Copying XML documentation to {newDocumentationFullPath}"); 139 | File.Copy(originalDocumentationFullPath, newDocumentationFullPath, overwrite: true); 140 | } 141 | } 142 | 143 | referencePathsToDelete.Add(reference); 144 | ITaskItem newReference = new TaskItem(outputAssemblyPath); 145 | reference.CopyMetadataTo(newReference); 146 | reference.SetMetadata("Publicized", bool.TrueString); 147 | referencePathsToAdd.Add(newReference); 148 | scopedLogger.Info("Assembly processing finished"); 149 | } 150 | 151 | ReferencePathsToDelete = referencePathsToDelete.ToArray(); 152 | ReferencePathsToAdd = referencePathsToAdd.ToArray(); 153 | 154 | logger.Info($"Finished processing {assemblyContexts.Count} assemblies. Terminating task."); 155 | 156 | return true; 157 | } 158 | 159 | private static Dictionary GetPublicizerAssemblyContexts( 160 | ITaskItem[] publicizeItems, 161 | ITaskItem[] doNotPublicizeItems, 162 | ITaskLogger logger) 163 | { 164 | var contexts = new Dictionary(); 165 | 166 | foreach (ITaskItem item in publicizeItems) 167 | { 168 | int index = item.ItemSpec.IndexOf(':'); 169 | bool isAssemblyPattern = index == -1; 170 | string assemblyName = isAssemblyPattern ? item.ItemSpec : item.ItemSpec.Substring(0, index); 171 | 172 | if (!contexts.TryGetValue(assemblyName, out PublicizerAssemblyContext? assemblyContext)) 173 | { 174 | assemblyContext = new PublicizerAssemblyContext(assemblyName); 175 | contexts.Add(assemblyName, assemblyContext); 176 | } 177 | 178 | if (isAssemblyPattern) 179 | { 180 | assemblyContext.IncludeCompilerGeneratedMembers = item.IncludeCompilerGeneratedMembers(); 181 | assemblyContext.IncludeVirtualMembers = item.IncludeVirtualMembers(); 182 | assemblyContext.ExplicitlyPublicizeAssembly = true; 183 | assemblyContext.PublicizeMemberRegexPattern = item.MemberPattern(); 184 | logger.Info($"Publicize: {item}, virtual members: {assemblyContext.IncludeVirtualMembers}, compiler-generated members: {assemblyContext.IncludeCompilerGeneratedMembers}, member pattern: {assemblyContext.PublicizeMemberRegexPattern}"); 185 | } 186 | else 187 | { 188 | string memberPattern = item.ItemSpec.Substring(index + 1); 189 | assemblyContext.PublicizeMemberPatterns.Add(memberPattern); 190 | logger.Info($"Publicize: {item}"); 191 | } 192 | } 193 | 194 | foreach (ITaskItem item in doNotPublicizeItems) 195 | { 196 | int index = item.ItemSpec.IndexOf(':'); 197 | bool isAssemblyPattern = index == -1; 198 | string assemblyName = isAssemblyPattern ? item.ItemSpec : item.ItemSpec.Substring(0, index); 199 | 200 | if (!contexts.TryGetValue(assemblyName, out PublicizerAssemblyContext? assemblyContext)) 201 | { 202 | assemblyContext = new PublicizerAssemblyContext(assemblyName); 203 | contexts.Add(assemblyName, assemblyContext); 204 | } 205 | 206 | if (isAssemblyPattern) 207 | { 208 | assemblyContext.ExplicitlyDoNotPublicizeAssembly = true; 209 | } 210 | else 211 | { 212 | string memberPattern = item.ItemSpec.Substring(index + 1); 213 | assemblyContext.DoNotPublicizeMemberPatterns.Add(memberPattern); 214 | } 215 | 216 | logger.Info($"DoNotPublicize: {item}"); 217 | } 218 | 219 | return contexts; 220 | } 221 | 222 | private static bool PublicizeAssembly(ModuleDef module, PublicizerAssemblyContext assemblyContext, ITaskLogger logger) 223 | { 224 | bool publicizedAnyMemberInAssembly = false; 225 | var doNotPublicizePropertyMethods = new HashSet(); 226 | 227 | int publicizedTypesCount = 0; 228 | int publicizedPropertiesCount = 0; 229 | int publicizedMethodsCount = 0; 230 | int publicizedFieldsCount = 0; 231 | 232 | // TYPES 233 | foreach (TypeDef? typeDef in module.GetTypes()) 234 | { 235 | doNotPublicizePropertyMethods.Clear(); 236 | 237 | bool publicizedAnyMemberInType = false; 238 | string typeName = typeDef.ReflectionFullName; 239 | 240 | bool explicitlyDoNotPublicizeType = assemblyContext.DoNotPublicizeMemberPatterns.Contains(typeName); 241 | 242 | // PROPERTIES 243 | foreach (PropertyDef? propertyDef in typeDef.Properties) 244 | { 245 | string propertyName = $"{typeName}.{propertyDef.Name}"; 246 | 247 | bool explicitlyDoNotPublicizeProperty = assemblyContext.DoNotPublicizeMemberPatterns.Contains(propertyName); 248 | if (explicitlyDoNotPublicizeProperty) 249 | { 250 | if (propertyDef.GetMethod is MethodDef getter) 251 | { 252 | doNotPublicizePropertyMethods.Add(getter); 253 | } 254 | if (propertyDef.SetMethod is MethodDef setter) 255 | { 256 | doNotPublicizePropertyMethods.Add(setter); 257 | } 258 | logger.Verbose($"Explicitly ignoring property: {propertyName}"); 259 | continue; 260 | } 261 | 262 | bool explicitlyPublicizeProperty = assemblyContext.PublicizeMemberPatterns.Contains(propertyName); 263 | if (explicitlyPublicizeProperty) 264 | { 265 | if (AssemblyEditor.PublicizeProperty(propertyDef)) 266 | { 267 | publicizedAnyMemberInType = true; 268 | publicizedAnyMemberInAssembly = true; 269 | publicizedPropertiesCount++; 270 | logger.Verbose($"Explicitly publicizing property: {propertyName}"); 271 | } 272 | continue; 273 | } 274 | 275 | if (explicitlyDoNotPublicizeType) 276 | { 277 | continue; 278 | } 279 | 280 | if (assemblyContext.ExplicitlyDoNotPublicizeAssembly) 281 | { 282 | continue; 283 | } 284 | 285 | if (assemblyContext.ExplicitlyPublicizeAssembly) 286 | { 287 | bool isCompilerGeneratedProperty = IsCompilerGenerated(propertyDef); 288 | if (isCompilerGeneratedProperty && !assemblyContext.IncludeCompilerGeneratedMembers) 289 | { 290 | continue; 291 | } 292 | 293 | bool isRegexPatternMatch = assemblyContext.PublicizeMemberRegexPattern?.IsMatch(propertyName) ?? true; 294 | if (!isRegexPatternMatch) 295 | { 296 | continue; 297 | } 298 | 299 | if (AssemblyEditor.PublicizeProperty(propertyDef, assemblyContext.IncludeVirtualMembers)) 300 | { 301 | publicizedAnyMemberInType = true; 302 | publicizedAnyMemberInAssembly = true; 303 | publicizedPropertiesCount++; 304 | } 305 | } 306 | } 307 | 308 | // METHODS 309 | foreach (MethodDef? methodDef in typeDef.Methods) 310 | { 311 | string methodName = $"{typeName}.{methodDef.Name}"; 312 | 313 | bool isMethodOfNonPublicizedProperty = doNotPublicizePropertyMethods.Contains(methodDef); 314 | if (isMethodOfNonPublicizedProperty) 315 | { 316 | continue; 317 | } 318 | 319 | bool explicitlyDoNotPublicizeMethod = assemblyContext.DoNotPublicizeMemberPatterns.Contains(methodName); 320 | if (explicitlyDoNotPublicizeMethod) 321 | { 322 | logger.Verbose($"Explicitly ignoring method: {methodName}"); 323 | continue; 324 | } 325 | 326 | bool explicitlyPublicizeMethod = assemblyContext.PublicizeMemberPatterns.Contains(methodName); 327 | if (explicitlyPublicizeMethod) 328 | { 329 | if (AssemblyEditor.PublicizeMethod(methodDef)) 330 | { 331 | publicizedAnyMemberInType = true; 332 | publicizedAnyMemberInAssembly = true; 333 | publicizedMethodsCount++; 334 | logger.Verbose($"Explicitly publicizing method: {methodName}"); 335 | } 336 | continue; 337 | } 338 | 339 | if (explicitlyDoNotPublicizeType) 340 | { 341 | continue; 342 | } 343 | 344 | if (assemblyContext.ExplicitlyDoNotPublicizeAssembly) 345 | { 346 | continue; 347 | } 348 | 349 | if (assemblyContext.ExplicitlyPublicizeAssembly) 350 | { 351 | bool isCompilerGeneratedMethod = IsCompilerGenerated(methodDef); 352 | if (isCompilerGeneratedMethod && !assemblyContext.IncludeCompilerGeneratedMembers) 353 | { 354 | continue; 355 | } 356 | 357 | bool isRegexPatternMatch = assemblyContext.PublicizeMemberRegexPattern?.IsMatch(methodName) ?? true; 358 | if (!isRegexPatternMatch) 359 | { 360 | continue; 361 | } 362 | 363 | if (AssemblyEditor.PublicizeMethod(methodDef, assemblyContext.IncludeVirtualMembers)) 364 | { 365 | publicizedAnyMemberInType = true; 366 | publicizedAnyMemberInAssembly = true; 367 | publicizedMethodsCount++; 368 | } 369 | } 370 | } 371 | 372 | // FIELDS 373 | foreach (FieldDef? fieldDef in typeDef.Fields) 374 | { 375 | string fieldName = $"{typeName}.{fieldDef.Name}"; 376 | 377 | bool explicitlyDoNotPublicizeField = assemblyContext.DoNotPublicizeMemberPatterns.Contains(fieldName); 378 | if (explicitlyDoNotPublicizeField) 379 | { 380 | logger.Verbose($"Explicitly ignoring field: {fieldName}"); 381 | continue; 382 | } 383 | 384 | bool explicitlyPublicizeField = assemblyContext.PublicizeMemberPatterns.Contains(fieldName); 385 | if (explicitlyPublicizeField) 386 | { 387 | if (AssemblyEditor.PublicizeField(fieldDef)) 388 | { 389 | publicizedAnyMemberInType = true; 390 | publicizedAnyMemberInAssembly = true; 391 | publicizedFieldsCount++; 392 | logger.Verbose($"Explicitly publicizing field: {fieldName}"); 393 | } 394 | continue; 395 | } 396 | 397 | if (explicitlyDoNotPublicizeType) 398 | { 399 | continue; 400 | } 401 | 402 | if (assemblyContext.ExplicitlyDoNotPublicizeAssembly) 403 | { 404 | continue; 405 | } 406 | 407 | if (assemblyContext.ExplicitlyPublicizeAssembly) 408 | { 409 | bool isCompilerGeneratedField = IsCompilerGenerated(fieldDef); 410 | if (isCompilerGeneratedField && !assemblyContext.IncludeCompilerGeneratedMembers) 411 | { 412 | continue; 413 | } 414 | 415 | bool isRegexPatternMatch = assemblyContext.PublicizeMemberRegexPattern?.IsMatch(fieldName) ?? true; 416 | if (!isRegexPatternMatch) 417 | { 418 | continue; 419 | } 420 | 421 | if (AssemblyEditor.PublicizeField(fieldDef)) 422 | { 423 | publicizedAnyMemberInType = true; 424 | publicizedAnyMemberInAssembly = true; 425 | publicizedFieldsCount++; 426 | } 427 | } 428 | } 429 | 430 | if (publicizedAnyMemberInType) 431 | { 432 | if (AssemblyEditor.PublicizeType(typeDef)) 433 | { 434 | publicizedAnyMemberInAssembly = true; 435 | publicizedTypesCount++; 436 | } 437 | continue; 438 | } 439 | 440 | if (explicitlyDoNotPublicizeType) 441 | { 442 | logger.Verbose($"Explicitly ignoring type: {typeName}"); 443 | continue; 444 | } 445 | 446 | bool explicitlyPublicizeType = assemblyContext.PublicizeMemberPatterns.Contains(typeName); 447 | if (explicitlyPublicizeType) 448 | { 449 | if (AssemblyEditor.PublicizeType(typeDef)) 450 | { 451 | publicizedAnyMemberInAssembly = true; 452 | publicizedTypesCount++; 453 | logger.Verbose($"Explicitly publicizing type: {typeName}"); 454 | } 455 | continue; 456 | } 457 | 458 | if (assemblyContext.ExplicitlyDoNotPublicizeAssembly) 459 | { 460 | continue; 461 | } 462 | 463 | if (assemblyContext.ExplicitlyPublicizeAssembly) 464 | { 465 | bool isCompilerGeneratedType = IsCompilerGenerated(typeDef); 466 | if (isCompilerGeneratedType && !assemblyContext.IncludeCompilerGeneratedMembers) 467 | { 468 | continue; 469 | } 470 | 471 | bool isRegexPatternMatch = assemblyContext.PublicizeMemberRegexPattern?.IsMatch(typeName) ?? true; 472 | if (!isRegexPatternMatch) 473 | { 474 | continue; 475 | } 476 | 477 | if (AssemblyEditor.PublicizeType(typeDef)) 478 | { 479 | publicizedAnyMemberInAssembly = true; 480 | publicizedTypesCount++; 481 | } 482 | } 483 | } 484 | 485 | logger.Info("Publicized types: " + publicizedTypesCount); 486 | logger.Info("Publicized properties: " + publicizedPropertiesCount); 487 | logger.Info("Publicized methods: " + publicizedMethodsCount); 488 | logger.Info("Publicized fields: " + publicizedFieldsCount); 489 | 490 | return publicizedAnyMemberInAssembly; 491 | } 492 | 493 | private static bool IsCompilerGenerated(IHasCustomAttribute memberDef) 494 | { 495 | return memberDef.CustomAttributes.Any(x => x.TypeFullName == "System.Runtime.CompilerServices.CompilerGeneratedAttribute"); 496 | } 497 | } 498 | -------------------------------------------------------------------------------- /src/Publicizer/Publicizer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net472 5 | 11 6 | enable 7 | true 8 | true 9 | build 10 | true 11 | true 12 | true 13 | bin 14 | $(OutDir) 15 | $(TargetsForTfmSpecificBuildOutput);IncludePackageDependencies 16 | All 17 | 18 | 19 | 20 | Publicizer 21 | Krafs.Publicizer 22 | Krafs 23 | © Krafs 2024 24 | MIT 25 | https://github.com/krafs/Publicizer 26 | https://github.com/krafs/Publicizer.git 27 | MSBuild library for allowing direct access to non-public members in .NET assemblies. 28 | msbuild accesschecks public publicizer 29 | icon.png 30 | README.md 31 | 2.3.0 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/Publicizer/PublicizerAssemblyContext.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace Publicizer; 5 | 6 | internal sealed class PublicizerAssemblyContext 7 | { 8 | internal PublicizerAssemblyContext(string assemblyName) 9 | { 10 | AssemblyName = assemblyName; 11 | } 12 | 13 | internal string AssemblyName { get; } 14 | internal bool ExplicitlyPublicizeAssembly { get; set; } = false; 15 | internal bool IncludeCompilerGeneratedMembers { get; set; } = true; 16 | internal bool IncludeVirtualMembers { get; set; } = true; 17 | internal bool ExplicitlyDoNotPublicizeAssembly { get; set; } = false; 18 | internal HashSet PublicizeMemberPatterns { get; } = new HashSet(); 19 | internal Regex? PublicizeMemberRegexPattern { get; set; } 20 | internal HashSet DoNotPublicizeMemberPatterns { get; } = new HashSet(); 21 | } 22 | -------------------------------------------------------------------------------- /src/Publicizer/TaskItemExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Microsoft.Build.Framework; 3 | 4 | namespace Publicizer; 5 | 6 | internal static class TaskItemExtensions 7 | { 8 | internal static string FileName(this ITaskItem item) 9 | { 10 | return item.GetMetadata("Filename"); 11 | } 12 | 13 | internal static string FullPath(this ITaskItem item) 14 | { 15 | return item.GetMetadata("Fullpath"); 16 | } 17 | 18 | internal static bool IncludeCompilerGeneratedMembers(this ITaskItem item) 19 | { 20 | if (bool.TryParse(item.GetMetadata("IncludeCompilerGeneratedMembers"), out bool includeCompilerGeneratedMembers)) 21 | { 22 | return includeCompilerGeneratedMembers; 23 | } 24 | 25 | return true; 26 | } 27 | 28 | internal static bool IncludeVirtualMembers(this ITaskItem item) 29 | { 30 | if (bool.TryParse(item.GetMetadata("IncludeVirtualMembers"), out bool includeVirtualMembers)) 31 | { 32 | return includeVirtualMembers; 33 | } 34 | 35 | return true; 36 | } 37 | 38 | internal static Regex? MemberPattern(this ITaskItem item) 39 | { 40 | string? memberPattern = item.GetMetadata("MemberPattern"); 41 | if (string.IsNullOrWhiteSpace(memberPattern)) 42 | { 43 | return null; 44 | } 45 | 46 | return new Regex(memberPattern, RegexOptions.Compiled); 47 | } 48 | } 49 | --------------------------------------------------------------------------------