Slide 1

Slide 1 text

TDDで作るRoslynアナライザー 品質本部品質管理部SWET第二グループ Kazuma Inagaki 1

Slide 2

Slide 2 text

Kazuma Inagaki 品質本部品質管理部SWET第二グループ @get_me_power
 令和にVimで就活した2021年新卒
 
 ゲーム領域における静的解析器の作成
 
 
 @get-me-power
 ※ TWITTER, TWEET, RETWEET and the Twitter Bird logo are trademarks of Twitter Inc. or its affiliates. 2

Slide 3

Slide 3 text

この発表で伝えたいこと 3

Slide 4

Slide 4 text

Dena.CodeAnalysis.Testingで Roslynアナライザーの ユニットテストを簡単にしよう 4

Slide 5

Slide 5 text

Dena.CodeAnalysis.Testingで Roslynアナライザーの ユニットテストを簡単にしよう 5

Slide 6

Slide 6 text

Roslynとは 公開されているもの ■ C#、VB向けコンパイラ ■ コード生成API ■ コード解析API C#6.0から導入された.NETコンパイラプラットフォームの通称 6

Slide 7

Slide 7 text

Roslynとは 公開されているもの ■ C#、VB向けコンパイラ ■ コード生成API ■ コード解析API C#6.0から導入された.NETコンパイラプラットフォームの通称 7

Slide 8

Slide 8 text

Roslynとは 公開されているもの ■ C#、VB向けコンパイラ ■ コード生成API ■ コード解析API コードの問題を検出するプログラム Roslynアナライザー C#6.0から導入された.NETコンパイラプラットフォームの通称 8

Slide 9

Slide 9 text

https://speakerdeck.com/dena_tech/unitykai-fa-defalsemisuwowei-ran-nifang-guroslynanaraizafalsesu-me-dena-techcon-2022 9

Slide 10

Slide 10 text

Dena.CodeAnalysis.Testingで Roslynアナライザーの ユニットテストを簡単にしよう 10

Slide 11

Slide 11 text

ユニットテストの必要性 11

Slide 12

Slide 12 text

ユニットテストの必要性 早期に欠陥を検知できる デグレーションを検知できる 12

Slide 13

Slide 13 text

Roslynアナライザーを作る上でも ユニットテストは有効 13

Slide 14

Slide 14 text

Roslynアナライザーのユニットテストに必要なもの ● 検査対象ソースコード ● アナライザーが出力する警告の期待結果 ● アナライザーが実際に出力した値 14

Slide 15

Slide 15 text

Roslynアナライザーのユニットテストに必要なもの ● 検査対象ソースコード ● アナライザーが出力する警告の期待結果 ● アナライザーが実際に出力した値 15

Slide 16

Slide 16 text

Roslynアナライザーのユニットテストは難しい 16

Slide 17

Slide 17 text

Roslynアナライザーのユニットテストは難しい 既存のテスティングライブラリにデメリットがある 17

Slide 18

Slide 18 text

既存のテスティングライブラリにデメリットがある Roslynアナライザーのユニットテストは難しい 18

Slide 19

Slide 19 text

既存のテスティングライブラリのデメリット ● アナライザーの実行とアサーションが一体化している ● 引数が必要なアナライザーをインスタンス化するのが難しい ● 検査対象のソースコードのうちどこが問題なのか分かりにくい ● CodeFixProviderを指定する必要があった ● 19

Slide 20

Slide 20 text

解決法 20

Slide 21

Slide 21 text

解決法 Dena.CodeAnalysis.Testingを使おう! 21

Slide 22

Slide 22 text

Dena.CodeAnalysis.Testingで Roslynアナライザーのユニットテストを簡単 にしよう 22

Slide 23

Slide 23 text

Dena.CodeAnalysis.Testingができること ● 23

Slide 24

Slide 24 text

Dena.CodeAnalysis.Testingができること ● アナライザーの実行とアサーションを分離させる ● 引数が必要なアナライザーを指定する ● 検査対象ソースコードから得られる情報を増やす 24

Slide 25

Slide 25 text

Dena.CodeAnalysis.Testingができること ● アナライザーの実行とアサーションを分離させる ● 引数が必要なアナライザーを指定する ● 検査対象ソースコードから得られる情報を増やす 25

Slide 26

Slide 26 text

using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task EmptySourceCode_NoDiagnosticReport() { var source = ""; await Verify.VerifyAnalyzerAsync(source); } 標準ライブラリを使った テストコード 26

Slide 27

Slide 27 text

using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task EmptySourceCode_NoDiagnosticReport() { var source = ""; await Verify.VerifyAnalyzerAsync(source); } アナライザー実行と アサーションが一体化している 27

Slide 28

Slide 28 text

using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task EmptySourceCode_NoDiagnosticReport() { var source = ""; await Verify.VerifyAnalyzerAsync(source); } 独自の比較ができない アナライザー実行とアサーションの間 に処理を挟めない アナライザー実行と アサーションが一体化している 28

Slide 29

Slide 29 text

using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task EmptySourceCode_NoDiagnosticReport() { var source = ""; await Verify.VerifyAnalyzerAsync(source); } public async Task EmptySourceCode_NoDiagnosticReport() { var source = ""; var analyzer = new YourAnalyzer(); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); Assert.That(diagnostics.Length, Is.EqualTo(0)); } Dena.CodeAnalysis.Testing使用 29

Slide 30

Slide 30 text

using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task EmptySourceCode_NoDiagnosticReport() { var source = ""; await Verify.VerifyAnalyzerAsync(source); } public async Task EmptySourceCode_NoDiagnosticReport() { var source = ""; var analyzer = new YourAnalyzer(); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); Assert.That(diagnostics.Length, Is.EqualTo(0)); } Dena.CodeAnalysis.Testing使用 アナライザー実行とアサーションを 分離できる! 30

Slide 31

Slide 31 text

using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task EmptySourceCode_NoDiagnosticReport() { var source = ""; await Verify.VerifyAnalyzerAsync(source); } public async Task EmptySourceCode_NoDiagnosticReport() { var source = ""; var analyzer = new YourAnalyzer(); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); Assert.That(diagnostics.Length, Is.EqualTo(0)); } Dena.CodeAnalysis.Testing使用 アナライザー実行とアサーションを 分離できる! 31

Slide 32

Slide 32 text

Dena.CodeAnalysis.Testingができること ● アナライザーの実行とアサーションを分離させる ● 引数が必要なアナライザーを指定する ● 検査対象ソースコードから得られる情報を増やす 32

Slide 33

Slide 33 text

using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task Foo() { var source = ""; await Verify.VerifyAnalyzerAsync(source); } 標準ライブラリを使った テストコード 33

Slide 34

Slide 34 text

using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task Foo() { var source = ""; await Verify.VerifyAnalyzerAsync(source); } アナライザーを指定 34

Slide 35

Slide 35 text

using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task Foo() { var source = ""; await Verify.VerifyAnalyzerAsync(source); } using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task Foo() { var source = ""; await Verify.VerifyAnalyzerAsync(source); } アナライザーを指定 35

Slide 36

Slide 36 text

using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task Foo() { var source = ""; await Verify.VerifyAnalyzerAsync(source); } using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task Foo() { var source = ""; await Verify.VerifyAnalyzerAsync(source); } アナライザーを指定 コンパイルエラー 36

Slide 37

Slide 37 text

using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task Foo() { var source = ""; await Verify.VerifyAnalyzerAsync(source); } using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task Foo() { var source = ""; await Verify.VerifyAnalyzerAsync(source); } 引数が必要なアナライザーに対応できない! コンパイルエラー 37

Slide 38

Slide 38 text

using Dena.CodeAnalysis.CSharp.Testing; [Test] public async Task EmptySourceCode_NoDiagnosticReport() { const string source = ""; var analyzer = new YourAnalyzer(new Component()); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); Assert.That(diagnostics.Length, Is.EqualTo(0)); } using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task Foo() { var source = ""; await Verify.VerifyAnalyzerAsync(source); } コンパイルエラー 38

Slide 39

Slide 39 text

using Dena.CodeAnalysis.CSharp.Testing; [Test] public async Task EmptySourceCode_NoDiagnosticReport() { const string source = ""; var analyzer = new YourAnalyzer(new Component()); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); Assert.That(diagnostics.Length, Is.EqualTo(0)); } using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task Foo() { var source = ""; await Verify.VerifyAnalyzerAsync(source); } コンパイルエラー テストコード内で インスタンス化 39

Slide 40

Slide 40 text

using Dena.CodeAnalysis.CSharp.Testing; [Test] public async Task EmptySourceCode_NoDiagnosticReport() { const string source = ""; var analyzer = new YourAnalyzer(new Component()); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); Assert.That(diagnostics.Length, Is.EqualTo(0)); } using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task Foo() { var source = ""; await Verify.VerifyAnalyzerAsync(source); } コンパイルエラー テストコード内で インスタンス化 40

Slide 41

Slide 41 text

Dena.CodeAnalysis.Testingができること ● アナライザーの実行とアサーションを分離させる ● 引数が必要なアナライザーを指定する ● 検査対象ソースコードから得られる情報を増やす 41

Slide 42

Slide 42 text

標準ライブラリを使った テストコード using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task TypeNameContainingLowercase_ReportOneDiagnostic() { var source = @" class TypeName {} "; await Verify.VerifyAnalyzerAsync(source); } 42

Slide 43

Slide 43 text

using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task TypeNameContainingLowercase_ReportOneDiagnostic() { var source = @" class TypeName {} "; await Verify.VerifyAnalyzerAsync(source); } 検査対象ソースコード 43

Slide 44

Slide 44 text

using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task TypeNameContainingLowercase_ReportOneDiagnostic() { var source = @" class TypeName {} "; await Verify.VerifyAnalyzerAsync(source); } どこがエラー箇所? 44

Slide 45

Slide 45 text

using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task TypeNameContainingLowercase_ReportOneDiagnostic() { var source = @" class TypeName {} "; await Verify.VerifyAnalyzerAsync(source); } public async Task TypeNameContainingLowercase_ReportOneDiagnostic() { var analyzer = new RoslynAnalyzerTemplate(); var (source, expected) = TestDataParser.CreateSourceAndExpectedDiagnostic(@" namespace TypeNameContainingLowercase { class {|TypeName|RoslynAnalyzerTemplate0001|Type name 'TypeName' contains lowercase letters|} { } }"); var actual = await DiagnosticAnalyzerRunner.Run(analyzer, source); Assert.That(actual.Length, Is.EqualTo(expected.Count)); Assert.That(actual.First().Id, Is.EqualTo(expected.First().Id)) Assert.That(actual.First().GetMessage(), Is.EqualTo(expected.First().GetMessage())); Dena.CodeAnalysis.Testing を使ったテストコード 45

Slide 46

Slide 46 text

public async Task TypeNameContainingLowercase_ReportOneDiagnostic() { var analyzer = new RoslynAnalyzerTemplate(); var (source, expected) = TestDataParser.CreateSourceAndExpectedDiagnostic(@" namespace TypeNameContainingLowercase { class {|TypeName|RoslynAnalyzerTemplate0001|Type name 'TypeName' contains lowercase letters|} { } }"); var actual = await DiagnosticAnalyzerRunner.Run(analyzer, source); Assert.That(actual.Length, Is.EqualTo(expected.Count)); Assert.That(actual.First().Id, Is.EqualTo(expected.First().Id)) Assert.That(actual.First().GetMessage(), Is.EqualTo(expected.First().GetMessage())); using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task TypeNameContainingLowercase_ReportOneDiagnostic() { var source = @" class TypeName {} "; await Verify.VerifyAnalyzerAsync(source); } 46

Slide 47

Slide 47 text

public async Task TypeNameContainingLowercase_ReportOneDiagnostic() { var analyzer = new RoslynAnalyzerTemplate(); var (source, expected) = TestDataParser.CreateSourceAndExpectedDiagnostic(@" namespace TypeNameContainingLowercase { class {|TypeName|RoslynAnalyzerTemplate0001|Type name 'TypeName' contains lowercase letters|} { } }"); var actual = await DiagnosticAnalyzerRunner.Run(analyzer, source); Assert.That(actual.Length, Is.EqualTo(expected.Count)); Assert.That(actual.First().Id, Is.EqualTo(expected.First().Id)) Assert.That(actual.First().GetMessage(), Is.EqualTo(expected.First().GetMessage())); using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task TypeNameContainingLowercase_ReportOneDiagnostic() { var source = @" class TypeName {} "; await Verify.VerifyAnalyzerAsync(source); } 47 エラー箇所

Slide 48

Slide 48 text

public async Task TypeNameContainingLowercase_ReportOneDiagnostic() { var analyzer = new RoslynAnalyzerTemplate(); var (source, expected) = TestDataParser.CreateSourceAndExpectedDiagnostic(@" namespace TypeNameContainingLowercase { class {|TypeName|RoslynAnalyzerTemplate0001|Type name 'TypeName' contains lowercase letters|} { } }"); var actual = await DiagnosticAnalyzerRunner.Run(analyzer, source); Assert.That(actual.Length, Is.EqualTo(expected.Count)); Assert.That(actual.First().Id, Is.EqualTo(expected.First().Id)) Assert.That(actual.First().GetMessage(), Is.EqualTo(expected.First().GetMessage())); using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task TypeNameContainingLowercase_ReportOneDiagnostic() { var source = @" class TypeName {} "; await Verify.VerifyAnalyzerAsync(source); } 48 DDID エラー箇所

Slide 49

Slide 49 text

public async Task TypeNameContainingLowercase_ReportOneDiagnostic() { var analyzer = new RoslynAnalyzerTemplate(); var (source, expected) = TestDataParser.CreateSourceAndExpectedDiagnostic(@" namespace TypeNameContainingLowercase { class {|TypeName|RoslynAnalyzerTemplate0001|Type name 'TypeName' contains lowercase letters|} { } }"); var actual = await DiagnosticAnalyzerRunner.Run(analyzer, source); Assert.That(actual.Length, Is.EqualTo(expected.Count)); Assert.That(actual.First().Id, Is.EqualTo(expected.First().Id)) Assert.That(actual.First().GetMessage(), Is.EqualTo(expected.First().GetMessage())); エラーメッセージ DDID エラー箇所 using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task TypeNameContainingLowercase_ReportOneDiagnostic() { var source = @" class TypeName {} "; await Verify.VerifyAnalyzerAsync(source); } 49

Slide 50

Slide 50 text

public async Task TypeNameContainingLowercase_ReportOneDiagnostic() { var analyzer = new RoslynAnalyzerTemplate(); var (source, expected) = TestDataParser.CreateSourceAndExpectedDiagnostic(@" namespace TypeNameContainingLowercase { class {|TypeName|RoslynAnalyzerTemplate0001|Type name 'TypeName' contains lowercase letters|} { } }"); var actual = await DiagnosticAnalyzerRunner.Run(analyzer, source); Assert.That(actual.Length, Is.EqualTo(expected.Count)); Assert.That(actual.First().Id, Is.EqualTo(expected.First().Id)) Assert.That(actual.First().GetMessage(), Is.EqualTo(expected.First().GetMessage())); 書式を基に期待結果 を作成 作成された期待結果を利用 using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task TypeNameContainingLowercase_ReportOneDiagnostic() { var source = @" class TypeName {} "; await Verify.VerifyAnalyzerAsync(source); }

Slide 51

Slide 51 text

public async Task TypeNameContainingLowercase_ReportOneDiagnostic() { var analyzer = new RoslynAnalyzerTemplate(); var (source, expected) = TestDataParser.CreateSourceAndExpectedDiagnostic(@" namespace TypeNameContainingLowercase { class {|TypeName|RoslynAnalyzerTemplate0001|Type name 'TypeName' contains lowercase letters|} { } }"); var actual = await DiagnosticAnalyzerRunner.Run(analyzer, source); Assert.That(actual.Length, Is.EqualTo(expected.Count)); Assert.That(actual.First().Id, Is.EqualTo(expected.First().Id)) Assert.That(actual.First().GetMessage(), Is.EqualTo(expected.First().GetMessage())); 書式を基に期待結果 を作成 using Verify = Microsoft.CodeAnalysis.CSharp.Testing.NUnit.AnalyzerVerifier ; [Test] public async Task TypeNameContainingLowercase_ReportOneDiagnostic() { var source = @" class TypeName {} "; await Verify.VerifyAnalyzerAsync(source); } 作成された期待結果を利用

Slide 52

Slide 52 text

Let’s Try! 実際にDena.CodeAnalysis.Testing を使ってテストを書いてみよう 52

Slide 53

Slide 53 text

アナライザーを作成するための準備 1. テンプレートリポジトリを使ってアナライザーを作成 a. https://github.com/DeNA/RoslynAnalyzerTemplate 2. テンプレートに含まれるテストメソッドを削除する 3. アナライザーのテンプレートを削除する 53

Slide 54

Slide 54 text

アナライザーを作成するための準備 1. テンプレートリポジトリを使ってアナライザーを作成 a. https://github.com/DeNA/RoslynAnalyzerTemplate 2. テンプレートに含まれるテストメソッドを削除する 3. アナライザーのテンプレートを削除する 54 デモリポジトリurl: https://github.com/DeNA/BanAsyncTaskAnalyzer 該当コミットハッシュ: https://github.com/DeNA/BanAsyncTaskAnalyzer/commi t/0b2af3934a3a3759fbe25e73e527d4777b8a9d3b

Slide 55

Slide 55 text

リファクタリング テストを追加する 実装する テストを追加する TDDのモデル ✔Passed ❌Failed ✔Passed 55

Slide 56

Slide 56 text

public class C { public async Task Foo() { } } 作成するアナライザー 56

Slide 57

Slide 57 text

public class C { public async Task Foo() { } } 怒る ● async Task → async UniTask ● async void → async UniTaskVoid 作成するアナライザー 57

Slide 58

Slide 58 text

public class C { public async Task Foo() { } } 怒る ● async Task → async UniTask ● async void → async UniTaskVoid https://github.com/Cysharp/UniTask Copyright (c) 2019 Yoshifumi Kawai / Cysharp, Inc. 作成するアナライザー 58

Slide 59

Slide 59 text

public class C { public async Task Foo() { } } 動作 BanAsyncTask0001 BanAsyncTask0002 何もレポートされない 条件 async使用関数 System.Threading.Tasks.Task Y N N System.Void N Y N Cysharp.Threading.Tasks.UniTask N N Y NotSystem.Threading.Tasks.Task N N Y async未使用関数 System.Void N N Y 怒る ● async Task → async UniTask ● async void → async UniTaskVoid 作成するアナライザー 59

Slide 60

Slide 60 text

動作 BanAsyncTask0001 BanAsyncTask0002 何もレポートされない 条件 async使用関数 System.Threading.Tasks.Task Y N N System.Void N Y N Cysharp.Threading.Tasks.UniTask N N Y NotSystem.Threading.Tasks.Task N N Y async未使用関数 System.Void N N Y 作成するアナライザー 60 このケースを追加

Slide 61

Slide 61 text

[Test] public async Task 非asyncメソッド_戻り値がvoid_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("NoAsyncMethodCase.txt"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .ToArray(); DiagnosticsAssert.IsEmpty(actual); } 61

Slide 62

Slide 62 text

[Test] public async Task 非asyncメソッド_戻り値がvoid_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("NoAsyncMethodCase.txt"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .ToArray(); DiagnosticsAssert.IsEmpty(actual); } class NoAsyncMethodCase { void NoAsyncMethod() { } } 検査対象ソースコード 62

Slide 63

Slide 63 text

[Test] public async Task 非asyncメソッド_戻り値がvoid_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("NoAsyncMethodCase.txt"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .ToArray(); DiagnosticsAssert.IsEmpty(actual); } private static string[] ReadCodes(params string[] sources) { const string Path = "../../../TestData"; return sources.Select(file => File.ReadAllText($"{Path}/{file}", Encoding.UTF8)).ToArray(); } 63

Slide 64

Slide 64 text

[Test] public async Task 非asyncメソッド_戻り値がvoid_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("NoAsyncMethodCase.txt"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .ToArray(); DiagnosticsAssert.IsEmpty(actual); } 検査対象ソースコードを渡し、アナライザーを実 行 64

Slide 65

Slide 65 text

[Test] public async Task 非asyncメソッド_戻り値がvoid_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("NoAsyncMethodCase.txt"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .ToArray(); DiagnosticsAssert.IsEmpty(actual); } 65 実際の値を加工している

Slide 66

Slide 66 text

[Test] public async Task 非asyncメソッド_戻り値がvoid_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadFromFile.ReadFile(TestData.GetPath("NoAsyncMethodCase.txt")); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .ToArray(); DiagnosticsAssert.IsEmpty(actual); } 実際の値を加工している ✔Passed 66

Slide 67

Slide 67 text

[Test] public async Task 非asyncメソッド_戻り値がvoid_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadFromFile.ReadFile(TestData.GetPath("NoAsyncMethodCase.txt")); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .ToArray(); DiagnosticsAssert.IsEmpty(actual); } 実際の値を加工している Refactor Add Test 67

Slide 68

Slide 68 text

[Test] public async Task 非asyncメソッド_戻り値がvoid_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadFromFile.ReadFile(TestData.GetPath("NoAsyncMethodCase.txt")); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .ToArray(); DiagnosticsAssert.IsEmpty(actual); } 実際の値を加工している Refactor Add Test 68

Slide 69

Slide 69 text

動作 BanAsyncTask0001 BanAsyncTask0002 何もレポートされない 条件 async使用関数 System.Threading.Tasks.Task Y N N System.Void N Y N Cysharp.Threading.Tasks.UniTask N N Y NotSystem.Threading.Tasks.Task N N Y async未使用関数 System.Void N N Y 69

Slide 70

Slide 70 text

動作 BanAsyncTask0001 BanAsyncTask0002 何もレポートされない 条件 async使用関数 System.Threading.Tasks.Task Y N N System.Void N Y N Cysharp.Threading.Tasks.UniTask N N Y NotSystem.Threading.Tasks.Task N N Y async未使用関数 System.Void N N Y 70 このケースを追加

Slide 71

Slide 71 text

[Test] public async Task asyncメソッド_戻り値がTask_BanAsyncTask0001がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("NoAsyncMethodCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") .Where(x => x.Id != "CS8019") .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } テストメソッド 71

Slide 72

Slide 72 text

[Test] public async Task asyncメソッド_戻り値がTask_BanAsyncTask0001がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("NoAsyncMethodCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") .Where(x => x.Id != "CS8019") .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } using System.Threading; using System.Threading.Tasks; namespace UseTask { class C { async Task {|UseTask|BanAsyncTask0001|Do not use 'System.Threading.Tasks.Task', Should use UniTask|}() { await Task.Delay(100); } } } 検査対象ソースコード 72

Slide 73

Slide 73 text

[Test] public async Task asyncメソッド_戻り値がTask_BanAsyncTask0001がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("NoAsyncMethodCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") .Where(x => x.Id != "CS8019") .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } using System.Threading; using System.Threading.Tasks; namespace UseTask { class C { async Task {|UseTask|BanAsyncTask0001|Do not use 'System.Threading.Tasks.Task', Should use UniTask|}() { await Task.Delay(100); } } } 73 レポート箇所

Slide 74

Slide 74 text

[Test] public async Task asyncメソッド_戻り値がTask_BanAsyncTask0001がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("NoAsyncMethodCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") .Where(x => x.Id != "CS8019") .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } using System.Threading; using System.Threading.Tasks; namespace UseTask { class C { async Task {|UseTask|BanAsyncTask0001|Do not use 'System.Threading.Tasks.Task', Should use UniTask|}() { await Task.Delay(100); } } } DDID 74

Slide 75

Slide 75 text

[Test] public async Task asyncメソッド_戻り値がTask_BanAsyncTask0001がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("NoAsyncMethodCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") .Where(x => x.Id != "CS8019") .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } using System.Threading; using System.Threading.Tasks; namespace UseTask { class C { async Task {|UseTask|BanAsyncTask0001|Do not use 'System.Threading.Tasks.Task', Should use UniTask|}() { await Task.Delay(100); } } } 75 エラーメッセージ

Slide 76

Slide 76 text

[Test] public async Task asyncメソッド_戻り値がTask_BanAsyncTask0001がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("NoAsyncMethodCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") .Where(x => x.Id != "CS8019") .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } ソースコードと期 待結果を作成 76 using System.Threading; using System.Threading.Tasks; namespace UseTask { class C { async Task {|UseTask|BanAsyncTask0001|Do not use 'System.Threading.Tasks.Task', Should use UniTask|}() { await Task.Delay(100); } } }

Slide 77

Slide 77 text

[Test] public async Task asyncメソッド_戻り値がTask_BanAsyncTask0001がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("NoAsyncMethodCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") .Where(x => x.Id != "CS8019") .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } ● Diagnosticのcollectionを順不同で比較 ● 以下の観点がすべて一致するDIagnosticを等価と判断する ○ レポートされた問題のファイル内の位置(ファイルパス含む) ○ アナライザーのID ○ メッセージ 77

Slide 78

Slide 78 text

[Test] public async Task asyncメソッド_戻り値がTask_BanAsyncTask0001がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("NoAsyncMethodCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") .Where(x => x.Id != "CS8019") .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } ● Diagnosticのcollectionを順不同で比較 ● 以下の観点がすべて一致するDIagnosticを等価と判断する ○ レポートされた問題のファイル内の位置(ファイルパス含む) ○ アナライザーのID ○ メッセージ 78 ❌Failed

Slide 79

Slide 79 text

[Test] public async Task asyncメソッド_戻り値がTask_BanAsyncTask0001がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("NoAsyncMethodCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") .Where(x => x.Id != "CS8019") .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } Missing 1 diagnostics, extra 0 diagnostics of all 0 diagnostics: missing /0/Test0.cs: (7,18)-(7,25), BanAsyncTask0001, Do not use 'System.Threading.Tasks.Task', Should use UniTask 79

Slide 80

Slide 80 text

[Test] public async Task asyncメソッド_戻り値がTask_BanAsyncTask0001がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("NoAsyncMethodCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") .Where(x => x.Id != "CS8019") .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } Missing 1 diagnostics, extra 0 diagnostics of all 0 diagnostics: missing /0/Test0.cs: (7,18)-(7,25), BanAsyncTask0001, Do not use 'System.Threading.Tasks.Task', Should use UniTask 80 レポート箇所

Slide 81

Slide 81 text

[Test] public async Task asyncメソッド_戻り値がTask_BanAsyncTask0001がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("NoAsyncMethodCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") .Where(x => x.Id != "CS8019") .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } Missing 1 diagnostics, extra 0 diagnostics of all 0 diagnostics: missing /0/Test0.cs: (7,18)-(7,25), BanAsyncTask0001, Do not use 'System.Threading.Tasks.Task', Should use UniTask 81 DDID

Slide 82

Slide 82 text

[Test] public async Task asyncメソッド_戻り値がTask_BanAsyncTask0001がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("NoAsyncMethodCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") .Where(x => x.Id != "CS8019") .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } Missing 1 diagnostics, extra 0 diagnostics of all 0 diagnostics: missing /0/Test0.cs: (7,18)-(7,25), BanAsyncTask0001, Do not use 'System.Threading.Tasks.Task', Should use UniTask 82 エラーメッセージ

Slide 83

Slide 83 text

[Test] public async Task asyncメソッド_戻り値がTask_BanAsyncTask0001がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("NoAsyncMethodCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") .Where(x => x.Id != "CS8019") .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } Missing 1 diagnostics, extra 0 diagnostics of all 0 diagnostics: missing /0/Test0.cs: (7,18)-(7,25), BanAsyncTask0001, Do not use 'System.Threading.Tasks.Task', Should use UniTask 83 エラーメッセージ

Slide 84

Slide 84 text

アナライザー本体を実装 84

Slide 85

Slide 85 text

using System.Collections.Immutable; using BanAsyncTaskAnalyzer.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; namespace BanAsyncTaskAnalyzer; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class BanAsyncTaskAnalyzer : DiagnosticAnalyzer { private static readonly DiagnosticDescriptor Rule01 = new DiagnosticDescriptor( id: "BanAsyncTask0001", title: "Banned Task Rule", messageFormat: "Do not use '{0}', Should use UniTask", category: "Naming", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Type names should be all uppercase."); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule01); public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterSymbolAction(AnalyzeAsyncMethod, SymbolKind.Method); } private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); } } } アナライザー本体のソースコード 85

Slide 86

Slide 86 text

using System.Collections.Immutable; using BanAsyncTaskAnalyzer.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; namespace BanAsyncTaskAnalyzer; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class BanAsyncTaskAnalyzer : DiagnosticAnalyzer { private static readonly DiagnosticDescriptor Rule01 = new DiagnosticDescriptor( id: "BanAsyncTask0001", title: "Banned Task Rule", messageFormat: "Do not use '{0}', Should use UniTask", category: "Naming", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Type names should be all uppercase."); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule01); public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterSymbolAction(AnalyzeAsyncMethod, SymbolKind.Method); } private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); } } } アナライザーのDiagnosticを定義 86

Slide 87

Slide 87 text

using System.Collections.Immutable; using BanAsyncTaskAnalyzer.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; namespace BanAsyncTaskAnalyzer; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class BanAsyncTaskAnalyzer : DiagnosticAnalyzer { private static readonly DiagnosticDescriptor Rule01 = new DiagnosticDescriptor( id: "BanAsyncTask0001", title: "Banned Task Rule", messageFormat: "Do not use '{0}', Should use UniTask", category: "Naming", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Type names should be all uppercase."); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule01); public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterSymbolAction(AnalyzeAsyncMethod, SymbolKind.Method); } private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); } } } アナライザー本体の実装 87

Slide 88

Slide 88 text

using System.Collections.Immutable; using BanAsyncTaskAnalyzer.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; namespace BanAsyncTaskAnalyzer; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class BanAsyncTaskAnalyzer : DiagnosticAnalyzer { private static readonly DiagnosticDescriptor Rule01 = new DiagnosticDescriptor( id: "BanAsyncTask0001", title: "Banned Task Rule", messageFormat: "Do not use '{0}', Should use UniTask", category: "Naming", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Type names should be all uppercase."); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule01); public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterSymbolAction(AnalyzeAsyncMethod, SymbolKind.Method); } private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); } } } 88 定義されたメソッドに関する情報を取得

Slide 89

Slide 89 text

using System.Collections.Immutable; using BanAsyncTaskAnalyzer.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; namespace BanAsyncTaskAnalyzer; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class BanAsyncTaskAnalyzer : DiagnosticAnalyzer { private static readonly DiagnosticDescriptor Rule01 = new DiagnosticDescriptor( id: "BanAsyncTask0001", title: "Banned Task Rule", messageFormat: "Do not use '{0}', Should use UniTask", category: "Naming", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Type names should be all uppercase."); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule01); public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterSymbolAction(AnalyzeAsyncMethod, SymbolKind.Method); } private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); } } } 解析対象がAsyncメソッド以外ならreturn 89

Slide 90

Slide 90 text

using System.Collections.Immutable; using BanAsyncTaskAnalyzer.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; namespace BanAsyncTaskAnalyzer; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class BanAsyncTaskAnalyzer : DiagnosticAnalyzer { private static readonly DiagnosticDescriptor Rule01 = new DiagnosticDescriptor( id: "BanAsyncTask0001", title: "Banned Task Rule", messageFormat: "Do not use '{0}', Should use UniTask", category: "Naming", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Type names should be all uppercase."); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule01); public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterSymbolAction(AnalyzeAsyncMethod, SymbolKind.Method); } private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); } } } 定義されたメソッドの戻り値の型名が ”Task”だったらレポートする 90

Slide 91

Slide 91 text

using System.Collections.Immutable; using BanAsyncTaskAnalyzer.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; namespace BanAsyncTaskAnalyzer; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class BanAsyncTaskAnalyzer : DiagnosticAnalyzer { private static readonly DiagnosticDescriptor Rule01 = new DiagnosticDescriptor( id: "BanAsyncTask0001", title: "Banned Task Rule", messageFormat: "Do not use '{0}', Should use UniTask", category: "Naming", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Type names should be all uppercase."); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule01); public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterSymbolAction(AnalyzeAsyncMethod, SymbolKind.Method); } private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); } } } 91 ✔Passed

Slide 92

Slide 92 text

using System.Collections.Immutable; using BanAsyncTaskAnalyzer.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; namespace BanAsyncTaskAnalyzer; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class BanAsyncTaskAnalyzer : DiagnosticAnalyzer { private static readonly DiagnosticDescriptor Rule01 = new DiagnosticDescriptor( id: "BanAsyncTask0001", title: "Banned Task Rule", messageFormat: "Do not use '{0}', Should use UniTask", category: "Naming", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Type names should be all uppercase."); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule01); public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterSymbolAction(AnalyzeAsyncMethod, SymbolKind.Method); } private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); } } } 92 Refactor Add Test

Slide 93

Slide 93 text

using System.Collections.Immutable; using BanAsyncTaskAnalyzer.Extensions; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Diagnostics; namespace BanAsyncTaskAnalyzer; [DiagnosticAnalyzer(LanguageNames.CSharp)] public class BanAsyncTaskAnalyzer : DiagnosticAnalyzer { private static readonly DiagnosticDescriptor Rule01 = new DiagnosticDescriptor( id: "BanAsyncTask0001", title: "Banned Task Rule", messageFormat: "Do not use '{0}', Should use UniTask", category: "Naming", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Type names should be all uppercase."); public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule01); public override void Initialize(AnalysisContext context) { context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); context.EnableConcurrentExecution(); context.RegisterSymbolAction(AnalyzeAsyncMethod, SymbolKind.Method); } private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); } } } 93 Refactor Add Test

Slide 94

Slide 94 text

動作 BanAsyncTask0001 BanAsyncTask0002 何もレポートされない 条件 async使用関数 System.Threading.Tasks.Task Y N N System.Void N Y N Cysharp.Threading.Tasks.UniTask N N Y NotSystem.Threading.Tasks.Task N N Y async未使用関数 System.Void N N Y 94

Slide 95

Slide 95 text

動作 BanAsyncTask0001 BanAsyncTask0002 何もレポートされない 条件 async使用関数 System.Threading.Tasks.Task Y N N System.Void N Y N Cysharp.Threading.Tasks.UniTask N N Y NotSystem.Threading.Tasks.Task N N Y async未使用関数 System.Void N N Y 95 このケースを追加

Slide 96

Slide 96 text

public async Task asyncメソッド_戻り値がDummyTask_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("UseDummyTaskCase.txt", "Fakes.cs"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously". .ToArray(); DiagnosticsAssert.IsEmpty(actual); } テストメソッド 96

Slide 97

Slide 97 text

public async Task asyncメソッド_戻り値がDummyTask_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("UseDummyTaskCase.txt", "Fakes.cs"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously". .ToArray(); DiagnosticsAssert.IsEmpty(actual); } using NotSystem.Threading.Tasks; namespace UseDummyTask { class C { async Task UseDummyTask() { } } } 検査対象ソースコード 97

Slide 98

Slide 98 text

public async Task asyncメソッド_戻り値がDummyTask_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("UseDummyTaskCase.txt", "Fakes.cs"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously". .ToArray(); DiagnosticsAssert.IsEmpty(actual); } using NotSystem.Threading.Tasks; namespace UseDummyTask { class C { async Task UseDummyTask() { } } } 98 ”Task”という名前の、asyncメソッドの 戻り値になる型を持つnamespace

Slide 99

Slide 99 text

public async Task asyncメソッド_戻り値がDummyTask_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("UseDummyTaskCase.txt", "Fakes.cs"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously". .ToArray(); DiagnosticsAssert.IsEmpty(actual); } 99 namespace NotSystem.Threading.Tasks { [AsyncMethodBuilder(typeof(CompilerServices.AsyncUniTaskMethodBuil der))] public struct Task { } public class CompilerServices { public class AsyncUniTaskMethodBuilder { } } } ダミー実装

Slide 100

Slide 100 text

public async Task asyncメソッド_戻り値がDummyTask_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("UseDummyTaskCase.txt", "Fakes.cs"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously". .ToArray(); DiagnosticsAssert.IsEmpty(actual); } ダミー実装と検査対象ソースコードを 渡しアナライザーを実行 100

Slide 101

Slide 101 text

101 public async Task asyncメソッド_戻り値がDummyTask_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("UseDummyTaskCase.txt", "Fakes.cs"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously". .ToArray(); DiagnosticsAssert.IsEmpty(actual); } Diagnosticが存在しないことを検証できるカスタムアサーション 出力形式は、前述したDiagnosticsAssert.AreEqualと同じ

Slide 102

Slide 102 text

102 public async Task asyncメソッド_戻り値がDummyTask_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("UseDummyTaskCase.txt", "Fakes.cs"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously". .ToArray(); DiagnosticsAssert.IsEmpty(actual); } Diagnosticが存在しないことを検証できるカスタムアサーション 出力形式は、前述したDiagnosticsAssert.AreEqualと同じ ❌Failed

Slide 103

Slide 103 text

103 public async Task asyncメソッド_戻り値がDummyTask_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("UseDummyTaskCase.txt", "Fakes.cs"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously". .ToArray(); DiagnosticsAssert.IsEmpty(actual); } expected no diagnostics, but 1 diagnostics are reported extra /0/Test0.cs: (4,15)-(4,27), BanAsyncTask0001, Do not use 'NotSystem.Threading.Tasks.Task', Should use UniTask

Slide 104

Slide 104 text

if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } 変更前 アナライザー本体のコード 104

Slide 105

Slide 105 text

if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } 変更前 完全修飾型名を見ていない 105

Slide 106

Slide 106 text

if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } 変更前 完全修飾型名を見ていない 106

Slide 107

Slide 107 text

if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } 変更前 完全修飾型名を見ていない System.Threading.Tasks.Task 完全修飾型名 107

Slide 108

Slide 108 text

if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } 変更前 完全修飾型名を見ていない System.Threading.Tasks.Task 現実装 108

Slide 109

Slide 109 text

if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } 変更前 完全修飾型名を見ていない System.Threading.Tasks.Task 現実装 109

Slide 110

Slide 110 text

if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } 変更後 変更前 完全修飾型名を見ていない 110

Slide 111

Slide 111 text

if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } 変更後 変更前 完全修飾型名を見ていない 完全修飾型名を見る 111

Slide 112

Slide 112 text

if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } 変更後 変更前 using System.Collections.Generic; using Microsoft.CodeAnalysis; namespace BanAsyncTaskAnalyzer.Extensions; public static class NamespaceOrTypeSymbolExtensions { public static string FullName(this INamespaceOrTypeSymbol symbol) { var collectedNamespace = new List(); while (symbol.Name != "") { collectedNamespace.Add(symbol.Name); symbol = symbol.ContainingNamespace; } collectedNamespace.Reverse(); return string.Join(".", collectedNamespace); } } 完全修飾型名を取得する 拡張メソッド 112

Slide 113

Slide 113 text

if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task" ) { var diagnostic = Diagnostic.Create( Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } 変更後 変更前 using System.Collections.Generic; using Microsoft.CodeAnalysis; namespace BanAsyncTaskAnalyzer.Extensions; public static class NamespaceOrTypeSymbolExtensions { public static string FullName(this INamespaceOrTypeSymbol symbol) { var collectedNamespace = new List(); while (symbol.Name != "") { collectedNamespace.Add(symbol.Name); symbol = symbol.ContainingNamespace; } collectedNamespace.Reverse(); return string.Join(".", collectedNamespace); } } ✔Passed 113

Slide 114

Slide 114 text

if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task" ) { var diagnostic = Diagnostic.Create( Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } 変更後 変更前 using System.Collections.Generic; using Microsoft.CodeAnalysis; namespace BanAsyncTaskAnalyzer.Extensions; public static class NamespaceOrTypeSymbolExtensions { public static string FullName(this INamespaceOrTypeSymbol symbol) { var collectedNamespace = new List(); while (symbol.Name != "") { collectedNamespace.Add(symbol.Name); symbol = symbol.ContainingNamespace; } collectedNamespace.Reverse(); return string.Join(".", collectedNamespace); } } Refactor Add Test 114

Slide 115

Slide 115 text

if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task" ) { var diagnostic = Diagnostic.Create( Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } if (methodSymbol.ReturnType.Name == "Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } 変更後 変更前 using System.Collections.Generic; using Microsoft.CodeAnalysis; namespace BanAsyncTaskAnalyzer.Extensions; public static class NamespaceOrTypeSymbolExtensions { public static string FullName(this INamespaceOrTypeSymbol symbol) { var collectedNamespace = new List(); while (symbol.Name != "") { collectedNamespace.Add(symbol.Name); symbol = symbol.ContainingNamespace; } collectedNamespace.Reverse(); return string.Join(".", collectedNamespace); } } Refactor Add Test 115

Slide 116

Slide 116 text

動作 BanAsyncTask0001 BanAsyncTask0002 何もレポートされない 条件 async使用関数 System.Threading.Tasks.Task Y N N System.Void N Y N Cysharp.Threading.Tasks.UniTask N N Y NotSystem.Threading.Tasks.Task N N Y async未使用関数 System.Void N N Y 116

Slide 117

Slide 117 text

動作 BanAsyncTask0001 BanAsyncTask0002 何もレポートされない 条件 async使用関数 System.Threading.Tasks.Task Y N N System.Void N Y N Cysharp.Threading.Tasks.UniTask N N Y NotSystem.Threading.Tasks.Task N N Y async未使用関数 System.Void N N Y 117 このケースを追加

Slide 118

Slide 118 text

public async Task asyncメソッド_戻り値がUniTask_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("UseUniTaskCase.txt", "Fakes.cs"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously". .ToArray(); DiagnosticsAssert.IsEmpty(actual); } テストメソッド 118

Slide 119

Slide 119 text

public async Task asyncメソッド_戻り値がUniTask_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("UseUniTaskCase.txt", "Fakes.cs"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously". .ToArray(); DiagnosticsAssert.IsEmpty(actual); } 119 using Cysharp.Threading.Tasks; class C { async UniTask UseUniTask() { } } 検査対象ソースコード

Slide 120

Slide 120 text

public async Task asyncメソッド_戻り値がUniTask_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("UseUniTaskCase.txt", "Fakes.cs"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously". .ToArray(); DiagnosticsAssert.IsEmpty(actual); } 120 using Cysharp.Threading.Tasks; class C { async UniTask UseUniTask() { } } UniTaskの namespaceを指定

Slide 121

Slide 121 text

public async Task asyncメソッド_戻り値がUniTask_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("UseUniTaskCase.txt", "Fakes.cs"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously". .ToArray(); DiagnosticsAssert.IsEmpty(actual); } 121 using Cysharp.Threading.Tasks; class C { async UniTask UseUniTask() { } } using System.Runtime.CompilerServices; namespace Cysharp.Threading.Tasks { [AsyncMethodBuilder(typeof(CompilerServices.AsyncUniTaskMethodBuilder))] public struct UniTask { } [AsyncMethodBuilder(typeof(CompilerServices.AsyncUniTaskMethodBuilder))] public struct UniTaskVoid { } } namespace Cysharp.Threading.Tasks.CompilerServices { public struct AsyncUniTaskMethodBuilder { } } UniTaskの ダミー実装

Slide 122

Slide 122 text

public async Task asyncメソッド_戻り値がUniTask_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("UseUniTaskCase.txt", "Fakes.cs"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously". .ToArray(); DiagnosticsAssert.IsEmpty(actual); } 122 using Cysharp.Threading.Tasks; class C { async UniTask UseUniTask() { } } using System.Runtime.CompilerServices; namespace Cysharp.Threading.Tasks { [AsyncMethodBuilder(typeof(CompilerServices.AsyncUniTaskMethodBuilder))] public struct UniTask { } [AsyncMethodBuilder(typeof(CompilerServices.AsyncUniTaskMethodBuilder))] public struct UniTaskVoid { } } namespace Cysharp.Threading.Tasks.CompilerServices { public struct AsyncUniTaskMethodBuilder { } } ✔Passed

Slide 123

Slide 123 text

public async Task asyncメソッド_戻り値がUniTask_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("UseUniTaskCase.txt", "Fakes.cs"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously". .ToArray(); DiagnosticsAssert.IsEmpty(actual); } 123 using Cysharp.Threading.Tasks; class C { async UniTask UseUniTask() { } } using System.Runtime.CompilerServices; namespace Cysharp.Threading.Tasks { [AsyncMethodBuilder(typeof(CompilerServices.AsyncUniTaskMethodBuilder))] public struct UniTask { } [AsyncMethodBuilder(typeof(CompilerServices.AsyncUniTaskMethodBuilder))] public struct UniTaskVoid { } } namespace Cysharp.Threading.Tasks.CompilerServices { public struct AsyncUniTaskMethodBuilder { } } Refactor Add Test

Slide 124

Slide 124 text

public async Task asyncメソッド_戻り値がUniTask_何もレポートされない() { var analyzer = new BanAsyncTaskAnalyzer(); var source = ReadCodes("UseUniTaskCase.txt", "Fakes.cs"); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actual = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously". .ToArray(); DiagnosticsAssert.IsEmpty(actual); } 124 using Cysharp.Threading.Tasks; class C { async UniTask UseUniTask() { } } using System.Runtime.CompilerServices; namespace Cysharp.Threading.Tasks { [AsyncMethodBuilder(typeof(CompilerServices.AsyncUniTaskMethodBuilder))] public struct UniTask { } [AsyncMethodBuilder(typeof(CompilerServices.AsyncUniTaskMethodBuilder))] public struct UniTaskVoid { } } namespace Cysharp.Threading.Tasks.CompilerServices { public struct AsyncUniTaskMethodBuilder { } } Refactor Add Test

Slide 125

Slide 125 text

動作 BanAsyncTask0001 BanAsyncTask0002 何もレポートされない 条件 async使用関数 System.Threading.Tasks.Task Y N N System.Void N Y N Cysharp.Threading.Tasks.UniTask N N Y NotSystem.Threading.Tasks.Task N N Y async未使用関数 System.Void N N Y 125

Slide 126

Slide 126 text

動作 BanAsyncTask0001 BanAsyncTask0002 何もレポートされない 条件 async使用関数 System.Threading.Tasks.Task Y N N System.Void N Y N Cysharp.Threading.Tasks.UniTask N N Y NotSystem.Threading.Tasks.Task N N Y async未使用関数 System.Void N N Y このケースを追加 126

Slide 127

Slide 127 text

[Test] public async Task asyncメソッド_戻り値がvoid_BanAsyncTask0002がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("AsyncVoidCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS8019") // Ignore "Unnecessary using directive" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously." .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } 127

Slide 128

Slide 128 text

[Test] public async Task asyncメソッド_戻り値がvoid_BanAsyncTask0002がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("AsyncVoidCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS8019") // Ignore "Unnecessary using directive" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously." .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } 128 using System.Threading; using System.Threading.Tasks; namespace UseTask { class C { async void {|UseAsyncVoid|BanAsyncTask0002|Do not use 'void' in async Method, Should use UniTaskVoid|}() { await Task.Delay(100); } } } 検査対象ソースコード

Slide 129

Slide 129 text

[Test] public async Task asyncメソッド_戻り値がvoid_BanAsyncTask0002がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("AsyncVoidCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS8019") // Ignore "Unnecessary using directive" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously." .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } 129 using System.Threading; using System.Threading.Tasks; namespace UseTask { class C { async void {|UseAsyncVoid|BanAsyncTask0002|Do not use 'void' in async Method, Should use UniTaskVoid|}() { await Task.Delay(100); } } } レポート箇所

Slide 130

Slide 130 text

[Test] public async Task asyncメソッド_戻り値がvoid_BanAsyncTask0002がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("AsyncVoidCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS8019") // Ignore "Unnecessary using directive" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously." .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } 130 using System.Threading; using System.Threading.Tasks; namespace UseTask { class C { async void {|UseAsyncVoid|BanAsyncTask0002|Do not use 'void' in async Method, Should use UniTaskVoid|}() { await Task.Delay(100); } } } DDID

Slide 131

Slide 131 text

[Test] public async Task asyncメソッド_戻り値がvoid_BanAsyncTask0002がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("AsyncVoidCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS8019") // Ignore "Unnecessary using directive" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously." .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } 131 using System.Threading; using System.Threading.Tasks; namespace UseTask { class C { async void {|UseAsyncVoid|BanAsyncTask0002|Do not use 'void' in async Method, Should use UniTaskVoid|}() { await Task.Delay(100); } } } エラーメッセージ

Slide 132

Slide 132 text

using System.Threading; using System.Threading.Tasks; namespace UseTask { class C { async void {|UseAsyncVoid|BanAsyncTask0002|Do not use 'void' in async Method, Should use UniTaskVoid|}() { await Task.Delay(100); } } } [Test] public async Task asyncメソッド_戻り値がvoid_BanAsyncTask0002がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("AsyncVoidCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS8019") // Ignore "Unnecessary using directive" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously." .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } ❌Failed 132

Slide 133

Slide 133 text

[Test] public async Task asyncメソッド_戻り値がvoid_BanAsyncTask0002がレポートされる() { var analyzer = new BanAsyncTaskAnalyzer(); var testData = ReadCodes("AsyncVoidCase.txt"); var (source, expectedDiagnostics) = TestDataParser.CreateSourceAndExpectedDiagnostic(testData[0]); var diagnostics = await DiagnosticAnalyzerRunner.Run(analyzer, source); var actualDiagnostics = diagnostics .Where(x => x.Id != "CS1591") // Ignore "Missing XML comment for publicly visible type or member" .Where(x => x.Id != "CS8019") // Ignore "Unnecessary using directive" .Where(x => x.Id != "CS1998") // Ignore "This async method lacks 'await' operators and will run synchronously." .ToArray(); DiagnosticsAssert.AreEqual(expectedDiagnostics, actualDiagnostics); } 133 extra 0 diagnostics of all 0 diagnostics: missing /0/Test0.cs: (7,18)-(7,30), BanAsyncTask0002, Do not use 'void' in async Method, Should use UniTaskVoid

Slide 134

Slide 134 text

アナライザー本体を実装 134

Slide 135

Slide 135 text

private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } } 変更前 アナライザー本体のソースコード 135

Slide 136

Slide 136 text

private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } } 変更前 void型の指定を検知できない 136

Slide 137

Slide 137 text

private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } if (methodSymbol.ReturnType.FullName() == "System.Void") { var diagnostic = Diagnostic.Create(Rule02, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); } } private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } } ✔変更後 変更前 137

Slide 138

Slide 138 text

private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } if (methodSymbol.ReturnType.FullName() == "System.Void") { var diagnostic = Diagnostic.Create(Rule02, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); } } private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } } 変更前 ✔変更後 条件分岐を追加 138

Slide 139

Slide 139 text

private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } if (methodSymbol.ReturnType.FullName() == "System.Void") { var diagnostic = Diagnostic.Create(Rule02, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); } } private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } } 変更前 ✔変更後 完全修飾型名がSystem.Voidかどうか判定 139

Slide 140

Slide 140 text

private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } if (methodSymbol.ReturnType.FullName() == "System.Void") { var diagnostic = Diagnostic.Create(Rule02, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); } } private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } } 変更前 private static readonly DiagnosticDescriptor Rule02 = new DiagnosticDescriptor( id: "BanAsyncTask0002", title: "Banned void in async Method", messageFormat: "Do not use '{0}' in async Method, Should use UniTaskVoid", category: "Naming", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Type names should be all uppercase."); ✔変更後 System.Voidの使用をレポートする ルールを追加 140

Slide 141

Slide 141 text

private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } if (methodSymbol.ReturnType.FullName() == "System.Void") { var diagnostic = Diagnostic.Create(Rule02, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); } } private static void AnalyzeAsyncMethod(SymbolAnalysisContext context) { var methodSymbol = (IMethodSymbol)context.Symbol; if (!methodSymbol.IsAsync) { return; } if (methodSymbol.ReturnType.FullName() == "System.Threading.Tasks.Task") { var diagnostic = Diagnostic.Create(Rule01, methodSymbol.Locations[0], methodSymbol.ReturnType); context.ReportDiagnostic(diagnostic); return; } } 変更前 private static readonly DiagnosticDescriptor Rule02 = new DiagnosticDescriptor( id: "BanAsyncTask0002", title: "Banned void in async Method", messageFormat: "Do not use '{0}' in async Method, Should use UniTaskVoid", category: "Naming", defaultSeverity: DiagnosticSeverity.Warning, isEnabledByDefault: true, description: "Type names should be all uppercase."); ✔Passed ✔変更後 141

Slide 142

Slide 142 text

すべてのテストケースの実装が完了 142

Slide 143

Slide 143 text

まとめ ● DiagnosticAnalyzer.Runを使ってアナライザーの実際の値を簡 単に入手しよう ● DiagnosticsAssertでDiagnostic同士を比較しよう ● TestDataParserで簡単にテストの期待結果を作成しよう 143

Slide 144

Slide 144 text

144