Skip to main content

react_auditor/rules/quality/
complexity.rs

1use oxc_ast::ast::Program;
2use oxc_ast_visit::Visit;
3use oxc_ast_visit::walk;
4use oxc_semantic::Semantic;
5use oxc_syntax::scope::ScopeFlags;
6
7use crate::rules::{Rule, RuleFinding, RuleMeta, Severity};
8
9pub struct Complexity;
10
11const RULE_META: RuleMeta = RuleMeta {
12    id: "complexity",
13    default_severity: Severity::Warning,
14    category: "quality",
15    description: "Cyclomatic complexity should not exceed 10",
16};
17
18const MAX_COMPLEXITY: usize = 10;
19
20impl Rule for Complexity {
21    fn meta(&self) -> &RuleMeta {
22        &RULE_META
23    }
24
25    fn run(&self, program: &Program, _semantic: &Semantic, source_text: &str) -> Vec<RuleFinding> {
26        let mut collector = ComplexityCollector {
27            findings: Vec::new(),
28            source: source_text,
29        };
30        collector.visit_program(program);
31        collector.findings
32    }
33}
34
35struct ComplexityCollector<'a> {
36    findings: Vec<RuleFinding>,
37    source: &'a str,
38}
39
40impl<'a> Visit<'a> for ComplexityCollector<'a> {
41    fn visit_function(&mut self, func: &oxc_ast::ast::Function<'a>, _flags: ScopeFlags) {
42        if let Some(body) = &func.body {
43            let score = count_complexity(&body.statements);
44            if score > MAX_COMPLEXITY {
45                let name = func
46                    .id
47                    .as_ref()
48                    .map(|id| id.name.as_str())
49                    .unwrap_or("anonymous");
50                let start = func.span.start as usize;
51                let line = self.source[..start].lines().count().max(1);
52                let col = start - self.source[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
53                self.findings.push(RuleFinding {
54                    line,
55                    column: col + 1,
56                    message: format!(
57                        "Function `{name}` has complexity {score}, max {MAX_COMPLEXITY}"
58                    ),
59                });
60            }
61        }
62        walk::walk_function(self, func, _flags);
63    }
64
65    fn visit_arrow_function_expression(
66        &mut self,
67        func: &oxc_ast::ast::ArrowFunctionExpression<'a>,
68    ) {
69        let score = count_complexity(&func.body.statements);
70        if score > MAX_COMPLEXITY {
71            let start = func.span.start as usize;
72            let line = self.source[..start].lines().count().max(1);
73            let col = start - self.source[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
74            self.findings.push(RuleFinding {
75                line,
76                column: col + 1,
77                message: format!("Arrow function has complexity {score}, max {MAX_COMPLEXITY}"),
78            });
79        }
80        walk::walk_arrow_function_expression(self, func);
81    }
82}
83
84fn count_statement(stmt: &oxc_ast::ast::Statement) -> usize {
85    match stmt {
86        oxc_ast::ast::Statement::IfStatement(i) => {
87            let mut score = 1;
88            if i.alternate.is_some() {
89                score += 1;
90            }
91            score += count_statement(&i.consequent);
92            if let Some(alt) = &i.alternate {
93                score += count_statement(alt);
94            }
95            score
96        }
97        oxc_ast::ast::Statement::ForStatement(f) => 1 + count_statement(&f.body),
98        oxc_ast::ast::Statement::ForInStatement(f) => 1 + count_statement(&f.body),
99        oxc_ast::ast::Statement::ForOfStatement(f) => 1 + count_statement(&f.body),
100        oxc_ast::ast::Statement::WhileStatement(w) => 1 + count_statement(&w.body),
101        oxc_ast::ast::Statement::DoWhileStatement(d) => 1 + count_statement(&d.body),
102        oxc_ast::ast::Statement::SwitchStatement(s) => s
103            .cases
104            .iter()
105            .map(|case| 1 + count_complexity(&case.consequent))
106            .sum(),
107        oxc_ast::ast::Statement::TryStatement(t) => {
108            let mut score = 0;
109            if let Some(handler) = &t.handler {
110                score += 1;
111                score += count_complexity(&handler.body.body);
112            }
113            if let Some(ref finalizer) = t.finalizer {
114                score += count_complexity(&finalizer.body);
115            }
116            score
117        }
118        oxc_ast::ast::Statement::BlockStatement(b) => count_complexity(&b.body),
119        _ => 0,
120    }
121}
122
123fn count_complexity(stmts: &[oxc_ast::ast::Statement]) -> usize {
124    let mut score = 1;
125    for stmt in stmts {
126        score += count_statement(stmt);
127    }
128    score
129}