react-auditor 0.1.8

A blazing-fast Rust CLI to scan JS/TS/React code for best practices, quality, and security issues.
Documentation
use oxc_ast::ast::Program;
use oxc_ast_visit::Visit;
use oxc_ast_visit::walk;
use oxc_semantic::Semantic;
use oxc_syntax::scope::ScopeFlags;

use crate::rules::{Rule, RuleFinding, RuleMeta, Severity};

pub struct Complexity;

const RULE_META: RuleMeta = RuleMeta {
    id: "complexity",
    default_severity: Severity::Warning,
    category: "quality",
    description: "Cyclomatic complexity should not exceed 10",
};

const MAX_COMPLEXITY: usize = 10;

impl Rule for Complexity {
    fn meta(&self) -> &RuleMeta {
        &RULE_META
    }

    fn run(&self, program: &Program, _semantic: &Semantic, source_text: &str) -> Vec<RuleFinding> {
        let mut collector = ComplexityCollector {
            findings: Vec::new(),
            source: source_text,
        };
        collector.visit_program(program);
        collector.findings
    }
}

struct ComplexityCollector<'a> {
    findings: Vec<RuleFinding>,
    source: &'a str,
}

impl<'a> Visit<'a> for ComplexityCollector<'a> {
    fn visit_function(&mut self, func: &oxc_ast::ast::Function<'a>, _flags: ScopeFlags) {
        if let Some(body) = &func.body {
            let score = count_complexity(&body.statements);
            if score > MAX_COMPLEXITY {
                let name = func
                    .id
                    .as_ref()
                    .map(|id| id.name.as_str())
                    .unwrap_or("anonymous");
                let start = func.span.start as usize;
                let line = self.source[..start].lines().count().max(1);
                let col = start - self.source[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
                self.findings.push(RuleFinding {
                    line,
                    column: col + 1,
                    message: format!(
                        "Function `{name}` has complexity {score}, max {MAX_COMPLEXITY}"
                    ),
                });
            }
        }
        walk::walk_function(self, func, _flags);
    }

    fn visit_arrow_function_expression(
        &mut self,
        func: &oxc_ast::ast::ArrowFunctionExpression<'a>,
    ) {
        let score = count_complexity(&func.body.statements);
        if score > MAX_COMPLEXITY {
            let start = func.span.start as usize;
            let line = self.source[..start].lines().count().max(1);
            let col = start - self.source[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
            self.findings.push(RuleFinding {
                line,
                column: col + 1,
                message: format!("Arrow function has complexity {score}, max {MAX_COMPLEXITY}"),
            });
        }
        walk::walk_arrow_function_expression(self, func);
    }
}

fn count_statement(stmt: &oxc_ast::ast::Statement) -> usize {
    match stmt {
        oxc_ast::ast::Statement::IfStatement(i) => {
            let mut score = 1;
            if i.alternate.is_some() {
                score += 1;
            }
            score += count_statement(&i.consequent);
            if let Some(alt) = &i.alternate {
                score += count_statement(alt);
            }
            score
        }
        oxc_ast::ast::Statement::ForStatement(f) => 1 + count_statement(&f.body),
        oxc_ast::ast::Statement::ForInStatement(f) => 1 + count_statement(&f.body),
        oxc_ast::ast::Statement::ForOfStatement(f) => 1 + count_statement(&f.body),
        oxc_ast::ast::Statement::WhileStatement(w) => 1 + count_statement(&w.body),
        oxc_ast::ast::Statement::DoWhileStatement(d) => 1 + count_statement(&d.body),
        oxc_ast::ast::Statement::SwitchStatement(s) => s
            .cases
            .iter()
            .map(|case| 1 + count_complexity(&case.consequent))
            .sum(),
        oxc_ast::ast::Statement::TryStatement(t) => {
            let mut score = 0;
            if let Some(handler) = &t.handler {
                score += 1;
                score += count_complexity(&handler.body.body);
            }
            if let Some(ref finalizer) = t.finalizer {
                score += count_complexity(&finalizer.body);
            }
            score
        }
        oxc_ast::ast::Statement::BlockStatement(b) => count_complexity(&b.body),
        _ => 0,
    }
}

fn count_complexity(stmts: &[oxc_ast::ast::Statement]) -> usize {
    let mut score = 1;
    for stmt in stmts {
        score += count_statement(stmt);
    }
    score
}