diff --git a/src/CSharp/CodeCracker/CodeCracker.csproj b/src/CSharp/CodeCracker/CodeCracker.csproj
index e1a60a05e..73fae27b6 100644
--- a/src/CSharp/CodeCracker/CodeCracker.csproj
+++ b/src/CSharp/CodeCracker/CodeCracker.csproj
@@ -46,6 +46,8 @@
+
+
diff --git a/src/CSharp/CodeCracker/Design/ResultInAsyncAnalyzer.cs b/src/CSharp/CodeCracker/Design/ResultInAsyncAnalyzer.cs
new file mode 100644
index 000000000..c553d7881
--- /dev/null
+++ b/src/CSharp/CodeCracker/Design/ResultInAsyncAnalyzer.cs
@@ -0,0 +1,66 @@
+using CodeCracker.Properties;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+using System;
+using System.Collections.Immutable;
+using System.Linq;
+
+namespace CodeCracker.CSharp.Design
+{
+ [DiagnosticAnalyzer(LanguageNames.CSharp)]
+ public class ResultInAsyncAnalyzer : DiagnosticAnalyzer
+ {
+ internal static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.ResultInAsyncAnalyzer_Title), Resources.ResourceManager, typeof(Resources));
+ internal static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.ResultInAsync_MessageFormat), Resources.ResourceManager, typeof(Resources));
+ internal static readonly LocalizableString Description = new LocalizableResourceString(nameof(Resources.ResultInAsync_Description), Resources.ResourceManager, typeof(Resources));
+ internal const string Category = SupportedCategories.Design;
+
+ internal static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
+ DiagnosticId.ResultInAsync.ToDiagnosticId(),
+ Title,
+ MessageFormat,
+ Category,
+ DiagnosticSeverity.Warning,
+ true,
+ description: Description,
+ helpLinkUri: HelpLink.ForDiagnostic(DiagnosticId.ResultInAsync));
+
+ public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule);
+
+ public override void Initialize(AnalysisContext context) =>
+ context.RegisterSyntaxNodeAction(Analyzer, SyntaxKind.InvocationExpression);
+
+ private static void Analyzer(SyntaxNodeAnalysisContext context)
+ {
+ if (context.IsGenerated()) return;
+ var invocation = (InvocationExpressionSyntax)context.Node;
+ var parentMethod = invocation.Ancestors().OfType().FirstOrDefault();
+ if (parentMethod == null) return;
+ var parentIsAsync = parentMethod.Modifiers.Any(n => n.IsKind(SyntaxKind.AsyncKeyword));
+ if (!parentIsAsync) return;
+ // We now know that we are in async method
+
+ var memberAccess = invocation.Parent as MemberAccessExpressionSyntax;
+ if (memberAccess == null) return;
+ var member = memberAccess.Name;
+ if (member.ToString() != "Result") return;
+ // We now know that we are accessing .Result
+
+ var identifierSymbol = context.SemanticModel.GetSymbolInfo(memberAccess, context.CancellationToken).Symbol;
+ if (identifierSymbol.OriginalDefinition.ToString() != "System.Threading.Tasks.Task.Result") return;
+ // We now know that we are accessing System.Threading.Tasks.Task.Result
+
+ SimpleNameSyntax identifier;
+ identifier = invocation.Expression as IdentifierNameSyntax;
+ if (identifier == null)
+ {
+ var transient = invocation.Expression as MemberAccessExpressionSyntax;
+ identifier = transient.Name;
+ }
+ if (identifier == null) return; // It's not supposed to happen. Don't throw an exception, though.
+ context.ReportDiagnostic(Diagnostic.Create(Rule, identifier.GetLocation(), identifier.Identifier.Text));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/CSharp/CodeCracker/Design/ResultInAsyncCodeFixProvider.cs b/src/CSharp/CodeCracker/Design/ResultInAsyncCodeFixProvider.cs
new file mode 100644
index 000000000..a6b5fe1b8
--- /dev/null
+++ b/src/CSharp/CodeCracker/Design/ResultInAsyncCodeFixProvider.cs
@@ -0,0 +1,75 @@
+using CodeCracker.Properties;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CodeActions;
+using Microsoft.CodeAnalysis.CodeFixes;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Formatting;
+using Microsoft.CodeAnalysis.Simplification;
+using System;
+using System.Collections.Immutable;
+using System.Composition;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace CodeCracker.CSharp.Design
+{
+ [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(ResultInAsyncCodeFixProvider)), Shared]
+ public class ResultInAsyncCodeFixProvider : CodeFixProvider
+ {
+ public sealed override ImmutableArray FixableDiagnosticIds =>
+ ImmutableArray.Create(DiagnosticId.ResultInAsync.ToDiagnosticId());
+
+ public sealed override FixAllProvider GetFixAllProvider() => WellKnownFixAllProviders.BatchFixer;
+
+ public async sealed override Task RegisterCodeFixesAsync(CodeFixContext context)
+ {
+ var diagnostic = context.Diagnostics.First();
+ var compilation = (CSharpCompilation)await context.Document.Project.GetCompilationAsync();
+ context.RegisterCodeFix(CodeAction.Create(
+ Resources.ResultInAsyncCodeFixProvider_Title,
+ ct => ReplaceResultWithAwaitAsync(context.Document, diagnostic, ct),
+ nameof(ResultInAsyncCodeFixProvider)
+ ), diagnostic);
+ }
+
+ private async static Task ReplaceResultWithAwaitAsync(Document document, Diagnostic diagnostic, CancellationToken cancellationToken)
+ {
+ var root = (await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false));
+ var sourceSpan = diagnostic.Location.SourceSpan;
+ var invocation = root.FindToken(sourceSpan.Start).Parent.AncestorsAndSelf().OfType().First();
+ var memberAccess = invocation.Parent as MemberAccessExpressionSyntax;
+
+ // Replace memberAccess with the async invocation
+ SyntaxNode newRoot;
+
+ // See if the member access expression is a part of something bigger
+ // i.e. something.Result.something. Then we need to produce (await something.Result).something
+ var parentAccess = memberAccess.Parent as MemberAccessExpressionSyntax;
+ if (parentAccess != null)
+ {
+ var rewritten =
+ SyntaxFactory.ParenthesizedExpression(
+ SyntaxFactory.AwaitExpression(
+ invocation)
+ )
+ .WithLeadingTrivia(invocation.GetLeadingTrivia())
+ .WithTrailingTrivia(invocation.GetTrailingTrivia());
+ var subExpression = parentAccess.Expression;
+ newRoot = root.ReplaceNode(subExpression, rewritten);
+ }
+ else
+ {
+ var rewritten =
+ SyntaxFactory.AwaitExpression(
+ invocation)
+ .WithLeadingTrivia(invocation.GetLeadingTrivia())
+ .WithTrailingTrivia(invocation.GetTrailingTrivia());
+ newRoot = root.ReplaceNode(memberAccess, rewritten);
+ }
+
+ return document.WithSyntaxRoot(newRoot);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Common/CodeCracker.Common/DiagnosticId.cs b/src/Common/CodeCracker.Common/DiagnosticId.cs
index 156dc6285..e0e2e9576 100644
--- a/src/Common/CodeCracker.Common/DiagnosticId.cs
+++ b/src/Common/CodeCracker.Common/DiagnosticId.cs
@@ -80,5 +80,6 @@ public enum DiagnosticId
NameOf_External = 108,
StringFormatArgs_ExtraArgs = 111,
AlwaysUseVarOnPrimitives = 105,
+ ResultInAsync = 122,
}
}
\ No newline at end of file
diff --git a/src/Common/CodeCracker.Common/Properties/Resources.Designer.cs b/src/Common/CodeCracker.Common/Properties/Resources.Designer.cs
index a9cc2ac5f..bf6c17f22 100644
--- a/src/Common/CodeCracker.Common/Properties/Resources.Designer.cs
+++ b/src/Common/CodeCracker.Common/Properties/Resources.Designer.cs
@@ -259,6 +259,42 @@ public static string NameOfCodeFixProvider_Title {
}
}
+ ///
+ /// Looks up a localized string similar to Calling Task.Result in an awaited method may lead to a deadlock. Obtain the result of the task with the await keyword to avoid deadlocks..
+ ///
+ public static string ResultInAsync_Description {
+ get {
+ return ResourceManager.GetString("ResultInAsync_Description", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to await '{0}' rather than calling its Result..
+ ///
+ public static string ResultInAsync_MessageFormat {
+ get {
+ return ResourceManager.GetString("ResultInAsync_MessageFormat", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Replace Task.Result with await Task.
+ ///
+ public static string ResultInAsyncAnalyzer_Title {
+ get {
+ return ResourceManager.GetString("ResultInAsyncAnalyzer_Title", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Await the asynchronous call.
+ ///
+ public static string ResultInAsyncCodeFixProvider_Title {
+ get {
+ return ResourceManager.GetString("ResultInAsyncCodeFixProvider_Title", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to String interpolation allows for better reading of the resulting string when compared to String.Format. You should use String.Format only when another method is supplying the format string..
///
diff --git a/src/Common/CodeCracker.Common/Properties/Resources.fr.resx b/src/Common/CodeCracker.Common/Properties/Resources.fr.resx
index a7953b4d6..ca2dd81b9 100644
--- a/src/Common/CodeCracker.Common/Properties/Resources.fr.resx
+++ b/src/Common/CodeCracker.Common/Properties/Resources.fr.resx
@@ -214,4 +214,16 @@ Si l'erreur est attendu considérer ajouter du logging ou modifier le flow de co
Enlever le block Catch vide et ajouter un lien vers la documentation des bonnes pratiques de Try...Catch
+
+ Replace Task.Result with await Task
+
+
+ Await the asynchronous call
+
+
+ Calling Task.Result in an awaited method may lead to a deadlock. Obtain the result of the task with the await keyword to avoid deadlocks.
+
+
+ await '{0}' rather than calling its Result.
+
\ No newline at end of file
diff --git a/src/Common/CodeCracker.Common/Properties/Resources.resx b/src/Common/CodeCracker.Common/Properties/Resources.resx
index 1b6e5d917..b16a1c60c 100644
--- a/src/Common/CodeCracker.Common/Properties/Resources.resx
+++ b/src/Common/CodeCracker.Common/Properties/Resources.resx
@@ -216,4 +216,16 @@
Remove wrapping Try Block
+
+ Replace Task.Result with await Task
+
+
+ Await the asynchronous call
+
+
+ Calling Task.Result in an awaited method may lead to a deadlock. Obtain the result of the task with the await keyword to avoid deadlocks.
+
+
+ await '{0}' rather than calling its Result.
+
\ No newline at end of file
diff --git a/test/CSharp/CodeCracker.Test/CodeCracker.Test.csproj b/test/CSharp/CodeCracker.Test/CodeCracker.Test.csproj
index b17ba4a79..f0be21fcf 100644
--- a/test/CSharp/CodeCracker.Test/CodeCracker.Test.csproj
+++ b/test/CSharp/CodeCracker.Test/CodeCracker.Test.csproj
@@ -129,6 +129,7 @@
+
diff --git a/test/CSharp/CodeCracker.Test/Design/ResultInAsyncTest.cs b/test/CSharp/CodeCracker.Test/Design/ResultInAsyncTest.cs
new file mode 100644
index 000000000..8b09ac0b9
--- /dev/null
+++ b/test/CSharp/CodeCracker.Test/Design/ResultInAsyncTest.cs
@@ -0,0 +1,167 @@
+using CodeCracker.CSharp.Design;
+using Microsoft.CodeAnalysis;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace CodeCracker.Test.CSharp.Design
+{
+ public class ResultInAsyncTest : CodeFixVerifier
+ {
+ [Fact]
+ public async Task ResultInNonAsyncMethodIsOk()
+ {
+ const string test = @"
+ using System.Threading.Tasks;
+ public class MyClass
+ {
+ public Task Execute()
+ {
+ return Asynchronous().Result;
+ }
+ public async Task Asynchronous()
+ {
+ return;
+ }
+ }";
+ await VerifyCSharpHasNoDiagnosticsAsync(test);
+ }
+
+ [Fact]
+ public async Task WarningIfResultInAsync()
+ {
+ const string test = @"
+ using System.Threading.Tasks;
+ public class MyClass
+ {
+ public async Task Execute()
+ {
+ return Asynchronous().Result;
+ }
+ public async Task Asynchronous()
+ {
+ return 5;
+ }
+ }";
+
+ var expected = new DiagnosticResult
+ {
+ Id = DiagnosticId.ResultInAsync.ToDiagnosticId(),
+ Message = string.Format(ResultInAsyncAnalyzer.MessageFormat.ToString(), "Asynchronous"),
+ Severity = DiagnosticSeverity.Warning,
+ Locations = new[] { new DiagnosticResultLocation("Test0.cs", 7, 32) }
+ };
+ await VerifyCSharpDiagnosticAsync(test, expected);
+ }
+
+ [Fact]
+ public async Task FixResultInAsync()
+ {
+ const string source = @"
+ using System.Threading.Tasks;
+ public class MyClass
+ {
+ public async Task Execute()
+ {
+ return Asynchronous().Result;
+ }
+ public async Task Asynchronous()
+ {
+ return 5;
+ }
+ }";
+ const string fixtest = @"
+ using System.Threading.Tasks;
+ public class MyClass
+ {
+ public async Task Execute()
+ {
+ return await Asynchronous();
+ }
+ public async Task Asynchronous()
+ {
+ return 5;
+ }
+ }";
+ await VerifyCSharpFixAsync(source, fixtest);
+ }
+
+ [Fact]
+ public async Task WarningIfNestedResultInAsync()
+ {
+ const string test = @"
+ namespace Nested.Namespaces
+ {
+ using System.Threading.Tasks;
+ public class ParentClass
+ {
+ public class MyClass
+ {
+ public async Task Execute()
+ {
+ var x = ParentClass.MyClass.Asynchronous().Result.Length;
+ }
+ public static async Task Asynchronous()
+ {
+ return ""Test"";
+ }
+ }
+ }
+ }";
+
+ var expected = new DiagnosticResult
+ {
+ Id = DiagnosticId.ResultInAsync.ToDiagnosticId(),
+ Message = string.Format(ResultInAsyncAnalyzer.MessageFormat.ToString(), "Asynchronous"),
+ Severity = DiagnosticSeverity.Warning,
+ Locations = new[] { new DiagnosticResultLocation("Test0.cs", 11, 53) }
+ };
+
+ await VerifyCSharpDiagnosticAsync(test, expected);
+ }
+
+ [Fact]
+ public async Task FixNestedResultInAsync()
+ {
+ const string source = @"
+ namespace Nested.Namespaces
+ {
+ using System.Threading.Tasks;
+ public class ParentClass
+ {
+ public class MyClass
+ {
+ public async Task Execute()
+ {
+ var x = MyClass.Asynchronous().Result.Length;
+ }
+ public static async Task Asynchronous()
+ {
+ return ""Test"";
+ }
+ }
+ }
+ }";
+ const string fixtest = @"
+ namespace Nested.Namespaces
+ {
+ using System.Threading.Tasks;
+ public class ParentClass
+ {
+ public class MyClass
+ {
+ public async Task Execute()
+ {
+ var x = (await MyClass.Asynchronous()).Length;
+ }
+ public static async Task Asynchronous()
+ {
+ return ""Test"";
+ }
+ }
+ }
+ }";
+
+ await VerifyCSharpFixAsync(source, fixtest);
+ }
+ }
+}
\ No newline at end of file