[C#][Roslyn] Roslyn による Visual Studio のアドイン

※ この内容は、『こみゅぷらす Tech Aid 2013』 (2013-07-27 新宿,東京) にて実際のデモと共に発表予定。
■ Roslyn について
Roslyn は、C# や Visual Basic のコンパイラーの内部の API 等を公開したものだ。"Compiler as a Service" と表現されている。
現在、次の場所や NuGet で入手可能だ。
コンパイラーのコード解析部分等の機能が公開されている。
次にあげる Channel 9 のビデオと資料が参考になるだろう。
Channel 9 にある Roslyn 関連ビデオ (古い順)
- Future directions for C# and Visual Basic – BUILD2011 (September 15, 2011)
- Going Deeper with Project Roslyn: Exposing the C# and VB compiler’s code analysis – Lang.NEXT 2012 (April 3, 2012)
- Roslyn Anders Hejlsberg Q&A: TypeScript, C#, Roslyn, and More – Build 2013 (June 27, 2013)
Roslyn には次にあげるような機能がある。
Roslyn の機能
- C# や Visual Basic のソースコード解析機能
- Visual Studio の拡張機能
- C#スクリプト機能
- 等
今回は、この中の「ソースコード解析機能」を用いて、簡単な Visual Studio のアドインを作ってみたい。
C# のソースコードを解析して、識別子等を数えて、それを WPF で作成したウィンドウに表示させてみよう。
■ 「Visual Studio アドイン」プロジェクトの新規作成
先ず、Visual Studio を用いて、「Visual Studio アドイン」プロジェクトを新規作成する。

次に Roslyn をインストールし、このプロジェクトから参照する。
NuGet を使うことで、簡単にこの作業を行うことができる。
「ソリューション エクスプローラー」でプロジェクト名を右クリックし、「NuGet パッケージの管理…」を選択する。
「NuGet パッケージの管理」ダイアログ ボックスが現れるので、次のように「オンラインの検索」で、"Roslyn" を検索し、"Roslyn" を選択して「インストール」する。

Roslyn の追加後のプロジェクトの参照設定は、次のようになる。

■ ViewModel の作成
今回は、WPF を使ってウィンドウを出すことにする。
そのウィンドウ用に、次のような ViewModel を用意した。
ソースコード内の識別子や using、クラス等の数を保持するクラスだ。
※ 実装の参考:
using System;
using System.ComponentModel;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
namespace AddinByRoslyn
{
public static class PropertyChangedEventHandlerExtensions
{
public static void Raise(this PropertyChangedEventHandler onPropertyChanged, object sender, [CallerMemberName] string propertyName = "")
{
if (onPropertyChanged != null)
onPropertyChanged(sender, new PropertyChangedEventArgs(propertyName));
}
public static void Raise<PropertyType>(this PropertyChangedEventHandler onPropertyChanged, object sender, Expression<Func<PropertyType>> propertyExpression)
{ onPropertyChanged.Raise(sender, propertyExpression.GetMemberName()); }
static string GetMemberName<MemberType>(this Expression<Func<MemberType>> expression)
{ return ((MemberExpression)expression.Body).Member.Name; }
}
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
int identifierCount = 0;
public int IdentifierCount
{
get { return identifierCount; }
set {
if (value != identifierCount) {
identifierCount = value;
PropertyChanged.Raise(this);
RaiseUpdateData();
}
}
}
int usingCount = 0;
public int UsingCount
{
get { return usingCount; }
set {
if (value != usingCount) {
usingCount = value;
PropertyChanged.Raise(this);
RaiseUpdateData();
}
}
}
int classCount = 0;
public int ClassCount
{
get { return classCount; }
set {
if (value != classCount) {
classCount = value;
PropertyChanged.Raise(this);
RaiseUpdateData();
}
}
}
int fieldCount = 0;
public int FieldCount
{
get { return fieldCount; }
set {
if (value != fieldCount) {
fieldCount = value;
PropertyChanged.Raise(this);
RaiseUpdateData();
}
}
}
int propertyCount = 0;
public int PropertyCount
{
get { return propertyCount; }
set {
if (value != propertyCount) {
propertyCount = value;
PropertyChanged.Raise(this);
RaiseUpdateData();
}
}
}
int methodCount = 0;
public int MethodCount
{
get { return methodCount; }
set {
if (value != methodCount) {
methodCount = value;
PropertyChanged.Raise(this);
RaiseUpdateData();
}
}
}
int variableCount = 0;
public int VariableCount
{
get { return variableCount; }
set {
if (value != variableCount) {
variableCount = value;
PropertyChanged.Raise(this);
RaiseUpdateData();
}
}
}
int ifCount = 0;
public int IfCount
{
get { return ifCount; }
set {
if (value != ifCount) {
ifCount = value;
PropertyChanged.Raise(this);
RaiseUpdateData();
}
}
}
int lambdaCount = 0;
public int LambdaCount
{
get { return lambdaCount; }
set {
if (value != lambdaCount) {
lambdaCount = value;
PropertyChanged.Raise(this);
RaiseUpdateData();
}
}
}
public object Data
{
get {
return new [] {
new { 名称 = "識別子の数" , 結果 = IdentifierCount },
new { 名称 = "using の数" , 結果 = UsingCount },
new { 名称 = "クラスの数" , 結果 = ClassCount },
new { 名称 = "フィールドの数", 結果 = FieldCount },
new { 名称 = "プロパティの数", 結果 = PropertyCount },
new { 名称 = "メソッドの数" , 結果 = MethodCount },
new { 名称 = "変数の数" , 結果 = VariableCount },
new { 名称 = "if の数" , 結果 = IfCount },
new { 名称 = "ラムダ式の数" , 結果 = LambdaCount }
};
}
}
string result = string.Empty;
public string Result
{
get { return result; }
set {
if (value != result) {
result = value;
PropertyChanged.Raise(this);
}
}
}
public void Clear()
{
IdentifierCount =
UsingCount =
ClassCount =
FieldCount =
PropertyCount =
MethodCount =
VariableCount =
IfCount =
LambdaCount = 0;
Result = string.Empty;
}
void RaiseUpdateData()
{ PropertyChanged.Raise(this, () => Data); }
}
}
■ Roslyn を用いたコード解析部の作成
では、Roslyn を用いたコード解析部を作成してみよう。
ここでは、Roslyn にある Roslyn.Compilers.CSharp.SyntaxWalker というクラスを継承して SyntaxCounter というクラスを作成する。
この SyntaxWalker クラスは、Visitor パターンになっていて、Visit メソッドを呼ぶことで、全ノードを辿り、ノードの種類毎の virtual メソッドを呼んでくれる。
例えば、識別子のノードの場合には、VisitIdentifierName という virtual メソッドが呼ばれる。
それぞれの virtual メソッドをオーバーライドすることで、そこに処理を書くことができる訳だ。
SyntaxCounter では、オーバーライドしたメソッドのそれぞれで呼ばれた回数を数えることで、ソースコード内の、識別子や using、クラス等の数を知ることにする。
using Roslyn.Compilers.CSharp;
using System.IO;
using System.Text;
namespace AddinByRoslyn
{
class SyntaxCounter : SyntaxWalker
{
readonly ViewModel viewModel;
StringBuilder stringBuilder; // viewModel.Result 用
public SyntaxCounter(ViewModel viewModel)
{ this.viewModel = viewModel; }
// ソース ファイルの中を解析
public void AnalyzeSourceFile(string sourceFileName)
{
using (var reader = new StreamReader(sourceFileName, false)) {
var sourceCode = reader.ReadToEnd();
Analyze(sourceCode);
}
}
// ソース コードの中を解析
void Analyze(string sourceCode)
{
// Roslyn.Compilers.CSharp.SyntaxTree クラスによるシンタックス ツリーの取得
var tree = SyntaxTree.ParseText(sourceCode);
Analyze(tree.GetRoot()); // シンタックス ツリーのルート要素を解析
}
// ルート要素の中を解析
void Analyze(CompilationUnitSyntax root)
{
viewModel.Clear();
stringBuilder = new StringBuilder();
Visit(root); // Visit メソッドにより、全ノードを辿る (Visitor パターン)
viewModel.Result = stringBuilder.ToString();
}
// 全ノードについて
public override void DefaultVisit(SyntaxNode node)
{
base.DefaultVisit(node);
// ノードの情報を stringBuilder に追加
stringBuilder.AppendLine(string.Format("NodeType: {0}, Node: {1}", node.GetType().Name, node.GetText()));
}
// 識別子のノードの場合
public override void VisitIdentifierName(IdentifierNameSyntax node)
{
base.VisitIdentifierName(node);
viewModel.IdentifierCount++; // 識別子を数える
}
// using のノードの場合
public override void VisitUsingDirective(UsingDirectiveSyntax node)
{
base.VisitUsingDirective(node);
viewModel.UsingCount++; // using を数える
}
// クラスのノードの場合
public override void VisitClassDeclaration(ClassDeclarationSyntax node)
{
base.VisitClassDeclaration(node);
viewModel.ClassCount++; // クラスを数える
}
// フィールドのノードの場合
public override void VisitFieldDeclaration(FieldDeclarationSyntax node)
{
base.VisitFieldDeclaration(node);
viewModel.FieldCount++; // フィールドを数える
}
// プロパティのノードの場合
public override void VisitPropertyDeclaration(PropertyDeclarationSyntax node)
{
base.VisitPropertyDeclaration(node);
viewModel.PropertyCount++; // プロパティを数える
}
// メソッドのノードの場合
public override void VisitMethodDeclaration(MethodDeclarationSyntax node)
{
base.VisitMethodDeclaration(node);
viewModel.MethodCount++; // メソッドを数える
}
// 変数のノードの場合
public override void VisitVariableDeclaration(VariableDeclarationSyntax node)
{
base.VisitVariableDeclaration(node);
viewModel.VariableCount++; // 変数を数える
}
// if のノードの場合
public override void VisitIfStatement(IfStatementSyntax node)
{
base.VisitIfStatement(node);
viewModel.IfCount++; // if を数える
}
// シンプルなラムダ式のノードの場合
public override void VisitSimpleLambdaExpression(SimpleLambdaExpressionSyntax node)
{
base.VisitSimpleLambdaExpression(node);
viewModel.LambdaCount++; // ラムダ式を数える
}
// 括弧付きのラムダ式のノードの場合
public override void VisitParenthesizedLambdaExpression(ParenthesizedLambdaExpressionSyntax node)
{
base.VisitParenthesizedLambdaExpression(node);
viewModel.LambdaCount++; // ラムダ式を数える
}
}
}
■ View の作成
次に View を追加する。プロジェクトに "View" という名前の WPF のウィンドウを一つ追加する。
この View は、先の ViewModel を表示するためのウィンドウだ。
プロジェクトに System.Xaml への参照設定の追加が必要になる。
View.xaml
XAML の Grid 内に、ViewModel の "Result" を表示するための TextBox と "Data" を表示するための DataGrid を追加しておく。
<Window x:Class="AddinByRoslyn.View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="500">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBox TextWrapping="Wrap" Text="{Binding Path=Result}" Grid.RowSpan="2" FontFamily="Meiryo" FontSize="14" IsReadOnlyCaretVisible="True" VerticalScrollBarVisibility="Auto" />
<DataGrid Grid.Row="1" ItemsSource="{Binding Path=Data}" FontFamily="Meiryo" FontSize="14" />
</Grid>
</Window>

View.xaml.cs
View の C# 部分では、ViewModel のインスタンスを保持し、それを DataContext とする。
また、コンストラクターで C# のソース ファイル名を受け取り、それを上で作成した SyntaxCounter クラスを使って解析する。
using System.Windows;
namespace AddinByRoslyn
{
public partial class View : Window
{
ViewModel viewModel = new ViewModel();
public View(string sourceFileName)
{
InitializeComponent();
DataContext = viewModel; // DataContext に ViewModel のインスタンスを設定
if (!string.IsNullOrWhiteSpace(sourceFileName))
// SyntaxCounter クラスを用いたソース ファイルの解析
new SyntaxCounter(viewModel).AnalyzeSourceFile(sourceFileName);
}
}
}
■ Visual Studio アドイン部
最後に、Visual Studio アドイン部のコードをカスタマイズする。
カスタマイズするのは、プロジェクトの新規作成時に自動生成された Connect.cs だ。
Exec というメソッドがあるので、この中で View を作成し、ソースコード ファイル名を渡すようにする。
using EnvDTE;
using EnvDTE80;
using Extensibility;
using Microsoft.VisualStudio.CommandBars;
using System;
namespace AddinByRoslyn
{
public class Connect : IDTExtensibility2, IDTCommandTarget
{
DTE2 _applicationObject = null;
AddIn _addInInstance = null;
public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
{
_applicationObject = (DTE2)application;
_addInInstance = (AddIn)addInInst;
if(connectMode == ext_ConnectMode.ext_cm_UISetup) {
var contextGUIDS = new object[] { };
var commands = (Commands2)_applicationObject.Commands;
var toolsMenuName = "Tools";
var menuBarCommandBar = ((CommandBars)_applicationObject.CommandBars)["MenuBar"];
var toolsControl = menuBarCommandBar.Controls[toolsMenuName];
var toolsPopup = (CommandBarPopup)toolsControl;
try {
var command = commands.AddNamedCommand2(_addInInstance, "AddinByRoslyn", "AddinByRoslyn", "Executes the command for AddinByRoslyn", true, 59, ref contextGUIDS, (int)vsCommandStatus.vsCommandStatusSupported+(int)vsCommandStatus.vsCommandStatusEnabled, (int)vsCommandStyle.vsCommandStylePictAndText, vsCommandControlType.vsCommandControlTypeButton);
if (command != null && toolsPopup != null)
command.AddControl(toolsPopup.CommandBar, 1);
} catch(System.ArgumentException) {
}
}
}
public void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom) {}
public void OnAddInsUpdate (ref Array custom) {}
public void OnStartupComplete(ref Array custom) {}
public void OnBeginShutdown (ref Array custom) {}
public void QueryStatus(string commandName, vsCommandStatusTextWanted neededText, ref vsCommandStatus status, ref object commandText)
{
if (neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedNone &&
commandName == "AddinByRoslyn.Connect.AddinByRoslyn")
status = (vsCommandStatus)vsCommandStatus.vsCommandStatusSupported|vsCommandStatus.vsCommandStatusEnabled;
}
// カスタマイズする Exec メソッド
public void Exec(string commandName, vsCommandExecOption executeOption, ref object varIn, ref object varOut, ref bool handled)
{
if (executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault &&
commandName == "AddinByRoslyn.Connect.AddinByRoslyn") {
var view = new View(GetSourceFileName()); // View のインスタンスにソースファイル名を渡す
view.Show();
handled = true;
} else {
handled = false;
}
}
// ソース ファイル名の取得 (追加する private メソッド)
string GetSourceFileName()
{
var items = _applicationObject.SelectedItems;
if (items.Count == 0)
return null;
var item = items.Item(1);
return item.ProjectItem.get_FileNames(1);
}
}
}
■ 実行
それでは、試してみよう。
Visual Studio から実行してみると、別の Visual Studio が起動する。
新たに起動した Visual Studio で、メニュー から「ツール」 – 「アドイン マネージャー」を開く。

作成したアドインが追加できるようになっているのが分かる。
実際に、新たに起動した Visual Studio で今回作成したプロジェクトを開き、ViewModel.cs を開いた状態で、アドインを実行してみよう。
新たに起動した方の Visual Studio で、メニュー から「ツール」 で、新しいアドインを実行する。

アドインが起動すると、ウィンドウが開き、結果が表示される。

■ まとめ
今回は、Roslyn を使って単に何種類かの文法要素を数えただけだが、同様の方法で、より複雑な解析を行ったり、一部を変更したりすることもできる。
また、先に述べたように、Roslyn の機能は C# や Visual Basic のソースコードの解析だけではない。他の機能についても、いずれ解説して行きたい。
ディスカッション
コメント一覧
まだ、コメントがありません