18.5K Views
August 31, 24
スライド概要
静的解析は、バグの原因となる箇所を早期に発見し、コードの品質を向上させるための有効な手段です。本発表では、C#のコード解析用APIを持つRoslynアナライザーを用いた静的解析の実装方法について解説します。構文解析とフロー解析の具体的な例を示し、DeNAで作成したLinterレベルに留まらない独自のコード検査ルールを紹介します。また、RoslynアナライザーをUnityプロジェクトに導入する方法と、解析結果を効率的に収集・フィードバックする仕組みについて説明します。収集した解析結果をもとに、コードの品質を継続的に改善するためのアクションについても議論します。
DeNA が社会の技術向上に貢献するため、業務で得た知見を積極的に外部に発信する、DeNA 公式のアカウントです。DeNA エンジニアの登壇資料をお届けします。
Roslynアナライザー Unityでの開発環境を改善するための静的解析の仕組みの構築 株式会社ディー・エヌ・エー IT本部品質管理部SWET第二グループ Tomomi Hatano Kazuma Inagaki 1
本発表のゴール ● 静的解析ツールの役割と仕組みを知る ● C# 向けの静的解析ツールを自作できる環境を知る ● 静的解析ツールを作りたくなる ● RoslynアナライザーをUnityプロジェクトに導入する方法を知る ● Roslynアナライザーの解析結果を活かす方法を知る 2
本発表のゴール hatanoが話す領域 ● 静的解析ツールの役割と仕組みを知る ● C# 向けの静的解析ツールを自作できる環境を知る ● 静的解析ツールを作りたくなる ● RoslynアナライザーをUnityプロジェクトに導入する方法を知る ● Roslynアナライザーの解析結果を活かす方法を知る 3
Tomomi Hatano 株式会社ディー・エヌ・エー IT本部品質管理部SWET第二グループ 学生時代から静的解析技術について研究 就職後も静的解析ツールの開発に従事 2023年12月から現職 4
静的解析ツールが必要になる背景 5
不具合の影響 不具合の発見が遅れるほど、その修正コストは高くなる 1 要求 設計 コーティング 開発者テスト ユーザテスト リリース 1 JSTQB https://jstqb.jp/dl/JSTQB-SyllabusFoundation_VersionV40.J01.pdf 6
不具合をできるだけ 早く見つけたい 7
コーディングの不具合 Q. コーティングの不具合を最も早く発見できるタイミングは? 要求 設計 コーティング 開発者テスト ユーザテスト リリース 8
A. コーディングしているとき 9
コーディングしているときに コーディングの不具合を見つけて くれるのが 静的解析ツール 10
静的解析ツールの例 JetBrains Rider の画面 未使用の変数があることを教 えてくれる 11
既存の静的解析ツールで 検知できない問題に直面したら? 12
自分で作ろう! 13
私たちが開発したツールを例に どんなことができるのか どのように実装するのか を説明します 14
お題: 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
お題: 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
お題: 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
お題: 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
お題: 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
await 忘れを検知する 静的解析ツールを作ろう 20
await 忘れ検知の仕様 返り値の型が UniTask 1 であるメソッドのメソッド呼び出しに await が付与 2 されていなければ、その箇所を出力する https://github.com/Cysharp/UniTask 1 正確には Cysharp.Threading.Tasks.UniTask 2 より正確にはメソッド呼び出しの結果に await 演算子が適用 21
どうやって実装する? 文字列検索? でもいろいろ問題がありそう ● メソッド呼び出し箇所をどう見つける? ● たまたまの文字列一致がありうる (コメントにマッチ、など) ● await とメソッド呼び出しの間に改行がある かも async UniTask Show(bool first) { if (first) { await UniTask.Run( () => {/*...*/}); } else { DoTask1Async(); } } 22
構文解析しよう! 23
簡単に言えば 構文解析とは コードを木構造に変換すること 24
構文解析 MethodDeclaration IfStatement async UniTask Show(bool first) { if (first) { await UniTask.Run( () => {/*...*/}); } else { DoTask1Async(); } } Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 25
構文解析 MethodDeclaration まずメソッド宣言があって IfStatement async UniTask Show(bool first) { if (first) { await UniTask.Run( () => {/*...*/}); } else { DoTask1Async(); } } Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 26
構文解析 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
構文解析 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
構文解析 MethodDeclaration 条件式は識別子(変数)で IfStatement async UniTask Show(bool first) { if (first) { await UniTask.Run( () => {/*...*/}); } else { DoTask1Async(); } } Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 29
構文解析 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
構文解析 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
構文解析 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
構文木 MethodDeclaration コードを木構造で表現したもので 構文木と呼ばれる IfStatement Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 33
構文木 MethodDeclaration コードと木構造は相性がいい IfStatement Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 34
構文木 MethodDeclaration コードと木構造は相性がいい IfStatement コードは階層構造を持つ (例) クラス、メソッド、文 文法には再帰的な要素がある (例) if 文の中にさらに if 文 Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 35
構文木 MethodDeclaration コードと木構造は相性がいい そして木構造は解析しやすい コードは階層構造を持つ (例) クラス、メソッド、文 文法には再帰的な要素がある (例) if 文の中にさらに if 文 IfStatement Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 36
実際どうやって 構文木を作るの? 37
構文木を(イチから)作るには ソースコード 字句解析 文法規則 トークン列 構文解析 構文木 38
構文木を(イチから)作るには ソースコード 字句解析 文法規則 ちょっと大変そう😅 トークン列 構文解析 構文木 39
Roslyn を使おう! 40
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
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
Roslyn を使う Roslyn がやってくれる😊 ソースコード 字句解析 文法規則 トークン列 構文解析 構文木 43
検知箇所をエラーや警告として IDE などに表示する API もある! 44
Roslyn アナライザー Roslyn Roslyn アナライザー .NETコンパイラプラットフォー ムの通称 コードの問題を検出する ● C#、VB向けコンパイラ ● コード生成API ● コード解析API プログラム コード解析APIを使ってプ ログラムを自作
実装はこんな感じ 46
実装(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
実装(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
実装(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
実装(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
実装(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
実装(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
実装(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
実装(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
実装(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
実装(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
動かしてみると あらゆるメソッド呼び出しをエラーとして出力する謎ツールができた JetBrains Rider の画面 57
await 忘れ検知の仕様 返り値の型が UniTask 1 であるメソッドのメソッド呼び出し に await が付与 2 されていなければ、その箇所を出力する 「メソッド呼び出しを見つけて それを出力する」はできた! 1 正確には Cysharp.Threading.Tasks.UniTask 2 より正確にはメソッド呼び出しの結果に await 演算子が適用 58
await 忘れ検知の仕様 返り値の型が UniTask 1 であるメソッド のメソッド呼び出し に await が付与 2 されていなければ、その箇所を出力する 残りはどうする? 1 正確には Cysharp.Threading.Tasks.UniTask 2 より正確にはメソッド呼び出しの結果に await 演算子が適用 59
返り値の型が UniTask であるメソッド 60
返り値の型を調べる 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
返り値の型を調べる
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
返り値の型を調べる
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
返り値の型を調べる
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
返り値の型を調べる
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
返り値の型を調べる
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
await 忘れ検知の仕様 返り値の型が UniTask 1 であるメソッド のメソッド呼び出し に await が付与 2 されていなければ、その箇所を出力する 1 正確には Cysharp.Threading.Tasks.UniTask 2 より正確にはメソッド呼び出しの結果に await 演算子が適用 67
await が付与 68
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
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
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
構文木をチェック MethodDeclaration IfStatement async UniTask Show(bool first) { if (first) { await UniTask.Run( () => {/*...*/}); } else { DoTask1Async(); } } Condition Statement Else Identifier AwaitExpression Invocation Expression Invocation Expression 72
構文木をチェック 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
構文木をチェック 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
ここまでのまとめ ● UniTask の await 忘れを防ぐために静的解析ツールを作るぞ ○ 仕様: 返り値の型が UniTask であるメソッドのメソッド呼び出しに await が付与されていなければ、その箇所を出力する ● Roslyn と Roslyn アナライザーを使った構文解析で実装できた ○ ちなみに、実際に開発したツールの初期リリース版は 146 sloc でし た 76
改善点がある! 77
こんな await の仕方もある async UniTask Show() { var task = DoTask1Async(); // 処理いろいろ await task; } メソッドの返り値を変数に代入し、後で その変数を await する 78
ツールの解析結果はどうなる? MethodDeclaration async UniTask Show() { var task = DoTask1Async(); // 処理いろいろ await task; } Variable Declaration Identifier Invocation Expression Await Expression Identifier 79
ツールの解析結果はどうなる? MethodDeclaration async UniTask Show() { var task = DoTask1Async(); // 処理いろいろ await task; } Variable Declaration Identifier 😡 Invocation Expression Await Expression Identifier Invocation の親が Await じゃない 80
誤検知になってしまう async UniTask Show() { var task = DoTask1Async(); // 処理いろいろ await task; } 😑 ここで await してるのに UniTask を返すのに await が付いていない! 😡 実際には問題がないのにツールは問 題として検知してしまう誤検知(偽陽 性)が発生する 81
偽陽性を解消したい 82
しかし変数を使う 記述パターンはさまざま 83
こんなケースも 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
こんなケースも 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
なぜつらいのか? 文の間の依存関係(文A が文B に影響を与える)を解析しなければならない 構文解析では困難 87
フロー解析しよう! 88
簡単に言えば フロー解析とは ・プログラムの実行経路を洗い出し ・変数の定義と参照の関係を調べること 89
身近なフロー解析の例 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
身近なフロー解析の例 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
フロー解析を(イチから)やるには 構文木 意味解析 中間コード データフロー 解析 制御フロー 解析 制御フローグ ラフ データ 依存関係 93
フロー解析を(イチから)やるには 構文木 意味解析 中間コード データフロー 解析 制御フロー 解析 制御フローグ ラフ データ 依存関係 大変そう😨 94
Roslyn を使おう! 95
Roslyn を使う Roslyn がやってくれる😄 構文木 意味解析 中間コード データフロー 解析 制御フロー 解析 制御フローグ ラフ データ 依存関係 96
Roslyn の前に フロー解析の理論を簡単に見ていこう 97
フロー解析は大きく 2種類 ● 制御フロー解析 ○ プログラムの文の実行順序、分岐、合流を解析し、それらを有向グラ フで表現する ● データフロー解析 ○ どこの代入文で定義された値がどこで参照されるかを解析する 98
制御フロー解析 プログラムの文の実行順序、分岐、合流を解析し、 それらを有向グラフで表現する 99
制御フロー解析 左のコードから 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
制御フロー解析 左のコードから右のグラフを作る 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
制御フロー解析 左のコードから右のグラフを作る 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
制御フローグラフ 分岐や合流を辺 で表した有向グ ラフ 入口 list = new List<UniTask>() task1 = DoTask1Async() if (b1) 各ノードは、その途中に分岐も合流 もない文の列 (逆に言えば、分岐や合流がある箇 所で区切られている) task2 = DoTask1Async() list.Add(task2) list.Add(task1) await list 出口 103
制御フローグラフ 実行順序を解析できる ● 各ノードの中は上から下の順で実 入口 list = new List<UniTask>() task1 = DoTask1Async() if (b1) 行される ● ノード間は有向グラフにおける「接 続元 → 接続先」の順で実行される 実行経路を解析できる task2 = DoTask1Async() list.Add(task2) list.Add(task1) await list ● グラフ上の経路を数え上げる 出口 104
Roslyn には 制御フローグラフを 取得する API がある! 105
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
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
データフロー解析 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
データフロー解析 各文に 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
データフロー解析 各文に 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
データフロー解析 変数の定義元と参照先の関係を 制御フローグラフを使って計算す る 入口 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
データフロー解析 変数の定義元と参照先の関係を 制御フローグラフを使って計算す る ( 定義元, 参照先, 変数名 ) の形 式で書くと ● (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
データフロー解析 ラベル付き有向グラフで書くと 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
Roslyn には データフローを 取得する API がある! 115
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
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: }
偽陽性の問題に戻って async UniTask Show() { var task = DoTask1Async(); // 処理いろいろ await task; } 検知しないようにする 118
偽陽性の問題に戻って 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
検知ロジックを変える 返り値の型が UniTask 1 であるメソッドのメソッド呼び出しに await が付与 2 されていなければ、その箇所を出力する 120
検知ロジックを変える 返り値の型が UniTask 1 であるメソッドのメソッド呼び出し M に await が付 与 2 されておらず、かつ以下のいずれの条件も満たさないならば、その箇所を 出力する ● 条件A: M の返り値が代入される変数の参照先で await が付与されてい る ● 条件B: M の返り値が代入される変数の参照先で UniTask の List に Add されており、その List の参照先で await が付与されている 121
条件A のケース ● 条件A: M の返り値が代入される変数の参照先で await が付与さ れている async UniTask Show() { s1 var task = DoTask1Async(); // 処理いろいろ s2 await task; } 検知するか? s1 task s2 122
条件A のケース ● 条件A: M の返り値が代入される変数の参照先で await が付与さ れている async UniTask Show() { s1 var task = DoTask1Async(); // 処理いろいろ s2 await task; } 条件A にマッチ s1 task s2 123
条件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
条件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
条件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
条件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
条件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
条件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
条件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
条件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
条件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
偽陰性を防ぐ 検知すべき問題が検知されない(偽陰性が発生している) async UniTask Show(bool b) { var task = DoTask1Async(); if (b) { await task; } } 検知すべき 134
偽陰性を防ぐ ● 条件A: M の返り値が代入される変数の参照先で await が付与さ れている async UniTask Show(bool b) { s1 var task = DoTask1Async(); s2 if (b) { s3 await task; } } 条件A にマッチするので検知されない s1 task s3 135
偽陰性を防ぐ 両者を区別するには? 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
後支配 文s から出口に到達するまでに文t を必 入口 ず通らなければいけないとき、t は s を後 支配するという async UniTask Show(bool b) { var task = DoTask1Async(); if (b) { await task; } } task = DoTask1Async() if (b) await task 出口 138
後支配 文s から出口に到達するまでに文t を必 入口 ず通らなければいけないとき、t は s を後 支配するという 「await task」は task = DoTask1Async() if (b) 「task = DoTask1Async()」を 後支配しない await task 出口 139
後支配 文s から出口に到達するまでに文t を必 入口 ず通らなければいけないとき、t は s を後 支配するという if (b) async UniTask Show(bool b) { if (b) { var task = DoTask1Async(); await task; } } task = DoTask1Async() await task 出口 140
後支配 文s から出口に到達するまでに文t を必 入口 ず通らなければいけないとき、t は s を後 支配するという if (b) 「await task」は 「task = DoTask1Async()」を 後支配する task = DoTask1Async() await task 出口 141
偽陰性を防ぐ 後支配するかどうかで区別する 後支配しない 後支配する 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
検知ロジックに条件を追加する 返り値の型が UniTask 1 であるメソッドのメソッド呼び出し M に await が付 与 2 されておらず、かつ以下のいずれの条件も満たさないならば、その箇所を 出力する ● 条件A: M の返り値が代入される変数の参照先で await が付与されてい る ● 条件B: M の返り値が代入される変数の参照先で UniTask の List に Add されており、その List の参照先で await が付与されている 143
検知ロジックに条件を追加する 返り値の型が UniTask 1 であるメソッドのメソッド呼び出し M に await が付 与 2 されておらず、かつ以下のいずれの条件も満たさないならば、その箇所を 出力する ● 条件A: M の返り値が代入される変数の参照先で await が付与されてお り、その await 式が変数の定義元を後支配している ● 条件B: M の返り値が代入される変数の参照先で UniTask の List に Add されており、その List の参照先で await が付与されており、その await 式が List の定義元と List への Add を後支配している 144
偽陰性が解消された フロー解析によってより正確に判定できる 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
こんなケースでも 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
こんなケースでも 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
こんなケースでも 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
こんなケースでも 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
こんなケースでも 入口 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
前半部分のまとめ ● 不具合を早く見つけるには静的解析ツールが有効 ● 静的解析ツール作成に役立つ概念(構文解析、フロー解析)を紹介 ● Roslyn と Roslyn アナライザーが便利! 151
本発表のゴール ● 静的解析ツールの役割と仕組みを知る ● C# 向けの静的解析ツールを自作できる環境を知る ●Inagakiが話す領域 静的解析ツールを作りたくなる ● RoslynアナライザーをUnityプロジェクトに導入する方法を知る ● Roslynアナライザーの解析結果を活かす方法を知る 152
Kazuma Inagaki 株式会社ディー・エヌ・エー IT本部品質管理部SWET第二グループ 令和にVimで就活をした2021年新卒 静的解析を起点としたゲーム領域の品質向上 @get-me-power @get_me_power 153
本発表のゴール ● 静的解析ツールの役割と仕組みを知る ● C# 向けの静的解析ツールを自作できる環境を知る ● 静的解析ツールを作りたくなる ● RoslynアナライザーをUnityプロジェクトに導入する方法を知る ● Roslynアナライザーの解析結果を活かす方法を知る 154
Roslynアナライザーを Unityプロジェクトに導入するまでの流れ アナライザー作成 社内向けにアナライザーを配 布する アナライザーをインストール アナライザーの設定 155
Roslynアナライザーを Unityプロジェクトに導入するまでの流れ アナライザー作成 社内向けにアナライザーを配 布する アナライザーをインストール アナライザーの設定 156
UnityにRoslynアナライザーを組み込む方法 ● nupkgとして組み込む ● dllを直接Unityプロジェクトに配置する ● UPMパッケージとして組み込む 157
UnityにRoslynアナライザーを組み込む方法 ● nupkgとして組み込む Unityでは動かない ● dllを直接Unityプロジェクトに配置する アナライザーの更新が めんどくさい ● UPMパッケージとして組み込む 158
UnityにRoslynアナライザーを組み込む方法 ● nupkgとして組み込む Unityでは動かない ● dllを直接Unityプロジェクトに配置する アナライザーの更新が めんどくさい UPMパッケージとして配布しよう ● UPMパッケージとして組み込む 159
UPMパッケージとは Unity Package Managerを通してUnityにインストールできる 拡張機能のこと see also: https://docs.unity3d.com/ja/2021.3/Manual/CustomPackages.html 160
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
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
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
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
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
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
UPMパッケージを社内に配布するためのフロー アナライザーのdll を作成 UPMパッケージの構 成を作成 npm publishを実行 167
UPMパッケージを社内に配布するためのフロー アナライザーのdll を作成 UPMパッケージの構 成を作成 npm publishを実行 ● ● ● ● GitHub Packages openupm.com npmjs etc… 168
UPMパッケージを社内に配布するためのフロー CIで自動化 アナライザーのdll を作成 UPMパッケージの構 成を作成 npm publishを実行 ● ● ● ● GitHub Packages openupm.com npmjs etc… 169
Roslynアナライザーを Unityプロジェクトに導入するまでの流れ アナライザー作成 アナライザーをpublish アナライザーをインストール アナライザーの設定 170
一般的な静的解析器のインストール 静的解析器をインストール 静的解析器が実行できる UnityにRoslynアナライザーをインストール Roslynアナライザーをインストール Roslynアナライザーの設定を行う 静的解析器が実行できる 171
一般的な静的解析器のインストール $ npm install $ go install 静的解析器をインストール 静的解析器が実行できる UnityにRoslynアナライザーをインストール Roslynアナライザーをインストール Roslynアナライザーの設定を行う 静的解析器が実行できる 172
一般的な静的解析器のインストール 静的解析器をインストール 静的解析器が実行できる UnityにRoslynアナライザーをインストール Roslynアナライザーをインストール Roslynアナライザーの設定を行う 静的解析器が実行できる 173
一般的な静的解析器のインストール 静的解析器をインストール 静的解析器が実行できる UnityにRoslynアナライザーをインストール !? Roslynアナライザーをインストール Roslynアナライザーの設定を行う 静的解析器が実行できる 174
Roslynアナライザーを Unityプロジェクトに導入するまでの流れ アナライザー作成 アナライザーをpublish アナライザーをインストール アナライザーの有効化 175
アナライザーの有効化 ● アナライザーへの参照をasmdefに加える やらないと動かない ● 不必要な警告が出ないように抑制する 大量のノイズが出る 176
アナライザーの有効化 ● アナライザーへの参照をasmdefに加える やらないと動かない ● 不必要な警告が出ないように抑制する 大量のノイズが出る 177
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
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
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
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
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
大体3~5つくらいやろ
400以上!? 大体3~5つくらいやろ
400以上!? 大体3~5つくらいやろ submodule
400以上!? 大体3~5つくらいやろ submodule 依存関係の意識
YOU DIED
これは人がやることじゃない
これは人がやることじゃない プログラムにやらせよう
asmdef1 asmdef3 asmdef2 asmdef4 asmdef5 asmdef6
asmdef1 asmdef3 asmdef2 asmdef4 asmdef5 asmdef6
asmdef1 asmdef3 asmdef2 asmdef4 2つのasmdefにアナライザーの 参照を加えれば良い asmdef5 asmdef6
asmdef1 2つのasmdefにアナライザーの 参照を加えれば良い asmdef3 これらを CLIで割り出す asmdef2 asmdef4 asmdef5 asmdef6
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
Roslynアナライザーの設定 ● アナライザーへの参照をasmdefに加える やらないと動かない ● 不必要な警告が出ないように抑制する 大量のノイズが出る 195
アナライザーの診断項目ごとに重大度が存在する ● error ● warning ● suggestion ● silent ● none NOTE: 何も設定しなければアナライザーの診断項目ごとに 定義された重大度レベルが指定される
● 診断項目1: error ● 診断項目2: warning デフォルトで設定された重大度 197
● 診断項目1: error ● 診断項目2: warning 診断項目2をerrorにしたいなぁ デフォルトで設定された重大度 198
重大度設定の方法 ● ruleset Unity ● ● ● ruleset editorconfig IDE独自 JetBrains Rider 199
重大度設定の方法 ● ruleset Unity ● ● ● ruleset editorconfig IDE独自 JetBrains Rider 200
重大度設定の方法 ● ruleset ● ● ● ruleset editorconfig IDE独自 UnityとIDEが共通で使い回せる Unity JetBrains Rider 201
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
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
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
本発表のゴール ● 静的解析ツールの役割と仕組みを知る ● C# 向けの静的解析ツールを自作できる環境を知る ● 静的解析ツールを作りたくなる ● RoslynアナライザーをUnityプロジェクトに導入する方法を知る ● Roslynアナライザーの解析結果を活かす方法を知る 205
一般的な静的解析の利用方法 206
静的解析の実行 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
静的解析の実行 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
放置される /無効化される要因の一例 ● 静的解析器の結果に偽陽性が多い ● 開発者が重要視していない警告ばかり出る ● 自動生成されたコードに対して静的解析を行っている 210
なぜ放置 /無効化されているのかを 突き止めるのが大事 211
なぜ放置 /無効化されているのかを 突き止めるのが大事 静的解析のデータを蓄積して分析しよう 212
規約違反のデータを蓄積するメリット 規約違反が放置されている 理由を調査できる 蓄積した規約違反をまとめて リストアップできる 蓄積された 規約違反のデータ 規約違反が放置されているこ とに気づける 静的解析器の偽陽性を 疑うきっかけになる 規約違反の推移を グラフで可視化できる 213
規約違反のデータを蓄積するメリット まずは効率よくデータを集 める方法について解説 規約違反が放置されている 理由を調査できる 蓄積した規約違反をまとめて リストアップできる 蓄積された 規約違反のデータ 規約違反が放置されているこ とに気づける 静的解析器の偽陽性を 疑うきっかけになる 規約違反の推移を グラフで可視化できる 214
静的解析の実行 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
静的解析の実行 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
静的解析のデータを蓄積するフロー jb inspectcodeを実行 CI上にcheckout されたリポジトリ 解析結果がまとまっ たxmlファイル 必要な観点を抽出 GCPにpublish 管理に必要なデータ クラウドに蓄積 されたデータ 217
静的解析のデータを蓄積するフロー jb inspectcodeを実行 CI上にcheckout されたリポジトリ 解析結果がまとまっ たxmlファイル 必要な観点を抽出 GCPにpublish 管理に必要なデータ クラウドに蓄積 されたデータ 218
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
実行コマンド $ jb inspectcode --swea --no-build --output="logFile.xml" path/to/yourProject.sln 220
出力結果 (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
静的解析のデータを publishするフロー jb inspectcodeを実行 CI上にcheckout されたリポジトリ 解析結果がまとまっ たxmlファイル 必要な観点を抽出 規約違反のリスト GCPにpublish クラウドにpublish されたデータ 222
静的解析のデータを publishするフロー jb inspectcodeを実行 CI上にcheckout されたリポジトリ 解析結果がまとまっ たxmlファイル 必要な観点を抽出 規約違反のリスト GCPにpublish クラウドにpublish されたデータ 223
管理に必要なデータ ● 警告のID ● 警告の重大度 ● 警告が出たファイルパス ● 規約違反の詳細 ● 該当箇所のGitHubへのリンク ● 最後にその箇所を編集した人、日付 ● ハッシュ 224
管理に必要なデータ ● 警告のID アナライザーによって生成されるコンパイラ エラーや 診断などの特定の診断に関連付けられている文字列 ● 警告の重大度 ● 警告が出たファイルパス ● 規約違反の詳細 ● 該当箇所のGitHubへのリンク ● 最後にその箇所を編集した人、日付 ● ハッシュ 225
管理に必要なデータ ● 警告のID ● 警告の重大度 診断項目毎に設定されている影響の深刻度 ● 警告が出たファイルパス ● 規約違反の詳細 ● 該当箇所のGitHubへのリンク ● 最後にその箇所を編集した人、日付 ● ハッシュ 226
管理に必要なデータ ● 警告のID ● 警告の重大度 ● 警告が出たファイルパス プロジェクトルートから見た ファイルパス ● 規約違反の詳細 ● 該当箇所のGitHubへのリンク ● 最後にその箇所を編集した人、日付 ● ハッシュ 227
管理に必要なデータ ● 警告のID ● 警告の重大度 ● 警告が出たファイルパス ● 規約違反の詳細 エラーメッセージや修正内容 ● 該当箇所のGitHubへのリンク ● 最後にその箇所を編集した人、日付 ● ハッシュ 228
管理に必要なデータ ● 警告のID ● 警告の重大度 ● 警告が出たファイルパス ● 規約違反の詳細 ● 該当箇所のGitHubへのリンク ● 最後にその箇所を編集した人、日付 ● ハッシュ 229
管理に必要なデータ ● 警告のID ● 警告の重大度 ● 警告が出たファイルパス ● 規約違反の詳細 ● 該当箇所のGitHubへのリンク ● 最後にその箇所を編集した人、日付 それぞれのプロパティの説明 ● ハッシュ 230
管理に必要なデータ ● 警告のID ● 警告の重大度 ● 警告が出たファイルパス ● 規約違反の詳細 ● 該当箇所のGitHubへのリンク ● 最後にその箇所を編集した人、日付 ● ハッシュ ● ● ● 規約違反のID プロジェクトルートからの相対パス 規約違反の行の内容 231
なぜハッシュが必要か 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
なぜハッシュが必要か 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
なぜハッシュが必要か 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
なぜハッシュが必要か 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
なぜハッシュが必要か 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
なぜハッシュが必要か 解決策 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
なぜハッシュが必要か 解決策 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
なぜハッシュが必要か 解決策 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
なぜハッシュが必要か 解決策 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
なぜハッシュが必要か 解決策 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
なぜハッシュが必要か 解決策 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
なぜハッシュが必要か 解決策 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
なぜハッシュが必要か 解決策 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
なぜハッシュが必要か 解決策 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
なぜハッシュが必要か 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
なぜハッシュが必要か 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
静的解析のデータを publishするフロー jb inspectcodeを実行 CI上にcheckout されたリポジトリ 解析結果がまとまっ たxmlファイル 必要な観点を抽出 規約違反のリスト GCPにpublish クラウドにpublish されたデータ 248
静的解析のデータを publishするフロー CIで自動化 CI上にcheckout されたリポジトリ 解析結果がまとまっ たxmlファイル 規約違反のリスト クラウドにpublish されたデータ 249
規約違反のデータを蓄積するメリット 規約違反が放置されている 理由を調査できる 蓄積した規約違反をまとめて リストアップできる 蓄積された 規約違反のデータ 規約違反が放置されているこ とに気づける 静的解析器の偽陽性を 疑うきっかけになる 規約違反の推移を グラフで可視化できる 250
クラウドに静的解 析のデータを蓄積 蓄積された静的解 析の結果をリスト アップ 251
蓄積した警告を一箇所で管理できる クラウドに静的解 析のデータを蓄積 蓄積された静的解 析の結果をリスト アップ 252
🏆実績 DeNAのあるプロジェクトではバグの起因をリストアップ された結果からの推定で450件ほど検知しました。 クラウドに静的解 析のデータを蓄積 蓄積された静的解 析の結果をリスト アップ これらの2/3ほどはメモリリークに起因する問題で QAでは検証しづらい欠陥です。 253
DeNAで行っている規約違反修正フロー クラウドに静的解 析のデータを蓄積 蓄積された静的解 析の結果をリスト アップ 管理者が修正担当 者をアサイン 修正担当者が 修正可否を判断 修正/抑制 254
DeNAで行っている規約違反修正フロー このフローに従えば警告箇所が 0件になる クラウドに静的解 析のデータを蓄積 シートに静的解析 の結果を表示 管理者が修正担当 者をアサイン 修正担当者が 修正可否を判断 修正/抑制 255
規約違反のデータを蓄積するメリット 規約違反が放置されている 理由を調査できる 蓄積した規約違反をまとめて リストアップできる 蓄積された 規約違反のデータ 規約違反が放置されているこ とに気づける 静的解析器の偽陽性を 疑うきっかけになる 規約違反の推移を グラフで可視化できる 256
可視化された エラーレポート 257
アナライザーの ID 可視化された エラーレポート 258
アナライザー警告推移 可視化された エラーレポート 259
警告の推移が見れる 可視化された エラーレポート 260
ずっと警告が減ってない 可視化された エラーレポート 261
● 修正不要と判断された 規約違反が放置されている ● アナライザーの偽陽性 可視化された エラーレポート 262
修正不要と判断された規約違反が 放置されている アナライザーの偽陽性 警告の推移を基に見直すきっかけができる! 可視化された エラーレポート 263
アナライザーの 抑制を行う 統計データを 見る 傾向をもとに アナライザーの 調査をする 現場のエンジニア にヒアリングする アナライザーの 仕様を見直す 264
アナライザーの 抑制を行う このサイクルを繰り返すことが大事 統計データを 見る 傾向をもとに アナライザーの 調査をする 現場のエンジニア にヒアリングする アナライザーの 仕様を見直す 265
後半部分のまとめ ● 規約違反は簡単に蓄積できる ● 規約違反を蓄積することで静的解析器の偽陽性を調査できる ○ 改良したアナライザーを他のプロジェクトにも適用できる ● 規約違反を蓄積することで規約違反をなくすための施策を考えられる ● 可視化したあとに規約違反を改善するプロセスが大事 266
special fumiko thanks yamamoto https://x.com/ dailykiso