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

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年7月13日日曜日

[.NET]XmlSerializerでシリアライズするクラスって…

publicなコンストラクタを持ってなくてもいいんですね。ずっとpublicな引数無しコンストラクタが必須だと勘違いしてました。
MSDNを確認してみると…

XML シリアル化の概要 - XML シリアル化に関する考慮事項
  • クラスを XmlSerializer でシリアル化するには、そのクラスが既定のコンストラクターを持つ必要があります。
と書いてあり、どこにもpublicでないとダメだなんて書いてないんですよね。
まあ「既定のコンストラクター」っていうと、クラスにコンストラクタが定義されていない時にコンパイラが自動生成するpublicな引数無しコンストラクタを思い浮かべるから、それで勝手にpublicな引数無しコンストラクタが必須だと思い込んでたんでしょうねw

実際にpublicなコンストラクタを持っていないクラスをシリアライズ&デシリアライズしてみました。
using System.IO;
using System.Xml.Serialization;

namespace XmlSerializerSample
{
    public class SerializeTarget
    {
        private SerializeTarget () {}

        public SerializeTarget(int value1, int value2)
        {
            Prop1 = value1;
            Prop2 = value2;
        }

        public int Prop1 { get; set; }
        public int Prop2 { get; set; }
    }

    class Program
    {
        static void Main(string[] args)
        {
            SerializeTarget target = new SerializeTarget(100, 200);

            string serialized;
            XmlSerializer xs = new XmlSerializer(typeof(SerializeTarget));
            using (StringWriter sw = new StringWriter())
            {
                xs.Serialize(sw, target);
                serialized = sw.ToString();
            }

            SerializeTarget deserialized;
            using (StringReader sr = new StringReader(serialized))
            {
                deserialized = (SerializeTarget)xs.Deserialize(sr);
            }
        }
    }
}
この通りコンストラクタがprivateでもシリアライズ&デシリアライズできます。

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の中身を探ってみます。

2012年10月21日日曜日

Irony コメントを文法に追加する。

今回もIronyネタ。

コメントを文法に追加する方法として次の2通りの方法があります。

1.GrammarクラスのNonGrammarTerminals変数に、コメントを表すTerminalクラスを追加する。

GrammarクラスのNonGrammarTerminalsに追加したTerminalは、
文字列をパースする際に無視されるようになります。ParseTreeにも追加されなくなります。
以下が自作GrammarクラスのコンストラクタでコメントをNonGrammarTerminalsに追加する例です。
//コメントを定義
var comment = new CommentTerminal("comment", "//", "\n", "\r");
//コメントを非文法終端記号として追加
this.NonGrammarTerminals.Add(comment);

2.コメントを表すTerminalを文法の1部として扱う

ParseTreeでコメントを扱いたい場合があります。
コメントからドキュメント生成したい!といった場合などです。
そのような場合、1の方法だとParseTreeにコメントが追加されないので不都合です。
こんな時はコメントを文法の中に組み込む方法を使います。
//コメントを定義
var comment = new CommentTerminal("comment", "//", "\n", "\r");
//コメントを文法の一部として追加
ProgramLine.Rule = Statement + ToTerm(";") | comment;
Program.Rule = MakeStarRule(Program, ProgramLine);
こっちの方法だとParseTreeにコメントが追加されます。

2012年10月15日月曜日

Irony ParseTreeに不要な項目を追加しないようにする。

IronyでParser.Parse()を使いParseTreeを構築すると、
1ステートメントの区切りである";"(セミコロン)や、単なる区切りの","(カンマ)といった文字まで、
1つのノードとしてParseTreeに追加されます。

以前のエントリーで定義した文法クラスで作成したParseTree。
画面右側のParseTreeに"; (Key symbol)"というノードが確認できます。














多くの場合、これらのノードはその後の処理において不要なノードであり、
ParseTreeを走査するうえで邪魔なノードになります。
なので、指定した項目をノードに追加しないようにする方法があったら便利ですよね。

そんな「あったらいいなぁ」な方法が、Ironyにはちゃんと用意されています。

方法はとっても簡単です。
文法クラスのコンストラクタでMarkPunctuation()を呼び出して、
ParseTreeに追加しない項目を登録するだけです。

下の例では、";"(セミコロン)をParseTreeに追加しないようにする命令です。
this.MarkPunctuation(";");
この命令をコンストラクタに追加した文法クラスでParseTreeを作成してみると…












画面右側のParseTreeから"; (Key symbol)"というノードが消えているのが分かります。

文法が複雑になればなるほど不要なノードが増えてくるので、MarkPunctuation()をうまく使って、
綺麗なParseTreeが作成されるようにしときましょう。

2012年10月13日土曜日

IronyのGrammarExplorerを使って文法をテストする。

IronyにはGrammarExplorerというツールがあります。
GrammarExplorerを使うことで定義した文法クラスを簡単にテストすることができます。

IronyとGrammarExplorerはこちらからダウンロードできます。
Irony - .NET Language Implementation Kit
Ironyのソリューションファイルの中にGrammarExplorerが含まれています。

GrammarExplorerを起動すると次の画面が表示されます。












画面左上のGrammarコンボボックスの右側にあるボタンでAdd grammarを選択すると、
文法クラスを含むアセンブリを選択するダイアログが表示されます。
初期表示ではDllファイルしか選択できないように見えますが、
ファイル名を直接入力すればexeファイルも選択できます。

アセンブリを選択するとアセンブリに含まれる文法の一覧が表示されるので、
テストしたい文法を選択します。













今回は以前のエントリーで定義したサンプル文法クラスをテストしてみます。

文法を選択すると、終端文字一覧や非終端文字一覧、パーサの状態一覧が各タブページに表示されます。
これらを見れば、ちゃんと意図した通りに定義できているのか確認することができます。

文法のテストで使用するのはTestタブです。
まず、Testタブページのテキストボックスにパース対象の文字列を記述します。
次にParseボタンを押すと、右側のParse Treeにパーシングによって生成されたTreeが表示されます。














Tree上でノードを選択すると、テキストボックスで該当する箇所が選択状態になります。
(ただし、選択状態になるのは該当箇所の先頭文字のみみたいです)
逆の機能もあります。Locate>>ボタンを押すとテキストボックス上のカーソル位置にあるノードが
Tree上で選択状態になります。

パース対象文字列に文法エラーがある場合、ParseTreeは表示されません。
代わりに画面下部のParser Outputにエラーとなった箇所とエラーメッセージが表示されます。
VSのエラー一覧のような感じに表示されます。

また、画面下部のParser TraceでEnable Traceにチェックを入れると、
パーサの動作をトレースするようになります。
チェックをいれてParseボタンを押すと、次のようなトレース情報が表示されます。














パーサの状態、スタックのトップ、入力トークン、パーサの動作が時系列に表示されるので、
パーサが文字列をパースする経過がよく分かります。

GrammarExplorerは複雑な文法を定義するときに欠かせないツールなので、
Ironyを使うなら使いこなせるようになっておきましょう!

2012年10月11日木曜日

C#のパーサジェネレータ Ironyを使ってみた。その3。

前回に引き続き、Ironyについてです。

前回は文法を定義する方法を書きました。今回は定義した文法で実際にパースしてみます。

まずはサンプルソース。
string src =
    "a = 10;" +
    "b = 20;" +
    "c = a + b;" +
    "c *= c;";

MyGrammar grammar = new MyGrammar();
Parser parser = new Parser(grammar);

ParseTree parseTree = parser.Parse(src);
MyGrammarは、前回定義した文法クラスです。
ご覧のとおり、パース処理自体は3行で書けます。

  1. 文法クラスのインスタンスを生成。
  2. 文法インスタンスを引数にパーサクラスのインスタンスを生成。
  3. パーサインスタンスのパーサメソッドに文字列を渡す。

たったこれだけです。
Parser.Parse()の戻り値であるParseTreeはASTになってます。
ParseTree.RootがASTのルートノードで、子ノードがParseTree.Root.ChildNodesに繋がっています。
このASTは様々な情報を持っているので、あんなことやこんなことに使えちゃいます^^

ParseTree.Root変数がnullの場合、パース失敗です。
パースエラーに関する情報は、ParseTreeクラスのParserMessages変数で得ることができ、
エラーとなったソース上の場所はParseTree.ParserMessages[ ].Location、
修正候補のリストはParseTree.ParserMessages[ ].ParserState.ExpectedTerminalsで取得できます。

Ironyを使ってみて感動したのは、凄く簡単に使えるようになる!ということです。
他のパーサジェネレータなどを使うには、文法定義に専用の文法を覚えないとダメな場合が多いですが、
IronyはBNFとC#の文法が分かっていれば、すぐに文法定義ができるようになります。

日本語の資料がほとんど無いのが難点ですが、C#でパーサジェネレータが必要な方は是非使ってみてください!

2012年10月9日火曜日

C#のパーサジェネレータ Ironyを使ってみた。その2。

前回に引き続き、Ironyについてです。

前回はプロジェクトの参照設定にIrony.dllを追加するところまで書きました。
今回はIronyで文法を定義する方法です。

Ironyで文法を定義するには、Irony.Parsing.Grammarを継承した文法クラスを作成し、
そのクラスのコンストラクタに文法を定義する処理を記述します。
下記のコードは試しに作ってみた簡単な文法クラスのコードです。

[Language("MyGrammar")]
public class MyGrammar : Grammar
{
    public MyGrammar() : base(true)
    {
        //
        //終端記号を定義
        //
        
        //数字
        var number = new NumberLiteral("number");
        number.DefaultIntTypes = new [] { TypeCode.Int32,
                                          TypeCode.Int64,
                                          NumberLiteral.TypeCodeBigInt };
        number.DefaultFloatType = TypeCode.Double;
        //識別子
        var identifier = new IdentifierTerminal("identifier");
        //コメント
        var comment = new CommentTerminal("comment", "//", "\n", "\r");

        //
        //非終端記号を定義
        //

        var Expr = new NonTerminal("Expression");
        var Term = new NonTerminal("Term");
        var BinExpr = new NonTerminal("BinaryExpression");
        var BinOp = new NonTerminal("BinaryOperator");
        var AssignmentStmt = new NonTerminal("AssignmentStatement");
        var AssignmentOp = new NonTerminal("AssignmentOperator");
        var Statement = new NonTerminal("Statement");
        var ProgramLine = new NonTerminal("ProgramLine");
        var Program = new NonTerminal("Program");

        //
        //文法を定義
        //

        Expr.Rule = Term | BinExpr;
        Term.Rule = number | identifier;
        BinExpr.Rule = Expr + BinOp + Expr;
        BinOp.Rule = ToTerm("+") | "-" | "*" | "/";
        AssignmentStmt.Rule = identifier + AssignmentOp + Expr;
        AssignmentOp.Rule = ToTerm("=") | "+=" | "-=" | "*=" | "/=";
        Statement.Rule = AssignmentStmt | Expr | Empty;
        ProgramLine.Rule = Statement + ToTerm(";");
        Program.Rule = MakeStarRule(Program, ProgramLine);
        //文法のルートを設定
        this.Root = Program;

        //
        //演算子の優先順位を定義
        //

        RegisterOperators(1, "+", "-");
        RegisterOperators(2, "*", "/");
        
        this.LanguageFlags = LanguageFlags.NewLineBeforeEOF |
                             LanguageFlags.SupportsBigInt;
    }
}
まず、6~9行目で終端記号を定義しています。
数字や識別子、コメントといった一般的な終端記号は、すでにクラスが用意してあるのでそれを使います。
他にもRegexLiteralやCustomTerminalなどといった終端記号用のクラスがあるので、必要に応じて使い分けます。

次に21~33行目で非終端記号を定義しています。
引数で与えた名前は、パースツリー内の各ノード名として使用されます。

35~49行目では文法を定義しています。
'+'と'-'が演算子オーバーロードされているので、BNFライクに記述することができます。
・定義に文字列リテラルを使う場合は、ToTerm()を使う
・'*'(繰り返し)を記述するには、MakeStarRule()を使う
といったところが、Irony独特の記述方法です。

最後に演算子の優先順位の設定とオプションの設定をしています。

以上で簡単な文法ですが定義完了です。
Irony付属のサンプルとかを見ると、C#の文法とかは定義がもっと複雑になるみたいですが、
上記のような簡単な文法なら簡潔に記述することができてイイ感じです^^
(複雑な文法定義に関しては、そのうち別エントリーで書く予定です)

次回は定義した文法でパーサを作成し、実際にパースしてみます。

2012年10月8日月曜日

C#のパーサジェネレータ Ironyを使ってみた。その1。

C#で簡単に使えるパーサジェネレータってないかな~と探していたところ、
Ironyが結構良さげだったので試しに使ってみました。

IronyはオープンソースのLALR(1)パーサジェネレータで、CodePlexで公開されています。
Irony - .NET Language Implementation Kit

Ironyの大きな特徴として、C#のコードで文法を定義できるという点があります。
下記のコードは、Irony - .NET Language Implementation Kitにあるサンプルの一部ですが、
こんな感じでC#で自然に文法を定義できます。

// 2. Non-terminals
var Expr = new NonTerminal("Expr");
var Term = new NonTerminal("Term");
var BinExpr = new NonTerminal("BinExpr", typeof(BinExprNode));
var ParExpr = new NonTerminal("ParExpr");
var UnExpr = new NonTerminal("UnExpr", typeof(UnExprNode));
var UnOp = new NonTerminal("UnOp");
var BinOp = new NonTerminal("BinOp", "operator");
var PostFixExpr = new NonTerminal("PostFixExpr", typeof(UnExprNode));
var PostFixOp = new NonTerminal("PostFixOp");
var AssignmentStmt = new NonTerminal("AssignmentStmt", typeof(AssigmentNode));
var AssignmentOp = new NonTerminal("AssignmentOp", "assignment operator");
var Statement = new NonTerminal("Statement");
var ProgramLine = new NonTerminal("ProgramLine");
var Program = new NonTerminal("Program", typeof(StatementListNode));

// 3. BNF rules
Expr.Rule = Term | UnExpr | BinExpr | PostFixExpr;
Term.Rule = number | ParExpr | identifier;
ParExpr.Rule = "(" + Expr + ")";
UnExpr.Rule = UnOp + Term;

また、ライセンスはMITライセンスなので、著作権・無保証の明示をしておけば利用や改変は自由です。
ちなみにIrony - .NET Language Implementation KitのSystem Requirementsに
>>Windows 7, .NET Framework 4.0, Visual Studio 2010
とありますが、VistaやVSExpress2010でも使えます(XPは確認していませんが、たぶん使えます)。

さてIronyの使い方ですが、まずはCodePlexのページのDOWNLOADSからzipファイルを落としてきます。
zipファイルの中にはIronyのソリューション一式が入ってるので、ソリューションを開きます。

※このときVSがExpressの場合、テスト関係のプロジェクトなど一部のプロジェクトが開けませんが、
 Ironyを使うだけなら問題ないです。ソリューションを開くごとにメッセージが表示されるので、
 うっとおしい場合は(利用不可)となっているプロジェクトをソリューションから削除しとくと楽です。

ソリューションが開けたら、ビルドを行いIrony.Dllを作成します(Irony\bin\Debug[or Release]に出力されます)。
この出来上がったDllを、パーサを使いたい(作りたい)プロジェクトの参照に追加したら準備完了です。

今回はここまで。次回は文法の定義の仕方について書きます。

2012年9月20日木曜日

Sandcastle Help File Builderを使ってみた。その2。

前回のエントリーに引き続いて、C#のドキュメントコメントを用いたドキュメント生成についてです。
今回はドキュメントコメントからどのようなドキュメントが出力されるか試してみました。

まず、以下のようなサンプルコードを書きました。
ドキュメントコメントの各タグの出力結果を確認するためのコードなので、処理内容は適当です。

/// <summary>
/// ここにクラスの概要。
/// </summary>
public class Program
{
    /// <summary>
    /// ここにプロパティの概要。
    /// </summary>
    /// <value>ここに値の説明。</value>
    public int MyProperty { get; set; }

    /// <summary>
    /// ここに概要を記述。
    /// </summary>
    /// <param name="args">引数。</param>
    public static void Main(string[] args)
    {
    }

    /// <summary>
    /// ここに概要を記述。
    /// </summary>
    /// <remarks>
    /// ここに詳しい説明。
    /// <para>段落を区切る場合はparaタグを使う。</para>
    /// <see cref="System.Int32"/>
    /// </remarks>
    /// <param name="value1">値1。</param>
    /// <param name="value2">値2。</param>
    /// <returns>value1にvalue2を加えた結果を返します。</returns>
    /// <exception cref="System.ArgumentOutOfRangeException">
    /// 引数が範囲外です。
    /// </exception>
    /// <exception cref="System.ArgumentNullException">
    /// 引数がnullです。
    /// </exception>
    /// <example>
    /// ここに使用例を記述。コード例はcodeタグ内に記述。
    /// <code>
    /// int result;
    /// result = Program.Add&lt;int, int&gt;(10, 5);
    /// </code>
    /// </example>
    /// <seealso cref="Program.Main"/>
    public static int Add(int value1, int value2)
    {
        return Program.AddHelper<int, int>(value1, value2);
    }

    /// <summary>
    /// Public以外は出力されない。
    /// </summary>
    /// <typeparam name="T1">***の型。</typeparam>
    /// <typeparam name="T2">***の型。</typeparam>
    /// <param name="value1">値1。</param>
    /// <param name="value2">値2。</param>
    /// <returns>value1にvalue2を加えた結果を返します。</returns>
    internal static int AddHelper<T1, T2>(int value1, int value2)
    {
        return value1 + value2;
    }
}

このコードをSandcastle Help File Builderで出力すると以下のようなドキュメントが作成されます。

クラスのドキュメント

MyPropertyのドキュメント













Add()のドキュメント
AddHelper()のドキュメント













ドキュメントコメント記述時の注意点
・タグの記述順は生成されるドキュメントレイアウトにも反映されるため、
   VSで///を入力したときの自動挿入の順序に従った方がmsdnと同じレイアウトになって○。
・ドキュメントコメント内の<>は&lt;&gt;で記述する。

実際に出力してみると各タグの役割がよく分かりますね。
振り返ってみると、今まで場所違い(タグ違い?)なコメントを記述をしていた気がします。。。

ちゃんと各タグの役割を理解して、より良いドキュメントが作成されるようなコメントを
記述するようにしましょう。じゃないと、わざわざドキュメントコメントで記述している意味がないですからね~。

2012年9月18日火曜日

Sandcastle Help File Builderを使ってみた。その1。


C#にはドキュメントコメントがあります。
特定のコードブロックの直前にXMLタグを記述することで、
コードのドキュメントを作成できるというものです。
(詳細はXMLドキュメント コメント(C# プログラミング ガイド)を参照のこと)

このドキュメントコメント、今までコーディング規約やC#標準ということで使ってきたけど、
ふと思い起こしてみると一度もドキュメント作成で使ったことがない。。。
そこで、ドキュメントコメントが実際にどんな感じのドキュメントになるのか実践してみました。

1.XMLドキュメントファイルを作成する。
ドキュメントを作成するには、まずドキュメントコメントからXMLドキュメントファイルを
作成する必要があります。方法は簡単で、
  • VisualStudioの場合
プロジェクトのプロパティで、ビルドタブの"XMLドキュメントファイル"の
チェックをONにしてビルド。
  • コマンドラインでコンパイルする場合
/doc:ファイル名のコンパイラオプションを使用。

これだけです。これでXMLドキュメントファイルが指定した場所に出力されます。

2.Sandcastle Help File Builderをインストールする。
1で作成したXMLドキュメントファイルですが、単なるXMLファイルです。
これをちゃんとしたドキュメントにするには、NDocやSandcastleといった
ドキュメント作成ツールを使います。今回はSandcastle Help File Builderを使って
ドキュメントを作成しようと思います。

Sandcastle Help File BuilderはCodePlexからダウンロードできます。
http://shfb.codeplex.com/
DOWNLOADSからSandcastle Help File Builder Installerをダウンロードしてください。

ダウンロードしたSHFBGuidedInstaller_***.zipを解凍したフォルダに、
SandcastleInstaller.exeがあるのでこれを実行。
あとはウィザードに従って必要なものをインストールします。

インストールは下記のブログが参考になります。
http://d.hatena.ne.jp/tueda_wolf/20120717/p3

3.Sandcastle Help File Builderでドキュメントを作成する。
インストールが完了したら、スタートメニューのすべてのプログラム-Sandcastle Help File Builderから
Sandcastle Help File Builder GUIを起動します。

起動したら、画面右側にあるProject Exploreにドキュメントを作成するexeファイルと
XMLドキュメントファイルを追加します。
Project Explore - Documentation Sourcesの右クリックメニュー[Add Documentation Source...]を
選択すると、ファイル選択ダイアログが開くので、対象のexeファイルを選択してください。

※XMLドキュメントファイルとexeファイルの拡張子以外の部分が同じなら、exeファイルを追加するだけで
 XMLドキュメントファイルも自動で追加されます。追加されない場合は、同じ方法でXMLドキュメントファイルも追加します。

次に画面左側のProject Propertiesで出力方法等の各種設定を行います。

プロパティ説明
Build
BuildAssemblerVerbosityビルドログの詳細度を設定。
BuildLogFileビルドログファイルを設定。
指定無しの場合、OutputPaehで指定したフォルダにLastBuild.logが作成される。
CleanIntermediatestrueならビルド成功時に中間ファイルを削除する。
ComponentConfigurationsビルドコンポーネントを設定。
Cached~を追加するとビルド時間を短く出来たりする。
CppCommentsFixupC++コンパイラは特定の状況で非標準のXMLコメントが生成することがあり、C++のメソッドのドキュメント化に失敗することがある。そのような場合、ここをtrueにすればドキュメント化可能になる場合がある。
DisableCodeBlockComponenttrueなら<code>タグで囲まれた部分のコードに対する色付けが無くなる。
FrameworkVersion対象の.NET Framework versionを設定。
HelpFileFormatドキュメントの出力形式を設定。選択したすべての形式で出力される。
IndentHtmlデバッグ用のプロパティ。通常はfalseで問題なし。
KeepLogFiletrueならビルド成功後もlogファイルを削除しない。falseなら削除する。
PlugInConfigurationsビルドプロセスプラグインを設定。
UserDefinedPropertiesMSBuildのプロジェクトプロパティを追加する。
Help File
ContentPlacement???(調べたんですが分かりませんでした...)
CopyrightHref"コピーライト"リンクのリンク先URL。
CopyrightText"コピーライト"リンクに表示される文字列。
FeedbackEMailAddress"フィードバックの送信"リンクのE-Mailアドレス。
FeedbackEMailLinkText"フィードバックの送信"リンクに表示される文字列。
FooterTextドキュメントの各ページに表示するフッター文字列。
HeaderTextドキュメントの各ページに表示するフッター文字列。
HelpTitleHelpFileのTitleを設定。
HtmlHelpNameコンパイルされたHtmlヘルプファイルにつけられる名前。
HtmlHelp1で出力すると[名前].chmでヘルプファイルが作成される。
LanguageHelpFileBuilderによって挿入される文字列の言語を設定。
NamingMethodHelp-htmlフォルダに作成されるファイル等の命名方法を設定。
Preliminarytrueならページヘッダーに仮のドキュメントであるという警告を出す。
Languageが日本語の場合、"[これは仮のドキュメントであり、予告なく変更されます。]"が表示される。
PresentationStyle生成されるページの外観を設定。
RootNamespaceContainertrueならルート名前空間のページが生成される。
RootNamespaceTitleルート名前空間の名前を設定。
SdkLinkTargetMSDNのLinkをクリックしたときのリンク先の開き方を設定。
HTML Help1、MS Help 2、MS Help Viewerのプロパティ-SDKLinkTypeがMsdnの場合のみ、このプロパティは有効となる。
SyntaxFiltersSyntax欄に表示するプログラミング言語を設定。
そのほかの重要なプロパティ
OutputPath生成されたHelpFileの出力先パスを設定。
Show Missing Tagsの各プロパティドキュメントコメントでタグが抜けている場合、抜けているタグのドキュメント出力位置にデフォルトのメッセージを表示するかどうかを設定。
Visibilityの各プロパティドキュメント化する対象を設定。

デフォルトの設定でもいいのですが、HelpFileFormatLanguagePresentationStyleあたりは
ちゃんと設定した方がいいと思います。

設定が完了したら、いよいよドキュメントを作成します。
メニューの[Documentation]-[Build Project]でビルドを実行します。
Build Outputウィンドウに"Build completed successfully at..."が出力されれば作成成功です。

Project PropertiesのOutputPathで設定した場所にMSDNチックなドキュメントが
作成されているのが確認できると思います。

今回はとりあえずここまで。
次回のエントリーでは、実際のドキュメントコメントとドキュメントの対応について書こうと思います。