Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Remove Extraneous Semicolons in C# Using Roslyn - (replace w empty trivia)

Tags:

c#

.net

roslyn

I've figured out how to open a solution and then iterate through the Projects and then Documents. I'm stuck with how to look for C# Classes, Enums, Structs, and Interfaces that may have an extraneous semicolon at the end of the declaration (C++ style). I'd like to remove those and save the .cs files back to disk. There are approximately 25 solutions written at my current company that I would run this against. Note: The reason we are doing this is to move forward with a better set of coding standards. (And I'd like to learn how to use Roslyn to do these 'simple' adjustments)

Example (UPDATED):

class Program
{
    static void Main(string[] args)
    {
        string solutionFile = @"S:\source\dotnet\SimpleApp\SimpleApp.sln";
        IWorkspace workspace = Workspace.LoadSolution(solutionFile);
        var proj = workspace.CurrentSolution.Projects.First();
        var doc = proj.Documents.First();
        var root = (CompilationUnitSyntax)doc.GetSyntaxRoot();
        var classes = root.DescendantNodes().OfType<ClassDeclarationSyntax>();
        foreach (var decl in classes)
        {
            ProcessClass(decl);
        }
        Console.ReadKey();

    }

    private static SyntaxNode ProcessClass(ClassDeclarationSyntax node)
    {
        ClassDeclarationSyntax newNode;
        if (node.HasTrailingTrivia)
        {
            foreach (var t in node.GetTrailingTrivia())
            {
                var es = new SyntaxTrivia();
                es.Kind = SyntaxKind.EmptyStatement;
                // kind is readonly - what is the right way to create
                // the right SyntaxTrivia?
                if (t.Kind == SyntaxKind.EndOfLineTrivia)
                {
                    node.ReplaceTrivia(t, es);
                }
            }
            return // unsure how to do transform and return it
        }
    }

Example Code I Want to Transform

using System;

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
};
// note: the semicolon at the end of the Person class
like image 960
BuddyJoe Avatar asked Jan 29 '14 15:01

BuddyJoe


2 Answers

Here is a little program that removes the optional semicolon after all class-, struct-, interface and enum-declarations within a solution. The program loops through documents within the solution, and uses a SyntaxWriter for rewriting the syntaxtree. If any changes were made, the original code-files are overwritten with the new syntax.

using System;
using System.IO;
using System.Linq;
using Roslyn.Compilers.CSharp;
using Roslyn.Services;

namespace TrailingSemicolon
{
  class Program
  {
    static void Main(string[] args)
    {
      string solutionfile = @"c:\temp\mysolution.sln";
      var workspace = Workspace.LoadSolution(solutionfile);
      var solution = workspace.CurrentSolution;

      var rewriter = new TrailingSemicolonRewriter();

      foreach (var project in solution.Projects)
      {
        foreach (var document in project.Documents)
        {
          SyntaxTree tree = (SyntaxTree)document.GetSyntaxTree();

          var newSource = rewriter.Visit(tree.GetRoot());

          if (newSource != tree.GetRoot())
          {
            File.WriteAllText(tree.FilePath, newSource.GetText().ToString());
          }
        }
      }
    }

    class TrailingSemicolonRewriter : SyntaxRewriter
    {
      public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node)
      {
        return RemoveSemicolon(node, node.SemicolonToken, t => node.WithSemicolonToken(t));
      }

      public override SyntaxNode VisitInterfaceDeclaration(InterfaceDeclarationSyntax node)
      {
        return RemoveSemicolon(node, node.SemicolonToken, t => node.WithSemicolonToken(t));
      }

      public override SyntaxNode VisitStructDeclaration(StructDeclarationSyntax node)
      {
        return RemoveSemicolon(node, node.SemicolonToken, t => node.WithSemicolonToken(t));
      }

      public override SyntaxNode VisitEnumDeclaration(EnumDeclarationSyntax node)
      {
        return RemoveSemicolon(node, node.SemicolonToken, t => node.WithSemicolonToken(t));
      }

      private SyntaxNode RemoveSemicolon(SyntaxNode node,
                                         SyntaxToken semicolonToken,
                                         Func<SyntaxToken, SyntaxNode> withSemicolonToken)
      {
        if (semicolonToken.Kind != SyntaxKind.None)
        {
          var leadingTrivia = semicolonToken.LeadingTrivia;
          var trailingTrivia = semicolonToken.TrailingTrivia;

          SyntaxToken newToken = Syntax.Token(
            leadingTrivia,
            SyntaxKind.None,
            trailingTrivia);

          bool addNewline = semicolonToken.HasTrailingTrivia
            && trailingTrivia.Count() == 1
            && trailingTrivia.First().Kind == SyntaxKind.EndOfLineTrivia;

          var newNode = withSemicolonToken(newToken);

          if (addNewline)
            return newNode.WithTrailingTrivia(Syntax.Whitespace(Environment.NewLine));
          else
            return newNode;
        }
        return node;
      }
    }
  }
}

Hopefully it is something along the lines of what you were looking for.

like image 72
Daniel Brixen Avatar answered Nov 07 '22 12:11

Daniel Brixen


This information would have to be stored in the ClassDeclaration node - as, according to the C# specification, the semi-colon is an optional token in the end of its productions:

class-declaration: attributesopt class-modifiersopt partialopt class identifier type-parameter-listopt class-baseopt type-parameter-constraints-clausesopt class-body ;opt

UPDATE

According to Roslyn's documentation, you cannot actually change Syntax Trees - as they are immutable structures. That's probably the reason why kind is readonly. You may, however, create a new tree, using With* methods, defined for each changeable tree property, and using ReplaceNode. There is a good example on Roslyn documentation:

var root = (CompilationUnitSyntax)tree.GetRoot();
var oldUsing = root.Usings[1];
var newUsing = oldUsing.WithName(name); //changes the name property of a Using statement
root = root.ReplaceNode(oldUsing, newUsing);

For converting your new tree into code again (aka pretty printing), you could use the GetText() method from the compilation unit node (in our example, the root variable).

You can also extend a SyntaxRewriter class for performing code transformations. There is an extensive example for doing so in the official Roslyn website; take a look at this particular walkthrough. The following commands write the transformed tree back to the original file:

SyntaxNode newSource = rewriter.Visit(sourceTree.GetRoot());
if (newSource != sourceTree.GetRoot())
{
    File.WriteAllText(sourceTree.FilePath, newSource.GetFullText());
}

where rewriter is an instance of a SyntaxRewriter.

like image 27
rla4 Avatar answered Nov 07 '22 13:11

rla4