Skip to main content

forge_lint/sol/info/
cyclomatic_complexity.rs

1use super::CyclomaticComplexity;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::sema::{
7    Gcx,
8    hir::{self, Expr, ExprKind, Hir, Stmt, StmtKind, Visit},
9};
10use std::{convert::Infallible, ops::ControlFlow};
11
12declare_forge_lint!(
13    CYCLOMATIC_COMPLEXITY,
14    Severity::Info,
15    "cyclomatic-complexity",
16    "function has a cyclomatic complexity above 11"
17);
18
19/// The threshold Slither's detector of the same name uses: a function reports when its
20/// complexity is strictly above this value.
21const MAX_COMPLEXITY: usize = 11;
22
23impl<'gcx> LateLintPass<'gcx> for CyclomaticComplexity {
24    fn check_function(
25        &mut self,
26        ctx: &LintContext,
27        gcx: Gcx<'gcx>,
28        func: &'gcx hir::Function<'gcx>,
29    ) {
30        // Modifier definitions are never reported, matching Slither which iterates only
31        // declared and top-level functions. Yul helpers declared inside `assembly {}` DO
32        // report: Slither scores them as functions of their own.
33        if func.kind == hir::FunctionKind::Modifier || func.body.is_none() {
34            return;
35        }
36        // Visiting the whole function rather than only the body statements also counts
37        // decision points in modifier-invocation and base-constructor call arguments. For a
38        // structured program the complexity is one plus the decision points.
39        let mut counter = DecisionCounter { hir: &gcx.hir, decisions: 0 };
40        let _ = counter.visit_function(func);
41        if counter.decisions + 1 > MAX_COMPLEXITY {
42            // A Yul helper's span starts at its name rather than a `function` keyword.
43            let span = match func.name {
44                Some(name) if func.is_yul => name.span,
45                _ => func.keyword_span(),
46            };
47            ctx.emit(&CYCLOMATIC_COMPLEXITY, span);
48        }
49    }
50}
51
52/// Counts the decision points of a function body. For a structured program the cyclomatic
53/// complexity `E - N + 2P` of the control-flow graph equals one plus the number of decision
54/// points, so no graph needs building.
55///
56/// Loops count through their condition: solar desugars every `for`, `while` and `do while`
57/// into `Loop { ... if (cond) ... }`, so the synthetic `if` carries the loop's decision and a
58/// condition-less `for (;;)` correctly adds nothing. Boolean `&&` / `||` operators are not
59/// counted, matching the control-flow graph Slither computes on.
60struct DecisionCounter<'gcx> {
61    hir: &'gcx Hir<'gcx>,
62    decisions: usize,
63}
64
65impl<'gcx> Visit<'gcx> for DecisionCounter<'gcx> {
66    type BreakValue = Infallible;
67
68    fn hir(&self) -> &'gcx Hir<'gcx> {
69        self.hir
70    }
71
72    fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Self::BreakValue> {
73        self.decisions += match &stmt.kind {
74            StmtKind::If(..) => 1,
75            // The first clause is the `returns` one; each `catch` clause is a branch.
76            StmtKind::Try(stmt_try) => stmt_try.clauses.len().saturating_sub(1),
77            // Each non-default case of a Yul switch is a branch; the `default` clause
78            // (`constant == None`) opens no decision of its own.
79            StmtKind::Switch(switch) => {
80                switch.cases.iter().filter(|c| c.constant.is_some()).count()
81            }
82            _ => 0,
83        };
84        self.walk_stmt(stmt)
85    }
86
87    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
88        // A ternary is an `if` in expression position.
89        self.decisions += usize::from(matches!(expr.kind, ExprKind::Ternary(..)));
90        self.walk_expr(expr)
91    }
92}