[C#][Roslyn]SyntaxWalkerでメソッドコメントを取得する | 妄想プログラマのらくがき帳

2015年5月21日木曜日

[C#][Roslyn]SyntaxWalkerでメソッドコメントを取得する

リバースエンジニアリングをするときやコーディングが先でドキュメントを後に書くような開発のときとか、C#のソースコードからメソッドコメントをツールで抽出したいなあーと思うことがたまにあります。

既存のツールで抽出できるものがあるかもしれませんが、有料だったり、細かいところに手が届かなかったりで、結局手作業でやるはめになりがちです。そんなとき、Roslynを使えば簡単にメソッドコメントを抽出できますよーっていうのが今回の内容です。

XMLドキュメントコメントを格納するクラスを作る

まずメソッドコメントであるXMLを格納するクラスを作ります。
class DocumentComment
{
    public string Summary { get; set; }
    public List<ParamDocumentComment> Params { get; set; }

    public DocumentComment()
    {
        Params = new List<ParamDocumentComment>();
    }
}

class ParamDocumentComment
{
    public string Name { get; set; }
    public string Comment { get; set; }
}
今回はsummaryとparamsを取ってくるだけにするので、それぞれのプロパティのみの構成にしています。

メソッドコメントのXMLをパースするクラスを作る

次にメソッドコメントのXMLをパースするクラスを作ります。
class DocumentCommentParser
{
    public static DocumentComment Parse(string documentComment)
    {
        // XDocumentクラスを用いてsammary要素とparam要素を取得する
        XDocument doc = XDocument.Parse(documentComment);
        var summary = doc.Descendants("summary").FirstOrDefault();
        var paramList = doc.Descendants("param");

        var docComment = new DocumentComment();
        // summary要素が取得できた場合、不要な改行や空白を取り除く
        docComment.Summary = (summary != null) ? RemoveAnySpaceChar(summary.Value) : string.Empty;

        foreach (var param in paramList)
        {
            // param要素はname属性が取得できたもののみ有効な要素とする
            var name = param.Attribute("name");
            if (name != null)
            {
                var pdc = new ParamDocumentComment();
                pdc.Name = RemoveAnySpaceChar(name.Value);
                pdc.Comment = RemoveAnySpaceChar(param.Value);
                docComment.Params.Add(pdc);
            }
        }
        return docComment;
    }

    // 引数の文字列から正規表現のパターン「\s」に該当する文字を削除した文字列を返す
    private static string RemoveAnySpaceChar(string str)
    {
        return Regex.Replace(str, "\\s", "");

    }
}
引数のXML文字列をパースし、DocumentCommentクラスを返すParse()メソッドを定義します。XDocumentクラスを使えばXMLのパースも簡単です。

CSharpSyntaxWalkerを継承したメソッドコメント収集クラスを作る

次にCSharpSymtaxWalkerを継承してメソッドコメントを収集するクラスを作ります。
class MethodDocumentCommentWalker : CSharpSyntaxWalker
{
    private SemanticModel m_semanticModel; // ドキュメントコメントを取得するにはセマンティックモデルが必要
    private Dictionary<MethodDeclarationSyntax, DocumentComment> m_documentComments = new Dictionary<MethodDeclarationSyntax, DocumentComment>();

    // 取得したドキュメントコメントのリストを返すプロパティ
    public IReadOnlyDictionary<MethodDeclarationSyntax, DocumentComment> DocumentComments
    {
        get { return m_documentComments; }
    }

    public MethodDocumentCommentWalker(SyntaxTree tree)
    {
        // コンストラクタでセマンティックモデルを作成しておく
        var compilation = CSharpCompilation.Create("tmpcompilation", syntaxTrees: new[] { tree });
        m_semanticModel = compilation.GetSemanticModel(compilation.SyntaxTrees[0], true);
    }

    // VisitMethodDeclaration()をオーバーライドし、各メソッド宣言からドキュメントコメントを取得する
    public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
    {
        base.VisitMethodDeclaration(node);

        // IMethodSymbol.GetDocumentationCommentXml()でメソッドコメントのXML文字列を取得し、
        // パーサークラスを用いてパースする
        IMethodSymbol symbol = m_semanticModel.GetDeclaredSymbol(node);
        m_documentComments[node] = DocumentCommentParser.Parse(symbol.GetDocumentationCommentXml());
    }
}

メソッドコメント収集クラスを用いてメソッドコメントを収集する

最後にメソッドコメント収集クラスでメソッドコメントを収集します。
class Program
{
    static void Main(string[] args)
    {
        SyntaxTree syntaxTree = CSharpSyntaxTree.ParseText(File.ReadAllText("sample.cs"));
        
        // MethodDocumentCommentWalker.Visit()を呼び出せば、
        // MethodDocumentCommentWalker.DocumentCommentsに収集した結果が格納される
        var walker = new MethodDocumentCommentWalker(syntaxTree);
        walker.Visit(syntaxTree.GetRoot());

        foreach (var docComment in walker.DocumentComments)
        {
            MethodDeclarationSyntax method = docComment.Key;
            DocumentComment comment = docComment.Value;

            // ここで取得したメソッドコメントを整形してファイルに出力したりする
            Console.WriteLine("#" + method.Identifier);
            Console.WriteLine("##Summary");
            Console.WriteLine(comment.Summary);
            Console.WriteLine("##parameters");
            foreach (var param in comment.Params)
            {
                Console.WriteLine(param.Name + "\t" + param.Comment);
            }
            Console.WriteLine();
        }
    }
}

上記処理では、以下のサンプルクラスに対してメソッドコメント収集を行って、メソッド名とsummary、paramsのセットでコンソールに出力しています。
class sample
{
    /// <summary>
    /// サンプルクラスのコンストラクタコメント。
    /// </summary>
    public sample()
    {
    }

    /// <summary>
    /// Method1のメソッドコメント。
    /// </summary>
    /// <param name="arg">Method1の引数1。</param>
    /// <returns>Method1の戻り値。</returns>
    public int Method1(int arg)
    {
        return arg * 2;
    }

    /// <summary>
    /// Method2のメソッドコメント。
    /// </summary>
    /// <param name="arg1">Method2の引数1。</param>
    /// <param name="arg2">Method2の引数2。</param>
    /// <returns>Method2の戻り値。</returns>
    public int Method2(int arg1, int arg2)
    {
        return arg1 * arg2;
    }
}
出力結果はこんな↓感じになります。

#Method1
##Summary
Method1のメソッドコメント。
##parameters
arg Method1の引数1。

#Method2
##Summary
Method2のメソッドコメント。
##parameters
arg1 Method2の引数1。
arg2 Method2の引数2。

収集する部分と出力する部分が独立しているので、出力形式や出力先の変更も簡単です。

0 件のコメント:

コメントを投稿