ノートの端の書き残し

UnityやらC#やら。設計が得意かもしれない。

無駄なawaitキーワードを警告するRoslynAnalyzer

結果

説明

awaitが最後に置かれているasyncメソッド、というのはasyncメソッドである必要がありません。 というのも、上記のケースの場合、Task.Delayが完了になったら完了になるTaskがこのメソッド呼び出し時に生成されているのですが、後者のTaskは明らかに不要ですよね。直接Task.Delayの完了を検知するTaskを伝播させればいいわけです。
つまり、上記のメソッドなら以下のように書き直せます。

private static Task AsyncMethod()
{
    Console.WriteLine(10);
    return Task.Delay(1000);
}

アナライザ

例によってテンプレートは以下のブログに倣います。VisualStudioのアナライザテンプレートから生成してもいいと思います。
テンプレからの主な変更部分だけ載せます。

public override void Initialize(AnalysisContext context)
{
    // お約束。
    context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
    context.EnableConcurrentExecution();
        
    context.RegisterSyntaxNodeAction(AnalyzeUnnecessaryAwait, SyntaxKind.MethodDeclaration);
    context.RegisterSyntaxNodeAction(AnalyzeRambdaUnnecessaryAwait, 
        SyntaxKind.ParenthesizedLambdaExpression, SyntaxKind.SimpleLambdaExpression);
}

// メソッド内の不要awaitを検出する
private static void AnalyzeUnnecessaryAwait(SyntaxNodeAnalysisContext context)
{
    // メソッドのみ検査
    if (context.Node is not MethodDeclarationSyntax methodSyntax) return;
    // asyncメソッドのみ検査
    if (!methodSyntax.Modifiers.Any(SyntaxKind.AsyncKeyword)) return;
    if (methodSyntax.Body == null) return;

    var location = AnalyzeInBlockUnnecessaryAwait(methodSyntax.Body);
    if (location == Location.None) return;

    var diagnostic = Diagnostic.Create(_rule, location, "");
    context.ReportDiagnostic(diagnostic);
}
    
// ラムダ式内の不要awaitを検出する
private static void AnalyzeRambdaUnnecessaryAwait(SyntaxNodeAnalysisContext context)
{
    // ラムダ式のみ検査
    if (context.Node is not LambdaExpressionSyntax ramdaSyntax) return;
    // asyncメソッドのみ検査
    if (!ramdaSyntax.Modifiers.Any(SyntaxKind.AsyncKeyword)) return;

    // 式形式の場合、asyncもawaitも要らないはず
    // これメソッドとラムダ式でそもそも別のファイルに分けて記述した方がいいなって書いてて思った
    var block = ramdaSyntax.Block;
    if (block == null && ramdaSyntax.ChildNodes().OfType<AwaitExpressionSyntax>().Any())
    {
        var diagnostic = Diagnostic.Create(_rule, ramdaSyntax.GetLocation(), "");
        context.ReportDiagnostic(diagnostic);
        return;
    }

    if(block == null) return;
    var location = AnalyzeInBlockUnnecessaryAwait(block);
    if (location == Location.None) return;

    var diagnostic2 = Diagnostic.Create(_rule, location, "");
    context.ReportDiagnostic(diagnostic2);
}

// {}ブロック内の不要awaitを検出する
private static Location AnalyzeInBlockUnnecessaryAwait(BlockSyntax blockSyntax)
{
    // ブロック直下の式一覧
    var expressions = blockSyntax
        .ChildNodes()
        .OfType<ExpressionStatementSyntax>().ToArray();

    // 最後の式
    var lastExpression = expressions.Last();

    // awaitが使われてる最初の式
    var firstAwaitExpression = expressions.FirstOrDefault(expression => 
        expression.ChildNodes().OfType<AwaitExpressionSyntax>().Any());

    // awaitが最後の式でしか使われてないなら警告箇所として報告
    return firstAwaitExpression == lastExpression ? lastExpression.GetLocation() : Location.None;
}

アナライザの診断ルール

コメントに書いてるような内容ですが、以下のようなケースで警告を出します。
パッと書けるかなと思ったらラムダ式のこと忘れてて、結構面倒でした。アナライザってこういうこと多いと思います。(ラムダ式を忘れるのは初心者ムーブ極まってますが)
アナライザのコードを書くよりも、その前のルール決めの方が厄介という。

  1. asyncメソッドである。
  2. そのメソッド直下の式一覧のうち、awaitキーワードが最後の式でのみ使用されている。

または、

  1. asyncラムダ式である
  2. ブロックが無いのにawaitが使われている。またはブロック内でawaitキーワードが最後の式でのみ使用されている。

ChildNodesで検索しているので、ローカル関数の有無とかは気にしなくていいはずです。 ちょっと突貫工事なので穴があるかも。

あと、本当にこのawaitが100%無駄か、というのはそこまでちゃんと考えてないです。無駄だとは思うんですけどね。どうでしょう。

使用例:初学者の勉強用

async学びたてとか、あんまり言語仕様の詳細を調べないまま仕事で使う人は割とこの無駄awaitやりがちなので、コードレビューで指摘するのが面倒ならアナライザで教えてあげる方が労力の削減につながるかもしれません。