我正在用 C# 编写一个程序,用于从代码中提取注释。我正在使用 Roslyn 编译器来做到这一点。这很棒,因为我只是访问整个抽象语法树并从解决方案中的文件中获取 SingleLineComment trivia、MultiLineComment trivia 和 DocumentationComment trivia 语法。但有一个问题,因为程序员经常写这样的注释:
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
您可以看到这是三个单行注释,但我希望它们作为一个注释从代码中获取。我可以通过 Roslyn 实现这一目标吗?或者也许还有其他方法?因为当程序员使用单行注释语法编写多行注释时,这是常见的情况。
我提取评论的代码如下所示:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Collections.Generic;
namespace RoslynPlay
{
public class CommentStore
{
public List<Comment> Comments { get; } = new List<Comment>();
public void AddCommentTrivia(SyntaxTrivia trivia,
LocationStore commentLocationstore, string fileName)
{
if (trivia.Kind() == SyntaxKind.SingleLineCommentTrivia)
{
Comments.Add(new SingleLineComment(trivia.ToString(),
trivia.GetLocation().GetLineSpan().EndLinePosition.Line + 1, commentLocationstore)
{
FileName = fileName,
});
}
else if (trivia.Kind() == SyntaxKind.MultiLineCommentTrivia)
{
Comments.Add(new MultiLineComment(trivia.ToString(),
trivia.GetLocation().GetLineSpan().StartLinePosition.Line + 1,
trivia.GetLocation().GetLineSpan().EndLinePosition.Line + 1, commentLocationstore)
{
FileName = fileName,
});
}
}
public void AddCommentNode(DocumentationCommentTriviaSyntax node,
LocationStore commentLocationstore, string fileName)
{
Comments.Add(new DocComment(node.ToString(),
node.GetLocation().GetLineSpan().StartLinePosition.Line + 1,
node.GetLocation().GetLineSpan().EndLinePosition.Line,
commentLocationstore)
{
FileName = fileName,
});
}
}
}
在主文件(Program.cs)中,我正在从这样的代码中启动注释提取:
string fileContent;
SyntaxTree tree;
SyntaxNode root;
CommentsWalker commentWalker;
MethodsAndClassesWalker methodWalker;
string[] files = Directory.GetFiles(projectPath, $"*.cs", SearchOption.AllDirectories);
var commentStore = new CommentStore();
Console.WriteLine("Reading files...");
ProgressBar progressBar = new ProgressBar(files.Length);
foreach (var file in files)
{
fileContent = File.ReadAllText(file);
string filePath = new Regex($@"{projectPath}\\(.*)$").Match(file).Groups[1].ToString();
tree = CSharpSyntaxTree.ParseText(fileContent);
root = tree.GetRoot();
commentWalker = new CommentsWalker(filePath, commentStore);
commentWalker.Visit(root);
progressBar.UpdateAndDisplay();
}
这也是评论步行者:
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace RoslynPlay
{
public class CommentsWalker : CSharpSyntaxWalker
{
private string _fileName;
private CommentStore _commentStore;
public CommentsWalker(string fileName,
CommentStore commentStore)
: base(SyntaxWalkerDepth.StructuredTrivia)
{
_fileName = fileName;
_commentStore = commentStore;
}
public override void VisitTrivia(SyntaxTrivia trivia)
{
if (trivia.Kind() == SyntaxKind.SingleLineCommentTrivia
|| trivia.Kind() == SyntaxKind.MultiLineCommentTrivia)
{
_commentStore.AddCommentTrivia(trivia, _commentLocationStore, _fileName);
}
base.VisitTrivia(trivia);
}
public override void VisitDocumentationCommentTrivia(DocumentationCommentTriviaSyntax node)
{
_commentStore.AddCommentNode(node, _commentLocationStore, _fileName);
base.VisitDocumentationCommentTrivia(node);
}
}
}
问题是因为 trivia.Kind() == SyntaxKind.SingleLineCommentTrivia 只提取单行注释,但我想提取单行注释块作为一条注释。
为了简洁起见,我省略了部分文档注释,
这可以通过跟踪前一个注释块的行结尾来完成。
有一些边缘情况需要处理,但这些情况非常罕见(当单行注释与多行注释相邻时)
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
internal class Program
{
private static void Main(string[] args)
{
// example code to check for comments
const string programText =
@"
using System;
using System.Collections;
using System.Linq;
using System.Text;
/* Block start 1
*
*
*
*
* Block End 2
*/
// single line comment 0
// single line comment 1
// single line comment 2
// single line comment 3
namespace HelloWorld
{
/* Block start 3
*
*
*
*
* Block End 4
*/
class Program
{
/// documentation 1
/// documentation 2
/// documentation 3
/// documentation 4
static void Main(string[] args)
{
Console.WriteLine(""Hello, World!"");
}
}
// another comment 1
// another comment 2
// another comment 3
/* Adjacent
*
*/
// another comment 4
// another comment 5
/* Adjacent
*
*
*/
}";
SyntaxTree tree = CSharpSyntaxTree.ParseText(programText);
var root = tree.GetRoot();
var walker = new CommentsWalker();
walker.Visit(root);
// printing out all comments in groups that we found
foreach (var (start, end) in walker.GetCommentBlocks())
{
Console.WriteLine($"Comment block: {start} - {end}");
}
}
}
public class CommentsWalker : CSharpSyntaxWalker
{
// keeps track of all comment blocks in code
private List<(int start, int end)> commentBlocks;
// keeps track of current continuous comment block
// using linked list so it is easy to access first and last
// line in a comment block
private LinkedList<(int start, int end)> currentBlock;
public CommentsWalker()
: base(SyntaxWalkerDepth.StructuredTrivia)
{
commentBlocks = new();
currentBlock = new();
}
public List<(int start, int end)> GetCommentBlocks()
{
// before returning all comments
// we have to ensure that all comment blocks were added
if (currentBlock.First != null)
commentBlocks.Add((currentBlock.First.Value.start, currentBlock.Last.Value.end));
return commentBlocks;
}
public override void VisitTrivia(SyntaxTrivia trivia)
{
var span = trivia.GetLocation()
.GetLineSpan();
// get starting and ending line numbers of a current comment
// it can be a multi-line and a single-line comment
(int start, int end) = (span.StartLinePosition.Line, span.EndLinePosition.Line);
switch (trivia.Kind())
{
case SyntaxKind.SingleLineCommentTrivia:
{
// if this is a first comment in a block
// or if this is a continuation of a previous comment
if (currentBlock.First == null || currentBlock.Last.Value.end + 1 == start)
currentBlock.AddLast((start, end));
else
{
// this is a new single line comment that isn't adjacent to previous one
// add previous block and start a new one with current comment
commentBlocks.Add((currentBlock.First.Value.start, currentBlock.Last.Value.end));
currentBlock = new();
currentBlock.AddFirst((start, end));
}
}
break;
case SyntaxKind.MultiLineCommentTrivia:
{
// treat whole comment block at once
if (currentBlock.First != null)
{
commentBlocks.Add((currentBlock.First.Value.start, currentBlock.Last.Value.end));
currentBlock = new();
}
currentBlock.AddFirst((start, end));
}
break;
}
base.VisitTrivia(trivia);
}
}
输出示例:
Comment block: 6 - 12
Comment block: 15 - 15
Comment block: 17 - 19
Comment block: 22 - 28
Comment block: 41 - 41
Comment block: 44 - 45
Comment block: 48 - 50
Comment block: 52 - 53
Comment block: 55 - 58
链接到dotnet小提琴