From 22243de7b087fdd46470c2fb79faab625c83fbef Mon Sep 17 00:00:00 2001 From: Chicken-Bones Date: Sat, 28 Dec 2019 23:36:18 +1000 Subject: [PATCH 01/10] Improve ReduceNestingTransform by considering nested containers (Try/Using/Lock/Pinned/etc) --- .../TestCases/Pretty/ExpressionTrees.cs | 14 +- .../TestCases/Pretty/ReduceNesting.cs | 135 ++++++++++++- .../IL/Transforms/ReduceNestingTransform.cs | 189 +++++++++++++++--- 3 files changed, 296 insertions(+), 42 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExpressionTrees.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExpressionTrees.cs index eb8d29d05..4bcc19f1a 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExpressionTrees.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ExpressionTrees.cs @@ -1037,17 +1037,15 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty public async Task Issue1524(string str) { await Task.Delay(100); - if (string.IsNullOrEmpty(str)) { #if CS70 - if (int.TryParse(str, out int id)) { + if (string.IsNullOrEmpty(str) && int.TryParse(str, out int id)) { #else - int id; - if (int.TryParse(str, out id)) { + int id; + if (string.IsNullOrEmpty(str) && int.TryParse(str, out id)) { #endif - (from a in new List().AsQueryable() - where a == id - select a).FirstOrDefault(); - } + (from a in new List().AsQueryable() + where a == id + select a).FirstOrDefault(); } } diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ReduceNesting.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ReduceNesting.cs index a08cd43e9..9b72b0044 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ReduceNesting.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ReduceNesting.cs @@ -194,7 +194,7 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty } } - // nesting should not be reduced as maximum nesting level is 2 + // nesting should be reduced as maximum nesting level is 2 public void EarlyExit2() { if (B(0)) { @@ -266,5 +266,138 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty } return s; } + + public void EarlyExitBeforeTry() + { + if (B(0)) { + return; + } + + try { + if (B(1)) { + Console.WriteLine(); + } + } catch { + } + } + + public void EarlyExitInTry() + { + try { + if (B(0)) { + return; + } + + Console.WriteLine(); + + if (B(1)) { + for (int i = 0; i < 10; i++) { + Console.WriteLine(i); + } + } + } catch { + } + } + + public void ContinueLockInLoop() + { + while (B(0)) { + lock (Console.Out) { + if (B(1)) { + continue; + } + + Console.WriteLine(); + + if (B(2)) { + for (int i = 0; i < 10; i++) { + Console.WriteLine(i); + } + } + } + } + } + + public void BreakLockInLoop() + { + while (B(0)) { + lock (Console.Out) { + // Before ReduceNestingTransform, the rest of the lock body is nested in if(!B(1)) with a continue; + // the B(1) case falls through to a break outside the lock + if (B(1)) { + break; + } + + Console.WriteLine(); + + if (B(2)) { + for (int i = 0; i < 10; i++) { + Console.WriteLine(i); + } + } + + // the break gets duplicated into the lock (replacing the leave) making the lock 'endpoint unreachable' and the break outside the lock is removed + // After the condition is inverted, ReduceNestingTransform isn't smart enough to then move the continue out of the lock + // Thus the redundant continue; + continue; + } + } + Console.WriteLine(); + } + + public unsafe void BreakPinnedInLoop(int[] arr) + { + while (B(0)) { + fixed (int* ptr = arr) { + if (B(1)) { + break; + } + + Console.WriteLine(); + + if (B(2)) { + for (int i = 0; i < 10; i++) { + Console.WriteLine(ptr[i]); + } + } + + // Same reason as BreakLockInLoop + continue; + } + } + Console.WriteLine(); + } + + public void CannotEarlyExitInTry() + { + try { + if (B(0)) { + Console.WriteLine(); + + if (B(1)) { + for (int i = 0; i < 10; i++) { + Console.WriteLine(i); + } + } + } + } catch { + } + Console.WriteLine(); + } + + public void EndpointUnreachableDueToEarlyExit() + { + using (Console.Out) { + if (B(0)) { + return; + } + do { + if (B(1)) { + return; + } + } while (B(2)); + throw new Exception(); + } + } } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs index 332a1a113..5a98235e3 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs @@ -33,7 +33,7 @@ namespace ICSharpCode.Decompiler.IL /// This can lead to excessive indentation when the entire rest of the method/loop is included in the else block/default case. /// When an If/SwitchInstruction is followed immediately by a keyword exit, the exit can be moved into the child blocks /// allowing the else block or default case to be moved after the if/switch as all prior cases exit. - /// Most importantly, this transformatino does not change the IL order of any code. + /// Most importantly, this transformation does not change the IL order of any code. /// /// ConditionDetection also has a block exit priority system to assist exit point reduction which in some cases ignores IL order. /// After HighLevelLoopTransform has run, all structures have been detected and preference can be returned to maintaining IL ordering. @@ -99,16 +99,16 @@ namespace ICSharpCode.Decompiler.IL // reduce nesting in switch blocks if (container.Kind == ContainerKind.Switch && - CanDuplicateExit(NextInsn(), continueTarget) && - ReduceSwitchNesting(block, container, NextInsn())) { + CanDuplicateExit(NextInsn(), continueTarget, out var keywordExit1) && + ReduceSwitchNesting(block, container, keywordExit1)) { RemoveRedundantExit(block, nextInstruction); } break; case IfInstruction ifInst: - ImproveILOrdering(block, ifInst); + ImproveILOrdering(block, ifInst, continueTarget); // reduce nesting in if/else blocks - if (CanDuplicateExit(NextInsn(), continueTarget) && ReduceNesting(block, ifInst, NextInsn())) + if (CanDuplicateExit(NextInsn(), continueTarget, out var keywordExit2) && ReduceNesting(block, ifInst, keywordExit2)) RemoveRedundantExit(block, nextInstruction); // visit content blocks @@ -124,27 +124,69 @@ namespace ICSharpCode.Decompiler.IL Visit(falseBlock, continueTarget, NextInsn()); } break; + default: + // blocks can only exit containers via leave instructions, not fallthrough, so the only relevant context is `continueTarget` + VisitContainers(inst, continueTarget); + + // reducing nesting inside Try/Using/Lock etc, may make the endpoint unreachable. + // This should only happen by replacing a Leave with the exit instruction we're about to delete, but I can't see a good way to assert this + // This would be better placed in ReduceNesting, but it's more difficult to find the affected instructions/blocks there than here + if (i == block.Instructions.Count - 2 && inst.HasFlag(InstructionFlags.EndPointUnreachable)) { + context.Step("Remove unreachable exit", block.Instructions.Last()); + block.Instructions.RemoveLast(); + + // This would be the right place to check and fix the redundant continue; in TestCases.Pretty.ReduceNesting.BreakLockInLoop + // but doing so would require knowledge of what `inst` is, and how it works. (eg. to target the try block and not catch or finally blocks) + } + break; } } } + // search for child containers to reduce nesting in + private void VisitContainers(ILInstruction inst, Block continueTarget) + { + switch (inst) { + case ILFunction _: + break; // assume inline ILFunctions are already transformed + case BlockContainer cont: + Visit(cont, continueTarget); + break; + default: + foreach (var child in inst.Children) + VisitContainers(child, continueTarget); + break; + } + } + /// /// For an if statement with an unreachable end point and no else block, /// inverts to match IL order of the first statement of each branch /// - private void ImproveILOrdering(Block block, IfInstruction ifInst) + private void ImproveILOrdering(Block block, IfInstruction ifInst, Block continueTarget) { if (!block.HasFlag(InstructionFlags.EndPointUnreachable) - || !ifInst.TrueInst.HasFlag(InstructionFlags.EndPointUnreachable) - || !ifInst.FalseInst.MatchNop()) + || !ifInst.TrueInst.HasFlag(InstructionFlags.EndPointUnreachable) + || !ifInst.FalseInst.MatchNop()) return; - + Debug.Assert(ifInst != block.Instructions.Last()); var trueRangeStart = ConditionDetection.GetStartILOffset(ifInst.TrueInst, out bool trueRangeIsEmpty); - var falseRangeStart = ConditionDetection.GetStartILOffset(block.Instructions[block.Instructions.IndexOf(ifInst)+1], out bool falseRangeIsEmpty); - if (!trueRangeIsEmpty && !falseRangeIsEmpty && falseRangeStart < trueRangeStart) - ConditionDetection.InvertIf(block, ifInst, context); + var falseRangeStart = ConditionDetection.GetStartILOffset(block.Instructions[block.Instructions.IndexOf(ifInst) + 1], out bool falseRangeIsEmpty); + if (trueRangeIsEmpty || falseRangeIsEmpty || falseRangeStart >= trueRangeStart) + return; + + if (block.Instructions.Last() is Leave leave && !leave.IsLeavingFunction && leave.TargetContainer.Kind == ContainerKind.Normal) { + // non-keyword leave. Can't move out of the last position in the block (fall-through) without introducing goto, unless it can be replaced with a keyword (return/continue) + if (!CanDuplicateExit(block.Instructions.Last(), continueTarget, out var keywordExit)) + return; + + context.Step("Replace leave with keyword exit", ifInst.TrueInst); + block.Instructions.Last().ReplaceWith(keywordExit.Clone()); + } + + ConditionDetection.InvertIf(block, ifInst, context); } /// @@ -159,16 +201,27 @@ namespace ICSharpCode.Decompiler.IL // if (cond) { ... } exit; if (ifInst.FalseInst.MatchNop()) { - // a separate heuristic tp ShouldReduceNesting as there is visual balancing to be performed based on number of statments + // a separate heuristic to ShouldReduceNesting as there is visual balancing to be performed based on number of statments if (maxDepth < 2) return false; // -> // if (!cond) exit; // ...; exit; - EnsureEndPointUnreachable(ifInst.TrueInst, exitInst); EnsureEndPointUnreachable(block, exitInst); + Debug.Assert(ifInst == block.Instructions.SecondToLastOrDefault()); + + // use the same exit the block has. If the block already has one (such as a leave from a try), keep it in place + EnsureEndPointUnreachable(ifInst.TrueInst, block.Instructions.Last()); ConditionDetection.InvertIf(block, ifInst, context); + + // ensure the exit inst of the if instruction is a keyword + Debug.Assert(!(ifInst.TrueInst is Block)); + if (!ifInst.TrueInst.Match(exitInst).Success) { + Debug.Assert(ifInst.TrueInst is Leave); + context.Step("Replace leave with keyword exit", ifInst.TrueInst); + ifInst.TrueInst.ReplaceWith(exitInst.Clone()); + } return true; } @@ -292,8 +345,37 @@ namespace ICSharpCode.Decompiler.IL /// /// Checks if an exit is a duplicable keyword exit (return; break; continue;) /// - private bool CanDuplicateExit(ILInstruction exit, Block continueTarget) => - exit != null && (exit is Leave leave && leave.Value.MatchNop() || exit.MatchBranch(continueTarget)); + private bool CanDuplicateExit(ILInstruction exit, Block continueTarget, out ILInstruction keywordExit) + { + keywordExit = exit; + if (exit != null && exit.MatchBranch(continueTarget)) + return true; // keyword is continue + + if (!(exit is Leave leave && leave.Value.MatchNop())) + return false; // don't duplicate valued returns + + if (leave.IsLeavingFunction || leave.TargetContainer.Kind != ContainerKind.Normal) + return true; // keyword is return || break + + // leave from a try/pinned/lock etc, check if the target (the instruction following the target container) is duplicable, if so, set keywordExit to that + ILInstruction leavingInst = leave.TargetContainer; + Debug.Assert(!leavingInst.HasFlag(InstructionFlags.EndPointUnreachable)); + while (!(leavingInst.Parent is Block b) || leavingInst == b.Instructions.Last()) { + // cannot duplicate leaves from finally containers + if (leavingInst.Parent is TryFinally tryFinally && leavingInst.SlotInfo == TryFinally.FinallyBlockSlot) { + Debug.Assert(leave.TargetContainer == tryFinally.FinallyBlock); //finally cannot have control flow + return false; + } + + leavingInst = leavingInst.Parent; + Debug.Assert(!leavingInst.HasFlag(InstructionFlags.EndPointUnreachable)); + Debug.Assert(!(leavingInst is ILFunction)); + } + + var block = (Block)leavingInst.Parent; + var targetInst = block.Instructions[block.Instructions.IndexOf(leavingInst)+1]; + return CanDuplicateExit(targetInst, continueTarget, out keywordExit); + } /// /// Ensures the end point of a block is unreachable by duplicating and appending the [exit] instruction following the end point @@ -353,28 +435,54 @@ namespace ICSharpCode.Decompiler.IL /// /// Recursively computes the number of statements and maximum nested depth of an instruction /// - private void ComputeStats(ILInstruction inst, ref int numStatements, ref int maxDepth, int currentDepth) + private void ComputeStats(ILInstruction inst, ref int numStatements, ref int maxDepth, int currentDepth, bool isStatement = true) { + if (isStatement) + numStatements++; + + if (currentDepth > maxDepth) { + Debug.Assert(isStatement); + maxDepth = currentDepth; + } + + // enumerate children statements and containers switch (inst) { case Block block: - foreach (var i in block.Instructions) - ComputeStats(i, ref numStatements, ref maxDepth, currentDepth); + if (isStatement) + numStatements--; // don't count blocks as statements + + // add each child as a statement (unless we're a named block) + foreach (var child in block.Instructions) + ComputeStats(child, ref numStatements, ref maxDepth, currentDepth, block.Kind != BlockKind.CallWithNamedArgs && block.Kind != BlockKind.CallInlineAssign); + + // final instruction as an expression + ComputeStats(block.FinalInstruction, ref numStatements, ref maxDepth, currentDepth, false); break; case BlockContainer container: - numStatements++; // one statement for the container head (switch/loop) + if (!isStatement) + numStatements++; //always add a statement for a container in an expression var containerBody = container.EntryPoint; if (container.Kind == ContainerKind.For || container.Kind == ContainerKind.While) { + Debug.Assert(isStatement); + if (!container.MatchConditionBlock(container.EntryPoint, out _, out containerBody)) throw new NotSupportedException("Invalid condition block in loop."); } + // don't count implicit leave. Can't avoid counting for loop initializers but close enough, for loops can have an extra statement of visual weight + var lastInst = containerBody.Instructions.Last(); + if ((container.Kind == ContainerKind.For || container.Kind == ContainerKind.DoWhile) && lastInst.MatchBranch(container.Blocks.Last()) || + (container.Kind == ContainerKind.Loop || container.Kind == ContainerKind.While) && lastInst.MatchBranch(container.Blocks[0]) || + container.Kind == ContainerKind.Normal && lastInst.MatchLeave(container) || + container.Kind == ContainerKind.Switch) // SwitchInstructyion always counts as a statement anyway, so no need to count the container as well + numStatements--; + // add the nested body ComputeStats(containerBody, ref numStatements, ref maxDepth, currentDepth + 1); break; - case IfInstruction ifInst: - numStatements++; // one statement for the if/condition itself - + case IfInstruction ifInst when ifInst.ResultType == StackType.Void: + Debug.Assert(isStatement); // nested then instruction ComputeStats(ifInst.TrueInst, ref numStatements, ref maxDepth, currentDepth + 1); @@ -389,21 +497,36 @@ namespace ICSharpCode.Decompiler.IL // include all nested else instruction ComputeStats(elseInst, ref numStatements, ref maxDepth, currentDepth + 1); break; - case SwitchInstruction switchInst: - // one statement per case label - numStatements += switchInst.Sections.Count + 1; + case SwitchSection section: + Debug.Assert(!isStatement); // labels are just children of the SwitchInstruction + numStatements++; // add a statement for each case label + // add all the case blocks at the current depth // most formatters indent switch blocks twice, but we don't want this heuristic to be based on formatting // so we remain conservative and only include the increase in depth from the container and not the labels - foreach (var section in switchInst.Sections) - if (section.Body.MatchBranch(out var caseBlock) && caseBlock.Parent == switchInst.Parent.Parent) - ComputeStats(caseBlock, ref numStatements, ref maxDepth, currentDepth); + if (section.Body.MatchBranch(out var caseBlock) && caseBlock.Parent == section.Parent.Parent.Parent) + ComputeStats(caseBlock, ref numStatements, ref maxDepth, currentDepth); + break; + case ILFunction func: + Debug.Assert(!isStatement); + + int bodyStatements = 0; + int bodyMaxDepth = maxDepth; + ComputeStats(func.Body, ref bodyStatements, ref bodyMaxDepth, currentDepth); + if (bodyStatements >= 2) { // don't count inline functions + numStatements += bodyStatements; + maxDepth = bodyMaxDepth; + } break; default: - // just a regular statement - numStatements++; - if (currentDepth > maxDepth) - maxDepth = currentDepth; + // search each child instruction. Containers will contain statements and contribute to stats + int subStatements = 0; + foreach (var child in inst.Children) + ComputeStats(child, ref subStatements, ref maxDepth, currentDepth, false); + + numStatements += subStatements; + if (isStatement && subStatements > 0) + numStatements--; // don't count the first container, only its contents, because this statement is already counted break; } } From 967aed2667fb7325f992458d66e966d4c192984f Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Fri, 5 Jun 2020 17:39:29 +0200 Subject: [PATCH 02/10] Add an option for new SDK style project format --- ICSharpCode.Decompiler/DecompilerSettings.cs | 18 ++++++++++++++++++ ILSpy/Properties/Resources.Designer.cs | 9 +++++++++ ILSpy/Properties/Resources.resx | 3 +++ ILSpy/Properties/Resources.zh-Hans.resx | 3 +++ 4 files changed, 33 insertions(+) diff --git a/ICSharpCode.Decompiler/DecompilerSettings.cs b/ICSharpCode.Decompiler/DecompilerSettings.cs index 18efc74a0..8dd738273 100644 --- a/ICSharpCode.Decompiler/DecompilerSettings.cs +++ b/ICSharpCode.Decompiler/DecompilerSettings.cs @@ -1329,6 +1329,24 @@ namespace ICSharpCode.Decompiler } } + bool useSdkStyleProjectFormat = true; + + /// + /// Gets or sets a value indicating whether the new SDK style format + /// shall be used for the generated project files. + /// + [Category("DecompilerSettings.Other")] + [Description("DecompilerSettings.UseSdkStyleProjectFormat")] + public bool UseSdkStyleProjectFormat { + get { return useSdkStyleProjectFormat; } + set { + if (useSdkStyleProjectFormat != value) { + useSdkStyleProjectFormat = value; + OnPropertyChanged(); + } + } + } + CSharpFormattingOptions csharpFormattingOptions; [Browsable(false)] diff --git a/ILSpy/Properties/Resources.Designer.cs b/ILSpy/Properties/Resources.Designer.cs index fd8ab2914..b67a9aaba 100644 --- a/ILSpy/Properties/Resources.Designer.cs +++ b/ILSpy/Properties/Resources.Designer.cs @@ -1109,6 +1109,15 @@ namespace ICSharpCode.ILSpy.Properties { } } + /// + /// Looks up a localized string similar to Use new SDK style format for generated project files (*.csproj). + /// + public static string DecompilerSettings_UseSdkStyleProjectFormat { + get { + return ResourceManager.GetString("DecompilerSettings.UseSdkStyleProjectFormat", resourceCulture); + } + } + /// /// Looks up a localized string similar to Use stackalloc initializer syntax. /// diff --git a/ILSpy/Properties/Resources.resx b/ILSpy/Properties/Resources.resx index 79feebc20..673ad4223 100644 --- a/ILSpy/Properties/Resources.resx +++ b/ILSpy/Properties/Resources.resx @@ -870,4 +870,7 @@ Do you want to continue? New Tab + + Use new SDK style format for generated project files (*.csproj) + \ No newline at end of file diff --git a/ILSpy/Properties/Resources.zh-Hans.resx b/ILSpy/Properties/Resources.zh-Hans.resx index 0540d4faf..d53064059 100644 --- a/ILSpy/Properties/Resources.zh-Hans.resx +++ b/ILSpy/Properties/Resources.zh-Hans.resx @@ -732,4 +732,7 @@ 关于 + + 使用新的 SDK 格式 (*.csproj) 生成项目文件 + \ No newline at end of file From 48be6267f32d10a796e0f99705b380d52ed9d183 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Fri, 5 Jun 2020 21:08:43 +0200 Subject: [PATCH 03/10] Replace tuples with value tuples --- .../CSharp/WholeProjectDecompiler.cs | 24 +++++++++---------- ILSpy/Languages/CSharpLanguage.cs | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs index 785841255..5bae9c990 100644 --- a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs @@ -128,7 +128,7 @@ namespace ICSharpCode.Decompiler.CSharp } #region WriteProjectFile - ProjectId WriteProjectFile(TextWriter writer, IEnumerable> files, Metadata.PEFile module) + ProjectId WriteProjectFile(TextWriter writer, IEnumerable<(string itemType, string fileName)> files, Metadata.PEFile module) { const string ns = "http://schemas.microsoft.com/developer/msbuild/2003"; string platformName = GetPlatformName(module); @@ -238,7 +238,7 @@ namespace ICSharpCode.Decompiler.CSharp } w.WriteEndElement(); // (References) - foreach (IGrouping gr in (from f in files group f.Item2 by f.Item1 into g orderby g.Key select g)) { + foreach (IGrouping gr in (from f in files group f.fileName by f.itemType into g orderby g.Key select g)) { w.WriteStartElement("ItemGroup"); foreach (string file in gr.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)) { w.WriteStartElement(gr.Key); @@ -340,7 +340,7 @@ namespace ICSharpCode.Decompiler.CSharp return decompiler; } - IEnumerable> WriteAssemblyInfo(DecompilerTypeSystem ts, CancellationToken cancellationToken) + IEnumerable<(string itemType, string fileName)> WriteAssemblyInfo(DecompilerTypeSystem ts, CancellationToken cancellationToken) { var decompiler = CreateDecompiler(ts); decompiler.CancellationToken = cancellationToken; @@ -354,10 +354,10 @@ namespace ICSharpCode.Decompiler.CSharp using (StreamWriter w = new StreamWriter(Path.Combine(targetDirectory, assemblyInfo))) { syntaxTree.AcceptVisitor(new CSharpOutputVisitor(w, settings.CSharpFormattingOptions)); } - return new Tuple[] { Tuple.Create("Compile", assemblyInfo) }; + return new[] { ("Compile", assemblyInfo) }; } - IEnumerable> WriteCodeFilesInProject(Metadata.PEFile module, CancellationToken cancellationToken) + IEnumerable<(string itemType, string fileName)> WriteCodeFilesInProject(Metadata.PEFile module, CancellationToken cancellationToken) { var metadata = module.Metadata; var files = module.Metadata.GetTopLevelTypeDefinitions().Where(td => IncludeTypeWhenDecompilingProject(module, td)).GroupBy( @@ -395,12 +395,12 @@ namespace ICSharpCode.Decompiler.CSharp } progress?.Report(new DecompilationProgress(total, file.Key)); }); - return files.Select(f => Tuple.Create("Compile", f.Key)).Concat(WriteAssemblyInfo(ts, cancellationToken)); + return files.Select(f => ("Compile", f.Key)).Concat(WriteAssemblyInfo(ts, cancellationToken)); } #endregion #region WriteResourceFilesInProject - protected virtual IEnumerable> WriteResourceFilesInProject(Metadata.PEFile module) + protected virtual IEnumerable<(string itemType, string fileName)> WriteResourceFilesInProject(Metadata.PEFile module) { foreach (var r in module.Resources.Where(r => r.ResourceType == Metadata.ResourceType.Embedded)) { Stream stream = r.TryOpenStream(); @@ -408,7 +408,7 @@ namespace ICSharpCode.Decompiler.CSharp if (r.Name.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) { bool decodedIntoIndividualFiles; - var individualResources = new List>(); + var individualResources = new List<(string itemType, string fileName)>(); try { var resourcesFile = new ResourcesFile(stream); if (resourcesFile.AllEntriesAreStreams()) { @@ -449,12 +449,12 @@ namespace ICSharpCode.Decompiler.CSharp stream.Position = 0; stream.CopyTo(fs); } - yield return Tuple.Create("EmbeddedResource", fileName); + yield return ("EmbeddedResource", fileName); } } } - protected virtual IEnumerable> WriteResourceToFile(string fileName, string resourceName, Stream entryStream) + protected virtual IEnumerable<(string itemType, string fileName)> WriteResourceToFile(string fileName, string resourceName, Stream entryStream) { if (fileName.EndsWith(".resources", StringComparison.OrdinalIgnoreCase)) { string resx = Path.ChangeExtension(fileName, ".resx"); @@ -465,7 +465,7 @@ namespace ICSharpCode.Decompiler.CSharp writer.AddResource(entry.Key, entry.Value); } } - return new[] { Tuple.Create("EmbeddedResource", resx) }; + return new[] { ("EmbeddedResource", resx) }; } catch (BadImageFormatException) { // if the .resources can't be decoded, just save them as-is } catch (EndOfStreamException) { @@ -475,7 +475,7 @@ namespace ICSharpCode.Decompiler.CSharp using (FileStream fs = new FileStream(Path.Combine(targetDirectory, fileName), FileMode.Create, FileAccess.Write)) { entryStream.CopyTo(fs); } - return new[] { Tuple.Create("EmbeddedResource", fileName) }; + return new[] { ("EmbeddedResource", fileName) }; } string GetFileNameForResource(string fullName) diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 4f40c308d..14343eaa4 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -445,13 +445,13 @@ namespace ICSharpCode.ILSpy base.DebugInfoProvider = assembly.GetDebugInfoOrNull(); } - protected override IEnumerable> WriteResourceToFile(string fileName, string resourceName, Stream entryStream) + protected override IEnumerable<(string itemType, string fileName)> WriteResourceToFile(string fileName, string resourceName, Stream entryStream) { foreach (var handler in App.ExportProvider.GetExportedValues()) { if (handler.CanHandle(fileName, options)) { entryStream.Position = 0; fileName = handler.WriteResourceToFile(assembly, fileName, entryStream, options); - return new[] { Tuple.Create(handler.EntryType, fileName) }; + return new[] { (handler.EntryType, fileName) }; } } return base.WriteResourceToFile(fileName, resourceName, entryStream); From a952cda502a26865c035df8f18d12287991dcae9 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 6 Jun 2020 21:38:24 +0200 Subject: [PATCH 04/10] Create dedicated namespace for project decompiler --- ICSharpCode.Decompiler.Console/IlspyCmdProgram.cs | 1 + ICSharpCode.Decompiler.PowerShell/GetDecompiledProjectCmdlet.cs | 1 + ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs | 1 + .../CSharp/{ => ProjectDecompiler}/WholeProjectDecompiler.cs | 2 +- ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs | 1 + ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj | 2 +- ILSpy/Languages/CSharpLanguage.cs | 1 + ILSpy/TextView/DecompilerTextView.cs | 2 +- 8 files changed, 8 insertions(+), 3 deletions(-) rename ICSharpCode.Decompiler/CSharp/{ => ProjectDecompiler}/WholeProjectDecompiler.cs (99%) diff --git a/ICSharpCode.Decompiler.Console/IlspyCmdProgram.cs b/ICSharpCode.Decompiler.Console/IlspyCmdProgram.cs index 74013f342..8f409a956 100644 --- a/ICSharpCode.Decompiler.Console/IlspyCmdProgram.cs +++ b/ICSharpCode.Decompiler.Console/IlspyCmdProgram.cs @@ -13,6 +13,7 @@ using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using ICSharpCode.Decompiler.DebugInfo; using ICSharpCode.Decompiler.PdbProvider; +using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; // ReSharper disable All namespace ICSharpCode.Decompiler.Console diff --git a/ICSharpCode.Decompiler.PowerShell/GetDecompiledProjectCmdlet.cs b/ICSharpCode.Decompiler.PowerShell/GetDecompiledProjectCmdlet.cs index dadb0f8f4..70ecdc439 100644 --- a/ICSharpCode.Decompiler.PowerShell/GetDecompiledProjectCmdlet.cs +++ b/ICSharpCode.Decompiler.PowerShell/GetDecompiledProjectCmdlet.cs @@ -5,6 +5,7 @@ using System.Management.Automation; using System.Threading; using System.Threading.Tasks; using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; using ICSharpCode.Decompiler.Metadata; namespace ICSharpCode.Decompiler.PowerShell diff --git a/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs b/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs index fa58d103e..92e7a16c9 100644 --- a/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs +++ b/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs @@ -24,6 +24,7 @@ using System.Reflection.PortableExecutable; using System.Text.RegularExpressions; using System.Threading; using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Tests.Helpers; using Microsoft.Build.Locator; diff --git a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs similarity index 99% rename from ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs rename to ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs index 5bae9c990..1dfa9020b 100644 --- a/ICSharpCode.Decompiler/CSharp/WholeProjectDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs @@ -37,7 +37,7 @@ using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Solution; using ICSharpCode.Decompiler.DebugInfo; -namespace ICSharpCode.Decompiler.CSharp +namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler { /// /// Decompiles an assembly into a visual studio project file. diff --git a/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs b/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs index 64eda349e..28038144a 100644 --- a/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs +++ b/ICSharpCode.Decompiler/DebugInfo/PortablePdbWriter.cs @@ -30,6 +30,7 @@ using System.Security.Cryptography; using System.Text; using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.CSharp.OutputVisitor; +using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.IL; using ICSharpCode.Decompiler.Metadata; diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 3d735ec42..a8a940e6d 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -277,7 +277,7 @@ - + diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 14343eaa4..7fd6fe6cb 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -32,6 +32,7 @@ using ICSharpCode.AvalonEdit.Utils; using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.CSharp.OutputVisitor; +using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Transforms; using ICSharpCode.Decompiler.Metadata; diff --git a/ILSpy/TextView/DecompilerTextView.cs b/ILSpy/TextView/DecompilerTextView.cs index e26704dde..3e4e8d0b4 100644 --- a/ILSpy/TextView/DecompilerTextView.cs +++ b/ILSpy/TextView/DecompilerTextView.cs @@ -45,8 +45,8 @@ using ICSharpCode.AvalonEdit.Highlighting.Xshd; using ICSharpCode.AvalonEdit.Rendering; using ICSharpCode.AvalonEdit.Search; using ICSharpCode.Decompiler; -using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.CSharp.OutputVisitor; +using ICSharpCode.Decompiler.CSharp.ProjectDecompiler; using ICSharpCode.Decompiler.Documentation; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Output; From b492a20442c1d0efd03f77a92849e67aaffcca73 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 6 Jun 2020 21:59:58 +0200 Subject: [PATCH 05/10] Move assembly resolution logic to better place WholeProjectDecompiler shall not care about checking whether an assembly is in GAC. --- .../IlspyCmdProgram.cs | 4 +- .../GetDecompiledProjectCmdlet.cs | 4 +- .../ICSharpCode.Decompiler.Tests.csproj | 1 + .../RoundtripAssembly.cs | 28 +++----- .../TestAssemblyResolver.cs | 28 ++++++++ .../WholeProjectDecompiler.cs | 70 +++++++++++-------- .../Metadata/AssemblyReferences.cs | 1 + .../Metadata/UniversalAssemblyResolver.cs | 5 ++ ILSpy/Languages/CSharpLanguage.cs | 4 +- ILSpy/LoadedAssembly.cs | 5 ++ 10 files changed, 96 insertions(+), 54 deletions(-) create mode 100644 ICSharpCode.Decompiler.Tests/TestAssemblyResolver.cs diff --git a/ICSharpCode.Decompiler.Console/IlspyCmdProgram.cs b/ICSharpCode.Decompiler.Console/IlspyCmdProgram.cs index 8f409a956..684933a25 100644 --- a/ICSharpCode.Decompiler.Console/IlspyCmdProgram.cs +++ b/ICSharpCode.Decompiler.Console/IlspyCmdProgram.cs @@ -176,14 +176,12 @@ Remarks: int DecompileAsProject(string assemblyFileName, string outputDirectory) { - var decompiler = new WholeProjectDecompiler() { Settings = GetSettings() }; var module = new PEFile(assemblyFileName); var resolver = new UniversalAssemblyResolver(assemblyFileName, false, module.Reader.DetectTargetFrameworkId()); foreach (var path in ReferencePaths) { resolver.AddSearchDirectory(path); } - decompiler.AssemblyResolver = resolver; - decompiler.DebugInfoProvider = TryLoadPDB(module); + var decompiler = new WholeProjectDecompiler(GetSettings(), resolver, TryLoadPDB(module)); decompiler.DecompileProject(module, outputDirectory); return 0; } diff --git a/ICSharpCode.Decompiler.PowerShell/GetDecompiledProjectCmdlet.cs b/ICSharpCode.Decompiler.PowerShell/GetDecompiledProjectCmdlet.cs index 70ecdc439..736f59933 100644 --- a/ICSharpCode.Decompiler.PowerShell/GetDecompiledProjectCmdlet.cs +++ b/ICSharpCode.Decompiler.PowerShell/GetDecompiledProjectCmdlet.cs @@ -78,9 +78,9 @@ namespace ICSharpCode.Decompiler.PowerShell private void DoDecompile(string path) { - WholeProjectDecompiler decompiler = new WholeProjectDecompiler(); PEFile module = Decompiler.TypeSystem.MainModule.PEFile; - decompiler.AssemblyResolver = new UniversalAssemblyResolver(module.FileName, false, module.Reader.DetectTargetFrameworkId()); + var assemblyResolver = new UniversalAssemblyResolver(module.FileName, false, module.Reader.DetectTargetFrameworkId()); + WholeProjectDecompiler decompiler = new WholeProjectDecompiler(assemblyResolver); decompiler.ProgressIndicator = this; fileName = module.FileName; completed = 0; diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index b409376e0..4aaec9f79 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -88,6 +88,7 @@ + diff --git a/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs b/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs index 92e7a16c9..2139269c9 100644 --- a/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs +++ b/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs @@ -148,16 +148,19 @@ namespace ICSharpCode.Decompiler.Tests Stopwatch w = Stopwatch.StartNew(); using (var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read)) { PEFile module = new PEFile(file, fileStream, PEStreamOptions.PrefetchEntireImage); - var resolver = new UniversalAssemblyResolver(file, false, module.Reader.DetectTargetFrameworkId(), PEStreamOptions.PrefetchMetadata); + var resolver = new TestAssemblyResolver(file, inputDir, module.Reader.DetectTargetFrameworkId()); resolver.AddSearchDirectory(inputDir); resolver.RemoveSearchDirectory("."); - var decompiler = new TestProjectDecompiler(inputDir); - decompiler.AssemblyResolver = resolver; + + // use a fixed GUID so that we can diff the output between different ILSpy runs without spurious changes + var projectGuid = Guid.Parse("{127C83E4-4587-4CF9-ADCA-799875F3DFE6}"); + // Let's limit the roundtrip tests to C# 7.3 for now; because 8.0 is still in preview // and the generated project doesn't build as-is. - decompiler.Settings = new DecompilerSettings(LanguageVersion.CSharp7_3); - // use a fixed GUID so that we can diff the output between different ILSpy runs without spurious changes - decompiler.ProjectGuid = Guid.Parse("{127C83E4-4587-4CF9-ADCA-799875F3DFE6}"); + var settings = new DecompilerSettings(LanguageVersion.CSharp7_3); + + var decompiler = new TestProjectDecompiler(projectGuid, resolver, settings); + if (snkFilePath != null) { decompiler.StrongNameKeyFile = Path.Combine(inputDir, snkFilePath); } @@ -260,18 +263,9 @@ namespace ICSharpCode.Decompiler.Tests class TestProjectDecompiler : WholeProjectDecompiler { - readonly string[] localAssemblies; - - public TestProjectDecompiler(string baseDir) - { - localAssemblies = new DirectoryInfo(baseDir).EnumerateFiles("*.dll").Select(f => f.FullName).ToArray(); - } - - protected override bool IsGacAssembly(IAssemblyReference r, PEFile asm) + public TestProjectDecompiler(Guid projecGuid, IAssemblyResolver resolver, DecompilerSettings settings) + : base(settings, projecGuid, resolver, debugInfoProvider: null) { - if (asm == null) - return false; - return !localAssemblies.Contains(asm.FileName); } } diff --git a/ICSharpCode.Decompiler.Tests/TestAssemblyResolver.cs b/ICSharpCode.Decompiler.Tests/TestAssemblyResolver.cs new file mode 100644 index 000000000..d97c6d526 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/TestAssemblyResolver.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; +using System.Reflection.PortableExecutable; +using ICSharpCode.Decompiler.Metadata; + +namespace ICSharpCode.Decompiler.Tests +{ + sealed class TestAssemblyResolver : UniversalAssemblyResolver + { + readonly HashSet localAssemblies = new HashSet(); + + public TestAssemblyResolver(string mainAssemblyFileName, string baseDir, string targetFramework) + : base(mainAssemblyFileName, false, targetFramework, PEStreamOptions.PrefetchMetadata, MetadataReaderOptions.ApplyWindowsRuntimeProjections) + { + var assemblyNames = new DirectoryInfo(baseDir).EnumerateFiles("*.dll").Select(f => Path.GetFileNameWithoutExtension(f.Name)); + foreach (var name in assemblyNames) { + localAssemblies.Add(name); + } + } + + public override bool IsGacAssembly(IAssemblyReference reference) + { + return reference != null && !localAssemblies.Contains(reference.Name); + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs index 1dfa9020b..5e024b790 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs @@ -45,18 +45,10 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler public class WholeProjectDecompiler { #region Settings - DecompilerSettings settings = new DecompilerSettings(); - - public DecompilerSettings Settings { - get { - return settings; - } - set { - if (value == null) - throw new ArgumentNullException(); - settings = value; - } - } + /// + /// Gets the setting this instance uses for decompiling. + /// + public DecompilerSettings Settings { get; } LanguageVersion? languageVersion; @@ -71,15 +63,14 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler } } - public IAssemblyResolver AssemblyResolver { get; set; } + public IAssemblyResolver AssemblyResolver { get; } - public IDebugInfoProvider DebugInfoProvider { get; set; } + public IDebugInfoProvider DebugInfoProvider { get; } /// /// The MSBuild ProjectGuid to use for the new project. - /// null to automatically generate a new GUID. /// - public Guid? ProjectGuid { get; set; } + public Guid ProjectGuid { get; } /// /// Path to the snk file to use for signing. @@ -92,6 +83,31 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler public IProgress ProgressIndicator { get; set; } #endregion + public WholeProjectDecompiler(IAssemblyResolver assemblyResolver) + : this(new DecompilerSettings(), assemblyResolver, debugInfoProvider: null) + { + } + + public WholeProjectDecompiler( + DecompilerSettings settings, + IAssemblyResolver assemblyResolver, + IDebugInfoProvider debugInfoProvider) + : this(settings, Guid.NewGuid(), assemblyResolver, debugInfoProvider) + { + } + + protected WholeProjectDecompiler( + DecompilerSettings settings, + Guid projectGuid, + IAssemblyResolver assemblyResolver, + IDebugInfoProvider debugInfoProvider) + { + Settings = settings ?? throw new ArgumentNullException(nameof(settings)); + ProjectGuid = projectGuid; + AssemblyResolver = assemblyResolver ?? throw new ArgumentNullException(nameof(assemblyResolver)); + DebugInfoProvider = debugInfoProvider; + } + // per-run members HashSet directories = new HashSet(Platform.FileNameComparer); @@ -132,7 +148,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler { const string ns = "http://schemas.microsoft.com/developer/msbuild/2003"; string platformName = GetPlatformName(module); - Guid guid = this.ProjectGuid ?? Guid.NewGuid(); + Guid guid = this.ProjectGuid; var targetFramework = DetectTargetFramework(module); List typeGuids = new List(); @@ -228,7 +244,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler w.WriteStartElement("Reference"); w.WriteAttributeString("Include", r.Name); var asm = AssemblyResolver.Resolve(r); - if (!IsGacAssembly(r, asm)) { + if (!AssemblyResolver.IsGacAssembly(r)) { if (asm != null) { w.WriteElementString("HintPath", asm.FileName); } @@ -313,18 +329,14 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler return result; } - protected virtual bool IsGacAssembly(Metadata.IAssemblyReference r, Metadata.PEFile asm) - { - return false; - } #endregion #region WriteCodeFilesInProject - protected virtual bool IncludeTypeWhenDecompilingProject(Metadata.PEFile module, TypeDefinitionHandle type) + protected virtual bool IncludeTypeWhenDecompilingProject(PEFile module, TypeDefinitionHandle type) { var metadata = module.Metadata; var typeDef = metadata.GetTypeDefinition(type); - if (metadata.GetString(typeDef.Name) == "" || CSharpDecompiler.MemberIsHidden(module, type, settings)) + if (metadata.GetString(typeDef.Name) == "" || CSharpDecompiler.MemberIsHidden(module, type, Settings)) return false; if (metadata.GetString(typeDef.Namespace) == "XamlGeneratedNamespace" && metadata.GetString(typeDef.Name) == "GeneratedInternalTypeHelper") return false; @@ -333,7 +345,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler CSharpDecompiler CreateDecompiler(DecompilerTypeSystem ts) { - var decompiler = new CSharpDecompiler(ts, settings); + var decompiler = new CSharpDecompiler(ts, Settings); decompiler.DebugInfoProvider = DebugInfoProvider; decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers()); decompiler.AstTransforms.Add(new RemoveCLSCompliantAttribute()); @@ -352,7 +364,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler Directory.CreateDirectory(Path.Combine(targetDirectory, prop)); string assemblyInfo = Path.Combine(prop, "AssemblyInfo.cs"); using (StreamWriter w = new StreamWriter(Path.Combine(targetDirectory, assemblyInfo))) { - syntaxTree.AcceptVisitor(new CSharpOutputVisitor(w, settings.CSharpFormattingOptions)); + syntaxTree.AcceptVisitor(new CSharpOutputVisitor(w, Settings.CSharpFormattingOptions)); } return new[] { ("Compile", assemblyInfo) }; } @@ -374,8 +386,8 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler } }, StringComparer.OrdinalIgnoreCase).ToList(); int total = files.Count; - var progress = this.ProgressIndicator; - DecompilerTypeSystem ts = new DecompilerTypeSystem(module, AssemblyResolver, settings); + var progress = ProgressIndicator; + DecompilerTypeSystem ts = new DecompilerTypeSystem(module, AssemblyResolver, Settings); Parallel.ForEach( files, new ParallelOptions { @@ -388,7 +400,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler CSharpDecompiler decompiler = CreateDecompiler(ts); decompiler.CancellationToken = cancellationToken; var syntaxTree = decompiler.DecompileTypes(file.ToArray()); - syntaxTree.AcceptVisitor(new CSharpOutputVisitor(w, settings.CSharpFormattingOptions)); + syntaxTree.AcceptVisitor(new CSharpOutputVisitor(w, Settings.CSharpFormattingOptions)); } catch (Exception innerException) when (!(innerException is OperationCanceledException || innerException is DecompilerException)) { throw new DecompilerException(module, $"Error decompiling for '{file.Key}'", innerException); } diff --git a/ICSharpCode.Decompiler/Metadata/AssemblyReferences.cs b/ICSharpCode.Decompiler/Metadata/AssemblyReferences.cs index 22d154960..d066778af 100644 --- a/ICSharpCode.Decompiler/Metadata/AssemblyReferences.cs +++ b/ICSharpCode.Decompiler/Metadata/AssemblyReferences.cs @@ -46,6 +46,7 @@ namespace ICSharpCode.Decompiler.Metadata { PEFile Resolve(IAssemblyReference reference); PEFile ResolveModule(PEFile mainModule, string moduleName); + bool IsGacAssembly(IAssemblyReference reference); } public interface IAssemblyReference diff --git a/ICSharpCode.Decompiler/Metadata/UniversalAssemblyResolver.cs b/ICSharpCode.Decompiler/Metadata/UniversalAssemblyResolver.cs index 22fc24105..3186fa3d3 100644 --- a/ICSharpCode.Decompiler/Metadata/UniversalAssemblyResolver.cs +++ b/ICSharpCode.Decompiler/Metadata/UniversalAssemblyResolver.cs @@ -173,6 +173,11 @@ namespace ICSharpCode.Decompiler.Metadata return new PEFile(moduleFileName, new FileStream(moduleFileName, FileMode.Open, FileAccess.Read), streamOptions, metadataOptions); } + public virtual bool IsGacAssembly(IAssemblyReference reference) + { + return GetAssemblyInGac(reference) != null; + } + public string FindAssemblyFile(IAssemblyReference name) { if (name.IsWindowsRuntime) { diff --git a/ILSpy/Languages/CSharpLanguage.cs b/ILSpy/Languages/CSharpLanguage.cs index 7fd6fe6cb..52c7e4644 100644 --- a/ILSpy/Languages/CSharpLanguage.cs +++ b/ILSpy/Languages/CSharpLanguage.cs @@ -438,12 +438,10 @@ namespace ICSharpCode.ILSpy readonly DecompilationOptions options; public ILSpyWholeProjectDecompiler(LoadedAssembly assembly, DecompilationOptions options) + : base(options.DecompilerSettings, assembly.GetAssemblyResolver(), assembly.GetDebugInfoOrNull()) { this.assembly = assembly; this.options = options; - base.Settings = options.DecompilerSettings; - base.AssemblyResolver = assembly.GetAssemblyResolver(); - base.DebugInfoProvider = assembly.GetDebugInfoOrNull(); } protected override IEnumerable<(string itemType, string fileName)> WriteResourceToFile(string fileName, string resourceName, Stream entryStream) diff --git a/ILSpy/LoadedAssembly.cs b/ILSpy/LoadedAssembly.cs index 4ce66d2b5..fc0e1bcdc 100644 --- a/ILSpy/LoadedAssembly.cs +++ b/ILSpy/LoadedAssembly.cs @@ -253,6 +253,11 @@ namespace ICSharpCode.ILSpy this.parent = parent; } + public bool IsGacAssembly(IAssemblyReference reference) + { + return parent.universalResolver?.IsGacAssembly(reference) == true; + } + public PEFile Resolve(Decompiler.Metadata.IAssemblyReference reference) { return parent.LookupReferencedAssembly(reference)?.GetPEFileOrNull(); From fdef5d11c615cc61e738849156508669c495a7a9 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 6 Jun 2020 23:55:14 +0200 Subject: [PATCH 06/10] Rearrange project writing logic The WholeProjectDecompiler shall not have too many responsibilities. --- .../ICSharpCode.Decompiler.Tests.csproj | 1 + .../ProjectDecompiler/TargetFrameworkTests.cs | 92 +++++++ .../ProjectDecompiler/IProjectFileWriter.cs | 41 ++++ .../ProjectDecompiler/IProjectInfoProvider.cs | 49 ++++ .../ProjectFileWriterDefault.cs | 175 ++++++++++++++ .../ProjectDecompiler/TargetFramework.cs | 94 ++++++++ .../ProjectDecompiler/TargetServices.cs | 129 ++++++++++ .../WholeProjectDecompiler.cs | 226 +----------------- .../ICSharpCode.Decompiler.csproj | 5 + 9 files changed, 594 insertions(+), 218 deletions(-) create mode 100644 ICSharpCode.Decompiler.Tests/ProjectDecompiler/TargetFrameworkTests.cs create mode 100644 ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectFileWriter.cs create mode 100644 ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectInfoProvider.cs create mode 100644 ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterDefault.cs create mode 100644 ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetFramework.cs create mode 100644 ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj index 4aaec9f79..f14b1077e 100644 --- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj +++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj @@ -88,6 +88,7 @@ + diff --git a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/TargetFrameworkTests.cs b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/TargetFrameworkTests.cs new file mode 100644 index 000000000..9a3b8b124 --- /dev/null +++ b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/TargetFrameworkTests.cs @@ -0,0 +1,92 @@ +// Copyright (c) 2020 Daniel Grunwald +// +// 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 ICSharpCode.Decompiler.CSharp.ProjectDecompiler; +using NUnit.Framework; + +namespace ICSharpCode.Decompiler.Tests +{ + [TestFixture] + public sealed class TargetFrameworkTests + { + [TestCase(-1)] + [TestCase(0)] + [TestCase(1)] + [TestCase(99)] + [TestCase(int.MinValue)] + public void VerifyThrowsForInvalidVersion(int invalidVersion) + { + // Arrange - nothing + + // Act + void CreateInstance() => new TargetFramework(identifier: null, invalidVersion, profile: null); + + // Assert + Assert.Throws(CreateInstance); + } + + [TestCase(100, "v1.0")] + [TestCase(102, "v1.0.2")] + [TestCase(130, "v1.3")] + [TestCase(145, "v1.4.5")] + [TestCase(1670, "v16.7")] + [TestCase(1800, "v18.0")] + public void VerifyVersion(int version, string expectedVersion) + { + // Arrange - nothing + + // Act + var targetFramework = new TargetFramework(identifier: null, version, profile: null); + + // Assert + Assert.AreEqual(version, targetFramework.VersionNumber); + Assert.AreEqual(expectedVersion, targetFramework.VersionString); + } + + [Test] + public void VerifyPortableLibrary() + { + // Arrange + const string identifier = ".NETPortable"; + + // Act + var targetFramework = new TargetFramework(identifier, 100, profile: null); + + // Assert + Assert.IsTrue(targetFramework.IsPortableClassLibrary); + Assert.AreEqual(identifier, targetFramework.Identifier); + } + + [Test] + [Pairwise] + public void VerifyIdentifierAndProfile( + [Values(null, "", ".NETFramework")] string identifier, + [Values(null, "", ".Client")] string profile) + { + // Arrange - nothing + + // Act + var targetFramework = new TargetFramework(identifier, 100, profile); + + // Assert + Assert.AreEqual(identifier, targetFramework.Identifier); + Assert.AreEqual(profile, targetFramework.Profile); + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectFileWriter.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectFileWriter.cs new file mode 100644 index 000000000..9dbef8595 --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectFileWriter.cs @@ -0,0 +1,41 @@ +// Copyright (c) 2020 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.Collections.Generic; +using System.IO; +using ICSharpCode.Decompiler.Metadata; + +namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler +{ + /// + /// An interface for a service that creates and writes a project file structure + /// for a specific module being decompiled. + /// + interface IProjectFileWriter + { + /// + /// Writes the content of a new project file for the specified being decompiled. + /// + /// The target to write to. + /// The information about the project being created. + /// A collection of source files to be included into the project, each item is a pair + /// of the project entry type and the file path. + /// The module being decompiled. + void Write(TextWriter target, IProjectInfoProvider project, IEnumerable<(string itemType, string fileName)> files, PEFile module); + } +} diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectInfoProvider.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectInfoProvider.cs new file mode 100644 index 000000000..6eb47a102 --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/IProjectInfoProvider.cs @@ -0,0 +1,49 @@ +// Copyright (c) 2020 Daniel Grunwald +// +// 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 ICSharpCode.Decompiler.Metadata; + +namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler +{ + /// + /// An interface that provides common information for a project being decompiled to. + /// + interface IProjectInfoProvider + { + /// + /// Gets the assembly resolver active for the project. + /// + IAssemblyResolver AssemblyResolver { get; } + + /// + /// Gets the C# language version of the project. + /// + LanguageVersion LanguageVersion { get; } + + /// + /// Gets the unique ID of the project. + /// + Guid ProjectGuid { get; } + + /// + /// Gets the name of the key file being used for strong name signing. Can be null if no file is available. + /// + string StrongNameKeyFile { get; } + } +} \ No newline at end of file diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterDefault.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterDefault.cs new file mode 100644 index 000000000..6f21f0567 --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterDefault.cs @@ -0,0 +1,175 @@ +// Copyright (c) 2020 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.Reflection.PortableExecutable; +using System.Xml; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.Solution; + +namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler +{ + /// + /// A implementation that creates the projects in the default format. + /// + sealed class ProjectFileWriterDefault : IProjectFileWriter + { + /// + /// Creates a new instance of the class. + /// + /// A new instance of the class. + public static IProjectFileWriter Create() => new ProjectFileWriterDefault(); + + /// + public void Write( + TextWriter target, + IProjectInfoProvider project, + IEnumerable<(string itemType, string fileName)> files, + PEFile module) + { + const string ns = "http://schemas.microsoft.com/developer/msbuild/2003"; + string platformName = TargetServices.GetPlatformName(module); + var targetFramework = TargetServices.DetectTargetFramework(module); + + List typeGuids = new List(); + if (targetFramework.IsPortableClassLibrary) + typeGuids.Add(ProjectTypeGuids.PortableLibrary); + typeGuids.Add(ProjectTypeGuids.CSharpWindows); + + using (XmlTextWriter w = new XmlTextWriter(target)) { + w.Formatting = Formatting.Indented; + w.WriteStartDocument(); + w.WriteStartElement("Project", ns); + w.WriteAttributeString("ToolsVersion", "4.0"); + w.WriteAttributeString("DefaultTargets", "Build"); + + w.WriteStartElement("PropertyGroup"); + w.WriteElementString("ProjectGuid", project.ProjectGuid.ToString("B").ToUpperInvariant()); + w.WriteElementString("ProjectTypeGuids", string.Join(";", typeGuids.Select(g => g.ToString("B").ToUpperInvariant()))); + + w.WriteStartElement("Configuration"); + w.WriteAttributeString("Condition", " '$(Configuration)' == '' "); + w.WriteValue("Debug"); + w.WriteEndElement(); // + + w.WriteStartElement("Platform"); + w.WriteAttributeString("Condition", " '$(Platform)' == '' "); + w.WriteValue(platformName); + w.WriteEndElement(); // + + if (module.Reader.PEHeaders.IsDll) { + w.WriteElementString("OutputType", "Library"); + } else { + switch (module.Reader.PEHeaders.PEHeader.Subsystem) { + case Subsystem.WindowsGui: + w.WriteElementString("OutputType", "WinExe"); + break; + case Subsystem.WindowsCui: + w.WriteElementString("OutputType", "Exe"); + break; + default: + w.WriteElementString("OutputType", "Library"); + break; + } + } + + w.WriteElementString("LangVersion", project.LanguageVersion.ToString().Replace("CSharp", "").Replace('_', '.')); + + w.WriteElementString("AssemblyName", module.Name); + if (targetFramework.Identifier != null) + w.WriteElementString("TargetFrameworkIdentifier", targetFramework.Identifier); + if (targetFramework.VersionString != null) + w.WriteElementString("TargetFrameworkVersion", targetFramework.VersionString); + if (targetFramework.Profile != null) + w.WriteElementString("TargetFrameworkProfile", targetFramework.Profile); + w.WriteElementString("WarningLevel", "4"); + w.WriteElementString("AllowUnsafeBlocks", "True"); + + if (project.StrongNameKeyFile != null) { + w.WriteElementString("SignAssembly", "True"); + w.WriteElementString("AssemblyOriginatorKeyFile", Path.GetFileName(project.StrongNameKeyFile)); + } + + w.WriteEndElement(); // + + w.WriteStartElement("PropertyGroup"); // platform-specific + w.WriteAttributeString("Condition", " '$(Platform)' == '" + platformName + "' "); + w.WriteElementString("PlatformTarget", platformName); + if (targetFramework.VersionNumber > 400 && platformName == "AnyCPU" && (module.Reader.PEHeaders.CorHeader.Flags & CorFlags.Prefers32Bit) == 0) { + w.WriteElementString("Prefer32Bit", "false"); + } + w.WriteEndElement(); // (platform-specific) + + w.WriteStartElement("PropertyGroup"); // Debug + w.WriteAttributeString("Condition", " '$(Configuration)' == 'Debug' "); + w.WriteElementString("OutputPath", "bin\\Debug\\"); + w.WriteElementString("DebugSymbols", "true"); + w.WriteElementString("DebugType", "full"); + w.WriteElementString("Optimize", "false"); + w.WriteEndElement(); // (Debug) + + w.WriteStartElement("PropertyGroup"); // Release + w.WriteAttributeString("Condition", " '$(Configuration)' == 'Release' "); + w.WriteElementString("OutputPath", "bin\\Release\\"); + w.WriteElementString("DebugSymbols", "true"); + w.WriteElementString("DebugType", "pdbonly"); + w.WriteElementString("Optimize", "true"); + w.WriteEndElement(); // (Release) + + + w.WriteStartElement("ItemGroup"); // References + foreach (var r in module.AssemblyReferences) { + if (r.Name != "mscorlib") { + w.WriteStartElement("Reference"); + w.WriteAttributeString("Include", r.Name); + var asm = project.AssemblyResolver.Resolve(r); + if (asm != null && !project.AssemblyResolver.IsGacAssembly(r)) { + w.WriteElementString("HintPath", asm.FileName); + } + w.WriteEndElement(); + } + } + w.WriteEndElement(); // (References) + + foreach (IGrouping gr in from f in files group f.fileName by f.itemType into g orderby g.Key select g) { + w.WriteStartElement("ItemGroup"); + foreach (string file in gr.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)) { + w.WriteStartElement(gr.Key); + w.WriteAttributeString("Include", file); + w.WriteEndElement(); + } + w.WriteEndElement(); + } + if (targetFramework.IsPortableClassLibrary) { + w.WriteStartElement("Import"); + w.WriteAttributeString("Project", "$(MSBuildExtensionsPath32)\\Microsoft\\Portable\\$(TargetFrameworkVersion)\\Microsoft.Portable.CSharp.targets"); + w.WriteEndElement(); + } else { + w.WriteStartElement("Import"); + w.WriteAttributeString("Project", "$(MSBuildToolsPath)\\Microsoft.CSharp.targets"); + w.WriteEndElement(); + } + + w.WriteEndDocument(); + } + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetFramework.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetFramework.cs new file mode 100644 index 000000000..43e8642ee --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetFramework.cs @@ -0,0 +1,94 @@ +// Copyright (c) 2020 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.Text; + +namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler +{ + /// + /// A class describing the target framework of a module. + /// + sealed class TargetFramework + { + const string DotNetPortableIdentifier = ".NETPortable"; + + /// + /// Initializes a new instance of the class. + /// + /// The framework identifier string. Can be null. + /// The framework version string. Must be greater than 100 (where 100 corresponds to v1.0). + /// The framework profile. Can be null. + public TargetFramework(string identifier, int version, string profile) + { + if (version < 100) { + throw new ArgumentException("The version number must be greater than or equal to 100", nameof(version)); + } + + Identifier = identifier; + VersionNumber = version; + VersionString = "v" + GetVersionString(version); + Profile = profile; + IsPortableClassLibrary = identifier == DotNetPortableIdentifier; + } + + /// + /// Gets the target framework identifier. Can be null if not defined. + /// + public string Identifier { get; } + + /// + /// Gets the target framework version, e.g. "v4.5". + /// + public string VersionString { get; } + + /// + /// Gets the target framework version as integer (multiplied by 100), e.g. 450. + /// + public int VersionNumber { get; } + + /// + /// Gets the target framework profile. Can be null if not set or not available. + /// + public string Profile { get; } + + /// + /// Gets a value indicating whether the target is a portable class library (PCL). + /// + public bool IsPortableClassLibrary { get; } + + static string GetVersionString(int version) + { + int major = version / 100; + int minor = version % 100 / 10; + int patch = version % 10; + + var versionBuilder = new StringBuilder(8); + versionBuilder.Append(major); + versionBuilder.Append('.'); + versionBuilder.Append(minor); + + if (patch != 0) { + versionBuilder.Append('.'); + versionBuilder.Append(patch); + } + + return versionBuilder.ToString(); + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs new file mode 100644 index 000000000..94d1dc296 --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs @@ -0,0 +1,129 @@ +// Copyright (c) 2020 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.Linq; +using System.Reflection.PortableExecutable; +using ICSharpCode.Decompiler.Metadata; + +namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler +{ + /// + /// Helper services for determining the target framework and platform of a module. + /// + static class TargetServices + { + const string VersionToken = "Version="; + const string ProfileToken = "Profile="; + + /// + /// Gets the for the specified . + /// + /// The module to get the target framework description for. Cannot be null. + /// A new instance of the class that describes the specified . + /// + public static TargetFramework DetectTargetFramework(PEFile module) + { + if (module is null) { + throw new ArgumentNullException(nameof(module)); + } + + int versionNumber; + switch (module.GetRuntime()) { + case TargetRuntime.Net_1_0: + versionNumber = 100; + break; + + case TargetRuntime.Net_1_1: + versionNumber = 110; + break; + + case TargetRuntime.Net_2_0: + versionNumber = 200; + // TODO: Detect when .NET 3.0/3.5 is required + break; + + default: + versionNumber = 400; + break; + } + + string targetFrameworkIdentifier = null; + string targetFrameworkProfile = null; + + string targetFramework = module.DetectTargetFrameworkId(); + if (!string.IsNullOrEmpty(targetFramework)) { + string[] frameworkParts = targetFramework.Split(','); + 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) { + versionNumber = int.Parse(frameworkVersion.Substring(VersionToken.Length + 1).Replace(".", "")); + if (versionNumber < 100) versionNumber *= 10; + } + + string frameworkProfile = frameworkParts.FirstOrDefault(a => a.StartsWith(ProfileToken, StringComparison.OrdinalIgnoreCase)); + if (frameworkProfile != null) + targetFrameworkProfile = frameworkProfile.Substring(ProfileToken.Length); + } + + return new TargetFramework(targetFrameworkIdentifier, versionNumber, targetFrameworkProfile); + } + + /// + /// Gets the string representation (name) of the target platform of the specified . + /// + /// The module to get the target framework description for. Cannot be null. + /// The platform name, e.g. "AnyCPU" or "x86". + public static string GetPlatformName(PEFile module) + { + if (module is null) { + throw new ArgumentNullException(nameof(module)); + } + + var headers = module.Reader.PEHeaders; + var architecture = headers.CoffHeader.Machine; + var characteristics = headers.CoffHeader.Characteristics; + var corflags = headers.CorHeader.Flags; + + switch (architecture) { + case Machine.I386: + if ((corflags & CorFlags.Prefers32Bit) != 0) + return "AnyCPU"; + + if ((corflags & CorFlags.Requires32Bit) != 0) + return "x86"; + + // According to ECMA-335, II.25.3.3.1 CorFlags.Requires32Bit and Characteristics.Bit32Machine must be in sync + // for assemblies containing managed code. However, this is not true for C++/CLI assemblies. + if ((corflags & CorFlags.ILOnly) == 0 && (characteristics & Characteristics.Bit32Machine) != 0) + return "x86"; + return "AnyCPU"; + + case Machine.Amd64: + return "x64"; + + case Machine.IA64: + return "Itanium"; + + default: + return architecture.ToString(); + } + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs index 5e024b790..cc6b4503e 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs @@ -21,7 +21,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; -using System.Xml; using ICSharpCode.Decompiler.CSharp.OutputVisitor; using ICSharpCode.Decompiler.CSharp.Syntax; using ICSharpCode.Decompiler.CSharp.Transforms; @@ -29,9 +28,7 @@ using ICSharpCode.Decompiler.TypeSystem; using ICSharpCode.Decompiler.Util; using System.Threading; using System.Text; -using System.Reflection.PortableExecutable; using System.Reflection.Metadata; -using static ICSharpCode.Decompiler.Metadata.DotNetCorePathFinderExtensions; using static ICSharpCode.Decompiler.Metadata.MetadataExtensions; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Solution; @@ -42,7 +39,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler /// /// Decompiles an assembly into a visual studio project file. /// - public class WholeProjectDecompiler + public class WholeProjectDecompiler : IProjectInfoProvider { #region Settings /// @@ -106,6 +103,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler ProjectGuid = projectGuid; AssemblyResolver = assemblyResolver ?? throw new ArgumentNullException(nameof(assemblyResolver)); DebugInfoProvider = debugInfoProvider; + projectWriter = ProjectFileWriterDefault.Create(); } // per-run members @@ -120,6 +118,8 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler /// protected string targetDirectory; + readonly IProjectFileWriter projectWriter; + public void DecompileProject(PEFile moduleDefinition, string targetDirectory, CancellationToken cancellationToken = default(CancellationToken)) { string projectFileName = Path.Combine(targetDirectory, CleanUpFileName(moduleDefinition.Name) + ".csproj"); @@ -140,197 +140,13 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler if (StrongNameKeyFile != null) { File.Copy(StrongNameKeyFile, Path.Combine(targetDirectory, Path.GetFileName(StrongNameKeyFile))); } - return WriteProjectFile(projectFileWriter, files, moduleDefinition); - } - - #region WriteProjectFile - ProjectId WriteProjectFile(TextWriter writer, IEnumerable<(string itemType, string fileName)> files, Metadata.PEFile module) - { - const string ns = "http://schemas.microsoft.com/developer/msbuild/2003"; - string platformName = GetPlatformName(module); - Guid guid = this.ProjectGuid; - var targetFramework = DetectTargetFramework(module); - - List typeGuids = new List(); - if (targetFramework.IsPortableClassLibrary) - typeGuids.Add(ProjectTypeGuids.PortableLibrary); - typeGuids.Add(ProjectTypeGuids.CSharpWindows); - // TODO: .NET core support - - using (XmlTextWriter w = new XmlTextWriter(writer)) { - w.Formatting = Formatting.Indented; - w.WriteStartDocument(); - w.WriteStartElement("Project", ns); - w.WriteAttributeString("ToolsVersion", "4.0"); - w.WriteAttributeString("DefaultTargets", "Build"); - - w.WriteStartElement("PropertyGroup"); - w.WriteElementString("ProjectGuid", guid.ToString("B").ToUpperInvariant()); - w.WriteElementString("ProjectTypeGuids", string.Join(";", typeGuids.Select(g => g.ToString("B").ToUpperInvariant()))); - - w.WriteStartElement("Configuration"); - w.WriteAttributeString("Condition", " '$(Configuration)' == '' "); - w.WriteValue("Debug"); - w.WriteEndElement(); // - - w.WriteStartElement("Platform"); - w.WriteAttributeString("Condition", " '$(Platform)' == '' "); - w.WriteValue(platformName); - w.WriteEndElement(); // - - if (module.Reader.PEHeaders.IsDll) { - w.WriteElementString("OutputType", "Library"); - } else { - switch (module.Reader.PEHeaders.PEHeader.Subsystem) { - case Subsystem.WindowsGui: - w.WriteElementString("OutputType", "WinExe"); - break; - case Subsystem.WindowsCui: - w.WriteElementString("OutputType", "Exe"); - break; - default: - w.WriteElementString("OutputType", "Library"); - break; - } - } - - w.WriteElementString("LangVersion", LanguageVersion.ToString().Replace("CSharp", "").Replace('_', '.')); - - w.WriteElementString("AssemblyName", module.Name); - if (targetFramework.TargetFrameworkIdentifier != null) - w.WriteElementString("TargetFrameworkIdentifier", targetFramework.TargetFrameworkIdentifier); - if (targetFramework.TargetFrameworkVersion != null) - w.WriteElementString("TargetFrameworkVersion", targetFramework.TargetFrameworkVersion); - if (targetFramework.TargetFrameworkProfile != null) - w.WriteElementString("TargetFrameworkProfile", targetFramework.TargetFrameworkProfile); - w.WriteElementString("WarningLevel", "4"); - w.WriteElementString("AllowUnsafeBlocks", "True"); - - if (StrongNameKeyFile != null) { - w.WriteElementString("SignAssembly", "True"); - w.WriteElementString("AssemblyOriginatorKeyFile", Path.GetFileName(StrongNameKeyFile)); - } - w.WriteEndElement(); // - - w.WriteStartElement("PropertyGroup"); // platform-specific - w.WriteAttributeString("Condition", " '$(Platform)' == '" + platformName + "' "); - w.WriteElementString("PlatformTarget", platformName); - if (targetFramework.VersionNumber > 400 && platformName == "AnyCPU" && (module.Reader.PEHeaders.CorHeader.Flags & CorFlags.Prefers32Bit) == 0) { - w.WriteElementString("Prefer32Bit", "false"); - } - w.WriteEndElement(); // (platform-specific) - - w.WriteStartElement("PropertyGroup"); // Debug - w.WriteAttributeString("Condition", " '$(Configuration)' == 'Debug' "); - w.WriteElementString("OutputPath", "bin\\Debug\\"); - w.WriteElementString("DebugSymbols", "true"); - w.WriteElementString("DebugType", "full"); - w.WriteElementString("Optimize", "false"); - w.WriteEndElement(); // (Debug) - - w.WriteStartElement("PropertyGroup"); // Release - w.WriteAttributeString("Condition", " '$(Configuration)' == 'Release' "); - w.WriteElementString("OutputPath", "bin\\Release\\"); - w.WriteElementString("DebugSymbols", "true"); - w.WriteElementString("DebugType", "pdbonly"); - w.WriteElementString("Optimize", "true"); - w.WriteEndElement(); // (Release) - - - w.WriteStartElement("ItemGroup"); // References - foreach (var r in module.AssemblyReferences) { - if (r.Name != "mscorlib") { - w.WriteStartElement("Reference"); - w.WriteAttributeString("Include", r.Name); - var asm = AssemblyResolver.Resolve(r); - if (!AssemblyResolver.IsGacAssembly(r)) { - if (asm != null) { - w.WriteElementString("HintPath", asm.FileName); - } - } - w.WriteEndElement(); - } - } - w.WriteEndElement(); // (References) - - foreach (IGrouping gr in (from f in files group f.fileName by f.itemType into g orderby g.Key select g)) { - w.WriteStartElement("ItemGroup"); - foreach (string file in gr.OrderBy(f => f, StringComparer.OrdinalIgnoreCase)) { - w.WriteStartElement(gr.Key); - w.WriteAttributeString("Include", file); - w.WriteEndElement(); - } - w.WriteEndElement(); - } - if (targetFramework.IsPortableClassLibrary) { - w.WriteStartElement("Import"); - w.WriteAttributeString("Project", "$(MSBuildExtensionsPath32)\\Microsoft\\Portable\\$(TargetFrameworkVersion)\\Microsoft.Portable.CSharp.targets"); - w.WriteEndElement(); - } else { - w.WriteStartElement("Import"); - w.WriteAttributeString("Project", "$(MSBuildToolsPath)\\Microsoft.CSharp.targets"); - w.WriteEndElement(); - } - - w.WriteEndDocument(); - } - - return new ProjectId(platformName, guid, ProjectTypeGuids.CSharpWindows); + projectWriter.Write(projectFileWriter, this, files, moduleDefinition); + + string platformName = TargetServices.GetPlatformName(moduleDefinition); + return new ProjectId(platformName, ProjectGuid, ProjectTypeGuids.CSharpWindows); } - struct TargetFramework - { - public string TargetFrameworkIdentifier; - public string TargetFrameworkVersion; - public string TargetFrameworkProfile; - public int VersionNumber; - public bool IsPortableClassLibrary => TargetFrameworkIdentifier == ".NETPortable"; - } - - private TargetFramework DetectTargetFramework(PEFile module) - { - TargetFramework result = default; - - switch (module.GetRuntime()) { - case Metadata.TargetRuntime.Net_1_0: - result.VersionNumber = 100; - result.TargetFrameworkVersion = "v1.0"; - break; - case Metadata.TargetRuntime.Net_1_1: - result.VersionNumber = 110; - result.TargetFrameworkVersion = "v1.1"; - break; - case Metadata.TargetRuntime.Net_2_0: - result.VersionNumber = 200; - result.TargetFrameworkVersion = "v2.0"; - // TODO: Detect when .NET 3.0/3.5 is required - break; - default: - result.VersionNumber = 400; - result.TargetFrameworkVersion = "v4.0"; - break; - } - - string targetFramework = module.DetectTargetFrameworkId(); - if (!string.IsNullOrEmpty(targetFramework)) { - string[] frameworkParts = targetFramework.Split(','); - result.TargetFrameworkIdentifier = frameworkParts.FirstOrDefault(a => !a.StartsWith("Version=", StringComparison.OrdinalIgnoreCase) && !a.StartsWith("Profile=", StringComparison.OrdinalIgnoreCase)); - string frameworkVersion = frameworkParts.FirstOrDefault(a => a.StartsWith("Version=", StringComparison.OrdinalIgnoreCase)); - if (frameworkVersion != null) { - result.TargetFrameworkVersion = frameworkVersion.Substring("Version=".Length); - result.VersionNumber = int.Parse(frameworkVersion.Substring("Version=v".Length).Replace(".", "")); - if (result.VersionNumber < 100) result.VersionNumber *= 10; - } - string frameworkProfile = frameworkParts.FirstOrDefault(a => a.StartsWith("Profile=", StringComparison.OrdinalIgnoreCase)); - if (frameworkProfile != null) - result.TargetFrameworkProfile = frameworkProfile.Substring("Profile=".Length); - } - return result; - } - - #endregion - #region WriteCodeFilesInProject protected virtual bool IncludeTypeWhenDecompilingProject(PEFile module, TypeDefinitionHandle type) { @@ -571,32 +387,6 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler return false; } } - - public static string GetPlatformName(Metadata.PEFile module) - { - var headers = module.Reader.PEHeaders; - var architecture = headers.CoffHeader.Machine; - var characteristics = headers.CoffHeader.Characteristics; - var corflags = headers.CorHeader.Flags; - switch (architecture) { - case Machine.I386: - if ((corflags & CorFlags.Prefers32Bit) != 0) - return "AnyCPU"; - if ((corflags & CorFlags.Requires32Bit) != 0) - return "x86"; - // According to ECMA-335, II.25.3.3.1 CorFlags.Requires32Bit and Characteristics.Bit32Machine must be in sync - // for assemblies containing managed code. However, this is not true for C++/CLI assemblies. - if ((corflags & CorFlags.ILOnly) == 0 && (characteristics & Characteristics.Bit32Machine) != 0) - return "x86"; - return "AnyCPU"; - case Machine.Amd64: - return "x64"; - case Machine.IA64: - return "Itanium"; - default: - return architecture.ToString(); - } - } } public readonly struct DecompilationProgress diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index a8a940e6d..1a62ad667 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -61,9 +61,14 @@ + + + + + From bfb57da93b866a2e3ae1cb1776d65c0f83771561 Mon Sep 17 00:00:00 2001 From: dymanoid <9433345+dymanoid@users.noreply.github.com> Date: Sat, 6 Jun 2020 23:57:49 +0200 Subject: [PATCH 07/10] Implement project writer for SDK style projects --- .../ProjectDecompiler/TargetFrameworkTests.cs | 29 +++ .../RoundtripAssembly.cs | 13 +- .../ProjectFileWriterSdkStyle.cs | 212 ++++++++++++++++++ .../ProjectDecompiler/TargetFramework.cs | 56 ++++- .../WholeProjectDecompiler.cs | 2 +- .../ICSharpCode.Decompiler.csproj | 1 + 6 files changed, 303 insertions(+), 10 deletions(-) create mode 100644 ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs diff --git a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/TargetFrameworkTests.cs b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/TargetFrameworkTests.cs index 9a3b8b124..d484714f8 100644 --- a/ICSharpCode.Decompiler.Tests/ProjectDecompiler/TargetFrameworkTests.cs +++ b/ICSharpCode.Decompiler.Tests/ProjectDecompiler/TargetFrameworkTests.cs @@ -88,5 +88,34 @@ namespace ICSharpCode.Decompiler.Tests Assert.AreEqual(identifier, targetFramework.Identifier); Assert.AreEqual(profile, targetFramework.Profile); } + + [TestCase(null, 350, "net35")] + [TestCase(".NETFramework", 350, "net35")] + [TestCase(".NETFramework", 400, "net40")] + [TestCase(".NETFramework", 451, "net451")] + [TestCase(".NETCoreApp", 200, "netcoreapp2.0")] + [TestCase(".NETCoreApp", 310, "netcoreapp3.1")] + [TestCase(".NETStandard", 130, "netstandard1.3")] + [TestCase(".NETStandard", 200, "netstandard2.0")] + [TestCase("Silverlight", 400, "sl4")] + [TestCase("Silverlight", 550, "sl5")] + [TestCase(".NETCore", 450, "netcore45")] + [TestCase(".NETCore", 451, "netcore451")] + [TestCase("WindowsPhone", 700, "wp7")] + [TestCase("WindowsPhone", 810, "wp81")] + [TestCase(".NETMicroFramework", 100, "netmf")] + [TestCase(".NETMicroFramework", 210, "netmf")] + [TestCase(".NETPortable", 100, null)] + [TestCase("Unsupported", 100, null)] + public void VerifyMoniker(string identifier, int version, string expectedMoniker) + { + // Arrange - nothing + + // Act + var targetFramework = new TargetFramework(identifier, version, profile: null); + + // Assert + Assert.AreEqual(expectedMoniker, targetFramework.Moniker); + } } } diff --git a/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs b/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs index 2139269c9..3b369e3fc 100644 --- a/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs +++ b/ICSharpCode.Decompiler.Tests/RoundtripAssembly.cs @@ -55,7 +55,7 @@ namespace ICSharpCode.Decompiler.Tests public void NewtonsoftJson_pcl_debug() { try { - RunWithTest("Newtonsoft.Json-pcl-debug", "Newtonsoft.Json.dll", "Newtonsoft.Json.Tests.dll"); + RunWithTest("Newtonsoft.Json-pcl-debug", "Newtonsoft.Json.dll", "Newtonsoft.Json.Tests.dll", useOldProjectFormat: true); } catch (CompilationFailedException) { Assert.Ignore("Cannot yet re-compile PCL projects."); } @@ -103,9 +103,9 @@ namespace ICSharpCode.Decompiler.Tests RunWithOutput("Random Tests\\TestCases", "TestCase-1.exe"); } - void RunWithTest(string dir, string fileToRoundtrip, string fileToTest, string keyFile = null) + void RunWithTest(string dir, string fileToRoundtrip, string fileToTest, string keyFile = null, bool useOldProjectFormat = false) { - RunInternal(dir, fileToRoundtrip, outputDir => RunTest(outputDir, fileToTest), keyFile); + RunInternal(dir, fileToRoundtrip, outputDir => RunTest(outputDir, fileToTest), keyFile, useOldProjectFormat); } void RunWithOutput(string dir, string fileToRoundtrip) @@ -120,7 +120,7 @@ namespace ICSharpCode.Decompiler.Tests RunInternal(dir, fileToRoundtrip, outputDir => { }); } - void RunInternal(string dir, string fileToRoundtrip, Action testAction, string snkFilePath = null) + void RunInternal(string dir, string fileToRoundtrip, Action testAction, string snkFilePath = null, bool useOldProjectFormat = false) { if (!Directory.Exists(TestDir)) { Assert.Ignore($"Assembly-roundtrip test ignored: test directory '{TestDir}' needs to be checked out separately." + Environment.NewLine + @@ -158,6 +158,9 @@ namespace ICSharpCode.Decompiler.Tests // Let's limit the roundtrip tests to C# 7.3 for now; because 8.0 is still in preview // and the generated project doesn't build as-is. var settings = new DecompilerSettings(LanguageVersion.CSharp7_3); + if (useOldProjectFormat) { + settings.UseSdkStyleProjectFormat = false; + } var decompiler = new TestProjectDecompiler(projectGuid, resolver, settings); @@ -212,7 +215,7 @@ namespace ICSharpCode.Decompiler.Tests static void Compile(string projectFile, string outputDir) { var info = new ProcessStartInfo(FindMSBuild()); - info.Arguments = $"/nologo /v:minimal /p:OutputPath=\"{outputDir}\" \"{projectFile}\""; + info.Arguments = $"/nologo /v:minimal /restore /p:OutputPath=\"{outputDir}\" \"{projectFile}\""; info.CreateNoWindow = true; info.UseShellExecute = false; info.RedirectStandardOutput = true; diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs new file mode 100644 index 000000000..479acc419 --- /dev/null +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/ProjectFileWriterSdkStyle.cs @@ -0,0 +1,212 @@ +// Copyright (c) 2020 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.Reflection.PortableExecutable; +using System.Xml; +using ICSharpCode.Decompiler.Metadata; + +namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler +{ + /// + /// A implementation that creates the projects in the SDK style format. + /// + sealed class ProjectFileWriterSdkStyle : IProjectFileWriter + { + const string AspNetCorePrefix = "Microsoft.AspNetCore"; + const string PresentationFrameworkName = "PresentationFramework"; + const string WindowsFormsName = "System.Windows.Forms"; + const string TrueString = "True"; + const string FalseString = "False"; + const string AnyCpuString = "AnyCPU"; + + 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", + }; + + enum ProjectType { Default, WinForms, Wpf, Web } + + /// + /// Creates a new instance of the class. + /// + /// A new instance of the class. + public static IProjectFileWriter Create() => new ProjectFileWriterSdkStyle(); + + /// + public void Write( + TextWriter target, + IProjectInfoProvider project, + IEnumerable<(string itemType, string fileName)> files, + PEFile module) + { + using (XmlTextWriter xmlWriter = new XmlTextWriter(target)) { + xmlWriter.Formatting = Formatting.Indented; + Write(xmlWriter, project, module); + } + } + + static void Write(XmlTextWriter xml, IProjectInfoProvider project, PEFile module) + { + xml.WriteStartElement("Project"); + + var projectType = GetProjectType(module); + xml.WriteAttributeString("Sdk", GetSdkString(projectType)); + + PlaceIntoTag("PropertyGroup", xml, () => WriteAssemblyInfo(xml, module, projectType)); + PlaceIntoTag("PropertyGroup", xml, () => WriteProjectInfo(xml, project)); + PlaceIntoTag("ItemGroup", xml, () => WriteReferences(xml, module, project)); + + xml.WriteEndElement(); + } + + static void PlaceIntoTag(string tagName, XmlTextWriter xml, Action content) + { + xml.WriteStartElement(tagName); + try { + content(); + } finally { + xml.WriteEndElement(); + } + } + + static void WriteAssemblyInfo(XmlTextWriter xml, PEFile module, ProjectType projectType) + { + xml.WriteElementString("AssemblyName", module.Name); + + // Since we create AssemblyInfo.cs manually, we need to disable the auto-generation + xml.WriteElementString("GenerateAssemblyInfo", FalseString); + + // 'Library' is default, so only need to specify output type for executables + if (!module.Reader.PEHeaders.IsDll) { + WriteOutputType(xml, module.Reader.PEHeaders.PEHeader.Subsystem); + } + + WriteDesktopExtensions(xml, projectType); + + string platformName = TargetServices.GetPlatformName(module); + var targetFramework = TargetServices.DetectTargetFramework(module); + + if (targetFramework.Moniker == null) { + throw new NotSupportedException($"Cannot decompile this assembly to a SDK style project. Use default project format instead."); + } + + xml.WriteElementString("TargetFramework", targetFramework.Moniker); + + // 'AnyCPU' is default, so only need to specify platform if it differs + if (platformName != AnyCpuString) { + xml.WriteElementString("PlatformTarget", platformName); + } + + if (platformName == AnyCpuString && (module.Reader.PEHeaders.CorHeader.Flags & CorFlags.Prefers32Bit) != 0) { + xml.WriteElementString("Prefer32Bit", TrueString); + } + } + + static void WriteOutputType(XmlTextWriter xml, Subsystem moduleSubsystem) + { + switch (moduleSubsystem) { + case Subsystem.WindowsGui: + xml.WriteElementString("OutputType", "WinExe"); + break; + case Subsystem.WindowsCui: + xml.WriteElementString("OutputType", "Exe"); + break; + } + } + + static void WriteDesktopExtensions(XmlTextWriter xml, ProjectType projectType) + { + if (projectType == ProjectType.Wpf) { + xml.WriteElementString("UseWPF", TrueString); + } else if (projectType == ProjectType.WinForms) { + xml.WriteElementString("UseWindowsForms", TrueString); + } + } + + static void WriteProjectInfo(XmlTextWriter xml, IProjectInfoProvider project) + { + xml.WriteElementString("LangVersion", project.LanguageVersion.ToString().Replace("CSharp", "").Replace('_', '.')); + xml.WriteElementString("AllowUnsafeBlocks", TrueString); + + if (project.StrongNameKeyFile != null) { + xml.WriteElementString("SignAssembly", TrueString); + xml.WriteElementString("AssemblyOriginatorKeyFile", Path.GetFileName(project.StrongNameKeyFile)); + } + } + + static void WriteReferences(XmlTextWriter xml, PEFile module, IProjectInfoProvider project) + { + foreach (var reference in module.AssemblyReferences.Where(r => !ImplicitReferences.Contains(r.Name))) { + xml.WriteStartElement("Reference"); + xml.WriteAttributeString("Include", reference.Name); + + var asembly = project.AssemblyResolver.Resolve(reference); + if (asembly != null) { + xml.WriteElementString("HintPath", asembly.FileName); + } + + xml.WriteEndElement(); + } + } + + static string GetSdkString(ProjectType projectType) + { + switch (projectType) { + case ProjectType.WinForms: + case ProjectType.Wpf: + return "Microsoft.NET.Sdk.WindowsDesktop"; + case ProjectType.Web: + return "Microsoft.NET.Sdk.Web"; + default: + return "Microsoft.NET.Sdk"; + } + } + + static ProjectType GetProjectType(PEFile module) + { + foreach (var referenceName in module.AssemblyReferences.Select(r => r.Name)) { + if (referenceName.StartsWith(AspNetCorePrefix, StringComparison.Ordinal)) { + return ProjectType.Web; + } + + if (referenceName == PresentationFrameworkName) { + return ProjectType.Wpf; + } + + if (referenceName == WindowsFormsName) { + return ProjectType.WinForms; + } + } + + return ProjectType.Default; + } + } +} diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetFramework.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetFramework.cs index 43e8642ee..c340578e3 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetFramework.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetFramework.cs @@ -42,7 +42,8 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler Identifier = identifier; VersionNumber = version; - VersionString = "v" + GetVersionString(version); + VersionString = "v" + GetVersionString(version, withDots: true); + Moniker = GetTargetFrameworkMoniker(Identifier, version); Profile = profile; IsPortableClassLibrary = identifier == DotNetPortableIdentifier; } @@ -52,6 +53,11 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler /// public string Identifier { get; } + /// + /// Gets the target framework moniker. Can be null if not supported. + /// + public string Moniker { get; } + /// /// Gets the target framework version, e.g. "v4.5". /// @@ -72,19 +78,61 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler /// public bool IsPortableClassLibrary { get; } - static string GetVersionString(int version) + static string GetTargetFrameworkMoniker(string frameworkIdentifier, int version) + { + // Reference: https://docs.microsoft.com/en-us/dotnet/standard/frameworks + switch (frameworkIdentifier) { + case null: + case ".NETFramework": + return "net" + GetVersionString(version, withDots: false); + + case ".NETCoreApp": + return "netcoreapp" + GetVersionString(version, withDots: true); + + case ".NETStandard": + return "netstandard" + GetVersionString(version, withDots: true); + + case "Silverlight": + return "sl" + version / 100; + + case ".NETCore": + return "netcore" + GetVersionString(version, withDots: false); + + case "WindowsPhone": + return "wp" + GetVersionString(version, withDots: false, omitMinorWhenZero: true); + + case ".NETMicroFramework": + return "netmf"; + + default: + return null; + } + } + + static string GetVersionString(int version, bool withDots, bool omitMinorWhenZero = false) { int major = version / 100; int minor = version % 100 / 10; int patch = version % 10; + if (omitMinorWhenZero && minor == 0 && patch == 0) { + return major.ToString(); + } + var versionBuilder = new StringBuilder(8); versionBuilder.Append(major); - versionBuilder.Append('.'); + + if (withDots) { + versionBuilder.Append('.'); + } + versionBuilder.Append(minor); if (patch != 0) { - versionBuilder.Append('.'); + if (withDots) { + versionBuilder.Append('.'); + } + versionBuilder.Append(patch); } diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs index cc6b4503e..56394502f 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs +++ b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/WholeProjectDecompiler.cs @@ -103,7 +103,7 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler ProjectGuid = projectGuid; AssemblyResolver = assemblyResolver ?? throw new ArgumentNullException(nameof(assemblyResolver)); DebugInfoProvider = debugInfoProvider; - projectWriter = ProjectFileWriterDefault.Create(); + projectWriter = Settings.UseSdkStyleProjectFormat ? ProjectFileWriterSdkStyle.Create() : ProjectFileWriterDefault.Create(); } // per-run members diff --git a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj index 1a62ad667..b207c5f6d 100644 --- a/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj +++ b/ICSharpCode.Decompiler/ICSharpCode.Decompiler.csproj @@ -64,6 +64,7 @@ + From e0e26a0e7721f69111b056104a7d8cb41daa1436 Mon Sep 17 00:00:00 2001 From: Chicken-Bones Date: Tue, 9 Jun 2020 19:55:34 +1000 Subject: [PATCH 08/10] Fix ReduceNestingTransform when extracting default block of switch in a try container --- .../TestCases/Pretty/ReduceNesting.cs | 64 +++++++++++++++++++ .../IL/Transforms/ReduceNestingTransform.cs | 17 +++-- 2 files changed, 74 insertions(+), 7 deletions(-) diff --git a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ReduceNesting.cs b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ReduceNesting.cs index 9b72b0044..8d9b4c841 100644 --- a/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ReduceNesting.cs +++ b/ICSharpCode.Decompiler.Tests/TestCases/Pretty/ReduceNesting.cs @@ -399,5 +399,69 @@ namespace ICSharpCode.Decompiler.Tests.TestCases.Pretty throw new Exception(); } } + + public void SwitchInTry() + { + try { + switch (I(0)) { + case 1: + Console.WriteLine(1); + return; + case 2: + Console.WriteLine(2); + return; + } + Console.WriteLine(3); + for (int i = 0; i < 10; i++) { + Console.WriteLine(i); + } + } catch { + throw; + } + } + + public void SwitchInTryInLoopReturn() + { + for (int i = 0; i < 10; i++) { + try { + switch (I(0)) { + case 1: + Console.WriteLine(1); + return; + case 2: + Console.WriteLine(2); + return; + } + Console.WriteLine(3); + for (int j = 0; j < 10; j++) { + Console.WriteLine(j); + } + } catch { + throw; + } + } + } + + public void SwitchInTryInLoopContinue() + { + for (int i = 0; i < 10; i++) { + try { + switch (I(0)) { + case 1: + Console.WriteLine(1); + continue; + case 2: + Console.WriteLine(2); + continue; + } + Console.WriteLine(3); + for (int j = 0; j < 10; j++) { + Console.WriteLine(j); + } + } catch { + throw; + } + } + } } } diff --git a/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs index 5a98235e3..d8c7f6de8 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs @@ -307,6 +307,16 @@ namespace ICSharpCode.Decompiler.IL context.Step("Extract default case of switch", switchContainer); + // if the switch container is followed by an instruction, it must be a Leave from a try/pinned/etc or exitInst + // When it's a leave from a container, it's better to let the extracted default block 'fall through' rather than duplicating whatever + // instruction eventually follows the container + if (parentBlock.Instructions.SecondToLastOrDefault() == switchContainer) { + if (defaultBlock.Instructions.Last().MatchLeave(switchContainer)) + defaultBlock.Instructions.Last().ReplaceWith(parentBlock.Instructions.Last()); + + parentBlock.Instructions.RemoveLast(); + } + // replace all break; statements with the exitInst var leaveInstructions = switchContainer.Descendants.Where(inst => inst.MatchLeave(switchContainer)); foreach (var leaveInst in leaveInstructions.ToArray()) @@ -320,13 +330,6 @@ namespace ICSharpCode.Decompiler.IL foreach (var block in defaultBlocks) switchContainer.Blocks.Remove(block); - // replace the parent block exit with the default case instructions - if (parentBlock.Instructions.Last() == exitInst) { - parentBlock.Instructions.RemoveLast(); - } - // Note: even though we don't check that the switchContainer is near the end of the block, - // we know this must be the case because we know "exitInst" is a leave/branch and directly - // follows the switchContainer. Debug.Assert(parentBlock.Instructions.Last() == switchContainer); parentBlock.Instructions.AddRange(defaultBlock.Instructions); From abd9af29c61fe1910eb49915e4ec37575a230a26 Mon Sep 17 00:00:00 2001 From: Chicken-Bones Date: Tue, 9 Jun 2020 20:28:53 +1000 Subject: [PATCH 09/10] Fix failed assertion when encountering a finally block with an unreachable endpoint in ReduceNestingTransform. --- .../IL/Transforms/ReduceNestingTransform.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs index d8c7f6de8..7e2dd7dc7 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs @@ -364,13 +364,18 @@ namespace ICSharpCode.Decompiler.IL ILInstruction leavingInst = leave.TargetContainer; Debug.Assert(!leavingInst.HasFlag(InstructionFlags.EndPointUnreachable)); while (!(leavingInst.Parent is Block b) || leavingInst == b.Instructions.Last()) { - // cannot duplicate leaves from finally containers - if (leavingInst.Parent is TryFinally tryFinally && leavingInst.SlotInfo == TryFinally.FinallyBlockSlot) { - Debug.Assert(leave.TargetContainer == tryFinally.FinallyBlock); //finally cannot have control flow - return false; - } - leavingInst = leavingInst.Parent; + + if (leavingInst is TryFinally tryFinally) { + if (leavingInst.SlotInfo == TryFinally.FinallyBlockSlot) { // cannot duplicate leaves from finally containers + Debug.Assert(leave.TargetContainer == tryFinally.FinallyBlock); //finally cannot have control flow + return false; + } + if (leavingInst.HasFlag(InstructionFlags.EndPointUnreachable)) { // finally block changes return value/throws an exception? Yikes. Lets leave it alone + Debug.Assert(tryFinally.FinallyBlock.HasFlag(InstructionFlags.EndPointUnreachable)); + return false; + } + } Debug.Assert(!leavingInst.HasFlag(InstructionFlags.EndPointUnreachable)); Debug.Assert(!(leavingInst is ILFunction)); } From da905acc6da557b8dae8c80c1ac51358c7956047 Mon Sep 17 00:00:00 2001 From: Chicken-Bones Date: Thu, 11 Jun 2020 22:23:32 +1000 Subject: [PATCH 10/10] Fix logic error in last commit, and ignore fault clauses in ReduceNestingTransform --- .../IL/Transforms/ReduceNestingTransform.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs b/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs index 7e2dd7dc7..d73cf222f 100644 --- a/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs +++ b/ICSharpCode.Decompiler/IL/Transforms/ReduceNestingTransform.cs @@ -364,18 +364,22 @@ namespace ICSharpCode.Decompiler.IL ILInstruction leavingInst = leave.TargetContainer; Debug.Assert(!leavingInst.HasFlag(InstructionFlags.EndPointUnreachable)); while (!(leavingInst.Parent is Block b) || leavingInst == b.Instructions.Last()) { - leavingInst = leavingInst.Parent; - - if (leavingInst is TryFinally tryFinally) { + if (leavingInst.Parent is TryFinally tryFinally) { if (leavingInst.SlotInfo == TryFinally.FinallyBlockSlot) { // cannot duplicate leaves from finally containers Debug.Assert(leave.TargetContainer == tryFinally.FinallyBlock); //finally cannot have control flow return false; } - if (leavingInst.HasFlag(InstructionFlags.EndPointUnreachable)) { // finally block changes return value/throws an exception? Yikes. Lets leave it alone + if (tryFinally.HasFlag(InstructionFlags.EndPointUnreachable)) { // finally block changes return value/throws an exception? Yikes. Lets leave it alone Debug.Assert(tryFinally.FinallyBlock.HasFlag(InstructionFlags.EndPointUnreachable)); return false; } } + else if (leavingInst.Parent is TryFault tryFault && leavingInst.SlotInfo == TryFault.FaultBlockSlot) { // cannot duplicate leaves from fault containers either + Debug.Assert(leave.TargetContainer == tryFault.FaultBlock); + return false; + } + + leavingInst = leavingInst.Parent; Debug.Assert(!leavingInst.HasFlag(InstructionFlags.EndPointUnreachable)); Debug.Assert(!(leavingInst is ILFunction)); }