Найти не ожидаемые вызовы метода async

Я просто наткнулся на довольно опасный сценарий при переносе приложения ASP.NET в модель async/await.

Ситуация заключается в том, что я создал метод async: async Task DoWhateverAsync(), изменил объявление в интерфейсе на Task DoWhateverAsync() и выразил надежду, что компилятор скажет мне, где теперь код неправильный, через это предупреждение. Ну, тяжелая удача. Везде, где этот объект вводится через интерфейс, предупреждение не происходит.: - (

Это опасно. Есть ли способ автоматически проверять не ожидаемые методы, возвращающие задачи? Я не против нескольких предупреждений слишком много, но я не хочу пропустить его.

Вот пример:

using System.Threading.Tasks;
namespace AsyncAwaitGames
{
    // In my real case, that method just returns Task.
    public interface ICallee { Task<int> DoSomethingAsync(); }

    public class Callee: ICallee
    {
        public async Task<int> DoSomethingAsync() => await Task.FromResult(0);
    }
    public class Caller
    {
        public void DoCall()
        {
            ICallee xxx = new Callee();

            // In my real case, the method just returns Task,
            // so there is no type mismatch when assigning a result 
            // either.
            xxx.DoSomethingAsync(); // This is where I had hoped for a warning.
        }
    }
}

Ответы

Ответ 1

В конце мы использовали roslyn, чтобы найти все экземпляры, в которых игнорировалось возвращаемое значение Task или Task < > :

if (methodSymbol.ReturnType.Equals(syntaxNodeAnalysisContext.SemanticModel.Compilation.GetTypeByMetadataName(typeof(Task).FullName)))
{
    // For all such symbols, produce a diagnostic.
    var diagnostic = Diagnostic.Create(Rule, node.GetLocation(), methodSymbol.ToDisplayString());

    syntaxNodeAnalysisContext.ReportDiagnostic(diagnostic);
}
if (((INamedTypeSymbol) methodSymbol.ReturnType).IsGenericType && ((INamedTypeSymbol) methodSymbol.ReturnType).BaseType.Equals(syntaxNodeAnalysisContext.SemanticModel.Compilation.GetTypeByMetadataName(typeof(Task).FullName)))
{
    // For all such symbols, produce a diagnostic.
    var diagnostic = Diagnostic.Create(Rule, node.GetLocation(), methodSymbol.ToDisplayString());

    syntaxNodeAnalysisContext.ReportDiagnostic(diagnostic);
}

Ответ 2

После некоторых трудностей с этой проблемой я решил создать Анализатор с исправлением кода для ее решения.

Код доступен здесь: https://github.com/ykoksen/unused-task-warning

Он также представляет собой пакет NuGet, который можно использовать в качестве анализатора для проекта (при его сборке): https://www.nuget.org/packages/Lindhart.Analyser.MissingAwaitWarning/#

Кроме того, он также доступен как расширение Visual Studio (на 2017 год). Однако это только анализирует в настоящее время открытые файлы, поэтому я бы рекомендовал использовать пакет NuGet. Расширение доступно здесь (или ищите его в Visual Studio): https://marketplace.visualstudio.com/items?itemName=Lindhart.missingAwaitWarning#overview

Код для анализатора:

    public override void Initialize(AnalysisContext context)
    {
        context.RegisterSyntaxNodeAction(AnalyseSymbolNode, SyntaxKind.InvocationExpression);
    }

    private void AnalyseSymbolNode(SyntaxNodeAnalysisContext syntaxNodeAnalysisContext)
    {
        if (syntaxNodeAnalysisContext.Node is InvocationExpressionSyntax node)
        {
            if (syntaxNodeAnalysisContext
                    .SemanticModel
                    .GetSymbolInfo(node.Expression, syntaxNodeAnalysisContext.CancellationToken)
                    .Symbol is IMethodSymbol methodSymbol)
            {
                if (node.Parent is ExpressionStatementSyntax)
                {
                    // Only checks for the two most common awaitable types. In principle this should instead check all types that are awaitable
                    if (EqualsType(methodSymbol.ReturnType, typeof(Task), typeof(ConfiguredTaskAwaitable)))
                    {
                        var diagnostic = Diagnostic.Create(Rule, node.GetLocation(), methodSymbol.ToDisplayString());

                        syntaxNodeAnalysisContext.ReportDiagnostic(diagnostic);
                    }
                }
            }
        }
    }

    /// <summary>
    /// Checks if the <paramref name="typeSymbol"/> is one of the types specified
    /// </summary>
    /// <param name="typeSymbol"></param>
    /// <param name="type"></param>
    /// <returns></returns>
    /// <remarks>This method should probably be rewritten so it does not merely compare the names, but instead the actual type.</remarks>
    private static bool EqualsType(ITypeSymbol typeSymbol, params Type[] type)
    {
        var fullSymbolNameWithoutGeneric = $"{typeSymbol.ContainingNamespace.ToDisplayString()}.{typeSymbol.Name}";
        return type.Any(x => fullSymbolNameWithoutGeneric.Equals(x.FullName));
    }

Ответ 3

Компилятор выдаст предупреждение CS4014, но он отправляется только в том случае, если вызывающим методом является async.

Нет предупреждения:

Task CallingMethod() {
    DoWhateverAsync();
    // More code that eventually returns a task.
}

Предупреждение CS4014: поскольку этот вызов не ожидается, выполнение текущего метода продолжается до завершения вызова. Попробуйте применить оператор 'await' к результату вызова.

async Task CallingMethod() {
    DoWhateverAsync();
}

Это не очень полезно в вашем конкретном случае, потому что вы должны найти все места, где вызывается DoWhateverAsync, изменить их, чтобы получить предупреждение, а затем исправить код. Но вы хотели использовать предупреждение компилятора, чтобы найти эти вызовы в первую очередь.

Я предлагаю вам использовать Visual Studio, чтобы найти все случаи использования DoWhateverAsync. Вам все равно придется изменить окружающий код, пройдя через предупреждения компилятора или обработав список использований.

Ответ 4

У вас есть несколько вариантов:

  • Это простейшее решение "Caveman", использующее встроенную функцию поиска VS (CTRL + SHIFT + F), поиск в полном решении, также в разделе Параметры поиска установите флажок Используйте регулярное выражение и используйте это регулярное выражение: (?<!await|task(.*))\s([_a-zA-Z0-9\.])*Async\( Предполагается, что вы опубликуете все свои асинхронные методы с ключевым словом Async, а вызов метода находится в одной строке, Если это не так, не используйте его (или не добавляйте недостающие проверки в выражение).
  • Используйте сторонний инструмент для анализа кода, пакет Nuget. ReSharper очень популярен, и я считаю, что он способен обнаружить эту проблему или вы можете создать свои собственные правила.
  • Мой выбор - использовать Roslyn (@Volker предоставил одно решение). Вы можете создать свой собственный набор правил с помощью решений по исправлению кода (значок лампочки покажет исправление кода), так что это лучшее.
  • ОБНОВЛЕНИЕ: VS 2019 проверяет эту проблему по умолчанию и выдает предупреждения.enter image description here

Как использовать Roslyn:

  • Вы должны установить .NET Compiler Platform SDK: здесь
  • Использовать VS 2017 версии 15.2 (или выше)
  • Создайте новый проект Файл → Новый → Проект, в группе Расширяемость выберите: Анализатор с исправлением кода (Nuget + VSIX) Для создания этого проекта вам нужно настроить таргетинг на .NET Framework 4.6.2. enter image description here

Вы можете скопировать вставить предыдущее решение. Создание

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class AsyncAwaitAnalyzer : DiagnosticAnalyzer
{ ...
}

класс с логикой, чтобы обнаружить проблему. И создать

[ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(AsyncAwaitCodeFixProvider)), Shared]
public class AsyncAwaitCodeFixProvider : CodeFixProvider
{ ...
}

класс для предоставления предложений по устранению проблем (добавьте ожидание).

После успешной сборки вы получите свой собственный пакет .wsix, вы можете установить его на свой экземпляр VS, и после перезапуска VS должен начать обнаруживать проблемы.

Ответ 5

Вы можете добавить конкретное предупреждение в свойствах проекта VS, как то, которое выдает ошибку компиляции, как описано здесь

Вы можете добавить список предупреждающих кодов, разделенных запятой, например. CS4014, и компилятор не сможет скомпилироваться, если вы не ожидаете метода async.

Вот скриншот с VS2017: конфигурация VS2017 для выброса ошибок компилятора для предупреждения CS4014