diff --git a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue1047.cs b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue1047.cs index f5d32e786..93f375a02 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue1047.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/ILPretty/Issue1047.cs @@ -6,12 +6,8 @@ private void ProblemMethod() { - IL_0000: while (!dummy) { } - return; - IL_0014: - goto IL_0000; } } } \ No newline at end of file diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LocalFunctions.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LocalFunctions.cs index f66883c7b..e8c8c1201 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LocalFunctions.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/LocalFunctions.cs @@ -273,6 +273,19 @@ namespace LocalFunctions } }); } + + public static IEnumerable YieldReturn(int n) + { + return GetNumbers(); + + IEnumerable GetNumbers() + { + for (int i = 0; i < n; i++) { + yield return i; + } + } + } + //public static void LocalFunctionInUsing() //{ // using (MemoryStream memoryStream = new MemoryStream()) { diff --git a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs index d467ba883..377a617d5 100644 --- a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs @@ -35,6 +35,7 @@ using System.Reflection.Metadata; using static ICSharpCode.Decompiler.Metadata.DotNetCorePathFinderExtensions; using static ICSharpCode.Decompiler.Metadata.MetadataExtensions; using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.Solution; namespace ICSharpCode.Decompiler.CSharp { @@ -101,7 +102,7 @@ namespace ICSharpCode.Decompiler.CSharp } } - public void DecompileProject(PEFile moduleDefinition, string targetDirectory, TextWriter projectFileWriter, CancellationToken cancellationToken = default(CancellationToken)) + public ProjectId DecompileProject(PEFile moduleDefinition, string targetDirectory, TextWriter projectFileWriter, CancellationToken cancellationToken = default(CancellationToken)) { if (string.IsNullOrEmpty(targetDirectory)) { throw new InvalidOperationException("Must set TargetDirectory"); @@ -110,7 +111,7 @@ namespace ICSharpCode.Decompiler.CSharp directories.Clear(); var files = WriteCodeFilesInProject(moduleDefinition, cancellationToken).ToList(); files.AddRange(WriteResourceFilesInProject(moduleDefinition)); - WriteProjectFile(projectFileWriter, files, moduleDefinition); + return WriteProjectFile(projectFileWriter, files, moduleDefinition); } enum LanguageTargets @@ -120,11 +121,12 @@ namespace ICSharpCode.Decompiler.CSharp } #region WriteProjectFile - void WriteProjectFile(TextWriter writer, IEnumerable> files, Metadata.PEFile module) + ProjectId WriteProjectFile(TextWriter writer, IEnumerable> files, Metadata.PEFile module) { const string ns = "http://schemas.microsoft.com/developer/msbuild/2003"; string platformName = GetPlatformName(module); Guid guid = this.ProjectGuid ?? Guid.NewGuid(); + using (XmlTextWriter w = new XmlTextWriter(writer)) { w.Formatting = Formatting.Indented; w.WriteStartDocument(); @@ -281,6 +283,8 @@ namespace ICSharpCode.Decompiler.CSharp w.WriteEndDocument(); } + + return new ProjectId(platformName, guid); } protected virtual bool IsGacAssembly(Metadata.IAssemblyReference r, Metadata.PEFile asm) diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index e2080e799..6c985b465 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -65,6 +65,9 @@ + + + diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/ConditionDetection.cs b/ICSharpCode.Decompiler/IL/ControlFlow/ConditionDetection.cs index 7025208f1..bb7a120b1 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/ConditionDetection.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/ConditionDetection.cs @@ -354,7 +354,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow //assert then block terminates var trueExitInst = GetExit(ifInst.TrueInst); var exitInst = GetExit(block); - context.Step("Negate if for desired branch "+trueExitInst, ifInst); + context.Step($"InvertIf at IL_{ifInst.StartILOffset:x4}", ifInst); //if the then block terminates, else blocks are redundant, and should not exist Debug.Assert(IsEmpty(ifInst.FalseInst)); diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowSimplification.cs b/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowSimplification.cs index d06e00012..4627c73c8 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowSimplification.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/ControlFlowSimplification.cs @@ -158,9 +158,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow } // Remove return blocks that are no longer reachable: container.Blocks.RemoveAll(b => b.IncomingEdgeCount == 0 && b.Instructions.Count == 0); - if (context.Settings.RemoveDeadCode) { - container.SortBlocks(deleteUnreachableBlocks: true); - } } } diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/LoopDetection.cs b/ICSharpCode.Decompiler/IL/ControlFlow/LoopDetection.cs index a8ec926a9..0ae42262b 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/LoopDetection.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/LoopDetection.cs @@ -107,7 +107,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow IncludeNestedContainers(loop); // Try to extend the loop to reduce the number of exit points: ExtendLoop(h, loop, out var exitPoint); - IncludeUnreachablePredecessors(loop); // Sort blocks in the loop in reverse post-order to make the output look a bit nicer. // (if the loop doesn't contain nested loops, this is a topological sort) @@ -115,7 +114,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow Debug.Assert(loop[0] == h); foreach (var node in loop) { node.Visited = false; // reset visited flag so that we can find outer loops - Debug.Assert(h.Dominates(node) || !node.IsReachable, "The loop body must be dominated by the loop head"); + Debug.Assert(h.Dominates(node), "The loop body must be dominated by the loop head"); } ConstructLoop(loop, exitPoint); } @@ -150,7 +149,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow // (the entry-point itself doesn't have a CFG node, because it's newly created by this transform) for (int i = 1; i < nestedContainer.Blocks.Count; i++) { var node = context.ControlFlowGraph.GetNode(nestedContainer.Blocks[i]); - Debug.Assert(loop[0].Dominates(node) || !node.IsReachable); + Debug.Assert(loop[0].Dominates(node)); if (!node.Visited) { node.Visited = true; loop.Add(node); @@ -258,6 +257,22 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow loop.Add(node); } } + // The loop/switch can only be entered through the entry point. + if (isSwitch) { + // In the case of a switch, false positives in the "continue;" detection logic + // can lead to falsely excludes some blocks from the body. + // Fix that by including all predecessors of included blocks. + Debug.Assert(loop[0] == loopHead); + for (int i = 1; i < loop.Count; i++) { + foreach (var p in loop[i].Predecessors) { + if (!p.Visited) { + p.Visited = true; + loop.Add(p); + } + } + } + } + Debug.Assert(loop.All(n => n == loopHead || n.Predecessors.All(p => p.Visited))); } else { // We are in case 2, but could not find a suitable exit point. // Heuristically try to minimize the number of exit points @@ -602,30 +617,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow } #endregion - /// - /// While our normal dominance logic ensures the loop has just a single reachable entry point, - /// it's possible that there are unreachable code blocks that have jumps into the loop. - /// We'll also include those into the loop. - /// - /// Requires and maintains the invariant that a node is marked as visited iff it is contained in the loop. - /// - private void IncludeUnreachablePredecessors(List loop) - { - for (int i = 1; i < loop.Count; i++) { - Debug.Assert(loop[i].Visited); - foreach (var pred in loop[i].Predecessors) { - if (!pred.Visited) { - if (pred.IsReachable) { - Debug.Fail("All jumps into the loop body should go through the entry point"); - } else { - pred.Visited = true; - loop.Add(pred); - } - } - } - } - } - /// /// Move the blocks associated with the loop into a new block container. /// @@ -708,7 +699,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow } exitPoint = null; } - IncludeUnreachablePredecessors(nodesInSwitch); context.Step("Create BlockContainer for switch", switchInst); // Sort blocks in the loop in reverse post-order to make the output look a bit nicer. @@ -717,7 +707,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow Debug.Assert(nodesInSwitch[0] == h); foreach (var node in nodesInSwitch) { node.Visited = false; // reset visited flag so that we can find outer loops - Debug.Assert(h.Dominates(node) || !node.IsReachable, "The switch body must be dominated by the switch head"); + Debug.Assert(h.Dominates(node), "The switch body must be dominated by the switch head"); } BlockContainer switchContainer = new BlockContainer(ContainerKind.Switch); diff --git a/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs b/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs index 0247894ce..bcd9e047b 100644 --- a/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs +++ b/ICSharpCode.Decompiler/IL/ControlFlow/YieldReturnDecompiler.cs @@ -142,7 +142,6 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow function.Body = newBody; // register any locals used in newBody function.Variables.AddRange(newBody.Descendants.OfType().Select(inst => inst.Variable).Distinct()); - function.CheckInvariant(ILPhase.Normal); PrintFinallyMethodStateRanges(newBody); @@ -164,6 +163,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow // Note: because this only deletes blocks outright, the 'stateChanges' entries remain valid // (though some may point to now-deleted blocks) newBody.SortBlocks(deleteUnreachableBlocks: true); + function.CheckInvariant(ILPhase.Normal); if (!isCompiledWithMono) { DecompileFinallyBlocks(); @@ -338,7 +338,7 @@ namespace ICSharpCode.Decompiler.IL.ControlFlow public static bool IsCompilerGeneratorEnumerator(TypeDefinitionHandle type, MetadataReader metadata) { TypeDefinition td; - if (type.IsNil || !type.IsCompilerGenerated(metadata) || (td = metadata.GetTypeDefinition(type)).GetDeclaringType().IsNil) + if (type.IsNil || !type.IsCompilerGeneratedOrIsInCompilerGeneratedClass(metadata) || (td = metadata.GetTypeDefinition(type)).GetDeclaringType().IsNil) return false; foreach (var i in td.GetInterfaceImplementations()) { var tr = metadata.GetInterfaceImplementation(i).Interface.GetFullTypeName(metadata); diff --git a/ICSharpCode.Decompiler/IL/ILReader.cs b/ICSharpCode.Decompiler/IL/ILReader.cs index 49ce05219..88b48da73 100644 --- a/ICSharpCode.Decompiler/IL/ILReader.cs +++ b/ICSharpCode.Decompiler/IL/ILReader.cs @@ -493,8 +493,18 @@ namespace ICSharpCode.Decompiler.IL CollectionExtensions.AddRange(function.Variables, stackVariables); CollectionExtensions.AddRange(function.Variables, variableByExceptionHandler.Values); function.AddRef(); // mark the root node + var removedBlocks = new List(); foreach (var c in function.Descendants.OfType()) { - c.SortBlocks(); + var newOrder = c.TopologicalSort(deleteUnreachableBlocks: true); + if (newOrder.Count < c.Blocks.Count) { + removedBlocks.AddRange(c.Blocks.Except(newOrder)); + } + c.Blocks.ReplaceList(newOrder); + } + if (removedBlocks.Count > 0) { + removedBlocks.SortBy(b => b.StartILOffset); + function.Warnings.Add("Discarded unreachable code: " + + string.Join(", ", removedBlocks.Select(b => $"IL_{b.StartILOffset:x4}"))); } function.Warnings.AddRange(Warnings); return function; diff --git a/ICSharpCode.Decompiler/IL/Instructions/BlockContainer.cs b/ICSharpCode.Decompiler/IL/Instructions/BlockContainer.cs index 0e178853b..b824794a2 100644 --- a/ICSharpCode.Decompiler/IL/Instructions/BlockContainer.cs +++ b/ICSharpCode.Decompiler/IL/Instructions/BlockContainer.cs @@ -184,6 +184,7 @@ namespace ICSharpCode.Decompiler.IL Debug.Assert(EntryPoint == null || Parent is ILFunction || !HasILRange); Debug.Assert(Blocks.All(b => b.HasFlag(InstructionFlags.EndPointUnreachable))); Debug.Assert(Blocks.All(b => b.Kind == BlockKind.ControlFlow)); // this also implies that the blocks don't use FinalInstruction + Debug.Assert(TopologicalSort(deleteUnreachableBlocks: true).Count == Blocks.Count, "Container should not have any unreachable blocks"); Block bodyStartBlock; switch (Kind) { case ContainerKind.Normal: @@ -237,45 +238,56 @@ namespace ICSharpCode.Decompiler.IL return InstructionFlags.ControlFlow; } } - + /// - /// Sort the blocks in reverse post-order over the control flow graph between the blocks. + /// Topologically sort the blocks. + /// The new order is returned without modifying the BlockContainer. /// - public void SortBlocks(bool deleteUnreachableBlocks = false) + /// If true, unreachable blocks are not included in the new order. + public List TopologicalSort(bool deleteUnreachableBlocks = false) { - if (Blocks.Count < 2) - return; - // Visit blocks in post-order BitSet visited = new BitSet(Blocks.Count); List postOrder = new List(); - - Action visit = null; - visit = delegate(Block block) { + Visit(EntryPoint); + postOrder.Reverse(); + if (!deleteUnreachableBlocks) { + for (int i = 0; i < Blocks.Count; i++) { + if (!visited[i]) + postOrder.Add(Blocks[i]); + } + } + return postOrder; + + void Visit(Block block) + { Debug.Assert(block.Parent == this); if (!visited[block.ChildIndex]) { visited[block.ChildIndex] = true; foreach (var branch in block.Descendants.OfType()) { if (branch.TargetBlock.Parent == this) { - visit(branch.TargetBlock); + Visit(branch.TargetBlock); } } postOrder.Add(block); } }; - visit(EntryPoint); - - postOrder.Reverse(); - if (!deleteUnreachableBlocks) { - for (int i = 0; i < Blocks.Count; i++) { - if (!visited[i]) - postOrder.Add(Blocks[i]); - } - } - Debug.Assert(postOrder[0] == Blocks[0]); - Blocks.ReplaceList(postOrder); + } + + /// + /// Topologically sort the blocks. + /// + /// If true, delete unreachable blocks. + public void SortBlocks(bool deleteUnreachableBlocks = false) + { + if (Blocks.Count < 2) + return; + + var newOrder = TopologicalSort(deleteUnreachableBlocks); + Debug.Assert(newOrder[0] == Blocks[0]); + Blocks.ReplaceList(newOrder); } public static BlockContainer FindClosestContainer(ILInstruction inst) diff --git a/ICSharpCode.Decompiler/IL/Transforms/BlockTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/BlockTransform.cs index 07b151d4f..9836b1242 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/BlockTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/BlockTransform.cs @@ -85,7 +85,6 @@ namespace ICSharpCode.Decompiler.IL.Transforms context.CancellationToken.ThrowIfCancellationRequested(); blockContext.ControlFlowGraph = new ControlFlowGraph(container, context.CancellationToken); VisitBlock(blockContext.ControlFlowGraph.GetNode(container.EntryPoint), blockContext); - // TODO: handle unreachable code? } } finally { running = false; diff --git a/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs index cdd1822a0..e73fab690 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs @@ -194,6 +194,18 @@ namespace ICSharpCode.Decompiler.IL // if (cond) { ...; exit; } // ...; exit; EnsureEndPointUnreachable(ifInst.TrueInst, exitInst); + if (ifInst.FalseInst.HasFlag(InstructionFlags.EndPointUnreachable)) { + Debug.Assert(ifInst.HasFlag(InstructionFlags.EndPointUnreachable)); + Debug.Assert(ifInst.Parent == block); + int removeAfter = ifInst.ChildIndex + 1; + if (removeAfter < block.Instructions.Count) { + // Remove all instructions that ended up dead + // (this should just be exitInst itself) + Debug.Assert(block.Instructions.SecondToLastOrDefault() == ifInst); + Debug.Assert(block.Instructions.Last() == exitInst); + block.Instructions.RemoveRange(removeAfter, block.Instructions.Count - removeAfter); + } + } ExtractElseBlock(ifInst); ifInst = elseIfInst; } while (ifInst != null); diff --git a/ICSharpCode.Decompiler/SRMExtensions.cs b/ICSharpCode.Decompiler/SRMExtensions.cs index 44fd7af01..39af674cd 100644 --- a/ICSharpCode.Decompiler/SRMExtensions.cs +++ b/ICSharpCode.Decompiler/SRMExtensions.cs @@ -309,6 +309,17 @@ namespace ICSharpCode.Decompiler return false; } + public static bool IsCompilerGeneratedOrIsInCompilerGeneratedClass(this TypeDefinitionHandle handle, MetadataReader metadata) + { + TypeDefinition type = metadata.GetTypeDefinition(handle); + if (type.IsCompilerGenerated(metadata)) + return true; + TypeDefinitionHandle declaringTypeHandle = type.GetDeclaringType(); + if (!declaringTypeHandle.IsNil && declaringTypeHandle.IsCompilerGenerated(metadata)) + return true; + return false; + } + public static bool IsCompilerGenerated(this MethodDefinition method, MetadataReader metadata) { return method.GetCustomAttributes().HasKnownAttribute(metadata, KnownAttribute.CompilerGenerated); diff --git a/ICSharpCode.Decompiler/Solution/ProjectId.cs b/ICSharpCode.Decompiler/Solution/ProjectId.cs new file mode 100644 index 000000000..65a4e8d9e --- /dev/null +++ b/ICSharpCode.Decompiler/Solution/ProjectId.cs @@ -0,0 +1,56 @@ +// Copyright (c) 2019 AlphaSierraPapa for the SharpDevelop Team +// +// 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; + +namespace ICSharpCode.Decompiler.Solution +{ + /// + /// A container class that holds platform and GUID information about a Visual Studio project. + /// + public class ProjectId + { + /// + /// Initializes a new instance of the class. + /// + /// The project platform. + /// The project GUID. + /// + /// Thrown when + /// or is null or empty. + public ProjectId(string projectPlatform, Guid projectGuid) + { + if (string.IsNullOrWhiteSpace(projectPlatform)) { + throw new ArgumentException("The platform cannot be null or empty.", nameof(projectPlatform)); + } + + Guid = projectGuid; + PlatformName = projectPlatform; + } + + /// + /// Gets the GUID of this project. + /// + public Guid Guid { get; } + + /// + /// Gets the platform name of this project. Only single platform per project is supported. + /// + public string PlatformName { get; } + } +} diff --git a/ICSharpCode.Decompiler/Solution/ProjectItem.cs b/ICSharpCode.Decompiler/Solution/ProjectItem.cs new file mode 100644 index 000000000..bf1222368 --- /dev/null +++ b/ICSharpCode.Decompiler/Solution/ProjectItem.cs @@ -0,0 +1,55 @@ +// Copyright (c) 2019 AlphaSierraPapa for the SharpDevelop Team +// +// 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.IO; + +namespace ICSharpCode.Decompiler.Solution +{ + /// + /// A container class that holds information about a Visual Studio project. + /// + public sealed class ProjectItem : ProjectId + { + /// + /// Initializes a new instance of the class. + /// + /// The full path of the project file. + /// The project platform. + /// The project GUID. + /// + /// Thrown when + /// or is null or empty. + public ProjectItem(string projectFile, string projectPlatform, Guid projectGuid) + : base(projectPlatform, projectGuid) + { + ProjectName = Path.GetFileNameWithoutExtension(projectFile); + FilePath = projectFile; + } + + /// + /// Gets the name of the project. + /// + public string ProjectName { get; } + + /// + /// Gets the full path to the project file. + /// + public string FilePath { get; } + } +} diff --git a/ICSharpCode.Decompiler/Solution/SolutionCreator.cs b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs new file mode 100644 index 000000000..824f03a23 --- /dev/null +++ b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs @@ -0,0 +1,201 @@ +// Copyright (c) 2019 AlphaSierraPapa for the SharpDevelop Team +// +// 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.Xml.Linq; + +namespace ICSharpCode.Decompiler.Solution +{ + /// + /// A helper class that can write a Visual Studio Solution file for the provided projects. + /// + public static class SolutionCreator + { + private static readonly XNamespace ProjectFileNamespace = XNamespace.Get("http://schemas.microsoft.com/developer/msbuild/2003"); + + /// + /// Writes a solution file to the specified . + /// + /// The full path of the file to write. + /// The projects contained in this solution. + /// + /// Thrown when is null or empty. + /// Thrown when is null. + /// Thrown when contains no items. + public static void WriteSolutionFile(string targetFile, IEnumerable projects) + { + if (string.IsNullOrWhiteSpace(targetFile)) { + throw new ArgumentException("The target file cannot be null or empty.", nameof(targetFile)); + } + + if (projects == null) { + throw new ArgumentNullException(nameof(projects)); + } + + if (!projects.Any()) { + throw new InvalidOperationException("At least one project is expected."); + } + + using (var writer = new StreamWriter(targetFile)) { + WriteSolutionFile(writer, projects, targetFile); + } + + FixProjectReferences(projects); + } + + private static void WriteSolutionFile(TextWriter writer, IEnumerable projects, string solutionFilePath) + { + WriteHeader(writer); + WriteProjects(writer, projects, solutionFilePath); + + writer.WriteLine("Global"); + + var platforms = WriteSolutionConfigurations(writer, projects); + WriteProjectConfigurations(writer, projects, platforms); + + writer.WriteLine("\tGlobalSection(SolutionProperties) = preSolution"); + writer.WriteLine("\t\tHideSolutionNode = FALSE"); + writer.WriteLine("\tEndGlobalSection"); + + writer.WriteLine("EndGlobal"); + } + + private static void WriteHeader(TextWriter writer) + { + writer.WriteLine("Microsoft Visual Studio Solution File, Format Version 12.00"); + writer.WriteLine("# Visual Studio 14"); + writer.WriteLine("VisualStudioVersion = 14.0.24720.0"); + writer.WriteLine("MinimumVisualStudioVersion = 10.0.40219.1"); + } + + private static void WriteProjects(TextWriter writer, IEnumerable projects, string solutionFilePath) + { + var solutionGuid = Guid.NewGuid().ToString("B").ToUpperInvariant(); + + foreach (var project in projects) { + var projectRelativePath = GetRelativePath(solutionFilePath, project.FilePath); + var projectGuid = project.Guid.ToString("B").ToUpperInvariant(); + + writer.WriteLine($"Project(\"{solutionGuid}\") = \"{project.ProjectName}\", \"{projectRelativePath}\", \"{projectGuid}\""); + writer.WriteLine("EndProject"); + } + } + + private static IEnumerable WriteSolutionConfigurations(TextWriter writer, IEnumerable projects) + { + var platforms = projects.GroupBy(p => p.PlatformName).Select(g => g.Key).ToList(); + + platforms.Sort(); + + writer.WriteLine("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution"); + foreach (var platform in platforms) { + writer.WriteLine($"\t\tDebug|{platform} = Debug|{platform}"); + } + + foreach (var platform in platforms) { + writer.WriteLine($"\t\tRelease|{platform} = Release|{platform}"); + } + + writer.WriteLine("\tEndGlobalSection"); + + return platforms; + } + + private static void WriteProjectConfigurations( + TextWriter writer, + IEnumerable projects, + IEnumerable solutionPlatforms) + { + writer.WriteLine("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution"); + + foreach (var project in projects) { + var projectGuid = project.Guid.ToString("B").ToUpperInvariant(); + + foreach (var platform in solutionPlatforms) { + writer.WriteLine($"\t\t{projectGuid}.Debug|{platform}.ActiveCfg = Debug|{project.PlatformName}"); + writer.WriteLine($"\t\t{projectGuid}.Debug|{platform}.Build.0 = Debug|{project.PlatformName}"); + } + + foreach (var platform in solutionPlatforms) { + writer.WriteLine($"\t\t{projectGuid}.Release|{platform}.ActiveCfg = Release|{project.PlatformName}"); + writer.WriteLine($"\t\t{projectGuid}.Release|{platform}.Build.0 = Release|{project.PlatformName}"); + } + } + + writer.WriteLine("\tEndGlobalSection"); + } + + private static void FixProjectReferences(IEnumerable projects) + { + var projectsMap = projects.ToDictionary(p => p.ProjectName, p => p); + + foreach (var project in projects) { + XDocument projectDoc = XDocument.Load(project.FilePath); + + var referencesItemGroups = projectDoc.Root + .Elements(ProjectFileNamespace + "ItemGroup") + .Where(e => e.Elements(ProjectFileNamespace + "Reference").Any()); + + foreach (var itemGroup in referencesItemGroups) { + FixProjectReferences(project.FilePath, itemGroup, projectsMap); + } + + projectDoc.Save(project.FilePath); + } + } + + private static void FixProjectReferences(string projectFilePath, XElement itemGroup, IDictionary projects) + { + foreach (var item in itemGroup.Elements(ProjectFileNamespace + "Reference").ToList()) { + var assemblyName = item.Attribute("Include")?.Value; + if (assemblyName != null && projects.TryGetValue(assemblyName, out var referencedProject)) { + item.Remove(); + + var projectReference = new XElement(ProjectFileNamespace + "ProjectReference", + new XElement(ProjectFileNamespace + "Project", referencedProject.Guid.ToString("B").ToUpperInvariant()), + new XElement(ProjectFileNamespace + "Name", referencedProject.ProjectName)); + projectReference.SetAttributeValue("Include", GetRelativePath(projectFilePath, referencedProject.FilePath)); + + itemGroup.Add(projectReference); + } + } + } + + private static string GetRelativePath(string fromFilePath, string toFilePath) + { + Uri fromUri = new Uri(fromFilePath); + Uri toUri = new Uri(toFilePath); + + if (fromUri.Scheme != toUri.Scheme) { + return toFilePath; + } + + Uri relativeUri = fromUri.MakeRelativeUri(toUri); + string relativePath = Uri.UnescapeDataString(relativeUri.ToString()); + + if (string.Equals(toUri.Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase)) { + relativePath = relativePath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + } + + return relativePath; + } + } +} diff --git a/ILSpy.BamlDecompiler/Handlers/Records/PropertyTypeReferenceHandler.cs b/ILSpy.BamlDecompiler/Handlers/Records/PropertyTypeReferenceHandler.cs index c9212db25..148ded0e2 100644 --- a/ILSpy.BamlDecompiler/Handlers/Records/PropertyTypeReferenceHandler.cs +++ b/ILSpy.BamlDecompiler/Handlers/Records/PropertyTypeReferenceHandler.cs @@ -40,7 +40,7 @@ namespace ILSpy.BamlDecompiler.Handlers { var elemAttr = ctx.ResolveProperty(record.AttributeId); elem.Xaml = new XElement(elemAttr.ToXName(ctx, null)); - if (attr.ResolvedMember.FullNameIs("System.Windows.Style", "TargetType")) { + if (attr.ResolvedMember?.FullNameIs("System.Windows.Style", "TargetType") == true) { parent.Xaml.Element.AddAnnotation(new TargetTypeAnnotation(type)); } diff --git a/ILSpy/Commands/SaveCodeContextMenuEntry.cs b/ILSpy/Commands/SaveCodeContextMenuEntry.cs new file mode 100644 index 000000000..8ee945f0d --- /dev/null +++ b/ILSpy/Commands/SaveCodeContextMenuEntry.cs @@ -0,0 +1,133 @@ +// Copyright (c) 2011 AlphaSierraPapa for the SharpDevelop Team +// +// 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.Windows; +using System.Windows.Input; +using ICSharpCode.ILSpy.Properties; +using ICSharpCode.ILSpy.TreeNodes; +using ICSharpCode.TreeView; +using Microsoft.Win32; + +namespace ICSharpCode.ILSpy.TextView +{ + [ExportContextMenuEntry(Header = nameof(Resources._SaveCode), Category = nameof(Resources.Save), Icon = "Images/SaveFile.png")] + sealed class SaveCodeContextMenuEntry : IContextMenuEntry + { + public void Execute(TextViewContext context) + { + Execute(context.SelectedTreeNodes); + } + + public bool IsEnabled(TextViewContext context) => true; + + public bool IsVisible(TextViewContext context) + { + return CanExecute(context.SelectedTreeNodes); + } + + public static bool CanExecute(IReadOnlyList selectedNodes) + { + if (selectedNodes == null || selectedNodes.Any(n => !(n is ILSpyTreeNode))) + return false; + return selectedNodes.Count == 1 + || (selectedNodes.Count > 1 && (selectedNodes.All(n => n is AssemblyTreeNode) || selectedNodes.All(n => n is IMemberTreeNode))); + } + + public static void Execute(IReadOnlyList selectedNodes) + { + var currentLanguage = MainWindow.Instance.CurrentLanguage; + var textView = MainWindow.Instance.TextView; + if (selectedNodes.Count == 1 && selectedNodes[0] is ILSpyTreeNode singleSelection) { + // if there's only one treenode selected + // we will invoke the custom Save logic + if (singleSelection.Save(textView)) + return; + } else if (selectedNodes.Count > 1 && selectedNodes.All(n => n is AssemblyTreeNode)) { + var initialPath = Path.GetDirectoryName(((AssemblyTreeNode)selectedNodes[0]).LoadedAssembly.FileName); + var selectedPath = SelectSolutionFile(initialPath); + + if (!string.IsNullOrEmpty(selectedPath)) { + var assemblies = selectedNodes.OfType() + .Select(n => n.LoadedAssembly) + .Where(a => !a.HasLoadError).ToArray(); + SolutionWriter.CreateSolution(textView, selectedPath, currentLanguage, assemblies); + } + return; + } + + // Fallback: if nobody was able to handle the request, use default behavior. + // try to save all nodes to disk. + var options = new DecompilationOptions() { FullDecompilation = true }; + textView.SaveToDisk(currentLanguage, selectedNodes.OfType(), options); + } + + /// + /// Shows a File Selection dialog where the user can select the target file for the solution. + /// + /// The initial path to show in the dialog. If not specified, the 'Documents' directory + /// will be used. + /// + /// The full path of the selected target file, or null if the user canceled. + static string SelectSolutionFile(string path) + { + const string SolutionExtension = ".sln"; + const string DefaultSolutionName = "Solution"; + + if (string.IsNullOrWhiteSpace(path)) { + path = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments); + } + + SaveFileDialog dlg = new SaveFileDialog(); + dlg.InitialDirectory = path; + dlg.FileName = Path.Combine(path, DefaultSolutionName + SolutionExtension); + dlg.Filter = "Visual Studio Solution file|*" + SolutionExtension; + + bool targetInvalid; + do { + if (dlg.ShowDialog() != true) { + return null; + } + + string selectedPath = Path.GetDirectoryName(dlg.FileName); + try { + targetInvalid = Directory.EnumerateFileSystemEntries(selectedPath).Any(); + } catch (Exception e) when (e is IOException || e is UnauthorizedAccessException || e is System.Security.SecurityException) { + MessageBox.Show( + "The directory cannot be accessed. Please ensure it exists and you have sufficient rights to access it.", + "Solution directory not accessible", + MessageBoxButton.OK, MessageBoxImage.Error); + targetInvalid = true; + continue; + } + + if (targetInvalid) { + MessageBox.Show( + "The directory is not empty. Please select an empty directory.", + "Solution directory not empty", + MessageBoxButton.OK, MessageBoxImage.Warning); + } + } while (targetInvalid); + + return dlg.FileName; + } + } +} diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj index c7f0658d6..24f0d0b59 100644 --- a/ILSpy/ILSpy.csproj +++ b/ILSpy/ILSpy.csproj @@ -204,9 +204,11 @@ + + diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 2441e0eb2..35f614764 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -34,6 +34,7 @@ using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Transforms; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Output; +using ICSharpCode.Decompiler.Solution; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.Util; using ICSharpCode.ILSpy.TreeNodes; @@ -343,12 +344,12 @@ namespace ICSharpCode.ILSpy } } - public override void DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) + public override ProjectId DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) { var module = assembly.GetPEFileOrNull(); if (options.FullDecompilation && options.SaveAsProjectDirectory != null) { var decompiler = new ILSpyWholeProjectDecompiler(assembly, options); - decompiler.DecompileProject(module, options.SaveAsProjectDirectory, new TextOutputWriter(output), options.CancellationToken); + return decompiler.DecompileProject(module, options.SaveAsProjectDirectory, new TextOutputWriter(output), options.CancellationToken); } else { AddReferenceAssemblyWarningMessage(module, output); AddReferenceWarningMessage(module, output); @@ -389,7 +390,7 @@ namespace ICSharpCode.ILSpy } if (metadata.IsAssembly) { var asm = metadata.GetAssemblyDefinition(); - if (asm.HashAlgorithm != System.Reflection.AssemblyHashAlgorithm.None) + if (asm.HashAlgorithm != AssemblyHashAlgorithm.None) output.WriteLine("// Hash algorithm: " + asm.HashAlgorithm.ToString().ToUpper()); if (!asm.PublicKey.IsNil) { output.Write("// Public key: "); @@ -415,6 +416,7 @@ namespace ICSharpCode.ILSpy } WriteCode(output, options.DecompilerSettings, st, decompiler.TypeSystem); } + return null; } } diff --git a/ILSpy/Languages/ILLanguage.cs b/ILSpy/Languages/ILLanguage.cs index e2ab9d1f3..3dd1ec1ab 100644 --- a/ILSpy/Languages/ILLanguage.cs +++ b/ILSpy/Languages/ILLanguage.cs @@ -27,6 +27,8 @@ using System.Reflection.Metadata.Ecma335; using System.Linq; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.Decompiler.Util; +using ICSharpCode.Decompiler.Solution; namespace ICSharpCode.ILSpy { @@ -150,7 +152,7 @@ namespace ICSharpCode.ILSpy dis.DisassembleNamespace(nameSpace, module, types.Select(t => (TypeDefinitionHandle)t.MetadataToken)); } - public override void DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) + public override ProjectId DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) { output.WriteLine("// " + assembly.FileName); output.WriteLine(); @@ -174,6 +176,7 @@ namespace ICSharpCode.ILSpy dis.WriteModuleContents(module); } } + return null; } } } diff --git a/ILSpy/Languages/Language.cs b/ILSpy/Languages/Language.cs index a1aad8d64..e97696fea 100644 --- a/ILSpy/Languages/Language.cs +++ b/ILSpy/Languages/Language.cs @@ -23,6 +23,7 @@ using System.Reflection.PortableExecutable; using System.Text; using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.Solution; using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.TypeSystem.Implementation; using ICSharpCode.Decompiler.Util; @@ -131,11 +132,11 @@ namespace ICSharpCode.ILSpy WriteCommentLine(output, nameSpace); } - public virtual void DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) + public virtual ProjectId DecompileAssembly(LoadedAssembly assembly, ITextOutput output, DecompilationOptions options) { WriteCommentLine(output, assembly.FileName); var asm = assembly.GetPEFileOrNull(); - if (asm == null) return; + if (asm == null) return null; var metadata = asm.Metadata; if (metadata.IsAssembly) { var name = metadata.GetAssemblyDefinition(); @@ -147,6 +148,7 @@ namespace ICSharpCode.ILSpy } else { WriteCommentLine(output, metadata.GetString(metadata.GetModuleDefinition().Name)); } + return null; } public virtual void WriteCommentLine(ITextOutput output, string comment) diff --git a/ILSpy/MainWindow.xaml b/ILSpy/MainWindow.xaml index a168d2e66..ed8cc1b1b 100644 --- a/ILSpy/MainWindow.xaml +++ b/ILSpy/MainWindow.xaml @@ -28,6 +28,7 @@ Executed="RefreshCommandExecuted" /> + /// Looks up a localized string similar to The directory is not empty. File will be overwritten.\r\nAre you sure you want to continue?. + /// + public static string AssemblySaveCodeDirectoryNotEmpty { + get { + return ResourceManager.GetString("AssemblySaveCodeDirectoryNotEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Project Directory not empty. + /// + public static string AssemblySaveCodeDirectoryNotEmptyTitle { + get { + return ResourceManager.GetString("AssemblySaveCodeDirectoryNotEmptyTitle", resourceCulture); + } + } + /// /// Looks up a localized string similar to Automatically check for updates every week. /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index a315d0e8a..e71b775c1 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -732,4 +732,10 @@ Use 'ref' extension methods + + The directory is not empty. File will be overwritten.\r\nAre you sure you want to continue? + + + Project Directory not empty + \ No newline at end of file diff --git a/ILSpy/SolutionWriter.cs b/ILSpy/SolutionWriter.cs new file mode 100644 index 000000000..d892e22ec --- /dev/null +++ b/ILSpy/SolutionWriter.cs @@ -0,0 +1,182 @@ +// Copyright (c) 2011 AlphaSierraPapa for the SharpDevelop Team +// +// 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.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.Solution; +using ICSharpCode.Decompiler.Util; +using ICSharpCode.ILSpy.TextView; + +namespace ICSharpCode.ILSpy +{ + /// + /// An utility class that creates a Visual Studio solution containing projects for the + /// decompiled assemblies. + /// + internal class SolutionWriter + { + /// + /// Creates a Visual Studio solution that contains projects with decompiled code + /// of the specified . The solution file will be saved + /// to the . The directory of this file must either + /// be empty or not exist. + /// + /// A reference to the instance. + /// The target file path of the solution file. + /// The assembly nodes to decompile. + /// + /// Thrown when is null, + /// an empty or a whitespace string. + /// Thrown when > or + /// is null. + public static void CreateSolution(DecompilerTextView textView, string solutionFilePath, Language language, IEnumerable assemblies) + { + if (textView == null) { + throw new ArgumentNullException(nameof(textView)); + } + + if (string.IsNullOrWhiteSpace(solutionFilePath)) { + throw new ArgumentException("The solution file path cannot be null or empty.", nameof(solutionFilePath)); + } + + if (assemblies == null) { + throw new ArgumentNullException(nameof(assemblies)); + } + + var writer = new SolutionWriter(solutionFilePath); + + textView + .RunWithCancellation(ct => writer.CreateSolution(assemblies, language, ct)) + .Then(output => textView.ShowText(output)) + .HandleExceptions(); + } + + readonly string solutionFilePath; + readonly string solutionDirectory; + readonly ConcurrentBag projects; + readonly ConcurrentBag statusOutput; + + SolutionWriter(string solutionFilePath) + { + this.solutionFilePath = solutionFilePath; + solutionDirectory = Path.GetDirectoryName(solutionFilePath); + statusOutput = new ConcurrentBag(); + projects = new ConcurrentBag(); + } + + async Task CreateSolution(IEnumerable assemblies, Language language, CancellationToken ct) + { + var result = new AvalonEditTextOutput(); + + var duplicates = new HashSet(); + if (assemblies.Any(asm => !duplicates.Add(asm.ShortName))) { + result.WriteLine("Duplicate assembly names selected, cannot generate a solution."); + return result; + } + + Stopwatch stopwatch = Stopwatch.StartNew(); + + try { + await Task.Run(() => Parallel.ForEach(assemblies, n => WriteProject(n, language, solutionDirectory, ct))) + .ConfigureAwait(false); + + await Task.Run(() => SolutionCreator.WriteSolutionFile(solutionFilePath, projects)) + .ConfigureAwait(false); + } catch (AggregateException ae) { + if (ae.Flatten().InnerExceptions.All(e => e is OperationCanceledException)) { + result.WriteLine(); + result.WriteLine("Generation was cancelled."); + return result; + } + + result.WriteLine(); + result.WriteLine("Failed to generate the Visual Studio Solution. Errors:"); + ae.Handle(e => { + result.WriteLine(e.Message); + return true; + }); + + return result; + } + + foreach (var item in statusOutput) { + result.WriteLine(item); + } + + 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())) { + result.WriteLine(item); + } + + result.WriteLine(); + + if (assemblies.Count() == projects.Count) { + result.WriteLine("Created the Visual Studio Solution file."); + } + + result.WriteLine(); + result.WriteLine("Elapsed time: " + stopwatch.Elapsed.TotalSeconds.ToString("F1") + " seconds."); + result.WriteLine(); + result.AddButton(null, "Open Explorer", delegate { Process.Start("explorer", "/select,\"" + solutionFilePath + "\""); }); + } + + return result; + } + + void WriteProject(LoadedAssembly loadedAssembly, Language language, string targetDirectory, CancellationToken ct) + { + targetDirectory = Path.Combine(targetDirectory, loadedAssembly.ShortName); + string projectFileName = Path.Combine(targetDirectory, loadedAssembly.ShortName + language.ProjectFileExtension); + + if (!Directory.Exists(targetDirectory)) { + try { + Directory.CreateDirectory(targetDirectory); + } catch (Exception e) { + statusOutput.Add($"Failed to create a directory '{targetDirectory}':{Environment.NewLine}{e}"); + return; + } + } + + try { + using (var projectFileWriter = new StreamWriter(projectFileName)) { + var projectFileOutput = new PlainTextOutput(projectFileWriter); + var options = new DecompilationOptions() { + FullDecompilation = true, + CancellationToken = ct, + SaveAsProjectDirectory = targetDirectory + }; + + var projectInfo = language.DecompileAssembly(loadedAssembly, projectFileOutput, options); + if (projectInfo != null) { + projects.Add(new ProjectItem(projectFileName, projectInfo.PlatformName, projectInfo.Guid)); + } + } + } catch (Exception e) when (!(e is OperationCanceledException)) { + statusOutput.Add($"Failed to decompile the assembly '{loadedAssembly.FileName}':{Environment.NewLine}{e}"); + } + } + } +} diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index 51aa65bdf..13e2c1df2 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -292,9 +292,8 @@ namespace ICSharpCode.ILSpy.TreeNodes foreach (string entry in Directory.GetFileSystemEntries(options.SaveAsProjectDirectory)) { if (!string.Equals(entry, dlg.FileName, StringComparison.OrdinalIgnoreCase)) { var result = MessageBox.Show( - "The directory is not empty. File will be overwritten." + Environment.NewLine + - "Are you sure you want to continue?", - "Project Directory not empty", + Resources.AssemblySaveCodeDirectoryNotEmpty, + Resources.AssemblySaveCodeDirectoryNotEmptyTitle, MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.No); if (result == MessageBoxResult.No) return true; // don't save, but mark the Save operation as handled diff --git a/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs b/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs index 331f8da08..fde4630f1 100644 --- a/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs +++ b/ILSpy/TreeNodes/ReferenceFolderTreeNode.cs @@ -86,7 +86,6 @@ namespace ICSharpCode.ILSpy.TreeNodes output.Unindent(); output.WriteLine(); } - } } }