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}