From 98f52ed035310bc91621d7152a9069260cd241f7 Mon Sep 17 00:00:00 2001
From: Smaug123 <3138005+Smaug123@users.noreply.github.com>
Date: Wed, 27 Aug 2025 12:15:02 +0100
Subject: [PATCH 1/6] Scaffold new analyzer
---
docs/analyzers/SyncBlockingAnalyzer.md | 20 +++++
src/FSharp.Analyzers/FSharp.Analyzers.fsproj | 3 +-
src/FSharp.Analyzers/SyncBlockingAnalyzer.fs | 14 ++++
.../FSharp.Analyzers.Tests.fsproj | 77 ++++++++++---------
.../SyncBlockingAnalyzerTests.fs | 56 ++++++++++++++
.../data/syncBlocking/Sample.fs | 2 +
6 files changed, 133 insertions(+), 39 deletions(-)
create mode 100644 docs/analyzers/SyncBlockingAnalyzer.md
create mode 100644 src/FSharp.Analyzers/SyncBlockingAnalyzer.fs
create mode 100644 tests/FSharp.Analyzers.Tests/SyncBlockingAnalyzerTests.fs
create mode 100644 tests/FSharp.Analyzers.Tests/data/syncBlocking/Sample.fs
diff --git a/docs/analyzers/SyncBlockingAnalyzer.md b/docs/analyzers/SyncBlockingAnalyzer.md
new file mode 100644
index 0000000..241ebc2
--- /dev/null
+++ b/docs/analyzers/SyncBlockingAnalyzer.md
@@ -0,0 +1,20 @@
+---
+title: SyncBlocking Analyzer
+category: analyzers
+categoryindex: 1
+index:
+---
+
+# SyncBlocking Analyzer
+
+## Problem
+
+```fsharp
+
+```
+
+## Fix
+
+```fsharp
+
+```
diff --git a/src/FSharp.Analyzers/FSharp.Analyzers.fsproj b/src/FSharp.Analyzers/FSharp.Analyzers.fsproj
index d648b31..8a51ed5 100644
--- a/src/FSharp.Analyzers/FSharp.Analyzers.fsproj
+++ b/src/FSharp.Analyzers/FSharp.Analyzers.fsproj
@@ -29,6 +29,7 @@
+
@@ -44,4 +45,4 @@
-
+
\ No newline at end of file
diff --git a/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs b/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs
new file mode 100644
index 0000000..1845168
--- /dev/null
+++ b/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs
@@ -0,0 +1,14 @@
+module GR.FSharp.Analyzers.SyncBlockingAnalyzer
+
+open System
+open FSharp.Analyzers.SDK
+open FSharp.Analyzers.SDK.TASTCollecting
+open FSharp.Compiler.Symbols
+open FSharp.Compiler.Syntax
+open FSharp.Compiler.Text
+
+[]
+let syncBlockingAnalyzer : Analyzer =
+ fun (ctx : CliContext) -> async { return List.empty }
diff --git a/tests/FSharp.Analyzers.Tests/FSharp.Analyzers.Tests.fsproj b/tests/FSharp.Analyzers.Tests/FSharp.Analyzers.Tests.fsproj
index a3ce1a4..4f0bd18 100644
--- a/tests/FSharp.Analyzers.Tests/FSharp.Analyzers.Tests.fsproj
+++ b/tests/FSharp.Analyzers.Tests/FSharp.Analyzers.Tests.fsproj
@@ -1,41 +1,42 @@
-
- net8.0
- false
- false
- true
- G-Research.FSharp.Analyzers.Tests
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ net8.0
+ false
+ false
+ true
+ G-Research.FSharp.Analyzers.Tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/FSharp.Analyzers.Tests/SyncBlockingAnalyzerTests.fs b/tests/FSharp.Analyzers.Tests/SyncBlockingAnalyzerTests.fs
new file mode 100644
index 0000000..e65d9fa
--- /dev/null
+++ b/tests/FSharp.Analyzers.Tests/SyncBlockingAnalyzerTests.fs
@@ -0,0 +1,56 @@
+module GR.FSharp.Analyzers.Tests.SyncBlockingAnalyzerTests
+
+open System.Collections
+open System.IO
+open NUnit.Framework
+open FSharp.Compiler.CodeAnalysis
+open FSharp.Analyzers.SDK.Testing
+open GR.FSharp.Analyzers
+open GR.FSharp.Analyzers.Tests.Common
+
+let mutable projectOptions : FSharpProjectOptions = FSharpProjectOptions.zero
+
+[]
+let Setup () =
+ task {
+ let! options = mkOptionsFromProject "net7.0" []
+ projectOptions <- options
+ }
+
+type TestCases() =
+
+ interface IEnumerable with
+ member _.GetEnumerator () : IEnumerator =
+ constructTestCaseEnumerator [| "syncBlocking" |]
+
+[)>]
+let SyncBlockingAnalyzerTests (fileName : string) =
+ task {
+ let fileName = Path.Combine (dataFolder, fileName)
+
+ let! messages =
+ File.ReadAllText fileName
+ |> getContext projectOptions
+ |> SyncBlockingAnalyzer.syncBlockingAnalyzer
+
+ do! assertExpected fileName messages
+ }
+
+type NegativeTestCases() =
+
+ interface IEnumerable with
+ member _.GetEnumerator () : IEnumerator =
+ constructTestCaseEnumerator [| "syncBlocking" ; "negative" |]
+
+[)>]
+let NegativeTests (fileName : string) =
+ task {
+ let fileName = Path.Combine (dataFolder, fileName)
+
+ let! messages =
+ File.ReadAllText fileName
+ |> getContext projectOptions
+ |> SyncBlockingAnalyzer.syncBlockingAnalyzer
+
+ Assert.That (messages, Is.Empty)
+ }
diff --git a/tests/FSharp.Analyzers.Tests/data/syncBlocking/Sample.fs b/tests/FSharp.Analyzers.Tests/data/syncBlocking/Sample.fs
new file mode 100644
index 0000000..a1562db
--- /dev/null
+++ b/tests/FSharp.Analyzers.Tests/data/syncBlocking/Sample.fs
@@ -0,0 +1,2 @@
+module Sample
+
From 6f870e8f41996ea73d6c989ac4784aba33108c8e Mon Sep 17 00:00:00 2001
From: Smaug123 <3138005+Smaug123@users.noreply.github.com>
Date: Wed, 27 Aug 2025 12:34:22 +0100
Subject: [PATCH 2/6] Initial implementation
---
src/FSharp.Analyzers/SyncBlockingAnalyzer.fs | 138 +++++++++++++++++-
.../SyncBlockingAnalyzerTests.fs | 4 +-
2 files changed, 135 insertions(+), 7 deletions(-)
diff --git a/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs b/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs
index 1845168..80b661b 100644
--- a/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs
+++ b/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs
@@ -3,12 +3,140 @@ module GR.FSharp.Analyzers.SyncBlockingAnalyzer
open System
open FSharp.Analyzers.SDK
open FSharp.Analyzers.SDK.TASTCollecting
+open FSharp.Compiler.CodeAnalysis
open FSharp.Compiler.Symbols
open FSharp.Compiler.Syntax
+open FSharp.Compiler.SyntaxTrivia
open FSharp.Compiler.Text
-[]
-let syncBlockingAnalyzer : Analyzer =
- fun (ctx : CliContext) -> async { return List.empty }
+[]
+let Code = "GRA-SYNCBLOCK-001"
+
+[]
+let SwitchOffComment = "synchronous blocking call allowed"
+
+let problematicMethods =
+ [
+ "Microsoft.FSharp.Control.Async.RunSynchronously"
+ "Microsoft.FSharp.Control.FSharpAsync.RunSynchronously"
+ "System.Threading.Tasks.Task.Wait"
+ "System.Threading.Tasks.Task.WaitAll"
+ "System.Threading.Tasks.Task.WaitAny"
+ "System.Runtime.CompilerServices.TaskAwaiter.GetResult"
+ "System.Runtime.CompilerServices.TaskAwaiter`1.GetResult"
+ "System.Runtime.CompilerServices.ValueTaskAwaiter.GetResult"
+ "System.Runtime.CompilerServices.ValueTaskAwaiter`1.GetResult"
+ ]
+ |> Set.ofList
+
+let problematicProperties =
+ [
+ "System.Threading.Tasks.Task`1.Result"
+ "System.Threading.Tasks.ValueTask`1.Result"
+ ]
+ |> Set.ofList
+
+let isSwitchedOffPerComment (sourceText: ISourceText) (comments: CommentTrivia list) (range: Range) =
+ comments
+ |> List.exists (fun c ->
+ match c with
+ | CommentTrivia.LineComment r ->
+ // Check same line or line above
+ if r.StartLine = range.StartLine || r.StartLine = range.StartLine - 1 then
+ let lineOfComment = sourceText.GetLineString(r.StartLine - 1) // 0-based
+ lineOfComment.Contains(SwitchOffComment, StringComparison.OrdinalIgnoreCase)
+ else
+ false
+ | CommentTrivia.BlockComment r ->
+ // Check if block comment is on same line or line above
+ if r.StartLine = range.StartLine || r.EndLine = range.StartLine - 1 then
+ let startLine = sourceText.GetLineString(r.StartLine - 1)
+ startLine.Contains(SwitchOffComment, StringComparison.OrdinalIgnoreCase)
+ else
+ false
+ )
+
+let analyze (sourceText: ISourceText) (ast: ParsedInput) (checkFileResults: FSharpCheckFileResults) =
+ let comments =
+ match ast with
+ | ParsedInput.ImplFile parsedImplFileInput -> parsedImplFileInput.Trivia.CodeComments
+ | _ -> []
+
+ let violations = ResizeArray()
+
+ let walker =
+ { new TypedTreeCollectorBase() with
+ override _.WalkCall _ (mfv: FSharpMemberOrFunctionOrValue) _ _ _ (m: range) =
+ if problematicMethods.Contains mfv.FullName then
+ if not (isSwitchedOffPerComment sourceText comments m) then
+ let methodName =
+ if mfv.DisplayName.Contains(".") then
+ mfv.DisplayName
+ else
+ // Get more context for better error messages
+ let parts = mfv.FullName.Split('.')
+ if parts.Length >= 2 then
+ $"{parts.[parts.Length - 2]}.{parts.[parts.Length - 1]}"
+ else
+ mfv.DisplayName
+ violations.Add(m, "method", methodName)
+
+ override _.WalkFSharpFieldGet _ _ (field: FSharpField) =
+ // Handle Result property access
+ let fullName =
+ match field.DeclaringEntity with
+ | Some entity -> $"{entity.FullName}.{field.Name}"
+ | None -> field.Name
+
+ if problematicProperties.Contains fullName then
+ let range = field.DeclarationLocation
+ if not (isSwitchedOffPerComment sourceText comments range) then
+ violations.Add(range, "property", field.Name)
+ }
+
+ match checkFileResults.ImplementationFile with
+ | Some typedTree -> walkTast walker typedTree
+ | None -> ()
+
+ violations
+ |> Seq.map (fun (range, _kind, name) ->
+ {
+ Type = "SyncBlockingAnalyzer"
+ Message =
+ $"Synchronous blocking call '%s{name}' should be avoided. " +
+ "This can cause deadlocks and thread pool starvation. " +
+ "Consider using `let!` in a `task` or `async` computation expression. " +
+ "Suppress with comment including text 'synchronous blocking call allowed'."
+ Code = Code
+ Severity = Severity.Warning
+ Range = range
+ Fixes = []
+ }
+ )
+ |> Seq.toList
+
+[]
+let Name = "SyncBlockingAnalyzer"
+
+[]
+let ShortDescription =
+ "Bans synchronous blocking operations like Task.Result and Async.RunSynchronously"
+
+[]
+let HelpUri =
+ "https://g-research.github.io/fsharp-analyzers/analyzers/SyncBlockingAnalyzer.html"
+
+[]
+let syncBlockingCliAnalyzer: Analyzer =
+ fun (ctx: CliContext) ->
+ async { return analyze ctx.SourceText ctx.ParseFileResults.ParseTree ctx.CheckFileResults }
+
+[]
+let syncBlockingEditorAnalyzer: Analyzer =
+ fun (ctx: EditorContext) ->
+ async {
+ return
+ ctx.CheckFileResults
+ |> Option.map (analyze ctx.SourceText ctx.ParseFileResults.ParseTree)
+ |> Option.defaultValue []
+ }
\ No newline at end of file
diff --git a/tests/FSharp.Analyzers.Tests/SyncBlockingAnalyzerTests.fs b/tests/FSharp.Analyzers.Tests/SyncBlockingAnalyzerTests.fs
index e65d9fa..ffc5563 100644
--- a/tests/FSharp.Analyzers.Tests/SyncBlockingAnalyzerTests.fs
+++ b/tests/FSharp.Analyzers.Tests/SyncBlockingAnalyzerTests.fs
@@ -31,7 +31,7 @@ let SyncBlockingAnalyzerTests (fileName : string) =
let! messages =
File.ReadAllText fileName
|> getContext projectOptions
- |> SyncBlockingAnalyzer.syncBlockingAnalyzer
+ |> SyncBlockingAnalyzer.syncBlockingCliAnalyzer
do! assertExpected fileName messages
}
@@ -50,7 +50,7 @@ let NegativeTests (fileName : string) =
let! messages =
File.ReadAllText fileName
|> getContext projectOptions
- |> SyncBlockingAnalyzer.syncBlockingAnalyzer
+ |> SyncBlockingAnalyzer.syncBlockingCliAnalyzer
Assert.That (messages, Is.Empty)
}
From c3c4e56f1e1c53eb394c20e02671c9f953a0330f Mon Sep 17 00:00:00 2001
From: Smaug123 <3138005+Smaug123@users.noreply.github.com>
Date: Wed, 27 Aug 2025 16:56:06 +0100
Subject: [PATCH 3/6] Tests
---
src/FSharp.Analyzers/SyncBlockingAnalyzer.fs | 39 ++++++++++---------
.../SyncBlockingAnalyzerTests.fs | 2 +-
.../syncBlocking/Async RunSynchronously.fs | 18 +++++++++
.../Async RunSynchronously.fs.expected | 3 ++
.../data/syncBlocking/GetResult methods.fs | 26 +++++++++++++
.../GetResult methods.fs.expected | 4 ++
.../data/syncBlocking/Sample.fs | 2 -
.../data/syncBlocking/Task Result property.fs | 21 ++++++++++
.../Task Result property.fs.expected | 4 ++
.../data/syncBlocking/Task Wait methods.fs | 23 +++++++++++
.../Task Wait methods.fs.expected | 5 +++
.../syncBlocking/negative/Async operations.fs | 35 +++++++++++++++++
.../negative/Async operations.fs.expected | 0
.../negative/Non async methods.fs | 31 +++++++++++++++
.../negative/Non async methods.fs.expected | 0
.../negative/With suppression comment.fs | 29 ++++++++++++++
.../With suppression comment.fs.expected | 0
17 files changed, 220 insertions(+), 22 deletions(-)
create mode 100644 tests/FSharp.Analyzers.Tests/data/syncBlocking/Async RunSynchronously.fs
create mode 100644 tests/FSharp.Analyzers.Tests/data/syncBlocking/Async RunSynchronously.fs.expected
create mode 100644 tests/FSharp.Analyzers.Tests/data/syncBlocking/GetResult methods.fs
create mode 100644 tests/FSharp.Analyzers.Tests/data/syncBlocking/GetResult methods.fs.expected
delete mode 100644 tests/FSharp.Analyzers.Tests/data/syncBlocking/Sample.fs
create mode 100644 tests/FSharp.Analyzers.Tests/data/syncBlocking/Task Result property.fs
create mode 100644 tests/FSharp.Analyzers.Tests/data/syncBlocking/Task Result property.fs.expected
create mode 100644 tests/FSharp.Analyzers.Tests/data/syncBlocking/Task Wait methods.fs
create mode 100644 tests/FSharp.Analyzers.Tests/data/syncBlocking/Task Wait methods.fs.expected
create mode 100644 tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/Async operations.fs
create mode 100644 tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/Async operations.fs.expected
create mode 100644 tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/Non async methods.fs
create mode 100644 tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/Non async methods.fs.expected
create mode 100644 tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/With suppression comment.fs
create mode 100644 tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/With suppression comment.fs.expected
diff --git a/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs b/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs
index 80b661b..42995a9 100644
--- a/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs
+++ b/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs
@@ -17,6 +17,7 @@ let SwitchOffComment = "synchronous blocking call allowed"
let problematicMethods =
[
+ "Microsoft.FSharp.Control.RunSynchronously" // This is how it appears in the TAST
"Microsoft.FSharp.Control.Async.RunSynchronously"
"Microsoft.FSharp.Control.FSharpAsync.RunSynchronously"
"System.Threading.Tasks.Task.Wait"
@@ -31,8 +32,8 @@ let problematicMethods =
let problematicProperties =
[
- "System.Threading.Tasks.Task`1.Result"
- "System.Threading.Tasks.ValueTask`1.Result"
+ "System.Threading.Tasks.Task.get_Result" // Note: F# doesn't include `1 in FullName for generic type property getters
+ "System.Threading.Tasks.ValueTask.get_Result" // Same here
]
|> Set.ofList
@@ -67,31 +68,31 @@ let analyze (sourceText: ISourceText) (ast: ParsedInput) (checkFileResults: FSha
let walker =
{ new TypedTreeCollectorBase() with
override _.WalkCall _ (mfv: FSharpMemberOrFunctionOrValue) _ _ _ (m: range) =
+
+ // Check for regular method calls
if problematicMethods.Contains mfv.FullName then
if not (isSwitchedOffPerComment sourceText comments m) then
let methodName =
if mfv.DisplayName.Contains(".") then
mfv.DisplayName
else
- // Get more context for better error messages
- let parts = mfv.FullName.Split('.')
- if parts.Length >= 2 then
- $"{parts.[parts.Length - 2]}.{parts.[parts.Length - 1]}"
+ // Special handling for Async.RunSynchronously
+ if mfv.FullName = "Microsoft.FSharp.Control.RunSynchronously" then
+ "Async.RunSynchronously"
else
- mfv.DisplayName
+ // Get more context for better error messages
+ let parts = mfv.FullName.Split('.')
+ if parts.Length >= 2 then
+ $"{parts.[parts.Length - 2]}.{parts.[parts.Length - 1]}"
+ else
+ mfv.DisplayName
violations.Add(m, "method", methodName)
-
- override _.WalkFSharpFieldGet _ _ (field: FSharpField) =
- // Handle Result property access
- let fullName =
- match field.DeclaringEntity with
- | Some entity -> $"{entity.FullName}.{field.Name}"
- | None -> field.Name
-
- if problematicProperties.Contains fullName then
- let range = field.DeclarationLocation
- if not (isSwitchedOffPerComment sourceText comments range) then
- violations.Add(range, "property", field.Name)
+
+ // Check for property getters (Result property access)
+ elif problematicProperties.Contains mfv.FullName then
+ if not (isSwitchedOffPerComment sourceText comments m) then
+ // For property getters, use just the property name
+ violations.Add(m, "property", "Result")
}
match checkFileResults.ImplementationFile with
diff --git a/tests/FSharp.Analyzers.Tests/SyncBlockingAnalyzerTests.fs b/tests/FSharp.Analyzers.Tests/SyncBlockingAnalyzerTests.fs
index ffc5563..2c4b90e 100644
--- a/tests/FSharp.Analyzers.Tests/SyncBlockingAnalyzerTests.fs
+++ b/tests/FSharp.Analyzers.Tests/SyncBlockingAnalyzerTests.fs
@@ -13,7 +13,7 @@ let mutable projectOptions : FSharpProjectOptions = FSharpProjectOptions.zero
[]
let Setup () =
task {
- let! options = mkOptionsFromProject "net7.0" []
+ let! options = mkOptionsFromProject framework []
projectOptions <- options
}
diff --git a/tests/FSharp.Analyzers.Tests/data/syncBlocking/Async RunSynchronously.fs b/tests/FSharp.Analyzers.Tests/data/syncBlocking/Async RunSynchronously.fs
new file mode 100644
index 0000000..707f57c
--- /dev/null
+++ b/tests/FSharp.Analyzers.Tests/data/syncBlocking/Async RunSynchronously.fs
@@ -0,0 +1,18 @@
+module AsyncRunSynchronously
+
+open System.Threading.Tasks
+
+let testAsyncRunSynchronously () =
+ let computation = async { return 42 }
+ let result = Async.RunSynchronously computation
+ result
+
+let testAsyncRunSynchronouslyWithTimeout () =
+ let computation = async { return "hello" }
+ let result = Async.RunSynchronously(computation, 1000)
+ result
+
+let testAsyncRunSynchronouslyQualified () =
+ let computation = async { return true }
+ let result = Microsoft.FSharp.Control.Async.RunSynchronously computation
+ result
\ No newline at end of file
diff --git a/tests/FSharp.Analyzers.Tests/data/syncBlocking/Async RunSynchronously.fs.expected b/tests/FSharp.Analyzers.Tests/data/syncBlocking/Async RunSynchronously.fs.expected
new file mode 100644
index 0000000..6821589
--- /dev/null
+++ b/tests/FSharp.Analyzers.Tests/data/syncBlocking/Async RunSynchronously.fs.expected
@@ -0,0 +1,3 @@
+GRA-SYNCBLOCK-001 | Warning | (7,17 - 7,51) | Synchronous blocking call 'Async.RunSynchronously' should be avoided. This can cause deadlocks and thread pool starvation. Consider using `let!` in a `task` or `async` computation expression. Suppress with comment including text 'synchronous blocking call allowed'. | []
+GRA-SYNCBLOCK-001 | Warning | (12,17 - 12,58) | Synchronous blocking call 'Async.RunSynchronously' should be avoided. This can cause deadlocks and thread pool starvation. Consider using `let!` in a `task` or `async` computation expression. Suppress with comment including text 'synchronous blocking call allowed'. | []
+GRA-SYNCBLOCK-001 | Warning | (17,17 - 17,76) | Synchronous blocking call 'Async.RunSynchronously' should be avoided. This can cause deadlocks and thread pool starvation. Consider using `let!` in a `task` or `async` computation expression. Suppress with comment including text 'synchronous blocking call allowed'. | []
diff --git a/tests/FSharp.Analyzers.Tests/data/syncBlocking/GetResult methods.fs b/tests/FSharp.Analyzers.Tests/data/syncBlocking/GetResult methods.fs
new file mode 100644
index 0000000..1280466
--- /dev/null
+++ b/tests/FSharp.Analyzers.Tests/data/syncBlocking/GetResult methods.fs
@@ -0,0 +1,26 @@
+module GetResultMethods
+
+open System.Threading.Tasks
+open System.Runtime.CompilerServices
+
+let testTaskAwaiterGetResult () =
+ let t = Task.Run(fun () -> 42)
+ let awaiter = t.GetAwaiter()
+ let result = awaiter.GetResult()
+ result
+
+let testTaskAwaiterGetResultGeneric () =
+ let t = Task.Run(fun () -> "hello")
+ let awaiter = t.GetAwaiter()
+ awaiter.GetResult()
+
+let testValueTaskAwaiterGetResult () =
+ let vt = ValueTask.FromResult 42
+ let awaiter = vt.GetAwaiter()
+ awaiter.GetResult()
+
+let testValueTaskAwaiterGetResultGeneric () =
+ let vt = ValueTask("hello")
+ let awaiter = vt.GetAwaiter()
+ let result = awaiter.GetResult()
+ result
\ No newline at end of file
diff --git a/tests/FSharp.Analyzers.Tests/data/syncBlocking/GetResult methods.fs.expected b/tests/FSharp.Analyzers.Tests/data/syncBlocking/GetResult methods.fs.expected
new file mode 100644
index 0000000..ce32daa
--- /dev/null
+++ b/tests/FSharp.Analyzers.Tests/data/syncBlocking/GetResult methods.fs.expected
@@ -0,0 +1,4 @@
+GRA-SYNCBLOCK-001 | Warning | (9,17 - 9,36) | Synchronous blocking call 'TaskAwaiter.GetResult' should be avoided. This can cause deadlocks and thread pool starvation. Consider using `let!` in a `task` or `async` computation expression. Suppress with comment including text 'synchronous blocking call allowed'. | []
+GRA-SYNCBLOCK-001 | Warning | (15,4 - 15,23) | Synchronous blocking call 'TaskAwaiter.GetResult' should be avoided. This can cause deadlocks and thread pool starvation. Consider using `let!` in a `task` or `async` computation expression. Suppress with comment including text 'synchronous blocking call allowed'. | []
+GRA-SYNCBLOCK-001 | Warning | (20,4 - 20,23) | Synchronous blocking call 'ValueTaskAwaiter.GetResult' should be avoided. This can cause deadlocks and thread pool starvation. Consider using `let!` in a `task` or `async` computation expression. Suppress with comment including text 'synchronous blocking call allowed'. | []
+GRA-SYNCBLOCK-001 | Warning | (25,17 - 25,36) | Synchronous blocking call 'ValueTaskAwaiter.GetResult' should be avoided. This can cause deadlocks and thread pool starvation. Consider using `let!` in a `task` or `async` computation expression. Suppress with comment including text 'synchronous blocking call allowed'. | []
diff --git a/tests/FSharp.Analyzers.Tests/data/syncBlocking/Sample.fs b/tests/FSharp.Analyzers.Tests/data/syncBlocking/Sample.fs
deleted file mode 100644
index a1562db..0000000
--- a/tests/FSharp.Analyzers.Tests/data/syncBlocking/Sample.fs
+++ /dev/null
@@ -1,2 +0,0 @@
-module Sample
-
diff --git a/tests/FSharp.Analyzers.Tests/data/syncBlocking/Task Result property.fs b/tests/FSharp.Analyzers.Tests/data/syncBlocking/Task Result property.fs
new file mode 100644
index 0000000..e6b7253
--- /dev/null
+++ b/tests/FSharp.Analyzers.Tests/data/syncBlocking/Task Result property.fs
@@ -0,0 +1,21 @@
+module TaskResultProperty
+
+open System.Threading.Tasks
+
+let testTaskResult () =
+ let t = Task.Run(fun () -> 42)
+ let result = t.Result
+ result
+
+let testTaskOfTResult () =
+ let t : Task = Task.Run(fun () -> "hello")
+ t.Result
+
+let testChainedResult () =
+ let result = Task.Run(fun () -> 42).Result
+ result
+
+let testValueTaskResult () =
+ let vt = ValueTask(42)
+ let result = vt.Result
+ result
\ No newline at end of file
diff --git a/tests/FSharp.Analyzers.Tests/data/syncBlocking/Task Result property.fs.expected b/tests/FSharp.Analyzers.Tests/data/syncBlocking/Task Result property.fs.expected
new file mode 100644
index 0000000..02bfcfd
--- /dev/null
+++ b/tests/FSharp.Analyzers.Tests/data/syncBlocking/Task Result property.fs.expected
@@ -0,0 +1,4 @@
+GRA-SYNCBLOCK-001 | Warning | (7,17 - 7,25) | Synchronous blocking call 'Result' should be avoided. This can cause deadlocks and thread pool starvation. Consider using `let!` in a `task` or `async` computation expression. Suppress with comment including text 'synchronous blocking call allowed'. | []
+GRA-SYNCBLOCK-001 | Warning | (12,4 - 12,12) | Synchronous blocking call 'Result' should be avoided. This can cause deadlocks and thread pool starvation. Consider using `let!` in a `task` or `async` computation expression. Suppress with comment including text 'synchronous blocking call allowed'. | []
+GRA-SYNCBLOCK-001 | Warning | (15,17 - 15,46) | Synchronous blocking call 'Result' should be avoided. This can cause deadlocks and thread pool starvation. Consider using `let!` in a `task` or `async` computation expression. Suppress with comment including text 'synchronous blocking call allowed'. | []
+GRA-SYNCBLOCK-001 | Warning | (20,17 - 20,26) | Synchronous blocking call 'Result' should be avoided. This can cause deadlocks and thread pool starvation. Consider using `let!` in a `task` or `async` computation expression. Suppress with comment including text 'synchronous blocking call allowed'. | []
diff --git a/tests/FSharp.Analyzers.Tests/data/syncBlocking/Task Wait methods.fs b/tests/FSharp.Analyzers.Tests/data/syncBlocking/Task Wait methods.fs
new file mode 100644
index 0000000..2133ad4
--- /dev/null
+++ b/tests/FSharp.Analyzers.Tests/data/syncBlocking/Task Wait methods.fs
@@ -0,0 +1,23 @@
+module TaskWaitMethods
+
+open System.Threading.Tasks
+
+let testTaskWait () =
+ let t = Task.Run(fun () -> 42)
+ t.Wait()
+ t.Result
+
+let testTaskWaitWithTimeout () =
+ let t = Task.Delay(100)
+ t.Wait(1000)
+
+let testTaskWaitAll () =
+ let t1 = Task.Run(fun () -> 1)
+ let t2 = Task.Run(fun () -> 2)
+ Task.WaitAll(t1, t2)
+
+let testTaskWaitAny () =
+ let t1 = Task.Delay(100)
+ let t2 = Task.Delay(200)
+ let index = Task.WaitAny(t1, t2)
+ index
\ No newline at end of file
diff --git a/tests/FSharp.Analyzers.Tests/data/syncBlocking/Task Wait methods.fs.expected b/tests/FSharp.Analyzers.Tests/data/syncBlocking/Task Wait methods.fs.expected
new file mode 100644
index 0000000..e2092a2
--- /dev/null
+++ b/tests/FSharp.Analyzers.Tests/data/syncBlocking/Task Wait methods.fs.expected
@@ -0,0 +1,5 @@
+GRA-SYNCBLOCK-001 | Warning | (7,4 - 7,12) | Synchronous blocking call 'Task.Wait' should be avoided. This can cause deadlocks and thread pool starvation. Consider using `let!` in a `task` or `async` computation expression. Suppress with comment including text 'synchronous blocking call allowed'. | []
+GRA-SYNCBLOCK-001 | Warning | (8,4 - 8,12) | Synchronous blocking call 'Result' should be avoided. This can cause deadlocks and thread pool starvation. Consider using `let!` in a `task` or `async` computation expression. Suppress with comment including text 'synchronous blocking call allowed'. | []
+GRA-SYNCBLOCK-001 | Warning | (12,4 - 12,16) | Synchronous blocking call 'Task.Wait' should be avoided. This can cause deadlocks and thread pool starvation. Consider using `let!` in a `task` or `async` computation expression. Suppress with comment including text 'synchronous blocking call allowed'. | []
+GRA-SYNCBLOCK-001 | Warning | (17,4 - 17,24) | Synchronous blocking call 'Task.WaitAll' should be avoided. This can cause deadlocks and thread pool starvation. Consider using `let!` in a `task` or `async` computation expression. Suppress with comment including text 'synchronous blocking call allowed'. | []
+GRA-SYNCBLOCK-001 | Warning | (22,16 - 22,36) | Synchronous blocking call 'Task.WaitAny' should be avoided. This can cause deadlocks and thread pool starvation. Consider using `let!` in a `task` or `async` computation expression. Suppress with comment including text 'synchronous blocking call allowed'. | []
diff --git a/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/Async operations.fs b/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/Async operations.fs
new file mode 100644
index 0000000..bf518aa
--- /dev/null
+++ b/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/Async operations.fs
@@ -0,0 +1,35 @@
+module AsyncOperations
+
+open System.Threading.Tasks
+
+let testAsyncComputation () = async {
+ let! result = async { return 42 }
+ return result
+}
+
+let testTaskComputation () = task {
+ let! result = Task.Run(fun () -> 42)
+ return result
+}
+
+let testAsyncStartWithContinuation () =
+ let computation = async { return 42 }
+ Async.StartWithContinuations(
+ computation,
+ (fun result -> printfn $"Result: %d{result}"),
+ (fun exn -> printfn $"Error: %O{exn}"),
+ (fun cancel -> printfn "Cancelled")
+ )
+
+let testAsyncStartAsTask () =
+ let computation = async { return 42 }
+ let task = Async.StartAsTask computation
+ task
+
+let testTaskRun () =
+ let t = Task.Run(fun () -> 42)
+ t
+
+let testValueTask () =
+ let vt = ValueTask(42)
+ vt
\ No newline at end of file
diff --git a/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/Async operations.fs.expected b/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/Async operations.fs.expected
new file mode 100644
index 0000000..e69de29
diff --git a/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/Non async methods.fs b/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/Non async methods.fs
new file mode 100644
index 0000000..c9cd6de
--- /dev/null
+++ b/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/Non async methods.fs
@@ -0,0 +1,31 @@
+module NonAsyncMethods
+
+open System
+open System.Collections.Generic
+
+let testRegularMethods () =
+ let list = List()
+ list.Add(42)
+ let count = list.Count
+ let str = "hello"
+ let result = str.ToUpper()
+ let parsed = Int32.Parse("123")
+ parsed
+
+type MyType() =
+ member _.Result = 42
+ member _.GetResult() = "hello"
+ member _.Wait() = ()
+
+let testCustomTypeMethods () =
+ let obj = MyType()
+ let r1 = obj.Result
+ let r2 = obj.GetResult()
+ obj.Wait()
+ r1 + r2.Length
+
+let testUnrelatedAsync () =
+ let asyncValue = Some 42
+ match asyncValue with
+ | Some v -> v
+ | None -> 0
\ No newline at end of file
diff --git a/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/Non async methods.fs.expected b/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/Non async methods.fs.expected
new file mode 100644
index 0000000..e69de29
diff --git a/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/With suppression comment.fs b/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/With suppression comment.fs
new file mode 100644
index 0000000..c7efb40
--- /dev/null
+++ b/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/With suppression comment.fs
@@ -0,0 +1,29 @@
+module WithSuppressionComment
+
+open System.Threading.Tasks
+
+let testAsyncRunSynchronouslyWithComment () =
+ let computation = async { return 42 }
+ // synchronous blocking call allowed
+ let result = Async.RunSynchronously computation
+ result
+
+let testTaskWaitWithComment () =
+ let t = Task.Run(fun () -> 42)
+ t.Wait() // synchronous blocking call allowed for testing
+ t.Result
+
+let testTaskResultWithCommentAbove () =
+ let t = Task.Run(fun () -> "hello")
+ // Synchronous Blocking Call Allowed (case insensitive)
+ t.Result
+
+let testWithBlockComment () =
+ let t = Task.Run(fun () -> 42)
+ (* synchronous blocking call allowed *)
+ t.Wait()
+
+let testGetResultWithComment () =
+ let t = Task.Run(fun () -> 42)
+ let awaiter = t.GetAwaiter()
+ awaiter.GetResult() // synchronous blocking call allowed in main
\ No newline at end of file
diff --git a/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/With suppression comment.fs.expected b/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/With suppression comment.fs.expected
new file mode 100644
index 0000000..e69de29
From a311901099112e179dd213ca0b90f5a77ecf1ae0 Mon Sep 17 00:00:00 2001
From: Smaug123 <3138005+Smaug123@users.noreply.github.com>
Date: Wed, 27 Aug 2025 16:57:32 +0100
Subject: [PATCH 4/6] Changelog
---
CHANGELOG.md | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 736b246..afafde7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,10 @@
# Changelog
+## 0.19.0 - 2025-08-27
+
+### Added
+* New analyzer, SyncBlockingAnalyzer, which bans the use of synchronous blocking methods like `Task.Wait`, unless specifically permitted per-line with a magic comment.
+
## 0.18.0 - 2025-08-27
### Changed
From 5fd36adcf4e285300f329471b035faa7f5559bd1 Mon Sep 17 00:00:00 2001
From: Smaug123 <3138005+Smaug123@users.noreply.github.com>
Date: Wed, 27 Aug 2025 17:04:15 +0100
Subject: [PATCH 5/6] Markups
---
CHANGELOG.md | 2 +-
src/FSharp.Analyzers/Comments.fs | 2 +-
src/FSharp.Analyzers/SyncBlockingAnalyzer.fs | 86 ++++++++-----------
.../negative/With suppression comment.fs | 8 +-
4 files changed, 43 insertions(+), 55 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index afafde7..2b91dab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,7 +3,7 @@
## 0.19.0 - 2025-08-27
### Added
-* New analyzer, SyncBlockingAnalyzer, which bans the use of synchronous blocking methods like `Task.Wait`, unless specifically permitted per-line with a magic comment.
+* New analyzer, SyncBlockingAnalyzer, which bans the use of synchronous blocking methods like `Task.Wait`, unless specifically permitted per-line with a magic comment. [#96](https://github.com/G-Research/fsharp-analyzers/pull/96)
## 0.18.0 - 2025-08-27
diff --git a/src/FSharp.Analyzers/Comments.fs b/src/FSharp.Analyzers/Comments.fs
index 4e76f9e..80f45e2 100644
--- a/src/FSharp.Analyzers/Comments.fs
+++ b/src/FSharp.Analyzers/Comments.fs
@@ -17,6 +17,7 @@ let isSwitchedOffPerComment
comments
|> List.exists (fun c ->
match c with
+ | CommentTrivia.BlockComment r
| CommentTrivia.LineComment r ->
if r.StartLine <> analyzerTriggeredOn.StartLine - 1 then
false
@@ -24,5 +25,4 @@ let isSwitchedOffPerComment
let lineOfComment = sourceText.GetLineString (r.StartLine - 1) // 0-based
lineOfComment.Contains (magicComment, StringComparison.OrdinalIgnoreCase)
- | _ -> false
)
diff --git a/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs b/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs
index 42995a9..907fcde 100644
--- a/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs
+++ b/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs
@@ -1,6 +1,7 @@
module GR.FSharp.Analyzers.SyncBlockingAnalyzer
open System
+open FSharp.Analyzers.Comments
open FSharp.Analyzers.SDK
open FSharp.Analyzers.SDK.TASTCollecting
open FSharp.Compiler.CodeAnalysis
@@ -17,7 +18,7 @@ let SwitchOffComment = "synchronous blocking call allowed"
let problematicMethods =
[
- "Microsoft.FSharp.Control.RunSynchronously" // This is how it appears in the TAST
+ "Microsoft.FSharp.Control.RunSynchronously" // This is how it appears in the TAST
"Microsoft.FSharp.Control.Async.RunSynchronously"
"Microsoft.FSharp.Control.FSharpAsync.RunSynchronously"
"System.Threading.Tasks.Task.Wait"
@@ -32,67 +33,50 @@ let problematicMethods =
let problematicProperties =
[
- "System.Threading.Tasks.Task.get_Result" // Note: F# doesn't include `1 in FullName for generic type property getters
- "System.Threading.Tasks.ValueTask.get_Result" // Same here
+ "System.Threading.Tasks.Task.get_Result" // Note: F# doesn't include `1 in FullName for generic type property getters
+ "System.Threading.Tasks.ValueTask.get_Result" // Same here
]
|> Set.ofList
-let isSwitchedOffPerComment (sourceText: ISourceText) (comments: CommentTrivia list) (range: Range) =
- comments
- |> List.exists (fun c ->
- match c with
- | CommentTrivia.LineComment r ->
- // Check same line or line above
- if r.StartLine = range.StartLine || r.StartLine = range.StartLine - 1 then
- let lineOfComment = sourceText.GetLineString(r.StartLine - 1) // 0-based
- lineOfComment.Contains(SwitchOffComment, StringComparison.OrdinalIgnoreCase)
- else
- false
- | CommentTrivia.BlockComment r ->
- // Check if block comment is on same line or line above
- if r.StartLine = range.StartLine || r.EndLine = range.StartLine - 1 then
- let startLine = sourceText.GetLineString(r.StartLine - 1)
- startLine.Contains(SwitchOffComment, StringComparison.OrdinalIgnoreCase)
- else
- false
- )
-
-let analyze (sourceText: ISourceText) (ast: ParsedInput) (checkFileResults: FSharpCheckFileResults) =
+let analyze (sourceText : ISourceText) (ast : ParsedInput) (checkFileResults : FSharpCheckFileResults) =
let comments =
match ast with
| ParsedInput.ImplFile parsedImplFileInput -> parsedImplFileInput.Trivia.CodeComments
| _ -> []
- let violations = ResizeArray()
+ let violations = ResizeArray ()
let walker =
{ new TypedTreeCollectorBase() with
- override _.WalkCall _ (mfv: FSharpMemberOrFunctionOrValue) _ _ _ (m: range) =
-
+ override _.WalkCall _ (mfv : FSharpMemberOrFunctionOrValue) _ _ _ (m : range) =
+
// Check for regular method calls
if problematicMethods.Contains mfv.FullName then
- if not (isSwitchedOffPerComment sourceText comments m) then
+ if not (isSwitchedOffPerComment SwitchOffComment comments sourceText m) then
let methodName =
- if mfv.DisplayName.Contains(".") then
+ if mfv.DisplayName.Contains '.' then
mfv.DisplayName
- else
+ else if
// Special handling for Async.RunSynchronously
- if mfv.FullName = "Microsoft.FSharp.Control.RunSynchronously" then
- "Async.RunSynchronously"
+ mfv.FullName = "Microsoft.FSharp.Control.RunSynchronously"
+ then
+ "Async.RunSynchronously"
+ else
+ // Get more context for better error messages
+ let parts = mfv.FullName.Split '.'
+
+ if parts.Length >= 2 then
+ $"{parts.[parts.Length - 2]}.{parts.[parts.Length - 1]}"
else
- // Get more context for better error messages
- let parts = mfv.FullName.Split('.')
- if parts.Length >= 2 then
- $"{parts.[parts.Length - 2]}.{parts.[parts.Length - 1]}"
- else
- mfv.DisplayName
- violations.Add(m, "method", methodName)
-
+ mfv.DisplayName
+
+ violations.Add (m, "method", methodName)
+
// Check for property getters (Result property access)
elif problematicProperties.Contains mfv.FullName then
- if not (isSwitchedOffPerComment sourceText comments m) then
+ if not (isSwitchedOffPerComment SwitchOffComment comments sourceText m) then
// For property getters, use just the property name
- violations.Add(m, "property", "Result")
+ violations.Add (m, "property", "Result")
}
match checkFileResults.ImplementationFile with
@@ -104,10 +88,10 @@ let analyze (sourceText: ISourceText) (ast: ParsedInput) (checkFileResults: FSha
{
Type = "SyncBlockingAnalyzer"
Message =
- $"Synchronous blocking call '%s{name}' should be avoided. " +
- "This can cause deadlocks and thread pool starvation. " +
- "Consider using `let!` in a `task` or `async` computation expression. " +
- "Suppress with comment including text 'synchronous blocking call allowed'."
+ $"Synchronous blocking call '%s{name}' should be avoided. "
+ + "This can cause deadlocks and thread pool starvation. "
+ + "Consider using `let!` in a `task` or `async` computation expression. "
+ + "Suppress with comment including text 'synchronous blocking call allowed'."
Code = Code
Severity = Severity.Warning
Range = range
@@ -128,16 +112,16 @@ let HelpUri =
"https://g-research.github.io/fsharp-analyzers/analyzers/SyncBlockingAnalyzer.html"
[]
-let syncBlockingCliAnalyzer: Analyzer =
- fun (ctx: CliContext) ->
+let syncBlockingCliAnalyzer : Analyzer =
+ fun (ctx : CliContext) ->
async { return analyze ctx.SourceText ctx.ParseFileResults.ParseTree ctx.CheckFileResults }
[]
-let syncBlockingEditorAnalyzer: Analyzer =
- fun (ctx: EditorContext) ->
+let syncBlockingEditorAnalyzer : Analyzer =
+ fun (ctx : EditorContext) ->
async {
return
ctx.CheckFileResults
|> Option.map (analyze ctx.SourceText ctx.ParseFileResults.ParseTree)
|> Option.defaultValue []
- }
\ No newline at end of file
+ }
diff --git a/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/With suppression comment.fs b/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/With suppression comment.fs
index c7efb40..53e92e4 100644
--- a/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/With suppression comment.fs
+++ b/tests/FSharp.Analyzers.Tests/data/syncBlocking/negative/With suppression comment.fs
@@ -10,7 +10,10 @@ let testAsyncRunSynchronouslyWithComment () =
let testTaskWaitWithComment () =
let t = Task.Run(fun () -> 42)
- t.Wait() // synchronous blocking call allowed for testing
+ t
+ // ANALYZER: synchronous blocking call allowed for testing
+ |> fun x -> x.Wait()
+ // ANALYZER: synchronous blocking call allowed for testing
t.Result
let testTaskResultWithCommentAbove () =
@@ -26,4 +29,5 @@ let testWithBlockComment () =
let testGetResultWithComment () =
let t = Task.Run(fun () -> 42)
let awaiter = t.GetAwaiter()
- awaiter.GetResult() // synchronous blocking call allowed in main
\ No newline at end of file
+ // synchronous blocking call allowed in main
+ awaiter.GetResult()
\ No newline at end of file
From c41afa57cfda221549766e49d3dc1dd27cd3f551 Mon Sep 17 00:00:00 2001
From: Smaug123 <3138005+Smaug123@users.noreply.github.com>
Date: Wed, 27 Aug 2025 17:30:02 +0100
Subject: [PATCH 6/6] Docs
---
docs/analyzers/SyncBlockingAnalyzer.md | 61 +++++++++++++++++++-
src/FSharp.Analyzers/SyncBlockingAnalyzer.fs | 8 +--
2 files changed, 62 insertions(+), 7 deletions(-)
diff --git a/docs/analyzers/SyncBlockingAnalyzer.md b/docs/analyzers/SyncBlockingAnalyzer.md
index 241ebc2..b8f8fb7 100644
--- a/docs/analyzers/SyncBlockingAnalyzer.md
+++ b/docs/analyzers/SyncBlockingAnalyzer.md
@@ -9,12 +9,71 @@ index:
## Problem
-```fsharp
+Calls to blocking methods and properties like `Task.Wait` and `Task.Result`, or `Async.RunSynchronously`, consume a thread in the thread pool for as long as they are running.
+The .NET runtime tries to cope with this by spinning up new operating system threads if it detects thread pool starvation, but [there is a maximum number of threads](https://learn.microsoft.com/en-us/dotnet/standard/threading/the-managed-thread-pool#maximum-number-of-thread-pool-threads), and attempting to exceed this number will cause the application to deadlock once all threads are blocked waiting for a synchronous call to complete.
+```fsharp
+let doTheThing (input : Task) : int =
+ // This line blocks
+ let input = input.Result
+ input + 1
```
## Fix
+The correct fix depends on context.
+
+## In a library function
+
+Usually the correct answer is to propagate the asynchronous nature of the workflow up into the function signature:
+
+```fsharp
+let doTheThing (input : Task) : Task =
+ task {
+ // Correct: await the asynchronous workflow
+ let! input = input
+ return input + 1
+ }
+```
+
+## In the main method
+F# [does not support async main methods](https://github.com/dotnet/fsharp/issues/11631#issuecomment-855052325), so a blocking call will always be necessary in the entry point.
+
+```fsharp
+[]
+let main argv =
+ doTheThing ()
+ // ANALYZER: synchronous blocking call allowed in entry point
+ |> Async.RunSynchronously
+```
+
+## In tests
+
+Many standard test runners support asynchronous tests out of the box.
+
+Avoid synchronous blocking in tests.
+Test runners may give you a much more restrictive thread pool than you are used to; this can result in heavy slowdowns or deadlocks in tests even when you wouldn't expect to see them in prod.
+
```fsharp
+open NUnit.Framework
+// NUnit supports Task and Async out of the box, for example
+[]
+let myAsyncTest () = task {
+ let! result = computeMyResult ()
+ result |> shouldEqual 8
+}
+```
+
+## If you already have a proof that the access is safe
+
+You might have some proof that a `Task` has already completed at the time you want to access it.
+In that case, just suppress the analyzer at the point of access.
+
+```fsharp
+if myTask.IsCompletedSuccessfully then
+ // SAFETY: synchronous blocking call allowed because task has completed
+ myTask.Result
+else
+ ...
```
diff --git a/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs b/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs
index 907fcde..2e02a90 100644
--- a/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs
+++ b/src/FSharp.Analyzers/SyncBlockingAnalyzer.fs
@@ -19,8 +19,6 @@ let SwitchOffComment = "synchronous blocking call allowed"
let problematicMethods =
[
"Microsoft.FSharp.Control.RunSynchronously" // This is how it appears in the TAST
- "Microsoft.FSharp.Control.Async.RunSynchronously"
- "Microsoft.FSharp.Control.FSharpAsync.RunSynchronously"
"System.Threading.Tasks.Task.Wait"
"System.Threading.Tasks.Task.WaitAll"
"System.Threading.Tasks.Task.WaitAny"
@@ -56,10 +54,8 @@ let analyze (sourceText : ISourceText) (ast : ParsedInput) (checkFileResults : F
let methodName =
if mfv.DisplayName.Contains '.' then
mfv.DisplayName
- else if
- // Special handling for Async.RunSynchronously
- mfv.FullName = "Microsoft.FSharp.Control.RunSynchronously"
- then
+ elif mfv.FullName = "Microsoft.FSharp.Control.RunSynchronously" then
+ // Special handling for Async.RunSynchronously, which has a different name in the TAST
"Async.RunSynchronously"
else
// Get more context for better error messages