From 1534a051fe83f69bf935b80d094feb5072e3c9fe Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 2 Sep 2026 10:15:11 +0200 Subject: [PATCH 1/5] #2253: Export the WPF application definition as such, not as a Page The WPF markup compiler generates the program entry point from the ApplicationDefinition item, so an exported project that lists App.xaml as a Page has no Main at all and fails to build with CS5001. Both the UI and ilspycmd already resolve the BAML root's partial class, which makes deriving Application from System.Windows.Application the natural signal. The module additionally has to have an entry point of its own: a library that merely contains an Application subclass would otherwise have MSBuild generate a Main into it. Assisted-by: Claude:claude-opus-5[1m]:Claude Code --- .../WpfProjectExportTests.cs | 216 ++++++++++++++++++ .../WholeProjectDecompiler.cs | 21 ++ .../BamlAwareWholeProjectDecompiler.cs | 5 +- ILSpy/Languages/CSharpLanguage.cs | 2 +- ILSpy/Languages/IResourceFileHandler.cs | 7 + ILSpy/TreeNodes/BamlResourceNodeFactory.cs | 2 + 6 files changed, 251 insertions(+), 2 deletions(-) create mode 100644 ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs diff --git a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs new file mode 100644 index 000000000..c47af7f48 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs @@ -0,0 +1,216 @@ +// 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 +{ + readonly List openedModules = 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 { } + } + """); + presentationCore = CompileTo("PresentationCore.dll", "PresentationCore", """ + namespace System.Windows.Media + { + public class Brush { } + } + """); + } + + [OneTimeTearDown] + public void TearDown() + { + foreach (var module in openedModules) + module.Dispose(); + if (Directory.Exists(tempDirectory)) + Directory.Delete(tempDirectory, recursive: true); + } + + /// + /// 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); + } + + 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); + } + + 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; + } +} diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs index 3f5da58e5..5cb703218 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs @@ -1177,6 +1177,27 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler { return TargetServices.DetectTargetFramework(module).Moniker != null; } + + /// + /// Determines whether the XAML file whose root object is belongs + /// into an MSBuild <ApplicationDefinition> item instead of a <Page> item. + /// The WPF markup compiler generates the program entry point from the application definition, + /// so a module that has no entry point of its own - a library that happens to contain an + /// Application subclass - must not get one. + /// + public static bool IsApplicationDefinition(ITypeDefinition? rootType, MetadataFile? module) + { + if (rootType == null) + return false; + if (module is not PEFile { Reader.PEHeaders.CorHeader.EntryPointTokenOrRelativeVirtualAddress: not 0 }) + return false; + foreach (var baseType in rootType.GetNonInterfaceBaseTypes()) + { + if (baseType.FullName == "System.Windows.Application") + return true; + } + return false; + } } public record struct ProjectItemInfo(string ItemType, string FileName) diff --git a/ICSharpCode.ILSpyCmd/BamlAwareWholeProjectDecompiler.cs b/ICSharpCode.ILSpyCmd/BamlAwareWholeProjectDecompiler.cs index c03334e09..4f450a35f 100644 --- a/ICSharpCode.ILSpyCmd/BamlAwareWholeProjectDecompiler.cs +++ b/ICSharpCode.ILSpyCmd/BamlAwareWholeProjectDecompiler.cs @@ -83,7 +83,10 @@ namespace ICSharpCode.ILSpyCmd Directory.CreateDirectory(directory); result.Xaml.Save(fullPath); - var item = new ProjectItemInfo("Page", xamlFileName) + string itemType = IsApplicationDefinition(typeDefinition, bamlTypeSystem.MainModule.MetadataFile) + ? "ApplicationDefinition" + : "Page"; + var item = new ProjectItemInfo(itemType, xamlFileName) .With("Generator", "MSBuild:Compile") .With("SubType", "Designer"); if (partialTypeInfo != null) diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 10a46c120..5b77f9310 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -616,7 +616,7 @@ namespace ICSharpCode.ILSpy.Languages continue; entryStream.Position = 0; fileName = handler.WriteResourceToFile(assembly, fileName, entryStream, context); - var item = new ProjectItemInfo(handler.EntryType, fileName) { PartialTypes = context.PartialTypes }; + var item = new ProjectItemInfo(context.EntryTypeOverride ?? handler.EntryType, fileName) { PartialTypes = context.PartialTypes }; item = item.With(context.AdditionalProperties); return new[] { item }; } diff --git a/ILSpy/Languages/IResourceFileHandler.cs b/ILSpy/Languages/IResourceFileHandler.cs index 98ebddd1b..5937dc206 100644 --- a/ILSpy/Languages/IResourceFileHandler.cs +++ b/ILSpy/Languages/IResourceFileHandler.cs @@ -68,6 +68,13 @@ namespace ICSharpCode.ILSpy.Languages public DecompilationOptions DecompilationOptions { get; } + /// + /// MSBuild item type for the entry currently being written, when it differs from the + /// handler's . XAML recovered from BAML is a + /// "Page", except for the file defining the application, which is an "ApplicationDefinition". + /// + public string? EntryTypeOverride { get; set; } + public ResourceFileHandlerContext(DecompilationOptions options) { this.DecompilationOptions = options; diff --git a/ILSpy/TreeNodes/BamlResourceNodeFactory.cs b/ILSpy/TreeNodes/BamlResourceNodeFactory.cs index 8d38f741d..def7ad3b9 100644 --- a/ILSpy/TreeNodes/BamlResourceNodeFactory.cs +++ b/ILSpy/TreeNodes/BamlResourceNodeFactory.cs @@ -92,6 +92,8 @@ namespace ICSharpCode.ILSpy.Baml foreach (var member in result.GeneratedMembers) partialTypeInfo.AddDeclaredMember(member); context.AddPartialTypeInfo(partialTypeInfo); + if (WholeProjectDecompiler.IsApplicationDefinition(typeDefinition, typeSystem.MainModule.MetadataFile)) + context.EntryTypeOverride = "ApplicationDefinition"; } else { From fe8477e9430f5e28720e423e9701d277e0ee447f Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 2 Sep 2026 10:16:33 +0200 Subject: [PATCH 2/5] #3315: Keep the target platform in exported target framework monikers A .NET 5 or later project that sets UseWPF or UseWindowsForms is rejected outright (NETSDK1136) unless its target framework names the Windows platform, so an exported WPF assembly produced a project that could not build at all. The platform belongs to the assembly rather than to WPF - TargetPlatformAttribute records it, SupportedOSPlatform its minimum version - so the moniker follows the attributes wherever they are present, and falls back to plain "windows" only for a desktop project built before those attributes existed. Monikers older than net5.0 take no platform suffix and must not grow one. Assisted-by: Claude:claude-opus-5[1m]:Claude Code --- .../WpfProjectExportTests.cs | 73 ++++++++++++++++ .../ProjectFileWriterSdkStyle.cs | 83 ++++++++++++++++++- .../ProjectDecompiler/TargetServices.cs | 53 ++++++++++++ 3 files changed, 206 insertions(+), 3 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs index c47af7f48..c89cf128d 100644 --- a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs +++ b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs @@ -83,6 +83,79 @@ public sealed class WpfProjectExportTests 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")); + } + } + /// /// 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). diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs index 5af87d04c..1f8392a23 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs @@ -38,6 +38,8 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler const string AspNetCorePrefix = "Microsoft.AspNetCore"; const string PresentationFrameworkName = "PresentationFramework"; const string WindowsFormsName = "System.Windows.Forms"; + const string NetCoreAppIdentifier = ".NETCoreApp"; + const string WindowsPlatformName = "Windows"; const string TrueString = "True"; const string FalseString = "False"; const string AnyCpuString = "AnyCPU"; @@ -155,7 +157,19 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler WriteDesktopExtensions(xml, projectType); - xml.WriteElementString("TargetFramework", GetTargetFrameworkMoniker(module, project)); + string moniker = GetTargetFrameworkMoniker(module, project); + string? platform = GetTargetPlatform(module, projectType); + if (platform != null) + { + moniker += "-" + platform; + } + xml.WriteElementString("TargetFramework", moniker); + + string? minVersion = platform != null ? GetTargetPlatformMinVersion(module, platform) : null; + if (minVersion != null) + { + xml.WriteElementString("TargetPlatformMinVersion", minVersion); + } // 'AnyCPU' is default, so only need to specify platform if it differs if (platformName != AnyCpuString) @@ -169,6 +183,68 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler } } + /// + /// Gets the target platform part of the target framework moniker, e.g. "windows7.0" in + /// "net10.0-windows7.0". Only .NET 5 and later monikers carry one; "net472" or + /// "netcoreapp3.1" with a platform suffix names no target pack at all. + /// + /// The lower-case platform, or null when the moniker takes none. + static string? GetTargetPlatform(MetadataFile module, ProjectType projectType) + { + var targetFramework = TargetServices.DetectTargetFramework(module); + if (targetFramework.Identifier != NetCoreAppIdentifier || targetFramework.VersionNumber < 500) + { + return null; + } + + string? platform = TargetServices.DetectTargetPlatform(module); + if (platform == null && projectType is ProjectType.Wpf or ProjectType.WinForms) + { + // Assemblies built before platform-suffixed monikers existed carry no + // TargetPlatformAttribute, but WPF and Windows Forms are Windows-only and the SDK + // rejects the project outright (NETSDK1136) unless the moniker says so. + platform = WindowsPlatformName; + } + + return platform?.ToLowerInvariant(); + } + + /// + /// Gets the lowest platform version the assembly supports, when it is lower than the version + /// it targets. Without it the exported project would raise its own floor to the target version. + /// + static string? GetTargetPlatformMinVersion(MetadataFile module, string platform) + { + string? supported = TargetServices.DetectSupportedOSPlatform(module)?.ToLowerInvariant(); + if (supported == null) + { + return null; + } + + var (supportedName, supportedVersion) = SplitPlatform(supported); + var (platformName, platformVersion) = SplitPlatform(platform); + if (supportedName != platformName || supportedVersion.Length == 0 || supportedVersion == platformVersion) + { + return null; + } + + return supportedVersion; + } + + /// + /// Splits a platform such as "windows10.0.17763.0" into its name and its version. + /// + static (string Name, string Version) SplitPlatform(string platform) + { + int versionStart = 0; + while (versionStart < platform.Length && !char.IsDigit(platform[versionStart])) + { + versionStart++; + } + + return (platform.Substring(0, versionStart), platform.Substring(versionStart)); + } + /// /// Gets the target framework moniker for the specified module and project. /// @@ -349,12 +425,13 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler /// An enumerable of assembly references. protected virtual IEnumerable GetReferences(MetadataFile module, IProjectInfoProvider project) { - bool isNetCoreApp = TargetServices.DetectTargetFramework(module).Identifier == ".NETCoreApp"; + bool isNetCoreApp = TargetServices.DetectTargetFramework(module).Identifier == NetCoreAppIdentifier; + var projectType = GetProjectType(module); var targetPacks = new HashSet(); if (isNetCoreApp) { targetPacks.Add("Microsoft.NETCore.App"); - switch (GetProjectType(module)) + switch (projectType) { case ProjectType.WinForms: case ProjectType.Wpf: diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs index dbd9eae1e..dc7060df5 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs @@ -22,6 +22,7 @@ using System.Linq; using System.Reflection.PortableExecutable; using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.TypeSystem; namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler { @@ -91,6 +92,58 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler return new TargetFramework(targetFrameworkIdentifier, versionNumber, targetFrameworkProfile); } + /// + /// Gets the OS platform the specified was built for, as recorded by + /// [assembly: TargetPlatform(...)], e.g. "Windows7.0". Null if the assembly names no platform. + /// + public static string DetectTargetPlatform(MetadataFile module) + { + return GetAssemblyAttributeArgument(module, "System.Runtime.Versioning.TargetPlatformAttribute"); + } + + /// + /// Gets the lowest OS platform version the specified supports, as recorded + /// by [assembly: SupportedOSPlatform(...)], e.g. "Windows10.0.17763.0". Null if the assembly + /// declares no minimum. + /// + public static string DetectSupportedOSPlatform(MetadataFile module) + { + return GetAssemblyAttributeArgument(module, "System.Runtime.Versioning.SupportedOSPlatformAttribute"); + } + + /// + /// Reads the single string argument of an assembly-level attribute of the given type. + /// + static string GetAssemblyAttributeArgument(MetadataFile module, string attributeTypeName) + { + if (module is null) + { + throw new ArgumentNullException(nameof(module)); + } + + var metadata = module.Metadata; + foreach (var handle in metadata.GetCustomAttributes(System.Reflection.Metadata.Handle.AssemblyDefinition)) + { + try + { + var attribute = metadata.GetCustomAttribute(handle); + if (attribute.GetAttributeType(metadata).GetFullTypeName(metadata).ToString() != attributeTypeName) + continue; + var blobReader = metadata.GetBlobReader(attribute.Value); + if (blobReader.ReadUInt16() == 0x0001) + { + return blobReader.ReadSerializedString(); + } + } + catch (BadImageFormatException) + { + // ignore malformed attributes + } + } + + return null; + } + /// /// Gets the string representation (name) of the target platform of the specified . /// From 2900d18bca093f98c4868320a91508c3fd9267ef Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 2 Sep 2026 10:17:28 +0200 Subject: [PATCH 3/5] #3315: Ask for the WindowsDesktop SDK only where it is still needed Microsoft.NET.Sdk imports the Windows Desktop targets itself for .NET Framework and for .NET 5 and later, and warns (NETSDK1137) about every project that still names the separate SDK. Only .NET Core 3.x, where those targets are not imported without a platform-suffixed moniker, genuinely needs Microsoft.NET.Sdk.WindowsDesktop. Assisted-by: Claude:claude-opus-5[1m]:Claude Code --- .../ProjectDecompiler/WpfProjectExportTests.cs | 14 ++++++++++++++ .../ProjectDecompiler/ProjectFileWriterSdkStyle.cs | 13 ++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs index c89cf128d..69d48486b 100644 --- a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs +++ b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs @@ -156,6 +156,20 @@ public sealed class WpfProjectExportTests } } + /// + /// 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($@"")); + } + /// /// 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). diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs index 1f8392a23..58d9f88f8 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs @@ -96,7 +96,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler xml.WriteStartElement("Project"); var projectType = GetProjectType(module); - xml.WriteAttributeString("Sdk", GetSdkString(projectType)); + xml.WriteAttributeString("Sdk", GetSdkString(projectType, TargetServices.DetectTargetFramework(module))); using (new Group(xml, "PropertyGroup")) { @@ -477,13 +477,20 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler xml.WriteEndElement(); } - static string GetSdkString(ProjectType projectType) + static string GetSdkString(ProjectType projectType, TargetFramework targetFramework) { switch (projectType) { case ProjectType.WinForms: case ProjectType.Wpf: - return "Microsoft.NET.Sdk.WindowsDesktop"; + // Microsoft.NET.Sdk carries the Windows Desktop targets itself since .NET 5 and + // warns (NETSDK1137) about projects that still name the separate SDK; only + // .NET Core 3.x, where the desktop targets are not imported for a plain + // framework moniker, still needs it. + return targetFramework.Identifier == NetCoreAppIdentifier + && targetFramework.VersionNumber >= 300 && targetFramework.VersionNumber < 500 + ? "Microsoft.NET.Sdk.WindowsDesktop" + : "Microsoft.NET.Sdk"; case ProjectType.Web: return "Microsoft.NET.Sdk.Web"; default: From d93bbcdb7308dc0fb8c5cc1b2a369280dd9ab44d Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 2 Sep 2026 10:19:19 +0200 Subject: [PATCH 4/5] #3315: Drop references that UseWPF already supplies An exported WPF project listed PresentationCore next to the implicit Windows Desktop framework reference, which is a duplicate reference (MSB3243) or an unresolvable one (MSB3245) once the hint path stops pointing anywhere. The target-pack filter that should have caught it asks the assembly resolver, which answers by probing the shared frameworks installed on the machine running the export - so the same assembly exported from Linux, or from a Windows box without the desktop runtime, produced a different project file. What the SDK adds for UseWPF is a fixed list, so match it by name instead. Assisted-by: Claude:claude-opus-5[1m]:Claude Code --- .../WpfProjectExportTests.cs | 17 ++++++++++++++ .../ProjectFileWriterSdkStyle.cs | 23 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs index 69d48486b..9ec17ce7f 100644 --- a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs +++ b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs @@ -170,6 +170,23 @@ public sealed class WpfProjectExportTests 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). diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs index 58d9f88f8..7721cb05e 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs @@ -44,6 +44,25 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler const string FalseString = "False"; const string AnyCpuString = "AnyCPU"; + /// + /// References the .NET SDK adds by itself when UseWPF is set, both through the implicit + /// Microsoft.WindowsDesktop.App framework reference on .NET Core and through the implicit + /// .NET Framework references. Listing them a second time is a duplicate reference (MSB3243), + /// or an unresolvable one (MSB3245) once the hint path no longer points anywhere. + /// Membership must not be decided by probing the machine that runs the export: the + /// Windows Desktop runtime pack is absent on non-Windows hosts, and the exported project + /// has to come out the same everywhere. + /// + static readonly HashSet WpfImplicitReferences = new HashSet { + "PresentationCore", + "System.Windows.Controls.Ribbon", + "UIAutomationClient", + "UIAutomationClientSideProviders", + "UIAutomationProvider", + "UIAutomationTypes", + "WindowsBase", + }; + static readonly HashSet ImplicitReferences = new HashSet { "mscorlib", "netstandard", @@ -449,6 +468,10 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler { continue; } + if (projectType == ProjectType.Wpf && WpfImplicitReferences.Contains(reference.Name)) + { + continue; + } if (isNetCoreApp && project.AssemblyReferenceClassifier.IsSharedAssembly(reference, out string? runtimePack) && targetPacks.Contains(runtimePack)) { continue; From 0b7fa142d72e495296acb5ff7f1078a021d76869 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 2 Sep 2026 13:09:16 +0200 Subject: [PATCH 5/5] #3315: Gate implicit references the way the SDK gates them The exporter dropped PresentationFramework, System.Xaml, System.Windows.Forms and System.Drawing from every project it wrote, whatever the assembly used, while a second list held the remaining WPF assemblies behind a WPF check. The SDK draws the line elsewhere: Microsoft.NET.Sdk.WindowsDesktop.props promotes the nine _WpfCommonNetFxReference items to _SDKImplicitReference only when UseWPF is set, System.Windows.Forms only when UseWindowsForms is, and WindowsFormsIntegration only when both are. A XAML-only assembly therefore lost a reference that nothing supplied, and a WPF application that also used Windows Forms lost the Windows Forms references while the project only said UseWPF. Following the SDK there makes WPF and Windows Forms independent rather than alternatives, which is what the flags enum is for: an assembly can use both, and then both properties have to be written. Where an assembly looks like more than one kind of project, the web SDK wins the Sdk attribute, because Microsoft.NET.Sdk.Web imports Microsoft.NET.Sdk and so carries the desktop targets, while Microsoft.NET.Sdk.WindowsDesktop carries no web targets. System.Drawing stays unconditional: Microsoft.NET.Sdk.BeforeCommon.targets adds it for every .NETFramework target rather than only for Windows Forms ones, and on .NET Core it ships in the Microsoft.NETCore.App reference pack. Assisted-by: Claude:claude-opus-5[1m]:Claude Code --- .../WpfProjectExportTests.cs | 129 +++++++++++++++++ .../ProjectFileWriterSdkStyle.cs | 131 ++++++++++++------ 2 files changed, 214 insertions(+), 46 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs index 9ec17ce7f..50b30d556 100644 --- a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs +++ b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/WpfProjectExportTests.cs @@ -43,7 +43,24 @@ namespace ICSharpCode.Decompiler.Tests.ProjectDecompiler; [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; @@ -65,13 +82,23 @@ public sealed class WpfProjectExportTests 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] @@ -221,6 +248,79 @@ public sealed class WpfProjectExportTests 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(); @@ -246,6 +346,35 @@ public sealed class WpfProjectExportTests """, 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(); diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs index 7721cb05e..73ac74f5a 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs @@ -38,6 +38,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler const string AspNetCorePrefix = "Microsoft.AspNetCore"; const string PresentationFrameworkName = "PresentationFramework"; const string WindowsFormsName = "System.Windows.Forms"; + const string WindowsFormsIntegrationName = "WindowsFormsIntegration"; const string NetCoreAppIdentifier = ".NETCoreApp"; const string WindowsPlatformName = "Windows"; const string TrueString = "True"; @@ -52,10 +53,15 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler /// Membership must not be decided by probing the machine that runs the export: the /// Windows Desktop runtime pack is absent on non-Windows hosts, and the exported project /// has to come out the same everywhere. + /// The SDK gates the version-specific members of this set on the target framework version, + /// which an assembly satisfies by construction: it can only reference what its own target + /// framework ships. /// static readonly HashSet WpfImplicitReferences = new HashSet { "PresentationCore", + "PresentationFramework", "System.Windows.Controls.Ribbon", + "System.Xaml", "UIAutomationClient", "UIAutomationClientSideProviders", "UIAutomationProvider", @@ -63,18 +69,19 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler "WindowsBase", }; + /// + /// References the SDK adds for every project, whatever it uses: they are either implicit + /// for .NET Framework targets or part of the Microsoft.NETCore.App shared framework. + /// static readonly HashSet ImplicitReferences = new HashSet { "mscorlib", "netstandard", - "PresentationFramework", "System", "System.Diagnostics.Debug", "System.Diagnostics.Tools", "System.Drawing", "System.Runtime", "System.Runtime.Extensions", - "System.Windows.Forms", - "System.Xaml", }; /// @@ -89,7 +96,20 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler "EmbeddedResource", }; - enum ProjectType { Default, WinForms, Wpf, Web } + /// + /// What an assembly uses of the Windows desktop stacks. WPF and Windows Forms are not + /// alternatives: an assembly can use both, and each brings its own set of implicit + /// references and its own SDK property. + /// + [Flags] + enum ProjectType + { + Default = 0, + WinForms = 1, + Wpf = 2, + Web = 4, + Desktop = WinForms | Wpf, + } /// /// Gets the default instance of the class. @@ -217,7 +237,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler } string? platform = TargetServices.DetectTargetPlatform(module); - if (platform == null && projectType is ProjectType.Wpf or ProjectType.WinForms) + if (platform == null && (projectType & ProjectType.Desktop) != 0) { // Assemblies built before platform-suffixed monikers existed carry no // TargetPlatformAttribute, but WPF and Windows Forms are Windows-only and the SDK @@ -302,7 +322,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler else { // 'Library' is default, so only need to specify output type for executables (excludes ProjectType.Web) - if (projectType == ProjectType.Web) + if (projectType.HasFlag(ProjectType.Web)) { xml.WriteElementString("OutputType", "Library"); } @@ -311,11 +331,11 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler static void WriteDesktopExtensions(XmlTextWriter xml, ProjectType projectType) { - if (projectType == ProjectType.Wpf) + if (projectType.HasFlag(ProjectType.Wpf)) { xml.WriteElementString("UseWPF", TrueString); } - else if (projectType == ProjectType.WinForms) + if (projectType.HasFlag(ProjectType.WinForms)) { xml.WriteElementString("UseWindowsForms", TrueString); } @@ -450,25 +470,19 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler if (isNetCoreApp) { targetPacks.Add("Microsoft.NETCore.App"); - switch (projectType) + if ((projectType & ProjectType.Desktop) != 0) { - case ProjectType.WinForms: - case ProjectType.Wpf: - targetPacks.Add("Microsoft.WindowsDesktop.App"); - break; - case ProjectType.Web: - targetPacks.Add("Microsoft.AspNetCore.App"); - targetPacks.Add("Microsoft.AspNetCore.All"); - break; + targetPacks.Add("Microsoft.WindowsDesktop.App"); + } + if (projectType.HasFlag(ProjectType.Web)) + { + targetPacks.Add("Microsoft.AspNetCore.App"); + targetPacks.Add("Microsoft.AspNetCore.All"); } } foreach (var reference in module.AssemblyReferences) { - if (ImplicitReferences.Contains(reference.Name)) - { - continue; - } - if (projectType == ProjectType.Wpf && WpfImplicitReferences.Contains(reference.Name)) + if (IsSuppliedBySdk(reference.Name, projectType)) { continue; } @@ -480,6 +494,28 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler } } + /// + /// Determines whether the SDK adds the named reference by itself, given what the project uses. + /// Each set matches an _SDKImplicitReference group of the SDK and carries the same condition. + /// + static bool IsSuppliedBySdk(string referenceName, ProjectType projectType) + { + if (ImplicitReferences.Contains(referenceName)) + { + return true; + } + if (projectType.HasFlag(ProjectType.Wpf) && WpfImplicitReferences.Contains(referenceName)) + { + return true; + } + if (projectType.HasFlag(ProjectType.WinForms) && referenceName == WindowsFormsName) + { + return true; + } + // The interop assembly is implicit only where both stacks meet. + return projectType.HasFlag(ProjectType.Desktop) && referenceName == WindowsFormsIntegrationName; + } + /// /// Writes an assembly reference to the project file. /// @@ -502,46 +538,49 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler static string GetSdkString(ProjectType projectType, TargetFramework targetFramework) { - switch (projectType) - { - case ProjectType.WinForms: - case ProjectType.Wpf: - // Microsoft.NET.Sdk carries the Windows Desktop targets itself since .NET 5 and - // warns (NETSDK1137) about projects that still name the separate SDK; only - // .NET Core 3.x, where the desktop targets are not imported for a plain - // framework moniker, still needs it. - return targetFramework.Identifier == NetCoreAppIdentifier - && targetFramework.VersionNumber >= 300 && targetFramework.VersionNumber < 500 - ? "Microsoft.NET.Sdk.WindowsDesktop" - : "Microsoft.NET.Sdk"; - case ProjectType.Web: - return "Microsoft.NET.Sdk.Web"; - default: - return "Microsoft.NET.Sdk"; + if (projectType.HasFlag(ProjectType.Web)) + { + // A project names a single SDK, and Microsoft.NET.Sdk.Web is the wider one: it + // imports Microsoft.NET.Sdk, which carries the desktop targets UseWPF and + // UseWindowsForms need. The desktop SDK carries no web targets, so an assembly + // that looks like both is exported as a web project. + return "Microsoft.NET.Sdk.Web"; + } + + // Microsoft.NET.Sdk carries the Windows Desktop targets itself since .NET 5 and + // warns (NETSDK1137) about projects that still name the separate SDK; only + // .NET Core 3.x, where the desktop targets are not imported for a plain + // framework moniker, still needs it. + if ((projectType & ProjectType.Desktop) != 0 + && targetFramework.Identifier == NetCoreAppIdentifier + && targetFramework.VersionNumber >= 300 && targetFramework.VersionNumber < 500) + { + return "Microsoft.NET.Sdk.WindowsDesktop"; } + + return "Microsoft.NET.Sdk"; } static ProjectType GetProjectType(MetadataFile module) { + var projectType = ProjectType.Default; foreach (var referenceName in module.AssemblyReferences.Select(r => r.Name)) { if (referenceName.StartsWith(AspNetCorePrefix, StringComparison.Ordinal)) { - return ProjectType.Web; + projectType |= ProjectType.Web; } - - if (referenceName == PresentationFrameworkName) + else if (referenceName == PresentationFrameworkName) { - return ProjectType.Wpf; + projectType |= ProjectType.Wpf; } - - if (referenceName == WindowsFormsName) + else if (referenceName == WindowsFormsName) { - return ProjectType.WinForms; + projectType |= ProjectType.WinForms; } } - return ProjectType.Default; + return projectType; } readonly struct Group : IDisposable