UnityでもRoslynで構文解析やコード評価したい

# はじめに

RoslynはC#の構文解析やコード評価を行えるライブラリでC# 6以降のコンパイラでも使われています。Roslynを利用すると構文解析された結果を利用できるので文字列比較や正規表現と比べると解析漏れをなくせます。

RoslynのインストールはNuGetで使えますがUnityだと重複するdllがありそのままでは導入できません。それをUPMで簡単に導入する方法が分かったのでまとめてみました。

- UPM化されたRoslynのドキュメント [Code Analysis](https://docs.unity3d.com/Packages/com.unity.code-analysis@latest/)

Unityでは以下のプロダクトでRoslynが使われています。

- UPMの[Immediate Window](https://docs.unity3d.com/Packages/com.unity.immediate-window@latest/)
- [Utf8Json](https://github.com/neuecc/Utf8Json)のUtf8Json.UniversalCodeGenerator
- [MessagePack-CSharp](https://github.com/neuecc/MessagePack-CSharp)のMessagePack.Generator

# 環境構築

Unity2018.3以上

- Package Managerから `Add package from git URL...` できるバージョンでは `com.unity.code-analysis` を追加する
- Package Managerから `Add package from git URL...` できないバージョンでは `Packages/manifest.json` に `"com.unity.code-analysis": "0.1.2-preview",` を追加する

インストールできると以下のようにPackage Managerに追加されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8995/431d8c28-08f9-78f9-3b97-bd1a3bd42771.png)

インストールだけではDLLを参照できないのでRoslynを使いたいスクリプトフォルダにアセンブリ定義を作り、以下の設定を変更します。

- 一般 > リファレンスをオーバーライドをチェック
- アセンブリ参照に `Microsoft.CodeAnalysis` で始まるdllを追加
- エディタ拡張で使うならプラットフォームの `Editor` のみをチェック、アプリ内で使うならデフォルトの `任意のプラットフォーム` のままでOK

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8995/60b61a79-454c-7ada-02b7-c8f88b1a3228.png)

# Roslynのサンプル

[Scripting API Samples](https://github.com/dotnet/roslyn/wiki/Scripting-API-Samples)と[Getting Started C# Syntax Analysis](https://github.com/dotnet/roslyn/wiki/Getting-Started-C%23-Syntax-Analysis)を参考にサンプルを実行してみます。それぞれcode欄に実行または解析するコード、result欄にその結果を表示しています。また実行できるUnityプロジェクトは https://github.com/shiena/UnityRoslynSample にありメニューの `Tools > Roslyn Sample` を選択するとウインドウが開きます。

## Evaluate a C# expression

コードを実行します。

```csharp
string code = "1 + 2";
var result = CSharpScript.EvaluateAsync(code);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8995/c68563b7-bf8b-c3d4-2e41-d9082706bbbd.png)

## Evaluate a C# expression (strongly-typed)

ジェネリクスで結果の型を指定してコードを実行します。

```csharp
string code = "1 + 2";
var result = CSharpScript.EvaluateAsync<int>(code);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8995/be1c0894-c885-aecb-d834-9946eccaf1d4.png)

## Parameterize a script

クラス定義したパラメータをコードに適用して実行します。

```csharp
public class Globals
{
    public int X;
    public int Y;
}
string code = "X+Y";
var globals = new Globals {X = 1, Y = 2};
var result = CSharpScript.EvaluateAsync<int>(code, globals: globals);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8995/cf7aa3b3-0f2c-cebd-478b-c259646b655f.png)

## Query Methods

コードを解析してMainメソッドの最初の引数を出力します。

```csharp
string code =
            @"using System;
using System.Collections;
using System.Linq;
using System.Text;
 
namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(""Hello, World!"");
        }
    }
}";
SyntaxTree tree = CSharpSyntaxTree.ParseText(code);

var root = (CompilationUnitSyntax) tree.GetRoot();
var firstMember = root.Members[0];
var helloWorldDeclaration = (NamespaceDeclarationSyntax) firstMember;
var programDeclaration = (ClassDeclarationSyntax) helloWorldDeclaration.Members[0];
var mainDeclaration = (MethodDeclarationSyntax) programDeclaration.Members[0];
var argsParameter = mainDeclaration.ParameterList.Parameters[0];
var firstParameters = from methodDeclaration in root.DescendantNodes()
                .OfType<MethodDeclarationSyntax>()
            where methodDeclaration.Identifier.ValueText == "Main"
            select methodDeclaration.ParameterList.Parameters.First();
var argsParameter2 = firstParameters.Single();
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8995/b6306bf8-ca5e-4175-3f32-f268992f206d.png)

### SyntaxWalkers

コードを解析してSystemまたはSystem.以外で始まるusingを出力します。

```csharp
class UsingCollector : CSharpSyntaxWalker
{
    public readonly List<UsingDirectiveSyntax> Usings = new List<UsingDirectiveSyntax>();
                                                                                         
    public override void VisitUsingDirective(UsingDirectiveSyntax node)
    {
        if (node.Name.ToString() != "System" &&
            !node.Name.ToString().StartsWith("System."))
        {
            this.Usings.Add(node);
        }
    }
}
string code =
            @"using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
 
namespace TopLevel
{
    using Microsoft;
    using System.ComponentModel;
 
    namespace Child1
    {
        using Microsoft.Win32;
        using System.Runtime.InteropServices;
 
        class Foo { }
    }
 
    namespace Child2
    {
        using System.CodeDom;
        using Microsoft.CSharp;
 
        class Bar { }
    }
}";

SyntaxTree tree = CSharpSyntaxTree.ParseText(code);
var root = (CompilationUnitSyntax) tree.GetRoot();

var collector = new UsingCollector();
collector.Visit(root);
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8995/28bcd909-7f95-b18a-6831-2e93467a30c6.png)

# 参考リンク

- UPMの[Immediate Window](https://docs.unity3d.com/Packages/com.unity.immediate-window@latest/)
- [Utf8Json](https://github.com/neuecc/Utf8Json)のUtf8Json.UniversalCodeGenerator
- [MessagePack-CSharp](https://github.com/neuecc/MessagePack-CSharp)のMessagePack.Generator
- UPM化されたRoslynのドキュメント [Code Analysis](https://docs.unity3d.com/Packages/com.unity.code-analysis@latest/)
- [Unity Roslyn Sample](https://github.com/shiena/UnityRoslynSample)