react-auditor 0.5.1

A blazing-fast Rust CLI to scan JS/TS/React code for best practices, quality, and security issues.
Documentation
use std::collections::HashSet;
use std::path::Path;
use std::sync::mpsc;
use std::time::Duration;

use anyhow::Result;
use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher};

use crate::formatters;
use crate::scanner::Scanner;

const DEBOUNCE_MS: u64 = 200;

pub fn watch(scanner: &Scanner) -> Result<()> {
    let dirs = resolve_watch_dirs(&scanner.files);
    if dirs.is_empty() {
        eprintln!("Nothing to watch. Specify a directory or file pattern.");
        return Ok(());
    }

    let (tx, rx) = mpsc::channel::<Vec<String>>();

    let mut watcher = RecommendedWatcher::new(
        move |res: Result<Event, notify::Error>| {
            if let Ok(event) = res {
                let paths: Vec<String> = event
                    .paths
                    .iter()
                    .filter_map(|p| {
                        let ext = p.extension().and_then(|e| e.to_str())?;
                        if matches!(ext, "js" | "jsx" | "ts" | "tsx") {
                            Some(p.to_string_lossy().to_string())
                        } else {
                            None
                        }
                    })
                    .collect();
                if !paths.is_empty() {
                    let _ = tx.send(paths);
                }
            }
        },
        Config::default(),
    )?;

    for dir in &dirs {
        watcher.watch(dir, RecursiveMode::Recursive)?;
    }

    eprintln!(
        "Watching {} director(ies) for changes. Ctrl+C to stop.",
        dirs.len()
    );

    run_scan_loop(scanner, &rx)
}

fn run_scan_loop(scanner: &Scanner, rx: &mpsc::Receiver<Vec<String>>) -> Result<()> {
    loop {
        let mut changed = HashSet::new();

        match rx.recv() {
            Ok(paths) => {
                for p in paths {
                    changed.insert(p);
                }
            }
            Err(_) => break,
        }

        loop {
            match rx.recv_timeout(Duration::from_millis(DEBOUNCE_MS)) {
                Ok(paths) => {
                    for p in paths {
                        changed.insert(p);
                    }
                }
                Err(mpsc::RecvTimeoutError::Timeout) => break,
                Err(mpsc::RecvTimeoutError::Disconnected) => return Ok(()),
            }
        }

        let changed: Vec<String> = changed.into_iter().collect();

        if changed.is_empty() {
            continue;
        }

        eprint!("\r[{} file(s) changed] Scanning...", changed.len());

        match scanner.scan_paths(&changed) {
            Ok(results) => {
                let formatter = formatters::get_formatter("stylish");
                let output = formatter.format(&results, false);
                print!("{output}");
            }
            Err(e) => {
                eprintln!("\rScan error: {e}");
            }
        }
    }

    Ok(())
}

fn resolve_watch_dirs(patterns: &[String]) -> Vec<Box<Path>> {
    let mut dirs = Vec::new();
    for pattern in patterns {
        let path = Path::new(pattern);
        if path.is_dir() {
            dirs.push(path.into());
        } else if path.is_file() {
            if let Some(parent) = path.parent() {
                dirs.push(parent.into());
            }
        } else {
            if let Some(parent) = path.parent() {
                if parent.to_string_lossy().is_empty() || parent == Path::new(".") {
                    if let Ok(cwd) = std::env::current_dir() {
                        dirs.push(cwd.into());
                    }
                } else if parent.is_dir() {
                    dirs.push(parent.into());
                }
            }
        }
    }
    dirs.sort();
    dirs.dedup();
    dirs
}