// Copyright (c) 2026 Siegfried Pammer
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using ICSharpCode.Decompiler.CSharp.ProjectDecompiler;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using NUnit.Framework;
namespace ICSharpCode.Decompiler.Tests.ProjectDecompiler;
///
/// Exporting a WPF assembly has to produce a project the .NET SDK actually builds. The inputs are
/// compiled on the fly: what the export writes depends on the assembly references and the
/// versioning attributes of the module, so the tests need real modules carrying them, not stubs.
///
[TestFixture]
public sealed class WpfProjectExportTests
{
///
/// The nine assemblies the .NET SDK references by itself for a project that sets UseWPF
/// (item _WpfCommonNetFxReference in Microsoft.NET.Sdk.WindowsDesktop.props).
///
static readonly string[] UseWpfReferences = {
"PresentationCore",
"PresentationFramework",
"System.Windows.Controls.Ribbon",
"System.Xaml",
"UIAutomationClient",
"UIAutomationClientSideProviders",
"UIAutomationProvider",
"UIAutomationTypes",
"WindowsBase",
};
readonly List openedModules = new();
readonly Dictionary stubAssemblies = new();
string tempDirectory;
string presentationFramework;
string presentationCore;
int assemblyCounter;
[OneTimeSetUp]
public void SetUp()
{
tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
Directory.CreateDirectory(tempDirectory);
// Stand-ins for the two WPF assemblies the exporter recognizes by name. They only have to
// carry the right assembly name and the types the fixture assemblies derive from; the real
// ones are not available on non-Windows hosts, which is exactly the situation that made the
// export depend on the machine it ran on.
presentationFramework = CompileTo("PresentationFramework.dll", "PresentationFramework", """
namespace System.Windows
{
public class Application { }
public class Window { }
}
namespace Stubs
{
public class PresentationFramework { }
}
""");
presentationCore = CompileTo("PresentationCore.dll", "PresentationCore", """
namespace System.Windows.Media
{
public class Brush { }
}
namespace Stubs
{
public class PresentationCore { }
}
""");
stubAssemblies.Add("PresentationFramework", presentationFramework);
stubAssemblies.Add("PresentationCore", presentationCore);
}
[OneTimeTearDown]
public void TearDown()
{
foreach (var module in openedModules)
module.Dispose();
if (Directory.Exists(tempDirectory))
Directory.Delete(tempDirectory, recursive: true);
}
///
/// NETSDK1136: a .NET 5+ project that sets UseWPF or UseWindowsForms is rejected outright
/// unless its target framework names the Windows platform.
///
[Test]
public void WpfTargetFrameworkKeepsThePlatformSuffix()
{
string project = WriteProjectFile(WpfApplication(targetFramework: ".NETCoreApp,Version=v10.0", targetPlatform: "Windows7.0"));
using (Assert.EnterMultipleScope())
{
Assert.That(project, Does.Contain("True"));
Assert.That(project, Does.Contain("net10.0-windows7.0"));
}
}
///
/// The platform belongs to the assembly, not to WPF: a plain library built for Windows keeps
/// its suffix as well, and one built for no particular platform does not grow one.
///
[TestCase("Windows7.0", "net10.0-windows7.0")]
[TestCase(null, "net10.0")]
public void PlainLibraryFollowsItsTargetPlatformAttribute(string targetPlatform, string expectedMoniker)
{
string project = WriteProjectFile(Compile("PlainLibrary",
AssemblyAttributes(".NETCoreApp,Version=v10.0", targetPlatform) + "public class C { }"));
Assert.That(project, Does.Contain($"{expectedMoniker}"));
}
///
/// Assemblies built before the platform-suffixed monikers existed carry no TargetPlatform
/// attribute; a WPF project still has to name Windows to build at all.
///
[Test]
public void WpfWithoutTargetPlatformAttributeStillNamesWindows()
{
string project = WriteProjectFile(WpfApplication(targetFramework: ".NETCoreApp,Version=v10.0", targetPlatform: null));
Assert.That(project, Does.Contain("net10.0-windows"));
}
///
/// Platform suffixes only exist for .NET 5 and later. A .NET Framework moniker that grew one
/// would no longer resolve to any target pack.
///
[Test]
public void NetFrameworkMonikerTakesNoPlatformSuffix()
{
string project = WriteProjectFile(WpfApplication(targetFramework: ".NETFramework,Version=v4.7.2", targetPlatform: "Windows7.0"));
Assert.That(project, Does.Contain("net472"));
}
///
/// An assembly whose minimum supported OS version is lower than the version it targets says so
/// through SupportedOSPlatform; without TargetPlatformMinVersion the exported project would
/// silently raise its own floor to the target version.
///
[Test]
public void LowerSupportedOSPlatformBecomesTargetPlatformMinVersion()
{
string project = WriteProjectFile(Compile("VersionedLibrary",
AssemblyAttributes(".NETCoreApp,Version=v10.0", "Windows10.0.19041.0", supportedOSPlatform: "Windows10.0.17763.0")
+ "public class C { }"));
using (Assert.EnterMultipleScope())
{
Assert.That(project, Does.Contain("net10.0-windows10.0.19041.0"));
Assert.That(project, Does.Contain("10.0.17763.0"));
}
}
///
/// NETSDK1137: Microsoft.NET.Sdk carries the Windows Desktop targets itself since .NET 5, and
/// says so whenever a project still asks for the separate SDK.
///
[TestCase(".NETCoreApp,Version=v10.0", "Microsoft.NET.Sdk")]
[TestCase(".NETFramework,Version=v4.7.2", "Microsoft.NET.Sdk")]
[TestCase(".NETCoreApp,Version=v3.1", "Microsoft.NET.Sdk.WindowsDesktop")]
public void WindowsDesktopSdkOnlyForNetCore3(string targetFramework, string expectedSdk)
{
string project = WriteProjectFile(WpfApplication(targetFramework, targetPlatform: null));
Assert.That(project, Does.Contain($@""));
}
///
/// UseWPF brings in the whole Windows Desktop framework reference. Listing one of its
/// assemblies again is a duplicate reference (MSB3243), or a missing one (MSB3245) when the
/// hint path does not survive the move to the machine that rebuilds the project.
///
[Test]
public void ReferencesSuppliedByUseWpfAreNotListed()
{
string project = WriteProjectFile(WpfApplication(targetFramework: ".NETCoreApp,Version=v10.0", targetPlatform: "Windows7.0"));
using (Assert.EnterMultipleScope())
{
Assert.That(project, Does.Not.Contain("PresentationCore"));
Assert.That(project, Does.Not.Contain("PresentationFramework"));
}
}
///
/// The markup compiler generates the program entry point into the application definition, so
/// the XAML file of the Application subclass is the one item that must not be a Page (#2253).
///
[Test]
public void ApplicationSubclassOfAnExecutableIsTheApplicationDefinition()
{
var (module, typeSystem) = Load(WpfApplication(targetFramework: ".NETCoreApp,Version=v10.0", targetPlatform: "Windows7.0"));
using (Assert.EnterMultipleScope())
{
Assert.That(WholeProjectDecompiler.IsApplicationDefinition(FindType(typeSystem, "Fixture.App"), module), Is.True);
Assert.That(WholeProjectDecompiler.IsApplicationDefinition(FindType(typeSystem, "Fixture.MainWindow"), module), Is.False,
"a Window is a Page like any other");
}
}
///
/// A library has no entry point for the markup compiler to generate a Main into; an Application
/// subclass shipped inside one stays a Page.
///
[Test]
public void ApplicationSubclassOfALibraryIsNotTheApplicationDefinition()
{
var (module, typeSystem) = Load(Compile("WpfLibrary", """
namespace Fixture
{
public class App : System.Windows.Application { }
}
""", OutputKind.DynamicallyLinkedLibrary, presentationFramework));
Assert.That(WholeProjectDecompiler.IsApplicationDefinition(FindType(typeSystem, "Fixture.App"), module), Is.False);
}
///
/// UseWPF is what makes System.Xaml implicit; an assembly that uses the XAML services without
/// WPF gets no UseWPF, so nothing supplies the reference and the export has to keep it.
///
[Test]
public void SystemXamlIsKeptWhenTheAssemblyIsNotWpf()
{
string project = WriteProjectFile(LibraryReferencing("System.Xaml"));
using (Assert.EnterMultipleScope())
{
Assert.That(project, Does.Contain(@"Include=""System.Xaml"""));
Assert.That(project, Does.Not.Contain("UseWPF"));
}
}
///
/// Every assembly of the UseWPF set is a duplicate reference (MSB3243) once the project sets
/// the property.
///
[Test]
public void WpfProjectDropsEveryReferenceUseWpfSupplies()
{
string project = WriteProjectFile(LibraryReferencing(UseWpfReferences));
using (Assert.EnterMultipleScope())
{
Assert.That(project, Does.Contain("True"));
foreach (string name in UseWpfReferences)
{
Assert.That(project, Does.Not.Contain(name), $"{name} is supplied by UseWPF");
}
}
}
///
/// WPF and Windows Forms are not alternatives: an assembly can use both, and then both sets of
/// implicit references are supplied only if both properties are written.
///
[Test]
public void AssemblyUsingWpfAndWindowsFormsSetsBothProperties()
{
string project = WriteProjectFile(LibraryReferencing("PresentationFramework", "System.Windows.Forms", "System.Drawing"));
using (Assert.EnterMultipleScope())
{
Assert.That(project, Does.Contain("True"));
Assert.That(project, Does.Contain("True"));
Assert.That(project, Does.Not.Contain("PresentationFramework"));
Assert.That(project, Does.Not.Contain("System.Windows.Forms"));
Assert.That(project, Does.Not.Contain("System.Drawing"));
}
}
///
/// The interop assembly is implicit only where both worlds meet; a project that sets just one
/// of the two properties has to reference it itself.
///
[Test]
public void WindowsFormsIntegrationIsImplicitOnlyWhenBothAreInPlay()
{
string wpfOnly = WriteProjectFile(LibraryReferencing("PresentationFramework", "WindowsFormsIntegration"));
string windowsFormsOnly = WriteProjectFile(LibraryReferencing("System.Windows.Forms", "WindowsFormsIntegration"));
string both = WriteProjectFile(LibraryReferencing("PresentationFramework", "System.Windows.Forms", "WindowsFormsIntegration"));
using (Assert.EnterMultipleScope())
{
Assert.That(wpfOnly, Does.Contain(@"Include=""WindowsFormsIntegration"""));
Assert.That(windowsFormsOnly, Does.Contain(@"Include=""WindowsFormsIntegration"""));
Assert.That(both, Does.Not.Contain("WindowsFormsIntegration"));
}
}
static ITypeDefinition FindType(IDecompilerTypeSystem typeSystem, string fullTypeName)
{
var type = typeSystem.FindType(new FullTypeName(fullTypeName)).GetDefinition();
Assert.That(type, Is.Not.Null, $"the fixture assembly is expected to contain {fullTypeName}");
return type;
}
string WpfApplication(string targetFramework, string targetPlatform)
{
return Compile("WpfFixture", AssemblyAttributes(targetFramework, targetPlatform) + """
namespace Fixture
{
public class App : System.Windows.Application { }
public class MainWindow : System.Windows.Window
{
public System.Windows.Media.Brush Brush;
}
public static class Program
{
public static void Main() { }
}
}
""", OutputKind.WindowsApplication, presentationFramework, presentationCore);
}
///
/// Compiles a .NET Framework library referencing exactly the named assemblies. Which references
/// the export keeps is decided by assembly name, so the referenced assemblies only need the
/// right name and one public type each to survive as a reference in the compiled metadata.
///
string LibraryReferencing(params string[] assemblyNames)
{
string fields = string.Join(Environment.NewLine,
assemblyNames.Select((name, index) => $"public Stubs.{MarkerTypeName(name)} field{index};"));
return Compile("ReferenceFixture",
AssemblyAttributes(".NETFramework,Version=v4.7.2", targetPlatform: null)
+ "public class UsesEverything { " + fields + " }",
OutputKind.DynamicallyLinkedLibrary,
assemblyNames.Select(Stub).ToArray());
}
string Stub(string assemblyName)
{
if (!stubAssemblies.TryGetValue(assemblyName, out string path))
{
path = CompileTo(assemblyName + ".dll", assemblyName,
$"namespace Stubs {{ public class {MarkerTypeName(assemblyName)} {{ }} }}");
stubAssemblies.Add(assemblyName, path);
}
return path;
}
static string MarkerTypeName(string assemblyName) => assemblyName.Replace(".", "_");
static string AssemblyAttributes(string targetFramework, string targetPlatform, string supportedOSPlatform = null)
{
var attributes = new StringBuilder();
attributes.AppendLine($"[assembly: System.Runtime.Versioning.TargetFramework(\"{targetFramework}\")]");
if (targetPlatform != null)
attributes.AppendLine($"[assembly: System.Runtime.Versioning.TargetPlatform(\"{targetPlatform}\")]");
if (supportedOSPlatform != null)
attributes.AppendLine($"[assembly: System.Runtime.Versioning.SupportedOSPlatform(\"{supportedOSPlatform}\")]");
return attributes.ToString();
}
///
/// Each compilation lands in a file of its own: the modules stay open until the fixture is torn
/// down, so the same path must not be written twice.
///
string Compile(string assemblyName, string source, OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary, params string[] references)
{
string extension = outputKind == OutputKind.DynamicallyLinkedLibrary ? ".dll" : ".exe";
string fileName = assemblyName + "." + Interlocked.Increment(ref assemblyCounter) + extension;
return CompileTo(fileName, assemblyName, source, outputKind, references);
}
string CompileTo(string fileName, string assemblyName, string source, OutputKind outputKind = OutputKind.DynamicallyLinkedLibrary, params string[] references)
{
var compilation = CSharpCompilation.Create(assemblyName,
new[] { CSharpSyntaxTree.ParseText(SourceText.From(source, Encoding.UTF8)) },
RuntimeReferences.Concat(references.Select(r => MetadataReference.CreateFromFile(r))),
new CSharpCompilationOptions(outputKind, deterministic: true));
string fullPath = Path.Combine(tempDirectory, fileName);
var result = compilation.Emit(fullPath);
Assert.That(result.Success, Is.True, () => string.Join(Environment.NewLine, result.Diagnostics));
return fullPath;
}
static IEnumerable RuntimeReferences
=> ((string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES"))
.Split(Path.PathSeparator)
.Where(path => path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
.Select(path => MetadataReference.CreateFromFile(path));
(MetadataFile Module, IDecompilerTypeSystem TypeSystem) Load(string fileName)
{
var module = OpenModule(fileName);
return (module, new DecompilerTypeSystem(module, new UniversalAssemblyResolver(fileName, throwOnError: false, targetFramework: null)));
}
MetadataFile OpenModule(string fileName)
{
var module = new PEFile(fileName);
openedModules.Add(module);
return module;
}
string WriteProjectFile(string fileName)
{
StringWriter output = new();
ProjectFileWriterSdkStyle.Default.Write(output, new TestProjectInfoProvider(tempDirectory), Array.Empty(), OpenModule(fileName));
return output.ToString();
}
sealed class TestProjectInfoProvider(string targetDirectory) : IProjectInfoProvider
{
public IAssemblyResolver AssemblyResolver { get; } = new UniversalAssemblyResolver(null, false, null);
public IAssemblyReferenceClassifier AssemblyReferenceClassifier { get; } = new AssemblyReferenceClassifier();
public CSharp.LanguageVersion LanguageVersion => CSharp.LanguageVersion.Latest;
public bool CheckForOverflowUnderflow => false;
public Guid ProjectGuid { get; } = Guid.NewGuid();
public string TargetDirectory => targetDirectory;
public string StrongNameKeyFile => null;
}
}