Chapter 27 - The Roslyn Compiler

Syntax Trees

Using the LINQPad Syntax Visualizer

/*  In LINQPad, the output window shows you the Roslyn Syntax Tree automatically.
  Run this query, then click 'Tree'.
  
  Note that you can update the source code and click the 'Refresh' button to 
  update the syntax tree without re-running the query. */

class Test
{
  static void Main() => Console.WriteLine ("Hello");
}

Obtaining a syntax tree

SyntaxTree tree = CSharpSyntaxTree.ParseText (@"class Test
{
  static void Main() => Console.WriteLine (""Hello"");
}");

Console.WriteLine (tree.ToString());

tree.DumpSyntaxTree();    // Displays Syntax Tree Visualizer in LINQPad

Traversing a tree - root node

var tree = CSharpSyntaxTree.ParseText (@"class Test
{
  static void Main() => Console.WriteLine (""Hello"");
}");

SyntaxNode root = tree.GetRoot();
Console.WriteLine (root.GetType().Name);   // CompilationUnitSyntax

Traversing a tree - ClassDeclarationSyntax members

var tree = CSharpSyntaxTree.ParseText (@"class Test
{
  static void Main() => Console.WriteLine (""Hello"");
}");

SyntaxNode root = tree.GetRoot();

var cds = (ClassDeclarationSyntax) root.ChildNodes().Single();

foreach (MemberDeclarationSyntax member in cds.Members)
  Console.WriteLine (member.ToString());

Traversing a tree - descendant tokens

var tree = CSharpSyntaxTree.ParseText (@"class Test
{
  static void Main() => Console.WriteLine (""Hello"");
}");

SyntaxNode root = tree.GetRoot();

var cds = (ClassDeclarationSyntax) root.ChildNodes().Single();

root.DescendantTokens()
  .Select (t => new { Kind = t.Kind(), t.Text })
  .Dump();

Traversing a tree - finding a method

var tree = CSharpSyntaxTree.ParseText (@"class Test
{
  static void Main() => Console.WriteLine (""Hello"");
}");

SyntaxNode root = tree.GetRoot();

root.DescendantNodes()
  .First (m => m.Kind() == SyntaxKind.MethodDeclaration)
  .Dump(1);

root.DescendantNodes()
  .OfType<MethodDeclarationSyntax>()
  .Single()
  .Dump(1);
  
root.DescendantNodes()
  .OfType<MethodDeclarationSyntax>()
  .Single (m => m.Identifier.Text == "Main")
  .Dump(1);

root.DescendantNodes()
  .First (m =>
       m.Kind() == SyntaxKind.MethodDeclaration &&
       m.ChildTokens().Any (t => t.Kind() == SyntaxKind.IdentifierToken && t.Text == "Main"))
   .Dump(1);

CSharpSyntaxWalker

void Main()
{
  var tree = CSharpSyntaxTree.ParseText (@"class Test
{
  static void Main()
  {
    if (true)
    if (true);
  };
}");

  SyntaxNode root = tree.GetRoot();

  var ifCounter = new IfCounter ();
  ifCounter.Visit (root);
  Console.WriteLine ($"I found {ifCounter.IfCount} if statements");

  root.DescendantNodes().OfType<IfStatementSyntax>().Count().Dump ("Functional equivalent");
}

class IfCounter : CSharpSyntaxWalker
{
  public int IfCount { get; private set; }

  public override void VisitIfStatement (IfStatementSyntax node)
  {
    IfCount++;
    // Call the base method if you want to descend into children.
    base.VisitIfStatement (node);
  }
}

The White Walker

void Main()
{
  var tree = CSharpSyntaxTree.ParseText (@"class Test
{
  static void Main()
  {
    if (true)
    if (true);
  };
}");

  SyntaxNode root = tree.GetRoot();

  var whiteWalker = new WhiteWalker ();
  whiteWalker.Visit (root);
  whiteWalker.SpaceCount.Dump ("spaces");
}

class WhiteWalker : CSharpSyntaxWalker   // Counts space characters
{
  public int SpaceCount { get; private set; }

  public WhiteWalker() : base (SyntaxWalkerDepth.Trivia) { }

  public override void VisitTrivia (SyntaxTrivia trivia)
  {
    SpaceCount += trivia.ToString().Count (char.IsWhiteSpace);
    base.VisitTrivia (trivia);
  }
}

Trivia

var tree = CSharpSyntaxTree.ParseText (@"class Program
{
    static /*comment*/ void Main() {}
}");

SyntaxNode root = tree.GetRoot();

// Find the static keyword token:
var method = root.DescendantTokens().Single (t =>
   t.Kind() == SyntaxKind.StaticKeyword);

// Print out the trivia around the static keyword token:
foreach (SyntaxTrivia t in method.LeadingTrivia)
  Console.WriteLine (new { Kind = "Leading " + t.Kind(), t.Span.Length });

foreach (SyntaxTrivia t in method.TrailingTrivia)
  Console.WriteLine (new { Kind = "Trailing " + t.Kind(), t.Span.Length });

Preprocessor directives

// Take a look at the syntax tree in the output window. The grey text is parsed as DisabledTextTrivia.

#define FOO

#if FOO
    Console.WriteLine ("FOO is defined");
#else
    Console.WriteLine ("FOO is not defined");
#endif

Structured trivia

var tree = CSharpSyntaxTree.ParseText (@"#define FOO");

// In LINQPad:
tree.DumpSyntaxTree();  // LINQPad displays structured trivia in Visualizer

SyntaxNode root = tree.GetRoot();

var trivia = root.DescendantTrivia().First();
Console.WriteLine (trivia.HasStructure);           // True
Console.WriteLine (trivia.GetStructure().Kind());  // DefineDirectiveTrivia

Structured trivia - navigating preprocessor directives

var tree = CSharpSyntaxTree.ParseText (@"#define FOO");
SyntaxNode root = tree.GetRoot();

Console.WriteLine (root.ContainsDirectives);      // True

// directive is the root node of the structured trivia:
var directive = root.GetFirstDirective();
Console.WriteLine (directive.Kind());             // DefineDirectiveTrivia
Console.WriteLine (directive.ToString());         // #define FOO

// If there were more directives, we could get to them as follows:
Console.WriteLine (directive.GetNextDirective());    // (null)

var hashDefine = (DefineDirectiveTriviaSyntax)root.GetFirstDirective();
Console.WriteLine (hashDefine.Name.Text);     // FOO

Transforming trees - handling changes to source code

SourceText sourceText = SourceText.From ("class Program {}");
var tree = CSharpSyntaxTree.ParseText (sourceText);

var newSource = sourceText.Replace (0, 5, "struct");

var newTree = tree.WithChangedText (newSource);
Console.WriteLine (newTree.ToString());         // struct Program {}

Transforming trees - visualizing node

CSharpSyntaxTree.ParseText ("using System.Text;").DumpSyntaxTree();

Transforming trees - creating nodes and tokens

QualifiedNameSyntax qualifiedName = SyntaxFactory.QualifiedName (
  SyntaxFactory.IdentifierName ("System"),
  SyntaxFactory.IdentifierName ("Text"));

UsingDirectiveSyntax usingDirective =
  SyntaxFactory.UsingDirective (qualifiedName);

Console.WriteLine (usingDirective.ToFullString());  // usingSystem.Text;

// Explicitly adding trivia:
usingDirective = usingDirective.WithUsingKeyword (
  usingDirective.UsingKeyword.WithTrailingTrivia (
  SyntaxFactory.Whitespace (" ")));

Console.WriteLine (usingDirective.ToFullString());  // using System.Text;

var existingTree = CSharpSyntaxTree.ParseText ("class Program {}");
var existingUnit = (CompilationUnitSyntax)existingTree.GetRoot();

var unitWithUsing = existingUnit.AddUsings (usingDirective);

var treeWithUsing = CSharpSyntaxTree.Create (unitWithUsing.NormalizeWhitespace());
treeWithUsing.ToString().Dump ("Final result");

Transforming trees - creating a new tree

QualifiedNameSyntax qualifiedName = SyntaxFactory.QualifiedName (
  SyntaxFactory.IdentifierName ("System"),
  SyntaxFactory.IdentifierName ("Text"));

UsingDirectiveSyntax usingDirective =
  SyntaxFactory.UsingDirective (qualifiedName);

var unit = SyntaxFactory.CompilationUnit().AddUsings (usingDirective);

// Create a simple empty class definition:
unit = unit.AddMembers (SyntaxFactory.ClassDeclaration ("Program"));

var tree = CSharpSyntaxTree.Create (unit.NormalizeWhitespace());
Console.WriteLine (tree.ToString());

Transforming trees - CSharpSyntaxRewriter

void Main()
{
  var tree = CSharpSyntaxTree.ParseText (@"class Program
{
  static void Main() { Test(); }
  static void Test() {         }
}");

  var rewriter = new MyRewriter();
  var newRoot = rewriter.Visit (tree.GetRoot());
  Console.WriteLine (newRoot.ToFullString());
}

class MyRewriter : CSharpSyntaxRewriter
{
  public override SyntaxNode VisitMethodDeclaration
    (MethodDeclarationSyntax node)
  {
    // "Replace" the method’s identifier with an uppercase version:
    return node.WithIdentifier (
      SyntaxFactory.Identifier (
      node.Identifier.LeadingTrivia,            // Preserve old trivia
      node.Identifier.Text.ToUpperInvariant(),
      node.Identifier.TrailingTrivia));         // Preserve old trivia
  }
}
Compilations and Semantic Models

Creating a Compilation

var compilation = CSharpCompilation.Create ("test");

compilation = compilation.WithOptions (
  new CSharpCompilationOptions (OutputKind.ConsoleApplication));

var tree = CSharpSyntaxTree.ParseText (@"class Program 
{
  static void Main() => System.Console.WriteLine (""Hello"");
}");

compilation = compilation.AddSyntaxTrees (tree);

string trustedAssemblies = (string)AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES");
var trustedAssemblyPaths = trustedAssemblies.Split (Path.PathSeparator);
var references = trustedAssemblyPaths.Select (path => MetadataReference.CreateFromFile (path));

compilation = compilation.AddReferences (references);

// Or, in one step:

compilation = CSharpCompilation
  .Create ("test")
  .WithOptions (new CSharpCompilationOptions (OutputKind.ConsoleApplication))
  .AddSyntaxTrees (tree)
  .AddReferences (references);
  
compilation.GetDiagnostics().Dump ("Errors and warnings");

Emitting

var tree = CSharpSyntaxTree.ParseText (@"class Program 
{
  static void Main() => System.Console.WriteLine (""Hello"");
}");

string trustedAssemblies = (string)AppContext.GetData ("TRUSTED_PLATFORM_ASSEMBLIES");
var trustedAssemblyPaths = trustedAssemblies.Split (Path.PathSeparator);
var references = trustedAssemblyPaths.Select (path => MetadataReference.CreateFromFile (path));

var compilation = CSharpCompilation
  .Create ("test")
  .WithOptions (new CSharpCompilationOptions (OutputKind.ConsoleApplication))
  .AddSyntaxTrees (tree)
  .AddReferences (references);

string outputPath = "test.dll";
EmitResult result = compilation.Emit (outputPath);
Console.WriteLine (result.Success);

// Execute the program we just compiled.
Util.Cmd (@"dotnet.exe", "\"" + outputPath + "\"");

Querying the semantic model

var tree = CSharpSyntaxTree.ParseText (@"class Program 
{
  static void Main() => System.Console.WriteLine (123);
}");

var references = ((string)AppContext.GetData ("TRUSTED_PLATFORM_ASSEMBLIES"))
  .Split (Path.PathSeparator)
  .Select (path => MetadataReference.CreateFromFile (path));

var compilation = CSharpCompilation.Create ("test")
  .AddReferences (references)
  .AddSyntaxTrees (tree);

SemanticModel model = compilation.GetSemanticModel (tree);

var writeLineNode = tree.GetRoot().DescendantTokens().Single (
   t => t.Text == "WriteLine").Parent;

SymbolInfo symbolInfo = model.GetSymbolInfo (writeLineNode);
Console.WriteLine (symbolInfo.Symbol.ToString());  // System.Console.WriteLine(int)

symbolInfo.Symbol.Dump ("In more detail", 2);

Semantic model - symbols

var tree = CSharpSyntaxTree.ParseText (@"class Program 
{
  static void Main() => System.Console.WriteLine (123);
}");

var references = ((string)AppContext.GetData ("TRUSTED_PLATFORM_ASSEMBLIES"))
  .Split (Path.PathSeparator)
  .Select (path => MetadataReference.CreateFromFile (path));

var compilation = CSharpCompilation.Create ("test")
  .AddReferences (references)
  .AddSyntaxTrees (tree);

SemanticModel model = compilation.GetSemanticModel (tree);

var writeLineNode = tree.GetRoot().DescendantTokens().Single (
   t => t.Text == "WriteLine").Parent;

ISymbol symbol = model.GetSymbolInfo (writeLineNode).Symbol;

Console.WriteLine (symbol.Name);                   // WriteLine
Console.WriteLine (symbol.Kind);                   // Method
Console.WriteLine (symbol.IsStatic);               // True
Console.WriteLine (symbol.ContainingType.Name);    // Console

var method = (IMethodSymbol)symbol;
Console.WriteLine (method.ReturnType.ToString());  // void

Console.WriteLine (symbol.Language);                // C#

var location = symbol.Locations.First();
Console.WriteLine (location.Kind);                       // MetadataFile
Console.WriteLine (location.MetadataModule 
                   == compilation.References.Single());  // Trye
Console.WriteLine (location.SourceTree == null);         // True
Console.WriteLine (location.SourceSpan);                 // [0..0)

symbol.ContainingType.GetMembers ("WriteLine").OfType<IMethodSymbol>()
  .Select (m => m.ToString()).Dump();

Semantic model - declared symbols

var tree = CSharpSyntaxTree.ParseText (@"class Program 
{
  static void Main() => System.Console.WriteLine (123);
}");

var references = ((string)AppContext.GetData ("TRUSTED_PLATFORM_ASSEMBLIES"))
  .Split (Path.PathSeparator)
  .Select (path => MetadataReference.CreateFromFile (path));

var compilation = CSharpCompilation.Create ("test")
  .AddReferences (references)
  .AddSyntaxTrees (tree);

SemanticModel model = compilation.GetSemanticModel (tree);

var mainMethod = tree.GetRoot().DescendantTokens().Single (
  t => t.Text == "Main").Parent;

SymbolInfo symbolInfo = model.GetSymbolInfo (mainMethod);
Console.WriteLine (symbolInfo.Symbol == null);              // True
Console.WriteLine (symbolInfo.CandidateSymbols.Length);     // 0

ISymbol symbol = model.GetDeclaredSymbol (mainMethod);
symbol.Dump(1);

Semantic model - declared symbol local variable

var tree = CSharpSyntaxTree.ParseText (@"class Program 
{
  static void Main()
  {
    int xyz = 123;
  }
}");

var references = ((string)AppContext.GetData ("TRUSTED_PLATFORM_ASSEMBLIES"))
  .Split (Path.PathSeparator)
  .Select (path => MetadataReference.CreateFromFile (path));

var compilation = CSharpCompilation.Create ("test")
  .AddReferences (references)
  .AddSyntaxTrees (tree);

SemanticModel model = compilation.GetSemanticModel (tree);

SyntaxNode variableDecl = tree.GetRoot().DescendantTokens().Single (
  t => t.Text == "xyz").Parent;

var local = (ILocalSymbol) model.GetDeclaredSymbol (variableDecl);
Console.WriteLine (local.Type.ToString());             // int
Console.WriteLine (local.Type.BaseType.ToString());    // System.ValueType

// If the following line throws an InvalidOperationException, you will need to update to the latest LINQPad beta.
local.Type.Dump ("More detail", 1);

Semantic model - TypeInfo

var tree = CSharpSyntaxTree.ParseText (@"class Program 
{
  static void Main()
  {
    var now = System.DateTime.Now;
    System.Console.WriteLine (now - now);
  }
}");

var references = ((string)AppContext.GetData ("TRUSTED_PLATFORM_ASSEMBLIES"))
  .Split (Path.PathSeparator)
  .Select (path => MetadataReference.CreateFromFile (path));

var compilation = CSharpCompilation.Create ("test")
  .AddReferences (references)
  .AddSyntaxTrees (tree);

SemanticModel model = compilation.GetSemanticModel (tree);

SyntaxNode binaryExpr = tree.GetRoot().DescendantTokens().Single (
  t => t.Text == "-").Parent;

var typeInfo = model.GetTypeInfo (binaryExpr);

Console.WriteLine (typeInfo.Type.ToString());             // System.TimeSpan
Console.WriteLine (typeInfo.ConvertedType.ToString());    // object

Semantic model - looking up symbols

var tree = CSharpSyntaxTree.ParseText (@"class Program
{
  static void Main()
  {
    int x = 123, y = 234;

  }
}");

var references = ((string)AppContext.GetData ("TRUSTED_PLATFORM_ASSEMBLIES"))
  .Split (Path.PathSeparator)
  .Select (path => MetadataReference.CreateFromFile (path));

var compilation = CSharpCompilation.Create ("test")
  .AddReferences (references)
  .AddSyntaxTrees (tree);

SemanticModel model = compilation.GetSemanticModel (tree);

// Look for available symbols at start of 6th line:
int index = tree.GetText().Lines[5].Start;

foreach (ISymbol symbol in model.LookupSymbols (index))
  Console.WriteLine (symbol.ToString());

// If the following line throws an InvalidOperationException, you will need to update to the latest LINQPad beta.
model.LookupSymbols (index).Dump ("More detail", 1);

Renaming a symbol

void Main()
{
  var tree = CSharpSyntaxTree.ParseText (@"class Program
{
  static Program() {}
  public Program() {}

  static void Main()
  {
    Program p = new Program();
    p.Foo();
  }

  static void Foo() => Bar();
  static void Bar() => Foo();  
}
");

  var references = ((string)AppContext.GetData ("TRUSTED_PLATFORM_ASSEMBLIES"))
    .Split (Path.PathSeparator)
    .Select (path => MetadataReference.CreateFromFile (path));

  var compilation = CSharpCompilation.Create ("test")
    .AddReferences (references)
    .AddSyntaxTrees (tree);

  var model = compilation.GetSemanticModel (tree);

  var tokens = tree.GetRoot().DescendantTokens();

  // Rename the Program class to Program2:
  SyntaxToken program = tokens.First (t => t.Text == "Program");
  Console.WriteLine (RenameSymbol (model, program, "Program2").ToString());

  // Rename the Foo method to Foo2:
  SyntaxToken foo = tokens.Last (t => t.Text == "Foo");
  Console.WriteLine (RenameSymbol (model, foo, "Foo2").ToString());

  // Rename the p local variable to p2:
  SyntaxToken p = tokens.Last (t => t.Text == "p");
  Console.WriteLine (RenameSymbol (model, p, "p2").ToString());
}

public SyntaxTree RenameSymbol (SemanticModel model, SyntaxToken token,
                string newName)
{
  IEnumerable<TextSpan> renameSpans =
    GetRenameSpans (model, token).OrderBy (s => s);

  SourceText newSourceText = model.SyntaxTree.GetText().WithChanges (
    renameSpans.Select (s => new TextChange (s, newName)));

  return model.SyntaxTree.WithChangedText (newSourceText);
}

public IEnumerable<TextSpan> GetRenameSpans (SemanticModel model,
                       SyntaxToken token)
{
  var node = token.Parent;

  ISymbol symbol =
    model.GetSymbolInfo (node).Symbol ??
    model.GetDeclaredSymbol (node);

  if (symbol == null) return null;   // No symbol to rename.

  var definitions =
    from location in symbol.Locations
    where location.SourceTree == node.SyntaxTree
    select location.SourceSpan;

  var usages =
    from t in model.SyntaxTree.GetRoot().DescendantTokens ()
    where t.Text == symbol.Name
    let s = model.GetSymbolInfo (t.Parent).Symbol
    where s == symbol
    select t.Span;

  if (symbol.Kind != SymbolKind.NamedType)
    return definitions.Concat (usages);

  var structors =
    from type in model.SyntaxTree.GetRoot().DescendantNodes()
                       .OfType<TypeDeclarationSyntax>()
    where type.Identifier.Text == symbol.Name
    let declaredSymbol = model.GetDeclaredSymbol (type)
    where declaredSymbol == symbol
    from method in type.Members
    let constructor = method as ConstructorDeclarationSyntax
    let destructor = method as DestructorDeclarationSyntax
    where constructor != null || destructor != null
    let identifier = constructor?.Identifier ?? destructor.Identifier
    select identifier.Span;

  return definitions.Concat (usages).Concat (structors);
}
C# 12 in a Nutshell
Buy from amazon.com Buy print or Kindle edition
Buy from ebooks.com Buy PDF edition
Buy from O'Reilly Read via O'Reilly subscription