Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I remove regions from code programatically using Roslyn?

Tags:

c#

roslyn

I am parsing C# code from text using Roslyn. Some of the code has regions surrounding multiple classes. Example:

#region Classes
public class MyClass
{
}

public class MyClass2
{
    #region Methods
    #endregion
}
#endregion

I'd like to remove the region surrounding the classes ("Classes" in the above example) but leave the inner regions intact like the one named "Methods" in the example above. How can I go about doing this?

like image 472
Mark T Avatar asked Dec 18 '17 11:12

Mark T


2 Answers

Regions are rather peculiar, as they do not follow the usual tree structure. For instance you can create a construct such as this:

public class TestClass{
    public void TestMethod(){
        #region TestRegion
    }
}
#endregion

This will still be valid. With that in mind there is an additional problem when analyzing regions: They are nodes inside of trivia. So to obtain the relevant nodes you can either use a SyntaxRewriter (and pass the constructor "true" to enable trivia analysis) or find the descendant nodes using node.DescendantNodes(descendIntoTrivia: true).

As the regions start and end could be anywhere in the file you should always start analysis at the root of the syntax tree to ensure that you will find the end / start of a region.

In order to find the region you can override the VisitRegionDirectiveTrivia as well as the VisitEndRegionDirectiveTrivia. Since the start and end of the RegionTrivia do not know each other you will need to match them yourself. In the example below I simple counted how many regions I already passed and and noted a list of #endregion positions which should be deleted when stepping out of a region.

To identify the relevant regions I provided two approaches: You can either use the name of the region or identify whether the attached Node is a ClassDeclaration.

Neither approach considers cases such as a property declaration before the class declaration. In case you want to handle this as well you need to take a look at the sibling nodes and check whether any of them starts within the confines of the region.

private class RegionSyntaxRewriter : CSharpSyntaxRewriter
{
    int currentPosition = 0;
    private List<int> EndRegionsForDeletion = new List<int>();
    private string deletedRegion;
    private bool useRegionNameForAnalysis = false;

    public RegionSyntaxRewriter(string deletedRegion) : base(true)
    {
        this.deletedRegion = deletedRegion;
    }

    public override SyntaxNode VisitRegionDirectiveTrivia(
            RegionDirectiveTriviaSyntax node)
    {
        currentPosition++;
        var regionText = node.ToFullString().Substring(8).Trim();
        if (!useRegionNameForAnalysis &&
            node.ParentTrivia.Token.Parent is ClassDeclarationSyntax)
        {
            EndRegionsForDeletion.Add(currentPosition);
            return SyntaxFactory.SkippedTokensTrivia();
        }
        if (useRegionNameForAnalysis && 
            regionText == deletedRegion)
        {
            EndRegionsForDeletion.Add(currentPosition);
            return SyntaxFactory.SkippedTokensTrivia();
        }

        return base.VisitRegionDirectiveTrivia(node);
    }

    public override SyntaxNode VisitEndRegionDirectiveTrivia(
            EndRegionDirectiveTriviaSyntax node)
    {
        var oldPosition = currentPosition;
        currentPosition--;
        if (EndRegionsForDeletion.Contains(oldPosition))
        {
            EndRegionsForDeletion.Remove(currentPosition);
            return SyntaxFactory.SkippedTokensTrivia();
        }

        return base.VisitEndRegionDirectiveTrivia(node);
    }
}
like image 77
SJP Avatar answered Nov 10 '22 22:11

SJP


As Sievajet suggested, you can use CSharpSyntaxRewriter to remove Region attached to particular node (in your case : ClassDeclarationSyntax).

Here is code to get you started :

 public class RegionRemoval : CSharpSyntaxRewriter
    {
        public override SyntaxNode VisitClassDeclaration(ClassDeclarationSyntax node)
        {

            if(node.HasLeadingTrivia)
            {
                var enumerator = node.GetLeadingTrivia().GetEnumerator();

                while(enumerator.MoveNext())
                {
                    var syntaxTrivia = enumerator.Current;
                    if(syntaxTrivia.Kind().Equals(SyntaxKind.RegionDirectiveTrivia))
                    {
                        node = node.ReplaceTrivia(syntaxTrivia, SyntaxFactory.Whitespace("\n"));
                    }
                }

            }
            return node;
        }
    }

    class RoslynTry
    {
        public static void RegionRemover()
        {
            //A syntax tree with an unnecessary semicolon on its own line
            var tree = CSharpSyntaxTree.ParseText(@"
                  #region Classes
        public class MyClass
        {
        }

        public class MyClass2
        {
            #region Methods
            #endregion
        }
        #endregion
        ");

            var rewriter = new RegionRemoval();
            var result = rewriter.Visit(tree.GetRoot());
            Console.WriteLine(result.ToFullString());
        }
    }

Your Output should look like :

        public class MyClass
        {
        }

        public class MyClass2
        {
            #region Methods
            #endregion
        }
        #endregion

P.S. : This is not complete solution. I agree with mjwills and you should show some progress before posting question.

P.S. : Code is inspired from JoshVarty's EmptyStatementRemoval

like image 2
Utsav Chokshi Avatar answered Nov 11 '22 00:11

Utsav Chokshi