diff --git a/README.md b/README.md
index 6ec1d62..fc22aef 100644
--- a/README.md
+++ b/README.md
@@ -50,22 +50,7 @@ Without it, such a project is reported as `RESWP0005` and no code is generated f
### Injectable resource interfaces
-Resource classes are static by default. Projects that inject localization into view models or services can opt into an interface for every generated resource class:
-
-```xml
-
- true
-
-```
-
-For `Resources.resw`, ReswPlus then generates `IResources` and makes `Resources` a sealed, instantiable implementation while retaining its existing static members. Existing calls such as `Resources.WelcomeTitle` keep working. The instance members are explicit interface implementations, so dependency-injected code accesses them through `IResources`:
-
-```csharp
-IResources resources = new Resources();
-var title = resources.WelcomeTitle;
-```
-
-The interface includes `GetString`, regular resource properties, and all generated formatting, plural, and variant overloads.
+🗨 [How to inject generated resources](docs/Injectable-resource-providers.md)
### Generator performance diagnostics
diff --git a/docs/Injectable-resource-providers.md b/docs/Injectable-resource-providers.md
new file mode 100644
index 0000000..43290cf
--- /dev/null
+++ b/docs/Injectable-resource-providers.md
@@ -0,0 +1,78 @@
+# Injectable resource providers
+
+ReswPlus generates an interface and provider for each resource file so that view models and services can
+receive localized resources through dependency injection. The existing static resource API remains unchanged.
+
+## Generated types
+
+The generated type names follow the resource file name:
+
+| Resource file | Static API | Injectable API | Default implementation |
+| --- | --- | --- | --- |
+| `Resources.resw` | `Resources` | `IResources` | `ResourcesProvider` |
+| `Errors.resw` | `Errors` | `IErrors` | `ErrorsProvider` |
+
+The interface includes:
+
+- `GetString`
+- regular resource properties
+- formatted resource methods
+- plural methods
+- variant and plural-variant overloads
+
+The provider delegates every interface member to the existing static API. It does not create a separate
+resource-loading path or cache.
+
+## Use constructor injection
+
+Receive the generated interface instead of referring to the static class:
+
+```csharp
+public sealed class WelcomeViewModel
+{
+ private readonly IResources _resources;
+
+ public WelcomeViewModel(IResources resources)
+ {
+ _resources = resources;
+ }
+
+ public string Title => _resources.WelcomeTitle;
+}
+```
+
+Register the generated provider with the dependency-injection container used by the application. For
+`Microsoft.Extensions.DependencyInjection`, register it as follows:
+
+```csharp
+services.AddSingleton();
+```
+
+The provider implements the interface explicitly, so consume a `ResourcesProvider` through `IResources`.
+Tests can supply a stub, fake, or mock implementation of the same interface.
+
+## Compatibility with the static API
+
+The generated resource class remains static:
+
+```csharp
+var title = Resources.WelcomeTitle;
+```
+
+Adding the injectable types therefore does not change existing call sites, construction behavior, or the
+shape of the static resource class.
+
+## Disable generation
+
+Interfaces and providers are generated by default. A project that does not want the additional public types
+can disable them:
+
+```xml
+
+ false
+
+```
+
+With the option set to `false`, ReswPlus emits only the existing static resource class and markup extension.
+Resource keys such as `IResources` and `ResourcesProvider`, which would otherwise conflict with generated
+members, are available again.
diff --git a/nuget/ReswPlus.targets b/nuget/ReswPlus.targets
index 8299c9e..bf19306 100644
--- a/nuget/ReswPlus.targets
+++ b/nuget/ReswPlus.targets
@@ -14,7 +14,7 @@
$(AdditionalFileItemNames);PRIResource
false
- false
+ true
false
diff --git a/src/ReswPlus.SourceGenerator/Analysis/ReswResourceAnalyzer.cs b/src/ReswPlus.SourceGenerator/Analysis/ReswResourceAnalyzer.cs
index 3c043bb..5885557 100644
--- a/src/ReswPlus.SourceGenerator/Analysis/ReswResourceAnalyzer.cs
+++ b/src/ReswPlus.SourceGenerator/Analysis/ReswResourceAnalyzer.cs
@@ -66,10 +66,20 @@ private static void AnalyzeCompilation(CompilationAnalysisContext context)
return;
}
- var defaultLanguage = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions.TryGetValue("build_property.DefaultLanguage", out var value)
+ var globalOptions = context.Options.AnalyzerConfigOptionsProvider.GlobalOptions;
+ var defaultLanguage = globalOptions.TryGetValue("build_property.DefaultLanguage", out var value)
? value
: null;
+ var generateResourceInterfaces =
+ !globalOptions.TryGetValue("build_property.ReswPlusGenerateResourceInterfaces", out var interfaceOption)
+ || !bool.TryParse(interfaceOption, out var parsedInterfaces)
+ || parsedInterfaces;
- ReswResourceRules.Analyze(reswFiles, defaultLanguage, context.ReportDiagnostic, context.CancellationToken);
+ ReswResourceRules.Analyze(
+ reswFiles,
+ defaultLanguage,
+ generateResourceInterfaces,
+ context.ReportDiagnostic,
+ context.CancellationToken);
}
}
diff --git a/src/ReswPlus.SourceGenerator/Analysis/ReswResourceRules.cs b/src/ReswPlus.SourceGenerator/Analysis/ReswResourceRules.cs
index 6efd891..7a26770 100644
--- a/src/ReswPlus.SourceGenerator/Analysis/ReswResourceRules.cs
+++ b/src/ReswPlus.SourceGenerator/Analysis/ReswResourceRules.cs
@@ -35,11 +35,13 @@ internal static class ReswResourceRules
///
/// The .resw files of the project, with their content.
/// The default language of the project, if it declares one.
+ /// Whether injectable resource interfaces and providers are generated.
/// The callback invoked for every problem found.
/// The token used to cancel the operation.
public static void Analyze(
IReadOnlyList<(string Path, SourceText Text)> reswFiles,
string? defaultLanguage,
+ bool generateResourceInterfaces,
Action reportDiagnostic,
CancellationToken cancellationToken)
{
@@ -63,7 +65,7 @@ public static void Analyze(
var defaultModel = ReswResourceModel.Create(defaultDocument);
- AnalyzeDocument(defaultModel, defaultModel, reportDiagnostic);
+ AnalyzeDocument(defaultModel, defaultModel, generateResourceInterfaces, reportDiagnostic);
foreach (var path in group)
{
@@ -74,15 +76,19 @@ public static void Analyze(
continue;
}
- AnalyzeDocument(ReswResourceModel.Create(document), defaultModel, reportDiagnostic);
+ AnalyzeDocument(ReswResourceModel.Create(document), defaultModel, generateResourceInterfaces, reportDiagnostic);
}
}
}
- private static void AnalyzeDocument(ReswResourceModel model, ReswResourceModel defaultModel, Action reportDiagnostic)
+ private static void AnalyzeDocument(
+ ReswResourceModel model,
+ ReswResourceModel defaultModel,
+ bool generateResourceInterfaces,
+ Action reportDiagnostic)
{
ReportDuplicateMembers(model, reportDiagnostic);
- ReportReservedNames(model, reportDiagnostic);
+ ReportReservedNames(model, generateResourceInterfaces, reportDiagnostic);
ReportDuplicateFormatParameters(model, reportDiagnostic);
ReportMissingPluralForms(model, defaultModel, reportDiagnostic);
ReportFormattingProblems(model, defaultModel, reportDiagnostic);
@@ -95,13 +101,16 @@ private static void AnalyzeDocument(ReswResourceModel model, ReswResourceModel d
/// The generator skips these resources rather than emitting a member that would not compile, which makes
/// them silently absent from the generated class. This is what says so.
///
- private static void ReportReservedNames(ReswResourceModel model, Action reportDiagnostic)
+ private static void ReportReservedNames(
+ ReswResourceModel model,
+ bool generateResourceInterfaces,
+ Action reportDiagnostic)
{
var className = Path.GetFileNameWithoutExtension(model.Document.Path);
foreach (var member in model.Members)
{
- if (!GeneratedIdentifier.ConflictsWithGeneratedMember(member.Name, className))
+ if (!GeneratedIdentifier.ConflictsWithGeneratedMember(member.Name, className, generateResourceInterfaces))
{
continue;
}
diff --git a/src/ReswPlus.SourceGenerator/ClassGenerators/Models/StronglyTypedClass.cs b/src/ReswPlus.SourceGenerator/ClassGenerators/Models/StronglyTypedClass.cs
index 742cf6b..4ad190d 100644
--- a/src/ReswPlus.SourceGenerator/ClassGenerators/Models/StronglyTypedClass.cs
+++ b/src/ReswPlus.SourceGenerator/ClassGenerators/Models/StronglyTypedClass.cs
@@ -10,14 +10,14 @@ public StronglyTypedClass(
string resoureFile,
string className,
AppType appType,
- bool generateResourceInterface)
+ bool generateResourceInterfaces)
{
IsAdvanced = isAdvanced;
Namespaces = namespaces;
ResoureFile = resoureFile;
ClassName = className;
AppType = appType;
- GenerateResourceInterface = generateResourceInterface;
+ GenerateResourceInterfaces = generateResourceInterfaces;
Items = [];
}
@@ -26,7 +26,7 @@ public StronglyTypedClass(
public string ResoureFile { get; }
public string ClassName { get; }
public AppType AppType { get; }
- public bool GenerateResourceInterface { get; }
+ public bool GenerateResourceInterfaces { get; }
public List Items { get; }
}
diff --git a/src/ReswPlus.SourceGenerator/ClassGenerators/ReswClassGenerator.cs b/src/ReswPlus.SourceGenerator/ClassGenerators/ReswClassGenerator.cs
index 49da1ac..dfe2817 100644
--- a/src/ReswPlus.SourceGenerator/ClassGenerators/ReswClassGenerator.cs
+++ b/src/ReswPlus.SourceGenerator/ClassGenerators/ReswClassGenerator.cs
@@ -91,7 +91,7 @@ private StronglyTypedClass Parse(
string defaultNamespace,
bool isAdvanced,
AppType appType,
- bool generateResourceInterface)
+ bool generateResourceInterfaces)
{
var namespacesToUse = ExtractNamespace(defaultNamespace);
var resourceFileName = Path.GetFileName(_resourceFileInfo.Path);
@@ -110,14 +110,14 @@ private StronglyTypedClass Parse(
resourceLoaderName,
className,
appType,
- generateResourceInterface
+ generateResourceInterfaces
);
// Only use items with valid keys, that do not carry the ignore tag, and whose name the generated types
// don't already declare.
var stringItems = reswInfo.Items
.Where(i => IsValidPropertyName(i.Key) && !(i.Comment?.Contains(TagIgnore) ?? false))
- .Where(i => !GeneratedIdentifier.ConflictsWithGeneratedMember(i.Key, className, generateResourceInterface))
+ .Where(i => !GeneratedIdentifier.ConflictsWithGeneratedMember(i.Key, className, generateResourceInterfaces))
.ToArray();
if (isAdvanced)
@@ -132,7 +132,7 @@ private StronglyTypedClass Parse(
// The forms of the resource are already out of the plain items, so a conflicting group is
// dropped here rather than declined into members the generated types already declare.
- if (GeneratedIdentifier.ConflictsWithGeneratedMember(itemKey, className, generateResourceInterface))
+ if (GeneratedIdentifier.ConflictsWithGeneratedMember(itemKey, className, generateResourceInterfaces))
{
continue;
}
@@ -220,9 +220,9 @@ internal static bool IsValidPropertyName(string propertyName)
string defaultNamespace,
bool isAdvanced,
AppType appType,
- bool generateResourceInterface)
+ bool generateResourceInterfaces)
{
- var stronglyTypedClassInfo = Parse(content, defaultNamespace, isAdvanced, appType, generateResourceInterface);
+ var stronglyTypedClassInfo = Parse(content, defaultNamespace, isAdvanced, appType, generateResourceInterfaces);
if (stronglyTypedClassInfo is null)
{
return null;
diff --git a/src/ReswPlus.SourceGenerator/CodeGenerators/CsharpCodeGenerator.cs b/src/ReswPlus.SourceGenerator/CodeGenerators/CsharpCodeGenerator.cs
index 9aa2fcc..4f8478b 100644
--- a/src/ReswPlus.SourceGenerator/CodeGenerators/CsharpCodeGenerator.cs
+++ b/src/ReswPlus.SourceGenerator/CodeGenerators/CsharpCodeGenerator.cs
@@ -170,19 +170,23 @@ public IEnumerable GetGeneratedFiles(string? baseFilename, Strong
strongClassDecl = strongClassDecl.AddMembers(formatMembers.ToArray());
InterfaceDeclarationSyntax? resourceInterfaceDecl = null;
- if (info.GenerateResourceInterface)
+ ClassDeclarationSyntax? resourceProviderDecl = null;
+ if (info.GenerateResourceInterfaces)
{
var interfaceName = "I" + info.ClassName;
+ var providerName = info.ClassName + "Provider";
var publicMembers = strongClassDecl.Members
.Where(member =>
member is MethodDeclarationSyntax or PropertyDeclarationSyntax
&& member.Modifiers.Any(SyntaxKind.PublicKeyword))
.ToArray();
-
resourceInterfaceDecl = CreateResourceInterface(interfaceName, info, publicMembers);
- strongClassDecl = strongClassDecl
- .AddBaseListTypes(SimpleBaseType(IdentifierName(interfaceName)))
- .AddMembers(CreateExplicitInterfaceImplementations(interfaceName, info.ClassName, publicMembers).ToArray());
+ resourceProviderDecl = CreateResourceProvider(
+ providerName,
+ interfaceName,
+ info.ClassName,
+ info,
+ publicMembers);
}
// Create the markup extension class that allows resource keys to be used in XAML.
@@ -193,17 +197,17 @@ member is MethodDeclarationSyntax or PropertyDeclarationSyntax
{
var nsName = string.Join(".", info.Namespaces);
var namespaceDecl = NamespaceDeclaration(ParseName(nsName));
- namespaceDecl = resourceInterfaceDecl is null
+ namespaceDecl = resourceInterfaceDecl is null || resourceProviderDecl is null
? namespaceDecl.AddMembers(strongClassDecl, markupExtensionDecl)
- : namespaceDecl.AddMembers(resourceInterfaceDecl, strongClassDecl, markupExtensionDecl);
+ : namespaceDecl.AddMembers(resourceInterfaceDecl, resourceProviderDecl, strongClassDecl, markupExtensionDecl);
compilationUnit = compilationUnit.AddMembers(namespaceDecl);
}
else
{
// Otherwise, add the classes at the root level.
- compilationUnit = resourceInterfaceDecl is null
+ compilationUnit = resourceInterfaceDecl is null || resourceProviderDecl is null
? compilationUnit.AddMembers(strongClassDecl, markupExtensionDecl)
- : compilationUnit.AddMembers(resourceInterfaceDecl, strongClassDecl, markupExtensionDecl);
+ : compilationUnit.AddMembers(resourceInterfaceDecl, resourceProviderDecl, strongClassDecl, markupExtensionDecl);
}
// Normalize the whitespace (formatting) and return the generated source code.
@@ -442,9 +446,7 @@ private ClassDeclarationSyntax CreateStronglyTypedClass(StronglyTypedClass info)
var classDecl = ClassDeclaration(info.ClassName)
.WithAttributeLists(attributes)
.WithLeadingTrivia(CreateDocumentation($"Provides strongly-typed access to the strings of the '{info.ResoureFile}' resource file."))
- .WithModifiers(info.GenerateResourceInterface
- ? TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.SealedKeyword))
- : TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)))
+ .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.StaticKeyword)))
.AddMembers(resourceField, staticCtor, getStringMethod);
return classDecl;
@@ -495,6 +497,26 @@ private static InterfaceDeclarationSyntax CreateResourceInterface(
.AddMembers(publicMembers.Select(CreateInterfaceMember).ToArray());
}
+ ///
+ /// Creates an injectable adapter that delegates to the existing static resource API.
+ ///
+ private static ClassDeclarationSyntax CreateResourceProvider(
+ string providerName,
+ string interfaceName,
+ string className,
+ StronglyTypedClass info,
+ IEnumerable publicMembers)
+ {
+ return ClassDeclaration(providerName)
+ .WithAttributeLists(CreateGeneratedTypeAttributes())
+ .WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword), Token(SyntaxKind.SealedKeyword)))
+ .WithBaseList(BaseList(SingletonSeparatedList(
+ SimpleBaseType(IdentifierName(interfaceName)))))
+ .WithLeadingTrivia(CreateDocumentation(
+ $"Provides injectable access to the strings of the '{info.ResoureFile}' resource file."))
+ .AddMembers(CreateExplicitInterfaceImplementations(interfaceName, className, publicMembers).ToArray());
+ }
+
///
/// Removes the implementation and static modifiers from a generated lookup member.
///
diff --git a/src/ReswPlus.SourceGenerator/CodeGenerators/GeneratedIdentifier.cs b/src/ReswPlus.SourceGenerator/CodeGenerators/GeneratedIdentifier.cs
index adf7f3b..85f4ad9 100644
--- a/src/ReswPlus.SourceGenerator/CodeGenerators/GeneratedIdentifier.cs
+++ b/src/ReswPlus.SourceGenerator/CodeGenerators/GeneratedIdentifier.cs
@@ -57,10 +57,12 @@ public static string Escape(string name)
/// beside the generated GetString without conflicting with it. Resources whose names only differ by
/// case conflict for a different reason, and are reported by RESWP0009.
///
- public static bool ConflictsWithGeneratedMember(string name, string className, bool hasResourceInterface = false)
+ public static bool ConflictsWithGeneratedMember(string name, string className, bool generateResourceInterfaces)
{
return string.Equals(name, className, StringComparison.Ordinal)
- || (hasResourceInterface && string.Equals(name, "I" + className, StringComparison.Ordinal))
+ || generateResourceInterfaces && (
+ string.Equals(name, "I" + className, StringComparison.Ordinal)
+ || string.Equals(name, className + "Provider", StringComparison.Ordinal))
|| Array.IndexOf(GeneratedMemberNames, name) >= 0;
}
diff --git a/src/ReswPlus.SourceGenerator/Pipeline/ReswBuildOptions.cs b/src/ReswPlus.SourceGenerator/Pipeline/ReswBuildOptions.cs
index 57ee499..4f27908 100644
--- a/src/ReswPlus.SourceGenerator/Pipeline/ReswBuildOptions.cs
+++ b/src/ReswPlus.SourceGenerator/Pipeline/ReswBuildOptions.cs
@@ -68,7 +68,7 @@ private ReswBuildOptions(
public bool UseUwp { get; }
///
- /// Gets whether the project opted into generated injectable resource interfaces.
+ /// Gets whether injectable resource interfaces and providers should be generated.
///
public bool GenerateResourceInterfaces { get; }
@@ -88,7 +88,7 @@ public static ReswBuildOptions Read(AnalyzerConfigOptions globalOptions)
Get("build_property.RootNamespace"),
bool.TryParse(Get("build_property.ReswPlusUseApplicationLanguages"), out var parsed) && parsed,
bool.TryParse(Get("build_property.UseUwp"), out var parsedUseUwp) && parsedUseUwp,
- bool.TryParse(Get("build_property.ReswPlusGenerateResourceInterfaces"), out var parsedInterfaces) && parsedInterfaces);
+ !bool.TryParse(Get("build_property.ReswPlusGenerateResourceInterfaces"), out var parsedInterfaces) || parsedInterfaces);
string? Get(string key) => globalOptions.TryGetValue(key, out var value) ? value : null;
}
@@ -137,7 +137,6 @@ public override int GetHashCode()
hash = (hash * 31) + UseApplicationLanguages.GetHashCode();
hash = (hash * 31) + UseUwp.GetHashCode();
-
return (hash * 31) + GenerateResourceInterfaces.GetHashCode();
}
}
diff --git a/src/ReswPlus.SourceGenerator/Pipeline/ReswProject.cs b/src/ReswPlus.SourceGenerator/Pipeline/ReswProject.cs
index 1d39226..e4ae65a 100644
--- a/src/ReswPlus.SourceGenerator/Pipeline/ReswProject.cs
+++ b/src/ReswPlus.SourceGenerator/Pipeline/ReswProject.cs
@@ -139,7 +139,7 @@ public static ReswProject Create(CompilationInfo compilationInfo, ReswBuildOptio
new EquatableArray(problems));
ReswProject Unsupported(params string[] problems) =>
- new(false, AppType.Unknown, "", "", "", false, false, false, options.DefaultLanguage, new EquatableArray(problems));
+ new(false, AppType.Unknown, "", "", "", false, false, options.GenerateResourceInterfaces, options.DefaultLanguage, new EquatableArray(problems));
}
///
diff --git a/src/ReswPlus.SourceGenerator/ReswGenerator.cs b/src/ReswPlus.SourceGenerator/ReswGenerator.cs
index fd94c84..8597bfc 100644
--- a/src/ReswPlus.SourceGenerator/ReswGenerator.cs
+++ b/src/ReswPlus.SourceGenerator/ReswGenerator.cs
@@ -184,7 +184,7 @@ private static void ReportSetupProblems(SourceProductionContext spc, ReswProject
defaultNamespace: project.GetNamespace(file.Path),
isAdvanced: true,
appType: project.AppType,
- generateResourceInterface: project.GenerateResourceInterfaces);
+ generateResourceInterfaces: project.GenerateResourceInterfaces);
if (generated?.Files.FirstOrDefault() is not { } generatedFile)
{
diff --git a/tests/ReswPlusUnitTests/GeneratedCodeHygiene.cs b/tests/ReswPlusUnitTests/GeneratedCodeHygiene.cs
index 106722c..759dc7b 100644
--- a/tests/ReswPlusUnitTests/GeneratedCodeHygiene.cs
+++ b/tests/ReswPlusUnitTests/GeneratedCodeHygiene.cs
@@ -80,7 +80,9 @@ public void EveryVisibleMemberIsDocumented()
.OfType()
.Where(member => member is EnumMemberDeclarationSyntax ||
member.Modifiers.Any(SyntaxKind.PublicKeyword) ||
- member.Modifiers.Any(SyntaxKind.ProtectedKeyword))
+ member.Modifiers.Any(SyntaxKind.ProtectedKeyword) ||
+ member.Parent is InterfaceDeclarationSyntax interfaceDeclaration &&
+ interfaceDeclaration.Modifiers.Any(SyntaxKind.PublicKeyword))
.ToArray();
Assert.NotEmpty(visibleMembers);
@@ -100,7 +102,14 @@ public void EveryParameterOfADocumentedMethodIsDocumented()
{
var root = CSharpSyntaxTree.ParseText(GenerateSample()).GetRoot();
- var methods = root.DescendantNodes().OfType().ToArray();
+ var methods = root.DescendantNodes()
+ .OfType()
+ .Where(method =>
+ method.Modifiers.Any(SyntaxKind.PublicKeyword) ||
+ method.Modifiers.Any(SyntaxKind.ProtectedKeyword) ||
+ method.Parent is InterfaceDeclarationSyntax interfaceDeclaration &&
+ interfaceDeclaration.Modifiers.Any(SyntaxKind.PublicKeyword))
+ .ToArray();
Assert.NotEmpty(methods);
diff --git a/tests/ReswPlusUnitTests/GeneratorEndToEnd.cs b/tests/ReswPlusUnitTests/GeneratorEndToEnd.cs
index 7432d03..8fbd2fe 100644
--- a/tests/ReswPlusUnitTests/GeneratorEndToEnd.cs
+++ b/tests/ReswPlusUnitTests/GeneratorEndToEnd.cs
@@ -48,36 +48,51 @@ public void TheGeneratedCodeCompiles(ReswPlus.SourceGenerator.AppType appType)
}
[Fact]
- public void ResourceInterfacesAreNotGeneratedByDefault()
+ public void ResourceInterfacesAndProvidersAreGeneratedByDefault()
{
var run = ReswGeneratorHarness.Run(
[ReswGeneratorHarness.File("en-US", ReswTestHelpers.CreateResw(("Plain", "A plain string", null)))]);
- Assert.DoesNotContain("interface IResources", run.Source("Resources.resw"));
+ Assert.Contains("public interface IResources", run.Source("Resources.resw"));
+ Assert.Contains("public sealed class ResourcesProvider : IResources", run.Source("Resources.resw"));
Assert.Contains("public static class Resources", run.Source("Resources.resw"));
}
[Fact]
- public void ResourceInterfaceOptionIsReadFromTheBuild()
+ public void ResourceInterfacesAndProvidersCanBeDisabled()
{
- var options = new System.Collections.Generic.Dictionary(AnalyzerConfigOptions.KeyComparer)
- {
- ["build_property.ReswPlusGenerateResourceInterfaces"] = "true",
- };
+ var run = ReswGeneratorHarness.Run(
+ [ReswGeneratorHarness.File("en-US", ReswTestHelpers.CreateResw(
+ ("Plain", "A plain string", null),
+ ("IResources", "An interface-shaped resource", null),
+ ("ResourcesProvider", "A provider-shaped resource", null)))],
+ generateResourceInterfaces: false);
- var buildOptions = ReswBuildOptions.Read(new TestAnalyzerConfigOptionsProvider(options).GlobalOptions);
+ var generated = run.Source("Resources.resw");
- Assert.True(buildOptions.GenerateResourceInterfaces);
+ Assert.DoesNotContain("interface IResources", generated);
+ Assert.DoesNotContain("class ResourcesProvider", generated);
+ Assert.Contains("public static class Resources", generated);
+ Assert.Contains("public static string IResources", generated);
+ Assert.Contains("public static string ResourcesProvider", generated);
+ run.AssertCompiles();
+
+ Assert.Empty(ReswTestHelpers.Analyze(
+ defaultLanguage: null,
+ generateResourceInterfaces: false,
+ ("en-US", ReswTestHelpers.CreateResw(
+ ("IResources", "An interface-shaped resource", null),
+ ("ResourcesProvider", "A provider-shaped resource", null)))));
}
[Fact]
public void ResourceInterfaceCanBeGeneratedDirectly()
{
var generated = ReswTestHelpers.GenerateCode(
- ReswTestHelpers.CreateResw(("Plain", "A plain string", null)),
- generateResourceInterface: true);
+ ReswTestHelpers.CreateResw(("Plain", "A plain string", null)));
Assert.Contains("public interface IResources", generated);
+ Assert.Contains("public sealed class ResourcesProvider : IResources", generated);
}
[Theory]
@@ -87,14 +102,14 @@ public void ResourceInterfacesCanBeInjectedWithoutChangingTheStaticApi(ReswPlus.
{
var run = ReswGeneratorHarness.Run(
[ReswGeneratorHarness.File("en-US", EveryFeature)],
- appType,
- generateResourceInterfaces: true);
+ appType);
var generated = run.Source("Resources.resw");
Assert.Contains("public interface IResources", generated);
Assert.Contains("GeneratedCodeAttribute(\"ReswPlus\"", generated);
- Assert.Contains("public sealed class Resources : IResources", generated);
+ Assert.Contains("public sealed class ResourcesProvider : IResources", generated);
+ Assert.Contains("public static class Resources", generated);
Assert.Contains("string IResources.Plain => Resources.Plain;", generated);
Assert.Contains("string IResources.Formatted(string name, int age) => Resources.Formatted(name, age);", generated);
@@ -117,7 +132,7 @@ public ViewModel(global::TestProject.Strings.IResources resources)
public static class Composition
{
public static ViewModel Create() =>
- new ViewModel(new global::TestProject.Strings.Resources());
+ new ViewModel(new global::TestProject.Strings.ResourcesProvider());
}
}
""");
@@ -127,10 +142,12 @@ public static ViewModel Create() =>
public void AResourceCannotCollideWithItsGeneratedInterface()
{
var run = ReswGeneratorHarness.Run(
- [ReswGeneratorHarness.File("en-US", ReswTestHelpers.CreateResw(("IResources", "A collision", null)))],
- generateResourceInterfaces: true);
+ [ReswGeneratorHarness.File("en-US", ReswTestHelpers.CreateResw(
+ ("IResources", "An interface collision", null),
+ ("ResourcesProvider", "A provider collision", null)))]);
Assert.DoesNotContain("string IResources {", run.Source("Resources.resw"));
+ Assert.DoesNotContain("string ResourcesProvider {", run.Source("Resources.resw"));
run.AssertCompiles();
}
diff --git a/tests/ReswPlusUnitTests/GeneratorHarness.cs b/tests/ReswPlusUnitTests/GeneratorHarness.cs
index d109e8d..f595b1f 100644
--- a/tests/ReswPlusUnitTests/GeneratorHarness.cs
+++ b/tests/ReswPlusUnitTests/GeneratorHarness.cs
@@ -64,7 +64,7 @@ public static ReswFile File(string language, string content, string baseName = "
/// The DefaultLanguage of the project, which picks the language the code is generated from.
/// The ReswPlusUseApplicationLanguages of the project, or to leave it undeclared.
/// The UseUwp of the project, which is how a UWP project built for Native AOT says what it is, or to leave it undeclared.
- /// Whether to generate injectable resource interfaces.
+ /// Whether to generate injectable resource interfaces and providers.
/// The name of the assembly being compiled.
/// Whether the compilation should look like a UWP project built for Native AOT, which has the UWP types but no recognizable API contract reference.
/// Files to pass to the compiler on top of , to cover what the generator does with the ones it doesn't own.
diff --git a/tests/ReswPlusUnitTests/ReswTestHelpers.cs b/tests/ReswPlusUnitTests/ReswTestHelpers.cs
index 389dab8..5f81f12 100644
--- a/tests/ReswPlusUnitTests/ReswTestHelpers.cs
+++ b/tests/ReswPlusUnitTests/ReswTestHelpers.cs
@@ -55,9 +55,9 @@ public static string CreateResw(params (string Key, string Value, string? Commen
public static string GenerateCode(
string reswContent,
AppType appType = AppType.WindowsAppSDK,
- bool generateResourceInterface = false)
+ bool generateResourceInterfaces = true)
{
- return GenerateFile(reswContent, appType, generateResourceInterface).Content;
+ return GenerateFile(reswContent, appType, generateResourceInterfaces).Content;
}
///
@@ -69,7 +69,7 @@ public static string GenerateCode(
public static GeneratedFile GenerateFile(
string reswContent,
AppType appType = AppType.WindowsAppSDK,
- bool generateResourceInterface = false)
+ bool generateResourceInterfaces = true)
{
var resourceFileInfo = new ResourceFileInfo(@"C:\Project\Strings\en-US\Resources.resw", new Project("TestProject", isLibrary: false));
var generator = ReswClassGenerator.CreateGenerator(resourceFileInfo, logger: null);
@@ -82,7 +82,7 @@ public static GeneratedFile GenerateFile(
defaultNamespace: "TestProject.Strings",
isAdvanced: true,
appType: appType,
- generateResourceInterface: generateResourceInterface);
+ generateResourceInterfaces: generateResourceInterfaces);
Assert.NotNull(result);
@@ -96,6 +96,17 @@ public static GeneratedFile GenerateFile(
/// The files of the project, as language folder name and content pairs.
/// The diagnostics reported for those files.
public static IReadOnlyList Analyze(string? defaultLanguage, params (string Language, string Content)[] files)
+ {
+ return Analyze(defaultLanguage, generateResourceInterfaces: true, files);
+ }
+
+ ///
+ /// Runs the resource analysis with the configured resource-interface behavior.
+ ///
+ public static IReadOnlyList Analyze(
+ string? defaultLanguage,
+ bool generateResourceInterfaces,
+ params (string Language, string Content)[] files)
{
var documents = files
.Select(file => (GetPath(file.Language), SourceText.From(file.Content)))
@@ -103,7 +114,12 @@ public static IReadOnlyList Analyze(string? defaultLanguage, params
var diagnostics = new List();
- ReswResourceRules.Analyze(documents, defaultLanguage, diagnostics.Add, CancellationToken.None);
+ ReswResourceRules.Analyze(
+ documents,
+ defaultLanguage,
+ generateResourceInterfaces,
+ diagnostics.Add,
+ CancellationToken.None);
return diagnostics;
}