使用 Roslyn 在区域琐事之间插入代码

Inserting Code Between Region Trivia With Roslyn

如何在 Roslyn 中的区域指令后插入变量声明?我希望能够做类似的事情:

class MyClass 
{
    #region myRegion
    #endregion
}

对此:

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

我似乎找不到任何以这种方式处理琐事的例子。

CSharpSyntaxRewriter 做起来非常棘手,因为 #region <name>#endregion 最终都在同一个 SyntaxTriviaList 中,您必须将其拆分弄清楚要创建什么。不理会所有复杂问题的最简单方法是创建相应的 TextChange 并修改 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\";")));

之后,tree是:

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

m0sa 的 答案适用于空白区域,但它不能替换现有代码,这可能是需要这样做的原因,即重新运行代码生成工具。

实现这一点需要找到该区域的完整范围。这也很困难,因为目标文件可以包含多个嵌套区域。为此,我处理所有指令并构建区域层次结构:

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;
    }
}