kazuk は null に触れてしまった

C# / .NET 系技術ネタ縛りでお送りしております

an Quick Start for Roslyn (2)–IText と TextSpan


というわけで Roslyn でのパース結果の詳細な見方、主に位置情報について。

前回は SyntaxNode を列挙して軽く眺めてみただけですので、その先の詳細を見ていきましょう。

まず、SyntaxNode には全く文字列は保持されていません。SyntaxNode には Span と FullSpan というTextSpan型のプロパティがありこれらのプロパティには開始位置と長さが保持されていて SyntaxTree の IText 上の範囲の文字列が SyntaxNode の文字列を表現します。

IText の取得方法

まずダメな方法です。

各 SyntaxNode には GetFullTextAsIText() があります、このメソッドはその要素の内容を示す IText を返します。結果のIText の中での位置 0 はその要素の先頭を示しますので、もとになるITextとは別のものです SyntaxNode の位置を示すSpan/FullSpanプロパティの値は意味を持ちません。

続いてイケる方法です。

各SyntaxNodeはSyntaxTreeへの参照を持っています。SyntaxNode.SyntaxTree プロパティ、このSyntaxTreeは TryGetRoot メソッドでルート要素を取得したSyntaxTreeへの参照を保持しています。SyntaxTreeのGetTextはITextを返しますので、このITextを利用します。

また IText は StringText クラスで実装されていますので、 文字列を元に IText を構築することができます。IText をパラメータとして ParseCompirationUnit によってSyntaxTreeを取得することができます。このSyntaxTree のGetTextはもとになったStringTextを返します。

            var text = new StringText(@"using System;

namespace RoslynTest
{
    public class Program
    {
        public static void Main( string[] args )
        {
            Console.WriteLine( Console.ReadLine() );
        }
    }
}
");
            var tree = SyntaxTree.ParseCompilationUnit(text);
            var itxt= tree.GetText();
            Console.WriteLine(object.ReferenceEquals(text, itxt));
            Console.ReadLine();

結果: True

結果的に SyntaxNode の SyntaxTree プロパティは ParseCompilationUnit の返した SyntaxTree であり、SyntaxTreeから取得された IText は ParseCompilationUnit に渡された、または内部で生成されたITextで、これらは常に同一の参照を返します(imuttableであるといえます)ので元にしたIText を利用する、SyntaxTreeからITextを取得するのは同じ事で、SyntaxNodeからSyntaxTreeを参照してGetTextでITextを利用するのも同じことです。

僕は自分でStringTextで生成したIText (コード中のtext変数)を直接に参照するようにしました。

.NET Framework は純粋さを前提にできないので、各SyntaxNodeがSyntaxTreeの同一の参照を返すという仮定はできないはずだし、SyntaxTreeのGetTextが返すITextが常に同一という仮定もできないはずなので、同一の参照を使い続けるという仮定を実現させるにはできるだけ早い時期に取得したIText をそのまま使う必要があります。また new を明示的にしているのでnullではないという仮定も成立するはずなので null チェックを省略させる意味もあります。枝葉の最適化にコダワル所じゃないのですけど脳内で降りた判定を明かしておくと「同じこと」という物に微妙な効率差が有りそうと踏んで選んでることだけはなんか書きたかったので書いときます。

オプティマイザの観点でいうと仮定できないというと語弊があって、楽観的に仮定してみて仮定に危うさを感じる要素が一つでも見つかればその仮定を取りやめるのが普通だったりします。この楽観的仮定を最後まで通してあげるとオプティマイザが効率的に処理するコードに落としてくれるわけですので、危うさを臭わせないほうが早いコードが出るという事になります。

TextSpanとSyntaxNodeのSpan/FullSpanの違い

さて、ITextがあり、ITextと関連性のあるTextSpanがあればstring IText.GetText( TextSpan ) によって実際の文字列を取得する事ができます。SyntaxNode は Span と FullSpan の二つのTextSpan型プロパティを持っていてそれぞれ構文要素そのものと、その構文要素に付随するデリミタを保持します。

TextSpan は operator == / operator !=の適切な演算子オーバーロードを持っていますので、これらの違いが見える部分だけを抽出してみましょう。

        // 概要:
        //     Determines if two instances of TextSpan are different.
        public static bool operator !=(TextSpan left, TextSpan right);
        //
        // 概要:
        //     Determines if two instances of TextSpan are the same.
        public static bool operator ==(TextSpan left, TextSpan right);

前回のコードを元に Span と FullSpan が異なる要素だけテキストを取得して表示しているのが以下になります。

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Roslyn.Compilers;
using Roslyn.Compilers.CSharp;
using Roslyn.Services;
using Roslyn.Services.CSharp;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            var text = new StringText(@"using System;

namespace RoslynTest
{
    public class Program
    {
        public static void Main( string[] args )
        {
            Console.WriteLine( Console.ReadLine() );
        }
    }
}
");
            var tree = SyntaxTree.ParseCompilationUnit(text);
            var itxt= tree.GetText();
            Console.WriteLine(object.ReferenceEquals(text, itxt));
            Console.ReadLine();

            Stack<SyntaxNode> nodes = new Stack<SyntaxNode>();
            SyntaxNode node;
            tree.TryGetRoot(out node);
            nodes.Push(node);

            while (nodes.Count != 0)
            {
                node = nodes.Pop();
                if (node.Span != node.FullSpan)
                {
                    Console.WriteLine("Full:\t"+node.FullSpan +"\'"+ text.GetText(node.FullSpan)+"\'");
                    Console.WriteLine("Span:\t"+node.Span +"\'"+ text.GetText(node.Span)+"\'");
                }

                foreach (var child in node.ChildNodes())
                {
                    nodes.Push(child);
                }
            }
            Console.ReadLine();
        }
    }
}

わかりやすい例としては以下の様な表示が含まれます。

Full:   [136..190)’            Console.WriteLine( Console.ReadLine() );

Span:   [148..188)’Console.WriteLine( Console.ReadLine() );’

FullSpan には前方のスペースおよび、後方の改行が含まれますが、Spanには含まれない事が解ります。

それって何行目?その行は?

IText には int GetLineNumberFromPosition(int position )があり、これにより指定の位置の行の番号を得る事ができます。

行番号、position ともに0始まりのようです。

            SyntaxNode node;
            tree.TryGetRoot(out node);
            Console.WriteLine(node.FullSpan.Start);
            Console.WriteLine(text.GetLineNumberFromPosition(node.FullSpan.Start));

の出力は 0, 0 です。

同様に IText には ITextLine GetLineFromPosition(int position ) があり、指定の位置を含む行を取得することができます。

ITextLine の実装である TextLine は ToString をオーバライドしていないので単純に ToString すると残念な結果である型名だけが出ます。GetText で内容を取得すると、行末の改行を含まない文字列が返されます

ITextLine はいくつかのプロパティを持っていて、これらで行の範囲を示します。

Extent は行の改行を含まない範囲を示すTextSpanを返します。ExtentIncludeLineBreakは同様に改行を含む範囲を返します。Start、Endはそれぞれ行頭の位置、行末の位置を示します。EndIncludeLineBreakは改行を含む行末です。

(2)のまとめ

シンタックスツリーの操作をやるって言っておきながら、書いてみたら長くなったので回を分けるというやつで、今回は SyntaxNode の位置情報である TextSpan と、位置情報と文字列の関係を示す IText 、位置と行の関係を示す ITextLineをもとに SyntaxNode の位置を解説してみました。

次回もシンタックスツリーの操作にはきっと入れずにシンタックスに付随する SyntaxTrivia についての話となると思います。

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中

%d人のブロガーが「いいね」をつけました。