【问题标题】:Using Roslyn to replace all nodes within span使用 Roslyn 替换 span 内的所有节点
【发布时间】:2019-08-23 03:49:45
【问题描述】:

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

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

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

例如下面的代码:

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,并且已经为这些 cmets 提取了SyntaxTrivia 对象的集合。我天真的方法是覆盖VisitMethodDeclaration(),识别开始和结束注释块之间的代码跨度范围,并以某种方式提取节点。

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

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

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

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

【问题讨论】:

    标签: c# roslyn


    【解决方案1】:

    我已经设法通过DocumentEditor 获得了可喜的结果。

    我的代码看起来像是有人通过 SDK 摸索了他们的方式,反复试验,删除尾随 cmets 的方法似乎非常笨拙,但这一切似乎都有效(至少对于琐碎的示例)。

    这是粗略的概念证明。

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-04-23
      • 2019-03-05
      • 2015-09-27
      • 2022-06-10
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多