WoofWare.FSharpAnalyzers
0.2.9
dotnet add package WoofWare.FSharpAnalyzers --version 0.2.9
NuGet\Install-Package WoofWare.FSharpAnalyzers -Version 0.2.9
<PackageReference Include="WoofWare.FSharpAnalyzers" Version="0.2.9"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="WoofWare.FSharpAnalyzers" Version="0.2.9" />
<PackageReference Include="WoofWare.FSharpAnalyzers"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add WoofWare.FSharpAnalyzers --version 0.2.9
#r "nuget: WoofWare.FSharpAnalyzers, 0.2.9"
#:package WoofWare.FSharpAnalyzers@0.2.9
#addin nuget:?package=WoofWare.FSharpAnalyzers&version=0.2.9
#tool nuget:?package=WoofWare.FSharpAnalyzers&version=0.2.9
WoofWare.FSharpAnalyzers
A set of F# source analyzers, using the Ionide analyzer SDK.
They are modelled on the G-Research analyzers, but are much more opinionated. They're intended for my personal use.
If you find false negatives or false positives, please do raise GitHub issues! (Though I reserve the right just to say I'm happy with the status quo.)
Analyzers
MissingCancellationTokenAnalyzer
Prompts you to use overloads of Task/ValueTask-returning methods that take CancellationTokens.
Use the suppression comment "fsharpanalyzer: ignore-line WOOF-MISSING-CT" to suppress the analyzer.
Rationale
.NET's cooperative multitasking requires you to thread CancellationTokens around the code if you want your asynchronous operations to be cancellable.
Nevertheless, idiomatic .NET APIs let you simply not do that by default, which means by default your code won't be cancellable.
This analyzer detects when you've fallen into that pit of failure.
BlockingCallsAnalyzer
Bans the use of blocking calls like Async.RunSynchronously.
You will have to have a blocking call in your main method; use the suppression comment "fsharpanalyzer: ignore-line WOOF-BLOCKING" to guard that line.
Rationale
Prevent sync-over-async.
SuppressThrowingGenericAnalyzer
Detects use of ConfigureAwaitOptions.SuppressThrowing with generic Task<TResult>.
(For some reason, ValueTask doesn't accept ConfigureAwaitOptions.)
Use the suppression comment "fsharpanalyzer: ignore-line WOOF-SUPPRESS-THROWING-GENERIC" to suppress the analyzer; but note that Microsoft says this code pattern is always wrong.
Rationale
The ConfigureAwaitOptions.SuppressThrowing option is not supported by generic Task<TResult> because it can lead to returning an invalid TResult when an exception occurs. This is a runtime error that should be caught at build time.
If you need to use SuppressThrowing, cast the task to non-generic Task first:
let t = Task.FromResult(42)
// Bad: will fail at runtime
let! _ = t.ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing)
// Good: cast to Task first
do! (t :> Task).ConfigureAwait(ConfigureAwaitOptions.SuppressThrowing)
This analyzer mirrors the C# analyzer CA2261, surfacing the error at build time rather than run time.
Limitations
We don't try hard to follow e.g. options defined through a let-binding, so false negatives are possible.
ReferenceEqualsAnalyzer
Bans the use of Object.ReferenceEquals.
Use the suppression comment "fsharpanalyzer: ignore-line WOOF-REFEQUALS" to suppress the analyzer.
(If you define a type-safe version of ReferenceEquals - see the next section - then you will have to specify the suppression inside that function.)
Rationale
Object.ReferenceEquals has two significant problems:
It silently does the wrong thing on value types. When you pass value types to
Object.ReferenceEquals, they get boxed, and the function compares the boxed instances rather than the original values. This meansObject.ReferenceEquals(42, 42)will always returnfalse, which is rarely what you want.It lacks type safety. The function accepts any two objects, making it too easy to accidentally compare objects of completely different types (e.g.,
Object.ReferenceEquals("hello", 42)), which will always returnfalsebut compiles without warning.
Instead, use a type-safe wrapper that enforces reference type constraints and type consistency:
let referenceEquals<'a when 'a : not struct> (x : 'a) (y : 'a) : bool =
obj.ReferenceEquals(x, y)
This prevents both issues: the not struct constraint prevents value types from being passed, and the type parameter 'a ensures both arguments are the same type.
StreamReadAnalyzer
Detects calls to Stream.Read and Stream.ReadAsync where the return value is explicitly ignored.
Use the suppression comment fsharpanalyzer: ignore-line WOOF-STREAM-READ to suppress the analyzer.
Rationale
Stream.Read and Stream.ReadAsync are not guaranteed to read the requested number of bytes. They may return fewer bytes than requested for various reasons:
- End of stream is reached
- Network conditions (for network streams)
- Implementation-specific buffering
This can lead to subtle bugs where code assumes a full buffer was read when only partial data was actually received. Always check the return value to determine how many bytes were actually read.
If you need to read an exact number of bytes and throw if fewer are available, use Stream.ReadExactly or Stream.ReadExactlyAsync instead (available in .NET 7+).
What this analyzer detects
The analyzer flags these specific patterns where the return value is discarded:
- Piping to
ignore:stream.Read(...) |> ignoreorstream.ReadAsync(...) |> ignore - Calling
ignoredirectly:ignore (stream.Read(...))orignore (stream.ReadAsync(...)) - Assignment to underscore (synchronous):
let _ = stream.Read(...) - Unused binding in computation expressions:
let! _ = stream.ReadAsync(...)or anylet!binding of aStream.ReadAsynccall where the bound result is not used
TaskCompletionSourceAnalyzer
Requires TaskCompletionSource<T> to be created with TaskCreationOptions.RunContinuationsAsynchronously.
Use the suppression comment fsharpanalyzer: ignore-line WOOF-TCS-ASYNC to suppress the analyzer.
Rationale
By default, when you call SetResult, SetException, or SetCanceled on a TaskCompletionSource<T>, any continuations attached to the resulting task will run synchronously on the thread that completes the task. This can lead to serious problems:
Deadlocks: If the continuation tries to acquire a lock or synchronization context that the calling thread holds, you get a deadlock.
Thread-pool starvation: Continuations may perform long-running work, blocking the thread that called
SetResultand preventing it from doing other work.State corruption: The continuation runs with the calling thread's execution context, which may have unexpected side effects like running on a UI thread or within a specific synchronization context.
Always create TaskCompletionSource<T> with TaskCreationOptions.RunContinuationsAsynchronously to ensure continuations are scheduled asynchronously on the thread pool:
let tcs = TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously)
EarlyReturnAnalyzer
Detects return and return! expressions used in non-terminal positions inside async { }, task { }, and related computation expressions.
Use the suppression comment fsharpanalyzer: ignore-line WOOF-EARLY-RETURN to suppress the analyzer.
Rationale
In computation expressions, return simply builds a value for the builder; it does not exit the computation the way imperative languages do.
Any code following the return (other statements, loop iterations, finally blocks, etc) still runs.
Here is a concrete computation expression and desugaring that demonstrates the problem:
async {
if true then
return ()
printfn "hi!"
return ()
}
fun () ->
async.Combine (
if true then
async.Return ()
else
async.Zero ()
,
async.Delay (fun () ->
printfn "hi!"
async.Return ()
)
)
|> async.Delay
Notice that the initial if branch has not caused any kind of short-circuiting; we continue unconditionally to the Delay.
This analyzer highlights those non-terminal return calls so you can restructure the logic using explicit if/else or match patterns that clearly indicate what happens in every branch.
(GPT-5 wanted me to clarify this point, although I think nobody would expect different behaviour: we don't flag return statements after a use call, even though disposal is code that runs after the return statement. Leaving the scope by any means, including a return, should intuitively trigger the disposal; which is indeed what happens.)
ThrowingInDisposeAnalyzer
Bans throwing in a Dispose implementation.
Use the suppression comment "fsharpanalyzer: ignore-line WOOF-THROWING-DISPOSE" to suppress the analyzer.
Rationale
See the C# analyzer.
Basically, users find it very confusing when a finally clause still needs exception handling inside it.
Any Dispose (isDisposing = false) code path (conventionally called by the finaliser thread) is especially bad to throw in, because such errors are completely recoverable.
ReturnBangOnlyAnalyzer
Detects computation expressions that consist only of return!, which is likely unnecessary indirection.
Use the suppression comment "fsharpanalyzer: ignore-line WOOF-RETURN-BANG-ONLY" to suppress the analyzer.
Rationale
A computation expression like async { return! x } where x is already an Async<'T> is almost always unnecessary indirection - you can just use x directly. For example:
// Unnecessary indirection
let simpleAsync (x : Async<int>) = async { return! x }
// Better - just use the value directly
let simpleAsync (x : Async<int>) = x
They're not quite the same, because the return! phrasing can involve a call to builder.Delay and/or builder.Run, but I claim that this is a baroque and needlessly clever way to phrase that.
If you genuinely need the Delay behavior for laziness, consider making this explicit with a function or documenting why the indirection is necessary.
Licence
WoofWare.FSharpAnalyzers is licensed to you under the MIT licence, a copy of which can be found at LICENSE.md.
Learn more about Target Frameworks and .NET Standard.
This package has no dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.2.9 | 168 | 11/25/2025 |
| 0.2.8 | 171 | 11/25/2025 |
| 0.2.7 | 163 | 11/24/2025 |
| 0.2.6 | 166 | 11/23/2025 |
| 0.2.5 | 163 | 11/23/2025 |
| 0.2.4 | 129 | 11/23/2025 |
| 0.2.3 | 380 | 11/20/2025 |
| 0.2.2 | 298 | 11/12/2025 |
| 0.2.1 | 203 | 10/20/2025 |
| 0.1.5 | 166 | 10/19/2025 |
| 0.1.4 | 164 | 10/19/2025 |
| 0.1.3 | 385 | 10/4/2025 |
| 0.1.2 | 121 | 10/3/2025 |
| 0.1.1 | 156 | 10/3/2025 |