Cyclomatic Complexity threshold exceeded (LC0010)
This rule triggers when a procedure’s Cyclomatic Complexity meets or exceeds a configurable threshold. High complexity means more independent execution paths, making the code harder to understand, test exhaustively, and maintain safely.
What triggers this diagnostic?
The analyzer calculates the Cyclomatic Complexity for every procedure and trigger in your AL project. When the calculated score is greater than or equal to the configured threshold, this diagnostic is reported on the procedure.
By default the threshold is 8. This aligns with McCabe’s well-established risk categorization:
| Cyclomatic Complexity | Risk level |
|---|---|
| 1 – 10 | 🟢 Low — simple, easy to test |
| 11 – 20 | 🟡 Moderate — review recommended |
| 21 – 50 | 🟠 High — consider refactoring |
| > 50 | 🔴 Very high — refactoring strongly recommended |
A threshold of 8 provides an early warning before a procedure crosses into moderate risk territory. For a full explanation of how the score is calculated and which AL constructs contribute to it, see LC0009 — Cyclomatic Complexity metric .
Why refactor high-CC code?
Procedures with high Cyclomatic Complexity are statistically more likely to:
- Contain hidden defects — each additional execution path is another place where bugs can hide, and it is easy to overlook edge cases.
- Require more tests — Cyclomatic Complexity represents the minimum number of test cases needed for full branch coverage. A procedure with a CC of 15 needs at least 15 test paths.
- Resist safe modification — even small changes in a complex procedure carry a higher risk of unintended side effects, because the developer must reason about all the interacting paths.
- Slow down development — developers spend more time understanding the code before they can make progress, especially team members who did not write the original code.
- Complicate upgrades — complex procedures are disproportionately likely to cause issues during Business Central major version upgrades.
Addressing high-CC procedures proactively reduces long-term maintenance cost and keeps your extension healthy across upgrade cycles.
Configuration
Adjusting the threshold
Add or update the CyclomaticComplexityThreshold property in the alcops.json file at your project root (alongside app.json):
{
"CyclomaticComplexityThreshold": 10
}
| Property | Type | Default | Description |
|---|---|---|---|
CyclomaticComplexityThreshold | integer | 8 | Procedures with a Cyclomatic Complexity at or above this value trigger a warning. |
Raise the threshold if your team is comfortable with moderately complex procedures (e.g. 10 or 15). Lower it for stricter quality enforcement.
After changing alcops.json in VS Code, reload the window (Ctrl+Shift+P → Developer: Reload Window) for the new settings to take effect.
Controlling severity
Use a .ruleset.json file to change how the diagnostic is reported — or to disable it entirely:
{
"name": "My Project Ruleset",
"rules": [
{
"id": "LC0010",
"action": "Error"
}
]
}
You can also suppress the diagnostic for individual procedures using a #pragma directive:
#pragma warning disable LC0010
procedure LegacyDataMigration()
begin
// Complex but stable legacy code — suppressed intentionally
end;
#pragma warning restore LC0010
See Configuration for more details on all configuration options.
Reducing Cyclomatic Complexity
Each decision point — every if, case line, loop, and logical operator — adds one to the complexity count. The strategies below target these constructs directly.
Extract helper procedures
Break large procedures into smaller, focused helpers. Each extracted procedure carries its own separate complexity score, and the calling procedure becomes simpler.
procedure ProcessDocument(var SalesHeader: Record "Sales Header")
begin
case SalesHeader."Document Type" of
SalesHeader."Document Type"::Order:
ProcessOrder(SalesHeader);
SalesHeader."Document Type"::Quote:
ProcessQuote(SalesHeader);
end;
end;Use early returns
Replace deeply nested if/else chains with guard clauses that exit the procedure early. This eliminates else branches and reduces nesting:
procedure ValidateCustomer(Customer: Record Customer)
begin
if Customer."No." = '' then
exit;
if Customer.Blocked <> Customer.Blocked::" " then
exit;
// Main validation logic — no deep nesting
ValidatePostingSetup(Customer);
end;
Simplify compound conditionals
Extract compound boolean expressions into well-named helper procedures. Each and/or operator adds to CC — moving the logic out keeps the calling procedure simple and makes the condition self-documenting:
procedure IsEligibleForDiscount(Customer: Record Customer): Boolean
begin
exit(
Customer."Customer Posting Group" = 'PREMIUM' and
(Customer."Balance (LCY)" > 10000)
);
end;
Break down large case statements
Each case line adds to Cyclomatic Complexity. If a case statement has many branches, move the body of each branch into its own procedure or consider rethinking the design with enum extensibility:
procedure HandleRecordType(ImportBuffer: Record "Import Buffer")
begin
case ImportBuffer."Record Type" of
'CUSTOMER':
ImportCustomer(ImportBuffer);
'HEADER':
ImportSalesHeader(ImportBuffer);
'LINE':
ImportSalesLine(ImportBuffer);
end;
end;
Example
The following procedure triggers LC0010 because its Cyclomatic Complexity exceeds the default threshold of 8:
procedure ImportData(var ImportBuffer: Record "Import Buffer") // LC0010: Cyclomatic Complexity exceeds threshold
var
Customer: Record Customer;
SalesHeader: Record "Sales Header";
SalesLine: Record "Sales Line";
begin
if ImportBuffer.FindSet() then
repeat
if ImportBuffer."Record Type" = 'CUSTOMER' then begin
if not Customer.Get(ImportBuffer."No.") then begin
Customer.Init();
Customer."No." := ImportBuffer."No.";
Customer.Name := ImportBuffer.Description;
if ImportBuffer."Country Code" <> '' then
Customer."Country/Region Code" := ImportBuffer."Country Code";
Customer.Insert(true);
end else begin
Customer.Name := ImportBuffer.Description;
if ImportBuffer."Country Code" <> '' then
Customer."Country/Region Code" := ImportBuffer."Country Code";
Customer.Modify(true);
end;
end else
if ImportBuffer."Record Type" = 'HEADER' then begin
SalesHeader.Init();
SalesHeader."No." := ImportBuffer."No.";
SalesHeader."Sell-to Customer No." := ImportBuffer."Customer No.";
SalesHeader.Insert(true);
end else
if ImportBuffer."Record Type" = 'LINE' then begin
SalesLine.Init();
SalesLine."Document No." := ImportBuffer."No.";
SalesLine."Line No." := ImportBuffer."Line No.";
SalesLine.Description := ImportBuffer.Description;
SalesLine.Quantity := ImportBuffer.Quantity;
SalesLine.Insert(true);
end;
until ImportBuffer.Next() = 0;
end;After refactoring into focused helper procedures, each procedure has fewer branches and a lower Cyclomatic Complexity:
procedure ImportData(var ImportBuffer: Record "Import Buffer")
begin
if not ImportBuffer.FindSet() then
exit;
repeat
case ImportBuffer."Record Type" of
'CUSTOMER':
ImportCustomer(ImportBuffer);
'HEADER':
ImportSalesHeader(ImportBuffer);
'LINE':
ImportSalesLine(ImportBuffer);
end;
until ImportBuffer.Next() = 0;
end;
local procedure ImportCustomer(ImportBuffer: Record "Import Buffer")
var
Customer: Record Customer;
begin
if not Customer.Get(ImportBuffer."No.") then begin
Customer.Init();
Customer."No." := ImportBuffer."No.";
end;
Customer.Name := ImportBuffer.Description;
if ImportBuffer."Country Code" <> '' then
Customer."Country/Region Code" := ImportBuffer."Country Code";
if Customer.Insert(true) then
exit;
Customer.Modify(true);
end;
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