Cyclomatic Complexity metric (LC0009)

This rule reports the calculated Cyclomatic Complexity for every procedure and trigger, allowing developers to identify overly complex code. The companion rule LC0010 triggers a warning when the complexity meets or exceeds a configurable threshold.

What is Cyclomatic Complexity?

Cyclomatic Complexity is a software metric that quantifies the number of linearly independent paths through a section of source code. It was introduced by Thomas J. McCabe Sr. in 1976 and has since become one of the most widely adopted complexity metrics in the software industry.

The idea is straightforward: every decision point in a procedure — an if, a loop, or a case branch — introduces a new path that code execution can follow. The more paths there are, the harder the code is to understand, test, and maintain. A procedure with a Cyclomatic Complexity of 1 has only a single path through it (no decisions at all), while a procedure with a complexity of 15 has fifteen independent paths that a developer — and a test suite — must account for.

Formally, Cyclomatic Complexity is defined using the control-flow graph of a program:

M = E − N + 2P

Where E is the number of edges, N is the number of nodes, and P is the number of connected components. For a single procedure P = 1, which simplifies to M = E − N + 2. In practice, this is equivalent to counting the number of decision points and adding one.

Why it matters for AL development

Business Central AL extensions often have a long lifespan. Code written today may be maintained through multiple major version upgrades and by developers who did not author it originally. Tracking Cyclomatic Complexity helps teams:

  • Gauge testing effort — Cyclomatic Complexity represents the minimum number of test cases needed to cover every branch. A procedure with a complexity of 12 requires at least 12 test paths for full branch coverage.
  • Detect deteriorating code early — before a procedure becomes too expensive to safely change.
  • Prioritize refactoring — by surfacing the procedures with the most decision paths.
  • Guard upgrade readiness — complex procedures are disproportionately likely to cause issues during Business Central major version upgrades.
  • Support onboarding — new team members can ramp up faster on code with lower complexity.

Risk scale

McCabe’s original categorization, later corroborated by the NIST Structured Testing methodology, provides a useful rule of thumb:

Cyclomatic ComplexityRisk levelInterpretation
1 – 10🟢 LowSimple procedure, easy to test and maintain
11 – 20🟡 ModerateMore complex; review recommended
21 – 50🟠 HighComplex; consider refactoring
> 50🔴 Very highUntestable; refactoring strongly recommended

The NIST publication notes: “The original limit of 10 as proposed by McCabe has significant supporting evidence, but limits as high as 15 have been used successfully as well.” The default threshold for LC0010 in this analyzer is 8, providing an early warning before complexity becomes problematic.

How Cyclomatic Complexity is calculated in AL

Every procedure starts with a base complexity of 1 (the single default path). Each of the following AL constructs adds one to the count:

ConstructAL exampleReason
ifif Customer.Find() thenIntroduces a true/false branch
else ifelse if Status = Status::Open thenAdds another conditional branch
andif (A) and (B) thenEach logical operator is a separate decision (short-circuit evaluation)
orif (A) or (B) thenSame as and — each operand can independently determine the outcome
forfor i := 1 to 10 doLoop body may or may not execute; creates a branch
foreachforeach Permission in Permissions doSame as for
whilewhile not EOF doConditional loop entry
untilrepeat ... until Next() = 0Conditional loop exit
Each case line'CUSTOMER': inside a case blockEach case label is a distinct path
Ternary ?:x := if(Condition, ValueA, ValueB)Inline conditional — equivalent to an if/else

Note: Compound conditions count each logical operator separately. The expression if (A) and (B) or (C) then adds 2 to the complexity (one for and, one for or), on top of the if keyword itself.

Example

The following procedure has a Cyclomatic Complexity of 10 — above the default threshold of 8:

procedure ProcessImportBuffer(var ImportBuffer: Record "Import Buffer")
var
    Customer: Record Customer;
    SalesHeader: Record "Sales Header";
begin
    if not ImportBuffer.FindSet() then
        exit;

    repeat
        case ImportBuffer."Record Type" of
            'CUSTOMER':
                if Customer.Get(ImportBuffer."No.") then
                    Customer.Modify(true)
                else begin
                    Customer.Init();
                    Customer."No." := ImportBuffer."No.";
                    if ImportBuffer."Country Code" <> '' then
                        Customer."Country/Region Code" := ImportBuffer."Country Code";
                    Customer.Insert(true);
                end;
            'HEADER':
                if ImportBuffer."Document Type" = 'ORDER' then
                    CreateSalesOrder(ImportBuffer)
                else
                    CreateSalesQuote(ImportBuffer);
        end;
    until ImportBuffer.Next() = 0;
end;

Walking through the calculation:

#ConstructRunning total
Base complexity1
1if not ImportBuffer.FindSet()2
2until ImportBuffer.Next() = 03
3case line 'CUSTOMER':4
4if Customer.Get(...)5
5if ImportBuffer."Country Code" <> ''6
6case line 'HEADER':7
7if ImportBuffer."Document Type" = 'ORDER'8

Total: 8

After extracting the case branches into focused helper procedures, each procedure has a lower Cyclomatic Complexity and is easier to test independently:

procedure ProcessImportBuffer(var ImportBuffer: Record "Import Buffer")
begin
    if not ImportBuffer.FindSet() then
        exit;

    repeat
        case ImportBuffer."Record Type" of
            'CUSTOMER':
                ProcessCustomer(ImportBuffer);
            'HEADER':
                ProcessHeader(ImportBuffer);
        end;
    until ImportBuffer.Next() = 0;
end;

local procedure ProcessCustomer(ImportBuffer: Record "Import Buffer")
var
    Customer: Record Customer;
begin
    if Customer.Get(ImportBuffer."No.") then begin
        Customer.Modify(true);
        exit;
    end;

    Customer.Init();
    Customer."No." := ImportBuffer."No.";
    if ImportBuffer."Country Code" <> '' then
        Customer."Country/Region Code" := ImportBuffer."Country Code";
    Customer.Insert(true);
end;

local procedure ProcessHeader(ImportBuffer: Record "Import Buffer")
begin
    if ImportBuffer."Document Type" = 'ORDER' then
        CreateSalesOrder(ImportBuffer)
    else
        CreateSalesQuote(ImportBuffer);
end;

Limitations

Like any single metric, Cyclomatic Complexity has limitations worth keeping in mind:

  • Blind to nesting depth — A flat case statement with ten branches and a deeply nested chain of ten if/else blocks produce the same Cyclomatic Complexity, yet the nested version is far harder to understand. This is because Cyclomatic Complexity counts decision points, not how they are structured. CodeScene calls this the “Bumpy Road” code smell — functions with multiple chunks of nested logic that tax working memory beyond its limits.
  • Does not measure cognitive load — Two procedures with identical complexity scores can differ greatly in readability depending on naming, structure, and domain fit. Cognitive Complexity (a metric introduced by SonarSource) attempts to address this by weighting nesting depth, but is a separate measure.
  • Insensitive to code length — A 200-line procedure and a 20-line procedure with the same number of branches will have the same Cyclomatic Complexity, even though the longer one is harder to navigate.
  • Not a complete picture — Cyclomatic Complexity does not capture naming quality, architectural fitness, domain correctness, or how well the code integrates with Business Central patterns.

Use Cyclomatic Complexity as one signal among many. A high CC is a strong hint that a procedure deserves attention, but a low CC alone does not guarantee good design. Pair it with the Maintainability Index (LC0007) for a more rounded assessment.

See also

  • LC0010 — Cyclomatic Complexity threshold exceeded
  • LC0007 — Maintainability Index metric
  • LC0008 — Maintainability Index threshold exceeded