├── .editorconfig
├── .gitignore
├── LICENSE
├── README.md
├── VocaDb.ResXFileCodeGenerator.Tests
├── AdditionalTextStub.cs
├── CodeGenTests.cs
├── GeneratorTests.cs
├── GithubIssues
│ └── Issue3
│ │ └── GeneratorTests.cs
├── GroupResxFilesTests.cs
├── HelperGeneratorTests.cs
├── IntegrationTests
│ ├── Test1.da-dk.resx
│ ├── Test1.da.resx
│ ├── Test1.en-us.resx
│ ├── Test1.resx
│ ├── Test2.da-dk.resx
│ ├── Test2.da.resx
│ ├── Test2.en-us.resx
│ ├── Test2.resx
│ └── TestResxFiles.cs
├── Properties
│ └── launchSettings.json
├── SettingsTests.cs
├── UtilitiesTests.cs
└── VocaDb.ResXFileCodeGenerator.Tests.csproj
├── VocaDb.ResXFileCodeGenerator.sln
├── VocaDb.ResXFileCodeGenerator.sln.DotSettings
└── VocaDb.ResXFileCodeGenerator
├── AdditionalTextWithHash.cs
├── AnalyzerReleases.Shipped.md
├── AnalyzerReleases.Unshipped.md
├── Constants.cs
├── CultureInfoCombo.cs
├── FileOptions.cs
├── GlobalOptions.cs
├── GroupResxFiles.cs
├── GroupedAdditionalFile.cs
├── IGenerator.cs
├── InnerClassVisibility.cs
├── IsExternalInit.cs
├── NullableAttributes.cs
├── Properties
└── launchSettings.json
├── SourceGenerator.cs
├── StringBuilderExtensions.cs
├── StringBuilderGenerator.ComboGenerator.cs
├── StringBuilderGenerator.ResourceManager.cs
├── StringBuilderGenerator.cs
├── StringExtensions.cs
├── Utilities.cs
├── VocaDb.ResXFileCodeGenerator.csproj
├── VocaDb.ResXFileCodeGenerator.targets
└── build
└── VocaDb.ResXFileCodeGenerator.props
/.editorconfig:
--------------------------------------------------------------------------------
1 | # To learn more about .editorconfig see https://aka.ms/editorconfigdocs
2 | ###############################
3 | # Core EditorConfig Options #
4 | ###############################
5 | root = true
6 | # All files
7 | [*]
8 | end_of_line = lf
9 | charset = utf-8
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 | indent_style = tab
13 | indent_size = 4
14 |
15 | # XML project files
16 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
17 | indent_size = 2
18 |
19 | # XML config files
20 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
21 | indent_size = 2
22 |
23 | # Code files
24 | [*.{cs,csx,vb,vbx}]
25 | indent_size = 4
26 | insert_final_newline = true
27 | charset = utf-8-bom
28 | ###############################
29 | # .NET Coding Conventions #
30 | ###############################
31 | [*.{cs,vb}]
32 | # Organize usings
33 | csharp_style_namespace_declarations=file_scoped:suggestion
34 | dotnet_sort_system_directives_first = true
35 | # this. preferences
36 | dotnet_style_qualification_for_field = false:silent
37 | dotnet_style_qualification_for_property = false:silent
38 | dotnet_style_qualification_for_method = false:silent
39 | dotnet_style_qualification_for_event = false:silent
40 | # Language keywords vs BCL types preferences
41 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent
42 | dotnet_style_predefined_type_for_member_access = true:silent
43 | # Parentheses preferences
44 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
45 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
46 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
47 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
48 | # Modifier preferences
49 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
50 | dotnet_style_readonly_field = true:suggestion
51 | # Expression-level preferences
52 | dotnet_style_object_initializer = true:suggestion
53 | dotnet_style_collection_initializer = true:suggestion
54 | dotnet_style_explicit_tuple_names = true:suggestion
55 | dotnet_style_null_propagation = true:suggestion
56 | dotnet_style_coalesce_expression = true:suggestion
57 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent
58 | dotnet_style_prefer_inferred_tuple_names = true:suggestion
59 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
60 | dotnet_style_prefer_auto_properties = true:silent
61 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent
62 | dotnet_style_prefer_conditional_expression_over_return = true:silent
63 | ###############################
64 | # Naming Conventions #
65 | ###############################
66 | # Style Definitions
67 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case
68 | # Use PascalCase for constant fields
69 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
70 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
71 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
72 | dotnet_naming_symbols.constant_fields.applicable_kinds = field
73 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = *
74 | dotnet_naming_symbols.constant_fields.required_modifiers = const
75 | tab_width=4
76 | ###############################
77 | # C# Coding Conventions #
78 | ###############################
79 | [*.cs]
80 | # var preferences
81 | csharp_style_var_for_built_in_types = true:silent
82 | csharp_style_var_when_type_is_apparent = true:silent
83 | csharp_style_var_elsewhere = true:silent
84 | # Expression-bodied members
85 | csharp_style_expression_bodied_methods = false:silent
86 | csharp_style_expression_bodied_constructors = false:silent
87 | csharp_style_expression_bodied_operators = false:silent
88 | csharp_style_expression_bodied_properties = true:silent
89 | csharp_style_expression_bodied_indexers = true:silent
90 | csharp_style_expression_bodied_accessors = true:silent
91 | # Pattern matching preferences
92 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
93 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
94 | # Null-checking preferences
95 | csharp_style_throw_expression = true:suggestion
96 | csharp_style_conditional_delegate_call = true:suggestion
97 | # Modifier preferences
98 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
99 | # Expression-level preferences
100 | csharp_prefer_braces = true:silent
101 | csharp_style_deconstructed_variable_declaration = true:suggestion
102 | csharp_prefer_simple_default_expression = true:suggestion
103 | csharp_style_pattern_local_over_anonymous_function = true:suggestion
104 | csharp_style_inlined_variable_declaration = true:suggestion
105 | ###############################
106 | # C# Formatting Rules #
107 | ###############################
108 | # New line preferences
109 | csharp_new_line_before_open_brace = all
110 | csharp_new_line_before_else = true
111 | csharp_new_line_before_catch = true
112 | csharp_new_line_before_finally = true
113 | csharp_new_line_before_members_in_object_initializers = true
114 | csharp_new_line_before_members_in_anonymous_types = true
115 | csharp_new_line_between_query_expression_clauses = true
116 | # Indentation preferences
117 | csharp_indent_case_contents = true
118 | csharp_indent_switch_labels = true
119 | csharp_indent_labels = flush_left
120 | # Space preferences
121 | csharp_space_after_cast = false
122 | csharp_space_after_keywords_in_control_flow_statements = true
123 | csharp_space_between_method_call_parameter_list_parentheses = false
124 | csharp_space_between_method_declaration_parameter_list_parentheses = false
125 | csharp_space_between_parentheses = false
126 | csharp_space_before_colon_in_inheritance_clause = true
127 | csharp_space_after_colon_in_inheritance_clause = true
128 | csharp_space_around_binary_operators = before_and_after
129 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
130 | csharp_space_between_method_call_name_and_opening_parenthesis = false
131 | csharp_space_between_method_call_empty_parameter_list_parentheses = false
132 | # Wrapping preferences
133 | csharp_preserve_single_line_statements = true
134 | csharp_preserve_single_line_blocks = true
135 | ###############################
136 | # VB Coding Conventions #
137 | ###############################
138 | [*.vb]
139 | # Modifier preferences
140 | visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion
141 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET Core
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # ASP.NET Scaffolding
66 | ScaffoldingReadMe.txt
67 |
68 | # StyleCop
69 | StyleCopReport.xml
70 |
71 | # Files built by Visual Studio
72 | *_i.c
73 | *_p.c
74 | *_h.h
75 | *.ilk
76 | *.meta
77 | *.obj
78 | *.iobj
79 | *.pch
80 | *.pdb
81 | *.ipdb
82 | *.pgc
83 | *.pgd
84 | *.rsp
85 | *.sbr
86 | *.tlb
87 | *.tli
88 | *.tlh
89 | *.tmp
90 | *.tmp_proj
91 | *_wpftmp.csproj
92 | *.log
93 | *.vspscc
94 | *.vssscc
95 | .builds
96 | *.pidb
97 | *.svclog
98 | *.scc
99 |
100 | # Chutzpah Test files
101 | _Chutzpah*
102 |
103 | # Visual C++ cache files
104 | ipch/
105 | *.aps
106 | *.ncb
107 | *.opendb
108 | *.opensdf
109 | *.sdf
110 | *.cachefile
111 | *.VC.db
112 | *.VC.VC.opendb
113 |
114 | # Visual Studio profiler
115 | *.psess
116 | *.vsp
117 | *.vspx
118 | *.sap
119 |
120 | # Visual Studio Trace Files
121 | *.e2e
122 |
123 | # TFS 2012 Local Workspace
124 | $tf/
125 |
126 | # Guidance Automation Toolkit
127 | *.gpState
128 |
129 | # ReSharper is a .NET coding add-in
130 | _ReSharper*/
131 | *.[Rr]e[Ss]harper
132 | *.DotSettings.user
133 |
134 | # TeamCity is a build add-in
135 | _TeamCity*
136 |
137 | # DotCover is a Code Coverage Tool
138 | *.dotCover
139 |
140 | # AxoCover is a Code Coverage Tool
141 | .axoCover/*
142 | !.axoCover/settings.json
143 |
144 | # Coverlet is a free, cross platform Code Coverage Tool
145 | coverage*.json
146 | coverage*.xml
147 | coverage*.info
148 |
149 | # Visual Studio code coverage results
150 | *.coverage
151 | *.coveragexml
152 |
153 | # NCrunch
154 | _NCrunch_*
155 | .*crunch*.local.xml
156 | nCrunchTemp_*
157 |
158 | # MightyMoose
159 | *.mm.*
160 | AutoTest.Net/
161 |
162 | # Web workbench (sass)
163 | .sass-cache/
164 |
165 | # Installshield output folder
166 | [Ee]xpress/
167 |
168 | # DocProject is a documentation generator add-in
169 | DocProject/buildhelp/
170 | DocProject/Help/*.HxT
171 | DocProject/Help/*.HxC
172 | DocProject/Help/*.hhc
173 | DocProject/Help/*.hhk
174 | DocProject/Help/*.hhp
175 | DocProject/Help/Html2
176 | DocProject/Help/html
177 |
178 | # Click-Once directory
179 | publish/
180 |
181 | # Publish Web Output
182 | *.[Pp]ublish.xml
183 | *.azurePubxml
184 | # Note: Comment the next line if you want to checkin your web deploy settings,
185 | # but database connection strings (with potential passwords) will be unencrypted
186 | *.pubxml
187 | *.publishproj
188 |
189 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
190 | # checkin your Azure Web App publish settings, but sensitive information contained
191 | # in these scripts will be unencrypted
192 | PublishScripts/
193 |
194 | # NuGet Packages
195 | *.nupkg
196 | # NuGet Symbol Packages
197 | *.snupkg
198 | # The packages folder can be ignored because of Package Restore
199 | **/[Pp]ackages/*
200 | # except build/, which is used as an MSBuild target.
201 | !**/[Pp]ackages/build/
202 | # Uncomment if necessary however generally it will be regenerated when needed
203 | #!**/[Pp]ackages/repositories.config
204 | # NuGet v3's project.json files produces more ignorable files
205 | *.nuget.props
206 | *.nuget.targets
207 |
208 | # Microsoft Azure Build Output
209 | csx/
210 | *.build.csdef
211 |
212 | # Microsoft Azure Emulator
213 | ecf/
214 | rcf/
215 |
216 | # Windows Store app package directories and files
217 | AppPackages/
218 | BundleArtifacts/
219 | Package.StoreAssociation.xml
220 | _pkginfo.txt
221 | *.appx
222 | *.appxbundle
223 | *.appxupload
224 |
225 | # Visual Studio cache files
226 | # files ending in .cache can be ignored
227 | *.[Cc]ache
228 | # but keep track of directories ending in .cache
229 | !?*.[Cc]ache/
230 |
231 | # Others
232 | ClientBin/
233 | ~$*
234 | *~
235 | *.dbmdl
236 | *.dbproj.schemaview
237 | *.jfm
238 | *.pfx
239 | *.publishsettings
240 | orleans.codegen.cs
241 |
242 | # Including strong name files can present a security risk
243 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
244 | #*.snk
245 |
246 | # Since there are multiple workflows, uncomment next line to ignore bower_components
247 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
248 | #bower_components/
249 |
250 | # RIA/Silverlight projects
251 | Generated_Code/
252 |
253 | # Backup & report files from converting an old project file
254 | # to a newer Visual Studio version. Backup files are not needed,
255 | # because we have git ;-)
256 | _UpgradeReport_Files/
257 | Backup*/
258 | UpgradeLog*.XML
259 | UpgradeLog*.htm
260 | ServiceFabricBackup/
261 | *.rptproj.bak
262 |
263 | # SQL Server files
264 | *.mdf
265 | *.ldf
266 | *.ndf
267 |
268 | # Business Intelligence projects
269 | *.rdl.data
270 | *.bim.layout
271 | *.bim_*.settings
272 | *.rptproj.rsuser
273 | *- [Bb]ackup.rdl
274 | *- [Bb]ackup ([0-9]).rdl
275 | *- [Bb]ackup ([0-9][0-9]).rdl
276 |
277 | # Microsoft Fakes
278 | FakesAssemblies/
279 |
280 | # GhostDoc plugin setting file
281 | *.GhostDoc.xml
282 |
283 | # Node.js Tools for Visual Studio
284 | .ntvs_analysis.dat
285 | node_modules/
286 |
287 | # Visual Studio 6 build log
288 | *.plg
289 |
290 | # Visual Studio 6 workspace options file
291 | *.opt
292 |
293 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
294 | *.vbw
295 |
296 | # Visual Studio LightSwitch build output
297 | **/*.HTMLClient/GeneratedArtifacts
298 | **/*.DesktopClient/GeneratedArtifacts
299 | **/*.DesktopClient/ModelManifest.xml
300 | **/*.Server/GeneratedArtifacts
301 | **/*.Server/ModelManifest.xml
302 | _Pvt_Extensions
303 |
304 | # Paket dependency manager
305 | .paket/paket.exe
306 | paket-files/
307 |
308 | # FAKE - F# Make
309 | .fake/
310 |
311 | # CodeRush personal settings
312 | .cr/personal
313 |
314 | # Python Tools for Visual Studio (PTVS)
315 | __pycache__/
316 | *.pyc
317 |
318 | # Cake - Uncomment if you are using it
319 | # tools/**
320 | # !tools/packages.config
321 |
322 | # Tabs Studio
323 | *.tss
324 |
325 | # Telerik's JustMock configuration file
326 | *.jmconfig
327 |
328 | # BizTalk build output
329 | *.btp.cs
330 | *.btm.cs
331 | *.odx.cs
332 | *.xsd.cs
333 |
334 | # OpenCover UI analysis results
335 | OpenCover/
336 |
337 | # Azure Stream Analytics local run output
338 | ASALocalRun/
339 |
340 | # MSBuild Binary and Structured Log
341 | *.binlog
342 |
343 | # NVidia Nsight GPU debugger configuration file
344 | *.nvuser
345 |
346 | # MFractors (Xamarin productivity tool) working folder
347 | .mfractor/
348 |
349 | # Local History for Visual Studio
350 | .localhistory/
351 |
352 | # BeatPulse healthcheck temp database
353 | healthchecksdb
354 |
355 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
356 | MigrationBackup/
357 |
358 | # Ionide (cross platform F# VS Code tools) working folder
359 | .ionide/
360 |
361 | # Fody - auto-generated XML schema
362 | FodyWeavers.xsd
363 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 VocaDB Devgroup
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ResXFileCodeGenerator
2 | ResX Designer Source Generator. Generates strongly-typed resource classes for looking up localized strings.
3 |
4 | ## Usage
5 |
6 | Install the `VocaDb.ResXFileCodeGenerator` package:
7 |
8 | ```psl
9 | dotnet add package VocaDb.ResXFileCodeGenerator
10 | ```
11 |
12 | Generated source from [ActivityEntrySortRuleNames.resx](https://github.com/VocaDB/vocadb/blob/6ac18dd2981f82100c8c99566537e4916920219e/VocaDbWeb.Resources/App_GlobalResources/ActivityEntrySortRuleNames.resx):
13 |
14 | ```cs
15 | // ------------------------------------------------------------------------------
16 | //
17 | // This code was generated by a tool.
18 | //
19 | // Changes to this file may cause incorrect behavior and will be lost if
20 | // the code is regenerated.
21 | //
22 | // ------------------------------------------------------------------------------
23 | #nullable enable
24 | namespace Resources
25 | {
26 | using System.Globalization;
27 | using System.Resources;
28 |
29 | public static class ActivityEntrySortRuleNames
30 | {
31 | private static ResourceManager? s_resourceManager;
32 | public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames", typeof(ActivityEntrySortRuleNames).Assembly);
33 | public static CultureInfo? CultureInfo { get; set; }
34 |
35 | ///
36 | /// Looks up a localized string similar to Oldest.
37 | ///
38 | public static string? CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo);
39 |
40 | ///
41 | /// Looks up a localized string similar to Newest.
42 | ///
43 | public static string? CreateDateDescending => ResourceManager.GetString(nameof(CreateDateDescending), CultureInfo);
44 | }
45 | }
46 | ```
47 |
48 | ## New in version 3
49 |
50 | * The generator now utilizes the IIncrementalGenerator API to instantly update the generated code, thus giving you instant intellisense.
51 |
52 | * Added error handling for multiple members of same name, and members that have same name as class. These are clickable in visual studio to lead you to the source of the error, unlike before where they resulted in broken builds and you had to figure out why.
53 |
54 | * Namespace naming fixed for resx files in the top level folder.
55 |
56 | * Resx files can now be named with multiple extensions, e.g. myresources.cshtml.resx and will result in class being called myresources.
57 |
58 | * Added the ability to generate inner classes, partial outer classes and non-static members. Very useful if you want to ensure that only a particular class can use those resources instead of being spread around the codebase.
59 |
60 | * Use same 'Link' setting as msbuild uses to determine embedded file name.
61 |
62 | * Can set a class postfix name
63 |
64 | ## New in version 3.1
65 |
66 | * The generator can now generate code to lookup translations instead of using the 20 year old System.Resources.ResourceManager
67 |
68 | ## Options
69 |
70 | ### PublicClass (per file or globally)
71 |
72 | Use cases: https://github.com/VocaDB/ResXFileCodeGenerator/issues/2.
73 |
74 | Since version 2.0.0, VocaDB.ResXFileCodeGenerator generates internal classes by default. You can change this behavior by setting `PublicClass` to `true`.
75 |
76 | ```xml
77 |
78 |
79 | true
80 |
81 |
82 | ```
83 | or
84 | ```xml
85 |
86 |
87 |
88 | ```
89 |
90 | If you want to apply this globally, use
91 | ```xml
92 |
93 | true
94 |
95 | ```
96 |
97 | ### NullForgivingOperators (globally)
98 |
99 | Use cases: https://github.com/VocaDB/ResXFileCodeGenerator/issues/1.
100 |
101 | ```xml
102 |
103 | true
104 |
105 | ```
106 |
107 | By setting `ResXFileCodeGenerator_NullForgivingOperators` to `true`, VocaDB.ResXFileCodeGenerator generates
108 | ```cs
109 | public static string CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo)!;
110 | ```
111 | instead of
112 | ```cs
113 | public static string? CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo);
114 | ```
115 |
116 | ### Non-static classes (per file or globally)
117 |
118 | To use generated resources with [Microsoft.Extensions.Localization](https://docs.microsoft.com/en-us/dotnet/core/extensions/localization) `IStringLocalizer` and resource manager, the resolved type cannot be a static class. You can disable default behaviour per file by setting the value to `false`.
119 |
120 | ```xml
121 |
122 |
123 | false
124 |
125 |
126 | ```
127 |
128 | or globally
129 |
130 | ```xml
131 |
132 | false
133 |
134 | ```
135 |
136 | With global non-static class you can also reset `StaticClass` per file by setting the value to anything but `false`.
137 |
138 | ### Partial classes (per file or globally)
139 |
140 | To extend an existing class, you can make your classes partial.
141 |
142 | ```xml
143 |
144 |
145 | true
146 |
147 |
148 | ```
149 |
150 | or globally
151 |
152 | ```xml
153 |
154 | true
155 |
156 | ```
157 |
158 | ### Static Members (per file or globally)
159 |
160 | In some rare cases it might be useful for the members to be non-static.
161 |
162 | ```xml
163 |
164 |
165 | false
166 |
167 |
168 | ```
169 |
170 | or globally
171 |
172 | ```xml
173 |
174 | false
175 |
176 | ```
177 |
178 | ### Postfix class name (per file or globally)
179 |
180 | In some cases the it is useful if the name of the generated class doesn't follow the filename.
181 |
182 | A clear example is Razor pages that always generates a class for the code-behind named "-Model".
183 | This example configuration allows you to use Resources.MyResource in your model, or @Model.Resources.MyResource in your cshtml file.
184 |
185 | ```xml
186 |
187 |
188 | Model
189 | false
190 | false
191 | true
192 | true
193 | public
194 | false
195 | Resources
196 | _Resources
197 |
198 |
199 | ```
200 |
201 |
202 | or just the postfix globally
203 |
204 | ```xml
205 |
206 | Model
207 |
208 | ```
209 |
210 | ## Inner classes (per file or globally)
211 |
212 | If your resx files are organized along with code files, it can be quite useful to ensure that the resources are not accessible outside the specific class the resx file belong to.
213 |
214 | ```xml
215 |
216 |
217 | $([System.String]::Copy('%(FileName).cs'))
218 | MyResources
219 | private
220 | EveryoneLikeMyNaming
221 | false
222 | false
223 | true
224 |
225 |
226 | $([System.IO.Path]::GetFileNameWithoutExtension('%(FileName)')).resx
227 |
228 |
229 | ```
230 |
231 | or globally
232 |
233 | ```xml
234 |
235 | MyResources
236 | private
237 | EveryoneLikeMyNaming
238 | false
239 | false
240 | true
241 |
242 | ```
243 |
244 | This example would generate files like this:
245 |
246 | ```cs
247 | // ------------------------------------------------------------------------------
248 | //
249 | // This code was generated by a tool.
250 | //
251 | // Changes to this file may cause incorrect behavior and will be lost if
252 | // the code is regenerated.
253 | //
254 | // ------------------------------------------------------------------------------
255 | #nullable enable
256 | namespace Resources
257 | {
258 | using System.Globalization;
259 | using System.Resources;
260 |
261 | public partial class ActivityEntryModel
262 | {
263 | public MyResources EveryoneLikeMyNaming { get; } = new();
264 |
265 | private class MyResources
266 | {
267 | private static ResourceManager? s_resourceManager;
268 | public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager("VocaDb.Web.App_GlobalResources.ActivityEntryModel", typeof(ActivityEntryModel).Assembly);
269 | public CultureInfo? CultureInfo { get; set; }
270 |
271 | ///
272 | /// Looks up a localized string similar to Oldest.
273 | ///
274 | public string? CreateDate => ResourceManager.GetString(nameof(CreateDate), CultureInfo);
275 |
276 | ///
277 | /// Looks up a localized string similar to Newest.
278 | ///
279 | public string? CreateDateDescending => ResourceManager.GetString(nameof(CreateDateDescending), CultureInfo);
280 | }
281 | }
282 | }
283 | ```
284 |
285 | ### Inner Class Visibility (per file or globally)
286 |
287 | By default inner classes are not generated, unless this setting is one of the following:
288 |
289 | * Public
290 | * Internal
291 | * Private
292 | * Protected
293 | * SameAsOuter
294 |
295 | Case is ignored, so you could use "private".
296 |
297 | It is also possible to use "NotGenerated" to override on a file if the global setting is to generate inner classes.
298 |
299 | ```xml
300 |
301 |
302 | private
303 |
304 |
305 | ```
306 |
307 | or globally
308 |
309 | ```xml
310 |
311 | private
312 |
313 | ```
314 |
315 | ### Inner Class name (per file or globally)
316 |
317 | By default the inner class is named "Resources", which can be overriden with this setting:
318 |
319 | ```xml
320 |
321 |
322 | MyResources
323 |
324 |
325 | ```
326 |
327 | or globally
328 |
329 | ```xml
330 |
331 | MyResources
332 |
333 | ```
334 |
335 |
336 | ### Inner Class instance name (per file or globally)
337 |
338 | By default no instance is available of the class, but that can be made available if this setting is given.
339 |
340 | ```xml
341 |
342 |
343 | EveryoneLikeMyNaming
344 |
345 |
346 | ```
347 |
348 | or globally
349 |
350 | ```xml
351 |
352 | EveryoneLikeMyNaming
353 |
354 | ```
355 |
356 | For brevity, settings to make everything non-static is obmitted.
357 |
358 | ### Generate Code (per file or globally)
359 |
360 | By default the ancient `System.Resources.ResourceManager` is used.
361 |
362 | Benefits of using `System.Resources.ResourceManager`:
363 |
364 | * Supports custom `CultureInfo`
365 | * Languages are only loaded the first time a language is referenced
366 | * Only use memory for the languages used
367 | * Can ship satellite dlls seperately
368 |
369 | Disadvantages of using `System.Resources.ResourceManager`
370 |
371 | * The satellite dlls are always lazy loaded, so cold start penalty is high
372 | * Satellite dlls requires that you can scan the dir for which files are available, which can cause issues in some project types
373 | * Loading a satellite dll takes way more memory than just loading the respective strings
374 | * Build time for .resources -> satellite dll can be quite slow (~150msec per file)
375 | * Linker optimization doesn't work, since it cannot know which resources are referenced
376 |
377 | Benefits of using `VocaDB` code generation:
378 |
379 | * All languages are placed in the main dll, no more satellite dlls
380 | * Lookup speed is ~600% faster (5ns vs 33ns)
381 | * Zero allocations
382 | * Very small code footprint (about 10 bytes per language, instead of including the entire `System.Resources`)
383 | * Very fast build time
384 | * Because all code is referencing the strings directly, the linker can see which strings are actually used and which are not.
385 | * No cold start penalty
386 | * Smaller combined size of dll (up to 50%, since it doesn't need to store the keys for every single language)
387 |
388 | Disadvantages of using `VocaDB` code generation
389 |
390 | * Since `CultureInfo` are pre-computed, custom `CultureInfo` are not supported (or rather, they always return the default language)
391 | * Cannot lookup "all" keys (unless using reflection)
392 | * Main dll size increased since it contains all language strings (sometimes, the compiler can pack code strings much better than resource strings and it doesn't need to store the keys)
393 |
394 | Notice, it is required to set `GenerateResource` to false for all resx files to prevent the built-in resgen.exe from running.
395 |
396 | ```xml
397 |
398 |
399 | true
400 | false
401 |
402 |
403 | ```
404 |
405 | or globally
406 |
407 | ```xml
408 |
409 | true
410 |
411 |
412 |
413 | false
414 |
415 |
416 | ```
417 |
418 | If you get build error MSB3030, add this to your csproj to prevent it from trying to copy satellite dlls that no longer exists
419 |
420 | ```xml
421 |
422 |
423 |
424 |
425 |
426 | ```
427 |
428 |
429 | ## Resource file namespaces
430 |
431 | Linked resources namespace follow `Link` if it is set. The `Link` setting is also used by msbuild built-in 'resgen.exe' to determine the embedded filename.
432 |
433 | Use-case: Linking `.resx` files from outside source (e.g. generated in a localization sub-module by translators) and expose them as "Resources" namespace.
434 |
435 | ```xml
436 |
437 |
438 | Resources\%(FileName)%(Extension)
439 | true
440 | false
441 |
442 |
443 | $([System.IO.Path]::GetFilenameWithoutExtension([System.String]::Copy('%(FileName)'))).resx
444 |
445 |
446 | ```
447 |
448 | You can also use the `TargetPath` to just overwrite the namespace
449 |
450 | ```xml
451 |
452 |
453 | Resources\%(FileName)%(Extension)
454 | true
455 | false
456 |
457 |
458 | $([System.IO.Path]::GetFilenameWithoutExtension([System.String]::Copy('%(FileName)'))).resx
459 |
460 |
461 | ```
462 |
463 | It is also possible to set the namespace using the `CustomToolNamespace` setting. Unlike the `Link` and `TargetPath`, which will prepend the assemblys namespace and includes the filename, the `CustomToolNamespace` is taken verbatim.
464 |
465 | ```xml
466 |
467 |
468 | MyNamespace.AllMyResourcesAreBelongToYouNamespace
469 |
470 |
471 | ```
472 |
473 | ## References
474 | - [Introducing C# Source Generators | .NET Blog](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/)
475 | - [microsoft/CsWin32: A source generator to add a user-defined set of Win32 P/Invoke methods and supporting types to a C# project.](https://github.com/microsoft/cswin32)
476 | - [kenkendk/mdresxfilecodegenerator: Resx Designer Generator](https://github.com/kenkendk/mdresxfilecodegenerator)
477 | - [dotnet/ResXResourceManager: Manage localization of all ResX-Based resources in one central place.](https://github.com/dotnet/ResXResourceManager)
478 | - [roslyn/source-generators.cookbook.md at master · dotnet/roslyn](https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.cookbook.md)
479 | - [roslyn/Using Additional Files.md at master · dotnet/roslyn](https://github.com/dotnet/roslyn/blob/master/docs/analyzers/Using%20Additional%20Files.md)
480 | - [ufcpp - YouTube](https://www.youtube.com/channel/UCY-z_9mau6X-Vr4gk2aWtMQ)
481 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/AdditionalTextStub.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | using Microsoft.CodeAnalysis.Text;
3 |
4 | namespace VocaDb.ResXFileCodeGenerator.Tests;
5 |
6 | internal class AdditionalTextStub : AdditionalText
7 | {
8 | private readonly SourceText? _text;
9 |
10 | public override string Path { get; }
11 |
12 | public AdditionalTextStub(string path, string? text = null)
13 | {
14 | _text = text is null ? null : SourceText.From(text);
15 | Path = path;
16 | }
17 |
18 | public override SourceText? GetText(CancellationToken cancellationToken = new()) => _text;
19 | }
20 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/CodeGenTests.cs:
--------------------------------------------------------------------------------
1 | using FluentAssertions;
2 | using Xunit;
3 | using static System.Guid;
4 |
5 | namespace VocaDb.ResXFileCodeGenerator.Tests;
6 |
7 | public class CodeGenTests
8 | {
9 | private const string Text = @"
10 |
11 |
12 |
13 |
14 |
15 |
16 |
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 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | text/microsoft-resx
59 |
60 |
61 | 2.0
62 |
63 |
64 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
65 |
66 |
67 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
68 |
69 |
70 | Oldest
71 |
72 |
73 | Newest
74 |
75 | ";
76 |
77 | private const string TextDa = @"
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | text/microsoft-resx
127 |
128 |
129 | 2.0
130 |
131 |
132 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
133 |
134 |
135 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
136 |
137 |
138 | OldestDa
139 |
140 |
141 | NewestDa
142 |
143 | ";
144 |
145 | private const string TextDaDk = @"
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 | text/microsoft-resx
195 |
196 |
197 | 2.0
198 |
199 |
200 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
201 |
202 |
203 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
204 |
205 |
206 | OldestDaDK
207 |
208 |
209 | NewestDaDK
210 |
211 | ";
212 | private static void Generate(
213 | IGenerator generator,
214 | bool publicClass = true,
215 | bool staticClass = true,
216 | bool partial = false,
217 | bool nullForgivingOperators = false,
218 | bool staticMembers = true
219 | )
220 | {
221 | var expected = $@"// ------------------------------------------------------------------------------
222 | //
223 | // This code was generated by a tool.
224 | //
225 | // Changes to this file may cause incorrect behavior and will be lost if
226 | // the code is regenerated.
227 | //
228 | // ------------------------------------------------------------------------------
229 | #nullable enable
230 | namespace Resources;
231 | using static VocaDb.ResXFileCodeGenerator.Helpers;
232 |
233 | {(publicClass ? "public" : "internal")}{(partial ? " partial" : string.Empty)}{(staticClass ? " static" : string.Empty)} class ActivityEntrySortRuleNames
234 | {{
235 |
236 | ///
237 | /// Looks up a localized string similar to Oldest.
238 | ///
239 | public{(staticMembers ? " static" : string.Empty)} string{(nullForgivingOperators ? string.Empty : "?")} CreateDate => GetString_1030_6(""Oldest"", ""OldestDaDK"", ""OldestDa"");
240 |
241 | ///
242 | /// Looks up a localized string similar to Newest.
243 | ///
244 | public{(staticMembers ? " static" : string.Empty)} string{(nullForgivingOperators ? string.Empty : "?")} CreateDateDescending => GetString_1030_6(""Newest"", ""NewestDaDK"", ""NewestDa"");
245 | }}
246 | ";
247 | var (_, SourceCode, ErrorsAndWarnings) = generator.Generate(
248 | options: new FileOptions()
249 | {
250 | LocalNamespace = "VocaDb.Web.App_GlobalResources",
251 | EmbeddedFilename = "VocaDb.Web.App_GlobalResources.ActivityEntrySortRuleNames",
252 | CustomToolNamespace = "Resources",
253 | ClassName = "ActivityEntrySortRuleNames",
254 | GroupedFile = new GroupedAdditionalFile(
255 | mainFile: new AdditionalTextWithHash(new AdditionalTextStub(string.Empty, Text), NewGuid()),
256 | subFiles: new[]
257 | {
258 | new AdditionalTextWithHash(new AdditionalTextStub("test.da.rex", TextDa), NewGuid()),
259 | new AdditionalTextWithHash(new AdditionalTextStub("test.da-dk.rex", TextDaDk), NewGuid()),
260 | }
261 | ),
262 | PublicClass = publicClass,
263 | UseVocaDbResManager = true,
264 | NullForgivingOperators = nullForgivingOperators,
265 | StaticClass = staticClass,
266 | PartialClass = partial,
267 | StaticMembers = staticMembers
268 | }
269 | );
270 | ErrorsAndWarnings.Should().BeNullOrEmpty();
271 | SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings());
272 | }
273 |
274 |
275 | [Fact]
276 | public void Generate_StringBuilder_Public()
277 | {
278 | var generator = new StringBuilderGenerator();
279 | Generate(generator);
280 | Generate(generator, true, nullForgivingOperators: true);
281 | }
282 | }
283 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/GithubIssues/Issue3/GeneratorTests.cs:
--------------------------------------------------------------------------------
1 | using System.Xml;
2 | using FluentAssertions;
3 | using Xunit;
4 | using static System.Guid;
5 |
6 | namespace VocaDb.ResXFileCodeGenerator.Tests.GithubIssues.Issue3;
7 |
8 | public class GeneratorTests
9 | {
10 | [Fact]
11 | public void Generate_StringBuilder_Name_NotValidIdentifier()
12 | {
13 | var text = @"
14 |
15 |
16 |
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 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | text/microsoft-resx
63 |
64 |
65 | 2.0
66 |
67 |
68 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
69 |
70 |
71 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
72 |
73 |
74 | String '{0}' is not a valid identifier.
75 |
76 | ";
77 |
78 | var expected = @"// ------------------------------------------------------------------------------
79 | //
80 | // This code was generated by a tool.
81 | //
82 | // Changes to this file may cause incorrect behavior and will be lost if
83 | // the code is regenerated.
84 | //
85 | // ------------------------------------------------------------------------------
86 | #nullable enable
87 | namespace VocaDb.Web.App_GlobalResources;
88 | using System.Globalization;
89 | using System.Resources;
90 |
91 | public static class CommonMessages
92 | {
93 | private static ResourceManager? s_resourceManager;
94 | public static ResourceManager ResourceManager => s_resourceManager ??= new ResourceManager(""VocaDb.Web.App_GlobalResources.CommonMessages"", typeof(CommonMessages).Assembly);
95 | public static CultureInfo? CultureInfo { get; set; }
96 |
97 | ///
98 | /// Looks up a localized string similar to String '{0}' is not a valid identifier..
99 | ///
100 | public static string? Invalid_identifier__0_ => ResourceManager.GetString(""Invalid identifier {0}"", CultureInfo);
101 | }
102 | ";
103 | var generator = new StringBuilderGenerator();
104 | var source = generator.Generate(
105 | new FileOptions()
106 | {
107 | LocalNamespace = "VocaDb.Web.App_GlobalResources",
108 | EmbeddedFilename = "VocaDb.Web.App_GlobalResources.CommonMessages",
109 | CustomToolNamespace = null,
110 | GroupedFile = new GroupedAdditionalFile(
111 | mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()),
112 | subFiles: Array.Empty()
113 | ),
114 | ClassName = "CommonMessages",
115 | PublicClass = true,
116 | NullForgivingOperators = false,
117 | StaticClass = true,
118 | StaticMembers = true
119 | }
120 | );
121 | source.ErrorsAndWarnings.Should().BeNullOrEmpty();
122 | source.SourceCode.ReplaceLineEndings().Should().Be(expected.ReplaceLineEndings());
123 | }
124 |
125 | [Fact]
126 | public void Generate_StringBuilder_Value_InvalidCharacter()
127 | {
128 | var text = $@"
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 | text/microsoft-resx
178 |
179 |
180 | 2.0
181 |
182 |
183 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
184 |
185 |
186 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
187 |
188 |
189 | Old{"\0"}est
190 |
191 |
192 | Newest
193 |
194 | ";
195 | var generator = new StringBuilderGenerator();
196 | var options = new FileOptions
197 | {
198 | LocalNamespace = "VocaDb.Web.App_GlobalResources",
199 | CustomToolNamespace = "Resources",
200 | ClassName = "ActivityEntrySortRuleNames",
201 | GroupedFile = new GroupedAdditionalFile(
202 | mainFile: new AdditionalTextWithHash(new AdditionalTextStub("", text), NewGuid()),
203 | subFiles: Array.Empty()
204 | ),
205 | PublicClass = true,
206 | NullForgivingOperators = false,
207 | StaticClass = true
208 | };
209 | generator.Invoking(subject => subject.Generate(options)).Should().Throw();
210 | }
211 | }
212 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/GroupResxFilesTests.cs:
--------------------------------------------------------------------------------
1 | using FluentAssertions;
2 | using Xunit;
3 | using static System.Guid;
4 |
5 | namespace VocaDb.ResXFileCodeGenerator.Tests;
6 |
7 | public class GroupResxFilesTests
8 | {
9 | [Fact]
10 | public void CompareGroupedAdditionalFile_SameRoot_SameSubFiles_DifferentOrder()
11 | {
12 | var v1 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub(
13 | @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")),
14 | new[]
15 | {
16 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")),
17 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")),
18 | });
19 |
20 | var v2 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub(
21 | @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")),
22 | new[]
23 | {
24 |
25 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")),
26 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")),
27 | }
28 | );
29 | v1.Should().Be(v2);
30 | }
31 |
32 | [Fact]
33 | public void CompareGroupedAdditionalFile_SameRoot_DiffSubFilesNames()
34 | {
35 | var v1 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub(
36 | @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")),
37 | new[]
38 | {
39 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.en.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")),
40 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.fr.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")),
41 | });
42 |
43 | var v2 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub(
44 | @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")),
45 | new[]
46 | {
47 |
48 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.de.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")),
49 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.ro.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")),
50 | }
51 | );
52 | v1.Should().NotBe(v2);
53 | }
54 |
55 | [Fact]
56 | public void CompareGroupedAdditionalFile_SameRoot_DiffSubFileContent()
57 | {
58 | var v1 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub(
59 | @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")),
60 | new[]
61 | {
62 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("771F9C76-D9F4-4AF4-95D2-B3426F9EC15A")),
63 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")),
64 | });
65 |
66 | var v2 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub(
67 | @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")),
68 | new[]
69 | {
70 |
71 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")),
72 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")),
73 | }
74 | );
75 | v1.Should().NotBe(v2);
76 | }
77 |
78 | [Fact]
79 | public void CompareGroupedAdditionalFile_DiffRootContent_SameSubFiles()
80 | {
81 | var v1 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub(
82 | @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("47FFD75C-3254-4851-8E1C-CBDDCDCE1D9B")),
83 | new[]
84 | {
85 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")),
86 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")),
87 | });
88 |
89 | var v2 = new GroupedAdditionalFile(new AdditionalTextWithHash(new AdditionalTextStub(
90 | @"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("A7E92264-8047-4668-979F-6EFC14EBAFC5")),
91 | new[]
92 | {
93 |
94 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("5B2BA95C-FB9C-47C5-9C03-280B63D8DD27")),
95 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("B7EDA261-6923-4526-AFB7-B2A64984F099")),
96 | }
97 | );
98 | v1.Should().NotBe(v2);
99 | }
100 |
101 | static readonly (string Path, Guid Hash)[] s_data =
102 | {
103 | (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx", Parse("00000000-0000-0000-0000-000000000001")),
104 | (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx", Parse("00000000-0000-0000-0000-000000000002")),
105 | (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx", Parse("00000000-0000-0000-0000-000000000003")),
106 | (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.da.resx", Parse("00000000-0000-0000-0000-000000000004")),
107 | (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.resx", Parse("00000000-0000-0000-0000-000000000005")),
108 | (@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.vi.resx", Parse("00000000-0000-0000-0000-000000000006")),
109 | (@"D:\src\xhg\y\Areas\Identity\Pages\Login.da.resx", Parse("00000000-0000-0000-0000-000000000007")),
110 | (@"D:\src\xhg\y\Areas\Identity\Pages\Login.resx", Parse("00000000-0000-0000-0000-000000000008")),
111 | (@"D:\src\xhg\y\Areas\Identity\Pages\Login.vi.resx", Parse("00000000-0000-0000-0000-000000000009")),
112 | (@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.da.resx", Parse("00000000-0000-0000-0000-000000000010")),
113 | (@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.resx", Parse("00000000-0000-0000-0000-000000000011")),
114 | (@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.vi.resx", Parse("00000000-0000-0000-0000-000000000012")),
115 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.cs-cz.resx", Parse("00000000-0000-0000-0000-000000000013")),
116 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.da.resx", Parse("00000000-0000-0000-0000-000000000014")),
117 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.de.resx", Parse("00000000-0000-0000-0000-000000000015")),
118 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.es.resx", Parse("00000000-0000-0000-0000-000000000016")),
119 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.fi.resx", Parse("00000000-0000-0000-0000-000000000017")),
120 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.fr.resx", Parse("00000000-0000-0000-0000-000000000018")),
121 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.it.resx", Parse("00000000-0000-0000-0000-000000000019")),
122 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.lt.resx", Parse("00000000-0000-0000-0000-000000000020")),
123 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.lv.resx", Parse("00000000-0000-0000-0000-000000000021")),
124 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.nb-no.resx", Parse("00000000-0000-0000-0000-000000000022")),
125 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.nl.resx", Parse("00000000-0000-0000-0000-000000000023")),
126 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.nn-no.resx", Parse("00000000-0000-0000-0000-000000000024")),
127 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.pl.resx", Parse("00000000-0000-0000-0000-000000000025")),
128 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.resx", Parse("00000000-0000-0000-0000-000000000026")),
129 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.ru.resx", Parse("00000000-0000-0000-0000-000000000027")),
130 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.sv.resx", Parse("00000000-0000-0000-0000-000000000028")),
131 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.tr.resx", Parse("00000000-0000-0000-0000-000000000029")),
132 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.vi.resx", Parse("00000000-0000-0000-0000-000000000030")),
133 | (@"D:\src\xhg\y\Areas\QxModule\QtrController.zh-cn.resx", Parse("00000000-0000-0000-0000-000000000031")),
134 | (@"D:\src\xhg\y\DataAnnotations\DataAnnotation.da.resx", Parse("00000000-0000-0000-0000-000000000032")),
135 | (@"D:\src\xhg\y\DataAnnotations\DataAnnotation.resx", Parse("00000000-0000-0000-0000-000000000033")),
136 | (@"D:\src\xhg\y\DataAnnotations\DataAnnotation2.resx", Parse("00000000-0000-0000-0000-000000000034")),
137 | };
138 |
139 | [Fact]
140 | public void FileGrouping()
141 | {
142 | var result = GroupResxFiles.Group(s_data.Select(x => new AdditionalTextWithHash(new AdditionalTextStub(x.Path), x.Hash)).OrderBy(x => NewGuid()).ToArray());
143 |
144 | var testData = new List
145 | {
146 | new GroupedAdditionalFile(
147 | mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.resx"), Parse("00000000-0000-0000-0000-000000000002")),
148 | subFiles: new[]
149 | {
150 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.da.resx"), Parse("00000000-0000-0000-0000-000000000001")),
151 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgControlCenter.vi.resx"), Parse("00000000-0000-0000-0000-000000000003")),
152 | }
153 | ),
154 | new GroupedAdditionalFile(
155 | mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.resx"), Parse("00000000-0000-0000-0000-000000000005")),
156 | subFiles: new[]
157 | {
158 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.da.resx"), Parse("00000000-0000-0000-0000-000000000004")),
159 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\CaModule\Pages\IdfgLive.vi.resx"), Parse("00000000-0000-0000-0000-000000000006")),
160 | }
161 | ),
162 | new GroupedAdditionalFile(
163 | mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\Identity\Pages\Login.resx"), Parse("00000000-0000-0000-0000-000000000008")),
164 | subFiles: new[]
165 | {
166 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\Identity\Pages\Login.da.resx"), Parse("00000000-0000-0000-0000-000000000007")),
167 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\Identity\Pages\Login.vi.resx"), Parse("00000000-0000-0000-0000-000000000009")),
168 | }
169 | ),
170 | new GroupedAdditionalFile(
171 | mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.resx"), Parse("00000000-0000-0000-0000-000000000011")),
172 | subFiles: new[]
173 | {
174 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.da.resx"), Parse("00000000-0000-0000-0000-000000000010")),
175 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\Pages\QasdLogon.vi.resx"), Parse("00000000-0000-0000-0000-000000000012")),
176 | }
177 | ),
178 | new GroupedAdditionalFile(
179 | mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.resx"), Parse("00000000-0000-0000-0000-000000000026")),
180 | subFiles: new[]
181 | {
182 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.cs-cz.resx"), Parse("00000000-0000-0000-0000-000000000013")),
183 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.da.resx"), Parse("00000000-0000-0000-0000-000000000014")),
184 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.de.resx"), Parse("00000000-0000-0000-0000-000000000015")),
185 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.es.resx"), Parse("00000000-0000-0000-0000-000000000016")),
186 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.fi.resx"), Parse("00000000-0000-0000-0000-000000000017")),
187 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.fr.resx"), Parse("00000000-0000-0000-0000-000000000018")),
188 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.it.resx"), Parse("00000000-0000-0000-0000-000000000019")),
189 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.lt.resx"), Parse("00000000-0000-0000-0000-000000000020")),
190 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.lv.resx"), Parse("00000000-0000-0000-0000-000000000021")),
191 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.nb-no.resx"), Parse("00000000-0000-0000-0000-000000000022")),
192 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.nl.resx"), Parse("00000000-0000-0000-0000-000000000023")),
193 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.nn-no.resx"), Parse("00000000-0000-0000-0000-000000000024")),
194 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.pl.resx"), Parse("00000000-0000-0000-0000-000000000025")),
195 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.ru.resx"), Parse("00000000-0000-0000-0000-000000000027")),
196 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.sv.resx"), Parse("00000000-0000-0000-0000-000000000028")),
197 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.tr.resx"), Parse("00000000-0000-0000-0000-000000000029")),
198 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.vi.resx"), Parse("00000000-0000-0000-0000-000000000030")),
199 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\Areas\QxModule\QtrController.zh-cn.resx"), Parse("00000000-0000-0000-0000-000000000031")),
200 | }),
201 | new GroupedAdditionalFile(
202 | mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\DataAnnotations\DataAnnotation.resx"), Parse("00000000-0000-0000-0000-000000000033")),
203 | subFiles: new[]
204 | {
205 | new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\DataAnnotations\DataAnnotation.da.resx"), Parse("00000000-0000-0000-0000-000000000032"))
206 | }
207 | ),
208 | new GroupedAdditionalFile(
209 | mainFile: new AdditionalTextWithHash(new AdditionalTextStub(@"D:\src\xhg\y\DataAnnotations\DataAnnotation2.resx"), Parse("00000000-0000-0000-0000-000000000034")),
210 | subFiles: Array.Empty()
211 | )
212 | };
213 | var resAsList = result.ToList();
214 | resAsList.Count.Should().Be(testData.Count);
215 | foreach (var groupedAdditionalFile in testData)
216 | {
217 | resAsList.Should().Contain(groupedAdditionalFile);
218 | }
219 | }
220 |
221 | [Fact]
222 | public void ResxGrouping()
223 | {
224 | var result = GroupResxFiles.DetectChildCombos(GroupResxFiles.Group(s_data.Select(x => new AdditionalTextWithHash(new AdditionalTextStub(x.Path), NewGuid())).OrderBy(x => NewGuid()).ToArray()).ToArray()).ToList();
225 | var expected = new List
226 | {
227 | new CultureInfoCombo(new[]
228 | {
229 | new AdditionalTextWithHash(new AdditionalTextStub("test.da.resx"), NewGuid()),
230 | new AdditionalTextWithHash(new AdditionalTextStub("test.vi.resx"), NewGuid())
231 | }),
232 | new CultureInfoCombo(new[]{ new AdditionalTextWithHash(new AdditionalTextStub("test.da.resx"), NewGuid())}),
233 | new CultureInfoCombo(Array.Empty()),
234 | new CultureInfoCombo(new[]
235 | {
236 | new AdditionalTextWithHash(new AdditionalTextStub("test.cs-cz.resx"), NewGuid()),
237 | new AdditionalTextWithHash(new AdditionalTextStub("test.da.resx"), NewGuid()),
238 | new AdditionalTextWithHash(new AdditionalTextStub("test.de.resx"), NewGuid()),
239 | new AdditionalTextWithHash(new AdditionalTextStub("test.es.resx"), NewGuid()),
240 | new AdditionalTextWithHash(new AdditionalTextStub("test.fi.resx"), NewGuid()),
241 | new AdditionalTextWithHash(new AdditionalTextStub("test.fr.resx"), NewGuid()),
242 | new AdditionalTextWithHash(new AdditionalTextStub("test.it.resx"), NewGuid()),
243 | new AdditionalTextWithHash(new AdditionalTextStub("test.lt.resx"), NewGuid()),
244 | new AdditionalTextWithHash(new AdditionalTextStub("test.lv.resx"), NewGuid()),
245 | new AdditionalTextWithHash(new AdditionalTextStub("test.nb-no.resx"), NewGuid()),
246 | new AdditionalTextWithHash(new AdditionalTextStub("test.nl.resx"), NewGuid()),
247 | new AdditionalTextWithHash(new AdditionalTextStub("test.nn-no.resx"), NewGuid()),
248 | new AdditionalTextWithHash(new AdditionalTextStub("test.pl.resx"), NewGuid()),
249 | new AdditionalTextWithHash(new AdditionalTextStub("test.ru.resx"), NewGuid()),
250 | new AdditionalTextWithHash(new AdditionalTextStub("test.sv.resx"), NewGuid()),
251 | new AdditionalTextWithHash(new AdditionalTextStub("test.tr.resx"), NewGuid()),
252 | new AdditionalTextWithHash(new AdditionalTextStub("test.vi.resx"), NewGuid()),
253 | new AdditionalTextWithHash(new AdditionalTextStub("test.zh-cn.resx"), NewGuid()),
254 | }),
255 | };
256 | result.Count.Should().Be(expected.Count);
257 | result.Should().BeEquivalentTo(expected);
258 | }
259 | }
260 |
261 |
262 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/HelperGeneratorTests.cs:
--------------------------------------------------------------------------------
1 | using FluentAssertions;
2 | using Xunit;
3 |
4 | namespace VocaDb.ResXFileCodeGenerator.Tests;
5 |
6 | public class HelperGeneratorTests
7 | {
8 | [Fact]
9 | public void CanGenerateCombo()
10 | {
11 | var (generatedFileName, sourceCode, errorsAndWarnings) = new StringBuilderGenerator()
12 | .Generate(
13 | combo: new CultureInfoCombo(
14 | files: new[]
15 | {
16 | new AdditionalTextWithHash(new AdditionalTextStub("test.da.rex"), Guid.NewGuid()),
17 | new AdditionalTextWithHash(new AdditionalTextStub("test.da-dk.rex"), Guid.NewGuid())
18 | }
19 | ),
20 | cancellationToken: default
21 | );
22 | var expected = @"// ------------------------------------------------------------------------------
23 | //
24 | // This code was generated by a tool.
25 | //
26 | // Changes to this file may cause incorrect behavior and will be lost if
27 | // the code is regenerated.
28 | //
29 | // ------------------------------------------------------------------------------
30 | #nullable enable
31 | namespace VocaDb.ResXFileCodeGenerator;
32 | internal static partial class Helpers
33 | {
34 | public static string GetString_1030_6(string fallback, string da_DK, string da) => System.Globalization.CultureInfo.CurrentUICulture.LCID switch
35 | {
36 | 1030 => da_DK,
37 | 6 => da,
38 | _ => fallback
39 | };
40 | }
41 | ";
42 | errorsAndWarnings.Should().BeNullOrEmpty();
43 | generatedFileName.Should().Be("VocaDb.ResXFileCodeGenerator.1030_6.g.cs");
44 | sourceCode.Should().Be(expected);
45 | }
46 | [Fact]
47 | public void CanGenerateEmptyCombo()
48 | {
49 | var (generatedFileName, sourceCode, errorsAndWarnings) = new StringBuilderGenerator().Generate(new CultureInfoCombo(), default);
50 | var expected = @"// ------------------------------------------------------------------------------
51 | //
52 | // This code was generated by a tool.
53 | //
54 | // Changes to this file may cause incorrect behavior and will be lost if
55 | // the code is regenerated.
56 | //
57 | // ------------------------------------------------------------------------------
58 | #nullable enable
59 | namespace VocaDb.ResXFileCodeGenerator;
60 | internal static partial class Helpers
61 | {
62 | public static string GetString_(string fallback) => System.Globalization.CultureInfo.CurrentUICulture.LCID switch
63 | {
64 | _ => fallback
65 | };
66 | }
67 | ";
68 | errorsAndWarnings.Should().BeNullOrEmpty();
69 | generatedFileName.Should().Be("VocaDb.ResXFileCodeGenerator..g.cs");
70 | sourceCode.Should().Be(expected);
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/IntegrationTests/Test1.da-dk.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | text/microsoft-resx
91 |
92 |
93 | 1.3
94 |
95 |
96 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
97 |
98 |
99 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
100 |
101 |
102 | OldestDaDK
103 |
104 |
105 | NewestDaDK
106 |
107 |
108 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/IntegrationTests/Test1.da.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | text/microsoft-resx
91 |
92 |
93 | 1.3
94 |
95 |
96 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
97 |
98 |
99 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
100 |
101 |
102 | OldestDa
103 |
104 |
105 | NewestDa
106 |
107 |
108 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/IntegrationTests/Test1.en-us.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | text/microsoft-resx
91 |
92 |
93 | 1.3
94 |
95 |
96 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
97 |
98 |
99 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
100 |
101 |
102 | OldestEnUs
103 |
104 |
105 | NewestEnUs
106 |
107 |
108 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/IntegrationTests/Test1.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | text/microsoft-resx
91 |
92 |
93 | 1.3
94 |
95 |
96 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
97 |
98 |
99 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
100 |
101 |
102 | Oldest
103 |
104 |
105 | Newest
106 |
107 |
108 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/IntegrationTests/Test2.da-dk.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | text/microsoft-resx
91 |
92 |
93 | 1.3
94 |
95 |
96 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
97 |
98 |
99 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
100 |
101 |
102 | OldestDaDK
103 |
104 |
105 | NewestDaDK
106 |
107 |
108 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/IntegrationTests/Test2.da.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | text/microsoft-resx
91 |
92 |
93 | 1.3
94 |
95 |
96 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
97 |
98 |
99 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
100 |
101 |
102 | OldestDa
103 |
104 |
105 | NewestDa
106 |
107 |
108 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/IntegrationTests/Test2.en-us.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | text/microsoft-resx
91 |
92 |
93 | 1.3
94 |
95 |
96 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
97 |
98 |
99 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
100 |
101 |
102 | OldestEnUs
103 |
104 |
105 | NewestEnUs
106 |
107 |
108 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/IntegrationTests/Test2.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | text/microsoft-resx
91 |
92 |
93 | 1.3
94 |
95 |
96 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
97 |
98 |
99 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.3500.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
100 |
101 |
102 | Oldest
103 |
104 |
105 | Newest
106 |
107 |
108 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/IntegrationTests/TestResxFiles.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using FluentAssertions;
3 | using Xunit;
4 |
5 | namespace VocaDb.ResXFileCodeGenerator.Tests.IntegrationTests;
6 |
7 | public class TestResxFiles
8 | {
9 | [Fact]
10 | public void TestNormalResourceGen()
11 | {
12 | Thread.CurrentThread.CurrentUICulture = new CultureInfo("da");
13 | Test1.CreateDate.Should().Be("OldestDa");
14 | Thread.CurrentThread.CurrentUICulture = new CultureInfo("en");
15 | Test1.CreateDate.Should().Be("Oldest");
16 | Thread.CurrentThread.CurrentUICulture = new CultureInfo("ch");
17 | Test1.CreateDate.Should().Be("Oldest");
18 | Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us");
19 | Test1.CreateDate.Should().Be("OldestEnUs");
20 | Thread.CurrentThread.CurrentUICulture = new CultureInfo("da-DK");
21 | Test1.CreateDate.Should().Be("OldestDaDK");
22 | }
23 | [Fact]
24 | public void TestCodeGenResourceGen()
25 | {
26 | Thread.CurrentThread.CurrentUICulture = new CultureInfo("da");
27 | Test2.CreateDate.Should().Be("OldestDa");
28 | Thread.CurrentThread.CurrentUICulture = new CultureInfo("en");
29 | Test2.CreateDate.Should().Be("Oldest");
30 | Thread.CurrentThread.CurrentUICulture = new CultureInfo("ch");
31 | Test2.CreateDate.Should().Be("Oldest");
32 | Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-us");
33 | Test2.CreateDate.Should().Be("OldestEnUs");
34 | Thread.CurrentThread.CurrentUICulture = new CultureInfo("da-DK");
35 | Test2.CreateDate.Should().Be("OldestDaDK");
36 | }
37 |
38 | }
39 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "VocaDb.ResXFileCodeGenerator.Tests": {
4 | "commandName": "Project",
5 | "launchBrowser": true,
6 | "environmentVariables": {
7 | "ASPNETCORE_ENVIRONMENT": "Development"
8 | },
9 | "applicationUrl": "https://localhost:1103;http://localhost:1119"
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/SettingsTests.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using FluentAssertions;
3 | using Microsoft.CodeAnalysis;
4 | using Microsoft.CodeAnalysis.Diagnostics;
5 | using Xunit;
6 |
7 | namespace VocaDb.ResXFileCodeGenerator.Tests;
8 |
9 | public class SettingsTests
10 | {
11 | private static readonly GlobalOptions s_globalOptions = GlobalOptions.Select(
12 | provider: new AnalyzerConfigOptionsProviderStub(
13 | globalOptions: new AnalyzerConfigOptionsStub
14 | {
15 | RootNamespace = "namespace1",
16 | MSBuildProjectFullPath = "project1.csproj",
17 | MSBuildProjectName = "project1",
18 | },
19 | fileOptions: null!
20 | ),
21 | token: default
22 | );
23 |
24 | [Fact]
25 | public void GlobalDefaults()
26 | {
27 | var globalOptions = s_globalOptions;
28 | globalOptions.ProjectName.Should().Be("project1");
29 | globalOptions.RootNamespace.Should().Be("namespace1");
30 | globalOptions.ProjectFullPath.Should().Be("project1.csproj");
31 | globalOptions.InnerClassName.Should().BeNullOrEmpty();
32 | globalOptions.ClassNamePostfix.Should().BeNullOrEmpty();
33 | globalOptions.InnerClassInstanceName.Should().BeNullOrEmpty();
34 | globalOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.NotGenerated);
35 | globalOptions.NullForgivingOperators.Should().Be(false);
36 | globalOptions.StaticClass.Should().Be(true);
37 | globalOptions.StaticMembers.Should().Be(true);
38 | globalOptions.PublicClass.Should().Be(false);
39 | globalOptions.PartialClass.Should().Be(false);
40 | globalOptions.IsValid.Should().Be(true);
41 | }
42 |
43 | [Fact]
44 | public void GlobalSettings_CanReadAll()
45 | {
46 | var globalOptions = GlobalOptions.Select(
47 | provider: new AnalyzerConfigOptionsProviderStub(
48 | globalOptions: new AnalyzerConfigOptionsStub
49 | {
50 | RootNamespace = "namespace1",
51 | MSBuildProjectFullPath = "project1.csproj",
52 | MSBuildProjectName = "project1",
53 | ResXFileCodeGenerator_InnerClassName = "test1",
54 | ResXFileCodeGenerator_InnerClassInstanceName = "test2",
55 | ResXFileCodeGenerator_ClassNamePostfix= "test3",
56 | ResXFileCodeGenerator_InnerClassVisibility = "public",
57 | ResXFileCodeGenerator_NullForgivingOperators = "true",
58 | ResXFileCodeGenerator_StaticClass = "false",
59 | ResXFileCodeGenerator_StaticMembers = "false",
60 | ResXFileCodeGenerator_UseVocaDbResManager = "true",
61 | ResXFileCodeGenerator_PublicClass = "true",
62 | ResXFileCodeGenerator_PartialClass = "true",
63 | },
64 | fileOptions: null!
65 | ),
66 | token: default
67 | );
68 | globalOptions.RootNamespace.Should().Be("namespace1");
69 | globalOptions.ProjectFullPath.Should().Be("project1.csproj");
70 | globalOptions.ProjectName.Should().Be("project1");
71 | globalOptions.InnerClassName.Should().Be("test1");
72 | globalOptions.InnerClassInstanceName.Should().Be("test2");
73 | globalOptions.ClassNamePostfix.Should().Be("test3");
74 | globalOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.Public);
75 | globalOptions.NullForgivingOperators.Should().Be(true);
76 | globalOptions.StaticClass.Should().Be(false);
77 | globalOptions.UseVocaDbResManager.Should().Be(true);
78 | globalOptions.StaticMembers.Should().Be(false);
79 | globalOptions.PublicClass.Should().Be(true);
80 | globalOptions.PartialClass.Should().Be(true);
81 | globalOptions.IsValid.Should().Be(true);
82 | }
83 |
84 | [Fact]
85 | public void FileDefaults()
86 | {
87 | var fileOptions = FileOptions.Select(
88 | file: new GroupedAdditionalFile(
89 | mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()),
90 | subFiles: Array.Empty()
91 | ),
92 | options: new AnalyzerConfigOptionsProviderStub(
93 | globalOptions: null!,
94 | fileOptions: new AnalyzerConfigOptionsStub()
95 | ),
96 | globalOptions: s_globalOptions
97 | );
98 | fileOptions.InnerClassName.Should().BeNullOrEmpty();
99 | fileOptions.InnerClassInstanceName.Should().BeNullOrEmpty();
100 | fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.NotGenerated);
101 | fileOptions.NullForgivingOperators.Should().Be(false);
102 | fileOptions.StaticClass.Should().Be(true);
103 | fileOptions.StaticMembers.Should().Be(true);
104 | fileOptions.PublicClass.Should().Be(false);
105 | fileOptions.PartialClass.Should().Be(false);
106 | fileOptions.UseVocaDbResManager.Should().Be(false);
107 | fileOptions.LocalNamespace.Should().Be("namespace1");
108 | fileOptions.CustomToolNamespace.Should().BeNullOrEmpty();
109 | fileOptions.GroupedFile.MainFile.File.Path.Should().Be("Path1.resx");
110 | fileOptions.ClassName.Should().Be("Path1");
111 | fileOptions.IsValid.Should().Be(true);
112 | }
113 |
114 | [Theory]
115 | [InlineData("project1.csproj", "Path1.resx", null, "project1", "project1.Path1")]
116 | [InlineData("project1.csproj", "Path1.resx", "", "project1", "project1.Path1")]
117 | [InlineData("project1.csproj", "Path1.resx", "rootNamespace","rootNamespace", "rootNamespace.Path1")]
118 | [InlineData(@"ProjectFolder\project1.csproj", @"ProjectFolder\SubFolder\Path1.resx", "rootNamespace", "rootNamespace.SubFolder", "rootNamespace.SubFolder.Path1")]
119 | [InlineData(@"ProjectFolder\project1.csproj", @"ProjectFolder\SubFolder With Space\Path1.resx", "rootNamespace", "rootNamespace.SubFolder_With_Space", "rootNamespace.SubFolder_With_Space.Path1")]
120 | [InlineData(@"ProjectFolder\project1.csproj", @"ProjectFolder\SubFolder\Path1.resx", null, "SubFolder", "SubFolder.Path1")]
121 | [InlineData(@"ProjectFolder\8 project.csproj", @"ProjectFolder\Path1.resx", null, "_8_project", "_8_project.Path1")]
122 | [InlineData(@"ProjectFolder\8 project.csproj", @"ProjectFolder\Path1.resx", "", "_8_project", "_8_project.Path1")]
123 | [InlineData(@"ProjectFolder\8 project.csproj", @"ProjectFolder\SubFolder\Path1.resx", null, "SubFolder", "SubFolder.Path1")]
124 | [InlineData(@"ProjectFolder\8 project.csproj", @"ProjectFolder\SubFolder\Path1.resx", "", "SubFolder", "SubFolder.Path1")]
125 | public void FileSettings_RespectsEmptyRootNamespace(
126 | string msBuildProjectFullPath,
127 | string mainFile,
128 | string rootNamespace,
129 | string expectedLocalNamespace,
130 | string expectedEmbeddedFilename
131 | )
132 | {
133 | var fileOptions = FileOptions.Select(
134 | file: new GroupedAdditionalFile(
135 | mainFile: new AdditionalTextWithHash(new AdditionalTextStub(mainFile), Guid.NewGuid()),
136 | subFiles: Array.Empty()
137 | ),
138 | options: new AnalyzerConfigOptionsProviderStub(
139 | globalOptions: null!,
140 | fileOptions: new AnalyzerConfigOptionsStub()
141 | ),
142 | globalOptions: GlobalOptions.Select(
143 | provider: new AnalyzerConfigOptionsProviderStub(
144 | globalOptions: new AnalyzerConfigOptionsStub
145 | {
146 | MSBuildProjectName = Path.GetFileNameWithoutExtension(msBuildProjectFullPath),
147 | RootNamespace = rootNamespace,
148 | MSBuildProjectFullPath = msBuildProjectFullPath
149 | },
150 | fileOptions: null!
151 | ),
152 | token: default
153 | )
154 | );
155 | fileOptions.InnerClassName.Should().BeNullOrEmpty();
156 | fileOptions.InnerClassInstanceName.Should().BeNullOrEmpty();
157 | fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.NotGenerated);
158 | fileOptions.NullForgivingOperators.Should().Be(false);
159 | fileOptions.StaticClass.Should().Be(true);
160 | fileOptions.StaticMembers.Should().Be(true);
161 | fileOptions.PublicClass.Should().Be(false);
162 | fileOptions.PartialClass.Should().Be(false);
163 | fileOptions.UseVocaDbResManager.Should().Be(false);
164 | fileOptions.LocalNamespace.Should().Be(expectedLocalNamespace);
165 | fileOptions.CustomToolNamespace.Should().BeNullOrEmpty();
166 | fileOptions.GroupedFile.MainFile.File.Path.Should().Be(mainFile);
167 | fileOptions.EmbeddedFilename.Should().Be(expectedEmbeddedFilename);
168 | fileOptions.ClassName.Should().Be("Path1");
169 | fileOptions.IsValid.Should().Be(true);
170 | }
171 |
172 | [Fact]
173 | public void File_PostFix()
174 | {
175 | var fileOptions = FileOptions.Select(
176 | file: new GroupedAdditionalFile(
177 | mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()),
178 | subFiles: Array.Empty()
179 | ),
180 | options: new AnalyzerConfigOptionsProviderStub(
181 | globalOptions: null!,
182 | fileOptions: new AnalyzerConfigOptionsStub { ClassNamePostfix = "test1" }
183 | ),
184 | globalOptions: s_globalOptions
185 | );
186 | fileOptions.ClassName.Should().Be("Path1test1");
187 | fileOptions.IsValid.Should().Be(true);
188 | }
189 |
190 | [Fact]
191 | public void FileSettings_CanReadAll()
192 | {
193 | var fileOptions = FileOptions.Select(
194 | file: new GroupedAdditionalFile(
195 | mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()),
196 | subFiles: Array.Empty()
197 | ),
198 | options: new AnalyzerConfigOptionsProviderStub(
199 | globalOptions: null!,
200 | fileOptions: new AnalyzerConfigOptionsStub
201 | {
202 | RootNamespace = "namespace1", MSBuildProjectFullPath = "project1.csproj",
203 | CustomToolNamespace = "ns1",
204 | InnerClassName = "test1",
205 | InnerClassInstanceName = "test2",
206 | InnerClassVisibility = "public",
207 | NullForgivingOperators = "true",
208 | StaticClass = "false",
209 | StaticMembers = "false",
210 | PublicClass = "true",
211 | PartialClass = "true",
212 | UseVocaDbResManager = "true",
213 | }
214 | ),
215 | globalOptions: s_globalOptions
216 | );
217 | fileOptions.InnerClassName.Should().Be("test1");
218 | fileOptions.InnerClassInstanceName.Should().Be("test2");
219 | fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.Public);
220 | fileOptions.NullForgivingOperators.Should().Be(false);
221 | fileOptions.StaticClass.Should().Be(false);
222 | fileOptions.StaticMembers.Should().Be(false);
223 | fileOptions.PublicClass.Should().Be(true);
224 | fileOptions.PartialClass.Should().Be(true);
225 | fileOptions.IsValid.Should().Be(true);
226 | fileOptions.UseVocaDbResManager.Should().Be(true);
227 | fileOptions.LocalNamespace.Should().Be("namespace1");
228 | fileOptions.CustomToolNamespace.Should().Be("ns1");
229 | fileOptions.GroupedFile.MainFile.File.Path.Should().Be("Path1.resx");
230 | fileOptions.ClassName.Should().Be("Path1");
231 | }
232 |
233 | [Fact]
234 | public void FileSettings_RespectsGlobalDefaults()
235 | {
236 | var globalOptions = GlobalOptions.Select(
237 | provider: new AnalyzerConfigOptionsProviderStub(
238 | globalOptions: new AnalyzerConfigOptionsStub
239 | {
240 | RootNamespace = "namespace1",
241 | MSBuildProjectFullPath = "project1.csproj",
242 | MSBuildProjectName = "project1",
243 | ResXFileCodeGenerator_InnerClassName = "test1",
244 | ResXFileCodeGenerator_InnerClassInstanceName = "test2",
245 | ResXFileCodeGenerator_ClassNamePostfix= "test3",
246 | ResXFileCodeGenerator_InnerClassVisibility = "public",
247 | ResXFileCodeGenerator_NullForgivingOperators = "true",
248 | ResXFileCodeGenerator_StaticClass = "false",
249 | ResXFileCodeGenerator_StaticMembers = "false",
250 | ResXFileCodeGenerator_PublicClass = "true",
251 | ResXFileCodeGenerator_PartialClass = "true",
252 | },
253 | fileOptions: null!
254 | ),
255 | token: default
256 | );
257 | var fileOptions = FileOptions.Select(
258 | file: new GroupedAdditionalFile(
259 | mainFile: new AdditionalTextWithHash(new AdditionalTextStub("Path1.resx"), Guid.NewGuid()),
260 | subFiles: Array.Empty()
261 | ),
262 | options: new AnalyzerConfigOptionsProviderStub(
263 | globalOptions: null!,
264 | fileOptions: new AnalyzerConfigOptionsStub()
265 | ),
266 | globalOptions: globalOptions
267 | );
268 | fileOptions.InnerClassName.Should().Be("test1");
269 | fileOptions.InnerClassInstanceName.Should().Be("test2");
270 | fileOptions.InnerClassVisibility.Should().Be(InnerClassVisibility.Public);
271 | fileOptions.NullForgivingOperators.Should().Be(true);
272 | fileOptions.StaticClass.Should().Be(false);
273 | fileOptions.StaticMembers.Should().Be(false);
274 | fileOptions.PublicClass.Should().Be(true);
275 | fileOptions.PartialClass.Should().Be(true);
276 | fileOptions.IsValid.Should().Be(true);
277 | fileOptions.UseVocaDbResManager.Should().Be(false);
278 | fileOptions.LocalNamespace.Should().Be("namespace1");
279 | fileOptions.CustomToolNamespace.Should().BeNullOrEmpty();
280 | fileOptions.GroupedFile.MainFile.File.Path.Should().Be("Path1.resx");
281 | fileOptions.ClassName.Should().Be("Path1test3");
282 | fileOptions.IsValid.Should().Be(true);
283 | }
284 |
285 | private class AnalyzerConfigOptionsStub : AnalyzerConfigOptions
286 | {
287 | // ReSharper disable InconsistentNaming
288 | public string? MSBuildProjectFullPath { get; init; }
289 | // ReSharper disable InconsistentNaming
290 | public string? MSBuildProjectName { get; init; }
291 | public string? RootNamespace { get; init; }
292 | public string? ResXFileCodeGenerator_ClassNamePostfix { get; init; }
293 | public string? ResXFileCodeGenerator_PublicClass { get; init; }
294 | public string? ResXFileCodeGenerator_NullForgivingOperators { get; init; }
295 | public string? ResXFileCodeGenerator_StaticClass { get; init; }
296 | public string? ResXFileCodeGenerator_StaticMembers { get; init; }
297 | public string? ResXFileCodeGenerator_PartialClass { get; init; }
298 | public string? ResXFileCodeGenerator_InnerClassVisibility { get; init; }
299 | public string? ResXFileCodeGenerator_InnerClassName { get; init; }
300 | public string? ResXFileCodeGenerator_InnerClassInstanceName { get; init; }
301 | public string? ResXFileCodeGenerator_UseVocaDbResManager { get; init; }
302 | public string? CustomToolNamespace { get; init; }
303 | public string? TargetPath { get; init; }
304 | public string? ClassNamePostfix { get; init; }
305 | public string? PublicClass { get; init; }
306 | public string? NullForgivingOperators { get; init; }
307 | public string? StaticClass { get; init; }
308 | public string? StaticMembers { get; init; }
309 | public string? PartialClass { get; init; }
310 | public string? InnerClassVisibility { get; init; }
311 | public string? InnerClassName { get; init; }
312 | public string? InnerClassInstanceName { get; init; }
313 | public string? UseVocaDbResManager { get; init; }
314 |
315 | // ReSharper restore InconsistentNaming
316 |
317 | public override bool TryGetValue(string key, [NotNullWhen(true)] out string? value)
318 | {
319 | string? GetVal() =>
320 | key switch
321 | {
322 | "build_property.MSBuildProjectFullPath" => MSBuildProjectFullPath,
323 | "build_property.MSBuildProjectName" => MSBuildProjectName,
324 | "build_property.RootNamespace" => RootNamespace,
325 | "build_property.ResXFileCodeGenerator_UseVocaDbResManager" => ResXFileCodeGenerator_UseVocaDbResManager,
326 | "build_property.ResXFileCodeGenerator_ClassNamePostfix" => ResXFileCodeGenerator_ClassNamePostfix,
327 | "build_property.ResXFileCodeGenerator_PublicClass" => ResXFileCodeGenerator_PublicClass,
328 | "build_property.ResXFileCodeGenerator_NullForgivingOperators" => ResXFileCodeGenerator_NullForgivingOperators,
329 | "build_property.ResXFileCodeGenerator_StaticClass" => ResXFileCodeGenerator_StaticClass,
330 | "build_property.ResXFileCodeGenerator_StaticMembers" => ResXFileCodeGenerator_StaticMembers,
331 | "build_property.ResXFileCodeGenerator_PartialClass" => ResXFileCodeGenerator_PartialClass,
332 | "build_property.ResXFileCodeGenerator_InnerClassVisibility" => ResXFileCodeGenerator_InnerClassVisibility,
333 | "build_property.ResXFileCodeGenerator_InnerClassName" => ResXFileCodeGenerator_InnerClassName,
334 | "build_property.ResXFileCodeGenerator_InnerClassInstanceName" => ResXFileCodeGenerator_InnerClassInstanceName,
335 | "build_metadata.EmbeddedResource.CustomToolNamespace" => CustomToolNamespace,
336 | "build_metadata.EmbeddedResource.TargetPath" => TargetPath,
337 | "build_metadata.EmbeddedResource.ClassNamePostfix" => ClassNamePostfix,
338 | "build_metadata.EmbeddedResource.PublicClass" => PublicClass,
339 | "build_metadata.EmbeddedResource.NullForgivingOperators" => NullForgivingOperators,
340 | "build_metadata.EmbeddedResource.StaticClass" => StaticClass,
341 | "build_metadata.EmbeddedResource.StaticMembers" => StaticMembers,
342 | "build_metadata.EmbeddedResource.PartialClass" => PartialClass,
343 | "build_metadata.EmbeddedResource.InnerClassVisibility" => InnerClassVisibility,
344 | "build_metadata.EmbeddedResource.InnerClassName" => InnerClassName,
345 | "build_metadata.EmbeddedResource.InnerClassInstanceName" => InnerClassInstanceName,
346 | "build_metadata.EmbeddedResource.UseVocaDbResManager" => UseVocaDbResManager,
347 | _ => null
348 | };
349 |
350 | value = GetVal();
351 | return value is not null;
352 | }
353 | }
354 |
355 | private class AnalyzerConfigOptionsProviderStub : AnalyzerConfigOptionsProvider
356 | {
357 | private readonly AnalyzerConfigOptions _fileOptions;
358 |
359 | public override AnalyzerConfigOptions GlobalOptions { get; }
360 |
361 | public AnalyzerConfigOptionsProviderStub(AnalyzerConfigOptions globalOptions, AnalyzerConfigOptions fileOptions)
362 | {
363 | _fileOptions = fileOptions;
364 | GlobalOptions = globalOptions;
365 | }
366 |
367 | public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => throw new NotImplementedException();
368 |
369 | public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => _fileOptions;
370 |
371 | }
372 | }
373 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/UtilitiesTests.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 | using FluentAssertions;
3 | using Microsoft.CodeAnalysis;
4 | using Microsoft.CodeAnalysis.Diagnostics;
5 | using Xunit;
6 |
7 | namespace VocaDb.ResXFileCodeGenerator.Tests;
8 |
9 | public class UtilitiesTests
10 | {
11 |
12 |
13 | [Theory]
14 | [InlineData("Valid", "Valid")]
15 | [InlineData("_Valid", "_Valid")]
16 | [InlineData("Valid123", "Valid123")]
17 | [InlineData("Valid_123", "Valid_123")]
18 | [InlineData("Valid.123", "Valid.123")]
19 | [InlineData("8Ns", "_8Ns")]
20 | [InlineData("Ns+InvalidChar", "Ns_InvalidChar")]
21 | [InlineData("Ns..Folder...Folder2", "Ns.Folder.Folder2")]
22 | [InlineData("Ns.Folder.", "Ns.Folder")]
23 | [InlineData(".Ns.Folder", "Ns.Folder")]
24 | [InlineData("Folder with space", "Folder_with_space")]
25 | [InlineData("folder with .. space", "folder_with_._space")]
26 | public void SanitizeNamespace(string input, string expected)
27 | {
28 | Utilities.SanitizeNamespace(input).Should().Be(expected);
29 | }
30 |
31 | [Theory]
32 | [InlineData("Valid", "Valid")]
33 | [InlineData(".Valid", ".Valid")]
34 | [InlineData("8Ns", "8Ns")]
35 | [InlineData("..Ns", ".Ns")]
36 | public void SanitizeNamespaceWithoutFirstCharRules(string input, string expected)
37 | {
38 | Utilities.SanitizeNamespace(input, false).Should().Be(expected);
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.Tests/VocaDb.ResXFileCodeGenerator.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | false
6 | latest
7 | enable
8 | enable
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | all
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 |
19 |
20 |
21 |
22 |
23 | runtime; build; native; contentfiles; analyzers; buildtransitive
24 | all
25 |
26 |
27 | runtime; build; native; contentfiles; analyzers; buildtransitive
28 | all
29 |
30 |
31 |
32 |
33 | $([System.IO.Path]::GetFileNameWithoutExtension('%(FileName)')).resx
34 |
35 |
36 | true
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.1.31903.286
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VocaDb.ResXFileCodeGenerator", "VocaDb.ResXFileCodeGenerator\VocaDb.ResXFileCodeGenerator.csproj", "{210EF250-4028-42AE-9BAB-80C22F236816}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "VocaDb.ResXFileCodeGenerator.Tests", "VocaDb.ResXFileCodeGenerator.Tests\VocaDb.ResXFileCodeGenerator.Tests.csproj", "{47BDEBDC-42BF-4A8E-9825-FC5ECC844542}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3B4CC333-0EEB-40E6-94EA-41928C6E252C}"
11 | ProjectSection(SolutionItems) = preProject
12 | .editorconfig = .editorconfig
13 | README.md = README.md
14 | EndProjectSection
15 | EndProject
16 | Global
17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
18 | Debug|Any CPU = Debug|Any CPU
19 | Release|Any CPU = Release|Any CPU
20 | EndGlobalSection
21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
22 | {210EF250-4028-42AE-9BAB-80C22F236816}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {210EF250-4028-42AE-9BAB-80C22F236816}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {210EF250-4028-42AE-9BAB-80C22F236816}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {210EF250-4028-42AE-9BAB-80C22F236816}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {47BDEBDC-42BF-4A8E-9825-FC5ECC844542}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {47BDEBDC-42BF-4A8E-9825-FC5ECC844542}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {47BDEBDC-42BF-4A8E-9825-FC5ECC844542}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {47BDEBDC-42BF-4A8E-9825-FC5ECC844542}.Release|Any CPU.Build.0 = Release|Any CPU
30 | EndGlobalSection
31 | GlobalSection(SolutionProperties) = preSolution
32 | HideSolutionNode = FALSE
33 | EndGlobalSection
34 | GlobalSection(ExtensibilityGlobals) = postSolution
35 | SolutionGuid = {13A48F86-92D7-401D-9A8E-69FFED20CD82}
36 | EndGlobalSection
37 | EndGlobal
38 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | <?xml version="1.0" encoding="utf-16"?>
3 | <Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns">
4 | <TypePattern DisplayName="Non-reorderable types">
5 | <TypePattern.Match>
6 | <Or>
7 | <And>
8 | <Kind Is="Interface" />
9 | <Or>
10 | <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" />
11 | <HasAttribute Name="System.Runtime.InteropServices.ComImport" />
12 | </Or>
13 | </And>
14 | <Kind Is="Struct" />
15 | <HasAttribute Name="JetBrains.Annotations.NoReorderAttribute" />
16 | <HasAttribute Name="JetBrains.Annotations.NoReorder" />
17 | </Or>
18 | </TypePattern.Match>
19 | </TypePattern>
20 | <TypePattern DisplayName="xUnit.net Test Classes" RemoveRegions="All">
21 | <TypePattern.Match>
22 | <And>
23 | <Kind Is="Class" />
24 | <HasMember>
25 | <And>
26 | <Kind Is="Method" />
27 | <HasAttribute Name="Xunit.FactAttribute" Inherited="True" />
28 | <HasAttribute Name="Xunit.TheoryAttribute" Inherited="True" />
29 | </And>
30 | </HasMember>
31 | </And>
32 | </TypePattern.Match>
33 | <Entry DisplayName="Setup/Teardown Methods">
34 | <Entry.Match>
35 | <Or>
36 | <Kind Is="Constructor" />
37 | <And>
38 | <Kind Is="Method" />
39 | <ImplementsInterface Name="System.IDisposable" />
40 | </And>
41 | </Or>
42 | </Entry.Match>
43 | <Entry.SortBy>
44 | <Kind Order="Constructor" />
45 | </Entry.SortBy>
46 | </Entry>
47 | <Entry DisplayName="All other members" />
48 | <Entry Priority="100" DisplayName="Test Methods">
49 | <Entry.Match>
50 | <And>
51 | <Kind Is="Method" />
52 | <HasAttribute Name="Xunit.FactAttribute" />
53 | <HasAttribute Name="Xunit.TheoryAttribute" />
54 | </And>
55 | </Entry.Match>
56 | <Entry.SortBy>
57 | <Name />
58 | </Entry.SortBy>
59 | </Entry>
60 | </TypePattern>
61 | <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All">
62 | <TypePattern.Match>
63 | <And>
64 | <Kind Is="Class" />
65 | <Or>
66 | <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="True" />
67 | <HasAttribute Name="NUnit.Framework.TestFixtureSourceAttribute" Inherited="True" />
68 | <HasMember>
69 | <And>
70 | <Kind Is="Method" />
71 | <HasAttribute Name="NUnit.Framework.TestAttribute" />
72 | <HasAttribute Name="NUnit.Framework.TestCaseAttribute" />
73 | <HasAttribute Name="NUnit.Framework.TestCaseSourceAttribute" />
74 | </And>
75 | </HasMember>
76 | </Or>
77 | </And>
78 | </TypePattern.Match>
79 | <Entry DisplayName="Setup/Teardown Methods">
80 | <Entry.Match>
81 | <And>
82 | <Kind Is="Method" />
83 | <Or>
84 | <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="True" />
85 | <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="True" />
86 | <HasAttribute Name="NUnit.Framework.TestFixtureSetUpAttribute" Inherited="True" />
87 | <HasAttribute Name="NUnit.Framework.TestFixtureTearDownAttribute" Inherited="True" />
88 | <HasAttribute Name="NUnit.Framework.OneTimeSetUpAttribute" Inherited="True" />
89 | <HasAttribute Name="NUnit.Framework.OneTimeTearDownAttribute" Inherited="True" />
90 | </Or>
91 | </And>
92 | </Entry.Match>
93 | </Entry>
94 | <Entry DisplayName="All other members" />
95 | <Entry Priority="100" DisplayName="Test Methods">
96 | <Entry.Match>
97 | <And>
98 | <Kind Is="Method" />
99 | <HasAttribute Name="NUnit.Framework.TestAttribute" />
100 | <HasAttribute Name="NUnit.Framework.TestCaseAttribute" />
101 | <HasAttribute Name="NUnit.Framework.TestCaseSourceAttribute" />
102 | </And>
103 | </Entry.Match>
104 | <Entry.SortBy>
105 | <Name />
106 | </Entry.SortBy>
107 | </Entry>
108 | </TypePattern>
109 | <TypePattern DisplayName="Default Pattern">
110 | <Entry Priority="100" DisplayName="Public Delegates">
111 | <Entry.Match>
112 | <And>
113 | <Access Is="Public" />
114 | <Kind Is="Delegate" />
115 | </And>
116 | </Entry.Match>
117 | <Entry.SortBy>
118 | <Name />
119 | </Entry.SortBy>
120 | </Entry>
121 | <Entry Priority="100" DisplayName="Public Enums">
122 | <Entry.Match>
123 | <And>
124 | <Access Is="Public" />
125 | <Kind Is="Enum" />
126 | </And>
127 | </Entry.Match>
128 | <Entry.SortBy>
129 | <Name />
130 | </Entry.SortBy>
131 | </Entry>
132 | <Entry DisplayName="Static Fields and Constants">
133 | <Entry.Match>
134 | <Or>
135 | <Kind Is="Constant" />
136 | <And>
137 | <Kind Is="Field" />
138 | <Static />
139 | </And>
140 | </Or>
141 | </Entry.Match>
142 | <Entry.SortBy>
143 | <Kind Order="Constant Field" />
144 | </Entry.SortBy>
145 | </Entry>
146 | <Entry DisplayName="Fields">
147 | <Entry.Match>
148 | <And>
149 | <Kind Is="Field" />
150 | <Not>
151 | <Static />
152 | </Not>
153 | </And>
154 | </Entry.Match>
155 | <Entry.SortBy>
156 | <Readonly />
157 | <Name />
158 | </Entry.SortBy>
159 | </Entry>
160 | <Entry DisplayName="Constructors">
161 | <Entry.Match>
162 | <Kind Is="Constructor" />
163 | </Entry.Match>
164 | <Entry.SortBy>
165 | <Static />
166 | </Entry.SortBy>
167 | </Entry>
168 | <Entry DisplayName="Properties, Indexers">
169 | <Entry.Match>
170 | <Or>
171 | <Kind Is="Property" />
172 | <Kind Is="Indexer" />
173 | </Or>
174 | </Entry.Match>
175 | </Entry>
176 | <Entry Priority="100" DisplayName="Interface Implementations">
177 | <Entry.Match>
178 | <And>
179 | <Kind Is="Member" />
180 | <ImplementsInterface />
181 | </And>
182 | </Entry.Match>
183 | <Entry.SortBy>
184 | <ImplementsInterface Immediate="True" />
185 | </Entry.SortBy>
186 | </Entry>
187 | <Entry DisplayName="All other members" />
188 | <Entry DisplayName="Nested Types">
189 | <Entry.Match>
190 | <Kind Is="Type" />
191 | </Entry.Match>
192 | </Entry>
193 | </TypePattern>
194 | </Patterns>
195 | <Policy Inspect="True" Prefix="s_" Suffix="" Style="aaBb" />
196 | True
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/AdditionalTextWithHash.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 |
3 | namespace VocaDb.ResXFileCodeGenerator;
4 |
5 | public readonly record struct AdditionalTextWithHash(AdditionalText File, Guid Hash)
6 | {
7 | public bool Equals(AdditionalTextWithHash other)
8 | {
9 | return File.Path.Equals(other.File.Path) && Hash.Equals(other.Hash);
10 | }
11 |
12 | public override int GetHashCode()
13 | {
14 | unchecked
15 | {
16 | return (File.GetHashCode() * 397) ^ Hash.GetHashCode();
17 | }
18 | }
19 |
20 | public override string ToString()
21 | {
22 | return $"{nameof(File)}: {File?.Path}, {nameof(Hash)}: {Hash}";
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/AnalyzerReleases.Shipped.md:
--------------------------------------------------------------------------------
1 | ; Shipped analyzer releases
2 | ; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
3 |
4 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/AnalyzerReleases.Unshipped.md:
--------------------------------------------------------------------------------
1 | ; Unshipped analyzer release
2 | ; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
3 |
4 | ### New Rules
5 | Rule ID | Category | Severity | Notes
6 | --------|----------|----------|-------
7 | VocaDbResXFileCodeGenerator001 | ResXFileCodeGenerator | Warning | StringBuilderGenerator
8 | VocaDbResXFileCodeGenerator002 | ResXFileCodeGenerator | Warning | StringBuilderGenerator
9 | VocaDbResXFileCodeGenerator003 | ResXFileCodeGenerator | Error | StringBuilderGenerator
10 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/Constants.cs:
--------------------------------------------------------------------------------
1 | namespace VocaDb.ResXFileCodeGenerator;
2 |
3 | internal static class Constants
4 | {
5 | public const string SystemDiagnosticsCodeAnalysis =
6 | $"{nameof(System)}.{nameof(System.Diagnostics)}.{nameof(System.Diagnostics.CodeAnalysis)}";
7 |
8 | public const string SystemGlobalization = $"{nameof(System)}.{nameof(System.Globalization)}";
9 | public const string SystemResources = $"{nameof(System)}.{nameof(System.Resources)}";
10 |
11 | public const string AutoGeneratedHeader =
12 | @"// ------------------------------------------------------------------------------
13 | //
14 | // This code was generated by a tool.
15 | //
16 | // Changes to this file may cause incorrect behavior and will be lost if
17 | // the code is regenerated.
18 | //
19 | // ------------------------------------------------------------------------------";
20 |
21 | public const string s_resourceManagerVariable = "s_resourceManager";
22 | public const string ResourceManagerVariable = "ResourceManager";
23 | public const string CultureInfoVariable = "CultureInfo";
24 | }
25 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/CultureInfoCombo.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 |
3 | namespace VocaDb.ResXFileCodeGenerator;
4 |
5 | ///
6 | /// Note: Equality takes into consideration Iso property only
7 | ///
8 | public readonly record struct CultureInfoCombo
9 | {
10 | // order by length desc, so that da-DK comes before da, meaning that it HashSet already doesn't contain da-DK when we process it
11 | public CultureInfoCombo(IReadOnlyList? files)
12 | {
13 | CultureInfos = files?
14 | .Select(x => (Path.GetExtension(Path.GetFileNameWithoutExtension(x.File.Path)).TrimStart('.'), y: x))
15 | .OrderByDescending(x => x.Item1.Length)
16 | .ThenBy(y => y.Item1)
17 | .ToList() ?? new List<(string, AdditionalTextWithHash)>();
18 | }
19 |
20 | public IReadOnlyList<(string Iso, AdditionalTextWithHash File)> CultureInfos { get;}
21 |
22 | public IReadOnlyList<(string Name, int LCID, AdditionalTextWithHash FileWithHash)> GetDefinedLanguages() => CultureInfos?
23 | .Select(x => (x.File, new CultureInfo(x.Iso)))
24 | .Select(x => (Name: x.Item2.Name.Replace('-', '_'), x.Item2.LCID, x.File))
25 | .ToList() ?? new List<(string Name, int LCID, AdditionalTextWithHash FileWithHash)>();
26 |
27 | public bool Equals(CultureInfoCombo other)
28 | {
29 | return (CultureInfos ?? Array.Empty<(string Iso, AdditionalTextWithHash File)>()).Select(x => x.Iso)
30 | .SequenceEqual(other.CultureInfos?.Select(x => x.Iso) ?? Array.Empty());
31 | }
32 |
33 | public override int GetHashCode()
34 | {
35 | unchecked
36 | {
37 | if (CultureInfos == null) return 0;
38 | const int seedValue = 0x2D2816FE;
39 | const int primeNumber = 397;
40 | return CultureInfos.Aggregate(seedValue, (current, item) => (current * primeNumber) + (Equals(item.Iso, null) ? 0 : item.Iso.GetHashCode()));
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/FileOptions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.Diagnostics;
2 |
3 | namespace VocaDb.ResXFileCodeGenerator;
4 |
5 | public readonly record struct FileOptions
6 | {
7 | public string InnerClassInstanceName { get; init; }
8 | public string InnerClassName { get; init; }
9 | public InnerClassVisibility InnerClassVisibility { get; init; }
10 | public bool PartialClass { get; init; }
11 | public bool StaticMembers { get; init; } = true;
12 | public GroupedAdditionalFile GroupedFile { get; init; }
13 | public bool StaticClass { get; init; }
14 | public bool NullForgivingOperators { get; init; }
15 | public bool PublicClass { get; init; }
16 | public string ClassName { get; init; }
17 | public string? CustomToolNamespace { get; init; }
18 | public string LocalNamespace { get; init; }
19 | public bool UseVocaDbResManager { get; init; }
20 | public string EmbeddedFilename { get; init; }
21 | public bool IsValid { get; init; }
22 |
23 | public FileOptions(
24 | GroupedAdditionalFile groupedFile,
25 | AnalyzerConfigOptions options,
26 | GlobalOptions globalOptions
27 | )
28 | {
29 | GroupedFile = groupedFile;
30 | var resxFilePath = groupedFile.MainFile.File.Path;
31 |
32 | var classNameFromFileName = Utilities.GetClassNameFromPath(resxFilePath);
33 |
34 | var detectedNamespace = Utilities.GetLocalNamespace(
35 | resxFilePath,
36 | options.TryGetValue("build_metadata.EmbeddedResource.Link", out var link) &&
37 | link is { Length: > 0 }
38 | ? link
39 | : null,
40 | globalOptions.ProjectFullPath,
41 | globalOptions.ProjectName,
42 | globalOptions.RootNamespace);
43 |
44 | EmbeddedFilename = string.IsNullOrEmpty(detectedNamespace) ? classNameFromFileName : $"{detectedNamespace}.{classNameFromFileName}";
45 |
46 | LocalNamespace =
47 | options.TryGetValue("build_metadata.EmbeddedResource.TargetPath", out var targetPath) &&
48 | targetPath is { Length: > 0 }
49 | ? Utilities.GetLocalNamespace(
50 | resxFilePath, targetPath,
51 | globalOptions.ProjectFullPath,
52 | globalOptions.ProjectName,
53 | globalOptions.RootNamespace)
54 | : string.IsNullOrEmpty(detectedNamespace)
55 | ? Utilities.SanitizeNamespace(globalOptions.ProjectName)
56 | : detectedNamespace;
57 |
58 | CustomToolNamespace =
59 | options.TryGetValue("build_metadata.EmbeddedResource.CustomToolNamespace", out var customToolNamespace) &&
60 | customToolNamespace is { Length: > 0 }
61 | ? customToolNamespace
62 | : null;
63 |
64 | ClassName =
65 | options.TryGetValue("build_metadata.EmbeddedResource.ClassNamePostfix", out var perFileClassNameSwitch) &&
66 | perFileClassNameSwitch is { Length: > 0 }
67 | ? classNameFromFileName + perFileClassNameSwitch
68 | : classNameFromFileName + globalOptions.ClassNamePostfix;
69 |
70 | NullForgivingOperators = globalOptions.NullForgivingOperators;
71 |
72 | PublicClass =
73 | options.TryGetValue("build_metadata.EmbeddedResource.PublicClass", out var perFilePublicClassSwitch) &&
74 | perFilePublicClassSwitch is { Length: > 0 }
75 | ? perFilePublicClassSwitch.Equals("true", StringComparison.OrdinalIgnoreCase)
76 | : globalOptions.PublicClass;
77 |
78 | StaticClass =
79 | options.TryGetValue("build_metadata.EmbeddedResource.StaticClass", out var perFileStaticClassSwitch) &&
80 | perFileStaticClassSwitch is { Length: > 0 }
81 | ? !perFileStaticClassSwitch.Equals("false", StringComparison.OrdinalIgnoreCase)
82 | : globalOptions.StaticClass;
83 |
84 | StaticMembers =
85 | options.TryGetValue("build_metadata.EmbeddedResource.StaticMembers", out var staticMembersSwitch) &&
86 | staticMembersSwitch is { Length: > 0 }
87 | ? !staticMembersSwitch.Equals("false", StringComparison.OrdinalIgnoreCase)
88 | : globalOptions.StaticMembers;
89 |
90 | PartialClass =
91 | options.TryGetValue("build_metadata.EmbeddedResource.PartialClass", out var partialClassSwitch) &&
92 | partialClassSwitch is { Length: > 0 }
93 | ? partialClassSwitch.Equals("true", StringComparison.OrdinalIgnoreCase)
94 | : globalOptions.PartialClass;
95 |
96 | InnerClassVisibility = globalOptions.InnerClassVisibility;
97 | if (
98 | options.TryGetValue("build_metadata.EmbeddedResource.InnerClassVisibility", out var innerClassVisibilitySwitch) &&
99 | Enum.TryParse(innerClassVisibilitySwitch, true, out InnerClassVisibility v) &&
100 | v != InnerClassVisibility.SameAsOuter
101 | )
102 | {
103 | InnerClassVisibility = v;
104 | }
105 |
106 | InnerClassName = globalOptions.InnerClassName;
107 | if (
108 | options.TryGetValue("build_metadata.EmbeddedResource.InnerClassName", out var innerClassNameSwitch) &&
109 | innerClassNameSwitch is { Length: > 0 }
110 | )
111 | {
112 | InnerClassName = innerClassNameSwitch;
113 | }
114 |
115 | InnerClassInstanceName = globalOptions.InnerClassInstanceName;
116 | if (
117 | options.TryGetValue("build_metadata.EmbeddedResource.InnerClassInstanceName", out var innerClassInstanceNameSwitch) &&
118 | innerClassInstanceNameSwitch is { Length: > 0 }
119 | )
120 | {
121 | InnerClassInstanceName = innerClassInstanceNameSwitch;
122 | }
123 |
124 | UseVocaDbResManager = globalOptions.UseVocaDbResManager;
125 | if (
126 | options.TryGetValue("build_metadata.EmbeddedResource.UseVocaDbResManager", out var genCodeSwitch) &&
127 | genCodeSwitch is { Length: > 0 }
128 | )
129 | {
130 | UseVocaDbResManager = genCodeSwitch.Equals("true", StringComparison.OrdinalIgnoreCase);
131 | }
132 |
133 | IsValid = globalOptions.IsValid;
134 | }
135 |
136 | public static FileOptions Select(
137 | GroupedAdditionalFile file,
138 | AnalyzerConfigOptionsProvider options,
139 | GlobalOptions globalOptions
140 | )
141 | {
142 | return new FileOptions(
143 | groupedFile: file,
144 | options: options.GetOptions(file.MainFile.File),
145 | globalOptions: globalOptions
146 | );
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/GlobalOptions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis.Diagnostics;
2 |
3 | namespace VocaDb.ResXFileCodeGenerator;
4 |
5 | public sealed record GlobalOptions // this must be a record or implement IEquatable
6 | {
7 | public string InnerClassInstanceName { get; }
8 | public bool StaticMembers { get; }
9 | public string InnerClassName { get; }
10 | public InnerClassVisibility InnerClassVisibility { get; }
11 | public bool PartialClass { get; }
12 | public string? RootNamespace { get; }
13 | public string ProjectFullPath { get; }
14 | public string ProjectName { get; }
15 | public bool StaticClass { get; }
16 | public bool NullForgivingOperators { get; }
17 | public bool PublicClass { get; }
18 | public string ClassNamePostfix { get; }
19 | public bool UseVocaDbResManager { get; }
20 | public bool IsValid { get; }
21 |
22 | public GlobalOptions(AnalyzerConfigOptions options)
23 | {
24 | IsValid = true;
25 |
26 | if (!options.TryGetValue("build_property.MSBuildProjectFullPath", out var projectFullPath))
27 | {
28 | IsValid = false;
29 | }
30 | ProjectFullPath = projectFullPath!;
31 |
32 | if (options.TryGetValue("build_property.RootNamespace", out var rootNamespace))
33 | {
34 | RootNamespace = rootNamespace;
35 | }
36 |
37 | if (!options.TryGetValue("build_property.MSBuildProjectName", out var projectName))
38 | {
39 | IsValid = false;
40 | }
41 | ProjectName = projectName!;
42 |
43 | // Code from: https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md#consume-msbuild-properties-and-metadata
44 | PublicClass =
45 | options.TryGetValue("build_property.ResXFileCodeGenerator_PublicClass", out var publicClassSwitch) &&
46 | publicClassSwitch is { Length: > 0 } &&
47 | publicClassSwitch.Equals("true", StringComparison.OrdinalIgnoreCase);
48 |
49 | NullForgivingOperators =
50 | options.TryGetValue("build_property.ResXFileCodeGenerator_NullForgivingOperators", out var nullForgivingOperatorsSwitch) &&
51 | nullForgivingOperatorsSwitch is { Length: > 0 } &&
52 | nullForgivingOperatorsSwitch.Equals("true", StringComparison.OrdinalIgnoreCase);
53 |
54 | StaticClass =
55 | !(
56 | options.TryGetValue("build_property.ResXFileCodeGenerator_StaticClass", out var staticClassSwitch) &&
57 | staticClassSwitch is { Length: > 0 } &&
58 | staticClassSwitch.Equals("false", StringComparison.OrdinalIgnoreCase)
59 | );
60 |
61 | StaticMembers =
62 | !(
63 | options.TryGetValue("build_property.ResXFileCodeGenerator_StaticMembers", out var staticMembersSwitch) &&
64 | staticMembersSwitch is { Length: > 0 } &&
65 | staticMembersSwitch.Equals("false", StringComparison.OrdinalIgnoreCase)
66 | );
67 |
68 | PartialClass =
69 | options.TryGetValue("build_property.ResXFileCodeGenerator_PartialClass", out var partialClassSwitch) &&
70 | partialClassSwitch is { Length: > 0 } &&
71 | partialClassSwitch.Equals("true", StringComparison.OrdinalIgnoreCase);
72 |
73 | ClassNamePostfix = string.Empty;
74 | if (options.TryGetValue("build_property.ResXFileCodeGenerator_ClassNamePostfix", out var classNamePostfixSwitch))
75 | {
76 | ClassNamePostfix = classNamePostfixSwitch;
77 | }
78 |
79 | InnerClassVisibility = InnerClassVisibility.NotGenerated;
80 | if (
81 | options.TryGetValue("build_property.ResXFileCodeGenerator_InnerClassVisibility", out var innerClassVisibilitySwitch) &&
82 | Enum.TryParse(innerClassVisibilitySwitch, true, out InnerClassVisibility v)
83 | )
84 | {
85 | InnerClassVisibility = v;
86 | }
87 |
88 | InnerClassName = string.Empty;
89 | if (options.TryGetValue("build_property.ResXFileCodeGenerator_InnerClassName", out var innerClassNameSwitch))
90 | {
91 | InnerClassName = innerClassNameSwitch;
92 | }
93 |
94 | InnerClassInstanceName = string.Empty;
95 | if (options.TryGetValue("build_property.ResXFileCodeGenerator_InnerClassInstanceName", out var innerClassInstanceNameSwitch))
96 | {
97 | InnerClassInstanceName = innerClassInstanceNameSwitch;
98 | }
99 |
100 | UseVocaDbResManager = false;
101 | if (
102 | options.TryGetValue("build_property.ResXFileCodeGenerator_UseVocaDbResManager", out var genCodeSwitch) &&
103 | genCodeSwitch is { Length: > 0 } &&
104 | genCodeSwitch.Equals("true", StringComparison.OrdinalIgnoreCase)
105 | )
106 | {
107 | UseVocaDbResManager = true;
108 | }
109 | }
110 |
111 | public static GlobalOptions Select(AnalyzerConfigOptionsProvider provider, CancellationToken token)
112 | {
113 | token.ThrowIfCancellationRequested();
114 | return new GlobalOptions(provider.GlobalOptions);
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/GroupResxFiles.cs:
--------------------------------------------------------------------------------
1 | using System.Linq;
2 | using Microsoft.CodeAnalysis;
3 |
4 | namespace VocaDb.ResXFileCodeGenerator;
5 |
6 | public static class GroupResxFiles
7 | {
8 | public static IEnumerable Group(IReadOnlyList allFilesWithHash, CancellationToken cancellationToken = default)
9 | {
10 | var lookup = new Dictionary();
11 | var res = new Dictionary>();
12 | foreach (var file in allFilesWithHash)
13 | {
14 | cancellationToken.ThrowIfCancellationRequested();
15 |
16 | var path = file.File.Path;
17 | var pathName = Path.GetDirectoryName(path);
18 | var baseName = Utilities.GetBaseName(path);
19 | if (Path.GetFileNameWithoutExtension(path) == baseName)
20 | {
21 | var key = pathName + "\\" + baseName;
22 | //it should be impossible to exist already, but VS sometimes throws error about duplicate key added. Keep the original entry, not the new one
23 | if (!lookup.ContainsKey(key))
24 | lookup.Add(key, file);
25 | res.Add(file, new List());
26 | }
27 | }
28 | foreach (var fileWithHash in allFilesWithHash)
29 | {
30 | cancellationToken.ThrowIfCancellationRequested();
31 |
32 | var path = fileWithHash.File.Path;
33 | var pathName = Path.GetDirectoryName(path);
34 | var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(path);
35 | var baseName = Utilities.GetBaseName(path);
36 | if (fileNameWithoutExtension == baseName)
37 | continue;
38 | // this might happen if a .nn.resx file exists without a .resx file
39 | if (!lookup.TryGetValue(pathName + "\\" + baseName, out var additionalText))
40 | continue;
41 | res[additionalText].Add(fileWithHash);
42 | }
43 | // dont care at all HOW it is sorted, just that end result is the same
44 | foreach (var file in res)
45 | {
46 | cancellationToken.ThrowIfCancellationRequested();
47 |
48 | yield return new GroupedAdditionalFile(file.Key, file.Value);
49 | }
50 | }
51 |
52 | public static IEnumerable DetectChildCombos(IReadOnlyList groupedAdditionalFiles)
53 | {
54 | return groupedAdditionalFiles
55 | .Select(x => new CultureInfoCombo(x.SubFiles))
56 | .Distinct().ToList();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/GroupedAdditionalFile.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 | namespace VocaDb.ResXFileCodeGenerator;
3 |
4 | public readonly record struct GroupedAdditionalFile
5 | {
6 | public AdditionalTextWithHash MainFile { get; }
7 | public IReadOnlyList SubFiles { get; }
8 |
9 | public GroupedAdditionalFile(AdditionalTextWithHash mainFile, IReadOnlyList subFiles)
10 | {
11 | MainFile = mainFile;
12 | SubFiles = subFiles.OrderBy(x => x.File.Path, StringComparer.Ordinal).ToArray();
13 | }
14 |
15 | public bool Equals(GroupedAdditionalFile other)
16 | {
17 | return MainFile.Equals(other.MainFile) && SubFiles.SequenceEqual(other.SubFiles);
18 | }
19 |
20 | public override int GetHashCode()
21 | {
22 | unchecked
23 | {
24 | var hashCode = MainFile.GetHashCode();
25 |
26 | foreach (var additionalText in SubFiles)
27 | {
28 | hashCode = (hashCode * 397) ^ additionalText.GetHashCode();
29 | }
30 |
31 | return hashCode;
32 | }
33 | }
34 |
35 | public override string ToString()
36 | {
37 | return $"{nameof(MainFile)}: {MainFile}, {nameof(SubFiles)}: {string.Join("; ", SubFiles ?? Array.Empty())}";
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/IGenerator.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 |
3 | namespace VocaDb.ResXFileCodeGenerator;
4 |
5 | public interface IGenerator
6 | {
7 | ///
8 | /// Generate source file with properties for each translated resource
9 | ///
10 | (string GeneratedFileName, string SourceCode, IEnumerable ErrorsAndWarnings)
11 | Generate(FileOptions options, CancellationToken cancellationToken = default);
12 |
13 | ///
14 | /// Generate helper functions to determine which translated resource to use in the current moment
15 | ///
16 | (string GeneratedFileName, string SourceCode, IEnumerable ErrorsAndWarnings)
17 | Generate(CultureInfoCombo combo, CancellationToken cancellationToken);
18 | }
19 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/InnerClassVisibility.cs:
--------------------------------------------------------------------------------
1 | namespace VocaDb.ResXFileCodeGenerator;
2 |
3 | public enum InnerClassVisibility
4 | {
5 | NotGenerated = 0,
6 | Public,
7 | Internal,
8 | Private,
9 | Protected,
10 | SameAsOuter
11 | }
12 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/IsExternalInit.cs:
--------------------------------------------------------------------------------
1 | namespace System.Runtime.CompilerServices;
2 |
3 | internal sealed class IsExternalInit { }
4 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/NullableAttributes.cs:
--------------------------------------------------------------------------------
1 | // https://github.com/dotnet/runtime/blob/527f9ae88a0ee216b44d556f9bdc84037fe0ebda/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/NullableAttributes.cs
2 |
3 | #pragma warning disable
4 | #define INTERNAL_NULLABLE_ATTRIBUTES
5 |
6 | // Licensed to the .NET Foundation under one or more agreements.
7 | // The .NET Foundation licenses this file to you under the MIT license.
8 |
9 | namespace System.Diagnostics.CodeAnalysis
10 | {
11 | #if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48
12 | /// Specifies that null is allowed as an input even if the corresponding type disallows it.
13 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
14 | #if SYSTEM_PRIVATE_CORELIB
15 | public
16 | #else
17 | internal
18 | #endif
19 | sealed class AllowNullAttribute : Attribute
20 | { }
21 |
22 | /// Specifies that null is disallowed as an input even if the corresponding type allows it.
23 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)]
24 | #if SYSTEM_PRIVATE_CORELIB
25 | public
26 | #else
27 | internal
28 | #endif
29 | sealed class DisallowNullAttribute : Attribute
30 | { }
31 |
32 | /// Specifies that an output may be null even if the corresponding type disallows it.
33 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
34 | #if SYSTEM_PRIVATE_CORELIB
35 | public
36 | #else
37 | internal
38 | #endif
39 | sealed class MaybeNullAttribute : Attribute
40 | { }
41 |
42 | /// Specifies that an output will not be null even if the corresponding type allows it. Specifies that an input argument was not null when the call returns.
43 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)]
44 | #if SYSTEM_PRIVATE_CORELIB
45 | public
46 | #else
47 | internal
48 | #endif
49 | sealed class NotNullAttribute : Attribute
50 | { }
51 |
52 | /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it.
53 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
54 | #if SYSTEM_PRIVATE_CORELIB
55 | public
56 | #else
57 | internal
58 | #endif
59 | sealed class MaybeNullWhenAttribute : Attribute
60 | {
61 | /// Initializes the attribute with the specified return value condition.
62 | ///
63 | /// The return value condition. If the method returns this value, the associated parameter may be null.
64 | ///
65 | public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
66 |
67 | /// Gets the return value condition.
68 | public bool ReturnValue { get; }
69 | }
70 |
71 | /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it.
72 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
73 | #if SYSTEM_PRIVATE_CORELIB
74 | public
75 | #else
76 | internal
77 | #endif
78 | sealed class NotNullWhenAttribute : Attribute
79 | {
80 | /// Initializes the attribute with the specified return value condition.
81 | ///
82 | /// The return value condition. If the method returns this value, the associated parameter will not be null.
83 | ///
84 | public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue;
85 |
86 | /// Gets the return value condition.
87 | public bool ReturnValue { get; }
88 | }
89 |
90 | /// Specifies that the output will be non-null if the named parameter is non-null.
91 | [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)]
92 | #if SYSTEM_PRIVATE_CORELIB
93 | public
94 | #else
95 | internal
96 | #endif
97 | sealed class NotNullIfNotNullAttribute : Attribute
98 | {
99 | /// Initializes the attribute with the associated parameter name.
100 | ///
101 | /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null.
102 | ///
103 | public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName;
104 |
105 | /// Gets the associated parameter name.
106 | public string ParameterName { get; }
107 | }
108 |
109 | /// Applied to a method that will never return under any circumstance.
110 | [AttributeUsage(AttributeTargets.Method, Inherited = false)]
111 | #if SYSTEM_PRIVATE_CORELIB
112 | public
113 | #else
114 | internal
115 | #endif
116 | sealed class DoesNotReturnAttribute : Attribute
117 | { }
118 |
119 | /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value.
120 | [AttributeUsage(AttributeTargets.Parameter, Inherited = false)]
121 | #if SYSTEM_PRIVATE_CORELIB
122 | public
123 | #else
124 | internal
125 | #endif
126 | sealed class DoesNotReturnIfAttribute : Attribute
127 | {
128 | /// Initializes the attribute with the specified parameter value.
129 | ///
130 | /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to
131 | /// the associated parameter matches this value.
132 | ///
133 | public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue;
134 |
135 | /// Gets the condition parameter value.
136 | public bool ParameterValue { get; }
137 | }
138 | #endif
139 |
140 | #if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET46 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48
141 | /// Specifies that the method or property will ensure that the listed field and property members have not-null values.
142 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
143 | #if SYSTEM_PRIVATE_CORELIB
144 | public
145 | #else
146 | internal
147 | #endif
148 | sealed class MemberNotNullAttribute : Attribute
149 | {
150 | /// Initializes the attribute with a field or property member.
151 | ///
152 | /// The field or property member that is promised to be not-null.
153 | ///
154 | public MemberNotNullAttribute(string member) => Members = new[] { member };
155 |
156 | /// Initializes the attribute with the list of field and property members.
157 | ///
158 | /// The list of field and property members that are promised to be not-null.
159 | ///
160 | public MemberNotNullAttribute(params string[] members) => Members = members;
161 |
162 | /// Gets field or property member names.
163 | public string[] Members { get; }
164 | }
165 |
166 | /// Specifies that the method or property will ensure that the listed field and property members have not-null values when returning with the specified return value condition.
167 | [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
168 | #if SYSTEM_PRIVATE_CORELIB
169 | public
170 | #else
171 | internal
172 | #endif
173 | sealed class MemberNotNullWhenAttribute : Attribute
174 | {
175 | /// Initializes the attribute with the specified return value condition and a field or property member.
176 | ///
177 | /// The return value condition. If the method returns this value, the associated parameter will not be null.
178 | ///
179 | ///
180 | /// The field or property member that is promised to be not-null.
181 | ///
182 | public MemberNotNullWhenAttribute(bool returnValue, string member)
183 | {
184 | ReturnValue = returnValue;
185 | Members = new[] { member };
186 | }
187 |
188 | /// Initializes the attribute with the specified return value condition and list of field and property members.
189 | ///
190 | /// The return value condition. If the method returns this value, the associated parameter will not be null.
191 | ///
192 | ///
193 | /// The list of field and property members that are promised to be not-null.
194 | ///
195 | public MemberNotNullWhenAttribute(bool returnValue, params string[] members)
196 | {
197 | ReturnValue = returnValue;
198 | Members = members;
199 | }
200 |
201 | /// Gets the return value condition.
202 | public bool ReturnValue { get; }
203 |
204 | /// Gets field or property member names.
205 | public string[] Members { get; }
206 | }
207 | #endif
208 | }
209 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "Debug": {
4 | "commandName": "DebugRoslynComponent",
5 | "targetProject": "..\\VocaDb.ResXFileCodeGenerator.Tests\\VocaDb.ResXFileCodeGenerator.Tests.csproj"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/SourceGenerator.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.CodeAnalysis;
2 |
3 | namespace VocaDb.ResXFileCodeGenerator;
4 |
5 | [Generator]
6 | public class SourceGenerator : IIncrementalGenerator
7 | {
8 | private static readonly IGenerator s_generator = new StringBuilderGenerator();
9 |
10 | public void Initialize(IncrementalGeneratorInitializationContext context)
11 | {
12 | var globalOptions = context.AnalyzerConfigOptionsProvider.Select(GlobalOptions.Select);
13 |
14 | // Note: Each Resx file will get a hash (random guid) so we can easily differentiate in the pipeline when the file changed or just some options
15 | var allResxFiles = context.AdditionalTextsProvider.Where(static af => af.Path.EndsWith(".resx"))
16 | .Select(static (f, _) => new AdditionalTextWithHash(f, Guid.NewGuid()));
17 |
18 | var monitor = allResxFiles.Collect().SelectMany(static (x, _) => GroupResxFiles.Group(x));
19 |
20 | var inputs = monitor
21 | .Combine(globalOptions)
22 | .Combine(context.AnalyzerConfigOptionsProvider)
23 | .Select(static (x, _) => FileOptions.Select(
24 | file: x.Left.Left,
25 | options: x.Right,
26 | globalOptions: x.Left.Right
27 | ))
28 | .Where(static x => x.IsValid);
29 |
30 | context.RegisterSourceOutput(inputs, (ctx, file) =>
31 | {
32 | var (generatedFileName, sourceCode, errorsAndWarnings) =
33 | s_generator.Generate(file, ctx.CancellationToken);
34 | foreach (var sourceErrorsAndWarning in errorsAndWarnings)
35 | {
36 | ctx.ReportDiagnostic(sourceErrorsAndWarning);
37 | }
38 |
39 | ctx.AddSource(generatedFileName, sourceCode);
40 | });
41 |
42 | var detectAllCombosOfResx = monitor.Collect().SelectMany((x, _) => GroupResxFiles.DetectChildCombos(x));
43 | context.RegisterSourceOutput(detectAllCombosOfResx, (ctx, combo) =>
44 | {
45 | var (generatedFileName, sourceCode, errorsAndWarnings) =
46 | s_generator.Generate(combo, ctx.CancellationToken);
47 | foreach (var sourceErrorsAndWarning in errorsAndWarnings)
48 | {
49 | ctx.ReportDiagnostic(sourceErrorsAndWarning);
50 | }
51 |
52 | ctx.AddSource(generatedFileName, sourceCode);
53 | });
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/StringBuilderExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | namespace VocaDb.ResXFileCodeGenerator;
4 |
5 | internal static class StringBuilderExtensions
6 | {
7 | public static void AppendLineLF(this StringBuilder builder)
8 | {
9 | builder.Append('\n');
10 | }
11 |
12 | public static void AppendLineLF(this StringBuilder builder, string value)
13 | {
14 | builder.Append(value);
15 | builder.AppendLineLF();
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/StringBuilderGenerator.ComboGenerator.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Immutable;
2 | using System.Globalization;
3 | using System.Text;
4 | using Microsoft.CodeAnalysis;
5 | using Microsoft.CodeAnalysis.CSharp;
6 | using Microsoft.CodeAnalysis.Text;
7 |
8 | namespace VocaDb.ResXFileCodeGenerator
9 | {
10 |
11 | public sealed partial class StringBuilderGenerator : IGenerator
12 | {
13 | static readonly Dictionary> s_allChildren = new();
14 |
15 | ///
16 | /// Build all cultureinfo children
17 | ///
18 | static StringBuilderGenerator()
19 | {
20 | var all = CultureInfo.GetCultures(CultureTypes.AllCultures);
21 |
22 | foreach (var cultureInfo in all)
23 | {
24 | if (cultureInfo.LCID == 4096 || cultureInfo.IsNeutralCulture || cultureInfo.Name.IsNullOrEmpty())
25 | {
26 | continue;
27 | }
28 | var parent = cultureInfo.Parent;
29 | if (!s_allChildren.TryGetValue(parent.LCID, out var v))
30 | s_allChildren[parent.LCID] = v = new List();
31 | v.Add(cultureInfo.LCID);
32 | }
33 | }
34 |
35 | public (
36 | string GeneratedFileName,
37 | string SourceCode,
38 | IEnumerable ErrorsAndWarnings
39 | ) Generate(
40 | CultureInfoCombo combo,
41 | CancellationToken cancellationToken
42 | )
43 | {
44 | var definedLanguages = combo.GetDefinedLanguages();
45 | var builder = GetBuilder("VocaDb.ResXFileCodeGenerator");
46 |
47 | builder.AppendLineLF("internal static partial class Helpers");
48 | builder.AppendLineLF("{");
49 |
50 | builder.Append(" public static string GetString_");
51 | var functionNamePostFix = FunctionNamePostFix(definedLanguages);
52 | builder.Append(functionNamePostFix);
53 | builder.Append("(string fallback");
54 | foreach (var (name, _, _) in definedLanguages)
55 | {
56 | builder.Append(", ");
57 | builder.Append("string ");
58 | builder.Append(name);
59 | }
60 |
61 | builder.Append(") => ");
62 | builder.Append(Constants.SystemGlobalization);
63 | builder.AppendLineLF(".CultureInfo.CurrentUICulture.LCID switch");
64 | builder.AppendLineLF(" {");
65 | var already = new HashSet();
66 | foreach (var (name, lcid, _) in definedLanguages)
67 | {
68 | static IEnumerable FindParents(int toFind)
69 | {
70 | yield return toFind;
71 | if (!s_allChildren.TryGetValue(toFind, out var v))
72 | {
73 | yield break;
74 | }
75 |
76 | foreach (var parents in v)
77 | {
78 | yield return parents;
79 | }
80 | }
81 |
82 | var findParents = FindParents(lcid).Except(already).ToList();
83 | foreach (var parent in findParents)
84 | {
85 | already.Add(parent);
86 | builder.Append(" ");
87 | builder.Append(parent);
88 | builder.Append(" => ");
89 | builder.Append(name.Replace('-', '_'));
90 | builder.AppendLineLF(",");
91 | }
92 | }
93 |
94 | builder.AppendLineLF(" _ => fallback");
95 | builder.AppendLineLF(" };");
96 | builder.AppendLineLF("}");
97 |
98 | return (
99 | GeneratedFileName: "VocaDb.ResXFileCodeGenerator." + functionNamePostFix + ".g.cs",
100 | SourceCode: builder.ToString(),
101 | ErrorsAndWarnings: Array.Empty()
102 | );
103 | }
104 |
105 | private static string FunctionNamePostFix(
106 | IReadOnlyList<(string Name, int LCID, AdditionalTextWithHash FileWithHash)>? definedLanguages
107 | ) => string.Join("_", definedLanguages?.Select(x => x.LCID) ?? Array.Empty());
108 |
109 | private static void AppendCodeUsings(StringBuilder builder)
110 | {
111 | builder.AppendLineLF("using static VocaDb.ResXFileCodeGenerator.Helpers;");
112 | builder.AppendLineLF();
113 | }
114 |
115 | private void GenerateCode(
116 | FileOptions options,
117 | SourceText content,
118 | string indent,
119 | string containerClassName,
120 | StringBuilder builder,
121 | List errorsAndWarnings,
122 | CancellationToken cancellationToken
123 | )
124 | {
125 | var combo = new CultureInfoCombo(options.GroupedFile.SubFiles);
126 | var definedLanguages = combo.GetDefinedLanguages();
127 |
128 | var fallback = ReadResxFile(content);
129 | var subfiles = definedLanguages.Select(lang =>
130 | {
131 | var subcontent = lang.FileWithHash.File.GetText(cancellationToken);
132 | return subcontent is null
133 | ? null
134 | : ReadResxFile(subcontent)?
135 | .GroupBy(x => x.key)
136 | .ToImmutableDictionary(x => x.Key, x => x.First().value);
137 | }).ToList();
138 | if (fallback is null || subfiles.Any(x => x is null))
139 | {
140 | builder.AppendFormat("//could not read {0} or one of its children", options.GroupedFile.MainFile.File.Path);
141 | return;
142 | }
143 |
144 | var alreadyAddedMembers = new HashSet();
145 | foreach (var (key, value, line) in fallback)
146 | {
147 | cancellationToken.ThrowIfCancellationRequested();
148 | if (
149 | !GenerateMember(
150 | indent,
151 | builder,
152 | options,
153 | key,
154 | value,
155 | line,
156 | alreadyAddedMembers,
157 | errorsAndWarnings,
158 | containerClassName,
159 | out _
160 | )
161 | )
162 | {
163 | continue;
164 | }
165 |
166 | builder.Append(" => GetString_");
167 | builder.Append(FunctionNamePostFix(definedLanguages));
168 | builder.Append("(");
169 | builder.Append(SymbolDisplay.FormatLiteral(value, true));
170 |
171 | foreach (var xml in subfiles)
172 | {
173 | builder.Append(", ");
174 | if (!xml!.TryGetValue(key, out var langValue))
175 | langValue = value;
176 | builder.Append(SymbolDisplay.FormatLiteral(langValue, true));
177 | }
178 |
179 | builder.AppendLineLF(");");
180 | }
181 | }
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/StringBuilderGenerator.ResourceManager.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using System.Resources;
3 | using System.Text;
4 | using System.Xml;
5 | using Microsoft.CodeAnalysis;
6 | using Microsoft.CodeAnalysis.Text;
7 |
8 | namespace VocaDb.ResXFileCodeGenerator;
9 |
10 | public sealed partial class StringBuilderGenerator : IGenerator
11 | {
12 | private void GenerateResourceManager(
13 | FileOptions options,
14 | SourceText content,
15 | string indent,
16 | string containerClassName,
17 | StringBuilder builder,
18 | List errorsAndWarnings,
19 | CancellationToken cancellationToken
20 | )
21 | {
22 | GenerateResourceManagerMembers(builder, indent, containerClassName, options);
23 |
24 | var members = ReadResxFile(content);
25 | if (members is null)
26 | {
27 | return;
28 | }
29 |
30 | var alreadyAddedMembers = new HashSet() { Constants.CultureInfoVariable };
31 | foreach (var (key, value, line) in members)
32 | {
33 | cancellationToken.ThrowIfCancellationRequested();
34 | CreateMember(
35 | indent,
36 | builder,
37 | options,
38 | key,
39 | value,
40 | line,
41 | alreadyAddedMembers,
42 | errorsAndWarnings,
43 | containerClassName
44 | );
45 | }
46 | }
47 |
48 | private static void CreateMember(
49 | string indent,
50 | StringBuilder builder,
51 | FileOptions options,
52 | string name,
53 | string value,
54 | IXmlLineInfo line,
55 | HashSet alreadyAddedMembers,
56 | List errorsAndWarnings,
57 | string containerclassname
58 | )
59 | {
60 | if (!GenerateMember(indent, builder, options, name, value, line, alreadyAddedMembers, errorsAndWarnings, containerclassname, out var resourceAccessByName))
61 | {
62 | return;
63 | }
64 |
65 | if (resourceAccessByName)
66 | {
67 | builder.Append(" => ResourceManager.GetString(nameof(");
68 | builder.Append(name);
69 | builder.Append("), ");
70 | }
71 | else
72 | {
73 | builder.Append(@" => ResourceManager.GetString(""");
74 | builder.Append(name.Replace(@"""", @"\"""));
75 | builder.Append(@""", ");
76 | }
77 |
78 | builder.Append(Constants.CultureInfoVariable);
79 | builder.Append(")");
80 | builder.Append(options.NullForgivingOperators ? "!" : null);
81 | builder.AppendLineLF(";");
82 | }
83 |
84 | private static void AppendResourceManagerUsings(StringBuilder builder)
85 | {
86 | builder.Append("using ");
87 | builder.Append(Constants.SystemGlobalization);
88 | builder.AppendLineLF(";");
89 |
90 | builder.Append("using ");
91 | builder.Append(Constants.SystemResources);
92 | builder.AppendLineLF(";");
93 |
94 | builder.AppendLineLF();
95 | }
96 |
97 | private static void GenerateResourceManagerMembers(
98 | StringBuilder builder,
99 | string indent,
100 | string containerClassName,
101 | FileOptions options
102 | )
103 | {
104 | builder.Append(indent);
105 | builder.Append("private static ");
106 | builder.Append(nameof(ResourceManager));
107 | builder.Append("? ");
108 | builder.Append(Constants.s_resourceManagerVariable);
109 | builder.AppendLineLF(";");
110 |
111 | builder.Append(indent);
112 | builder.Append("public static ");
113 | builder.Append(nameof(ResourceManager));
114 | builder.Append(" ");
115 | builder.Append(Constants.ResourceManagerVariable);
116 | builder.Append(" => ");
117 | builder.Append(Constants.s_resourceManagerVariable);
118 | builder.Append(" ??= new ");
119 | builder.Append(nameof(ResourceManager));
120 | builder.Append("(\"");
121 | builder.Append(options.EmbeddedFilename);
122 | builder.Append("\", typeof(");
123 | builder.Append(containerClassName);
124 | builder.AppendLineLF(").Assembly);");
125 |
126 | builder.Append(indent);
127 | builder.Append("public ");
128 | builder.Append(options.StaticMembers ? "static " : string.Empty);
129 | builder.Append(nameof(CultureInfo));
130 | builder.Append("? ");
131 | builder.Append(Constants.CultureInfoVariable);
132 | builder.AppendLineLF(" { get; set; }");
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/StringBuilderGenerator.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 | using System.Text.RegularExpressions;
3 | using System.Web;
4 | using System.Xml;
5 | using System.Xml.Linq;
6 | using Microsoft.CodeAnalysis;
7 | using Microsoft.CodeAnalysis.Text;
8 |
9 | namespace VocaDb.ResXFileCodeGenerator;
10 |
11 | public sealed partial class StringBuilderGenerator : IGenerator
12 | {
13 | private static readonly Regex s_validMemberNamePattern = new(
14 | pattern: @"^[\p{L}\p{Nl}_][\p{Cf}\p{L}\p{Mc}\p{Mn}\p{Nd}\p{Nl}\p{Pc}]*$",
15 | options: RegexOptions.Compiled | RegexOptions.CultureInvariant
16 | );
17 |
18 | private static readonly Regex s_invalidMemberNameSymbols = new(
19 | pattern: @"[^\p{Cf}\p{L}\p{Mc}\p{Mn}\p{Nd}\p{Nl}\p{Pc}]",
20 | options: RegexOptions.Compiled | RegexOptions.CultureInvariant
21 | );
22 |
23 | private static readonly DiagnosticDescriptor s_duplicateWarning = new(
24 | id: "VocaDbResXFileCodeGenerator001",
25 | title: "Duplicate member",
26 | messageFormat: "Ignored added member '{0}'",
27 | category: "ResXFileCodeGenerator",
28 | defaultSeverity: DiagnosticSeverity.Warning,
29 | isEnabledByDefault: true
30 | );
31 |
32 | private static readonly DiagnosticDescriptor s_memberSameAsClassWarning = new(
33 | id: "VocaDbResXFileCodeGenerator002",
34 | title: "Member same name as class",
35 | messageFormat: "Ignored member '{0}' has same name as class",
36 | category: "ResXFileCodeGenerator",
37 | defaultSeverity: DiagnosticSeverity.Warning,
38 | isEnabledByDefault: true
39 | );
40 |
41 | private static readonly DiagnosticDescriptor s_memberWithStaticError = new(
42 | id: "VocaDbResXFileCodeGenerator003",
43 | title: "Incompatible settings",
44 | messageFormat: "Cannot have static members/class with an class instance",
45 | category: "ResXFileCodeGenerator",
46 | defaultSeverity: DiagnosticSeverity.Error,
47 | isEnabledByDefault: true
48 | );
49 |
50 | public (
51 | string GeneratedFileName,
52 | string SourceCode,
53 | IEnumerable ErrorsAndWarnings
54 | ) Generate(
55 | FileOptions options,
56 | CancellationToken cancellationToken = default
57 | )
58 | {
59 | var errorsAndWarnings = new List();
60 | var generatedFileName = $"{options.LocalNamespace}.{options.ClassName}.g.cs";
61 |
62 | var content = options.GroupedFile.MainFile.File.GetText(cancellationToken);
63 | if (content is null) return (generatedFileName, "//ERROR reading file:" + options.GroupedFile.MainFile.File.Path, errorsAndWarnings);
64 |
65 | // HACK: netstandard2.0 doesn't support improved interpolated strings?
66 | var builder = GetBuilder(options.CustomToolNamespace ?? options.LocalNamespace);
67 |
68 | if (options.UseVocaDbResManager)
69 | AppendCodeUsings(builder);
70 | else
71 | AppendResourceManagerUsings(builder);
72 |
73 | builder.Append(options.PublicClass ? "public" : "internal");
74 | builder.Append(options.StaticClass ? " static" : string.Empty);
75 | builder.Append(options.PartialClass ? " partial class " : " class ");
76 | builder.AppendLineLF(options.ClassName);
77 | builder.AppendLineLF("{");
78 |
79 | var indent = " ";
80 | string containerClassName = options.ClassName;
81 |
82 | if (options.InnerClassVisibility != InnerClassVisibility.NotGenerated)
83 | {
84 | containerClassName = string.IsNullOrEmpty(options.InnerClassName) ? "Resources" : options.InnerClassName;
85 | if (!string.IsNullOrEmpty(options.InnerClassInstanceName))
86 | {
87 | if (options.StaticClass || options.StaticMembers)
88 | {
89 | errorsAndWarnings.Add(Diagnostic.Create(
90 | descriptor: s_memberWithStaticError,
91 | location: Location.Create(
92 | filePath: options.GroupedFile.MainFile.File.Path,
93 | textSpan: new TextSpan(),
94 | lineSpan: new LinePositionSpan()
95 | )
96 | ));
97 | }
98 |
99 | builder.Append(indent);
100 | builder.Append("public ");
101 | builder.Append(containerClassName);
102 | builder.Append(" ");
103 | builder.Append(options.InnerClassInstanceName);
104 | builder.AppendLineLF(" { get; } = new();");
105 | builder.AppendLineLF();
106 | }
107 |
108 | builder.Append(indent);
109 | builder.Append(options.InnerClassVisibility == InnerClassVisibility.SameAsOuter
110 | ? options.PublicClass ? "public" : "internal"
111 | : options.InnerClassVisibility.ToString().ToLowerInvariant());
112 | builder.Append(options.StaticClass ? " static" : string.Empty);
113 | builder.Append(options.PartialClass ? " partial class " : " class ");
114 |
115 | builder.AppendLineLF(containerClassName);
116 | builder.Append(indent);
117 | builder.AppendLineLF("{");
118 |
119 | indent += " ";
120 |
121 | }
122 |
123 | if (options.UseVocaDbResManager)
124 | GenerateCode(options, content, indent, containerClassName, builder, errorsAndWarnings, cancellationToken);
125 | else
126 | GenerateResourceManager(options, content, indent, containerClassName, builder, errorsAndWarnings, cancellationToken);
127 |
128 | if (options.InnerClassVisibility != InnerClassVisibility.NotGenerated)
129 | {
130 | builder.AppendLineLF(" }");
131 | }
132 |
133 | builder.AppendLineLF("}");
134 |
135 | return (
136 | GeneratedFileName: generatedFileName,
137 | SourceCode: builder.ToString(),
138 | ErrorsAndWarnings: errorsAndWarnings
139 | );
140 | }
141 |
142 | private static IEnumerable<(string key, string value, IXmlLineInfo line)>? ReadResxFile(SourceText content)
143 | {
144 | using var reader = new StringReader(content.ToString());
145 |
146 | if (XDocument.Load(reader, LoadOptions.SetLineInfo).Root is { } element)
147 | return element
148 | .Descendants()
149 | .Where(static data => data.Name == "data")
150 | .Select(static data => (
151 | key: data.Attribute("name")!.Value,
152 | value: data.Descendants("value").First().Value,
153 | line: (IXmlLineInfo)data.Attribute("name")!
154 | ));
155 |
156 | return null;
157 | }
158 |
159 | private static bool GenerateMember(
160 | string indent,
161 | StringBuilder builder,
162 | FileOptions options,
163 | string name,
164 | string neutralValue,
165 | IXmlLineInfo line,
166 | HashSet alreadyAddedMembers,
167 | List errorsAndWarnings,
168 | string containerclassname,
169 | out bool resourceAccessByName
170 | )
171 | {
172 | string memberName;
173 |
174 | if (s_validMemberNamePattern.IsMatch(name))
175 | {
176 | memberName = name;
177 | resourceAccessByName = true;
178 | }
179 | else
180 | {
181 | memberName = s_invalidMemberNameSymbols.Replace(name, "_");
182 | resourceAccessByName = false;
183 | }
184 |
185 | static Location GetMemberLocation(FileOptions fileOptions, IXmlLineInfo line, string memberName) =>
186 | Location.Create(
187 | filePath: fileOptions.GroupedFile.MainFile.File.Path,
188 | textSpan: new TextSpan(),
189 | lineSpan: new LinePositionSpan(
190 | start: new LinePosition(line.LineNumber - 1, line.LinePosition - 1),
191 | end: new LinePosition(line.LineNumber - 1, line.LinePosition - 1 + memberName.Length)
192 | )
193 | );
194 |
195 | if (!alreadyAddedMembers.Add(memberName))
196 | {
197 | errorsAndWarnings.Add(Diagnostic.Create(
198 | descriptor: s_duplicateWarning,
199 | location: GetMemberLocation(options, line, memberName), memberName
200 | ));
201 | return false;
202 | }
203 |
204 | if (memberName == containerclassname)
205 | {
206 | errorsAndWarnings.Add(Diagnostic.Create(
207 | descriptor: s_memberSameAsClassWarning,
208 | location: GetMemberLocation(options, line, memberName), memberName
209 | ));
210 | return false;
211 | }
212 |
213 | builder.AppendLineLF();
214 |
215 | builder.Append(indent);
216 | builder.AppendLineLF("/// ");
217 |
218 | builder.Append(indent);
219 | builder.Append("/// Looks up a localized string similar to ");
220 | builder.Append(HttpUtility.HtmlEncode(neutralValue.Trim().Replace("\r\n", "\n").Replace("\r", "\n")
221 | .Replace("\n", Environment.NewLine + indent + "/// ")));
222 | builder.AppendLineLF(".");
223 |
224 | builder.Append(indent);
225 | builder.AppendLineLF("/// ");
226 |
227 | builder.Append(indent);
228 | builder.Append("public ");
229 | builder.Append(options.StaticMembers ? "static " : string.Empty);
230 | builder.Append("string");
231 | builder.Append(options.NullForgivingOperators ? null : "?");
232 | builder.Append(" ");
233 | builder.Append(memberName);
234 | return true;
235 | }
236 |
237 | private static StringBuilder GetBuilder(string withnamespace)
238 | {
239 | var builder = new StringBuilder();
240 |
241 | builder.AppendLineLF(Constants.AutoGeneratedHeader);
242 | builder.AppendLineLF("#nullable enable");
243 |
244 | builder.Append("namespace ");
245 | builder.Append(withnamespace);
246 | builder.AppendLineLF(";");
247 |
248 | return builder;
249 | }
250 |
251 | }
252 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/StringExtensions.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics.CodeAnalysis;
2 |
3 | namespace VocaDb.ResXFileCodeGenerator;
4 |
5 | internal static class StringExtensions
6 | {
7 | public static bool IsNullOrEmpty([NotNullWhen(false)] this string? value) => string.IsNullOrEmpty(value);
8 |
9 | public static string? NullIfEmpty(this string? value) => value.IsNullOrEmpty() ? null : value;
10 | }
11 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/Utilities.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using System.Text.RegularExpressions;
3 | using Microsoft.CodeAnalysis.CSharp.Syntax;
4 |
5 | namespace VocaDb.ResXFileCodeGenerator;
6 |
7 | public static class Utilities
8 | {
9 | // Code from: https://github.com/dotnet/ResXResourceManager/blob/0ec11bae232151400a5a8ca7b9835ac063c516d0/src/ResXManager.Model/ResourceManager.cs#L267
10 |
11 | private static bool IsValidLanguageName(string? languageName)
12 | {
13 | try
14 | {
15 | if (languageName.IsNullOrEmpty())
16 | {
17 | return false;
18 | }
19 |
20 | if (languageName.StartsWith("qps-", StringComparison.Ordinal))
21 | {
22 | return true;
23 | }
24 |
25 | var dash = languageName.IndexOf('-');
26 | if (dash >= 4 || (dash == -1 && languageName.Length >= 4))
27 | {
28 | return false;
29 | }
30 |
31 | var culture = new CultureInfo(languageName);
32 |
33 | while (!culture.IsNeutralCulture)
34 | {
35 | culture = culture.Parent;
36 | }
37 |
38 | return culture.LCID != 4096;
39 | }
40 | catch
41 | {
42 | return false;
43 | }
44 | }
45 |
46 | // Code from: https://github.com/dotnet/ResXResourceManager/blob/0ec11bae232151400a5a8ca7b9835ac063c516d0/src/ResXManager.Model/ProjectFileExtensions.cs#L77
47 |
48 | public static string GetBaseName(string filePath)
49 | {
50 | var name = Path.GetFileNameWithoutExtension(filePath);
51 | var innerExtension = Path.GetExtension(name);
52 | var languageName = innerExtension.TrimStart('.');
53 |
54 | return IsValidLanguageName(languageName) ? Path.GetFileNameWithoutExtension(name) : name;
55 | }
56 |
57 | // Code from: https://github.com/dotnet/ResXResourceManager/blob/c8b5798d760f202a1842a74191e6010c6e8bbbc0/src/ResXManager.VSIX/Visuals/MoveToResourceViewModel.cs#L120
58 |
59 | public static string GetLocalNamespace(
60 | string? resxPath,
61 | string? targetPath,
62 | string projectPath,
63 | string projectName,
64 | string? rootNamespace
65 | )
66 | {
67 | try
68 | {
69 | if (resxPath is null)
70 | {
71 | return string.Empty;
72 | }
73 |
74 | var resxFolder = Path.GetDirectoryName(resxPath);
75 | var projectFolder = Path.GetDirectoryName(projectPath);
76 | rootNamespace ??= string.Empty;
77 |
78 | if (resxFolder is null || projectFolder is null)
79 | {
80 | return string.Empty;
81 | }
82 |
83 | var localNamespace = string.Empty;
84 |
85 | if (!string.IsNullOrWhiteSpace(targetPath))
86 | {
87 | localNamespace = Path.GetDirectoryName(targetPath)
88 | .Trim(Path.DirectorySeparatorChar)
89 | .Trim(Path.AltDirectorySeparatorChar)
90 | .Replace(Path.DirectorySeparatorChar, '.')
91 | .Replace(Path.AltDirectorySeparatorChar, '.');
92 | }
93 | else if (resxFolder.StartsWith(projectFolder, StringComparison.OrdinalIgnoreCase))
94 | {
95 | localNamespace = resxFolder
96 | .Substring(projectFolder.Length)
97 | .Trim(Path.DirectorySeparatorChar)
98 | .Trim(Path.AltDirectorySeparatorChar)
99 | .Replace(Path.DirectorySeparatorChar, '.')
100 | .Replace(Path.AltDirectorySeparatorChar, '.');
101 | }
102 |
103 | if (string.IsNullOrEmpty(rootNamespace) && string.IsNullOrEmpty(localNamespace))
104 | {
105 | // If local namespace is empty, e.g file is in root project folder, root namespace set to empty
106 | // fallback to project name as a namespace
107 | localNamespace = SanitizeNamespace(projectName);
108 | }
109 | else
110 | {
111 | localNamespace = (string.IsNullOrEmpty(localNamespace)
112 | ? rootNamespace
113 | : $"{rootNamespace}.{SanitizeNamespace(localNamespace, false)}")
114 | .Trim('.');
115 | }
116 |
117 | return localNamespace;
118 | }
119 | catch (Exception)
120 | {
121 | return string.Empty;
122 | }
123 | }
124 |
125 | public static string GetClassNameFromPath(string resxFilePath)
126 | {
127 | // Fix issues with files that have names like xxx.aspx.resx
128 | var className = resxFilePath;
129 | while (className.Contains("."))
130 | {
131 | className = Path.GetFileNameWithoutExtension(className);
132 | }
133 |
134 | return className;
135 | }
136 |
137 | public static string SanitizeNamespace(string ns, bool sanitizeFirstChar = true)
138 | {
139 | if (string.IsNullOrEmpty(ns))
140 | {
141 | return ns;
142 | }
143 |
144 | // A namespace must contain only alphabetic characters, decimal digits, dots and underscores, and must begin with an alphabetic character or underscore (_)
145 | // In case there are invalid chars we'll use same logic as Visual Studio and replace them with underscore (_) and append underscore (_) if project does not start with alphabetic or underscore (_)
146 |
147 | var sanitizedNs = Regex
148 | .Replace(ns, @"[^a-zA-Z0-9_\.]", "_");
149 |
150 | // Handle folder containing multiple dots, e.g. 'test..test2' or starting, ending with dots
151 | sanitizedNs = Regex
152 | .Replace(sanitizedNs, @"\.+", ".");
153 |
154 | if (sanitizeFirstChar)
155 | {
156 | sanitizedNs = sanitizedNs.Trim('.');
157 | }
158 |
159 | return sanitizeFirstChar
160 | // Handle namespace starting with digit
161 | ? char.IsDigit(sanitizedNs[0]) ? $"_{sanitizedNs}" : sanitizedNs
162 | : sanitizedNs;
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/VocaDb.ResXFileCodeGenerator.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0
5 | latest
6 | enable
7 | true
8 | VocaDB
9 | MIT
10 | https://github.com/VocaDB/ResXFileCodeGenerator
11 | https://github.com/VocaDB/ResXFileCodeGenerator
12 | ResX Designer Source Generator.
13 | false
14 | false
15 | true
16 | $(NoWarn);NU5128
17 | true
18 | 3.2.1
19 | enable
20 | true
21 |
22 |
23 |
24 |
25 | true
26 | build\
27 |
28 |
29 |
30 |
31 |
32 | all
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/VocaDb.ResXFileCodeGenerator.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(TargetsForTfmSpecificContentInPackage);PackBuildOutputs
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/VocaDb.ResXFileCodeGenerator/build/VocaDb.ResXFileCodeGenerator.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(AdditionalFileItemNames);EmbeddedResource
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------