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}