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    "this function has a cyclomatic complexity above 11; consider splitting it into smaller functions"
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<'hir> LateLintPass<'hir> for CyclomaticComplexity {
24    fn check_function(
25        &mut self,
26        ctx: &LintContext,
27        gcx: Gcx<'hir>,
28        hir: &'hir Hir<'hir>,
29        func: &'hir hir::Function<'hir>,
30    ) {
31        let _ = gcx;
32        // Modifier definitions are never reported, matching Slither which iterates only
33        // declared and top-level functions. Yul helpers declared inside `assembly {}` DO
34        // report: Slither scores them as functions of their own.
35        if matches!(func.kind, hir::FunctionKind::Modifier) {
36            return;
37        }
38        if func.body.is_some() {
39            // Visiting the whole function rather than only the body statements also counts
40            // decision points in modifier-invocation and base-constructor call arguments.
41            let mut counter = DecisionCounter { hir, decisions: 0 };
42            let _ = counter.visit_function(func);
43            // For a structured program the complexity is one plus the decision points.
44            if counter.decisions + 1 > MAX_COMPLEXITY {
45                // A Yul helper's span starts at its name rather than a `function` keyword,
46                // so the name is the anchor there.
47                let span = if func.is_yul {
48                    func.name.map_or(func.span, |name| name.span)
49                } else {
50                    func.keyword_span()
51                };
52                ctx.emit(&CYCLOMATIC_COMPLEXITY, span);
53            }
54        }
55    }
56}
57
58/// Counts the decision points of a function body. For a structured program the cyclomatic
59/// complexity `E - N + 2P` of the control-flow graph equals one plus the number of decision
60/// points, so no graph needs building.
61///
62/// Loops count through their condition: solar desugars every `for`, `while` and `do while`
63/// into `Loop { ... if (cond) ... }`, so the synthetic `if` carries the loop's decision and a
64/// condition-less `for (;;)` correctly adds nothing. Boolean `&&` / `||` operators are not
65/// counted, matching the control-flow graph Slither computes on.
66struct DecisionCounter<'hir> {
67    hir: &'hir Hir<'hir>,
68    decisions: usize,
69}
70
71impl<'hir> Visit<'hir> for DecisionCounter<'hir> {
72    type BreakValue = Infallible;
73
74    fn hir(&self) -> &'hir Hir<'hir> {
75        self.hir
76    }
77
78    fn visit_stmt(&mut self, stmt: &'hir Stmt<'hir>) -> ControlFlow<Self::BreakValue> {
79        match &stmt.kind {
80            // One decision per `if`, the loop conditions included (see above).
81            StmtKind::If(..) => self.decisions += 1,
82            // The first clause is the `returns` one; each `catch` clause is a branch.
83            StmtKind::Try(stmt_try) => {
84                self.decisions += stmt_try.clauses.len().saturating_sub(1);
85            }
86            // Each non-default case of a Yul switch is a branch; `cases` includes the
87            // `default` clause (`constant == None`), which opens no decision of its own.
88            StmtKind::Switch(switch) => {
89                self.decisions += switch.cases.iter().filter(|c| c.constant.is_some()).count();
90            }
91            _ => {}
92        }
93        self.walk_stmt(stmt)
94    }
95
96    fn visit_expr(&mut self, expr: &'hir Expr<'hir>) -> ControlFlow<Self::BreakValue> {
97        // A ternary is an `if` in expression position.
98        if matches!(expr.kind, ExprKind::Ternary(..)) {
99            self.decisions += 1;
100        }
101        self.walk_expr(expr)
102    }
103}