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); }