.NET Decompiler with support for PDB generation, ReadyToRun, Metadata (&more) - cross-platform!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

217 lines
8.9 KiB

// Copyright (c) 2026 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.IO;
using System.Linq;
using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.CSharp.OutputVisitor;
using ICSharpCode.Decompiler.CSharp.Syntax;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.Tests.Helpers;
using ICSharpCode.Decompiler.TypeSystem;
using NUnit.Framework;
namespace ICSharpCode.Decompiler.Tests.Output
{
// Characterization coverage for the "AST with locations" output path
// (TokenWriter.CreateWriterThatSetsLocationsInAST -> InsertMissingTokensDecorator). The Pretty
// suite never drives this path, yet PDB sequence points depend on it (SequencePointBuilder reads
// node.StartLocation/EndLocation; the GUI mixed-language view consumes those points). These tests
// pin the observable consequences that must survive later AST changes: the located path emits the
// same text as the plain path, real nodes receive sensible locations, and sequence points are
// produced for a method body.
//
// Like the Pretty tests, the samples live in a TestCases fixture compiled through Tester with a
// fixed Roslyn version, rather than being read out of this test assembly: its Debug/Release build
// flavor would otherwise change the sample IL and shift the sequence-point coordinates locked below.
[TestFixture]
public class LocationsInAstTests
{
static readonly string TestCasePath = Tester.TestCasePath + "/LocationsInAst";
const CompilerOptions SampleCompilerOptions =
CompilerOptions.UseRoslyn4_14_0 | CompilerOptions.UseDebug | CompilerOptions.Library;
const string LocationSampleName = "ICSharpCode.Decompiler.Tests.TestCases.LocationsInAst.LocationSample";
const string SequencePointSampleName = "ICSharpCode.Decompiler.Tests.TestCases.LocationsInAst.SequencePointSample";
static CompilerResults compiledSamples;
[OneTimeSetUp]
public void CompileSamples()
{
var csFile = Path.Combine(TestCasePath, "LocationsInAstSamples.cs");
compiledSamples = Tester.CompileCSharp(csFile, SampleCompilerOptions).GetAwaiter().GetResult();
}
[OneTimeTearDown]
public void DeleteCompiledSamples()
{
compiledSamples?.DeleteTempFiles();
}
static CSharpDecompiler CreateDecompiler(out DecompilerSettings settings)
{
string assemblyPath = compiledSamples.PathToAssembly;
var module = new PEFile(assemblyPath);
var resolver = new UniversalAssemblyResolver(assemblyPath, false, module.Metadata.DetectTargetFrameworkId());
settings = new DecompilerSettings();
return new CSharpDecompiler(module, resolver, settings);
}
static SyntaxTree DecompileSample(out CSharpDecompiler decompiler, out DecompilerSettings settings)
{
decompiler = CreateDecompiler(out settings);
return decompiler.DecompileType(new FullTypeName(LocationSampleName));
}
static string Print(SyntaxTree syntaxTree, DecompilerSettings settings, bool setLocations)
{
var writer = new StringWriter();
TokenWriter tokenWriter = setLocations
? TokenWriter.CreateWriterThatSetsLocationsInAST(writer)
: TokenWriter.Create(writer);
syntaxTree.AcceptVisitor(new CSharpOutputVisitor(tokenWriter, settings.CSharpFormattingOptions));
return writer.ToString();
}
[Test]
public void LocatedPathEmitsSameTextAsPlainPath()
{
// The location-setting decorator must be output-transparent: inserting it changes only the
// AST side effects (locations + reconstructed tokens), never the emitted characters.
var plainTree = DecompileSample(out _, out var settings);
string plain = Print(plainTree, settings, setLocations: false);
var locatedTree = DecompileSample(out _, out settings);
string located = Print(locatedTree, settings, setLocations: true);
Assert.That(located, Is.EqualTo(plain));
}
// Returns the substring of <paramref name="length"/> characters that starts at the
// (1-based line, 1-based column) location, asserting the location is inside the text.
static string TextAt(string[] lines, TextLocation location, int length)
{
Assert.That(location.Line, Is.InRange(1, lines.Length), "location line is out of range");
string line = lines[location.Line - 1];
int start = location.Column - 1;
Assert.That(start, Is.GreaterThanOrEqualTo(0));
Assert.That(start + length, Is.LessThanOrEqualTo(line.Length), "location column + length exceeds the line");
return line.Substring(start, length);
}
[Test]
public void StoredIdentifierLocationsIndexTheirNamesInTheOutput()
{
// Correctness check for the locations the path assigns: an identifier's StartLocation must
// point at exactly where its name appears in the emitted text. Re-print without locations to
// the same text and slice each identifier's name back out of it.
var syntaxTree = DecompileSample(out _, out var settings);
string text = Print(syntaxTree, settings, setLocations: true);
string[] lines = text.Replace("\r\n", "\n").Split('\n');
var identifiers = syntaxTree.Descendants
.OfType<Identifier>()
.Where(id => !string.IsNullOrEmpty(id.Name) && id.StartLocation.Line > 0)
.ToList();
Assert.That(identifiers, Is.Not.Empty, "no positioned identifiers in the decompiled output");
foreach (var id in identifiers)
{
// A verbatim identifier is written as "@name"; its StartLocation points at the '@'.
var nameStart = new TextLocation(id.StartLocation.Line, id.StartLocation.Column + (id.IsVerbatim ? 1 : 0));
Assert.That(TextAt(lines, nameStart, id.Name.Length), Is.EqualTo(id.Name),
$"identifier '{id.Name}' at {id.StartLocation} does not index its name in the output");
}
// Guard against the loop passing vacuously: the sample's own members must be among them.
var names = identifiers.Select(id => id.Name).ToList();
Assert.That(names, Does.Contain("Add"));
Assert.That(names, Does.Contain("a").And.Contains("b"));
}
[Test]
public void SequencePointsAreGeneratedForTheMethodBody()
{
var syntaxTree = DecompileSample(out var decompiler, out var settings);
Print(syntaxTree, settings, setLocations: true);
var sequencePoints = decompiler.CreateSequencePoints(syntaxTree);
Assert.That(sequencePoints, Is.Not.Empty, "no sequence points were produced");
var points = sequencePoints
.First(kvp => kvp.Key.Name == "Add")
.Value;
Assert.That(points, Is.Not.Empty, "the sample method produced no sequence points");
Assert.That(points.Any(p => !p.IsHidden && p.StartLine > 0), Is.True,
"no visible sequence point carries a source line");
}
// Locks the exact source spans of the visible sequence points for a header-rich method, so a
// rewrite of how SequencePointBuilder sources brace/paren/keyword locations (e.g. deriving them
// from real nodes instead of reconstructed token nodes) cannot silently shift PDB coordinates.
// The coordinates are in decompiled-output space and are pinned to the fixed compilation above.
static readonly string[] ExpectedHeaderSequencePoints = {
"10,50 - 10,57",
"10,59 - 10,67",
"10,8 - 10,48",
"15,4 - 15,10",
"17,3 - 17,18",
"18,3 - 21,4",
"20,4 - 20,16",
"22,3 - 22,28",
"23,3 - 23,15",
"24,3 - 24,4",
"25,4 - 25,10",
"26,3 - 26,4",
"28,3 - 28,4",
"29,4 - 29,19",
"31,24 - 31,52",
"32,3 - 32,4",
"33,4 - 33,14",
"36,3 - 36,4",
"37,4 - 37,14",
"8,2 - 8,3",
"9,3 - 9,15",
};
[Test]
public void HeaderSequencePointCoordinatesAreStable()
{
var decompiler = CreateDecompiler(out var settings);
var syntaxTree = decompiler.DecompileType(new FullTypeName(SequencePointSampleName));
Print(syntaxTree, settings, setLocations: true);
var points = decompiler.CreateSequencePoints(syntaxTree)
.First(kvp => kvp.Key.Name == "Headers")
.Value;
var actual = points
.Where(p => !p.IsHidden)
.Select(p => $"{p.StartLine},{p.StartColumn} - {p.EndLine},{p.EndColumn}")
.OrderBy(s => s, StringComparer.Ordinal)
.ToArray();
Assert.That(actual, Is.EqualTo(ExpectedHeaderSequencePoints));
}
}
}