Andr0s
Andr0s

Reputation: 149

Inserting Code Between Region Trivia With Roslyn

How can I insert a variable declaration after a region directive in Roslyn? I'd like to be able to do something like going from this:

class MyClass 
{
    #region myRegion
    #endregion
}

to this:

class MyClass 
{
    #region myRegion
    private const string myData = "somedata";
    #endregion 
}

I can't seem to find any examples that deal with trivia in this manner.

Upvotes: 3

Views: 1331

Answers (2)

daw
daw

Reputation: 2049

m0sa's answer works for empty regions but it cannot replace existing code which is the likely reason to need to this i.e. rerunning a code-generation tool.

Achieving this requires finding the full extent of the region. This is also made difficult because the target file can contain multiple nested regions. To do this I process all directives and build a hierarchy of regions:

public class RegionInfo
{
    public RegionDirectiveTriviaSyntax Begin;
    public EndRegionDirectiveTriviaSyntax End;
    public RegionInfo Parent;
    public List<RegionInfo> Children = new List<RegionInfo>();
    public string Name => this.Begin.EndOfDirectiveToken.ToFullString().Trim();
}

public static class CodeMutator
{   
    public static string ReplaceRegion(string existingCode, string regionName, string newCode)
    {
        var syntaxTree = CSharpSyntaxTree.ParseText(existingCode);

        var region = CodeMutator.GetRegion(syntaxTree, regionName);

        if (region == null)
        {
            throw new Exception($"Cannot find region named {regionName}");
        }

        return 
            existingCode
                .Substring(0, region.Begin.FullSpan.End) +
            newCode +
            Environment.NewLine +
            existingCode
                .Substring(region.End.FullSpan.Start);
    }

    static RegionInfo GetRegion(SyntaxTree syntaxTree, string regionName) =>
        CodeMutator.GetRegions(syntaxTree)
            .FirstOrDefault(x => x.Name == regionName);

    static List<RegionInfo> GetRegions(SyntaxTree syntaxTree)
    {
        var directives = syntaxTree
            .GetRoot()
            .DescendantNodes(descendIntoTrivia: true)
            .OfType<DirectiveTriviaSyntax>()
            .Select(x => (x.GetLocation().SourceSpan.Start, x))
            .OrderBy(x => x.Item1)
            .ToList();

        var allRegions = new List<RegionInfo>();
        RegionInfo parent = null;

        foreach (var directive in directives)
        {
            if (directive.Item2 is RegionDirectiveTriviaSyntax begin)
            {
                var next = new RegionInfo() {Begin = begin, Parent = parent};

                allRegions.Add(next);
                parent?.Children.Add(next);

                parent = next;
            }
            else if (directive.Item2 is EndRegionDirectiveTriviaSyntax end)
            {
                if (parent == null)
                {
                    Log.Error("Unmatched end region");
                }
                else
                {
                    parent.End = end;
                    parent = parent.Parent;
                }
            }
        }

        return allRegions;
    }
}

Upvotes: 2

m0sa
m0sa

Reputation: 10940

That's quite tricky to do with a CSharpSyntaxRewriter, because both the #region <name> and #endregion end up in the same SyntaxTriviaList, which you'd have to split out and figure out what to create instead. The simplest way to not bother with all the intricacies, is to create the corresponding TextChange and modify the SourceText.

var tree = SyntaxFactory.ParseSyntaxTree(
@"class MyClass
{
    #region myRegion
    #endregion
}");

// get the region trivia
var region = tree.GetRoot()
    .DescendantNodes(descendIntoTrivia: true)
    .OfType<RegionDirectiveTriviaSyntax>()
    .Single();

// modify the source text
tree = tree.WithChangedText(
    tree.GetText().WithChanges(
        new TextChange(
            region.GetLocation().SourceSpan,
            region.ToFullString() + "private const string myData = \"somedata\";")));

After that, tree is:

class MyClass 
{
    #region myRegion
private const string myData = "somedata";
    #endregion
}

Upvotes: 4

Related Questions