C#/VB のソースコードを、Syntax Visualizer ライクのツールで確認する

SyntaxTree を見たいなら、純正の Syntax Visualizer を利用すればいいんだけど、なんだかなぁ~と思う日もあるかもしれません。それが今日でしたのでそれっぽく作ってみました。

ただし、本家より機能が少ないです。数か月に1回くらいの頻度で欲しくなるので書き留めておきます。使い道としては多分、何か Roslyn API を利用してソースコードをごにょごにょしたいときに、SyntaxNode の階層関係を知りたくなるはずなので、このツールを見ながら思いをはせるという流れを想定しています。

また、2020/01 現在、C# 6 くらい?の構文は解析対象になっていることの確認はしていますが(と言っても全部見ていないですが)、将来出るであろう新しいバージョンに対応するためには、NuGet から最新のバージョンを取得してビルドし直さないと対応しないんじゃないかなと思います。

目次

仕様

  1. 「ソースファイルの選択...」ボタンを押す → 「ファイルを開く」ダイアログが出る → C#、または VB のソースファイルを選択する。または、手書きでソースを書いていく。
  2. ツリーノードのクリックで、該当範囲が選択される。これを見ながらやりたいことをやる。

イメージ画像

「ソースファイルの選択...」ボタンを押します。 f:id:sutefu7:20200103161412p:plain

「ファイルを開く」ダイアログで、言語を選択して、ファイルを選択します。 f:id:sutefu7:20200103161424p:plain

ツリーノードを見つつ、ソースを確認しつつ、どうしようか考えます。 f:id:sutefu7:20200103161434p:plain

手書きも可能です。ただ TextBox.TextChanged イベントを購読して解析・ツリー表示し直しているので重たいかも。 f:id:sutefu7:20200103161444p:plain

ソースコード

プロジェクト構成

key value
プログラミング言語 C#
プロジェクト種類 WPF
.NET Framework 4.7.2
NuGet Microsoft.CodeAnalysis.CSharp
NuGet Microsoft.CodeAnalysis.VisualBasic
WpfApp1
  + MainWindow.xaml
  + MainWindow.xaml.cs
  + SampleCSharpSyntaxWalker.cs
  + SampleVisualBasicSyntaxWalker.cs

SampleCSharpSyntaxWalker.cs

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp1
{
    public class SampleCSharpSyntaxWalker : CSharpSyntaxWalker
    {
        private TreeView TreeView1 = null;
        private TextBox TextBox1 = null;

        public SampleCSharpSyntaxWalker(TreeView tree, TextBox box) : base(SyntaxWalkerDepth.Token)
        {
            TreeView1 = tree;
            TreeView1.MouseLeftButtonUp += TreeView_MouseLeftButtonUp;

            TextBox1 = box;
        }

        public override void Visit(SyntaxNode node)
        {
            if (node != null)
            {
                var item = new TreeViewItem
                {
                    Header = $"{node.GetType().Name} {node.Span}",
                    Tag = node,
                    IsExpanded = true,
                    Foreground = Brushes.Blue
                };
                
                if (node.Parent is null)
                {
                    TreeView1.Items.Add(item);
                }
                else
                {
                    var parentNode = FindNode(TreeView1.Items, node.Parent);
                    if (parentNode is null)
                        throw new InvalidOperationException("parentNode is null");

                    parentNode.Items.Add(item);
                }

                var tokens = node.ChildTokens();
                if (!(tokens is null))
                {
                    foreach (SyntaxToken token in tokens)
                    {
                        var tokenItem = new TreeViewItem
                        {
                            Header = $"{token.Kind()} {token.Span}",
                            Tag = token,
                            IsExpanded = true,
                            Foreground = Brushes.Green
                        };
                        item.Items.Add(tokenItem);

                        var trivias = token.GetAllTrivia();
                        if (!(trivias is null))
                        {
                            foreach (SyntaxTrivia trivia in trivias)
                            {
                                var triviaItem = new TreeViewItem
                                {
                                    Header = $"{trivia.Kind()} {trivia.Span}",
                                    Tag = trivia,
                                    IsExpanded = true,
                                    Foreground = Brushes.DarkRed
                                };
                                tokenItem.Items.Add(triviaItem);
                            }
                        }

                    }
                }
            }

            base.Visit(node);
        }

        private TreeViewItem FindNode(ItemCollection items, SyntaxNode node)
        {
            if (items is null)
                return null;

            foreach (TreeViewItem item in items)
            {
                if (!(item.Tag is null))
                {
                    var instance = item.Tag as SyntaxNode;
                    if (instance == node)
                        return item;
                }

                if (!(item.Items is null))
                {
                    var x = FindNode(item.Items, node);
                    if (!(x is null))
                    {
                        return x;
                    }
                }
            }
            
            return null;
        }

        private void TreeView_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (TreeView1.SelectedItem is null)
                return;

            var item = TreeView1.SelectedItem as TreeViewItem;
            var span = default(TextSpan);

            if (item.Tag is SyntaxNode)
                span = ((SyntaxNode)item.Tag).Span;
            else if (item.Tag is SyntaxToken)
                span = ((SyntaxToken)item.Tag).Span;
            else if (item.Tag is SyntaxTrivia)
                span = ((SyntaxTrivia)item.Tag).Span;

            TextBox1.SelectionLength = 0;
            TextBox1.SelectionStart = span.Start;
            TextBox1.SelectionLength = span.End - span.Start;
            TextBox1.Focus();
        }
        
        // 定義した位置順でノードをソートする
        public void OrderBySpanAsc()
        {
            var items = TreeView1.Items.OfType<TreeViewItem>()
                .OrderBy(x => (x.Tag as SyntaxNode).Span.Start)
                .ToList();

            foreach (var item in items)
                OrderBySpanAsc(item);

            TreeView1.Items.Clear();
            foreach (var item in items)
                TreeView1.Items.Add(item);
        }

        private void OrderBySpanAsc(TreeViewItem parent)
        {
            if (parent.Items is null)
                return;

            var items = parent.Items.OfType<TreeViewItem>()
                .OrderBy(x =>
                {
                    if (x.Tag is SyntaxNode)
                    {
                        return ((SyntaxNode)x.Tag).Span.Start;
                    }
                    else if (x.Tag is SyntaxToken)
                    {
                        return ((SyntaxToken)x.Tag).Span.Start;
                    }
                    else if (x.Tag is SyntaxTrivia)
                    {
                        return ((SyntaxTrivia)x.Tag).Span.Start;
                    }
                    else
                    {
                        return -1;
                    }
                })
                .ToList();

            foreach (var item in items)
                OrderBySpanAsc(item);

            parent.Items.Clear();
            foreach (var item in items)
                parent.Items.Add(item);
        }
    }
}

SampleVisualBasicSyntaxWalker.cs

ほぼ同じなので、処理を共通にすればよかった・・・。元気が出たらそのうち・・・。

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.VisualBasic;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp1
{
    public class SampleVisualBasicSyntaxWalker : VisualBasicSyntaxWalker
    {
        private TreeView TreeView1 = null;
        private TextBox TextBox1 = null;

        public SampleVisualBasicSyntaxWalker(TreeView tree, TextBox box) : base(SyntaxWalkerDepth.Token)
        {
            TreeView1 = tree;
            TreeView1.MouseLeftButtonUp += TreeView_MouseLeftButtonUp;

            TextBox1 = box;
        }

        public override void Visit(SyntaxNode node)
        {
            if (node != null)
            {
                var item = new TreeViewItem
                {
                    Header = $"{node.GetType().Name} {node.Span}",
                    Tag = node,
                    IsExpanded = true,
                    Foreground = Brushes.Blue
                };

                if (node.Parent is null)
                {
                    TreeView1.Items.Add(item);
                }
                else
                {
                    var parentNode = FindNode(TreeView1.Items, node.Parent);
                    if (parentNode is null)
                        throw new InvalidOperationException("parentNode is null");

                    parentNode.Items.Add(item);
                }

                var tokens = node.ChildTokens();
                if (!(tokens is null))
                {
                    foreach (SyntaxToken token in tokens)
                    {
                        var tokenItem = new TreeViewItem
                        {
                            Header = $"{token.Kind()} {token.Span}",
                            Tag = token,
                            IsExpanded = true,
                            Foreground = Brushes.Green
                        };
                        item.Items.Add(tokenItem);

                        var trivias = token.GetAllTrivia();
                        if (!(trivias is null))
                        {
                            foreach (SyntaxTrivia trivia in trivias)
                            {
                                var triviaItem = new TreeViewItem
                                {
                                    Header = $"{trivia.Kind()} {trivia.Span}",
                                    Tag = trivia,
                                    IsExpanded = true,
                                    Foreground = Brushes.DarkRed
                                };
                                tokenItem.Items.Add(triviaItem);
                            }
                        }

                    }
                }
            }

            base.Visit(node);
        }

        private TreeViewItem FindNode(ItemCollection items, SyntaxNode node)
        {
            if (items is null)
                return null;

            foreach (TreeViewItem item in items)
            {
                if (!(item.Tag is null))
                {
                    var instance = item.Tag as SyntaxNode;
                    if (instance == node)
                        return item;
                }

                if (!(item.Items is null))
                {
                    var x = FindNode(item.Items, node);
                    if (!(x is null))
                    {
                        return x;
                    }
                }
            }

            return null;
        }

        private void TreeView_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (TreeView1.SelectedItem is null)
                return;

            var item = TreeView1.SelectedItem as TreeViewItem;
            var span = default(TextSpan);

            if (item.Tag is SyntaxNode)
                span = ((SyntaxNode)item.Tag).Span;
            else if (item.Tag is SyntaxToken)
                span = ((SyntaxToken)item.Tag).Span;
            else if (item.Tag is SyntaxTrivia)
                span = ((SyntaxTrivia)item.Tag).Span;

            TextBox1.SelectionLength = 0;
            TextBox1.SelectionStart = span.Start;
            TextBox1.SelectionLength = span.End - span.Start;
            TextBox1.Focus();
        }
        
        // 定義した位置順でノードをソートする
        public void OrderBySpanAsc()
        {
            var items = TreeView1.Items.OfType<TreeViewItem>()
                .OrderBy(x => (x.Tag as SyntaxNode).Span.Start)
                .ToList();

            foreach (var item in items)
                OrderBySpanAsc(item);

            TreeView1.Items.Clear();
            foreach (var item in items)
                TreeView1.Items.Add(item);
        }

        private void OrderBySpanAsc(TreeViewItem parent)
        {
            if (parent.Items is null)
                return;

            var items = parent.Items.OfType<TreeViewItem>()
                .OrderBy(x =>
                {
                    if (x.Tag is SyntaxNode)
                    {
                        return ((SyntaxNode)x.Tag).Span.Start;
                    }
                    else if (x.Tag is SyntaxToken)
                    {
                        return ((SyntaxToken)x.Tag).Span.Start;
                    }
                    else if (x.Tag is SyntaxTrivia)
                    {
                        return ((SyntaxTrivia)x.Tag).Span.Start;
                    }
                    else
                    {
                        return -1;
                    }
                })
                .ToList();

            foreach (var item in items)
                OrderBySpanAsc(item);

            parent.Items.Clear();
            foreach (var item in items)
                parent.Items.Add(item);
        }
    }
}

MainWindow.xaml

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="簡易 Syntax Visualizer" Height="600" Width="800">
    
    <Grid>

        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>

        <ToolBarTray Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
            <ToolBar>
                <TextBlock x:Name="textBlock1" Text="C#" VerticalAlignment="Center" Margin="10,0,10,0" />
                <Button x:Name="button1" Content="ソースファイルの選択..." Click="Button1_Click" />
            </ToolBar>
        </ToolBarTray>
        
        <TreeView Grid.Row="1" Grid.Column="0" x:Name="treeView1" />

        <GridSplitter Grid.Row="1" Grid.Column="1" Width="5" HorizontalAlignment="Center" VerticalAlignment="Stretch" />

        <TextBox Grid.Row="1" Grid.Column="2" x:Name="textBox1" TextWrapping="WrapWithOverflow" AcceptsReturn="True" AcceptsTab="True" FontSize="16" TextChanged="TextBox1_TextChanged" />
        
    </Grid>
    
</Window>

MainWindow.xaml.cs

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.VisualBasic;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfApp1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

//        public void SetCSharpTree()
//        {
//            var sourceCode = @"using System;
//using System.Collections.Generic;
//using System.Linq;
//using System.Text;
//using System.Threading.Tasks;

//namespace ConsoleApp28
//{
//    class Program
//    {
//        static void Main(string[] args)
//        {
//            var x = 1;
//            int y = Math.Max(12, 24);
//            Console.WriteLine(""aaa"");
//            Console.WriteLine($""a{x}aa"");
//        }
//    }
//}
//";

//            // ソースコードを解析して、階層関係を TreeView にセット
//            var tree = CSharpSyntaxTree.ParseText(sourceCode);
//            var walker = new SampleCSharpSyntaxWalker(treeView1, textBox1);
//            walker.Visit(tree.GetRoot());
//            walker.OrderBySpanAsc();

//            // ソースコードを表示
//            textBox1.Text = sourceCode;
//        }

        private void Button1_Click(object sender, RoutedEventArgs e)
        {
            var dlg = new OpenFileDialog();
            dlg.Filter = "C# ソースファイル(*.cs)|*.cs|VisualBasic ソースファイル(*.vb)|*.vb|全てのファイル(*.*)|*.*";
            dlg.FilterIndex = 0;
            dlg.Multiselect = false;

            var result = dlg.ShowDialog(this);
            if (result.GetValueOrDefault())
            {
                InitializeAndAnalize(dlg.FileName);
            }
        }

        // インスタンス生成するたびにイベント購読し続けるのはよろしくないので、使いまわすように移動してきた
        private SampleCSharpSyntaxWalker csWalker = null;
        private SampleVisualBasicSyntaxWalker vbWalker = null;

        private void InitializeAndAnalize(string fileName)
        {
            var sourceCode = File.ReadAllText(fileName);
            textBox1.Text = sourceCode;
        }

        private void TextBox1_TextChanged(object sender, TextChangedEventArgs e)
        {
            treeView1.Items.Clear();

            // ソースコードのキーワードをもとに、プログラミング言語を判定
            var sourceCode = textBox1.Text;
            var csKeywords = new List<string> { "using ", "class ", "struct ", "var " };
            var vbKeywords = new List<string> { "Imports ", "Class ", "Structure ", "Dim " };

            if (csKeywords.Any(x => sourceCode.Contains(x)))
            {
                textBlock1.Text = "C#";

                if (csWalker is null)
                    csWalker = new SampleCSharpSyntaxWalker(treeView1, textBox1);

                var tree = CSharpSyntaxTree.ParseText(sourceCode);
                csWalker.Visit(tree.GetRoot());
                csWalker.OrderBySpanAsc();

            }
            else if (vbKeywords.Any(x => sourceCode.Contains(x)))
            {
                textBlock1.Text = "VisualBasic";

                if (vbWalker is null)
                    vbWalker = new SampleVisualBasicSyntaxWalker(treeView1, textBox1);

                var tree = VisualBasicSyntaxTree.ParseText(sourceCode);
                vbWalker.Visit(tree.GetRoot());
                vbWalker.OrderBySpanAsc();
            }
        }
    }
}