rrubocop 0.7.2

Fast RuboCop-compatible Ruby linter in Rust (tree-sitter). No Ruby for linting; ERB configs need ruby.
Documentation
//! `require:` / `plugins:` gem default config loading.

use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};

use serde_yml::Value;

use crate::cop::EnabledState;

use super::gem_path;
use super::load_recursive::load_config_recursive;
use super::merge::merge_layer_into;
use super::standard::{standard_gem_config_path, PLUGIN_GEM_DEPARTMENTS};
use super::types::ConfigLayer;

fn yaml_string_list(val: &Value) -> Vec<String> {
    match val {
        Value::String(s) => vec![s.clone()],
        Value::Sequence(seq) => seq
            .iter()
            .filter_map(|v| v.as_str().map(String::from))
            .collect(),
        _ => vec![],
    }
}

pub(crate) fn collect_yaml_strings(val: &Value) -> Vec<String> {
    yaml_string_list(val)
}

fn collect_require_gems(map: &serde_yml::Mapping) -> Vec<String> {
    let mut gems = Vec::new();
    for key in &["plugins", "require"] {
        if let Some(val) = map.get(Value::String(key.to_string())) {
            gems.extend(yaml_string_list(val));
        }
    }
    gems.dedup();
    gems
}

fn needs_wrapper(gems: &[String], injected: &[(usize, String)], name: &str) -> bool {
    !gems.iter().any(|g| g == name) && !injected.iter().any(|(_, g)| g == name)
}

fn inject_one_wrapper(gems: &[String], injected: &mut Vec<(usize, String)>, wrapper: &str, rubocop: &str) {
    for (i, gem) in gems.iter().enumerate() {
        if gem == wrapper && needs_wrapper(gems, injected, rubocop) {
            injected.push((i, rubocop.to_string()));
        }
    }
}

fn inject_standard_wrappers(gems: &mut Vec<String>) {
    let mut injected = Vec::new();
    inject_one_wrapper(gems, &mut injected, "standard-rails", "rubocop-rails");
    inject_one_wrapper(
        gems,
        &mut injected,
        "standard-performance",
        "rubocop-performance",
    );
    for (i, gem) in injected.into_iter().rev() {
        gems.insert(i, gem);
    }
}

fn gem_config_rel_path(gem_name: &str, ruby_version: Option<f64>) -> Option<String> {
    if gem_name.starts_with("rubocop-") {
        Some("config/default.yml".into())
    } else {
        standard_gem_config_path(gem_name, ruby_version).map(|path| path.into())
    }
}

fn resolve_named_gem(
    gem_name: &str,
    working_dir: &Path,
    gem_cache: Option<&HashMap<String, PathBuf>>,
) -> Option<PathBuf> {
    if let Some(path) = gem_cache.and_then(|c| c.get(gem_name)) {
        return Some(path.clone());
    }
    match gem_path::resolve_gem_path(gem_name, working_dir) {
        Ok(p) => Some(p),
        Err(e) => {
            eprintln!("warning: require '{}': {e:#}", gem_name);
            None
        }
    }
}

fn merge_require_layer(base_layer: &mut ConfigLayer, gem_name: &str, layer: ConfigLayer) {
    // Keep AllCops.Exclude from rubocop-* gems (e.g. rubocop-rails `db/*schema.rb`).
    // Patterns stay project-relative — do not rewrite them against the gem path.
    let _ = gem_name;
    merge_layer_into(base_layer, &layer, None);
}

fn load_require_fallback(
    gem_name: &str,
    gem_root: &Path,
    working_dir: &Path,
    visited: &mut HashSet<PathBuf>,
    gem_cache: Option<&HashMap<String, PathBuf>>,
    base_layer: &mut ConfigLayer,
) {
    if gem_name.starts_with("rubocop-") {
        return;
    }
    let fallback = gem_root.join("config").join("base.yml");
    if !fallback.exists() {
        return;
    }
    match load_config_recursive(&fallback, working_dir, visited, gem_cache) {
        Ok(layer) => merge_layer_into(base_layer, &layer, None),
        Err(e) => {
            eprintln!(
                "warning: failed to load default config for {}: {e:#}",
                gem_name
            );
        }
    }
}

fn load_one_require_gem(
    gem_name: &str,
    ruby_version: Option<f64>,
    working_dir: &Path,
    visited: &mut HashSet<PathBuf>,
    gem_cache: Option<&HashMap<String, PathBuf>>,
    base_layer: &mut ConfigLayer,
) {
    let Some(config_rel_path) = gem_config_rel_path(gem_name, ruby_version) else {
        return;
    };
    let Some(gem_root) = resolve_named_gem(gem_name, working_dir, gem_cache) else {
        return;
    };
    let config_file = gem_root.join(&config_rel_path);
    if !config_file.exists() {
        load_require_fallback(
            gem_name,
            &gem_root,
            working_dir,
            visited,
            gem_cache,
            base_layer,
        );
        return;
    }
    match load_config_recursive(&config_file, working_dir, visited, gem_cache) {
        Ok(layer) => merge_require_layer(base_layer, gem_name, layer),
        Err(e) => {
            eprintln!(
                "warning: failed to load default config for {}: {e:#}",
                gem_name
            );
        }
    }
}

fn record_require_enabled(base_layer: &mut ConfigLayer) {
    base_layer.require_enabled_cops = base_layer
        .cop_configs
        .iter()
        .filter(|(_, c)| c.enabled == EnabledState::True)
        .map(|(n, _)| n.clone())
        .collect();
    base_layer.require_enabled_depts = base_layer
        .department_configs
        .iter()
        .filter(|(_, c)| c.enabled == EnabledState::True)
        .map(|(n, _)| n.clone())
        .collect();
}

fn register_plugin_depts(base_layer: &mut ConfigLayer, gems: &[String]) {
    base_layer.require_known_cops = base_layer.cop_configs.keys().cloned().collect();
    base_layer.require_departments = base_layer.department_configs.keys().cloned().collect();
    for gem_name in gems {
        for (dept, gem) in PLUGIN_GEM_DEPARTMENTS {
            if gem_name.as_str() == *gem {
                base_layer.require_departments.insert(dept.to_string());
            }
        }
    }
}

/// Process `plugins:` / `require:` gem defaults into `base_layer`.
pub(crate) fn process_require_plugins(
    map: &serde_yml::Mapping,
    ruby_version: Option<f64>,
    working_dir: &Path,
    visited: &mut HashSet<PathBuf>,
    gem_cache: Option<&HashMap<String, PathBuf>>,
    base_layer: &mut ConfigLayer,
) {
    let mut gems = collect_require_gems(map);
    inject_standard_wrappers(&mut gems);
    let visited_before = visited.clone();
    for gem_name in &gems {
        load_one_require_gem(
            gem_name,
            ruby_version,
            working_dir,
            visited,
            gem_cache,
            base_layer,
        );
    }
    record_require_enabled(base_layer);
    *visited = visited_before;
    register_plugin_depts(base_layer, &gems);
}