All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.sonar.plugins.csharp.S3776.html Maven / Gradle / Ivy

There is a newer version: 10.2.0.105762
Show newest version

This rule raises an issue when the code cognitive complexity of a function is above a certain threshold.

Why is this an issue?

Cognitive Complexity is a measure of how hard it is to understand the control flow of a unit of code. Code with high cognitive complexity is hard to read, understand, test, and modify.

As a rule of thumb, high cognitive complexity is a sign that the code should be refactored into smaller, easier-to-manage pieces.

Which syntax in code does impact cognitive complexity score?

Here are the core concepts:

  • Cognitive complexity is incremented each time the code breaks the normal linear reading flow.
    This concerns, for example, loop structures, conditionals, catches, switches, jumps to labels, and conditions mixing multiple operators.
  • Each nesting level increases complexity.
    During code reading, the deeper you go through nested layers, the harder it becomes to keep the context in mind.
  • Method calls are free
    A well-picked method name is a summary of multiple lines of code. A reader can first explore a high-level view of what the code is performing then go deeper and deeper by looking at called functions content.
    Note: This does not apply to recursive calls, those will increment cognitive score.

The method of computation is fully detailed in the pdf linked in the resources.

What is the potential impact?

Developers spend more time reading and understanding code than writing it. High cognitive complexity slows down changes and increases the cost of maintenance.

How to fix it

Reducing cognitive complexity can be challenging.
Here are a few suggestions:

  • Extract complex conditions in a new function.
    Mixed operators in condition will increase complexity. Extracting the condition in a new function with an appropriate name will reduce cognitive load.
  • Break down large functions.
    Large functions can be hard to understand and maintain. If a function is doing too many things, consider breaking it down into smaller, more manageable functions. Each function should have a single responsibility.
  • Avoid deep nesting by returning early.
    To avoid the nesting of conditions, process exceptional cases first and return early.
  • Use null-safe operations (if available in the language).
    When available the .? or ?? operator replaces multiple tests and simplifies the flow.

Code examples

Extraction of a complex condition in a new function.

Noncompliant code example

The code is using a complex condition and has a cognitive cost of 3.

decimal CalculateFinalPrice(User user, Cart cart)
{
    decimal total = CalculateTotal(cart);
    if (user.HasMembership()               // +1 (if)
        && user.OrdersCount > 10           // +1 (more than one condition)
        && user.AccountActive
        && !user.HasDiscount
        || user.OrdersCount == 1)          // +1 (change of operator in condition)
    {

        total = ApplyDiscount(user, total);
    }
    return total;
}

Compliant solution

Even if the cognitive complexity of the whole program did not change, it is easier for a reader to understand the code of the calculateFinalPrice function, which now only has a cognitive cost of 1.

decimal CalculateFinalPrice(User user, Cart cart)
{
    decimal total = CalculateTotal(cart);
    if (IsEligibleForDiscount(user))       // +1 (if)
    {
        total = applyDiscount(user, total);
    }
    return total;
}

bool IsEligibleForDiscount(User user)
{
    return user.HasMembership()
            && user.OrdersCount > 10       // +1 (more than one condition)
            && user.AccountActive
            && !user.HasDiscount
            || user.OrdersCount == 1;      // +1 (change of operator in condition)
}

Break down large functions.

Noncompliant code example

For example, consider a function that calculates the total price of a shopping cart, including sales tax and shipping.
Note: The code is simplified here, to illustrate the purpose. Please imagine there is more happening in the foreach loops.

decimal CalculateTotal(Cart cart)
{
    decimal total = 0;
    foreach (Item item in cart.Items) // +1 (foreach)
    {
        total += item.Price;
    }

    // calculateSalesTax
    foreach (Item item in cart.Items) // +1 (foreach)
    {
        total += 0.2m * item.Price;
    }

    //calculateShipping
    total += 5m * cart.Items.Count;

    return total;
}

This function could be refactored into smaller functions: The complexity is spread over multiple functions and the complex CalculateTotal has now a complexity score of zero.

Compliant solution

decimal CalculateTotal(Cart cart)
{
    decimal total = 0;
    total = CalculateSubtotal(cart, total);
    total += CalculateSalesTax(cart, total);
    total += CalculateShipping(cart, total);
    return total;
}

decimal CalculateSubtotal(Cart cart, decimal total)
{
    foreach (Item item in cart.Items) // +1 (foreach)
    {
        total += item.Price;
    }

    return total;
}

decimal CalculateSalesTax(Cart cart, decimal total)
{
    foreach (Item item in cart.Items)  // +1 (foreach)
    {
        total += 0.2m * item.Price;
    }

    return total;
}

decimal CalculateShipping(Cart cart, decimal total)
{
    total += 5m * cart.Items.Count;
    return total;
}

Avoid deep nesting by returning early.

Noncompliant code example

The below code has a cognitive complexity of 6.

decimal CalculateDiscount(decimal price, User user)
{
    if (IsEligibleForDiscount(user))    // +1 ( if )
    {
        if (user.HasMembership())       // +2 ( nested if )
        {
            return price * 0.9m;
        }
        else if (user.OrdersCount == 1) // +1 ( else )
        {
            return price * 0.95m;
        }
        else                            // +1 ( else )
        {
            return price;
        }
    }
    else                                // +1 ( else )
    {
        return price;
    }
}

Compliant solution

Checking for the edge case first flattens the if statements and reduces the cognitive complexity to 3.

decimal CalculateDiscount(decimal price, User user)
{
    if (!IsEligibleForDiscount(user)) // +1 ( if )
    {
        return price;
    }

    if (user.HasMembership())         // +1 (  if )
    {
        return price * 0.9m;
    }

    if (user.OrdersCount == 1)        // +1 ( else )
    {
        return price * 0.95m;
    }

    return price;
}

Use the null-conditional operator to access data.

In the below code, the cognitive complexity is increased due to the multiple checks required to access the manufacturer’s name. This can be simplified using the optional chaining operator.

Noncompliant code example

string GetManufacturerName(Product product)
{
    string manufacturerName = null;
    if (product != null && product.Details != null &&
        product.Details.Manufacturer != null) // +1 (if) +1 (multiple condition)
    {
        manufacturerName = product.Details.Manufacturer.Name;
    }

    if (manufacturerName != null) // +1 (if)
    {
        return manufacturerName;
    }

    return "Unknown";
}

Compliant solution

The optional chaining operator will return null if any reference in the chain is null, avoiding multiple checks. The ?? operator allows to provide the default value to use.

string GetManufacturerName(Product product)
{
    return product?.Details?.Manufacturer?.Name ?? "Unknown";
}

Pitfalls

As this code is complex, ensure that you have unit tests that cover the code before refactoring.

Resources

Documentation

Articles & blog posts





© 2015 - 2024 Weber Informatics LLC | Privacy Policy