1 | //! calculate cognitive complexity and warn about overly complex functions |
2 | |
3 | use clippy_utils::diagnostics::span_lint_and_help; |
4 | use clippy_utils::source::snippet_opt; |
5 | use clippy_utils::ty::is_type_diagnostic_item; |
6 | use clippy_utils::visitors::for_each_expr; |
7 | use clippy_utils::{get_async_fn_body, is_async_fn, LimitStack}; |
8 | use core::ops::ControlFlow; |
9 | use rustc_ast::ast::Attribute; |
10 | use rustc_hir::intravisit::FnKind; |
11 | use rustc_hir::{Body, Expr, ExprKind, FnDecl}; |
12 | use rustc_lint::{LateContext, LateLintPass, LintContext}; |
13 | use rustc_session::impl_lint_pass; |
14 | use rustc_span::def_id::LocalDefId; |
15 | use rustc_span::{sym, BytePos, Span}; |
16 | |
17 | declare_clippy_lint! { |
18 | /// ### What it does |
19 | /// Checks for methods with high cognitive complexity. |
20 | /// |
21 | /// ### Why is this bad? |
22 | /// Methods of high cognitive complexity tend to be hard to |
23 | /// both read and maintain. Also LLVM will tend to optimize small methods better. |
24 | /// |
25 | /// ### Known problems |
26 | /// Sometimes it's hard to find a way to reduce the |
27 | /// complexity. |
28 | /// |
29 | /// ### Example |
30 | /// You'll see it when you get the warning. |
31 | #[clippy::version = "1.35.0" ] |
32 | pub COGNITIVE_COMPLEXITY, |
33 | nursery, |
34 | "functions that should be split up into multiple functions" |
35 | } |
36 | |
37 | pub struct CognitiveComplexity { |
38 | limit: LimitStack, |
39 | } |
40 | |
41 | impl CognitiveComplexity { |
42 | #[must_use ] |
43 | pub fn new(limit: u64) -> Self { |
44 | Self { |
45 | limit: LimitStack::new(limit), |
46 | } |
47 | } |
48 | } |
49 | |
50 | impl_lint_pass!(CognitiveComplexity => [COGNITIVE_COMPLEXITY]); |
51 | |
52 | impl CognitiveComplexity { |
53 | #[expect (clippy::cast_possible_truncation)] |
54 | fn check<'tcx>( |
55 | &mut self, |
56 | cx: &LateContext<'tcx>, |
57 | kind: FnKind<'tcx>, |
58 | decl: &'tcx FnDecl<'_>, |
59 | expr: &'tcx Expr<'_>, |
60 | body_span: Span, |
61 | ) { |
62 | if body_span.from_expansion() { |
63 | return; |
64 | } |
65 | |
66 | let mut cc = 1u64; |
67 | let mut returns = 0u64; |
68 | let _: Option<!> = for_each_expr(expr, |e| { |
69 | match e.kind { |
70 | ExprKind::If(_, _, _) => { |
71 | cc += 1; |
72 | }, |
73 | ExprKind::Match(_, arms, _) => { |
74 | if arms.len() > 1 { |
75 | cc += 1; |
76 | } |
77 | cc += arms.iter().filter(|arm| arm.guard.is_some()).count() as u64; |
78 | }, |
79 | ExprKind::Ret(_) => returns += 1, |
80 | _ => {}, |
81 | } |
82 | ControlFlow::Continue(()) |
83 | }); |
84 | |
85 | let ret_ty = cx.typeck_results().node_type(expr.hir_id); |
86 | let ret_adjust = if is_type_diagnostic_item(cx, ret_ty, sym::Result) { |
87 | returns |
88 | } else { |
89 | #[expect (clippy::integer_division)] |
90 | (returns / 2) |
91 | }; |
92 | |
93 | // prevent degenerate cases where unreachable code contains `return` statements |
94 | if cc >= ret_adjust { |
95 | cc -= ret_adjust; |
96 | } |
97 | |
98 | if cc > self.limit.limit() { |
99 | let fn_span = match kind { |
100 | FnKind::ItemFn(ident, _, _) | FnKind::Method(ident, _) => ident.span, |
101 | FnKind::Closure => { |
102 | let header_span = body_span.with_hi(decl.output.span().lo()); |
103 | let pos = snippet_opt(cx, header_span).and_then(|snip| { |
104 | let low_offset = snip.find('|' )?; |
105 | let high_offset = 1 + snip.get(low_offset + 1..)?.find('|' )?; |
106 | let low = header_span.lo() + BytePos(low_offset as u32); |
107 | let high = low + BytePos(high_offset as u32 + 1); |
108 | |
109 | Some((low, high)) |
110 | }); |
111 | |
112 | if let Some((low, high)) = pos { |
113 | Span::new(low, high, header_span.ctxt(), header_span.parent()) |
114 | } else { |
115 | return; |
116 | } |
117 | }, |
118 | }; |
119 | |
120 | span_lint_and_help( |
121 | cx, |
122 | COGNITIVE_COMPLEXITY, |
123 | fn_span, |
124 | &format!( |
125 | "the function has a cognitive complexity of ( {cc}/ {})" , |
126 | self.limit.limit() |
127 | ), |
128 | None, |
129 | "you could split it up into multiple smaller functions" , |
130 | ); |
131 | } |
132 | } |
133 | } |
134 | |
135 | impl<'tcx> LateLintPass<'tcx> for CognitiveComplexity { |
136 | fn check_fn( |
137 | &mut self, |
138 | cx: &LateContext<'tcx>, |
139 | kind: FnKind<'tcx>, |
140 | decl: &'tcx FnDecl<'_>, |
141 | body: &'tcx Body<'_>, |
142 | span: Span, |
143 | def_id: LocalDefId, |
144 | ) { |
145 | if !cx.tcx.has_attr(def_id, sym::test) { |
146 | let expr = if is_async_fn(kind) { |
147 | match get_async_fn_body(cx.tcx, body) { |
148 | Some(b) => b, |
149 | None => { |
150 | return; |
151 | }, |
152 | } |
153 | } else { |
154 | body.value |
155 | }; |
156 | |
157 | self.check(cx, kind, decl, expr, span); |
158 | } |
159 | } |
160 | |
161 | fn enter_lint_attrs(&mut self, cx: &LateContext<'tcx>, attrs: &'tcx [Attribute]) { |
162 | self.limit.push_attrs(cx.sess(), attrs, "cognitive_complexity" ); |
163 | } |
164 | fn exit_lint_attrs(&mut self, cx: &LateContext<'tcx>, attrs: &'tcx [Attribute]) { |
165 | self.limit.pop_attrs(cx.sess(), attrs, "cognitive_complexity" ); |
166 | } |
167 | } |
168 | |