我有大量生成的 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/