├── .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
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 |
--------------------------------------------------------------------------------