Structured trivia are an interesting corner case within Roslyn. Take a look at the following syntax tree representing a simple program with regions.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class MyClass | |
{ | |
#region MyRegion | |
#endregion | |
} |
Typically the terminals of a Roslyn syntax tree are pieces of trivia. However, in the above program we can see that a RegionDirectiveTrivia
has a child RegionDirectiveTriviaSyntax
syntax node.
Well, why don’t we try getting access to these syntax nodes? Originally, I tried the following:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var tree = CSharpSyntaxTree.ParseText(@" | |
public class MyClass{ | |
#region MyRegion | |
#endregion | |
}"); | |
//Try finding them as nodes. | |
var regionNodes = tree.GetRoot().DescendantNodes().OfType<RegionDirectiveTriviaSyntax>(); | |
//Try finding them as trivia? | |
var regionTrivia = tree.GetRoot().DescendantTrivia().OfType<RegionDirectiveTriviaSyntax>(); |
Oddly enough (to me, at least), neither of these approaches worked and both returned nothing. They also didn’t throw or indicate any error on my part. Thankfully @Pilchie pointed out that DescendantNodes
has a couple of optional arguments.
Func<SyntaxNode, bool> descendIntoChildren -
A function that decides whether or not we should descend into the children of a given node. We could use this to avoid descending into nodes we know we don’t care about.bool descendIntoTrivia
– A simple boolean that signals if we’d like to descend into the children of trivia when searching for nodes.
In our case, we’d like to search for syntax nodes within trivia, so we’ll signal that.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var tree = CSharpSyntaxTree.ParseText(@" | |
public class MyClass{ | |
#region MyRegion | |
#endregion | |
}"); | |
//Descend into all node children | |
//Descend into all trivia children | |
var regionNodes = tree.GetRoot().DescendantNodes(descendIntoChildren: null, descendIntoTrivia: true).OfType<RegionDirectiveTriviaSyntax>(); |
This time, everything works as we’d expect and we get access to our RegionDirectiveTriviaSyntax
node as expected.
So why does Roslyn avoid descending into trivia by default? My guess is performance. Most consumers of Roslyn will be looking for nodes such as methods, properties, and fields. None of these are contained within syntax trivia so it would be a waste of CPU cycles to consider their children.
This is something to be aware of when working with structured trivia and something that’s not immediately obvious.