From da59702339aa8b92535e976bc93a2de1ab37394f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 13 Oct 2026 18:22:23 -0700 Subject: [PATCH 1/4] Recognize Uno WinUI without selecting UWP XAML Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 5 + docs/FAQ.md | 17 + nuget/README.md | 4 + .../CodeGenerators/CsharpCodeGenerator.cs | 4 +- src/ReswPlus.SourceGenerator/Diagnostics.cs | 4 +- .../Pipeline/ReswProject.cs | 2 +- src/ReswPlus.SourceGenerator/ReswGenerator.cs | 10 +- tests/ReswPlusUnitTests/AppTypeDetection.cs | 337 ++++++++++++++++++ tests/ReswPlusUnitTests/GeneratorHarness.cs | 25 +- 9 files changed, 399 insertions(+), 9 deletions(-) create mode 100644 tests/ReswPlusUnitTests/AppTypeDetection.cs diff --git a/README.md b/README.md index 24bdb7d..0f81716 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,11 @@ _**Now available as a Source Generator!**_ ⚡ [How to Install ReswPlus](https://github.com/reswplus/ReswPlus/wiki/How-to-install-ReswPlus) – Step-by-step installation guide. +The source generator supports UWP, Windows App SDK, and Uno WinUI applications and libraries. +Uno WinUI is detected automatically from its referenced types and uses `Microsoft.UI.Xaml` helpers +with `Windows.ApplicationModel.Resources.ResourceLoader`; no `UseUwp=true` override or +`Windows.UI.Xaml` compatibility types are needed. + ReswPlus recognizes a UWP project by the `Windows.Foundation.UniversalApiContract` reference it carries, and a Windows App SDK project by its own. A **UWP project built for Native AOT** has no such reference, so it declares what it is with the standard `UseUwp` property instead, which ReswPlus reads and trusts over the references: ```xml diff --git a/docs/FAQ.md b/docs/FAQ.md index e21a307..a4d1163 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -38,6 +38,23 @@ Everything else ReswPlus generates works with Native AOT. VB support is currently under development. C++/CX and C++/WinRT support are planned for a future update. +## Does the source generator support Uno WinUI? + +Yes. ReswPlus detects `Microsoft.UI.Xaml.DependencyObject` from the `Uno.UI` assembly, +regardless of the NuGet package path or whether the type is a class or interface. +It generates `Microsoft.UI.Xaml` helpers and uses +`Windows.ApplicationModel.Resources.ResourceLoader.GetForViewIndependentUse`. + +Generated macro helpers are still subject to Uno's Windows API support. Uno may +report `Uno0001` warnings for unsupported APIs in those helpers, even when the +resource uses a different macro. + +If you previously worked around `RESWP0005` by setting `UseUwp=true` and adding +`Windows.UI.Xaml` versions of `MarkupExtension`, `MarkupExtensionReturnTypeAttribute`, +and `IValueConverter`, remove that override and those compatibility types after updating. +A separate `WinRT.DynamicWindowsRuntimeCastAttribute` polyfill, if required by your app, +is unrelated to ReswPlus and should be kept. + ## Is it free? Yes and it won't change. diff --git a/nuget/README.md b/nuget/README.md index a754e7b..da15818 100644 --- a/nuget/README.md +++ b/nuget/README.md @@ -10,6 +10,10 @@ dotnet add package ReswPlus The package adds the generator and its transitive MSBuild integration. Add or update the default-language `.resw` file in your project and ReswPlus generates resource properties and methods during compilation. +Supports UWP, Windows App SDK, and Uno WinUI applications and libraries. Uno WinUI is detected +automatically and uses WinUI XAML helpers with the Windows resource loader, without `UseUwp=true` +or `Windows.UI.Xaml` compatibility types. + ## Features - Strongly typed resource properties for XAML and C# diff --git a/src/ReswPlus.SourceGenerator/CodeGenerators/CsharpCodeGenerator.cs b/src/ReswPlus.SourceGenerator/CodeGenerators/CsharpCodeGenerator.cs index 4f8478b..f31bbe7 100644 --- a/src/ReswPlus.SourceGenerator/CodeGenerators/CsharpCodeGenerator.cs +++ b/src/ReswPlus.SourceGenerator/CodeGenerators/CsharpCodeGenerator.cs @@ -336,7 +336,7 @@ private static string DescribeParameter(Localization item, FunctionFormatTagPara /// private static string XamlNamespace(AppType appType) { - return appType == AppType.WindowsAppSDK ? "global::Microsoft.UI.Xaml" : "global::Windows.UI.Xaml"; + return appType is AppType.WindowsAppSDK or AppType.UnoWinUI ? "global::Microsoft.UI.Xaml" : "global::Windows.UI.Xaml"; } /// @@ -349,7 +349,7 @@ private IEnumerable GetUsings(AppType appType) UsingDirective(ParseName("System")) }; - if (appType == AppType.WindowsAppSDK) + if (appType is AppType.WindowsAppSDK or AppType.UnoWinUI) { usings.Add(UsingDirective(ParseName("Microsoft.UI.Xaml.Markup"))); usings.Add(UsingDirective(ParseName("Microsoft.UI.Xaml.Data"))); diff --git a/src/ReswPlus.SourceGenerator/Diagnostics.cs b/src/ReswPlus.SourceGenerator/Diagnostics.cs index 0b9d4f7..062e7ec 100644 --- a/src/ReswPlus.SourceGenerator/Diagnostics.cs +++ b/src/ReswPlus.SourceGenerator/Diagnostics.cs @@ -71,12 +71,12 @@ internal static class Diagnostics isEnabledByDefault: true); /// - /// RESWP0005: the project is neither a UWP nor a WinAppSDK project. + /// RESWP0005: the project is not a UWP, WinAppSDK, or Uno WinUI project. /// public static readonly DiagnosticDescriptor UnrecognizedAppType = new( "RESWP0005", "Project type not recognized", - "ReswPlus only supports UWP and WinAppSDK applications/libraries", + "ReswPlus only supports UWP, WinAppSDK, and Uno WinUI applications/libraries", CompatibilityCategory, DiagnosticSeverity.Error, isEnabledByDefault: true); diff --git a/src/ReswPlus.SourceGenerator/Pipeline/ReswProject.cs b/src/ReswPlus.SourceGenerator/Pipeline/ReswProject.cs index e4ae65a..464da55 100644 --- a/src/ReswPlus.SourceGenerator/Pipeline/ReswProject.cs +++ b/src/ReswPlus.SourceGenerator/Pipeline/ReswProject.cs @@ -116,7 +116,7 @@ public static ReswProject Create(CompilationInfo compilationInfo, ReswBuildOptio ? AppType.UWP : compilationInfo.AppType; - if (appType is not (AppType.WindowsAppSDK or AppType.UWP)) + if (appType is not (AppType.WindowsAppSDK or AppType.UWP or AppType.UnoWinUI)) { return Unsupported([.. problems, Diagnostics.UnrecognizedAppType.Id]); } diff --git a/src/ReswPlus.SourceGenerator/ReswGenerator.cs b/src/ReswPlus.SourceGenerator/ReswGenerator.cs index 8597bfc..7bbdaa5 100644 --- a/src/ReswPlus.SourceGenerator/ReswGenerator.cs +++ b/src/ReswPlus.SourceGenerator/ReswGenerator.cs @@ -26,6 +26,7 @@ public enum AppType Unknown, WindowsAppSDK, UWP, + UnoWinUI, } /// @@ -264,10 +265,17 @@ private static void EmitSupport(SourceProductionContext spc, ReswSupport support } /// - /// Determines the application type (WindowsAppSDK, UWP, or Unknown) by inspecting the compilation's external references. + /// Determines the application type by inspecting the compilation's referenced types and assemblies. /// private static AppType RetrieveAppType(Compilation compilation) { + // Uno.WinUI defines WinUI's types in Uno.UI, but implements the Windows resource loader. + var dependencyObject = compilation.GetTypeByMetadataName("Microsoft.UI.Xaml.DependencyObject"); + if (dependencyObject?.ContainingAssembly.Identity.Name == "Uno.UI") + { + return AppType.UnoWinUI; + } + return compilation.ExternalReferences.Any(r => r.Display?.IndexOf("Microsoft.WindowsAppSdk", StringComparison.OrdinalIgnoreCase) >= 0) ? AppType.WindowsAppSDK diff --git a/tests/ReswPlusUnitTests/AppTypeDetection.cs b/tests/ReswPlusUnitTests/AppTypeDetection.cs new file mode 100644 index 0000000..b7fce6b --- /dev/null +++ b/tests/ReswPlusUnitTests/AppTypeDetection.cs @@ -0,0 +1,337 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using ReswPlus.SourceGenerator; +using ReswPlus.SourceGenerator.Pipeline; +using Xunit; + +namespace ReswPlusUnitTests; + +public class AppTypeDetection +{ + private static ImmutableArray FrameworkReferences => PlatformStubs.ReferencesFor(AppType.Unknown); + + private static string SampleResources => ReswTestHelpers.CreateResw( + ("Welcome", "Welcome!", null), + ("Greeting", "Hello {0}", "#Format[String name]"), + ("FileCount_None", "No files", null), + ("FileCount_One", "{0} file", "#Format[Plural Int count]"), + ("FileCount_Other", "{0} files", "#Format[Plural Int count]")); + + [Theory] + [InlineData("interface", false, false)] + [InlineData("interface", true, false)] + [InlineData("class", false, false)] + [InlineData("class", true, false)] + [InlineData("interface", false, true)] + [InlineData("interface", true, true)] + [InlineData("class", false, true)] + [InlineData("class", true, true)] + public void UnoWinUIUsesWinUIHelpersAndWindowsResources(string typeKind, bool isLibrary, bool useApplicationLanguages) + { + var reference = CreatePlatformReference(AppType.UnoWinUI, typeKind); + Assert.DoesNotContain("Uno", reference.Display!); + + var run = Run([reference], isLibrary, useApplicationLanguages); + Assert.Empty(run.Diagnostics); + + var resources = run.Source("Resources.resw"); + Assert.Contains("using Microsoft.UI.Xaml.Markup;", resources); + Assert.Contains("using Microsoft.UI.Xaml.Data;", resources); + Assert.Contains(isLibrary ? "\"TestProject/Resources\"" : "\"Resources\"", resources); + Assert.Contains("public static string Welcome", resources); + Assert.Contains("public static string Greeting(string name)", resources); + Assert.Contains("public static string FileCount(int count)", resources); + Assert.Contains("[global::Microsoft.UI.Xaml.Markup.MarkupExtensionReturnType(ReturnType = typeof(string))]", resources); + Assert.Contains("public global::Microsoft.UI.Xaml.Data.IValueConverter Converter", resources); + Assert.Contains("public interface IResources", resources); + Assert.Contains("public sealed class ResourcesProvider : IResources", resources); + + var provider = run.Source("ResourceStringProvider"); + Assert.Contains("using Windows.ApplicationModel.Resources;", provider); + Assert.Contains("ResourceLoader.GetForViewIndependentUse(resourceMapId)", provider); + var plurals = run.Source("ResourceLoaderExtension"); + Assert.Contains("GetPlural", plurals); + if (useApplicationLanguages) + { + Assert.Contains("global::Windows.Globalization.ApplicationLanguages.Languages", plurals); + } + else + { + Assert.DoesNotContain("global::Windows.Globalization.ApplicationLanguages", plurals); + } + + foreach (var source in run.Sources.Values) + { + Assert.DoesNotContain("Windows.UI.Xaml", source); + Assert.DoesNotContain("Microsoft.Windows.ApplicationModel.Resources", source); + Assert.DoesNotContain("Microsoft.Windows.Globalization", source); + } + + run.AssertCompiles(); + AssertHelperTypes(run.OutputCompilation, "Microsoft.UI.Xaml", "Uno.UI"); + } + + [Theory] + [InlineData(AppType.WindowsAppSDK, "Microsoft.UI.Xaml", "Microsoft.Windows.ApplicationModel.Resources")] + [InlineData(AppType.UWP, "Windows.UI.Xaml", "Windows.ApplicationModel.Resources")] + public void ExistingPlatformsKeepTheirHelpersAndResourceLoaders(AppType appType, string xamlNamespace, string resourceNamespace) + { + var run = Run([CreatePlatformReference(appType)]); + + Assert.Empty(run.Diagnostics); + Assert.Equal(ReswTestHelpers.GenerateCode(SampleResources, appType), run.Source("Resources.resw")); + var provider = run.Source("ResourceStringProvider"); + Assert.Contains($"using {resourceNamespace};", provider); + Assert.Contains( + appType == AppType.UWP + ? "ResourceLoader.GetForViewIndependentUse(resourceMapId)" + : "new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), resourceMapId)", + provider); + run.AssertCompiles(); + AssertHelperTypes(run.OutputCompilation, xamlNamespace, appType == AppType.UWP ? "Windows.Foundation.UniversalApiContract" : "Microsoft.WinUI"); + } + + [Theory] + [InlineData("Unrelated.UI", "Microsoft.UI.Xaml")] + [InlineData("Uno.WinUI", "Microsoft.UI.Xaml")] + [InlineData("Uno.UI", "Windows.UI.Xaml")] + public void MatchingNamespaceOrPackageNameAloneDoesNotSelectUno(string assemblyName, string xamlNamespace) + { + var reference = CreateReference(assemblyName, $"namespace {xamlNamespace} {{ public interface DependencyObject {{ }} }}"); + + AssertUnrecognized(Run([reference])); + } + + [Fact] + public void MissingDependencyObjectDoesNotSelectUno() + { + AssertUnrecognized(Run([CreateReference("Uno.UI", "public class UnrelatedType { }")])); + } + + [Fact] + public void AmbiguousDependencyObjectDoesNotSelectUno() + { + const string source = "namespace Microsoft.UI.Xaml { public interface DependencyObject { } }"; + var run = Run([CreateReference("Uno.UI", source), CreateReference("Unrelated.UI", source)]); + + Assert.Null(run.OutputCompilation.GetTypeByMetadataName("Microsoft.UI.Xaml.DependencyObject")); + AssertUnrecognized(run); + } + + [Theory] + [InlineData(AppType.WindowsAppSDK)] + [InlineData(AppType.UWP)] + public void UnoWinUIDetectionTakesPrecedenceOverReferencePathHints(AppType hintedAppType) + { + var hintedReference = CreateReference("PathHint", "public class UnrelatedType { }", GetReferencePath(hintedAppType)); + var run = Run([CreatePlatformReference(AppType.UnoWinUI), hintedReference]); + + Assert.Empty(run.Diagnostics); + Assert.Contains("using Microsoft.UI.Xaml.Markup;", run.Source("Resources.resw")); + Assert.Contains("using Windows.ApplicationModel.Resources;", run.Source("ResourceStringProvider")); + } + + [Theory] + [InlineData(AppType.WindowsAppSDK, false)] + [InlineData(AppType.WindowsAppSDK, true)] + [InlineData(AppType.UWP, false)] + [InlineData(AppType.UWP, true)] + public void RejectedUnoTypesPreserveExistingFallbacks(AppType appType, bool ambiguous) + { + var references = ImmutableArray.Create(CreatePlatformReference(appType)); + if (ambiguous) + { + const string source = "namespace Microsoft.UI.Xaml { public interface DependencyObject { } }"; + references = references.Add(CreateReference("Uno.UI", source)).Add(CreateReference("Unrelated.UI", source)); + } + else + { + references = references.Add(CreateReference("Uno.UI", "public class UnrelatedType { }")); + } + + var run = Run(references); + + Assert.Empty(run.Diagnostics); + Assert.Equal(ReswTestHelpers.GenerateCode(SampleResources, appType), run.Source("Resources.resw")); + Assert.Contains( + appType == AppType.UWP ? "using Windows.ApplicationModel.Resources;" : "using Microsoft.Windows.ApplicationModel.Resources;", + run.Source("ResourceStringProvider")); + } + + [Fact] + public void WinAppSDKStillTakesPrecedenceOverUWPReferencePath() + { + var run = Run([ + CreatePlatformReference(AppType.WindowsAppSDK), + CreateReference("UwpPathHint", "public class UnrelatedType { }", GetReferencePath(AppType.UWP))]); + + Assert.Empty(run.Diagnostics); + Assert.Equal(ReswTestHelpers.GenerateCode(SampleResources), run.Source("Resources.resw")); + Assert.Contains("using Microsoft.Windows.ApplicationModel.Resources;", run.Source("ResourceStringProvider")); + } + + [Fact] + public void ChangingReferencesInvalidatesTheIncrementalAppType() + { + var before = Run([CreatePlatformReference(AppType.WindowsAppSDK)]); + Assert.Contains("using Microsoft.Windows.ApplicationModel.Resources;", before.Source("ResourceStringProvider")); + + var after = before.RunAgainWithReferences(FrameworkReferences.Add(CreatePlatformReference(AppType.UnoWinUI))); + + Assert.Empty(after.Diagnostics); + Assert.Contains("using Windows.ApplicationModel.Resources;", after.Source("ResourceStringProvider")); + Assert.Equal(before.Source("Resources.resw"), after.Source("Resources.resw")); + Assert.Equal(1, after.RecomputedCount(TrackingNames.CompilationInfo)); + Assert.Equal(1, after.RecomputedCount(TrackingNames.Project)); + Assert.Equal(1, after.RecomputedCount(TrackingNames.Support)); + after.AssertCompiles(); + + Assert.NotEqual( + new CompilationInfo(true, AppType.WindowsAppSDK, "TestProject"), + new CompilationInfo(true, AppType.UnoWinUI, "TestProject")); + Assert.Equal( + new CompilationInfo(true, AppType.UnoWinUI, "TestProject"), + new CompilationInfo(true, AppType.UnoWinUI, "TestProject")); + } + + private static void AssertHelperTypes(Compilation compilation, string xamlNamespace, string assemblyName) + { + var extension = compilation.GetTypeByMetadataName("TestProject.Strings.ResourcesExtension"); + Assert.NotNull(extension); + Assert.Equal($"{xamlNamespace}.Markup.MarkupExtension", extension.BaseType!.ToDisplayString()); + Assert.Equal(assemblyName, extension.BaseType.ContainingAssembly.Identity.Name); + var converter = Assert.IsAssignableFrom(Assert.Single(extension.GetMembers("Converter"))); + Assert.Equal($"{xamlNamespace}.Data.IValueConverter", converter.Type.ToDisplayString()); + Assert.Equal(assemblyName, converter.Type.ContainingAssembly.Identity.Name); + Assert.Contains(extension.GetAttributes(), attribute => + attribute.AttributeClass?.ToDisplayString() == $"{xamlNamespace}.Markup.MarkupExtensionReturnTypeAttribute" && + attribute.AttributeClass.ContainingAssembly.Identity.Name == assemblyName); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void UseUwpDoesNotOverrideUnoWinUIReferences(bool useUwp) + { + var run = Run([CreatePlatformReference(AppType.UnoWinUI)], useUwp: useUwp); + + Assert.Empty(run.Diagnostics); + AssertHelperTypes(run.OutputCompilation, "Microsoft.UI.Xaml", "Uno.UI"); + run.AssertCompiles(); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void UnoWinUIHonorsResourceInterfaceGeneration(bool generateResourceInterfaces) + { + var run = Run([CreatePlatformReference(AppType.UnoWinUI)], generateResourceInterfaces: generateResourceInterfaces); + var resources = run.Source("Resources.resw"); + + Assert.Empty(run.Diagnostics); + Assert.Equal(generateResourceInterfaces, resources.Contains("public interface IResources")); + Assert.Equal(generateResourceInterfaces, resources.Contains("public sealed class ResourcesProvider : IResources")); + run.AssertCompiles(); + } + + private static void AssertUnrecognized(ReswGeneratorRun run) + { + Assert.Equal("RESWP0005", Assert.Single(run.Diagnostics).Id); + Assert.Empty(run.Sources); + } + + private static ReswGeneratorRun Run( + ImmutableArray references, + bool isLibrary = false, + bool useApplicationLanguages = false, + bool? useUwp = null, + bool? generateResourceInterfaces = null) + { + return ReswGeneratorHarness.Run( + [ReswGeneratorHarness.File("en-US", SampleResources)], + outputType: isLibrary ? "Library" : "WinExe", + defaultLanguage: "en-US", + useApplicationLanguages: useApplicationLanguages, + useUwp: useUwp, + generateResourceInterfaces: generateResourceInterfaces, + references: FrameworkReferences.AddRange(references), + projectDir: null, + msBuildProjectFullPath: @"C:\Project\TestProject.csproj"); + } + + private static MetadataReference CreateReference(string assemblyName, string source, string? filePath = null) + { + return PlatformStubs.Compile(assemblyName, source, filePath ?? @"C:\References\Platform.dll"); + } + + private static string GetReferencePath(AppType appType) + { + return appType switch + { + AppType.WindowsAppSDK => @"C:\Packages\Microsoft.WindowsAppSdk\Platform.dll", + AppType.UWP => @"C:\References\Windows.Foundation.UniversalApiContract.winmd", + _ => @"C:\References\Platform.dll" + }; + } + + private static MetadataReference CreatePlatformReference(AppType appType, string typeKind = "class") + { + var xamlNamespace = appType == AppType.UWP ? "Windows.UI.Xaml" : "Microsoft.UI.Xaml"; + var resourceNamespace = appType == AppType.WindowsAppSDK + ? "Microsoft.Windows.ApplicationModel.Resources" + : "Windows.ApplicationModel.Resources"; + var assemblyName = appType switch + { + AppType.WindowsAppSDK => "Microsoft.WinUI", + AppType.UWP => "Windows.Foundation.UniversalApiContract", + _ => "Uno.UI" + }; + var source = $$""" + namespace {{xamlNamespace}} + { + public {{typeKind}} DependencyObject { } + } + namespace {{xamlNamespace}}.Markup + { + public abstract class MarkupExtension + { + protected abstract object ProvideValue(); + } + public sealed class MarkupExtensionReturnTypeAttribute : System.Attribute + { + public System.Type ReturnType { get; set; } + } + } + namespace {{xamlNamespace}}.Data + { + public interface IValueConverter + { + object Convert(object value, System.Type targetType, object parameter, string language); + object ConvertBack(object value, System.Type targetType, object parameter, string language); + } + } + namespace {{resourceNamespace}} + { + public sealed class ResourceLoader + { + {{(appType == AppType.WindowsAppSDK ? """ + public ResourceLoader(string filePath, string resourceMapId) { } + public static string GetDefaultResourceFilePath() => ""; + """ : """ + public static ResourceLoader GetForViewIndependentUse(string resourceMapId) => new ResourceLoader(); + """)}} + public string GetString(string resourceId) => resourceId; + } + } + namespace Windows.Globalization + { + public static class ApplicationLanguages + { + public static System.Collections.Generic.IReadOnlyList Languages { get; set; } + } + } + """; + + return CreateReference(assemblyName, source, GetReferencePath(appType)); + } +} diff --git a/tests/ReswPlusUnitTests/GeneratorHarness.cs b/tests/ReswPlusUnitTests/GeneratorHarness.cs index f595b1f..8ae3ec2 100644 --- a/tests/ReswPlusUnitTests/GeneratorHarness.cs +++ b/tests/ReswPlusUnitTests/GeneratorHarness.cs @@ -68,6 +68,7 @@ public static ReswFile File(string language, string content, string baseName = " /// 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. + /// Custom compilation references, or to use the platform stubs. /// The result of the run. public static ReswGeneratorRun Run( IEnumerable files, @@ -83,7 +84,8 @@ public static ReswGeneratorRun Run( bool? generateResourceInterfaces = null, string assemblyName = "TestProject", bool nativeAotUwp = false, - IEnumerable? additionalFiles = null) + IEnumerable? additionalFiles = null, + IEnumerable? references = null) { var texts = files .Concat(additionalFiles ?? []) @@ -107,7 +109,7 @@ public static ReswGeneratorRun Run( var compilation = CSharpCompilation.Create( assemblyName, syntaxTrees: null, - nativeAotUwp ? PlatformStubs.NativeAotUwpReferences() : PlatformStubs.ReferencesFor(appType), + references ?? (nativeAotUwp ? PlatformStubs.NativeAotUwpReferences() : PlatformStubs.ReferencesFor(appType)), new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); var driver = CSharpGeneratorDriver.Create( @@ -243,6 +245,16 @@ public ReswGeneratorRun RunAgain(IEnumerable files) return From(_driver.ReplaceAdditionalTexts(texts), _inputCompilation, texts); } + /// + /// Runs the generator again with new compilation references, reusing the state of the previous run. + /// + /// The references of the new compilation. + /// The result of the run with the new references. + public ReswGeneratorRun RunAgainWithReferences(IEnumerable references) + { + return From(_driver, _inputCompilation.RemoveAllReferences().AddReferences(references), _texts); + } + /// /// Returns the source emitted under the hint name holding the given text. /// @@ -627,7 +639,14 @@ private static bool IsWindowsProjection(string path) || name.Equals("WinRT.Runtime", StringComparison.OrdinalIgnoreCase); } - private static MetadataReference Compile(string assemblyName, string source, string filePath) + /// + /// Compiles platform types into an in-memory reference with a caller-supplied identity and display path. + /// + /// The defining assembly identity. + /// The source of the platform types. + /// The reference path the generator sees. + /// The compiled reference. + public static MetadataReference Compile(string assemblyName, string source, string filePath) { var compilation = CSharpCompilation.Create( assemblyName, From b56bb2c2938ceafce172aaa649ac67d2ab1d2150 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 13 Oct 2026 18:28:52 -0700 Subject: [PATCH 2/4] Remove manual wrapping from documentation paragraphs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 5 +---- docs/FAQ.md | 36 ++++++++---------------------------- nuget/README.md | 4 +--- 3 files changed, 10 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 0f81716..530f2a1 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,7 @@ _**Now available as a Source Generator!**_ ⚡ [How to Install ReswPlus](https://github.com/reswplus/ReswPlus/wiki/How-to-install-ReswPlus) – Step-by-step installation guide. -The source generator supports UWP, Windows App SDK, and Uno WinUI applications and libraries. -Uno WinUI is detected automatically from its referenced types and uses `Microsoft.UI.Xaml` helpers -with `Windows.ApplicationModel.Resources.ResourceLoader`; no `UseUwp=true` override or -`Windows.UI.Xaml` compatibility types are needed. +The source generator supports UWP, Windows App SDK, and Uno WinUI applications and libraries. Uno WinUI is detected automatically from its referenced types and uses `Microsoft.UI.Xaml` helpers with `Windows.ApplicationModel.Resources.ResourceLoader`; no `UseUwp=true` override or `Windows.UI.Xaml` compatibility types are needed. ReswPlus recognizes a UWP project by the `Windows.Foundation.UniversalApiContract` reference it carries, and a Windows App SDK project by its own. A **UWP project built for Native AOT** has no such reference, so it declares what it is with the standard `UseUwp` property instead, which ReswPlus reads and trusts over the references: diff --git a/docs/FAQ.md b/docs/FAQ.md index a4d1163..dfd06ee 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,32 +1,21 @@ ## What's the minimum version of Windows 10 supported for applications using Resw? -Because ReswPlus's markup extension (deprecated — see below) uses [MarkupExtension](https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.markup.markupextension), an app that still uses it needs -Windows 10 Fall Creators Update (1709). An app that uses only the generated class and `x:Bind` has no such requirement. +Because ReswPlus's markup extension (deprecated — see below) uses [MarkupExtension](https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.markup.markupextension), an app that still uses it needs Windows 10 Fall Creators Update (1709). An app that uses only the generated class and `x:Bind` has no such requirement. ## Can I use the markup extension in an app compiled with Native AOT? -No, and it is deprecated for that reason. A markup extension is created by the XAML parser while it reads a -page, and a UWP app compiled with Native AOT cannot create it, so a page that uses one fails to load with -`Markup extension could not provide value`. Preserving the generated types with `rd.xml` or a trimmer root -does not change it. +No, and it is deprecated for that reason. A markup extension is created by the XAML parser while it reads a page, and a UWP app compiled with Native AOT cannot create it, so a page that uses one fails to load with `Markup extension could not provide value`. Preserving the generated types with `rd.xml` or a trimmer root does not change it. -It is still generated, so an app that does not use Native AOT keeps building, but it is marked `[Obsolete]` -and the build tells you what to write instead. A page that uses it reports `WMC1500` on the line of the -markup itself, and the XAML compiler's own generated type table reports `CS0618` alongside it. A build with -`TreatWarningsAsErrors` will therefore fail until the markup is rewritten or the warning is suppressed. +It is still generated, so an app that does not use Native AOT keeps building, but it is marked `[Obsolete]` and the build tells you what to write instead. A page that uses it reports `WMC1500` on the line of the markup itself, and the XAML compiler's own generated type table reports `CS0618` alongside it. A build with `TreatWarningsAsErrors` will therefore fail until the markup is rewritten or the warning is suppressed. -Use `x:Bind`, which reads the same generated members, is resolved while the app is compiled, and works -whichever way the app is built: +Use `x:Bind`, which reads the same generated members, is resolved while the app is compiled, and works whichever way the app is built: | Instead of | Write | | --- | --- | | `{strings:Resources Key=Foo}` | `{x:Bind strings:Resources.Foo}` | | `{strings:Resources Key=Foo, Converter={StaticResource C}}` | `{x:Bind strings:Resources.Foo, Converter={StaticResource C}}` | -`x:Bind` is not available everywhere the markup extension was. A `Setter` in a `Style`, and a standalone -`ResourceDictionary` with no code-behind — a shared `Styles.xaml`, say — are the two common cases: `x:Bind` -needs a compiled-binding host, which those do not have. Set those from code-behind using the same generated -members: +`x:Bind` is not available everywhere the markup extension was. A `Setter` in a `Style`, and a standalone `ResourceDictionary` with no code-behind — a shared `Styles.xaml`, say — are the two common cases: `x:Bind` needs a compiled-binding host, which those do not have. Set those from code-behind using the same generated members: ```csharp myTextBlock.Text = Resources.Foo; @@ -40,20 +29,11 @@ VB support is currently under development. C++/CX and C++/WinRT support are plan ## Does the source generator support Uno WinUI? -Yes. ReswPlus detects `Microsoft.UI.Xaml.DependencyObject` from the `Uno.UI` assembly, -regardless of the NuGet package path or whether the type is a class or interface. -It generates `Microsoft.UI.Xaml` helpers and uses -`Windows.ApplicationModel.Resources.ResourceLoader.GetForViewIndependentUse`. +Yes. ReswPlus detects `Microsoft.UI.Xaml.DependencyObject` from the `Uno.UI` assembly, regardless of the NuGet package path or whether the type is a class or interface. It generates `Microsoft.UI.Xaml` helpers and uses `Windows.ApplicationModel.Resources.ResourceLoader.GetForViewIndependentUse`. -Generated macro helpers are still subject to Uno's Windows API support. Uno may -report `Uno0001` warnings for unsupported APIs in those helpers, even when the -resource uses a different macro. +Generated macro helpers are still subject to Uno's Windows API support. Uno may report `Uno0001` warnings for unsupported APIs in those helpers, even when the resource uses a different macro. -If you previously worked around `RESWP0005` by setting `UseUwp=true` and adding -`Windows.UI.Xaml` versions of `MarkupExtension`, `MarkupExtensionReturnTypeAttribute`, -and `IValueConverter`, remove that override and those compatibility types after updating. -A separate `WinRT.DynamicWindowsRuntimeCastAttribute` polyfill, if required by your app, -is unrelated to ReswPlus and should be kept. +If you previously worked around `RESWP0005` by setting `UseUwp=true` and adding `Windows.UI.Xaml` versions of `MarkupExtension`, `MarkupExtensionReturnTypeAttribute`, and `IValueConverter`, remove that override and those compatibility types after updating. A separate `WinRT.DynamicWindowsRuntimeCastAttribute` polyfill, if required by your app, is unrelated to ReswPlus and should be kept. ## Is it free? diff --git a/nuget/README.md b/nuget/README.md index da15818..4877cc4 100644 --- a/nuget/README.md +++ b/nuget/README.md @@ -10,9 +10,7 @@ dotnet add package ReswPlus The package adds the generator and its transitive MSBuild integration. Add or update the default-language `.resw` file in your project and ReswPlus generates resource properties and methods during compilation. -Supports UWP, Windows App SDK, and Uno WinUI applications and libraries. Uno WinUI is detected -automatically and uses WinUI XAML helpers with the Windows resource loader, without `UseUwp=true` -or `Windows.UI.Xaml` compatibility types. +Supports UWP, Windows App SDK, and Uno WinUI applications and libraries. Uno WinUI is detected automatically and uses WinUI XAML helpers with the Windows resource loader, without `UseUwp=true` or `Windows.UI.Xaml` compatibility types. ## Features From abe8863e7e528b4857ce3ef16bf1c5026d3196ed Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 14 Oct 2026 10:50:32 -0700 Subject: [PATCH 3/4] Recognize the Uno 7 WinUI assembly Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/FAQ.md | 2 +- src/ReswPlus.SourceGenerator/ReswGenerator.cs | 4 +- tests/ReswPlusUnitTests/AppTypeDetection.cs | 125 ++++++++++++------ 3 files changed, 91 insertions(+), 40 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index dfd06ee..2b149fc 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -29,7 +29,7 @@ VB support is currently under development. C++/CX and C++/WinRT support are plan ## Does the source generator support Uno WinUI? -Yes. ReswPlus detects `Microsoft.UI.Xaml.DependencyObject` from the `Uno.UI` assembly, regardless of the NuGet package path or whether the type is a class or interface. It generates `Microsoft.UI.Xaml` helpers and uses `Windows.ApplicationModel.Resources.ResourceLoader.GetForViewIndependentUse`. +Yes. ReswPlus detects `Microsoft.UI.Xaml.DependencyObject` from the `Uno.UI` assembly or the `Uno.WinUI` assembly used by Uno 7.0 and later, regardless of the NuGet package path or whether the type is a class or interface. It generates `Microsoft.UI.Xaml` helpers and uses `Windows.ApplicationModel.Resources.ResourceLoader.GetForViewIndependentUse`. Generated macro helpers are still subject to Uno's Windows API support. Uno may report `Uno0001` warnings for unsupported APIs in those helpers, even when the resource uses a different macro. diff --git a/src/ReswPlus.SourceGenerator/ReswGenerator.cs b/src/ReswPlus.SourceGenerator/ReswGenerator.cs index 7bbdaa5..5177d60 100644 --- a/src/ReswPlus.SourceGenerator/ReswGenerator.cs +++ b/src/ReswPlus.SourceGenerator/ReswGenerator.cs @@ -269,9 +269,9 @@ private static void EmitSupport(SourceProductionContext spc, ReswSupport support /// private static AppType RetrieveAppType(Compilation compilation) { - // Uno.WinUI defines WinUI's types in Uno.UI, but implements the Windows resource loader. + // Uno provides WinUI XAML types, but implements the Windows resource loader. var dependencyObject = compilation.GetTypeByMetadataName("Microsoft.UI.Xaml.DependencyObject"); - if (dependencyObject?.ContainingAssembly.Identity.Name == "Uno.UI") + if (dependencyObject?.ContainingAssembly.Identity.Name is "Uno.UI" or "Uno.WinUI") { return AppType.UnoWinUI; } diff --git a/tests/ReswPlusUnitTests/AppTypeDetection.cs b/tests/ReswPlusUnitTests/AppTypeDetection.cs index b7fce6b..571ebdd 100644 --- a/tests/ReswPlusUnitTests/AppTypeDetection.cs +++ b/tests/ReswPlusUnitTests/AppTypeDetection.cs @@ -18,17 +18,25 @@ public class AppTypeDetection ("FileCount_Other", "{0} files", "#Format[Plural Int count]")); [Theory] - [InlineData("interface", false, false)] - [InlineData("interface", true, false)] - [InlineData("class", false, false)] - [InlineData("class", true, false)] - [InlineData("interface", false, true)] - [InlineData("interface", true, true)] - [InlineData("class", false, true)] - [InlineData("class", true, true)] - public void UnoWinUIUsesWinUIHelpersAndWindowsResources(string typeKind, bool isLibrary, bool useApplicationLanguages) + [InlineData("Uno.UI", "interface", false, false)] + [InlineData("Uno.UI", "interface", true, false)] + [InlineData("Uno.UI", "class", false, false)] + [InlineData("Uno.UI", "class", true, false)] + [InlineData("Uno.UI", "interface", false, true)] + [InlineData("Uno.UI", "interface", true, true)] + [InlineData("Uno.UI", "class", false, true)] + [InlineData("Uno.UI", "class", true, true)] + [InlineData("Uno.WinUI", "interface", false, false)] + [InlineData("Uno.WinUI", "interface", true, false)] + [InlineData("Uno.WinUI", "class", false, false)] + [InlineData("Uno.WinUI", "class", true, false)] + [InlineData("Uno.WinUI", "interface", false, true)] + [InlineData("Uno.WinUI", "interface", true, true)] + [InlineData("Uno.WinUI", "class", false, true)] + [InlineData("Uno.WinUI", "class", true, true)] + public void UnoWinUIUsesWinUIHelpersAndWindowsResources(string unoAssemblyName, string typeKind, bool isLibrary, bool useApplicationLanguages) { - var reference = CreatePlatformReference(AppType.UnoWinUI, typeKind); + var reference = CreatePlatformReference(AppType.UnoWinUI, typeKind, unoAssemblyName); Assert.DoesNotContain("Uno", reference.Display!); var run = Run([reference], isLibrary, useApplicationLanguages); @@ -68,7 +76,7 @@ public void UnoWinUIUsesWinUIHelpersAndWindowsResources(string typeKind, bool is } run.AssertCompiles(); - AssertHelperTypes(run.OutputCompilation, "Microsoft.UI.Xaml", "Uno.UI"); + AssertHelperTypes(run.OutputCompilation, "Microsoft.UI.Xaml", unoAssemblyName); } [Theory] @@ -93,38 +101,45 @@ public void ExistingPlatformsKeepTheirHelpersAndResourceLoaders(AppType appType, [Theory] [InlineData("Unrelated.UI", "Microsoft.UI.Xaml")] - [InlineData("Uno.WinUI", "Microsoft.UI.Xaml")] + [InlineData("Uno.WinUI.Unrelated", "Microsoft.UI.Xaml")] [InlineData("Uno.UI", "Windows.UI.Xaml")] - public void MatchingNamespaceOrPackageNameAloneDoesNotSelectUno(string assemblyName, string xamlNamespace) + [InlineData("Uno.WinUI", "Windows.UI.Xaml")] + public void MatchingNamespaceOrSimilarAssemblyNameAloneDoesNotSelectUno(string assemblyName, string xamlNamespace) { var reference = CreateReference(assemblyName, $"namespace {xamlNamespace} {{ public interface DependencyObject {{ }} }}"); AssertUnrecognized(Run([reference])); } - [Fact] - public void MissingDependencyObjectDoesNotSelectUno() + [Theory] + [InlineData("Uno.UI")] + [InlineData("Uno.WinUI")] + public void MissingDependencyObjectDoesNotSelectUno(string unoAssemblyName) { - AssertUnrecognized(Run([CreateReference("Uno.UI", "public class UnrelatedType { }")])); + AssertUnrecognized(Run([CreateReference(unoAssemblyName, "public class UnrelatedType { }")])); } - [Fact] - public void AmbiguousDependencyObjectDoesNotSelectUno() + [Theory] + [InlineData("Uno.UI")] + [InlineData("Uno.WinUI")] + public void AmbiguousDependencyObjectDoesNotSelectUno(string unoAssemblyName) { const string source = "namespace Microsoft.UI.Xaml { public interface DependencyObject { } }"; - var run = Run([CreateReference("Uno.UI", source), CreateReference("Unrelated.UI", source)]); + var run = Run([CreateReference(unoAssemblyName, source), CreateReference("Unrelated.UI", source)]); Assert.Null(run.OutputCompilation.GetTypeByMetadataName("Microsoft.UI.Xaml.DependencyObject")); AssertUnrecognized(run); } [Theory] - [InlineData(AppType.WindowsAppSDK)] - [InlineData(AppType.UWP)] - public void UnoWinUIDetectionTakesPrecedenceOverReferencePathHints(AppType hintedAppType) + [InlineData(AppType.WindowsAppSDK, "Uno.UI")] + [InlineData(AppType.UWP, "Uno.UI")] + [InlineData(AppType.WindowsAppSDK, "Uno.WinUI")] + [InlineData(AppType.UWP, "Uno.WinUI")] + public void UnoWinUIDetectionTakesPrecedenceOverReferencePathHints(AppType hintedAppType, string unoAssemblyName) { var hintedReference = CreateReference("PathHint", "public class UnrelatedType { }", GetReferencePath(hintedAppType)); - var run = Run([CreatePlatformReference(AppType.UnoWinUI), hintedReference]); + var run = Run([CreatePlatformReference(AppType.UnoWinUI, unoAssemblyName: unoAssemblyName), hintedReference]); Assert.Empty(run.Diagnostics); Assert.Contains("using Microsoft.UI.Xaml.Markup;", run.Source("Resources.resw")); @@ -170,13 +185,15 @@ public void WinAppSDKStillTakesPrecedenceOverUWPReferencePath() Assert.Contains("using Microsoft.Windows.ApplicationModel.Resources;", run.Source("ResourceStringProvider")); } - [Fact] - public void ChangingReferencesInvalidatesTheIncrementalAppType() + [Theory] + [InlineData("Uno.UI")] + [InlineData("Uno.WinUI")] + public void ChangingReferencesInvalidatesTheIncrementalAppType(string unoAssemblyName) { var before = Run([CreatePlatformReference(AppType.WindowsAppSDK)]); Assert.Contains("using Microsoft.Windows.ApplicationModel.Resources;", before.Source("ResourceStringProvider")); - var after = before.RunAgainWithReferences(FrameworkReferences.Add(CreatePlatformReference(AppType.UnoWinUI))); + var after = before.RunAgainWithReferences(FrameworkReferences.Add(CreatePlatformReference(AppType.UnoWinUI, unoAssemblyName: unoAssemblyName))); Assert.Empty(after.Diagnostics); Assert.Contains("using Windows.ApplicationModel.Resources;", after.Source("ResourceStringProvider")); @@ -194,6 +211,34 @@ public void ChangingReferencesInvalidatesTheIncrementalAppType() new CompilationInfo(true, AppType.UnoWinUI, "TestProject")); } + [Fact] + public void MovingDependencyObjectToTheUnoWinUIAssemblyKeepsTheAppModelUnchanged() + { + var before = Run([CreatePlatformReference(AppType.UnoWinUI)]); + var after = before.RunAgainWithReferences(FrameworkReferences.Add( + CreatePlatformReference(AppType.UnoWinUI, unoAssemblyName: "Uno.WinUI"))); + + Assert.Empty(after.Diagnostics); + Assert.Equal(before.Source("Resources.resw"), after.Source("Resources.resw")); + Assert.Equal(before.Source("ResourceStringProvider"), after.Source("ResourceStringProvider")); + after.AssertReused(TrackingNames.CompilationInfo); + after.AssertReused(TrackingNames.Project); + after.AssertReused(TrackingNames.Generation); + after.AssertReused(TrackingNames.Support); + AssertHelperTypes(after.OutputCompilation, "Microsoft.UI.Xaml", "Uno.WinUI"); + after.AssertCompiles(); + } + + [Fact] + public void ADependencyObjectDefinedInBothUnoAssembliesIsAmbiguous() + { + const string source = "namespace Microsoft.UI.Xaml { public interface DependencyObject { } }"; + var run = Run([CreateReference("Uno.UI", source), CreateReference("Uno.WinUI", source)]); + + Assert.Null(run.OutputCompilation.GetTypeByMetadataName("Microsoft.UI.Xaml.DependencyObject")); + AssertUnrecognized(run); + } + private static void AssertHelperTypes(Compilation compilation, string xamlNamespace, string assemblyName) { var extension = compilation.GetTypeByMetadataName("TestProject.Strings.ResourcesExtension"); @@ -209,23 +254,29 @@ private static void AssertHelperTypes(Compilation compilation, string xamlNamesp } [Theory] - [InlineData(false)] - [InlineData(true)] - public void UseUwpDoesNotOverrideUnoWinUIReferences(bool useUwp) + [InlineData(false, "Uno.UI")] + [InlineData(true, "Uno.UI")] + [InlineData(false, "Uno.WinUI")] + [InlineData(true, "Uno.WinUI")] + public void UseUwpDoesNotOverrideUnoWinUIReferences(bool useUwp, string unoAssemblyName) { - var run = Run([CreatePlatformReference(AppType.UnoWinUI)], useUwp: useUwp); + var run = Run([CreatePlatformReference(AppType.UnoWinUI, unoAssemblyName: unoAssemblyName)], useUwp: useUwp); Assert.Empty(run.Diagnostics); - AssertHelperTypes(run.OutputCompilation, "Microsoft.UI.Xaml", "Uno.UI"); + AssertHelperTypes(run.OutputCompilation, "Microsoft.UI.Xaml", unoAssemblyName); run.AssertCompiles(); } [Theory] - [InlineData(false)] - [InlineData(true)] - public void UnoWinUIHonorsResourceInterfaceGeneration(bool generateResourceInterfaces) + [InlineData(false, "Uno.UI")] + [InlineData(true, "Uno.UI")] + [InlineData(false, "Uno.WinUI")] + [InlineData(true, "Uno.WinUI")] + public void UnoWinUIHonorsResourceInterfaceGeneration(bool generateResourceInterfaces, string unoAssemblyName) { - var run = Run([CreatePlatformReference(AppType.UnoWinUI)], generateResourceInterfaces: generateResourceInterfaces); + var run = Run( + [CreatePlatformReference(AppType.UnoWinUI, unoAssemblyName: unoAssemblyName)], + generateResourceInterfaces: generateResourceInterfaces); var resources = run.Source("Resources.resw"); Assert.Empty(run.Diagnostics); @@ -274,7 +325,7 @@ private static string GetReferencePath(AppType appType) }; } - private static MetadataReference CreatePlatformReference(AppType appType, string typeKind = "class") + private static MetadataReference CreatePlatformReference(AppType appType, string typeKind = "class", string unoAssemblyName = "Uno.UI") { var xamlNamespace = appType == AppType.UWP ? "Windows.UI.Xaml" : "Microsoft.UI.Xaml"; var resourceNamespace = appType == AppType.WindowsAppSDK @@ -284,7 +335,7 @@ private static MetadataReference CreatePlatformReference(AppType appType, string { AppType.WindowsAppSDK => "Microsoft.WinUI", AppType.UWP => "Windows.Foundation.UniversalApiContract", - _ => "Uno.UI" + _ => unoAssemblyName }; var source = $$""" namespace {{xamlNamespace}} From e9ced7bdeb02e09d1a81012692072de92bb0a10e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 14 Oct 2026 11:15:36 -0700 Subject: [PATCH 4/4] Add an opt-in application model override Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 18 +- docs/FAQ.md | 6 + nuget/README.md | 2 + nuget/ReswPlus.targets | 2 + .../AnalyzerReleases.Unshipped.md | 1 + src/ReswPlus.SourceGenerator/Diagnostics.cs | 14 +- .../Pipeline/AppTypeSelection.cs | 10 + .../Pipeline/ReswBuildOptions.cs | 40 +- .../Pipeline/ReswProject.cs | 23 +- .../AppTypeBuildIntegration.cs | 138 ++++++ tests/ReswPlusUnitTests/AppTypeOverride.cs | 428 ++++++++++++++++++ tests/ReswPlusUnitTests/GeneratorHarness.cs | 72 ++- 12 files changed, 737 insertions(+), 17 deletions(-) create mode 100644 src/ReswPlus.SourceGenerator/Pipeline/AppTypeSelection.cs create mode 100644 tests/ReswPlusUnitTests/AppTypeBuildIntegration.cs create mode 100644 tests/ReswPlusUnitTests/AppTypeOverride.cs diff --git a/README.md b/README.md index 530f2a1..7ac798e 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ _**Now available as a Source Generator!**_ The source generator supports UWP, Windows App SDK, and Uno WinUI applications and libraries. Uno WinUI is detected automatically from its referenced types and uses `Microsoft.UI.Xaml` helpers with `Windows.ApplicationModel.Resources.ResourceLoader`; no `UseUwp=true` override or `Windows.UI.Xaml` compatibility types are needed. -ReswPlus recognizes a UWP project by the `Windows.Foundation.UniversalApiContract` reference it carries, and a Windows App SDK project by its own. A **UWP project built for Native AOT** has no such reference, so it declares what it is with the standard `UseUwp` property instead, which ReswPlus reads and trusts over the references: +ReswPlus recognizes a UWP project by the `Windows.Foundation.UniversalApiContract` reference it carries, and a Windows App SDK project by its own. A **UWP project built for Native AOT** has no such reference, so it declares what it is with the standard `UseUwp` property instead. This property fills in the app model only when the references cannot identify it: ```xml @@ -48,7 +48,21 @@ ReswPlus recognizes a UWP project by the `Windows.Foundation.UniversalApiContrac ``` -Without it, such a project is reported as `RESWP0005` and no code is generated for it. The `samples/UWP` folder has one sample of each kind: `ReswPlusUWPSample` built with .NET Native, and `ReswPlusNativeAotUwpSample` built on modern .NET with Native AOT. +Without `UseUwp=true` or an explicit `ReswPlusAppType=UWP`, such a project is reported as `RESWP0005` and no code is generated for it. The `samples/UWP` folder has one sample of each kind: `ReswPlusUWPSample` built with .NET Native, and `ReswPlusNativeAotUwpSample` built on modern .NET with Native AOT. + +### Override application model detection + +Automatic detection remains the default. If it cannot recognize your configuration or selects the wrong app model, opt in to an explicit `ReswPlusAppType`. For example, an Uno application can override its non-Windows heads without changing a Windows App SDK head: + +```xml + + UnoWinUI + +``` + +Supported values are `Auto`, `WindowsAppSDK`, `UWP`, and `UnoWinUI`, matched case-insensitively with surrounding whitespace ignored. Unset, empty, or `Auto` keeps automatic detection and the existing `UseUwp` fallback. Any other value reports `RESWP0021` and stops generation rather than silently falling back. + +An explicit app model takes precedence over both the detected references and `UseUwp`. The override selects the generated XAML helpers, resource loader, and optional application-language resolver; it does not add platform references. Choose the model whose APIs your target actually provides, and condition the property per target framework when different heads use different app models. ### Injectable resource interfaces diff --git a/docs/FAQ.md b/docs/FAQ.md index 2b149fc..bbd1b77 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -35,6 +35,12 @@ Generated macro helpers are still subject to Uno's Windows API support. Uno may If you previously worked around `RESWP0005` by setting `UseUwp=true` and adding `Windows.UI.Xaml` versions of `MarkupExtension`, `MarkupExtensionReturnTypeAttribute`, and `IValueConverter`, remove that override and those compatibility types after updating. A separate `WinRT.DynamicWindowsRuntimeCastAttribute` polyfill, if required by your app, is unrelated to ReswPlus and should be kept. +## Can I override the detected application model? + +Yes. Set `ReswPlusAppType` to `WindowsAppSDK`, `UWP`, or `UnoWinUI` to explicitly select the app model. Leaving it unset, empty, or `Auto` preserves automatic detection and the existing `UseUwp` fallback. Supported names are case-insensitive and ignore surrounding whitespace; invalid values report `RESWP0021` and stop generation. + +An explicit `ReswPlusAppType` takes precedence over both references and `UseUwp`, but does not supply any missing platform APIs. For a multi-targeting Uno app, apply the override only to the affected target frameworks: Windows App SDK heads should keep their own app model. See the [configuration example](../README.md#override-application-model-detection). + ## Is it free? Yes and it won't change. diff --git a/nuget/README.md b/nuget/README.md index 4877cc4..8811bcc 100644 --- a/nuget/README.md +++ b/nuget/README.md @@ -12,6 +12,8 @@ The package adds the generator and its transitive MSBuild integration. Add or up Supports UWP, Windows App SDK, and Uno WinUI applications and libraries. Uno WinUI is detected automatically and uses WinUI XAML helpers with the Windows resource loader, without `UseUwp=true` or `Windows.UI.Xaml` compatibility types. +If automatic detection cannot recognize your configuration or selects the wrong app model, opt in by setting `ReswPlusAppType` to `WindowsAppSDK`, `UWP`, or `UnoWinUI`. The default `Auto` preserves detection and the existing `UseUwp` fallback; an explicit selection overrides both. Configure it per target framework when needed, and retain the corresponding platform references. Invalid values report `RESWP0021`. + ## Features - Strongly typed resource properties for XAML and C# diff --git a/nuget/ReswPlus.targets b/nuget/ReswPlus.targets index f99e603..c0697d1 100644 --- a/nuget/ReswPlus.targets +++ b/nuget/ReswPlus.targets @@ -7,12 +7,14 @@ + $(AdditionalFileItemNames);PRIResource + Auto false true diff --git a/src/ReswPlus.SourceGenerator/AnalyzerReleases.Unshipped.md b/src/ReswPlus.SourceGenerator/AnalyzerReleases.Unshipped.md index 6108aa0..292ac88 100644 --- a/src/ReswPlus.SourceGenerator/AnalyzerReleases.Unshipped.md +++ b/src/ReswPlus.SourceGenerator/AnalyzerReleases.Unshipped.md @@ -14,3 +14,4 @@ RESWP0017 | Resources | Info | A translated resource does not exist in th RESWP0018 | Resources | Info | A translated value is identical to its default-language value. RESWP0019 | Resources | Warning | A translation cannot serve the resource shape generated from the default language. RESWP0020 | Resources | Info | A translation defines variants that do not exist in the default language. +RESWP0021 | Compatibility | Error | ReswPlusAppType specifies an unsupported application model. diff --git a/src/ReswPlus.SourceGenerator/Diagnostics.cs b/src/ReswPlus.SourceGenerator/Diagnostics.cs index 062e7ec..06991d8 100644 --- a/src/ReswPlus.SourceGenerator/Diagnostics.cs +++ b/src/ReswPlus.SourceGenerator/Diagnostics.cs @@ -76,7 +76,7 @@ internal static class Diagnostics public static readonly DiagnosticDescriptor UnrecognizedAppType = new( "RESWP0005", "Project type not recognized", - "ReswPlus only supports UWP, WinAppSDK, and Uno WinUI applications/libraries", + "ReswPlus only supports UWP, WinAppSDK, and Uno WinUI applications/libraries; set ReswPlusAppType to WindowsAppSDK, UWP, or UnoWinUI to override automatic detection", CompatibilityCategory, DiagnosticSeverity.Error, isEnabledByDefault: true); @@ -272,6 +272,17 @@ internal static class Diagnostics DiagnosticSeverity.Info, isEnabledByDefault: true); + /// + /// RESWP0021: the explicit application model selection is not supported. + /// + public static readonly DiagnosticDescriptor InvalidAppTypeOverride = new( + "RESWP0021", + "Invalid application model override", + "ReswPlusAppType must be Auto, WindowsAppSDK, UWP, or UnoWinUI", + CompatibilityCategory, + DiagnosticSeverity.Error, + isEnabledByDefault: true); + /// /// Returns the descriptor of a diagnostic from its identifier. /// @@ -291,6 +302,7 @@ public static DiagnosticDescriptor GetDescriptor(string id) "RESWP0003" => MissingRootPath, "RESWP0004" => UnknownProjectType, "RESWP0005" => UnrecognizedAppType, + "RESWP0021" => InvalidAppTypeOverride, _ => throw new System.ArgumentOutOfRangeException(nameof(id), id, "No diagnostic carries that identifier."), }; } diff --git a/src/ReswPlus.SourceGenerator/Pipeline/AppTypeSelection.cs b/src/ReswPlus.SourceGenerator/Pipeline/AppTypeSelection.cs new file mode 100644 index 0000000..d45a4dc --- /dev/null +++ b/src/ReswPlus.SourceGenerator/Pipeline/AppTypeSelection.cs @@ -0,0 +1,10 @@ +namespace ReswPlus.SourceGenerator.Pipeline; + +internal enum AppTypeSelection +{ + Auto, + WindowsAppSDK, + UWP, + UnoWinUI, + Invalid, +} diff --git a/src/ReswPlus.SourceGenerator/Pipeline/ReswBuildOptions.cs b/src/ReswPlus.SourceGenerator/Pipeline/ReswBuildOptions.cs index 4f27908..218f8bb 100644 --- a/src/ReswPlus.SourceGenerator/Pipeline/ReswBuildOptions.cs +++ b/src/ReswPlus.SourceGenerator/Pipeline/ReswBuildOptions.cs @@ -25,6 +25,7 @@ private ReswBuildOptions( string? rootNamespace, bool useApplicationLanguages, bool useUwp, + AppTypeSelection appTypeOverride, bool generateResourceInterfaces) { ProjectDir = projectDir; @@ -35,6 +36,7 @@ private ReswBuildOptions( RootNamespace = rootNamespace; UseApplicationLanguages = useApplicationLanguages; UseUwp = useUwp; + AppTypeOverride = appTypeOverride; GenerateResourceInterfaces = generateResourceInterfaces; } @@ -63,10 +65,16 @@ private ReswBuildOptions( /// A UWP project is otherwise recognized by the Windows.Foundation.UniversalApiContract reference it /// carries, which a UWP project built for Native AOT does not have. Such a project says so with this /// property instead. It fills in what the references don't say rather than overriding them: a compilation - /// whose references positively identify it is left alone. + /// whose references positively identify it is left alone. An explicit ReswPlusAppType takes + /// precedence over both the references and this fallback. /// public bool UseUwp { get; } + /// + /// Gets the explicit application model selection, or to use detection. + /// + public AppTypeSelection AppTypeOverride { get; } + /// /// Gets whether injectable resource interfaces and providers should be generated. /// @@ -88,11 +96,39 @@ 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, + ParseAppTypeOverride(Get("build_property.ReswPlusAppType")), !bool.TryParse(Get("build_property.ReswPlusGenerateResourceInterfaces"), out var parsedInterfaces) || parsedInterfaces); string? Get(string key) => globalOptions.TryGetValue(key, out var value) ? value : null; } + private static AppTypeSelection ParseAppTypeOverride(string? value) + { + value = value?.Trim(); + + if (string.IsNullOrEmpty(value) || string.Equals(value, "Auto", StringComparison.OrdinalIgnoreCase)) + { + return AppTypeSelection.Auto; + } + + if (string.Equals(value, "WindowsAppSDK", StringComparison.OrdinalIgnoreCase)) + { + return AppTypeSelection.WindowsAppSDK; + } + + if (string.Equals(value, "UWP", StringComparison.OrdinalIgnoreCase)) + { + return AppTypeSelection.UWP; + } + + if (string.Equals(value, "UnoWinUI", StringComparison.OrdinalIgnoreCase)) + { + return AppTypeSelection.UnoWinUI; + } + + return AppTypeSelection.Invalid; + } + /// /// Returns the directory the project is rooted at. /// @@ -119,6 +155,7 @@ public bool Equals(ReswBuildOptions? other) && RootNamespace == other.RootNamespace && UseApplicationLanguages == other.UseApplicationLanguages && UseUwp == other.UseUwp + && AppTypeOverride == other.AppTypeOverride && GenerateResourceInterfaces == other.GenerateResourceInterfaces; } @@ -137,6 +174,7 @@ public override int GetHashCode() hash = (hash * 31) + UseApplicationLanguages.GetHashCode(); hash = (hash * 31) + UseUwp.GetHashCode(); + hash = (hash * 31) + AppTypeOverride.GetHashCode(); return (hash * 31) + GenerateResourceInterfaces.GetHashCode(); } } diff --git a/src/ReswPlus.SourceGenerator/Pipeline/ReswProject.cs b/src/ReswPlus.SourceGenerator/Pipeline/ReswProject.cs index 464da55..3bbff3c 100644 --- a/src/ReswPlus.SourceGenerator/Pipeline/ReswProject.cs +++ b/src/ReswPlus.SourceGenerator/Pipeline/ReswProject.cs @@ -108,13 +108,22 @@ public static ReswProject Create(CompilationInfo compilationInfo, ReswBuildOptio problems.Add(Diagnostics.UnknownProjectType.Id); } - // A UWP project built for Native AOT carries no 'Windows.Foundation.UniversalApiContract' reference for - // the compilation to be recognized by, and says what it is with the 'UseUwp' property instead. The - // property fills in what the references don't say rather than overriding them, so that a project whose - // references positively identify it can never be taken for something else by a stray property. - var appType = compilationInfo.AppType is AppType.Unknown && options.UseUwp - ? AppType.UWP - : compilationInfo.AppType; + if (options.AppTypeOverride == AppTypeSelection.Invalid) + { + return Unsupported([.. problems, Diagnostics.InvalidAppTypeOverride.Id]); + } + + // The explicit selection overrides detection; UseUwp remains only a fallback for unrecognized projects. + var appType = options.AppTypeOverride switch + { + AppTypeSelection.WindowsAppSDK => AppType.WindowsAppSDK, + AppTypeSelection.UWP => AppType.UWP, + AppTypeSelection.UnoWinUI => AppType.UnoWinUI, + AppTypeSelection.Auto => compilationInfo.AppType is AppType.Unknown && options.UseUwp + ? AppType.UWP + : compilationInfo.AppType, + _ => throw new ArgumentOutOfRangeException(nameof(options.AppTypeOverride)), + }; if (appType is not (AppType.WindowsAppSDK or AppType.UWP or AppType.UnoWinUI)) { diff --git a/tests/ReswPlusUnitTests/AppTypeBuildIntegration.cs b/tests/ReswPlusUnitTests/AppTypeBuildIntegration.cs new file mode 100644 index 0000000..99e425f --- /dev/null +++ b/tests/ReswPlusUnitTests/AppTypeBuildIntegration.cs @@ -0,0 +1,138 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.Json; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Xunit; + +namespace ReswPlusUnitTests; + +public class AppTypeBuildIntegration +{ + [Theory] + [InlineData(null, "Auto")] + [InlineData("", "Auto")] + [InlineData("WindowsAppSDK", "WindowsAppSDK")] + [InlineData("UWP", "UWP")] + [InlineData("UnoWinUI", "UnoWinUI")] + [InlineData(" uWp ", " uWp ")] + [InlineData("Invalid", "Invalid")] + public async Task ThePackagedTargetsExposeTheDefaultOrUserSelection(string? selection, string expected) + { + var properties = new XElement("PropertyGroup"); + if (selection is not null) + { + properties.Add(new XElement("ReswPlusAppType", selection)); + } + + await AssertSelectionAsync(properties, expected); + } + + [Theory] + [InlineData("", "Auto")] + [InlineData("net10.0-android", "UnoWinUI")] + [InlineData("net10.0-ios", "UnoWinUI")] + [InlineData("net10.0", "UnoWinUI")] + [InlineData("net10.0-windows10.0.19041.0", "Auto")] + public async Task TheDocumentedConditionLeavesWindowsHeadsOnAutomaticDetection(string targetFramework, string expected) + { + var readme = File.ReadAllText(Path.Combine(RepositoryPath(), "README.md")); + var example = Regex.Match( + readme, + @"### Override application model detection.*?```xml\s*(?.*?)```", + RegexOptions.Singleline); + Assert.True(example.Success, "The README must include the per-target-framework override example."); + + var properties = XElement.Parse(example.Groups["xml"].Value); + await AssertSelectionAsync( + new XElement("PropertyGroup", new XElement("TargetFramework", targetFramework)), + expected, + properties); + } + + private static async Task AssertSelectionAsync(XElement properties, string expected, XElement? additionalProperties = null) + { + var root = Path.Combine(Path.GetTempPath(), "ReswPlusUnitTests", $"AppType-{Guid.NewGuid():N}"); + var logDirectory = Path.Combine(AppContext.BaseDirectory, "BuildLogs"); + var binlog = Path.Combine(logDirectory, $"app-type-{Guid.NewGuid():N}.binlog"); + + try + { + Directory.CreateDirectory(root); + Directory.CreateDirectory(logDirectory); + var project = new XElement( + "Project", + properties, + additionalProperties, + new XElement("Import", new XAttribute( + "Project", + Path.Combine(RepositoryPath(), "nuget", "ReswPlus.targets")))); + var projectPath = Path.Combine(root, "PropertyProbe.proj"); + project.Save(projectPath); + + var startInfo = new ProcessStartInfo("dotnet") + { + WorkingDirectory = RepositoryPath(), + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + }; + foreach (var argument in new[] + { + "msbuild", + projectPath, + "-nologo", + "-verbosity:quiet", + "-getProperty:ReswPlusAppType", + "-getItem:CompilerVisibleProperty", + $"-bl:{binlog}", + }) + { + startInfo.ArgumentList.Add(argument); + } + + using var process = Process.Start(startInfo) + ?? throw new InvalidOperationException("Could not start dotnet msbuild."); + var outputTask = process.StandardOutput.ReadToEndAsync(); + var errorTask = process.StandardError.ReadToEndAsync(); + using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + try + { + await process.WaitForExitAsync(timeout.Token); + } + catch (OperationCanceledException) + { + process.Kill(entireProcessTree: true); + await process.WaitForExitAsync(); + throw; + } + var output = await outputTask; + var errors = await errorTask; + Assert.True(process.ExitCode == 0, $"MSBuild property evaluation failed:{Environment.NewLine}{output}{errors}{Environment.NewLine}Binlog: {binlog}"); + + using var result = JsonDocument.Parse(output); + Assert.Equal(expected, result.RootElement.GetProperty("Properties").GetProperty("ReswPlusAppType").GetString()); + var exportedProperties = result.RootElement.GetProperty("Items").GetProperty("CompilerVisibleProperty") + .EnumerateArray() + .Select(item => item.GetProperty("Identity").GetString()); + Assert.Contains("ReswPlusAppType", exportedProperties); + } + finally + { + if (Directory.Exists(root)) + { + Directory.Delete(root, recursive: true); + } + } + } + + private static string RepositoryPath([CallerFilePath] string sourceFile = "") + { + return Path.GetFullPath(Path.Combine(Path.GetDirectoryName(sourceFile)!, "..", "..")); + } +} diff --git a/tests/ReswPlusUnitTests/AppTypeOverride.cs b/tests/ReswPlusUnitTests/AppTypeOverride.cs new file mode 100644 index 0000000..926b534 --- /dev/null +++ b/tests/ReswPlusUnitTests/AppTypeOverride.cs @@ -0,0 +1,428 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using ReswPlus.SourceGenerator; +using ReswPlus.SourceGenerator.Pipeline; +using Xunit; + +namespace ReswPlusUnitTests; + +public class AppTypeOverride +{ + private static string SampleResources => ReswTestHelpers.CreateResw( + ("Welcome", "Welcome!", null), + ("Greeting", "Hello {0}", "#Format[String name]"), + ("Items_One", "{0} item", "#Format[Plural Int count]"), + ("Items_Other", "{0} items", "#Format[Plural Int count]")); + + [Theory] + [InlineData(AppType.WindowsAppSDK, null)] + [InlineData(AppType.WindowsAppSDK, "")] + [InlineData(AppType.WindowsAppSDK, " \t ")] + [InlineData(AppType.WindowsAppSDK, "Auto")] + [InlineData(AppType.WindowsAppSDK, " auto ")] + [InlineData(AppType.UWP, null)] + [InlineData(AppType.UWP, "")] + [InlineData(AppType.UWP, " \t ")] + [InlineData(AppType.UWP, "Auto")] + [InlineData(AppType.UWP, " auto ")] + [InlineData(AppType.UnoWinUI, null)] + [InlineData(AppType.UnoWinUI, "")] + [InlineData(AppType.UnoWinUI, " \t ")] + [InlineData(AppType.UnoWinUI, "Auto")] + [InlineData(AppType.UnoWinUI, " auto ")] + public void AutomaticSelectionPreservesDetection(AppType appType, string? selection) + { + var run = Run(appType, selection); + + Assert.Empty(run.Diagnostics); + AssertAppModel(run, appType); + run.AssertCompiles(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" \t ")] + [InlineData("Auto")] + [InlineData(" auto ")] + public void AutomaticSelectionPreservesTheLegacyUseUwpFallback(string? selection) + { + var run = Run( + AppType.Unknown, + selection, + useUwp: true, + references: PlatformStubs.NativeAotUwpReferences()); + + Assert.Empty(run.Diagnostics); + AssertAppModel(run, AppType.UWP); + run.AssertCompiles(); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("Auto")] + public void AutomaticSelectionDoesNotInventAnAppModel(string? selection) + { + var run = Run(AppType.Unknown, selection); + var diagnostic = Assert.Single(run.Diagnostics); + + Assert.Equal("RESWP0005", diagnostic.Id); + Assert.Contains("ReswPlusAppType", diagnostic.GetMessage()); + Assert.Empty(run.Sources); + } + + [Theory] + [InlineData(AppType.WindowsAppSDK, false)] + [InlineData(AppType.WindowsAppSDK, true)] + [InlineData(AppType.UWP, false)] + [InlineData(AppType.UWP, true)] + [InlineData(AppType.UnoWinUI, false)] + [InlineData(AppType.UnoWinUI, true)] + public void AnExplicitSelectionRecognizesAnOtherwiseUnknownProjection(AppType appType, bool useApplicationLanguages) + { + var references = PlatformStubs.ReferencesWithDetectionHint(appType); + Assert.Equal("RESWP0005", Assert.Single(Run(AppType.Unknown, references: references).Diagnostics).Id); + + var run = Run( + AppType.Unknown, + appType.ToString(), + useApplicationLanguages: useApplicationLanguages, + references: references); + + Assert.Empty(run.Diagnostics); + AssertAppModel(run, appType); + Assert.Contains("public interface IResources", run.Source("Resources.resw")); + Assert.Contains("public sealed class ResourcesProvider : IResources", run.Source("Resources.resw")); + if (useApplicationLanguages) + { + var pluralSupport = run.Source("ResourceLoaderExtension"); + Assert.Contains("global::Windows.Globalization.ApplicationLanguages.Languages", pluralSupport); + Assert.Equal( + appType == AppType.WindowsAppSDK, + pluralSupport.Contains("global::Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride")); + } + run.AssertCompiles(); + } + + [Theory] + [InlineData(AppType.WindowsAppSDK, AppType.UWP)] + [InlineData(AppType.UWP, AppType.WindowsAppSDK)] + [InlineData(AppType.UnoWinUI, AppType.WindowsAppSDK)] + [InlineData(AppType.UnoWinUI, AppType.UWP)] + public void AnExplicitSelectionOverridesMisleadingReferences(AppType actualAppType, AppType hintedAppType) + { + var references = PlatformStubs.ReferencesWithDetectionHint(actualAppType, hintedAppType); + var automatic = Run(AppType.Unknown, references: references); + Assert.Empty(automatic.Diagnostics); + AssertAppModel(automatic, hintedAppType); + + var explicitSelection = Run( + AppType.Unknown, + actualAppType.ToString(), + useApplicationLanguages: true, + references: references); + + Assert.Empty(explicitSelection.Diagnostics); + AssertAppModel(explicitSelection, actualAppType); + explicitSelection.AssertCompiles(); + } + + [Theory] + [InlineData(AppType.WindowsAppSDK)] + [InlineData(AppType.UWP)] + [InlineData(AppType.UnoWinUI)] + public void AnExplicitSelectionTakesPrecedenceOverUseUwp(AppType appType) + { + var run = Run( + AppType.Unknown, + appType.ToString(), + useUwp: true, + references: PlatformStubs.ReferencesWithDetectionHint(appType)); + + Assert.Empty(run.Diagnostics); + AssertAppModel(run, appType); + run.AssertCompiles(); + } + + [Theory] + [InlineData(AppType.WindowsAppSDK, AppType.UWP)] + [InlineData(AppType.WindowsAppSDK, AppType.UnoWinUI)] + [InlineData(AppType.UWP, AppType.WindowsAppSDK)] + [InlineData(AppType.UWP, AppType.UnoWinUI)] + [InlineData(AppType.UnoWinUI, AppType.WindowsAppSDK)] + [InlineData(AppType.UnoWinUI, AppType.UWP)] + public void AnExplicitSelectionTakesPrecedenceOverEveryDetectedModel(AppType detectedAppType, AppType selectedAppType) + { + var run = Run(detectedAppType, selectedAppType.ToString(), useUwp: true); + + Assert.Empty(run.Diagnostics); + AssertAppModel(run, selectedAppType); + } + + [Theory] + [InlineData(AppType.WindowsAppSDK)] + [InlineData(AppType.UWP)] + [InlineData(AppType.UnoWinUI)] + public void OverridesPreserveTheResourceInterfaceGenerationSetting(AppType appType) + { + var run = ReswGeneratorHarness.Run( + [ReswGeneratorHarness.File("en-US", SampleResources)], + references: PlatformStubs.ReferencesWithDetectionHint(appType), + appTypeOverride: appType.ToString(), + generateResourceInterfaces: false); + + Assert.Empty(run.Diagnostics); + Assert.DoesNotContain("public interface IResources", run.Source("Resources.resw")); + Assert.DoesNotContain("public sealed class ResourcesProvider", run.Source("Resources.resw")); + run.AssertCompiles(); + } + + [Theory] + [InlineData(" windowsappsdk ", AppType.WindowsAppSDK)] + [InlineData(" uwp ", AppType.UWP)] + [InlineData(" unowinui ", AppType.UnoWinUI)] + public void NamedSelectionsIgnoreCaseAndSurroundingWhitespace(string selection, AppType appType) + { + var run = Run(AppType.Unknown, selection, references: PlatformStubs.ReferencesWithDetectionHint(appType)); + + Assert.Empty(run.Diagnostics); + AssertAppModel(run, appType); + run.AssertCompiles(); + } + + [Theory] + [InlineData("Unknown")] + [InlineData("Uno.WinUI")] + [InlineData("WinAppSDK")] + [InlineData("3")] + [InlineData(" 1 ")] + [InlineData("-1")] + [InlineData("WindowsAppSDK, UWP")] + [InlineData("Auto;UnoWinUI")] + [InlineData("UnowinU\u0131")] + public void InvalidSelectionsReportAnErrorInsteadOfFallingBack(string selection) + { + foreach (var detectedAppType in new[] { AppType.Unknown, AppType.WindowsAppSDK, AppType.UWP, AppType.UnoWinUI }) + { + var run = Run(detectedAppType, selection, useUwp: true); + var diagnostic = Assert.Single(run.Diagnostics); + + Assert.Equal("RESWP0021", diagnostic.Id); + Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity); + Assert.Contains("ReswPlusAppType", diagnostic.GetMessage()); + Assert.Contains("Auto, WindowsAppSDK, UWP, or UnoWinUI", diagnostic.GetMessage()); + Assert.Empty(run.Sources); + } + } + + [Fact] + public void AnInvalidSelectionPreservesOtherSetupProblemsWithoutReportingUnrecognizedAppType() + { + var run = ReswGeneratorHarness.Run( + [ReswGeneratorHarness.File("en-US", SampleResources)], + outputType: null, + appTypeOverride: "Unknown"); + + Assert.Equal(new[] { "RESWP0004", "RESWP0021" }, run.DiagnosticIds); + Assert.Empty(run.Sources); + } + + [Fact] + public void AnOverrideDoesNotSupplyMissingPlatformReferences() + { + var run = Run(AppType.Unknown, "UnoWinUI"); + + Assert.Empty(run.Diagnostics); + AssertAppModel(run, AppType.UnoWinUI); + Assert.Contains(run.OutputCompilation.GetDiagnostics(), diagnostic => diagnostic.Severity == DiagnosticSeverity.Error); + } + + [Theory] + [InlineData(null, "Auto")] + [InlineData("", " auto ")] + [InlineData(" \t ", "Auto")] + [InlineData("WindowsAppSDK", " windowsappsdk ")] + [InlineData("UWP", " uwp ")] + [InlineData("UnoWinUI", " unowinui ")] + [InlineData("Unknown", "AnotherInvalidValue")] + public void EquivalentSelectionsCompareAndHashAlike(string? first, string? second) + { + var firstOptions = ReadOptions(first); + var secondOptions = ReadOptions(second); + + Assert.Equal(firstOptions, secondOptions); + Assert.Equal(firstOptions.GetHashCode(), secondOptions.GetHashCode()); + } + + [Theory] + [InlineData("auto", "Auto")] + [InlineData("windowsappsdk", "WindowsAppSDK")] + [InlineData("uwp", "UWP")] + [InlineData("unowinui", "UnoWinUI")] + [InlineData("Unknown", "Invalid")] + public void OptionsCaptureTheParsedSelection(string selection, string expected) + { + Assert.Equal(expected, ReadOptions(selection).AppTypeOverride.ToString()); + } + + [Fact] + public void AChangedSelectionInvalidatesTheOptionsModel() + { + Assert.NotEqual(ReadOptions(null), ReadOptions("UnoWinUI")); + Assert.NotEqual(ReadOptions("UnoWinUI"), ReadOptions("Unknown")); + } + + [Fact] + public void SelectionParsingIsIndependentOfTheCurrentCulture() + { + var previousCulture = CultureInfo.CurrentCulture; + + try + { + CultureInfo.CurrentCulture = new CultureInfo("tr-TR"); + Assert.Equal(AppTypeSelection.UnoWinUI, ReadOptions("unowinui").AppTypeOverride); + Assert.Equal(AppTypeSelection.WindowsAppSDK, ReadOptions("windowsappsdk").AppTypeOverride); + Assert.Equal(AppTypeSelection.Auto, ReadOptions("auto").AppTypeOverride); + } + finally + { + CultureInfo.CurrentCulture = previousCulture; + } + } + + [Fact] + public void SelectingTheDetectedModelDoesNotRegenerateSources() + { + var automatic = Run(AppType.WindowsAppSDK); + var explicitSelection = automatic.RunAgainWithOptions(Options("WindowsAppSDK")); + + Assert.Empty(explicitSelection.Diagnostics); + Assert.Equal(1, explicitSelection.RecomputedCount(TrackingNames.Options)); + explicitSelection.AssertReused(TrackingNames.CompilationInfo); + explicitSelection.AssertReused(TrackingNames.Project); + explicitSelection.AssertReused(TrackingNames.Generation); + explicitSelection.AssertReused(TrackingNames.Support); + AssertSameSources(automatic, explicitSelection); + } + + [Fact] + public void EquivalentSelectionSpellingsDoNotInvalidateThePipeline() + { + var before = Run(AppType.UnoWinUI, "UnoWinUI"); + var after = before.RunAgainWithOptions(Options(" unowinui ")); + + after.AssertReused(TrackingNames.Options); + after.AssertReused(TrackingNames.Project); + after.AssertReused(TrackingNames.Generation); + after.AssertReused(TrackingNames.Support); + AssertSameSources(before, after); + } + + [Fact] + public void ChangingOnlyTheSelectionUpdatesEveryAppModelDependentOutput() + { + var before = Run(AppType.WindowsAppSDK); + var after = before.RunAgainWithOptions(Options("UWP")); + + Assert.Empty(after.Diagnostics); + Assert.Equal(1, after.RecomputedCount(TrackingNames.Options)); + after.AssertReused(TrackingNames.CompilationInfo); + Assert.Equal(1, after.RecomputedCount(TrackingNames.Project)); + Assert.Equal(1, after.RecomputedCount(TrackingNames.Generation)); + Assert.Equal(1, after.RecomputedCount(TrackingNames.Support)); + AssertAppModel(after, AppType.UWP); + Assert.NotEqual(before.Source("Resources.resw"), after.Source("Resources.resw")); + Assert.NotEqual(before.Source("ResourceStringProvider"), after.Source("ResourceStringProvider")); + } + + [Fact] + public void InvalidatingAndRestoringTheSelectionRemovesAndRestoresAllSources() + { + var before = Run(AppType.UnoWinUI, "UnoWinUI"); + var invalid = before.RunAgainWithOptions(Options("Unknown")); + Assert.Equal("RESWP0021", Assert.Single(invalid.Diagnostics).Id); + Assert.Empty(invalid.Sources); + + var restored = invalid.RunAgainWithOptions(Options("UnoWinUI")); + Assert.Empty(restored.Diagnostics); + AssertSameSources(before, restored); + restored.AssertCompiles(); + + var automatic = restored.RunAgainWithOptions(Options(null)); + Assert.Empty(automatic.Diagnostics); + AssertSameSources(before, automatic); + automatic.AssertReused(TrackingNames.Generation); + } + + private static ReswGeneratorRun Run( + AppType appType, + string? selection = null, + bool useUwp = false, + bool useApplicationLanguages = false, + IEnumerable? references = null) + { + return ReswGeneratorHarness.Run( + [ReswGeneratorHarness.File("en-US", SampleResources)], + appType, + defaultLanguage: "en-US", + useUwp: useUwp, + useApplicationLanguages: useApplicationLanguages, + references: references, + appTypeOverride: selection); + } + + private static Dictionary Options(string? selection) + { + var options = new Dictionary(AnalyzerConfigOptions.KeyComparer) + { + ["build_property.ProjectDir"] = ReswGeneratorHarness.ProjectDir, + ["build_property.OutputType"] = "Library", + ["build_property.RootNamespace"] = "TestProject", + ["build_property.DefaultLanguage"] = "en-US", + }; + if (selection is not null) + { + options["build_property.ReswPlusAppType"] = selection; + } + return options; + } + + private static ReswBuildOptions ReadOptions(string? selection) + { + return ReswBuildOptions.Read(new TestAnalyzerConfigOptionsProvider(Options(selection)).GlobalOptions); + } + + private static void AssertSameSources(ReswGeneratorRun first, ReswGeneratorRun second) + { + Assert.Equal( + first.Sources.OrderBy(source => source.Key, StringComparer.Ordinal), + second.Sources.OrderBy(source => source.Key, StringComparer.Ordinal)); + } + + private static void AssertAppModel(ReswGeneratorRun run, AppType appType) + { + var xamlNamespace = appType == AppType.UWP ? "Windows.UI.Xaml" : "Microsoft.UI.Xaml"; + var generated = run.Source("Resources.resw"); + Assert.Contains($"using {xamlNamespace}.Markup;", generated); + Assert.Contains($"global::{xamlNamespace}.Markup.MarkupExtensionReturnType", generated); + Assert.Contains($"global::{xamlNamespace}.Markup.MarkupExtension", generated); + Assert.Contains($"global::{xamlNamespace}.Data.IValueConverter", generated); + + var provider = run.Source("ResourceStringProvider"); + Assert.Contains( + appType == AppType.WindowsAppSDK + ? "using Microsoft.Windows.ApplicationModel.Resources;" + : "using Windows.ApplicationModel.Resources;", + provider); + Assert.Contains( + appType == AppType.WindowsAppSDK + ? "new ResourceLoader(ResourceLoader.GetDefaultResourceFilePath(), resourceMapId)" + : "ResourceLoader.GetForViewIndependentUse(resourceMapId)", + provider); + } +} diff --git a/tests/ReswPlusUnitTests/GeneratorHarness.cs b/tests/ReswPlusUnitTests/GeneratorHarness.cs index 8ae3ec2..f561573 100644 --- a/tests/ReswPlusUnitTests/GeneratorHarness.cs +++ b/tests/ReswPlusUnitTests/GeneratorHarness.cs @@ -69,6 +69,7 @@ public static ReswFile File(string language, string content, string baseName = " /// 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. /// Custom compilation references, or to use the platform stubs. + /// The opt-in ReswPlusAppType selection, or to leave it undeclared. /// The result of the run. public static ReswGeneratorRun Run( IEnumerable files, @@ -85,7 +86,8 @@ public static ReswGeneratorRun Run( string assemblyName = "TestProject", bool nativeAotUwp = false, IEnumerable? additionalFiles = null, - IEnumerable? references = null) + IEnumerable? references = null, + string? appTypeOverride = null) { var texts = files .Concat(additionalFiles ?? []) @@ -104,6 +106,7 @@ public static ReswGeneratorRun Run( Declare("build_property.RootNamespace", rootNamespace); Declare("build_property.ReswPlusUseApplicationLanguages", useApplicationLanguages?.ToString().ToLowerInvariant()); Declare("build_property.UseUwp", useUwp?.ToString().ToLowerInvariant()); + Declare("build_property.ReswPlusAppType", appTypeOverride); Declare("build_property.ReswPlusGenerateResourceInterfaces", generateResourceInterfaces?.ToString().ToLowerInvariant()); var compilation = CSharpCompilation.Create( @@ -255,6 +258,19 @@ public ReswGeneratorRun RunAgainWithReferences(IEnumerable re return From(_driver, _inputCompilation.RemoveAllReferences().AddReferences(references), _texts); } + /// + /// Runs the generator again with new project options, reusing its compilation, files, and cached state. + /// + /// The complete set of global project options for the new run. + /// The result of the run with the new options. + public ReswGeneratorRun RunAgainWithOptions(IReadOnlyDictionary globalOptions) + { + return From( + _driver.WithUpdatedAnalyzerConfigOptions(new TestAnalyzerConfigOptionsProvider(globalOptions)), + _inputCompilation, + _texts); + } + /// /// Returns the source emitted under the hint name holding the given text. /// @@ -562,10 +578,19 @@ public static class ApplicationLanguages .Select(path => (MetadataReference)MetadataReference.CreateFromFile(path))]); private static readonly Lazy WindowsAppSdk = new(() => - Compile("ReswPlusTests.WindowsAppSdkStub", WindowsAppSdkStub, @"C:\Packages\Microsoft.WindowsAppSdk\Microsoft.WindowsAppSdk.dll")); + Compile("ReswPlusTests.WindowsAppSdkStub", WindowsAppSdkStub, @"C:\Packages\Microsoft.WindowsAppSdk\Microsoft.WindowsAppSdk.dll", registerForRuntime: true)); private static readonly Lazy Uwp = new(() => - Compile("ReswPlusTests.UwpStub", UwpStub, @"C:\Packages\UAP\Windows.Foundation.UniversalApiContract.winmd")); + Compile("ReswPlusTests.UwpStub", UwpStub, @"C:\Packages\UAP\Windows.Foundation.UniversalApiContract.winmd", registerForRuntime: true)); + + private static readonly string UnoWinUIStub = UwpStub.Replace("Windows.UI.Xaml", "Microsoft.UI.Xaml"); + + private static readonly Lazy UnoWinUI = new(() => + Compile( + "Uno.WinUI", + UnoWinUIStub + "\nnamespace Microsoft.UI.Xaml { public interface DependencyObject { } }", + @"C:\References\Platform.dll", + registerForRuntime: true)); /// /// The same UWP types, under a name the app type detection cannot recognize. @@ -576,7 +601,7 @@ public static class ApplicationLanguages /// such a project look like no supported app type at all unless it declares UseUwp. /// private static readonly Lazy NativeAotUwp = new(() => - Compile("ReswPlusTests.NativeAotUwpStub", UwpStub, @"C:\Packages\Microsoft.Windows.SDK.NET\Microsoft.Windows.SDK.NET.dll")); + Compile("ReswPlusTests.NativeAotUwpStub", UwpStub, @"C:\Packages\Microsoft.Windows.SDK.NET\Microsoft.Windows.SDK.NET.dll", registerForRuntime: true)); /// /// Returns the references of a UWP project built for Native AOT: the UWP types are there, but nothing names @@ -598,10 +623,41 @@ public static ImmutableArray ReferencesFor(AppType appType) { AppType.WindowsAppSDK => RuntimeReferences.Value.Add(WindowsAppSdk.Value), AppType.UWP => RuntimeReferences.Value.Add(Uwp.Value), + AppType.UnoWinUI => RuntimeReferences.Value.Add(UnoWinUI.Value), _ => RuntimeReferences.Value, }; } + /// + /// Returns the APIs of an app model under an unrecognized or misleading reference path. + /// + /// The app model whose APIs are available. + /// The app model suggested by the path, or for no hint. + /// The references with no conclusive Uno type identity. + public static ImmutableArray ReferencesWithDetectionHint(AppType appType, AppType? hintedAppType = null) + { + var source = appType switch + { + AppType.WindowsAppSDK => WindowsAppSdkStub, + AppType.UWP => UwpStub, + AppType.UnoWinUI => UnoWinUIStub, + _ => throw new ArgumentOutOfRangeException(nameof(appType)), + }; + var path = hintedAppType switch + { + AppType.WindowsAppSDK => @"C:\Packages\Microsoft.WindowsAppSdk\Platform.dll", + AppType.UWP => @"C:\Packages\UAP\Windows.Foundation.UniversalApiContract.winmd", + null => @"C:\References\Platform.dll", + _ => throw new ArgumentOutOfRangeException(nameof(hintedAppType)), + }; + + return RuntimeReferences.Value.Add(Compile( + $"ReswPlusTests.Custom{appType}", + source, + path, + registerForRuntime: true)); + } + /// /// The images of the stubs compiled so far, so that generated code referencing one can be run and not only /// compiled. @@ -645,8 +701,9 @@ private static bool IsWindowsProjection(string path) /// The defining assembly identity. /// The source of the platform types. /// The reference path the generator sees. + /// Whether the reference is a shared platform stub that can be loaded at runtime. /// The compiled reference. - public static MetadataReference Compile(string assemblyName, string source, string filePath) + public static MetadataReference Compile(string assemblyName, string source, string filePath, bool registerForRuntime = false) { var compilation = CSharpCompilation.Create( assemblyName, @@ -669,7 +726,10 @@ public static MetadataReference Compile(string assemblyName, string source, stri // path the real framework would have. var image = peStream.ToArray(); - StubImages[assemblyName] = image; + if (registerForRuntime) + { + StubImages[assemblyName] = image; + } return AssemblyMetadata.CreateFromImage(image) .GetReference(filePath: filePath, display: filePath);