Deeply nested loops (three or more levels)
ID |
vbnet.maintainability.deep_nesting |
Severity |
high |
Remediation Complexity |
hard |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Complexity |
Language |
VB.NET |
Description
Reports a loop that has two further loops nested inside it. The threshold is fixed at three levels, so a pair of nested loops is not reported, and neither are sibling loops that sit at the same level inside a common parent. The counted forms are For, For Each, While, Do While and Do Until, in any combination; a condition-less Do … Loop does not take part in the count and breaks the chain if it sits in the middle of one. A nest deeper than three levels raises one finding per level that still has two loops below it - four levels give two findings, the second contained inside the first - so the report shows how deep the nest goes rather than only where it starts.
Rationale
Nesting depth is the one metric where reading cost and running cost grow together. To follow three nested loops a reader has to hold three loop variables, three collections and three exit conditions in mind at the same time, and to work out which loop an Exit For or Continue For leaves - in Visual Basic it leaves only the innermost one, which is easy to misread and easy to get wrong when editing. The same shape usually means the body runs a product of the three collection sizes, so a method that is fast on test data becomes the slowest part of the system once the collections grow. Because everything happens in one procedure, the inner levels also have unrestricted access to the outer loop variables, which is how accumulator and off-by-one defects survive review here.
The following code illustrates the pattern detected by this rule:
Public Function TotalUnits(ByVal regions As List(Of Region)) As Integer
Dim total As Integer = 0
' FLAGGED: Deeply nested loops (three or more levels)
For r As Integer = 0 To regions.Count - 1
For Each store As Store In regions(r).Stores
While store.HasPendingOrders
total += store.NextOrder().Units
End While
Next
Next
Return total
End Function
Remediation
Extract the inner levels into helper procedures named after what they do, so each procedure deals with a single level and can be read and tested on its own: replace the innermost loop with a call such as total += DrainOrders(store). Where the loops only walk a hierarchy to reach the leaves, flatten the walk instead with a query over the collections - SelectMany produces one sequence of leaves that a single For Each can iterate. If the nesting exists to combine every element with every other, consider whether an index, dictionary or join can replace the innermost scan altogether.