# Unity 中使用 Roslyn ## 背景 & 目标 之前因为时间原因,一直没有功夫研究 Roslyn,最近刚好有时间,开篇博客记录一下过程。在我这的实际使用情况下,需要将现有的 Roslyn 规则封装在 UPM 中,业务只需要引用一行 Package 记录,就可以拥有自定义编译规则的能力 刚好在 ET 的开源代码库中, sj 老哥带来了很多自定义的编译规则,但是 ET 的实际使用方式和 Package 的封装有出入,因此这个部分需要适配 由于引入 Package 机制,这样就导致实际测试 Roslyn 规则时,带来了新的麻烦,非常不方便,因此需要一个 Roslyn 的单元测试机制 ## Unity Roslyn 规则 官方文档写的非常模糊,跟着官方文档一步一步做,最后你会发现怎么都不行,逛了相关论坛发现了这个仓库 [unity-roslyn-analyzers](https://github.com/sandolkakos/unity-roslyn-analyzers) 如果你也有类似需求,建议直接拷贝这个仓库的 RoslynAnalyzers 文件夹 到你实际的项目中 ![](https://blog.liuocean.synology.me:9001/blog/old/16973476048177.jpg) - 在实际需要 Roslyn 的代码库中引用 asmdef - Emptyxx.cs 文件,不可以删,否则 Analyzers.asmdef 不会编译 - Microsoft.Unity.Analyzers.dll 就是实际的 Roslyn 代码 这里的 dll 文件需要注意,platforms 全部为空,否则会编译错误 ![](https://blog.liuocean.synology.me:9001/blog/old/16973477824420.jpg) ## Roslyn Package 项目 这里推荐直接去 [Microsoft.Unity.Analyzers](https://github.com/microsoft/Microsoft.Unity.Analyzers) 下载,作为 Unity Roslyn 的初始项目,然后按照下方的目录层级创建 Roslyn Package 项目 ```bash . ├── Assets │ └── RoslynAnalyzers └── Tools └── Microsoft.Unity.Analyzers ``` 当对 Roslyn 代码修改后,直接运行下方代码,更新到 Unity Assets 文件夹,然后 npm 发布到 self host UPM 即可 ```bash #!/bin/bash dotnet build ./Tools/Microsoft.Unity.Analyzers/Microsoft.Unity.Analyzers.csproj -c Release -o ./Assets/RoslynAnalyzers ``` ## Roslyn 单元测试 这里以 **Microsoft.Unity.Analyzers.sln** 为例,其中微软提供了 Microsoft.Unity.Analyzers.Tests 示例项目,这里微软的实现非常有趣,为了完成 Roslyn 的单元测试,实际在运行时,会创建一个临时的 sln,然后运行 Roslyn 检测,我们以官方的一个单元测试为例[SetPixelsTests.cs](https://github.com/microsoft/Microsoft.Unity.Analyzers/blob/305d1028bba52367392780fef11dce0a1f167abf/src/Microsoft.Unity.Analyzers.Tests/SetPixelsTests.cs#L35C1-L55C3) ```csharp [Fact] public async Task Texture3DTest() { const string test = @" using UnityEngine; class Camera : MonoBehaviour { private void Test(Texture3D test) { test.SetPixels(null); } } "; var diagnostic = ExpectDiagnostic() .WithLocation(8, 14) .WithArguments("SetPixels"); await VerifyCSharpDiagnosticAsync(test, diagnostic); } ``` 通过直接写字符串的方式,创建一个单元测试,比如这里的 SetPixels 检测,我们已经知道在第 8 行的第 14 个字符存在问题,因此需要追加 **WithLocation(8, 14)**,同时此处在 Roslyn 的规则中,动态拼接了 **SetPixels** 作为参数,因此追加了 **WithArguments("SetPixels")** 最后 **await VerifyCSharpDiagnosticAsync(test, diagnostic)** 会创建一个临时项目,并检查 **test** 中的语法错误,最后和 **ExpectDiagnostic** 的结果进行对比 ![](https://blog.liuocean.synology.me:9001/blog/old/16973489288551.jpg) ### dll 引入 既然单元测试的原理是创建了一个临时的 sln,那么为了完成诸如上面的 **using UnityEngine;** 等内容,势必要完成对 **UnityEngine.dll** 等相关文件的引入,实际的代码写在 [DiagnosticVerifier.cs](https://github.com/microsoft/Microsoft.Unity.Analyzers/blob/305d1028bba52367392780fef11dce0a1f167abf/src/Microsoft.Unity.Analyzers.Tests/Infrastructure/DiagnosticVerifier.cs#L327C1-L344C3) 文件中 这样如果需要引入一些第三方 Unity 项目所需的 dll,只需要追加对应的 **yield return xxx.dll** 即可 ### ET Roslyn 的适配 在 ET 的规则中,大部分规则都是视为 **Error**,但是在微软的这个单元测试项目中,如果 Roslyn 匹配到的结果是一个 **Error** 会触发断言,导致单元测试失败,因此解决方案就有两种 - 调整 ET Roslyn 的规则为 Warning - 删除单元测试的 Error 断言 这里我说一下第二个方案的具体内容,首先在 [DiagnosticVerifier.cs](https://github.com/microsoft/Microsoft.Unity.Analyzers/blob/305d1028bba52367392780fef11dce0a1f167abf/src/Microsoft.Unity.Analyzers.Tests/Infrastructure/DiagnosticVerifier.cs#L250C1-L251C102) 中关闭这两行断言,直接注释就行 ```csharp foreach (var error in errors) Assert.Fail($"Line {error.Location.GetLineSpan().StartLinePosition.Line}: {error.GetMessage()}"); ``` 接着注释掉 [DiagnosticVerifier.cs](https://github.com/microsoft/Microsoft.Unity.Analyzers/blob/305d1028bba52367392780fef11dce0a1f167abf/src/Microsoft.Unity.Analyzers.Tests/Infrastructure/DiagnosticVerifier.cs#L257C1-L257C20) 文件中 **Expect(erros)** 即可 ```csharp var diags = allDiagnostics // .Except(errors) .Where(d => d.Location.IsInSource); //only keep diagnostics related to a source location ``` 由于 [ExpectDiagnostic](https://github.com/microsoft/Microsoft.Unity.Analyzers/blob/305d1028bba52367392780fef11dce0a1f167abf/src/Microsoft.Unity.Analyzers.Tests/Infrastructure/DiagnosticVerifier.cs#L54C1-L67C3) 函数中会强制校验是否只有一个 **DiagnosticResult**,因此部分 ET 的 Roslyn 规则需要自行拆分,否则会导致单元测试无法通过 > 这里的 xxx.Single() 函数 ```csharp protected DiagnosticResult ExpectDiagnostic() { var analyzer = GetCSharpDiagnosticAnalyzer(); try { return ExpectDiagnostic(analyzer.SupportedDiagnostics.Single()); } catch (InvalidOperationException ex) { throw new InvalidOperationException( $"'{nameof(Diagnostic)}()' can only be used when the analyzer has a single supported diagnostic. Use the '{nameof(Diagnostic)}(DiagnosticDescriptor)' overload to specify the descriptor from which to create the expected result.", ex); } } ``` ### 完整示例 这里我以 **ChildOf** 规则为例 ```csharp public class FriendOfTest : BaseDiagnosticVerifierTest { [Fact] public async Task AccessFromClass() { const string test=@" using ET; public class Unit : Entity { public int value; } public static class Test { public static void Run(this Unit self) { self.value = 1; } } " var diagnostic = ExpectDiagnostic() .WithLocation(13, 9) .WithArguments("Unit", "value"); await VerifyCSharpDiagnosticAsync(test, diagnostic); } } ``` ## 最后 虽然所有流程都跑通了,但是由于 Unity 自身蹩脚的设计,导致原本 C# 支持的功能变的非常繁琐,或许这些在未来 dotnet 版本的 Unity 后会得到非常大的改善 Roslyn 的单元测试相关的代码非常有趣,能看出来微软在 C# 生态下了很多功夫,有了这套工具链,测试修改 Roslyn 会非常非常方便~