diff --git a/ILSpy.Tests/Languages/CSharpILMixedLanguageTests.cs b/ILSpy.Tests/Languages/CSharpILMixedLanguageTests.cs new file mode 100644 index 000000000..fcae5e7a1 --- /dev/null +++ b/ILSpy.Tests/Languages/CSharpILMixedLanguageTests.cs @@ -0,0 +1,74 @@ +// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of this +// software and associated documentation files (the "Software"), to deal in the Software +// without restriction, including without limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +// to whom the Software is furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all copies or +// substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, +// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE +// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +using System.Linq; +using System.Threading.Tasks; + +using Avalonia.Headless.NUnit; + +using AwesomeAssertions; + +using ILSpy; +using ILSpy.AppEnv; +using ILSpy.Languages; +using ILSpy.TextView; +using ILSpy.TreeNodes; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests.Languages; + +/// +/// The "IL with C#" language disassembles IL with the decompiled C# source interleaved as gray +/// comments. (Regression: the language was missing entirely from the Avalonia port.) +/// +[TestFixture] +public class CSharpILMixedLanguageTests +{ + [AvaloniaTest] + public async Task ILWithCSharp_Is_Registered_And_Interleaves_CSharp_Into_The_IL() + { + var (_, vm) = await TestHarness.BootAsync(3); + var languages = AppComposition.Current.GetExport().Languages; + + var mixed = languages.FirstOrDefault(l => l.Name == "IL with C#"); + Assert.That(mixed, Is.Not.Null, "the 'IL with C#' language must be registered"); + var plainIL = languages.First(l => l.Name == "IL"); + + var asm = vm.AssemblyTreeModel.FindNode("System.Linq"); + Assert.That(asm, Is.Not.Null); + var typeSystem = asm!.LoadedAssembly.GetTypeSystemOrNull(); + Assert.That(typeSystem, Is.Not.Null); + var type = typeSystem!.MainModule.TypeDefinitions.First(t => t.FullName == "System.Linq.Enumerable"); + var method = type.Methods.First(m => m.HasBody); + + string Decompile(Language language) + { + var output = new AvaloniaEditTextOutput(); + language.DecompileMethod(method, output, new DecompilationOptions()); + return output.GetText(); + } + + var mixedText = Decompile(mixed!); + var ilText = Decompile(plainIL); + + mixedText.Should().Contain("//", "the decompiled C# is interleaved as comments"); + mixedText.Length.Should().BeGreaterThan(ilText.Length, + "the interleaved C# source makes the mixed output longer than plain IL"); + } +} diff --git a/ILSpy/Languages/CSharpILMixedLanguage.cs b/ILSpy/Languages/CSharpILMixedLanguage.cs new file mode 100644 index 000000000..ff5944723 --- /dev/null +++ b/ILSpy/Languages/CSharpILMixedLanguage.cs @@ -0,0 +1,201 @@ +// Copyright (c) 2018 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.Composition; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; + +using Avalonia.Media; + +using AvaloniaEdit.Highlighting; + +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.CSharp.OutputVisitor; +using ICSharpCode.Decompiler.CSharp.Syntax; +using ICSharpCode.Decompiler.Disassembler; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.TypeSystem; +using ICSharpCode.Decompiler.Util; +using ICSharpCode.ILSpyX; +using ICSharpCode.ILSpyX.Extensions; + +using ILSpy.TextView; + +namespace ILSpy.Languages +{ + using SequencePoint = ICSharpCode.Decompiler.DebugInfo.SequencePoint; + + /// + /// Disassembles IL with the decompiled C# source interleaved above each instruction (mapped via + /// sequence points) as gray comments. + /// + [Export(typeof(Language))] + [Shared] + [method: ImportingConstructor] + class CSharpILMixedLanguage(SettingsService settingsService) : ILLanguage + { + public override string Name => "IL with C#"; + + protected override ReflectionDisassembler CreateDisassembler(ITextOutput output, DecompilationOptions options) + { + var displaySettings = settingsService.DisplaySettings; + return new ReflectionDisassembler(output, + new MixedMethodBodyDisassembler(output, options) { + DetectControlStructure = detectControlStructure, + ShowSequencePoints = options.DecompilerSettings.ShowDebugInfo + }, + options.CancellationToken) { + ShowMetadataTokens = displaySettings.ShowMetadataTokens, + ShowMetadataTokensInBase10 = displaySettings.ShowMetadataTokensInBase10, + ShowRawRVAOffsetAndBytes = displaySettings.ShowRawOffsetsAndBytesBeforeInstruction, + ExpandMemberDefinitions = options.DecompilerSettings.ExpandMemberDefinitions, + DecodeCustomAttributeBlobs = displaySettings.DecodeCustomAttributeBlobs + }; + } + + static CSharpDecompiler CreateDecompiler(MetadataFile module, DecompilationOptions options) + { + CSharpDecompiler decompiler = new CSharpDecompiler(module, module.GetAssemblyResolver(), options.DecompilerSettings); + decompiler.CancellationToken = options.CancellationToken; + return decompiler; + } + + static void WriteCode(TextWriter output, DecompilerSettings settings, SyntaxTree syntaxTree, IDecompilerTypeSystem typeSystem) + { + syntaxTree.AcceptVisitor(new InsertParenthesesVisitor { InsertParenthesesForReadability = true }); + TokenWriter tokenWriter = new TextWriterTokenWriter(output) { IndentationString = settings.CSharpFormattingOptions.IndentationString }; + tokenWriter = TokenWriter.WrapInWriterThatSetsLocationsInAST(tokenWriter); + syntaxTree.AcceptVisitor(new CSharpOutputVisitor(tokenWriter, settings.CSharpFormattingOptions)); + } + + class MixedMethodBodyDisassembler : MethodBodyDisassembler + { + readonly DecompilationOptions options; + // list sorted by IL offset; non-null only for the duration of a Disassemble pass + IList? sequencePoints; + // lines of raw c# source code; non-null only for the duration of a Disassemble pass + string[]? codeLines; + + public MixedMethodBodyDisassembler(ITextOutput output, DecompilationOptions options) + : base(output, options.CancellationToken) + { + this.options = options; + } + + public override void Disassemble(MetadataFile module, MethodDefinitionHandle handle) + { + try + { + var csharpOutput = new StringWriter(); + CSharpDecompiler decompiler = CreateDecompiler(module, options); + var st = decompiler.Decompile(handle); + WriteCode(csharpOutput, options.DecompilerSettings, st, decompiler.TypeSystem); + var mapping = decompiler.CreateSequencePoints(st).FirstOrDefault(kvp => (kvp.Key.MoveNextMethod ?? kvp.Key.Method)?.MetadataToken == handle); + this.sequencePoints = mapping.Value ?? (IList)EmptyList.Instance; + this.codeLines = csharpOutput.ToString().Split(new[] { Environment.NewLine }, StringSplitOptions.None); + base.Disassemble(module, handle); + } + finally + { + this.sequencePoints = null; + this.codeLines = null; + } + } + + protected override void WriteInstruction(ITextOutput output, MetadataFile metadata, MethodDefinitionHandle methodHandle, ref BlobReader blob, int methodRva) + { + if (sequencePoints is null || codeLines is null) + { + base.WriteInstruction(output, metadata, methodHandle, ref blob, methodRva); + return; + } + int index = sequencePoints.BinarySearch(blob.Offset, seq => seq.Offset); + if (index >= 0) + { + var info = sequencePoints[index]; + var highlightingOutput = output as ISmartTextOutput; + if (!info.IsHidden) + { + for (int line = info.StartLine; line <= info.EndLine; line++) + { + if (highlightingOutput != null) + { + string text = codeLines[line - 1]; + int startColumn = 1; + int endColumn = text.Length + 1; + if (line == info.StartLine) + startColumn = info.StartColumn; + if (line == info.EndLine) + endColumn = info.EndColumn; + WriteHighlightedCommentLine(highlightingOutput, text, startColumn - 1, endColumn - 1, info.StartLine == info.EndLine); + } + else + WriteCommentLine(output, codeLines[line - 1]); + } + } + else + { + output.Write("// "); + highlightingOutput?.BeginSpan(gray); + output.WriteLine("(no C# code)"); + highlightingOutput?.EndSpan(); + } + } + base.WriteInstruction(output, metadata, methodHandle, ref blob, methodRva); + } + + HighlightingColor gray = new HighlightingColor { Foreground = new SimpleHighlightingBrush(Colors.DarkGray) }; + + void WriteHighlightedCommentLine(ISmartTextOutput output, string text, int startColumn, int endColumn, bool isSingleLine) + { + if (startColumn > text.Length) + { + Debug.Fail("startColumn is invalid"); + startColumn = text.Length; + } + if (endColumn > text.Length) + { + Debug.Fail("endColumn is invalid"); + endColumn = text.Length; + } + output.Write("// "); + output.BeginSpan(gray); + if (isSingleLine) + output.Write(text.Substring(0, startColumn).TrimStart()); + else + output.Write(text.Substring(0, startColumn)); + output.EndSpan(); + output.Write(text.Substring(startColumn, endColumn - startColumn)); + output.BeginSpan(gray); + output.Write(text.Substring(endColumn)); + output.EndSpan(); + output.WriteLine(); + } + + void WriteCommentLine(ITextOutput output, string text) + { + output.WriteLine("// " + text); + } + } + } +}