mirror of https://github.com/icsharpcode/ILSpy.git
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.
239 lines
10 KiB
239 lines
10 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 |
|
{ |
|
// The sample source sits next to this test file under Output/, not in TestCases/. |
|
static readonly string SamplesPath = Path.Combine(Tester.TestCasePath, "..", "Output"); |
|
|
|
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(SamplesPath, "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 RenamedConstructorNameSurvivesLocationSetting() |
|
{ |
|
// A constructor whose name differs from its declaring type (e.g. after a type rename) makes |
|
// CSharpOutputVisitor.VisitConstructorDeclaration print a detached clone of the type's name |
|
// token. That clone reaches the location-setting decorator parentless, so it has no slot; the |
|
// decorator must skip it instead of routing SlotKind.None into the throwing child setter. |
|
var type = new TypeDeclaration { ClassType = ClassType.Class, Name = "Foo" }; |
|
type.Members.Add(new ConstructorDeclaration { Name = "Bar", Body = new BlockStatement() }); |
|
var syntaxTree = new SyntaxTree(); |
|
syntaxTree.Members.Add(type); |
|
|
|
var settings = new DecompilerSettings(); |
|
string located = null; |
|
Assert.That(() => located = Print(syntaxTree, settings, setLocations: true), Throws.Nothing); |
|
|
|
// The clone path is the one under test: the constructor is named after the type, not "Bar". |
|
Assert.That(located, Does.Contain("Foo()")); |
|
Assert.That(located, Is.EqualTo(Print(syntaxTree, settings, setLocations: false))); |
|
} |
|
|
|
[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 |
|
// change to how SequencePointBuilder derives brace/paren/keyword locations from the surrounding |
|
// real 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)); |
|
} |
|
} |
|
}
|
|
|