complexity/lib.rs
1//! Calculate cognitive complexity of Rust code.
2//!
3//! Based on [Cognitive Complexity][pdf] by G. Ann Campbell.
4//!
5//! ## Getting started
6//!
7//! You'll need to bring the [`Complexity`] trait into scope, and probably some
8//! things from [`syn`].
9//!
10//! ```rust
11//! use complexity::Complexity;
12//! use syn::{Expr, parse_quote};
13//! ```
14//!
15//! Complexity of expressions and other [`syn`] types is as simple as calling
16//! [`.complexity()`] on an instance of that type.
17//!
18//! ```rust
19//! # use complexity::Complexity;
20//! # use syn::{Expr, parse_quote};
21//! let expr: Expr = parse_quote! {
22//! for element in iterable { // +1
23//! if something { // +2 (nesting = 1)
24//! do_something();
25//! }
26//! }
27//! };
28//! assert_eq!(expr.complexity(), 3);
29//! ```
30//!
31//! ## Examples
32//!
33//! The implementation of cognitive complexity in this crate is heavily based on
34//! [Cognitive Complexity][pdf] by G. Ann Campbell. And reading it would be
35//! beneficial to understanding how the complexity index is calculated.
36//!
37//! Loops and structures that introduce branching increment the complexity by
38//! one each. Some syntax structures introduce a "nesting" level which increases
39//! some expressions complexity by that nesting level in addition to their
40//! regular increment. In the example below we see how two nested loops and an
41//! if statement can produce quite a high complexity of **7**.
42//!
43//! ```rust
44//! use complexity::Complexity;
45//! use syn::{ItemFn, parse_quote};
46//!
47//! let func: ItemFn = parse_quote! {
48//! fn sum_of_primes(max: u64) -> u64 {
49//! let mut total = 0;
50//! 'outer: for i in 1..=max { // +1
51//! for j in 2..i { // +2 (nesting = 1)
52//! if i % j == 0 { // +3 (nesting = 2)
53//! continue 'outer; // +1
54//! }
55//! }
56//! total += i;
57//! }
58//! }
59//! };
60//! assert_eq!(func.complexity(), 7);
61//! ```
62//!
63//! But some structures are rewarded. Particularly a `match` statement, which
64//! only increases the complexity by one no matter how many branches there are.
65//! (It does increase the nesting level though.) In the example below we see how
66//! even though there are a lot of branches in the code (which would contribute
67//! a lot to a more traditional *cyclomatic complexity* measurement), the
68//! complexity is quite low at **1**.
69//!
70//! ```rust
71//! use complexity::Complexity;
72//! use syn::{ItemFn, parse_quote};
73//!
74//! let func: ItemFn = parse_quote! {
75//! fn get_words(number: u64) -> &str {
76//! match number { // +1
77//! 1 => "one",
78//! 2 => "a couple",
79//! 3 => "a few",
80//! _ => "lots",
81//! }
82//! }
83//! };
84//! assert_eq!(func.complexity(), 1);
85//! ```
86//!
87//! An example is provided to calculate and nicely print out the cognitive
88//! complexity of each function and method in an entire Rust file. See
89//! [examples/lint-files.rs](examples/lint-files.rs). You can run it on Rust
90//! files like this:
91//!
92//! ```sh
93//! cargo run --example lint-files -- src/
94//! ```
95//!
96//! [pdf]: https://www.sonarsource.com/docs/CognitiveComplexity.pdf
97//! [`Complexity`]: trait.Complexity.html
98//! [`.complexity()`]: trait.Complexity.html#tymethod.complexity
99//! [`syn`]: https://docs.rs/syn/1
100
101use std::{iter, ops};
102
103use syn::*;
104
105/////////////////////////////////////////////////////////////////////////
106// Complexity trait
107/////////////////////////////////////////////////////////////////////////
108
109mod private {
110 pub trait Sealed {}
111 impl Sealed for super::Expr {}
112 impl Sealed for super::ItemFn {}
113 impl Sealed for super::ImplItemMethod {}
114}
115
116/// A trait for calculating the cognitive complexity of a Rust type.
117///
118/// This is a *sealed* trait so only this crate can implement it.
119pub trait Complexity: private::Sealed {
120 /// Returns the cognitive complexity index for the implementor.
121 fn complexity(&self) -> u32;
122}
123
124impl Complexity for Expr {
125 fn complexity(&self) -> u32 {
126 eval_expr(self, State::default()).0
127 }
128}
129
130impl Complexity for ItemFn {
131 fn complexity(&self) -> u32 {
132 eval_block(&self.block, State::default()).0
133 }
134}
135
136impl Complexity for ImplItemMethod {
137 fn complexity(&self) -> u32 {
138 eval_block(&self.block, State::default()).0
139 }
140}
141
142/////////////////////////////////////////////////////////////////////////
143// Index type
144/////////////////////////////////////////////////////////////////////////
145
146/// Represents a complexity index.
147#[derive(Debug, Clone, Copy)]
148struct Index(u32);
149
150impl Index {
151 /// Construct a new zero `Index` that does not contribute to complexity.
152 fn zero() -> Self {
153 Self(0)
154 }
155
156 /// Construct a new `Index` that adds one to the complexity.
157 fn one() -> Self {
158 Self(1)
159 }
160
161 /// Construct a new `Index` that adds one to the complexity and one for each
162 /// level of nesting.
163 fn with_context(state: State) -> Self {
164 Self(state.nesting + 1)
165 }
166}
167
168impl ops::Add for Index {
169 type Output = Self;
170
171 /// Add one `Index` to another.
172 ///
173 /// This simply is the addition of both complexities.
174 fn add(self, other: Self) -> Self {
175 Self(self.0 + other.0)
176 }
177}
178
179impl iter::Sum<Index> for Index {
180 /// Sum an iterable of `Index` by simply accumulating all the complexities
181 /// into one.
182 fn sum<I>(iter: I) -> Self
183 where
184 I: Iterator<Item = Self>,
185 {
186 iter.fold(Self::zero(), ops::Add::add)
187 }
188}
189
190/////////////////////////////////////////////////////////////////////////
191// Logical boolean operator type
192/////////////////////////////////////////////////////////////////////////
193
194/// Represents a logical boolean operator.
195#[derive(Debug, Clone, Copy, PartialEq)]
196enum LogBoolOp {
197 /// The `!` operator (logical not).
198 Not,
199 /// The `&&` operator (logical and).
200 And,
201 /// The `||` operator (logical or).
202 Or,
203}
204
205impl LogBoolOp {
206 /// Create a new `LogBoolOp` from a `syn::UnOp`.
207 fn from_un_op(un_op: UnOp) -> Option<Self> {
208 match un_op {
209 UnOp::Not(_) => Some(Self::Not),
210 _ => None,
211 }
212 }
213
214 /// Create a new `LogBoolOp` from a `syn::BinOp`.
215 fn from_bin_op(bin_op: &BinOp) -> Option<Self> {
216 match bin_op {
217 BinOp::And(_) => Some(Self::And),
218 BinOp::Or(_) => Some(Self::Or),
219 _ => None,
220 }
221 }
222
223 /// Compares this `LogBoolOp` with the previous one and returns a complexity
224 /// index.
225 fn eval_based_on_prev(self, prev: Option<Self>) -> Index {
226 match (prev, self) {
227 (Some(prev), current) => {
228 if prev == current {
229 Index::zero()
230 } else {
231 Index::one()
232 }
233 }
234 (None, _) => Index::one(),
235 }
236 }
237}
238
239/////////////////////////////////////////////////////////////////////////
240// State type
241/////////////////////////////////////////////////////////////////////////
242
243/// Represents the current state during parsing. We use this type to track the
244/// nesting level and the previous logical boolean operator.
245#[derive(Debug, Default, Clone, Copy)]
246struct State {
247 /// The nesting level.
248 nesting: u32,
249 /// The previous logical boolean operator.
250 log_bool_op: Option<LogBoolOp>,
251}
252
253impl State {
254 /// Create a new `State` with an extra level of nesting.
255 fn increase_nesting(self) -> Self {
256 Self {
257 nesting: self.nesting + 1,
258 log_bool_op: self.log_bool_op,
259 }
260 }
261}
262
263/////////////////////////////////////////////////////////////////////////
264// Evaluation functions
265/////////////////////////////////////////////////////////////////////////
266
267/// Returns the complexity of a `syn::Block`.
268fn eval_block(block: &Block, state: State) -> Index {
269 block
270 .stmts
271 .iter()
272 .map(|e| eval_stmt(e, state))
273 .sum::<Index>()
274}
275
276/// Returns the complexity of a `syn::Stmt`.
277fn eval_stmt(stmt: &Stmt, state: State) -> Index {
278 match stmt {
279 Stmt::Local(Local {
280 init: Some((_, expr)),
281 ..
282 }) => eval_expr(expr, state),
283 Stmt::Local(Local { init: None, .. }) => Index::zero(),
284 Stmt::Item(item) => eval_item(item, state),
285 Stmt::Expr(expr) | Stmt::Semi(expr, _) => eval_expr(expr, state),
286 }
287}
288
289/// Returns the complexity of a `syn::Item`.
290fn eval_item(item: &Item, state: State) -> Index {
291 match item {
292 Item::Const(ItemConst { expr, .. }) => eval_expr(expr, state),
293 Item::Static(ItemStatic { expr, .. }) => eval_expr(expr, state),
294 _ => Index::zero(),
295 }
296}
297
298/// Returns the complexity of a `syn::ExprUnary`.
299///
300/// This function also updates the previous logical boolean operator if it is
301/// `!`.
302fn eval_expr_unary(expr_unary: &ExprUnary, mut state: State) -> Index {
303 let ExprUnary { op, expr, .. } = expr_unary;
304 if let Some(current) = LogBoolOp::from_un_op(*op) {
305 state.log_bool_op = Some(current);
306 }
307 eval_expr(expr, state)
308}
309
310/// Returns the complexity of a `syn::ExprBinary`.
311///
312/// This function handles logical boolean operators `&&` and `||` by doing the
313/// following:
314/// - If the operator is the different then add one to the complexity.
315/// - Update the previous logical boolean operator.
316fn eval_expr_binary(expr_binary: &ExprBinary, mut state: State) -> Index {
317 let ExprBinary {
318 left, op, right, ..
319 } = expr_binary;
320 let index = match LogBoolOp::from_bin_op(op) {
321 Some(current) => {
322 let index = current.eval_based_on_prev(state.log_bool_op);
323 state.log_bool_op = Some(current);
324 index
325 }
326 None => Index::zero(),
327 };
328 index + eval_expr(left, state) + eval_expr(right, state)
329}
330
331/// Returns the complexity of a `syn::ExprRange`.
332fn eval_expr_range(expr_range: &ExprRange, state: State) -> Index {
333 let ExprRange { from, to, .. } = expr_range;
334 from.as_ref()
335 .map(|e| eval_expr(e, state))
336 .unwrap_or_else(Index::zero)
337 + to.as_ref()
338 .map(|e| eval_expr(e, state))
339 .unwrap_or_else(Index::zero)
340}
341
342/// Returns the complexity of a `syn::ExprIf`.
343fn eval_expr_if(expr_if: &ExprIf, state: State) -> Index {
344 let ExprIf {
345 cond,
346 then_branch,
347 else_branch,
348 ..
349 } = expr_if;
350 Index::with_context(state)
351 + eval_expr(cond, state)
352 + eval_block(then_branch, state.increase_nesting())
353 + else_branch
354 .as_ref()
355 .map(|(_, expr)| Index::one() + eval_expr(&expr, state.increase_nesting()))
356 .unwrap_or_else(Index::zero)
357}
358
359/// Returns the complexity of a `syn::ExprMatch`.
360fn eval_expr_match(expr_match: &ExprMatch, state: State) -> Index {
361 let ExprMatch { expr, arms, .. } = expr_match;
362 Index::with_context(state)
363 + eval_expr(expr, state)
364 + arms
365 .iter()
366 .map(|arm| {
367 arm.guard
368 .as_ref()
369 .map(|(_, expr)| eval_expr(&expr, state))
370 .unwrap_or_else(Index::zero)
371 + eval_expr(&arm.body, state.increase_nesting())
372 })
373 .sum::<Index>()
374}
375
376/// Returns the complexity of a `syn::ExprForLoop`.
377fn eval_expr_for_loop(expr_for_loop: &ExprForLoop, state: State) -> Index {
378 let ExprForLoop { expr, body, .. } = expr_for_loop;
379 Index::with_context(state) + eval_expr(expr, state) + eval_block(body, state.increase_nesting())
380}
381
382/// Returns the complexity of a `syn::ExprWhile`.
383fn eval_expr_while(expr_while: &ExprWhile, state: State) -> Index {
384 let ExprWhile { cond, body, .. } = expr_while;
385 Index::with_context(state) + eval_expr(cond, state) + eval_block(body, state.increase_nesting())
386}
387
388/// Returns the complexity of a `syn::ExprStruct`.
389fn eval_expr_struct(expr_struct: &ExprStruct, state: State) -> Index {
390 let ExprStruct { fields, rest, .. } = expr_struct;
391 fields
392 .iter()
393 .map(|v| eval_expr(&v.expr, state))
394 .sum::<Index>()
395 + rest
396 .as_ref()
397 .map(|e| eval_expr(e, state))
398 .unwrap_or_else(Index::zero)
399}
400
401/// Returns the complexity of a `syn::ExprCall`.
402fn eval_expr_call(expr_call: &ExprCall, state: State) -> Index {
403 let ExprCall { func, args, .. } = expr_call;
404 eval_expr(func, state) + args.iter().map(|a| eval_expr(a, state)).sum::<Index>()
405}
406
407/// Returns the complexity of a `syn::ExprMethodCall`.
408fn eval_expr_method_call(expr_method_call: &ExprMethodCall, state: State) -> Index {
409 let ExprMethodCall { receiver, args, .. } = expr_method_call;
410 eval_expr(receiver, state) + args.iter().map(|a| eval_expr(a, state)).sum::<Index>()
411}
412
413/// Returns the complexity of a `syn::Expr`.
414///
415/// This function contains most of the logic for calculating cognitive
416/// complexity. Expressions that create nesting increase the complexity and
417/// expressions that increase the branching increasing the complexity.
418fn eval_expr(expr: &Expr, state: State) -> Index {
419 match expr {
420 // Expressions that map to multiple expressions.
421 // --------------------------------------------
422 Expr::Array(ExprArray { elems, .. }) | Expr::Tuple(ExprTuple { elems, .. }) => {
423 elems.iter().map(|e| eval_expr(e, state)).sum()
424 }
425
426 // Unary and binary operators.
427 // ---------------------------
428 // These are handled specially because of logical boolean operator complexity.
429 Expr::Unary(expr_unary) => eval_expr_unary(expr_unary, state),
430 Expr::Binary(expr_binary) => eval_expr_binary(expr_binary, state),
431
432 // Expressions that have a left and right part.
433 // --------------------------------------------
434 Expr::Assign(ExprAssign { left, right, .. })
435 | Expr::AssignOp(ExprAssignOp { left, right, .. })
436 | Expr::Index(ExprIndex {
437 expr: left,
438 index: right,
439 ..
440 })
441 | Expr::Repeat(ExprRepeat {
442 expr: left,
443 len: right,
444 ..
445 }) => eval_expr(left, state) + eval_expr(right, state),
446
447 Expr::Range(expr_range) => eval_expr_range(expr_range, state),
448
449 // Expressions that create a nested block like `async { .. }`.
450 // -----------------------------------------------------------
451 Expr::Async(ExprAsync { block, .. })
452 | Expr::Block(ExprBlock { block, .. })
453 | Expr::Loop(ExprLoop { body: block, .. })
454 | Expr::TryBlock(ExprTryBlock { block, .. })
455 | Expr::Unsafe(ExprUnsafe { block, .. }) => eval_block(block, state.increase_nesting()),
456 Expr::ForLoop(expr_for_loop) => eval_expr_for_loop(expr_for_loop, state),
457 Expr::While(expr_while) => eval_expr_while(expr_while, state),
458
459 // Expressions that do not not nest any further, and do not contribute to complexity.
460 // ----------------------------------------------------------------------------------
461 Expr::Lit(_) | Expr::Path(_) => Index::zero(),
462
463 // Expressions that wrap a single expression.
464 // ------------------------------------------
465 Expr::Await(ExprAwait { base: expr, .. })
466 | Expr::Box(ExprBox { expr, .. })
467 | Expr::Break(ExprBreak {
468 expr: Some(expr), ..
469 })
470 | Expr::Cast(ExprCast { expr, .. })
471 | Expr::Closure(ExprClosure { body: expr, .. })
472 | Expr::Field(ExprField { base: expr, .. })
473 | Expr::Group(ExprGroup { expr, .. })
474 | Expr::Let(ExprLet { expr, .. })
475 | Expr::Paren(ExprParen { expr, .. })
476 | Expr::Reference(ExprReference { expr, .. })
477 | Expr::Return(ExprReturn {
478 expr: Some(expr), ..
479 })
480 | Expr::Try(ExprTry { expr, .. })
481 | Expr::Type(ExprType { expr, .. })
482 | Expr::Yield(ExprYield {
483 expr: Some(expr), ..
484 }) => eval_expr(expr, state),
485
486 // Expressions that introduce branching.
487 // -------------------------------------
488 Expr::If(expr_if) => eval_expr_if(expr_if, state),
489 Expr::Match(expr_match) => eval_expr_match(expr_match, state),
490 Expr::Continue(_) | Expr::Break(_) => Index::one(),
491
492 // Expressions that call functions / construct types.
493 // --------------------------------------------------
494 Expr::Struct(expr_struct) => eval_expr_struct(expr_struct, state),
495 Expr::Call(expr_call) => eval_expr_call(expr_call, state),
496 Expr::MethodCall(expr_method_call) => eval_expr_method_call(expr_method_call, state),
497 // FIXME: should we attempt to parse macro the tokens into something that we can calculate
498 // the complexity for?
499 Expr::Macro(_) => Index::zero(),
500
501 // `Expr` is non-exhaustive, so this has to be here. But we should have handled everything.
502 _ => Index::zero(),
503 }
504}
505
506/////////////////////////////////////////////////////////////////////////
507// Unit tests
508/////////////////////////////////////////////////////////////////////////
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513
514 #[test]
515 fn if_statement() {
516 let expr: Expr = parse_quote! {
517 if true { // +1
518 println!("test");
519 }
520 };
521 assert_eq!(expr.complexity(), 1);
522 }
523
524 #[test]
525 fn if_statement_nesting_increment() {
526 let expr: Expr = parse_quote! {
527 if true { // +1
528 if true { // +2 (nesting = 1)
529 println!("test");
530 }
531 }
532 };
533 assert_eq!(expr.complexity(), 3);
534 }
535
536 #[test]
537 fn if_else_statement_no_nesting_increment() {
538 let expr: Expr = parse_quote! {
539 if true { // +1
540 if true { // +2 (nesting = 1)
541 println!("test");
542 } else { // +1
543 println!("test");
544 }
545 }
546 };
547 assert_eq!(expr.complexity(), 4);
548 }
549
550 #[test]
551 fn for_loop() {
552 let expr: Expr = parse_quote! {
553 for element in iterable { // +1
554 if true { // +2 (nesting = 1)
555 println!("test");
556 }
557 }
558 };
559 assert_eq!(expr.complexity(), 3);
560 }
561
562 #[test]
563 fn for_loop_nesting_increment() {
564 let expr: Expr = parse_quote! {
565 if true { // +1
566 for element in iterable { // +2 (nesting = 1)
567 println!("test");
568 }
569 }
570 };
571 assert_eq!(expr.complexity(), 3);
572 }
573
574 #[test]
575 fn while_loop() {
576 let expr: Expr = parse_quote! {
577 while true { // +1
578 if true { // +2 (nesting = 1)
579 println!("test");
580 }
581 }
582 };
583 assert_eq!(expr.complexity(), 3);
584 }
585
586 #[test]
587 fn while_loop_nesting_increment() {
588 let expr: Expr = parse_quote! {
589 if true { // +1
590 while true { // +2 (nesting = 1)
591 println!("test");
592 }
593 }
594 };
595 assert_eq!(expr.complexity(), 3);
596 }
597
598 #[test]
599 fn match_statement_nesting_increment() {
600 let expr: Expr = parse_quote! {
601 if true { // +1
602 match true { // +2 (nesting = 1)
603 true => println!("test"),
604 false => println!("test"),
605 }
606 }
607 };
608 assert_eq!(expr.complexity(), 3);
609 }
610
611 #[test]
612 fn logical_boolean_operators_same() {
613 let expr: Expr = parse_quote! { x && y };
614 assert_eq!(expr.complexity(), 1);
615 let expr: Expr = parse_quote! { x && y && z };
616 assert_eq!(expr.complexity(), 1);
617 let expr: Expr = parse_quote! { w && x && y && z };
618 assert_eq!(expr.complexity(), 1);
619 let expr: Expr = parse_quote! { x || y };
620 assert_eq!(expr.complexity(), 1);
621 let expr: Expr = parse_quote! { x || y || z };
622 assert_eq!(expr.complexity(), 1);
623 let expr: Expr = parse_quote! { w || x || y || z };
624 assert_eq!(expr.complexity(), 1);
625 }
626
627 #[test]
628 fn logical_boolean_operators_changing() {
629 let expr: Expr = parse_quote! { w && x || y || z };
630 assert_eq!(expr.complexity(), 2);
631 let expr: Expr = parse_quote! { w && x && y || z };
632 assert_eq!(expr.complexity(), 2);
633 let expr: Expr = parse_quote! { w && x || y && z };
634 assert_eq!(expr.complexity(), 3);
635 }
636
637 #[test]
638 fn logical_boolean_operators_not_operator() {
639 let expr: Expr = parse_quote! { !a && !b };
640 assert_eq!(expr.complexity(), 1);
641 let expr: Expr = parse_quote! { a && !(b && c) };
642 assert_eq!(expr.complexity(), 2);
643 let expr: Expr = parse_quote! { !(a || b) && !(c || d) };
644 assert_eq!(expr.complexity(), 3);
645 }
646}