Skip to main content

Analyzers

Install the optional analyzer package in each project that builds Kevlar pipelines:

dotnet add package Kevlar.Analyzers

The analyzers report only hazards that are provable from the current expression or a stable local initializer. They do not guess what a factory method returns or follow a local that is reassigned. Generated code is ignored.

RuleSeverityHazard
KEV001Warningexecution delegate ignores its CancellationToken
KEV002Warningknown multi-attempt hedging pipeline is passed to synchronous Execute
KEV003Warninginner fallback makes retry, hedging, or circuit breaker unreachable
KEV004Warningstateful shield or partition provider is constructed for one execution

KEV001: ignored execution cancellation

Kevlar cancels the token passed to an execution delegate when the caller cancels, a timeout expires, or a hedge loses. Work that ignores the token can continue after the shield has returned.

await shield.ExecuteAsync(ct => client.GetAsync(url)); // KEV001
await shield.ExecuteAsync(ct => client.GetAsync(url, ct)); // clean

Pass the token to cancellable work. Name it _ only when the operation is truly synchronous or uncancellable and ignoring cancellation is deliberate.

KEV002: synchronous hedging

Multi-attempt hedging races concurrent attempts and therefore supports only asynchronous execution. Synchronous Execute throws for a shield containing a hedge with a known MaxAttempts greater than one, so use ExecuteAsync or remove hedging:

var hedged = Shield.Hedge(2, TimeSpan.FromMilliseconds(50));

var value = await hedged.ExecuteAsync(ct => LoadAsync(ct)); // clean

The rule recognizes direct fluent chains, composed shields, and stable local aliases, including typed shields and extension-method syntax. It deliberately skips opaque factory results, reassigned locals, and hedging whose configured attempt count is not a compile-time constant.

KEV003: unreachable reactive strategy

A fallback placed inside retry, hedging, or circuit breaker under the same handling clause consumes every handled failure before the outer strategy sees it. Kevlar rejects that pipeline at construction time. Put fallback first so it wraps the reactive strategy:

var shield = Shield.For<int>()
.Fallback(-1)
.Retry(3);

Alternatively, start a new, narrower handling clause before the inner fallback when layered recovery is intentional:

var shield = Shield.For<int>()
.Retry(3)
.When<TimeoutExceededException>()
.Fallback(-1);

No automatic code fix is supplied for KEV002 or KEV003: changing sync control flow or strategy order can change application semantics.

KEV004: per-execution stateful shields

Circuit breakers, rate limiters, concurrency limiters, and partition providers retain state between executions. Constructing one immediately before Execute, ExecuteAsync, or ExecuteOutcomeAsync discards that state after one call, so a circuit never accumulates failures and limiter capacity is not shared:

// KEV004: a new circuit exists for only this call.
await Shield.CircuitBreaker(5, TimeSpan.FromSeconds(30))
.ExecuteAsync(static _ => ValueTask.CompletedTask);
// Clean: every call shares the same circuit.
private static readonly Shield _dependencyShield =
Shield.CircuitBreaker(5, TimeSpan.FromSeconds(30));

await _dependencyShield.ExecuteAsync(static _ => ValueTask.CompletedTask);

Store stateful shields in a field, singleton or keyed dependency-injection registration, or registry. Store PartitionedShield<TKey> and PartitionedShield<TKey, TResult> providers for the same reason: their retained per-key shields disappear when the provider is constructed per call.

The rule is deliberately conservative. It reports inline construction and a stable local or local alias that has exactly one use in the same method or lambda. Fields, parameters, opaque factory results, locals with multiple uses, locals captured by nested lambdas, stateless-only pipelines, generated code, and assemblies ending in .Test or .Tests remain clean. Custom Strategy instances are not assumed to be stateful because that cannot be proven from their public contract. Test methods marked with standard TUnit, xUnit, NUnit, or MSTest attributes are also ignored.

Suppression

Suppress one reviewed site with ordinary C# warning pragmas and record why the analyzer cannot see the wider invariant:

#pragma warning disable KEV004 // This isolated circuit is intentional in a one-shot diagnostic.
var value = Shield.CircuitBreaker(1, TimeSpan.FromSeconds(1)).Execute(_ => 1);
#pragma warning restore KEV004

To suppress a rule project-wide, append its ID to NoWarn:

<NoWarn>$(NoWarn);KEV004</NoWarn>

Prefer a narrow pragma. Project-wide suppression can hide newly introduced unsafe call sites.