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