From d0e62a980ff2330a9e9f86df294fd12502049121 Mon Sep 17 00:00:00 2001 From: Peter Crabtree Date: Tue, 18 Mar 2025 22:17:35 -0400 Subject: [PATCH 1/7] nfc: Clean up multiple enumerations of IEnumerable<> in WriteSolutionFile(). (This is potentially expensive and the method is public, just a minor code smell.) --- .../Solution/SolutionCreator.cs | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/ICSharpCode.Decompiler/Solution/SolutionCreator.cs b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs index 1619ea8f1..7cb07c9d5 100644 --- a/ICSharpCode.Decompiler/Solution/SolutionCreator.cs +++ b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs @@ -29,7 +29,7 @@ namespace ICSharpCode.Decompiler.Solution /// public static class SolutionCreator { - private static readonly XNamespace ProjectFileNamespace = XNamespace.Get("http://schemas.microsoft.com/developer/msbuild/2003"); + static readonly XNamespace ProjectFileNamespace = XNamespace.Get("http://schemas.microsoft.com/developer/msbuild/2003"); /// /// Writes a solution file to the specified . @@ -52,20 +52,22 @@ namespace ICSharpCode.Decompiler.Solution throw new ArgumentNullException(nameof(projects)); } - if (!projects.Any()) + var projectList = projects.ToList(); + + if (!projectList.Any()) { throw new InvalidOperationException("At least one project is expected."); } using (var writer = new StreamWriter(targetFile)) { - WriteSolutionFile(writer, projects, targetFile); + WriteSolutionFile(writer, projectList, targetFile); } - FixProjectReferences(projects); + FixProjectReferences(projectList); } - private static void WriteSolutionFile(TextWriter writer, IEnumerable projects, string solutionFilePath) + static void WriteSolutionFile(TextWriter writer, List projects, string solutionFilePath) { WriteHeader(writer); WriteProjects(writer, projects, solutionFilePath); @@ -90,7 +92,7 @@ namespace ICSharpCode.Decompiler.Solution writer.WriteLine("MinimumVisualStudioVersion = 10.0.40219.1"); } - private static void WriteProjects(TextWriter writer, IEnumerable projects, string solutionFilePath) + static void WriteProjects(TextWriter writer, List projects, string solutionFilePath) { foreach (var project in projects) { @@ -103,7 +105,7 @@ namespace ICSharpCode.Decompiler.Solution } } - private static IEnumerable WriteSolutionConfigurations(TextWriter writer, IEnumerable projects) + static List WriteSolutionConfigurations(TextWriter writer, List projects) { var platforms = projects.GroupBy(p => p.PlatformName).Select(g => g.Key).ToList(); @@ -125,10 +127,10 @@ namespace ICSharpCode.Decompiler.Solution return platforms; } - private static void WriteProjectConfigurations( + static void WriteProjectConfigurations( TextWriter writer, - IEnumerable projects, - IEnumerable solutionPlatforms) + List projects, + List solutionPlatforms) { writer.WriteLine("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution"); @@ -152,9 +154,11 @@ namespace ICSharpCode.Decompiler.Solution writer.WriteLine("\tEndGlobalSection"); } - private static void FixProjectReferences(IEnumerable projects) + static void FixProjectReferences(List projects) { - var projectsMap = projects.ToDictionary(p => p.ProjectName, p => p); + var projectsMap = projects.ToDictionary( + p => p.ProjectName, + p => p); foreach (var project in projects) { @@ -192,7 +196,7 @@ namespace ICSharpCode.Decompiler.Solution } } - private static string GetRelativePath(string fromFilePath, string toFilePath) + static string GetRelativePath(string fromFilePath, string toFilePath) { Uri fromUri = new Uri(fromFilePath); Uri toUri = new Uri(toFilePath); From 67366fe77ab17dd68d43c031a74b39f5b8a0cd27 Mon Sep 17 00:00:00 2001 From: Peter Crabtree Date: Tue, 18 Mar 2025 22:23:58 -0400 Subject: [PATCH 2/7] fix: Handle SDK-style project references in WriteSolutionFile() --- .../Solution/SolutionCreator.cs | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/ICSharpCode.Decompiler/Solution/SolutionCreator.cs b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs index 7cb07c9d5..a4d9fb04c 100644 --- a/ICSharpCode.Decompiler/Solution/SolutionCreator.cs +++ b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs @@ -33,6 +33,7 @@ namespace ICSharpCode.Decompiler.Solution /// /// Writes a solution file to the specified . + /// Also fixes intra-solution project references in the project files. /// /// The full path of the file to write. /// The projects contained in this solution. @@ -164,22 +165,37 @@ namespace ICSharpCode.Decompiler.Solution { XDocument projectDoc = XDocument.Load(project.FilePath); + if (projectDoc.Root?.Name.LocalName != "Project") + { + throw new InvalidOperationException( + $"The file {project.FilePath} is not a valid project file, " + + $"no at the root; could not fix project references."); + } + + var sdkStyle = projectDoc.Root.Attribute("Sdk") != null; + var itemGroupTagName = sdkStyle ? "ItemGroup" : ProjectFileNamespace + "ItemGroup"; + var referenceTagName = sdkStyle ? "Reference" : ProjectFileNamespace + "Reference"; + var referencesItemGroups = projectDoc.Root - .Elements(ProjectFileNamespace + "ItemGroup") - .Where(e => e.Elements(ProjectFileNamespace + "Reference").Any()); + .Elements(itemGroupTagName) + .Where(e => e.Elements(referenceTagName).Any()) + .ToList(); foreach (var itemGroup in referencesItemGroups) { - FixProjectReferences(project.FilePath, itemGroup, projectsMap); + FixProjectReferences(project.FilePath, itemGroup, projectsMap, sdkStyle); } projectDoc.Save(project.FilePath); } } - private static void FixProjectReferences(string projectFilePath, XElement itemGroup, IDictionary projects) + static void FixProjectReferences(string projectFilePath, XElement itemGroup, + Dictionary projects, bool sdkStyle) { - foreach (var item in itemGroup.Elements(ProjectFileNamespace + "Reference").ToList()) + var referenceTagName = sdkStyle ? "Reference" : ProjectFileNamespace + "Reference"; + + foreach (var item in itemGroup.Elements(referenceTagName).ToList()) { var assemblyName = item.Attribute("Include")?.Value; if (assemblyName != null && projects.TryGetValue(assemblyName, out var referencedProject)) @@ -187,7 +203,7 @@ namespace ICSharpCode.Decompiler.Solution item.Remove(); var projectReference = new XElement(ProjectFileNamespace + "ProjectReference", - new XElement(ProjectFileNamespace + "Project", referencedProject.Guid.ToString("B").ToUpperInvariant()), + new XElement(ProjectFileNamespace + "Project", referencedProject.Guid.ToString("B").ToLowerInvariant()), new XElement(ProjectFileNamespace + "Name", referencedProject.ProjectName)); projectReference.SetAttributeValue("Include", GetRelativePath(projectFilePath, referencedProject.FilePath)); From 9f89346057902517098376d6822fd826ca1f28bf Mon Sep 17 00:00:00 2001 From: Peter Crabtree Date: Sat, 5 Jul 2025 20:23:02 -0400 Subject: [PATCH 3/7] fix: XML namespace for SDK-style projects in ProjectReferences I accept any sideways glance for the allocation-averse code --- ICSharpCode.Decompiler/Solution/SolutionCreator.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ICSharpCode.Decompiler/Solution/SolutionCreator.cs b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs index a4d9fb04c..749a772c0 100644 --- a/ICSharpCode.Decompiler/Solution/SolutionCreator.cs +++ b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs @@ -193,7 +193,12 @@ namespace ICSharpCode.Decompiler.Solution static void FixProjectReferences(string projectFilePath, XElement itemGroup, Dictionary projects, bool sdkStyle) { - var referenceTagName = sdkStyle ? "Reference" : ProjectFileNamespace + "Reference"; + XName GetElementName(string localName) => sdkStyle ? localName : ProjectFileNamespace + localName; + + var referenceTagName = GetElementName("Reference"); + var projectReferenceTagName = GetElementName("ProjectReference"); + var projectTagName = GetElementName("Project"); + var nameTagName = GetElementName("Name"); foreach (var item in itemGroup.Elements(referenceTagName).ToList()) { @@ -202,9 +207,9 @@ namespace ICSharpCode.Decompiler.Solution { item.Remove(); - var projectReference = new XElement(ProjectFileNamespace + "ProjectReference", - new XElement(ProjectFileNamespace + "Project", referencedProject.Guid.ToString("B").ToLowerInvariant()), - new XElement(ProjectFileNamespace + "Name", referencedProject.ProjectName)); + var projectReference = new XElement(projectReferenceTagName, + new XElement(projectTagName, referencedProject.Guid.ToString("B").ToLowerInvariant()), + new XElement(nameTagName, referencedProject.ProjectName)); projectReference.SetAttributeValue("Include", GetRelativePath(projectFilePath, referencedProject.FilePath)); itemGroup.Add(projectReference); From 7e1349583f5218f307f41d6579088e2bbedb2e69 Mon Sep 17 00:00:00 2001 From: Peter Crabtree Date: Sat, 5 Jul 2025 20:31:10 -0400 Subject: [PATCH 4/7] breaking: Change WriteSolutionFile to take a List<> --- ICSharpCode.Decompiler/Solution/SolutionCreator.cs | 10 ++++------ ILSpy/SolutionWriter.cs | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/ICSharpCode.Decompiler/Solution/SolutionCreator.cs b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs index 749a772c0..ea21a82a7 100644 --- a/ICSharpCode.Decompiler/Solution/SolutionCreator.cs +++ b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs @@ -41,7 +41,7 @@ namespace ICSharpCode.Decompiler.Solution /// Thrown when is null or empty. /// Thrown when is null. /// Thrown when contains no items. - public static void WriteSolutionFile(string targetFile, IEnumerable projects) + public static void WriteSolutionFile(string targetFile, List projects) { if (string.IsNullOrWhiteSpace(targetFile)) { @@ -53,19 +53,17 @@ namespace ICSharpCode.Decompiler.Solution throw new ArgumentNullException(nameof(projects)); } - var projectList = projects.ToList(); - - if (!projectList.Any()) + if (!projects.Any()) { throw new InvalidOperationException("At least one project is expected."); } using (var writer = new StreamWriter(targetFile)) { - WriteSolutionFile(writer, projectList, targetFile); + WriteSolutionFile(writer, projects, targetFile); } - FixProjectReferences(projectList); + FixProjectReferences(projects); } static void WriteSolutionFile(TextWriter writer, List projects, string solutionFilePath) diff --git a/ILSpy/SolutionWriter.cs b/ILSpy/SolutionWriter.cs index 29e8db8b0..06d71e701 100644 --- a/ILSpy/SolutionWriter.cs +++ b/ILSpy/SolutionWriter.cs @@ -153,8 +153,8 @@ namespace ICSharpCode.ILSpy } else { - await Task.Run(() => SolutionCreator.WriteSolutionFile(solutionFilePath, projects)) - .ConfigureAwait(false); + await Task.Run(() => SolutionCreator.WriteSolutionFile(solutionFilePath, projects.ToList())) + .ConfigureAwait(false); } } catch (AggregateException ae) From fdb0703179c82a1209d68ff598db1d9712b09f72 Mon Sep 17 00:00:00 2001 From: Peter Crabtree Date: Sun, 6 Jul 2025 21:10:32 -0400 Subject: [PATCH 5/7] fix: inter-project reference for SDK-style solutions --- .../Solution/SolutionCreator.cs | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/ICSharpCode.Decompiler/Solution/SolutionCreator.cs b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs index ea21a82a7..2b11a712f 100644 --- a/ICSharpCode.Decompiler/Solution/SolutionCreator.cs +++ b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs @@ -29,7 +29,7 @@ namespace ICSharpCode.Decompiler.Solution /// public static class SolutionCreator { - static readonly XNamespace ProjectFileNamespace = XNamespace.Get("http://schemas.microsoft.com/developer/msbuild/2003"); + static readonly XNamespace NonSDKProjectFileNamespace = XNamespace.Get("http://schemas.microsoft.com/developer/msbuild/2003"); /// /// Writes a solution file to the specified . @@ -63,7 +63,7 @@ namespace ICSharpCode.Decompiler.Solution WriteSolutionFile(writer, projects, targetFile); } - FixProjectReferences(projects); + FixAllProjectReferences(projects); } static void WriteSolutionFile(TextWriter writer, List projects, string solutionFilePath) @@ -153,7 +153,7 @@ namespace ICSharpCode.Decompiler.Solution writer.WriteLine("\tEndGlobalSection"); } - static void FixProjectReferences(List projects) + static void FixAllProjectReferences(List projects) { var projectsMap = projects.ToDictionary( p => p.ProjectName, @@ -170,9 +170,11 @@ namespace ICSharpCode.Decompiler.Solution $"no at the root; could not fix project references."); } + // sdk style projects don't use a namespace for the elements, + // but we still need to use the namespace for non-sdk style projects. var sdkStyle = projectDoc.Root.Attribute("Sdk") != null; - var itemGroupTagName = sdkStyle ? "ItemGroup" : ProjectFileNamespace + "ItemGroup"; - var referenceTagName = sdkStyle ? "Reference" : ProjectFileNamespace + "Reference"; + var itemGroupTagName = sdkStyle ? "ItemGroup" : NonSDKProjectFileNamespace + "ItemGroup"; + var referenceTagName = sdkStyle ? "Reference" : NonSDKProjectFileNamespace + "Reference"; var referencesItemGroups = projectDoc.Root .Elements(itemGroupTagName) @@ -191,12 +193,11 @@ namespace ICSharpCode.Decompiler.Solution static void FixProjectReferences(string projectFilePath, XElement itemGroup, Dictionary projects, bool sdkStyle) { - XName GetElementName(string localName) => sdkStyle ? localName : ProjectFileNamespace + localName; + + XName GetElementName(string name) => sdkStyle ? name : NonSDKProjectFileNamespace + name; var referenceTagName = GetElementName("Reference"); var projectReferenceTagName = GetElementName("ProjectReference"); - var projectTagName = GetElementName("Project"); - var nameTagName = GetElementName("Name"); foreach (var item in itemGroup.Elements(referenceTagName).ToList()) { @@ -205,10 +206,20 @@ namespace ICSharpCode.Decompiler.Solution { item.Remove(); - var projectReference = new XElement(projectReferenceTagName, - new XElement(projectTagName, referencedProject.Guid.ToString("B").ToLowerInvariant()), - new XElement(nameTagName, referencedProject.ProjectName)); - projectReference.SetAttributeValue("Include", GetRelativePath(projectFilePath, referencedProject.FilePath)); + var projectReference = new XElement( + projectReferenceTagName, + new XAttribute("Include", GetRelativePath(projectFilePath, referencedProject.FilePath))); + + // SDK-style projects do not use the and elements for project references. + // (Instead, those get read from the .csproj file in "Include".) + if (!sdkStyle) + { + projectReference.Add( + // no ToUpper() for uuids, most Microsoft tools seem to emit them in lowercase + // (no .ToLower() as .ToString("B") already outputs lowercase) + new XElement(NonSDKProjectFileNamespace + "Project", referencedProject.Guid.ToString("B")), + new XElement(NonSDKProjectFileNamespace + "Name", referencedProject.ProjectName)); + } itemGroup.Add(projectReference); } From f9b0411a9612c047a1e427073e0d78b944b78bf2 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 9 Jul 2025 22:32:06 +0200 Subject: [PATCH 6/7] Use List in ILSpy/SolutionWriter.cs --- ILSpy/Commands/SaveCodeContextMenuEntry.cs | 2 +- ILSpy/SolutionWriter.cs | 44 ++++++++-------------- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/ILSpy/Commands/SaveCodeContextMenuEntry.cs b/ILSpy/Commands/SaveCodeContextMenuEntry.cs index 071f29599..2950a3200 100644 --- a/ILSpy/Commands/SaveCodeContextMenuEntry.cs +++ b/ILSpy/Commands/SaveCodeContextMenuEntry.cs @@ -78,7 +78,7 @@ namespace ICSharpCode.ILSpy.TextView { var assemblies = selectedNodes.OfType() .Select(n => n.LoadedAssembly) - .Where(a => a.IsLoadedAsValidAssembly).ToArray(); + .Where(a => a.IsLoadedAsValidAssembly).ToList(); SolutionWriter.CreateSolution(tabPage, textView, selectedPath, currentLanguage, assemblies); } return; diff --git a/ILSpy/SolutionWriter.cs b/ILSpy/SolutionWriter.cs index 06d71e701..62f6ef6c2 100644 --- a/ILSpy/SolutionWriter.cs +++ b/ILSpy/SolutionWriter.cs @@ -56,7 +56,7 @@ namespace ICSharpCode.ILSpy /// Thrown when > or /// is null. public static void CreateSolution(TabPageModel tabPage, DecompilerTextView textView, string solutionFilePath, - Language language, IEnumerable assemblies) + Language language, List assemblies) { if (textView == null) { @@ -77,7 +77,7 @@ namespace ICSharpCode.ILSpy textView .RunWithCancellation(ct => writer.CreateSolution(tabPage, assemblies, language, ct)) - .Then(output => textView.ShowText(output)) + .Then(textView.ShowText) .HandleExceptions(); } @@ -94,41 +94,29 @@ namespace ICSharpCode.ILSpy projects = new ConcurrentBag(); } - async Task CreateSolution(TabPageModel tabPage, IEnumerable assemblies, Language language, CancellationToken ct) + async Task CreateSolution(TabPageModel tabPage, List allAssemblies, Language language, CancellationToken ct) { var result = new AvalonEditTextOutput(); - var assembliesByShortName = assemblies.ToLookup(_ => _.ShortName); + var assembliesByShortName = allAssemblies.GroupBy(_ => _.ShortName).ToDictionary(_ => _.Key, _ => _.ToList()); bool first = true; bool abort = false; - foreach (var item in assembliesByShortName) + foreach (var (shortName, assemblies) in assembliesByShortName) { - var enumerator = item.GetEnumerator(); - if (!enumerator.MoveNext()) - continue; - var firstAssembly = enumerator.Current; - if (!enumerator.MoveNext()) + if (assemblies.Count == 1) + { continue; + } + if (first) { result.WriteLine("Duplicate assembly names selected, cannot generate a solution:"); abort = true; + first = false; } - result.Write("- " + firstAssembly.Text + " conflicts with "); - - first = true; - do - { - var asm = enumerator.Current; - if (!first) - result.Write(", "); - result.Write(asm.Text); - first = false; - } while (enumerator.MoveNext()); - result.WriteLine(); - first = false; + result.WriteLine("- " + assemblies[0].Text + " conflicts with " + string.Join(", ", assemblies.Skip(1))); } if (abort) @@ -141,7 +129,7 @@ namespace ICSharpCode.ILSpy // Explicitly create an enumerable partitioner here to avoid Parallel.ForEach's special cases for lists, // as those seem to use static partitioning which is inefficient if assemblies take differently // long to decompile. - await Task.Run(() => Parallel.ForEach(Partitioner.Create(assemblies), + await Task.Run(() => Parallel.ForEach(Partitioner.Create(allAssemblies), new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = ct }, item => WriteProject(tabPage, item, language, solutionDirectory, ct))) .ConfigureAwait(false); @@ -153,7 +141,7 @@ namespace ICSharpCode.ILSpy } else { - await Task.Run(() => SolutionCreator.WriteSolutionFile(solutionFilePath, projects.ToList())) + await Task.Run(() => SolutionCreator.WriteSolutionFile(solutionFilePath, projects.ToList()), ct) .ConfigureAwait(false); } } @@ -184,14 +172,14 @@ namespace ICSharpCode.ILSpy if (statusOutput.Count == 0) { result.WriteLine("Successfully decompiled the following assemblies into Visual Studio projects:"); - foreach (var item in assemblies.Select(n => n.Text.ToString())) + foreach (var n in allAssemblies) { - result.WriteLine(item); + result.WriteLine(n.Text.ToString()); } result.WriteLine(); - if (assemblies.Count() == projects.Count) + if (allAssemblies.Count == projects.Count) { result.WriteLine("Created the Visual Studio Solution file."); } From d4a27b9b7528fa373fa46cb712d32104090b75ed Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 9 Jul 2025 22:48:24 +0200 Subject: [PATCH 7/7] Fix net10.0-related bug in TargetServices.DetectTargetFramework --- .../CSharp/ProjectDecompiler/TargetServices.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs index 61b3ad39d..48675a2f1 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs @@ -76,11 +76,11 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler targetFrameworkIdentifier = frameworkParts.FirstOrDefault(a => !a.StartsWith(VersionToken, StringComparison.OrdinalIgnoreCase) && !a.StartsWith(ProfileToken, StringComparison.OrdinalIgnoreCase)); string frameworkVersion = frameworkParts.FirstOrDefault(a => a.StartsWith(VersionToken, StringComparison.OrdinalIgnoreCase)); - if (frameworkVersion != null) + if (frameworkVersion != null && Version.TryParse(frameworkVersion.Substring(VersionToken.Length).Replace("v", ""), out var version)) { - versionNumber = int.Parse(frameworkVersion.Substring(VersionToken.Length).Replace("v", "").Replace(".", "")); - if (versionNumber < 100) - versionNumber *= 10; + versionNumber = version.Major * 100 + version.Minor * 10; + if (version.Build > 0) + versionNumber += version.Build; } string frameworkProfile = frameworkParts.FirstOrDefault(a => a.StartsWith(ProfileToken, StringComparison.OrdinalIgnoreCase));