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 Complexity | Risk level | Interpretation |
|---|---|---|
| 1 – 10 | 🟢 Low | Simple procedure, easy to test and maintain |
| 11 – 20 | 🟡 Moderate | More complex; review recommended |
| 21 – 50 | 🟠 High | Complex; consider refactoring |
| > 50 | 🔴 Very high | Untestable; 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:
| Construct | AL example | Reason |
|---|---|---|
if | if Customer.Find() then | Introduces a true/false branch |
else if | else if Status = Status::Open then | Adds another conditional branch |
and | if (A) and (B) then | Each logical operator is a separate decision (short-circuit evaluation) |
or | if (A) or (B) then | Same as and — each operand can independently determine the outcome |
for | for i := 1 to 10 do | Loop body may or may not execute; creates a branch |
foreach | foreach Permission in Permissions do | Same as for |
while | while not EOF do | Conditional loop entry |
until | repeat ... until Next() = 0 | Conditional loop exit |
Each case line | 'CUSTOMER': inside a case block | Each 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) thenadds 2 to the complexity (one forand, one foror), on top of theifkeyword 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:
| # | Construct | Running total |
|---|---|---|
| — | Base complexity | 1 |
| 1 | if not ImportBuffer.FindSet() | 2 |
| 2 | until ImportBuffer.Next() = 0 | 3 |
| 3 | case line 'CUSTOMER': | 4 |
| 4 | if Customer.Get(...) | 5 |
| 5 | if ImportBuffer."Country Code" <> '' | 6 |
| 6 | case line 'HEADER': | 7 |
| 7 | if 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
casestatement with ten branches and a deeply nested chain of tenif/elseblocks 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.
Related content
- Cyclomatic complexity — Wikipedia
- Code Metrics — Cyclomatic Complexity — Microsoft Learn
- Cyclomatic Complexity — SonarSource
- The Bumpy Road Code Complexity in Context — CodeScene
- Cyclomatic Complexity as a Code Quality Metric — itnext.io
- Code Metrics — Maintainability Index range and meaning — Microsoft Learn