Browse Source

Add the "IL with C#" language

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 Code
pull/3755/head
Siegfried Pammer 1 month ago
parent
commit
69a55ea732
  1. 74
      ILSpy.Tests/Languages/CSharpILMixedLanguageTests.cs
  2. 201
      ILSpy/Languages/CSharpILMixedLanguage.cs

74
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;
/// <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");
}
}

201
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;
/// <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…
Cancel
Save