Skip to content

Commit 9911fdd

Browse files
authored
Strong api string localizers generator using incremental generator (#994)
* StrongApiStringLocalizersGenerator as an incremental generator * StrongApiStringLocalizersGenerator does not use ResourceServiceCollectionInstaller anymore * StrongApiStringLocalizersGenerator generates neutral values to code docs. * Fix StrongApiStringLocalizersGenerator unit test name * Code polishing * Removed SourceGeneratorAdapter * Align Microsoft.Extensions.Localization versions * StrongApiStringLocalizersGenerator - OS independent tests * StrongApiStringLocalizersGenerator - memory optimization * StrongApiStringLocalizersGenerator - works without csproj change, removed analyzer diagnostic
1 parent 1be9694 commit 9911fdd

26 files changed

+861
-311
lines changed

BlazorAppTest/BlazorAppTest.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,7 @@
2828
</Compile>
2929
</ItemGroup>
3030

31+
<ItemGroup>
32+
<AdditionalFiles Include="**\*.resx" />
33+
</ItemGroup>
3134
</Project>

BlazorAppTest/Resources/ResourcesServiceCollectionInstaller.cs

Lines changed: 0 additions & 6 deletions
This file was deleted.

Directory.Packages.props

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="$(AspNetCoreVersion8)" Condition="'$(TargetFramework)' == 'net8.0'" />
2323
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="$(AspNetCoreVersion9)" />
2424
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="$(AspNetCoreVersion9)" />
25-
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.12.0" />
25+
<PackageVersion Include="Microsoft.CodeAnalysis" Version="4.8.0" />
26+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
27+
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.2" />
2628
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.1" />
2729
<PackageVersion Include="Microsoft.Extensions.Localization" Version="9.0.1" />
2830
<PackageVersion Include="Microsoft.Extensions.Localization.Abstractions" Version="9.0.1" />

Havit.Blazor.sln

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Havit.Blazor.TestApp", "Hav
7878
EndProject
7979
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Havit.Blazor.TestApp.Client", "Havit.Blazor.TestApp\Havit.Blazor.TestApp.Client\Havit.Blazor.TestApp.Client.csproj", "{38D87399-13C1-4C86-9343-712EAE6095F0}"
8080
EndProject
81+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Havit.SourceGenerators.StrongApiStringLocalizers.Tests", "Havit.SourceGenerators.StrongApiStringLocalizers.Tests\Havit.SourceGenerators.StrongApiStringLocalizers.Tests.csproj", "{9236499E-62FF-4C2F-92A0-408143D67E72}"
82+
EndProject
8183
Global
8284
GlobalSection(SolutionConfigurationPlatforms) = preSolution
8385
Debug|Any CPU = Debug|Any CPU
@@ -180,6 +182,10 @@ Global
180182
{38D87399-13C1-4C86-9343-712EAE6095F0}.Debug|Any CPU.Build.0 = Debug|Any CPU
181183
{38D87399-13C1-4C86-9343-712EAE6095F0}.Release|Any CPU.ActiveCfg = Release|Any CPU
182184
{38D87399-13C1-4C86-9343-712EAE6095F0}.Release|Any CPU.Build.0 = Release|Any CPU
185+
{9236499E-62FF-4C2F-92A0-408143D67E72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
186+
{9236499E-62FF-4C2F-92A0-408143D67E72}.Debug|Any CPU.Build.0 = Debug|Any CPU
187+
{9236499E-62FF-4C2F-92A0-408143D67E72}.Release|Any CPU.ActiveCfg = Release|Any CPU
188+
{9236499E-62FF-4C2F-92A0-408143D67E72}.Release|Any CPU.Build.0 = Release|Any CPU
183189
EndGlobalSection
184190
GlobalSection(SolutionProperties) = preSolution
185191
HideSolutionNode = FALSE
@@ -196,6 +202,7 @@ Global
196202
{E64B4752-B697-4B5A-91F0-8A8581B1ABE5} = {79C29E4F-EF98-4F43-9782-B0D58A533C4D}
197203
{F6A546C8-60C3-47AC-A58B-66E3513DCC7A} = {79C29E4F-EF98-4F43-9782-B0D58A533C4D}
198204
{45BC138B-4F00-43DD-BF5D-D2FC36972857} = {D7B56FC7-6322-4B66-B34F-A972805BF740}
205+
{9236499E-62FF-4C2F-92A0-408143D67E72} = {201D627C-BC35-4971-95D0-5BA656E771F6}
199206
EndGlobalSection
200207
GlobalSection(ExtensibilityGlobals) = postSolution
201208
SolutionGuid = {BA2F0FE2-9DA0-4C29-8772-0802E2E67119}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<root>
3+
<!--
4+
Microsoft ResX Schema
5+
6+
Version 2.0
7+
8+
The primary goals of this format is to allow a simple XML format
9+
that is mostly human readable. The generation and parsing of the
10+
various data types are done through the TypeConverter classes
11+
associated with the data types.
12+
13+
Example:
14+
15+
... ado.net/XML headers & schema ...
16+
<resheader name="resmimetype">text/microsoft-resx</resheader>
17+
<resheader name="version">2.0</resheader>
18+
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
19+
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
20+
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
21+
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
22+
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
23+
<value>[base64 mime encoded serialized .NET Framework object]</value>
24+
</data>
25+
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
26+
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
27+
<comment>This is a comment</comment>
28+
</data>
29+
30+
There are any number of "resheader" rows that contain simple
31+
name/value pairs.
32+
33+
Each data row contains a name, and value. The row also contains a
34+
type or mimetype. Type corresponds to a .NET class that support
35+
text/value conversion through the TypeConverter architecture.
36+
Classes that don't support this are serialized and stored with the
37+
mimetype set.
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
41+
extensible. For a given mimetype the value must be set accordingly:
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
45+
read any of the formats listed below.
46+
47+
mimetype: application/x-microsoft.net.object.binary.base64
48+
value : The object must be serialized with
49+
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
50+
: and then encoded with base64 encoding.
51+
52+
mimetype: application/x-microsoft.net.object.soap.base64
53+
value : The object must be serialized with
54+
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
55+
: and then encoded with base64 encoding.
56+
57+
mimetype: application/x-microsoft.net.object.bytearray.base64
58+
value : The object must be serialized into a byte array
59+
: using a System.ComponentModel.TypeConverter
60+
: and then encoded with base64 encoding.
61+
-->
62+
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
63+
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
64+
<xsd:element name="root" msdata:IsDataSet="true">
65+
<xsd:complexType>
66+
<xsd:choice maxOccurs="unbounded">
67+
<xsd:element name="metadata">
68+
<xsd:complexType>
69+
<xsd:sequence>
70+
<xsd:element name="value" type="xsd:string" minOccurs="0" />
71+
</xsd:sequence>
72+
<xsd:attribute name="name" use="required" type="xsd:string" />
73+
<xsd:attribute name="type" type="xsd:string" />
74+
<xsd:attribute name="mimetype" type="xsd:string" />
75+
<xsd:attribute ref="xml:space" />
76+
</xsd:complexType>
77+
</xsd:element>
78+
<xsd:element name="assembly">
79+
<xsd:complexType>
80+
<xsd:attribute name="alias" type="xsd:string" />
81+
<xsd:attribute name="name" type="xsd:string" />
82+
</xsd:complexType>
83+
</xsd:element>
84+
<xsd:element name="data">
85+
<xsd:complexType>
86+
<xsd:sequence>
87+
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
88+
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
89+
</xsd:sequence>
90+
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
91+
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
92+
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
93+
<xsd:attribute ref="xml:space" />
94+
</xsd:complexType>
95+
</xsd:element>
96+
<xsd:element name="resheader">
97+
<xsd:complexType>
98+
<xsd:sequence>
99+
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
100+
</xsd:sequence>
101+
<xsd:attribute name="name" type="xsd:string" use="required" />
102+
</xsd:complexType>
103+
</xsd:element>
104+
</xsd:choice>
105+
</xsd:complexType>
106+
</xsd:element>
107+
</xsd:schema>
108+
<resheader name="resmimetype">
109+
<value>text/microsoft-resx</value>
110+
</resheader>
111+
<resheader name="version">
112+
<value>2.0</value>
113+
</resheader>
114+
<resheader name="reader">
115+
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
116+
</resheader>
117+
<resheader name="writer">
118+
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
119+
</resheader>
120+
<data name="CzechAndHtml" xml:space="preserve">
121+
<value>Čeština je &lt;b&gt;skvělá&lt;/b&gt;!</value>
122+
</data>
123+
<data name="HelloWorld" xml:space="preserve">
124+
<value>Hello world!!!</value>
125+
<comment>Hello world resource comment.</comment>
126+
</data>
127+
</root>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net9.0</TargetFrameworks>
5+
<IsPackable>false</IsPackable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<EnableMSTestRunner>true</EnableMSTestRunner>
8+
<OutputType>Exe</OutputType>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<EmbeddedResource Remove="Global.resx" />
13+
</ItemGroup>
14+
15+
<ItemGroup>
16+
<Content Include="Global.resx">
17+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
18+
</Content>
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
23+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" />
24+
<PackageReference Include="MSTest" />
25+
</ItemGroup>
26+
27+
<ItemGroup>
28+
<ProjectReference Include="..\Havit.SourceGenerators.StrongApiStringLocalizers\Havit.SourceGenerators.StrongApiStringLocalizers.csproj" />
29+
</ItemGroup>
30+
31+
<ItemGroup>
32+
<Using Include="Microsoft.VisualStudio.TestTools.UnitTesting" />
33+
</ItemGroup>
34+
35+
</Project>
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
using System.Collections.Immutable;
2+
using Microsoft.CodeAnalysis.Testing;
3+
using Microsoft.CodeAnalysis.Text;
4+
using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel;
5+
6+
namespace Havit.SourceGenerators.StrongApiStringLocalizers.Tests;
7+
8+
[TestClass]
9+
public class StrongApiStringLocalizersGeneratorTests
10+
{
11+
[TestMethod]
12+
public async Task StrongApiStringLocalizersGenerator_Test()
13+
{
14+
// Arrange
15+
16+
using var globalResxStream = File.OpenRead("Global.resx");
17+
18+
string projectDir = Environment.CurrentDirectory;
19+
20+
var test = new Microsoft.CodeAnalysis.CSharp.Testing.CSharpSourceGeneratorTest<StrongApiStringLocalizersGenerator, Microsoft.CodeAnalysis.Testing.DefaultVerifier>
21+
{
22+
TestState =
23+
{
24+
AnalyzerConfigFiles =
25+
{
26+
($"/.editorconfig", $@"""
27+
is_global=true
28+
build_property.RootNamespace = MyApp.Resources
29+
build_property.ProjectDir = {projectDir}
30+
""")
31+
}
32+
},
33+
ReferenceAssemblies = ReferenceAssemblies.Net
34+
.Net90
35+
.AddPackages(ImmutableArray.Create(
36+
new PackageIdentity("Microsoft.Extensions.Localization", "9.0.1"))) // we are using IStringLocalizer from this package in the generated code
37+
};
38+
39+
// resource file
40+
test.TestState.AdditionalFiles.Add((Path.Combine(projectDir, "MyResources", "Global.resx"), SourceText.From(globalResxStream)));
41+
42+
// EXPECTED OUTPUT
43+
44+
test.TestState.GeneratedSources.Add((typeof(StrongApiStringLocalizersGenerator), "MyApp.Resources.MyResources.IGlobalLocalizer.g.cs", @"// <auto-generated />
45+
46+
namespace MyApp.Resources.MyResources;
47+
48+
using System.CodeDom.Compiler;
49+
using Microsoft.Extensions.Localization;
50+
51+
[GeneratedCode(""Havit.SourceGenerators.StrongApiStringLocalizers.StrongApiStringLocalizersGenerator"", ""2.0.0.0"")]
52+
public interface IGlobalLocalizer : IStringLocalizer
53+
{
54+
/// <summary>
55+
/// Čeština je &lt;b&gt;skvělá&lt;/b&gt;!
56+
/// </summary>
57+
LocalizedString CzechAndHtml { get; }
58+
59+
/// <summary>
60+
/// Hello world resource comment.
61+
/// </summary>
62+
LocalizedString HelloWorld { get; }
63+
64+
}
65+
"));
66+
67+
// TestProject - defined by the TestState implementation
68+
test.TestState.GeneratedSources.Add((typeof(StrongApiStringLocalizersGenerator), $"MyApp.Resources.MyResources.GlobalLocalizer.g.cs", @"// <auto-generated />
69+
70+
namespace MyApp.Resources.MyResources;
71+
72+
using System.CodeDom.Compiler;
73+
using System.Collections.Generic;
74+
using Microsoft.Extensions.Localization;
75+
76+
[GeneratedCode(""Havit.SourceGenerators.StrongApiStringLocalizers.StrongApiStringLocalizersGenerator"", ""2.0.0.0"")]
77+
public class GlobalLocalizer : IGlobalLocalizer
78+
{
79+
private readonly IStringLocalizer _localizer;
80+
81+
public GlobalLocalizer(IStringLocalizerFactory stringLocalizerFactory)
82+
{
83+
_localizer = stringLocalizerFactory.Create(""MyResources.Global"", ""TestProject"");
84+
}
85+
86+
/// <summary>
87+
/// Čeština je &lt;b&gt;skvělá&lt;/b&gt;!
88+
/// </summary>
89+
public LocalizedString CzechAndHtml => _localizer[""CzechAndHtml""];
90+
91+
/// <summary>
92+
/// Hello world resource comment.
93+
/// </summary>
94+
public LocalizedString HelloWorld => _localizer[""HelloWorld""];
95+
96+
LocalizedString IStringLocalizer.this[string name] => _localizer[name];
97+
LocalizedString IStringLocalizer.this[string name, params object[] arguments] => _localizer[name, arguments];
98+
IEnumerable<LocalizedString> IStringLocalizer.GetAllStrings(bool includeParentCultures) => _localizer.GetAllStrings(includeParentCultures);
99+
}
100+
"));
101+
102+
test.TestState.GeneratedSources.Add((typeof(StrongApiStringLocalizersGenerator), "MyApp.Resources.ServiceCollectionExtensions.g.cs", @"// <auto-generated />
103+
104+
namespace MyApp.Resources;
105+
106+
using System.CodeDom.Compiler;
107+
using Microsoft.Extensions.DependencyInjection;
108+
using Microsoft.Extensions.Localization;
109+
110+
[GeneratedCode(""Havit.SourceGenerators.StrongApiStringLocalizers.StrongApiStringLocalizersGenerator"", ""2.0.0.0"")]
111+
public static class ServiceCollectionExtensions
112+
{
113+
public static IServiceCollection AddGeneratedResourceWrappers(this IServiceCollection services)
114+
{
115+
services.AddTransient<MyApp.Resources.MyResources.IGlobalLocalizer, MyApp.Resources.MyResources.GlobalLocalizer>();
116+
return services;
117+
}
118+
}
119+
"));
120+
121+
// Act + Assert
122+
await test.RunAsync();
123+
}
124+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
; Shipped analyzer releases
2+
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
3+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
; Unshipped analyzer release
2+
; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
3+
4+
### New Rules
5+
6+
Rule ID | Category | Severity | Notes
7+
--------|----------|----------|--------------------
8+
HLG1002 | Usage | Warning | Cannot parse RESX file

Havit.SourceGenerators.StrongApiStringLocalizers/BuilderExtensions.cs

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)