c# - 使用Roslyn替换span内的所有节点

标签 c# roslyn

我有大量生成的 C# 代码,我希望使用 Roslyn 对其进行预处理,以协助后续的手动重构。

代码包含具有已知结构的开始和结束注释 block ,我需要将 block 之间的代码重构为方法。

幸运的是,生成代码中的所有状态都是全局的,因此我们可以保证目标方法不需要任何参数。

例如以下代码:

public void Foo()
{
    Console.WriteLine("Before block");

    // Start block
    var foo = 1;
    var bar = 2;
    // End block

    Console.WriteLine("After block");
}

应转换为类似的内容:

public void Foo()
{
    Console.WriteLine("Before block");

    TestMethod();

    Console.WriteLine("After block");
}

private void TestMethod()
{
    var foo = 1;
    var bar = 2;
}

显然,这是一个人为的例子。单个方法可以具有任意数量的这些注释和代码块。

我研究了 CSharpSyntaxRewriter 并为这些注释提取了 SyntaxTrivia 对象的集合。我天真的方法是重写 VisitMethodDeclaration(),识别开始和结束注释 block 之间的代码范围,并以某种方式提取节点。

我已经能够使用node.GetText().Replace(codeSpan),但我不知道如何使用结果。

我见过很多使用CSharpSyntaxRewriter的例子,但所有的例子看起来都微不足道,而且不涉及涉及多个相关节点的重构。

使用DocumentEditor会更好吗?这种重构有通用的方法吗?

我可能很懒,根本不使用 Roslyn,但结构化代码解析似乎是比正则表达式和将源代码视为纯文本更优雅的解决方案。

最佳答案

我成功地使用 DocumentEditor 获得了有希望的结果。

我的代码看起来像是有人通过 SDK 摸索、反复试验,并且删除尾随注释的方法似乎非常笨拙,但这一切似乎都有效(至少对于简单的示例)。

这是概念的粗略证明。

public class Program
{
    static async Task Main()
    {
        var document = CreateDocument(@"..\..\..\TestClass.cs");

        var refactoredClass = await Refactor(document);
        Console.Write(await refactoredClass.GetTextAsync());
    }

    private static async Task<Document> Refactor(Document document)
    {
        var documentEditor = await DocumentEditor.CreateAsync(document);

        var syntaxRoot = await document.GetSyntaxRootAsync();
        var comments = syntaxRoot
            .DescendantTrivia()
            .Where(t => t.IsKind(SyntaxKind.SingleLineCommentTrivia))
            .ToList();

        // Identify comments which are used to target candidate code to be refactored
        var startComments = new Queue<SyntaxTrivia>(comments.Where(c => c.ToString().TrimEnd() == "// Start block"));
        var endBlock = new Queue<SyntaxTrivia>(comments.Where(c => c.ToString().TrimEnd() == "// End block"));

        // Identify class in target file
        var parentClass = syntaxRoot.DescendantNodes().OfType<ClassDeclarationSyntax>().First();

        var blockIndex = 0;

        foreach (var startComment in startComments)
        {
            var targetMethodName = $"TestMethod_{blockIndex}";

            var endComment = endBlock.Dequeue();

            // Create invocation for method containing refactored code
            var testMethodInvocation =
                ExpressionStatement(
                        InvocationExpression(
                            IdentifierName(targetMethodName)))
                    .WithLeadingTrivia(Whitespace("\n"))
                    .WithTrailingTrivia(Whitespace("\n\n"));

            // Identify nodes between start and end comments, recursing only for nodes outside comments
            var nodes = syntaxRoot.DescendantNodes(c => c.SpanStart <= startComment.Span.Start)
                .Where(n =>
                    n.Span.Start > startComment.Span.End &&
                    n.Span.End < endComment.SpanStart)
                .Cast<StatementSyntax>()
                .ToList();

            // Construct list of nodes to add to target method, removing starting comment
            var targetNodes = nodes.Select((node, nodeIndex) => nodeIndex == 0 ? node.WithoutLeadingTrivia() : node).ToList();

            // Remove end comment trivia which is attached to the node after the nodes we have refactored
            // FIXME this is nasty and doesn't work if there are no nodes after the end comment
            var endCommentNode = syntaxRoot.DescendantNodes().FirstOrDefault(n => n.SpanStart > nodes.Last().Span.End && n is StatementSyntax);
            if (endCommentNode != null) documentEditor.ReplaceNode(endCommentNode, endCommentNode.WithoutLeadingTrivia());

            // Create target method, containing selected nodes
            var testMethod =
                MethodDeclaration(
                        PredefinedType(
                            Token(SyntaxKind.VoidKeyword)),
                        Identifier(targetMethodName))
                    .WithModifiers(
                        TokenList(
                            Token(SyntaxKind.PublicKeyword)))
                    .WithBody(Block(targetNodes))
                    .NormalizeWhitespace()
                    .WithTrailingTrivia(Whitespace("\n\n"));

            // Add method invocation
            documentEditor.InsertBefore(nodes.Last(), testMethodInvocation);

            // Remove nodes from main method
            foreach (var node in nodes) documentEditor.RemoveNode(node);

            // Add new method to class
            documentEditor.InsertMembers(parentClass, 0, new List<SyntaxNode> { testMethod });

            blockIndex++;
        }

        // Return formatted document
        var updatedDocument = documentEditor.GetChangedDocument();
        return await Formatter.FormatAsync(updatedDocument);
    }

    private static Document CreateDocument(string sourcePath)
    {
        var workspace = new AdhocWorkspace();
        var projectId = ProjectId.CreateNewId();
        var versionStamp = VersionStamp.Create();
        var projectInfo = ProjectInfo.Create(projectId, versionStamp, "NewProject", "Test", LanguageNames.CSharp);
        var newProject = workspace.AddProject(projectInfo);

        var source = File.ReadAllText(sourcePath);
        var sourceText = SourceText.From(source);

        return workspace.AddDocument(newProject.Id, Path.GetFileName(sourcePath), sourceText);
    }
}

我很想知道我是否因为这些而让自己的生活变得艰难——我确信有更优雅的方式来完成我想做的事情。

关于c# - 使用Roslyn替换span内的所有节点,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57619676/

相关文章:

c# - WebAPI : Creating custom BadRequest status outside of ApiController

c# - Roslyn CTP 是否仍可用于 Visual Studio 2010 SP1

c# - .NET 罗斯林 : runtime configuration

c# - 如何将信息从 OnAuthenticated 事件传递到 Controller 和登录?

c# - .NET TimeZone.CurrentTimeZone.GetDaylightChanges 返回错误的 2005 夏令时

c# - 如何插入 BLOB 数据类型

c# - 使用 DataBinding 时如何自动滚动 AvaloniaUI ScrollViewer?

c# - VS2015 构建失败,动态没有错误消息

c# - Roslyn Analyzer 分析命名空间

c# - 使用 Roslyn 引用 PCL 库会导致 .NET 版本问题