Browse Source

Store locations at print time; derive sequence points from nodes

Source locations were virtual, computed by recursing to the first and last
child, whose leftmost and rightmost leaves are token nodes; sequence-point
coordinates likewise came from reconstructed token nodes. Store locations as
fields assigned while printing, and derive sequence-point coordinates from the
surrounding real nodes plus the decompiler's fixed formatting, so neither
depends on token children. The using/foreach await modifier becomes a plain
bool field. Characterization gates lock the emitted locations and PDB
coordinates, which are unchanged.
Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3807/head
Siegfried Pammer 3 weeks ago committed by Siegfried Pammer
parent
commit
be565b5b4c
  1. 1
      ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
  2. 96
      ICSharpCode.Decompiler.Tests/Output/LocationsInAstTests.cs
  3. 72
      ICSharpCode.Decompiler.Tests/TestCases/LocationsInAst/LocationsInAstSamples.cs
  4. 34
      ICSharpCode.Decompiler/CSharp/OutputVisitor/InsertMissingTokensDecorator.cs
  5. 55
      ICSharpCode.Decompiler/CSharp/SequencePointBuilder.cs
  6. 27
      ICSharpCode.Decompiler/CSharp/Syntax/AstNode.cs
  7. 9
      ICSharpCode.Decompiler/CSharp/Syntax/Statements/ForeachStatement.cs
  8. 9
      ICSharpCode.Decompiler/CSharp/Syntax/Statements/UsingStatement.cs

1
ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj

@ -101,6 +101,7 @@ @@ -101,6 +101,7 @@
<None Include="TestCases\IsolatedDecompilation\IsolatedStaticCtor.il" />
<None Include="TestCases\ILPretty\ExtensionEncodingV2.il" />
<None Include="testcases\ilpretty\ExtensionEncodingV1.il" />
<None Include="TestCases\LocationsInAst\LocationsInAstSamples.cs" />
<None Include="TestCases\ILPretty\GuessAccessors.cs" />
<None Include="TestCases\ILPretty\GuessAccessors.il" />
<None Include="TestCases\ILPretty\Issue2260SwitchString.il" />

96
ICSharpCode.Decompiler.Tests/Output/LocationsInAstTests.cs

@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
// 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;
@ -23,23 +24,13 @@ using ICSharpCode.Decompiler.CSharp; @@ -23,23 +24,13 @@ 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
{
// Sample type decompiled by the tests below. A small method body gives the
// location-setting output path several statements and tokens to assign positions to.
internal class LocationSample
{
public int Add(int a, int b)
{
int sum = a + b;
return sum;
}
}
// 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
@ -47,12 +38,39 @@ namespace ICSharpCode.Decompiler.Tests.Output @@ -47,12 +38,39 @@ namespace ICSharpCode.Decompiler.Tests.Output
// 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 = typeof(LocationsInAstTests).Assembly.Location;
string assemblyPath = compiledSamples.PathToAssembly;
var module = new PEFile(assemblyPath);
var resolver = new UniversalAssemblyResolver(assemblyPath, false, module.Metadata.DetectTargetFrameworkId());
settings = new DecompilerSettings();
@ -62,7 +80,7 @@ namespace ICSharpCode.Decompiler.Tests.Output @@ -62,7 +80,7 @@ namespace ICSharpCode.Decompiler.Tests.Output
static SyntaxTree DecompileSample(out CSharpDecompiler decompiler, out DecompilerSettings settings)
{
decompiler = CreateDecompiler(out settings);
return decompiler.DecompileType(new FullTypeName(typeof(LocationSample).FullName));
return decompiler.DecompileType(new FullTypeName(LocationSampleName));
}
static string Print(SyntaxTree syntaxTree, DecompilerSettings settings, bool setLocations)
@ -127,7 +145,7 @@ namespace ICSharpCode.Decompiler.Tests.Output @@ -127,7 +145,7 @@ namespace ICSharpCode.Decompiler.Tests.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(nameof(LocationSample.Add)));
Assert.That(names, Does.Contain("Add"));
Assert.That(names, Does.Contain("a").And.Contains("b"));
}
@ -141,11 +159,59 @@ namespace ICSharpCode.Decompiler.Tests.Output @@ -141,11 +159,59 @@ namespace ICSharpCode.Decompiler.Tests.Output
Assert.That(sequencePoints, Is.Not.Empty, "no sequence points were produced");
var points = sequencePoints
.First(kvp => kvp.Key.Name == nameof(LocationSample.Add))
.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));
}
}
}

72
ICSharpCode.Decompiler.Tests/TestCases/LocationsInAst/LocationsInAstSamples.cs

@ -0,0 +1,72 @@ @@ -0,0 +1,72 @@
using System;
namespace ICSharpCode.Decompiler.Tests.TestCases.LocationsInAst
{
// A small method body gives the location-setting output path several statements and tokens
// to assign positions to.
internal class LocationSample
{
public int Add(int a, int b)
{
int sum = a + b;
return sum;
}
}
// Exercises the statement headers whose sequence-point coordinates SequencePointBuilder derives
// from token positions ('{'/'}', if/while/do-while/foreach/switch/lock '(...)', catch/when).
internal class SequencePointSample
{
public int Headers(int n, int[] items)
{
int sum = 0;
if (n > 0)
{
sum++;
}
else
{
sum--;
}
while (sum < n)
{
sum += 2;
}
do
{
sum--;
}
while (sum > 0);
foreach (int item in items)
{
sum += item;
}
switch (n)
{
case 1:
sum = 1;
break;
default:
sum = 0;
break;
}
lock (items)
{
sum++;
}
try
{
sum += n;
}
catch (Exception ex) when (ex.Message.Length > 0)
{
sum = -1;
}
catch (Exception)
{
sum = -2;
}
return sum;
}
}
}

34
ICSharpCode.Decompiler/CSharp/OutputVisitor/InsertMissingTokensDecorator.cs

@ -29,6 +29,15 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor @@ -29,6 +29,15 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
List<AstNode> currentList;
readonly ILocatable locationProvider;
// Nodes that have been started but whose first token has not been written yet. The start
// location of a node is the position of its first printed token (not the StartNode position,
// which precedes any leading newline/indentation), so it is assigned lazily on the next write.
readonly List<AstNode> nodesAwaitingStart = new List<AstNode>();
// Position immediately after the most recently written token. A node's end location is the end
// of its last token (not the EndNode position, which follows the trailing newline/indentation).
TextLocation lastTokenEnd;
public InsertMissingTokensDecorator(TokenWriter writer, ILocatable locationProvider)
: base(writer)
{
@ -36,6 +45,16 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor @@ -36,6 +45,16 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
currentList = new List<AstNode>();
}
void AssignPendingStartLocations()
{
if (nodesAwaitingStart.Count == 0)
return;
TextLocation location = locationProvider.Location;
foreach (var node in nodesAwaitingStart)
node.StorePrintStart(location);
nodesAwaitingStart.Clear();
}
public override void StartNode(AstNode node)
{
// ignore whitespace: these don't need to be processed.
@ -45,6 +64,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor @@ -45,6 +64,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
currentList.Add(node);
nodes.Push(currentList);
currentList = new List<AstNode>();
nodesAwaitingStart.Add(node);
}
else if (node is Comment comment)
{
@ -63,6 +83,10 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor @@ -63,6 +83,10 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
// StartNode/EndNode is only called for them to support folding of comments.
if (node.NodeType != NodeType.Whitespace)
{
// A node that printed no tokens of its own collapses to a zero-width span here.
if (nodesAwaitingStart.Remove(node))
node.StorePrintStart(lastTokenEnd);
node.StorePrintEnd(lastTokenEnd);
System.Diagnostics.Debug.Assert(currentList != null);
foreach (var removable in node.Children.Where(n => n is CSharpTokenNode))
{
@ -85,6 +109,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor @@ -85,6 +109,7 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
public override void WriteToken(Role role, string token)
{
AssignPendingStartLocations();
switch (nodes.Peek().LastOrDefault())
{
case EmptyStatement emptyStatement:
@ -100,10 +125,12 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor @@ -100,10 +125,12 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
break;
}
base.WriteToken(role, token);
lastTokenEnd = locationProvider.Location;
}
public override void WriteKeyword(Role role, string keyword)
{
AssignPendingStartLocations();
TextLocation start = locationProvider.Location;
CSharpTokenNode t = null;
if (role is TokenRole)
@ -128,18 +155,22 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor @@ -128,18 +155,22 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
t.Role = role;
}
base.WriteKeyword(role, keyword);
lastTokenEnd = locationProvider.Location;
}
public override void WriteIdentifier(Identifier identifier)
{
AssignPendingStartLocations();
if (!identifier.IsNull)
identifier.SetStartLocation(locationProvider.Location);
currentList.Add(identifier);
base.WriteIdentifier(identifier);
lastTokenEnd = locationProvider.Location;
}
public override void WritePrimitiveValue(object value, LiteralFormat format = LiteralFormat.None)
{
AssignPendingStartLocations();
Expression node = nodes.Peek().LastOrDefault() as Expression;
var startLocation = locationProvider.Location;
base.WritePrimitiveValue(value, format);
@ -151,14 +182,17 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor @@ -151,14 +182,17 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
{
((NullReferenceExpression)node).SetStartLocation(startLocation);
}
lastTokenEnd = locationProvider.Location;
}
public override void WritePrimitiveType(string type)
{
AssignPendingStartLocations();
PrimitiveType node = nodes.Peek().LastOrDefault() as PrimitiveType;
if (node != null)
node.SetStartLocation(locationProvider.Location);
base.WritePrimitiveType(type);
lastTokenEnd = locationProvider.Location;
}
}
}

55
ICSharpCode.Decompiler/CSharp/SequencePointBuilder.cs

@ -119,7 +119,7 @@ namespace ICSharpCode.Decompiler.CSharp @@ -119,7 +119,7 @@ namespace ICSharpCode.Decompiler.CSharp
var blockContainer = blockStatement.Annotation<BlockContainer>();
if (blockContainer != null)
{
StartSequencePoint(blockStatement.LBraceToken);
StartSequencePoint(blockStatement);
int intervalStart;
if (blockContainer.Parent is TryCatchHandler handler && !handler.ExceptionSpecifierILRange.IsEmpty)
{
@ -140,7 +140,7 @@ namespace ICSharpCode.Decompiler.CSharp @@ -140,7 +140,7 @@ namespace ICSharpCode.Decompiler.CSharp
intervals.Add(interval);
current.Intervals.AddRange(intervals);
current.Function = blockContainer.Ancestors.OfType<ILFunction>().FirstOrDefault();
EndSequencePoint(blockStatement.LBraceToken.StartLocation, blockStatement.LBraceToken.EndLocation);
EndSequencePoint(blockStatement.StartLocation, Offset(blockStatement.StartLocation, 1));
}
else
{
@ -157,9 +157,9 @@ namespace ICSharpCode.Decompiler.CSharp @@ -157,9 +157,9 @@ namespace ICSharpCode.Decompiler.CSharp
var implicitReturn = blockStatement.Annotation<ImplicitReturnAnnotation>();
if (implicitReturn != null && !isEnhancedUsing)
{
StartSequencePoint(blockStatement.RBraceToken);
StartSequencePoint(blockStatement);
AddToSequencePoint(implicitReturn.Leave);
EndSequencePoint(blockStatement.RBraceToken.StartLocation, blockStatement.RBraceToken.EndLocation);
EndSequencePoint(Offset(blockStatement.EndLocation, -1), blockStatement.EndLocation);
}
}
@ -214,7 +214,7 @@ namespace ICSharpCode.Decompiler.CSharp @@ -214,7 +214,7 @@ namespace ICSharpCode.Decompiler.CSharp
// add switch statement itself to sequence point
// (call only after the sections are visited)
AddToSequencePoint(switchStatement);
EndSequencePoint(switchStatement.StartLocation, switchStatement.RParToken.EndLocation);
EndSequencePoint(switchStatement.StartLocation, Offset(switchStatement.Expression.EndLocation, 1));
}
public override void VisitSwitchSection(Syntax.SwitchSection switchSection)
@ -298,7 +298,7 @@ namespace ICSharpCode.Decompiler.CSharp @@ -298,7 +298,7 @@ namespace ICSharpCode.Decompiler.CSharp
if (usingStatement.IsEnhanced)
EndSequencePoint(usingStatement.StartLocation, usingStatement.ResourceAcquisition.EndLocation);
else
EndSequencePoint(usingStatement.StartLocation, usingStatement.RParToken.EndLocation);
EndSequencePoint(usingStatement.StartLocation, Offset(usingStatement.ResourceAcquisition.EndLocation, 1));
}
public override void VisitForeachStatement(ForeachStatement foreachStatement)
@ -317,7 +317,7 @@ namespace ICSharpCode.Decompiler.CSharp @@ -317,7 +317,7 @@ namespace ICSharpCode.Decompiler.CSharp
StartSequencePoint(foreachStatement);
AddToSequencePoint(foreachInfo.MoveNextCall);
EndSequencePoint(foreachStatement.InToken.StartLocation, foreachStatement.InToken.EndLocation);
EndSequencePoint(Offset(foreachStatement.VariableDesignation.EndLocation, 1), Offset(foreachStatement.VariableDesignation.EndLocation, 1 + "in".Length));
StartSequencePoint(foreachStatement);
AddToSequencePoint(foreachInfo.GetCurrentCall);
@ -332,7 +332,7 @@ namespace ICSharpCode.Decompiler.CSharp @@ -332,7 +332,7 @@ namespace ICSharpCode.Decompiler.CSharp
lockStatement.Expression.AcceptVisitor(this);
VisitAsSequencePoint(lockStatement.EmbeddedStatement);
AddToSequencePoint(lockStatement);
EndSequencePoint(lockStatement.StartLocation, lockStatement.RParToken.EndLocation);
EndSequencePoint(lockStatement.StartLocation, Offset(lockStatement.Expression.EndLocation, 1));
}
public override void VisitIfElseStatement(IfElseStatement ifElseStatement)
@ -342,7 +342,7 @@ namespace ICSharpCode.Decompiler.CSharp @@ -342,7 +342,7 @@ namespace ICSharpCode.Decompiler.CSharp
VisitAsSequencePoint(ifElseStatement.TrueStatement);
VisitAsSequencePoint(ifElseStatement.FalseStatement);
AddToSequencePoint(ifElseStatement);
EndSequencePoint(ifElseStatement.StartLocation, ifElseStatement.RParToken.EndLocation);
EndSequencePoint(ifElseStatement.StartLocation, Offset(ifElseStatement.Condition.EndLocation, 1));
}
public override void VisitWhileStatement(WhileStatement whileStatement)
@ -351,7 +351,7 @@ namespace ICSharpCode.Decompiler.CSharp @@ -351,7 +351,7 @@ namespace ICSharpCode.Decompiler.CSharp
whileStatement.Condition.AcceptVisitor(this);
VisitAsSequencePoint(whileStatement.EmbeddedStatement);
AddToSequencePoint(whileStatement);
EndSequencePoint(whileStatement.StartLocation, whileStatement.RParToken.EndLocation);
EndSequencePoint(whileStatement.StartLocation, Offset(whileStatement.Condition.EndLocation, 1));
}
public override void VisitDoWhileStatement(DoWhileStatement doWhileStatement)
@ -360,7 +360,8 @@ namespace ICSharpCode.Decompiler.CSharp @@ -360,7 +360,8 @@ namespace ICSharpCode.Decompiler.CSharp
VisitAsSequencePoint(doWhileStatement.EmbeddedStatement);
doWhileStatement.Condition.AcceptVisitor(this);
AddToSequencePoint(doWhileStatement);
EndSequencePoint(doWhileStatement.WhileToken.StartLocation, doWhileStatement.RParToken.EndLocation);
// The trailing 'while (' precedes the condition: "while" + ' ' + '('.
EndSequencePoint(Offset(doWhileStatement.Condition.StartLocation, -("while".Length + 2)), Offset(doWhileStatement.Condition.EndLocation, 1));
}
public override void VisitFixedStatement(FixedStatement fixedStatement)
@ -389,17 +390,18 @@ namespace ICSharpCode.Decompiler.CSharp @@ -389,17 +390,18 @@ namespace ICSharpCode.Decompiler.CSharp
var tryCatchHandler = catchClause.Annotation<TryCatchHandler>();
if (tryCatchHandler != null && !tryCatchHandler.ExceptionSpecifierILRange.IsEmpty)
{
StartSequencePoint(catchClause.CatchToken);
StartSequencePoint(catchClause);
var function = tryCatchHandler.Ancestors.OfType<ILFunction>().FirstOrDefault();
AddToSequencePointRaw(function, new[] { tryCatchHandler.ExceptionSpecifierILRange });
EndSequencePoint(catchClause.CatchToken.StartLocation, catchClause.RParToken.IsNull ? catchClause.CatchToken.EndLocation : catchClause.RParToken.EndLocation);
EndSequencePoint(catchClause.StartLocation, CatchHeaderEnd(catchClause));
}
}
else
{
StartSequencePoint(catchClause.WhenToken);
StartSequencePoint(catchClause);
AddToSequencePoint(catchClause.Condition);
EndSequencePoint(catchClause.WhenToken.StartLocation, catchClause.CondRParToken.EndLocation);
// The 'when (' precedes the condition: "when" + ' ' + '('.
EndSequencePoint(Offset(catchClause.Condition.StartLocation, -("when".Length + 2)), Offset(catchClause.Condition.EndLocation, 1));
}
VisitAsSequencePoint(catchClause.Body);
}
@ -413,6 +415,29 @@ namespace ICSharpCode.Decompiler.CSharp @@ -413,6 +415,29 @@ namespace ICSharpCode.Decompiler.CSharp
current = new StatePerSequencePoint(primaryNode);
}
// Token nodes are not part of the AST, so the brace/parenthesis/keyword positions that
// statement-header sequence points address are derived from the surrounding real nodes plus a
// fixed column offset. The offsets assume the decompiler's own formatting -- one space before a
// header's '(' and none within it, single spaces around keywords -- which holds because output is
// always printed with DecompilerSettings.CSharpFormattingOptions (CreateAllman plus those
// overrides). Sequence points are only valid under that formatting: printing the tree with a
// customized CSharpFormattingOptions (e.g. SpacesWithinWhileParentheses) before generating them
// would shift these columns and produce wrong PDB line/column mappings.
static TextLocation Offset(TextLocation location, int columns)
{
return new TextLocation(location.Line, location.Column + columns);
}
// End of a 'catch' header: the ')' after the variable (or the type, if unnamed), or just past
// the 'catch' keyword when there is no exception specifier.
static TextLocation CatchHeaderEnd(CatchClause catchClause)
{
if (catchClause.Type.IsNull)
return Offset(catchClause.StartLocation, "catch".Length);
AstNode beforeRParen = catchClause.VariableNameToken.IsNull ? catchClause.Type : catchClause.VariableNameToken;
return Offset(beforeRParen.EndLocation, 1);
}
void EndSequencePoint(TextLocation startLocation, TextLocation endLocation)
{
Debug.Assert(!startLocation.IsEmpty, "missing startLocation");

27
ICSharpCode.Decompiler/CSharp/Syntax/AstNode.cs

@ -68,24 +68,35 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax @@ -68,24 +68,35 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
}
}
TextLocation startLocation = TextLocation.Empty;
TextLocation endLocation = TextLocation.Empty;
// Source locations are assigned at print time (the output visitor brackets every node with
// StartNode/EndNode and InsertMissingTokensDecorator records the writer's position) rather than
// computed by recursion to child token positions. A node's span runs from its first printed
// token to its last; leaf nodes that print as a single token override these with their own value.
public virtual TextLocation StartLocation {
get {
var child = firstChild;
if (child == null)
return TextLocation.Empty;
return child.StartLocation;
return startLocation;
}
}
public virtual TextLocation EndLocation {
get {
var child = lastChild;
if (child == null)
return TextLocation.Empty;
return child.EndLocation;
return endLocation;
}
}
internal void StorePrintStart(TextLocation value)
{
startLocation = value;
}
internal void StorePrintEnd(TextLocation value)
{
endLocation = value;
}
public AstNode? Parent {
get { return parent; }
}

9
ICSharpCode.Decompiler/CSharp/Syntax/Statements/ForeachStatement.cs

@ -36,14 +36,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax @@ -36,14 +36,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
public static readonly TokenRole ForeachKeywordRole = new TokenRole("foreach");
public static readonly TokenRole InKeywordRole = new TokenRole("in");
public CSharpTokenNode AwaitToken {
get { return GetChildByRole(AwaitRole); }
}
public bool IsAsync {
get { return !GetChildByRole(AwaitRole).IsNull; }
set { SetChildByRole(AwaitRole, value ? new CSharpTokenNode(TextLocation.Empty, null) : null); }
}
public bool IsAsync { get; set; }
public CSharpTokenNode ForeachToken {
get { return GetChildByRole(ForeachKeywordRole); }

9
ICSharpCode.Decompiler/CSharp/Syntax/Statements/UsingStatement.cs

@ -40,14 +40,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax @@ -40,14 +40,7 @@ namespace ICSharpCode.Decompiler.CSharp.Syntax
get { return GetChildByRole(UsingKeywordRole); }
}
public CSharpTokenNode AwaitToken {
get { return GetChildByRole(AwaitRole); }
}
public bool IsAsync {
get { return !GetChildByRole(AwaitRole).IsNull; }
set { SetChildByRole(AwaitRole, value ? new CSharpTokenNode(TextLocation.Empty, null) : null); }
}
public bool IsAsync { get; set; }
public CSharpTokenNode LParToken {
get { return GetChildByRole(Roles.LPar); }

Loading…
Cancel
Save