Roslynアナライザー_ Unityでの開発環境を改善するための静的解析の仕組みの構築

17.5K Views

August 31, 24

スライド概要

静的解析は、バグの原因となる箇所を早期に発見し、コードの品質を向上させるための有効な手段です。本発表では、C#のコード解析用APIを持つRoslynアナライザーを用いた静的解析の実装方法について解説します。構文解析とフロー解析の具体的な例を示し、DeNAで作成したLinterレベルに留まらない独自のコード検査ルールを紹介します。また、RoslynアナライザーをUnityプロジェクトに導入する方法と、解析結果を効率的に収集・フィードバックする仕組みについて説明します。収集した解析結果をもとに、コードの品質を継続的に改善するためのアクションについても議論します。

profile-image

DeNA が社会の技術向上に貢献するため、業務で得た知見を積極的に外部に発信する、DeNA 公式のアカウントです。DeNA エンジニアの登壇資料をお届けします。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

Roslynアナライザー Unityでの開発環境を改善するための静的解析の仕組みの構築 株式会社ディー・エヌ・エー IT本部品質管理部SWET第二グループ Tomomi Hatano Kazuma Inagaki 1

2.

本発表のゴール ● 静的解析ツールの役割と仕組みを知る ● C# 向けの静的解析ツールを自作できる環境を知る ● 静的解析ツールを作りたくなる ● RoslynアナライザーをUnityプロジェクトに導入する方法を知る ● Roslynアナライザーの解析結果を活かす方法を知る 2

3.

本発表のゴール hatanoが話す領域 ● 静的解析ツールの役割と仕組みを知る ● C# 向けの静的解析ツールを自作できる環境を知る ● 静的解析ツールを作りたくなる ● RoslynアナライザーをUnityプロジェクトに導入する方法を知る ● Roslynアナライザーの解析結果を活かす方法を知る 3

4.

Tomomi Hatano 株式会社ディー・エヌ・エー IT本部品質管理部SWET第二グループ 学生時代から静的解析技術について研究 就職後も静的解析ツールの開発に従事 2023年12月から現職 4

5.

静的解析ツールが必要になる背景 5

6.

不具合の影響 不具合の発見が遅れるほど、その修正コストは高くなる 1 要求 設計 コーティング 開発者テスト ユーザテスト リリース 1 JSTQB https://jstqb.jp/dl/JSTQB-SyllabusFoundation_VersionV40.J01.pdf 6

7.

不具合をできるだけ 早く見つけたい 7

8.

コーディングの不具合 Q. コーティングの不具合を最も早く発見できるタイミングは? 要求 設計 コーティング 開発者テスト ユーザテスト リリース 8

9.

A. コーディングしているとき 9

10.

コーディングしているときに コーディングの不具合を見つけて くれるのが 静的解析ツール 10

11.

静的解析ツールの例 JetBrains Rider の画面 未使用の変数があることを教 えてくれる 11

12.

既存の静的解析ツールで 検知できない問題に直面したら? 12

13.

自分で作ろう! 13

14.

私たちが開発したツールを例に どんなことができるのか どのように実装するのか を説明します 14

15.
[beta]
お題: UniTask の await 忘れを防ぐ
1: class Presenter
2: {
3:
async UniTask Show(bool first)
4:
{
5:
if (first)
6:
{
7:
await UniTask.Run(
8:
() => {/*...*/});
9:
}
10:
else
11:
{
12:
DoTask1Async();
13:
}
14:
}

15: List<string> _items;
16: async UniTask DoTask1Async()
17: {
18:
if (_items.Count > 0)
19:
{
20:
await UniTask.Run(
21:
() => {/*...*/});
22:
}
23:
else
24:
{
25:
throw new Exception("Fatal Error!!");
26:
}
27: }
28:}

15

16.
[beta]
お題: UniTask の await 忘れを防ぐ
1: class Presenter
2: {
3:
async UniTask Show(bool first)
4:
{
5:
if (first)
6:
{
7:
await UniTask.Run(
8:
() => {/*...*/});
9:
}
10:
else
11:
{
12:
DoTask1Async();
13:
}
14:
}

ココ!

15: List<string> _items;
16: async UniTask DoTask1Async()
17: {
18:
if (_items.Count > 0)
19:
{
20:
await UniTask.Run(
21:
() => {/*...*/});
22:
}
23:
else
24:
{
25:
throw new Exception("Fatal Error!!");
26:
}
27: }
28:}

16

17.
[beta]
お題: UniTask の await 忘れを防ぐ
1: class Presenter
2: {
3:
async UniTask Show(bool first)
4:
{
5:
if (first)
6:
{
7:
await UniTask.Run(
8:
() => {/*...*/});
9:
}
10:
else
11:
{
12:
DoTask1Async();
13:
}
14:
}
await

DoTask1Async();
と書くべき

15: List<string> _items;
16: async UniTask DoTask1Async()
17: {
18:
if (_items.Count > 0)
19:
{
20:
await UniTask.Run(
21:
() => {/*...*/});
22:
}
23:
else
24:
{
25:
throw new Exception("Fatal Error!!");
26:
}
27: }
28:}

※ 標準の警告 CS4014 では少し不十分でした

17

18.
[beta]
お題: UniTask の await 忘れを防ぐ
1: class Presenter
2: {
3:
async UniTask Show(bool first)
4:
{
5:
if (first)
6:
{
7:
await UniTask.Run(
8:
() => {/*...*/});
9:
}
10:
else
11:
{
12:
DoTask1Async();
13:
}
14:
}

15: List<string> _items;
16: async UniTask DoTask1Async()
17: {
18:
if (_items.Count > 0)
19:
{
20:
await UniTask.Run(
21:
() => {/*...*/});
22:
}
23:
else
24:
{
25:
throw new Exception("Fatal Error!!");
26:
}
27: }
28:}
例外が捕捉されない

※ 標準の警告 CS4014 では少し不十分でした

18

19.
[beta]
お題: UniTask の await 忘れを防ぐ

😱

1: class Presenter
2: {
3:
async UniTask Show(bool first)
4:
{
5:
if (first)
6:
{
7:
await UniTask.Run(
8:
() => {/*...*/});
9:
}
10:
else
11:
{
12:
DoTask1Async();
13:
}
14:
}

15: List<string> _items;
16: async UniTask DoTask1Async()
17: {
18:
if (_items.Count > 0)
19:
{
20:
await UniTask.Run(
21:
() => {/*...*/});
22:
}
23:
else
24:
{
25:
throw new Exception("Fatal Error!!");
26:
}
27: }
28:}

予期せぬ不具合

19

20.

await 忘れを検知する 静的解析ツールを作ろう 20

21.

await 忘れ検知の仕様 返り値の型が UniTask 1 であるメソッドのメソッド呼び出しに await が付与 2 されていなければ、その箇所を出力する https://github.com/Cysharp/UniTask 1 正確には Cysharp.Threading.Tasks.UniTask 2 より正確にはメソッド呼び出しの結果に await 演算子が適用 21

22.

どうやって実装する? 文字列検索? でもいろいろ問題がありそう ● メソッド呼び出し箇所をどう見つける? ● たまたまの文字列一致がありうる (コメントにマッチ、など) ● await とメソッド呼び出しの間に改行がある かも async UniTask Show(bool first) { if (first) { await UniTask.Run( () => {/*...*/}); } else { DoTask1Async(); } } 22

23.

構文解析しよう! 23

24.

簡単に言えば 構文解析とは コードを木構造に変換すること 24

25.

構文解析 MethodDeclaration IfStatement async UniTask Show(bool first) { if (first) { await UniTask.Run( () => {/*...*/}); } else { DoTask1Async(); } } Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 25

26.

構文解析 MethodDeclaration まずメソッド宣言があって IfStatement async UniTask Show(bool first) { if (first) { await UniTask.Run( () => {/*...*/}); } else { DoTask1Async(); } } Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 26

27.

構文解析 MethodDeclaration その中に if 文があって IfStatement async UniTask Show(bool first) { if (first) { await UniTask.Run( () => {/*...*/}); } else { DoTask1Async(); } } Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 27

28.

構文解析 MethodDeclaration if 文には条件式と true 側の文と false 側の文があって async UniTask Show(bool first) { if (first) { await UniTask.Run( () => {/*...*/}); } else { DoTask1Async(); } } IfStatement Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 28

29.

構文解析 MethodDeclaration 条件式は識別子(変数)で IfStatement async UniTask Show(bool first) { if (first) { await UniTask.Run( () => {/*...*/}); } else { DoTask1Async(); } } Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 29

30.

構文解析 MethodDeclaration true 側の文は await 式で IfStatement async UniTask Show(bool first) { if (first) { await UniTask.Run( () => {/*...*/}); } else { DoTask1Async(); } } Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 30

31.

構文解析 MethodDeclaration await 式の対象は メソッド呼び出しで async UniTask Show(bool first) { if (first) { await UniTask.Run( () => {/*...*/}); } else { DoTask1Async(); } } IfStatement Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 31

32.

構文解析 MethodDeclaration false 側の文は メソッド呼び出しである async UniTask Show(bool first) { if (first) { await UniTask.Run( () => {/*...*/}); } else { DoTask1Async(); } } IfStatement Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 32

33.

構文木 MethodDeclaration コードを木構造で表現したもので 構文木と呼ばれる IfStatement Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 33

34.

構文木 MethodDeclaration コードと木構造は相性がいい IfStatement Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 34

35.

構文木 MethodDeclaration コードと木構造は相性がいい IfStatement コードは階層構造を持つ (例) クラス、メソッド、文 文法には再帰的な要素がある (例) if 文の中にさらに if 文 Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 35

36.

構文木 MethodDeclaration コードと木構造は相性がいい そして木構造は解析しやすい コードは階層構造を持つ (例) クラス、メソッド、文 文法には再帰的な要素がある (例) if 文の中にさらに if 文 IfStatement Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 36

37.

実際どうやって 構文木を作るの? 37

38.

構文木を(イチから)作るには ソースコード 字句解析 文法規則 トークン列 構文解析 構文木 38

39.

構文木を(イチから)作るには ソースコード 字句解析 文法規則 ちょっと大変そう😅 トークン列 構文解析 構文木 39

40.

Roslyn を使おう! 40

41.

Roslyn とは Roslyn の GitHub リポジトリ † より Roslyn is the open-source implementation of both the C# and Visual Basic compilers with an API surface for building code analysis tools. † https://github.com/dotnet/roslyn 41

42.

Roslyn とは Roslyn の GitHub リポジトリ † より Roslyn is the open-source implementation of both the C# and Visual Basic compilers with an API surface for building code analysis tools. 静的解析に使える API を提供してくれている! † https://github.com/dotnet/roslyn 42

43.

Roslyn を使う Roslyn がやってくれる😊 ソースコード 字句解析 文法規則 トークン列 構文解析 構文木 43

44.

検知箇所をエラーや警告として IDE などに表示する API もある! 44

45.

Roslyn アナライザー Roslyn Roslyn アナライザー .NETコンパイラプラットフォー ムの通称 コードの問題を検出する ● C#、VB向けコンパイラ ● コード生成API ● コード解析API プログラム コード解析APIを使ってプ ログラムを自作

46.

実装はこんな感じ 46

47.
[beta]
実装(30行くらい 前半)
l: [DiagnosticAnalyzer(LanguageNames.CSharp)]
2: public class AwaitAnalyzer : DiagnosticAnalyzer
3: {
4:
private static readonly DiagnosticDescriptor awaitRule = new(
5:
id: "My0001",
6:
title: "Must use await",
7:
messageFormat: "メソッドが await されていません ",
8:
category: "UnityAnalyzers",
9:
defaultSeverity: DiagnosticSeverity.Error,
10:
isEnabledByDefault: true
l1:
);
l2:
l3:
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
l4:
ImmutableArray.Create(awaitRule);
l5:
…
}
47

48.
[beta]
実装(30行くらい 前半)

DiagnosticAnalyzer を継承すると
独自の Roslyn アナライザーを作れる

l: [DiagnosticAnalyzer(LanguageNames.CSharp)]
2: public class AwaitAnalyzer : DiagnosticAnalyzer
3: {
4:
private static readonly DiagnosticDescriptor awaitRule = new(
5:
id: "My0001",
6:
title: "Must use await",
7:
messageFormat: "メソッドが await されていません ",
8:
category: "UnityAnalyzers",
9:
defaultSeverity: DiagnosticSeverity.Error,
10:
isEnabledByDefault: true
l1:
);
l2:
l3:
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
l4:
ImmutableArray.Create(awaitRule);
l5:
…
}
48

49.
[beta]
実装(30行くらい 前半)
l: [DiagnosticAnalyzer(LanguageNames.CSharp)]
2: public class AwaitAnalyzer : DiagnosticAnalyzer
3: {
4:
private static readonly DiagnosticDescriptor awaitRule = new(
5:
id: "My0001",
6:
title: "Must use await",
7:
messageFormat: "メソッドが await されていません ",
8:
category: "UnityAnalyzers",
9:
defaultSeverity: DiagnosticSeverity.Error,
10:
isEnabledByDefault: true
検知ルールの各種情報を記述
l1:
);
l2:
l3:
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
l4:
ImmutableArray.Create(awaitRule);
l5:
…
}

49

50.
[beta]
実装(30行くらい 前半)
l: [DiagnosticAnalyzer(LanguageNames.CSharp)]
2: public class AwaitAnalyzer : DiagnosticAnalyzer
3: {
4:
private static readonly DiagnosticDescriptor awaitRule = new(
5:
id: "My0001",
6:
title: "Must use await",
7:
messageFormat: "メソッドが await されていません ",
8:
category: "UnityAnalyzers",
9:
defaultSeverity: DiagnosticSeverity.Error,
10:
isEnabledByDefault: true
l1:
);
l2:
l3:
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics =>
l4:
ImmutableArray.Create(awaitRule);
l5:
検知ルールのリストを作る
…
(複数のルールを適用できる)
}
50

51.

実装(30行くらい 後半) l: [DiagnosticAnalyzer(LanguageNames.CSharp)] 2: public class AwaitAnalyzer : DiagnosticAnalyzer 3: { … 16: public override void Initialize(AnalysisContext context) l7: { l8: context.RegisterOperationAction(ReportNonAwaitedMethod, l9: OperationKind.Invocation); 20: } 2l: 22: private void ReportNonAwaitedMethod(OperationAnalysisContext context) 23: { 24: var diagnostic = Diagnostic.Create(awaitRule, 25: context.Operation.Syntax.GetLocation()); 26: context.ReportDiagnostic(diagnostic); 27: } 28: } 51

52.

実装(30行くらい 後半) l: [DiagnosticAnalyzer(LanguageNames.CSharp)] 2: public class AwaitAnalyzer : DiagnosticAnalyzer 3: { … 16: public override void Initialize(AnalysisContext context) l7: { l8: context.RegisterOperationAction(ReportNonAwaitedMethod, l9: OperationKind.Invocation); 20: } どの命令に対してどのような解析をする 2l: 22: private void ReportNonAwaitedMethod(OperationAnalysisContext context) のか登録する 23: { 24: var diagnostic = Diagnostic.Create(awaitRule, 25: context.Operation.Syntax.GetLocation()); 26: context.ReportDiagnostic(diagnostic); 27: } 28: } 52

53.

実装(30行くらい 後半) l: [DiagnosticAnalyzer(LanguageNames.CSharp)] 2: public class AwaitAnalyzer : DiagnosticAnalyzer 3: { … 16: public override void Initialize(AnalysisContext context) l7: { l8: context.RegisterOperationAction(ReportNonAwaitedMethod, l9: OperationKind.Invocation); 20: } 2l: メソッド呼び出し命令を対象に context) 22: private void ReportNonAwaitedMethod(OperationAnalysisContext 23: { 24: var diagnostic = Diagnostic.Create(awaitRule, 25: context.Operation.Syntax.GetLocation()); 26: context.ReportDiagnostic(diagnostic); 27: } 28: } 53

54.

実装(30行くらい 後半) l: [DiagnosticAnalyzer(LanguageNames.CSharp)] 2: public class AwaitAnalyzer : DiagnosticAnalyzer 3: { ReportNonAwaitedMethod … メソッドを実行する 16: public override void Initialize(AnalysisContext context) l7: { l8: context.RegisterOperationAction(ReportNonAwaitedMethod, l9: OperationKind.Invocation); 20: } 2l: メソッド呼び出し命令を対象に context) 22: private void ReportNonAwaitedMethod(OperationAnalysisContext 23: { 24: var diagnostic = Diagnostic.Create(awaitRule, 25: context.Operation.Syntax.GetLocation()); 26: context.ReportDiagnostic(diagnostic); 27: } 28: } 54

55.

実装(30行くらい 後半) l: [DiagnosticAnalyzer(LanguageNames.CSharp)] 2: public class AwaitAnalyzer : DiagnosticAnalyzer 3: { … 16: public override void Initialize(AnalysisContext context) l7: { l8: context.RegisterOperationAction(ReportNonAwaitedMethod, メソッド呼び出しを見つけるとこのロ l9: OperationKind.Invocation); ジックが走る 20: } 2l: 22: private void ReportNonAwaitedMethod(OperationAnalysisContext context) 23: { 24: var diagnostic = Diagnostic.Create(awaitRule, 25: context.Operation.Syntax.GetLocation()); 26: context.ReportDiagnostic(diagnostic); 27: } 28: } 55

56.

実装(30行くらい 後半) l: [DiagnosticAnalyzer(LanguageNames.CSharp)] 2: public class AwaitAnalyzer : DiagnosticAnalyzer 3: { … 16: public override void Initialize(AnalysisContext context) l7: { l8: context.RegisterOperationAction(ReportNonAwaitedMethod, l9: OperationKind.Invocation); 20: } 2l: 22: private void ReportNonAwaitedMethod(OperationAnalysisContext context) 23: { 24: var diagnostic = Diagnostic.Create(awaitRule, 25: context.Operation.Syntax.GetLocation()); 26: context.ReportDiagnostic(diagnostic); 27: } エラー情報(Diagnostic)を生 28: } 成してそれを出力する 56

57.

動かしてみると あらゆるメソッド呼び出しをエラーとして出力する謎ツールができた JetBrains Rider の画面 57

58.

await 忘れ検知の仕様 返り値の型が UniTask 1 であるメソッドのメソッド呼び出し に await が付与 2 されていなければ、その箇所を出力する 「メソッド呼び出しを見つけて それを出力する」はできた! 1 正確には Cysharp.Threading.Tasks.UniTask 2 より正確にはメソッド呼び出しの結果に await 演算子が適用 58

59.

await 忘れ検知の仕様 返り値の型が UniTask 1 であるメソッド のメソッド呼び出し に await が付与 2 されていなければ、その箇所を出力する 残りはどうする? 1 正確には Cysharp.Threading.Tasks.UniTask 2 より正確にはメソッド呼び出しの結果に await 演算子が適用 59

60.

返り値の型が UniTask であるメソッド 60

61.

返り値の型を調べる l: [DiagnosticAnalyzer(LanguageNames.CSharp)] 2: public class AwaitAnalyzer : DiagnosticAnalyzer 3: { … 22: private void ReportNonAwaitedMethod(OperationAnalysisContext context) 23: { 24: var diagnostic = Diagnostic.Create(awaitRule, 25: context.Operation.Syntax.GetLocation()); 26: context.ReportDiagnostic(diagnostic); 27: } 28: } メソッド呼び出しを見つけた ときのロジックを変更する 61

62.
[beta]
返り値の型を調べる
1: [DiagnosticAnalyzer(LanguageNames.CSharp)]
2: public class AwaitAnalyzer : DiagnosticAnalyzer
3: {
…
22:
private void ReportNonAwaitedMethod(OperationAnalysisContext context)
23:
{
var invocation = (IInvocationOperation)context.Operation;
+24:
if (invocation.TargetMethod.ReturnType.Name == "UniTask")
+25:
{
+26:
27:
var diagnostic = Diagnostic.Create(awaitRule,
28:
context.Operation.Syntax.GetLocation());
29:
context.ReportDiagnostic(diagnostic);
}
+30:
31:
}
32: }
62

63.
[beta]
返り値の型を調べる
1: [DiagnosticAnalyzer(LanguageNames.CSharp)]
2: public class AwaitAnalyzer : DiagnosticAnalyzer
3: {
…
22:
private void ReportNonAwaitedMethod(OperationAnalysisContext context)
23:
{
24:
var invocation = (IInvocationOperation)context.Operation;
25:
if (invocation.TargetMethod.ReturnType.Name == "UniTask")
26:
{
context.Operation に解析対象の
27:
var diagnostic = Diagnostic.Create(awaitRule,
命令に関する情報が入っている
28:
context.Operation.Syntax.GetLocation());
29:
context.ReportDiagnostic(diagnostic);
30:
}
31:
}
32: }
63

64.
[beta]
返り値の型を調べる
1: [DiagnosticAnalyzer(LanguageNames.CSharp)]
2: public class AwaitAnalyzer : DiagnosticAnalyzer
3: {
…
22:
private void ReportNonAwaitedMethod(OperationAnalysisContext context)
23:
{
24:
var invocation = (IInvocationOperation)context.Operation;
25:
if (invocation.TargetMethod.ReturnType.Name == "UniTask")
26:
{
メソッド呼び出し命令に関する
27:
var diagnostic = Diagnostic.Create(awaitRule,
詳細情報を取得するためキャスト
28:
context.Operation.Syntax.GetLocation());
29:
context.ReportDiagnostic(diagnostic);
30:
}
31:
}
32: }
64

65.
[beta]
返り値の型を調べる
1: [DiagnosticAnalyzer(LanguageNames.CSharp)]
2: public class AwaitAnalyzer : DiagnosticAnalyzer
3: {
…
22:
private void ReportNonAwaitedMethod(OperationAnalysisContext context)
23:
{
24:
var invocation = (IInvocationOperation)context.Operation;
25:
if (invocation.TargetMethod.ReturnType.Name == "UniTask")
26:
{
27:
var diagnostic = Diagnostic.Create(awaitRule,
メソッド呼び出しの返り値の型に
28:
context.Operation.Syntax.GetLocation());
関する情報を取得
29:
context.ReportDiagnostic(diagnostic);
30:
}
31:
}
32: }
65

66.
[beta]
返り値の型を調べる
1: [DiagnosticAnalyzer(LanguageNames.CSharp)]
2: public class AwaitAnalyzer : DiagnosticAnalyzer
3: {
…
22:
private void ReportNonAwaitedMethod(OperationAnalysisContext context)
23:
{
24:
var invocation = (IInvocationOperation)context.Operation;
25:
if (invocation.TargetMethod.ReturnType.Name == "UniTask")
26:
{
27:
var diagnostic = Diagnostic.Create(awaitRule,
メソッド呼び出しの返り値の型に
28:
context.Operation.Syntax.GetLocation());
関する情報を取得
29:
context.ReportDiagnostic(diagnostic);
30:
}
31:
}
32: }

※ Roslyn では構文解析と同時に意味解析もできるため型名が分かります

66

67.

await 忘れ検知の仕様 返り値の型が UniTask 1 であるメソッド のメソッド呼び出し に await が付与 2 されていなければ、その箇所を出力する 1 正確には Cysharp.Threading.Tasks.UniTask 2 より正確にはメソッド呼び出しの結果に await 演算子が適用 67

68.

await が付与 68

69.
[beta]
await が付与されているかを調べる
1: [DiagnosticAnalyzer(LanguageNames.CSharp)]
2: public class AwaitAnalyzer : DiagnosticAnalyzer
3: {
…
22:
private void ReportNonAwaitedMethod(OperationAnalysisContext context)
23:
{
24:
var invocation = (IInvocationOperation)context.Operation;
25:
if (invocation.TargetMethod.ReturnType.Name == "UniTask")
26:
{
27:
var diagnostic = Diagnostic.Create(awaitRule,
28:
context.Operation.Syntax.GetLocation());
29:
context.ReportDiagnostic(diagnostic);
30:
}
31:
}
32: }
先ほどのロジックをさらに変更する
69

70.
[beta]
await が付与されているかを調べる
1: [DiagnosticAnalyzer(LanguageNames.CSharp)]
2: public class AwaitAnalyzer : DiagnosticAnalyzer
3: {
…
22:
private void ReportNonAwaitedMethod(OperationAnalysisContext context)
23:
{
24:
var invocation = (IInvocationOperation)context.Operation;
25:
if (invocation.TargetMethod.ReturnType.Name == "UniTask")
&& invocation.Syntax.Parent is not AwaitExpressionSyntax)
+ 26:
27:
{
28:
var diagnostic = Diagnostic.Create(awaitRule,
29:
context.Operation.Syntax.GetLocation());
30:
context.ReportDiagnostic(diagnostic);
31:
}
32:
}
33: }
70

71.
[beta]
await が付与されているかを調べる
1: [DiagnosticAnalyzer(LanguageNames.CSharp)]
2: public class AwaitAnalyzer : DiagnosticAnalyzer
3: {
…
22:
private void ReportNonAwaitedMethod(OperationAnalysisContext context)
23:
{
24:
var invocation = (IInvocationOperation)context.Operation;
25:
if (invocation.TargetMethod.ReturnType.Name == "UniTask")
&& invocation.Syntax.Parent is not AwaitExpressionSyntax)
+ 26:
27:
{
28:
var diagnostic = Diagnostic.Create(awaitRule,
構文木をチェック
29:
context.Operation.Syntax.GetLocation());
30:
context.ReportDiagnostic(diagnostic);
31:
}
32:
}
33: }
71

72.

構文木をチェック MethodDeclaration IfStatement async UniTask Show(bool first) { if (first) { await UniTask.Run( () => {/*...*/}); } else { DoTask1Async(); } } Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 72

73.

構文木をチェック MethodDeclaration IfStatement async UniTask Show(bool first) { if (first) Condition { await UniTask.Run( () => {/*...*/}); } Identifier Invocation の else { 親が Await DoTask1Async(); } } 😊 Statement Else AwaitExpression Invocation Expression Invocation Expression 73

74.

構文木をチェック MethodDeclaration IfStatement async UniTask Show(bool first) { if (first) { await UniTask.Run( () => {/*...*/}); } else { DoTask1Async(); } } Condition Statement Invocation の親が 😡 Identifier Else Await じゃない AwaitExpression Invocation Expression Invocation Expression 74

75.

できた! 75

76.

ここまでのまとめ ● UniTask の await 忘れを防ぐために静的解析ツールを作るぞ ○ 仕様: 返り値の型が UniTask であるメソッドのメソッド呼び出しに await が付与されていなければ、その箇所を出力する ● Roslyn と Roslyn アナライザーを使った構文解析で実装できた ○ ちなみに、実際に開発したツールの初期リリース版は 146 sloc でし た 76

77.

改善点がある! 77

78.

こんな await の仕方もある async UniTask Show() { var task = DoTask1Async(); // 処理いろいろ await task; } メソッドの返り値を変数に代入し、後で その変数を await する 78

79.

ツールの解析結果はどうなる? MethodDeclaration async UniTask Show() { var task = DoTask1Async(); // 処理いろいろ await task; } Variable Declaration Identifier Invocation Expression Await Expression Identifier 79

80.

ツールの解析結果はどうなる? MethodDeclaration async UniTask Show() { var task = DoTask1Async(); // 処理いろいろ await task; } Variable Declaration Identifier 😡 Invocation Expression Await Expression Identifier Invocation の親が Await じゃない 80

81.

誤検知になってしまう async UniTask Show() { var task = DoTask1Async(); // 処理いろいろ await task; } 😑 ここで await してるのに UniTask を返すのに await が付いていない! 😡 実際には問題がないのにツールは問 題として検知してしまう誤検知(偽陽 性)が発生する 81

82.

偽陽性を解消したい 82

83.

しかし変数を使う 記述パターンはさまざま 83

84.

こんなケースも async UniTask Show(bool b) { var task = DoTask1Async(); if (b) { 検知すべき await task; } } async UniTask Show(bool b) { if (b) { var task = DoTask1Async(); await task; } 問題なし } if の条件が false の場合に await されない 84

85.

こんなケースも async UniTask MultiTask(bool b1) UniTask 型の変数を List に追加し { て、後でまとめて await する var list = new List<UniTask>(); var task1 = DoTask1Async(); 問題なし if (b1) { var task2 = DoTask1Async(); 問題なし list.Add(task2); } list.Add(task1); await list; } 85

86.

つらい😇 86

87.

なぜつらいのか? 文の間の依存関係(文A が文B に影響を与える)を解析しなければならない 構文解析では困難 87

88.

フロー解析しよう! 88

89.

簡単に言えば フロー解析とは ・プログラムの実行経路を洗い出し ・変数の定義と参照の関係を調べること 89

90.

身近なフロー解析の例 void NotNull(bool b) { string? s; // null 許容 if (b) { s = "abc"; } else { s = "x"; } Console.WriteLine(s.Length); } void MayBeNull(bool b) { string? s; // null 許容 if (b) { s = "abc"; } else { s = null; } Console.WriteLine(s.Length); } 90

91.

身近なフロー解析の例 void NotNull(bool b) { string? s; // null 許容 if (b) { s = "abc"; } else { s = "x"; 何も警告されない } Console.WriteLine(s.Length); } void MayBeNull(bool b) { string? s; // null 許容 if (b) { s = "abc"; } else { s = null; ぬるぽ起きるかもよ? } Console.WriteLine(s.Length); } 😏 91

92.

実際どうやって フロー解析するの? 92

93.

フロー解析を(イチから)やるには 構文木 意味解析 中間コード データフロー 解析 制御フロー 解析 制御フローグ ラフ データ 依存関係 93

94.

フロー解析を(イチから)やるには 構文木 意味解析 中間コード データフロー 解析 制御フロー 解析 制御フローグ ラフ データ 依存関係 大変そう😨 94

95.

Roslyn を使おう! 95

96.

Roslyn を使う Roslyn がやってくれる😄 構文木 意味解析 中間コード データフロー 解析 制御フロー 解析 制御フローグ ラフ データ 依存関係 96

97.

Roslyn の前に フロー解析の理論を簡単に見ていこう 97

98.

フロー解析は大きく 2種類 ● 制御フロー解析 ○ プログラムの文の実行順序、分岐、合流を解析し、それらを有向グラ フで表現する ● データフロー解析 ○ どこの代入文で定義された値がどこで参照されるかを解析する 98

99.

制御フロー解析 プログラムの文の実行順序、分岐、合流を解析し、 それらを有向グラフで表現する 99

100.

制御フロー解析 左のコードから async UniTask MultiTask(bool b1) { var list = new List<UniTask>(); var task1 = DoTask1Async(); if (b1) { var task2 = DoTask1Async(); list.Add(task2); } list.Add(task1); await list; } 100

101.

制御フロー解析 左のコードから右のグラフを作る async UniTask MultiTask(bool b1) { var list = new List<UniTask>(); var task1 = DoTask1Async(); if (b1) { var task2 = DoTask1Async(); list.Add(task2); } list.Add(task1); await list; } 入口 list = new List<UniTask>() task1 = DoTask1Async() if (b1) task2 = DoTask1Async() list.Add(task2) list.Add(task1) await list 出口 101

102.

制御フロー解析 左のコードから右のグラフを作る async UniTask MultiTask(bool b1) { var list = new List<UniTask>(); var task1 = DoTask1Async(); if (b1) { var task2 = DoTask1Async(); list.Add(task2); } list.Add(task1); await list; } 入口 list = new List<UniTask>() task1 = DoTask1Async() if (b1) task2 = DoTask1Async() list.Add(task2) list.Add(task1) await list 出口 102

103.

制御フローグラフ 分岐や合流を辺 で表した有向グ ラフ 入口 list = new List<UniTask>() task1 = DoTask1Async() if (b1) 各ノードは、その途中に分岐も合流 もない文の列 (逆に言えば、分岐や合流がある箇 所で区切られている) task2 = DoTask1Async() list.Add(task2) list.Add(task1) await list 出口 103

104.

制御フローグラフ 実行順序を解析できる ● 各ノードの中は上から下の順で実 入口 list = new List<UniTask>() task1 = DoTask1Async() if (b1) 行される ● ノード間は有向グラフにおける「接 続元 → 接続先」の順で実行される 実行経路を解析できる task2 = DoTask1Async() list.Add(task2) list.Add(task1) await list ● グラフ上の経路を数え上げる 出口 104

105.

Roslyn には 制御フローグラフを 取得する API がある! 105

106.
[beta]
Roslyn の制御フローグラフ
1: [DiagnosticAnalyzer(LanguageNames.CSharp)]
2: public class AwaitAnalyzer : DiagnosticAnalyzer
3: {
…

先ほどのツールの実装

22:
private void ReportNonAwaitedMethod(OperationAnalysisContext context)
23:
{
24:
var invocation = (IInvocationOperation)context.Operation;
25:
if (invocation.TargetMethod.ReturnType.Name == "UniTask")
26:
&& invocation.Syntax.Parent is not AwaitExpressionSyntax)
27:
{
28:
var diagnostic = Diagnostic.Create(awaitRule,
29:
context.Operation.Syntax.GetLocation());
30:
context.ReportDiagnostic(diagnostic);
31:
}
32:
}
33: }
106

107.
[beta]
Roslyn の制御フローグラフ
1: [DiagnosticAnalyzer(LanguageNames.CSharp)]
2: public class AwaitAnalyzer : DiagnosticAnalyzer
3: {
…
22:
private void ReportNonAwaitedMethod(OperationAnalysisContext context)
23:
{
var cfg = context.GetControlFlowGraph();
+ 24:
25:
var invocation = (IInvocationOperation)context.Operation;
26:
if (invocation.TargetMethod.ReturnType.Name == "UniTask")
27:
&& invocation.Syntax.Parent is not AwaitExpressionSyntax)
28:
{
29:
var diagnostic = Diagnostic.Create(awaitRule,
30:
context.Operation.Syntax.GetLocation());
詳細はこちら
31:
context.ReportDiagnostic(diagnostic);
32:
}
https://github.com/dotnet/roslyn/blob/main/src/Compilers/Core/Portabl
33:
}
e/Operations/ControlFlowGraph.cs
34: }

107

108.

データフロー解析 どこの代入文で定義された値がどこで参照されるかを解析する 108

109.

データフロー解析 async UniTask MultiTask(bool b1) { var list = new List<UniTask>(); var task1 = DoTask1Async(); if (b1) { var task2 = DoTask1Async(); list.Add(task2); } list.Add(task1); await list; } 入口 list = new List<UniTask>() task1 = DoTask1Async() if (b1) task2 = DoTask1Async() list.Add(task2) list.Add(task1) await list 出口 109

110.

データフロー解析 各文に ID を振る async UniTask MultiTask(bool b1) { s1 var list = new List<UniTask>(); s2 var task1 = DoTask1Async(); s3 if (b1) { s4 var task2 = DoTask1Async(); s5 list.Add(task2); } s6 list.Add(task1); s7 await list; } 入口 list = new List<UniTask>() task1 = DoTask1Async() if (b1) task2 = DoTask1Async() list.Add(task2) list.Add(task1) await list 出口 110

111.

データフロー解析 各文に ID を振る async UniTask MultiTask(bool b1) { s1 var list = new List<UniTask>(); s2 var task1 = DoTask1Async(); s3 if (b1) { s4 var task2 = DoTask1Async(); s5 list.Add(task2); } s6 list.Add(task1); s7 await list; } 入口 s1 list = new List<UniTask>() s2 task1 = DoTask1Async() s3 if (b1) s4 task2 = DoTask1Async() s5 list.Add(task2) s6 list.Add(task1) s7 await list 出口 111

112.

データフロー解析 変数の定義元と参照先の関係を 制御フローグラフを使って計算す る 入口 s1 list = new List<UniTask>() s2 task1 = DoTask1Async() s3 if (b1) s4 task2 = DoTask1Async() s5 list.Add(task2) s6 list.Add(task1) s7 await list 出口 112

113.

データフロー解析 変数の定義元と参照先の関係を 制御フローグラフを使って計算す る ( 定義元, 参照先, 変数名 ) の形 式で書くと ● (s1, s5, list) ● (s1, s6, list) ● (s1, s7, list) ● (s2, s6, task1) ● (s4, s5, task2) 入口 s1 list = new List<UniTask>() s2 task1 = DoTask1Async() s3 if (b1) s4 task2 = DoTask1Async() s5 list.Add(task2) s6 list.Add(task1) s7 await list 出口 113

114.

データフロー解析 ラベル付き有向グラフで書くと s1 list task2 s4 s2 s1 list = new List<UniTask>() s2 task1 = DoTask1Async() s3 if (b1) s5 list s6 task1 入口 list s7 s4 task2 = DoTask1Async() s5 list.Add(task2) s6 list.Add(task1) s7 await list 出口 114

115.

Roslyn には データフローを 取得する API がある! 115

116.
[beta]
Roslyn のデータフロー解析
1: [DiagnosticAnalyzer(LanguageNames.CSharp)]
2: public class AwaitAnalyzer : DiagnosticAnalyzer
3: {
…

先ほどのツールの実装

22:
private void ReportNonAwaitedMethod(OperationAnalysisContext context)
23:
{
24:
var invocation = (IInvocationOperation)context.Operation;
25:
if (invocation.TargetMethod.ReturnType.Name == "UniTask")
26:
&& invocation.Syntax.Parent is not AwaitExpressionSyntax)
27:
{
28:
var diagnostic = Diagnostic.Create(awaitRule,
29:
context.Operation.Syntax.GetLocation());
30:
context.ReportDiagnostic(diagnostic);
31:
}
32:
}
33: }
116

117.
[beta]
Roslyn のデータフロー解析
1: [DiagnosticAnalyzer(LanguageNames.CSharp)]
2: public class AwaitAnalyzer : DiagnosticAnalyzer
3: {
…
22:
private void ReportNonAwaitedMethod(OperationAnalysisContext context)
23:
{
24:
var invocation = (IInvocationOperation)context.Operation;
var dataflow = invocation.SemanticModel.AnalyzeDataFlow(invocation.Syntax);
+ 25:
26:
if (invocation.TargetMethod.ReturnType.Name == "UniTask")
27:
&& invocation.Syntax.Parent is not AwaitExpressionSyntax)
28:
{
29:
var diagnostic = Diagnostic.Create(awaitRule,
30:
context.Operation.Syntax.GetLocation());
より高度なデータフロー解析もあります
31:
context.ReportDiagnostic(diagnostic);
32:
}
https://github.com/dotnet/roslyn-analyzers/blob/main/docs/Writing%20d
33:
}
ataflow%20analysis%20based%20analyzers.md
117
34: }

118.

偽陽性の問題に戻って async UniTask Show() { var task = DoTask1Async(); // 処理いろいろ await task; } 検知しないようにする 118

119.

偽陽性の問題に戻って async UniTask MultiTask(bool b1) { var list = new List<UniTask>(); var task1 = DoTask1Async(); 検知しないようにする if (b1) { var task2 = DoTask1Async(); 検知しないようにする list.Add(task2); } list.Add(task1); await list; } 119

120.

検知ロジックを変える 返り値の型が UniTask 1 であるメソッドのメソッド呼び出しに await が付与 2 されていなければ、その箇所を出力する 120

121.

検知ロジックを変える 返り値の型が UniTask 1 であるメソッドのメソッド呼び出し M に await が付 与 2 されておらず、かつ以下のいずれの条件も満たさないならば、その箇所を 出力する ● 条件A: M の返り値が代入される変数の参照先で await が付与されてい る ● 条件B: M の返り値が代入される変数の参照先で UniTask の List に Add されており、その List の参照先で await が付与されている 121

122.

条件A のケース ● 条件A: M の返り値が代入される変数の参照先で await が付与さ れている async UniTask Show() { s1 var task = DoTask1Async(); // 処理いろいろ s2 await task; } 検知するか? s1 task s2 122

123.

条件A のケース ● 条件A: M の返り値が代入される変数の参照先で await が付与さ れている async UniTask Show() { s1 var task = DoTask1Async(); // 処理いろいろ s2 await task; } 条件A にマッチ s1 task s2 123

124.

条件B のケース async UniTask MultiTask(bool b1) { s1 var list = new List<UniTask>(); s2 var task1 = DoTask1Async(); s3 if (b1) { s4 var task2 = DoTask1Async(); s5 list.Add(task2); } s6 list.Add(task1); s7 await list; } s1 list task2 s4 list s6 task1 s2 s5 list s7 124

125.

条件B のケース async UniTask MultiTask(bool b1) { 検知するか? s1 var list = new List<UniTask>(); s2 var task1 = DoTask1Async(); s3 if (b1) { s4 var task2 = DoTask1Async(); s5 list.Add(task2); } s6 list.Add(task1); s7 await list; } s1 list task2 s4 list s6 task1 s2 s5 list s7 125

126.
[beta]
条件B のケース
●

条件B: M の返り値が代入される変数の参照先で UniTask の List に Add さ
れており、その List の参照先で await が付与されている

async UniTask MultiTask(bool b1)
{
s1 var list = new List<UniTask>();
s2 var task1 = DoTask1Async();
s3 if (b1)
{
s4 var task2 = DoTask1Async();
s5 list.Add(task2);
}
s6 list.Add(task1);
s7 await list;
}

s1

list

task2
s4

list
s6

task1
s2

s5

list
s7
126

127.
[beta]
条件B のケース
●

条件B: M の返り値が代入される変数の参照先で UniTask の List に Add さ
れており、その List の参照先で await が付与されている

async UniTask MultiTask(bool b1)
{
s1 var list = new List<UniTask>();
s2 var task1 = DoTask1Async();
s3 if (b1)
{
s4 var task2 = DoTask1Async();
s5 list.Add(task2);
}
s6 list.Add(task1);
s7 await list;
s2 から辺をたどる
}

s1

list

task2
s4

list
s6

task1
s2

s5

list
s7
127

128.
[beta]
条件B のケース
●

条件B: M の返り値が代入される変数の参照先で UniTask の List に Add さ
れており、その List の参照先で await が付与されている

async UniTask MultiTask(bool b1)
{
s1 var list = new List<UniTask>();
s2 var task1 = DoTask1Async();
s3 if (b1)
{
変数の型は
Roslyn
の
var task2
= DoTask1Async();
s4
API で分かる
s5 list.Add(task2);
}
s6 list.Add(task1);
s7 await list;
}

s1

list

task2
s4

list
s6

task1
s2

s5

list
s7
128

129.
[beta]
条件B のケース
●

条件B: M の返り値が代入される変数の参照先で UniTask の List に Add さ
れており、その List の参照先で await が付与されている

async UniTask MultiTask(bool b1)
{
s1 var list = new List<UniTask>();
s2 var task1 = DoTask1Async();
s3 if (b1)
{
s4 var task2 = DoTask1Async();
s5 list.Add(task2);
}
s6 list.Add(task1);
s7 await list;
}

s1

list

task2
s4

list
s6

task1
s2

s5

list
s7
129

130.
[beta]
条件B のケース
●

条件B: M の返り値が代入される変数の参照先で UniTask の List に Add さ
れており、その List の参照先で await が付与されている

async UniTask MultiTask(bool b1)
{
s1 var list = new List<UniTask>();
s2 var task1 = DoTask1Async();
list
s3 if (b1)
s1
{
s4 var task2 = DoTask1Async();
task2
s5 list.Add(task2);
s4
list 辺の先が参照先
}
s6 list.Add(task1);
task1
s7 await list;
s2
}

s5

list
s6

list
s7
130

131.
[beta]
条件B のケース
●

条件B: M の返り値が代入される変数の参照先で UniTask の List に Add さ
れており、その List の参照先で await が付与されている

async UniTask MultiTask(bool b1)
{
s1 var list = new List<UniTask>();
s2 var task1 = DoTask1Async();
s3 if (b1)
{
s4 var task2 = DoTask1Async();
s5 list.Add(task2);
}
s6 list.Add(task1);
s7 await list;
}

s1

list

task2
s4

s5

list
s6

task1
await
s2 している

list
s7
131

132.
[beta]
条件B のケース
●

条件B: M の返り値が代入される変数の参照先で UniTask の List に Add さ
れており、その List の参照先で await が付与されている

async UniTask MultiTask(bool b1)
{
条件B にマッチ
s1 var list = new List<UniTask>();
s2 var task1 = DoTask1Async();
s3 if (b1)
{
s4 var task2 = DoTask1Async();
s5 list.Add(task2);
}
s6 list.Add(task1);
s7 await list;
}

s1

list

task2
s4

list
s6

task1
s2

s5

list
s7
132

133.

もう一工夫 133

134.

偽陰性を防ぐ 検知すべき問題が検知されない(偽陰性が発生している) async UniTask Show(bool b) { var task = DoTask1Async(); if (b) { await task; } } 検知すべき 134

135.

偽陰性を防ぐ ● 条件A: M の返り値が代入される変数の参照先で await が付与さ れている async UniTask Show(bool b) { s1 var task = DoTask1Async(); s2 if (b) { s3 await task; } } 条件A にマッチするので検知されない s1 task s3 135

136.

偽陰性を防ぐ 両者を区別するには? async UniTask Show(bool b) { var task = DoTask1Async(); if (b) { 検知すべき await task; } } async UniTask Show(bool b) { if (b) { var task = DoTask1Async(); await task; } 問題なし } 136

137.

後支配 137

138.

後支配 文s から出口に到達するまでに文t を必 入口 ず通らなければいけないとき、t は s を後 支配するという async UniTask Show(bool b) { var task = DoTask1Async(); if (b) { await task; } } task = DoTask1Async() if (b) await task 出口 138

139.

後支配 文s から出口に到達するまでに文t を必 入口 ず通らなければいけないとき、t は s を後 支配するという 「await task」は task = DoTask1Async() if (b) 「task = DoTask1Async()」を 後支配しない await task 出口 139

140.

後支配 文s から出口に到達するまでに文t を必 入口 ず通らなければいけないとき、t は s を後 支配するという if (b) async UniTask Show(bool b) { if (b) { var task = DoTask1Async(); await task; } } task = DoTask1Async() await task 出口 140

141.

後支配 文s から出口に到達するまでに文t を必 入口 ず通らなければいけないとき、t は s を後 支配するという if (b) 「await task」は 「task = DoTask1Async()」を 後支配する task = DoTask1Async() await task 出口 141

142.

偽陰性を防ぐ 後支配するかどうかで区別する 後支配しない 後支配する async UniTask Show(bool b) { var task = DoTask1Async(); if (b) { 検知すべき await task; } } async UniTask Show(bool b) { if (b) { var task = DoTask1Async(); await task; } 問題なし } 142

143.

検知ロジックに条件を追加する 返り値の型が UniTask 1 であるメソッドのメソッド呼び出し M に await が付 与 2 されておらず、かつ以下のいずれの条件も満たさないならば、その箇所を 出力する ● 条件A: M の返り値が代入される変数の参照先で await が付与されてい る ● 条件B: M の返り値が代入される変数の参照先で UniTask の List に Add されており、その List の参照先で await が付与されている 143

144.

検知ロジックに条件を追加する 返り値の型が UniTask 1 であるメソッドのメソッド呼び出し M に await が付 与 2 されておらず、かつ以下のいずれの条件も満たさないならば、その箇所を 出力する ● 条件A: M の返り値が代入される変数の参照先で await が付与されてお り、その await 式が変数の定義元を後支配している ● 条件B: M の返り値が代入される変数の参照先で UniTask の List に Add されており、その List の参照先で await が付与されており、その await 式が List の定義元と List への Add を後支配している 144

145.

偽陰性が解消された フロー解析によってより正確に判定できる async UniTask Show(bool b) { var task = DoTask1Async(); if (b) { 検知される await task; } } 😡 async UniTask Show(bool b) { if (b) 検知されない { var task = DoTask1Async(); await task; } } 😊 145

146.

こんなケースでも async UniTask MultiTask(bool b1) { var list = new List<UniTask>(); var task1 = DoTask1Async(); if (b1) { var task2 = DoTask1Async(); list.Add(task2); } await list; list.Add(task1); } 146

147.

こんなケースでも async UniTask MultiTask(bool b1) { var list = new List<UniTask>(); var task1 = DoTask1Async(); if (b1) { var task2 = DoTask1Async(); list.Add(task2); } await list; 順序を間違えている list.Add(task1); } 147

148.

こんなケースでも async UniTask MultiTask(bool b1) { var list = new List<UniTask>(); var task1 = DoTask1Async(); if (b1) { var task2 = DoTask1Async(); list.Add(task2); } await list; list.Add(task1); } 入口 list = new List<UniTask>() task1 = DoTask1Async() if (b1) task2 = DoTask1Async() list.Add(task2) await list list.Add(task1) 出口 148

149.

こんなケースでも async UniTask MultiTask(bool b1) { var list = new List<UniTask>(); var task1 = DoTask1Async(); if (b1) { var task2 = DoTask1Async(); await list が list.Add(task2); list.Add(task1)を } 後支配していない await list; list.Add(task1); } 入口 list = new List<UniTask>() task1 = DoTask1Async() if (b1) task2 = DoTask1Async() list.Add(task2) await list list.Add(task1) 出口 149

150.

こんなケースでも 入口 async UniTask MultiTask(bool b1) { var list = new List<UniTask>(); var task1 = DoTask1Async(); if (b1) { 検知される var task2 = DoTask1Async(); list.Add(task2); } await list; list.Add(task1); } list = new List<UniTask>() task1 = DoTask1Async() if (b1) task2 = DoTask1Async() list.Add(task2) 😡 await list list.Add(task1) 出口 150

151.

前半部分のまとめ ● 不具合を早く見つけるには静的解析ツールが有効 ● 静的解析ツール作成に役立つ概念(構文解析、フロー解析)を紹介 ● Roslyn と Roslyn アナライザーが便利! 151

152.

本発表のゴール ● 静的解析ツールの役割と仕組みを知る ● C# 向けの静的解析ツールを自作できる環境を知る ●Inagakiが話す領域 静的解析ツールを作りたくなる ● RoslynアナライザーをUnityプロジェクトに導入する方法を知る ● Roslynアナライザーの解析結果を活かす方法を知る 152

153.

Kazuma Inagaki 株式会社ディー・エヌ・エー IT本部品質管理部SWET第二グループ 令和にVimで就活をした2021年新卒 静的解析を起点としたゲーム領域の品質向上 @get-me-power @get_me_power 153

154.

本発表のゴール ● 静的解析ツールの役割と仕組みを知る ● C# 向けの静的解析ツールを自作できる環境を知る ● 静的解析ツールを作りたくなる ● RoslynアナライザーをUnityプロジェクトに導入する方法を知る ● Roslynアナライザーの解析結果を活かす方法を知る 154

155.

Roslynアナライザーを Unityプロジェクトに導入するまでの流れ アナライザー作成 社内向けにアナライザーを配 布する アナライザーをインストール アナライザーの設定 155

156.

Roslynアナライザーを Unityプロジェクトに導入するまでの流れ アナライザー作成 社内向けにアナライザーを配 布する アナライザーをインストール アナライザーの設定 156

157.

UnityにRoslynアナライザーを組み込む方法 ● nupkgとして組み込む ● dllを直接Unityプロジェクトに配置する ● UPMパッケージとして組み込む 157

158.

UnityにRoslynアナライザーを組み込む方法 ● nupkgとして組み込む Unityでは動かない ● dllを直接Unityプロジェクトに配置する アナライザーの更新が めんどくさい ● UPMパッケージとして組み込む 158

159.

UnityにRoslynアナライザーを組み込む方法 ● nupkgとして組み込む Unityでは動かない ● dllを直接Unityプロジェクトに配置する アナライザーの更新が めんどくさい UPMパッケージとして配布しよう ● UPMパッケージとして組み込む 159

160.

UPMパッケージとは Unity Package Managerを通してUnityにインストールできる 拡張機能のこと see also: https://docs.unity3d.com/ja/2021.3/Manual/CustomPackages.html 160

161.

UPMパッケージの構成 com.your.unity-analyzer/ ├── AssemblyInfo.cs ├── AssemblyInfo.cs.meta ├── YourAnalyzer.asmdef ├── YourAnalyzer.asmdef.meta ├── Analyzer.dll ├── Analyzer.dll.meta ├── package.json └── package.json.meta 161

162.

UPMパッケージの構成 com.your.unity-analyzer/ C#ファイルが存在しないUPMパッケージをUnityに ├── AssemblyInfo.cs 取り込むとwarningが出る ├── AssemblyInfo.cs.meta ├── YourAnalyzer.asmdef ├── YourAnalyzer.asmdef.meta ├── Analyzer.dll ├── Analyzer.dll.meta ├── package.json └── package.json.meta 162

163.

UPMパッケージの構成 com.your.unity-analyzer/ ├── AssemblyInfo.cs ├── AssemblyInfo.cs.meta UPMパッケージのasmdef ├── YourAnalyzer.asmdef ├── YourAnalyzer.asmdef.meta ├── Analyzer.dll ├── Analyzer.dll.meta ├── package.json └── package.json.meta 163

164.

UPMパッケージの構成 com.your.unity-analyzer/ ├── AssemblyInfo.cs ├── AssemblyInfo.cs.meta ├── YourAnalyzer.asmdef ├── YourAnalyzer.asmdef.meta 配布したいRoslynアナライザーのdll ├── Analyzer.dll ├── Analyzer.dll.meta ├── package.json └── package.json.meta 164

165.

UPMパッケージの構成 com.your.unity-analyzer/ ├── AssemblyInfo.cs ├── AssemblyInfo.cs.meta ├── YourAnalyzer.asmdef ├── YourAnalyzer.asmdef.meta ├── Analyzer.dll ├── Analyzer.dll.meta ├── package.json UPMパッケージのjson └── package.json.meta 165

166.

UPMパッケージの構成 com.your.unity-analyzer/ ├── AssemblyInfo.cs ├── AssemblyInfo.cs.meta ├── YourAnalyzer.asmdef ├── YourAnalyzer.asmdef.meta ├── Analyzer.dll ├── Analyzer.dll.meta ├── package.json └── package.json.meta このファイル構成を社内に配布する 166

167.

UPMパッケージを社内に配布するためのフロー アナライザーのdll を作成 UPMパッケージの構 成を作成 npm publishを実行 167

168.

UPMパッケージを社内に配布するためのフロー アナライザーのdll を作成 UPMパッケージの構 成を作成 npm publishを実行 ● ● ● ● GitHub Packages openupm.com npmjs etc… 168

169.

UPMパッケージを社内に配布するためのフロー CIで自動化 アナライザーのdll を作成 UPMパッケージの構 成を作成 npm publishを実行 ● ● ● ● GitHub Packages openupm.com npmjs etc… 169

170.

Roslynアナライザーを Unityプロジェクトに導入するまでの流れ アナライザー作成 アナライザーをpublish アナライザーをインストール アナライザーの設定 170

171.

一般的な静的解析器のインストール 静的解析器をインストール 静的解析器が実行できる UnityにRoslynアナライザーをインストール Roslynアナライザーをインストール Roslynアナライザーの設定を行う 静的解析器が実行できる 171

172.

一般的な静的解析器のインストール $ npm install $ go install 静的解析器をインストール 静的解析器が実行できる UnityにRoslynアナライザーをインストール Roslynアナライザーをインストール Roslynアナライザーの設定を行う 静的解析器が実行できる 172

173.

一般的な静的解析器のインストール 静的解析器をインストール 静的解析器が実行できる UnityにRoslynアナライザーをインストール Roslynアナライザーをインストール Roslynアナライザーの設定を行う 静的解析器が実行できる 173

174.

一般的な静的解析器のインストール 静的解析器をインストール 静的解析器が実行できる UnityにRoslynアナライザーをインストール !? Roslynアナライザーをインストール Roslynアナライザーの設定を行う 静的解析器が実行できる 174

175.

Roslynアナライザーを Unityプロジェクトに導入するまでの流れ アナライザー作成 アナライザーをpublish アナライザーをインストール アナライザーの有効化 175

176.

アナライザーの有効化 ● アナライザーへの参照をasmdefに加える やらないと動かない ● 不必要な警告が出ないように抑制する 大量のノイズが出る 176

177.

アナライザーの有効化 ● アナライザーへの参照をasmdefに加える やらないと動かない ● 不必要な警告が出ないように抑制する 大量のノイズが出る 177

178.

com.your-analyzer.upm-package/ │ Assets/your.library/ ├── your.library.analyzers.dll ├── your.library.analyzers.dll.meta ├── Editor │ └── YourLibrary.Editor.asmdef ├── Runtime │ ├── YourLibrary.Runtime.asmdef │ └── YourLibraryCode.cs └── Tests └── YourLibrary.Tests.asmdef 178

179.

com.your-analyzer.upm-package/ インストールされた UPMパッケージ │ Assets/your.library/ ├── your.library.analyzers.dll ├── your.library.analyzers.dll.meta ├── Editor │ └── YourLibrary.Editor.asmdef ├── Runtime │ ├── YourLibrary.Runtime.asmdef │ └── YourLibraryCode.cs └── Tests └── YourLibrary.Tests.asmdef 179

180.

com.your-analyzer.upm-package/ インストールされた UPMパッケージ │ Assets/your.library/ ├── your.library.analyzers.dll ├── your.library.analyzers.dll.meta ├── Editor │ └── YourLibrary.Editor.asmdef ├── Runtime │ ├── YourLibrary.Runtime.asmdef asmdefファイルにUPMの参照を加える │ └── YourLibraryCode.cs └── Tests └── YourLibrary.Tests.asmdef 180

181.

com.your-analyzer.upm-package/ インストールされた UPMパッケージ │ Assets/your.library/ ├── your.library.analyzers.dll ├── your.library.analyzers.dll.meta ├── Editor │ └── YourLibrary.Editor.asmdef ├── Runtime │ ├── YourLibrary.Runtime.asmdef この範囲でアナライザーが動作する │ └── YourLibraryCode.cs └── Tests └── YourLibrary.Tests.asmdef 181

182.

com.your-analyzer.upm-package/ インストールされた UPMパッケージ │ Assets/your.library/ ├── your.library.analyzers.dll ├── your.library.analyzers.dll.meta ├── Editor │ └── YourLibrary.Editor.asmdef ├── Runtime │ ├── YourLibrary.Runtime.asmdef この範囲でアナライザーが動作する │ └── YourLibraryCode.cs └── Tests └── YourLibrary.Tests.asmdef 設定したい asmdefの数だけ行う 182

183.

大体3~5つくらいやろ

184.

400以上!? 大体3~5つくらいやろ

185.

400以上!? 大体3~5つくらいやろ submodule

186.

400以上!? 大体3~5つくらいやろ submodule 依存関係の意識

187.

YOU DIED

188.

これは人がやることじゃない

189.

これは人がやることじゃない プログラムにやらせよう

190.

asmdef1 asmdef3 asmdef2 asmdef4 asmdef5 asmdef6

191.

asmdef1 asmdef3 asmdef2 asmdef4 asmdef5 asmdef6

192.

asmdef1 asmdef3 asmdef2 asmdef4 2つのasmdefにアナライザーの 参照を加えれば良い asmdef5 asmdef6

193.

asmdef1 2つのasmdefにアナライザーの 参照を加えれば良い asmdef3 これらを CLIで割り出す asmdef2 asmdef4 asmdef5 asmdef6

194.

DeNAで行っている asmdef設定のCLI化 設定が必要な asmdefを割り出す 割り出したasmdefに アナライザーの参照を追加する path/to/file1.asmdef path/to/file2.asmdef path/to/file3.asmdef path/to/file4.asmdef path/to/file5.asmdef path/to/file6.asmdef Unityプロジェクト CLIが出力した 極小元のasmdefへの path Roslynアナライザーが動作する Unityプロジェクト 194

195.

Roslynアナライザーの設定 ● アナライザーへの参照をasmdefに加える やらないと動かない ● 不必要な警告が出ないように抑制する 大量のノイズが出る 195

196.

アナライザーの診断項目ごとに重大度が存在する ● error ● warning ● suggestion ● silent ● none NOTE: 何も設定しなければアナライザーの診断項目ごとに 定義された重大度レベルが指定される

197.

● 診断項目1: error ● 診断項目2: warning デフォルトで設定された重大度 197

198.

● 診断項目1: error ● 診断項目2: warning 診断項目2をerrorにしたいなぁ デフォルトで設定された重大度 198

199.

重大度設定の方法 ● ruleset Unity ● ● ● ruleset editorconfig IDE独自 JetBrains Rider 199

200.

重大度設定の方法 ● ruleset Unity ● ● ● ruleset editorconfig IDE独自 JetBrains Rider 200

201.

重大度設定の方法 ● ruleset ● ● ● ruleset editorconfig IDE独自 UnityとIDEが共通で使い回せる Unity JetBrains Rider 201

202.

rulesetによる重大度の設定 Assets/ ├── your.library.analyzers.dll ├── your.library.analyzers.dll.meta ├── Hoge.cs Assets直下に ├── Default.ruleset └── Subfolder Default.rulesetを配置する ├── Subfoloder.asmdef └── Fuga.cs <?xml version="1.0" encoding="utf-8"?> <RuleSet Name="New Rule Set" Description=" " ToolsVersion="10.0"> <Rules AnalyzerId="your.library.analyzers" RuleNamespace="your.library.analyzers"> <Rule Id="YourAnalyzersID" Action="Error" /> </Rules> </RuleSet> 202

203.

rulesetによる重大度の設定 Assets/ ├── your.library.analyzers.dll ├── your.library.analyzers.dll.meta ├── Hoge.cs Assets直下に ├── Default.ruleset └── Subfolder Default.rulesetを配置する ├── Subfoloder.asmdef └── Fuga.cs <?xml version="1.0" encoding="utf-8"?> <RuleSet Name="New Rule Set" Description=" " ToolsVersion="10.0"> <Rules AnalyzerId="your.library.analyzers" RuleNamespace="your.library.analyzers"> <Rule Id="YourAnalyzersID" Action="Error" /> </Rules> </RuleSet> アナライザーの DDIDを指定 203

204.

rulesetによる重大度の設定 Assets/ ├── your.library.analyzers.dll ├── your.library.analyzers.dll.meta ├── Hoge.cs Assets直下に ├── Default.ruleset └── Subfolder Default.rulesetを配置する ├── Subfoloder.asmdef └── Fuga.cs <?xml version="1.0" encoding="utf-8"?> <RuleSet Name="New Rule Set" Description=" " ToolsVersion="10.0"> <Rules AnalyzerId="your.library.analyzers" RuleNamespace="your.library.analyzers"> <Rule Id="YourAnalyzersID" Action="Error" /> </Rules> </RuleSet> アナライザーの重大度レベルを指定 204

205.

本発表のゴール ● 静的解析ツールの役割と仕組みを知る ● C# 向けの静的解析ツールを自作できる環境を知る ● 静的解析ツールを作りたくなる ● RoslynアナライザーをUnityプロジェクトに導入する方法を知る ● Roslynアナライザーの解析結果を活かす方法を知る 205

206.

一般的な静的解析の利用方法 206

207.

静的解析の実行 path/to/hoge.cs(374,24): error DENA005: Must Implement all Enum label for switch statement path/to/moge.cs(213,24): error DENA006: Must Implement all Enum label for switch expression ✅ Success ❌ Failed 各種CI エラーレポートが作成される 静的解析の実行 path/to/hoge.cs(374,24): error DENA005: Must Implement all Enum label for switch statement path/to/moge.cs(213,24): error DENA006: Must Implement all Enum label for switch expression 各種エディタ エディタ上に解析結果が出力される 207

208.

静的解析の実行 path/to/hoge.cs(374,24): error DENA005: Must Implement all Enum label for switch statement path/to/moge.cs(213,24): error DENA006: Must Implement all Enum label for switch expression ✅ Success ❌ Failed 各種CI エラーレポートが作成される こんな事ありませんか? 静的解析の実行 path/to/hoge.cs(374,24): error DENA005: Must Implement all Enum label for switch statement path/to/moge.cs(213,24): error DENA006: Must Implement all Enum label for switch expression 各種エディタ エディタ上に解析結果が出力される 208

209.

静的解析のエラーが 放置されている 導入した静的解析器 が無効化される 209

210.

放置される /無効化される要因の一例 ● 静的解析器の結果に偽陽性が多い ● 開発者が重要視していない警告ばかり出る ● 自動生成されたコードに対して静的解析を行っている 210

211.

なぜ放置 /無効化されているのかを 突き止めるのが大事 211

212.

なぜ放置 /無効化されているのかを 突き止めるのが大事 静的解析のデータを蓄積して分析しよう 212

213.

規約違反のデータを蓄積するメリット 規約違反が放置されている 理由を調査できる 蓄積した規約違反をまとめて リストアップできる 蓄積された 規約違反のデータ 規約違反が放置されているこ とに気づける 静的解析器の偽陽性を 疑うきっかけになる 規約違反の推移を グラフで可視化できる 213

214.

規約違反のデータを蓄積するメリット まずは効率よくデータを集 める方法について解説 規約違反が放置されている 理由を調査できる 蓄積した規約違反をまとめて リストアップできる 蓄積された 規約違反のデータ 規約違反が放置されているこ とに気づける 静的解析器の偽陽性を 疑うきっかけになる 規約違反の推移を グラフで可視化できる 214

215.

静的解析の実行 path/to/hoge.cs(374,24): error DENA005: Must Implement all Enum label for switch statement path/to/moge.cs(213,24): error DENA006: Must Implement all Enum label for switch expression ✅ Success ❌ Failed 各種CI エラーレポートが作成される 静的解析の実行 path/to/hoge.cs(374,24): error DENA005: Must Implement all Enum label for switch statement path/to/moge.cs(213,24): error DENA006: Must Implement all Enum label for switch expression 各種エディタ エディタ上に解析結果が出力される 215

216.

静的解析の実行 path/to/hoge.cs(374,24): error DENA005: Must Implement all Enum label for switch statement path/to/moge.cs(213,24): error DENA006: Must Implement all Enum label for switch expression ✅ Success ❌ Failed 各種CI エラーレポートが作成される CI上で作成されたエラーレポートを活かしたい 静的解析の実行 path/to/hoge.cs(374,24): error DENA005: Must Implement all Enum label for switch statement path/to/moge.cs(213,24): error DENA006: Must Implement all Enum label for switch expression 各種エディタ エディタ上に解析結果が出力される 216

217.

静的解析のデータを蓄積するフロー jb inspectcodeを実行 CI上にcheckout されたリポジトリ 解析結果がまとまっ たxmlファイル 必要な観点を抽出 GCPにpublish 管理に必要なデータ クラウドに蓄積 されたデータ 217

218.

静的解析のデータを蓄積するフロー jb inspectcodeを実行 CI上にcheckout されたリポジトリ 解析結果がまとまっ たxmlファイル 必要な観点を抽出 GCPにpublish 管理に必要なデータ クラウドに蓄積 されたデータ 218

219.

jb inspectcodeとは ● Resharperのインスペクションをコマンドラインで完結させるCLI ● 様々な言語に対応 ● 本発表ではRoslynアナライザーの解析結果をCUIに see also: https://www.jetbrains.com/ja-jp/resharper/ https://pleiades.io/help/resharper/Finding_Code_Issues.html 219

220.

実行コマンド $ jb inspectcode --swea --no-build --output="logFile.xml" path/to/yourProject.sln 220

221.
[beta]
出力結果 (xml)
<?xml version="1.0" encoding="utf-8"?>
<!-- Generated by JetBrains Inspect Code 2023.2.2 -->
<Report ToolsVersion="232.0.20230920.171904">
<Information>
<Solution>YourProject.sln</Solution>
<InspectionScope>
<Element>Solution</Element>
</InspectionScope>
</Information>
<IssueTypes>
<IssueType Id="RedundantUsingDirective" Category="Redundancies in Code" CategoryId="CodeRedundancy"
Description="Redundant using directive" Severity="WARNING"
WikiUrl="https://www.jetbrains.com/resharperplatform/help?Keyword=RedundantUsingDirective"/>
<IssueType Id="SWEAFileErrors" Category="Solution-Wide Analysis Errors" CategoryId="SWEAFileErrors"
Description="" Severity="ERROR"/>
</IssueTypes>
<Issues>
<Project Name="YourProject">
<Issue TypeId="SWEAFileErrors" File="YourProject\YourProject.csproj"
Message="Program 'YourProject.dll' does not contain a static 'Main' method suitable for an entry
point"/>
<Issue TypeId="RedundantUsingDirective" File="YourProject\Src\CommandAll.cs" Offset="0-25"
Message="Using directive is not required by the code and can be safely removed"/>
<Issue TypeId="CSharpErrors" File="YourProject\Src\CommandAll.cs" Offset="6-12" Line="1"
Message="Cannot resolve symbol 'System'"/>
</Project>
</Issues>
</Report>

$ jb inspectcode
--swea
--no-build
--telemetry-optout
--output="logFile.xml"
path/to/yourProject.sln

221

222.

静的解析のデータを publishするフロー jb inspectcodeを実行 CI上にcheckout されたリポジトリ 解析結果がまとまっ たxmlファイル 必要な観点を抽出 規約違反のリスト GCPにpublish クラウドにpublish されたデータ 222

223.

静的解析のデータを publishするフロー jb inspectcodeを実行 CI上にcheckout されたリポジトリ 解析結果がまとまっ たxmlファイル 必要な観点を抽出 規約違反のリスト GCPにpublish クラウドにpublish されたデータ 223

224.

管理に必要なデータ ● 警告のID ● 警告の重大度 ● 警告が出たファイルパス ● 規約違反の詳細 ● 該当箇所のGitHubへのリンク ● 最後にその箇所を編集した人、日付 ● ハッシュ 224

225.

管理に必要なデータ ● 警告のID アナライザーによって生成されるコンパイラ エラーや 診断などの特定の診断に関連付けられている文字列 ● 警告の重大度 ● 警告が出たファイルパス ● 規約違反の詳細 ● 該当箇所のGitHubへのリンク ● 最後にその箇所を編集した人、日付 ● ハッシュ 225

226.

管理に必要なデータ ● 警告のID ● 警告の重大度 診断項目毎に設定されている影響の深刻度 ● 警告が出たファイルパス ● 規約違反の詳細 ● 該当箇所のGitHubへのリンク ● 最後にその箇所を編集した人、日付 ● ハッシュ 226

227.

管理に必要なデータ ● 警告のID ● 警告の重大度 ● 警告が出たファイルパス プロジェクトルートから見た ファイルパス ● 規約違反の詳細 ● 該当箇所のGitHubへのリンク ● 最後にその箇所を編集した人、日付 ● ハッシュ 227

228.

管理に必要なデータ ● 警告のID ● 警告の重大度 ● 警告が出たファイルパス ● 規約違反の詳細 エラーメッセージや修正内容 ● 該当箇所のGitHubへのリンク ● 最後にその箇所を編集した人、日付 ● ハッシュ 228

229.

管理に必要なデータ ● 警告のID ● 警告の重大度 ● 警告が出たファイルパス ● 規約違反の詳細 ● 該当箇所のGitHubへのリンク ● 最後にその箇所を編集した人、日付 ● ハッシュ 229

230.

管理に必要なデータ ● 警告のID ● 警告の重大度 ● 警告が出たファイルパス ● 規約違反の詳細 ● 該当箇所のGitHubへのリンク ● 最後にその箇所を編集した人、日付 それぞれのプロパティの説明 ● ハッシュ 230

231.

管理に必要なデータ ● 警告のID ● 警告の重大度 ● 警告が出たファイルパス ● 規約違反の詳細 ● 該当箇所のGitHubへのリンク ● 最後にその箇所を編集した人、日付 ● ハッシュ ● ● ● 規約違反のID プロジェクトルートからの相対パス 規約違反の行の内容 231

232.

なぜハッシュが必要か path/to/file1.cs 1 namespace x 2 { 3 abstract public class clx 4 { 5 6 int i 7 8 public static int Main() 9 { 10 return 0; 11 } 12 } 13 } 232

233.

なぜハッシュが必要か path/to/file1.cs 1 namespace x 2 { 3 abstract public class clx 4 { CS1002 5 6 int i 7 missing semicolon 8 public static int Main() 9 { 10 return 0; 11 } 12 } 13 } 233

234.

なぜハッシュが必要か path/to/file1.cs path/to/file1.cs 1 namespace x 2 { 3 abstract public class clx 4 { CS1002 5 6 int i 7 missing semicolon 8 public static int Main() 9 { 10 return 0; 11 } 12 } 13 } 1 namespace x 2 { 3 abstract public class clx 4 { 5 6 int h; 7 int i 8 9 public static int Main() 10 { 11 return 0; 12 } 13 } 14 } ファイルが書き換わると ... 234

235.

なぜハッシュが必要か path/to/file1.cs 1 namespace x 2 { 3 abstract public class clx 4 { CS1002 5 6 int i 7 missing semicolon 8 public static int Main() 9 { 10 return 0; 11 } 12 } 13 } path/to/file1.cs 1 namespace x 2 { 3 abstract public class clx 4 { 5 6 int h; 7 int i 8 9 public static int Main() 10 { 11 return 0; 12 } 13 } 14 } 規約違反の位置が変わ る 235

236.

なぜハッシュが必要か path/to/file1.cs 1 namespace x 2 { 3 abstract public class clx 4 { CS1002 5 6 int i 7 missing semicolon 8 public static int Main() 9 { 10 return 0; 11 } 12 } 13 } path/to/file1.cs 1 namespace x 2 { 3 abstract public class clx 4 { 5 6 int h; 7 int i 8 9 public static int Main() 10 { 11 return 0; 12 } 13 } 14 } 同じ規約違反なのに別扱いになる 規約違反の位置が変わ る 236

237.

なぜハッシュが必要か 解決策 path/to/file1.cs path/to/file1.cs 1. 以下のプロパティを連結して文字列を作成する 1 namespace x 2 { a. プロジェクトルートからの相対パス 3 abstract public class clx 4 { b. 規約違反のID CS1002 c. 規約違反のコード 5 6 2. 作成した文字列から int i hashを作成する 7 missing semicolon 8 public static int Main() 9 { 10 return 0; 11 } 12 } 13 } 1 namespace x 2 { 3 abstract public class clx 4 { 5 CS1002 6 int h; 7 int i 8 missing semicolon 9 public static int Main() 10 { 11 return 0; 12 } 13 } 14 } 237

238.

なぜハッシュが必要か 解決策 path/to/file1.cs path/to/file1.cs 1. 以下のプロパティを連結して文字列を作成する 1 namespace x 2 { a. プロジェクトルートからの相対パス 3 abstract public class clx 4 { b. 規約違反のID CS1002 c. 規約違反のコード 5 6 2. 作成した文字列から int i hashを作成する 7 missing semicolon 8 public static int Main() 9 { 10 return 0; 11 } 12 } 13 } 1 namespace x 2 { 3 abstract public class clx 4 { 5 CS1002 6 int h; 7 int i 8 missing semicolon 9 public static int Main() 10 { 11 return 0; 12 } 13 } 14 } 238

239.

なぜハッシュが必要か 解決策 path/to/file1.cs path/to/file1.cs 1. 以下のプロパティを連結して文字列を作成する 1 namespace x 2 { a. プロジェクトルートからの相対パス 3 abstract public class clx 4 { b. 規約違反のID CS1002 c. 規約違反のコード 5 6 2. 作成した文字列から int i hashを作成する 7 missing semicolon 8 public static int Main() 9 { 10 return 0; 11 } 12 } 13 } 1 namespace x 2 { 3 abstract public class clx 4 { 5 CS1002 6 int h; 7 int i 8 missing semicolon 9 public static int Main() 10 { 11 return 0; 12 } 13 } 14 } 239

240.

なぜハッシュが必要か 解決策 path/to/file1.cs path/to/file1.cs 1. 以下のプロパティを連結して文字列を作成する 1 namespace x 2 { a. プロジェクトルートからの相対パス 3 abstract public class clx 4 { b. 規約違反のID CS1002 c. 規約違反のコード 5 6 2. 作成した文字列から int i hashを作成する 7 missing semicolon 8 public static int Main() 9 { 10 return 0; 11 } 12 } 13 } 1 namespace x 2 { 3 abstract public class clx 4 { 5 CS1002 6 int h; 7 int i 8 missing semicolon 9 public static int Main() 10 { 11 return 0; 12 } 13 } 14 } CS1002/path/to/file1.cs/int i 240

241.

なぜハッシュが必要か 解決策 path/to/file1.cs path/to/file1.cs 1. 以下のプロパティを連結して文字列を作成する 1 namespace x 2 { a. プロジェクトルートからの相対パス 3 abstract public class clx 4 { b. 規約違反のID CS1002 c. 規約違反のコード 5 6 2. 作成した文字列から int i hashを作成する 7 missing semicolon 8 public static int Main() 9 { 10 return 0; 11 } 12 } 13 } 1 namespace x 2 { 3 abstract public class clx 4 { 5 CS1002 6 int h; 7 int i 8 missing semicolon 9 public static int Main() 10 { 11 return 0; 12 } 13 } 14 } CS1002/path/to/file1.cs/int i 規約違反のID 241

242.

なぜハッシュが必要か 解決策 path/to/file1.cs path/to/file1.cs 1. 以下のプロパティを連結して文字列を作成する 1 namespace x 2 { a. プロジェクトルートからの相対パス 3 abstract public class clx 4 { b. 規約違反のID CS1002 c. 規約違反のコード 5 6 2. 作成した文字列から int i hashを作成する 7 missing semicolon 8 public static int Main() 9 { 10 return 0; 11 } 12 } 13 } 1 namespace x 2 { 3 abstract public class clx 4 { 5 CS1002 6 int h; 7 int i 8 missing semicolon 9 public static int Main() 10 { 11 return 0; 12 } 13 } 14 } CS1002/path/to/file1.cs/int i プロジェクトルート からの相対パス 242

243.

なぜハッシュが必要か 解決策 path/to/file1.cs path/to/file1.cs 1. 以下のプロパティを連結して文字列を作成する 1 namespace x 2 { a. プロジェクトルートからの相対パス 3 abstract public class clx 4 { b. 規約違反のID CS1002 c. 規約違反のコード 5 6 2. 作成した文字列から int i hashを作成する 7 missing semicolon 8 public static int Main() 9 { 10 return 0; 11 } 12 } 13 } 1 namespace x 2 { 3 abstract public class clx 4 { 5 CS1002 6 int h; 7 int i 8 missing semicolon 9 public static int Main() 10 { 11 return 0; 12 } 13 } 14 } CS1002/path/to/file1.cs/int i 規約違反のコード 243

244.

なぜハッシュが必要か 解決策 path/to/file1.cs path/to/file1.cs 1. 以下のプロパティを連結して文字列を作成する 1 namespace x 2 { a. プロジェクトルートからの相対パス 3 abstract public class clx 4 { b. 規約違反のID CS1002 c. 規約違反のコード 5 6 2. 作成した文字列から int i hashを作成する 7 missing semicolon 8 public static int Main() 9 { 10 return 0; 11 } 12 } 13 } 1 namespace x 2 { 3 abstract public class clx 4 { 5 CS1002 6 int h; 7 int i 8 missing semicolon 9 public static int Main() 10 { 11 return 0; 12 } 13 } 14 } CS1002/path/to/file1.cs/int i 244

245.

なぜハッシュが必要か 解決策 path/to/file1.cs path/to/file1.cs 1. 以下のプロパティを連結して文字列を作成する 1 namespace x 2 { a. プロジェクトルートからの相対パス 3 abstract public class clx 4 { b. 規約違反のID CS1002 c. 規約違反のコード 5 6 2. 作成した文字列から int i hashを作成する 7 missing semicolon 8 public static int Main() 9 { 10 return 0; 11 } 12 } 13 } 1 namespace x 2 { 3 abstract public class clx 4 { 5 CS1002 6 int h; 7 int i 8 missing semicolon 9 public static int Main() 10 { 11 return 0; 12 } 13 } 14 } CS1002/path/to/file1.cs/int i c5b8fab4e54dc7e2f138ad8d6bf8f027 245

246.

なぜハッシュが必要か path/to/file1.cs path/to/file1.cs 1 namespace x 2 { 3 abstract public class clx 4 { CS1002 5 6 int i 7 missing semicolon 8 public static int Main() 9 { 10 return 0; 11 } 12 } 13 } 1 namespace x 2 { 3 abstract public class clx 4 { 5 CS1002 6 int h; 7 int i 8 missing semicolon 9 public static int Main() 10 { 11 return 0; 12 } 13 } 14 } c5b8fab4e54dc7e2f138ad8d6bf8f027 246 c5b8fab4e54dc7e2f138ad8d6bf8f027

247.

なぜハッシュが必要か path/to/file1.cs 1 namespace x 2 { 3 abstract public class clx 4 { CS1002 5 6 int i 7 missing semicolon 8 public static int Main() 9 { 10 return 0; 11 } 12 } 13 } path/to/file1.cs 1 namespace x 2 { 3 abstract public class clx 4 { 5 CS1002 6 int h; 7 int i 8 missing semicolon 9 public static int Main() 10 { 11 return 0; 12 } 13 } 14 } 同一の規約違反として扱える! 247

248.

静的解析のデータを publishするフロー jb inspectcodeを実行 CI上にcheckout されたリポジトリ 解析結果がまとまっ たxmlファイル 必要な観点を抽出 規約違反のリスト GCPにpublish クラウドにpublish されたデータ 248

249.

静的解析のデータを publishするフロー CIで自動化 CI上にcheckout されたリポジトリ 解析結果がまとまっ たxmlファイル 規約違反のリスト クラウドにpublish されたデータ 249

250.

規約違反のデータを蓄積するメリット 規約違反が放置されている 理由を調査できる 蓄積した規約違反をまとめて リストアップできる 蓄積された 規約違反のデータ 規約違反が放置されているこ とに気づける 静的解析器の偽陽性を 疑うきっかけになる 規約違反の推移を グラフで可視化できる 250

251.

クラウドに静的解 析のデータを蓄積 蓄積された静的解 析の結果をリスト アップ 251

252.

蓄積した警告を一箇所で管理できる クラウドに静的解 析のデータを蓄積 蓄積された静的解 析の結果をリスト アップ 252

253.

🏆実績 DeNAのあるプロジェクトではバグの起因をリストアップ された結果からの推定で450件ほど検知しました。 クラウドに静的解 析のデータを蓄積 蓄積された静的解 析の結果をリスト アップ これらの2/3ほどはメモリリークに起因する問題で QAでは検証しづらい欠陥です。 253

254.

DeNAで行っている規約違反修正フロー クラウドに静的解 析のデータを蓄積 蓄積された静的解 析の結果をリスト アップ 管理者が修正担当 者をアサイン 修正担当者が 修正可否を判断 修正/抑制 254

255.

DeNAで行っている規約違反修正フロー このフローに従えば警告箇所が 0件になる クラウドに静的解 析のデータを蓄積 シートに静的解析 の結果を表示 管理者が修正担当 者をアサイン 修正担当者が 修正可否を判断 修正/抑制 255

256.

規約違反のデータを蓄積するメリット 規約違反が放置されている 理由を調査できる 蓄積した規約違反をまとめて リストアップできる 蓄積された 規約違反のデータ 規約違反が放置されているこ とに気づける 静的解析器の偽陽性を 疑うきっかけになる 規約違反の推移を グラフで可視化できる 256

257.

可視化された エラーレポート 257

258.

アナライザーの ID 可視化された エラーレポート 258

259.

アナライザー警告推移 可視化された エラーレポート 259

260.

警告の推移が見れる 可視化された エラーレポート 260

261.

ずっと警告が減ってない 可視化された エラーレポート 261

262.

● 修正不要と判断された 規約違反が放置されている ● アナライザーの偽陽性 可視化された エラーレポート 262

263.

修正不要と判断された規約違反が 放置されている アナライザーの偽陽性 警告の推移を基に見直すきっかけができる! 可視化された エラーレポート 263

264.

アナライザーの 抑制を行う 統計データを 見る 傾向をもとに アナライザーの 調査をする 現場のエンジニア にヒアリングする アナライザーの 仕様を見直す 264

265.

アナライザーの 抑制を行う このサイクルを繰り返すことが大事 統計データを 見る 傾向をもとに アナライザーの 調査をする 現場のエンジニア にヒアリングする アナライザーの 仕様を見直す 265

266.

後半部分のまとめ ● 規約違反は簡単に蓄積できる ● 規約違反を蓄積することで静的解析器の偽陽性を調査できる ○ 改良したアナライザーを他のプロジェクトにも適用できる ● 規約違反を蓄積することで規約違反をなくすための施策を考えられる ● 可視化したあとに規約違反を改善するプロセスが大事 266

267.

special fumiko thanks yamamoto https://x.com/ dailykiso