[英]Using Roslyn to replace all nodes within span
我有大量生成的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
並為這些注釋提取了SyntaxTrivia
對象的集合。 我的幼稚方法是重寫VisitMethodDeclaration()
,確定代碼在開始和結束注釋塊之間的范圍,並以某種方式提取節點。
我已經能夠使用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);
}
}
我很想看看我是否為此感到難過-我敢肯定還有更多優雅的方法可以做我想做的事情。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.