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