diff --git a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
index 617eaebb0..f51b3cd32 100644
--- a/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
+++ b/ICSharpCode.Decompiler.Tests/ICSharpCode.Decompiler.Tests.csproj
@@ -101,6 +101,7 @@
+
diff --git a/ICSharpCode.Decompiler.Tests/Output/LocationsInAstTests.cs b/ICSharpCode.Decompiler.Tests/Output/LocationsInAstTests.cs
index 10425eb71..d6a8d9acf 100644
--- a/ICSharpCode.Decompiler.Tests/Output/LocationsInAstTests.cs
+++ b/ICSharpCode.Decompiler.Tests/Output/LocationsInAstTests.cs
@@ -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;
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
// 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
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
// 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
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));
+ }
}
}
diff --git a/ICSharpCode.Decompiler.Tests/TestCases/LocationsInAst/LocationsInAstSamples.cs b/ICSharpCode.Decompiler.Tests/TestCases/LocationsInAst/LocationsInAstSamples.cs
new file mode 100644
index 000000000..4013465ed
--- /dev/null
+++ b/ICSharpCode.Decompiler.Tests/TestCases/LocationsInAst/LocationsInAstSamples.cs
@@ -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;
+ }
+ }
+}
diff --git a/ICSharpCode.Decompiler/CSharp/OutputVisitor/InsertMissingTokensDecorator.cs b/ICSharpCode.Decompiler/CSharp/OutputVisitor/InsertMissingTokensDecorator.cs
index 63f2e57c2..d21ab7104 100644
--- a/ICSharpCode.Decompiler/CSharp/OutputVisitor/InsertMissingTokensDecorator.cs
+++ b/ICSharpCode.Decompiler/CSharp/OutputVisitor/InsertMissingTokensDecorator.cs
@@ -29,6 +29,15 @@ namespace ICSharpCode.Decompiler.CSharp.OutputVisitor
List 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 nodesAwaitingStart = new List();
+
+ // 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
currentList = new List();
}
+ 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
currentList.Add(node);
nodes.Push(currentList);
currentList = new List();
+ nodesAwaitingStart.Add(node);
}
else if (node is Comment comment)
{
@@ -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
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
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
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
{
((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;
}
}
}
diff --git a/ICSharpCode.Decompiler/CSharp/SequencePointBuilder.cs b/ICSharpCode.Decompiler/CSharp/SequencePointBuilder.cs
index 150191ead..171b177c0 100644
--- a/ICSharpCode.Decompiler/CSharp/SequencePointBuilder.cs
+++ b/ICSharpCode.Decompiler/CSharp/SequencePointBuilder.cs
@@ -119,7 +119,7 @@ namespace ICSharpCode.Decompiler.CSharp
var blockContainer = blockStatement.Annotation();
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
intervals.Add(interval);
current.Intervals.AddRange(intervals);
current.Function = blockContainer.Ancestors.OfType().FirstOrDefault();
- EndSequencePoint(blockStatement.LBraceToken.StartLocation, blockStatement.LBraceToken.EndLocation);
+ EndSequencePoint(blockStatement.StartLocation, Offset(blockStatement.StartLocation, 1));
}
else
{
@@ -157,9 +157,9 @@ namespace ICSharpCode.Decompiler.CSharp
var implicitReturn = blockStatement.Annotation();
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
// 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
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
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
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
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
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
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
var tryCatchHandler = catchClause.Annotation();
if (tryCatchHandler != null && !tryCatchHandler.ExceptionSpecifierILRange.IsEmpty)
{
- StartSequencePoint(catchClause.CatchToken);
+ StartSequencePoint(catchClause);
var function = tryCatchHandler.Ancestors.OfType().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
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");
diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/AstNode.cs b/ICSharpCode.Decompiler/CSharp/Syntax/AstNode.cs
index 8aded0b75..5b8010079 100644
--- a/ICSharpCode.Decompiler/CSharp/Syntax/AstNode.cs
+++ b/ICSharpCode.Decompiler/CSharp/Syntax/AstNode.cs
@@ -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; }
}
diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Statements/ForeachStatement.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Statements/ForeachStatement.cs
index 703517afe..acb74bae0 100644
--- a/ICSharpCode.Decompiler/CSharp/Syntax/Statements/ForeachStatement.cs
+++ b/ICSharpCode.Decompiler/CSharp/Syntax/Statements/ForeachStatement.cs
@@ -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); }
diff --git a/ICSharpCode.Decompiler/CSharp/Syntax/Statements/UsingStatement.cs b/ICSharpCode.Decompiler/CSharp/Syntax/Statements/UsingStatement.cs
index 3e90d5854..d853b2b8c 100644
--- a/ICSharpCode.Decompiler/CSharp/Syntax/Statements/UsingStatement.cs
+++ b/ICSharpCode.Decompiler/CSharp/Syntax/Statements/UsingStatement.cs
@@ -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); }