Chapter 27 - The Roslyn Compiler
Syntax TreesUsing 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); }