Analyzer

SF0004

Flags source methods that route through more than eight abstraction layers because the primary path has been buried under wrappers.

Diagnostic contract

A deep call chain makes it hard to see where the product behavior actually lives. The deeper the path, the more ceremony a reader must traverse before reaching the point of change.

SF0004 SimplicityFirst.PrimaryPathFirst Warning Advisory only
When it fires

The computed source-level call graph shows a method depth above the fixed threshold of 8.

Rule message

Method {0} passes through {1} abstraction layers, exceeding the limit of 8

Help link target

/analyzers/sf0004/

When it fires

These are the concrete cases to look for in code review and IDE diagnostics.

  • The analyzer builds a call graph from source methods and resolves unique dispatch targets for interface and override calls when possible.
  • It reports only when the computed depth exceeds eight layers.
  • Because it reasons over the whole compilation, the diagnostic fires at compilation end instead of per node.

Bad / better example

Use these examples to explain the rule, not just silence it.

Before

public sealed class CheckoutController(CheckoutApplicationService app)
{
    public Task<Result> PostAsync(Request request) => app.HandleAsync(request);
}

public sealed class CheckoutApplicationService(CheckoutCoordinator coordinator)
{
    public Task<Result> HandleAsync(Request request) => coordinator.RunAsync(request);
}

public sealed class CheckoutCoordinator(CheckoutPipeline pipeline)
{
    public Task<Result> RunAsync(Request request) => pipeline.ExecuteAsync(request);
}

// ... Validator -> Mapper -> RepositoryFacade -> Repository -> SqlGateway ...

The business path is technically present, but most of it is spent traversing wrappers.

After

public sealed class CheckoutController(CheckoutHandler handler)
{
    public Task<Result> PostAsync(Request request) => handler.HandleAsync(request);
}

public sealed class CheckoutHandler(OrderRepository repository, PaymentGateway gateway)
{
    public async Task<Result> HandleAsync(Request request)
    {
        var order = await repository.LoadAsync(request.OrderId);
        return await gateway.AuthorizeAsync(order);
    }
}

The reader reaches the real flow quickly, with supporting concerns still available but not piled on top.

Code fix

There is no automatic code fix for this rule today. Treat it as a prompt to simplify deliberately.

  • Look for wrapper classes that simply forward the same arguments to the next layer.
  • Prefer collapsing the path toward a direct handler over adding another “shared abstraction” to hide it.
  • Pair this rule with explicit [PrimaryPath] annotations when conventions are not enough to tell the truth.

Source and follow-up links

Use these links when you need to validate behavior against the source or connect the docs back to project tracking.