rrubocop 0.7.2

Fast RuboCop-compatible Ruby linter in Rust (tree-sitter). No Ruby for linting; ERB configs need ruby.
Documentation
pub mod abc;
pub mod baseline;
pub mod cache;
pub mod cli;
pub mod config;
pub mod cop;
pub mod correction;
pub mod diagnostic;
pub mod formatter;
pub mod fs;
pub mod linter;
pub mod model;
pub mod parse;
pub mod testutil;

use std::io::Read;
use std::process::ExitCode;

use anyhow::Result;
use cli::Args;
use config::{CopFilterSet, ResolvedConfig, load_config};
use cop::registry::CopRegistry;
use diagnostic::Diagnostic;
use formatter::color::Color;
use formatter::{create_formatter, Formatter, ProgressSink};
use linter::{run_linter, run_linter_with, should_fail};
use parse::source::SourceFile;

pub fn run() -> Result<ExitCode> {
    unsafe {
        libc::signal(libc::SIGPIPE, libc::SIG_DFL);
    }

    let args = Args::parse_cli();
    let registry = CopRegistry::default_registry();
    if let Some(code) = list_and_exit(&args, &registry) {
        return Ok(code);
    }

    let config = resolved_config(&args)?;
    if args.stdin.is_some() {
        return lint_stdin(&args, &config, &registry);
    }
    lint_paths(&args, &config, &registry)
}

fn resolved_config(args: &Args) -> Result<ResolvedConfig> {
    if args.force_default_config {
        let mut cfg = ResolvedConfig::empty();
        cfg.apply_display_cli(args.display_cop_names_override());
        return Ok(cfg);
    }
    let target = args.paths.first().map(|p| p.as_path());
    let mut config = load_config(args.config.as_deref(), target, None)?;
    config.apply_display_cli(args.display_cop_names_override());
    Ok(config)
}

fn list_and_exit(args: &Args, registry: &CopRegistry) -> Option<ExitCode> {
    if args.list_cops {
        for name in registry.names() {
            println!("{name}");
        }
        return Some(ExitCode::SUCCESS);
    }
    if args.list_autocorrectable_cops {
        for cop in registry.cops() {
            if cop.supports_autocorrect() {
                println!("{}", cop.name());
            }
        }
        return Some(ExitCode::SUCCESS);
    }
    None
}

fn exit_from_diags(diags: &[Diagnostic], fail_level: &str) -> ExitCode {
    if should_fail(diags, fail_level) {
        ExitCode::from(1)
    } else {
        ExitCode::SUCCESS
    }
}

fn print_results(args: &Args, diags: &[Diagnostic], files: &[std::path::PathBuf]) -> ExitCode {
    create_formatter(&args.format, Color::resolve(args.color_force())).print(diags, files);
    exit_from_diags(diags, &args.fail_level)
}

fn list_target_files(args: &Args, config: &ResolvedConfig) -> Result<ExitCode> {
    let discovered = fs::discover_files_filtered(
        &args.paths,
        &CopFilterSet::for_discover(config),
        args.force_exclusion,
    )?;
    for f in &discovered.files {
        println!("{}", f.display());
    }
    Ok(ExitCode::SUCCESS)
}

fn lint_paths(
    args: &Args,
    config: &ResolvedConfig,
    registry: &CopRegistry,
) -> Result<ExitCode> {
    if args.list_target_files {
        return list_target_files(args, config);
    }
    let fmt = create_formatter(&args.format, Color::resolve(args.color_force()));
    if fmt.streams_marks() {
        return lint_paths_streaming(args, config, registry, fmt.as_ref());
    }
    let result = run_linter(args, config, registry, &args.paths)?;
    fmt.print(&result.diagnostics, &result.files);
    Ok(exit_from_diags(&result.diagnostics, &args.fail_level))
}

fn lint_paths_streaming(
    args: &Args,
    config: &ResolvedConfig,
    registry: &CopRegistry,
    fmt: &dyn Formatter,
) -> Result<ExitCode> {
    let sink = ProgressSink::new(fmt);
    let result = run_linter_with(
        args,
        config,
        registry,
        &args.paths,
        |n| sink.started(n),
        |diags| sink.file_finished(diags),
    )?;
    sink.finished(&result.diagnostics, &result.files);
    Ok(exit_from_diags(&result.diagnostics, &args.fail_level))
}

fn lint_stdin(args: &Args, config: &ResolvedConfig, registry: &CopRegistry) -> Result<ExitCode> {
    let path = args.stdin.as_ref().unwrap();
    let mut buf = Vec::new();
    std::io::stdin().read_to_end(&mut buf)?;
    let diags = lint_stdin_source(
        args,
        config,
        registry,
        &SourceFile::from_bytes(path.clone(), buf),
    )?;
    Ok(print_results(args, &diags, std::slice::from_ref(path)))
}

fn lint_stdin_source(
    args: &Args,
    config: &ResolvedConfig,
    registry: &CopRegistry,
    source: &SourceFile,
) -> Result<Vec<Diagnostic>> {
    let filters = CopFilterSet::build(config, registry);
    let only = (!args.only.is_empty()).then(|| args.only.clone());
    linter::lint_source(
        source,
        config,
        registry,
        &filters,
        only.as_deref(),
        &args.except,
        args.autocorrect_mode(),
        args.ignore_disable_comments,
        false,
    )
}