react_auditor/rules/quality/
complexity.rs1use 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}