妄想プログラマのらくがき帳 : Roslyn
ラベル Roslyn の投稿を表示しています。 すべての投稿を表示
ラベル Roslyn の投稿を表示しています。 すべての投稿を表示

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。

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

2014年11月24日月曜日

[Roslyn]SyntaxRewriterでソースコードを変更してみる。

今回は SyntaxRewriter を用いた方法でソースコードを変更してみます。

SyntaxRewriter を使ってアクセス修飾子を付加する

C#はメンバ変数のアクセス修飾子を指定しないと既定で private になります。そのため private 変数の場合、private を付けても付けなくても良いのですが、コード内で private 有りと private 無しが混在してると統一感に欠けるので統一したいところです。が、既存コードがすでに有り無し混在状態だと手作業で修正するのは大変ですよね。そこで SyntaxRewriter の出番です。

と前置きはこれくらいにしておいて、サンプルコードは以下の通りになります。


まず CSharpSyntaxRewriter を継承したクラスを作成し、 Visit***() メソッドをオーバーライドします。今回はフィールド宣言が変更対象なので、VisitFieldDeclaration() をオーバーライドします。
VisitFieldDeclaration() は SyntaxTree 内の各 FieldDeclarationSyntax ごとに呼び出されるメソッドで、メソッド内で引数で渡された node を変更し、変更後 node を base.VisitFieldDeclaration() に渡してやることで、SyntaxTree 内にある node を変更後のもので置き換えることができます。変更する必要が無い場合は node をそのまま base.VisitFieldDeclaration() に渡します。

このサンプルコードでは、 アクセス修飾子が付いていない FieldDeclarationSyntax に対し、private を付加して base.VisitFieldDeclaration() に渡しています。


次に作成したクラスを使用し、実際にソースコードを変更する方のサンプルコードです。
パース対象のコード内にある RoslynSample. field1 に private を付加しています。

こちらで行うことは、先ほど作成したクラスの Visit() に SyntaxTree のルートノードを渡すだけです。Visit() の戻り値として変更後の SyntaxTree を取得できます。

変更後の SyntaxTree が表すソースコードを出力すると以下のようになります。


変更前はアクセス修飾子が無かった field1 に private が付いていますね!

と、まあこんな感じで、ソースコードをコーディング規約に従うように自動修正したりするような用途に SyntaxRewriter は便利です。

2014年9月21日日曜日

[Roslyn]SyntaxNodeの派生クラスでソースコードを変更してみる。

今回は SyntaxNode の派生クラスを使ってソースコードを変更してみます。

MethodDeclarationSyntax を使ってメソッドを変更する

まずは MethodDeclarationSyntax を使ってソースコードを変更してみました。
上側の RoslynSample_ModifyMethod.cs が MethodDeclarationSyntax を使ってソースコードを変更するサンプルコード、下側の RoslynSample_ModifyMethod_Output.cs が RoslynSample_ModifyMethod.cs で変更したソースコードを出力した結果です。 メソッド static HelloRoslyn() が static private NewHelloRoslyn() に変わっているのがわかると思います。 見てのとおり変更内容は
  • 名前を HelloRoslyn から NewHelloRoslyn に変更
  • private 修飾子の追加
の2点です。

サンプルコードの解説

上側の RoslynSample_ModifyMethod.cs について1行目から順に見ていきましょう。

1行目~25行目:
変更の対象となるソースコードです。

27行目:
ソースコードをパースして SyntaxTree を作成しています。サンプルでは文字列で定義したソースコードをパースするので ParseText() を呼び出しています。ファイルに記述されたソースコードをパースする場合、代わりに ParseFile() を呼び出します。

28行目:
SyntaxTree から MethodDeclarationSyntax 型のノードを抽出しています。

30行目~32行目:
MethodDeclarationSyntax 型のノードの中から識別子が "HelloRoslyn" のノードを取得しています。

34行目:
識別子が "HelloRoslyn" のノードが見つからなかった場合 targetMethod は null になるためガードを入れています。今回はパース対象のソースコードに識別子 "HelloRoslyn" のメソッドが必ず存在するので if 条件が false になることはありませんが、未知のソースコードをパースするような場合はこのようなガードが必要になります。

37行目~41行目:
ここで名前の変更と private 修飾子の追加を行っています。
まず WithIdentifier() で名前の変更をしています。 WithIdentifier() の引数に変更後の識別子 "NewHelloRoslyn" を渡すことで、識別子が "NewHelloRoslyn" の MethodDeclarationSyntax を戻り値として取得できます。
次に AddModifiers() で private 修飾子を追加しています。 AddModifiers() の引数には private 修飾子を表す SyntaxToken を渡していますが、 WithTrailingTrivia() で private の後ろに半角スペースを付加するようにしています。何故半角スペースを付加しているのかは後程解説します。

43行目:
ReplaceNode() を用いて変更後のノードで SyntaxTree 内の変更前ノードを置換しています。

44行目~46行目:
コードを整形しています。このサンプルのケースでは整形不要ですが、 SyntaxTree に新たにノードを追加した場合等はインデントが崩れるため、明示的にコードフォーマットを行う必要があります。

48行目:
変更後のソースコードを出力しています。

***DeclarationSyntax の Add***() と With***()

***DeclarationSyntax には、引数、属性、ステートメント等のノードを構成する要素を新規追加する Add***() という名前のメソッド群と、引数、属性、ステートメント等を変更する With***() という名前のメソッド群が定義されています。これらのメソッドでノードを構成する要素を変更(=ソースコードを変更)することができます。

これらのメソッドを使う時に注意しなければならないのが ***DeclarationSyntax が不変型であるという点です(というより、Roslynのクラスは基本的に不変型です)。すなわち、各メソッドはノード自身の値は変更せず、戻り値として変更適用後のノードを返すということです。 System.String 型と同じですね。そのためノードに対し複数の変更を行う場合、サンプルのようにメソッドチェーンで記述するかまたは逐次戻り値を変数に代入し、代入後の変数でメソッド呼び出しを行う必要があります。

Add***() の注意点

Add***() のメソッドを使う時に注意しなければならないのが、 Add***() は追加対象の部分しか追加してくれないということです。例えばサンプルにある AddModifiers() の場合、修飾子は追加してくれますが前後のスペースは追加してくれません。そのため、サンプルでは WithTrailingTrivia() で半角スペースを付加しています。 WithTrailingTrivia() を使わないと該当箇所は次のようなソースコードになってしまいます。

static privateNewHelloRoslyn()  // privateの後ろにスペースが無い!

WithTrailingTrivia() でスペースを追加する代わりに SyntaxNode.NormalizeWhitespace() を使うことで適切な位置(上記例の場合は private の後ろ)にスペースを追加することも出来ますが、メソッド前後の空行やインデントを削除しちゃったりする場合があるので使い勝手があまり良くないです。なので WithTrailingTrivia() を使うことの方が多くなると思います。


ClassDeclarationSyntax を使ってメソッドを追加する

次は ClassDeclarationSyntax を使ってクラスにメソッドを追加してみました。
上側の RoslynSample_AddMethod.cs が ClassDeclarationSyntax を使ってメソッドを追加するサンプルコード、下側の RoslynSample_AddMethod_Output.cs が RoslynSample_AddMethod.cs で変更したソースコードを出力した結果です。メソッド NewMethod() が追加されているのがわかると思います。

サンプルコードの解説

RoslynSample_AddMethod.cs について見ていきましょう。

1行目~27行目:
RoslynSample_ModifyMethod.cs と同じです。

29行目~30行目:
SyntaxTree から ClassDeclarationSyntax 型のノードを抽出し、その中から識別子が "Program" のノードを取得しています。

36行目~74行目:
ここでクラス Program に NewMethod() を追加しています。ご覧のとおりもの凄く大変です(^_^;)
Roslyn はこういうメソッドをまるまる追加といった用途に使うにはあまり向いてないですね。もしそれでも Roslyn でコードジェネレータのようなものを作りたいのならば、ラッパーメソッドを用意するかまたは文字列でコードを生成してから ParseText() や ParseExpression() 等でパースした方が良いでしょう。サンプルのような方法だとコンパイルが通るようにするのにも一苦労です。実際このサンプルも苦労しました(;^_^A

処理の内容は SyntaxFactory.MethodDeclaration() で MethodDeclarationSyntax を作成し、それを ClassDeclarationSyntax.AddMembers() に渡しているだけです。
特筆すべき箇所を挙げるとすれば NewMethod() の処理部分を作成している68行目~72行目でしょうか、 SyntaxNode.NormalizeWhitespace() を呼び出して適宜スペースを追加しています。このように Roslyn で新たにコードを追加する場合、スペースを必要に応じて追加してやる必要があるのが結構メンドイです(´Д`)

75行目~:
RoslynSample_ModifyMethod.cs と同じですが、こっちのサンプルではコードフォーマットの部分が必要です。コードフォーマットを行わないと追加したメソッド部分だけインデントが無い状態になってしまいます。

SyntaxNode の派生クラスでソースコードを変更してみてわかったこと

ソースコードの一部を変更するのは容易ですが新規にクラスやメソッドを追加するのはすごく大変ですね。特にコメントやスペースといった Roslyn で Trivia と表現される要素の扱いが難しい。 NormalizeWhitespace() の動作もリファレンスが無いため良くわからず、色々と試行錯誤する羽目になりました。


次回はソースコードを変更するもう1つの方法である SyntaxRewriter を使う方法でソースコードを変更してみます。

2014年8月14日木曜日

[Roslyn]SyntaxTreeを探索してみる

CSharpSyntaxTree.ParseText()の戻り値であるSyntaxTreeにはソースコードの様々な情報が格納されています。どんな情報が格納されているのか実際にサンプルソースをパースして確かめてみましょう。

SyntaxTree

今回パースしてみるのは以下のサンプルソースです。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RoslynSample
{
    /// 
    /// 氏名を表します。
    /// 
    class Name
    {
        private string m_firstName;
        private string m_lastName;

        /// 名を取得します。
        public string FirstName
        { 
            get { return m_firstName; }
        }

        /// 氏を取得します。
        public string LastName
        {
            get { return m_lastName; }
        }

        /// 氏名を取得します。
        public string FullName
        {
            get { return m_firstName + m_lastName; }
        }

        /// 
        /// 氏名を指定してNameクラスの新しいインスタンスを初期化します。
        /// 
        /// 名        /// 氏        public Name(string firstName, string lastName)         {             m_firstName = firstName;             m_lastName = lastName;         }          ///          /// 現在のオブジェクトを表す文字列を返します。         ///          /// 現在のオブジェクトを表す文字列。         public override string ToString()         {             return m_firstName + " " + m_lastName;         }     } } 
なんてことないValueObjectです。これをCSharpSyntaxTree.ParseText()でパースしてSyntaxTreeを取得し、中身を調べてみます。
SyntaxTreeは木構造なのでツリービューで表示してみました。 各ノードにはノードのクラス名とソースコード上の位置を表示しています。 見てのとおり、SyntaxTreeはCompilationUnitSyntaxをルートに様々な種類のクラスによって構成されています。

SyntaxTreeを構成するSyntaxNode

SyntaxTreeを構成するクラスは全てSyntaxNodeの派生クラスとなっています。SyntaxNodeは、ソースコード内におけるノードの位置やノードの文法上の種類を保持しており、またSyntaxTree内を渡り歩くためのメソッドも提供します。
SntaxNodeの主なプロパティとメソッドは以下の通りです。

表1. SyntaxNodeの主なプロパティ
プロパティ名説明
TextSpanFullSpanパースしたテキスト内におけるSyntaxNodeの範囲。FullSpan.Startがテキスト内でのSyntaxNodeの開始位置(0スタートの文字index)、FullSpan.LengthがSyntaxNodeの範囲長(文字数)を表す。
boolHasLeadingTriviaSyntaxNodeの前方にTrivia(コメント、改行文字、空白等)が存在するならtrue、それ以外はfalse。
boolHasLeadingTriviaSyntaxNodeの後方にTrivia(コメント、改行文字、空白等)が存在するならtrue、それ以外はfalse。
SyntaxKindKindSyntaxNodeの文法上の種類。
SyntaxNodeParent親のSyntaxNode
SyntaxTreeSyntaxTreeSyntaxNodeが含まれるツリーへの参照。


表2. SyntaxNodeの主なメソッド
メソッド名説明
Ancestors()先祖のSyntaxNodeのリストを取得。
ChildNodes()のSyntaxNodeのリストを取得。
DescendantNodes()子孫のSyntaxNodeのリストを取得。
GetLocation()SyntaxNodeの位置を取得。GetLocation().GetLineSpan()でSyntaxNodeが含まれるファイルのパスやSyntaxNodeの開始行が取得できる。
GetLeadingTrivia()SyntaxNodeの前方のTriviaを取得。
GetTrailingTrivia()SyntaxNodeの後方のTriviaを取得。
ToFullString()SyntaxNodeを表す文字列を取得(前後のTriviaを含む)。
ToString()SyntaxNodeを表す文字列を取得(前後のTriviaを含まない)。

メソッドには「Ancestors()」や「DescendantNodes()」といった LINQ to XML でお馴染みのメソッドがあります。ソースコード内のメソッド定義にアクセスする場合、これらのメソッドを用いて次のようにアクセスすることができます。
    var tree = CSharpSyntaxTree.ParseText(sourceCode);
    // メソッド定義を全て取得
   var methodDeclarations = tree.GetRoot().DescendantNodes().OfType<MethodDeclarationSyntax>();
同様に、クラスの定義には。OfType<Classdeclarationsyntax>()、プロパティの定義にはOfType<Propertydeclarationsyntax>()でアクセスできます。

SyntaxNodeの派生クラス

SyntaxNodeクラスの派生クラスはそれぞれがクラス宣言、メソッド宣言、foreach文、ブロック、式といった文法的な要素を表します。前項で出てきた「MethodDeclarationSyntax」はメソッド宣言、「ClassDeclarationSyntax」はクラス宣言、「Propertydeclarationsyntax」プロパティ宣言を表しています。これらのクラスは(C#の場合)Microsoft.CodeAnalysis.CSharp.Syntax名前空間に***Syntaxクラスとして定義されており、各クラスのプロパティを通して文法的な値(プロパティの型や名前等)を取得できます。また各クラスのメソッドを通して文法的な値を変更することも可能です。

次回はこのSyntaxNodeの派生クラスをいくつか取り上げ、実際に文法的な値を変更してみます。

2014年6月22日日曜日

[Roslyn]とりあえず使ってみる

最初の発表からずいぶん目立った動きの無かったRoslynですが、最近になって動きが活発になってきました。なので、そんな流れに合わせて少しの間Roslynネタで記事を書こうと思います。

Roslynとは?

コンパイルの過程やコンパイラが持っている情報をAPIとして提供するコンパイラプラットフォームです。Roslynを使えば、C#のソースコードをパースしたり、メソッドや変数やの情報を取得できたりします。

つい最近公開されたVisualStudio14 CTPでRoslynが使用されていることからも、今後Roslynが.NETの中核になっていくのではないかと思います。要注目ですね!

※ちなみにRoslynはオープンソースとしてCodePlexで公開されています。

最近のMicrosoftはオープンで良いですねd(*´∀`*)

使ってみよう

早速Roslynを使ってみましょう。まずはNuGetでRoslynのパッケージをインストールします。VisualStudioのメニューから[プロジェクト]‐[NuGet パッケージの管理]を選択すると下記のダイアログが開きます。


左側の項目で「オンライン」を選択し、
1. 上部のコンボボックスで「リリース前のパッケージを含める」を選択
2. 右上にある検索ボックスに「Microsoft.CodeAnalysis」を入力
とすると、一番上に「Microsoft.CodeAnalysis」が出てくるので、これをインストールします。

インストールが終わった後にプロジェクトの参照設定を見ると、Microsoft.CodeAnalysis関係の項目が追加されています。

これでRoslynを使う準備は完了です。

C#のソースコードをパースしてみる

準備が出来たら実際にソースコードをパースしてみましょう。以下のコードがソースコードをパースするコードです(...と言ってもパースしてるのはMainの中の1行だけですけど(^_^;))。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// usingは以下の2つを追加
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;

namespace RoslynSample
{
    class Program
    {
        // パース対象のコード
        private static string m_sourceCode =
@"using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace RoslynSample
{
    class Program
    {
        /// 
        /// エントリポイントです。
        /// 
        /// コマンドライン引数        static void Main(string[] args)
        {
            System.Console.WriteLine(""Hello Roslyn!"");
        }
    }
}";
        static void Main(string[] args)
        {
            // ParseText()にパース対象のコードを渡せば、コードの構文木を作成できる。
            // コードをファイルで指定する場合はParseFile()を使用する。
            SyntaxTree tree = CSharpSyntaxTree.ParseText(m_sourceCode);
        }
    }
}

パースしたいソースコードをCSharpSyntaxTree.ParseText()に渡すだけです。
ParseText()の戻り値のSyntaxTreeは、ソースコードの構文を表した木構造になっていて、SyntaxTreeのノードであるSyntaxNodeにメソッドや変数の情報が格納されています。

次回はこのSyntaxTreeの中身を探ってみます。