mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
Port the mixed IL/C# disassembler language that the previous version shipped: it disassembles IL with the decompiled C# source interleaved above each instruction (mapped via sequence points) as gray comments. The language was missing entirely from the Avalonia tree, though it is a Release-visible entry in the language dropdown. Assisted-by: Claude:claude-opus-4-8:Claude Codepull/3755/head
2 changed files with 275 additions and 0 deletions
@ -0,0 +1,74 @@
@@ -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; |
||||
|
||||
/// <summary>
|
||||
/// 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.)
|
||||
/// </summary>
|
||||
[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<LanguageService>().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<AssemblyTreeNode>("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"); |
||||
} |
||||
} |
||||
@ -0,0 +1,201 @@
@@ -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; |
||||
|
||||
/// <summary>
|
||||
/// Disassembles IL with the decompiled C# source interleaved above each instruction (mapped via
|
||||
/// sequence points) as gray comments.
|
||||
/// </summary>
|
||||
[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<SequencePoint>? 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<SequencePoint>)EmptyList<SequencePoint>.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); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue